DIY 一台遥控小车带监控 (4G网络版 ) [上]
文章目录
用4G网络实现不限距离遥控,有监控,还带车载蓝牙MP3播放,还DIY了一个可以击发的玩具枪管.
网上都是WIFI小车的方案,有距离限制.
好多还借助云平台.
我自己不借助任何IOT云平台.
纯粹是TCP/IPV6的应用.
用网页控制.而且不需要额外的服务器.
大概是这个亚子.
材料
8266板子一个(推荐 nodemcu 10元左右或者wemos D1 R1 12元左右)
手机两个 (我有个坏了屏幕的手机.好的手机做控制端,在自己手里. 坏了屏幕那个放小车里,当监控,当热点,还能当电源)
TT马达4个,4个轮子
直流电机驱动 1-2个.
充电宝一个 (可选)
可选,蓝牙功放
带串口蓝牙板
5V功放板
喇叭两个
可选,diy炮管
5v激光
舵机2个,
一个云台支架一个 (自己淘宝搜索 舵机 fpv支架 1元以内是正常价,远高于这个价格乃奸商.)
DIY一个玩具炮管
有击发装置.
参考油管视频
https://www.youtube.com/watch?v=tRjyFghnVxU
https://www.youtube.com/watch?v=COPN8b1THPk&t=331s
建议参考第一个视频,比较简单, 一个马达一个橡皮筋就能DIY一个有击发装置的玩具枪.
第二个视频难度颇大,另外可能在中国是一个十年起步的套餐...建议没有背景的玩家不要去.
DIY一个车载蓝牙播放系统
这步也非常简单
我买的是BT201这个板子,有串口控制
蓝牙自动连接手机,用8266串口蓝牙板子控制音量,播放,暂停等动作,还能修改蓝牙名.
http://blog.sina.com.cn/s/blog_427f3f2c0102yo8u.html
这里有个详细的指令说明.
这步也是大概说下,功放板我买的不好,太便宜了.
功放板和蓝牙板没有过滤,不能共用电源,不然有很大杂音,我是通过两个供电处理的.
DIY一个监控系统
这步也非常简单
手机上安装一个APP即可
项目是开源的,源代码这里,APP你去应用商店下载
https://github.com/shenyaocn/IP-Camera-Bridge
它能直接让一个手机当监控用.
4G小车原理
它和wifi小车不同就是它能在户外浪.
4G模块很贵,兼容单片机的摄像头也贵.
所以我是用一个坏了屏幕的手机 既让它当热点,也让它当监控.
当然,我想家里怎么也有个淘汰的手机吧,. 当热点也行,当个监控是没毛病的.手机最好是root了的.
1,一个手机当热点,现在4G手机都是有IPV6,这个是公网IPV6.你可以直接通过IPV6连接到这个手机.
2,8266的板子(前面我推荐的两个,我选的是WEMOS D1 R1) 通过坏了屏幕手机分享热点.实现互联网.
3,8266上开启网页服务器,也就是网页板控制器.
4,安卓上用 caddy 端口转发,协议转发 ,将8266端口暴露在IPV6公网
8266网页控制部分原理
nodemcu 还有 wemos d1 r1 这两个8266的板子 都是4 M flash.
1M 代码区, 3M可以作为SPIFFS 文件区,SPIFFS可以放网页文件,JS,CSS.图片之类的. 它是网页版控制小车的关键部分.
8266官方有个示例代码 菜单--文件--示例--8266-CheckFlashConfig
直接烧录你板子,串口控制台就能看到你FLASH总大小.
8266几个常见板子FLASH参考图
8266之SPIFFS烧录
SPIFFS区域烧录和代码区域是分开的,烧录代码不会影响你SPIFFS.反之也是.
建议烧录顺序是先烧录代码,再烧录SPIFFS.
上传网页文件到8266的板子
需要用到官方一个工具,arduino-esp8266fs-plugin
源代码和下载
https://github.com/esp8266/arduino-esp8266fs-plugin/releases/latest
一个jar文件,解压安装到
Arduino目录\tools\ESP8266FS\tool\
注意目录结构,文件名都不要搞错.
重启Arduino 开发工具 ,在菜单--工具--ESP8266 Sketch Data Upload的工具. (直译就是草图上传工具)
它的作用是将当前项目,目录下的"data"文件夹里面的内容打包烧录到SPIFFS区域.
比你当前打开了C:\CCC\A.ino 项目
C:\CCC\A.ino
C:\CCC\data\index.htm
C:\CCC\data\aabb.htm
C:\CCC\data\index.js
....等
data目录所有文件自动打包,烧录到8266 SPIFFS区域.只要不超过容量即可.虽然3M SPIFFS,但是打包的时候,文件之间有间隔.
反正你文件总大小建议控制2M内,不然可能烧录失败.
每次这样用草图上传工具烧录都是擦除整个SPIFFS区域,再上传覆盖,所以会覆盖之前的文件.
题外话,8266有FS.h 可以单独上传,删除改变一个文件.
8266之SPIFFS当web服务器
8266除了连接WIFI它能干的事情太多了,当网页服务器,当TCP/UDP服务器.当DNS服务器.等等.
前面说了上传了网页代码,现在说下如何将SPIFFS文件当web服务器.
我直接上代码
核心头文件
#include
#include
全局变量
ESP8266WebServer server(HTTPPORT); //HTTPPORT = 网页端口 定义默认为80
String formatBytes(size_t bytes) {
if (bytes < 1024) {
return String(bytes) + "B";
} else if (bytes < (1024 * 1024)) {
return String(bytes / 1024.0) + "KB";
} else if (bytes < (1024 * 1024 * 1024)) {
return String(bytes / 1024.0 / 1024.0) + "MB";
} else {
return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB";
}
}
String getContentType(String filename) {
if (server.hasArg("download")) {
return "application/octet-stream";
} else if (filename.endsWith(".htm")) {
return "text/html";
} else if (filename.endsWith(".html")) {
return "text/html";
} else if (filename.endsWith(".css")) {
return "text/css";
} else if (filename.endsWith(".js")) {
return "application/javascript";
} else if (filename.endsWith(".png")) {
return "image/png";
} else if (filename.endsWith(".gif")) {
return "image/gif";
} else if (filename.endsWith(".jpg")) {
return "image/jpeg";
} else if (filename.endsWith(".ico")) {
return "image/x-icon";
} else if (filename.endsWith(".xml")) {
return "text/xml";
} else if (filename.endsWith(".pdf")) {
return "application/x-pdf";
} else if (filename.endsWith(".zip")) {
return "application/x-zip";
} else if (filename.endsWith(".gz")) {
return "application/x-gzip";
}
return "text/plain";
}
bool handleFileRead(String path) {
//USE_SERIAL.println("handleFileRead: " + path);
if (path.endsWith("/")) {
path += "index.htm";
}
String contentType = getContentType(path);
String pathWithGz = path + ".gz";
if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) {
//USE_SERIAL.println("is have: " + path);
if (SPIFFS.exists(pathWithGz)) {
path += ".gz";
}
File file = SPIFFS.open(path, "r");
server.streamFile(file, contentType);
file.close();
return true;
}
return false;
}
void handleFileList() {
if (!server.hasArg("dir")) {
server.send(500, "text/plain", "BAD ARGS");
return;
}
String path = server.arg("dir");
//USE_SERIAL.println("list : handleFileList: " + path);
Dir dir = SPIFFS.openDir(path);
path = String();
String output = "[";
while (dir.next()) {
File entry = dir.openFile("r");
if (output != "[") {
output += ',';
}
bool isDir = false;
output += "{\"type\":\"";
output += (isDir) ? "dir" : "file";
output += "\",\"name\":\"";
if (entry.name()[0] == '/') {
output += &(entry.name()[1]);
} else {
output += entry.name();
}
output += "\"}";
entry.close();
}
output += "]";
server.send(200, "text/json", output);
}
setup里面
调用这个初始化函数
void init_web() {
//调试模式多一个链接,查看文件列表
if (is_debug) {
server.on("/list", HTTP_GET, handleFileList);
}
server.onNotFound([]() {
if (!handleFileRead(server.uri())) {
server.send(404, "text/plain", "FileNotFound");
}
});
server.begin();
}
然后
loop函数里面响应下web消息
一行代码即可
server.handleClient();
这样,8266的网页服务器就跑起来了.
非常巧妙的自动映射
"/" - > (SPIFFS分区的) index.htm
"/a.js" - > (SPIFFS分区的) a.js
其他文件是直接一一对应.
另外还支持gz压缩:
"/a.js" -> a.js.gz
8266之websocket服务器
单一个网页,是很难控制小车,控制板子的.
要实现网页和8266板子交互,有两个方案比较方便,第一个是通过URL,post/get方式板子接受参数.
另外一个就是继续在8266上开启一个websocket服务器.利用websocket进行网页和板子交互.
不要担心性能够不够.我告诉你性能还绰绰有余.
头文件,也是8266自带
#include
全局变量
WebSocketsServer webSocket = WebSocketsServer(WSPORT); //WSPORT = 81 端口号,自定义
初始化setup函数
两行代码
webSocket.begin();
webSocket.onEvent(webSocketEvent); //webSocketEvent是接管loop的函数名
loop函数
就一行代码
webSocket.loop();
核心是webSocketEvent函数
这是我还没写完的webSocketEvent已经能实现蓝牙板控制,炮管移动.激光打开,发射.
小车控制代码后续在后面博文直接整套代码上传.
ws是任何设备只要知道你端口和IP都能访问的,所以,我加了一个自定义字符串,防止别人控制我的板子,如果发的不是授权字符串,那么他发来任何指令都不接受.
即使他知道指令,一个8266板子我也限制了远程控制的用户只能为1.
这样别人就不会拐跑你小车了.
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
//如果客户端断开
case WStype_DISCONNECTED:
//调试输出
if (is_debug) {
USE_SERIAL.printf("[%u] Disconnected!", num);
}
//如果断开的用户是授权用户,恢复初始值
if (num == auth_user) {
auth_user = 60000;
//调试输出
if (is_debug) {
USE_SERIAL.println(" (auth user)");
}
}else{
//调试输出
if (is_debug) {
USE_SERIAL.println();
}
}
break;
//如果客户端链接
case WStype_CONNECTED:
{
IPAddress ip = webSocket.remoteIP(num);
//调试输出
if (is_debug) {
USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
}
// 发送数据给客户端
webSocket.sendTXT(num, "Connected");
}
break;
//如果客户端发来数据
case WStype_TEXT:
//调试输出
if (is_debug) {
USE_SERIAL.printf("debug : user[%u] Send Text: %s Lenght: %d \n", num, payload,length);
}
//判断授权用户是否存在
//不是授权用户
if (auth_user != 60000 && num != auth_user) {
//授权用户已经存在,而且授权用户不是当前用户
//直接ban掉
//调试输出
if (is_debug) {
USE_SERIAL.println(ERR_BAD_USER);
}
webSocket.sendTXT(num, ERR_BAD_USER);
}else if (auth_user == 60000 ){
//授权用户不存在
//判断用户发来的字符是token
if (strcmp((const char *) &payload[0], &auth_token[0]) == 0){
auth_user = num ; //记录授权用户
//调试输出
if (is_debug) {
USE_SERIAL.println(INOF_TRUE_TOKEN);
}
//todo,发送网页参数
webSocket.sendTXT(num, INOF_TRUE_TOKEN);
}else{
//调试输出
if (is_debug) {
USE_SERIAL.println(ERR_TOKEN);
}
webSocket.sendTXT(num, ERR_TOKEN);
}
}else if ( auth_user != 60000 && num == auth_user){
//当前用户是授权用户
//接收用户其他指令
//调试输出
if (is_debug) {
USE_SERIAL.printf("debug : [auth] user[%u] Send CMD: %s Lenght: %d \n", num, payload,length);
}
//#todo
//自定义控制指令#
//长度最少为3位
if (payload[0] == '#' && length > 2) {
//指令校验两个字母
//C 表示car 汽车相关指令
//M 表示music 播放MP3板子相关指令
//G 表示Gun 玩具枪管控制指令
//S 表示Set 参数更改
//车运动相关指令处理
if ( payload[1] == 'C' ) {
//车 - 启动
//完整指令: #CS
if (payload[2] == 'S') {
}
//车 - 停止
//完整指令: #CO
if (payload[2] == 'O') {
}
//车 - 引擎正转 (默认)
//完整指令: #CU
if (payload[2] == 'U') {
}
//车 - 引擎反转
//完整指令: #CD
if (payload[2] == 'D') {
}
//车 - 左转
//完整指令: #CL
if (payload[2] == 'L') {
}
//车 - 右转
//完整指令: #CR
if (payload[2] == 'R') {
}
//车 - 引擎速度
//完整指令: #Cs1 或者 #Cs2 - 6
//需要再加一个数字来设置等级 引擎速度等级 1-6 为合法
if (payload[2] == 's') {
int tmp_lv = 1;
if ( '0' < payload[3] && '7' > payload[3] ) {
tmp_lv = payload[3] - '0'; //char转数字
//#todo
}
}
}
//车载MP3播放相关处理
//todo
if ( payload[1] == 'M' ) {
//音乐 - 播放/暂停
//完整指令: #MS
if (payload[2] == 'S') {
bt_cmd_Play_or_Suspend();
}
//音乐 - 音量加
//完整指令: #MU
if (payload[2] == 'U') {
bt_cmd_VOL_UP();
}
//音乐 - 音量减
//完整指令: #MD
if (payload[2] == 'D') {
bt_cmd_VOL_DN();
}
//音乐 - 下一曲
//完整指令: #MN
if (payload[2] == 'N') {
bt_cmd_NEXT();
}
//音乐 - 上一曲
//完整指令: #MP
if (payload[2] == 'P') {
bt_cmd_PREV();
}
}
//枪炮相关处理
//todo
if ( payload[1] == 'G' ) {
//枪炮 - 上调角度
//完整指令: #GU
if (payload[2] == 'U') {
gun_cmd_up();
}
//枪炮 - 下调角度
//完整指令: #GD
if (payload[2] == 'D') {
gun_cmd_down();
}
//枪炮 - 左调角度
//完整指令: #GL
if (payload[2] == 'L') {
gun_cmd_left();
}
//枪炮 - 右调角度
//完整指令: #GR
if (payload[2] == 'R') {
gun_cmd_right();
}
//枪炮 - 发射
//完整指令: #GG
if (payload[2] == 'G') {
gun_cmd_send();
}
//枪炮 - 激光
//完整指令: #Gl (小写L)
if (payload[2] == 'l') {
gun_cmd_laser();
}
}
if ( payload[1] == 'S' ) {
//设置指令--恢复出厂
//完整指令: #SYakE+ (中间两个字母小写,最后一个加号)
if (payload[2] == 'Y' && payload[3] == 'a' && payload[4] == 'k' && payload[5] == 'E'&& payload[6] == '+') {
reset_all();
}
//设置指令--关闭调试
//完整指令: #SO
if (payload[2] == 'O'){
off_debug();
}
}
}else{
//控制台和ws返回错误
webSocket.sendTXT(num, ERR_CMD);
//调试输出
if (is_debug) {
USE_SERIAL.println(ERR_CMD);
}
}
}
// send message to client
// webSocket.sendTXT(num, "message here");
// send data to all connected clients
// webSocket.broadcastTXT("message here");
break;
//如果客户端发来二进制文件
//直接ban掉,不处理
case WStype_BIN:
webSocket.sendTXT(num, ERR_TYPE);
break;
}
}