粉紅渦蟲養殖場

國中生物會教到兩次渦蟲,一次在無性生殖,第二次在扁形動物。課堂上播放的影片總是讓學生一提到渦蟲就直呼「好可愛!」、紛紛說著想把牠「切一切」,甚至還有幾個學生興致勃勃地說好想帶回家養。

聽學生說了幾次之後,我就隨口應了一句:「那我弄個數位板的渦蟲給你們養吧!」

就像過去開發教學工具一樣,先在腦海中想好具體目標,接著透過 AI Agent 協作,這款療癒系的「數位渦蟲養殖場」就這麼來了!

線上連結:
https://chihhsiangchien.github.io/pinky-planarian/

數位渦蟲的技術與特色

這款養殖模擬器利用了物理模擬技術,呈現出渦蟲那種軟綿綿、四處游動的動態。在功能設計上可以進行餵食,觀察牠們長大,還可以切斷再生。

網頁支援 PWA (Progressive Web App) 技術。使用者可以像安裝手機 App 一樣,直接將這個網頁添加到手機的主畫面,隨時隨地開啟這個口袋渦蟲水族箱。

互動工具箱功能

系統的互動工具如下:

  • 觀察: 好奇的渦蟲會受到吸引,主動游向你的滑鼠指標並盯著它看。
  • 餵食: 投餵愛心形狀的食物,點擊後看著渦蟲慢慢將其吸收並逐漸長大。
  • 魔法棒: 揮舞魔法棒切割渦蟲,牠們就會在畫面上即時分裂並展現再生能力。
  • 逗弄: 游標直接接觸渦蟲時,可以真實地撥弄、干擾牠們的游動方向。
  • 召喚: 點擊畫面任意空白處,可以發出訊號吸引全場的渦蟲聚集。
  • 換水: 當背景逐漸變綠、代表水質惡化,渦蟲的活動力會隨之下降。此時必須趕快換水,否則渦蟲會開始萎縮。
  • 療癒音訊: 背景搭配了即時合成的 3/4 拍音樂盒 BGM,並針對各項互動加入了清脆的特效音。

用電腦顯示MLX90640熱像攝影模組的熱像

幾年前買了一個熱影像攝影模組/紅外線陣列 MLX90640(110度版本),當時搭配 Wio Terminal 一起購買,主要是將影像直接放在 Wio Terminal 的小螢幕中顯示。那時的程式碼直接套用廠商提供的範例,也沒有深入去管它的運作原理。

最近重新翻出這個有趣的機器,心裡冒出一個新目標:想要把熱影像直接在電腦螢幕上呈現。畢竟在教學現場或實驗演示時,這樣才方便直接投影到大螢幕上讓大家看清楚。稍微研究了一下它的規格,原來 MLX90640 的通訊方式是 $I^2C$。那基本邏輯就很簡單了:我只要用一個開發板負責透過 $I^2C$ 把 MLX90640 偵測到的溫度數據拿回來,再透過 UART 序列通訊(Serial)轉送進電腦,最後電腦端用 Python 來處理這些數據並即時繪圖就可以了!

架構:
MLX90640 (感測器) → [I2C] → Wio Terminal (轉運站) → [USB Serial] → 電腦 → [Python + OpenCV] → 大螢幕投影

既然原本就是搭配 Wio Terminal,那我就直接用它來當作數據的「轉運站」。以下是處理序列傳輸的 Arduino 端的程式碼:

Wio Terminal 端:序列轉運站程式碼

#include <Adafruit_MLX90640.h>

Adafruit_MLX90640 mlx;
float frame[32*24]; 

void setup() {
  Serial.begin(115200); 
  while (!Serial) delay(10); 

  Wire.begin();
  // 如果 1MHz 會卡死,先退回標準的快速模式 400kHz,這最安全穩定
  Wire.setClock(400000); 

  if (!mlx.begin(MLX90640_I2CADDR_DEFAULT, &Wire)) {
    while (1) delay(10); 
  }

  mlx.setMode(MLX90640_CHESS); 
  
  // 將解析度降為 16 位元(或 MLX90640_ADC_17BIT)
  mlx.setResolution(MLX90640_ADC_16BIT); 

  mlx.setRefreshRate(MLX90640_4_HZ); // 可以先從 2Hz/4Hz 驗證,再嘗試調成 MLX90640_8_HZ
}

void loop() {
  // 抓取一影格的溫度資料 (這一步在高頻時會阻塞等待感測器準備好)
  if (mlx.getFrame(frame) != 0) {
    return; 
  }

  // 將 768 (32x24) 個點的數據輸出到電腦
  for (int i = 0; i < 768; i++) {
    // 小數點改為 1 位即可(例如 26.5),減少一半的 Serial 傳輸字元量,讓速度更快
    Serial.print(frame[i], 1); 
    if (i < 767) {
      Serial.print(",");
    }
  }
  Serial.println(); 
}

接著是電腦端的重頭戲。我們利用 Python 讀取 Serial 連接埠的資料,將收到的 768 個浮點數重新組裝成 $32 \times 24$ 的矩陣。為了讓視覺效果更好,程式中使用了雙三次插值(Bicubic Interpolation)將解析度平滑放大,並套用內建的 COLORMAP_TURBO ,加上了滑鼠移動即時顯示該點「真實溫度」的功能!

電腦端:Python 數據接收與熱影像視覺化

import serial
import serial.tools.list_ports
import numpy as np
import cv2
import time

BAUD_RATE = 115200
SCALE_FACTOR = 20     # 32x24 放大 20 倍 = 640x480

# 全域變數,用來記錄滑鼠當前在放大影像上的 (x, y) 座標
mouse_x = -1
mouse_y = -1

def mouse_move_callback(event, x, y, flags, param):
    """ 監聽滑鼠移動事件 """
    global mouse_x, mouse_y
    if event == cv2.EVENT_MOUSEMOVE:
        mouse_x = x
        mouse_y = y

def find_wio_port():
    """ 自動尋找或讓使用者手動選取序列埠 """
    ports = list(serial.tools.list_ports.comports())
    if not ports:
        print(" 錯誤:電腦目前沒有連接任何序列埠裝置(請檢查 USB 線)。")
        return None

    for p in ports:
        description = p.description.lower()
        manufacturer = (p.manufacturer or "").lower()
        if "seeed" in description or "arduino" in description or "seeed" in manufacturer:
            print(f" 自動偵測到 Wio Terminal: {p.device}")
            return p.device
            
    if len(ports) == 1:
        print(f" 自動選定唯一的序列埠: {ports[0].device}")
        return ports[0].device
        
    print("\n 找到多個序列埠,請手動選擇 Wio Terminal 連接的編號:")
    for i, p in enumerate(ports):
        print(f"  [{i}] {p.device} - {p.description}")
        
    while True:
        try:
            choice = input(f"\n請輸入編號 (0-{len(ports)-1}): ").strip()
            idx = int(choice)
            if 0 <= idx < len(ports):
                return ports[idx].device
            else:
                print(f"請輸入 0 到 {len(ports)-1} 之間的數字。")
        except ValueError:
            print("請輸入有效的數字。")

def main():
    global mouse_x, mouse_y
    
    PORT = find_wio_port()
    if not PORT:
        return

    try:
        ser = serial.Serial(PORT, BAUD_RATE, timeout=1)
        print(f" 成功連線至: {PORT}")
    except Exception as e:
        print(f" 錯誤:無法開啟序列埠 {PORT}。原因: {e}")
        return

    time.sleep(1.5)
    ser.reset_input_buffer()

    # 先建立視窗,這樣才能綁定滑鼠事件
    window_name = "Wio Terminal MLX90640 Smooth Thermal"
    cv2.namedWindow(window_name)
    cv2.setMouseCallback(window_name, mouse_move_callback)

    print("\n 影像視窗已啟動!")
    print(" 提示:將滑鼠移入畫面即可顯示當前位置溫度。點擊影像視窗並按 'q' 鍵可關閉程式。")

    while True:
        if ser.in_waiting > 0:
            try:
                line = ser.readline().decode('utf-8').strip()
                if not line:
                    continue
                
                str_list = line.split(',')
                if len(str_list) != 768:
                    continue
                
                # 重組原始 24x32 的溫度矩陣
                raw_matrix = np.array(str_list, dtype=np.float32).reshape(24, 32)
                raw_matrix = np.fliplr(raw_matrix)  # 鏡像修正(讓畫面與手同向)

                min_temp = np.min(raw_matrix)
                max_temp = np.max(raw_matrix)
                
                # 正規化到 0~255 灰階
                if max_temp == min_temp:
                    gray_img = np.zeros((24, 32), dtype=np.uint8)
                else:
                    gray_img = (255 * (raw_matrix - min_temp) / (max_temp - min_temp)).astype(np.uint8)
                
                # 雙三次插值美化放大
                target_size = (32 * SCALE_FACTOR, 24 * SCALE_FACTOR)
                smooth_img = cv2.resize(gray_img, target_size, interpolation=cv2.INTER_CUBIC)
                
                # Turbo 高質感色圖
                color_heatmap = cv2.applyColorMap(smooth_img, cv2.COLORMAP_TURBO)
                
                # --- 核心:計算滑鼠所在位置的溫度 ---
                if 0 <= mouse_x < target_size[0] and 0 <= mouse_y < target_size[1]:
                    # 將放大後的像素座標 (mouse_x, mouse_y) 反推回原始的 32x24 矩陣索引
                    orig_col = int(mouse_x / SCALE_FACTOR)
                    orig_row = int(mouse_y / SCALE_FACTOR)
                    
                    # 邊界安全檢查
                    orig_col = max(0, min(orig_col, 31))
                    orig_row = max(0, min(orig_row, 23))
                    
                    # 抓取未經插值的「真實溫度」
                    current_pixel_temp = raw_matrix[orig_row, orig_col]
                    
                    # 在滑鼠游標右下方繪製十字與溫度標籤
                    text_x = mouse_x + 15
                    text_y = mouse_y + 15
                    
                    # 防止文字超出右方與下方邊界
                    if text_x > target_size[0] - 120: text_x = mouse_x - 110
                    if text_y > target_size[1] - 10: text_y = mouse_y - 15
                    
                    # 畫一個小小的中心點十字提示
                    cv2.drawMarker(color_heatmap, (mouse_x, mouse_y), (255, 255, 255), 
                                   markerType=cv2.MARKER_CROSS, markerSize=10, thickness=1, line_type=cv2.LINE_AA)
                    
                    # 秀出滑鼠指著的溫度(黑框白字看得比較清楚)
                    cv2.putText(color_heatmap, f"{current_pixel_temp:.1f} C", (text_x, text_y), 
                                cv2.FONT_HERSHEY_DUPLEX, 0.5, (0, 0, 0), 3, cv2.LINE_AA)
                    cv2.putText(color_heatmap, f"{current_pixel_temp:.1f} C", (text_x, text_y), 
                                cv2.FONT_HERSHEY_DUPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
                
                # 固定顯示左上角的最高/最低溫
                cv2.putText(color_heatmap, f"Max: {max_temp:.1f} C", (15, 40), 
                            cv2.FONT_HERSHEY_DUPLEX, 0.6, (255, 255, 255), 1, cv2.LINE_AA)
                cv2.putText(color_heatmap, f"Min: {min_temp:.1f} C", (15, 70), 
                            cv2.FONT_HERSHEY_DUPLEX, 0.6, (255, 255, 255), 1, cv2.LINE_AA)
                
                cv2.imshow(window_name, color_heatmap)

            except Exception as e:
                pass
                
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    ser.close()
    cv2.destroyAllWindows()
    print("程式已安全結束。")

if __name__ == "__main__":
    main()

這樣一來,原本在 2.4 吋小螢幕裡的熱影像,就能順利以 640x480 的畫面投影到教室的大螢幕上了!就能拿來做生物體溫、植物蒸散作用或環境物理性質的演示。




無線傳電的Demo教具

上週研習時,學校的君翰老師帶大家做了電磁學的教具,原型是其他老師去科工館看到的。原本的設計是透過繞線圈,觀察通電後的磁場改變如何使得指北針偏轉。

四周有兩處缺刻的壓克力板,對於固定繞線圈十分友善。看著繞好的線圈,我突然有了靈感,好像可以把它改造成其他教具?於是,我跟同事要了一套他的材料,拿回來摸索一下,最後成功做出了一個「無線傳電教具」。

無線傳電教具成果展示

先來看看實際的展示效果。下圖左邊是發射線圈(加了一些電子元件,後面會詳細說明),右邊則是接收線圈,結構非常純粹:只有漆包線圈直接接上一顆紅光 LED 燈。

實驗操作時,完全不需要任何實體導線連接,只要將接收線圈逐漸靠近發射線圈,右側的紅光 LED 就會神奇地亮起來!

動態演示影片如下:

自製電路佈線與製作細節

這個無線傳電教具的核心在於發射端的「自激震盪電路」。電路的繞線與接線方式如下:

  • 發射線圈繞法:先繞 10 圈之後,留出一段漆包線不繞(作為中央抽頭),隨後順著相同方向繼續繞 10 圈。這個中央抽頭要連接到 3V 電池的正極。(註:也可以用兩組獨立的 10 圈線圈,將其中一組的尾端與另一組的頭端相接,同樣能拉出中央抽頭)。
  • 電晶體接線:整組發射線圈的「頭端」和「尾端」,分別連結到三極體(電晶體)的 C極(集極)B極(基極)
  • 安全保護:在連接 B極 之前,必須先串聯一個電阻,避免流入基極的電流過強而燒毀元件。
  • 電源迴路:電晶體的 E極(射極) 則直接接回電池的負極(GND)。
  • 接收線圈:同樣繞 10 圈並直接閉路接上紅光 LED。依據實際測試,接收端理論上可以用更少的圈數達到同樣的感應效果。

科學原理

自激震盪與電磁感應:
這個電路的核心目的,是利用電晶體達成極高頻率的「導通與斷電」切換。通電瞬間,連接 B極 的線圈導電並產生初始磁場,此磁場的變化使連接 C極 的線圈產生感應電動勢,進而誘發更大的電流。這個增強的電流反過來進一步推動 B極,使電晶體迅速達到「飽和導通」。當電晶體飽和後,電流不再變化,磁場隨之停止增長;失去了變化的磁場,B極 失去推動力,電晶體隨即斷電。斷電後整個過程再度重演。

透過上述的循環,發射線圈便能產生高頻率的電磁震盪。此時,一旁的接收線圈切換進入這個快速變化的磁場中,根據法拉第電磁感應定律,接收線圈內就會產生感應電流,成功推動紅光 LED 發光。

原型實驗與替代材料建議

除了使用研習提供的壓克力板作為繞線骨架外,其實我一開始進行概念驗證時,是直接拿市售整捆未拆的漆包線圈來做發射端的。當時先把線圈拆開重新繞製,抓出中央抽頭;而接收端則是隨手繞在普通的紙杯或塑膠杯上。這種作法,也適合作為學生探究與實作的課堂材料,在此一併分享給大家參考。


科學魔法車的兩級 RC 弛張震盪器 (RC Relaxation Oscillator)

 我在學校有開一個給資優生的選修課,用的是科學魔法車,是教學生電子電路應用和寫程式。


在這個課程中,用的是曹老師設計的教材,其中有一個電路很有趣,是我會多花一點時間給學生玩的,就是這個兩級 RC 弛張震盪器 (RC Relaxation Oscillator)




用的就是一顆反向器IC 4069,藉由四個反向器搭建出來的高頻與低頻振盪電路,確定會發出聲音之後,下一步就可以把30 KΩ 和 20 KΩ 換成可變電阻,然後就可以任意用這兩個電阻創造不同的聲音。

最後再把可變電阻換掉,變成兩條鱷魚夾。然後準備一張紙,紙上用2B鉛筆畫出筆跡,延伸到紙緣,用其中一條鱷魚夾夾住。另外一個鱷魚夾就可以在筆跡上面移動,就像是可變電阻那樣控制出不同的聲音。

如果設計得當,甚至是可以創造出音階,好好唱一首歌曲喔。




用micro:bit玩體感遊戲

前一陣子都在玩 micro:bit,嘗試用它來做各項應用。回憶起很早以前玩 Scratch 時,除了在電腦上直接按鍵控制角色外,也可以透過一些軟硬體套件配合,利用 Arduino 來連動。後來也有軟體搭配,讓 micro:bit 能夠控制 Scratch 角色。這讓我想到:我應該可以繞過這些複雜的套件,直接用 micro:bit 來控制網頁遊戲吧?

技術原理與可行性

結合以下三個核心特色,就能達成硬體與網頁遊戲的互動:

  • 感測器豐富:micro:bit 內建傾斜、加速度和光線感測器,能即時產生動態數據。
  • 多元傳輸介面:數據可以透過 UART Serial(USB 線)或藍牙(Bluetooth)傳回電腦。
  • 現代瀏覽器支援:目前的瀏覽器(如 Chrome)已支援 Web BluetoothWeb Serial API,可直接讀取硬體資料。

micro:bit 體感遊戲控制專案

基於上述構想,我製作了一個小專案。透過這個網頁介面,你可以輕鬆實現體感操作:


設定教學與操作流程

1. MakeCode 專案設定

在使用藍牙連線前,務必先在 MakeCode 進行以下設定:

  1. 點擊 MakeCode 右上角的齒輪 ⚙️。
  2. 選擇「專案設定」(Project Settings)。
  3. 勾選 No Pairing Required (不需要配對)。

2. 產生並燒錄程式

在專案網頁中,你可以選擇想要的「控制方式」與「傳輸介面」,網頁會自動產生對應的程式碼。請切換到 micro:bit MakeCode 的 JavaScript 標籤區,將代碼貼上並上傳至開發板。


micro:bit 體感遊戲控制介面展示


3. 開始玩耍

設定完成後,下方就有一些簡單的體感遊戲可以直接測試玩耍囉!

透過這種方式,就可以用 micro:bit 成了網頁遊戲的搖桿。

【自製運動感測器】用 Micro:bit 測量槓鈴速度:挑戰 1000Hz 高頻採樣的底層開發實錄

在我的重量訓練課表中,除了肌力訓練外,也會加入「爆發力(Power)」訓練,例如舉重衍生動作或壺鈴抓舉。雖然口語說爆發「力」,但在運動科學的定義中,它更精確的定義是「功率」。

爆發力的物理本質

爆發力並非單純的力量,而是功率的表現。我們可以透過以下公式來理解:

爆發力(Power)是力與位移的乘積除以時間,即:
$$ P = \frac{F \times s}{t} = F \times v $$
其中 $F$ 為力量($m \times a$),$v$ 為速度。

換句話說,在處理特定重量時,若能產生更大的加速度與速度,或是在更短的時間內完成更長的位移,就代表爆發力表現越佳。

測量技術的挑戰

在運動科學領域,測量功率通常有兩種路徑:

  • 線性位移感測器 (LPT):測量外部負載(如槓鈴)的位移,透過時間差分算出瞬時速度、加速度與功率。
  • 加速度感測器 (IMU):直接測量負載加速度。雖然理論上可以透過積分算出速度與位移,但微小的加速度偏差在積分過程中會產生巨大的累積誤差。

為了打造自己的測量裝置,我一開始先嘗試使用 Arduino Nano 33 IoT,但電源配置很麻煩,我沒找到可以用的電池方案。後來我想到,其實microbit有很多穿戴式的應用,再加上它也有三軸加速度和藍牙裝置,所以最終我選擇了 Micro:bit V2 作為開發核心。

開發關鍵:突破採樣頻率瓶頸

若要精準測量槓鈴移動,理想的採樣頻率需要達到 1000Hz。然而,標準的積木程式(MakeCode)底層架構無法支援如此高頻的數據傳輸。經過翻閱文件與 AI 協作,我決定從底層進行開發。

核心邏輯如下:

  1. 使用底層 I2C (400kHz) 達成穩定 1ms 的加速度採樣。
  2. 暫存數據,每累積 20 筆資料(約 120 Bytes)封裝為一組 BLE (Bluetooth Low Energy) 封包傳出,以確保傳輸效率。

韌體下載與安裝

我已經將編譯好的 Hex 檔案放在 GitHub,請下載後直接拖入 Micro:bit V2 在電腦上辨識成的隨身碟即可:

下載韌體 (v1.0)

操作說明

Micro:bit 端控制

  • A 鍵:切換感測器量程 (2g, 4g, 8g, 16g)。LED 螢幕首列會亮起對應數量的燈號表示目前量程。
  • B 鍵:開啟或關閉 LED 矩陣顯示。
  • 藍牙配對:開機即自動進入廣播模式,無需 PIN 碼。裝置名稱為 MB_[五個英文字母]

視覺化圖表與網頁介面

數據呈現交給瀏覽器的 Web Bluetooth API。您可以使用 Chrome(Windows/Linux/Android)或 Bluefy(iOS)連結至以下網頁:

線上測量工具網頁

實作與安裝建議

在開始測量前,務必將 Micro:bit 固定後靜止進行重力校準。因為感測器固定在器材上時必然會有傾斜角度,校準後才能獲得正確的垂直向加速度。

關於硬體固定方式,建議搜尋 "microbit 2032 watch" 相關擴充套件,找附帶電池座的穿戴式套件,適合綁在手上或固定在器材上。

校準完成後,您可以透過網頁端的「定時錄製」功能,設定延遲啟動與錄製時長,如同相機自拍設定一般簡單。以下是實際測量與錄製的數據介面參考:

數據結果 1
數據結果 2
所有原始碼都在這,如果你有興趣看看的話