新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 設(shè)計應(yīng)用 > 終于搞定了!花30元,DIY了一只機(jī)器狗!

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

作者: 時間:2024-12-31 來源:嘉立創(chuàng) 收藏

30元,了一只超可愛的機(jī)器狗!

本文引用地址:http://m.butianyuan.cn/article/202412/465941.htm

目前,項目已全開源!

它有哪些功能(第1章)?軟硬件怎么設(shè)計(2-3章)?如何校準(zhǔn)舵機(jī)(4章)?開源資料入口(6章)下文咱們一一了解~

1.功能&亮點

  • 支持手機(jī)遙控

  • 支持表情、每日天氣、時間顯示

  • 電路大部分使用插件封裝,成本低廉且易于新手焊接,適用于教學(xué)

  • 本項目不含電池,成本大致為30元,主要費(fèi)用在于舵機(jī)12元,屏幕4.5元,主控4元

  • 支持功能拓展,代碼已完全開源,可以根據(jù)現(xiàn)有代碼邏輯框架,添加更多好玩有趣的功能

當(dāng)然你也可以完全重構(gòu),使用更好性能的主控

在軟件上可以添加語音交互,大模型對話等等

在硬件上也可以添加避障,測溫,搭載炮臺等等

2.硬件設(shè)計

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

EDA-Robot插件版原理圖

EDA-Robot插件版PCB圖

①硬件參數(shù)

  • 主控:ESP8266,內(nèi)置WIFI功能,通過AP模式遙控

  • OLED顯示屏:0.96寸,可顯示表情、時鐘、天氣等信息

  • LDO線性穩(wěn)壓器:AMS1117 ,負(fù)責(zé)將8.4V和5V電壓分別轉(zhuǎn)換成5V和3.3V,為舵機(jī)及主控提供電源

  • 舵機(jī):SG-90/MG90,支持180度/360度版本,本文以360度版本為主

  • 供電:14500雙節(jié)電池組,通過LDO降壓穩(wěn)壓器供電

  • OLED顯示屏支持SSD1315,SSD1306驅(qū)動,該模塊自帶屏幕驅(qū)動電路,僅需接口接入即可。

  • 電路設(shè)計軟件:嘉立創(chuàng)EDA

②原理解析

(1)ADC電量檢測電路

修改分壓器適配 8.4V 到 1V

現(xiàn)在需要適配新的輸入電壓范圍(最大 8.4V)到 ESP8266 的 1.0V ADC 輸入。

分壓比計算如下:

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

根據(jù)分壓公式:

R2R1+R2=0.119R1+R2R2=0.119

假設(shè)保持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范圍內(nèi)

(2)外部接口電路

串口:為方便下載,單獨引出了IO0及GND接口作為跳帽插入接口,當(dāng)插入跳帽時,IO0被拉低,進(jìn)入下載模式。反之被主控部分電路拉高,進(jìn)入工作模式。

電池:引出了外部充電拓展接口,VIN與VBAT是開關(guān)接口,VIN與GND接口是外部充電模塊接口。充電模塊選擇滿電電壓大概在8.4V的2串鋰電池充電模塊。

按鍵:使用IO2和IO15引腳,IO2按鍵按下時拉低,空閑時被拉高。但由于IO15必須接下拉電阻,所以這里開關(guān)邏輯與IO2相反,按鍵按下時拉高,空閑時被拉低。

3.軟件代碼

本章節(jié)只介紹部分比較重要的關(guān)鍵代碼。

開源網(wǎng)址:
https://oshwhub.com/course-examples/bot-dog 
開發(fā)文檔:
https://wiki.lceda.cn/zh-hans/course-projects/smart-internet/eda-robot/eda-robot-introduce.html

如何通過手機(jī)【控制】機(jī)器狗?

為了控制機(jī)器狗,我寫了一個網(wǎng)頁,你可以直接使用,也可以參考下方了邏輯,自己寫一個,并在此基礎(chǔ)上進(jìn)行拓展。

①控制頁面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; /* 通過調(diào)整line-height的值來調(diào)整文字的垂直位置 */
            text-align: center; /* 文字居中 */
            box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* 添加立體感 */
            transition: all 0.3s ease; /* 添加過渡效果 */
        }        button:hover {            background-color: skyblue; /* 鼠標(biāo)懸停時的背景顏色 */
            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 請求函數(shù)
        function sendCommand(action) {
            fetch(`/${action}`)
                .then(response => response.text())
                .catch(() => alert('發(fā)送失敗,請檢查設(shè)備連接'));
        }    
        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主控開發(fā)</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>運(yùn)動控制</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>
            抬左手
            抬右手
            坐下
            趴下
            自由模式開
            自由模式關(guān)        </div>
    </div>
    
    <div style="background-color:limegreen">
        <h3>表情控制</h3>
        <div>
            開心
            生氣
            難受
            好奇
            喜歡
            錯誤
            暈        </div>
    </div>
    
    <div style="background-color:orange">
        <h3>聯(lián)網(wǎng)功能</h3>
        <div>
            時間
            天氣        </div>
    </div></div>

控制頁面的代碼是存放在FS文件系統(tǒng)中的,這里主要看AJAX請求函數(shù),這部分的請求與下一小節(jié)的頁面路由監(jiān)聽代碼相對應(yīng),我們通過點擊頁面按鈕觸發(fā)請求。

這里進(jìn)行了一些簡化操作,避免html過長過大導(dǎo)致html加載和響應(yīng)緩慢,這可能導(dǎo)致esp8266無法正確顯示頁面。

如何讓機(jī)器狗【運(yùn)行起來】?給它注入點賽博靈魂~~

④頁面路由監(jiān)聽

void handleWiFiConfig()
{    // 啟動服務(wù)器
    server.on("/left90", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 10;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/right90", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 11;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/front", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 1;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/back", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 4;   // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/left", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 2;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/right", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 3;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/toplefthand", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 5;   // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/toprighthand", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 6;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/sitdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 8;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
    //     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;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/angrystate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        emojiState = 1;   // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/errorstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 2;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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++;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/lovestate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 4;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/sickstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 5;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        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;  // 設(shè)置標(biāo)志,執(zhí)行舵機(jī)動作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/connect", HTTP_POST, [](AsyncWebServerRequest *request)
              {        // 獲取POST參數(shù):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();        // 打印接收到的參數(shù)
        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");  // 打開文件進(jìn)行寫入
        if (file) {
            serializeJson(doc, file);  // 將JSON內(nèi)容寫入文件
            file.close();  // 關(guān)閉文件
        }        // 更新全局變量
        useruid = uid;
        cityname = city;
        weatherapi = api;        // 開始連接WiFi
        WiFi.begin(ssid.c_str(), pass.c_str());        // 發(fā)送HTML響應(yīng),告知用戶正在連接
        request->send(200, "text/html", "<h1>Connecting...</h1>"); });
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
              {        // 檢查SPIFFS文件系統(tǒng)中是否存在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;                // 逐字節(jié)讀取文件內(nèi)容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 關(guān)閉文件
                // 返回HTML內(nèi)容
                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文件系統(tǒng)中是否存在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;                // 逐字節(jié)讀取文件內(nèi)容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 關(guān)閉文件
                // 返回HTML內(nèi)容
                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文件系統(tǒng)中是否存在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;                // 逐字節(jié)讀取文件內(nèi)容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 關(guān)閉文件
                // 返回HTML內(nèi)容
                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文件系統(tǒng)中是否存在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;                // 逐字節(jié)讀取文件內(nèi)容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 關(guān)閉文件
                // 返回HTML內(nèi)容
                request->send(200, "text/html", fileContent);                return;
            }
        }        // 如果文件不存在,返回404錯誤
        request->send(404, "text/plain", "File Not Found"); });    // 啟動服務(wù)器
    server.begin();
};

這部分的代碼較長,是所有WebServer的頁面路由監(jiān)聽,與頁面中按鈕觸發(fā)的url對應(yīng),這里的url務(wù)必檢查仔細(xì),如果不能對應(yīng)就無法監(jiān)聽到頁面請求是否觸發(fā),硬件也無法做出對應(yīng)的響應(yīng)。

另外,在/connect下還添加了寫入信息到FS文件系統(tǒng)中的功能,只要每次開機(jī)執(zhí)行讀取就不需要重復(fù)配置網(wǎng)絡(luò)信息了。

⑤讀取FS系統(tǒng)保存的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);
                }                // 如果連接失敗,打印狀態(tài)
                if (WiFi.status() != WL_CONNECTED)
                {
                    Serial.println("WiFi connection failed, starting captive portal...");
                    handleWiFiConfig(); // 啟動強(qiáng)制門戶
                }                else
                {
                    Serial.println("WiFi connected");
                    timeClient.begin();
                }
            }
            file.close();
        }
    }
}

前面我們講了在/connect路由監(jiān)聽下,添加了將信息保存的FS文件系統(tǒng),那么,這里的loadWiFiConfig()方法就是讀取FS文件系統(tǒng)的Json文件,并將數(shù)據(jù)同步到全局變量之中,這樣就不需要每次開機(jī)進(jìn)入配置頁面配網(wǎng)了,程序會自動加載上次配網(wǎng)保存的信息,極為方便。

⑥運(yùn)動狀態(tài)

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

運(yùn)動狀態(tài)代碼與前面的路由監(jiān)聽對應(yīng),之所以沒有把動作函數(shù)直接寫入路由監(jiān)聽的代碼,這是因為會導(dǎo)致頁面響應(yīng)過久,導(dǎo)致頁面無法加載或者觸發(fā)程序死機(jī)然后重啟。

為了避免這個情況發(fā)生,我們通過actionstate變量定義運(yùn)動狀態(tài),然后再loop函數(shù)中判斷。

這里選擇的是switch,而并沒有使用if-else,理論上對應(yīng)順序較長的數(shù)據(jù)switch性能略好,看個人喜歡,其實都可以用。

⑦前進(jìn)運(yùn)動

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 數(shù)據(jù)多次采樣并計算平均值float getAverageAdcVoltage() {  long totalAdcValue = 0;  // 多次采樣
  for (int i = 0; i < numSamples; i++) {
    totalAdcValue += analogRead(A0); // 讀取 ADC 數(shù)據(jù)
    delay(10); // 每次采樣間隔 10ms
  }  // 計算平均 ADC 值
  float averageAdcValue = totalAdcValue / (float)numSamples;  // 將 ADC 值轉(zhuǎn)換為電壓
  return (averageAdcValue / 1023.0) * 1.0; // ESP8266 的參考電壓為 1.0V}// 計算電池電量百分比的函數(shù)int mapBatteryPercentage(float voltage) {  if (voltage <= minVoltage) return 0;   // 小于等于最小電壓時,電量為 0%
  if (voltage >= maxVoltage) return 100; // 大于等于最大電壓時,電量為 100%
  // 根據(jù)線性比例計算電量百分比
  return (int)((voltage - minVoltage) / (maxVoltage - minVoltage) * 100);
}

與小車不同,機(jī)器狗不能像小車那樣簡單控制電機(jī)正反轉(zhuǎn),實現(xiàn)前進(jìn)后退,這里需要觀察四足動物,進(jìn)行一些仿生模擬,用舵機(jī)模擬四足動物前進(jìn)時的四足變化情況。下一章,我們就講這個!

4.舵機(jī)校準(zhǔn)

如何讓機(jī)器狗麻溜滴【走起來】且不順拐?

機(jī)器小狗使用360度舵機(jī),其拓展性高,但不像180度舵機(jī)那樣,可以直接控制旋轉(zhuǎn)角度,所有我們需要進(jìn)行舵機(jī)校準(zhǔn),確保舵機(jī)轉(zhuǎn)速,角度均合適。

說明:刷入程序的舵機(jī)校準(zhǔn)數(shù)據(jù)并不是通用的,這要根據(jù)自己的舵機(jī)情況進(jìn)行調(diào)整。

精確校準(zhǔn)

接著,請按一下步驟進(jìn)行精調(diào)。

1.將所有腳固定到相同角度。 2.滑到校準(zhǔn)頁的底部,點擊4次‘電機(jī)左轉(zhuǎn)90度’。 3.找到轉(zhuǎn)動大于360度或小于360度的腳,進(jìn)行舵機(jī)補(bǔ)償。

修改程序重新燒錄

記錄下認(rèn)為合理的各個電機(jī)補(bǔ)償值,修改程序的補(bǔ)償定義,重新刷入程序,當(dāng)然,不重新輸入也可以,這個值是立即生效的。

但是為了能快速響應(yīng),避免重復(fù)刷寫降低壽命,所以不會保存到FS文件系統(tǒng),下次重啟也不會被保留。

當(dāng)然啦!其實更推薦使用180度版本,因為其自帶限位器,為了便于大家,原工程中,已開源了180度舵機(jī)的版本。可前往原工程查看!

開源網(wǎng)址:
https://oshwhub.com/course-examples/bot-dog 

開發(fā)文檔:
https://wiki.lceda.cn/zh-hans/course-projects/smart-internet/eda-robot/eda-robot-introduce.html



關(guān)鍵詞: DIY 電子狗

評論


相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉