新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 终于搞定了!花30元,DIY了一只机器狗!

终于搞定了!花30元,DIY了一只机器狗!

作者: 时间:2024-12-31 来源:嘉立创 收藏

30元了一只超可爱的机器狗!

本文引用地址:https://www.eepw.com.cn/article/202412/465941.htm

目前,项目已全开源!

它有哪些功能(第1章)软硬件怎么设计(2-3章)如何校准舵机(4章)开源资料入口(6章)下文咱们一一了解~

1.功能&亮点

  • 支持手机遥控

  • 支持表情、每日天气、时间显示

  • 电路大部分使用插件封装,成本低廉且易于新手焊接,适用于教学

  • 本项目不含电池,成本大致为30元,主要费用在于舵机12元,屏幕4.5元,主控4元

  • 支持功能拓展,代码已完全开源,可以根据现有代码逻辑框架,添加更多好玩有趣的功能

当然你也可以完全重构,使用更好性能的主控

在软件上可以添加语音交互,大模型对话等等

在硬件上也可以添加避障,测温,搭载炮台等等

2.硬件设计

电路由以下部分组成——电源部分、ESP8266主控、外部接口。

EDA-Robot插件版原理图

EDA-Robot插件版PCB图

①硬件参数

  • 主控:ESP8266,内置WIFI功能,通过AP模式遥控

  • OLED显示屏:0.96寸,可显示表情、时钟、天气等信息

  • LDO线性稳压器:AMS1117 ,负责将8.4V和5V电压分别转换成5V和3.3V,为舵机及主控提供电源

  • 舵机:SG-90/MG90,支持180度/360度版本,本文以360度版本为主

  • 供电:14500双节电池组,通过LDO降压稳压器供电

  • OLED显示屏支持SSD1315,SSD1306驱动,该模块自带屏幕驱动电路,仅需接口接入即可。

  • 电路设计软件:嘉立创EDA

②原理解析

(1)ADC电量检测电路

修改分压器适配 8.4V 到 1V

现在需要适配新的输入电压范围(最大 8.4V)到 ESP8266 的 1.0V ADC 输入。

分压比计算如下:

分压比=1V8.4V=18.4≈0.119分压比=8.4V1V=8.41≈0.119

根据分压公式:

R2R1+R2=0.119R1+R2R2=0.119

假设保持100k ,计算 :

100kR1+100k=0.119R1+100k100k=0.119

R1+100k=100k0.119≈840kR1+100k=0.119100k≈840k

R1≈740kΩR1≈740kΩ

对于 ,输出电压:

Vout=8.4×100k740k+100k=8.4×100840≈1.0VVout=8.4×740k+100k100k=8.4×840100≈1.0V

对于电压较低时(如 4.2V),输出电压为:

Vout=4.2×100k740k+100k=4.2×100840≈0.5VVout=4.2×740k+100k100k=4.2×840100≈0.5V

分压电路成功将8.4V的输入电压,压缩到0-1V范围内

(2)外部接口电路

串口:为方便下载,单独引出了IO0及GND接口作为跳帽插入接口,当插入跳帽时,IO0被拉低,进入下载模式。反之被主控部分电路拉高,进入工作模式。

电池:引出了外部充电拓展接口,VIN与VBAT是开关接口,VIN与GND接口是外部充电模块接口。充电模块选择满电电压大概在8.4V的2串锂电池充电模块。

按键:使用IO2和IO15引脚,IO2按键按下时拉低,空闲时被拉高。但由于IO15必须接下拉电阻,所以这里开关逻辑与IO2相反,按键按下时拉高,空闲时被拉低。

3.软件代码

本章节只介绍部分比较重要的关键代码。

开源网址:
https://oshwhub.com/course-examples/bot-dog 
开发文档:
https://wiki.lceda.cn/zh-hans/course-projects/smart-internet/eda-robot/eda-robot-introduce.html

如何通过手机【控制】机器狗?

为了控制机器狗,我写了一个网页,你可以直接使用,也可以参考下方了逻辑,自己写一个,并在此基础上进行拓展。

①控制页面CSS样式表

    body {            margin: 0;            padding: 0;            font-family: Arial, sans-serif;
        }        .container {            max-width: 800px;            margin: 0 auto;            padding: 20px;            text-align: center;
        }        h1 {            text-align: center;
        }        button {            display: inline-block;            height: auto;            width: auto;            margin-top: 20px;            padding: 10px 20px;            background-color: deepskyblue;            color: #fff;            border: none;            border-radius: 20px; /* 添加圆角 */
            text-decoration: none;            line-height: 2; /* 通过调整line-height的值来调整文字的垂直位置 */
            text-align: center; /* 文字居中 */
            box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* 添加立体感 */
            transition: all 0.3s ease; /* 添加过渡效果 */
        }        button:hover {            background-color: skyblue; /* 鼠标悬停时的背景颜色 */
            transform: translateY(2px); /* 点击效果 */
            box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3); /* 添加更多立体感 */
        }        .button-grid3 {            display: grid;            grid-template-columns: repeat(3, 1fr);            gap: 10px;            justify-content: center;            align-content: center;            text-align: center;            margin: 20px;
        }        .button-grid2 {            display: grid;            grid-template-columns: repeat(2, 1fr);            gap: 10px;            justify-content: center;            align-content: center;            text-align: center;            margin: 20px;
        } .button-grid1 {              display: grid;              border-radius: 20px; /* 添加圆角 */
              grid-template-columns: repeat(1, 1fr);              justify-content: center;              align-content: center;              text-align: center;              margin: 10px;
          }

②控制页面JavaScript代码

 // 简化 AJAX 请求函数
        function sendCommand(action) {
            fetch(`/${action}`)
                .then(response => response.text())
                .catch(() => alert('发送失败,请检查设备连接'));
        }    
        function refreshState(url, displayElementId) {
            fetch(url)
                .then(response => response.text())
                .then(data => {                    document.getElementById(displayElementId).innerText = data;
                });
        }        function setRefreshInterval(url, displayElementId) {
            setInterval(() => refreshState(url, displayElementId), 1000);
        }        const states = [
         { url: '/batteryVoltage', displayId: 'batteryVoltageDisplay' },
            { url: '/batteryPercentage', displayId: 'batteryPercentageDisplay' },
            { url: '/engine1offsetleftpwm', displayId: 'engine1offsetleftpwmDisplay' },
            { url: '/engine1offsetrightpwm', displayId: 'engine1offsetrightpwmDisplay' },
            { url: '/engine2offsetleftpwm', displayId: 'engine2offsetleftpwmDisplay' },
            { url: '/engine2offsetrightpwm', displayId: 'engine2offsetrightpwmDisplay' },
            { url: '/engine3offsetleftpwm', displayId: 'engine3offsetleftpwmDisplay' },
            { url: '/engine3offsetrightpwm', displayId: 'engine3offsetrightpwmDisplay' },
            { url: '/engine4offsetleftpwm', displayId: 'engine4offsetleftpwmDisplay' },
            { url: '/engine4offsetrightpwm', displayId: 'engine4offsetrightpwmDisplay' }
        ];
        states.forEach(state => setRefreshInterval(state.url, state.displayId));

③控制页面HTML代码

<div>
    <h1>EDA-Robot遥控台</h1>
    <p>本项目基于ESP8266主控开发</p>
            <div style="display:flex;justify-content:center">
            <p>电压:<span id="batteryVoltageDisplay">0</span></p>
            <p>电量:<span id="batteryPercentageDisplay">0</span></p>
        </div>
    <div style="background-color:papayawhip">
        <h3>运动控制</h3>
        <div style="display:flex;justify-content:center">
            ↑        </div>
        <div style="display:flex;justify-content:center">
            ←
            →        </div>
        <div style="display:flex;justify-content:center">
            ↓        </div>
        <div>
            抬左手
            抬右手
            坐下
            趴下
            自由模式开
            自由模式关        </div>
    </div>
    
    <div style="background-color:limegreen">
        <h3>表情控制</h3>
        <div>
            开心
            生气
            难受
            好奇
            喜欢
            错误
            晕        </div>
    </div>
    
    <div style="background-color:orange">
        <h3>联网功能</h3>
        <div>
            时间
            天气        </div>
    </div></div>

控制页面的代码是存放在FS文件系统中的,这里主要看AJAX请求函数,这部分的请求与下一小节的页面路由监听代码相对应,我们通过点击页面按钮触发请求

这里进行了一些简化操作,避免html过长过大导致html加载和响应缓慢,这可能导致esp8266无法正确显示页面。

如何让机器狗【运行起来】?给它注入点赛博灵魂~~

④页面路由监听

void handleWiFiConfig()
{    // 启动服务器
    server.on("/left90", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 10;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/right90", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 11;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/front", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 1;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/back", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 4;   // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/left", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 2;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/right", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 3;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/toplefthand", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 5;   // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/toprighthand", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 6;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/sitdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 8;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/lie", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      actionstate = 7; 
        request->send(200, "text/plain", "Front function started"); });    // server.on("/dance", HTTP_GET, [](AsyncWebServerRequest *request)
    //           {
    //     actionstate = 7;  // 设置标志,执行舵机动作
    //     request->send(200, "text/plain", "Front function started"); });
    server.on("/free", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      freestate=true;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/offfree", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      freestate=false;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/histate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        emojiState = 0;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/angrystate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        emojiState = 1;   // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/errorstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 2;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetleftpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine1offsetleftpwm)); });
    server.on("/engine2offsetleftpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine2offsetleftpwm)); });
    server.on("/engine3offsetleftpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine3offsetleftpwm)); });
    server.on("/engine4offsetleftpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine4offsetleftpwm)); });
    server.on("/engine1offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine1offsetrightpwm)); });
    server.on("/engine2offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine2offsetrightpwm)); });
    server.on("/engine3offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine3offsetrightpwm)); });
    server.on("/engine4offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine4offsetrightpwm)); });
    server.on("/engine4offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine4offsetrightpwm)); });
                  server.on("/batteryVoltage", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(batteryVoltage)); });     
    server.on("/batteryPercentage", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(batteryPercentage)); });  
    server.on("/speed", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(speed)); });
    server.on("/speedup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       speed++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/speeddown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
    speed--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetrightpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine1offsetrightpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetrightpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine1offsetrightpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetleftpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine1offsetleftpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetleftpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine1offsetleftpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine2offsetrightpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine2offsetrightpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine2offsetrightpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine2offsetrightpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine2offsetleftpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine2offsetleftpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine2offsetleftpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine2offsetleftpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine3offsetrightpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine3offsetrightpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine3offsetrightpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine3offsetrightpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine3offsetleftpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine3offsetleftpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine3offsetleftpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine3offsetleftpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine4offsetrightpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine4offsetrightpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine4offsetrightpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine4offsetrightpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine4offsetleftpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine4offsetleftpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine4offsetleftpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine4offsetleftpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/speedup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       speed++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/speeddown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       speed--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/dowhatstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 3;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/lovestate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 4;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/sickstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 5;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/yunstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 6; 
        request->send(200, "text/plain", "Front function started"); });
    server.on("/time", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 8; 
        request->send(200, "text/plain", "Front function started"); });
    server.on("/weather", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 7;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/connect", HTTP_POST, [](AsyncWebServerRequest *request)
              {        // 获取POST参数:ssid、pass、uid、city、api
        String ssid = request->getParam("ssid", true)->value();        String pass = request->getParam("pass", true)->value();        String uid = request->getParam("uid", true)->value();        String city = request->getParam("city", true)->value();        String api = request->getParam("api", true)->value();        // 打印接收到的参数
        Serial.println(ssid);
        Serial.println(pass);        // 保存WiFi信息到JSON文件
        DynamicJsonDocument doc(1024);
        doc["ssid"] = ssid;
        doc["pass"] = pass;
        doc["uid"] = uid;
        doc["city"] = city;
        doc["api"] = api;
        fs::File file = SPIFFS.open(ssidFile, "w");  // 打开文件进行写入
        if (file) {
            serializeJson(doc, file);  // 将JSON内容写入文件
            file.close();  // 关闭文件
        }        // 更新全局变量
        useruid = uid;
        cityname = city;
        weatherapi = api;        // 开始连接WiFi
        WiFi.begin(ssid.c_str(), pass.c_str());        // 发送HTML响应,告知用户正在连接
        request->send(200, "text/html", "<h1>Connecting...</h1>"); });
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
              {        // 检查SPIFFS文件系统中是否存在index.html文件
        if (SPIFFS.exists("/index.html")) {
            fs::File file = SPIFFS.open("/index.html", "r");  // 打开index.html文件
            if (file) {
                size_t fileSize = file.size();  // 获取文件大小
                String fileContent;                // 逐字节读取文件内容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 关闭文件
                // 返回HTML内容
                request->send(200, "text/html", fileContent);                return;
            }
        }        // 如果文件不存在,返回404错误
        request->send(404, "text/plain", "File Not Found"); });
    server.on("/control.html", HTTP_GET, [](AsyncWebServerRequest *request)
              {        // 检查SPIFFS文件系统中是否存在index.html文件
        if (SPIFFS.exists("/control.html")) {
            fs::File file = SPIFFS.open("/control.html", "r");  // 打开index.html文件
            if (file) {
                size_t fileSize = file.size();  // 获取文件大小
                String fileContent;                // 逐字节读取文件内容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 关闭文件
                // 返回HTML内容
                request->send(200, "text/html", fileContent);                return;
            }
        }        // 如果文件不存在,返回404错误
        request->send(404, "text/plain", "File Not Found"); });
    server.on("/engine.html", HTTP_GET, [](AsyncWebServerRequest *request)
              {        // 检查SPIFFS文件系统中是否存在index.html文件
        if (SPIFFS.exists("/engine.html")) {
            fs::File file = SPIFFS.open("/engine.html", "r");  // 打开index.html文件
            if (file) {
                size_t fileSize = file.size();  // 获取文件大小
                String fileContent;                // 逐字节读取文件内容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 关闭文件
                // 返回HTML内容
                request->send(200, "text/html", fileContent);                return;
            }
        }        // 如果文件不存在,返回404错误
        request->send(404, "text/plain", "File Not Found"); });
    server.on("/setting.html", HTTP_GET, [](AsyncWebServerRequest *request)
              {        // 检查SPIFFS文件系统中是否存在index.html文件
        if (SPIFFS.exists("/setting.html")) {
            fs::File file = SPIFFS.open("/setting.html", "r");  // 打开index.html文件
            if (file) {
                size_t fileSize = file.size();  // 获取文件大小
                String fileContent;                // 逐字节读取文件内容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 关闭文件
                // 返回HTML内容
                request->send(200, "text/html", fileContent);                return;
            }
        }        // 如果文件不存在,返回404错误
        request->send(404, "text/plain", "File Not Found"); });    // 启动服务器
    server.begin();
};

这部分的代码较长,是所有WebServer的页面路由监听,与页面中按钮触发的url对应,这里的url务必检查仔细,如果不能对应就无法监听到页面请求是否触发,硬件也无法做出对应的响应。

另外,在/connect下还添加了写入信息到FS文件系统中的功能,只要每次开机执行读取就不需要重复配置网络信息了。

⑤读取FS系统保存的json文件

void loadWiFiConfig(){    if (SPIFFS.begin())
    {
        fs::File file = SPIFFS.open(ssidFile, "r");        if (file)
        {            DynamicJsonDocument doc(1024);
            DeserializationError error = deserializeJson(doc, file);            if (!error)
            {
                String ssid = doc["ssid"];
                String pass = doc["pass"];
                String uid = doc["uid"];
                String city = doc["city"];
                String api = doc["api"];
                useruid = uid;
                cityname = city;
                weatherapi = api;
                WiFi.begin(ssid.c_str(), pass.c_str());                // 尝试连接WiFi,最多等待10秒
                unsigned long startAttemptTime = millis();                while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 5000)
                {
                    delay(500);
                }                // 如果连接失败,打印状态
                if (WiFi.status() != WL_CONNECTED)
                {
                    Serial.println("WiFi connection failed, starting captive portal...");
                    handleWiFiConfig(); // 启动强制门户
                }                else
                {
                    Serial.println("WiFi connected");
                    timeClient.begin();
                }
            }
            file.close();
        }
    }
}

前面我们讲了在/connect路由监听下,添加了将信息保存的FS文件系统,那么,这里的loadWiFiConfig()方法就是读取FS文件系统的Json文件,并将数据同步到全局变量之中,这样就不需要每次开机进入配置页面配网了,程序会自动加载上次配网保存的信息,极为方便。

⑥运动状态

switch (actionstate)
    {    case 0 /* constant-expression */:        /* code */
        break;    case 1:
        front(); // 执行一次舵机动作
        actionstate = 0;        break;    case 2:        left(); // 执行一次舵机动作
        actionstate = 0;        break;    case 3:        right(); // 执行一次舵机动作
        actionstate = 0;        break;    case 4:
        back(); // 执行一次舵机动作
        actionstate = 0;        break;    case 5:
        toplefthand(); // 执行一次舵机动作
        actionstate = 0;        break;    case 6:
        toprighthand(); // 执行一次舵机动作
        actionstate = 0;        break;    case 10:
        left90(); // 执行一次舵机动作
        actionstate = 0;        break;    case 11:
        right90(); // 执行一次舵机动作
        actionstate = 0;        break;    case 7:
        lie(); // 执行一次舵机动作
        actionstate = 0;        break;    case 8:
        sitdown(); // 执行一次舵机动作
        actionstate = 0;        break;    case 9:
        emojiState = random(0, 7); // 执行一次舵机动作
        actionstate = 0;        break;    default:        break;
    }

运动状态代码与前面的路由监听对应,之所以没有把动作函数直接写入路由监听的代码,这是因为会导致页面响应过久,导致页面无法加载或者触发程序死机然后重启。

为了避免这个情况发生,我们通过actionstate变量定义运动状态,然后再loop函数中判断。

这里选择的是switch,而并没有使用if-else,理论上对应顺序较长的数据switch性能略好,看个人喜欢,其实都可以用。

⑦前进运动

void front()
{    //+30C 2/3
    servo2.writeMicroseconds(1500 + speed + engine2offsetleftpwm);    servo3.writeMicroseconds(1500 - speed - engine3offsetleftpwm);    delay(500-runtime);    servo2.writeMicroseconds(1500);    servo3.writeMicroseconds(1500);    //-30C 1/4
    servo1.writeMicroseconds(1500 - speed - engine1offsetrightpwm);    servo4.writeMicroseconds(1500 + speed + engine4offsetrightpwm);    delay(500-runtime);    servo1.writeMicroseconds(1500);    servo4.writeMicroseconds(1500);    // 0C 2/3
    servo2.writeMicroseconds(1500 - speed - engine2offsetrightpwm);    servo3.writeMicroseconds(1500 + speed + engine3offsetrightpwm);    delay(500-runtime);    servo2.writeMicroseconds(1500);    servo3.writeMicroseconds(1500);    // 0C 1/4
    servo1.writeMicroseconds(1500 + speed + engine1offsetleftpwm);    servo4.writeMicroseconds(1500 - speed - engine4offsetleftpwm);    delay(500-runtime);    servo1.writeMicroseconds(1500);    servo4.writeMicroseconds(1500);    //+30C 1/4
    servo1.writeMicroseconds(1500 + speed + engine1offsetleftpwm);    servo4.writeMicroseconds(1500 - speed - engine4offsetleftpwm);    delay(500-runtime);    servo1.writeMicroseconds(1500);    servo4.writeMicroseconds(1500);    //-30C 2/3
    servo2.writeMicroseconds(1500 - speed - engine2offsetrightpwm);    servo3.writeMicroseconds(1500 + speed + engine3offsetrightpwm);    delay(500-runtime);    servo2.writeMicroseconds(1500);    servo3.writeMicroseconds(1500);    // 0C 1/4
    servo1.writeMicroseconds(1500 - speed - engine1offsetrightpwm);    servo4.writeMicroseconds(1500 + speed + engine4offsetrightpwm);    delay(500-runtime);    servo1.writeMicroseconds(1500);    servo4.writeMicroseconds(1500);    // 0C 2/3
    servo2.writeMicroseconds(1500 + speed + engine2offsetleftpwm);    servo3.writeMicroseconds(1500 - speed - engine3offsetleftpwm);    delay(500-runtime);    servo2.writeMicroseconds(1500);    servo3.writeMicroseconds(1500);
}

⑧ADC电量检测

// 对 ADC 数据多次采样并计算平均值float getAverageAdcVoltage() {  long totalAdcValue = 0;  // 多次采样
  for (int i = 0; i < numSamples; i++) {
    totalAdcValue += analogRead(A0); // 读取 ADC 数据
    delay(10); // 每次采样间隔 10ms
  }  // 计算平均 ADC 值
  float averageAdcValue = totalAdcValue / (float)numSamples;  // 将 ADC 值转换为电压
  return (averageAdcValue / 1023.0) * 1.0; // ESP8266 的参考电压为 1.0V}// 计算电池电量百分比的函数int mapBatteryPercentage(float voltage) {  if (voltage <= minVoltage) return 0;   // 小于等于最小电压时,电量为 0%
  if (voltage >= maxVoltage) return 100; // 大于等于最大电压时,电量为 100%
  // 根据线性比例计算电量百分比
  return (int)((voltage - minVoltage) / (maxVoltage - minVoltage) * 100);
}

与小车不同,机器狗不能像小车那样简单控制电机正反转,实现前进后退,这里需要观察四足动物,进行一些仿生模拟,用舵机模拟四足动物前进时的四足变化情况。下一章,我们就讲这个!

4.舵机校准

如何让机器狗麻溜滴【走起来】且不顺拐?

机器小狗使用360度舵机,其拓展性高,但不像180度舵机那样,可以直接控制旋转角度,所有我们需要进行舵机校准,确保舵机转速,角度均合适。

说明:刷入程序的舵机校准数据并不是通用的,这要根据自己的舵机情况进行调整。

精确校准

接着,请按一下步骤进行精调。

1.将所有脚固定到相同角度。 2.滑到校准页的底部,点击4次‘电机左转90度’。 3.找到转动大于360度或小于360度的脚,进行舵机补偿

修改程序重新烧录

记录下认为合理的各个电机补偿值,修改程序的补偿定义,重新刷入程序,当然,不重新输入也可以,这个值是立即生效的。

但是为了能快速响应,避免重复刷写降低寿命,所以不会保存到FS文件系统,下次重启也不会被保留。

当然啦!其实更推荐使用180度版本,因为其自带限位器,为了便于大家,原工程中,已开源了180度舵机的版本。可前往原工程查看!

开源网址:
https://oshwhub.com/course-examples/bot-dog 

开发文档:
https://wiki.lceda.cn/zh-hans/course-projects/smart-internet/eda-robot/eda-robot-introduce.html



关键词: DIY 电子狗

评论


相关推荐

技术专区

关闭