2026年8月15日 星期六

[水井村USR] ESP32 × JQ6500:打造會感應、會發光、會說故事的「水井三寶」智慧互動展覽系統

ESP32 × JQ6500:打造會感應、會發光、會說故事的「水井三寶」智慧互動展覽系統

從 UART 的 TX/RX 交叉接線,理解 ESP32 如何控制語音模組,並進一步整合 ToF、按鈕、WS2812 與狀態機
ESP32 JQ6500 UART VL53L0X WS2812B 智慧展覽 IoT實作
「水井三寶智慧互動展覽系統」第二層 Layer 2 的任務,是讓原本靜態的白馬、烏龜與姻緣花作品, 進一步具有感應、選擇、語音與燈光互動能力。 系統以 ESP32 NodeMCU-32S 為控制核心,利用 VL53L0X 偵測觀眾靠近, 再以三顆實體按鈕選擇故事,透過 UART 控制 JQ6500 播放語音, 並同步驅動 62 顆 WS2812B RGB LED 呈現不同故事燈效。

一、Layer 2:從靜態展品走向智慧互動

智慧展覽的核心並不只是「放一塊 ESP32」,而是要思考: 當觀眾走近作品之後,系統如何感知?如何判斷?如何回應?

觀眾靠近

VL53L0X ToF 距離感測

ESP32 判斷互動狀態

白馬/烏龜/姻緣花三按鈕選擇

ESP32 透過 UART 命令 JQ6500 播放故事

WS2812B 同步產生故事燈效

整套 Layer 2 因此包含四種重要能力: 感知、判斷、通訊與回饋。 其中 ESP32 與 JQ6500 之間的 UART 通訊,是整個「讓作品開口說故事」的關鍵。

二、水井三寶智慧互動層 V1.1 接線圖

圖1 水井三寶智慧互動展覽系統|第二層 智慧互動層 接線圖 V1.1(修正版)

V1.1 接線圖中特別重新整理了 JQ6500 UART 部分。 原因是 UART 最常見的錯誤,就是把 TX 接到 TX、RX 接到 RX。

三、ESP32 與 JQ6500 最重要的觀念:TX 要接 RX

正確 UART 接線:

ESP32 GPIO26(TX) → JQ6500 RX
ESP32 GPIO27(RX) ← JQ6500 TX
JQ6500 BUSY → ESP32 GPIO34
ESP32 GND ↔ JQ6500 GND
ESP32                         JQ6500
──────────────────────────────────────
GPIO26 / TX  ───────────────→ RX
GPIO27 / RX  ←─────────────── TX
GPIO34       ←─────────────── BUSY
GND          ──────────────── GND

UART 是 Universal Asynchronous Receiver/Transmitter, 中文通常稱為通用非同步收發傳輸

TX:Transmit

負責把資料「送出去」,就像裝置的嘴巴。

RX:Receive

負責把資料「收進來」,就像裝置的耳朵。

因此兩個設備互相溝通時,必須是:

TX → RX
RX ← TX
可以讓學生記住一句很簡單的 UART 口訣: 「TX 找 RX,RX 找 TX。」

如果接成:

TX → TX
RX → RX

就像兩個人的「嘴巴接嘴巴、耳朵接耳朵」, 當然無法形成正常的資料傳遞。

四、為什麼程式寫 JQ_TX_PIN = 26?

Layer 2 V1.0 程式一開始定義:

#define JQ_TX_PIN 26
#define JQ_RX_PIN 27
#define JQ_BUSY_PIN 34

這裡的名稱很容易讓初學者混淆。

JQ_TX_PIN 所代表的 GPIO26,是 ESP32 用來把指令送給 JQ6500 的 TX 腳

因此 GPIO26 必須接到:

ESP32 GPIO26 TX → JQ6500 RX

同樣地,GPIO27 是 ESP32 的接收腳:

ESP32 GPIO27 RX ← JQ6500 TX

五、ESP32 使用 UART2 與 JQ6500 通訊

程式建立一個硬體序列埠:

HardwareSerial JQ(2);

這代表我們使用 ESP32 的 Hardware UART2 專門負責和 JQ6500 溝通。

setup() 中:

JQ.begin(9600,SERIAL_8N1,JQ_RX_PIN,JQ_TX_PIN);

這一行的參數概念可以理解成:

JQ.begin(
    傳輸速率,
    資料格式,
    ESP32 RX腳,
    ESP32 TX腳
);

代入目前設定,就是:

JQ.begin(
    9600,
    SERIAL_8N1,
    27,
    26
);
ESP32 功能 JQ6500
GPIO26 ESP32 TX RX
GPIO27 ESP32 RX TX
GPIO34 讀取播放狀態 BUSY

六、ESP32 如何命令 JQ6500 播放聲音?

ESP32 不會直接把「播放白馬故事」這句文字傳給 JQ6500, 而是依照 JQ6500 的通訊格式,送出一串位元組命令。

Layer 2 程式把 UART 傳送功能集中在:

void sendJQ(const uint8_t *data,size_t length){
  JQ.write(data,length);
  JQ.flush();

  Serial.print("JQ TX: ");

  for(size_t i=0;i<length;i++){
    if(data[i]<0x10) Serial.print("0");
    Serial.print(data[i],HEX);
    Serial.print(" ");
  }

  Serial.println();
}

真正把資料傳到 JQ6500 的核心就是:

JQ.write(data,length);

這些資料會經由:

ESP32 UART2

GPIO26 TX

JQ6500 RX

七、播放第 1、2、3 首故事

播放指定音軌的函式為:

void jqPlayTrack(uint16_t track){
  uint8_t cmd[]={
    0x7E,
    0x04,
    0x03,
    (uint8_t)(track>>8),
    (uint8_t)(track&0xFF),
    0xEF
  };

  sendJQ(cmd,sizeof(cmd));
}

當白馬按鈕被按下:

jqPlayTrack(1);

烏龜則是:

jqPlayTrack(2);

姻緣花則是:

jqPlayTrack(3);
故事 按鈕 GPIO 播放音軌
白馬故事 GPIO32 Track 1
烏龜故事 GPIO33 Track 2
姻緣花故事 GPIO25 Track 3

因此形成了一條完整的事件鏈:

使用者按下實體按鈕

ESP32 GPIO 偵測 LOW

程式判斷故事類別

jqPlayTrack()

UART 傳送命令

JQ6500 播放 MP3

喇叭播放水井三寶故事

八、只有 TX/RX 還不夠:BUSY 是重要的回饋訊號

如果 ESP32 只是把播放命令送出去,它並不知道 JQ6500 到底何時播放完成。

因此 Layer 2 又加入一條非常重要的訊號:

JQ6500 BUSY → ESP32 GPIO34

程式設定:

#define JQ_BUSY_PIN 34

初始化:

pinMode(JQ_BUSY_PIN,INPUT);

接著利用:

bool jqIsBusy(){
  bool level=digitalRead(JQ_BUSY_PIN);
  return BUSY_ACTIVE_HIGH ? level==HIGH : level==LOW;
}

讓 ESP32 知道 JQ6500 是否正在播放。

這裡呈現一個非常重要的智慧系統觀念:
UART TX 是「ESP32 命令 JQ6500」,BUSY 則是「JQ6500 回報自己的工作狀態」。 因此系統不再只是單純的單向控制,而具有基本的回饋機制。

九、BUSY_ACTIVE_HIGH 為什麼要能調整?

程式中有:

// 若實測播放時 BUSY=LOW,改成 false
const bool BUSY_ACTIVE_HIGH=true;

不同版本的 JQ6500 模組或板子,其 BUSY 邏輯可能需要依實機確認。

如果實測發現「播放時 BUSY 是 LOW」,只要改成:

const bool BUSY_ACTIVE_HIGH=false;

這種設計比把 HIGH/LOW 直接寫死在各處更好, 因為只需要修改一個參數,就能適應不同硬體。

十、為什麼播放開始後要等待 300ms?

程式設定:

const unsigned long BUSY_GUARD_MS=300;

播放狀態中:

if(millis()-playStartTime<BUSY_GUARD_MS)break;

原因是 ESP32 執行速度非常快。 剛把 UART 播放命令送出去時,JQ6500 可能還需要一小段時間開啟音檔、開始解碼並改變 BUSY 狀態。

如果 ESP32 太早讀 BUSY,就可能把「尚未開始播放」誤認為「已經播放完畢」。

這也是實作 IoT 與嵌入式系統時很值得教學生的觀念: 程式執行完成,不等於外部硬體已經完成動作。

十一、播放結束後還有 2 秒 Cooldown

程式另外設定:

const unsigned long COOLDOWN_MS=2000;

播放完成後,不會立即重新接受下一次按鍵,而是進入 STATE_COOLDOWN

這可以避免使用者手指還沒有離開按鈕, 就再度觸發同一段語音。

十二、JQ6500 只是 Layer 2 的其中一部分

Layer 2 其實整合了多種不同的通訊與控制方式:

模組 ESP32 接腳 技術 用途
JQ6500 GPIO26、27 UART MP3 故事語音控制
JQ6500 BUSY GPIO34 Digital Input 確認語音播放狀態
VL53L0X GPIO21、22 I²C 觀眾距離感測
白馬按鈕 GPIO32 Digital Input 選擇白馬故事
烏龜按鈕 GPIO33 Digital Input 選擇烏龜故事
姻緣花按鈕 GPIO25 Digital Input 選擇姻緣花故事
WS2812B × 62 GPIO13 單線數位控制 故事燈光效果

十三、62 顆 WS2812B 如何分區?

程式把 62 顆 LED 分成四個區域:

#define BASE_START 0
#define BASE_COUNT 24

#define HORSE_START 24
#define HORSE_COUNT 10

#define TURTLE_START 34
#define TURTLE_COUNT 12

#define FLOWER_START 46
#define FLOWER_COUNT 16
燈區 LED 編號 數量
底座環境燈 0~23 24
白馬 24~33 10
烏龜 34~45 12
姻緣花 46~61 16

不同故事播放時,就可以只控制對應區域, 讓「聲音」和「燈光」建立關聯。

十四、真正讓智慧展品穩定運作的是「狀態機」

程式不是只寫:

if(按鈕按下){
  播放MP3;
}

而是定義:

enum SystemState{
  STATE_IDLE,
  STATE_ATTRACT,
  STATE_READY,
  STATE_PLAYING,
  STATE_COOLDOWN,
  STATE_ERROR
};
IDLE 等待

ATTRACT 吸引觀眾

READY 等待選擇

PLAYING 播放故事

COOLDOWN 防止連續誤觸

回到 READY 或 IDLE

這就是有限狀態機(Finite State Machine, FSM)的概念。

一旦系統從「單一 if 判斷」提升成狀態機, 程式就比較容易管理真正展場中可能出現的各種情況。

十五、Layer 2 V1.0 完整程式

以下為目前「水井三寶智慧互動展覽系統」第二層智慧互動層 Layer 2 V1.0 的完整 Arduino/ESP32 程式。

/*
 水井三寶智慧互動展覽系統
 第二層|智慧互動層 V1.0
 ESP32 NodeMCU-32S + VL53L0X + 3 Buttons + JQ6500 + WS2812
*/

#include <Arduino.h>
#include <Wire.h>
#include <HardwareSerial.h>
#include <Adafruit_VL53L0X.h>
#include <Adafruit_NeoPixel.h>

#define JQ_TX_PIN 26
#define JQ_RX_PIN 27
#define JQ_BUSY_PIN 34
#define BTN_HORSE 32
#define BTN_TURTLE 33
#define BTN_FLOWER 25
#define TOF_SDA 21
#define TOF_SCL 22
#define LED_PIN 13
#define LED_COUNT 62

#define BASE_START 0
#define BASE_COUNT 24
#define HORSE_START 24
#define HORSE_COUNT 10
#define TURTLE_START 34
#define TURTLE_COUNT 12
#define FLOWER_START 46
#define FLOWER_COUNT 16

const unsigned long DEBOUNCE_MS=50;
const unsigned long COOLDOWN_MS=2000;
const unsigned long BUSY_GUARD_MS=300;
const unsigned long MAX_PLAY_MS=120000;
const int PERSON_DISTANCE_MM=1200;
const unsigned long PERSON_DWELL_MS=1200;
const unsigned long TOF_INTERVAL_MS=100;
const unsigned long ATTRACT_MS=1500;

// 若實測播放時 BUSY=LOW,改成 false
const bool BUSY_ACTIVE_HIGH=true;

HardwareSerial JQ(2);
Adafruit_VL53L0X lox;
Adafruit_NeoPixel strip(LED_COUNT,LED_PIN,NEO_GRB+NEO_KHZ800);

enum SystemState{
  STATE_IDLE,
  STATE_ATTRACT,
  STATE_READY,
  STATE_PLAYING,
  STATE_COOLDOWN,
  STATE_ERROR
};

enum Story{
  STORY_NONE=0,
  STORY_HORSE=1,
  STORY_TURTLE=2,
  STORY_FLOWER=3
};

SystemState state=STATE_IDLE;
Story currentStory=STORY_NONE;

struct Button{
  uint8_t pin;
  bool stableState;
  bool lastReading;
  unsigned long lastChange;
  bool pressedEvent;
};

Button horseButton,turtleButton,flowerButton;

unsigned long stateStartTime=0;
unsigned long playStartTime=0;
unsigned long cooldownStartTime=0;
unsigned long lastToFTime=0;
unsigned long personStartTime=0;

bool personDetected=false;
bool personTiming=false;
bool tofOK=false;
bool busySeenPlaying=false;
int lastDistanceMM=-1;

const char* stateName(){
  switch(state){
    case STATE_IDLE:return "IDLE";
    case STATE_ATTRACT:return "ATTRACT";
    case STATE_READY:return "READY";
    case STATE_PLAYING:return "PLAYING";
    case STATE_COOLDOWN:return "COOLDOWN";
    case STATE_ERROR:return "ERROR";
  }
  return "UNKNOWN";
}

void initButton(Button &btn,uint8_t pin){
  btn.pin=pin;
  pinMode(pin,INPUT_PULLUP);
  btn.stableState=HIGH;
  btn.lastReading=HIGH;
  btn.lastChange=0;
  btn.pressedEvent=false;
}

void updateButton(Button &btn){
  bool reading=digitalRead(btn.pin);

  if(reading!=btn.lastReading){
    btn.lastReading=reading;
    btn.lastChange=millis();
  }

  if(millis()-btn.lastChange>=DEBOUNCE_MS){
    if(reading!=btn.stableState){
      btn.stableState=reading;

      if(btn.stableState==LOW)
        btn.pressedEvent=true;
    }
  }
}

void updateButtons(){
  updateButton(horseButton);
  updateButton(turtleButton);
  updateButton(flowerButton);
}

void clearButtonEvents(){
  horseButton.pressedEvent=false;
  turtleButton.pressedEvent=false;
  flowerButton.pressedEvent=false;
}

bool allButtonsReleased(){
  return horseButton.stableState==HIGH &&
         turtleButton.stableState==HIGH &&
         flowerButton.stableState==HIGH;
}

void sendJQ(const uint8_t *data,size_t length){
  JQ.write(data,length);
  JQ.flush();

  Serial.print("JQ TX: ");

  for(size_t i=0;i<length;i++){
    if(data[i]<0x10)
      Serial.print("0");

    Serial.print(data[i],HEX);
    Serial.print(" ");
  }

  Serial.println();
}

void jqSelectFlash(){
  uint8_t cmd[]={0x7E,0x03,0x09,0x04,0xEF};
  sendJQ(cmd,sizeof(cmd));
}

void jqSetVolume(uint8_t volume){
  if(volume>30)
    volume=30;

  uint8_t cmd[]={0x7E,0x03,0x06,volume,0xEF};
  sendJQ(cmd,sizeof(cmd));
}

void jqPlayTrack(uint16_t track){
  uint8_t cmd[]={
    0x7E,
    0x04,
    0x03,
    (uint8_t)(track>>8),
    (uint8_t)(track&0xFF),
    0xEF
  };

  sendJQ(cmd,sizeof(cmd));
}

void jqStop(){
  uint8_t cmd[]={0x7E,0x02,0x0E,0xEF};
  sendJQ(cmd,sizeof(cmd));
}

bool jqIsBusy(){
  bool level=digitalRead(JQ_BUSY_PIN);

  return BUSY_ACTIVE_HIGH
       ? level==HIGH
       : level==LOW;
}

void setZone(int start,int count,uint32_t color){
  for(int i=0;i<count;i++)
    strip.setPixelColor(start+i,color);
}

void ledIdle(){
  static unsigned long last=0;
  static int b=4,d=1;

  if(millis()-last<80)
    return;

  last=millis();
  b+=d;

  if(b>=14)d=-1;
  if(b<=4)d=1;

  strip.clear();
  setZone(BASE_START,BASE_COUNT,strip.Color(b,b/2,0));
  strip.show();
}

void ledAttract(){
  static unsigned long last=0;
  static int b=8,d=1;

  if(millis()-last<60)
    return;

  last=millis();
  b+=d;

  if(b>=35)d=-1;
  if(b<=8)d=1;

  strip.clear();
  setZone(HORSE_START,HORSE_COUNT,strip.Color(b,b/2,0));
  setZone(TURTLE_START,TURTLE_COUNT,strip.Color(0,b/2,b));
  setZone(FLOWER_START,FLOWER_COUNT,strip.Color(b,0,b/3));
  strip.show();
}

void ledReady(){
  strip.clear();
  setZone(HORSE_START,HORSE_COUNT,strip.Color(20,8,0));
  setZone(TURTLE_START,TURTLE_COUNT,strip.Color(0,8,18));
  setZone(FLOWER_START,FLOWER_COUNT,strip.Color(18,0,7));
  strip.show();
}

void ledHorse(){
  static unsigned long last=0;
  static int pos=0;

  if(millis()-last<80)
    return;

  last=millis();
  strip.clear();

  for(int i=0;i<HORSE_COUNT;i++){
    int dist=(i-pos+HORSE_COUNT)%HORSE_COUNT;

    if(dist==0)
      strip.setPixelColor(
        HORSE_START+i,
        strip.Color(100,45,0)
      );
    else if(dist<=2)
      strip.setPixelColor(
        HORSE_START+i,
        strip.Color(30,10,0)
      );
  }

  pos=(pos+1)%HORSE_COUNT;
  strip.show();
}

void ledTurtle(){
  static unsigned long last=0;
  static int b=10,d=1;

  if(millis()-last<70)
    return;

  last=millis();
  b+=d;

  if(b>=80)d=-1;
  if(b<=10)d=1;

  strip.clear();
  setZone(
    TURTLE_START,
    TURTLE_COUNT,
    strip.Color(0,b/2,b)
  );
  strip.show();
}

void ledFlower(){
  static unsigned long last=0;
  static int count=1;
  static bool growing=true;

  if(millis()-last<120)
    return;

  last=millis();
  strip.clear();

  for(int i=0;i<count;i++)
    strip.setPixelColor(
      FLOWER_START+i,
      strip.Color(80,5,25)
    );

  strip.show();

  if(growing){
    count++;

    if(count>=FLOWER_COUNT){
      count=FLOWER_COUNT;
      growing=false;
    }

  }else{
    count--;

    if(count<=2){
      count=2;
      growing=true;
    }
  }
}

void updateStoryLED(){
  switch(currentStory){
    case STORY_HORSE:
      ledHorse();
      break;

    case STORY_TURTLE:
      ledTurtle();
      break;

    case STORY_FLOWER:
      ledFlower();
      break;

    default:
      ledReady();
      break;
  }
}

void updateToF(){
  if(!tofOK)
    return;

  if(millis()-lastToFTime<TOF_INTERVAL_MS)
    return;

  lastToFTime=millis();

  VL53L0X_RangingMeasurementData_t measure;
  lox.rangingTest(&measure,false);

  bool nowDetected=false;

  if(measure.RangeStatus!=4){
    lastDistanceMM=measure.RangeMilliMeter;

    if(lastDistanceMM>50 &&
       lastDistanceMM<=PERSON_DISTANCE_MM)
      nowDetected=true;

  }else{
    lastDistanceMM=-1;
  }

  if(nowDetected){

    if(!personTiming){
      personTiming=true;
      personStartTime=millis();
    }

    if(millis()-personStartTime>=PERSON_DWELL_MS)
      personDetected=true;

  }else{
    personTiming=false;
    personDetected=false;
  }
}

void startStory(Story story){

  if(state==STATE_PLAYING ||
     state==STATE_COOLDOWN)
    return;

  currentStory=story;
  busySeenPlaying=false;
  clearButtonEvents();

  if(story==STORY_HORSE)
    jqPlayTrack(1);

  else if(story==STORY_TURTLE)
    jqPlayTrack(2);

  else if(story==STORY_FLOWER)
    jqPlayTrack(3);

  playStartTime=millis();
  state=STATE_PLAYING;
}

void handleButtons(){

  if(horseButton.pressedEvent){
    startStory(STORY_HORSE);
    return;
  }

  if(turtleButton.pressedEvent){
    startStory(STORY_TURTLE);
    return;
  }

  if(flowerButton.pressedEvent){
    startStory(STORY_FLOWER);
    return;
  }
}

void runStateMachine(){

  switch(state){

    case STATE_IDLE:

      ledIdle();
      handleButtons();

      if(state!=STATE_PLAYING &&
         personDetected){

        state=STATE_ATTRACT;
        stateStartTime=millis();
      }

      break;

    case STATE_ATTRACT:

      ledAttract();
      handleButtons();

      if(state!=STATE_PLAYING &&
         millis()-stateStartTime>=ATTRACT_MS)
        state=STATE_READY;

      break;

    case STATE_READY:

      ledReady();
      handleButtons();

      if(state!=STATE_PLAYING &&
         !personDetected)
        state=STATE_IDLE;

      break;

    case STATE_PLAYING:

      clearButtonEvents();
      updateStoryLED();

      if(millis()-playStartTime<BUSY_GUARD_MS)
        break;

      if(jqIsBusy())
        busySeenPlaying=true;

      if(busySeenPlaying &&
         !jqIsBusy()){

        cooldownStartTime=millis();
        state=STATE_COOLDOWN;
      }

      if(millis()-playStartTime>MAX_PLAY_MS){

        jqStop();

        cooldownStartTime=millis();
        state=STATE_COOLDOWN;
      }

      break;

    case STATE_COOLDOWN:

      clearButtonEvents();
      ledReady();

      if(millis()-cooldownStartTime>=COOLDOWN_MS &&
         allButtonsReleased()){

        currentStory=STORY_NONE;

        state=personDetected
             ? STATE_READY
             : STATE_IDLE;
      }

      break;

    case STATE_ERROR:

      strip.clear();
      strip.show();

      break;
  }
}

void setup(){

  Serial.begin(115200);
  delay(1000);

  Serial.println(
    "\nShuijing Treasures Layer 2 V1.0"
  );

  initButton(horseButton,BTN_HORSE);
  initButton(turtleButton,BTN_TURTLE);
  initButton(flowerButton,BTN_FLOWER);

  pinMode(JQ_BUSY_PIN,INPUT);

  strip.begin();
  strip.setBrightness(100);
  strip.clear();
  strip.show();

  JQ.begin(
    9600,
    SERIAL_8N1,
    JQ_RX_PIN,
    JQ_TX_PIN
  );

  delay(3000);

  jqSelectFlash();

  delay(2500);

  jqSetVolume(20);

  Wire.begin(TOF_SDA,TOF_SCL);

  tofOK=lox.begin();

  Serial.println(
    tofOK
    ? "VL53L0X OK"
    : "VL53L0X ERROR -> button-only mode"
  );

  Serial.println("SYSTEM READY");
}

void loop(){

  updateButtons();
  updateToF();
  runStateMachine();

  if(JQ.available()){

    Serial.print("JQ RX: ");

    while(JQ.available()){

      uint8_t b=JQ.read();

      if(b<0x10)
        Serial.print("0");

      Serial.print(b,HEX);
      Serial.print(" ");
    }

    Serial.println();
  }
}

十六、從 Arduino 範例走向真正的智慧互動系統

如果只看單一元件,JQ6500 是 MP3 模組、VL53L0X 是距離感測器、 WS2812B 是 RGB LED,而 ESP32 只是一塊微控制器。

但當這些元件透過程式整合之後,就形成了一個完整的智慧互動系統:

感知 Perception

判斷 Decision

通訊 Communication

行動 Action

回饋 Feedback

ToF 負責「感知」有人靠近; ESP32 負責「判斷」目前應該進入什麼狀態; UART 負責與 JQ6500「通訊」; JQ6500 與 WS2812B 負責產生聲音與燈光「行動」; BUSY 訊號再把 JQ6500 的狀態「回饋」給 ESP32。

十七、最值得拿來教學生的,不只是 GPIO 腳位

這個作品若只當成「接線實習」,學生可能只記住:

GPIO26
GPIO27
GPIO34

但真正應該學會的是:

1. UART:為什麼 TX 要接 RX?

2. 通訊協定:ESP32 如何把命令變成位元組送給 JQ6500?

3. Feedback:為什麼還需要 BUSY?

4. 非同步思維:為什麼送出命令後不能立刻假設硬體完成?

5. 狀態機:如何避免互動流程互相干擾?

6. 系統整合:如何讓感測器、聲音、燈光與地方故事形成完整體驗?

十八、結語:從兩條 UART 線,讓地方故事真正「活起來」

在整套水井三寶智慧互動展覽系統裡, ESP32 與 JQ6500 之間只有幾條線, 其中最不起眼的可能就是 TX 與 RX。

但正是:

ESP32 TX → JQ6500 RX
ESP32 RX ← JQ6500 TX

讓一塊微控制器可以命令另一個語音裝置工作。

再加入 BUSY、ToF、按鈕、RGB LED 與狀態機後, 原本只被觀看的地方工藝作品, 就開始具有感知環境、理解操作、播放故事與回應觀眾的能力。

這也是智慧生活科技很重要的核心精神:

科技不是目的,而是讓人、地方、文化與故事產生新的互動方式。

從 ESP32 與 JQ6500 的 UART 通訊開始, 學生學到的不只是「如何播放 MP3」, 而是如何讓不同裝置彼此溝通, 最後形成一套真正可以放進場域中持續運作的智慧系統。

案例:水井三寶智慧互動展覽系統
系統層級:Layer 2 智慧互動層 V1.0
接線版本:Wiring Diagram V1.1
控制核心:ESP32 NodeMCU-32S
語音模組:JQ6500
距離感測:VL53L0X ToF
視覺輸出:62 顆 WS2812B RGB LED
核心技術:UART、I²C、GPIO、WS2812B、Finite State Machine

沒有留言:

張貼留言