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]

这张表在代码里通常长这样:

// 人的概念 → 命令码(从协议文档抄来的)
const Y13_CMD = {
  POWER_SCENE: 0x21,       // 开关+场景
  CONCENTRATION: 0x22,     // 浓度
  INTERVAL: 0x24,          // 间隔时长
}

// 属性定义:告诉代码"这个属性的值应该怎么编码"
export const ATTR_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 后到 → 不明成功

通用的解决方案

  1. 串行队列:连接操作排队执行,不并行
  2. 单一权威:连接状态只有一个地方能修改
  3. 超时兜底:任何操作都有超时保护,超时后清理状态
  4. MTU 协商:BLE 默认 MTU 为 23 字节(payload 仅 20 字节),无法传输超过 20 字节的设备帧。连接后需协商 MTU 以获取更大的数据包。
    • 微信小程序最大值:512setBLEMTUmtu 参数不可超过此值,否则协商失败)
    • 协商失败后会回退到默认 23 字节 MTU,设备上报数据可能被截断
    • OTA 场景下建议协商到 512,以支持大文件高效传输
  5. 丢弃旧连接:新连接开始前,强制清理上一个连接的所有残留

模型五: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

模型六:多设备架构需要画像模式

一句话

当你只有一台设备时,协议写死在代码里最省事。但当你开始接第二台设备时,你就需要一个"设备画像"来把协议从业务代码里抽出去。

什么是设备画像

设备画像 = 描述"一个设备长什么样"的配置文件。它包含:

// 设备状态示例(每个设备定义自己的状态结构)
interface DeviceState {
  power: boolean      // 开关状态
  brightness: number  // 亮度 0-100
  mode: number        // 工作模式
}

// 每个设备画像回答三个问题:
interface IDeviceProfile {
  // 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 过滤

更多推荐