bryanwintermute/unspooled

GitHub: bryanwintermute/unspooled

一个纯标准库实现的ESC/POS热敏收据打印机工具包,提供品牌无关的收据渲染器与Rongta RP332逆向NV配置CLI。

Stars: 0 | Forks: 0

# unspooled `unspooled` 在一个 repo 中包含两样东西: 1. **通用的 stdlib ESC/POS 渲染器** (`receipt_print.py`) —— 适用于 任何兼容 ESC/POS 的 80mm 或 58mm 热敏收据打印机 (Rongta, Epson, Star, Bixolon, Xprinter, …)。可作为 CLI 使用或 作为库导入: from receipt_print import Receipt r = Receipt(title="Costco", style="checkbox", print_width=42) r.add_items(["milk", "eggs", "bread"]) open("/dev/usb/lp0", "wb").write(r.to_bytes()) 2. **Rongta RP332 NV-config CLI** (`nv_config.py`, `ethernet_config.py`, `papersave_config.py`, `blackmark_config.py`, `other1_config.py`, 全部通过 `rongta_config.py` 进行调度) —— 无需专有的 Windows 工具即可 修改持久化的出厂默认设置(自动切纸刀、蜂鸣器、钱箱接口、 纸张宽度、DHCP、静态 IP、MAC、43 个代码页、黑标传感器、 省纸裁剪等)。 渲染器在设计上是**品牌无关的**,如果你要开发任何 向热敏收据打印机进行打印的功能,这正是你需要的部分。 Rongta CLI 出于必要性是**特定于品牌的** —— NV-config 通信协议是 Rongta 专有的。根据需要选择对应的模块使用即可。 ## 为什么叫 "unspooled"? 供应商工具通过 Windows 打印后台处理程序与打印机进行通信。 我们通过 Linux 上的 CUPS 日志后端(该打印机在 Wine 中 呈现为 CUPS 打印机)路由了该后台处理流, 并捕获了每一个字节。这个项目就是打印机从供应商流程中被 字面意义上的“抽出”(unspooled)—— 而 协议本身也被解开并形成了文档。 ## 硬件 - **渲染器 (`receipt_print.py`):** 任何兼容 ESC/POS 的热敏 收据打印机(80mm 或 58mm 打印头)。无供应商锁定。 - **NV-config CLIs (`rongta_config.py` 等):** Rongta RP332, USB id `0fe6:811e`(该打印机显示为 "ICS Advent Parallel Adapter" —— Rongta 许可了该 USB 转 并口芯片)。 **极有可能**也适用于其他共享 `PrinterTool.exe` 配置工具的 Rongta SKU(RP325, RP326, RP328 等),但 **未经过测试** —— 欢迎提交 PR。 ## 环境要求 - Python 3.9+ —— **仅使用 stdlib,无第三方包。** 这就是 整个运行时的依赖占用。 - 一台通过 USB 连接打印机的 Linux 主机 - 属于 `plugdev` 组的成员身份(这样你就可以在不使用 `sudo` 的情况下 写入打印机) - 安装此 repo 中的 udev 规则(`99-rongta-receipt.rules`) 到 `/etc/udev/rules.d/` ## 设置 ``` git clone git@github.com:bryanwintermute/unspooled.git cd unspooled # 安装 udev 规则(一次性操作,需要 sudo) sudo install -o root -g root -m 0644 99-rongta-receipt.rules \ /etc/udev/rules.d/99-rongta-receipt.rules sudo udevadm control --reload-rules sudo udevadm trigger --action=change /sys/class/usbmisc/lp0 sudo udevadm settle # 验证 symlink 是否存在 ls -la /dev/rongta-receipt # should point to usb/lp0 ``` 如果你还没有将自己添加到 `plugdev` 组中,请添加(之后 注销并重新登录): ``` sudo usermod -aG plugdev "$USER" ``` ## ⚠️ 安全 —— 在写入任何内容之前请阅读此内容 此 CLI 中的大多数命令都会写入打印机的 **NV-RAM**。这些 写入操作是**在电源循环后持久存在的** —— 除了将之前的值 写回之外,没有任何“撤销”的方法。错误的值可能 会使打印机进入一种状态,此时唯一的恢复途径就是 此 CLI 本身(这也是该项目事实上的恢复出厂设置)。 在进行任何切换之前,值得了解的三种具体故障模式: 1. **`rongta_config.py other1 usb-mode virtual-serial`** —— 使打印机重新枚举为 `/dev/ttyACM*` 而不是 `/dev/usb/lp0`。此 repo 中的 udev 规则不会对 `ttyACM` 设备触发,因此 `/dev/rongta-receipt` 将不存在。 你需要一个不同的恢复路径。不要随意运行此命令。 2. **`rongta_config.py ethernet mac `** —— 如果你更改了 MAC 并且忘记了原值,你无法通过 USB 读回它(固件会回显确认信息,但仅限于 你发送的值)。在更改之前,请务必 从开机自检报告中记下现有的 MAC。 3. **`rongta_config.py ethernet static --ip `** —— 错误的 静态 IP / 网关 / 子网掩码可能会将打印机隔离在其 自己的以太网上,但如果你是通过 USB 驱动它,则是 无害的。 **务必首先使用 `--dry-run`。** 每个命令都支持此参数。 打印出字节,仔细检查它们,然后再去掉该标志。 ``` ./rongta_config.py base --cutter on --buzzer on --dry-run # 1f 73 02 00 00 01 00 00 00 00 00 1f 72 00 1f 74 00 ``` 如果你确实搞砸了某个设置,请使用所需的值重新运行。 该 CLI 就是其自身的恢复出厂设置。 ## 快速参考 统一的入口点是 `rongta_config.py`。它会调度到 六个按标签页划分的模块(如果你喜欢更精简的帮助信息, 每个模块也都可以独立运行): ``` # 开箱即用:启用 DHCP,使打印机可在 LAN 上访问。 ./rongta_config.py ethernet dhcp on # 开箱即用:启用 NV-gated auto-cutter(出厂时关闭)。 ./rongta_config.py base --cutter on # 针对购物清单样式收据的激进省纸模式。 ./rongta_config.py papersave --delete-top enable --cut-line-interval 75% # 切换纸张宽度至 58mm。 ./rongta_config.py other1 print-width 58mm # 打印列表。 echo -e 'milk\neggs\nbread' | ./rongta_config.py print --title 'Costco' # 任意区域的完整帮助: ./rongta_config.py --help ``` ### 区域 | 区域 | 模块 | 覆盖范围 | |---|---|---| | `base` | `nv_config.py` | 切纸刀、蜂鸣器、钱箱接口、字体、浓度、字符/行、代码页(43 个命名条目,源自打印机自带的自检报告;5 个保留插槽可通过 `--code-page-raw` 访问)、波特率、校验、自动重印、打印后蜂鸣。 | | `ethernet` | `ethernet_config.py` | DHCP、静态 IP、子网掩码、网关、MAC 地址、链接模式。 | | `papersave` | `papersave_config.py` | 空白裁剪(使用标准 Epson `GS ( E`)。 | | `blackmark` | `blackmark_config.py` | 黑标传感器:启用/禁用、长度、宽度、打印/切纸偏移量。 | | `other1` | `other1_config.py` | 纸张宽度 (80mm/58mm)、蜂鸣器音量、报警、USB 枚举模式、中文字符模式、切纸刀计数查询。 | | `print` | `receipt_print.py` | 将列表(带有 `--title`, `--style`, `--print-width`)渲染为标准 Epson ESC/POS。品牌无关。 | ## 作为库使用 (`receipt_print.py`) `receipt_print.py` 专门设计为可供下游 项目(例如 [`tickertape`](https://github.com/bryanwintermute/tickertape)) 导入, 而无需引入任何 Rongta 特定的模块。它 仅使用 stdlib、品牌无关,并且整个字节生成范围 都是标准 Epson ESC/POS(init、代码页 CP437、对齐、字体大小、 加粗、全切)。 ``` from receipt_print import Receipt # 80mm 打印头,Font A(默认设置 — 42 列)。 r = Receipt(title="Costco", style="checkbox") r.add_items(["milk", "eggs", "bread"]) with open("/dev/usb/lp0", "wb") as f: f.write(r.to_bytes()) # 58mm 打印头 — 传入 print_width=32。 r58 = Receipt(title="Reminders", style="bullet", print_width=32) r58.add_items(["pick up package", "water plants"]) ``` 构造函数签名: ``` Receipt( title: str | None = None, # optional bold/centered/upper-cased title timestamp: bool = True, # adds a YYYY-MM-DD HH:MM line under the title items: list[str] = [], # or use .add_item() / .add_items() style: str = "checkbox", # 'checkbox' / 'numbered' / 'bullet' / 'plain' cut: bool = True, # GS V 0 full-cut at the end print_width: int = 42, # 42=80mm Font A, 32=58mm Font A, 56=80mm Font B sanitize: bool | dict | callable = True, # see "Text sanitization" below ) ``` `Receipt.to_bytes()` 方法在给定相同 输入(时间戳除外)的情况下是确定性的,并且字节格式由 `tests/test_receipt_print_library.py` 中的测试套件锁定。 ### 直接渲染 Markdown 现实世界中的文本(笔记应用、网页粘贴、生成的待办事项列表) 通常是 Markdown 风格的。`render_markdown()` 解析受约束的 CommonMark 子集并发出 ESC/POS 字节 —— 无需第三方 Markdown 库。 ``` from receipt_print import render_markdown md = """# Shopping List Generated for **Saturday**. - [ ] milk - [ ] eggs - [x] bread (already bought) ## 说明 Store closes at 8pm. --- """ bytes_out = render_markdown(md, title="Costco", print_width=42) open("/dev/usb/lp0", "wb").write(bytes_out) ``` 支持的子集(stdlib 正则表达式分词器 —— 有关完整语法,请参见 `tests/test_markdown.py`): | Markdown | ESC/POS 渲染 | |---|---| | `# H1` | 两倍大小 + 加粗 + 居中 | | `## H2` | 加粗 + 居中 | | `### H3` | 加粗 + 左对齐 | | `**bold**` (inline) | 加粗跨度 | | `- item` / `* item` | 项目符号列表 | | `1. item` (literal numbers) | 编号列表 | | `- [ ] item` / `- [x] item` | 复选框(状态保留) | | `---` / `***` / `___` | 跨越 `print_width` 的水平线 | | paragraph | 换行至 `print_width`,连续行折叠 | `Receipt.from_markdown(text, **kwargs)` 也可作为 类方法使用,以保持 API 的对称性;它是 `render_markdown()` 的轻量级封装,并直接返回字节。 特意排除在范围之外 (v1):表格、代码块、图像、链接 (打印机无法跟踪它们)、嵌套列表、块引用。 ### 文本清理 默认情况下(自 v0.3.0 起),文本在 CP437 编码之前会通过 NFKD + 智能引号 / 破折号 / 省略号 / 箭头转换处理。这意味着从网页 粘贴的剪贴板内容不再静默渲染为 `?` 字符。 ``` # Smart quotes -> straight, em-dash -> --, ellipsis -> ..., café -> cafe. r = Receipt(items=['He said "hello"—then left…']) # 退出以使用 v0.2.0 的行为(原始 CP437 errors='replace'): r = Receipt(items=['raw\u00B5'], sanitize=False) # 扩展内置 map: r = Receipt(items=['10 \u00B5s'], sanitize={"\u00B5": "u"}) # -> "10 us" # 或者传入一个完整的自定义 callable: r = Receipt(items=['hello'], sanitize=lambda s: s.upper()) # -> "HELLO" ``` 内置转换表公开为 `DEFAULT_SANITIZE_MAP` 以供检查或扩展。如果 你想在 `Receipt` / `render_markdown` API 之外预处理文本, `sanitize()` 函数本身也是可导入的。 ## 命令族目录 供应商工具发出的大约一半内容是**标准 Epson ESC/POS** —— 记录在公开的 Epson TM-T88 / TM-T20 规范中。 另一半是无公开文档的 Rongta 供应商扩展。 | 前缀 | 族 | 覆盖范围 | |---|---|---| | `1f 73 XX ` | Rongta vendor | 基本标签页配置 (+ 子函数 `1f 72`, `1f 74`) | | `1f 69`, `1f 25`, `1f 4e`, `1f 6d`, `1f 70`, `1f 62 44` | Rongta vendor | 以太网 (IP/子网掩码/网关/MAC/双工/DHCP) | | `1f 1b 1f XX ` | Rongta vendor extended | BlackMark + Other1 | | `1f 7b X ` | Rongta vendor mode toggles | 纸张传感器 (`'p'`), USB 模式 (`'u'`) | | `1d 28 45 ...` | **Standard Epson `GS ( E`** | PaperSave + 音量 | | `1d 28 46 ...` | **Standard Epson `GS ( F`** | BlackMark 打印/切纸后偏移量 | | `1d 56 00` | **Standard Epson `GS V 0`** | 全切 (runtime) | | `12 54` | **Standard Epson DC2 'T'** | 自检触发器 | | `1b 1b 45 ... 0c 5a` | Rongta vendor "structured" | 重置按钮 —— 在此固件上发出 "Setting Fail!"。有文档记录但无实际功能。 | ## 我们是如何获取字节的 完整技术细节见 [`docs/wine-cups-backend-recovers-nv-bytes.md`](docs/wine-cups-backend-recovers-nv-bytes.md)。 简短版本: 1. **usbip 导出打印机** 从其所在的 Pi 导出到 x86_64 Linux 主机(这样 Wine 就可以在 x86 上运行,而打印机 保留在 Pi 上)。 2. **在 Wine 下运行 `PrinterTool.exe`**(Xvfb + x11vnc 让你能够 通过手机 VNC 客户端点击操作它)。 3. **安装自定义 CUPS 后端** 位于 `/usr/lib/cups/backend/rongta`(模式为 `0700` 以便以 root 身份运行), 将每个打印后台处理任务 `tee`(重定向并保留)到 `/tmp/rongta-writes/.bin`。 4. **点击 GUI**:每次点击 = 一个标记的 `.bin` 文件。对比它们以找出发生变化的字节。 一个具体的例子 —— 基本标签页的 "Set" 命令具有四个 独立的状态(全关、仅切纸刀、仅钱箱、仅蜂鸣器), 生成这四个 17 字节的文件。按列对齐它们: ``` ┌─cutter │ ┌─buzzer │ │ ┌─drawer all-off : 1f 73 02 | 01 01 01 | 00 00 00 00 00 | 1f 72 00 | 1f 74 00 cutter-only : 1f 73 02 | 00 01 01 | 00 00 00 00 00 | 1f 72 00 | 1f 74 00 drawer-only : 1f 73 02 | 01 01 00 | 00 00 00 00 00 | 1f 72 00 | 1f 74 00 buzzer-only : 1f 73 02 | 01 00 01 | 00 00 00 00 00 | 1f 72 00 | 1f 74 00 └────────────┘ three settings, inverted booleans (0 = on, 1 = off) ``` 位置 3 仅在切换切纸刀时改变,位置 4 仅在切换 蜂鸣器时改变,位置 5 仅在切换 钱箱时改变。编码是 反转的(0 = 开启,1 = 关闭),因为出厂固件在交付时 所有功能均关闭,而 "0" 表示 "默认无附加项"。四次 点击 → 30 秒的对比即可完成完整的位映射。 对于大型的枚举下拉菜单(如代码页),还有一个更 省事的技巧:**静态分析 PE 二进制文件**。MFC 下拉菜单 标签作为连续的字符串字面量存储在二进制文件的 `.rdata` 段中。MSVC 以**下而上**(相反的源代码 顺序)发出它们,因此: ``` strings -el -t d PrinterTool.exe | grep -E '^(CP|WCP|ISO|Katakana)' | sort -rn ``` …以它们的*视觉*顺序为你提供下拉菜单标签。 **重要提示:** 下拉菜单顺序与通信字节顺序不同。 RP332 的前 6 个代码页条目 (CP437, Katakana, CP850/860/863/865) 恰好是通信字节 0-5,因为最常用的 页面被列在最前面,并且恰好具有最低的 枚举值 —— 但在此之后,下拉顺序就会出现偏差。 **事实证明,真正廉价的真理来源是打印机自带的 自检报告**:RP332 的开机诊断会原封不动地打印其 完整的 48 项代码页表。我们只是之前没有读完 收据的最底部。在进行静态分析之前,务必 阅读设备公开的每一份诊断输出。完整的总结在 [`docs/wine-cups-backend-recovers-nv-bytes.md`](docs/wine-cups-backend-recovers-nv-bytes.md)。 ## 更多阅读 有关完整的经验总结,请参见 [`docs/`](docs/): - [`wine-cups-backend-recovers-nv-bytes.md`](docs/wine-cups-backend-recovers-nv-bytes.md) — 具有 7 种不同 RE 模式的核心技术。 - [`vendor-mobile-sdks-may-stub-nv-config.md`](docs/vendor-mobile-sdks-may-stub-nv-config.md) — 前传:我们如何反编译 Rongta 的 iOS/Android SDK 并 证明 NV-config 方法只是占位符。 - [`escpos-thermal-printers-need-no-cups-driver.md`](docs/escpos-thermal-printers-need-no-cups-driver.md) — 基础课程:ESC/POS 打印机在基本 打印路径下不需要 CUPS。 - [`rongta-rp332-vendor-tool-replacement-recap.md`](docs/rongta-rp332-vendor-tool-replacement-recap.md) — 项目总结:构建了什么,还有什么待完成 (TODO)。 - [`udev-settle-after-trigger-or-rebind.md`](docs/udev-settle-after-trigger-or-rebind.md) — 设置方案中使用的触发/稳定原则。 ## 状态 / TODO `PrinterTool.exe` v2.63.0 中所有主要的 NV 设置标签页都已完成 逆向工程。剩余项目(全部是锦上添花的功能): - **蓝牙设置标签页** —— RP332 没有 BT 硬件;该标签页可能 发出空操作命令,或者预览其他 Rongta SKU 的不同 功能族。 - **UDP 发现**(工具的“搜索打印机”标签页)—— 如果能有 一个等效的 Python 实现就太好了。 - **捕获切纸刀统计响应** —— 切纸刀计数查询作为 `rongta_config.py other1 cutter-query` 公开,但 响应在 BULK-IN 上传回,而 CUPS 后端不会 转发它。使用 `usbmon` 捕获以进行解码和解析。 - **寻找有效的恢复出厂设置命令** —— GUI 的重置 按钮发出一个 13 字节的结构化数据包,固件 拒绝了它("Setting Fail!")。尾部的 `0c 5a` 看起来像 校验和;弄清楚它并发送真正的重置命令将会很棒。 有关完整的愿望清单,请参见 [`docs/rongta-rp332-vendor-tool-replacement-recap.md`](docs/rongta-rp332-vendor-tool-replacement-recap.md)。 ## 许可证 Apache-2.0。请参见 [`LICENSE`](LICENSE)。 ## 贡献 欢迎提交 PR。有关添加新 Rongta SKU(或使用我们尚未 捕获的字节扩展协议目录)的快速路径,请参见 [`CONTRIBUTING.md`](CONTRIBUTING.md)。
标签:ESC/POS, Python, SOC Prime, 云资产清单, 开发工具, 无后门, 热敏打印机, 硬件接口, 逆向工具, 逆向工程