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, 安全插件, 热敏打印机, 物联网, 硬件通信, 蓝牙