用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;
    }

}