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)
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.
What works:
- Bluetooth device discovery and pairing
- RFCOMM channel 1 connection established
- Raw bytes successfully written to printer over Bluetooth
- Python script with
--bluetooth-addresssupport 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.
- scripts/openlabel_sk20.py
- cups/OpenLabel-SK20.ppd
- scripts/install_linux_cups.sh
- scripts/99-openlabel-sk20.rules
- docs/openlabel-sk20-linux.md
- docs/openlabel-android-app-findings.md
- tests/test_openlabel_sk20.py
Probe for the printer:
python3 scripts/openlabel_sk20.py probeShow the reverse-engineered transport identifiers:
python3 scripts/openlabel_sk20.py transport-infoScan nearby Bluetooth devices and flag likely OpenLabel matches:
python3 scripts/openlabel_sk20.py scan-bluetooth
python3 scripts/openlabel_sk20.py scan-bluetooth --allInstall the Python BLE client before using the BLE provisioning commands:
python3 -m pip install bleakBroadcast the recovered Android UDP LAN-discovery probe:
python3 scripts/openlabel_sk20.py scan-wifiQuery 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:FFSend a raw Feasycom AT command over BLE:
python3 scripts/openlabel_sk20.py ble-send-at AA:BB:CC:DD:EE:FF AT+VERProvision 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.1From 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;
Print a test page:
python3 scripts/openlabel_sk20.py test-pagePrint a test page directly over classic Bluetooth RFCOMM:
python3 scripts/openlabel_sk20.py test-page --bluetooth-address AA:BB:CC:DD:EE:FFTip: find the printer address first with:
python3 scripts/openlabel_sk20.py scan-bluetoothPrint 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 20Send a raw ESC/POS payload:
python3 scripts/openlabel_sk20.py send-raw ./job.binRender a CUPS raster fixture into ESC/POS bytes:
python3 scripts/openlabel_sk20.py render-raster ./job.raster --output ./job.binThe repo now contains a complete Linux CUPS path for USB printing:
scripts/openlabel_sk20.pydoubles 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.ppdprovides a queue definition and print optionsscripts/install_linux_cups.shinstalls 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/servicesFrom 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;
This is now a Linux USB driver path with CUPS integration.
What is here:
- USB detection via Linux
sysfs usblpdevice-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 -> 7200broadcast 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
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.
| 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.) |
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 EOTstatus requests (\x10\x04\x01). - No response to Feasycom
AUTHpackets 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)?
The BLE device (SK20_BLE_6CBC) advertises and is discoverable, but:
BleakClient.connect()times out after 20 seconds every attempt.bluetoothctl connect <BLE_MAC>returnsorg.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?
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?
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.
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 fixed — write_payload() and cups_backend_main() clear OPOST via termios and call tcdrain() before closing.
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"
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;
| 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 |
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 的相关信息,我都会非常感激。
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.
