ESP32 全系列 × 3D 列印實作系列
📡 ESP32-C5 雙頻 Wi-Fi 訊號偵測筆 — 完整實作指南
📅 2026-10-27 · 🏷️ 數位科技 · 📂 ESP32 全系列 × 3D 列印系列 · 27/52 · ⏱️ 實作約 1.5 小時(不含列印)
📋 專案概述
汐止的套房是典型的 20 年舊公寓:承重牆厚到 Wi-Fi 訊號要寫遺書,HiNet 500M 從數據機測是 480M,走到最裡面那間臥室剩 32M。我買了第二台分享器做中繼,裝好之後——好了嗎?「感覺有比較快」是唯一的情報,因為我根本不知道死點在哪、每 AP 各自蓋到哪裡、兩台之間有沒有互相踩腳。這跟醫生不開處方單只說「你感覺比較不痛」是同一種醫療。
問題是「量 Wi-Fi 訊號」這件事在手機上做不好:Android 9 之後系統每 2 分鐘才肯掃一次(背景掃描節流),Wi-Fi Analyzer 開到第二個房間還在轉圈;iOS 更慘,不給 App 完整掃描結果。專業 site survey 工具呢?
- Ekahau AI Pro(專業 site survey):軟體授權一年十幾萬台幣,客戶是 MIS,不是想把 AP 從客廳往陽台挪半公尺的你。
- 網通店的「訊號延伸實測」:免費,但結論通常是「再買一台我們家的 mesh」。
- 筆記型電腦 + inSSIDer:數據對了,但你不可能抱著筆電沿著牆角走一圈、蹲在床緣記錄「床頭左下角 -78dBm」。量測這種事,工具越不像電腦,才量得越勤。
而這整件事在 ESP32 家族裡有一個天生就是為了它存在的晶片:ESP32-C5——全家族唯一支援 5GHz 雙頻與 Wi-Fi 6 的型號。做一支手裡握著、開機就嗶嗶叫的訊號偵測筆:沿牆走一圈,12 顆 LED 燈條就是你的訊號溫度計,死角自己會叫出來。整支材料 660 元:
- 雙頻掃描:2.4G / 5G 單頻過濾掃描(單頻掃比全頻快一倍, 走得動),OLED 秀 RSSI 排序前 6 強 AP
- Walk 走位模式:盯單一 SSID,RSSI 直接映射 12 顆 WS2812 燈條+蜂鳴器音調隨訊號升高(像早期數據機接電話線聽線路那種「訊號音」),低頭走路不用抬頭看螢幕
- Track 連線模式:真的連上家裡 Wi-Fi,即時顯示 RSSI 與連線品質,AP 漫遊切換瞬間抓得到
- Speed 測速模式:HTTP 拉 2MB 實測吞吐(不是只測 ping),每 5 秒一輪,走一圈就知道「32M 臥室」到底有多難看
- 死角紀錄器:Walk 模式下每 1.2 秒一筆自動記錄,長按按鈕把整趟 walk 輸出成 CSV 貼給 GPT/Excel 畫Heatmap
- 18650 充電、充飽走一天:它跟螺絲起子一樣應該待在工作箱裡,不是待充電座上
🔬 為什麼選 ESP32-C5(選型說明)
本系列原則是「依專案需求選型號」,這篇是被規格逼選的:要量 5GHz,家族裡只有 C5。對比表:
| 方案 | 2.4G | 5G | 可連續掃描 | 可走到哪量到哪 | 單價 |
| ESP32-C5(本篇) | ✅ | ✅ 唯一 | ✅ 想掃就掃 | ✅ 筆形、25g | NT$390 |
| ESP32-C3 / C6 / S3 | ✅ | ❌ 硬體沒有 | ✅ | ✅ | NT$150–350 |
| 手機 + Wi-Fi 分析 App | ✅ | ✅ | ❌ 系統節流 2 分鐘/次 | ✅ | 0 |
| 筆記型電腦 + 分析軟體 | ✅ | ✅ | ✅ | ❌ 你不會抱筆電蹲床角 | — |
C5 是 Espressif 首顆雙頻晶片(2.4GHz + 5GHz、Wi-Fi 6、BLE 5),ESP-IDF v5.3+ 提供 esp_wifi_set_band_mode() 可在 2G-only / 5G-only / AUTO 之間切換——掃完 2.4 換 5G,同一支筆量的兩張地圖才可以直接疊圖。Arduino 端要用 arduino-esp32 core ≥ 3.1.0(C5 首次正式支援),本篇所有程式碼以 3.1.x 為準。
⚠️ C5 的腳位生態和 C3/C6 完全不同:strapping 腳是 GPIO2、GPIO3、GPIO7、GPIO25–28(開機敏感),安全通用腳是 GPIO0、1、4、5、6、8、9、10、23、24;GPIO13/14 是原生 USB、GPIO11/12 是 UART0、GPIO27 是板載 RGB 燈。本專案只碰安全腳,照著接就不會踩雷。
🛒 硬體清單
| 零件 | 規格 | 數量 | 單價 | 備註 |
| 主控 | ESP32-C5-DevKitC-1-N8R4 | 1 | 390 | 雙頻 Wi-Fi 6 唯一選項;N8R4 已夠用 |
| 螢幕 | 0.96" SSD1306 OLED I2C | 1 | 95 | 0x3C;走位模式其實靠聽的,螢幕看清單 |
| 燈條 | WS2812B 12 燈條型(8mm 寬) | 1 | 85 | 訊號強度條 本體,5V 供電 |
| 蜂鳴器 | 主動式蜂鳴器 5mm(S8050 驅動) | 1 | 10 | 含 10kΩ 基極電阻 |
| 按鈕 | 6×6 微動開關 | 3 | 9 | MODE / UP / DOWN,按=GND |
| 電源開關 | MSK-12C02 滑動開關 | 1 | 8 | 切 5V 主電源,省待機 |
| 電池 | 18650 3000mAh+單槽座(帶保護) | 1 | 95 | 筆握內藏;燈條全亮約 8 小時 |
| 充電 | TP4056 Type-C 模組(帶保護) | 1 | 20 | Rprog 1.2k≈1A |
| 升壓 | MT3608 模組 | 1 | 15 | 調到 5.0V;燈條+板子 5V 腳一起吃 |
| 電阻分壓 | 100kΩ ×2 | 1組 | 2 | 電壓偵測(ADC 上限 3.1V) |
| 總計 | ≈NT$730(比一支入門 Ekahau 天線便宜一個數量級) |
🔌 接線圖(ESP32-C5 腳位)
ESP32-C5 DevKitC-1 週邊
───────────────── ─────────────────────────────
GPIO4 ────── 藍線 ──── OLED SDA
GPIO5 ────── 綠線 ──── OLED SCL
GPIO8 ──330Ω─ 紫線 ─── WS2812 DIN(燈條資料)
GPIO9 ──10kΩ─ 黃線 ─── S8050 基極 → 蜂鳴器(集極→3V3)
GPIO0 ────── 黑線 ──── 按鈕 MODE → GND
GPIO10 ────── 黑線 ──── 按鈕 DOWN → GND
GPIO23 ────── 黑線 ──── 按鈕 UP → GND
GPIO1 ←─100k/100k 分壓─ 電池+(ADC1,讀 3.7V 電壓)
5V ←────────────── MT3608 OUT(燈條+板 VIN 共吃)
3V3 ──── OLED VCC、蜂鳴器 VCC
GND ═══ 全系統單點共地(電池負極端匯流)
C5 接線眉角:strapping 群(GPIO2/3/7/25/26/27/28)一個都不要接;GPIO27 虽然是板載 RGB 燈但屬 strapping 域,不要外接任何東西上去「借它用」;燈條 5V 從 MT3608 直接拉、不要從板上 3V3 拉(12 燈全亮白+紅=360mA,板載 LDO 會燙會飄)。
🖨️ 3D 列印設計
外形就是一支粗簽字筆:前段探頭(燈條+蜂鳴器)、中段握把(按鈕+螢幕)、後段電池艙(18650)。走位模式是「聽著聲音沿牆走」,筆形讓它像驗電筆一樣自然。
| 零件 | 材質 | 數量 | 列印設定 | 備註 |
| 筆身前殼 | PETG | 1 | 0.2mm/2 壁/25% | 燈條窗+蜂鳴孔,按配 OLED |
| 筆身後殼 | PETG | 1 | 0.2mm/2 壁/25% | 18650 卡榫+TP4056 USB-C 穿孔 |
| 按鈕帽 ×3 | PLA | 3 | 0.2mm/3 壁 | MODE 加紅(凸點可摸辨認) |
| 筆夾 | PETG | 1 | 0.2mm/3 壁/豎印 | 夾工作褲口袋,量測工具要跟著人走 |
| 尾蓋 | PETG | 1 | 0.2mm/2 壁 | 旋入式,換電池用 |
🧵 列印備註:燈條窗留 0.3mm 光帶、別貼膠帶(燈會變霧燈);蜂鳴器位預留 1.5mm 間隙否則聲音悶一半;所有殼件 PETG 是因為這支筆會跟著你蹲陽台、貼鐵窗,PLA 夏天車內會軟。總列印時間約 3.5 小時。
💻 Arduino 程式碼
Board Manager 選 esp32 > ESP32C5 Dev Module,core ≥3.1.0。庫依賴:Adafruit SSD1306、Adafruit NeoPixel。韌體重點:單頻過濾掃描(掃得快)、RSSI 平滑+燈條映射、音調導航、walk 環形紀錄緩衝。
/* ============================================================
* WiFi-Signal-Pen.ino — ESP32-C5 雙頻 Wi-Fi 訊號偵測筆
* ------------------------------------------------------------
* 功能 : 2.4/5GHz 頻段切換掃描、RSSI 燈條、走位蜂鳴導航、
* 連線即時訊號、HTTP 吞吐實測、walk 歷史 CSV 輸出
* 硬體 : ESP32-C5-DevKitC-1・SSD1306 I2C・WS2812B×12・
* 主動蜂鳴器(S8050)・微動×3・18650+TP4056+MT3608
* 供電 : 3.7V 升壓 5V(燈條與板 VIN);逻辑 3V3 板載 LDO
* 相依 : arduino-esp32 core >= 3.1.0、Adafruit SSD1306/NeoPixel
* 備註 : 頻段切換用 esp_wifi_set_band_mode(),需 IDF 支援 5G
* ============================================================ */
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
#include <HTTPClient.h>
#include <esp_wifi.h>
/* --- 1. 腳位定義(只碰 C5 安全腳,避開 strapping 群) --------- */
#define PIN_SDA 4
#define PIN_SCL 5
#define PIN_BAR 8 // WS2812 資料(串 330Ω)
#define PIN_BUZZ 9 // S8050 基極
#define PIN_MODE 0
#define PIN_DOWN 10
#define PIN_UP 23
#define PIN_VBAT 1 // 100k/100k 分壓→ADC1
#define BAR_N 12
/* --- 2. 組態參數 --------------------------------------------- */
#define SCAN_MS 1200 // 掃描週期:再快會掃到一半被打斷
#define LOG_MS 1200 // walk 記錄週期(與掃描同步)
#define LOG_CAP 600 // 環形緩衝 600 筆 ≈ 12 分鐘 walk
#define RSSI_MIN (-95) // 燈條映射下緣
#define RSSI_MAX (-35) // 燈條映射上緣
const char* TRACK_SSID = "YourHomeWiFi"; // Track 模式連線對象
const char* TRACK_PASS = "your_password";
const char* SPEED_URL = "https://speed.cloudflare.com/__down?bytes=2000000";
/* --- 3. 全域狀態 --------------------------------------------- */
enum Mode { M_LIST, M_WALK, M_TRACK, M_SPEED };
Mode mode = M_LIST;
bool band5 = false; // false=2.4G, true=5G
uint8_t selIdx = 0; // 選取 SSID 索引
int8_t rssiSmooth = 0; // EMA 平滑後 RSSI
uint16_t scanCount = 0;
String apSsid[40]; int8_t apRssi[40]; uint8_t apCh[40];
uint32_t logT[LOG_CAP]; int8_t logR[LOG_CAP]; uint16_t logN = 0, logW = 0;
bool trackReady = false;
float lastMbps = 0; bool measuring = false;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
Adafruit_NeoPixel bar(BAR_N, PIN_BAR, NEO_GRB + NEO_KHZ800);
/* --- 4. 按鈕(硬體消抖 RC+軟體雙保險) --------------------- */
struct Btn { uint8_t pin; bool last = HIGH, held = false; uint32_t lastMs = 0;
bool pressed(uint32_t now) { // 放開且按住>30ms = 一回擊
bool raw = digitalRead(pin);
if (raw != last) { last = raw; lastMs = now; }
if (!raw) held = true;
else if (held) { held = false; return (now - lastMs) > 30; }
return false; }
void init() { pinMode(pin, INPUT_PULLUP); }
};
Btn bDown{PIN_DOWN}, bUp{PIN_UP}; // MODE 鍵另店(要分長按/短按)
/* --- 5. 頻段切換:stop→set_band_mode→start ------------------ */
/* 用途:把掃描範圍限制在單一封頻,掃描時間減半、地圖可疊圖
* 參數:five=true 掃 5GHz;false 掃 2.4GHz
* 注意:連網狀態下不可切(Track/Speed 時鎖定) */
void setBand(bool five) {
if (mode == M_TRACK || mode == M_SPEED) return;
band5 = five;
esp_wifi_stop();
esp_wifi_set_band_mode(five ? WIFI_BAND_MODE_5G_ONLY
: WIFI_BAND_MODE_2G_ONLY);
esp_wifi_start();
scanCount = 0; // 切頻後舊清單作廢
}
/* --- 6. 掃描+RSSI 排序 ------------------------------------- */
void doScan() {
int n = WiFi.scanNetworks(false, true); // 同歩+含隱藏網
scanCount = (n > 0) ? min(n, (int)40) : 0;
for (int i = 0; i < scanCount; i++) {
apSsid[i] = WiFi.SSID(i); apRssi[i] = WiFi.RSSI(i); apCh[i] = WiFi.channel(i);
}
// 簡單 bubble sort(n≤40,夠快夠清楚)
for (int i = 0; i < scanCount - 1; i++)
for (int j = 0; j < scanCount - 1 - i; j++)
if (apRssi[j] < apRssi[j+1]) {
int8_t tr = apRssi[j]; apRssi[j] = apRssi[j+1]; apRssi[j+1] = tr;
String ts = apSsid[j]; apSsid[j] = apSsid[j+1]; apSsid[j+1] = ts;
uint8_t tc = apCh[j]; apCh[j] = apCh[j+1]; apCh[j+1] = tc;
}
WiFi.scanDelete();
if (selIdx >= scanCount) selIdx = 0;
}
/* --- 7. 燈條+蜂鳴:RSSI → 視覺與聽覺 -------------------------------- */
void rssiToBar(int8_t rssi) {
// EMA 平滑(α=0.3):單次掃描抖動 ±5dBm 很常見,不濾會閃
rssiSmooth = rssiSmooth ? (int8_t)(0.7 * rssiSmooth + 0.3 * rssi) : rssi;
int lit = map(constrain(rssiSmooth, RSSI_MIN, RSSI_MAX),
RSSI_MIN, RSSI_MAX, 0, BAR_N);
for (int i = 0; i < BAR_N; i++) {
uint32_t c = (i < lit * 2 / 3) ? bar.Color(0, 200, 0)
: (i < lit * 4 / 5) ? bar.Color(220, 180, 0) : bar.Color(200, 30, 0);
bar.setPixel(i, (i < lit) ? c : 0);
}
bar.show();
}
void beepFor(int8_t rssi, uint32_t now) {
// 訊號越強→嗶得越急越高音:低頭走路只听節奏就知道走向
static uint32_t next = 0;
int period = map(constrain(rssi, -95, -35), -95, -35, 900, 90);
if ((int32_t)(now - next) >= 0) {
tone(PIN_BUZZ, map(rssi, -95, -35, 600, 2400), 50);
next = now + period;
}
}
/* --- 8. 模式畫面 --------------------------------------------- */
void drawList() {
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
oled.printf("%s SCAN n=%d\n", band5 ? "5G " : "2.4G", scanCount);
for (int i = 0; i < 5 && i < scanCount; i++) {
oled.setCursor(0, 10 + i * 10);
String s = apSsid[i].substring(0, selIdx == i ? 11 : 14);
oled.printf("%s%-14s %3d\n", selIdx == i ? ">" : " ",
s.c_str(), apRssi[i]);
}
oled.display();
}
/* --- 9. Speed:HTTP 拉 2MB 實測吞吐 --------------------------- */
void speedStart() { measuring = true; }
void speedPump() {
HTTPClient http;
if (!http.begin(SPEED_URL)) { measuring = false; return; }
http.setInsecure(); // 只測位元組數,不驗網
http.setTimeout(8000);
int len = http.GET();
if (len <= 0) { http.end(); measuring = false; return; }
uint32_t t0 = millis(); uint32_t got = 0;
uint8_t buf[2048];
while (got < (uint32_t)len && millis() - t0 < 8000) {
int n = http.getStreamPtr()->read(buf, sizeof(buf));
if (n > 0) got += n;
else if (n < 0) break; // 連線斷了
}
float sec = (millis() - t0) / 1000.0;
lastMbps = sec > 0 ? (got * 8.0 / 1e6) / sec : 0;
http.end(); measuring = false;
}
/* --- 10. CSV dump:長按 MODE 0.9 秒 → 序列埠 ------------------ */
void dumpCsv() {
Serial.println("millis,rssi_dbm,band");
for (uint16_t i = 0; i < logN; i++) {
uint16_t k = (logW + i) % LOG_CAP;
Serial.printf("%lu,%d,%s\n", logT[k], logR[k],
band5 ? "5G" : "2.4G");
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_VBAT, INPUT);
bDown.init(); bUp.init();
Wire.begin(PIN_SDA, PIN_SCL);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
oled.clearDisplay();
bar.begin(); bar.setBrightness(80); bar.show();
WiFi.mode(WIFI_STA); // 初始化 radio
setBand(false); // 預設掃 2.4G
Serial.println(F("WiFi-Signal-Pen online. MODE=切模式, UP/DOWN=選單/切頻, MODE長按=輸出CSV"));
}
void loop() {
uint32_t now = millis();
static uint32_t lastScan = 0, lastLog = 0, lastBtn = 0;
/* --- 11. 按鈕輪詢(10ms 節奏+按住偵測) ------------------ */
static uint32_t modeHold = 0;
if (!digitalRead(PIN_MODE) && !modeHold) modeHold = now;
if (digitalRead(PIN_MODE)) {
if (modeHold && now - modeHold > 900) dumpCsv(); // 長按輸出
else if (modeHold) { // 短按切模式
mode = (Mode)((mode + 1) % 4);
if (mode == M_TRACK && !trackReady) {
WiFi.mode(WIFI_STA); WiFi.begin(TRACK_SSID, TRACK_PASS);
}
if (mode != M_TRACK && trackReady) { WiFi.disconnect(); trackReady = false; }
}
modeHold = 0;
}
if ((now - lastBtn) > 60) {
lastBtn = now;
if (bUp.pressed(now)) { if (mode == M_LIST) selIdx = (selIdx + 1) % max(scanCount,1); else setBand(!band5); }
if (bDown.pressed(now)) { if (mode == M_LIST && selIdx > 0) selIdx--; }
}
/* --- 12. 模式分派 ---------------------------------------- */
if (mode == M_LIST) {
if (now - lastScan > SCAN_MS) { lastScan = now; doScan(); }
rssiToBar(scanCount ? apRssi[min(selIdx, (uint8_t)scanCount-1)] : -95);
drawList(); noTone(PIN_BUZZ);
}
else if (mode == M_WALK) {
if (now - lastScan > SCAN_MS) {
lastScan = now; doScan();
int8_t r = scanCount ? apRssi[0] : RSSI_MIN;
if (scanCount > selIdx) r = apRssi[selIdx];
// 記錄進環形緩衝(給走完的 CSV 用)
logT[logW] = now; logR[logW] = r;
logW = (logW + 1) % LOG_CAP; if (logN < LOG_CAP) logN++;
rssiToBar(r);
oled.clearDisplay(); oled.setTextSize(2);
oled.printf("%d dBm\n", r);
oled.setTextSize(1); oled.printf("WALK %s %s\n",
band5?"5G":"2.4G", scanCount?apSsid[selIdx].c_str():"-");
oled.display();
}
if (scanCount) beepFor(rssiSmooth, now);
}
else if (mode == M_TRACK) {
if (WiFi.status() == WL_CONNECTED) {
trackReady = true;
int8_t r = WiFi.RSSI();
if (now - lastScan > 400) { lastScan = now; rssiToBar(r);
oled.clearDisplay(); oled.setTextSize(2);
oled.printf("%d dBm\n", r);
oled.setTextSize(1);
oled.printf("%s ch%d %.0fMbps\n", WiFi.SSID().c_str(),
WiFi.channel(), lastMbps);
oled.display(); }
beepFor(r, now);
} else if (now - lastScan > 2000) { // 逾時重試,不閃瞎
lastScan = now; WiFi.disconnect(); WiFi.begin(TRACK_SSID, TRACK_PASS);
}
}
else { // M_SPEED:每 5 秒一輪
if (!measuring && now - lastScan > 5000) {
lastScan = now; speedStart();
}
if (measuring) speedPump();
oled.clearDisplay(); oled.setTextSize(2);
oled.printf("%.1f M\n", lastMbps);
oled.setTextSize(1); oled.printf("SPEED %d dBm\n", WiFi.RSSI());
oled.display();
rssiToBar(WiFi.RSSI()); noTone(PIN_BUZZ);
}
/* --- 13. 低壓保護:剩 10% 蜂鳴提醒+亮燈減半 ---------------- */
float vb = analogReadMilliVolts(PIN_VBAT) * 2.0 / 1000.0; // ×2 分壓
static bool warned = false;
if (vb < 3.45f && !warned) { warned = true; bar.setBrightness(40); }
if (vb > 3.6f) warned = false;
yield();
}
🔧 組裝步驟
- 先把燈條剪成 12 燈(剪口處焊線:5V/GND/DIN),串 330Ω 後預粘前殼燈條槽——燈條方向有箭頭,焊反就是 12 顆裝飾燈。
- 蜂鳴器接 S8050:發射極 GND、集極+蜂鳴器負端、蜂鳴器正端接 3V3,基極走 10kΩ 到 GPIO9。用萬用表二極體檔確認三腳沒焊連。
- OLED、按鈕依接線圖上螺絲端子台(本系列愛用可焊端子台:後勤維修不用熔鐵)。共地只留電池負極一點,燈條地線直接回電池座,不繞板子。
- MT3608 先空載調到 5.00V 再接載;接上後量板上 VIN 是否 5V、燈條全亮白時不跌破 4.8V,跌了代表升壓太小或電池座彈片電阻大。
- 燒機:板上按 BOOT→USB 插電腦→Arduino IDE 選 ESP32C5 Dev Module 上傳。Serial 115200 看開機行。
- 試機再合殼:MODE 切四模式各跑 30 秒、UP 切頻見 5G AP 清單、長按 MODE 見 CSV 行流。全過再鎖後殼。
🧪 驗證
| 測試 | 動作 | 合格線 |
| RSSI 可信度 | 同位置與手機 Wi-Fi 分析 App(剛掃完的新鮮值)對照主 AP | 差值 ≤±4dBm(天線位置差屬正常) |
| 雙頻可見性 | UP 切 5G 重掃 | 家附近 5G AP 數量 >0;同一 SSID 出現兩頻段不同 RSSI(5G 低 10dB 以內屬正常) |
| Walk 節拍 | 從 AP 旁 1m 走到最遠房間 | 節拍從急→緩連續變化無跳格;燈條遞減無閃爍 |
| CSV 完整性 | 走 3 分鐘後長按 MODE,序列埠存檔數行 | 行數 ≈ 150 筆(1.2 秒/筆),無斷檔 |
| 測速交叉比對 | Track→Speed 模式,與同位置speedtest 網站對照 | 同數量級(±20%,CDN 節點差異屬正常) |
| 電流/續航 | 串電表量走位模式平均電流 | ≤180mA(燈條亮一半)→ 3000mAh 撐 >16 小時 |
🆘 問題排除
| 症狀 | 最可能的原因 | 處方 |
| 切 5G 後掃到 0 個 AP | 某些 5G AP 開 DFS 被動掃描,主動掃描探不到;或你的 AP 5G 頻寬設了 160MHz 而掃描限 20/40MHz | 先別懷疑晶片:拿手機確認該頻道 AP 本來就看得到;掃描改 WiFi.scanNetworks(false, true, true) 用被動掃描試 |
| 開機就重啟、燈條一亮就斷訊 | 升壓沒調/燈條從板上 3V3 拉電、brownout | MT3608 重調 5.0V;燈條電源改直吃升壓輸出;燈條端補 470µF |
| OLED 沒反應(I2C 掃不到) | 0x3C/0x3D ADDR 猜錯、SDA/SCL 接反 | 先跑 i2c_scanner;0x3C 改 0x3D 試一次;線最常見是反 |
| 蜂鳴器不嗶但燈條正常 | S8050 三腳順序焊錯(EBC 因廠牌不同);基極沒串電阻燒了腳 | 量基極對 GND 應有 0.6V 壓降在按嗶瞬間出現;換腳位順序重焊 |
| CSV 行數遠少於預期 | SCAN_MS 和實際掃描時間混了:掃描在走位模式被蜂鳴 tone() 阻塞了排程(tone 是背景没问题,但 scanNetworks 同歩阻塞 1 秒) | 記錄週期與掃描同步的設計屬正常(每掃一筆記錄一筆);想要更密就改 async 掃描+onScanCompleted 回調 |
| Track 模式連不到 | SSID 含中文/隱藏網路要先掃到再連;5G-only 的 SSID 在 2.4G band mode 下連不上 | 連線前把 band 切回 AUTO:esp_wifi_set_band_mode(WIFI_BAND_MODE_AUTO),Track/Speed 模式本函式已處理 |
| 按 BOOT 鍵才開機 | GPIO0 被當成 strapping 腳誤接了下拉負載 | C5 的 boot 腳是 GPIO26/28 不是 GPIO0——查是不是 MODE 鍵之外又在 GPIO0 掛了大電容,拔掉 |
📁 專案檔案結構
wifi-signal-pen/
├── firmware/
│ └── wifi_signal_pen.ino # 上方完整韌體
├── stl/
│ ├── pen-front.stl # 筆身前殼
│ ├── pen-back.stl # 筆身後殼(電池艙)
│ ├── button-cap.stl # 按鈕帽 ×3
│ ├── pen-clip.stl # 筆夾
│ └── end-cap.stl # 旋入尾蓋
├── f3d/
│ └── signal-pen.f3d # Fusion 360 原始檔
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件
🎯 結語
裝完那天晚上,我拿著這支筆沿牆走了三圈,聽著蜂鳴器從急嗶走成慢嗶,在書房北牆拐角蹲下來把 AP 往陽台方向挪了 80 公分——臥室從 32M 變 210M。這 80 公分你永遠不可能用「感覺」找到,它只藏在一條可以被走出來的曲線里。Wi-Fi 是家裡最寬的一條馬路,卻是唯一沒人畫地圖的;Ekahau 賣的地圖一年十幾萬,這支筆的地圖 660 元加半小時散步。下一期我們把家裡的網路到底斷在哪一層也交給一顆最新最便宜的晶片看門——ESP32-C61 斷網守護者,線路一掛它比誰都先知道。走著瞧。