顯示具有 水井USR 標籤的文章。 顯示所有文章
顯示具有 水井USR 標籤的文章。 顯示所有文章

2026年8月23日 星期日

[水井村USR] HUB 8735 Ultra實作:用三個實體按鈕選擇三個水井三寶故事

水井三寶故事機|V1.0-04

HUB 8735 Ultra實作:用三個實體按鈕選擇三個水井三寶故事

V1.0-03已完成microSD卡與三個MP3檔案的依序播放測試;本次V1.0-04加入三個實體按鈕,讓使用者可直接選擇白馬、烏龜或姻緣花故事,故事機正式進入互動操作階段。

一、測試目標與結果

本次使用HUB 8735 Ultra讀取microSD卡中的三個MP3檔案,並以GPIO10、GPIO11、GPIO12分別連接三個按鈕。按下不同按鈕後,播放對應的故事。

測試結果:功能正常。三個按鈕都能正確播放各自指定的MP3故事。

二、按鈕與故事配置

GPIO10
白馬按鈕

001.mp3
GPIO11
烏龜按鈕

002.mp3
GPIO12
姻緣花按鈕

003.mp3
GPIO腳位實體按鈕MP3檔案故事內容
GPIO10白馬按鈕001.mp3白馬故事
GPIO11烏龜按鈕002.mp3烏龜故事
GPIO12姻緣花按鈕003.mp3姻緣花故事

三、準備材料

  • HUB 8735 Ultra開發板
  • microSD卡一張
  • 瞬時按鈕三個
  • XPT8871單聲道功率放大板
  • 喇叭一顆
  • 5V電源、麵包板與杜邦線

四、按鈕接線圖

程式使用INPUT_PULLUP,所以每個按鈕的一端接GPIO,另一端接GND;未按下時讀到HIGH,按下時讀到LOW,不需要另外加外部上拉電阻。

GPIO10
白馬按鈕
GND
GPIO11
烏龜按鈕
GND
GPIO12
姻緣花按鈕
GND
注意:四腳按鈕同一側的兩隻腳通常已互相導通,請使用按鈕兩側的腳位接線。若接到同一側,GPIO可能一直維持相同狀態。

五、音訊接線圖

HUB AOUT
XPT8871 IN+
 
音訊訊號
HUB GND
XPT8871 IN−
 
共地
5V
XPT8871 5V+
 
放大板電源
XPT8871 OUT+/OUT−
喇叭 +/−
 
橋接輸出
重要:XPT8871的OUT−是橋接功率輸出,不可以接GND。喇叭必須接在OUT+與OUT−之間。若有雜聲,應縮短AOUT音訊線、讓訊號線遠離電源線與喇叭輸出線,並確認HUB與放大板共地。

六、microSD卡檔案配置

將三個MP3檔直接放在microSD卡根目錄,檔名必須完全一致:

microSD 根目錄
├── 001.mp3  (白馬)
├── 002.mp3  (烏龜)
└── 003.mp3  (姻緣花)

七、完整Arduino程式|V1.0-04

/*
 * HUB 8735 Ultra 三按鈕故事播放器
 * Version: V1.0-04
 * GPIO10 → 白馬 → 001.mp3
 * GPIO11 → 烏龜 → 002.mp3
 * GPIO12 → 姻緣花 → 003.mp3
 */

#include "AmebaFatFS.h"
#include "AmebaFatFSFile.h"

#define MP3_VOLUME        0x90
#define STORY_COUNT       3
#define DEBOUNCE_MS       50

#define BUTTON_HORSE_PIN   10
#define BUTTON_TURTLE_PIN  11
#define BUTTON_FLOWER_PIN  12

AmebaFatFS fs;

const int buttonPins[STORY_COUNT] = {
    BUTTON_HORSE_PIN,
    BUTTON_TURTLE_PIN,
    BUTTON_FLOWER_PIN
};

const char *storyFiles[STORY_COUNT] = {
    "001.mp3",
    "002.mp3",
    "003.mp3"
};

const char *storyNames[STORY_COUNT] = {
    "White Horse",
    "Turtle",
    "Marriage Flower"
};

String getFullPath(const char *filename)
{
    return String(fs.getRootPath()) + String(filename);
}

bool checkMP3File(const char *filename)
{
    String fullPath = getFullPath(filename);
    File testFile = fs.open(fullPath);

    Serial.print("[CHECK] ");
    Serial.print(fullPath);
    Serial.print(" ... ");

    if (!testFile.isOpen()) {
        Serial.println("NOT FOUND");
        return false;
    }

    Serial.print("OK, ");
    Serial.print(testFile.size());
    Serial.println(" bytes");
    testFile.close();
    return true;
}

bool checkAllStoryFiles()
{
    bool allReady = true;

    for (int i = 0; i < STORY_COUNT; i++) {
        if (!checkMP3File(storyFiles[i])) {
            allReady = false;
        }
    }
    return allReady;
}

bool playStory(int storyIndex)
{
    if (storyIndex < 0 || storyIndex >= STORY_COUNT) {
        return false;
    }

    String fullPath = getFullPath(storyFiles[storyIndex]);
    File mp3File = fs.open(fullPath);

    Serial.println();
    Serial.println("========================================");
    Serial.print("[BUTTON] GPIO: ");
    Serial.println(buttonPins[storyIndex]);
    Serial.print("[PLAY] Story: ");
    Serial.println(storyNames[storyIndex]);
    Serial.print("[PLAY] File: ");
    Serial.println(fullPath);

    if (!mp3File.isOpen()) {
        Serial.println("[ERROR] Unable to open MP3 file.");
        return false;
    }

    mp3File.setMp3DigitalVol(MP3_VOLUME);
    Serial.println("[AUDIO] Playback started.");

    // playMp3()會阻塞,直到目前故事播放完成
    mp3File.playMp3();

    Serial.println("[AUDIO] Playback completed.");
    mp3File.close();
    Serial.println("========================================");
    return true;
}

void waitButtonRelease(int pin)
{
    while (digitalRead(pin) == LOW) {
        delay(10);
    }
    delay(DEBOUNCE_MS);
}

void setup()
{
    Serial.begin(115200);
    delay(2000);

    Serial.println();
    Serial.println("========================================");
    Serial.println("HUB 8735 Ultra Story Player V1.0-04");
    Serial.println("========================================");

    for (int i = 0; i < STORY_COUNT; i++) {
        pinMode(buttonPins[i], INPUT_PULLUP);
    }

    Serial.println("[SD] Initializing microSD card...");
    fs.begin();
    Serial.print("[SD] Root path: ");
    Serial.println(fs.getRootPath());

    if (!checkAllStoryFiles()) {
        Serial.println("[FAIL] Check microSD card and filenames.");
        while (true) {
            delay(1000);
        }
    }

    Serial.println("[READY] Press a story button.");
}

void loop()
{
    for (int i = 0; i < STORY_COUNT; i++) {
        if (digitalRead(buttonPins[i]) == LOW) {
            delay(DEBOUNCE_MS);

            if (digitalRead(buttonPins[i]) == LOW) {
                playStory(i);
                waitButtonRelease(buttonPins[i]);
                Serial.println("[READY] Press a story button.");
            }
        }
    }

    delay(10);
}

八、操作方式

  1. 將三個MP3檔放入microSD卡根目錄。
  2. 依接線圖接好三個按鈕、XPT8871與喇叭。
  3. 上傳程式後,開啟115200 baud序列監控視窗。
  4. 按白馬按鈕播放001.mp3;按烏龜按鈕播放002.mp3;按姻緣花按鈕播放003.mp3。

九、序列監控預期訊息

HUB 8735 Ultra Story Player V1.0-04
[SD] Initializing microSD card...
[CHECK] ...001.mp3 ... OK
[CHECK] ...002.mp3 ... OK
[CHECK] ...003.mp3 ... OK
[READY] Press a story button.

[BUTTON] GPIO: 10
[PLAY] Story: White Horse
[PLAY] File: ...001.mp3
[AUDIO] Playback started.
[AUDIO] Playback completed.

十、V1.0-03與V1.0-04比較

項目V1.0-03V1.0-04
播放方式開機後依序播放三個故事按下實體按鈕選擇故事
互動性三個獨立按鈕
microSD三個MP3檔沿用相同三個MP3檔
GPIO未使用按鈕GPIO10、11、12

十一、目前限制與後續方向

目前使用的playMp3()會等待故事播放完畢,因此播放期間不會偵測其他按鈕。對「一按一個完整故事」的操作方式而言很單純穩定;未來若需要中途停止、切換故事或調整音量,則要再研究非阻塞播放或額外的播放控制機制。

下一階段可以加入LED播放指示、音量按鈕、停止鍵、開機提示音,或進一步加入網路語音與AI對話功能。

部落格貼文注意事項:請在Blogger的「HTML檢視」中貼上本檔內容。本檔刻意不包含<html><head><body>標籤,並已限制程式碼區塊寬度,避免破壞Blogger版型或把右側資訊欄擠出畫面。

[水井村USR] HUB 8735 Ultra實作:microSD卡與三個MP3故事檔案測試

水井三寶故事機|V1.0-03

HUB 8735 Ultra實作:microSD卡與三個MP3故事檔案測試

本次測試讓HUB 8735 Ultra從microSD卡讀取三個MP3故事, 經由AOUT類比音訊輸出,再透過XPT8871功率擴大板推動喇叭, 逐步完成水井三寶樹藝故事機的聲音系統。

測試結果:功能通過,音訊品質仍需改善。

三個MP3故事均可正常讀取及播放,但目前仍有些許雜聲。 初步研判主要與音訊線過長、麵包板接觸、電源干擾及接地方式有關。

一、V1.0-03測試目標

microSD卡 確認HUB 8735 Ultra能正確掛載與讀取記憶卡。
三個故事檔案 檢查001.mp3、002.mp3及003.mp3是否存在。
MP3解碼 驗證三個故事能否依序完成解碼與播放。
音訊輸出 驗證AOUT、XPT8871擴大板與喇叭的完整聲音路徑。

二、使用材料

材料 數量 用途
HUB 8735 Ultra 1 讀取microSD卡並解碼MP3
microSD卡 1 儲存三個水井三寶故事
XPT8871音訊擴大板 1 放大AOUT類比音訊
4Ω 3W或8Ω喇叭 1 播放故事語音
5V穩壓電源 1 供應XPT8871擴大板
麵包板、杜邦線 若干 原型接線與功能測試
USB Type-C資料線 1 程式燒錄及序列監控

三、microSD卡準備

將microSD卡格式化為FAT32,再把三個MP3故事檔案直接放在根目錄:

microSD
├── 001.mp3 姻緣花故事
├── 002.mp3 烏龜故事
└── 003.mp3 白馬故事
檔名建議: 第一階段只使用英文或數字,不加入中文、空格及特殊符號; 副檔名統一使用小寫的.mp3

四、HUB 8735 Ultra與XPT8871接線圖

microSD MP3音訊播放接線圖
HUB 8735 Ultra microSD+MP3解碼
AOUT:類比音訊
GND:音訊參考地
AOUT → IN+
GND → IN−
XPT8871 單聲道功率擴大板
IN+:音訊輸入
IN−:音訊地
OUT+:喇叭輸出
OUT−:喇叭輸出
OUT+ → 喇叭+
OUT− → 喇叭−
喇叭
建議4Ω 3W或8Ω 2W~3W
獨立穩壓5V電源 → XPT8871
● +5V → +5V ● GND → 5V−
來源端 XPT8871端 功能
HUB 8735 Ultra AOUT IN+ 類比音訊訊號
HUB 8735 Ultra GND IN−/GND 音訊參考地
獨立5V正極 +5V 擴大板電源
獨立5V負極 5V− 擴大板電源地
喇叭正端 OUT+ BTL喇叭輸出
喇叭負端 OUT− BTL喇叭輸出
重要: XPT8871採BTL橋接輸出,OUT−不是GND。 喇叭必須接在OUT+OUT−之間, 不可將OUT−接到HUB 8735 Ultra的GND。

五、V1.0-03完整測試程式

/*
 * ============================================================
 * 專案:HUB 8735 Ultra水井三寶故事機
 * 版本:V1.0-03
 * 功能:microSD卡與三個MP3故事檔案測試
 * ============================================================
 *
 * microSD卡根目錄:
 *   001.mp3
 *   002.mp3
 *   003.mp3
 */

#include "AmebaFatFS.h"
#include "AmebaFatFSFile.h"

#define STORY_COUNT 3

// 0xAF為最大值0dB
// 初次測試降低音量,避免擴大器輸入過載
#define MP3_VOLUME 0x90

#define STORY_INTERVAL_MS 2000

AmebaFatFS fs;

const char *storyFiles[STORY_COUNT] = {
    "001.mp3",
    "002.mp3",
    "003.mp3"
};

const char *storyNames[STORY_COUNT] = {
    "姻緣花故事",
    "烏龜故事",
    "白馬故事"
};

String getFullPath(const char *filename)
{
    return String(fs.getRootPath()) + String(filename);
}

bool checkMP3File(const char *filename)
{
    String fullPath = getFullPath(filename);
    File testFile;

    Serial.print("[CHECK] ");
    Serial.print(fullPath);
    Serial.print(" ... ");

    testFile = fs.open(fullPath);

    if (!testFile.isOpen()) {
        Serial.println("NOT FOUND");
        return false;
    }

    Serial.print("OK, ");
    Serial.print(testFile.size());
    Serial.println(" bytes");

    testFile.close();
    return true;
}

bool checkAllStoryFiles()
{
    bool allFilesReady = true;

    Serial.println();
    Serial.println("Checking MP3 story files...");
    Serial.println("--------------------------------");

    for (int i = 0; i < STORY_COUNT; i++) {
        if (!checkMP3File(storyFiles[i])) {
            allFilesReady = false;
        }
    }

    Serial.println("--------------------------------");

    if (allFilesReady) {
        Serial.println("[PASS] All three MP3 files are ready.");
    } else {
        Serial.println("[FAIL] One or more MP3 files are missing.");
    }

    return allFilesReady;
}

bool playStory(int storyIndex)
{
    if (storyIndex < 0 || storyIndex >= STORY_COUNT) {
        Serial.println("[ERROR] Invalid story index.");
        return false;
    }

    String fullPath = getFullPath(storyFiles[storyIndex]);
    File mp3File;

    Serial.println();
    Serial.println("================================");

    Serial.print("[PLAY] Story: ");
    Serial.println(storyNames[storyIndex]);

    Serial.print("[PLAY] File: ");
    Serial.println(fullPath);

    mp3File = fs.open(fullPath);

    if (!mp3File.isOpen()) {
        Serial.println("[ERROR] Unable to open MP3 file.");
        return false;
    }

    Serial.print("[INFO] File size: ");
    Serial.print(mp3File.size());
    Serial.println(" bytes");

    mp3File.setMp3DigitalVol(MP3_VOLUME);

    Serial.println("[AUDIO] Playback started.");

    mp3File.playMp3();

    Serial.println("[AUDIO] Playback completed.");

    mp3File.close();

    return true;
}

void playAllStories()
{
    Serial.println();
    Serial.println("Starting three-story playback test...");

    for (int i = 0; i < STORY_COUNT; i++) {
        if (!playStory(i)) {
            Serial.print("[FAIL] Story ");
            Serial.print(i + 1);
            Serial.println(" playback failed.");
        }

        if (i < STORY_COUNT - 1) {
            delay(STORY_INTERVAL_MS);
        }
    }

    Serial.println();
    Serial.println("[DONE] Three-story playback test completed.");
}

void setup()
{
    Serial.begin(115200);
    delay(2000);

    Serial.println();
    Serial.println("================================================");
    Serial.println("HUB 8735 Ultra Story Player");
    Serial.println("Version: V1.0-03");
    Serial.println("================================================");

    Serial.println("[SD] Initializing microSD card...");

    fs.begin();

    Serial.print("[SD] Root path: ");
    Serial.println(fs.getRootPath());

    if (!checkAllStoryFiles()) {
        Serial.println("[SYSTEM] Playback cancelled.");
        fs.end();
        return;
    }

    playAllStories();

    fs.end();

    Serial.println("[SYSTEM] V1.0-03 test finished.");
}

void loop()
{
    // 本版本只在開機後測試一次
    delay(1000);
}

六、預期序列監控結果

Arduino IDE的序列監控視窗設定為115200 baud。 正常執行時會看到:

HUB 8735 Ultra Story Player
Version: V1.0-03

[SD] Initializing microSD card...
[CHECK] 0:/001.mp3 ... OK
[CHECK] 0:/002.mp3 ... OK
[CHECK] 0:/003.mp3 ... OK
[PASS] All three MP3 files are ready.

[PLAY] Story: 姻緣花故事
[AUDIO] Playback started.
[AUDIO] Playback completed.

[PLAY] Story: 烏龜故事
[AUDIO] Playback started.
[AUDIO] Playback completed.

[PLAY] Story: 白馬故事
[AUDIO] Playback started.
[AUDIO] Playback completed.

[DONE] Three-story playback test completed.

七、實際測試結果

  • microSD卡可以正常掛載。
  • 三個MP3故事檔案均可找到。
  • 三個故事可以依序播放。
  • HUB 8735 Ultra的AOUT可以輸出MP3音訊。
  • XPT8871可以驅動外接喇叭。
  • 目前音訊仍有些許雜聲,需要改善接線及供電。
V1.0-03判定:功能測試成功。

下一步不是立即增加功能,而是先把類比音訊、接地與電源整理好。

八、為什麼目前會有雜聲?

1. AOUT音訊線過長

AOUT屬於小訊號類比音訊。使用過長且沒有屏蔽的杜邦線, 容易接收USB、Wi-Fi、開關電源及數位GPIO產生的干擾。

建議將AOUT與GND縮短至5~10公分,並把兩條線互相纏繞。 正式版本可以改用屏蔽音訊線:

中心導線 → HUB 8735 Ultra AOUT
屏蔽層   → HUB 8735 Ultra GND

2. 音訊線與電源線交錯

AOUT到XPT8871的輸入線應遠離:

  • 5V大電流電源線
  • USB傳輸線
  • 開關電源模組
  • WS2812燈條電源線
  • 伺服馬達與馬達線路
  • XPT8871的喇叭輸出線

3. 多點接地形成地迴路

HUB 8735 Ultra由電腦USB供電,而擴大板由另一組5V電源供電時, 應採用單點共地,避免GND經過多條不同路徑。

建議採用單點共地
單一共地點
↓ ↓ ↓
HUB 8735 Ultra
GND
XPT8871音訊輸入
IN−/GND
5V電源
負極

4. 5V電源紋波

XPT8871播放較大音量時,負載電流會快速變化。 如果5V電源含有開關紋波,雜訊可能直接進入擴大器。

建議在XPT8871電源端附近並聯一顆470µF電解電容, 再並聯一顆0.1µF陶瓷電容:

+5V ───┬──── XPT8871 +5V
       ├──── 470µF正極
       └──── 0.1µF

GND ───┬──── XPT8871 GND
       ├──── 470µF負極
       └──── 0.1µF

5. 輸入音量過高

若HUB 8735 Ultra輸出音量太大,可能讓XPT8871輸入過載, 產生破音或沙沙聲。目前程式使用:

#define MP3_VOLUME 0x90

如果仍有明顯失真,可以降低為:

#define MP3_VOLUME 0x80

如果降低音量後破音明顯改善,表示問題可能包含輸入過載, 不完全是外部電磁干擾。

九、建議的改善接線

HUB 8735 Ultra 5~10公分屏蔽音訊線 XPT8871 短喇叭線 喇叭
  1. 將AOUT與GND線縮短到5~10公分。
  2. 把AOUT與GND互相纏繞,或改用屏蔽音訊線。
  3. 將XPT8871移到靠近喇叭的位置。
  4. 讓音訊線遠離USB線、開關電源及5V大電流線。
  5. 在XPT8871旁增加470µF及0.1µF電容。
  6. 使用單點共地,避免形成地迴路。
  7. 將MP3數位音量由0x90降低到0x80進行比較。
  8. 正式版本改用焊接或洞洞板,減少麵包板接觸問題。
電源安全: 如果使用具有裸露端子的金屬外殼開關電源, AC市電輸入端必須加裝絕緣護蓋。 通電時不要在裸露的L、N端子附近調整杜邦線。 原型測試建議先使用有外殼、具安全認證的5V變壓器。

十、從聲音播放走向樹藝AI

microSD卡 三個MP3故事 HUB 8735解碼 AOUT XPT8871 喇叭

V1.0-03證明HUB 8735 Ultra除了可以進行影像辨識, 也能結合microSD卡、MP3解碼、類比音訊輸出與外接擴大器, 成為地方故事播放裝置。

對水井三寶而言,這不只是「播放三個MP3」, 而是讓姻緣花、烏龜與白馬開始擁有自己的聲音, 為後續樹藝作品導覽、影像辨識與AI對話建立基礎。

2026年8月18日 星期二

從接線施工到手機控制: 水井三寶 ESP32 擴充底板 × GPIO19 WS2812B × Web 音量控制實作

從接線施工到手機控制:
水井三寶 ESP32 擴充底板 × GPIO19 WS2812B × Web 音量控制實作

水井三寶智慧互動展覽系統|Layer 2~Layer 3 實作紀錄

ESP32 × WS2812B × JQ6500 × Edge Web × Realtime Status × Mobile Volume Control

「水井三寶智慧互動展覽系統」在實作過程中,除了要考慮程式能不能執行,更重要的是: 設備真正裝進展覽箱之後,好不好接線?好不好維修?現場人員能不能不用重新燒錄程式,就完成基本設定?

因此,本次系統在原有功能穩定之後,又進行兩項很重要的實務調整: 第一項是為了配合 ESP32 擴充底板與 WS2812B 三線式插頭,重新調整 LED DATA GPIO; 第二項則是在 V1.8 Realtime Web Status 基礎上加入手機音量控制,形成 Layer 4 V1.8.1「Realtime Status+Mobile Volume Control」

一、為什麼已經可以動了,還要修改GPIO?

早期測試 WS2812B 時,DATA 使用 GPIO13。從程式的角度來看完全沒有問題, 但當系統開始由麵包板實驗走向正式展覽箱施工時,問題就不再只是「GPIO能不能輸出」。

真正的問題變成:哪一支GPIO最適合施工?

  • 能不能直接配合擴充底板的三針插座?
  • 能不能使用現有 WS2812B 三線插頭?
  • 是否會和三顆實體按鈕衝突?
  • 是否會影響 JQ6500 UART?
  • 日後拆裝與維修是否方便?

二、從 GPIO13 改成 GPIO19

經過重新檢查擴充底板可使用的腳位後, GPIO19 可以直接配合板上的插座,而且不需要搬動目前已經測試穩定的按鈕與 JQ6500 接腳, 因此最後決定:

WS2812B DATA:GPIO13 → GPIO19
功能 GPIO 說明
白馬按鈕 GPIO32 維持原接線
烏龜按鈕 GPIO33 維持原接線
姻緣花按鈕 GPIO25 維持原接線
JQ6500 UART GPIO26 / GPIO27 已完成穩定測試,不再更動
ToF VL53L0X GPIO21 / GPIO22 I2C SDA / SCL
WS2812B DATA GPIO19 配合擴充底板三針插座

三、程式其實只需要改一個地方

這正是軟硬體模組化的好處。 LED控制邏輯沒有改變,只是將資料輸出的GPIO重新指定。

原來版本

#define LED_PIN 13

新版

#define LED_PIN 19

Adafruit NeoPixel 的初始化方式仍然相同:

#define LED_PIN    19
#define LED_COUNT  61

Adafruit_NeoPixel strip(
  LED_COUNT,
  LED_PIN,
  NEO_GRB + NEO_KHZ800
);
這次修改的重要經驗:
GPIO選擇不能只從「程式能不能跑」來思考。 當作品進入正式施工階段,還要把接頭、底板、線材、維修方式與模組位置一起納入設計。

四、WS2812B 如何接到 ESP32 擴充底板?

WS2812B 基本上需要三條線:

ESP32 擴充底板 GPIO19 / Signal │ ├────────────→ WS2812B DIN │ 5V / VCC ├────────────→ WS2812B +5V │ GND └────────────→ WS2812B GND

如果 LED 數量較多,正式展覽系統建議讓 WS2812B 使用獨立 5V 電源, ESP32只提供 DATA,但兩邊的 GND 必須共地。

ESP32 GPIO19 ─────────→ WS2812B DIN 外接 5V + ───────────→ WS2812B +5V 外接 5V GND ─────┬───→ WS2812B GND │ ESP32 GND ────────┘ 必須共地
施工注意: 三針插頭的線色不能直接當成腳位依據, 一定要確認擴充板的 Signal / VCC / GND, 以及 WS2812B 的 DIN / 5V / GND 順序。

五、61顆WS2812B不是只拿來「發亮」

目前系統將 61 顆 WS2812B 分成不同區域:

#define BASE_START       0
#define BASE_COUNT       9

#define HORSE_START      9
#define HORSE_COUNT     16

#define TURTLE_START    25
#define TURTLE_COUNT    12

#define FLOWER_START    37
#define FLOWER_COUNT    24
區域 LED數量 燈光意義
底座 9 系統基礎氛圍
白馬 16 行動、力量、向前
烏龜 12 水、生態、守護
姻緣花 24 祝福、緣分、生命與地方情感

尤其姻緣花已由原本的固定色彩改成 HSV 彩虹色, 讓不同 LED 呈現五顏六色的效果,再搭配逐漸綻放與收縮的動畫。

uint16_t hue =
  colorOffset +
  (65535UL * i / FLOWER_COUNT);

strip.setPixelColor(
  FLOWER_START + i,
  strip.gamma32(
    strip.ColorHSV(
      hue,
      255,
      100
    )
  )
);

因此燈光不只是裝飾,而是成為展覽系統的一種 視覺回饋介面

六、第二個問題:展場音量不能每次都重新燒錄

JQ6500 原本的音量是在 Arduino 程式中設定:

uint8_t currentVolume = 20;

JQ6500 的控制範圍設定為:

0 ~ 30

如果要從 20 改成 25 或 30,最簡單的方法當然是修改程式再重新燒錄。 但是到了真正的展覽現場,這種方式非常不方便。

不同場域需要不同音量:
  • 安靜教室可能只需要 15~20。
  • 兒童館可能需要 20~25。
  • 大型展覽空間可能需要更高音量。
  • 閉館測試時可能希望立即靜音。

因此 V1.8.1 的設計目標很直接:

讓手機成為水井三寶的音量遙控器。

七、V1.8.1:手機直接控制 JQ6500 音量

手機連上 ESP32 的:

Wi-Fi SSID: Shuijing-Treasures ↓ 瀏覽器 ↓ http://192.168.4.1

就可以在 Edge Web 控制頁看到:

🔊 音量控制

0 ─────────── ● ─────────── 30

目前音量:20 / 30

【靜音】 【15】 【20】 【25】 【最大30】

八、HTML使用 Range Slider

Web介面使用 HTML 的 range 元件:

<input
  type="range"
  id="volumeSlider"
  min="0"
  max="30"
  value="20"
>

這讓手機可以直接拖曳調整:

0 ↓ 5 ↓ 10 ↓ 15 ↓ 20 ↓ 25 ↓ 30

九、手機不是直接控制JQ6500

這一點非常適合拿來作為《網際網路應用》課程教材。

手機調整音量時,真正的資料流程是:

手機瀏覽器 │ │ HTTP Request ▼ ESP32 Edge Web │ │ /volume?v=25 ▼ Web Handler │ ▼ jqSetVolume(25) │ │ UART Command ▼ JQ6500 │ ▼ 喇叭音量改變

也就是:

Web介面 → HTTP → ESP32 → UART → JQ6500 → 喇叭

十、Web端如何送出音量設定?

JavaScript 可以使用 fetch()

async function setVolume(v)
{
  const r = await fetch(
    '/volume?v=' + encodeURIComponent(v),
    {
      cache:'no-store'
    }
  );

  const d = await r.json();

  document.getElementById(
    'volumeValue'
  ).textContent = d.volume;
}

例如使用者選擇 25,瀏覽器送出:

GET /volume?v=25

十一、ESP32如何接收?

ESP32 Web Server 收到 /volume Request 後, 讀取參數並限制在 0~30:

int volume =
  server.arg("v").toInt();

if (volume < 0)
{
  volume = 0;
}

if (volume > 30)
{
  volume = 30;
}

jqSetVolume(
  (uint8_t)volume
);

完成後再回傳 JSON:

{
  "ok": true,
  "volume": 25
}

十二、為什麼回傳JSON,而不是重新整理網頁?

早期 Web 控制很容易採用:

按按鈕 ↓ ESP32執行 ↓ Redirect ↓ 整個網頁重新載入

V1.8.1 則改成:

拖曳音量 ↓ fetch() ↓ /volume?v=25 ↓ ESP32設定JQ6500 ↓ 回傳JSON ↓ 只更新音量數字

因此操作更接近現代 Web App, 也不會因為每次調整音量就讓整個手機畫面閃動。

十三、V1.8的重要改進:Realtime Web Status

在早期版本中曾經出現一個很有意思的問題:

故事已經播放完畢,ESP32 Serial Monitor 也顯示 PLAY COMPLETE, 但是手機網頁仍然顯示:

PLAYING

原因不是 JQ6500,也不是 ESP32 狀態機, 而是瀏覽器看到的是載入網頁那一刻的狀態。

因此 V1.8 新增:

/api/status

手機每秒讀取一次最新狀態:

setInterval(
  updateStatus,
  1000
);

十四、Realtime Status與音量控制整合

V1.8.1 再把音量加入 Status JSON。 概念上可以得到:

{
  "state":"PLAYING",
  "story":"白馬故事",
  "jq_busy":true,
  "volume":25,
  "cloud_ok":18,
  "cloud_fail":0
}

因此手機控制頁可以同時知道:

  • 現在是不是正在播放。
  • 目前播放哪一個故事。
  • JQ6500 BUSY 是否有效。
  • 目前音量是多少。
  • ESP32目前的即時狀態。

十五、播放完成後,手機會自己變化

現在操作流程變成:

手機按下「白馬」 ↓ PLAYING 白馬故事 JQ BUSY:ACTIVE ↓ 語音播放完成 ↓ COOLDOWN 白馬故事 JQ BUSY:IDLE ↓ 約2秒 ↓ READY / IDLE 待機

使用者不再需要手動按「重新整理」。

十六、為什麼Status Polling不能算成「使用者操作」?

這是 V1.8 開發過程中另一個很重要的系統設計問題。

水井三寶採用:

Local First, Cloud Later
現場互動優先,雲端同步延後。

如果瀏覽器每秒查詢 /api/status, ESP32每次都把它當成「使用者正在操作」, Cloud Sync 就可能永遠等不到真正的 Idle 時間。

因此:

void handleStatus()
{
  // 不呼叫 markLocalActivity()

  ...
}

也就是把:

「查詢狀態」 和 「真正操作設備」 分開處理。

十七、V1.8.1的完整控制關係

手機 │ │ Wi-Fi ▼ ESP32 Edge Web 192.168.4.1 │ ┌─────────┼─────────┐ │ │ │ ▼ ▼ ▼ 故事控制 音量控制 即時狀態 /play /volume /api/status │ │ ▲ ▼ ▼ │ ESP32 ────────────────┘ │ ├──── UART ───→ JQ6500 ───→ 喇叭 │ ├──── GPIO19 ─→ WS2812B │ ├──── I2C ────→ VL53L0X │ └──── Wi-Fi STA │ ▼ Internet │ ▼ Django / Cloud

十八、從「接腳修改」看工程設計

GPIO13改成GPIO19,看起來只修改了一行程式:

#define LED_PIN 19

但背後其實代表系統已經從:

「麵包板能動」 進入 「正式作品能施工」 再進入 「現場人員能維護」

這也是 IoT 專題很重要的一課: 好的系統不只是功能正確,也要考慮安裝、操作與維護。

十九、從「音量控制」看Edge Web的價值

同樣地,音量原本只是 Arduino 裡的一個變數:

uint8_t currentVolume = 20;

但是加入 Edge Web 之後, 這個變數就成為使用者可以透過手機操作的系統參數。

Arduino Variable currentVolume ↓ ESP32 Web API /volume?v=25 ↓ HTML / JavaScript Volume Slider ↓ 手機操作介面

這就是 Edge Web 很重要的價值:

把嵌入式系統內部的控制功能, 轉換成一般使用者也能操作的 Web 服務。

二十、從水井三寶學「網際網路應用」

這次修改雖然從「換一支GPIO」和「增加音量滑桿」開始, 但其實可以串起很多《網際網路應用》的重要概念。

實作 可以學到的概念
GPIO19控制WS2812B Embedded I/O、硬體介面、模組化設計
手機連ESP32 SoftAP、Wi-Fi、IP、Client / Server
192.168.4.1 Private IP、Edge Web Server
/volume?v=25 URL、Path、Query Parameter、HTTP GET
fetch() 非同步Web Request
JSON Response Web資料交換格式
/api/status API、Realtime Status、Polling
JQ6500 UART、裝置控制
WS2812B 數位燈光與視覺回饋
Django同步 Edge → Internet → Cloud

二十一、版本演進

版本 主要功能
早期版本 ESP32+JQ6500基本故事播放
Layer 3 ESP32 Edge Web手機控制
Layer 4 V1.7 AP+STA+Django+Queue+Cloud Sync
Layer 4 V1.8 Realtime Web Status
Layer 4 V1.8.1 Realtime Status+Mobile Volume Control+GPIO19 WS2812B施工配置

二十二、結語:真正的IoT,是讓實體作品、網路與使用者連在一起

水井三寶智慧互動展覽系統的開發不是一次完成, 而是在實際接線、播放、展示、手機操作與雲端同步的過程中不斷修正。

從 GPIO13 改成 GPIO19, 是為了讓硬體更適合正式施工; 從固定音量改成手機控制, 是為了讓系統更適合真實展覽環境; 從靜態網頁改成 Realtime Status, 則是讓使用者真正看見設備當下的狀態。

文化作品是起點,ESP32負責互動,Edge Web負責控制, Internet負責連結,Django負責資料,而未來的AI/RAG則負責讓地方知識持續被理解與使用。

這也正是「水井三寶智慧互動展覽系統」作為 大學《網際網路應用》與USR實作教材最重要的價值: 不是只教學生把程式寫出來,而是讓學生從一個真實地方文化作品出發, 一路理解硬體、網路、Web、資料與使用者之間如何形成一個完整系統。

水井三寶 ESP32 GPIO19 WS2812B JQ6500 Edge Web Realtime Status Mobile Volume Control HTTP JSON IoT Django USR

2026年8月16日 星期日

以「水井三寶智慧互動展覽系統」為場域型專題教材

網際網路應用|課程大綱

以「水井三寶智慧互動展覽系統」為場域型專題教材

ESP32 → Edge Web → Internet → Django → Data → Knowledge → AI/RAG

模組名稱智慧生活與物聯網
課程名稱網際網路應用
計畫名稱水井村智慧減碳節水三生一體實踐計畫

一、課程簡介

本課程以「水井三寶智慧互動展覽系統」作為《網際網路應用》的場域型專題教材,將抽象的網路協定、位址、Client/Server、Web API與雲端資料概念,轉化為學生可以看見、操作、量測與除錯的真實系統。學生從白馬、烏龜、姻緣花三項地方文化作品出發,理解按鈕、感測器、JQ6500語音與WS2812B燈光如何由ESP32控制;再以手機連入ESP32 Edge Web,實際觀察AP、STA、IP、MAC、DNS、Domain、Port及HTTP Request的作用;進一步將匿名互動事件以JSON及REST API送往Django,建立Digital Twin、資料庫與雲端儀表板。課程不以「會做網頁」為終點,而是追問「一個按鈕按下後,資料去了哪裡?」讓學生沿著實體互動、區域網路、Internet、Web Server、API、Database一路追蹤資料生命週期。最後再由Data走向Knowledge與AI/RAG,思考地方故事如何被整理為可搜尋、可引用、可延伸的文化知識。藉由水井村USR真實場域,學生不只學習網際網路技術,也理解科技如何服務文化保存、地方教育與大學社會責任,培養需求分析、系統整合、網路診斷與知識應用能力。

二、課程核心問題

一個按鈕按下後,資料去了哪裡? ↓ ESP32 → Edge Web → Wi-Fi → IP / DNS → HTTP / JSON / REST API ↓ Django → Database → Digital Twin ↓ Data → Knowledge → AI/RAG

三、學習目標

  • 理解ESP32與周邊裝置通訊,以及實體事件如何轉為數位資料。
  • 理解AP、STA、LAN、IP、MAC、DNS、Domain、Port等網路概念。
  • 理解Browser、Client、Server、Edge Web及HTTP資料流程。
  • 理解GET/POST、Header、Body、JSON與REST API。
  • 將ESP32匿名事件送至Django,理解Database與Dashboard。
  • 能以分層方法診斷Wi-Fi、DNS、HTTPS、API與Cloud問題。
  • 理解Data、Knowledge與AI/RAG的關係。
  • 從USR場域需求思考網際網路技術的社會實踐價值。

四、五層架構與課程主軸

層級系統角色課程重點
Layer 1|文化作品層白馬、烏龜、姻緣花需求、情境、實體與數位系統邊界
Layer 2|智慧互動層ESP32+ToF+按鈕+JQ6500+RGBGPIO、UART、事件、裝置控制
Layer 3|Edge Web層ESP32內嵌網站AP/STA、IP、Web Server、HTTP
Layer 4|Digital Twin / Data層Django+Database+DashboardDNS、HTTPS、JSON、REST API、資料庫
Layer 5|文化知識層文化網站+AI/RAGData → Knowledge、RAG、知識服務

五、系列教材文章

#教材主題核心概念文章連結
1ESP32 × JQ6500 UART 通訊UART序列通訊、語音模組、封包命令閱讀教材
2Layer 2|智慧互動層GPIO、按鈕事件、JQ6500故事播放、互動狀態閱讀教材
3Layer 3|ESP32 Edge WebWeb Server、HTTP、手機控制、192.168.4.1、Edge Computing閱讀教材
4Layer 4|ESP32 × DjangoClient/Server、HTTP、Django、事件資料、Digital Twin閱讀教材
5ESP32 AP+STA 共存SoftAP、Station、LAN與Internet、本地控制與雲端連線閱讀教材
6Layer 4 V1.7|AP+STA+Django整合實測Wi-Fi診斷、DNS、HTTPS、Queue、Local First / Cloud Later閱讀教材
7水井三寶智慧互動展覽系統|五層架構文化作品、智慧互動、Edge Web、Digital Twin/Data、文化知識閱讀教材
8WS2812B × ESP32 燈光控制RGB LED、單線數位控制、色彩編碼、故事狀態回饋閱讀教材
9一個按鈕按下後,資料去了哪裡?Browser → Edge → HTTP → JSON → Django → Database → Dashboard閱讀教材
10IP、MAC、DNS、Domain與PortMAC、IP、Gateway、DNS、Domain、Port、網路診斷閱讀教材
11Data → Knowledge → AI/RAG事件資料、文化知識庫、Chunk、Metadata、Embedding、Vector Search、RAG閱讀教材
12HTTP+JSON+REST APIGET/POST、Header、Body、JSON、REST Endpoint、HTTP Status、Django API閱讀教材

六、建議學習路徑

裝置通訊:ESP32 × JQ6500 × WS2812B ↓ 智慧互動:按鈕 / 感測 → 故事 → 狀態 ↓ Edge Web:手機 → AP → 192.168.4.1 → HTTP ↓ 網路基礎:AP / STA → MAC → IP → Gateway → DNS → Domain → Port ↓ Internet應用:HTTP → JSON → REST API → HTTPS ↓ Cloud / Data:Django → Database → Dashboard → Digital Twin ↓ Knowledge / AI:Data → Knowledge Base → Retrieval → AI/RAG

七、從「做作品」轉向「理解系統」

本課程不把Arduino程式、ESP32接線或Django網站視為彼此獨立的單元,而是要求學生追蹤一筆資料的完整旅程。學生不只要知道「程式可以跑」,還要能回答:手機為什麼找到192.168.4.1?ESP32為什麼同時需要AP與STA?Domain如何透過DNS找到Server?JSON如何進入HTTP Body?Django為什麼回201?當Cloud HTTP=-1時,問題究竟發生在Wi-Fi、DNS、TLS還是API?

核心教學觀念:會使用網路工具只是起點;能畫出資料流、說明協定角色、判斷故障所在層級,才是真正理解「網際網路應用」。

八、USR × 網際網路應用

水井三寶讓網際網路課程不再只是虛擬案例。學生面對的是地方文化作品、展覽觀眾、現場網路、設備可靠度與文化知識保存等真實需求。技術因此不只是完成作業,而是成為連結地方故事、智慧互動、教育推廣與資料累積的工具。透過場域實作,學生可以理解大學社會責任不只是「到社區服務」,而是運用專業知識與地方共同解決問題,並留下可以持續運作、持續學習與持續擴充的系統。

九、關鍵字

ESP32IoTEdge WebAP / STAIP / MAC / DNSHTTPJSONREST APIDjangoDigital TwinAI/RAGUSR

[水井村USR] HTTP+JSON+REST API

HTTP+JSON+REST API

從「水井三寶智慧互動展覽系統」看懂ESP32如何把資料送到Django

HTTP Request × Header × JSON Body × REST Endpoint × Django API × Response

當ESP32已經能連上Internet,下一個問題就是:它到底如何把「烏龜故事被播放」這件事送到Django?答案不是單靠Wi-Fi,而是透過HTTP、JSON與REST API三個重要概念協同工作。這篇文章就用水井三寶Layer 4的資料流,帶學生看懂一筆匿名事件如何從ESP32送到雲端。

一、先從一筆事件開始

{
  "event_uuid":"SHUIJING-001-0000000062",
  "event_type":"STORY_START",
  "story":"002",
  "source":"web",
  "duration_ms":0,
  "completed":null,
  "network_status":"online",
  "metadata":{
    "firmware":"Layer4-V1.7",
    "jq_confirmed":true
  }
}

這段資料本身還沒有「送上網」。它只是ESP32中的一個JSON字串。接下來才輪到HTTP與REST API登場。

二、HTTP是什麼?

HTTP可以理解成Client與Server之間溝通的規則。水井三寶同時用到GET與POST:

Method水井三寶例子用途
GET/play?track=2手機要求ESP32播放烏龜故事
POST/api/exhibition/events/ESP32把匿名事件送到Django
一句話記:GET常用來取得資源或提出查詢;POST常用來把資料送給Server處理。

三、一個HTTP Request有哪三個部分?

Request Line POST /api/exhibition/events/ HTTP/1.1 Headers Content-Type: application/json X-Device-ID: SHUIJING-001 X-Device-Key: ******** Body { "event_type":"STORY_START", "story":"002" }
部分作用
Request Line使用什麼Method、要去哪一個Path
Headers提供內容格式、裝置識別、驗證資訊
Body真正要送給Server的資料

四、JSON是什麼?

JSON是一種文字格式,適合不同系統之間交換結構化資料。

{
  "story":"002",
  "source":"web",
  "completed":true
}
KeyValue意義
story"002"烏龜故事
source"web"由Edge Web啟動
completedtrue故事完整播放
JSON的價值:ESP32、JavaScript、Python、Django等平台都可以很容易解析,因此非常適合IoT與Web API。

五、為什麼要寫 Content-Type?

Content-Type: application/json

這一行是在告訴Django:「這次HTTP Body裡裝的是JSON。」如果Server不知道資料格式,就無法正確解析內容。

六、REST API是什麼?

REST API是一種設計Web服務的方式:把系統功能整理成清楚的URL Endpoint,再搭配HTTP Method進行操作。

https://shuijingtreasures.pythonanywhere.com/api/exhibition/events/
https:// 安全的HTTP通訊 shuijingtreasures.pythonanywhere.com Domain / Server /api/exhibition/events/ REST API Endpoint
Endpoint可以理解成「API的入口地址」。

七、為什麼ESP32不能直接呼叫Django函式?

ESP32和Django位於不同設備、不同執行環境,因此ESP32不能直接執行Server上的Python函式,只能透過網路把Request送到URL,再由Django URL Router找到對應View。

ESP32 ↓ HTTP POST /api/exhibition/events/ ↓ Django URL Router ↓ api_event() ↓ Database

八、Device ID與API Key放在哪裡?

X-Device-ID: SHUIJING-001
X-Device-Key: ********

它們放在HTTP Header,讓Django知道是哪一台設備,以及這台設備是否有權限傳送資料。

正式API Key不應公開放在部落格、簡報或公開GitHub倉庫。教材只示範格式即可。

九、ESP32端的HTTP POST概念

WiFiClientSecure client;
HTTPClient https;

https.begin(
  client,
  "https://shuijingtreasures.pythonanywhere.com/api/exhibition/events/"
);

https.addHeader(
  "Content-Type",
  "application/json"
);

https.addHeader(
  "X-Device-ID",
  DEVICE_ID
);

https.addHeader(
  "X-Device-Key",
  DEVICE_KEY
);

int code = https.POST(jsonBody);
建立HTTPS Client ↓ 指定REST API URL ↓ 加入Headers ↓ 放入JSON Body ↓ POST ↓ 等待HTTP Response

十、Server如何告訴ESP32成功或失敗?

Status Code常見意義
200Request成功
201資料建立成功
400Request內容有問題
401驗證失敗
404API路徑不存在
500Server內部錯誤

水井三寶Cloud Test成功時,可看到:

Cloud HTTP = 201
Response = {"ok":true,...}
CLOUD POST OK

這代表DNS、HTTPS、API URL、裝置驗證與Django資料接收都已經成功。

十一、為什麼 Cloud HTTP = -1 不是Django回的狀態碼?

實作過程中曾出現:

Cloud HTTP = -1

後來追查發現問題發生在DNS解析階段,也就是HTTP Request根本還沒到Django。

DNS失敗 ↓ 找不到Server IP ↓ 無法建立TCP/TLS連線 ↓ HTTP Request沒有到Django ↓ 自然也不會有401 / 404 / 500
除錯原則:Cloud失敗時,不要第一時間就修改Django;先確認Wi-Fi、DNS、TLS與HTTP是否真的走到Server。

十二、為什麼採用 Local First, Cloud Later?

如果每次故事一開始就同步做HTTPS POST,可能讓JQ6500播放、Edge Web與網路傳輸互相干擾。因此系統採取:

故事播放 ↓ 建立JSON Event ↓ 先存LittleFS Queue ↓ 播放完成 ↓ 系統空閒 ↓ HTTP POST ↓ Django
設計原則:現場互動優先;雲端同步延後。

十三、HTTP、JSON、REST API怎麼分工?

技術主要工作水井三寶角色
HTTP規定Client與Server怎麼請求與回應ESP32 POST事件到Django
JSON描述資料格式故事、來源、完成狀態等事件內容
REST API定義Server提供的URL服務入口/api/exhibition/events/
一句話記:HTTP是「怎麼送」,JSON是「送什麼」,REST API是「送到哪個功能入口」。

十四、一筆「烏龜故事」事件的完整旅程

使用者按「002 烏龜故事」 ↓ ESP32 Edge Web ↓ JQ6500開始播放 ↓ 建立 JSON ↓ LittleFS Queue ↓ ESP32 STA ↓ DNS找到PythonAnywhere ↓ HTTPS ↓ POST /api/exhibition/events/ ↓ Headers:Device ID / API Key ↓ JSON Body ↓ Django View ↓ Database ↓ HTTP 201 Response ↓ Dashboard

十五、和水井三寶五層架構的關係

層級HTTP / JSON / REST API的角色
Layer 2 智慧互動層產生故事與感測事件
Layer 3 Edge Web層本地HTTP GET控制ESP32
Layer 4 Digital Twin / Data層HTTP POST+JSON+REST API送到Django
Layer 5 文化知識層未來可透過API提供文化內容與AI/RAG服務

十六、真正理解API,要能回答七個問題

Client是誰? ESP32 Server是誰? Django Protocol? HTTPS Endpoint? /api/exhibition/events/ Method? POST 資料格式? JSON 成功如何判斷? HTTP Status + Response Body

十七、課堂思考題

問題一:如果JSON格式正確,但API URL打錯,可能得到哪一類HTTP狀態?
問題二:為什麼Device ID與API Key適合放Header,而故事內容放Body?
問題三:GET與POST都能帶資料,為什麼事件上傳通常選POST?
問題四:如果Django已回201,但Dashboard沒有資料,問題可能已經移到哪一層?

十八、結語:REST API是Edge與Cloud之間的橋

水井三寶的ESP32負責感測、故事播放與現場控制,Django負責跨場次資料、Dashboard與後續分析。兩邊位於不同設備、使用不同語言,卻能透過HTTP+JSON+REST API協同工作。

因此,REST API真正重要的地方,不只是讓ESP32「可以傳資料」,而是建立清楚的系統邊界:Edge負責即時互動,Cloud負責資料服務。HTTP定義溝通方式,JSON定義資料語言,而REST API定義雙方合作的入口。

HTTP GET POST JSON REST API Endpoint Header HTTPS ESP32 Django

[水井村USR] Data → Knowledge → AI/RAG

Data → Knowledge → AI/RAG

從「水井三寶智慧互動展覽系統」第五層看懂資料如何成為文化知識

Event Data × Database × Cultural Knowledge × Retrieval × LLM × RAG

前面的水井三寶教材已經讓一筆「烏龜故事被播放」的事件,從ESP32經過HTTP、JSON與REST API送到Django。但資料送到Database並不是終點。真正值得思考的是:「烏龜今天播放37次」和「烏龜為什麼代表守水?」是同一種資料嗎?答案是否定的。這正是水井三寶五層架構從Layer 4走向Layer 5,以及未來導入AI/RAG最重要的分界。

一、先分清楚:Data不等於Knowledge

假設Django儀表板顯示:

白馬故事:52次
烏龜故事:37次
姻緣花故事:46次
總故事:21次
今日互動人次:156

這些是非常有價值的Data(資料),它告訴我們系統發生了什麼。

但如果觀眾問:

「為什麼水井村會有白馬與烏龜?」
「姻緣花和地方記憶有什麼關係?」
「水井三寶是哪三件作品?」
「這些作品和生產、生態、生活三生一體有什麼關係?」

這些問題就不能只靠播放次數回答。它需要地方故事、創作者資料、作品說明、影音、教材與USR實踐內容,這些才逐步形成Knowledge(知識)

一句話記:Data告訴我們「發生了什麼」;Knowledge幫助我們理解「這代表什麼、為什麼」。

二、Layer 4和Layer 5到底差在哪裡?

比較Layer 4:Digital Twin / DataLayer 5:文化知識
核心事件、狀態、統計故事、脈絡、意義、教材
典型內容故事播放次數、時間、來源、完成率白馬、烏龜、姻緣花的文化故事
資料型態結構化資料文字、圖片、影音、文件等多元內容
主要工具Django Model、Database、Dashboard文化網站、知識庫、未來RAG
回答問題今天有多少互動?這個故事的文化意義是什麼?
第五層不是「再做一個網站」。網站只是呈現知識的介面;第五層真正的核心是把地方文化整理成可保存、可搜尋、可教學、可被AI檢索的知識資源。

三、從Data到Information,再到Knowledge

可以把轉換過程理解成:

Data 「002故事被播放」 「20:22:39」 「source = web」 ↓ Information 「今天烏龜故事共播放37次」 「手機操作占60%」 ↓ Knowledge 「觀眾為什麼對烏龜故事有興趣?」 「烏龜在水井三寶文化中的意義是什麼?」 ↓ AI / RAG 讓觀眾用自然語言詢問文化知識

從原始事件到統計,是「整理資料」;從統計到文化脈絡,則需要另外建立知識內容,不能只靠數字自動產生。

四、水井三寶的Knowledge從哪裡來?

第五層可以逐步整理以下知識來源:

知識類型內容例子
地方故事白馬、烏龜、姻緣花、水井村與地方記憶
作品資料作品名稱、創作者、材料、設計理念、創作歷程
工藝知識玉米籜、蓪草、姻緣花燈、地方工藝製作
教育教材繪本、教案、親子活動、科技教育教材
影音內容故事語音、影片、訪談、活動紀錄
USR實踐生產、生態、生活三生一體與大學社會責任歷程
互動資料Layer 4匿名事件與展覽統計

五、傳統搜尋和AI問答有什麼不同?

傳統網站通常要求使用者先知道要點哪一頁:

首頁 ↓ 水井三寶 ↓ 烏龜 ↓ 故事介紹 ↓ 閱讀內容

AI問答則可以讓使用者直接問:

「為什麼烏龜代表珍惜水資源?」
「請用小學生聽得懂的方式介紹水井三寶。」
「姻緣花和地方工藝有什麼關係?」

但這裡出現一個新問題:大型語言模型不一定知道水井村的地方知識,即使回答得很流暢,也可能產生錯誤內容。

六、這就是為什麼需要RAG

RAG是 Retrieval-Augmented Generation,中文常譯為「檢索增強生成」。

核心概念不是讓AI憑記憶回答,而是:

使用者問題 「姻緣花代表什麼?」 ↓ Retrieval 檢索 從水井三寶知識庫找相關資料 ↓ 找到: 作品介紹 創作者資料 地方故事 USR教材 ↓ 把相關內容交給LLM ↓ Generation 生成 ↓ 根據水井三寶資料回答
一句話理解RAG:先「查資料」,再「請AI根據查到的資料回答」。

七、為什麼不直接問Chatbot就好?

一般LLM可能具備廣泛知識,但「水井三寶」屬於高度地方化、專案化的內容。這類知識可能不在模型訓練資料中,也可能在作品持續發展後改變。

直接LLMRAG
主要依賴模型既有知識先查水井三寶自己的知識庫
地方內容可能不足可以納入地方採集與USR資料
更新通常不容易立即反映更新知識庫即可提供新內容
回答來源較難控制可要求依檢索內容回答並提供來源

八、RAG不是把整個網站一次丟給AI

真正的RAG通常會先對文化內容做前處理:

文化文章 / 教材 / 訪談 / 作品資料 ↓ 清理與整理 ↓ 切成較小的 Chunk ↓ 建立 Metadata ↓ Embedding ↓ Vector Database ↓ 等待使用者查詢

例如一篇長文章可以切成:

Chunk 001:白馬的故事
Chunk 002:烏龜與水文化
Chunk 003:姻緣花的文化意義
Chunk 004:創作者與作品歷程
Chunk 005:水井三寶與三生一體

九、Embedding是在做什麼?

Embedding可以簡化理解成:把文字轉換成一組數值向量,讓系統可以比較「語意上像不像」。

例如使用者問:

「哪一個角色和珍惜水資源有關?」

即使文章中沒有完全相同的句子,向量檢索仍可能找到:

「烏龜慢慢觀察土地與水,
提醒我們珍惜自然。」

這和傳統只比對關鍵字的搜尋方式不同。

十、Metadata為什麼很重要?

每個知識Chunk除了文字,也應保存來源資訊:

{
  "title":"水井三寶作品介紹",
  "category":"地方文化",
  "character":"turtle",
  "source":"official",
  "language":"zh-Hant",
  "updated_at":"2026-08"
}

這讓未來RAG可以限制:

只找「官方作品資料」
只找「烏龜」相關內容
只找「教育教材」
只找最新版本內容

十一、一個完整RAG問答會怎麼走?

① 使用者 「請告訴我烏龜和水井村的關係」 ↓ ② Web / Django 接收問題 ↓ ③ Embedding 把問題轉成向量 ↓ ④ Vector Search 找最相關的文化資料 ↓ ⑤ Retrieve 取回Top-K相關Chunk ↓ ⑥ Prompt 問題 + 檢索資料 + 回答規則 ↓ ⑦ LLM 根據資料生成回答 ↓ ⑧ Response 回答 + 來源

十二、Django在第五層還可以扮演什麼角色?

Django在Layer 4主要接收事件;到了Layer 5,可以進一步成為文化知識與AI服務的入口:

/api/exhibition/events/     → Layer 4事件
/api/knowledge/search/      → 搜尋文化知識
/api/ai/ask/                → AI/RAG問答
/api/treasures/white-horse/ → 白馬知識
/api/treasures/turtle/      → 烏龜知識
/api/treasures/flower/      → 姻緣花知識

這樣就形成:

ESP32 / Browser ↓ Django ↙ ↘ Data Knowledge 事件庫 文化知識庫 \ / \ / AI/RAG

十三、Layer 4資料能不能直接拿來做RAG?

可以使用,但用途不同。

例如Layer 4告訴我們:

今天:
白馬 52次
烏龜 37次
姻緣花 46次

AI可以根據這些結構化資料回答:

「今天哪個故事最多人選?」
「三個故事的播放比例是多少?」

文化知識庫則回答:

「白馬象徵什麼?」
「誰參與水井三寶作品創作?」
「水井三寶如何連結三生一體?」
因此未來更完整的AI,不只是RAG查文章,而可能同時查詢結構化Data+非結構化Knowledge

十四、未來可以形成兩種AI工具

AI角色資料來源問題例子
展覽數據助理Django事件資料庫今天哪個故事最熱門?
文化導覽助理RAG文化知識庫請介紹姻緣花的故事

再進一步,也可以讓兩者合作:

「今天白馬故事最多人選,請結合白馬的文化意義,產生一段展覽觀察摘要。」

十五、AI回答文化故事時,最重要的是「有根據」

文化AI不能只追求回答流暢,還必須重視:

原則說明
Grounding回答必須根據知識庫資料
Source能指出資料來源
Version知道資料更新時間
Authority區分官方、訪談、學生整理等來源
Uncertainty資料不足時應說不知道,而不是自行編故事
文化數位化最怕「AI把地方故事講得很好聽,卻不是地方真正的故事」。因此RAG的價值不只是提升回答能力,更重要的是讓AI回到地方資料與文化脈絡。

十六、五層架構到這裡終於完整串起來

Layer 1
文化作品層
白馬、烏龜、姻緣花實體作品,是文化的起點。
Layer 2
智慧互動層
ESP32、ToF、按鈕、JQ6500、WS2812B讓作品能感測、發聲與發光。
Layer 3
Edge Web層
手機透過ESP32內嵌網站控制作品、查看設備狀態。
Layer 4
Digital Twin / Data層
匿名事件經HTTP+JSON+REST API進入Django,形成Database與Dashboard。
Layer 5
文化知識層
地方故事、作品、工藝、影音、教育與USR內容形成Knowledge Base,未來再透過AI/RAG提供智慧文化導覽。

十七、從五層架構看資料價值如何增加

實體文化 Layer 1 ↓ 互動訊號 Layer 2 ↓ Web控制 Layer 3 ↓ Data Layer 4 ↓ Knowledge Layer 5 ↓ AI / RAG ↓ 新的文化學習與導覽體驗

這條路徑也說明了為什麼水井三寶不是單純的ESP32作品:硬體只是入口,真正長期累積的資產是資料、文化內容與知識。

十八、課堂實作可以怎麼做?

可以把學生分成四個階段:

階段任務
1. Data讀取Django事件資料,統計三個故事播放次數
2. Knowledge把白馬、烏龜、姻緣花文章整理成知識文件
3. Retrieval把文件切Chunk、加入Metadata並進行檢索
4. AI/RAG讓LLM只根據檢索內容回答文化問題

十九、課堂思考題

問題一:「烏龜故事今天播放37次」屬於Data、Information還是Knowledge?為什麼?
問題二:為什麼Layer 5不能只把Layer 4的Database直接改名叫「知識庫」?
問題三:如果LLM已經很強,為什麼地方文化仍然需要RAG?
問題四:RAG回答水井三寶故事時,為什麼來源與Metadata特別重要?
問題五:未來要如何讓AI同時回答「今天誰最熱門」和「這個角色代表什麼」?

二十、結語:真正的第五層,是把地方記憶變成可以延續的知識

從ESP32送出一筆事件,到Django形成Dashboard,我們完成的是Data;把作品故事、創作者、工藝、教育與USR歷程整理起來,我們開始建立Knowledge;當這些知識能被檢索、被引用,再交給LLM產生符合不同觀眾需求的回答,就進一步形成AI/RAG服務。

因此,水井三寶五層架構的最後一層並不是為了「加上AI」而加AI。真正的目標是先把地方知識整理好,再讓AI成為連結人與知識的新介面。

Data告訴我們發生了什麼;Knowledge保存地方為什麼重要;AI/RAG則讓更多人能用自己的問題,走進這些知識。

Data Knowledge AI RAG Embedding Vector Search Django LLM 文化數位化 USR

[水井村USR] IP、MAC、DNS、Domain與Port

IP、MAC、DNS、Domain與Port

從「水井三寶智慧互動展覽系統」看懂網際網路位址與服務

MAC → IP → DNS → Domain → Port → HTTP/HTTPS

當手機可以打開 192.168.4.1,ESP32也顯示 STA=ONLINE,為什麼Django有時還是收不到資料?這個問題正好可以帶學生理解網際網路中幾個最重要、也最容易混淆的概念:MAC、IP、DNS、Domain與Port。它們不是同一件事,而是在不同階段回答不同問題:你是誰?你在哪裡?我要找誰?服務開在哪裡?

一、先看水井三寶的真實網路環境

目前水井三寶智慧互動展覽系統同時有兩條重要網路路徑:

【本地控制】 手機 ↓ Wi-Fi ESP32 SoftAP ↓ http://192.168.4.1 【雲端同步】 ESP32 STA ↓ Wi-Fi Router ↓ Gateway ↓ DNS ↓ Internet ↓ shuijingtreasures.pythonanywhere.com ↓ Django API

這兩條路徑雖然都使用Wi-Fi,但目的完全不同。第一條是LAN內的本地控制;第二條才真正需要Internet與DNS。

二、MAC Address:你是哪一張網路卡?

MAC Address可以理解成網路介面的「硬體識別碼」。ESP32的Wi-Fi介面有自己的MAC,手機也有自己的MAC。

MAC主要回答:
「區域網路裡,這個網路介面是誰?」

在同一個LAN內,資料傳遞最後仍需要靠資料鏈結層識別實際設備。IP可以改變,但MAC通常是網路介面的基礎識別資訊。

ESP32 MAC:
AA:BB:CC:DD:EE:FF

手機 MAC:
11:22:33:44:55:66

對學生來說,最簡單的理解方式是:

MAC像「網路卡的身分證號」;IP像「目前所在位置的地址」。

三、IP Address:資料要送到哪一台設備?

在水井三寶系統中,最常看到兩個IP:

ESP32 AP IP:
192.168.4.1

ESP32 STA IP:
192.168.1.119

這兩個IP屬於同一顆ESP32,但代表不同網路介面角色。

IP用途誰會使用
192.168.4.1ESP32 SoftAP本地控制頁連到Shuijing-Treasures AP的手機
192.168.1.119ESP32連到外部Wi-Fi後取得的STA IP路由器與區域網路

所以同一台ESP32可以同時:

AP角色: 我提供一個Wi-Fi給別人連 IP = 192.168.4.1 STA角色: 我自己去連另一台Wi-Fi IP = 192.168.1.119
這就是AP+STA共存的核心:一邊當本地Server,一邊當Internet Client。

四、Private IP與Public IP有什麼差別?

192.168.x.x這類位址是Private IP,只在區域網路內使用,不能直接在Internet上被全球路由。

例如:

ESP32:192.168.1.119
Router:192.168.1.1

ESP32如果要存取Internet,通常必須經過Router,再由Router以Public IP代表內部設備與外界通訊。

ESP32 192.168.1.119 ↓ Router / NAT 192.168.1.1 ↓ Public IP ↓ Internet

五、Default Gateway:離開這個LAN,要先找誰?

當ESP32要連PythonAnywhere時,目標已經不在自己的區域網路內,因此資料必須先交給Default Gateway。

水井三寶實測曾顯示:

STA IP = 192.168.1.119
Gateway = 192.168.1.1
Gateway回答:
「如果目的地不在我這個LAN,我下一站要把封包交給誰?」

六、Domain Name:人類比較容易記住的名稱

與其讓程式直接記一串Server IP,我們通常使用Domain Name:

shuijingtreasures.pythonanywhere.com

這比IP好記,也方便Server未來搬移。

因此Domain可以理解成:

給人看的服務名稱。

但電腦不能只靠名稱送封包,它仍需要把Domain轉成IP,這就是DNS的工作。

七、DNS:把Domain翻譯成IP

DNS可以想成「Internet電話簿」。

shuijingtreasures.pythonanywhere.com ↓ DNS ↓ Server IP Address

水井三寶V1.7最重要的一次除錯,就是發現:

WiFi status = 3
STA IP = 192.168.1.119
Gateway = 192.168.1.1
DNS = 192.168.1.1
RSSI = -20

Resolving:
shuijingtreasures.pythonanywhere.com

DNS result = -54
ERROR: DNS FAILED

這個案例非常適合用來提醒學生:

Wi-Fi連線成功,不代表Internet服務一定可用。
當DNS失敗,即使STA已取得IP,仍然找不到Domain對應的Server。

最後系統改用Public DNS:

DNS1 = 8.8.8.8
DNS2 = 1.1.1.1

DNS正常後,Cloud Test才真正成功。

八、Domain與DNS不要混在一起

概念例子作用
Domainshuijingtreasures.pythonanywhere.com人類容易記住的Server名稱
DNS8.8.8.8把Domain查成IP的服務

一句話記:

Domain是「名字」,DNS是「查名字的服務」。

九、Port:同一台Server上,要找哪一個服務?

IP只能找到「哪一台主機」,但同一台主機上可能同時運行很多不同服務。

例如:

Web Server
SSH
Database
Email
API
...

Port就是用來區分服務。

服務常見Port
HTTP80
HTTPS443
SSH22

水井三寶ESP32本地Edge Web使用:

http://192.168.4.1

沒有寫Port時,HTTP預設使用80。

而Django雲端API使用:

https://shuijingtreasures.pythonanywhere.com/...

HTTPS預設使用443。

IP回答「哪台電腦」;Port回答「那台電腦上的哪個服務」。

十、URL其實把很多資訊組在一起

例如水井三寶Edge Web:

http://192.168.4.1/play?track=2

可以拆成:

部分內容意義
Protocolhttp使用HTTP
Host192.168.4.1ESP32本地IP
Port80省略時使用HTTP預設Port
Path/play播放功能
Querytrack=2播放第2個故事

再看Django API:

https://shuijingtreasures.pythonanywhere.com/api/exhibition/events/

可拆成:

https
  ↓
Domain
shuijingtreasures.pythonanywhere.com
  ↓
Port 443
  ↓
Path
/api/exhibition/events/

十一、把MAC、IP、DNS、Domain與Port放在同一條資料流

手機按「002 烏龜故事」 ↓ Wi-Fi區域網路 ↓ MAC 找到LAN中的實際網路介面 ↓ IP 找到ESP32:192.168.4.1 ↓ Port 80 找到ESP32 Web Server ↓ HTTP GET /play?track=2 ↓ 故事播放 之後雲端同步: ESP32 STA 192.168.1.119 ↓ Gateway 192.168.1.1 ↓ DNS 8.8.8.8 ↓ Domain shuijingtreasures.pythonanywhere.com ↓ Server IP ↓ Port 443 ↓ HTTPS ↓ Django API

十二、五個名詞,用一句話一起記

名稱一句話記憶
MAC這張網路卡是誰?
IP設備目前在哪裡?
Domain人類想找的服務叫什麼名字?
DNS這個名字對應哪個IP?
Port主機上的哪個服務?

十三、最容易搞錯的幾件事

1
有IP,不代表DNS一定正常。
ESP32可以取得192.168.1.119,但仍可能解析不了Domain。
2
能開192.168.4.1,不代表Internet正常。
因為這只是ESP32 SoftAP內的本地LAN通訊。
3
Domain不是IP。
Domain必須先透過DNS解析成IP。
4
IP相同,Port不同,可以是完全不同的服務。

十四、遇到「雲端連不上」應該怎麼查?

不要一開始就修改Django。應該由下往上逐層檢查:

① Wi-Fi connected? ↓ ② 有沒有取得 IP? ↓ ③ Gateway 正常? ↓ ④ DNS 可以解析 Domain? ↓ ⑤ TCP / Port 443 可連? ↓ ⑥ HTTPS 成功? ↓ ⑦ Django API URL 正確? ↓ ⑧ Device ID / API Key 正確? ↓ ⑨ JSON格式正確? ↓ ⑩ Database / Dashboard 有資料?
這就是分層除錯的價值:不是看到「Cloud失敗」就全部一起查,而是先判斷問題究竟在哪一層。

十五、這和OSI七層模型有什麼關係?

本篇出現的概念可以粗略放進OSI / TCP/IP架構中:

概念大致所在層級
Wi-Fi、MACData Link
IP、Router、GatewayNetwork
TCP、PortTransport
DNS、HTTP、HTTPSApplication

這也是下一步理解OSI七層模型的重要基礎。

十六、課堂思考題

問題一:
如果手機可以打開192.168.4.1,但是Django沒有收到資料,MAC、IP、DNS、Domain、Port中,哪些已經可以確定正常?哪些還不能?
問題二:
ESP32的STA IP從192.168.1.119變成192.168.1.125,為什麼Django網址不需要跟著改?
問題三:
為什麼 https://... 沒有寫 :443,瀏覽器仍知道要連443?
問題四:
如果DNS失敗,直接把Server IP寫進程式可以暫時解決嗎?這樣做又會產生什麼維護問題?

十七、結語:網際網路不是一條線,而是一連串「找到正確對象」的過程

從水井三寶實作可以看見,網際網路通訊不是單純「ESP32有Wi-Fi,所以就能上雲端」。資料必須先找到正確的網路介面、正確的IP、正確的Gateway、正確的Server名稱,再經由DNS取得Server IP,最後還要找到正確的Port與應用服務。

因此,MAC、IP、DNS、Domain與Port並不是五個孤立的名詞,而是一條完整通訊鏈上的不同角色。當學生能用一筆「烏龜故事事件」解釋這五個概念,就代表他已經開始真正理解網際網路是如何運作的。

MAC IP Gateway DNS Domain Port HTTP HTTPS ESP32 Django

2026年8月15日 星期六

[水井村USR] 水井三寶智慧互動展覽系統

水井三寶智慧互動展覽系統

Shuijing Treasures Smart Interactive Exhibition System

五層架構與設計理念:從文化作品,到互動裝置、Edge Web、Digital Twin,再到文化知識平台

「水井三寶智慧互動展覽系統」的核心,不是把一件文化作品變成單純的科技展示品,而是讓科技成為文化被理解、被互動、被記錄、被延伸的橋梁。因此整個系統刻意採用五層架構,讓每一層都有清楚角色:文化作品負責承載價值,智慧互動負責創造體驗,Edge Web負責現場控制,Digital Twin / Data負責留下匿名資料,文化知識層則負責把故事、教育、工藝與USR持續累積成長。

一、五層架構總覽

第五層|文化知識層 水井三寶網站、品牌故事、繪本、影音、教育、工藝、USR、未來AI/RAG ▲ │ 第四層|Digital Twin / Data層 Django匿名事件、展覽儀表板、跨場次資料 ▲ │ 第三層|Edge Web層 ESP32內嵌網站、現場控制、設備診斷、即時匿名統計 ▲ │ 第二層|智慧互動層 ESP32+ToF+三按鈕+JQ6500+RGB ▲ │ 第一層|文化作品層 水井三寶實體作品,文化展示為主,不直接觸碰

這五層並不是五套彼此獨立的系統,而是一條「由文化出發,再回到文化」的完整鏈結。第一層是核心,第二、三、四層提供互動、控制與資料能力,第五層再將資料與內容重新轉化為教育、品牌與文化傳播的長期價值。

二、第一層|文化作品層

水井三寶實體作品,文化展示為主,不直接觸碰

第一層是整個系統最重要的一層。白馬、烏龜與姻緣花不是互動裝置的配件,而是展覽真正的主角。系統設計的第一原則,就是科技不能破壞作品本體,也不能讓參觀者因為操作而直接觸碰文化作品

因此互動按鈕、燈光、聲音、感測器與控制設備,應盡量整合在木盒、底座或展示台中。參觀者操作的是「展覽介面」,不是作品本身。這樣可以兼顧保存、安全與展示尊嚴。

設計理念:作品是核心,科技退居幕後。不是把文化變成科技,而是用科技讓文化更容易被理解。

三、第二層|智慧互動層

ESP32+ToF+三按鈕+JQ6500+RGB

第二層負責把「觀看」轉化成「參與」。ESP32是互動控制核心;三顆按鈕分別對應白馬、烏龜與姻緣花;JQ6500負責播放故事音檔;RGB燈光則提供視覺回饋;ToF感測器則用來判斷是否有人靠近展區。

這一層的價值不只是「會播聲音」或「會亮燈」,而是建立一個能避免誤動作、能確認播放狀態、能支援展覽現場大量人流的互動邏輯。尤其在人多的展場,感測器與狀態機必須避免因路人經過、連續按鍵或多人同時操作而造成混亂。

第二層可視為「實體互動引擎」,主要任務是快速、穩定、即時,而且即使沒有Internet也必須能正常工作。

四、第三層|Edge Web層

ESP32內嵌網站,現場控制、設備診斷、即時匿名統計

第三層讓ESP32不再只是藏在盒子裡的控制板,而成為一個可被手機直接管理的Edge設備。工作人員只要連上ESP32的本地AP,就可以開啟內嵌網站,不需要額外安裝App。

Edge Web可以提供故事控制、音量設定、設備狀態、JQ6500播放狀態、Wi-Fi狀態、ToF距離、Queue大小與即時匿名統計。它同時也是維護工具:當展場出現「有燈沒聲音」、「有網路但沒同步」等問題時,工作人員可以直接從手機判斷問題在哪一層。

設計理念:現場控制應該留在Edge,不應依賴雲端。即使外網中斷,192.168.4.1仍應可以進入控制頁面。

五、第四層|Digital Twin / Data層

Django接收匿名事件,建立展覽儀表板與跨場次資料

第四層將實體展覽轉化成可觀察、可比較的Digital Twin / Data系統。ESP32不記錄個人姓名、不做人臉辨識,也不追蹤個人身分,而是只記錄匿名事件,例如:故事啟動、完整播放、Web操作、實體按鈕操作、展區喚醒、裝置狀態與錯誤事件。

Django接收這些事件後,可以建立展覽儀表板,顯示白馬、烏龜、姻緣花故事的互動次數、播放完成率、互動來源與設備健康狀態。當展覽移到不同場域、不同日期或不同活動時,也可以累積成跨場次資料。

這一層的重點不是「蒐集越多資料越好」,而是只記錄對展覽改善有用、又不涉及個人識別的資料。資料的價值,在於協助回答:哪個故事最受歡迎?觀眾是否完整聽完?實體按鈕和Web控制哪一種使用較多?哪一場展覽的互動最高?

六、第五層|文化知識層

水井三寶網站承接品牌故事、繪本、影音、教育、工藝、USR,未來再加入AI/RAG

第五層不是單純的「網站資料庫」,而是整個系統最長期的文化知識層。展場中的一段語音只能講幾十秒或幾分鐘,但水井三寶的品牌故事、地方記憶、創作者、工藝技法、繪本、影音、教育課程、USR成果,都需要一個可以持續累積的知識平台。

因此,ESP32內嵌網站負責「現場」,水井三寶網站負責「延伸」。參觀者在現場聽完故事後,可以再透過QR Code進入完整網站,繼續閱讀作品故事、觀看影音、了解地方文化與USR實踐。

未來如果加入AI與RAG,這一層還可以進一步發展成「水井三寶文化知識問答」,讓參觀者用自然語言詢問白馬、烏龜、姻緣花、水井村、工藝、USR與地方故事,讓文化知識從靜態瀏覽進入互動問答。

七、為什麼一定要分成五層?

層級 主要角色 即使其他層失效,仍應保留什麼?
第一層|文化作品 文化核心與展示主體 作品仍能被正常觀看與保存
第二層|智慧互動 按鈕、聲音、燈光、感測 即使沒有Internet,仍能互動與播放
第三層|Edge Web 本地控制、診斷、即時統計 即使雲端離線,仍能從192.168.4.1管理
第四層|Digital Twin / Data 匿名事件、跨場次分析、設備健康 網路恢復後可從Queue補同步
第五層|文化知識 品牌、教育、USR、知識累積 內容可持續成長,不受單一展場限制

八、五層架構背後的四個設計原則

文化優先Local First匿名資料可擴充

第一,文化優先。所有科技都服務於作品,不讓作品變成科技展示的配角。第二,Local First。故事播放與現場控制要在本地完成,雲端不能干擾基本互動。第三,匿名資料。只記錄事件,不記錄個人身分。第四,可擴充。未來可以增加AI、RAG、更多故事、更多展場與更多教學內容,而不必推翻原本架構。

九、這不是五個功能,而是一條文化數位轉譯鏈

文化作品 ↓ 被看見 ↓ 智慧互動 ↓ 被參與 ↓ Edge Web ↓ 被控制與理解 ↓ Digital Twin / Data ↓ 被記錄與分析 ↓ 文化知識平台 ↓ 被延伸、被教學、被再創作

從這個角度來看,「水井三寶智慧互動展覽系統」的真正價值,不只是完成一套ESP32裝置,而是建立一種地方文化數位化的方法:實體作品保留原貌,科技負責提升體驗,資料負責留下足跡,網站負責延伸知識,未來AI再負責讓文化被更自然地提問與理解。

十、結語

「水井三寶智慧互動展覽系統」五層架構,讓文化、硬體、Web、資料與知識不再混在同一層。第一層守住文化,第二層創造互動,第三層保障現場,第四層累積資料,第五層延伸知識。這樣的設計也讓系統更適合真正的展覽情境:某一層暫時失效時,不會讓整個展覽一起失效。

對USR而言,這五層架構還有另一層意義:它把地方故事從「一次性的展示內容」,轉化成可以持續累積、教育、分析、再利用與再傳播的地方文化數位資產。科技因此不只是工具,而是文化保存、教育與地方連結的一座橋。

[水井村USR] ESP32 同時當基地台又能上網! 水井三寶智慧互動展覽系統 AP+STA 共存實測

ESP32 同時當基地台又能上網!
水井三寶智慧互動展覽系統 AP+STA 共存實測

ESP32 Arduino SoftAP STA Edge Web IoT USR

在「水井三寶智慧互動展覽系統」的開發過程中,有一個很重要的網路問題: ESP32 能不能一方面提供現場手機連線控制,另一方面又同時連上 Internet?

經過實際測試,答案是:可以。 本次成功讓 ESP32 的 SoftAP 與 STA 同時運作,而且 AP 端的手機控制與 STA 端的 Internet 連線可以共存。

一、為什麼水井三寶故事機需要 AP+STA?

一般 ESP32 IoT 專題常見的做法,是讓 ESP32 連接既有 Wi-Fi,也就是 STA(Station)模式。 但「水井三寶智慧互動展覽系統」的使用情境不同。

展覽現場可能有大量參觀者,而且展場 Internet 並不一定穩定。因此,我們希望故事機即使沒有 Internet, 工作人員仍然可以拿手機直接連上 ESP32,進入內嵌控制網站進行播放、診斷與設備管理。

另一方面,如果展場具有 Internet,ESP32 又應該能將匿名互動事件傳送到遠端 Django 平台, 形成長期的展覽數據。

【參觀者/工作人員手機】

│ Wi-Fi

【ESP32 SoftAP】
Shuijing-Treasures
192.168.4.1

├──── Edge Web 現場控制

└──── ESP32 同時使用 STA
    │
    ▼
【手機熱點/展場 Wi-Fi】
    │
    ▼
   Internet
    │
    ▼
【Django 雲端平台】

二、AP 與 STA 分別做什麼?

模式 用途 水井三寶應用
AP ESP32 自己建立 Wi-Fi 手機直接連入故事機
SoftAP IP ESP32 本地控制網址 192.168.4.1
STA ESP32 連接外部 Wi-Fi 取得 Internet
Internet 連接遠端伺服器 Django 匿名事件平台

三、這次實測成功

本次使用 ESP32 實際測試 AP+STA 共存。 ESP32 一方面建立自己的 Wi-Fi:

SSID:Shuijing-Treasures
Local control:http://192.168.4.1

另一方面,再透過 STA 連接名為 ChiYuan 的外部 Wi-Fi。 實際取得:

STA CONNECTED
STA SSID : ChiYuan
STA IP   : 192.168.1.119
RSSI     : -21 dBm

==============================
SYSTEM READY
==============================

Local control: http://192.168.4.1
Internet: YES

▲ 圖1 ESP32 AP+STA 共存實測。 ESP32 本地 AP 維持 192.168.4.1,同時透過 STA 取得 192.168.1.119,Internet 狀態為 YES。
這張測試畫面證明了一件很重要的事:

ESP32 不需要在「本地控制」與「Internet」之間二選一。
它可以同時提供 192.168.4.1 Edge Web, 又透過另一個 Wi-Fi 介面連上 Internet。

四、連線過程

程式的核心概念其實很簡單:先建立 SoftAP,再使用 WiFi.begin() 建立 STA 連線。

STEP 1|ESP32 建立 SoftAP

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

    Serial.println("SoftAP FAILED");

    while (1) {
        delay(1000);
    }
}

Serial.println("SoftAP OK");

Serial.print("AP IP : ");
Serial.println(WiFi.softAPIP());

成功後,ESP32 本身就是一個 Wi-Fi 基地台。 手機可以搜尋到:

Shuijing-Treasures

連線後即可進入:

http://192.168.4.1

STEP 2|ESP32 再連接 Internet Wi-Fi

WiFi.begin(STA_SSID, STA_PASSWORD);

這時 ESP32 又以 STA 身分連接外部 AP。 程式最多等待 15 秒:

unsigned long start = millis();

while (
    WiFi.status() != WL_CONNECTED &&
    millis() - start < 15000
) {

    Serial.print(".");
    delay(500);
}

STEP 3|確認兩個 IP 同時存在

這是理解 AP+STA 最重要的地方。 ESP32 此時具有兩個不同用途的 IP。

介面 本次實測 IP 功能
SoftAP 192.168.4.1 手機連 ESP32
STA 192.168.1.119 ESP32 連 Internet

五、還能知道有幾台手機連進 ESP32

程式使用:

WiFi.softAPgetStationNum()

取得目前連接 ESP32 AP 的裝置數。 本次測試畫面持續出現:

AP Clients = 1

代表測試當下確實有一台裝置連著 Shuijing-Treasures

六、RSSI 也能成為展覽維護資訊

程式每五秒輸出 STA 狀態:

AP Clients = 1 | STA = ONLINE | IP = 192.168.1.119 | RSSI = -28
AP Clients = 1 | STA = ONLINE | IP = 192.168.1.119 | RSSI = -21
AP Clients = 1 | STA = ONLINE | IP = 192.168.1.119 | RSSI = -29

這些資訊未來可以直接整合進水井三寶 Edge Web 的「設備診斷」頁面, 讓工作人員不必接電腦,就可以從手機知道:

  • AP 是否正常
  • 目前有多少裝置連線
  • STA 是否 ONLINE
  • STA IP
  • Wi-Fi RSSI
  • Internet 是否可用

七、完整 Arduino 測試程式

以下是本次 AP+STA 共存實際測試使用的 Arduino 程式。 正式使用時只需要修改 STA 的 Wi-Fi 名稱與密碼。

#include <Arduino.h>
#include <WiFi.h>
#include <NetworkClient.h>
#include <WiFiAP.h>

// ===============================
// ESP32 AP + Internet STA TEST
// ===============================

// ESP32 本地 AP
const char *AP_SSID = "Shuijing-Treasures";
const char *AP_PASSWORD = "12345678";

// 可上網的 Wi-Fi
// 先用手機熱點測試最容易排除展場網路問題
const char *STA_SSID = "您的手機熱點名稱";
const char *STA_PASSWORD = "您的手機熱點密碼";

NetworkServer server(80);

void setup() {

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

  Serial.println();
  Serial.println("==============================");
  Serial.println("SHUIJING AP + STA TEST");
  Serial.println("==============================");

  // --------------------------------
  // STEP 1
  // 建立 SoftAP
  // --------------------------------

  Serial.println();
  Serial.println("[1] Starting SoftAP...");

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

    Serial.println("SoftAP FAILED");

    while (1) {
      delay(1000);
    }
  }

  Serial.println("SoftAP OK");

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

  Serial.print("AP IP   : ");
  Serial.println(WiFi.softAPIP());


  // --------------------------------
  // STEP 2
  // 建立 STA Internet
  // --------------------------------

  Serial.println();
  Serial.println("[2] Starting STA...");

  WiFi.begin(STA_SSID, STA_PASSWORD);

  unsigned long start = millis();

  while (
    WiFi.status() != WL_CONNECTED &&
    millis() - start < 15000
  ) {

    Serial.print(".");
    delay(500);
  }

  Serial.println();


  // --------------------------------
  // STEP 3
  // 檢查 STA
  // --------------------------------

  if (WiFi.status() == WL_CONNECTED) {

    Serial.println("STA CONNECTED");

    Serial.print("STA SSID : ");
    Serial.println(WiFi.SSID());

    Serial.print("STA IP   : ");
    Serial.println(WiFi.localIP());

    Serial.print("RSSI     : ");
    Serial.print(WiFi.RSSI());
    Serial.println(" dBm");

  } else {

    Serial.println("STA NOT CONNECTED");
    Serial.println("Local AP should still work.");
  }


  // --------------------------------
  // STEP 4
  // 啟動本地 Web Server
  // --------------------------------

  server.begin();

  Serial.println();
  Serial.println("==============================");
  Serial.println("SYSTEM READY");
  Serial.println("==============================");

  Serial.print("Local control: http://");
  Serial.println(WiFi.softAPIP());

  if (WiFi.status() == WL_CONNECTED) {

    Serial.println("Internet: YES");

  } else {

    Serial.println("Internet: NO");
  }
}


void loop() {

  // ===============================
  // 本地 AP Web Server
  // ===============================

  NetworkClient client = server.accept();

  if (client) {

    String currentLine = "";

    while (client.connected()) {

      if (client.available()) {

        char c = client.read();

        if (c == '\n') {

          if (currentLine.length() == 0) {

            client.println("HTTP/1.1 200 OK");
            client.println(
              "Content-Type: text/html; charset=utf-8"
            );
            client.println("Connection: close");
            client.println();

            client.println(
              "<!DOCTYPE html>"
              "<html>"
              "<head>"
              "<meta charset='UTF-8'>"
              "<meta name='viewport' "
              "content='width=device-width,initial-scale=1'>"
              "</head>"
              "<body>"
            );

            client.println(
              "<h1>水井三寶 AP + STA 測試</h1>"
            );

            client.print("<p>AP IP:");
            client.print(WiFi.softAPIP());
            client.println("</p>");

            client.print("<p>STA:");

            if (WiFi.status() == WL_CONNECTED) {

              client.println("CONNECTED</p>");

              client.print("<p>Internet IP:");
              client.print(WiFi.localIP());
              client.println("</p>");

              client.print("<p>RSSI:");
              client.print(WiFi.RSSI());
              client.println(" dBm</p>");

            } else {

              client.println("OFFLINE</p>");
            }

            client.println("</body></html>");

            break;

          } else {

            currentLine = "";
          }

        } else if (c != '\r') {

          currentLine += c;
        }
      }
    }

    client.stop();
  }


  // ===============================
  // STA 斷線診斷
  // ===============================

  static unsigned long lastPrint = 0;

  if (millis() - lastPrint > 5000) {

    lastPrint = millis();

    Serial.print("AP Clients = ");
    Serial.print(WiFi.softAPgetStationNum());

    Serial.print(" | STA = ");

    if (WiFi.status() == WL_CONNECTED) {

      Serial.print("ONLINE");

      Serial.print(" | IP = ");
      Serial.print(WiFi.localIP());

      Serial.print(" | RSSI = ");
      Serial.println(WiFi.RSSI());

    } else {

      Serial.println("OFFLINE");
    }
  }
}

八、為什麼這次測試對「水井三寶」很重要?

這不只是一個 ESP32 Wi-Fi 技術測試,而是決定「水井三寶智慧互動展覽系統」 能不能真正進入展覽現場的一個重要基礎。

第二層|智慧互動層
ESP32+ToF+三按鈕+JQ6500+RGB, 負責故事播放與現場互動。
第三層|Edge Web 層
ESP32 提供 192.168.4.1, 即使 Internet 中斷仍可現場控制。
第四層|Django Data 層
ESP32 透過 STA 與 Internet, 將匿名互動事件送往遠端平台。
內容層|水井三寶網站
白馬、烏龜、姻緣花、三寶故事與創作者資訊, 提供更完整的數位延伸閱讀。

九、最重要的展覽設計原則:Internet 不是故事機的開關

展覽系統不應該因為 Internet 斷線,就讓作品停止說故事。

因此,水井三寶智慧互動展覽系統採取的是 Edge First 的思考方式。

ToF、三個實體按鈕、JQ6500 故事播放、RGB 燈光與 192.168.4.1 本地控制都應該在 ESP32 本機完成。

STA 與 Internet 則是「加值能力」,主要負責匿名事件同步與遠端資料分析。

未來即使展場 Wi-Fi 暫時中斷:

JQ6500        → 繼續播放
三按鈕        → 繼續操作
ToF           → 繼續偵測
RGB           → 繼續互動
192.168.4.1   → 繼續控制

Internet      → 暫時離線
Django Event  → 暫存,恢復後補傳

十、下一步:從「會上網」進入「真正的四層系統」

本次測試已經完成一個重要的技術驗證: ESP32 SoftAP 與 Internet STA 可以穩定共存。

下一階段就可以把這項成果正式整合進「水井三寶智慧互動展覽系統」:

水井三寶作品
      │
      ▼
ESP32 智慧互動層
ToF+Button+JQ6500+WS2812
      │
      ▼
Edge Web
192.168.4.1
      │
      ├──────── 現場手機控制
      │
      ▼
STA / Internet
      │
      ▼
Django
匿名事件資料庫
      │
      ▼
展覽 Dashboard

當地方工藝遇上 ESP32、Edge Web 與 Django, 科技不再只是放在作品旁邊的設備, 而是開始成為地方故事與參觀者之間的互動媒介。

白馬行土地、烏龜守清水、姻緣花牽人情;
三寶同行,水井共生。