不限距离4g/5g信号远程遥控小车

news/2024/10/23 22:32:15/

       

4g/5g不限距离遥控小车(1)

        

4g/5g不限距离遥控小车(2)

        最开始学习编程也是源于一个想法, 无线遥控小车和飞机操作范围都是在几十米, 远的几百米, 再远的几公里, 那能不能把手机放在小车或飞机上, 利用手机的4g/5g信号来接收指令, 这样只要有手机信号的地方, 就可以不限距离的操作, 当时我面临两个非常棘手的问题, 一个是视频图像传输的实时性, 因为我要坐在电脑前实时操作小车和飞机的路线, 那么对视频传输的延时就要求很高; 第二个是当时我并不知道安卓手机可以利用AOA协议来扩展外设, 当年我看的所有安卓视频教程中从来没有一个老师提起过, 导致我这个想法一直没能实现. 非常偶然的一个机会, 我在百度里才发现了安卓手机可以用AOA协议来扩展外设, 后来通过学习又解决了视频传输实时性的问题, 好了, 开始动手搞起来~        

        服务端源码已经上传到gitee了, 喜欢的可以去下载, 记得手抖点个赞哈~

        git@gitee.com:phoenix3k/client-a_client-b_-server.git

        前一阵修复了clientA和clientB的两处bug, 其一是更新到最新版本的百度地图sdk 7.4版本, 其二是socketio在高版本AndroidSDK中无法通信的bug (为此我将单独写一篇博客记录下来), 目前已经适配了从Android12到Android4.4的兼容.

        后续会开放ClientA和ClientB的源代码

        小车改装升级为四驱越野, 2百多的车还是有点心疼的, 同时还做了一个简单的充电器, 直接上图

        先上两张图, 小车已经可以正常跑起来了~

        

再来说说我都消耗了哪些东西:

        1. 两部旧的安卓手机, 一个是乐视2安卓6.0, 一个是小米5s安卓7.0

        2. x宝上淘的一块FT311D开发模块

        3. x宝上淘的两块4000mAh, 3.7v可充电锂电池

        4. x宝上淘的一块pwm电机模块, 用于驱动马达

        5. x宝上淘的一块移动电源主板升压充电模块, 锂电池3v 3.7v升5v 1A升压板

        6. x宝上淘的一块DC-DC升压模块, 2A升压板, 宽压输入2/24v升5/9/12/28v 可调

        7. 拆掉了儿子一辆无线遥控小车

        8. 一台阿里云服务器, 配置8g内存, 2核CPU, 5m带宽

        9. 一台笔记本电脑或台式机

需要做的工作和目前实现了哪些功能:

        1. 乐视2手机用于和FT311D版相连, 用于接收控制指令, 以及传输视频图像, 经纬度坐标, 手机电量信息等, 出于起名困难, 我称这个手机为clientB.

        2. 小米5s手机用于发出控制指令, 接收经纬度坐标, 并在百度地图中显示, 接收clientB电量信息, 发出打开或关闭clientB的摄像头, 接收clientB的连接状态, 出于起名困难, 我称这个手机为clientA.

        3. 服务器上部署了srs服务, 用于低延时视频传输, 这部分我已经单独发了一篇帖子, 感兴趣的小伙伴可以去看看, 同时部署了服务端程序, 这部分的代码我并没有用java, 而是用来nodejs来实现, 功能相对简单, 只是负责转发clientB和clientA的指令和状态信息, 控制指令和消息的转发安卓端和服务端都是用了socketIO.

        需要懂一些原生安卓开发的知识, 当然如果小伙伴不想用nodejs来写后台服务, 完全可以替换成java, 了解一些Linux的知识最好, 方便应用的部署, 懂一些srs服务的部署, 这个可以参考gitee的官方wiki, 需要去看FT311D的文档, 了解如何使用FT311D开发版以及嵌入到自己的安卓项目中, 这部分资源我也上传到了我的CSDN资源中, 喜欢的小伙伴可以下载.

        先上nodejs服务器端的代码, 比较简单, 只是做了自定义命令的转发.

        app.js

var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);server.listen(6547); // 监听自己的服务端口app.use('/api', require('./routers/api'));io.on('connection', function(socket) {console.log("来了一个人  " + socket.id + "-" + socket.request.connection.remoteAddress);// 监听客户端离线事件socket.on('disconnect', function() {console.log("对方下线了");socket.broadcast.emit('cmd', '对方下线了');});// 监听clientA和clientB发送的cmd命令socket.on('cmd', function(msg) {if (msg == "B" || msg == "A") {socket.emit('cmd', "通信成功");}if (msg == "A:up") {console.log("up");socket.broadcast.emit('cmd', 'up');}if (msg == "A:back") {console.log("back");socket.broadcast.emit('cmd', 'back');}if (msg == "A:left") {console.log("left");socket.broadcast.emit('cmd', 'left');}if (msg == "A:right") {console.log("right");socket.broadcast.emit('cmd', 'right');}if (msg == "A:stop") {console.log("stop");socket.broadcast.emit('cmd', 'stop');}if (msg.indexOf("speed:") != -1) {socket.broadcast.emit('speed', msg.slice(6));console.log(msg.slice(6));}if (msg.indexOf("period:") != -1) {socket.broadcast.emit('period', msg.slice(7));console.log(msg.slice(7));}if (msg == "push") { // 发送推流命令到clientBsocket.broadcast.emit('cmd', 'push');console.log('push');}if (msg == "close") { // 发送关闭推流命令到clientBsocket.broadcast.emit('cmd', 'close');console.log('close');}});socket.on('location', function(msg) { // 转发clientB位置信息var LatLon = msg.split(",");socket.broadcast.emit('location', LatLon[0], LatLon[1]);});socket.on('elect', function(msg) { // 转发clientB电量信息socket.broadcast.emit('elect', msg);onsole.log(msg);});
});

        api.js 负责推流和拉流简单的鉴权验证

var express = require("express");
var app = express();
var router = express.Router();
var formidable = require("formidable");// 推流鉴权
router.post('/publish', function(req, res, next) {console.log("--------推流鉴权--------");var form = new formidable.IncomingForm();form.encoding = 'utf-8';form.parse(req, function(err, fields, files) {if (fields.stream == '147') { // 这里的流名称请参考srs的wikires.statusCode = 200;res.write("0");res.end();} else {res.write("1");res.end();}// console.log(fields);});
});// 拉流鉴权
router.post('/play', function(req, res, next) {console.log("--------拉流鉴权--------");var form = new formidable.IncomingForm();form.encoding = 'utf-8';form.parse(req, function(err, fields, files) {if (fields.stream == '147') { // 这里的流名称请参考srs的wikires.statusCode = 200;res.write("0");res.end();} else {res.write("1");res.end();}// console.log(fields);});
});// 声网分发token
const Role = {// DEPRECATED. Role::ATTENDEE has the same privileges as Role.PUBLISHER.ATTENDEE: 0,// RECOMMENDED. Use this role for a voice/video call or a live broadcast, if your scenario does not require authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in).PUBLISHER: 1,/* Only use this role if your scenario require authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in).* @note In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role.SUBSCRIBER still has the same privileges as Role.PUBLISHER.*/SUBSCRIBER: 2,// DEPRECATED. Role.ADMIN has the same privileges as Role.PUBLISHER.ADMIN: 101
}class RtcTokenBuilder {static buildTokenWithUid(appID, appCertificate, channelName, uid, role, privilegeExpiredTs) {return this.buildTokenWithAccount(appID, appCertificate, channelName, uid, role, privilegeExpiredTs)}static buildTokenWithAccount(appID, appCertificate, channelName, account, role, privilegeExpiredTs) {this.key = new AccessToken(appID, appCertificate, channelName, account)this.key.addPriviledge(Priviledges.kJoinChannel, privilegeExpiredTs)if (role == Role.ATTENDEE ||role == Role.PUBLISHER ||role == Role.ADMIN) {this.key.addPriviledge(Priviledges.kPublishAudioStream, privilegeExpiredTs)this.key.addPriviledge(Priviledges.kPublishVideoStream, privilegeExpiredTs)this.key.addPriviledge(Priviledges.kPublishDataStream, privilegeExpiredTs)}return this.key.build();}
}module.exports = router;
module.exports.Role = Role;
module.exports.RtcTokenBuilder = RtcTokenBuilder;

        最近用闲散时间改进了服务端和Android端的代码, 之前只能满足一个clientA和一个clientB进行通信, 无法实现多组clientA和clientB通信, 现在把Android端和nodejs的代码加入了"房间"的概念, 每个"房间"有且只有一组clientA和clientB, 而且只能通过clientA来创建房间, clientB来加入房间, 就可以有多个"房间"同时工作, 而各个房间里的clientA发出的指令只对该"房间"的clientB起作用, 而房间之间互不干扰, 先上改进后的服务端代码, 本次服务端代码只改了app.js.

const app = require('express')();
const server = require('http').Server(app);
const io = require('socket.io')(server);const RtcTokenBuilder = require('./routers/api').RtcTokenBuilder;
const RtcRole = require('./routers/api').Role;// 生成声网token所需参数
const appID = 'xxxx'; // 此处请填写自己在声网创建项目的appid
const appCertificate = 'xxxx'; // 此处请填写自己在声网创建项目的app证书
const uid = 0;
// const account = "2882341273";
const role = RtcRole.PUBLISHER;
const expirationTimeInSeconds = 3600;app.use('/api', require('./routers/api'));var rooms = []; // rooms数组中又存储每一个room对象
var tmp = [];
var channelName = '';
var token;io.on("connection", (socket) => {console.log("来了一个人  " + socket.id + "-" + socket.request.connection.remoteAddress);// 接到一个客户端连接后, 判断客户端类型, 加入的房间号, 房间密码, 以及房间里的人数, 每个房间只容纳两个客户端, 一个clientA, 一个clientBsocket.on("info", (roomNum, roomPwd, socketType) => {if (rooms.length == 0 && socketType == 'clientA') { // 只允许clientA创建房间var room = {};room.roomNum = roomNum;room.roomPwd = roomPwd;room.socketType = socketType;room.socketID = socket.id;rooms.push(room);socket.emit("message", 1);// 用户加入房间socket.join(roomNum);} else if (rooms.length == 0 && socketType == 'clientB') { // clientB不允许创建房间socket.emit("message", 0);} else {if (rooms.length != 0) {if (socketType == 'clientA') {for (let i = 0; i < rooms.length; i++) {if (rooms[i].roomNum == roomNum && rooms[i].socketType == 'clientA') {socket.emit("message", 0);break;}if (rooms[i].roomNum == roomNum && rooms[i].socketType == 'clientB' && i == (rooms.length - 1)) {var room = {};room.roomNum = roomNum;room.roomPwd = roomPwd;room.socketType = socketType;room.socketID = socket.id;rooms.push(room);// 用户加入房间socket.join(roomNum);socket.emit("message", 1);break;}if (rooms[i].roomNum != roomNum && i == (rooms.length - 1)) {var room = {};room.roomNum = roomNum;room.roomPwd = roomPwd;room.socketType = socketType;room.socketID = socket.id;rooms.push(room);// 用户加入房间socket.join(roomNum);socket.emit("message", 1);break;}}}if (socketType == 'clientB') {for (let i = 0; i < rooms.length; i++) {if (isContain(rooms, roomNum, 'clientA') && !isContain(rooms, roomNum, 'clientB') && rooms[i].roomPwd == roomPwd) {var room = {};room.roomNum = roomNum;room.roomPwd = roomPwd;room.socketType = socketType;room.socketID = socket.id;rooms.push(room);// 用户加入房间socket.join(roomNum);socket.emit("message", 1);break;}if (isContain(rooms, roomNum, 'clientA') && !isContain(rooms, roomNum, 'clientB') && rooms[i].roomPwd != roomPwd) {socket.emit("message", -1);break;}if (!isContain(rooms, roomNum, 'clientA') && !isContain(rooms, roomNum,'clientB')) {socket.emit("message", 0);break;}}}}}});// 监听客户端离线事件socket.on("disconnect", () => {for (let i = 0; i < rooms.length; i++) {if (rooms[i].socketID == socket.id) {socket.leave(rooms[i].roomNum);rooms.splice(i, 1);}}console.log("对方下线了" + socket.id);});// 监听clientA和clientB发送的cmd命令socket.on('cmd', function(msg) {if (msg == "B" || msg == "A") {socket.emit('cmd', "通信成功");}for (let i = 0; i < rooms.length; i++) {if (rooms[i].socketID == socket.id) {if (msg == "A:up") {console.log("up");socket.to(rooms[i].roomNum).emit('cmd', 'up');}if (msg == "A:back") {console.log("back");socket.to(rooms[i].roomNum).emit('cmd', 'back');}if (msg == "A:left") {console.log("left");socket.to(rooms[i].roomNum).emit('cmd', 'left');}if (msg == "A:right") {console.log("right");socket.to(rooms[i].roomNum).emit('cmd', 'right');}if (msg == "A:stop") {console.log("stop");socket.to(rooms[i].roomNum).emit('cmd', 'stop');}if (msg.indexOf("speed:") != -1) {socket.to(rooms[i].roomNum).emit('speed', msg.slice(6));console.log(msg.slice(6));}if (msg.indexOf("period:") != -1) {socket.to(rooms[i].roomNum).emit('period', msg.slice(7));console.log(msg.slice(7));}if (msg == "push") { // 发送推流命令到clientBsocket.to(rooms[i].roomNum).emit('cmd', 'push');console.log('push');}if (msg == "close") { // 发送关闭推流命令到clientBsocket.to(rooms[i].roomNum).emit('cmd', 'close');console.log('close');}if (msg == "token") { // 返回给客户端tokenchannelName = rooms[i].roomNum;if (rooms[i].socketType == 'clientB') {io.to(socket.id).emit('token', token); // 把token返回给发送请求的socket端} else {let currentTimestamp = Math.floor(Date.now() / 1000);let privilegeExpiredTs = currentTimestamp + expirationTimeInSeconds;token = RtcTokenBuilder.buildTokenWithUid(appID, appCertificate, channelName,uid, role, privilegeExpiredTs);io.to(socket.id).emit('token', token); // 把token返回给发送请求的socket端// socket.emit('cmd', token);}console.log("Token is: " + token);}}}});socket.on('location', function(msg) {for (let i = 0; i < rooms.length; i++) {if (rooms[i].socketID == socket.id) {var LatLon = msg.split(",");socket.broadcast.emit('location', LatLon[0], LatLon[1]);}}});socket.on('elect', function(msg) {for (let i = 0; i < rooms.length; i++) {if (rooms[i].socketID == socket.id) {socket.broadcast.emit('elect', msg);console.log(msg);}}});
});function isContain(rooms, roomNum, type) {for (let i = 0; i < rooms.length; i++) {if (rooms[i].roomNum == roomNum && rooms[i].socketType == type) {return true;} else {continue;}}return false;
}server.listen(3764);

        顺便说一下, 服务器端的socketIO版本升级到了最新, 4.2.0, 同时Android端的版本升级到了2.0.1, 这两个版本一定是对应的, 相关版本兼容说明可直接参考官网说明, 否则一定扑街!!!

        这次的改进也在两个Android端加入了语音功能, 引入了第三方的平台---声网, 之所以选择它是因为能快速接入语音功能, 目前已实现了clientA和clientB实时对讲功能, clientB端我默认开启了免提功能, 这样clientA和clientB就能相互喊话啦! 有的小伙伴要问, 为什么你视频功能不接入第三方平台呢, 原因很简单, 传输时效达不到我的要求~

        本次修改优化了声网语音电话的token申请, token的生成需要自己写逻辑, token过期时间为24小时, 这个在声网官网中都有明确的说明, 直接上我服务器生成token的代码, 也是从声网搬砖来的, 声网的token生成有多个语言版本, 由于我的后台服务是nodejs, 所以我选择的是这个版本的代码.

        AccessToken.js 是token生成的核心代码, 直接搬砖到自己服务器上ok了~

var crypto = require('crypto');
var crc32 = require('crc-32');
var UINT32 = require('cuint').UINT32;
var version = "006";
var randomInt = Math.floor(Math.random() * 0xFFFFFFFF);
const VERSION_LENGTH = 3;
const APP_ID_LENGTH = 32;var AccessToken = function(appID, appCertificate, channelName, uid) {let token = this;this.appID = appID;this.appCertificate = appCertificate;this.channelName = channelName;this.messages = {};this.salt = randomInt;this.ts = Math.floor(new Date() / 1000) + (24 * 3600);if (uid === 0) {this.uid = "";} else {this.uid = `${uid}`;}this.build = function() {var m = Message({salt: token.salt,ts: token.ts,messages: token.messages}).pack();var toSign = Buffer.concat([Buffer.from(token.appID, 'utf8'),Buffer.from(token.channelName, 'utf8'),Buffer.from(token.uid, 'utf8'),m]);var signature = encodeHMac(token.appCertificate, toSign);var crc_channel = UINT32(crc32.str(token.channelName)).and(UINT32(0xffffffff)).toNumber();var crc_uid = UINT32(crc32.str(token.uid)).and(UINT32(0xffffffff)).toNumber();var content = AccessTokenContent({signature: signature,crc_channel: crc_channel,crc_uid: crc_uid,m: m}).pack();return (version + token.appID + content.toString('base64'));}this.addPriviledge = function(priviledge, expireTimestamp) {token.messages[priviledge] = expireTimestamp;};this.fromString = function(originToken) {try {originVersion = originToken.substr(0, VERSION_LENGTH);if (originVersion != version) {return false;}var originAppID = originToken.substr(VERSION_LENGTH, (VERSION_LENGTH + APP_ID_LENGTH));var originContent = originToken.substr((VERSION_LENGTH + APP_ID_LENGTH));var originContentDecodedBuf = Buffer.from(originContent, 'base64');var content = unPackContent(originContentDecodedBuf);this.signature = content.signature;this.crc_channel_name = content.crc_channel_name;this.crc_uid = content.crc_uid;this.m = content.m;var msgs = unPackMessages(this.m);this.salt = msgs.salt;this.ts = msgs.ts;this.messages = msgs.messages;} catch (err) {console.log(err);return false;}return true;};
};module.exports.version = version;
module.exports.AccessToken = AccessToken;
module.exports.priviledges = {kJoinChannel: 1,kPublishAudioStream: 2,kPublishVideoStream: 3,kPublishDataStream: 4,kRtmLogin: 1000
};var encodeHMac = function(key, message) {return crypto.createHmac('sha256', key).update(message).digest();
};var ByteBuf = function() {var that = {buffer: Buffer.alloc(1024),position: 0};that.buffer.fill(0);that.pack = function() {var out = Buffer.alloc(that.position);that.buffer.copy(out, 0, 0, out.length);return out;};that.putUint16 = function(v) {that.buffer.writeUInt16LE(v, that.position);that.position += 2;return that;};that.putUint32 = function(v) {that.buffer.writeUInt32LE(v, that.position);that.position += 4;return that;};that.putBytes = function(bytes) {that.putUint16(bytes.length);bytes.copy(that.buffer, that.position);that.position += bytes.length;return that;};that.putString = function(str) {return that.putBytes(Buffer.from(str));};that.putTreeMap = function(map) {if (!map) {that.putUint16(0);return that;}that.putUint16(Object.keys(map).length);for (var key in map) {that.putUint16(key);that.putString(map[key]);}return that;};that.putTreeMapUInt32 = function(map) {if (!map) {that.putUint16(0);return that;}that.putUint16(Object.keys(map).length);for (var key in map) {that.putUint16(key);that.putUint32(map[key]);}return that;};return that;
}var ReadByteBuf = function(bytes) {var that = {buffer: bytes,position: 0};that.getUint16 = function() {var ret = that.buffer.readUInt16LE(that.position);that.position += 2;return ret;};that.getUint32 = function() {var ret = that.buffer.readUInt32LE(that.position);that.position += 4;return ret;};that.getString = function() {var len = that.getUint16();var out = Buffer.alloc(len);that.buffer.copy(out, 0, that.position, (that.position + len));that.position += len;return out;};that.getTreeMapUInt32 = function() {var map = {};var len = that.getUint16();for (var i = 0; i < len; i++) {var key = that.getUint16();var value = that.getUint32();map[key] = value;}return map;};return that;
}
var AccessTokenContent = function(options) {options.pack = function() {var out = new ByteBuf();return out.putString(options.signature).putUint32(options.crc_channel).putUint32(options.crc_uid).putString(options.m).pack();}return options;
}var Message = function(options) {options.pack = function() {var out = new ByteBuf();var val = out.putUint32(options.salt).putUint32(options.ts).putTreeMapUInt32(options.messages).pack();return val;}return options;
}var unPackContent = function(bytes) {var readbuf = new ReadByteBuf(bytes);return AccessTokenContent({signature: readbuf.getString(),crc_channel_name: readbuf.getUint32(),crc_uid: readbuf.getUint32(),m: readbuf.getString()});
}var unPackMessages = function(bytes) {var readbuf = new ReadByteBuf(bytes);return Message({salt: readbuf.getUint32(),ts: readbuf.getUint32(),messages: readbuf.getTreeMapUInt32()});
}


http://www.ppmy.cn/news/212876.html

相关文章

CI2451/CI2454无线收发SOC芯片2.4g内置MCU遥控门锁超高性价比

CI2451跟CI2454是一款2.4G的SOC无线收发芯片/集成无线收发器和 8 位 RISC&#xff08;精简指令集&#xff09;MCU,其中CI2451和ci2454在无线收发的特性是一样的&#xff0c;但是在MCU的资源上CI2454的资源会更加优秀&#xff0c;ci2451具有更高的性价比&#xff0c;对成本要求比…

【基于 Arduino 的 RFID门锁】

【基于 Arduino 的 RFID门锁】 1. 概述2. 射频识别的工作原理3. RFID 和 Arduino4. Arduino RFID门锁门禁项目5. 源代码 在本教程中&#xff0c;我们将了解什么是 RFID&#xff0c;它是如何工作的以及如何制作基于 Arduino 的 RFID 门锁。您可以观看以下视频或阅读下面的书面教…

基于STM32F407的智能门锁

智能门锁 一、项目背景 在消费升级渗透在各个领域的今天&#xff0c;国民消费发生着巨大的变化&#xff0c;与每个人息息相关的家居行业也是如此。现今&#xff0c;越来越多的智能家居产品出现在普通老百姓的生活中&#xff0c;智能照明、智能窗帘、智能扫地机器人等各种智能产…

汽车一键启动 汽车手机远程启动系统  汽车无钥匙进入 手机APP控车智能防盗

移动管家手机智能控制汽车系统具有汽车远程启动、汽车远程熄火、远程开关车门锁、远程断油&#xff01;智能感应开关门锁、免钥一键启动、一键熄火、GPS查车、APP软件控车、智能防盗、手机管控.移动管家手机控车系统&#xff0c;手机短信远程启动汽车&#xff0c;一键启动无钥匙…

wifi智能门锁远程控制方案能实现哪些功能

远程智能门锁控制方案&#xff0c;智能创新&#xff0c;改变生活。      生活中&#xff0c;我们都习惯性的携带门锁的钥匙&#xff0c;但没培养这个习惯时&#xff0c;总是会将钥匙落下&#xff0c;而这个事可以说可大可小&#xff0c;往小来说就需要拿备用钥匙开门&#…

手机app+esp8266控制小区大门门锁,实现远程开门

近日笔者自己动手修理自家的门铃时,发现只要接通开门的电源线,就可以打开楼宇的大门了&#xff0c;突发奇想&#xff1a;在门铃引出线加装继电器就可以用esp8266连上互联&#xff0c;实现远程开门了。&#xff08;我家门铃比较老旧&#xff0c;不知其他牌子门铃是否也是可以这样…

远程遥控小车搭建记录

22年国庆期间&#xff0c;无意从抖音上发现远程遥控小车已经很普及了&#xff0c;包含了摄像头、喊话器、闪光灯控制&#xff0c;系统均是单片机控制&#xff0c;再叠加一个360度摄像头&#xff0c;基本是两个系统的叠加&#xff0c;也有一些商业化的系统&#xff0c;硬件软件云…

基于 STM32 远程控制的多功能门锁

本项目有指纹识别、动态密码、普通钥匙以及管理员特有的微信等开 门方式&#xff0c;实验室人员可直接指纹识别进入&#xff1b;管理员手机微信获取的动态密 码给非实验室人员&#xff0c;具有随机性&#xff0c;不确定性&#xff0c;且隐私性较高&#xff0c;同时管理员 手机可…