IoT 系统开发入门:七个核心模型
IoT 系统开发入门:七个核心模型
摘要:本文系统性地介绍了 IoT 系统开发的七个核心模型,帮助开发者从底层理解 IoT 项目的通用结构。七个模型包括:1) 协议映射表(人与字节的翻译基础)、2) 状态三角(设备-缓存-界面的状态同步)、3) 编解码(数据转换的双向过程)、4) 连接状态机(连接生命周期的管理)、5) OTA 升级(安全固件切换机制)、6) 设备画像(多设备协议抽象)、7) 排查方法(撞库与否定式排查)。这些模型层层嵌套,构成了 IoT 开发的完整知识框架,适用于 BLE、Wi-Fi、ZigBee、LoRa、MQTT 等多种协议。
用途:本文面向零基础想入门 IoT 系统开发的新人,也适合 AI 理解 IoT 项目的通用结构。
读完本文后,你应当能看懂大多数 IoT 项目的代码组织方式,并能独立分析和排查基本问题。前置阅读:无。本文不依赖任何编程语言或硬件基础。
适用协议:BLE(蓝牙低功耗)、Wi-Fi、ZigBee、LoRa、MQTT。虽然示例来自 BLE 项目,但模型本身是协议无关的。
前言:IoT 是什么
IoT 做的事可以概括为一句话:
让程序能读取物理世界的数据,并控制物理世界的设备。
拆开来就是两个方向:
读取: 设备(物理) ──传感器──→ 字节 ──解码──→ 数值 ──→ 界面显示 控制: 界面操作 ──→ 数值 ──编码──→ 字节 ──写入──→ 设备执行整个过程的核心是信息在「人的概念」和「机器的字节」之间来回翻译。这篇文章讲的就是这个翻译过程的七个通用模型。
模型一:协议的本质是一张映射表
一句话
所有 IoT 协议的核心就是一张表:把人的概念(“开机”)映射成机器能理解的字节(
0x01)。
示意图
人的概念 协议约束 机器字节 ───────────────────────────────────────────── "关机" ──→ 命令码 0x21 ──→ [0x21, 0x00] "自定义模式" ──→ 数据值 1 ──→ [0x21, 0x01] "睡眠模式" ──→ 数据值 2 ──→ [0x21, 0x02]这张表在代码里通常长这样:
// 人的概念 → 命令码(从协议文档抄来的)constY13_CMD={POWER_SCENE:0x21,// 开关+场景CONCENTRATION:0x22,// 浓度INTERVAL:0x24,// 间隔时长}// 属性定义:告诉代码"这个属性的值应该怎么编码"exportconstATTR_POWER_SCENE={key:'power',// 人的概念valueType:ValueType.uint8,// 用1个无符号整数表示commandCode:0x21,// 命令码}为什么这很重要
不管你是连 BLE 香薰机、Wi-Fi 灯、还是 ZigBee 传感器,你的代码结构都是一样的——一张从"人"到"字节"的映射表。换设备只是换表的内容,不换表的结构。
常见误区
- ❌以为协议很神秘:协议文档就是一张 Excel 表或 Markdown 表格,列着"偏移、字段名、类型、说明"
- ❌以为不同协议差别很大:BLE 用 byte[],MQTT 用 JSON,但本质都是"约定好每个字节/字段的含义"
- ✅真相:所有协议都在做同一件事,只是传输的容器不同
模型二:状态三角
一句话
IoT 系统的核心挑战是管理"三个地方的状态"——它们永远不可能完全一致。
示意图
物理设备 (真实值) ↗ ↖ 写入通道 通知通道 ↙ ↘ 手机内存缓存 ←──── 界面显示 (最近一次写入) (用户看到的)这三个地方的值存在天然的时间差:
| 位置 | 特点 | 延迟来源 |
|---|---|---|
| 物理设备 | 唯一权威值 | — |
| 中间层缓存 | 最近一次写入/通知的值 | 通信延迟(BLE 约 10-100ms,Wi-Fi/ZigBee 因网络而异) |
| 界面显示 | 用户看到的值 | setData 渲染延迟 |
三个常见的坑
坑 1:乐观更新覆盖了设备真正的状态
用户操作 → 立即更新 UI(乐观更新) → BLE 写入 → 设备回复旧状态 Notify → 覆盖了 UI。
解决方案:标记"正在下发中的属性",短暂忽略对应的设备上报。 代码参考:markPendingKeys / syncDeviceState(见 BleControl.ts)坑 2:设备上报频率高于 UI 刷新频率
设备每秒报 10 次 Notify,每次都要 setData、解码、渲染——性能灾难。
解决方案:去重守卫。只有值真正变化时才 setData。 代码参考:BleControl.ts syncDeviceState 的 diff 守卫坑 3:用户操作后设备还没来得及回复,用户又操作了一次
连续两次快速操作,第一次的回复覆盖了第二次的设置。
解决方案:pending 超时机制。下发后 500ms 内过滤对应属性的 Notify。通用模式
不管什么通信方式,状态三角永远存在。Wi-Fi 设备的"云端→App"和 BLE 的"设备→手机"只是物理介质不同,状态管理的问题一模一样。
模型三:编解码是一枚硬币的两面
一句话
编码(encode)是把数值变成字节去写给设备;解码(decode)是把设备发来的字节变回数值。是同一张映射表的两个方向。
示意图
编码(写入设备) 数值 ─────────────────→ 字节 [600] [0x02, 0x58] 数值 ←───────────────── 字节 [600] [0x02, 0x58] 解码(读取设备上报)数据类型体系
IoT 里用到的数据类型很固定,几乎所有设备都在用同一套:
| 类型 | 字节数 | 示例数值 | 编码结果 |
|---|---|---|---|
| bool(布尔) | 1 | true | [0x01] |
| int8(有符号整数) | 1 | -50 | [0xCE] |
| uint8(无符号整数) | 1 | 100 | [0x64] |
| int16(有符号短整数) | 2 | -1000 | [0xFC, 0x18] |
| uint16(无符号短整数) | 2 | 300 | [0x01, 0x2C] |
| int32(有符号整数) | 4 | -100000 | [0xFF, 0xFE, 0x79, 0x60] |
| uint32(无符号整数) | 4 | 100000 | [0x00, 0x01, 0x86, 0xA0] |
| float(浮点数) | 4 | 3.14 | [0x40, 0x48, 0xF5, 0xC3] |
| string(字符串) | 不定 | “hello” | [0x68, 0x65, 0x6C, 0x6C, 0x6F] |
这些都是通用的——不管你连什么设备,你都在用这些类型。
特殊编码
有些设备不使用标准类型,而是用自己定义的编码格式:
| 编码名称 | 公式 | 示例 |
|---|---|---|
| H×60+L(分钟+秒余数) | H*60+L | 90s →[0x01, 0x1E] |
| 位图(bitmap) | 每位代表一个开关 | 选中周一到周三 →0b00000111 |
| 自定义公式 | 设备文档定义 | 温度 = raw * 0.1 |
重要经验:协议文档可能写错。当文档和实际设备行为不一致时,以设备实测为准(见模型七)。
模型四:连接生命周期是有限状态机
一句话
任何通信设备都只有三种连接状态:已连接、连接中、未连接。但"从任何状态都能切到任何其他状态"这件事,是所有 bug 的来源。
状态机
┌──────────┐ │ 未连接 │ └────┬─────┘ │ 发起连接 ↓ ┌──────────┐ │ 连接中 │ ←── 超时/失败 → 回到未连接 └────┬─────┘ │ 连接成功 ↓ ┌──────────┐ │ 已连接 │ ──→ 断连 → 回到未连接 └──────────┘三个必死的竞态条件
竞态 1:连接中发了指令
发起连接 → (还没连上)→ 用户点了开关 → 指令写入 → 写入失败竞态 2:断连中发了重连
检测到断连 → 发起重连 → (还没连上)→ 又检测到断连 → 又发起重连 → 两个连接同时在跑竞态 3:指令超时和 Notify 同时到
发送指令 → 启动超时计时器 → 设备回复 Notify → 超时计时器先触发 → 报错 → Notify 后到 → 不明成功通用的解决方案
- 串行队列:连接操作排队执行,不并行
- 单一权威:连接状态只有一个地方能修改
- 超时兜底:任何操作都有超时保护,超时后清理状态
- MTU 协商:BLE 默认 MTU 为 23 字节(payload 仅 20 字节),无法传输超过 20 字节的设备帧。连接后需协商 MTU 以获取更大的数据包。
- 微信小程序最大值:512(
setBLEMTU的mtu参数不可超过此值,否则协商失败) - 协商失败后会回退到默认 23 字节 MTU,设备上报数据可能被截断
- OTA 场景下建议协商到 512,以支持大文件高效传输
- 微信小程序最大值:512(
- 丢弃旧连接:新连接开始前,强制清理上一个连接的所有残留
模型五:OTA 的核心不是传文件
一句话
OTA(Over-The-Air 升级)的核心是"安全地把固件从旧版本切换到新版本,失败了还能回来"。传输文件只是表面功夫。
OTA 的通用流程
准备阶段 传输阶段 切换阶段 ┌──────┐ ┌──────────────┐ ┌──────────┐ │ 版本比对 │ → │ 扇区擦除+写入 │ → ... → │ 校验+重启 │ │ 固件下载 │ │ 分包+重传 │ │ 回滚保护 │ └──────┘ └──────────────┘ └──────────┘每个阶段的通用问题
| 阶段 | 问题 | 通用解法 |
|---|---|---|
| 版本比对 | 怎么知道该不该升级 | 比较 versionCode(数值),而非 versionName(字符串) |
| 固件下载 | 文件从哪里来 | 云存储(COS/OSS)+ 临时下载链接 |
| 分包传输 | 一次发不完怎么办 | MTU 协商 + 分包 + 重传机制 |
| 写入确认 | 设备有没有写对 | 每包回复 ACK,超时重发 |
| 校验 | 固件有没有损坏 | CRC32 / MD5 / SHA-256 校验 |
| 切换 | 新固件能不能跑 | 双 Bank:A 区跑新的,B 区保留旧的,不行回滚 |
双 Bank 模式
Bank A(当前运行) Bank B(待升级) ┌──────────────────┐ ┌──────────────────┐ │ 旧固件 v1.0 │ │ 新固件 v1.1 │ │(正常运行) │ │(写入中) │ └──────────────────┘ └──────────────────┘ │ │ └──────────────────────┘ 升级成功后标记 Bank B 为启动区 下次重启从 Bank B 启动 如果启动失败,回退到 Bank A模型六:多设备架构需要画像模式
一句话
当你只有一台设备时,协议写死在代码里最省事。但当你开始接第二台设备时,你就需要一个"设备画像"来把协议从业务代码里抽出去。
什么是设备画像
设备画像 = 描述"一个设备长什么样"的配置文件。它包含:
// 设备状态示例(每个设备定义自己的状态结构)interfaceDeviceState{power:boolean// 开关状态brightness:number// 亮度 0-100mode:number// 工作模式}// 每个设备画像回答三个问题:interfaceIDeviceProfile{// 1. 怎么连接你?ble:IBLEConfig// 2. 怎么跟你说话?parseNotify:(data:ArrayBuffer)=>DeviceState// 3. 你的属性有哪些?// (通过 command-encoder 的 ATTR_* 定义)}为什么需要画像模式
没有画像模式: 有画像模式: 控制页面 A(写死协议A) 控制页面(通用) 控制页面 B(写死协议B) ↑ 控制页面 C(写死协议C) 设备画像 A / B / C ↑ ProfileCatalog 注册表画像模式的通用结构
| 层 | 职责 | 是否通用 |
|---|---|---|
| 协议模块 | 命令码、编码函数、解码函数 | ❌ 每设备不同 |
| 控制组件 UI | 滑块、按钮、卡片 | ⚠️ 可复用,但需配置 |
| 通信层 | BLE 连接、写入、通知 | ✅ 完全通用 |
| 设备注册表 | 按设备型号索引画像 | ✅ 完全通用 |
经验
- 画像和平台分离越早越好。从第一台设备开始就按画像模式写,第二台设备几乎不需要改框架。
- 画像不等于抽象。画像是把差异性封装进配置文件,而不是用继承/多态去抽象它。
模型七:排查方法——撞库与否定式排查
一句话
不知道正确答案的时候,先排除错误的。跳过所有中间转换层,直接看设备吃什么字节、回什么字节。
排查方法论框架
步骤 1:分解链路 ───────────────────────────────────────── 用户操作 → 业务编码 → 帧组装 → BLE 写入 ↓ UI 显示 ← 业务解码 ← 帧解析 ← BLE 通知 步骤 2:跳过中间层 ───────────────────────────────────────── 不要从"用户操作"开始排查。 直接从"BLE 写入"这一层下手——写死原始测试字节。 步骤 3:设计否定性测试 ───────────────────────────────────────── 设计一组成对的测试值,让不同解释给出不同结果: | 发送字节 | 解释 A 的结果 | 解释 B 的结果 | |---------|-------------|-------------| | [15, 0] | 3840(超范围)<br>解释 A:按 uint16 解析<br>15×256+0=3840 | 900(范围内)<br>解释 B:按分钟+秒解析<br>15×60+0=900 | 谁的解释与设备行为一致,谁就对了。 步骤 4:确认事实,修正文档 ───────────────────────────────────────── 设备的行为是唯一客观标准。 确认事实后,更新协议文档以匹配实际行为。这个方法论通用在哪里
这个框架不限于 BLE,不限于协议排查。你可以用在:
- API 返回不符合预期 → 跳过封装层,直接看 HTTP 响应
- 数据库查询不对 → 跳过 ORM,直接跑 SQL
- 配置不生效 → 跳过框架解析,直接看原始配置文件
- 任何"中间层太多,不知道哪层出了问题"的场景
总结:七个模型的关系
┌──────────┐ │ 协议映射表 │ ← 最底层:人与字节的翻译 └────┬─────┘ │ ┌──────────┼──────────┐ ↓ ↓ ↓ ┌─────────┐ ┌────────┐ ┌──────────┐ │ 编解码 │ │ 状态三角│ │ OTA │ │ 双方向 │ │ 三处同步│ │ 安全升级 │ └─────────┘ └────────┘ └──────────┘ │ │ ↓ ↓ ┌─────────┐ ┌──────────┐ │ 连接状态机│ │ 多设备画像│ │ 三态+竞态│ │ 协议抽象化│ └─────────┘ └──────────┘ │ │ └────┬─────┘ ↓ ┌──────────┐ │ 排查方法 │ ← 兜底:所有模型都可能出错 │ 撞库+否定│ 用事实纠偏 └──────────┘七个模型不是孤立的。它们在项目中层层嵌套,一个模型出问题就影响上下游。但反过来,理解了这七个模型,任何 IoT 项目对你来说都不会是完全陌生的——你只是需要找到"把这张映射表填成新设备协议"的方式。
附录:从零开始的 IoT 学习路线
第一周:理解基础概念
- 读完本文档,理解七个模型
- 找一个身边的 IoT 设备(智能灯/插座/香薰机),看它的产品说明书
- 了解它的通信方式(BLE/Wi-Fi/ZigBee)
第二周:搭建开发环境
- 安装你所选平台的开发工具(如微信开发者工具、Android Studio 等)
- 跑通 BLE 扫描 demo(扫描附近设备)
- 跑通 BLE 连接 demo(连接一个已知设备)
第三周:实现第一个控制
- 找一个有开放协议的设备
- 实现:连接 → 发送一条指令 → 接收回复
- 打印所有原始字节,理解每一帧的含义
第四周:完整控制页
- 实现设备状态的读取和显示
- 实现用户的控制操作(开关/调参)
- 加入状态同步和去重
常见问题
| 问题 | 原因 | 解决方向 |
|---|---|---|
| 搜索不到设备 | 权限/BLE 未开启/UUID 不匹配 | 检查手机权限、BLE 开关、Service UUID |
| 连接成功但收不到数据 | Notify 未订阅/特征值 UUID 错误 | 检查订阅流程和特征值 UUID |
| 发送指令设备无响应 | 指令格式错误/命令码不对 | 用"撞库"发原始字节测试 |
| 数据有时对有时不对 | 字节对齐/端序/帧长度理解有误 | 仔细核对协议文档的偏移表 |
| 界面闪变/值跳来跳去 | 状态同步逻辑有循环/陈旧 Notify | 加 diff 守卫 + pendingKeys 过滤 |