| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Both changes are backward compatible (identical wire bytes for every
existing valid input) and verified live on a real Peripage A40.
printRowBytesList(): row_bytes/height are now both proper 2-byte
little-endian fields instead of 1-byte-plus-a-hardcoded-zero-padding-
byte. For every currently supported model (row_bytes always <= 255)
this produces byte-for-byte identical requests to before -- the old
padding byte was already implicitly the high byte of a 16-bit value,
just never treated as one. The only real behavior change is for
height > 0xff: instead of silently slicing into multiple 0xff-row
chunks (each getting its own reset()), a full image now goes out as
one continuous request, matching this method's own docstring, which
already said "this printer supports pages up to 0xffff rows" -- the
255-row limit was an artifact of this method's encoding, not a
firmware limit. Confirmed by decompiling the official Android app
(its equivalent height field is a full 16 bits) and by printing a
real 300-row striped test image through this patched printImage() in
one continuous request with no corruption at the old chunk boundary.
setConcentration(): removed the hardcoded clamp to {0, 1, 2}. Values
0/1/2 still produce the exact same request bytes as before. The
official app's decompiled code sends this same opcode with no range
check, and a real A40 accepts 3/4 without error (no visible density
difference observed on that specific unit above 2, so treat values
past 2 as hardware-dependent, not a guaranteed improvement -- just no
longer artificially blocked).
Both findings, plus a lot more protocol detail that didn't make it
into this small PR (a newer zlib-compressed 0x1f protocol variant,
async status packets the printer can push mid-print, paper-type
selection, etc.), came out of reverse engineering the official
com.ileadtek.peripage Android app for a downstream project. Full
writeup, including a byte-exact opcode table cross-referenced against
a live Bluetooth HCI capture:
https://github.com/RainManVays/perripage-ferrum/blob/main/docs/bluetooth-protocol-trace-analysis.md
Peripage A40 Bluetooth protocol: analysis of a real HCI trace from the official appDate: 2026-07-14/15 Printer: a real Peripage A40 (PPG_A40_34C4, firmware V1.4.5_SD). How to read this: every claim is tagged with a confidence level — confirmed (seen Later (2026-07-15) the app itself (com.ileadtek.peripage, versions 312 and 319, the latter 1. Capture overview
2. Per-print-action command sequenceReconstructed from all 3 print actions — byte-identical across all three except where noted as
The library's own image opcode (1d763000) never appears anywhere in the trace — the official 3. Image data transfer (opcode 0x1f)Update 2026-07-15: this section originally came from the live trace alone and had three 3.1. Preamble — now fully decodedFirst bytes of the first frame of each burst: Session 0 (t=34.538): 1f 00 00 d0 01 16 00 00 10 54 ed 92 4f 6c e4 46 ... Session 1 (t=740.561): 1f 00 00 d0 08 61 00 00 19 b4 ed d2 7d 8c 5c d7 ... Session 2 (t=774.124): 1f 00 00 d0 01 16 00 00 10 54 ed 92 4f 6c e4 46 ... (Sessions 0 and 2 print the exact same test image twice, hence identical bytes in both the Preamble format, byte-exact per the decompiled code (all multi-byte fields are big-endian —
3.2. Payload compression — it's plain zlib deflateThe main new finding (2026-07-15). Method w(Bitmap, int) builds the raster with the same Code.code() is a thin JNI wrapper around zlib's standard compress2() (or equivalent). The Practical takeaway for implementing this (details and code in raw = pack_1bpp_msb_first(bitmap) # same packing already used for the legacy protocol
compressed = zlib.compress(raw, level) # compression level doesn't affect decodability
payload = compressed[2:] # strip the 2-byte zlib header, as the app does⚠️ Real-hardware update, added after posting this analysis: we implemented this and tested 3.3. Timing — the headline finding
Confirmed: the official app inserts no manual delay whatsoever between That's 15-30x faster than the delay=0.05 a downstream project (PeriPrint) had been using Caveat: the 200-byte frame size is very likely just the negotiated RFCOMM/L2CAP packet size 3.4. End of burst — another findingThe last frame of each of the 3 bursts ends identically: Session 0: ...981b4a1010fffe45 Session 1: ...9a1b4a1010fffe45 Session 2: ...981b4a1010fffe45 Breaking down the trailing bytes: 1b 4a 10 + 10 ff fe 45.
This confirms the image data and trailing commands are one continuous Python/Java-level 4. Unsolicited (unrequested) messages from the printerDuring an idle gap between print session 1 and session 3 (~t=750-753s, no preceding app command t=750.67 ← fd01 t=751.29 ← aa t=753.28 ← fd02 Decoded by code (2026-07-15) — the single most important finding of this update. These are
In other words: fd 01 = the printer itself is asking to abort the print, fd 02 = the (Update: a downstream project implemented a listener for this and confirmed it live — printing 5. Opcode summary: known vs. found in the traceUpdated 2026-07-15 from the decompiled code — see §7.2 for the full opcode-by-opcode table of
6. Practical implications
7. Cross-reference with the official app's decompiled code (2026-07-15)7.1. MethodTwo instances of the official com.ileadtek.peripage Android app were analyzed:
Both were decompiled with jadx 1.5.6 (Java/Kotlin from classes*.dex); native libraries Important methodological note: between builds 312 and 319, obfuscation (ProGuard/R8) 7.2. Full command table from the protocol controller classThe controller class (u0.h/v0.h depending on build) is the single point all low-level
Decompilation note: jadx substituted numeric byte literals with static constant names from 7.3. Printer status responses — full semanticsThe status-message handler (interface implemented in the print-orchestration class) gives exact
7.4. Differences between the legacy (0x1d7630) and new (0x1f) image protocolsNot just a different opcode — the header formats also differ structurally in ways that are
This writeup, and a companion implementation plan with concrete code for each of these |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
What
Two small, backward-compatible changes to Printer, both verified live on a real Peripage A40:
printRowBytesList(): honest 16-bit row height. row_bytes/height are now proper
2-byte little-endian fields instead of 1-byte-plus-a-hardcoded-zero-padding-byte. For every
currently supported model (row_bytes is always ≤ 255) this produces byte-for-byte
identical requests to before — the old padding byte was already implicitly the high byte of
a 16-bit value, just never treated as one. The only real behavior change is for height > 0xff: instead of silently slicing into multiple 0xff-row chunks (each getting its own
reset()), a tall image now goes out as a single continuous request — matching this method's
own docstring, which already said "this printer supports pages up to 0xffff rows". The
255-row limit was an artifact of this method's own encoding choice, not a firmware limit.
setConcentration(): removed the hardcoded clamp to {0, 1, 2}. Values 0/1/2 still
produce the exact same request bytes as before. Values above 2 are no longer silently
clamped down to 2 — they're sent as-is (up to a byte, 0-255).
Why
I've been building an open-source Bluetooth printing app
(perripage-ferrum) on top of this library and
ran into real data loss printing anything taller than 255 rows in one job. Chasing it down, I
ended up decompiling the official com.ileadtek.peripage Android app (with a security check
first — no backdoor, wanted to make sure before trusting the APK) and cross-referencing it
against a live Bluetooth HCI capture of that app talking to the same printer. Full writeup is in
a comment below, but the two relevant findings:
(the legacy one closest to this library's 1d7630, and a newer one). The 255-row limit here
really is just this method's own 1-byte encoding, not something the firmware enforces.
real A40 accepts values 3/4 without erroring (no visible density improvement over 2 on
my specific unit, for what it's worth — so treat anything past 2 as
hardware-dependent/experimental, not a guaranteed win. Just no longer artificially blocked).
Testing
No test suite in this repo to hook into, so this was verified by hand against a real Peripage
A40 (PPG_A40_34C4, firmware V1.4.5_SD) via Printer.printImage() directly (i.e. through the
unmodified public API, not some bypass):
at the old 255-row boundary.
case.
Happy to share the exact test scripts if useful, or adjust anything about the approach — this is
my first time looking at this codebase in depth, so let me know if I've missed context on why
the original 1-byte/clamp-to-2 behavior was chosen.