跳转至

设备 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

需要认证

列出当前认证用户绑定的所有设备。

请求头:

Authorization: Bearer {accessToken}

响应: 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"
  }
]

如果用户没有绑定任何设备,返回空数组 []。可选字段(nicknamelatestPushIdlatestPushAt 等)在值为 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

{
  "screen": "text",
  "lastCmd": "abc123",
  "brightness": 50
}

提示

当需要以结构化 JSON 对象形式读取设备状态时,请使用此接口。GET /api/devices/{id} 接口在设备对象中以转义 JSON 字符串的形式返回状态。

错误:

状态码 原因
401 访问令牌缺失或无效
403 当前认证用户不是设备所有者
404 设备未找到或尚未上报状态

GET /api/devices/{id}/nickname

需要认证 --- 仅设备所有者

获取设备的显示名称。如果用户设置了昵称则返回昵称,否则回退到设备的 hwId

路径参数:

参数 描述
id 设备的 UUID

响应: 200 OK

{
  "nickname": "客厅墨水屏"
}

错误:

状态码 原因
403 当前认证用户不是设备所有者
404 设备未找到

PUT /api/devices/{id}/nickname

需要认证 --- 仅设备所有者

设置或清除设备的显示名称。发送空字符串可清除昵称(后续 GET 将返回 hwId)。

路径参数:

参数 描述
id 设备的 UUID

请求体:

{
  "nickname": "客厅墨水屏"
}
字段 类型 必填 描述
nickname string 要设置的显示名称。空字符串 "" 将清除昵称

响应: 200 OK

{
  "message": "nickname updated"
}

错误:

状态码 原因
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": "a1b2c3d4-5678-9012-abcd-ef0123456789",
  "signature": "3f7a8b2c1d9e0f4a"
}
字段 类型 必填 描述
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": "A3X9K2"
}
字段 类型 必填 描述
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

{
  "message": "device unbound"
}

解绑后,后端通过 MQTT 向设备发送 unbound 命令。设备清除屏幕内容并重新请求配对码。

错误:

状态码 原因
401 访问令牌缺失或无效
403 当前认证用户不是设备所有者
404 设备未找到

POST /api/devices/{id}/cmd

需要认证 --- 仅设备所有者

向设备发送命令。命令通过 MQTT 发送到设备的 down/cmd 主题。

路径参数:

参数 描述
id 设备的 UUID

请求体:

{
  "kind": "text",
  "text": "Hello from Inklet!"
}
字段 类型 必填 描述
kind string 命令类型(目前用户命令仅支持 text
text string 条件必填 kindtext 时必须提供

响应: 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 指向的位图格式,取值 pngraw2raw4(见下表)。未知值回退为 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 指向的位图格式,取值 pngraw2raw4。未知值回退为 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

行为:

  1. 查找该设备下一个优先级最高的 QUEUE 状态推送
  2. 如果找到:将之前的 latestPushId 标记为 EXPIRED,将新推送标记为 PUBLISHED,更新 latestPushId,返回新推送(changed: true
  3. 如果没有可用的队列推送:返回当前最新推送(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 指向的位图格式,取值 pngraw2raw4。未知值回退为 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 按推送状态筛选(PREPAREQUEUEPUBLISHEDCONFIRMEDEXPIRED)。省略则返回全部。
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;当 hasMorefalse 时省略
hasMore boolean 本页之后是否还有更多推送

无限滚动

首页请求不带 cursor。当用户滚动到接近底部时,若 hasMoretrue,用 ?cursor={nextCursor} 请求下一页;hasMorefalse 时停止。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": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb" }
字段 类型 必填 描述
pushId UUID 要设为当前的推送,必须属于该设备且已渲染出文件

响应: 200 OK

{ "message": "current push set" }

错误:

状态码 原因
400 设备 id 或推送 id 无效
401 访问令牌缺失或无效
403 当前认证用户不是设备所有者
404 设备未找到,或该设备下未找到此推送,或推送尚未渲染

POST /api/devices/{id}/custom-push/upload

需要认证 --- 仅设备所有者

上传自定义图片显示的第 1 步。返回一个预签名 S3 上传凭证(仅图片,≤10 MB,15 分钟有效)。前端将图片直传 S3 后,再调用 confirm 接口。

路径参数:

参数 描述
id 设备的 UUID

请求体:

{ "contentType": "image/png", "sizeBytes": 204800 }
字段 类型 必填 描述
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": "01912345-2b44-7c2b-8e36-d367ecab52b7", "title": "度假照片" }
字段 类型 必填 描述
fileId UUID 上传步骤返回的 fileId
title string 可选标题,显示在推送列表中

响应: 200 OK

{ "pushId": "01912345-cccc-7abc-def0-dddddddddddd" }

错误:

状态码 原因
400 设备 id 或 file id 无效
401 访问令牌缺失或无效
403 当前认证用户不是设备所有者
404 设备或文件未找到
409 文件不处于可确认状态

错误响应

所有错误响应遵循统一的格式:

{
  "error": "device not found"
}
状态码 描述
400 请求无效 --- 请求体格式错误、缺少必填字段或格式无效
401 未授权 --- 访问令牌缺失、已过期或无效
403 禁止访问 --- 当前认证用户不是目标设备的所有者
404 未找到 --- 设备或资源不存在
409 冲突 --- 设备已被其他用户绑定
410 已失效 --- 配对码已过期,需要重新生成