顯示具有 Raspberry Pi Pico W 標籤的文章。 顯示所有文章
顯示具有 Raspberry Pi Pico W 標籤的文章。 顯示所有文章

2026年9月12日 星期六

樹藝AI故事機 V1.3:硬體設計與接線的重要觀念

Raspberry Pi Pico WJQ6500HC‑SR04硬體實作

樹藝AI故事機 V1.3:硬體設計與接線的重要觀念

一台可在展場主動歡迎參觀者、播放作品故事,並讓工作人員以手機網頁維護的故事機,並不是把模組接上就完成。真正決定系統是否穩定的,是電壓、共地、訊號方向與逐步測試這些看似基本、卻最容易被忽略的硬體觀念。

一、系統組成與角色分工

元件工作角色
Raspberry Pi Pico W系統控制核心:讀取超音波與按鈕、控制 LED、透過 UART 指揮 JQ6500,也提供 Wi‑Fi 網頁控制。
JQ6500 MP3 模組從 microSD 卡讀取 001~006.mp3,驅動喇叭播放系統訊息、歡迎詞與內容。
HC‑SR04量測參觀者與作品的距離,觸發歡迎詞與離開致謝詞。
三個按鈕與四顆 LED提供參觀者內容選擇與現場可見狀態。

二、完整接腳圖



圖 1.Pico W、JQ6500、HC‑SR04、按鈕與 LED 的接線總覽。

圖中的 Pin 是 Pico W 排針的實體腳位;GP 則是程式中使用的 GPIO 名稱,例如 Pin(14) 代表 GP14,而不是實體 Pin 14。

三、第一原則:所有 GND 必須共地

Pico W、JQ6500、HC‑SR04、按鈕與 LED 必須共用同一個 GND。訊號電壓永遠是相對於 GND 而存在;若模組沒有共地,Pico W 讀到的 0 或 1 可能只是漂浮訊號,造成按鈕亂觸發、BUSY 不穩或超音波距離異常。

實作口訣:先接電源與 GND,再接訊號線;每加入一個模組,都確認它的 GND 已接回 Pico W。

四、5V 模組與 3.3V GPIO:ECHO 一定要分壓

HC‑SR04 以 Pico W 的 VBUS 5V 供電,但 ECHO 腳會輸出約 5V。Pico W GPIO 只能接受 3.3V 邏輯,不能將 ECHO 直接接到 GP15。

HC‑SR04 ECHO → 1 kΩ → GP15
GP15 → 2 kΩ → GND

分壓後的電壓約為 5V × 2kΩ ÷ (1kΩ + 2kΩ) = 3.33V,符合 Pico W GPIO 的安全範圍。

不可省略:把 HC‑SR04 的 ECHO 直接接 Pico W,可能損壞 GPIO。TRIG 是 Pico W 輸出訊號,可直接接到 HC‑SR04 TRIG。

五、UART 有方向:Pico W 指揮 JQ6500

連線用途程式對應
GP0(實體 Pin 1)→ JQ6500 RX送出播放命令UART(0, tx=Pin(0), baudrate=9600)
JQ6500 BUSY → GP20(實體 Pin 26)回報播放中/未播放busy = Pin(20, Pin.IN)
JQ6500 VCC ← Pico VBUS5V 供電硬體接線
SPK+/SPK− → 喇叭音訊輸出不可接到 GND

本作品實測 BUSY 在播放時約 2.4V、未播放時約 0V。因此它可接 Pico W 輸入腳,但不設定 PULL_UP 或 PULL_DOWN;程式再以短暫穩定時間過濾偶發跳動。

六、按鈕採 PULL_UP,LED 必須限流

三個按鈕分別接 GP10、GP11、GP12,另一端都接 GND。程式設定 Pin.IN, Pin.PULL_UP:未按下讀值為 1;按下接地後讀值為 0。V1.3 加入「按鈕放開鎖定」,按鍵必須先回到 1 才可再次播放,避免長按或接點不穩造成重播。

四顆 LED 分別由 GP16~GP19 控制,每顆均要串聯 220~330 Ω 電阻:

Pico GPIO → 220~330 Ω 電阻 → LED 正極(長腳)
LED 負極(短腳)→ GND
GPIO顏色意義
GP16綠待機
GP17黃開機、歡迎、致謝等系統語音
GP18藍作品內容播放
GP19紅超音波感測器連續量測失敗

紅燈是獨立診斷狀態,不應因為其他燈正在亮就被覆蓋;這是 V1.2 實測後修正的 LED 設計原則。

七、建議的硬體測試順序

  1. 只接 Pico W:確認 Thonny 能執行程式與儲存 main.py。
  2. 逐顆測 LED:確認開機依序亮綠、黃、藍、紅。
  3. 測 HC‑SR04:先列印距離,確認手靠近/移開數值合理,再加入濾波與離場判定。
  4. 測 JQ6500:確認 microSD 音檔連續編號 001~006,先播 004 開機訊息,再測各內容音檔。
  5. 測 BUSY:列印原始與穩定讀值,確認播放中為 1、未播放為 0。
  6. 接按鈕:逐一確認 GP10、GP11、GP12 按下只播放一次。
  7. 最後測 Wi‑Fi:手機連上 TreeAIStoryBot,開啟 http://192.168.4.1。

八、接線完成前檢查表

  • 所有模組 GND 已共地。
  • HC‑SR04 ECHO 已經過 1kΩ/2kΩ 分壓。
  • JQ6500 喇叭接在 SPK+、SPK−,沒有接到 GND。
  • 四顆 LED 都各自串聯 220~330Ω 電阻。
  • 按鈕另一端都接 GND,程式使用 PULL_UP。
  • BUSY 接 GP20,且輸入腳未設 PULL_UP 或 PULL_DOWN。
  • microSD 音檔使用連續編號 001.mp3~006.mp3。
結語:樹藝AI故事機的互動看似由語音與按鈕完成,但背後依賴安全電壓、正確訊號方向、共同接地,以及「每接一項就測一項」的工程習慣。這些基礎做好後,故事機才能在展場穩定地把樹藝工坊的作品、材料與創作故事說給每一位參觀者聽。

2026年9月6日 星期日

從「燈號優先序」到「訊號並列」:樹藝AI故事機 V1.2 的人機協作測試紀錄

從「燈號優先序」到「訊號並列」:樹藝AI故事機 V1.2 的人機協作測試紀錄

Raspberry Pi Pico W × JQ6500 BUSY × HC-SR04 × MicroPython

樹藝AI故事機的目標,是讓自然素材作品能主動迎接參觀者,並以語音介紹作品故事、使用材料與設計作者。這次開發最值得記錄的,並不是「AI 一次產生了正確程式」,而是透過實體接線、量測與反覆測試,修正了 AI 最初對燈號邏輯的假設,完成可穩定運作的 V1.2。

本次核心收穫:程式設計不能只看「看起來合理」的控制邏輯。當硬體中的訊號屬於不同層次時,不能強迫它們排成單一優先序;必須由人透過實測,辨識哪些狀態應該互斥、哪些訊號應該並列呈現。

一、樹藝AI故事機的功能

互動情境系統行為音檔
作品通電開機播放系統開機訊息004.mp3
參觀者靠近播放歡迎詞,引導選擇故事、材料或作者005.mp3
按下作品故事按鈕播放貓頭鷹博士的作品故事001.mp3
按下設計作者按鈕播放樹藝工坊李永謨工藝師介紹002.mp3
按下使用材料按鈕播放自然素材與創作材料說明003.mp3
參觀者真正離開連續五秒未偵測到近距離物體後,播放致謝詞006.mp3

二、從「推測」到「實測」:BUSY 訊號帶來的轉折

JQ6500 模組可由 Pico W 的 UART 送出播放命令,但如果沒有播放狀態回授,程式只能以「預估秒數」判斷音檔是否播完。為了讓燈號真正反映播放情況,本次將 JQ6500 的 BUSY 腳位接到 Pico W 的 GP20。

實測項目量測結果程式判斷
JQ6500 播放中BUSY 約 2.4V1,代表正在播放
JQ6500 未播放BUSY 約 0V0,代表未播放
BUSY 原始讀值偶爾在 0、1 間跳動需連續穩定 300 ms 才更新狀態

因此程式加入兩道保護:

  1. 播放啟動保護時間:送出播放命令後,等待 500 ms,避免 BUSY 尚未切換就被誤判為播放結束。
  2. BUSY 去彈跳:原始讀值必須連續穩定 300 ms,程式才接受新的播放狀態。

三、AI 初版邏輯的問題:所有燈號被迫只能選一個

AI 初版建議以「紅燈異常 > 黃燈系統語音 > 藍燈作品內容 > 綠燈待機」的優先序處理燈號。這種寫法適合每一種狀態都完全互斥的系統,但在實際硬體中,紅燈代表的是感測器診斷訊號,黃燈與藍燈代表的是語音互動狀態;兩者並不屬於同一個維度。

初版:單一優先序

程式一旦判定紅燈優先,就可能關閉黃燈、藍燈或綠燈。這雖然讓「某一刻只有一顆燈亮」,但會把不同來源的資訊混在一起,現場不容易理解真正狀態。

實測修正:訊號並列

紅燈獨立表示感測器異常;播放狀態再決定黃燈、藍燈或綠燈。如此可同時看見「系統正在播音」與「感測器出現異常」,不會互相遮蔽。

修正後的核心概念是:

# 紅燈:獨立的診斷訊號
if sensor_error:
    led_error.value(1)
else:
    led_error.value(0)

# 黃、藍、綠燈:依播放狀態顯示
if is_audio_playing():
    ...
else:
    ...
人機協作的關鍵:AI 可以快速提出架構、程式與可能的修正方向;但「紅燈是否應與其他燈號並列」並不是純粹語法問題,而是現場展示者對使用情境、可讀性與硬體行為的判斷。這個判斷必須回到實際作品前完成。

四、實測流程:從單元測試走向完整互動

測試階段做法得到的結果
JQ6500 語音測試依序播放 001 至 006 音檔。確認 microSD 必須連續命名,避免缺少音檔造成指定播放異常。
BUSY 訊號量測播放與停止時,以電表觀察 BUSY 電壓。確認播放為 2.4V、停止為 0V,建立播放狀態判斷基礎。
超音波測試觀察原始值與七筆中位數濾波後距離。可排除偶發的遠方反射值,減少重複迎賓與離場誤判。
近距離桌上測試設定靠近 10 cm、離開 25 cm。可在桌上快速重複觸發,不必每次走到正式展示距離。
LED 整合測試觀察綠、黃、藍、紅燈與 BUSY 的對應。發現單一優先序容易誤判,改為診斷燈與互動燈並列處理。

五、V1.2 燈號定義

LED接腳意義
綠燈GP16待機中,可等待參觀者靠近或選擇內容。
黃燈GP17播放開機訊息、歡迎詞或謝謝參觀。
藍燈GP18播放作品故事、使用材料或設計作者。
紅燈GP19超音波連續五次量測失敗的診斷提示,可與播放狀態並列。

六、測試成功的 V1.2 程式

以下程式使用 Pico W 的 GP20 讀取 JQ6500 BUSY,並加入 BUSY 穩定判斷、超音波中位數濾波、近距離測試模式,以及修正後的 LED 並列邏輯。

from machine import Pin, UART, time_pulse_us
import time

jq6500 = UART(0, baudrate=9600, tx=Pin(0))
busy = Pin(20, Pin.IN)

trig = Pin(14, Pin.OUT)
echo = Pin(15, Pin.IN)

button_story = Pin(10, Pin.IN, Pin.PULL_UP)
button_material = Pin(11, Pin.IN, Pin.PULL_UP)
button_author = Pin(12, Pin.IN, Pin.PULL_UP)

led_ready = Pin(16, Pin.OUT)
led_welcome = Pin(17, Pin.OUT)
led_playing = Pin(18, Pin.OUT)
led_error = Pin(19, Pin.OUT)

FILE_STORY = 1
FILE_AUTHOR = 2
FILE_MATERIAL = 3
FILE_BOOT = 4
FILE_WELCOME = 5
FILE_GOODBYE = 6

TEST_MODE = True

if TEST_MODE:
    ENTER_DISTANCE = 10
    LEAVE_DISTANCE = 25
else:
    ENTER_DISTANCE = 80
    LEAVE_DISTANCE = 120

LEAVE_CONFIRM_MS = 5000
BUTTON_DEBOUNCE_MS = 500
PLAY_START_LOCK_MS = 500
BUSY_DEBOUNCE_MS = 300
FILTER_SIZE = 7
DEBUG = False

distance_buffer = []
person_is_here = False
far_since = None
last_button_time = 0

current_audio_type = "idle"
audio_start_lock_until = 0

busy_raw = busy.value()
busy_stable = busy_raw
busy_changed_at = time.ticks_ms()

sensor_fail_count = 0
sensor_error = False


def time_is_active(end_time):
    return time.ticks_diff(time.ticks_ms(), end_time) < 0


def update_busy_state():
    global busy_raw, busy_stable, busy_changed_at

    now = time.ticks_ms()
    reading = busy.value()

    if reading != busy_raw:
        busy_raw = reading
        busy_changed_at = now

    if (
        busy_raw != busy_stable
        and time.ticks_diff(now, busy_changed_at) >= BUSY_DEBOUNCE_MS
    ):
        busy_stable = busy_raw

    return busy_stable


def is_audio_playing():
    if time_is_active(audio_start_lock_until):
        return True

    return update_busy_state() == 1


def is_managed_audio_playing():
    if current_audio_type == "idle":
        return False

    return is_audio_playing()


def update_leds():
    global current_audio_type

    # 紅燈是獨立診斷訊號,不以優先序排除其他燈號
    if sensor_error:
        led_error.value(1)
    else:
        led_error.value(0)

    # 黃、藍、綠燈表示目前互動狀態
    if is_audio_playing():
        led_ready.value(0)

        if current_audio_type in (
            "boot",
            "welcome",
            "goodbye"
        ):
            led_welcome.value(1)
        else:
            led_playing.value(1)

    else:
        led_playing.value(0)
        led_welcome.value(0)
        led_ready.value(1)


def play_file(number, audio_type):
    global current_audio_type, audio_start_lock_until

    command = bytes([0x7E, 0x04, 0x03, 0x00, number, 0xEF])
    jq6500.write(command)

    current_audio_type = audio_type
    audio_start_lock_until = time.ticks_add(
        time.ticks_ms(),
        PLAY_START_LOCK_MS
    )

    print("播放音檔:", number)


def get_distance():
    trig.value(0)
    time.sleep_us(2)

    trig.value(1)
    time.sleep_us(10)
    trig.value(0)

    try:
        duration = time_pulse_us(echo, 1, 30000)

        if duration < 0:
            return 999

        return duration * 0.0343 / 2

    except OSError:
        return 999


def get_filtered_distance():
    global sensor_fail_count, sensor_error

    raw_distance = get_distance()

    if raw_distance >= 999:
        sensor_fail_count += 1
    else:
        sensor_fail_count = 0

    sensor_error = sensor_fail_count >= 5

    distance_buffer.append(raw_distance)

    if len(distance_buffer) > FILTER_SIZE:
        distance_buffer.pop(0)

    sorted_distances = sorted(distance_buffer)
    filtered_distance = sorted_distances[
        len(sorted_distances) // 2
    ]

    return raw_distance, filtered_distance


def check_buttons():
    global last_button_time

    now = time.ticks_ms()

    if is_managed_audio_playing():
        return

    if time.ticks_diff(now, last_button_time) < BUTTON_DEBOUNCE_MS:
        return

    if button_story.value() == 0:
        print("選擇:作品故事")
        play_file(FILE_STORY, "content")
        last_button_time = now

    elif button_material.value() == 0:
        print("選擇:使用材料")
        play_file(FILE_MATERIAL, "content")
        last_button_time = now

    elif button_author.value() == 0:
        print("選擇:設計作者")
        play_file(FILE_AUTHOR, "content")
        last_button_time = now


print("樹藝AI故事機 V1.2 啟動中……")
play_file(FILE_BOOT, "boot")

while is_managed_audio_playing():
    update_leds()
    time.sleep_ms(100)

update_leds()
print("系統已就緒,等待參觀者靠近……")


while True:
    raw_distance, distance = get_filtered_distance()
    now = time.ticks_ms()

    update_leds()

    if DEBUG:
        print(
            "原始:", raw_distance,
            "cm;濾波後:", distance,
            "cm;BUSY原始:", busy.value(),
            ";BUSY穩定:", busy_stable
        )

    if sensor_error:
        time.sleep_ms(150)
        continue

    if (
        distance < ENTER_DISTANCE
        and not person_is_here
        and not is_managed_audio_playing()
    ):
        print("參觀者靠近:", distance, "cm")
        person_is_here = True
        far_since = None
        play_file(FILE_WELCOME, "welcome")

    if person_is_here:
        if not is_managed_audio_playing():
            check_buttons()

            if distance < LEAVE_DISTANCE:
                far_since = None

            else:
                if far_since is None:
                    far_since = now

                elif time.ticks_diff(
                    now,
                    far_since
                ) >= LEAVE_CONFIRM_MS:
                    print("參觀者已離開:", distance, "cm")
                    person_is_here = False
                    far_since = None
                    play_file(FILE_GOODBYE, "goodbye")

    time.sleep_ms(150)

七、可複製的協作方法

  1. 先讓 AI 建立初版:快速完成接線表、程式架構、可能的除錯方向。
  2. 分段測試硬體:先測音檔、再測按鈕、再測超音波,最後才整合燈號。
  3. 量測而不猜測:以電表確認 BUSY 在播放時為 2.4V、停止時為 0V,再寫程式判斷。
  4. 記錄異常現象:例如 BUSY 讀值跳動、綠燈閃爍、超音波量到背景反射。
  5. 修正模型,而非只修補結果:這次不是單純改亮燈順序,而是重新定義「診斷訊號」與「互動狀態」應採並列處理。
  6. 以可運作版本收斂:將測試成功的程式、音檔編號、接線與測試設定完整保存。
結語:樹藝AI故事機讓自然素材作品可以主動說故事;而這次 V1.2 的測試也說明,AI 的價值不在於取代人的判斷,而在於加速提出可測試的方案。真正讓作品變得可靠的,是 AI 的快速生成、人的現場觀察,以及一次次回到硬體與使用情境的驗證。

樹藝AI故事機:讓自然素材作品自己說故事

樹藝AI故事機:讓自然素材作品自己說故事

Raspberry Pi Pico W × JQ6500 × HC-SR04 × MicroPython × Thonny

樹藝作品不只是靜態展示品。透過 Raspberry Pi Pico W、JQ6500 語音播放模組、超音波感測器與三個按鈕,我們可以製作一臺會主動迎賓、能讓參觀者自行選擇導覽內容、並在離場時說謝謝的「樹藝AI故事機」。

完成後的互動:作品開機會播放系統訊息;參觀者靠近時播放歡迎詞;按下按鈕可聆聽作品故事、使用材料或設計作者;真正離開後,故事機會播放「謝謝參觀」。

一、樹藝AI故事機的互動流程

1開機
播放系統訊息
2靠近
自動迎賓
3選擇
按下導覽按鈕
4聆聽
播放介紹內容
5離開
播放謝謝參觀

二、需要準備的硬體

元件用途說明
Raspberry Pi Pico W故事機控制核心執行 MicroPython 程式,讀取感測器與按鈕,發送播放命令。
JQ6500 語音播放模組播放 MP3插入 microSD 卡,連接喇叭後播放故事、材料與作者介紹。
HC-SR04 超音波感測器偵測參觀者靠近或離開量測距離,作為迎賓與致謝的觸發條件。
按鈕 × 3選擇導覽內容建議加上大字標籤:作品故事、使用材料、設計作者。
喇叭與 microSD 卡輸出語音與保存音檔建議使用 4Ω、3W 左右的小喇叭;microSD 卡格式化為 FAT32。
1kΩ 與 2kΩ 電阻保護 Pico W將 HC-SR04 的 ECHO 5V 訊號降壓為約 3.3V。

三、Pico W、JQ6500、超音波與按鈕接線

1. JQ6500 語音模組

Pico WJQ6500用途
VBUS 5V(實體 Pin 40)VCCJQ6500 供電
GND(實體 Pin 38 或任一 GND)GND共地,必接
GP0/UART0 TX(實體 Pin 1)RXPico W 傳送播放指令
不接TX第一版不讀取 JQ6500 狀態,避免 5V 訊號直接進入 Pico W。
SPK+、SPK-喇叭正、負極喇叭兩端直接接 JQ6500,不可接到 GND。

2. HC-SR04 超音波感測器

Pico WHC-SR04用途
VBUS 5VVCC感測器供電
GNDGND共地
GP14(實體 Pin 19)TRIG發出超音波觸發訊號
GP15(實體 Pin 20)ECHO(經分壓)接收距離回傳訊號
重要安全提醒:HC-SR04 的 ECHO 是約 5V 輸出,不能直接接 Pico W 的 GP15。請用 1kΩ 與 2kΩ 電阻做分壓,保護 Pico W 的 3.3V GPIO。
HC-SR04 ECHO ── 1kΩ ──┬── Pico W GP15 │ 2kΩ │ GND

3. 三個導覽按鈕

功能Pico W GPIO播放音檔
作品故事GP10001.mp3
使用材料GP11003.mp3
設計作者GP12002.mp3

每一個按鈕的一端接對應 GPIO,另一端接 GND。程式使用 Pico W 的內建上拉電阻,因此不需再外接上拉電阻。

四、下載與使用 Thonny 開發 MicroPython

Thonny 是適合初學者的 Python 開發工具,可直接把程式寫入 Pico W,也能在 Shell 視窗查看感測器距離與除錯訊息。

  1. 到 Thonny 官方網站下載並安裝 Windows 版本。
  2. 第一次使用 Pico W 時,到 Raspberry Pi MicroPython 官方文件下載 Pico W 專用的 UF2 韌體。
  3. 按住 Pico W 的 BOOTSEL 按鈕,再插入 USB 資料線;電腦會出現 RPI-RP2 磁碟。
  4. 將下載的 UF2 檔拖曳到 RPI-RP2,Pico W 會自動重新啟動。
  5. 開啟 Thonny,從右下角選擇「MicroPython (Raspberry Pi Pico)」,並選擇 Pico W 對應的 COM Port。
  6. 貼上本文程式,按 Ctrl + S,選擇儲存到「Raspberry Pi Pico」,檔名務必為 main.py。
為什麼要存成 main.py?因為 Pico W 每次通電開機時,會自動執行內部儲存空間中的 main.py,故事機就不必再接著電腦才能運作。

五、完整 MicroPython 程式

本程式加入「七筆距離中位數濾波」與「連續五秒離場確認」,可減少 HC-SR04 因衣物、角度或背景反射造成的重複迎賓與致謝。

from machine import Pin, UART, time_pulse_us
import time

# 樹藝AI故事機:Pico W + JQ6500 + HC-SR04 + 三個按鈕

# JQ6500:Pico W GP0 接 JQ6500 RX
# 第一版只傳送播放命令,JQ6500 TX 可不接
jq6500 = UART(0, baudrate=9600, tx=Pin(0))

# HC-SR04 超音波感測器
trig = Pin(14, Pin.OUT)
echo = Pin(15, Pin.IN)

# 三個按鈕:另一端皆接 GND
button_story = Pin(10, Pin.IN, Pin.PULL_UP)      # 作品故事
button_material = Pin(11, Pin.IN, Pin.PULL_UP)   # 使用材料
button_author = Pin(12, Pin.IN, Pin.PULL_UP)     # 設計作者

# 距離與時間設定
ENTER_DISTANCE = 80        # 小於 80 cm:有人靠近
LEAVE_DISTANCE = 120       # 大於 120 cm:可能離開
WELCOME_WAIT_MS = 6000     # 歡迎詞播放後,等待 6 秒再開放按鈕
LEAVE_CONFIRM_MS = 5000    # 連續 5 秒在離場距離外,才判定離開
BUTTON_DEBOUNCE_MS = 500   # 按鍵防彈跳時間

# 最近 7 筆距離使用中位數濾波,降低錯誤反射的影響
FILTER_SIZE = 7
distance_buffer = []

# 系統狀態變數
person_is_here = False
last_button_time = 0
button_ready_time = 0
far_since = None


def play_file(number):
    """播放 JQ6500 microSD 卡內指定編號的 MP3。"""
    command = bytes([0x7E, 0x04, 0x03, 0x00, number, 0xEF])
    jq6500.write(command)
    print("播放音檔:", number)


def get_distance():
    """讀取 HC-SR04 距離;量測失敗時回傳 999。"""
    trig.value(0)
    time.sleep_us(2)

    trig.value(1)
    time.sleep_us(10)
    trig.value(0)

    try:
        duration = time_pulse_us(echo, 1, 30000)

        if duration < 0:
            return 999

        return duration * 0.0343 / 2

    except OSError:
        return 999


def get_filtered_distance():
    """保留最近 7 筆讀值,取中位數作為穩定距離。"""
    raw_distance = get_distance()
    distance_buffer.append(raw_distance)

    if len(distance_buffer) > FILTER_SIZE:
        distance_buffer.pop(0)

    sorted_distances = sorted(distance_buffer)
    filtered_distance = sorted_distances[len(sorted_distances) // 2]

    return raw_distance, filtered_distance


def check_buttons():
    """參觀者在場時,按鈕可選擇播放內容。"""
    global last_button_time

    now = time.ticks_ms()

    if time.ticks_diff(now, last_button_time) < BUTTON_DEBOUNCE_MS:
        return

    if button_story.value() == 0:
        print("選擇:作品故事")
        play_file(1)       # 001.mp3
        last_button_time = now

    elif button_material.value() == 0:
        print("選擇:使用材料")
        play_file(3)       # 003.mp3
        last_button_time = now

    elif button_author.value() == 0:
        print("選擇:設計作者")
        play_file(2)       # 002.mp3
        last_button_time = now


# 開機程序:004.mp3
print("樹藝AI故事機啟動中……")
time.sleep(2)
play_file(4)
print("系統已就緒,等待參觀者靠近……")


while True:
    raw_distance, distance = get_filtered_distance()
    now = time.ticks_ms()

    # 開發測試時可取消下一行註解,觀察距離變化
    # print("原始:", raw_distance, "cm;濾波後:", distance, "cm")

    # 第一次偵測到有人靠近:播放 005.mp3 歡迎詞
    if distance < ENTER_DISTANCE and not person_is_here:
        print("參觀者靠近:", distance, "cm")
        play_file(5)

        person_is_here = True
        far_since = None
        button_ready_time = time.ticks_add(now, WELCOME_WAIT_MS)

    if person_is_here:
        # 歡迎詞播放一段時間後,開放三個按鈕
        if time.ticks_diff(now, button_ready_time) >= 0:
            check_buttons()

        # 在範圍內:取消離場判斷
        if distance < LEAVE_DISTANCE:
            far_since = None

        # 在範圍外:開始連續離場確認
        else:
            if far_since is None:
                far_since = now

            elif time.ticks_diff(now, far_since) >= LEAVE_CONFIRM_MS:
                print("參觀者已離開:", distance, "cm")
                play_file(6)       # 006.mp3:謝謝參觀

                person_is_here = False
                far_since = None
                time.sleep(1)

    time.sleep_ms(150)

六、microSD 音檔與語音文字稿

音檔功能觸發方式
001.mp3作品故事按下 GP10 按鈕
002.mp3設計作者按下 GP12 按鈕
003.mp3使用材料按下 GP11 按鈕
004.mp3系統開機訊息故事機通電啟動
005.mp3歡迎參觀樹藝工坊參觀者靠近
006.mp3謝謝參觀參觀者連續離開感測範圍五秒後

001.mp3作品故事|《貓頭鷹博士的月光工坊》

很久以前,森林邊有一位愛思考的貓頭鷹博士。他白天站在老樹上,仔細觀察落葉、果實和被風吹落的小樹枝;夜晚,便飛回月光照亮的小工坊,把自然送來的禮物做成有生命的作品。

有一天,森林裡的小動物問博士:「這些不起眼的木頭,真的能變成好看的東西嗎?」博士笑著說:「每一片木頭,都記得陽光、雨水和風吹過的故事。只要用心傾聽,就能找到它的新模樣。」

於是,博士選了一段圓圓的樹枝當身體,用深色木片做成翅膀,再加上兩顆明亮的大眼睛。慢慢地,一隻精神十足的貓頭鷹出現在大家面前。牠不是普通的貓頭鷹,而是守護森林故事的小博士。

從那天起,貓頭鷹博士提醒每一位來訪的人:不要輕易丟棄自然材料,也不要忘記身邊的老樹與土地。只要願意觀察、珍惜與創作,平凡的材料,也能長出新的故事。

002.mp3設計作者|樹藝工坊/李永謨工藝師

本作品由樹藝工坊的李永謨工藝師設計創作。李永謨老師自一九八九年起投入樹藝自然創作,經常從樹皮、樹葉、葉脈、果實與種子之中,細心尋找最適合創作的自然材料;有些植物甚至只能運用果實來呈現特色。

在李老師眼中,樹皮、葉子與果實雖然不完美,卻都有自己獨特的形狀、色彩、紋理與生命價值。透過巧妙的雙手與細膩的觀察,他將原本容易被忽略的自然素材,創造成栩栩如生的昆蟲、動物與花卉。

樹藝創作不只是手作藝術,更是一種尊重自然的方式。李老師讓看似無用的素材重獲新生,也帶領我們看見樹的另一面生命價值,並從創作中體悟珍惜、感恩與生命的真諦。

003.mp3使用材料

這件貓頭鷹博士作品,主要使用森林與校園中常見的自然素材創作。圓圓的身體是一顆海檬果;頭頂和翅膀使用三片玉米雞羽毛,展現貓頭鷹蓬鬆的羽毛感。

大大的眼睛與臉部輪廓,搭配麻棟、蓮蕉、羊蹄甲和青剛櫟等種子與果實,讓作品有了靈活的表情。作品底座使用一片大的樟木片,小樟木片做為裝飾;兩段樟木枝則像是貓頭鷹停棲的樹枝。最後,以一圈鋁線固定與連接各個部件。

這些材料原本都是自然掉落的果實、種子、羽毛與枝條。透過觀察、整理與創作,它們重新變成守護森林故事的貓頭鷹博士。提醒大家:海檬果不可食用,製作時請由老師或大人協助處理。

004.mp3系統開機訊息

您好,樹藝AI故事機已經啟動。歡迎來到樹藝工坊,邀請您一起認識自然素材所創造的美麗作品。

005.mp3歡迎參觀樹藝工坊

歡迎參觀樹藝工坊。您現在看到的是由自然果實、種子、樹皮、樹葉與羽毛創作而成的作品。請按下按鈕,選擇聆聽作品故事、使用材料或設計作者。讓我們一起發現大自然的另一種生命價值。

006.mp3謝謝參觀

謝謝您的參觀。願您帶著一雙善於觀察的眼睛,重新看見身邊的樹木、葉片、果實與種子。期待您再次來到樹藝工坊,聆聽更多大自然的故事。

七、程式測試技巧:先分開,再整合

故事機包含語音、感測與按鈕三種功能。開發時最有效的原則是:一次只測試一個元件。若一開始就執行完整程式,聲音、距離與按鈕同時出問題時,往往很難快速找出原因。

1. 建議的測試順序

階段要確認的事情成功判斷
第一階段:Pico WThonny 能連線、程式能儲存到 Pico W。Shell 出現 MPY: soft reboot 後,能看到 print() 訊息。
第二階段:JQ6500供電、喇叭、UART 接線與 microSD 音檔編號正確。可個別播放 001 至 006 等音檔。
第三階段:按鈕三個 GPIO 都能讀到按下與放開。按下不同按鈕,Shell 顯示正確的功能名稱。
第四階段:超音波量測距離與實際距離大致相符,且不會頻繁跳動。靠近時讀值變小,離開時讀值變大。
第五階段:整合迎賓、按鈕導覽、離場致謝能依序運作。每次靠近只迎賓一次;真正離開後才播放致謝詞。

2. JQ6500 單獨播放測試

先確認 JQ6500、喇叭與 microSD 音檔完全正常,再與感測器整合。將下列短程式存到 Pico W 執行,故事機會每四秒輪流播放開機、歡迎與致謝音檔。

from machine import Pin, UART
import time

jq6500 = UART(0, baudrate=9600, tx=Pin(0))

def play_file(number):
    command = bytes([0x7E, 0x04, 0x03, 0x00, number, 0xEF])
    jq6500.write(command)
    print("播放音檔:", number)

while True:
    play_file(4)   # 系統開機訊息
    time.sleep(4)
    play_file(5)   # 歡迎參觀
    time.sleep(4)
    play_file(6)   # 謝謝參觀
    time.sleep(4)
若沒有聲音,依序檢查:JQ6500 的 VCC 與 GND、喇叭是否接在 SPK+ 與 SPK-、Pico W GP0 是否接到 JQ6500 RX、microSD 是否為 FAT32,以及音檔是否為 MP3 格式與正確編號。

3. 三個按鈕的讀值測試

在還沒有接 JQ6500 前,也可先用 Shell 訊息確認按鈕接線。按鈕另一端必須接 GND;按下時,內建上拉的 GPIO 讀值會變為 0。

from machine import Pin
import time

button_story = Pin(10, Pin.IN, Pin.PULL_UP)
button_material = Pin(11, Pin.IN, Pin.PULL_UP)
button_author = Pin(12, Pin.IN, Pin.PULL_UP)

while True:
    if button_story.value() == 0:
        print("按下:作品故事(GP10)")
        time.sleep_ms(300)

    elif button_material.value() == 0:
        print("按下:使用材料(GP11)")
        time.sleep_ms(300)

    elif button_author.value() == 0:
        print("按下:設計作者(GP12)")
        time.sleep_ms(300)

若按下按鈕沒有訊息,先不要修改主程式;請檢查按鈕兩腳是否接在正確的兩側、是否真的接到 GND,以及 GPIO 編號是否與實際接線一致。

4. 超音波感測器單獨測試

先讓 HC-SR04 只負責顯示距離。手持物品靠近感測器,距離數字應逐漸變小;移開後數字應逐漸變大。這一步可協助確認 TRIG、ECHO、分壓電阻與共地是否正確。

from machine import Pin, time_pulse_us
import time

trig = Pin(14, Pin.OUT)
echo = Pin(15, Pin.IN)

def get_distance():
    trig.value(0)
    time.sleep_us(2)
    trig.value(1)
    time.sleep_us(10)
    trig.value(0)

    try:
        duration = time_pulse_us(echo, 1, 30000)
        if duration < 0:
            return 999
        return duration * 0.0343 / 2
    except OSError:
        return 999

while True:
    print("距離:", round(get_distance(), 1), "cm")
    time.sleep_ms(500)
超音波測試提醒:HC-SR04 對柔軟衣物、斜面、細小物件與遠方牆面,可能出現不同反射距離。正式程式使用七筆中位數濾波與五秒離場確認,就是為了避免單次量測跳動而重複播放語音。

5. 善用 Thonny Shell 找問題

  • 開發時保留 print(),例如印出「播放音檔」、「按鈕名稱」與「原始/濾波後距離」。
  • 測試成功後,再將頻繁執行的距離列印註解掉,避免 Shell 訊息太多、影響觀察。
  • 每次只修改一小段程式,儲存、執行、確認結果,再進行下一步;不要一次改接線與程式邏輯。
  • 出現錯誤時,先看 Shell 最後一行的錯誤類型與行號;例如 OSError 常與感測器量測或接線有關。
  • 看到 MPY: soft reboot 是 Thonny 重新執行 Pico W 程式的正常訊息,不是硬體故障。

6. 整合測試紀錄表

測試項目預期結果測試結果
通電開機播放 004.mp3 系統開機訊息□ 通過 □ 待調整
有人靠近只播放一次 005.mp3 歡迎詞□ 通過 □ 待調整
GP10 按鈕播放 001.mp3 作品故事□ 通過 □ 待調整
GP11 按鈕播放 003.mp3 使用材料□ 通過 □ 待調整
GP12 按鈕播放 002.mp3 設計作者□ 通過 □ 待調整
參觀者離開連續約五秒離場後播放 006.mp3□ 通過 □ 待調整

八、測試與展示前檢查

  1. 確認 Pico W、JQ6500、HC-SR04 的 GND 全部共地。
  2. 確認 HC-SR04 的 ECHO 已通過分壓電阻才連到 GP15。
  3. 確認 microSD 卡使用 FAT32,音檔以 001.mp3 到 006.mp3 連續命名並依序放入。
  4. 通電後確認播放 004.mp3;靠近後確認只播放一次 005.mp3。
  5. 測試 GP10、GP11、GP12,確認分別播放作品故事、使用材料、設計作者。
  6. 離開感測範圍後,確認約五秒才播放 006.mp3,避免感測值偶爾跳動造成重複播放。
音檔管理建議:JQ6500 對 microSD 的檔案順序較敏感。建議先將記憶卡格式化為 FAT32,再依序放入 001.mp3 至 006.mp3,中間不要缺號。日後若新增內容,可接續使用 007.mp3、008.mp3,並同步修改程式中的播放編號。