新聞中心

        EEPW首頁 > 嵌入式系統 > 設計應用 > 終于搞定了!花30元,DIY了一只機器狗!

        終于搞定了!花30元,DIY了一只機器狗!

        作者: 時間:2024-12-31 來源:嘉立創 收藏

        30元,了一只超可愛的機器狗!

        本文引用地址:http://www.104case.com/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 電子狗

        評論


        相關推薦

        技術專區

        關閉
        主站蜘蛛池模板: 唐河县| 汉中市| 米易县| 乌兰县| 屯门区| 仪陇县| 衡水市| 盐城市| 比如县| 高青县| 平果县| 普宁市| 虞城县| 长岛县| 明溪县| 柳江县| 高密市| 徐州市| 沁水县| 巴里| 稻城县| 连平县| 松溪县| 喀喇沁旗| 冀州市| 宁南县| 庆安县| 石嘴山市| 湖南省| 上饶县| 昌江| 舒城县| 郁南县| 班玛县| 辽宁省| 神农架林区| 乳山市| 辰溪县| 青神县| 苍梧县| 龙陵县|