注意:藍芽晶片最好是用BLE,對iOS手機而言,比較容易使用Web來控制。
本篇採用上圖的藍芽晶片。
本篇提到HTML程式已經上傳到Github,體驗網址:https://chengminlin2.github.io/OTTOWeb/
注意需先進行手機藍芽和OTTO藍芽進行配對,然後在手機瀏覽器上輸入體驗網址。
HC-05(SPP) 還是 BLE(HM-10/BT-05 類)?
最快判斷法:
-
iPhone「設定 → 藍牙」找得到、而且要配對 PIN(1234/0000)→ 多半是 HC-05/HC-06(SPP)
-
用 BLE Scanner 才看得到、一般不走傳統配對 → 多半是 BLE 模組
Arduino程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | /* Otto + HC-05 (SPP) Controller for Arduino UNO/Nano Commands: forward/backward/left/right/stop/ultrasound/avoidance/force + speedIndex(0~5) + '\n' Gestures: happy/superhappy/sad/sleeping/confused/fretful/love/angry/magic/wave/victory/fail/fart + '0' + '\n' */ #include <Otto.h> #include <EEPROM.h> #include <SoftwareSerial.h> // Otto pins #define LEFTLEG 2 #define RIGHTLEG 3 #define LEFTFOOT 4 #define RIGHTFOOT 5 #define BUZZER 13 // Ultrasonic pins #define TRIG 8 #define ECHO 9 // HC-05 wiring (recommended) #define BT_RX 10 // Arduino receives <- HC-05 TXD #define BT_TX 11 // Arduino sends -> HC-05 RXD (use voltage divider!) SoftwareSerial bluetooth(BT_RX, BT_TX); String device_name = "NFU-OTTO-HC05"; Otto Ottobot; int move_speed[] = {3000, 2000, 1000, 750, 500, 250}; int n = 2; int ultrasound_threeshold = 15; int v; int positions[] = {90, 90, 90, 90}; int8_t trims[4] = {0, 0, 0, 0}; unsigned long sync_time = 0; bool calibration = false; bool autoavoid = false; bool us_stream = false; unsigned long us_last_ms = 0; const unsigned long US_STREAM_INTERVAL_MS = 200; long ultrasound_distance() { long duration; digitalWrite(TRIG, LOW); delayMicroseconds(2); digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW); duration = pulseIn(ECHO, HIGH, 30000); // 30ms timeout ~ 5m if (duration == 0) return 999; return duration / 58; } void sendUS() { long d = ultrasound_distance(); bluetooth.print("US:"); bluetooth.print(d); bluetooth.print("\n"); } void Forward() { Ottobot.walk(1, move_speed[n], FORWARD); } void Backward() { Ottobot.walk(1, move_speed[n], BACKWARD); } void Right() { Ottobot.walk(1, move_speed[n], RIGHT); } void Left() { Ottobot.walk(1, move_speed[n], LEFT); } void Stop() { Ottobot.home(); } void AvoidanceOnce() { if (ultrasound_distance() <= ultrasound_threeshold) { Ottobot.playGesture(OttoConfused); for (int count = 0; count < 2; count++) Ottobot.walk(1, move_speed[n], BACKWARD); for (int count = 0; count < 4; count++) Ottobot.turn(1, move_speed[n], LEFT); } Ottobot.walk(1, move_speed[n], FORWARD); } void UseForceOnce() { long d = ultrasound_distance(); if (d <= ultrasound_threeshold) { Ottobot.walk(1, move_speed[n], BACKWARD); } else if (d > 10 && d < 15) { Ottobot.home(); } else if (d > 15 && d < 30) { Ottobot.walk(1, move_speed[n], FORWARD); } else { Ottobot.home(); } } void setTrims() { Ottobot.setTrims(trims[0], trims[1], trims[2], trims[3]); Ottobot._moveServos(10, positions); } void readChar(char ch) { switch (ch) { case '0'...'9': v = (v * 10 + ch) - 48; break; case 'a': trims[0] = v - 90; setTrims(); v = 0; break; case 'b': trims[1] = v - 90; setTrims(); v = 0; break; case 'c': trims[2] = v - 90; setTrims(); v = 0; break; case 'd': trims[3] = v - 90; setTrims(); v = 0; break; case 's': for (int i = 0; i <= 3; i++) EEPROM.write(i, trims[i]); delay(200); Ottobot.sing(S_superHappy); bluetooth.print("CAL:SAVED\n"); break; } } void Calibration(String c) { if (sync_time < millis()) { sync_time = millis() + 50; for (int k = 1; k < (int)c.length(); k++) readChar(c[k]); } } void applySpeedFromLastChar(const char* buf, int len) { if (len <= 0) return; char last = buf[len - 1]; if (last >= '0' && last <= '9') { n = last - '0'; if (n < 0) n = 0; if (n > 5) n = 5; } } void handleLine(const char* line, int len) { applySpeedFromLastChar(line, len); // movement if (strncmp(line, "forward", 7) == 0) { Forward(); return; } if (strncmp(line, "backward", 8) == 0) { Backward(); return; } if (strncmp(line, "left", 4) == 0) { Left(); return; } if (strncmp(line, "right", 5) == 0) { Right(); return; } if (strncmp(line, "stop", 4) == 0) { autoavoid = false; Stop(); return; } // ultrasound if (strncmp(line, "ultrasound", 10) == 0) { Stop(); sendUS(); return; } if (strncmp(line, "avoidance", 9) == 0) { AvoidanceOnce(); return; } if (strncmp(line, "force", 5) == 0) { UseForceOnce(); return; } // continuous features if (strncmp(line, "autoavoid_on", 11) == 0) { autoavoid = true; bluetooth.print("AUTOAVOID:ON\n"); return; } if (strncmp(line, "autoavoid_off", 12) == 0) { autoavoid = false; bluetooth.print("AUTOAVOID:OFF\n"); Stop(); return; } if (strncmp(line, "us_stream_on", 12) == 0) { us_stream = true; bluetooth.print("US_STREAM:ON\n"); return; } if (strncmp(line, "us_stream_off", 13) == 0) { us_stream = false; bluetooth.print("US_STREAM:OFF\n"); return; } // gestures if (strncmp(line, "happy", 5) == 0) { Ottobot.playGesture(OttoHappy); return; } if (strncmp(line, "superhappy", 10) == 0) { Ottobot.playGesture(OttoSuperHappy); return; } if (strncmp(line, "sad", 3) == 0) { Ottobot.playGesture(OttoSad); return; } if (strncmp(line, "sleeping", 8) == 0) { Ottobot.playGesture(OttoSleeping); return; } if (strncmp(line, "confused", 8) == 0) { Ottobot.playGesture(OttoConfused); return; } if (strncmp(line, "fretful", 7) == 0) { Ottobot.playGesture(OttoFretful); return; } if (strncmp(line, "love", 4) == 0) { Ottobot.playGesture(OttoLove); return; } if (strncmp(line, "angry", 5) == 0) { Ottobot.playGesture(OttoAngry); return; } if (strncmp(line, "magic", 5) == 0) { Ottobot.playGesture(OttoMagic); return; } if (strncmp(line, "wave", 4) == 0) { Ottobot.playGesture(OttoWave); return; } if (strncmp(line, "victory", 7) == 0) { Ottobot.playGesture(OttoVictory); return; } if (strncmp(line, "fail", 4) == 0) { Ottobot.playGesture(OttoFail); return; } if (strncmp(line, "fart", 4) == 0) { Ottobot.playGesture(OttoFart); return; } // calibration if (line[0] == 'C') { if (!calibration) { Ottobot._moveServos(10, positions); calibration = true; delay(50); } Calibration(String(line)); return; } if (strncmp(line, "save_calibration", 16) == 0) { readChar('s'); return; } bluetooth.print("ERR:UNKNOWN\n"); } void setup() { Serial.begin(9600); Ottobot.init(LEFTLEG, RIGHTLEG, LEFTFOOT, RIGHTFOOT, true, BUZZER); pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); bluetooth.begin(9600); // HC-05 data mode commonly 9600 // 不在這裡送 AT 指令改名,避免干擾(要改名請進 AT 模式另外做) Ottobot.home(); bluetooth.print("READY\n"); } void loop() { // Read one line from bluetooth static char buf[32]; static int idx = 0; while (bluetooth.available() > 0) { char c = (char)bluetooth.read(); if (c == '\r') continue; if (c == '\n') { buf[idx] = '\0'; if (idx > 0) { handleLine(buf, idx); } idx = 0; } else { if (idx < (int)sizeof(buf) - 1) buf[idx++] = c; } } // optional: forward debug from USB serial to bluetooth while (Serial.available() > 0) { bluetooth.write(Serial.read()); } // distance stream if (us_stream && (millis() - us_last_ms >= US_STREAM_INTERVAL_MS)) { us_last_ms = millis(); sendUS(); } // auto avoid loop if (autoavoid) { AvoidanceOnce(); } } |
HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | <!DOCTYPE html> <html lang="zh-TW"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Otto Web Serial Controller (HC-05 / USB)</title> <style> body { font-family: Arial, sans-serif; text-align:center; background:#f9f9f9; margin:0; padding:24px; } button{ width:160px; height:56px; margin:8px; font-size:16px; border:none; border-radius:10px; background:#4CAF50; color:white; cursor:pointer; } button:disabled{ opacity:.5; cursor:not-allowed; } button.red{ background:#d9534f; } button.blue{ background:#0275d8; } .panel{ max-width:760px; margin:14px auto; background:#fff; border-radius:14px; padding:14px 12px; box-shadow:0 2px 10px rgba(0,0,0,.06); } .row{ display:flex; flex-wrap:wrap; justify-content:center; gap:8px; margin:8px 0; } .status{ font-size:14px; color:#333; } .mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Courier New", monospace; } .log{ text-align:left; max-width:760px; margin:10px auto 0; background:#111; color:#eee; padding:10px 12px; border-radius:10px; font-size:12px; height:160px; overflow:auto; } input[type="range"]{ width:260px; } </style> </head> <body> <h2>OTTO 網頁遙控(Web Serial / USB)</h2> <div class="panel"> <div class="row"> <button id="connect" class="blue">連線序列埠</button> <button id="disconnect" class="red" disabled>中斷</button> </div> <p id="status" class="status">尚未連線</p> <div class="row" style="align-items:center;"> <span>速度</span> <input id="speed" type="range" min="0" max="5" step="1" value="2" /> <span id="speedLabel" class="mono">2</span> </div> <div class="status">指令格式:<span class="mono">command + speedIndex + "\\n"</span></div> </div> <div class="panel"> <h3>移動</h3> <div class="row"><button onclick="sendMove('forward')">前進</button></div> <div class="row"> <button onclick="sendMove('left')">左</button> <button onclick="sendMove('stop')" class="red">停止</button> <button onclick="sendMove('right')">右</button> </div> <div class="row"><button onclick="sendMove('backward')">後退</button></div> </div> <div class="panel"> <h3>超音波</h3> <div class="row"> <button onclick="sendCmd('ultrasound')">量測一次</button> <button onclick="sendCmd('us_stream_on')" class="blue">距離串流 ON</button> <button onclick="sendCmd('us_stream_off')" class="blue">距離串流 OFF</button> </div> <div class="row"> <button onclick="sendCmd('autoavoid_on')" class="blue">自動避障 ON</button> <button onclick="sendCmd('autoavoid_off')" class="blue">自動避障 OFF</button> </div> <p class="status">距離:<span id="us" class="mono">--</span> cm</p> </div> <div class="panel"> <h3>表情</h3> <div class="row"> <button onclick="sendGesture('happy')">開心</button> <button onclick="sendGesture('sad')">難過</button> <button onclick="sendGesture('confused')">困惑</button> <button onclick="sendGesture('wave')">揮手</button> </div> </div> <div id="log" class="log"></div> <script> let port, reader, writer; let speedIndex = 2; let keepReading = false; const connectBtn = document.getElementById('connect'); const disconnectBtn = document.getElementById('disconnect'); const statusEl = document.getElementById('status'); const logEl = document.getElementById('log'); const speedEl = document.getElementById('speed'); const speedLabel = document.getElementById('speedLabel'); const usEl = document.getElementById('us'); speedEl.addEventListener('input', () => { speedIndex = Number(speedEl.value); speedLabel.textContent = String(speedIndex); }); function logLine(s){ const t = new Date().toLocaleTimeString(); logEl.textContent += `[${t}] ${s}\n`; logEl.scrollTop = logEl.scrollHeight; } function setUI(connected){ connectBtn.disabled = connected; disconnectBtn.disabled = !connected; statusEl.textContent = connected ? '已連線' : '尚未連線'; } async function writeLine(str){ if(!writer){ alert('請先連線序列埠'); return; } const data = new TextEncoder().encode(str); await writer.write(data); logLine('>>> ' + JSON.stringify(str)); } function sendMove(cmd){ writeLine(cmd + String(speedIndex) + "\n"); } function sendGesture(cmd){ writeLine(cmd + "0\n"); } function sendCmd(cmd){ writeLine(cmd + String(speedIndex) + "\n"); } connectBtn.onclick = async () => { try{ if(!('serial' in navigator)){ alert('此瀏覽器不支援 Web Serial,請用 Chrome / Edge 桌面版'); return; } port = await navigator.serial.requestPort(); await port.open({ baudRate: 9600 }); writer = port.writable.getWriter(); reader = port.readable.getReader(); keepReading = true; setUI(true); logLine('序列埠已連線(9600)'); readLoop(); }catch(e){ logLine('連線失敗: ' + e); alert('連線失敗: ' + e); } }; disconnectBtn.onclick = async () => { keepReading = false; try{ if(reader){ await reader.cancel(); reader.releaseLock(); } }catch(_){} try{ if(writer){ writer.releaseLock(); } }catch(_){} try{ if(port){ await port.close(); } }catch(_){} reader = null; writer = null; port = null; setUI(false); logLine('已中斷'); }; async function readLoop(){ let buf = ''; const dec = new TextDecoder(); while(keepReading && reader){ const {value, done} = await reader.read(); if(done) break; const chunk = dec.decode(value); buf += chunk; let idx; while((idx = buf.indexOf("\n")) !== -1){ const line = buf.slice(0, idx).trim(); buf = buf.slice(idx + 1); if(!line) continue; logLine('<<< ' + line); // parse US:xx if(line.startsWith('US:')){ const v = parseInt(line.slice(3), 10); if(!Number.isNaN(v)) usEl.textContent = String(v); } } if(buf.length > 500) buf = buf.slice(-200); } } setUI(false); </script> </body> </html> |

沒有留言:
張貼留言