終于搞定了!花30元,DIY了一只機(jī)器狗!
花30元,DIY了一只超可愛的機(jī)器狗!
本文引用地址:http://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度版本,因為其自帶限位器,為了便于大家DIY,原工程中,已開源了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
評論