2026年1月24日 星期六

[OTTO Biped] 使用HC-05藍芽晶片來通訊,並採Web控制方式(僅適用於Andriod手機)




注意:藍芽晶片最好是用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>


沒有留言:

張貼留言