IoT 协议(MQTT)¶
Inklet 设备通过 AWS IoT Core 使用 MQTT 协议与后端通信。所有连接均使用 X.509 证书双向 TLS(mTLS)进行认证。本页面介绍主题结构、消息格式和安全策略。
连接信息¶
| 参数 | 值 |
|---|---|
| 协议 | MQTT over TLS(端口 8883) |
| 认证方式 | X.509 客户端证书(mTLS) |
| 代理 | AWS IoT Core(xxxx-ats.iot.us-east-1.amazonaws.com) |
| QoS | 1(至少一次送达) |
主题结构¶
所有主题遵循 inklet/dev/{thingName}/direction/type 的模式,其中 {thingName} 是在配网过程中分配的 AWS IoT Core Thing 名称。
inklet/dev/{thingName}/
├── up/ # 设备 → 后端
│ ├── heartbeat # 定时健康上报
│ ├── state # 任意设备状态
│ ├── request_claim # 请求配对码
│ ├── bind_token # 通过 NFC token 无 App 辅助绑定
│ ├── get_push # 请求当前推送位图 URL
│ ├── refresh_push # 请求下一个推送
│ └── confirm_push # 确认推送已显示
└── down/ # 后端 → 设备
└── cmd # 后端下发的命令
上行主题(设备到后端)¶
inklet/dev/{thingName}/up/heartbeat¶
设备按可配置的间隔(默认 30 秒)发送的定时健康上报。
载荷:
{
"hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
"ts": 1705395000,
"firmware": "1.2.0",
"battery": 85
}
| 字段 | 类型 | 描述 |
|---|---|---|
hwId |
string | 硬件 UUID |
ts |
integer | Unix 时间戳(秒) |
firmware |
string | 固件版本字符串 |
battery |
integer | 电池百分比(0--100) |
后端行为:
- 如果数据库中不存在该设备记录,则根据
hwId和thingName创建 - 更新
firmware、battery、lastSeenAt和online状态 - 如果设备未绑定且没有配对码,后端会生成一个并发送
claim_code命令
inklet/dev/{thingName}/up/state¶
设备以 JSON 对象形式上报任意状态。后端将其作为 JSON 字符串存储在 state 列中。
载荷:
{
"screen": "text",
"lastCmd": "01912345-9999-7abc-def0-aaaaaaaaaaaa",
"brightness": 50,
"temperature": 22.5
}
载荷可以包含任意有效的 JSON。后端不解析其内容,直接存储。
后端行为:
- 将完整的 JSON 载荷存储为设备的
state - 更新
stateUpdatedAt
inklet/dev/{thingName}/up/request_claim¶
设备请求配对码以进行用户配对。在设备未绑定且需要显示配对码时发送。
载荷:
后端行为:
- 如果设备已绑定到用户,发送
already_bound命令 - 如果设备未绑定,生成一个 6 位字母数字配对码
- 将配对码存储到数据库
- 向设备发送
claim_code命令
inklet/dev/{thingName}/up/bind_token¶
设备通过 NFC token 发起无 App 辅助的绑定流程。设备从 NFC 标签读取 token 后,通过此消息请求后端完成绑定。
载荷:
| 字段 | 类型 | 描述 |
|---|---|---|
token |
string | 从 NFC 标签读取的绑定 token |
后端行为:
- 验证 token 有效性并解析目标用户
- 如果成功:将设备绑定到对应用户,向设备发送
bound命令 - 如果失败:向设备发送
bind_failed命令(含reason字段)
inklet/dev/{thingName}/up/get_push¶
设备请求当前的推送位图 URL。后端通过设备的下行主题回复 push_response 命令。
载荷:
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
format |
string | png |
请求的位图格式:png(1-bit PNG)、raw2(1 bpp 打包)、raw4(2 bpp 4 级灰度)。省略或未知值回退为 png。 |
后端行为:
- 如果设备有
latestPushId,返回当前推送 URL(changed: false) - 如果没有最新推送,查找优先级最高的
QUEUE状态推送 - 将找到的推送标记为
PUBLISHED,设置为设备的最新推送 - 回复包含 URL 的
push_response命令(changed: true) - 如果没有可用推送,不发送响应
inklet/dev/{thingName}/up/refresh_push¶
设备请求队列中的下一个推送。后端提升下一个优先级最高的 QUEUE 推送,并回复 push_response 命令。
载荷:
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
format |
string | png |
请求的位图格式,同 get_push。 |
后端行为:
- 查找该设备下一个优先级最高的
QUEUE状态推送 - 如果找到:替换设备的
latestPushId,标记为PUBLISHED,回复push_response(changed: true) - 如果没有新推送可用:返回当前推送 URL(
changed: false)
inklet/dev/{thingName}/up/confirm_push¶
设备确认推送已在电子墨水屏上成功显示。后端将设备当前最新推送(latestPushId)的状态更新为 CONFIRMED。
载荷:
后端不读取载荷内容,始终确认设备的 latestPushId。
后端行为:
- 找到设备的
latestPushId - 将该推送状态从
PUBLISHED更新为CONFIRMED
下行主题(后端到设备)¶
inklet/dev/{thingName}/down/cmd¶
后端向设备发送的命令。所有命令共享一个通用结构,通过 kind 字段区分命令类型。
命令:text¶
向设备发送要在电子墨水屏上渲染的文本内容。
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "text" |
id |
UUID | 用于跟踪的唯一命令 ID |
text |
string | 要渲染的文本内容 |
命令:claim_code¶
向设备发送配对码供其显示。用户输入此配对码即可将设备绑定到自己的账户。
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "claim_code" |
code |
string | 6 位字母数字配对码 |
命令:bound¶
通知设备已被绑定到某个用户。
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "bound" |
userId |
UUID | 绑定该设备的用户 ID |
命令:unbound¶
通知设备已与所有者解绑。设备应清除屏幕内容并重新请求配对码。
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "unbound" |
命令:already_bound¶
当设备请求配对码但已绑定到用户时发送。设备不应显示配对界面。
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "already_bound" |
命令:push_response¶
将推送请求(get_push 或 refresh_push)的结果发送回设备。包含设备需要下载并显示的位图 URL。
成功时:
{
"kind": "push_response",
"url": "https://cdn.iminklet.com/render/{userId}/{deviceId}/{pushId}/image.png?Expires=...&Signature=...&Key-Pair-Id=...",
"pushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
"status": "PUBLISHED",
"changed": true
}
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "push_response" |
url |
string | 推送位图的 CloudFront 签名 URL(15 分钟过期),实际格式为 render/{userId}/{deviceId}/{pushId}/image.png(或 image2.raw/image4.raw,取决于请求的 format) |
pushId |
UUID | 推送记录的 ID |
status |
string | 推送状态(PUBLISHED) |
changed |
boolean | 如果从队列中提升了新推送则为 true;如果返回的是现有推送则为 false |
出错时(无可用推送或内部错误):
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "push_response" |
error |
string | 错误原因描述 |
命令:bind_failed¶
当设备通过 bind_token 发起绑定但失败时,后端发送此命令。设备应提示用户绑定失败。
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "bind_failed" |
reason |
string | 失败原因描述(如 "bad_payload"、"invalid bind token" 等) |
命令:new_push¶
通知设备当前推送在带外发生了变化(例如用户在前端点了刷新、将某条 push 设为当前、或上传了自定义图片)。设备应立即用 get_push 请求抓取新的最新推送——get_push 不会轮换队列,因此设备会精确落在后端设定的当前推送上。
| 字段 | 类型 | 描述 |
|---|---|---|
kind |
string | "new_push" |
为什么用 get_push 而非 refresh_push
后端在发送 new_push 前已经选定了目标推送。用 get_push 响应会返回这条确切的推送;用 refresh_push 响应则会再次轮换队列,可能跳过它。
AWS IoT Core 策略¶
三个 IAM 策略管控 MQTT 访问权限,每个策略都遵循最小权限原则。
inklet-device-policy¶
附加到每个设备证书的逐设备隔离策略。使用 IoT Core 策略变量 ${iot:Connection.Thing.ThingName},确保设备只能访问自己的主题。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": "arn:aws:iot:us-east-1:*:client/${iot:Connection.Thing.ThingName}"
},
{
"Effect": "Allow",
"Action": "iot:Publish",
"Resource": [
"arn:aws:iot:us-east-1:*:topic/inklet/dev/${iot:Connection.Thing.ThingName}/up/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Subscribe",
"Resource": [
"arn:aws:iot:us-east-1:*:topicfilter/inklet/dev/${iot:Connection.Thing.ThingName}/down/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Receive",
"Resource": [
"arn:aws:iot:us-east-1:*:topic/inklet/dev/${iot:Connection.Thing.ThingName}/down/*"
]
}
]
}
逐设备隔离
${iot:Connection.Thing.ThingName} 变量由 AWS IoT Core 在连接时解析。关联到 Thing inklet-a1b2c3d4 的设备证书只能发布到 inklet/dev/inklet-a1b2c3d4/up/* 并订阅 inklet/dev/inklet-a1b2c3d4/down/*,无法访问其他设备的主题。
inklet-backend-policy¶
后端 MQTT 客户端(inklet-backend)的特权策略。后端订阅所有设备的上行主题,并可向任意设备的下行主题发布命令。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": "arn:aws:iot:us-east-1:*:client/inklet-backend"
},
{
"Effect": "Allow",
"Action": "iot:Subscribe",
"Resource": [
"arn:aws:iot:us-east-1:*:topicfilter/inklet/dev/+/up/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Receive",
"Resource": [
"arn:aws:iot:us-east-1:*:topic/inklet/dev/+/up/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Publish",
"Resource": [
"arn:aws:iot:us-east-1:*:topic/inklet/dev/+/down/*"
]
}
]
}
inklet-claim-policy¶
用于 Fleet Provisioning 的 claim 证书的受限策略。claim 证书只能执行 Fleet Provisioning 的 MQTT 事务 --- 不能发布或订阅应用主题。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "iot:Publish",
"Resource": [
"arn:aws:iot:us-east-1:*:topic/$aws/certificates/create/*",
"arn:aws:iot:us-east-1:*:topic/$aws/provisioning-templates/*/provision/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Subscribe",
"Resource": [
"arn:aws:iot:us-east-1:*:topicfilter/$aws/certificates/create/*",
"arn:aws:iot:us-east-1:*:topicfilter/$aws/provisioning-templates/*/provision/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Receive",
"Resource": [
"arn:aws:iot:us-east-1:*:topic/$aws/certificates/create/*",
"arn:aws:iot:us-east-1:*:topic/$aws/provisioning-templates/*/provision/*"
]
}
]
}
Claim 证书安全性
claim 证书在所有设备间共享,且只授予 Fleet Provisioning 主题的访问权限。它们应被安全存储,但敏感程度不及逐设备证书 --- 泄露的 claim 证书只能创建新的 Thing,不能冒充已有设备。
Fleet Provisioning¶
新设备使用 Fleet Provisioning by Claim 在首次启动时获取设备专属证书。
流程:
设备(首次启动)
│
├── 使用 claim 证书连接到 IoT Core
│
├── 发布到 $aws/certificates/create/json
│ └── 接收新证书 + 私钥
│
├── 发布到 $aws/provisioning-templates/{template}/provision/json
│ └── 发送: { "SerialNumber": "{hwId}" }
│ └── 接收: { "thingName": "inklet-{prefix}" }
│
├── 存储设备证书、私钥和 thingName
│
└── 使用设备证书重新连接
└── 开始正常运行(心跳、命令)
配网完成后,设备在本地存储证书和 Thing 名称,之后不再使用 claim 证书。