2026年8月15日 星期六

[水井村USR] ESP32 Edge Web × 手機:讓水井三寶智慧互動展覽進入第三層

ESP32 Edge Web × 手機:讓水井三寶智慧互動展覽進入第三層

Layer 3 V1.0:不需要外部網路,手機連上 ESP32 就能播放白馬、烏龜、姻緣花與水井三寶故事
ESP32 Edge Web SoftAP HTTP API JQ6500 手機互動 智慧展覽
「水井三寶智慧互動展覽系統」在 Layer 2 已經完成 ESP32、JQ6500、 VL53L0X、實體按鈕與 WS2812B 的軟硬體整合;到了 Layer 3, 則進一步加入 ESP32 SoftAP 與 Web Server, 讓手機不需要下載 App,也不需要連上 Internet, 只要加入 ESP32 建立的 Wi-Fi,就能透過瀏覽器直接控制故事播放。

一、手機就是水井三寶的新互動入口


圖1 水井三寶智慧互動展覽系統 Layer 3 Edge Web Control V1.0 手機操作畫面

從手機畫面可以看到,ESP32 已經不只是控制器, 還同時扮演一台小型 Web Server。

使用者可以直接看到目前狀態、正在播放的故事、ToF 距離、 JQ6500 BUSY、AP IP、目前連線裝置數, 並透過按鈕播放:

編號 內容
001 白馬故事
002 烏龜故事
003 姻緣花故事
004 水井三寶總故事
005 創作者資訊

二、使用者如何用手機播放故事?

Step 1|連接 ESP32 Wi-Fi 手機進入 Wi-Fi 設定,選擇 Shuijing-Treasures
Step 2|打開瀏覽器 使用 Safari、Chrome 或其他瀏覽器, 開啟 192.168.4.1
Step 3|選擇故事 點選白馬、烏龜、姻緣花、總故事或創作者資訊, ESP32 就會控制 JQ6500 播放。
使用者操作資訊:

Wi-Fi SSID:Shuijing-Treasures
Password:12345678
網頁網址:http://192.168.4.1

三、為什麼不需要 Internet?

關鍵在於 ESP32 使用:

WiFi.softAP(AP_SSID,AP_PASSWORD);

也就是由 ESP32 自己建立一個 Wi-Fi Access Point。

手機

Shuijing-Treasures Wi-Fi

ESP32 SoftAP

192.168.4.1

ESP32 Web Server

所以整個互動不需要經過外部路由器,也不需要 Internet。

這種架構特別適合展覽、社區場域、戶外活動或網路品質不穩定的地方, 因為即使現場沒有 Internet,手機仍然可以直接與 ESP32 溝通。

四、ESP32 如何建立 Edge Web Server?

Layer 3 程式首先設定:

const char *AP_SSID="Shuijing-Treasures";
const char *AP_PASSWORD="12345678";

NetworkServer server(80);

Port 80 就是一般 HTTP Web Server 常用的連接埠。

接著:

void setupAP(){

  Serial.println("\nStarting Shuijing SoftAP");

  if(!WiFi.softAP(AP_SSID,AP_PASSWORD)){
    Serial.println("Soft AP creation failed.");

    while(true)
      delay(1000);
  }

  IPAddress ip=WiFi.softAPIP();

  Serial.print("AP SSID : ");
  Serial.println(AP_SSID);

  Serial.print("AP IP   : ");
  Serial.println(ip);

  Serial.println("URL     : http://192.168.4.1");

  server.begin();

  Serial.println("Web Server started");
}

ESP32 開機後就會同時具備:

Wi-Fi Access Point

HTTP Web Server

實體裝置控制器

五、這就是 Edge Web

傳統雲端 Web 控制可能是:

手機
 ↓
Internet
 ↓
Cloud Server
 ↓
ESP32

Layer 3 則是:

手機
 ↓
Wi-Fi
 ↓
ESP32

Web Server 就直接執行在展品旁邊的 ESP32 上, 所以這裡可以稱為:

Edge Web Control:把網頁控制服務直接放在邊緣裝置上。

六、手機按下「001 白馬故事」時發生什麼?

手機頁面中的按鈕:

<button
 class="horse"
 onclick="playTrack(1)"
>
🐴 001 白馬故事
</button>

點下後 JavaScript 執行:

async function playTrack(n){

  try{

    await api(
      "/api/play?track="+n
    );

    refreshAll();

  }catch(e){

    alert(e.message);

  }
}

如果使用者按白馬,手機就會送出:

GET /api/play?track=1

整個控制流程為:

手機點「001 白馬故事」

JavaScript playTrack(1)

HTTP /api/play?track=1

ESP32 Web Server

startStory(STORY_HORSE,true)

jqPlayTrack(1)

UART

JQ6500

喇叭播放白馬故事

七、Layer 3 已經具有 API 架構

API 功能
/api/play?track=1 播放指定故事
/api/stop 停止播放
/api/volume?value=20 設定 JQ6500 音量
/api/status 取得目前系統狀態
/api/stats 取得匿名互動統計
/api/diag 取得設備診斷資料

這代表 Layer 3 已經不只是「手機網頁」, 而是具備一個簡單的 IoT Device API。

八、五個故事如何在程式中定義?

enum Story{
  STORY_NONE=0,
  STORY_HORSE=1,
  STORY_TURTLE=2,
  STORY_FLOWER=3,
  STORY_ALL=4,
  STORY_CREATOR=5
};

Layer 2 原本只有三顆實體按鈕,因此主要對應白馬、烏龜與姻緣花。 Layer 3 加入手機 Web 後,就可以突破實體按鈕數量限制, 再加入:

004 水井三寶總故事
005 創作者資訊

九、Web 最後仍然透過 UART 控制 JQ6500

手機只是新增一種輸入來源,真正播放 MP3 的核心仍然是 JQ6500。

void jqPlayTrack(uint16_t track){

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

  sendJQ(
    cmd,
    sizeof(cmd)
  );
}

硬體通訊仍然是:

ESP32 GPIO26 TX → JQ6500 RX
ESP32 GPIO27 RX ← JQ6500 TX
JQ6500 BUSY     → ESP32 GPIO34

所以 Layer 3 並不是取代 Layer 2, 而是在 Layer 2 上方再加入 Web 與手機互動。

Layer 3:手機 + Web + HTTP API

Layer 2:ESP32 + UART + JQ6500 + ToF + LED + Button

十、實體按鈕與手機 Web 可以共存

原本的三顆按鈕仍然保留:

void handlePhysicalButtons(){

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

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

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

手機則會使用:

startStory((Story)track,true);

其中:

false = 實體按鈕
true  = Web

因此還可以統計不同互動來源:

if(fromWeb)
  stats.webCount++;
else
  stats.buttonCount++;

十一、手機可以即時看到系統狀態

手機畫面上的:

PLAYING
目前故事:白馬故事
ToF距離:-1 mm
有人停留:false
JQ6500 BUSY:false
AP IP:192.168.4.1
已連線手機/平板:1

主要來自:

/api/status

ESP32 會產生 JSON:

String jsonStatus(){

  String j="{";

  j+="\"state\":\""+
     String(stateName())+"\",";

  j+="\"story\":\""+
     String(storyName(currentStory))+"\",";

  j+="\"distance_mm\":"+
     String(lastDistanceMM)+",";

  j+="\"person\":"+
     String(personDetected ? "true":"false")+",";

  j+="\"busy\":"+
     String(jqIsBusy() ? "true":"false")+",";

  j+="\"ap_ip\":\""+
     WiFi.softAPIP().toString()+"\",";

  j+="\"clients\":"+
     String(WiFi.softAPgetStationNum());

  j+="}";

  return j;
}

十二、手機網頁會定時更新,不需要整頁重新載入

setInterval(
  loadStatus,
  1000
);

setInterval(
  loadStats,
  2000
);

setInterval(
  loadDiag,
  5000
);

也就是:

週期 更新內容
每 1 秒 系統狀態
每 2 秒 匿名互動統計
每 5 秒 設備診斷資訊

十三、Layer 3 還加入匿名互動統計

struct Statistics{
  unsigned long wakeCount=0;
  unsigned long horseCount=0;
  unsigned long turtleCount=0;
  unsigned long flowerCount=0;
  unsigned long allStoryCount=0;
  unsigned long creatorCount=0;
  unsigned long buttonCount=0;
  unsigned long webCount=0;
  unsigned long completedCount=0;
  unsigned long stopCount=0;
};

可以統計:

統計項目 用途
白馬/烏龜/姻緣花 了解故事熱門程度
總故事/創作者資訊 了解延伸內容使用情況
實體按鈕播放 了解現場面板互動情形
Web 播放 了解手機互動情形
展區喚醒 觀察 ToF 偵測到的互動次數
完整播放 觀察故事是否聽完
STOP 了解中途停止情形
本版只記錄互動事件,不記錄姓名、人臉、電話或其他個人識別資料; 統計資料存在 ESP32 RAM 中,重新開機後歸零。

十四、手機也能直接調整 JQ6500 音量

Web 頁面中的 Slider:

<input
 id="vol"
 type="range"
 min="0"
 max="30"
 value="20"
 onchange="setVolume(this.value)"
>

手機會送出:

/api/volume?value=20

ESP32 再執行:

jqSetVolume((uint8_t)vol);
手機音量 Slider

HTTP API

ESP32

UART

JQ6500

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

/*
 水井三寶智慧互動展覽系統
 第三層|Edge Web 層 V1.0

 ESP32 NodeMCU-32S + VL53L0X + 3 Buttons
 + JQ6500 + WS2812 + SoftAP + Web

 AP:
   SSID     = Shuijing-Treasures
   Password = 12345678
   URL      = http://192.168.4.1

 Track:
   001 白馬
   002 烏龜
   003 姻緣花
   004 水井三寶總故事
   005 創作者資訊
*/

#include <Arduino.h>
#include <Wire.h>
#include <HardwareSerial.h>
#include <WiFi.h>
#include <NetworkClient.h>
#include <WiFiAP.h>
#include <Adafruit_VL53L0X.h>
#include <Adafruit_NeoPixel.h>

const char *AP_SSID="Shuijing-Treasures";
const char *AP_PASSWORD="12345678";

NetworkServer server(80);

#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=180000;

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;

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,
  STORY_ALL=4,
  STORY_CREATOR=5
};

SystemState state=STATE_IDLE;
Story currentStory=STORY_NONE;

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

Button horseButton;
Button turtleButton;
Button flowerButton;

struct Statistics{
  unsigned long wakeCount=0;
  unsigned long horseCount=0;
  unsigned long turtleCount=0;
  unsigned long flowerCount=0;
  unsigned long allStoryCount=0;
  unsigned long creatorCount=0;
  unsigned long buttonCount=0;
  unsigned long webCount=0;
  unsigned long completedCount=0;
  unsigned long stopCount=0;
};

Statistics stats;

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;
uint8_t currentVolume=20;


/* ================================
   手機 Web 頁面
   ================================ */

const char WEB_PAGE[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">

<head>

<meta charset="UTF-8">

<meta
 name="viewport"
 content="width=device-width,initial-scale=1"
>

<title>
水井三寶 Edge Control
</title>

<style>

body{
 margin:0;
 background:#f4f0e6;
 color:#333;
 font-family:
 Arial,
 "Microsoft JhengHei",
 sans-serif;
}

.wrap{
 max-width:760px;
 margin:auto;
 padding:16px;
}

.hero{
 background:#6b4d2e;
 color:white;
 padding:22px;
 border-radius:18px;
 text-align:center;
}

.hero h1{
 font-size:25px;
 margin:0;
}

.hero p{
 margin:6px 0 0;
 opacity:.85;
}

.card{
 background:white;
 margin-top:14px;
 padding:17px;
 border-radius:16px;
 box-shadow:
 0 3px 12px rgba(0,0,0,.08);
}

.status{
 font-size:28px;
 font-weight:bold;
 color:#2e7d32;
}

.grid{
 display:grid;
 grid-template-columns:1fr 1fr;
 gap:10px;
}

button{
 border:0;
 border-radius:12px;
 padding:15px 8px;
 font-size:16px;
 font-weight:bold;
 cursor:pointer;
}

.horse{background:#f6c96e;}
.turtle{background:#81d4fa;}
.flower{background:#f5a0bd;}
.total{background:#a5d6a7;}
.creator{background:#d7ccc8;}

.stop{
 background:#c62828;
 color:white;
}

.row{
 display:flex;
 justify-content:space-between;
 border-bottom:1px solid #eee;
 padding:7px 0;
}

.note{
 font-size:13px;
 color:#666;
}

</style>

</head>

<body>

<div class="wrap">

<div class="hero">

<h1>
🌳 水井三寶智慧互動展覽系統
</h1>

<p>
Edge Web Control V1.0
</p>

</div>


<div class="card">

<h2>
系統狀態
</h2>

<div
 id="state"
 class="status"
>
Loading...
</div>

<div id="statusDetail">
</div>

</div>


<div class="card">

<h2>
🎵 故事控制
</h2>

<div class="grid">

<button
 class="horse"
 onclick="playTrack(1)"
>
🐴 001 白馬故事
</button>

<button
 class="turtle"
 onclick="playTrack(2)"
>
🐢 002 烏龜故事
</button>

<button
 class="flower"
 onclick="playTrack(3)"
>
🌸 003 姻緣花故事
</button>

<button
 class="total"
 onclick="playTrack(4)"
>
🌳 004 水井三寶總故事
</button>

<button
 class="creator"
 onclick="playTrack(5)"
>
👨‍🎨 005 創作者資訊
</button>

<button
 class="stop"
 onclick="stopTrack()"
>
■ STOP
</button>

</div>

</div>


<div class="card">

<h2>
🔊 音量
</h2>

<input
 id="vol"
 type="range"
 min="0"
 max="30"
 value="20"
 style="width:100%"
 onchange="setVolume(this.value)"
>

<div>
目前:
<b id="volText">
20
</b>
</div>

</div>


<div class="card">

<h2>
📊 即時匿名統計
</h2>

<div id="stats">
Loading...
</div>

<p class="note">
只記錄互動事件,
不記錄姓名、人臉、電話
或其他個人識別資料。
本版統計存在 RAM,
ESP32 重啟後歸零。
</p>

</div>


<div class="card">

<h2>
🔧 設備診斷
</h2>

<div id="diag">
Loading...
</div>

</div>

</div>


<script>

async function api(url){

  const r=
    await fetch(url);

  const d=
    await r.json();

  if(!r.ok)
    throw new Error(
      d.reason ||
      "Request failed"
    );

  return d;
}


async function playTrack(n){

  try{

    await api(
      "/api/play?track="+n
    );

    refreshAll();

  }catch(e){

    alert(e.message);

  }
}


async function stopTrack(){

  try{

    await api(
      "/api/stop"
    );

    refreshAll();

  }catch(e){

    alert(e.message);

  }
}


async function setVolume(v){

  document
    .getElementById(
      "volText"
    )
    .textContent=v;

  try{

    await api(
      "/api/volume?value="+v
    );

  }catch(e){}
}


async function loadStatus(){

  try{

    const d=
      await api(
        "/api/status"
      );

    document
      .getElementById("state")
      .textContent=d.state;

  }catch(e){}
}


function refreshAll(){

  loadStatus();

}


setInterval(
  loadStatus,
  1000
);

refreshAll();

</script>

</body>

</html>
)rawliteral";


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";
}


const char* storyName(Story s){

  switch(s){

    case STORY_HORSE:
      return "白馬故事";

    case STORY_TURTLE:
      return "烏龜故事";

    case STORY_FLOWER:
      return "姻緣花故事";

    case STORY_ALL:
      return "水井三寶總故事";

    case STORY_CREATOR:
      return "創作者資訊";

    default:
      return "待機";
  }
}


void sendJQ(
  const uint8_t *data,
  size_t length
){

  JQ.write(
    data,
    length
  );

  JQ.flush();
}


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 startStory(
  Story story,
  bool fromWeb
){

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

  currentStory=story;
  busySeenPlaying=false;

  switch(story){

    case STORY_HORSE:

      jqPlayTrack(1);
      stats.horseCount++;

      break;

    case STORY_TURTLE:

      jqPlayTrack(2);
      stats.turtleCount++;

      break;

    case STORY_FLOWER:

      jqPlayTrack(3);
      stats.flowerCount++;

      break;

    case STORY_ALL:

      jqPlayTrack(4);
      stats.allStoryCount++;

      break;

    case STORY_CREATOR:

      jqPlayTrack(5);
      stats.creatorCount++;

      break;

    default:
      return;
  }

  if(fromWeb)
    stats.webCount++;
  else
    stats.buttonCount++;

  playStartTime=
    millis();

  state=
    STATE_PLAYING;
}


/* ================================
   HTTP
   ================================ */

String getTarget(
  const String &requestLine
){

  int a=
    requestLine.indexOf(' ');

  int b=
    requestLine.indexOf(
      ' ',
      a+1
    );

  if(
    a<0 ||
    b<0
  )
    return "/";

  return requestLine.substring(
    a+1,
    b
  );
}


String getPathOnly(
  const String &target
){

  int q=
    target.indexOf('?');

  if(q<0)
    return target;

  return target.substring(
    0,
    q
  );
}


String getQueryValue(
  const String &target,
  const String &key
){

  int q=
    target.indexOf('?');

  if(q<0)
    return "";

  String query=
    target.substring(
      q+1
    );

  String token=
    key+"=";

  int s=
    query.indexOf(
      token
    );

  if(s<0)
    return "";

  s+=
    token.length();

  int e=
    query.indexOf(
      '&',
      s
    );

  if(e<0)
    e=query.length();

  return query.substring(
    s,
    e
  );
}


void routeRequest(
  NetworkClient &client,
  const String &requestLine
){

  String target=
    getTarget(
      requestLine
    );

  String path=
    getPathOnly(
      target
    );


  if(path=="/"){

    sendHeader(
      client,
      "text/html; charset=utf-8"
    );

    client.print(
      WEB_PAGE
    );

    return;
  }


  if(
    path==
    "/api/play"
  ){

    int track=
      getQueryValue(
        target,
        "track"
      ).toInt();

    if(
      track<1 ||
      track>5
    ){

      sendJson(
        client,
        "{\"ok\":false,\"reason\":\"INVALID_TRACK\"}",
        400,
        "Bad Request"
      );

      return;
    }

    if(
      state==STATE_PLAYING
      ||
      state==STATE_COOLDOWN
    ){

      sendJson(
        client,
        "{\"ok\":false,\"reason\":\"目前故事播放中\"}",
        409,
        "Conflict"
      );

      return;
    }

    startStory(
      (Story)track,
      true
    );

    sendJson(
      client,
      "{\"ok\":true}"
    );

    return;
  }
}


/* ================================
   ESP32 SoftAP
   ================================ */

void setupAP(){

  Serial.println(
    "\nStarting Shuijing SoftAP"
  );

  if(
    !WiFi.softAP(
      AP_SSID,
      AP_PASSWORD
    )
  ){

    Serial.println(
      "Soft AP creation failed."
    );

    while(true)
      delay(1000);
  }

  IPAddress ip=
    WiFi.softAPIP();

  Serial.print(
    "AP SSID : "
  );

  Serial.println(
    AP_SSID
  );

  Serial.print(
    "AP IP   : "
  );

  Serial.println(ip);

  Serial.println(
    "URL     : http://192.168.4.1"
  );

  server.begin();

  Serial.println(
    "Web Server started"
  );
}


void setup(){

  Serial.begin(
    115200
  );

  delay(1000);

  Serial.println(
    "\nShuijing Treasures Layer 3 Edge Web V1.0"
  );

  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
  );

  Wire.begin(
    TOF_SDA,
    TOF_SCL
  );

  tofOK=
    lox.begin();

  setupAP();

  state=
    STATE_IDLE;

  Serial.println(
    "\nSYSTEM READY"
  );

  Serial.println(
    "WiFi    : Shuijing-Treasures"
  );

  Serial.println(
    "Password: 12345678"
  );

  Serial.println(
    "Open    : http://192.168.4.1"
  );
}


void loop(){

  updateButtons();

  updateToF();

  runStateMachine();

  handleWebClient();

}

十六、這個案例最值得教學生的是什麼?

1. SoftAP: ESP32 如何自己成為 Wi-Fi 基地台?

2. Web Server: 一塊微控制器如何直接提供 HTML 網頁?

3. HTTP: 手機按一個按鈕,如何變成 GET /api/play?track=1

4. API: Web 前端如何透過 API 控制實體裝置?

5. UART: ESP32 收到 Web 命令後,如何再控制 JQ6500?

6. Edge Computing: 為什麼沒有 Internet 仍然能提供完整互動?

7. 多介面整合: 實體按鈕與手機 Web 如何共享同一套故事播放核心?

十七、從 Layer 2 到 Layer 3,是一次很重要的升級

Layer 2:

人
↓
實體按鈕
↓
ESP32
↓
UART
↓
JQ6500

Layer 3:

人
├── 實體按鈕
└── 手機瀏覽器
       ↓
     HTTP API
       ↓
     ESP32
       ↓
     UART
       ↓
     JQ6500

這表示同一套展品已經同時擁有「實體介面」與「數位介面」。

十八、下一步可以走向 QR Code × AI 導覽

當 Layer 3 已經具備手機 Web 後, 下一步可以進一步加入 QR Code。

QR Code

手機 Web

作品識別

Django / API

LLM + RAG

AI 地方故事導覽

到那時,手機就不只是播放固定 MP3, 而可以進一步詢問:

「白馬故事是怎麼來的?」

「為什麼水井村有烏龜的故事?」

「姻緣花代表什麼?」

「這件作品是誰創作的?」

結語:手機不是遙控器,而是智慧展品的新入口

Layer 3 V1.0 最重要的意義,不只是增加一個漂亮的手機畫面。

真正的改變是:

ESP32 開始提供 Web Service,而實體文化展品開始擁有自己的數位入口。

使用者不需要下載 App,也不需要依賴 Internet, 只要加入 Shuijing-Treasures, 再開啟 192.168.4.1, 就可以直接選擇故事。

從 GPIO、UART 到 HTTP API, 從實體按鈕到手機 Edge Web, 這正好形成一條完整的智慧生活科技學習路徑:

感測

控制

通訊

Web

Edge

AI
案例:水井三寶智慧互動展覽系統
系統層級:Layer 3 Edge Web V1.0
控制核心:ESP32 NodeMCU-32S
手機連線:ESP32 SoftAP
Web Server:HTTP Port 80
語音控制:JQ6500 UART
互動方式:實體按鈕+手機 Web
核心技術:Wi-Fi SoftAP、HTTP、API、UART、Edge Computing

沒有留言:

張貼留言