# esp32Swich **Repository Path**: JackMoHeiHei/esp32Swich ## Basic Information - **Project Name**: esp32Swich - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-14 - **Last Updated**: 2026-09-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ESP32 智能开关 基于 **ESP32 Relay AC X1 v1.1**(ESP32-WROOM-32E)的智能开关固件, **开箱即开热点,连上热点用网页完成全部初始化**,支持巴法云远程控制与在线 OTA 升级。 - 固件版本:`1.2.0` - 开发框架:PlatformIO + Arduino ESP32 core 2.0.17 - 编译占用:Flash 约 1.10 MB / 1.88 MB(59%),RAM 约 61 KB / 320 KB(19%) - 不使用蓝牙:射频完全归 WiFi,省下约 240 KB Flash,也避开了 ESP32 上 WiFi/BT 共存带来的一系列坑 --- ## 一、硬件对应关系 | 功能 | 引脚 | 说明 | |---|---|---| | 继电器 | **IO16** | 默认高电平吸合,可在网页改引脚与极性 | | 按键 | **IO0** | 板载按键,低电平有效 | | 状态指示灯 | 默认不启用 | 如有外接 LED,可在网页指定引脚 | > **不确定继电器接哪个 IO?** > 网页【硬件】页面有「引脚探测」:选一个引脚点一下,该引脚输出 0.8 秒脉冲, > 听到继电器"咔哒"就是它。候选引脚已按常见接法预置。 > > **继电器反向动作(该开时关)?** > 【硬件】→ 触发电平 切到"低电平吸合"即可,切换后逻辑状态保持不变。 ### 按键功能(IO0) | 操作 | 功能 | |---|---| | 单击 | 切换继电器开/关 | | 双击 | 开启应急热点(忘记密码 / 换了路由器时的补救入口) | | 三击 | 重启设备 | | **长按 10 秒** | 恢复出厂设置(清空全部配置) | 长按过程中指示灯会从 50% 起快闪提示,**松手即取消**,不会误触。 --- ## 二、快速开始 ### 1. 编译与烧录 ```bash # 编译 pio run # 烧录(自动识别串口) pio run -t upload # 看串口日志 pio device monitor ``` 网页文件在 `web/index.html`,编译时由 `tools/build_web.py` 自动 gzip 后 生成 `include/web_assets.h` 内嵌进固件——**不需要单独烧录文件系统**, OTA 升级也不会丢页面。改完网页直接 `pio run` 即可。 ### 2. 首次配网 出厂默认就是 **AP 热点模式**,上电即可配置: 1. 上电后设备开热点 `ESP32-Switch-XXXX`,密码 `12345678` 2. 手机连上该热点,通常会自动弹出配置页;没弹出就浏览器打开 `http://192.168.4.1` 3. 登录(默认 `admin` / `admin123`) 4. 【网络】→ 点"扫描周边网络"选你的 WiFi(也可以直接手动输入名称)→ 填密码 → 保存 保存后设备会**先用 AP+STA 过渡**:热点继续开着,同时去连路由器。 连上后页面上能看到新的 IP,热点再过 15 秒才关闭—— 这样既能看到配网结果,密码填错时也不会失去补救入口。 > **关于"扫描周边网络"**:ESP32 只有一套射频,扫描时要逐个信道搜索, > 期间热点会有 1~2 秒的短暂卡顿。固件采用**逐信道扫描**(每次只离开约 200ms > 就回到热点信道),页面也会自动重试,正常情况下不会掉线。 > 若你的手机对断流比较敏感,直接手动输入 WiFi 名称即可,不影响使用。 ### 3. 日常访问 联网后可通过以下任一地址访问: - `http://设备IP`(串口日志里会打印,路由器后台也能看到) - `http://esp32sw-XXXX.local`(mDNS,同局域网内可用) > ⚠️ **首次登录后请立刻在【系统】页面修改默认密码**(页面顶部会一直提醒)。 --- ## 三、接入巴法云 > 📌 **顺序不能反**:必须先在巴法云 **MQTT 控制台「新建主题」**,设备才会出现在控制台里。 > 这是官方文档的"步骤一"。设备订阅一个**尚未创建**的主题时 MQTT 同样会返回订阅成功, > 日志里也会显示"已订阅",但控制台里看不到这台设备 —— > 这是"MQTT 连上了、控制台却没有设备"最常见的原因。 1. 在 [bemfa.com](https://cloud.bemfa.com) 注册,在控制台拿到 **32 位私钥** 2. **先**在 MQTT 控制台新建主题,例如 `switch001` 3. 再到网页【巴法云】页面:勾选启用 → 填私钥 → 填同名主题 → 保存 **主题名后三位数字决定设备类型**,网页会据此生成对应的控制卡片: | 后缀 | 类型 | 后缀 | 类型 | |---|---|---|---| | 001 | 开关 / 插座 | 005 | 空调 | | 002 | 灯 | 006 | 风扇 | | 003 | 窗帘 | 007 | 电视 | | 004 | 传感器 | | | 最多可绑定 4 个主题。勾选"绑定继电器"的主题会真正驱动继电器, 其余主题只在网页上显示状态卡片(留给后续扩展多路用)。 **服务器参数**:`bemfa.com:9501`(加密端口 9503),客户端 ID 即私钥,用户名密码留空。 多台设备共用一个私钥时,在"客户端后缀"里填不同的值,避免互相顶号。 ### 上报状态用哪个主题? 巴法云对**推送**的主题名有后缀约定(官方文档"set 指令 / up 指令"): | 发布到 | 含义 | 用途 | |---|---|---| | `主题/up` | **只更新云端数据,不做任何推送** | 设备上报自身状态(本固件默认) | | `主题/set` | 推送给该主题的所有订阅者,发送者自己收不到 | 由控制端下发指令 | | `主题` | 旧版接入方式 | 兼容老配置 | 控制台和手机 App 显示的开关状态,**取的就是云端数据**,所以设备上报必须用 `/up` (或 `/set`),发到裸主题名可能不会刷新控制台显示。 默认已是 `/up`,如需更改在【巴法云】→ 状态上报方式 里切换。 设备这边**订阅裸主题名**(`switch001`),这样 App 发到 `switch001/set` 的指令能收到。 下行消息若带 `/set` 之类的后缀,固件会自动剥掉后缀再匹配。 > 注意:巴法云只支持 QoS 0 / 1,**使用 QoS 2 会被强制下线**,次数多了账号会异常。 > 本固件收发一律使用 QoS 0。 --- ## 四、OTA 在线升级 **网页上传**:【升级】→ 选择 `.pio/build/esp32dev/firmware.bin` → 开始升级。 **远程 URL**:把固件放到任意 HTTP 服务器,在【升级】页填直链地址即可。 **检查更新**(可选):配置一个清单地址,返回如下 JSON: ```json { "version": "1.3.0", "url": "http://your-server/firmware.bin", "notes": "修复了…" } ``` ### 升级安全性 - 双分区 OTA:写入的是备用分区,**升级失败仍从原固件启动**,不会变砖 - 校验固件头魔数 `0xE9`,传错文件直接拒绝 - 升级前自动停掉 MQTT 连接,把内存和射频让给升级过程 - **配置保留**:配置以 JSON 存在 NVS,升级不清空;新版本新增的参数自动取默认值, 删掉的参数被忽略——所以老配置在新固件上一定能正常加载(见下文"配置兼容性") --- ## 五、目录结构 ``` esp32Swich/ ├── platformio.ini PlatformIO 配置(依赖、分区表、编译选项) ├── include/ │ ├── app_config.h 出厂默认值、容量上限等编译期常量 │ └── web_assets.h 自动生成(gzip 网页),勿手改 ├── web/ │ └── index.html 网页后台源码(编译时自动打包进固件) ├── tools/ │ └── build_web.py 构建前钩子:网页 → gzip → C 数组 ├── src/ │ ├── main.cpp 启动流程、回调接线、主循环、健康巡检 │ ├── core/ │ │ ├── Logger.* 日志:串口 + 环形缓冲(供网页增量拉取) │ │ ├── ConfigManager.* 配置中心:NVS 持久化、版本迁移、导入导出 │ │ └── SysInfo.* 芯片 / 内存 / Flash / 分区 / 温度信息 │ ├── io/ │ │ ├── RelayController.* 继电器:极性、上电策略、节流、统计、引脚探测 │ │ ├── ButtonHandler.* IO0 按键状态机:消抖 / 多击 / 长按 │ │ └── StatusLed.* 指示灯:不同闪烁节奏表达不同状态 │ ├── net/ │ │ ├── WiFiService.* AP/STA、DHCP/静态、扫描、退避重连、应急热点 │ │ ├── TimeService.* NTP 校时与时区 │ │ ├── MqttService.* 巴法云接入、订阅/上报、退避重连 │ │ ├── WebService.* 网页后台:认证、REST API、强制门户、OTA 上传 │ │ └── OtaService.* OTA:本地上传 / URL 升级 / 检查更新 │ └── app/ │ └── ScheduleService.* 定时任务 └── android/ ⚠ 早期的蓝牙配网 App,固件已移除蓝牙,此目录已失效 ``` **模块间是回调解耦的**,接线集中在 `main.cpp` 的 `wireCallbacks()`, 想改行为(比如让单击变成别的动作)只需要改那一处。 --- ## 六、稳定性设计 | 措施 | 位置 | |---|---| | 任务看门狗(默认 30 秒,可配) | `main.cpp: setupWatchdog()` | | 全模块非阻塞轮询,无 `while(!connected)` 死等 | 各模块 `tick()` | | WiFi 断线指数退避重连(15/30/60/120 秒) | `WiFiService::tick()` | | MQTT 断线指数退避重连(5→60 秒封顶) | `MqttService::tick()` | | 连不上路由器自动开应急热点,永远留有入口 | `WiFiService`(apFallback) | | 配网时用 AP+STA 过渡,联网后延迟 15 秒才关热点 | `WiFiService::applyConfig()` | | 逐信道扫描,避开内核 6 秒内部超时并减少热点中断 | `WiFiService::startChannelScan()` | | 内存监控:连续 5 次低于阈值主动重启自愈 | `main.cpp: healthCheck()` | | 可选定期重启(默认关闭) | 【系统】→ 定期自动重启 | | 配置写入后回读校验 + 备份槽回滚 | `ConfigManager::writeNvs()` | | 继电器最小动作间隔,保护触点 | `RelayController::set()` | | 上电先输出断开电平再设为输出,避免瞬间误吸合 | `RelayController::begin()` | | 登录失败 5 次锁定 5 分钟 | `WebService::handleLogin()` | | 日志用固定环形缓冲,无动态分配、不产生堆碎片 | `Logger` | --- ## 七、配置兼容性(升级后为什么不会丢设置) 配置以 **JSON 文本**存在 NVS,而不是结构体二进制转储。加载流程是: 1. 先把内存里的配置填成出厂默认值 2. 再用 NVS 里的 JSON **逐字段覆盖**——JSON 里没有的字段就保持默认值 所以: - 新固件**新增**的参数 → 老配置里没有 → 自动取默认值 ✅ - 新固件**删除**的参数 → 老配置里多余 → 直接忽略 ✅ - 字段**改名/换单位** → 在 `ConfigManager::migrate()` 里写一条迁移规则 `CONFIG_VERSION` 用于识别老版本并触发 `migrate()`。当前为 `3`。 --- ## 八、网页 API 一览 所有 `/api/*` 接口(除 `login`)都需要令牌, 通过 `X-Auth-Token` 请求头或 `ESPTOKEN` Cookie 携带。 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/login` `/api/logout` | 登录 / 登出 | | GET | `/api/status` | 运行状态总览(网页每 3 秒轮询) | | GET | `/api/sysinfo` | 芯片/固件/分区等静态信息 | | GET | `/api/logs?since=N` | 增量拉取日志 | | POST | `/api/logs/clear` | 清空日志 | | GET/POST | `/api/config` | 读取 / 增量更新配置 | | GET | `/api/config/export` | 导出配置备份 | | POST | `/api/config/import` | 导入配置备份 | | POST | `/api/relay` | `{"action":"on\|off\|toggle"}` | | POST | `/api/relay/probe` | 引脚探测 `{"pin":16,"pulseMs":800}` | | GET | `/api/wifi/scan[?start=1]` | 启动扫描 / 取结果 | | POST | `/api/wifi/test` | 测试连接(结果看日志) | | POST | `/api/mqtt/reconnect` `/api/mqtt/publish` | 重连 / 发消息 | | POST | `/api/time/sync` | 立即校时 | | GET | `/api/schedules` | 定时任务列表 | | GET | `/api/ota/status` | 升级状态 | | POST | `/api/ota/upload` | 上传固件(multipart) | | POST | `/api/ota/url` `/api/ota/check` | URL 升级 / 检查更新 | | POST | `/api/system/reboot` | 重启 | | POST | `/api/system/factory` | 恢复出厂(需 `{"confirm":"RESET"}`) | **密码字段的约定**:读取配置时密码一律返回 `********`; 保存时如果传回的还是 `********` 就表示"不修改",传别的值才会更新。 --- ## 九、常见问题 **Q:继电器不动作 / 反着动作** A:【硬件】页面用「引脚探测」确认引脚,再切换触发电平。 **Q:忘记网页密码了** A:长按 IO0 十秒恢复出厂设置,设备会回到出厂的热点模式重新配置。 **Q:点"扫描周边网络"时热点卡顿 / 页面转圈** A:正常现象。ESP32 只有一套射频,扫描时必须逐个信道搜索,热点会短暂离开。 固件已改为逐信道扫描(每次只离开约 200ms)并且页面会自动重试, 通常 3 秒左右出结果。如果你的手机还是容易断,**直接手动输入 WiFi 名称** 同样可以保存,扫描不是必须的。 **Q:保存 WiFi 后页面打不开了** A:设备联网成功 15 秒后会关掉热点,此时手机应切回自己的路由器, 再用日志里打印的新 IP 或 `http://esp32sw-XXXX.local` 访问。 如果密码填错没连上,热点会一直保留,重连热点改正即可。 **Q:巴法云连上了,但控制台里看不到设备** A:九成是**主题没有在 MQTT 控制台创建**。巴法云要求先建主题、设备再订阅; 订阅一个不存在的主题不会报错,所以日志看起来一切正常。 去控制台新建同名主题后,点【巴法云】→"立即重连"即可。 另外确认主题名大小写完全一致,以及上报方式保持默认的 `/up`。 **Q:巴法云一直连不上** A:看【巴法云】页面的"最近错误"。常见原因: 私钥填错(错误码 2 客户端 ID 被拒)、端口填成了 9503(TCP 创客云的端口,MQTT 要用 9501)、 多台设备共用私钥互相顶号(填不同的"客户端后缀")。 **Q:CPU 温度看着不准** A:ESP32(非 S2/S3/C3)的片内温度传感器没有出厂校准,**只能看趋势,不能看绝对值**, 网页上已注明。若需要准确温度请外接 DS18B20 之类的传感器。 **Q:想同时控制多路继电器** A:本板只有 1 路。代码里 `MAX_MQTT_TOPICS` 已预留 4 个主题位, 扩展时在 `RelayController` 里改成引脚数组、在 `MqttService::handleMessage()` 里 按 `topicIndex` 分发即可,其余模块不用动。 --- ## 十一、已知限制 - 不支持蓝牙配网(已按需求移除) - `android/` 下的蓝牙 App 已失效,见第九节 - 巴法云走的是 MQTT 协议(9501),未实现 TCP 创客云协议(8344) - 巴法云接入、OTA 升级、定时任务这几块尚未在真机上跑过完整流程验证