📋 專案概述

汐止的套房是典型的 20 年舊公寓:承重牆厚到 Wi-Fi 訊號要寫遺書,HiNet 500M 從數據機測是 480M,走到最裡面那間臥室剩 32M。我買了第二台分享器做中繼,裝好之後——好了嗎?「感覺有比較快」是唯一的情報,因為我根本不知道死點在哪、每 AP 各自蓋到哪裡、兩台之間有沒有互相踩腳。這跟醫生不開處方單只說「你感覺比較不痛」是同一種醫療。

問題是「量 Wi-Fi 訊號」這件事在手機上做不好:Android 9 之後系統每 2 分鐘才肯掃一次(背景掃描節流),Wi-Fi Analyzer 開到第二個房間還在轉圈;iOS 更慘,不給 App 完整掃描結果。專業 site survey 工具呢?

而這整件事在 ESP32 家族裡有一個天生就是為了它存在的晶片:ESP32-C5——全家族唯一支援 5GHz 雙頻與 Wi-Fi 6 的型號。做一支手裡握著、開機就嗶嗶叫的訊號偵測筆:沿牆走一圈,12 顆 LED 燈條就是你的訊號溫度計,死角自己會叫出來。整支材料 660 元:

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

本系列原則是「依專案需求選型號」,這篇是被規格逼選的:要量 5GHz,家族裡只有 C5。對比表:

方案2.4G5G可連續掃描可走到哪量到哪單價
ESP32-C5(本篇)✅✅ 唯一✅ 想掃就掃✅ 筆形、25gNT$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-N8R41390雙頻 Wi-Fi 6 唯一選項;N8R4 已夠用
螢幕0.96" SSD1306 OLED I2C1950x3C;走位模式其實靠聽的,螢幕看清單
燈條WS2812B 12 燈條型(8mm 寬)185訊號強度條 本體,5V 供電
蜂鳴器主動式蜂鳴器 5mm(S8050 驅動)110含 10kΩ 基極電阻
按鈕6×6 微動開關39MODE / UP / DOWN,按=GND
電源開關MSK-12C02 滑動開關18切 5V 主電源,省待機
電池18650 3000mAh+單槽座(帶保護)195筆握內藏;燈條全亮約 8 小時
充電TP4056 Type-C 模組(帶保護)120Rprog 1.2k≈1A
升壓MT3608 模組115調到 5.0V;燈條+板子 5V 腳一起吃
電阻分壓100kΩ ×21組2電壓偵測(ADC 上限 3.1V)
總計≈NT$730(比一支入門 Ekahau 天線便宜一個數量級)

🔌 接線圖(ESP32-C5 腳位)

ESP32-C5 Wi-Fi 訊號偵測筆 · 線路示意 ESP32-C5 DevKitC-1 GPIO4 SDAGPIO5 SCLGPIO8 燈條GPIO9 蜂鳴GPIO0 MODEGPIO10 DOWNGPIO23 UPGPIO1 VBAT . SSD1306 0.96" I2C 0x3C SDA·G4 SCL·G5 3V3 GND WS2812B ×12(訊號 bar) DIN·G8+330Ω 5V(MT3608) GND 共地 BUZ S8050 G9+10kΩ GND MODE DOWN UP 按鈕按下=GND(內部上拉) 18650 3.7V TP4056 充電・開關 →MT3608 5V 入板 VIN 100k/100k→G1
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)。走位模式是「聽著聲音沿牆走」,筆形讓它像驗電筆一樣自然。

零件材質數量列印設定備註
筆身前殼PETG10.2mm/2 壁/25%燈條窗+蜂鳴孔,按配 OLED
筆身後殼PETG10.2mm/2 壁/25%18650 卡榫+TP4056 USB-C 穿孔
按鈕帽 ×3PLA30.2mm/3 壁MODE 加紅(凸點可摸辨認)
筆夾PETG10.2mm/3 壁/豎印夾工作褲口袋,量測工具要跟著人走
尾蓋PETG10.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(); }

🔧 組裝步驟

  1. 先把燈條剪成 12 燈(剪口處焊線:5V/GND/DIN),串 330Ω 後預粘前殼燈條槽——燈條方向有箭頭,焊反就是 12 顆裝飾燈。
  2. 蜂鳴器接 S8050:發射極 GND、集極+蜂鳴器負端、蜂鳴器正端接 3V3,基極走 10kΩ 到 GPIO9。用萬用表二極體檔確認三腳沒焊連。
  3. OLED、按鈕依接線圖上螺絲端子台(本系列愛用可焊端子台:後勤維修不用熔鐵)。共地只留電池負極一點,燈條地線直接回電池座,不繞板子。
  4. MT3608 先空載調到 5.00V 再接載;接上後量板上 VIN 是否 5V、燈條全亮白時不跌破 4.8V,跌了代表升壓太小或電池座彈片電阻大。
  5. 燒機:板上按 BOOT→USB 插電腦→Arduino IDE 選 ESP32C5 Dev Module 上傳。Serial 115200 看開機行。
  6. 試機再合殼:MODE 切四模式各跑 30 秒、UP 切頻見 5G AP 清單、長按 MODE 見 CSV 行流。全過再鎖後殼。
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到書桌 → 開啟 HTML 組裝說明書

🧪 驗證

測試動作合格線
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 拉電、brownoutMT3608 重調 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 斷網守護者,線路一掛它比誰都先知道。走著瞧。

← 上一篇:ESP32-C6 陽台風速風向塔 📚 系列索引 下一篇:ESP32-S3 桌上型顯微鏡 →