slastra/yplib
GitHub: slastra/yplib
yplib 是一个 TypeScript 实现的 YPL 热敏标签打印机协议库,通过逆向工程为 KNAON / FlashToy 等贴牌打印机提供组帧、光栅编码及 Web Bluetooth 通信能力。
Stars: 0 | Forks: 0
# yplib
**YPL 热敏标签打印机协议,使用 TypeScript 实现。** 包含组帧、CRC、1-bit 光栅编码以及一个 Web Bluetooth 传输层。核心部分零依赖、不涉及 DOM,且每个常量均已经过硬件抓包验证。
```
npm install @slastra/yplib
```
YPL 是一系列廉价贴牌热敏标签打印机所使用的传输格式,这些打印机以 **KNAON**、**FlashToy** 等品牌名义销售。尽管这些设备附带的厂商工具可能会有所暗示,但它**并非 TSPL**。请参阅 [FINDINGS.md](./FINDINGS.md) 了解该协议是如何推导出来的。
## 快速开始
在浏览器中通过 canvas 打印标签:
```
import { buildStream, imageDataToRows } from '@slastra/yplib';
import { connect } from '@slastra/yplib/web-bluetooth';
const ctx = canvas.getContext('2d')!; // 400 × 240 for 50 × 30 mm stock
const rows = imageDataToRows(ctx.getImageData(0, 0, canvas.width, canvas.height));
const printer = await connect(); // must be called from a user gesture
await printer.send(buildStream(rows, 50));
```
带有进度显示和取消功能的批量打印:
```
import { printJob } from '@slastra/yplib';
const ac = new AbortController();
const printed = await printJob(
printer,
rowsPerLabel.map((rows) => () => Promise.resolve(buildStream(rows, 50))),
{ signal: ac.signal, onProgress: (done, total) => console.log(`${done}/${total}`) }
);
```
标签采用惰性构建与流水线处理:在标签 _n_ 仍在传输时,标签 _n+1_ 就已经开始渲染。打印任务会在标签之间检查打印机状态,因此一旦发生卡纸或盖子打开,就会停止运行,而不会继续向无法接收数据的打印机发送数据。
## 它不能做什么
**渲染由你自己完成。** 本库负责将像素转换为传输字节;它不会决定你的图像如何转换为 1-bit。我们提供了 `lumaOverWhite` 和 `imageDataToRows`,因为如果亮度公式弄错了,会导致预览与实际纸张效果不一致,但二值化策略、抖动 (dithering) 和布局则完全由你掌控。
如需基于此库构建的完整标签设计器,请参阅 [printrow](https://github.com/slastra/printrow)。
## API
### 核心部分 — `@slastra/yplib`
纯实现。可在浏览器、Node、Bun 和 Deno 中同样运行,无任何依赖。
| | |
| --------------------------------------------------- | ---------------------------------------------------------- |
| `frame(payload)` | 将 payload 包装为 `1a 01 a1` |
| `crc32(bytes)` | 反向 CRC-32,多项式 `0xEDB88320`,**初始值 `0xCA896ADE`** |
| `encodeRaster(rows)` / `decodeRaster(blob)` | 1-bit RLE 编解码器 |
| `buildStream(rows, widthMm, job?, trailer?)` | 一个完整的打印任务 |
| `parseReplies(buf)` | 支持重新同步的入站帧解析器 |
| `describeStatus(v)` / `STATUS_FLAGS` | 人类可读的打印机状态 |
| `imageDataToRows(src, threshold?)` | RGBA 像素 → 打印机行 |
| `lumaOverWhite(data, i)` | 基于 BT.601 亮度,在白色介质上合成 |
| `printJob(link, builds, opts?)` / `waitReady(link)` | 任务编排 |
| `selftest()` | 运行抓包推导出的测试向量;返回 `[]` 表示通过 |
### 传输层 — `@slastra/yplib/web-bluetooth`
`connect(options?)` 返回一个包含 `send`、`readStatus`、`disconnect` 和 `deviceName` 的 `Link` 对象。选项包括:`namePrefix`(默认为 `'Y50P'`)、`chunkSize` (20)、`paceMs` (8)、`statusTimeoutMs` (500) 和 `onDisconnect`。
`isSupported()` 会同时检查两项要求;`hasBluetooth()` 和 `isSecureContext()` 则分别对它们进行检查,因为对应的修复方式不同——缺少 API 意味着浏览器不对,而不安全的上下文意味着源 (origin) 不对。
你可以自行实现包含两个方法的 `Link` 接口,以便通过 Web Serial、经典的 SPP socket 或 `/dev/usb/lpN` 来驱动相同的协议。该协议与传输层无关;已通过这三种方式进行验证。
## 两个会让你吃尽苦头的坑
**光栅行不包含长度字段。** 如果某一行的宽度(以点数为单位)与介质宽度不完全一致,就会导致后续所有行的标记错位,固件可能会因此卡死。这绝非危言耸听——在开发过程中曾导致打印机死机两次。`buildStream` 会拒绝编码宽度错误的光栅,而不是让你在硬件上才发现问题。
**切勿通过 service UUID 过滤设备发现。** 使用 service-UUID 过滤器会导致 Chrome 向 BlueZ 推送一个 `SetDiscoveryFilter` UUID 列表,这会在桌面版 Linux 上**导致 `bluetoothd` 5.87 发生段错误 (segfault)**,从而使用户的整个蓝牙协议栈崩溃。出于这个原因,`connect()` 改为通过设备名称进行过滤。
## 硬件
已在 **KNAON Y50P** 上验证,使用 50 × 30 mm 介质,分辨率为 8 dots/mm (400 × 240),通过 USB、经典 Bluetooth SPP 和 BLE 进行连接。其他介质高度也是安全的,因为协议从不传输高度:打印机会持续接收数据行,直到光栅数据结束。
除 50 mm 外的宽度遵循已抓取的帧格式,但未在真实介质上进行过测试。
由于该硬件是贴牌产品,因此机箱上的商标并不能作为判断依据,USB 厂商 ID(`0x5958` 未注册,且与其他使用 TSPL 协议的打印机共用)也同样无法作为判断依据。最终决定性的依据是传输格式:
`reference/parse_frames.py` 可以直接根据抓包数据回答这个问题。
## 开发说明
```
bun install
bun test # unit vectors, capture conformance, transport
bun run check # tsc --noEmit
bun run build # dist/
```
测试套件会根据其自身解码出的光栅数据,**逐个字节地**重新构建真实的硬件抓包数据。只要测试通过,就意味着在传输数据上,本实现与厂商应用的输出别无二致。
## 鸣谢
本库为独立推导得出,随后发现与 [Souukou/OpenBluetoothPrinter](https://github.com/Souukou/OpenBluetoothPrinter) 的研究结果一致,后者将该协议命名为 YPL,并且其研究成果提供了此处使用的命令名称和三个状态位。请参阅 [ACKNOWLEDGEMENTS.md](./ACKNOWLEDGEMENTS.md)。
## 许可证
MIT
标签:MITM代理, TypeScript, 安全插件, 热敏打印机, 物联网, 硬件通信, 蓝牙