From e4bbcd87e001a5704ee4614524a5772561a959fb Mon Sep 17 00:00:00 2001 From: Thomas Leiter Date: Tue, 7 Apr 2026 23:25:49 +0200 Subject: [PATCH 1/2] Fix P15 clone crash by capping packet size and fixing flow control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pristar P15 (and likely other clones) powers off when printing because: 1. MTU notification (220) overrides the profile's packet size from 95 to 217, overwhelming the printer's application buffer. The official Marklife app forces device-specific sizes on every tick, ignoring MTU for P15 devices. 2. FlowController.reset() cleared hasRealCredits before each print job, causing starvation recovery to bypass credit-based flow control for the first few packets — even when the printer actively sends credits via CX. 3. Starvation recovery timeout was 30ms vs the official app's 1000ms, blasting packets far too aggressively when credits aren't flowing. Changes: - Cap MTU-derived packet size at profile's explicit packetSize - Preserve hasRealCredits across reset (connection property, not per-job) - Increase starvation timeout from 30ms to 1000ms to match official app --- packages/core/src/printer.ts | 10 ++++++++-- packages/core/src/transport/flow-control.ts | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/core/src/printer.ts b/packages/core/src/printer.ts index 6e8e93e..5080663 100644 --- a/packages/core/src/printer.ts +++ b/packages/core/src/printer.ts @@ -244,8 +244,14 @@ export class Printer { } else if (response.type === "mtu") { const mtu = response.value as number; if (mtu > 3) { - debugLog("BLE", `mtu=${mtu} packet=${mtu - 3}`); - this.flowController.setPacketSize(mtu - 3); + const mtuPacketSize = mtu - 3; + // Cap at profile-specified size — the profile knows what the device + // firmware can handle; a higher BLE MTU doesn't mean the printer's + // application-level buffer can keep up with larger writes. + const cap = this.profile.packetSize; + const effective = cap ? Math.min(mtuPacketSize, cap) : mtuPacketSize; + debugLog("BLE", `mtu=${mtu} packet=${effective}${cap && mtuPacketSize > cap ? ` (capped from ${mtuPacketSize})` : ""}`); + this.flowController.setPacketSize(effective); } } diff --git a/packages/core/src/transport/flow-control.ts b/packages/core/src/transport/flow-control.ts index 4b5ffe0..7040cbf 100644 --- a/packages/core/src/transport/flow-control.ts +++ b/packages/core/src/transport/flow-control.ts @@ -5,7 +5,7 @@ import { debugLog } from "../debug-log.js"; const DEFAULT_OPTIONS: FlowControlOptions = { initialCredits: 4, - starvationTimeoutMs: 30, + starvationTimeoutMs: 1000, timerIntervalMs: 5, packetDelayMs: 0, }; @@ -35,7 +35,10 @@ export class FlowController { /** Reset credits to initial state before a new print job. */ reset(): void { this.credits = this.options.initialCredits; - this.hasRealCredits = false; + // Preserve hasRealCredits — it reflects whether this device supports + // credit-based flow control, which is a connection-level property. + // Clearing it would let starvation recovery bypass flow control at the + // start of every print job. this.lastCreditTime = Date.now(); debugLog("FC", `reset, credits=${this.credits}`); } From 276d730bb50cc833b44fb32dae645563d118be20 Mon Sep 17 00:00:00 2001 From: Thomas Leiter Date: Tue, 7 Apr 2026 23:28:33 +0200 Subject: [PATCH 2/2] Add 30ms inter-packet delay matching official Marklife app The decompiled app uses a configurable dataPacketDelay (30ms for P15 and most devices) between BLE writes. Our packetDelayMs option existed in the type but was never wired into the send loop. --- packages/core/src/device/profiles/p12.ts | 1 + packages/core/src/device/profiles/p15.ts | 1 + packages/core/src/transport/flow-control.ts | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/packages/core/src/device/profiles/p12.ts b/packages/core/src/device/profiles/p12.ts index e865543..b6a626c 100644 --- a/packages/core/src/device/profiles/p12.ts +++ b/packages/core/src/device/profiles/p12.ts @@ -37,6 +37,7 @@ export const p12Profile: DeviceProfile = { packetSize: 90, flowControl: { initialCredits: 4, + packetDelayMs: 30, }, defaults: { density: 2, paperType: "gap" }, namePrefixes: ["P12", "LP90", "P11"], diff --git a/packages/core/src/device/profiles/p15.ts b/packages/core/src/device/profiles/p15.ts index ea62c37..b93fa28 100644 --- a/packages/core/src/device/profiles/p15.ts +++ b/packages/core/src/device/profiles/p15.ts @@ -25,6 +25,7 @@ export const p15Profile: DeviceProfile = { packetSize: 95, flowControl: { initialCredits: 4, + packetDelayMs: 30, }, defaults: { density: 2, paperType: "gap" }, namePrefixes: [ diff --git a/packages/core/src/transport/flow-control.ts b/packages/core/src/transport/flow-control.ts index 7040cbf..8698bea 100644 --- a/packages/core/src/transport/flow-control.ts +++ b/packages/core/src/transport/flow-control.ts @@ -64,9 +64,15 @@ export class FlowController { let offset = 0; debugLog("TX", `sending ${data.length}B in ${Math.ceil(data.length / this.packetSize)} packets, credits=${this.credits}`); + const { packetDelayMs } = this.options; + while (offset < data.length) { await this.waitForCredit(); + if (packetDelayMs > 0 && offset > 0) { + await new Promise((r) => setTimeout(r, packetDelayMs)); + } + const remaining = data.length - offset; const chunkSize = Math.min(remaining, this.packetSize); const chunk = data.subarray(offset, offset + chunkSize);