2026年2月12日 星期四

[OTTO GO] 超音波控制虎嘴


 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
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>

// ===== TFT pins (OTTO GO) =====
#define TFT_MOSI 19
#define TFT_SCLK 18
#define TFT_CS    5
#define TFT_DC   16
#define TFT_RST  23

// ===== Ultrasonic pins (OTTO GO) =====
#define US_TRIG 27
#define US_ECHO 39   // ESP32 輸入專用腳,適合 ECHO

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);

static inline uint16_t RGB565(uint8_t r, uint8_t g, uint8_t b) {
  return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}

// ================== 虎嘴繪圖 ==================
void drawMuzzleBase(int cx, int cy) {
  uint16_t furLight = RGB565(235, 228, 210);
  uint16_t shadow   = RGB565(160, 150, 135);
  uint16_t dotDark  = RGB565(70, 70, 70);

  tft.fillScreen(ST77XX_BLACK);

  // 口鼻白毛區
  tft.fillCircle(cx - 55, cy + 10, 55, furLight);
  tft.fillCircle(cx + 55, cy + 10, 55, furLight);
  tft.fillRoundRect(cx - 90, cy - 25, 180, 110, 28, furLight);

  // 下巴陰影
  tft.fillRoundRect(cx - 85, cy + 60, 170, 22, 12, shadow);
  tft.fillRoundRect(cx - 40, cy + 55, 80, 10, 5, RGB565(120,110,100));

  // 鬍鬚點
  int dotY1 = cy + 6;
  int dotY2 = cy + 26;
  for (int k = 0; k < 3; k++) {
    tft.fillCircle(cx - 55 - k * 12, dotY1 + k * 2, 2, dotDark);
    tft.fillCircle(cx - 55 - k * 12, dotY2 + k * 2, 2, dotDark);
    tft.fillCircle(cx + 55 + k * 12, dotY1 + k * 2, 2, dotDark);
    tft.fillCircle(cx + 55 + k * 12, dotY2 + k * 2, 2, dotDark);
  }
}

void drawMouthClosed(int cx, int cy) {
  uint16_t lineDark = RGB565(40, 40, 40);

  // 人中線
  tft.drawLine(cx,     cy - 5, cx,     cy + 20, lineDark);
  tft.drawLine(cx - 1, cy - 5, cx - 1, cy + 20, RGB565(80,80,80));

  // 上唇弧線
  int mouthY = cy + 18;
  for (int i = 0; i < 34; i++) {
    int y = mouthY + (i * i) / 95;
    tft.drawPixel(cx - i, y, lineDark);
    tft.drawPixel(cx - i, y + 1, lineDark);
    tft.drawPixel(cx + i, y, lineDark);
    tft.drawPixel(cx + i, y + 1, lineDark);
  }

  // 嘴角
  tft.fillCircle(cx - 36, mouthY + 12, 3, lineDark);
  tft.fillCircle(cx + 36, mouthY + 12, 3, lineDark);

  // 嘴中間小暗線
  tft.fillRoundRect(cx - 10, mouthY + 20, 20, 4, 2, RGB565(90,80,70));
}

void drawMouthOpen(int cx, int cy) {
  uint16_t lineDark = RGB565(40, 40, 40);
  uint16_t mouthIn  = RGB565(15, 15, 15);
  uint16_t tongue   = RGB565(210, 120, 120);

  // 人中線(到上唇)
  tft.drawLine(cx,     cy - 5, cx,     cy + 10, lineDark);
  tft.drawLine(cx - 1, cy - 5, cx - 1, cy + 10, RGB565(80,80,80));

  // 上唇弧線(更彎)
  int mouthY = cy + 16;
  for (int i = 0; i < 36; i++) {
    int y = mouthY + (i * i) / 85;
    tft.drawPixel(cx - i, y, lineDark);
    tft.drawPixel(cx - i, y + 1, lineDark);
    tft.drawPixel(cx + i, y, lineDark);
    tft.drawPixel(cx + i, y + 1, lineDark);
  }

  // 嘴角
  tft.fillCircle(cx - 38, mouthY + 14, 3, lineDark);
  tft.fillCircle(cx + 38, mouthY + 14, 3, lineDark);

  // 口腔
  int w = 90, h = 45;
  int x = cx - w / 2;
  int y0 = cy + 28;
  tft.fillRoundRect(x, y0, w, h, 18, mouthIn);
  tft.fillCircle(cx, y0 + h, 22, mouthIn);

  // 舌頭
  tft.fillRoundRect(cx - 22, y0 + 18, 44, 20, 10, tongue);
  tft.fillCircle(cx, y0 + 36, 14, tongue);
}

bool mouthOpen = false;

void drawTigerMouth(bool open) {
  int cx = tft.width() / 2;
  int cy = tft.height() / 2;

  drawMuzzleBase(cx, cy);
  if (open) drawMouthOpen(cx, cy);
  else      drawMouthClosed(cx, cy);

  // 顯示距離用的小區塊(可刪)
  tft.setTextSize(2);
  tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
  tft.setCursor(10, 10);
  tft.print(open ? "MOUTH: OPEN " : "MOUTH: CLOSE");
}

// ================== 超音波測距 ==================
long readDistanceCM() {
  // 觸發 10us 脈衝
  digitalWrite(US_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(US_TRIG, HIGH);
  delayMicroseconds(10);
  digitalWrite(US_TRIG, LOW);

  // 讀回波時間(避免卡住,加 timeout)
  // 30ms 約等於 5m(足夠)
  unsigned long duration = pulseIn(US_ECHO, HIGH, 30000UL);
  if (duration == 0) return -1; // 代表沒量到

  // 距離(cm) = 時間(us) / 58
  return (long)(duration / 58UL);
}

// ================== 主程式:距離控制開闔 ==================
// 遲滯門檻:靠近到 OPEN_CM 才張開;遠離到 CLOSE_CM 才閉合
const int OPEN_CM  = 15;
const int CLOSE_CM = 20;

// 更新間隔
const unsigned long UPDATE_MS = 80;
unsigned long lastUpdateMs = 0;

// 讓距離比較穩:簡單做 3 次取中位數
long median3(long a, long b, long c) {
  if (a > b) { long t=a; a=b; b=t; }
  if (b > c) { long t=b; b=c; c=t; }
  if (a > b) { long t=a; a=b; b=t; }
  return b;
}

void setup() {
  // TFT
  SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS);
  tft.init(240, 320);      // 若你確定是 170x320 才改
  tft.setRotation(1);

  // Ultrasonic
  pinMode(US_TRIG, OUTPUT);
  pinMode(US_ECHO, INPUT);

  // 初始狀態
  mouthOpen = false;
  drawTigerMouth(mouthOpen);
}

void loop() {
  unsigned long now = millis();
  if (now - lastUpdateMs < UPDATE_MS) return;
  lastUpdateMs = now;

  long d1 = readDistanceCM();
  long d2 = readDistanceCM();
  long d3 = readDistanceCM();
  long d  = median3(d1, d2, d3);

  // 沒量到就不改狀態
  if (d < 0) return;

  // 在畫面顯示距離(可刪)
  tft.fillRect(10, 30, 140, 24, ST77XX_BLACK);
  tft.setTextSize(2);
  tft.setTextColor(ST77XX_CYAN, ST77XX_BLACK);
  tft.setCursor(10, 30);
  tft.print("D=");
  tft.print(d);
  tft.print("cm");

  bool newState = mouthOpen;
  if (!mouthOpen && d <= OPEN_CM)  newState = true;   // 靠近 -> 張開
  if ( mouthOpen && d >= CLOSE_CM) newState = false;  // 遠離 -> 閉合

  if (newState != mouthOpen) {
    mouthOpen = newState;
    drawTigerMouth(mouthOpen);
  }
}


沒有留言:

張貼留言