Skip to content

bitpixi2/openlabel-sk20-linux-hack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

openlabel-sk20-linux-hack

Linux compatibility work for the OpenLabel SK20 thermal printer.

寻找共同的语言

Finding a shared language between hardware and software

OpenLabel SK20 during Linux Bluetooth/RFCOMM testing (April 2026)

OpenLabel SK20 during Linux Bluetooth/RFCOMM testing (April 2026)

This repo exists because there is no official Linux driver. The key hardware details were identified from the vendor's publicly available driver packages:

  • model: SK20
  • legacy alias: SK25
  • Windows queue alias: OpenLabel_CM20
  • USB VID:PID: EA62:1117
  • resolution: 203 dpi
  • driver family: POS80
  • command mode: ESC

That points to an ESC/POS-style USB printer path on Linux instead of a custom kernel driver port.

Status: Partial Success (Bluetooth RFCOMM connects, print output unconfirmed)

What works:

  • Bluetooth device discovery and pairing
  • RFCOMM channel 1 connection established
  • Raw bytes successfully written to printer over Bluetooth
  • Python script with --bluetooth-address support for direct printing

What's blocked:

  • Physical print output not confirmed yet via Linux RFCOMM (bytes sent, but printer may need specific initialization sequence or data format)
  • Possible issues: wrong ESC/POS command set, missing handshake, or vendor-specific protocol layer
  • Note: The printer hardware is confirmed working - it prints successfully from the official iOS app over BLE, and paper is loaded correctly

Debugging journey: This was debugged using a multi-agent workflow with OpenClaw (main orchestration), Hermes (monitoring), Claude Code, and Codex agents to identify the Bluetooth protocol, fix Python socket issues, and iterate on ESC/POS command sequences.

Contents

Quick start

Probe for the printer:

python3 scripts/openlabel_sk20.py probe

Show the reverse-engineered transport identifiers:

python3 scripts/openlabel_sk20.py transport-info

Scan nearby Bluetooth devices and flag likely OpenLabel matches:

python3 scripts/openlabel_sk20.py scan-bluetooth
python3 scripts/openlabel_sk20.py scan-bluetooth --all

Install the Python BLE client before using the BLE provisioning commands:

python3 -m pip install bleak

Broadcast the recovered Android UDP LAN-discovery probe:

python3 scripts/openlabel_sk20.py scan-wifi

Query the printer's current Wi-Fi state over BLE:

python3 scripts/openlabel_sk20.py ble-query-ip AA:BB:CC:DD:EE:FF
python3 scripts/openlabel_sk20.py ble-query-dhcp AA:BB:CC:DD:EE:FF

Send a raw Feasycom AT command over BLE:

python3 scripts/openlabel_sk20.py ble-send-at AA:BB:CC:DD:EE:FF AT+VER

Provision Wi-Fi over BLE using the same Feasycom flow as the Android app:

python3 scripts/openlabel_sk20.py ble-provision-wifi AA:BB:CC:DD:EE:FF "MySSID" "MyPassword"
python3 scripts/openlabel_sk20.py ble-provision-wifi AA:BB:CC:DD:EE:FF "MySSID" "MyPassword" \
  --static-ip 192.168.1.50 --gateway 192.168.1.1 --mask 255.255.255.0 --dns 1.1.1.1

Transport flow

From the Linux host to a reachable print transport.

flowchart TD
    A["Linux host"]
    B["USB EA62:1117"]
    C["Classic Bluetooth SPP"]
    D["BLE Feasycom control"]
    E["AUTH handshake"]
    F["Open AT engine"]
    G["AT+LIP / AT+DHCP / AT+RAP"]
    H["Wi-Fi configured"]
    I["UDP 7300 -> 7200 probe"]
    J["Printer reply: mfg;model;cmd;ip;port;"]
    K["Raw TCP print on 9100"]

    A --> B
    A --> C
    A --> D
    D --> E
    E --> F
    F --> G
    G --> H
    A --> I
    I --> J
    H --> K
    J --> K

    classDef rose fill:#F9EEF2,stroke:#C98EA2,color:#5B3241,stroke-width:1.2px;
    classDef blush fill:#F6E2E9,stroke:#C98EA2,color:#5B3241,stroke-width:1.2px;
    classDef petal fill:#F2D6E0,stroke:#B97A92,color:#4F2B38,stroke-width:1.2px;

    class A,I blush;
    class B,C,H,J rose;
    class D,E,F,G,K petal;
Loading

Print a test page:

python3 scripts/openlabel_sk20.py test-page

Print a test page directly over classic Bluetooth RFCOMM:

python3 scripts/openlabel_sk20.py test-page --bluetooth-address AA:BB:CC:DD:EE:FF

Tip: find the printer address first with:

python3 scripts/openlabel_sk20.py scan-bluetooth

Print a line of text:

python3 scripts/openlabel_sk20.py print-text "hello from linux"

Print a line of text directly over classic Bluetooth RFCOMM:

python3 scripts/openlabel_sk20.py print-text "hello from bluetooth" --bluetooth-address AA:BB:CC:DD:EE:FF
python3 scripts/openlabel_sk20.py print-text "hello from bluetooth" --bluetooth-address AA:BB:CC:DD:EE:FF --rfcomm-channel 1 --timeout 20

Send a raw ESC/POS payload:

python3 scripts/openlabel_sk20.py send-raw ./job.bin

Render a CUPS raster fixture into ESC/POS bytes:

python3 scripts/openlabel_sk20.py render-raster ./job.raster --output ./job.bin

CUPS path

The repo now contains a complete Linux CUPS path for USB printing:

  • scripts/openlabel_sk20.py doubles as:
    • a normal user CLI
    • a CUPS backend when installed as openlabel-sk20
    • a CUPS raster filter when installed as rastertoopenlabel-sk20
  • cups/OpenLabel-SK20.ppd provides a queue definition and print options
  • scripts/install_linux_cups.sh installs the backend, filter, PPD, and udev rule, then creates a queue

On a Linux host with CUPS installed:

sudo ./scripts/install_linux_cups.sh
lpstat -v | grep OpenLabel_SK20
lp -d OpenLabel_SK20 /etc/services

Print pipeline

From application output to bytes at the printer interface.

flowchart TD
    A["Application or lp"]
    B["CUPS queue"]
    C["rastertoopenlabel-sk20"]
    D["ESC/POS raster job"]
    E["openlabel-sk20 backend"]
    F["/dev/usb/lpN"]
    G["OpenLabel SK20"]
    H["scan-bluetooth"]
    I["Bluetooth candidates"]
    J["scan-wifi"]
    K["UDP discovery results"]
    L["ble-provision-wifi"]
    M["Printer joins LAN"]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G
    H --> I
    I --> A
    J --> K
    K --> A
    L --> M
    M --> K

    classDef rose fill:#FBF1F4,stroke:#C98EA2,color:#5B3241,stroke-width:1.2px;
    classDef blush fill:#F7E5EB,stroke:#C98EA2,color:#5B3241,stroke-width:1.2px;
    classDef petal fill:#F1D3DE,stroke:#B97A92,color:#4F2B38,stroke-width:1.2px;

    class A,B,C,D,E,F,G rose;
    class H,J,L blush;
    class I,K,M petal;
Loading

Status

This is now a Linux USB driver path with CUPS integration.

What is here:

  • USB detection via Linux sysfs
  • usblp device-node lookup
  • raw ESC/POS writes
  • a CUPS backend for USB discovery and job delivery
  • a CUPS raster filter that turns monochrome raster pages into ESC/POS raster commands
  • a PPD and install script for queue setup
  • a Linux Bluetooth scan helper keyed to the UUIDs exposed by the Android app
  • direct classic Bluetooth RFCOMM printing for raw payloads, text, and test pages
  • a Linux BLE client for Feasycom auth, AT commands, IP/DHCP queries, and Wi-Fi provisioning
  • a Linux UDP LAN scanner that reproduces the Android app's 7300 -> 7200 broadcast probe
  • unit tests for probe, backend, raster parsing, Feasycom auth, and UDP discovery parsing

What is still missing:

  • real hardware validation on Linux with the physical printer
  • tuning for density, feed, and any cutter quirks on the real unit
  • a first-class Linux TCP print helper for the already-provisioned Wi-Fi path
  • coverage for any vendor-specific commands beyond standard Feasycom and ESC/POS flows

Hardware testing log (April 2026) — help wanted

We have a physical SK20 unit on the bench connected to a Linux box (Ubuntu, BlueZ 5.72, Intel 8087:0a2a adapter). Everything below was tested against real hardware. We got as far as the printer's indicator light turning blue (data received) but no paper output. This section documents every blocker we hit so that the manufacturer or community can help close the last gap.

What works on real hardware

Layer Result
Classic BT discovery SK20_6CBC found via bluetoothctl scan on
Classic BT pairing Pairing successful with link key exchange
BLE discovery SK20_BLE_6CBC found at a separate MAC
RFCOMM channel 1 bind /dev/rfcomm0 bound, kernel auto-connects on open
RFCOMM tty write write() returns success, tcdrain() completes
CUPS backend job delivery Jobs complete with CUPS_BACKEND_OK (status 0)
CUPS raster filter Converts application/vnd.cups-raster to ESC/POS GS v 0 raster commands
Printer acknowledgement Blue indicator light turns on when data is written via rfcomm0 tty
Unit tests All 12 pass (raster parsing, ESC/POS rendering, Feasycom AUTH vector, etc.)

What is blocked

1. Feasycom module silently swallows data on classic BT SPP

The SK20 uses a Feasycom Bluetooth module (vendor UUID ffcacade-afde-cade-defa-cade00000000) that acts as a gateway between Bluetooth and the printer's UART. On Linux:

  • RFCOMM connections are accepted on all channels 1-10 (not just the SPP channel).
  • Zero bytes are ever returned from the printer on any channel.
  • No response to ESC/POS DLE EOT status requests (\x10\x04\x01).
  • No response to Feasycom AUTH packets over SPP.
  • No response to AT\r\n, AT+VER\r\n, Hayes +++, or $OpenFscAtEngine$.
  • Data written via raw Bluetooth sockets (AF_BLUETOOTH + BTPROTO_RFCOMM) is silently dropped — no blue light, no response.
  • Data written via rfcomm0 tty (/dev/rfcomm0) triggers the blue light but produces no print output.

The critical difference: the rfcomm tty path sends DTR/RTS modem status signals (MSC frames) that raw sockets do not. The Feasycom module appears to require DTR assertion before it begins forwarding data to the printer's UART. But even with DTR (blue light on), the printer does not print.

What we need from the manufacturer: Does the Feasycom module require a specific initialization sequence over classic BT SPP before it enters transparent data mode? Is there a Feasycom AT command or handshake required over SPP (separate from the BLE AUTH flow)?

2. BLE GATT connection times out

The BLE device (SK20_BLE_6CBC) advertises and is discoverable, but:

  • BleakClient.connect() times out after 20 seconds every attempt.
  • bluetoothctl connect <BLE_MAC> returns org.bluez.Error.NoReply.
  • D-Bus org.bluez.Device1.Connect() also times out.
  • This happens regardless of whether classic BT is connected or disconnected.

Without a working BLE connection, we cannot test:

  • The Feasycom TEA-encrypted AUTH handshake (code is implemented, test vectors pass).
  • The $OpenFscAtEngine$$OK,Opened$ session open.
  • AT commands (AT+VER, AT+LIP, etc.).
  • Whether BLE GATT writes can deliver ESC/POS print data directly.

What we need: Is the BLE GATT interface gated behind a physical button press or mode switch? Does the SK20 require BLE pairing before GATT connections are accepted? Is BLE only active during a short window after power-on?

3. CUPS raster page dimensions may be wrong

When the CUPS filter chain runs outside of cupsd (manual testing), cfFilterPDFToRaster warns:

WARN: Could not determine the output page dimensions, falling back to US Letter format

This produces a 1624-pixel-wide raster (US Letter at 203 dpi) instead of the correct 640 pixels (80 mm at 203 dpi). The ESC/POS GS v 0 command then specifies 203 bytes per row — far wider than the SK20's 80 mm print head. This may cause the printer to reject the raster command entirely.

We were not able to confirm whether real cupsd jobs produce the correct dimensions because the raster filter runs inside CUPS and the log level could not be raised without root. The filter now logs the actual raster dimensions via INFO: messages for future diagnosis.

What we need: Confirmation of the SK20's maximum pixel width per raster row. The PPD currently assumes 227 points (80 mm) at 203 dpi = 640 pixels = 80 bytes per row. Is this correct?

4. Bonding does not persist after disconnect

After bluetoothctl pair, the device shows Paired: yes while connected but reverts to Paired: no, Bonded: no after disconnect. The link key is not being persisted by BlueZ. This means every new connection attempt requires a fresh pairing, and the pairing window may already have closed.

5. tty output processing corrupts binary ESC/POS data (fixed)

Opening /dev/rfcomm0 without disabling the kernel's tty line discipline left OPOST + ONLCR active. Every \x0a byte in ESC/POS raster pixel data was transformed to \x0d\x0a, corrupting the GS v 0 command. This is now fixedwrite_payload() and cups_backend_main() clear OPOST via termios and call tcdrain() before closing.

6. udev rules needed for rfcomm permissions (fixed)

The original udev rule only matched USB usblp devices. Bluetooth rfcomm devices were owned by root:root with mode 0600, causing PermissionError when the CUPS backend (running as lp) tried to open /dev/rfcomm0. Fixed by adding:

KERNEL=="rfcomm[0-9]*", MODE="0660", GROUP="lp"

Where the pipeline breaks

From transport setup to paper output, with the current failure points marked.

flowchart TD
    A["Pair / bind transport"]
    B["BLE GATT connect"]
    C["Feasycom AUTH + open engine"]
    D["RFCOMM tty in raw mode"]
    E["CUPS raster sizing"]
    F["ESC/POS bytes delivered"]
    G["Paper output"]

    A -->|"[!] bond"| B
    A --> D
    B -->|"[X] timeout"| C
    C --> F
    D -->|"[!] gated?"| F
    E -->|"[!] width?"| F
    F -->|"[X] no print"| G

    B --> Bx["Open: GATT connect timeout"]
    A --> Ax["Open: bonding not persistent"]
    D --> Dx["Fixed: rfcomm permissions + tcdrain + raw tty"]
    E --> Ex["Needs verification: page width may be wrong"]
    F --> Fx["Open: classic SPP data path may still be gated"]
    G --> Gx["Open: blue light, no paper output"]

    classDef rose fill:#FBF1F4,stroke:#C98EA2,color:#5B3241,stroke-width:1.2px;
    classDef blush fill:#F7E5EB,stroke:#C98EA2,color:#5B3241,stroke-width:1.2px;
    classDef petal fill:#F1D3DE,stroke:#B97A92,color:#4F2B38,stroke-width:1.2px;
    classDef fault fill:#EEC5D4,stroke:#A95F78,color:#4A2432,stroke-width:1.2px;

    class A,B,C,D,E,F,G rose;
    class Ax,Ex blush;
    class Dx petal;
    class Bx,Fx,Gx fault;

    linkStyle 0,4,5 stroke:#C98EA2,color:#A95F78,stroke-width:2.5px;
    linkStyle 2,6 stroke:#A95F78,color:#A95F78,stroke-width:3px;
Loading

Summary of errors encountered

Error Where Cause Status
permission denied opening /dev/rfcomm0 CUPS backend Missing udev rule for rfcomm Fixed
tty OPOST corrupts ESC/POS binary data rfcomm write path Line discipline not set to raw Fixed
rfcomm0: channel 1 closed after write rfcomm tty No tcdrain() before close Fixed
Blue light on, no print Printer Unknown — Feasycom module or ESC/POS format Open
BLE TimeoutError on connect bleak / BlueZ Unknown — GATT connection never completes Open
Bonded: no after disconnect BlueZ Link key not persisted Open
Could not determine output page dimensions cups-filters cfFilterPDFToRaster PPD page size not parsed outside cupsd Needs verification
org.bluez.Error.InProgress bluetoothctl scan Stale discovery session in BlueZ Workaround: sudo hciconfig hci0 reset
All RFCOMM channels 1-10 accept but are silent Raw BT socket Feasycom module gates SPP data Open

Request to OpenLabel / Jancsinn (致 OpenLabel / 建辰科技 的开发团队)

I use the SK20 with OpenClaw for personal, non-commercial art projects. The hardware is excellent, and the official iOS app works very well. I would love to use the printer on Linux too, so that it can become part of a wider creative workflow.

This README documents the Linux compatibility work in the hope that it may be useful as reference for future official support, documentation, or tooling. If OpenLabel chooses to support Linux more directly, I believe many developers, artists, and businesses would benefit.

This project is shared respectfully and with appreciation for your products. If any official guidance, public documentation, Linux compatibility notes, or future SDK information can be provided, it would be greatly appreciated.


我使用 SK20 和 OpenClaw 进行个人、非商业性质的艺术项目。硬件本身很出色,官方 iOS 应用也运行得很好。我也非常希望能够在 Linux 上使用这台打印机,让它更自然地融入我的创作工作流程。

我在这里记录 Linux 兼容性的探索过程,希望这些内容也许能为未来的官方支持、文档或工具提供一些参考。如果 OpenLabel 未来愿意更直接地支持 Linux,我相信许多开发者、艺术工作者和企业用户都会从中受益。

这项工作是以尊重和感谢的态度分享的。如果能够提供任何官方指导、公开文档、Linux 兼容性说明,或未来 SDK 的相关信息,我都会非常感激。

General feedback welcome

If you have used the SK20, SK25, or CM20 on Linux or another desktop operating system, or if you have general suggestions for improving compatibility, feel free to open an issue or PR.

Useful contributions include high-level guidance, documentation references, setup notes, or practical testing results from legitimate desktop use.

About

Linux compatibility shim and CUPS path for the OpenLabel SK20 thermal printer.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors