📋 專案概述

上週部門季報,輪到我們組。同事阿立把公司那支邏輯題簡報筆帶來救場——結果那支筆用的是2.4G 專精接收器,插在上一位報告人的筆電上沒拔。他站在台上舉著筆對投影幕乾揮,台下三百雙眼睛看他表演默劇。最後他只能一路小跑步回座位:簡報筆最尷尬的瞬間,是你人站在台上、筆在手上、靈魂在插接收器的那台筆電旁邊。

市售簡報筆兩類,各有死法:

  1. 專精接收器款(NT$600~1,500):體積小容易插著被拔走、遺失接收器整支變按鈴、有些只支援自家驅動。
  2. 藍牙款(NT$900 起):免接收器了,但要配對、會斷線重連、Windows 睡眠醒來後「請重新配對你的裝置」——簡報開始前 30 秒你不想做這种事。

解法其實一直躺在 ESP32-S2 的腳位表上:它內建 USB OTG,可以直接把自己模擬成一支 USB 鍵盤。插上一個 20 元的 USB-A 公頭 dongle,任何電腦插上就是「有鍵盤」——免驅動、免配對、免接收器協議,Windows/macOS/Linux/Chromebook 全通。再配一颗 28 元的六軸慣性感測,把手勢寫成按鍵事件,就得到一支「揮手翻頁、連搖三下黑燈」的空氣簡報筆。全部材料 NT$262:

成本算盤:羅技 R500 約 NT$1,190,本專案 262 元,還多做得到「手勢」這件事——羅技要你有這種功能請買 NT$2,500 的 Spotlight 版。更實用的是:因為它是「鍵盤」,你可以用 AutoHotkey 把它改造成任何巨集扳機(按下去 = 開啟你的簡報模板),這是封閉協議題簡報筆永遠給不了的自由度。

🔬 為什麼選 ESP32-S2(選型說明)

本專案的硬需求只有一條:USB Device(OTG)模式跑 HID。家族裡有原生 USB 的型號不少,但定位各異:

型號USB 能力無線板價適合度
ESP32-S2(本篇)USB-OTG(可當 Device)Wi-Fi 4~NT$75⭐⭐⭐⭐⭐ 家族第一款 OTG 晶片、待機最省的 HID 平台
ESP32-S3USB-OTGWi-Fi+BLE~NT$130⭐⭐⭐⭐ 同能力但多付 BLE 錢,本專案用不到
ESP32-C3 / C6 / C61只有 USB-Serial-JTAG,不能當 USB DeviceWi-Fi 等~NT$40–120❌ 模擬不了鍵盤,直接出局
經典 ESP32無 USBWi-Fi+BT~NT$110❌ 得外加 CH9102 之類,做不了 HID

S2 的取舍很甜:它是「能做 USB HID 的型號」裡面最便宜、待機最省的(深度睡眠 ~20µA 級,比 S3 更低一档);簡報筆不需要 BLE、不需要第二顆晶片。慣性手勢那顆 MPU6050 走 I2C,S2 的預設腳位 GPIO8/9 即插即用。Arduino 支援:arduino-esp32 core ≥ 3.0,開發板選 ESP32S2 Dev Module,USB 模式選 Hardware CDC and JTAG 之上的原生 HID(本程式用 USBHIDKeyboard,core 內建)。

⚠️ S2 專屬眉角:S2 的 GPIO45/46 是 VDD_SPI 電壓 strapping 腳,接了外部電路會讓板子燒錄模式錯亂;GPIO33~37 在部分模組被 PSRAM/Flash 佔用。本專案腳位全部避開,以 S2 SuperMini 引出的 GPIO1~14 為準。

🧱 硬體清單

項目型號數量預估單價備註
主控板ESP32-S2 SuperMini1~75 元需可進入 ROM 燒錄模式(按 BOOT+RST)各廠板皆可
慣性感測MPU6050 模組(Gyro-521 亦可)1~28 元本程式直接讀寄存器,不用 DMP 韌體
鋰電鋰聚合物 502030(3.7V 400mAh)1~45 元6mm 厚以內,筆身塞得下
充電TP4056+DW01 保護二合一板1~10 元Rprog 10k ≈ 125mA,小電池安全慢充
按鍵6×6 輕觸開關2~4 元上一頁/下一頁
電源開MSK-12C02 側滑開關1~8 元切斷電池,久放不吃電
指示3mm LED + 1kΩ1~3 元手勢確認閃碼
隱藏連線USB-C 公轉 USB-A 母縮回轉接線1~48 元藏在筆身裡,需要時拉出來直插筆電——本專案的「可靠性底牌」,見下方架構說明
導線/固定30AWG 矽膠線、雙面膠、尼龍束帶1 組~20 元矽膠線軟、不易在筆身內折斷
合計~262 元不含列印耗材(筆身+底座約 12g PLA)

「無線」的代價與本專案的解法(最關鍵的一段設計)

USB HID 的代價很直白:它是或有線的。要嘛接受藍牙款的配對儀式與斷線重連,要嘛——承認「最後一段總得有個接頭」,然後把它做好。本專案的解法是筆尾內藏一支縮回式轉接線(舊式彈簧縮回手機轉接線同款思路):

少焊一顆晶片、少寫一份韌體、零配對、零斷線——把複雜度留給列印件,把可靠性留給銅線,這是本系列一貫的工法。

🔌 接線圖

ESP32-S2 SuperMini(USB-C 朝尾端) GPIO8 (SDA) GPIO9 (SCL) 3V3 GND MPU6050 SDASCL VCCGND 前頁鍵 GPIO4↔GND next 鍵 GPIO5↔GND GPIO4 / GPIO5 → 按鍵→GND(內部上拉) USB-C:縮回線/充電共用 LED GPIO7+1kΩ→GND
▲ 全機 8 支線:I2C 兩支、供電兩支、按鍵兩支(另一腳共 GND)、LED 一支。電池由 TP4056 板接入背板 VBAT 焊盤,經板載穩壓供電,見下表。

詳細接線

ESP32-S2 腳位連接說明
GPIO8 (SDA)MPU6050 SDAS2 Wire 預設腳,免引腳重新配置
GPIO9 (SCL)MPU6050 SCL模組自带 10k 上拉,不必外加
GPIO4按鍵 A → GND啟用內部上拉;按鍵的另一腳接 GND
GPIO5按鍵 B → GND同上;兩鍵同按 2 秒=手動重校準
GPIO7LED(串 1kΩ)→ GND手勢成功閃一下:台上確認「有收到」比什麼都重要
GPIO3電池電壓分壓(100k+100k)S2 ADC1 範圍,低於 3.45V 開機閃紅提示充電
3V3MPU6050 VCC模組板載 LDO,3V3 供電量測最準
GND全部 GND 共地必接,I2C 不共地只會印出亂碼
VBAT(背板焊盤)TP4056 板 B+繞過板載 LDO 直供 3.3V?不——鋰電 4.2V 進 VBAT 經板子降壓,效率比 LDO 高一截
⚠️ TP4056 板的 B-/B+ 接反會直接短路鋰電(三秒內發燙鼓包)。焊完先量 B-B+ 電壓再焊電池線;充電時 USB-C 插著,USB-C 同時供系統電力。

ASCII 對照表(SuperMini 絲印為準)

ESP32-S2 SuperMini MPU6050 / 週邊 ┌─────────────┐ │ USB-C(尾端)│←── 縮回轉接線(筆尾出線)→ 筆電 USB-A │ 3V3 │──→ MPU6050 VCC │ GND │──→ MPU6050 GND、按鍵冷端、LED 陰極 │ GPIO8 │──→ MPU6050 SDA │ GPIO9 │──→ MPU6050 SCL │ GPIO3 │←── 電池分壓中點(100k+100k) │ GPIO4 │←── 按鍵A(熱端,冷端→GND) │ GPIO5 │←── 按鍵B(熱端,冷端→GND) │ GPIO7 │──→ 1kΩ → LED+ → GND │ VBAT(背面) │──→ TP4056 板 B+(B−→GND 匯流排) └─────────────┘ 避開:GPIO0(BOOT)、GPIO45/46(VDD_SPI strapping)、GPIO33~37(Flash/PSRAM)

🖨️ 3D 列印零件

零件清單

零件數量材質建議顏色列印時間
筆身上殼(按鍵窗)1PLA深綠~1.5 hr
筆身下殼(電池艙)1PLA深綠~2 hr
按鍵帽 ×22PLA淺綠撞色~8 min
充電底座(帶收線槽)1PLA灰~1.2 hr
筆尾縮回線壓蓋1PETG黑~10 min

列印設定

設計要點

  1. 指窩防滑槽:上殼拇指區三道 0.6mm 深凹槽,簡報手汗也不打滑——這比任何 RGB 燈都實用
  2. 縮回線出線槽:筆尾 5×3mm 槽+PETG 壓蓋鎖住轉接線根部,插拔受力在殼不在焊點(縮回線斷裂 99% 發生在根部)
  3. 感測器艙與質心:MPU6050 鎖在筆身最前端、電池在尾——甩手勢時前端線性加速度最大,手勢信噪比直接翻倍;模組下方留 2mm 空腔隔絕手溫漂移
  4. 底座防倒配重槽:底座底部 30×30×8mm 槽放一枚舊 18650 或硬幣+雙面膠,桌面撞不倒

💻 程式碼

📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」。開發板選 ESP32S2 Dev Module,USB 選 Hardware CDC and JTAG 以外的預設(本程式 HID 自己接管 USB),cores ≥ 3.0。
/* ============================================================ * ESP32-S2 手勢簡報筆 | Gesture Presenter * ------------------------------------------------------------ * 功能:USB HID 鍵盤模擬(翻頁/F5/Esc/B)+ MPU6050 甩手手勢 * 開機桌面自校準 resting 手姿、低電偵測、深度睡眠 * 硬體:ESP32-S2 SuperMini + MPU6050(I2C 8/9) + 按鍵 4/5 * + LED 7 + 電池分壓 GPIO3 * 供電:3.7V LiPo via VBAT,USB-C 充電與有線 HID 共用 * 相依:arduino-esp32 core ≥3.0(USBHIDKeyboard 內建) * ============================================================ */ #include <Wire.h> #include "USB.h" #include "USBHIDKeyboard.h" #include <driver/gpio.h> #include <esp_sleep.h> USBHIDKeyboard Keyboard; /* ------------------------------------------------------------ * 1. 腳位與時序常數 * ------------------------------------------------------------ */ const int PIN_LED = 7; // 手勢回饋燈:台上要「看得見」鍵盤收到了 const int PIN_BTN_A = 4; // 前一頁(實體鍵永遠優先於手勢) const int PIN_BTN_B = 5; // 下一頁 const int PIN_VBAT = 3; // 電池分壓中點(100k+100k) const float VBAT_DIV = 2.0f; // 分壓比:ADC 讀值 ×2 = 實電壓 const uint8_t MPU_ADDR = 0x68; // AD0 接地時的預設地址 const unsigned long GESTURE_COOLDOWN_MS = 900; // 一次手勢後的冷卻:防止甩一次的振盪被數成兩次 const unsigned long SHAKE_WINDOW_MS = 1600; // 「連搖三下」的計數視窗 const float SWIPE_G = 0.55f; // 甩動峰值閾值(g):約 0.55g 邊界,低於日常走動的 0.2g 抖動 const float SHAKE_G = 1.9f; // 搖晃判定閾值:故意高於甩頁閾值,兩手勢不打架 const int IDLE_SLEEP_S = 90; // 靜置 90 秒進深度睡眠(按鍵喚醒;喚醒≈2 秒,開會前預先推一下鍵) /* ------------------------------------------------------------ * 2. 校準後的「resting 手姿」基準(開機時桌面記錄) * 記錄 2 秒平均的重力向量,之後只比較「相對傾角變化」—— * 這樣斜 30° 握著不會誤觸,躺平放桌上也不會 * ------------------------------------------------------------ */ float refAx = 0, refAy = 0, refAz = 1; // 預設值防止校準失敗時除零 /* 手勢狀態機 */ unsigned long cooldownUntil = 0; unsigned long shakeTimes[4] = {0, 0, 0, 0}; // 最近四次搖晃時間戳(環形) int shakeHead = 0; /* ------------------------------------------------------------ * mpuWrite / mpuRead —— MPU6050 寄存器讀寫薄封裝 * 為什麼不用現成函式庫:本專案只需 6 個寄存器, * 自己發 I2C 少了 40KB flash 與一堆版本相容性地雷 * ------------------------------------------------------------ */ void mpuWrite(uint8_t reg, uint8_t val) { Wire.beginTransmission(MPU_ADDR); Wire.write(reg); Wire.write(val); Wire.endTransmission(); } int16_t mpuRead16(uint8_t reg) { // 回傳:有號 16-bit(高字节在前) Wire.beginTransmission(MPU_ADDR); Wire.write(reg); Wire.endTransmission(false); // repeated start,省一次總線復位 Wire.requestFrom((uint8_t)MPU_ADDR, (uint8_t)2); return (int16_t)((Wire.read() << 8) | Wire.read()); } /* ------------------------------------------------------------ * mpuInit —— 初始化 MPU6050 * ±4g / ±500dps:±4g 讓「甩」的峰值落在 16-bit 量程前半段, * 飽和不會吃掉手勢波峰;500dps 對甩手腕(典型 <250dps)留頭 * ------------------------------------------------------------ */ bool mpuInit() { Wire.begin(8, 9); // SDA=GPIO8, SCL=GPIO9(S2 預設) delay(50); mpuWrite(0x6B, 0x80); // 復位 delay(100); mpuWrite(0x6B, 0x00); // 解睡眠(醒來預設是睡眠!忘了這步讀數全零) mpuWrite(0x1C, 0x08); // ±4g mpuWrite(0x1B, 0x08); // ±500dps mpuWrite(0x1A, 0x05); // DLPF 44Hz:甩手能量在 5~15Hz,低通先削一半震動雜訊 Wire.beginTransmission(MPU_ADDR); // 存在性探測:讀 WHO_AM_I Wire.write(0x75); Wire.endTransmission(false); Wire.requestFrom((uint8_t)MPU_ADDR, (uint8_t)1); uint8_t who = Wire.available() ? Wire.read() : 0; return (who == 0x68 || who == 0x98 || who == 0x72); // 6050/9250 家族都能過 } /* ------------------------------------------------------------ * readAccelG —— 讀三軸加速度(g) * 參數:x/y/z 輸出引號 * 為什麼每次現讀而非中斷緩衝:20ms 輪詢已綽綽有餘, * 省下的 INT 腳留給日後喚醒改版 * ------------------------------------------------------------ */ void readAccelG(float& x, float& y, float& z) { x = mpuRead16(0x3B) / 8192.0f; // ±4g → 8192 LSB/g y = mpuRead16(0x3D) / 8192.0f; z = mpuRead16(0x3F) / 8192.0f; } /* ------------------------------------------------------------ * calibrateRest —— 開機校準:2 秒平均取 resting 重力向量 * 前提:開機時筆是平放/自然握持且静止的——開機即甩必誤判, * 所以校準後 3 秒內不啟用檢測(給手離開筆的時間) * ------------------------------------------------------------ */ void calibrateRest() { double sx = 0, sy = 0, sz = 0; int n = 0; for (int i = 0; i < 100; i++) { float x, y, z; readAccelG(x, y, z); sx += x; sy += y; sz += z; n++; delay(20); } refAx = sx / n; refAy = sy / n; refAz = sz / n; Serial.printf("[CAL] rest = (%.2f, %.2f, %.2f)g\n", refAx, refAy, refAz); } /* ------------------------------------------------------------ * sendKey —— 按鍵事件統一出口(含 LED 回饋與序列埠除錯) * 參數:kc = Arduino key code;holdMs = 按住時長 * 為什麼要 holdMs:簡報軟體(PPT/Keynote)對「按一下」需要 * press+release 完整週期;F5 偶爾吃掉 release 會卡在全螢幕 * ------------------------------------------------------------ */ void sendKey(uint8_t kc, uint16_t holdMs = 60) { Keyboard.press(kc); delay(holdMs); Keyboard.release(kc); digitalWrite(PIN_LED, HIGH); // 視覺確認:0.06 秒的燈,台下看不见、台上看得見 delay(60); digitalWrite(PIN_LED, LOW); Serial.printf("[KEY] 0x%02X\n", kc); } /* ------------------------------------------------------------ * pressAndHoldKey —— 長按事件(開始/結束簡報用,按住語義不同) * ------------------------------------------------------------ */ void pressAndHoldKey(uint8_t kc, uint16_t ms) { Keyboard.press(kc); delay(ms); Keyboard.release(kc); } /* ------------------------------------------------------------ * handleButtons —— 實體按鍵:去彈跳 40ms + 雙鍵長按校準 * 回傳:true = 本迴圈已被按鍵消耗(手勢檢測讓位,雙保險防誤觸) * ------------------------------------------------------------ */ bool handleButtons() { static bool lastA = HIGH, lastB = HIGH; static unsigned long tA = 0, tB = 0, holdStart = 0; bool acted = false; bool a = digitalRead(PIN_BTN_A); bool b = digitalRead(PIN_BTN_B); if (a != lastA) { tA = millis(); lastA = a; } // 先記錄邊界,下面才判穩定 if (b != lastB) { tB = millis(); lastB = b; } if (millis() - tA > 40 && lastA == LOW) { // 按穩 40ms 才算按下(彈跳filt) if (lastB == LOW) { // 雙鍵同按 → 重校準 if (millis() - holdStart > 2000) { calibrateRest(); holdStart = millis() + 999999UL; } } else { sendKey(KEY_LEFT_ARROW); // 前一頁 acted = true; } if (a == HIGH) holdStart = 0; } if (millis() - tB > 40 && lastB == LOW && lastA == HIGH) { unsigned long down = tB; while (digitalRead(PIN_BTN_B) == LOW && millis() - down < 2100) delay(5); if (millis() - down >= 2000) pressAndHoldKey(KEY_F5, 120); // 長按 2 秒 → 開始簡報 else sendKey(KEY_RIGHT_ARROW); // 短按 → 下一頁 acted = true; } return acted; } /* ------------------------------------------------------------ * detectGesture —— 手勢狀態機 * 原理:以校準向量為參考,水平面內(左右甩主要激發 x 軸) * 的偏移超過 SWIPE_G 且 400ms 內回到參考±0.15g → 一次「甩」 * 甩方向由峰值符號決定;連搖計數滑動視窗決定「黑燈」巨集 * 回傳:true = 本迴圈觸發了手勢 * ------------------------------------------------------------ */ bool detectGesture() { float x, y, z; readAccelG(x, y, z); float dx = x - refAx, dy = y - refAy; // 相對參考手姿的水平偏移 float mag = sqrtf(dx*dx + dy*dy) + fabsf(z - refAz) * 0.4f; // z 加權低:跳動不是甩 static bool inSwipe = false; static float peakDx = 0; static unsigned long peakAt = 0; unsigned long now = millis(); if (now < cooldownUntil) return false; if (!inSwipe && fabsf(dx) > SWIPE_G) { // 進入甩動:記峰值方向 inSwipe = true; peakDx = dx; peakAt = now; } else if (inSwipe) { if (fabsf(dx) > fabsf(peakDx)) peakDx = dx; if (mag < 0.15f && now - peakAt < 400) { // 回穩且在單擊時窗內 → 判定一次甩 inSwipe = false; cooldownUntil = now + GESTURE_COOLDOWN_MS; // 冷卻先點火,再決定動作 if (fabsf(peakDx) < SHAKE_G) { // 一般力度 → 翻頁(方向=峰值符號) sendKey(peakDx > 0 ? KEY_RIGHT_ARROW : KEY_LEFT_ARROW); } else { // 大力搖 → 記帳進滑動視窗 shakeTimes[shakeHead] = now; shakeHead = (shakeHead + 1) % 4; if (shakeTimes[0] && shakeTimes[3] - shakeTimes[0] < SHAKE_WINDOW_MS) { sendKey('B'); // 1.6 秒內三次重搖 → 黑燈切換 shakeTimes[0] = shakeTimes[1] = shakeTimes[2] = shakeTimes[3] = 0; } } return true; } if (now - peakAt > 800) inSwipe = false; // 回穩失敗(一直甩):放棄這次 } return false; } /* ------------------------------------------------------------ * batteryGuard —— 電壓偵測:低電開機紅燈閃三下、運行中每 5 分報一次 * 為什麼用開機即報:HID 連線中斷時,低電+重列會變成 * 「簡報中途失靈」複合故障,提前一小時知道差很多 * ------------------------------------------------------------ */ bool batteryGuard() { float v = analogReadMilliVolts(PIN_VBAT) / 1000.0f * VBAT_DIV; static unsigned long lastReport = 0; if (v < 3.45f && millis() - lastReport > 300000) { lastReport = millis(); for (int i = 0; i < 3; i++) { // 快閃三下:不用聽聲音也知道該充電 digitalWrite(PIN_LED, HIGH); delay(120); digitalWrite(PIN_LED, LOW); delay(120); } Serial.printf("[BAT] %.2fV 偏低,請充電\n", v); } return v > 3.20f; // 3.2V 以下強迫進睡,保護電芯 } /* ------------------------------------------------------------ * setup —— USB 先於一切:電腦看到的是「插上一支鍵盤」 * ------------------------------------------------------------ */ void setup() { Serial.begin(115200); pinMode(PIN_LED, OUTPUT); pinMode(PIN_BTN_A, INPUT_PULLUP); // 用內部上拉,外部不用補電阻 pinMode(PIN_BTN_B, INPUT_PULLUP); analogSetPinAttenuation(PIN_VBAT, ADC_6db); // 6dB 衰減線性區到 3.3V,分壓後剛好蓋住 4.2V Keyboard.begin(); USB.begin(); // 枚舉開始:電腦側 1 秒內出現 HID keyboard if (!mpuInit()) { // IMU 掛了不能裝死:按鍵仍是完整簡報筆 Serial.println("[!] MPU6050 未回應——僅按鍵模式(檢查 I2C 兩線)"); } else { calibrateRest(); delay(3000); // 手離開筆的 3 秒保護窗 } Serial.println(" Gesture Presenter ready(甩動=翻頁,連搖三下=黑燈,長按B=F5)"); } /* ------------------------------------------------------------ * loop —— 50Hz 輪詢:按鍵優先、手勢其次、靜置進睡 * ------------------------------------------------------------ */ unsigned long lastMotionAt = 0; void loop() { if (!batteryGuard()) { // 電壓保護:深度睡眠,按鍵喚醒再戰 esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BTN_A, LOW); esp_deep_sleep_start(); } if (handleButtons()) { lastMotionAt = millis(); return; } static bool imuOk = true; if (imuOk && detectGesture()) lastMotionAt = millis(); if (millis() - lastMotionAt > IDLE_SLEEP_S * 1000UL) { // 靜置 90s:睡到按鍵喚醒 esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BTN_B, LOW); esp_deep_sleep_start(); // 深睡喚醒=重新開機=自動重校準,順利的副作用 } delay(20); }

⚙️ 使用流程(開會前 60 秒)

  1. 筆放桌面(或自然握持),滑開電源開關 → LED 亮一下=開始校準,別動它(2 秒)
  2. 筆尾縮回線拉出,插進筆電 USB-A → 系統自帶驅動,1 秒出現 HID keyboard
  3. PPT/Keynote 開始簡報;長按 B 鍵 2 秒=F5 開始、連搖三下=黑燈(B)、再搖三下=回復
  4. 換到陌生的投影環境時,改用實體鍵翻頁(手勢留給練過的場合),這就是「按鍵優先」的設計哲學

🏗️ 組裝步驟

📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
  1. 列印零件:筆身上/下殼、按鍵帽 ×2、底座、縮回線壓蓋
  2. 縮回線預處理:轉接線根部預留 8cm,線頭剝線鍬鋰;根部纏一層熱縮再進壓蓋(斷裂點就在這裡,雙保險)
  3. 焊開關與充電板:滑開關串在電池正極與 VBAT 之間;TP4056 板 B± 先量極性再上錫
  4. 主板與按鍵:按鍵焊在副板(或直接上殼卡位),30AWG 矽膠線走線,拍照記錄方向
  5. MPU6050 鎖前端:模組長邊平行筆身,箭頭朝筆尾(方向裝反=前後頁對調,韌體裡就不用改,改接線)
  6. 上電校準與試飛:開序列埠 115200,看 [CAL] 輸出;接上電腦開 PPT 試翻 10 頁,手勢成功率 <9/10 就把 SWIPE_G 下修 0.05 重試

✅ 驗收測試

測試步驟合格標準
USB HID 枚舉縮回線插電腦,開系統裝置管理員出現 HID Keyboard Device,1 秒內
按鍵零延遲連按 10 次下一頁10/10 頁頁對、無連發
手勢成功率站姿正常甩手 10 次≥9 次翻頁、0 次反向
走路不誤觸握在筆上走動 30 秒零翻頁(走路垂直加速度 <0.2g,遠低於閾值)
斜持穩定斜 45° 握持靜置 10 秒零誤觸(已對參考手姿做差分)
黑燈巨集1.6 秒內連搖三下PPT 黑屏/回復各一次,不誤翻頁
睡眠喚醒靜置 90 秒後按鍵≤2 秒恢复可操作;待機電流表測 <100µA(含分壓漏電)

🆘 問題排除

症狀最可能的原因處方
插電腦沒反應S2 停在 ROM 下載模式/USB 線是純充電線按 RST 退出燒錄模式;換已知可資料線;SuperMini 部分板需按住 BOOT 再按 RST 後立即重插
電腦出現鍵盤但按鍵無效USB.begin() 前先執行了 Keyboard 操作(枚舉未完成)本程式已把 Keyboard.begin()→USB.begin() 放 setup 最前;自行改版請維持順序
WHO_AM_I 讀不到SDA/SCL 接反、模組 VCC 接 5V 超 S2 邏輯I2C 兩線對調試一次;本專案全程 3V3
翻頁方向相反IMU 箭頭裝反(x 軸符號反)把 sendKey 兩個方向參數對調,或重裝模組——韌體一行的事
手勢偶發連翻兩頁冷卻窗短於你的手抖頻率SWIPE_G 上調 0.05 或 COOLDOWN 加到 1200ms
放進口袋自己狂翻頁未關電源開關(深睡要 90 秒才進入)養成「收進口袋前推一下 B 鍵倒數」習慣,或直接關開關——這就是實體開關的價值
充電時無法使用充電電流與 HID 供電共用路徑電壓跌落縮回線拔除電腦改充電;或充飽再上場(40 分鐘)

🎯 進階延伸

📁 專案檔案結構

gesture-presenter/
├── firmware/
│   └── gesture_presenter.ino    # 上方完整韌體
├── stl/
│   ├── pen-top.stl              # 筆身上殼(按鍵窗)
│   ├── pen-bottom.stl           # 筆身下殼(電池艙)
│   ├── keycap-x2.stl            # 按鍵帽
│   ├── dock.stl                 # 充電底座
│   └── cable-clamp.stl          # 縮回線壓蓋(PETG)
├── f3d/
│   └── gesture-presenter.f3d    # Fusion 360 原始檔
├── manual/
│   └── assembly.html            # 圖文組裝說明書
└── README.md                    # 說明文件

🎯 結語

阿立後來帶著這支筆上季報:站在台上,左手自然垂下往左一揮,投影片翻頁,台下安靜,鏡頭沒有一個人注意到他。只有他自己知道筆身裡那顆 28 元的陀螺儀剛剛做了兩次積分。「免驅動」三個字聽起來無聊,但它是簡報開始前那 30 秒裡,你唯一不需要祈禱的事。下一期我們讓煙霧自己報警——一個會看煙速風扇的智慧烙鐵抽風機,烙鐵舉起三秒後,風扇已經在等你了。

← 上一篇:ESP32-C61 斷網守護者 📚 系列索引 下一篇:ESP32-C3 智慧烙鐵抽煙機 →