跳转至

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)

后端行为:

  1. 如果数据库中不存在该设备记录,则根据 hwIdthingName 创建
  2. 更新 firmwarebatterylastSeenAtonline 状态
  3. 如果设备未绑定且没有配对码,后端会生成一个并发送 claim_code 命令

inklet/dev/{thingName}/up/state

设备以 JSON 对象形式上报任意状态。后端将其作为 JSON 字符串存储在 state 列中。

载荷:

{
  "screen": "text",
  "lastCmd": "01912345-9999-7abc-def0-aaaaaaaaaaaa",
  "brightness": 50,
  "temperature": 22.5
}

载荷可以包含任意有效的 JSON。后端不解析其内容,直接存储。

后端行为:

  1. 将完整的 JSON 载荷存储为设备的 state
  2. 更新 stateUpdatedAt

inklet/dev/{thingName}/up/request_claim

设备请求配对码以进行用户配对。在设备未绑定且需要显示配对码时发送。

载荷:

{}

后端行为:

  1. 如果设备已绑定到用户,发送 already_bound 命令
  2. 如果设备未绑定,生成一个 6 位字母数字配对码
  3. 将配对码存储到数据库
  4. 向设备发送 claim_code 命令

inklet/dev/{thingName}/up/bind_token

设备通过 NFC token 发起无 App 辅助的绑定流程。设备从 NFC 标签读取 token 后,通过此消息请求后端完成绑定。

载荷:

{
  "token": "eyJhbGciOiJIUzI1NiIs..."
}
字段 类型 描述
token string 从 NFC 标签读取的绑定 token

后端行为:

  1. 验证 token 有效性并解析目标用户
  2. 如果成功:将设备绑定到对应用户,向设备发送 bound 命令
  3. 如果失败:向设备发送 bind_failed 命令(含 reason 字段)

inklet/dev/{thingName}/up/get_push

设备请求当前的推送位图 URL。后端通过设备的下行主题回复 push_response 命令。

载荷:

{ "format": "png" }
字段 类型 默认值 描述
format string png 请求的位图格式:png(1-bit PNG)、raw2(1 bpp 打包)、raw4(2 bpp 4 级灰度)。省略或未知值回退为 png

后端行为:

  1. 如果设备有 latestPushId,返回当前推送 URL(changed: false
  2. 如果没有最新推送,查找优先级最高的 QUEUE 状态推送
  3. 将找到的推送标记为 PUBLISHED,设置为设备的最新推送
  4. 回复包含 URL 的 push_response 命令(changed: true
  5. 如果没有可用推送,不发送响应

inklet/dev/{thingName}/up/refresh_push

设备请求队列中的下一个推送。后端提升下一个优先级最高的 QUEUE 推送,并回复 push_response 命令。

载荷:

{ "format": "png" }
字段 类型 默认值 描述
format string png 请求的位图格式,同 get_push

后端行为:

  1. 查找该设备下一个优先级最高的 QUEUE 状态推送
  2. 如果找到:替换设备的 latestPushId,标记为 PUBLISHED,回复 push_responsechanged: true
  3. 如果没有新推送可用:返回当前推送 URL(changed: false

inklet/dev/{thingName}/up/confirm_push

设备确认推送已在电子墨水屏上成功显示。后端将设备当前最新推送(latestPushId)的状态更新为 CONFIRMED

载荷:

{}

后端不读取载荷内容,始终确认设备的 latestPushId

后端行为:

  1. 找到设备的 latestPushId
  2. 将该推送状态从 PUBLISHED 更新为 CONFIRMED

下行主题(后端到设备)

inklet/dev/{thingName}/down/cmd

后端向设备发送的命令。所有命令共享一个通用结构,通过 kind 字段区分命令类型。


命令:text

向设备发送要在电子墨水屏上渲染的文本内容。

{
  "kind": "text",
  "id": "01912345-9999-7abc-def0-aaaaaaaaaaaa",
  "text": "Hello from Inklet!"
}
字段 类型 描述
kind string "text"
id UUID 用于跟踪的唯一命令 ID
text string 要渲染的文本内容

命令:claim_code

向设备发送配对码供其显示。用户输入此配对码即可将设备绑定到自己的账户。

{
  "kind": "claim_code",
  "code": "A3X9K2"
}
字段 类型 描述
kind string "claim_code"
code string 6 位字母数字配对码

命令:bound

通知设备已被绑定到某个用户。

{
  "kind": "bound",
  "userId": "01912345-0000-7abc-def0-000000000001"
}
字段 类型 描述
kind string "bound"
userId UUID 绑定该设备的用户 ID

命令:unbound

通知设备已与所有者解绑。设备应清除屏幕内容并重新请求配对码。

{
  "kind": "unbound"
}
字段 类型 描述
kind string "unbound"

命令:already_bound

当设备请求配对码但已绑定到用户时发送。设备不应显示配对界面。

{
  "kind": "already_bound"
}
字段 类型 描述
kind string "already_bound"

命令:push_response

将推送请求(get_pushrefresh_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": "push_response",
  "error": "no push available"
}
字段 类型 描述
kind string "push_response"
error string 错误原因描述

命令:bind_failed

当设备通过 bind_token 发起绑定但失败时,后端发送此命令。设备应提示用户绑定失败。

{
  "kind": "bind_failed",
  "reason": "invalid bind token"
}
字段 类型 描述
kind string "bind_failed"
reason string 失败原因描述(如 "bad_payload""invalid bind token" 等)

命令:new_push

通知设备当前推送在带外发生了变化(例如用户在前端点了刷新、将某条 push 设为当前、或上传了自定义图片)。设备应立即用 get_push 请求抓取新的最新推送——get_push 不会轮换队列,因此设备会精确落在后端设定的当前推送上。

{
  "kind": "new_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 证书。