给一只毛绒玩偶装上感官,全程免焊接。板子按连到哪个 WiFi 自动切换在家 / 外出两种模式,出门不用重刷固件:
在家(家里WiFi) 外出(手机热点)
[玩偶] ──流式原始值──▶ 本机翻译官 [玩偶] ──板上判定动作──▶ 公网中继(nginx+relay.py)
touch_server.py │ HTTPS 只在有事件时发
自动体检 / 动作分类 ▼
│ 本机/服务器 poller.py 轮询取事件
▼ │
┌────────────────── 汇合 ───────────────────────┘
▼
[记录本] touch_moments.jsonl + 异常触发 trigger.py → Claude主动推送给你
目录
- 方案:从"想法"到"能落地的清单"
- 硬件准备(购买清单)
- 认识板子:先找到 6 类引脚
- 组装第一步:搭两条电源总线
- 接 5 路 FSR 压感
- 接泪水电极
- 接 MPU6050 发抖模块
- 塞进玩偶:位置布局
- 烧固件
- 跑起来 & 自检
- 调参与排错
1. 方案:
几个关键决策,以及为什么这么选:
| 决策点 | 选择 | 原因 |
|---|---|---|
| 主控 | ESP32-S3 | 自带 WiFi(能连家里/热点/VPS)、ADC 通道够用、板载 WS2812 灯可做反馈 |
| 免焊方式 | 带线传感器 + Wago/螺丝端子 | 传感器出厂就带引线,配 Wago 免焊连接器即可,全程无需烙铁、无需面包板 |
| 压感选型 | FSR402 ×4 + FSR406 ×1 | FSR402 小圆片贴爪子/头顶/后背;FSR406 面积大,贴肚子这种大面 |
| 发抖 | MPU6050 | 便宜、I2C 好接、能测三轴加速度,用来算抖动能量 |
| 泪水 | 导电线自制电极+导电布 | 两根导电线当"液体开关",比买成品液位计更贴合"缝在爪子表面"的需求,导电布增加接触面积 |
| 分压 | 每路一个 10K 电阻 | FSR / 液体本身是"可变电阻",必须配一个固定电阻组成分压才能被 ADC 读出 |
2. 硬件准备(购买清单)
| 器件 | 数量 | 说明 |
|---|---|---|
| ESP32-S3 开发板(如 YD-ESP32-S3,N8R2/N8R8/N16R8 均可) | 1 | 板载 WS2812 灯在 GPIO48;双 Type-C,认准带 COM 标记的口下载 |
| ESP32-S3 扩展底板(螺丝端子/排针) | 1 | 把细密排针变成好接的螺丝端子,免焊更省心 |
| 带线 FSR402(小圆片压感) | 4 | 左爪、右爪、头顶、后背 |
| 带线 FSR406(大面压感) | 1 | 肚子 |
| MPU6050 模块 | 1 | 发抖检测(I2C) |
| 不锈钢导电线 / 缝纫用导电线 | 少量 | 做泪水电极(两根) |
| 导电布 | 两块 | 连接泪水电极 |
| 10K 电阻(1/4W 直插) | 6 | 5 个给 FSR、1 个给泪水,做分压 |
| Wago 免焊连接器 | 5 孔 ×2、3 孔 ×4 | 用来搭 3.3V 和 GND 两条"总线" |
| 软硅胶线(22~26AWG) | 一小卷 | 加长/连接用,柔软不硌玩偶 |
| USB-C 数据线 | 1 | ⚠ 一定要数据线,纯充电线认不到端口 |
| 热缩管 / 绝缘胶带 | 少量 | 包裸露接头,防短路 |
除了 ESP32-S3、FSR、MPU6050、10K 电阻这几样要选对型号,其余(线、Wago、热缩管)
随便买规格匹配的就行。
3. 认识板子:先找到 6 类引脚
接线前,在板子(或扩展底板)上把这几类脚认出来:
| 标记 | 用途 | 本项目用到 |
|---|---|---|
3V3 |
3.3V 电源输出 | 所有传感器的"热脚"都接它(可能有多个 3V3 脚,都通用) |
GND |
地 | 所有 10K 电阻的"冷脚"、MPU 的 GND(多个 GND 脚都通用) |
GPIO3/5/6/7/8 |
模拟输入(ADC) | 接 5 路 FSR 的信号 |
GPIO4 |
模拟输入(ADC) | 接泪水信号 |
GPIO12 / GPIO13 |
I2C | MPU6050 的 SDA / SCL |
GPIO48 |
板载灯 | 固定,不用接线 |
最终接线一览(和固件里完全对应):
| 通道 | 位置 | 器件 | GPIO |
|---|---|---|---|
bd |
肚子 | FSR406 | 5 |
hd |
头顶 | FSR402 | 7 |
pr |
右爪 | FSR402 | 6 |
pl |
左爪 | FSR402 | 3 |
bk |
后背 | FSR402 | 8 |
tear |
右爪表面 | 导电线电极 | 4 |
| 发抖 | 胸/背 | MPU6050 | SDA=12 / SCL=13 |
总接线示意图
一张图看全:上面一条 3.3V 总线、下面一条 GND 总线,中间每一路都是
"传感器 + 10K"的分压,信号从中间的"分压点"引到对应 GPIO。MPU6050 不用分压,
直接 4 根线。
3.3V 总线 ●━━━┳━━━━┳━━━━┳━━━━┳━━━━┳━━━━━┳━━━━●
│ │ │ │ │ │ │
FSR406 FSR FSR FSR FSR 电极A MPU
肚子 头顶 右爪 左爪 后背 泪水 VCC
│ │ │ │ │ ┊水 │
信号点 → • • • • • 电极B• │ (• 引线去 GPIO)
G5 G7 G6 G3 G8 G4 │
│ │ │ │ │ │ │
10K 10K 10K 10K 10K 10K │
│ │ │ │ │ │ │
GND 总线 ●━━━┻━━━━┻━━━━┻━━━━┻━━━━┻━━━━━━━●
MPU: SDA→G12 SCL→G13
4. 组装第一步:搭两条电源总线
- 所有传感器有一只脚要吃 3.3V(5 个 FSR 各一脚 + 泪水电极 A + MPU 的 VCC = 7 处)。
- 所有 10K 电阻的另一脚要接 GND(6 个电阻 + MPU 的 GND = 7 处)。
板子上 3V3 / GND 的孔位有限,塞不下这么多线。Wago 就是个"排插"——
把它想象成插线板:一个 Wago 里所有孔都是内部导通的,插进去的线彼此相连。
于是我们用 Wago 把"1 个 3.3V"扩成"7 个 3.3V",GND 同理。
搭 3.3V 总线(要 7 个出口):把两个 5 孔 Wago 串起来:
板子 3V3 ──▶[5孔Wago A]── 其中1孔用短线跳到 ──▶[5孔Wago B]
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
3个出口 4个出口 = 共 7 个 3.3V 出口
- Wago A:1 孔进(来自板子 3V3),1 孔用一小段线"跳线"到 Wago B,剩 3 孔做出口。
- Wago B:1 孔进(来自跳线),剩 4 孔做出口。
- 合计 3 + 4 = 7 个 3.3V 出口,正好够 5 FSR + 泪水 A + MPU VCC。
搭 GND 总线(要 7 个出口):优先直接用板子上多个 GND 脚(扩展底板一般有好几个),
不够再用 3 孔 Wago 补。比如用 2 个 3 孔 Wago:
板子 GND ─▶[3孔Wago] 另一路 板子 GND ─▶[3孔Wago]
│ │ │ │
▼ ▼ ▼ ▼
2出口 2出口 + 板子自带 GND 脚若干 = 凑够 7
5. 接 5 路 FSR 压感
每个 FSR 都是分压接法,三根线:
3.3V总线 ──── FSR一只脚
FSR另一只脚 ──┬── GPIO端子(信号)
│
[10K]
│
GND总线
也就是:FSR 一脚 → 3.3V 总线;FSR 另一脚 → 同时接到 GPIO 和一个 10K 电阻的一脚;
10K 电阻另一脚 → GND 总线。GPIO 读到的就是"FSR 和 10K 的分压点",越用力压读数越高。
带线 FSR 和 10K 电阻怎么免焊接到一起:把 FSR 的信号线、10K 的一只脚、去 GPIO 的线,
三根拧在一起塞进同一个 Wago 孔(或螺丝端子),10K 另一脚单独进 GND 总线的 Wago。
6. 接泪水电极
泪水检测本质是一个液体导通开关,用的是和 FSR 一模一样的分压电路,只是把"压敏电阻"换成"两根裸露的导电电极+两块隔开的导电布":
3.3V ──┐
│
[电极 A +导电布] ← 埋在爪子表面
┊ ← 干燥时:两极之间是干燥的吸水布料,电阻≈无穷大
[电极 B +导电布] ← 埋在爪子表面,另一根
│
├──────────▶ GPIO4 (ADC 读这里的电压)
│
[10K]
│
GND

- 干燥:A、B 两极之间是布料,几乎不导通 → 分压点被 10K 拉到接近 GND → ADC 读数很低。
- 有泪水/水滴:水把两极桥接起来,电阻骤降 → 3.3V 经过"水+电极"分到 GPIO,读数抬高。
所以"湿不湿"就是看 ADC 读数是否越过一个阈值(电阻越大越灵敏,但也越容易被噪声/汗气误触)。
电极材料:不锈钢导电线 / 导电布 / 缝纫用导电线都行,两根互不接触地缝在爪子表面,间距几毫米。
7. 接 MPU6050 发抖模块
发抖 ≠ 倾斜,也 ≠ 移动。发抖的特征是高频、来回、幅度小的抖动。所以我们不能看加速度的"绝对值"(那会把"被举起来"也当成抖),而要看它波动得有多厉害。
做法分三步:
- 合成幅值:MPU6050 给出三轴加速度
ax, ay, az(单位 g)。合成总幅值
[ m = \sqrt{a_x^2 + a_y^2 + a_z^2} ]
静止时m ≈ 1(只有重力)。不管玩偶朝哪放,静止时 m 都稳定在 1 附近。 - 在一个时间窗内收集很多个 m(固件里每拍收满一小段 buffer)。
- 算这段 m 的标准差(就是"波动能量"):
[ E = \sqrt{\dfrac{1}{n}\sum (m_i - \bar m)^2} ]- 静止:m 几乎不变 → 标准差 ≈ 0。
- 缓慢倾斜/平移:m 变化平滑但也小 → 标准差小。
- 发抖:m 快速上下跳 → 标准差大。
用标准差而不是均值,正好过滤掉"重力/姿态"这种恒定分量,只留下"抖动"这种交流分量。这就是为什么倾斜、举起来都不会误报成发抖。
- 接线
MPU6050 是 I2C 器件,四根线:
| MPU6050 引脚 | 接到 |
|---|---|
| VCC | 3V3(⚠ 别接 5V) |
| GND | GND |
| SDA | GPIO12 |
| SCL | GPIO13 |
I2C 地址默认 0x68(AD0 悬空/接地)。接反 SDA/SCL 或接了 5V 是最常见的接不上原因。其余引脚(XCL/XDA/AD0/INT)悬空即可。
- 固件实现
初始化:MPU6050 上电默认在睡眠,要往电源管理寄存器 0x6B 写 0 唤醒:
void setup() {
Serial.begin(115200);
delay(200);
analogReadResolution(12);
Wire.begin(I2C_SDA, I2C_SCL);
mpuWrite(0x6B, 0x00);
delay(50);
读加速度:从寄存器 0x3B 连读 6 字节(X/Y/Z 各两字节),量程 ±2g 对应比例因子 16384:
bool mpuReadAccel(float &ax, float &ay, float &az) {
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B);
if (Wire.endTransmission(false) != 0) return false;
Wire.requestFrom(MPU_ADDR, 6);
if (Wire.available() < 6) return false;
int16_t x = (Wire.read() << 8) | Wire.read();
int16_t y = (Wire.read() << 8) | Wire.read();
int16_t z = (Wire.read() << 8) | Wire.read();
ax = x / 16384.0; ay = y / 16384.0; az = z / 16384.0;
return true;
}
算波动能量(标准差):
float shakeEnergy() {
if (shakeN < 2) return 0;
float mean = 0;
for (int i = 0; i < shakeN; i++) mean += shakeBuf[i];
mean /= shakeN;
float var = 0;
for (int i = 0; i < shakeN; i++) { float d = shakeBuf[i] - mean; var += d * d; }
return sqrt(var / shakeN);
}
采样节奏:主循环里每次都读一发 MPU 塞进 buffer(比主节拍高频,采得密),到了节拍才把这段 buffer 算成一个能量值、然后清空重来。注意末尾 * 1000.0 是把很小的标准差放大到好设阈值的量级:
void loop() {
// 高频采 MPU
float ax, ay, az;
if (mpuReadAccel(ax, ay, az) && shakeN < 64) shakeBuf[shakeN++] = sqrt(ax*ax + ay*ay + az*az);
unsigned long now = millis();
unsigned long interval = awayMode ? AWAY_TICK_MS : HOME_SEND_MS;
if (now - lastTick >= interval) {
lastTick = now;
int vals[N], maxv = 0;
for (int i = 0; i < N; i++) { vals[i] = readCh(i); if (vals[i] > maxv) maxv = vals[i]; }
float shake = shakeEnergy() * 1000.0;
shakeN = 0;
外出模式判定:能量超阈值 → 进入"抖动中"并记峰值;掉回阈值以下且持续够久(>0.3s)→ 报一条"发抖":
// 发抖
if (shake > EV_SHAKE_THR) {
if (!shakeActive) { shakeActive = true; shakeStart = now; shakePeak = shake; }
else if (shake > shakePeak) shakePeak = shake;
} else if (shakeActive) {
shakeActive = false;
unsigned long d = now - shakeStart;
if (d > 300) postEvent("全身", "发抖", (int)shakePeak, d / 1000.0);
}
8. 塞进玩偶:位置布局
- FSR 贴在对应部位的皮下(玩偶布料和棉花之间),感压面朝外,
用一点点棉花垫在上面让按压能传导过来。 - 泪水电极缝在右爪表面(要能碰到"眼泪")。
- MPU6050 固定在身体中心(胸/背),别晃动,否则自身松动会被当成抖动。
- 所有线顺着一个方向理到 ESP32-S3 所在的位置(一般放后背开口处),
裸露接头用热缩管/胶带包好,避免棉花里两根线打架短路。
9. 烧固件
打开 Arduino IDE :
- Board 选 ESP32S3 Dev Module(选成普通 "ESP32" 会报
This chip is ESP32-S3, not ESP32)。 - Flash Size 按你的模组选(如 16MB),PSRAM 选 OPI PSRAM(N8R8/N16R8)。
- Tools → Port 选出现的串口(看不到端口:换数据线 / 按住 BOOT 再插 / 装 CH343 驱动)。
- 点上传。串口监视器(115200)会打印连网状态、IP、以及上报日志。
10. 跑起来 & 自检
去摸玩偶,终端应实时打印,例如:
13:04:23 肚子 · 抱住不放 · 力度3182 · 5.74秒
13:04:40 右爪 · 擦眼泪 · 力度1487 · 1.20秒
13:05:02 全身 · 发抖 · 力度88.0 · 2.10秒
逐路自检建议:
- 依次按 肚子/头顶/左爪/右爪/后背,确认每路都能打印且部位名对得上(对不上=GPIO 接错)。
- 蘸一点水抹右爪 + 同时按右爪,应打出"擦眼泪"(只沾水不按 → 不报,这是双条件设计)。
- 拿起来快速抖几下,应打出"发抖"(缓慢倾斜/举高 → 不报)。
11.调参与排错
泪水
| 现象 | 原因 | 解决 |
|---|---|---|
| 干燥时也报湿 | 阈值太低 / 电极距离太近 / 下拉电阻太大 | 提高 EV_TEAR_THR(固件) 或 TEAR_THRESHOLD(主机);电极拉开一点;确认用的是 10K 下拉 |
| 真擦了不报 | 阈值太高 / GND 没接好 / 只满足单条件 | 降低阈值;检查 10K 到 GND;确认擦时右爪也被按住(双条件缺一不可) |
| 一闪而过误报 | 去抖太松 | 增大 EV_TEAR_HOLD / TEAR_HOLD_TICKS(每 +1 约多要求 50~100ms 持续湿) |
| 读数整体偏移 | 电极氧化/汗渍 | 擦净电极;必要时换导电线 |
发抖
| 现象 | 原因 | 解决 |
|---|---|---|
| 串口/日志里 MPU 读不到 | 接了 5V / SDA-SCL 接反 / 地址不对 | VCC 必须 3V3;核对 SDA=12、SCL=13;地址默认 0x68 |
| 静止也报发抖 | 阈值太低 / 放在震动面上 | 提高 EV_SHAKE_THR / SHAKE_THRESHOLD |
| 真抖不报 | 阈值太高 / 采样太稀 | 降低阈值;确认 loop() 里每圈都在采 MPU(buffer 要够密) |
| 举起来/走动被当成发抖 | 理论上不会(用的是标准差不是幅值);若发生多半是持续颠簸 | 适当提高阈值,或把 d > 300(ms) 的最短持续时间调长 |
剩余异常事件主动触发等已有成熟方案实现,此教程略。