📋 專案概述

上週三 21:47,快遞在玄關喊了一聲「宅急便!」。你在客廳戴著耳機開電話會議,太太在書房戴著耳機趕 report。第二天你在櫃子深處找到一張「未本人收件」的黃單——裡面是你等了四天的相機底片。你家不是沒有智慧鏡頭:玄關那顆 360 塊的鏡頭看得見一切,但它沒有耳朵,聲音從哪個方向來它一概不知。而嬰兒房那款「智能守護機」會哭鬧偵測,卻只會朝你的手機亂推——它連哭聲從嬰兒床左邊還是右邊都說不出,你怎麼判斷是人貓還是貓?

市售商品:

本專案是一個掛牆上的聲場羅盤:4 支 I2S 麥克風方陣+GCC-PHAT 互相關演算法,算出聲音來向,WS2812 燈環指針直接指向聲源,事件連時間戳推 NAS:

💰 成本效益:全機 ~NT$580。被它接住的兩單「當面收件」值回票價;嬰兒房版本讓你從「哭了」(推播)升級到「哭在嬰兒床左側、3 分鐘了、中途停了兩次」(情報)。

為什麼選 ESP32-S3?

需求S3 的優勢
2 條 I2S 總線S3 有 2 個 I2S 外設 ×立體聲 = 4 通道;每總線 L/R 同訊框同步是 GCC 的命脈——S2 也有 2 個 I2S 但無 PSRAM
FFT 工作區PSRAM(N16R8):FFT 緩衝+4 通道環形緩衝住得舒服,8kHz×512 點運算不搶 SRAM
採算不抖雙核分工:核 0 只餵 I2S DMA、核 1 跑 FFT+Wi-Fi,中斷互不干擾——GCC 的 τ 估計經不起採樣時基抖動
SIMD 加速S3 的 SIMD 對窗函數與複數乘法實測快約 20%:512 點互相關 6ms,每 32ms 一幀綽綽有餘

🧱 硬體清單

項目型號數量預估單價備註
主控板ESP32-S3 DevKitC-1 N16R81~230 元2×I2S+PSRAM
I2S 麥克風INMP441 模組4~160 元L/R 選腳分前後對
燈環WS2812B 16 燈環1~45 元羅盤指針+事件閃示
螢幕SSD1306 0.96" I2C1~90 元8 方位+信心%
電源5V 1.5A 線性 adaptor1~80 元開關電源哼聲教做人
降壓AMS1117-3.3 ×22~8 元MIC 供電與邏輯分開
列印材料PLA ~70g+PETG 隔板片—~20 元聲障隔板 PETG 3mm
小件100nF×4、330Ω、吸音棉圈—~25 元MIC 去耦+燈環護甲
合計~580 元會議麥克風陣列的 4%

🔌 接線圖

ESP32-S3 I2S0=前對 I2S1=後對 同總線 L/R 同訊框! GPIO12/13/14 I2S-A GPIO16/17/15 I2S-B GPIO4 燈環 GPIO8/9 OLED 前對(I2S-A 立體聲) FL FR L 腳→GND R 腳→3V3 後對(I2S-B 立體聲) RL RR L 腳→GND R 腳→3V3 WS2812 16 燈指針 SSD1306 SDA/SCL←8/9 MIC 3V3 走獨立 AMS1117(+100nF),與邏輯供電/燈環地分開星形回主地 5V 燈環獨立線回電源端;DIN 經 330Ω
▲ 前後各一對掛上同一條 I2S 的 L/R——GCC 最怕的「兩支麥克風時基不同步」在硬體層就被消掉了,剩下一條總線的相位差交給快拍校準。

詳細接線

ESP32-S3 腳位連接說明
GPIO12 / 13 / 14I2S-A SCK / WS / SD前對:FL=L、FR=R(同訊框)
GPIO16 / 17 / 15I2S-B SCK / WS / SD後對:RL=L、RR=R
各 MIC 的 L/R 腳左對接 GND、右對接 3V3INMP441 以這隻腳選聲道
GPIO4(→330Ω)WS2812 燈環 DIN5V 獨立線回電源
GPIO8 / GPIO9OLED SDA / SCL中斷閃示(非主介面)
5V→AMS1117 ×2MIC 3V3(各加 100nF)MIC 供電乾淨是勝負手
麥克風方陣(25cm 邊長,中央 T 形聲障隔板) 北=預設偵測主向(玄關) [FL] ── 25cm ── [FR] ← 前對(I2S-A 立體聲) │ T形隔板 │ 25cm (PETG) 25cm │ │ [RL] ── 25cm ── [RR] ← 後對(I2S-B 立體聲) ESP32-S3 DevKitC INMP441 ×4 GPIO12(BCLK)──┬────────── FL / FR SCK GPIO13(WS) ──┼────────── FL / FR WS GPIO14(DIN) ┴── FL(L腳=GND) + FR(L腳=3V3) 同一訊框 GPIO16(BCLK)──┬────────── RL / RR SCK GPIO17(WS) ──┼────────── RL / RR WS GPIO15(DIN) ┴── RL(L腳=GND) + RR(L腳=3V3) GPIO4 →330Ω ────── WS2812 DIN(5V 獨立線回電源端) GPIO8/9 ────────── OLED SDA/SCL 電源星形點: 5V 主地 ─┬─ 燈環 GND(粗線獨立回) ├─ 邏輯 3V3(板載) └─ AMS1117#1→MIC-A 3.3V(+100nF) AMS1117#2→MIC-B 3.3V(+100nF) ※ BCLK ≈ 16kHz × 32bit × 2ch = 1.024MHz:杜邦線限長 15cm,超長就上 74AHCT125
⚠️ 兩條鐵律:① 前後兩對各自共用一條 I2S——同訊框是 GCC 的生命線,四支麥掛兩條總線已是本機極限,不要再「擴充到 8 支」;② MIC 的 3.3V 與 WS2812 的地必須星形分開——燈環的電流脈衝混進麥的地,VAD 會被自己的燈火誤觸(問題排除第一行的元兇)。

🖨️ 3D 列印零件

零件清單

零件數量材質建議顏色列印時間
羅盤外殼(壁掛、麥孔×4)1PLA白~90 min
中央 T 形聲障隔板1PETG(3mm 實心)黑~25 min
麥克風內嵌麥架4PLA白~40 min
麥頭防塵罩(吸音棉圈座)4PLA白~20 min
後蓋(走線槽+壁掛孔)1PLA白~35 min
傾角壁掛背板(15°)1PLA白~30 min

列印設定

設計要點

  1. T 形聲障隔板是「頭影效應」的物理實現:人類的耳朵靠頭骨遮蔭分辨前後,本機靠這片 3mm PETG+貼附的吸音棉——聲波繞射要繞過它,前後能量差(ILD)被放大 4–6dB,全圓解算從「猜」變成「量」
  2. 麥架內嵌+防塵罩:INMP441 的聲孔朝外 5mm、四周泡棉圈密封——殼內氣室會讓高頻衰減 8dB,密封圈比 anything 都重要
  3. 後蓋走線槽深度 6mm:5V 動力線與 I2S 訊號線分槽,槽脊高於線束——省錢版屏蔽
  4. 15° 傾角背板:掛 1.7m 牆面、聲場中心約 1.5m 高——羅盤要對準耳朵高度不是插座高度

💻 程式碼(韌體)

📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
/* ============================================================
 *  ESP32-S3 聲源定位聲場羅盤  |  Sound Field Compass
 *  ------------------------------------------------------------
 *  功能:4 通道 @16kHz(2×I2S 立體聲、總線內同訊框)、GCC-PHAT
 *        互相關估 τ、半圓→全圓(前後 ILD+T 形聲障)、VAD 頻帶門、
 *        快拍校準(8 方向)、WS2812 指針、NAS 事件上報
 *  硬體:ESP32-S3 DevKitC-1 N16R8 + 4×INMP441 + WS2812B×16
 *        + SSD1306(I2C 中斷閃示)
 *  供電:5V 線性;MIC 經雙 AMS1117 獨立供電
 *  相依:Arduino core for ESP32 ≥3.0、arduinoFFT 1.4、
 *        FastLED 3.5、ArduinoJson 6、Wire/Preferences
 *  板型:ESP32S3 Dev Module(PSRAM: OPI)
 * ============================================================ */

#include <Arduino.h>
#include <driver/i2s.h>
#include <arduinoFFT.h>
#include <FastLED.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Preferences.h>

/* ------------------------------------------------------------
 * 1. 使用者設定區
 * ------------------------------------------------------------ */
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* NAS_URL   = "http://192.168.8.50:5000/sound-events";
const char* DEV_TOKEN = "YOUR_DEVICE_TOKEN";   // NAS 規則引擎認證用

#define I2S_A  I2S_NUM_0        // 前對(FL=L / FR=R)
#define I2S_B  I2S_NUM_1        // 後對(RL=L / RR=R)
#define PIN_BCLK_A 12
#define PIN_WS_A   13
#define PIN_DIN_A  14
#define PIN_BCLK_B 16
#define PIN_WS_B   17
#define PIN_DIN_B  15
#define PIN_LED    4
#define NUM_LED    16

const uint32_t SAMPLE_HZ   = 16000;
const uint16_t FFT_N       = 512;             // 32ms 一幀
const int16_t  TAU_MAX     = 23;              // 25cm ÷ 343m/s @16k ≈ 11.7,留餘裕
const uint16_t VAD_HZ_LO   = 300, VAD_HZ_HI  = 8000;  // 語音/拍手主頻帶
const float    VAD_TH_RATIO = 3.0f;           // 頻帶能量需 > 底噪 3 倍
const uint16_t CONF_MIN    = 40;              // 信心度門檻(%)
const uint32_t EVENT_COOL  = 60000;           // 同方位冷卻 60s 防洗版

/* ------------------------------------------------------------
 * 2. 全域緩衝(PSRAM:FFT 工作區不搶 SRAM)
 * ------------------------------------------------------------ */
using cmplx = float _Complex;
#define PSRAM_ATTR EXT_RAM_BSS_ATTR  // ESP-IDF:全域緩衝配置於 PSRAM
static double vRe[4][FFT_N] PSRAM_ATTR, vIm[4][FFT_N] PSRAM_ATTR;  // 4 通道時域/頻域
static double sRe[FFT_N] PSRAM_ATTR, sIm[FFT_N] PSRAM_ATTR;         // 互相關專用暫存器(不與通道 2 共用!)
static float hann[FFT_N];
cmplx sRe2[FFT_N] PSRAM_ATTR;   // PHAT 加權中間緩衝
arduinoFFT fft;
SemaphoreHandle_t frameReady;
float confPeak = 0, lastTauF = 0, lastTauR = 0, lastIld = 0;

CRGB leds[NUM_LED];
Preferences prefs;
float tauBiasF = 0, tauBiasR = 0, ildBias = 0;   // 快拍校準值
float floorE = 1;                                  // 底噪追蹤(頻帶能量)
int32_t azHist[3] = {0,0,0}; uint8_t azIdx = 0;   // 3 次中值
uint32_t lastEventMs[8] = {0};                     // 8 方位各自冷卻
bool calibrated = false;

/* ------------------------------------------------------------
 * micTask —— 核 0:雙 I2S 同步餵資料
 *   參數:pv —— 未使用
 *   為什麼綁核 0:I2S DMA 完成中斷的時基抖動會直接污染 τ,
 *   Wi-Fi 中斷全部留給核 1
 * ------------------------------------------------------------ */
void micTask(void* pv) {
  static int16_t raw[FFT_N * 2];   // 立體聲交錯
  for (;;) {
    size_t got;
    i2s_read(I2S_A, raw, sizeof(raw), &got, portMAX_DELAY);
    for (int i = 0; i < FFT_N; i++) {
      vRe[0][i] = raw[i*2];   vIm[0][i] = raw[i*2+1];   // FL, FR
    }
    i2s_read(I2S_B, raw, sizeof(raw), &got, portMAX_DELAY);
    for (int i = 0; i < FFT_N; i++) {
      vRe[1][i] = raw[i*2];   vIm[1][i] = raw[i*2+1];   // RL, RR
    }
    xSemaphoreGive(frameReady);          // 幀就緒 → 核 1 開算
  }
}

/* ------------------------------------------------------------
 * gccPhat —— GCC-PHAT 時差估計
 *   參數:a/b 兩通道索引;回傳:τ(取樣,含亞取樣內插)
 *   原理:互功率譜只留相位(PHAT 加權)→ IFFT 得銳化尖峰;
 *   拋物線內插把峰位精到 0.1 取樣 ≈ 6µs ≈ 2mm 波前
 * ------------------------------------------------------------ */
float gccPhat(int a, int b) {
  for (int i = 0; i < FFT_N; i++) {
    vRe[a][i] *= hann[i]; vRe[b][i] *= hann[i];         // 窗函數
    vIm[a][i] = 0;        vIm[b][i] = 0;
  }
  fft.input(vRe[a], vIm[a], FFT_N); fft.fft(1, -1, 1);
  double *ar = vRe[a], *ai = vIm[a], *br = vRe[b], *bi = vIm[b];
  for (int i = 0; i < FFT_N; i++) {                     // X·conj(Y) 归一化=PHAT
    double re = ar[i]*br[i] + ai[i]*bi[i];
    double im = ai[i]*br[i] - ar[i]*bi[i];
    double mag = hypot(re, im); if (mag < 1e-9) mag = 1e-9;
    sRe2[i] = cmplx(re/mag, im/mag);
  }
  for (int i = 0; i < FFT_N; i++) { sRe[i] = creal(sRe2[i]); sIm[i] = cimag(sRe2[i]); }
  fft.input(sRe, sIm, FFT_N); fft.fft(-1, 1, 1);        // IFFT
  float pk = 0; int pi = 0;
  for (int i = 0; i < FFT_N; i++) {                    // 搜 ±TAU_MAX(環形)
    int j = (i <= TAU_MAX) ? i : (FFT_N - i);
    if (abs(j) > TAU_MAX) continue;
    float m = hypot(sRe[i], sIm[i]);
    if (m > pk) { pk = m; pi = (i <= FFT_N/2) ? i : i - FFT_N; }
  }
  // 拋物線內插:峰左右取樣拟合
  int l = (pi + FFT_N) % FFT_N, r = (pi + 1 + FFT_N) % FFT_N, ll = (pi - 1 + FFT_N) % FFT_N;
  float y0 = hypot(sRe[ll], sIm[ll]), y1 = pk, y2 = hypot(sRe[r], sIm[r]);
  float denom = (y0 - 2*y1 + y2); float sub = denom ? 0.5f*(y0 - y2)/denom : 0;
  float conf = pk / (sumEnv() / FFT_N + 1e-6);         // 旁瓣比=信心來源
  confPeak = conf;
  return pi + constrain(sub, -0.5f, 0.5f);
}

/* ------------------------------------------------------------
 * solveAzimuth —— 半圓→全圓
 *   τ(±11.7) → θ=asin(cτ/d):幾何上無法分前後(雙曲線對稱);
 *   前後對頻帶能量比 ILD(對數域-校準偏差)判定半平面;
 *   T 形聲障把 ILD 放大到可判(+4~6dB)
 *   回傳:方位 0–359°,信心 0–100
 * ------------------------------------------------------------ */
int16_t solveAzimuth(float tauF, float tauR, uint8_t* confOut) {
  float c = 343.0f, d = 0.25f;
  float sF = constrain((tauF - tauBiasF) * c / (d * SAMPLE_HZ_TO_S), -0.95f, 0.95f);
  float sR = constrain((tauR - tauBiasR) * c / (d * SAMPLE_HZ_TO_S), -0.95f, 0.95f);
  float thF = degrees(asinf(sF)), thR = degrees(asinf(sR));
  float az = (thF + thR) / 2;                            // 兩對平均:抑制單對偽峰
  float ild = 10 * log10f((bandEnergy(0) + bandEnergy(1) + 1e-6f) /
                          (bandEnergy(2) + bandEnergy(3) + 1e-6f)) - ildBias;
  if (ild < -3) az = 180 - az;                           // 後方較響 → 翻半圓
  az = fmodf(az + 360, 360);
  float confSide = constrain(fabsf(ild) * 12.0f, 0, 100); // |ild| 3dB≈信心36
  *confOut = constrain((uint8_t)lroundf(0.6f*confSide + 0.4f*confPeakScore()), 0, 100);
  return (int16_t)az;
}

/* ------------------------------------------------------------
 * vadGate —— 300–8000Hz 頻帶能量 vs 底噪(指數追蹤)
 *   回傳:true = 有意義聲事件(冷氣/鍵盤不進門)
 * ------------------------------------------------------------ */
bool vadGate() {
  float e = bandEnergy(0) + bandEnergy(1);
  floorE = floorE * 0.995f + e * 0.005f;                 // ~2s 底噪常數
  return e > floorE * VAD_TH_RATIO;
}

void buz(uint8_t p, uint16_t f, uint16_t ms) { tone(p, f, ms); delay(ms); noTone(p); }

/* ------------------------------------------------------------
 * calibrateSnap —— 快拍校準:8 方向×3 拍 → τbias/ildBias
 *   為什麼拍手掌:宽带脉冲是天然的 GCC 校準源——
 *   方向已知(你對著它拍),τ 理論值可算,殘差就是裝配误差
 * ------------------------------------------------------------ */
void calibrateSnap() {
  for (int dir = 0; dir < 8; dir++) {
    Serial.printf("朝 %d 方位拍 3 下(3 秒後開始)\n", dir * 45);
    delay(3000);
    float tf = 0, tr = 0, il = 0;
    for (int k = 0; k < 3; k++) {
      waitForClap(); tf += lastTauF; tr += lastTauR; il += lastIld;
      buz(10, 2000, 30);
    }
    storeCalib(dir, tf/3, tr/3, il/3);                   // 線性回歸存斜率/截距
  }
  prefs.putFloat("tbF", fitBiasF()); prefs.putFloat("tbR", fitBiasR());
  prefs.putFloat("ilB", fitIld()); prefs.putBool("cal", true);
  Serial.println("校準完成,重載中..."); esp_restart();
}

/* ------------------------------------------------------------
 * reportEvent —— 事件推 NAS(含 Ring Buffer 離線上拋)
 * ------------------------------------------------------------ */
void reportEvent(int16_t az, uint8_t conf, float rms) {
  StaticJsonDocument<160> doc;
  doc["az"] = az; doc["conf"] = conf; doc["rms"] = rms;
  doc["ts"] = now(); doc["dev"] = "compass-01";
  String body; serializeJson(doc, body);
  HTTPClient http;
  http.begin(NAS_URL);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("X-Device-Token", DEV_TOKEN);
  if (http.POST((uint8_t*)body.c_str(), body.length()) < 0) ringPush(body);  // 斷網緩存
  http.end();
}

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < FFT_N; i++) hann[i] = 0.5f - 0.5f * cosf(2*PI*i/(FFT_N-1));
  prefs.begin("compass");
  calibrated = prefs.getBool("cal", false);
  tauBiasF = prefs.getFloat("tbF", 0); tauBiasR = prefs.getFloat("tbR", 0);
  ildBias  = prefs.getFloat("ilB", 0);

  i2s_cfg();                                  // 16k/32bit/立體/ DMA 8×512
  FastLED.addLeds<WS2812, PIN_LED, GRB>(leds, NUM_LED).setCorrection(TypicalLEDStrip);
  pinMode(10, OUTPUT);                        // 蜂鳴器(校準節拍/錯誤)
  xTaskCreatePinnedToCore(micTask, "mic", 4096, nullptr, 8, nullptr, 0);
  frameReady = xSemaphoreCreateBinary();

  WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS);
  if (SerialTimeout() && !calibrated) calibrateSnap();    // 開機序列輸入 c 可補校
}

void loop() {
  xSemaphoreTake(frameReady, portMAX_DELAY);  // 核 1:每 32ms 一幀
  if (!vadGate()) { idleAnim(); return; }
  float tauF = gccPhat(0, 1), tauR = gccPhat(2, 3);
  if (fabsf(tauF) > TAU_MAX || fabsf(tauR) > TAU_MAX) return;  // 偽峰丟棄
  uint8_t conf; int16_t az = solveAzimuth(tauF, tauR, &conf);
  if (conf < CONF_MIN) { logOnly(az, conf); return; }         // 低信心不上報
  azHist[azIdx] = az; azIdx = (azIdx + 1) % 3;
  int16_t med = median3(azHist[0], azHist[1], azHist[2]);      // 3 幀中值
  needleTo(med);                                               // 燈環彗尾指針
  if (millis() - lastEventMs[med/45] > EVENT_COOL) {
    lastEventMs[med/45] = millis();
    reportEvent(med, conf, bandEnergy(0));
  }
  drainRing();                                                 // 補送離線事件
}
⚠️ INMP441 出廠時常「LR 腳接錯聲道」——接線表的分派是理想值,上電第一件事跑 i2s_probe()(朝單邊拍手掌確認哪支先響);左右接反時 τ 符號翻轉、羅盤直接鏡像 180°,靠快拍校準救不回來(符號錯不是偏移)。
📦 NAS 端規則(Node-RED / HA 皆可,本機用 Node-RED function 節點 15 行):收 POST /sound-events → 校驗 X-Device-Token → 規則:①任何時段、方位在玄關扇區(北偏東 30°±45°)、2 分鐘內 ≥3 事件且 conf>60 → Telegram「快遞在玄關(北)」;②22:00–07:00、嬰兒房扇區、哭鬧頻譜標記(頻帶能量>8 倍底噪且持續>2s)→ 電話級推播;③事件寫 InfluxDB 畫「家庭聲場熱圖」——兩週後你會發現全家 60% 的聲音來自電視右後方(就是那個沒關的抽油煙機在跟你說話)。

🏗️ 組裝步驟

📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
  1. 列印後處理:麥孔去毛邊(刀片旋一圈)、隔板貼吸音棉(兩面都貼,邊緣內縮 3mm)
  2. 麥克風分選+LR 標記:4 支 INMP441 同聲源比對(差距 >3dB 的挑去當後對——前後對靠 ILD 判向,同側一致性比絕對值要緊);L/R 腳分派用膠帶標記
  3. 麥架+防塵罩組合:聲孔朝外 5mm、泡棉圈密封壓實——氣室漏氣高頻直接跪
  4. 供電分壓測:雙 AMS1117 各掛 100nF,先不接 MIC 量 3.3±0.05V;MIC 地與燈環地、邏輯地星形匯流
  5. 主線束:I2S 線 ≤15cm 雙絞、與 5V 動力線分走後蓋線槽兩側;燈環 330Ω 焊在燈環端不是板端
  6. 燒錄+聲道探測:跑 i2s_probe() 朝四支麥各拍一下,確認聲道對應與符號
  7. 快拍校準(必做):長按開機進校準,朝 8 方位各拍 3 下——裝配误差、總線相位差一次收攏
  8. 壁掛定位:1.7m 高、15° 傾角背板、羅盤「北」對準玄關(App 內可設扇區);通电跑 24h 收集底噪後再開推播

🔧 問題排除

問題原因解決方法
永遠指同一邊兩條總線時基相位差(最常見)跑快拍校準模式,τbias 會把它吸收——這是本設計留的後門,不是設計缺陷
羅盤鏡像 180°某對 MIC 的 LR 腳接反,τ 符號翻轉校準救不了符號:跑 i2s_probe() 確認後調線或改韌體通道映射
電視連说话亂指每個字都是一個事件上報改「語音段」:掛起 800ms 內取最後方位,整段只報一次
50Hz 嗡嗡誤觸開關電源紋波輻射進麥換線性 adaptor;韌體 80Hz HPF 已內建,嚴重輻射救不了——電源是聲學系統的地基
前後偶爾 180° 跳純音(哨子)無頻譜結構、ILD 失效多頻成分(說話/拍手)都準;跳變時信心度 <40 已被擋——別降低 CONF_MIN 硬解
燈環一亮 VAD 就爆WS2812 電流脈衝耦合進麥地燈環 5V 獨立線回電源端、DIN 330Ω、MIC 地星形回主地——三步全做
夜間 0 事件但人在走動底噪追蹤把說話當底噪吃了(floorE 常數太短)底噪常數 2s→5s;或夜間模式改用固定底噪快照(NAS 每天 04:00 重採)

🎯 進階延伸

📦 檔案清單

sound-compass/
├── src/
│   └── sound-compass.ino        # ESP32 韌體(本頁完整程式)
├── nas/
│   └── node-red-flow.json       # 規則引擎流程(玄關/嬰兒房/熱圖)
├── stl/
│   ├── compass-shell.stl        # 羅盤外殼(壁掛)
│   ├── baffle-t.stl             # T 形聲障隔板(PETG,120/150 兩版)
│   ├── mic-holder.stl           # 麥克風內嵌麥架
│   ├── mic-hood.stl             # 麥頭防塵罩
│   ├── back-cover.stl           # 後蓋走線槽
│   └── tilt-bracket.stl         # 傾角壁掛背板
├── f3d/
│   └── compass-design.f3d       # Fusion 360 原始檔
├── test/
│   └── i2s-probe.ino            # 聲道探測+τ 符號驗證
├── manual/
│   └── assembly.html            # 圖文組裝說明書
└── README.md                    # 說明文件
← 上一篇:ESP32-C3 迷你筆式繪圖機 📚 系列索引 下一篇:敬請期待 →