设备 API¶
所有设备接口需要认证,位于 /api/devices 路径前缀下。这些接口管理电子墨水屏设备的完整生命周期:列表查询、绑定、解绑、命令下发和状态读取。
设备模型¶
{
"id": "01912345-6789-7abc-def0-123456789abc",
"hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
"thingName": "inklet-a1b2c3d4",
"nickname": "客厅墨水屏",
"firmware": "1.2.0",
"battery": 85,
"online": true,
"lastSeenAt": "2026-01-16T08:30:00Z",
"ownerId": "01912345-0000-7abc-def0-000000000001",
"boundAt": "2026-01-15T14:00:00Z",
"claimCode": null,
"state": "{\"screen\":\"text\",\"lastCmd\":\"abc123\"}",
"stateUpdatedAt": "2026-01-16T08:25:00Z",
"tags": ["undeclared"],
"latestPushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
"latestPushAt": "2026-01-16T09:00:00Z"
}
| 字段 | 类型 | 描述 |
|---|---|---|
id |
UUID v7 | 数据库主键 |
hwId |
string | 出厂时烧录到设备中的硬件 UUID |
thingName |
string | AWS IoT Core Thing 名称(在配网时分配) |
nickname |
string 或 null | 用户自定义的显示名称(未设置时客户端应回退到 hwId) |
firmware |
string 或 null | 设备上报的固件版本 |
battery |
integer 或 null | 电池百分比(0--100) |
online |
boolean | 设备当前是否连接到 IoT Core |
lastSeenAt |
timestamp 或 null | 最后一次心跳的时间戳 |
ownerId |
UUID 或 null | 拥有此设备的用户(未绑定时为 null) |
boundAt |
timestamp 或 null | 设备绑定到当前所有者的时间 |
claimCode |
string 或 null | 6 位配对码(仅在设备未绑定时存在) |
state |
JSON string 或 null | 通过 MQTT 上报的任意设备状态 |
stateUpdatedAt |
timestamp 或 null | 状态最后更新的时间 |
tags |
string array | 用户定义的设备标签(默认值:["undeclared"]) |
latestPushId |
UUID 或 null | 最近发布的推送 ID |
latestPushAt |
timestamp 或 null | 最近一次推送发布到设备的时间 |
说明
claimCode 仅在设备未绑定时存在。设备绑定到用户后,配对码会被清除。
接口列表¶
GET /api/devices¶
需要认证
列出当前认证用户绑定的所有设备。
请求头:
响应: 200 OK
[
{
"id": "01912345-6789-7abc-def0-123456789abc",
"hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
"thingName": "inklet-a1b2c3d4",
"nickname": "客厅墨水屏",
"firmware": "1.2.0",
"battery": 85,
"online": true,
"lastSeenAt": "2026-01-16T08:30:00Z",
"ownerId": "01912345-0000-7abc-def0-000000000001",
"boundAt": "2026-01-15T14:00:00Z",
"tags": ["undeclared"],
"latestPushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
"latestPushAt": "2026-01-16T09:00:00Z"
}
]
如果用户没有绑定任何设备,返回空数组 []。可选字段(nickname、latestPushId、latestPushAt 等)在值为 null 时会从响应中省略(omitempty)。
GET /api/devices/{id}¶
需要认证 --- 仅设备所有者
通过 Thing 名称获取指定设备的详细信息。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID(例如 01912345-6789-7abc-def0-123456789abc) |
响应: 200 OK
{
"id": "01912345-6789-7abc-def0-123456789abc",
"hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
"thingName": "inklet-a1b2c3d4",
"nickname": "客厅墨水屏",
"firmware": "1.2.0",
"battery": 85,
"online": true,
"lastSeenAt": "2026-01-16T08:30:00Z",
"ownerId": "01912345-0000-7abc-def0-000000000001",
"boundAt": "2026-01-15T14:00:00Z",
"state": "{\"screen\":\"text\",\"lastCmd\":\"abc123\"}",
"stateUpdatedAt": "2026-01-16T08:25:00Z",
"tags": ["undeclared"],
"latestPushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
"latestPushAt": "2026-01-16T09:00:00Z"
}
错误:
| 状态码 | 原因 |
|---|---|
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到 |
GET /api/devices/{id}/state¶
需要认证 --- 仅设备所有者
获取设备上报的原始 JSON 状态。此接口返回解析后的 JSON 对象,而非字符串形式的 state 字段。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
响应: 200 OK
提示
当需要以结构化 JSON 对象形式读取设备状态时,请使用此接口。GET /api/devices/{id} 接口在设备对象中以转义 JSON 字符串的形式返回状态。
错误:
| 状态码 | 原因 |
|---|---|
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到或尚未上报状态 |
GET /api/devices/{id}/nickname¶
需要认证 --- 仅设备所有者
获取设备的显示名称。如果用户设置了昵称则返回昵称,否则回退到设备的 hwId。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
响应: 200 OK
错误:
| 状态码 | 原因 |
|---|---|
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到 |
PUT /api/devices/{id}/nickname¶
需要认证 --- 仅设备所有者
设置或清除设备的显示名称。发送空字符串可清除昵称(后续 GET 将返回 hwId)。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
请求体:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
nickname |
string | 是 | 要设置的显示名称。空字符串 "" 将清除昵称 |
响应: 200 OK
错误:
| 状态码 | 原因 |
|---|---|
400 |
请求体无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到 |
GET /api/devices/{id}/health¶
需要认证 --- 仅设备所有者
获取设备的实时健康状态快照,包括连接状态、电量、固件版本和推送状态。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
响应: 200 OK
{
"id": "01912345-6789-7abc-def0-123456789abc",
"hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
"nickname": "客厅墨水屏",
"online": true,
"state": "{\"screen\":\"text\"}",
"battery": 85,
"firmware": "1.2.0",
"lastSeenAt": "2026-01-16T08:30:00Z",
"stateUpdatedAt": "2026-01-16T08:25:00Z",
"latestPushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
"latestPushAt": "2026-01-16T09:00:00Z"
}
| 字段 | 类型 | 描述 |
|---|---|---|
id |
UUID | 设备 ID |
hwId |
string | 硬件 UUID |
nickname |
string | 显示名称(未设置时回退到 hwId) |
online |
boolean | 设备是否在线 |
state |
JSON string 或 null | 通过 MQTT 上报的设备状态 |
battery |
integer 或 null | 电池百分比 |
firmware |
string 或 null | 固件版本 |
lastSeenAt |
timestamp 或 null | 最后一次心跳 |
stateUpdatedAt |
timestamp 或 null | 最后一次状态更新 |
latestPushId |
UUID 或 null | 当前推送 ID |
latestPushAt |
timestamp 或 null | 最近一次推送发布时间 |
错误:
| 状态码 | 原因 |
|---|---|
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到 |
POST /api/devices/bind/nfc¶
需要认证
使用 NFC 载荷将设备绑定到当前认证用户。后端在绑定前会根据出厂密钥验证 HMAC 签名。
请求体:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
hwId |
string | 是 | 从 NFC 标签读取的硬件 UUID |
signature |
string | 是 | HMAC-SHA256(hwId, FACTORY_SECRET) 的前 16 个十六进制字符 |
响应: 200 OK
返回完整的设备对象,ownerId 设置为当前认证用户。
错误:
| 状态码 | 原因 |
|---|---|
400 |
字段缺失或签名无效 |
404 |
未找到对应 hwId 的设备 |
409 |
设备已被其他用户绑定 |
POST /api/devices/bind/code¶
需要认证
使用设备屏幕上显示的 6 位配对码将设备绑定到当前认证用户。
请求体:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
code |
string | 是 | 6 位配对码(不区分大小写) |
响应: 200 OK
返回完整的设备对象,ownerId 设置为当前认证用户。
错误:
| 状态码 | 原因 |
|---|---|
400 |
配对码缺失或格式无效 |
404 |
未找到对应配对码的设备 |
409 |
设备已被其他用户绑定 |
410 |
配对码已过期 |
配对码从何而来?
当设备处于未绑定状态并开机时,会通过 MQTT 发布 request_claim 消息。后端生成一个 6 位配对码,存储到数据库,并通过 claim_code 命令发送回设备。设备将其渲染到电子墨水屏上。
POST /api/devices/{id}/unbind¶
需要认证 --- 仅设备所有者
将设备从当前认证用户解绑。设备将恢复为未绑定状态,并重新请求配对码。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
响应: 200 OK
解绑后,后端通过 MQTT 向设备发送 unbound 命令。设备清除屏幕内容并重新请求配对码。
错误:
| 状态码 | 原因 |
|---|---|
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到 |
POST /api/devices/{id}/cmd¶
需要认证 --- 仅设备所有者
向设备发送命令。命令通过 MQTT 发送到设备的 down/cmd 主题。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
请求体:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
kind |
string | 是 | 命令类型(目前用户命令仅支持 text) |
text |
string | 条件必填 | 当 kind 为 text 时必须提供 |
响应: 200 OK
{
"id": "01912345-9999-7abc-def0-aaaaaaaaaaaa",
"thingName": "inklet-a1b2c3d4",
"kind": "text",
"payload": "{\"text\":\"Hello from Inklet!\"}",
"status": "delivered",
"createdAt": "2026-01-16T09:00:00Z"
}
响应包含 DeviceCommand 记录,用于跟踪命令下发状态。payload 为 JSON 字符串,包含命令的完整参数;status 在成功下发后为 delivered。
错误:
| 状态码 | 原因 |
|---|---|
400 |
命令字段缺失或无效 |
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到 |
POST /api/devices/{id}/refresh-code¶
需要认证
重新生成设备的配对码。当当前配对码已过期或丢失时使用。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
响应: 200 OK
返回完整的设备对象,claimCode 已更新为新的配对码。
错误:
| 状态码 | 原因 |
|---|---|
401 |
访问令牌缺失或无效 |
404 |
设备未找到 |
GET /api/devices/{id}/push¶
需要认证 --- 仅设备所有者
获取设备当前的推送。返回一个 JSON 对象,包含渲染位图的 CloudFront 签名 URL。如果设备已有最新推送,直接返回该推送(changed: false)。否则后端会提升优先级最高的 QUEUE 状态推送,标记为 PUBLISHED,设置为设备的最新推送(changed: true)。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
查询参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
format |
string | png |
返回的 url 指向的位图格式,取值 png、raw2、raw4(见下表)。未知值回退为 png。 |
位图格式:
| 格式 | 文件 | 描述 |
|---|---|---|
png |
image.png |
1-bit Floyd--Steinberg 抖动 PNG(800×480) |
raw2 |
image2.raw |
1 bpp 打包位图(48000 字节),黑白 |
raw4 |
image4.raw |
2 bpp 打包位图(96000 字节),4 级灰度 |
响应: 200 OK
{
"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": false
}
| 字段 | 类型 | 描述 |
|---|---|---|
url |
string | 位图的 CloudFront 签名 URL(15 分钟后过期) |
pushId |
UUID | 推送记录的 ID |
status |
string | 推送状态(PUBLISHED) |
changed |
boolean | 如果从队列中提升了新推送则为 true;如果返回的是现有最新推送则为 false |
错误:
| 状态码 | 原因 |
|---|---|
400 |
设备 id 无效 |
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到,或没有可用推送(既无最新推送也无队列推送) |
POST /api/devices/{id}/push/refresh¶
需要认证 --- 仅设备所有者
轮换到队列中的下一个推送。后端查找下一个优先级最高的 QUEUE 状态推送,将设备之前的最新推送标记为 EXPIRED,将新推送标记为 PUBLISHED,设置为设备的最新推送,并返回其签名 URL。如果没有可用的队列推送,则返回当前最新推送(changed: false)。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
查询参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
format |
string | png |
返回的 url 指向的位图格式,取值 png、raw2、raw4。未知值回退为 png。 |
响应: 200 OK
{
"url": "https://cdn.iminklet.com/render/{userId}/{deviceId}/{pushId}/image.png?Expires=...&Signature=...&Key-Pair-Id=...",
"pushId": "01912345-cccc-7abc-def0-dddddddddddd",
"status": "PUBLISHED",
"changed": true
}
| 字段 | 类型 | 描述 |
|---|---|---|
url |
string | 新位图的 CloudFront 签名 URL |
pushId |
UUID | 推送记录的 ID |
status |
string | 推送状态(PUBLISHED) |
changed |
boolean | 如果从队列中提升了新推送则为 true;如果没有新推送可用而返回当前推送则为 false |
行为:
- 查找该设备下一个优先级最高的
QUEUE状态推送 - 如果找到:将之前的
latestPushId标记为EXPIRED,将新推送标记为PUBLISHED,更新latestPushId,返回新推送(changed: true) - 如果没有可用的队列推送:返回当前最新推送(
changed: false)
错误:
| 状态码 | 原因 |
|---|---|
400 |
设备 id 无效 |
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到,或无可用内容(既无当前推送也无队列推送) |
GET /api/devices/{id}/push/{pushId}¶
需要认证 --- 仅设备所有者
通过 push ID 获取指定推送。工作方式类似 GET /api/devices/{id}/push,但不解析当前/队列推送,而是直接返回指定 pushId 的签名 URL——适用于重新展示历史推送。不改变任何推送状态。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
pushId |
推送的 UUID,必须属于该设备 |
查询参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
format |
string | png |
返回的 url 指向的位图格式,取值 png、raw2、raw4。未知值回退为 png。 |
响应: 200 OK
{
"url": "https://cdn.iminklet.com/render/{userId}/{deviceId}/{pushId}/image.png?Expires=...&Signature=...&Key-Pair-Id=...",
"pushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
"status": "CONFIRMED",
"changed": false
}
错误:
| 状态码 | 原因 |
|---|---|
400 |
设备 id 或推送 id 无效 |
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到,或该设备下未找到此推送,或推送无渲染文件 |
GET /api/devices/{id}/pushes¶
需要认证 --- 仅设备所有者
列出设备的所有推送,最新的在前。采用 cursor(keyset)分页——适合无限滚动 / 懒加载——可按状态和创建时间区间筛选。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
查询参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
status |
string | — | 按推送状态筛选(PREPARE、QUEUE、PUBLISHED、CONFIRMED、EXPIRED)。省略则返回全部。 |
from |
RFC3339 时间 | — | 仅返回此时间之后(含)创建的推送 |
to |
RFC3339 时间 | — | 仅返回此时间之前(含)创建的推送 |
cursor |
string | — | 上一次响应返回的 nextCursor。首页请求时省略。 |
limit |
integer | 20 |
每页数量(最大 50) |
响应: 200 OK
{
"items": [
{
"pushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
"title": "今日摘要",
"summary": "今天的重点包括 Rust 内存安全和一篇 WebAssembly 文章。",
"status": "CONFIRMED",
"priority": 10,
"createdAt": "2026-05-26T09:00:00Z"
}
],
"nextCursor": "MjAyNi0wNS0yNlQwOTowMDowMFo...",
"hasMore": true
}
| 字段 | 类型 | 描述 |
|---|---|---|
items[].pushId |
UUID | 推送记录的 ID |
items[].title |
string | 短标题(可能为空) |
items[].summary |
string | 1--2 句描述(可能为空) |
items[].status |
string | 推送状态 |
items[].priority |
integer | 推送优先级 |
items[].createdAt |
timestamp | 推送创建时间 |
nextCursor |
string | 下一页的 cursor;当 hasMore 为 false 时省略 |
hasMore |
boolean | 本页之后是否还有更多推送 |
无限滚动
首页请求不带 cursor。当用户滚动到接近底部时,若 hasMore 为 true,用 ?cursor={nextCursor} 请求下一页;hasMore 为 false 时停止。cursor 分页锚定在 (createdAt, id) 上,因此期间新创建或轮换的推送不会影响已加载的页(不会重复或漏掉),且翻得越深性能也不会下降。
配合 GET /api/devices/{id}/push/{pushId} 获取任一条目的位图 URL,或用 POST /api/devices/{id}/current-push 将某条设为设备当前推送。
错误:
| 状态码 | 原因 |
|---|---|
400 |
设备 id 无效,或 cursor 格式错误 |
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到 |
POST /api/devices/{id}/current-push¶
需要认证 --- 仅设备所有者
强制将设备当前推送设为指定 push。将设备之前的最新推送改回 QUEUE(未推送状态),将目标推送标记为 PUBLISHED 并设为设备最新,同时发送 MQTT new_push 信号让设备立即抓取。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
请求体:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
pushId |
UUID | 是 | 要设为当前的推送,必须属于该设备且已渲染出文件 |
响应: 200 OK
错误:
| 状态码 | 原因 |
|---|---|
400 |
设备 id 或推送 id 无效 |
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备未找到,或该设备下未找到此推送,或推送尚未渲染 |
POST /api/devices/{id}/custom-push/upload¶
需要认证 --- 仅设备所有者
上传自定义图片显示的第 1 步。返回一个预签名 S3 上传凭证(仅图片,≤10 MB,15 分钟有效)。前端将图片直传 S3 后,再调用 confirm 接口。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
请求体:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
contentType |
string | 是 | MIME 类型,必须是 image/* |
sizeBytes |
integer | 否 | 声明的大小;超过 10 MB 会被拒绝 |
响应: 200 OK
{
"fileId": "01912345-2b44-7c2b-8e36-d367ecab52b7",
"url": "https://inklet-dev.s3.us-east-1.amazonaws.com",
"fields": { "key": "user/.../custom.png", "...": "..." },
"expiresAt": "2026-05-30T13:47:09Z"
}
按 multipart/form-data POST 上传到 S3(先放所有 fields,最后放 file 字段),与内容上传一致。
错误:
| 状态码 | 原因 |
|---|---|
400 |
设备 id 无效,或 content type 不是图片 |
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
413 |
文件超过 10 MB 限制 |
POST /api/devices/{id}/custom-push/confirm¶
需要认证 --- 仅设备所有者
第 2 步:确认已上传的图片并为其创建推送。后端验证 S3 上传,创建 push + render task(指向内置 __custom_image__ 模板),并入队渲染。render worker 下载图片,缩放到 800×480,生成抖动/灰度位图。
返回的 push 初始为 PREPARE,渲染完成后变为 QUEUE。轮询 GET /api/devices/{id}/pushes(或 GET /push/{pushId})查询状态,再调用 POST /api/devices/{id}/current-push 显示它。
路径参数:
| 参数 | 描述 |
|---|---|
id |
设备的 UUID |
请求体:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
fileId |
UUID | 是 | 上传步骤返回的 fileId |
title |
string | 否 | 可选标题,显示在推送列表中 |
响应: 200 OK
错误:
| 状态码 | 原因 |
|---|---|
400 |
设备 id 或 file id 无效 |
401 |
访问令牌缺失或无效 |
403 |
当前认证用户不是设备所有者 |
404 |
设备或文件未找到 |
409 |
文件不处于可确认状态 |
错误响应¶
所有错误响应遵循统一的格式:
| 状态码 | 描述 |
|---|---|
400 |
请求无效 --- 请求体格式错误、缺少必填字段或格式无效 |
401 |
未授权 --- 访问令牌缺失、已过期或无效 |
403 |
禁止访问 --- 当前认证用户不是目标设备的所有者 |
404 |
未找到 --- 设备或资源不存在 |
409 |
冲突 --- 设备已被其他用户绑定 |
410 |
已失效 --- 配对码已过期,需要重新生成 |