跳转至

命令处理

sim-hw 订阅 inklet/dev/{thingName}/down/cmd MQTT 主题,处理来自后端的命令。每个命令通过 kind 字段决定处理方式。

命令参考

类型 显示操作 描述
text 将文本渲染为 800x480 位图,推送到 sim-dashboard 用户发送的文本内容,用于电子墨水屏显示
claim_code 渲染大尺寸的居中配对码,推送到显示 用于设备绑定的 6 位配对码
bound 显示"Device bound successfully"消息 绑定完成的通知
unbound 清除显示内容,通过 MQTT 重新请求配对码 设备已解绑的通知
already_bound 不进行显示操作(仅记录日志) 请求配对码时设备已绑定的响应

text

主要的命令类型。将用户提供的文本内容渲染到模拟电子墨水屏上。

MQTT 载荷:

{
  "kind": "text",
  "id": "01912345-9999-7abc-def0-aaaaaaaaaaaa",
  "text": "Hello from Inklet!\n\nThis is a multi-line message that will be word-wrapped to fit the 800×480 display."
}

行为:

  1. down/cmd 主题接收命令载荷
  2. 提取 text 字段
  3. 使用 Pillow 将文本渲染为 800x480 的 1-bit(单色)位图
  4. 对过长的行自动换行以适应显示宽度
  5. 将渲染后的图像导出为 PNG
  6. 通过 HTTP POST 将 PNG 发送到 sim-dashboard 的 Fastify 服务器
  7. sim-dashboard 将新的帧缓冲广播给所有已连接的 WebSocket 客户端

渲染细节:

  • 画布尺寸:800x480 像素
  • 色彩模式:1-bit(黑白),与真实电子墨水屏能力一致
  • 背景色:白色(1
  • 文字颜色:黑色(0
  • 字体:系统默认字体(或内置等宽字体)
  • 自动换行:根据字符宽度和画布尺寸自动换行
  • 内边距:四周留有边距,防止文字紧贴边缘

claim_code

在显示屏上醒目地渲染 6 位配对码。用户在门户或 sim-dashboard 中输入该码即可绑定设备。

MQTT 载荷:

{
  "kind": "claim_code",
  "code": "A3X9K2"
}

行为:

  1. 从后端接收配对码
  2. 在 800x480 画布上以大号居中字体渲染配对码
  3. 包含提示文字(例如"Enter this code to pair your device")
  4. 将渲染后的 PNG 推送到 sim-dashboard

显示布局:

┌──────────────────────────────────────────┐
│                                          │
│                                          │
│              Enter this code             │
│            to pair your device           │
│                                          │
│               A 3 X 9 K 2               │
│                                          │
│                                          │
│                                          │
└──────────────────────────────────────────┘

提示

配对码使用大尺寸的等宽高对比度字体,确保在模拟显示屏上也具有良好的可读性,即使 sim-dashboard 应用了电子墨水效果也是如此。


bound

设备成功绑定到用户账户时显示。

MQTT 载荷:

{
  "kind": "bound",
  "userId": "01912345-0000-7abc-def0-000000000001"
}

行为:

  1. 渲染确认消息:"Device bound successfully"
  2. 将渲染后的 PNG 推送到 sim-dashboard
  3. 设备现在可以接收来自所有者的 text 命令

unbound

设备所有者解绑设备时接收。设备应清除显示内容并回到配对状态。

MQTT 载荷:

{
  "kind": "unbound"
}

行为:

  1. 清除当前显示内容
  2. inklet/dev/{thingName}/up/request_claim 发布 request_claim 消息
  3. 后端生成新的配对码并通过 claim_code 命令发送回来
  4. 设备显示新的配对码,准备重新配对

这形成了一个自动循环:解绑触发配对码请求,配对码请求触发配对码显示。


already_bound

当设备发送 request_claim 消息但已绑定到用户时接收。

MQTT 载荷:

{
  "kind": "already_bound"
}

行为:

  1. 记录日志:INFO Device is already bound, ignoring claim request
  2. 不进行显示操作
  3. 设备继续正常运行

这通常发生在设备重启后,在收到首次心跳响应之前发送了 request_claim。后端识别出设备已绑定,发送 already_bound 而非配对码。


显示渲染流水线

所有产生显示输出的命令遵循相同的渲染流水线:

接收命令(MQTT)
从载荷中提取文本/配对码
Pillow Image.new("1", (800, 480), 1)    ← 白色背景
ImageDraw.text() with word wrapping      ← 黑色文字
image.save(buffer, format="PNG")         ← 导出为 PNG
HTTP POST to sim-dashboard               ← 推送帧缓冲
WebSocket broadcast to all clients       ← 实时显示更新

使用 Pillow 渲染

sim-hw 使用 Pillow 库生成 1-bit 单色图像。核心渲染逻辑:

from PIL import Image, ImageDraw, ImageFont

# Create a 1-bit white canvas
img = Image.new("1", (800, 480), 1)
draw = ImageDraw.Draw(img)

# Word-wrap and draw text
draw.text((20, 20), wrapped_text, fill=0, font=font)

# Export as PNG bytes
buffer = io.BytesIO()
img.save(buffer, format="PNG")
png_bytes = buffer.getvalue()

推送到 sim-dashboard

渲染后的 PNG 通过 HTTP POST 发送到 sim-dashboard 的 Fastify 服务器:

POST {sim-url}/api/device/{thingName}/framebuffer
Content-Type: image/png
Body: <raw PNG bytes>

Fastify 服务器存储帧缓冲并广播给所有已连接的 WebSocket 客户端。React 前端以逼真的电子墨水效果渲染图像。

错误处理

如果向 sim-dashboard 的 HTTP POST 失败(例如服务器未运行),sim-hw 会记录警告并继续运行。MQTT 连接和心跳不受显示渲染失败的影响。