Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions packages/core/src/device/profiles/p12.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
import type { DeviceProfile } from "../types.js";
import type { DeviceProfile, LabelSizePreset } from "../types.js";

const gapSizes: LabelSizePreset[] = [
{ widthMm: 22, heightMm: 12 },
{ widthMm: 22, heightMm: 14 },
{ widthMm: 26, heightMm: 15 },
{ widthMm: 30, heightMm: 12 },
{ widthMm: 30, heightMm: 14 },
{ widthMm: 30, heightMm: 15 },
{ widthMm: 40, heightMm: 12 },
{ widthMm: 40, heightMm: 14 },
{ widthMm: 40, heightMm: 15 },
{ widthMm: 50, heightMm: 15 },
];

const continuousSizes: LabelSizePreset[] = [
{ widthMm: 22, heightMm: 12 },
{ widthMm: 22, heightMm: 14 },
{ widthMm: 26, heightMm: 15 },
{ widthMm: 30, heightMm: 12 },
{ widthMm: 30, heightMm: 14 },
{ widthMm: 30, heightMm: 15 },
{ widthMm: 40, heightMm: 12 },
{ widthMm: 40, heightMm: 15 },
{ widthMm: 50, heightMm: 15 },
];

export const p12Profile: DeviceProfile = {
modelId: "p12",
Expand All @@ -12,9 +37,14 @@ export const p12Profile: DeviceProfile = {
packetSize: 90,
flowControl: {
initialCredits: 4,
starvationTimeoutMs: 1000,
timerIntervalMs: 30,
},
defaults: { density: 2, paperType: "gap" },
namePrefixes: ["P12", "LP90", "P11"],
labelConfig: {
supportedPaperTypes: ["gap", "continuous"],
defaultPaperType: "gap",
gapSizes,
continuousSizes,
defaultSize: { widthMm: 40, heightMm: 15 },
},
};
24 changes: 21 additions & 3 deletions packages/core/src/device/profiles/p15.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import type { DeviceProfile } from "../types.js";
import type { DeviceProfile, LabelSizePreset } from "../types.js";

const gapSizes: LabelSizePreset[] = [
{ widthMm: 22, heightMm: 12 },
{ widthMm: 22, heightMm: 14 },
{ widthMm: 26, heightMm: 15 },
{ widthMm: 30, heightMm: 12 },
{ widthMm: 30, heightMm: 14 },
{ widthMm: 30, heightMm: 15 },
{ widthMm: 40, heightMm: 12 },
{ widthMm: 40, heightMm: 14 },
{ widthMm: 40, heightMm: 15 },
{ widthMm: 50, heightMm: 15 },
];

export const p15Profile: DeviceProfile = {
modelId: "p15",
Expand All @@ -12,8 +25,6 @@ export const p15Profile: DeviceProfile = {
packetSize: 95,
flowControl: {
initialCredits: 4,
starvationTimeoutMs: 1000,
timerIntervalMs: 30,
},
defaults: { density: 2, paperType: "gap" },
namePrefixes: [
Expand All @@ -30,4 +41,11 @@ export const p15Profile: DeviceProfile = {
"P1s",
"LPC74",
],
labelConfig: {
supportedPaperTypes: ["gap"],
defaultPaperType: "gap",
gapSizes,
continuousSizes: [],
defaultSize: { widthMm: 40, heightMm: 12 },
},
};
15 changes: 15 additions & 0 deletions packages/core/src/device/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ export interface FlowControlOptions {
initialCredits: number;
starvationTimeoutMs: number;
timerIntervalMs: number;
packetDelayMs: number;
}

export interface LabelSizePreset {
widthMm: number;
heightMm: number;
}

export interface DeviceLabelConfig {
supportedPaperTypes: ("gap" | "continuous")[];
defaultPaperType: "gap" | "continuous";
gapSizes: LabelSizePreset[];
continuousSizes: LabelSizePreset[];
defaultSize: LabelSizePreset;
}

export interface DeviceProfile {
Expand All @@ -13,6 +27,7 @@ export interface DeviceProfile {
flowControl: Partial<FlowControlOptions>;
defaults: { density: number; paperType: "gap" | "continuous" };
namePrefixes: string[];
labelConfig?: DeviceLabelConfig;
}

export interface PrintOptions {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export { L11Protocol } from "./protocol/l11/protocol.js";
// Device types & registry (for adding new printer models)
export type {
DeviceProfile,
DeviceLabelConfig,
LabelSizePreset,
PrintOptions,
PrinterStatus,
PrinterEventMap,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ export class Printer {

if (response.type === "credit") {
this.flowController.grantCredits(response.value as number);
} else if (response.type === "mtu") {
const mtu = response.value as number;
if (mtu > 3) {
this.flowController.setPacketSize(mtu - 3);
}
}
}

Expand Down
27 changes: 20 additions & 7 deletions packages/core/src/transport/flow-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,42 @@ import { ThermoprintError, ErrorCode } from "../errors.js";

const DEFAULT_OPTIONS: FlowControlOptions = {
initialCredits: 4,
starvationTimeoutMs: 1000,
timerIntervalMs: 30,
starvationTimeoutMs: 30,
timerIntervalMs: 5,
packetDelayMs: 0,
};

export class FlowController {
private credits: number;
private readonly options: FlowControlOptions;
private lastCreditTime: number = Date.now();
private packetSize: number;

constructor(
private readonly tx: BleCharacteristic,
private readonly packetSize: number,
packetSize: number,
options?: Partial<FlowControlOptions>,
) {
this.packetSize = packetSize;
this.options = { ...DEFAULT_OPTIONS, ...options };
this.credits = this.options.initialCredits;
}

setPacketSize(size: number): void {
this.packetSize = size;
}

grantCredits(count: number): void {
this.credits += count;
this.lastCreditTime = Date.now();
}

/**
* Send data through the BLE characteristic with credit-based flow control.
* Chunks data into packets and waits for credits before each send.
*
* Uses a timer-style approach: each packet waits for a credit (or a short
* starvation recovery) before sending, ensuring steady throughput even when
* BLE notifications are delayed or dropped.
*/
async send(
data: Uint8Array,
Expand Down Expand Up @@ -66,16 +76,19 @@ export class FlowController {
return;
}

// Starvation recovery: force 1 credit after timeout
// Starvation recovery: force 1 credit after timeout.
// With a short timeout this acts as a natural pacer, producing
// steady ~1-packet-per-timeout throughput when the printer
// doesn't actively grant credits (common over Web Bluetooth).
if (Date.now() - this.lastCreditTime >= starvationTimeoutMs) {
this.credits = 1;
this.lastCreditTime = Date.now();
resolve();
return;
}

// Hard timeout: 10x starvation timeout
if (Date.now() - startTime >= starvationTimeoutMs * 10) {
// Hard timeout: give up after 5 seconds
if (Date.now() - startTime >= 5000) {
reject(
new ThermoprintError(
ErrorCode.FLOW_CONTROL_TIMEOUT,
Expand Down
5 changes: 0 additions & 5 deletions packages/web/src/editor/toolbar/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
import { useEditorStore } from "../../store/editor-store.ts";
import { useHistory } from "../../hooks/use-history.ts";
import type { EditorElement } from "../../store/types.ts";
import { LabelSizeSelector } from "../../label/label-size-selector.tsx";
import { ThemeToggle } from "../../theme/theme-toggle.tsx";
import { Tooltip } from "../../components/tooltip.tsx";

Expand Down Expand Up @@ -185,10 +184,6 @@ export function Toolbar() {
</Tooltip>
</div>

<div className="w-px h-7 bg-gray-200 dark:bg-gray-700 mx-1" />

<LabelSizeSelector />

<div className="flex-1" />
<ThemeToggle />
</div>
Expand Down
11 changes: 9 additions & 2 deletions packages/web/src/hooks/use-web-bluetooth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback } from "react";
import { Printer, type BlePeripheral } from "@thermoprint/core";
import { Printer, findDeviceByName, type BlePeripheral } from "@thermoprint/core";
import { WebBluetoothTransport } from "../transport/web-bluetooth.ts";
import { usePrinterStore } from "../store/printer-store.ts";
import { usePrinterStore, applyModelDefaults } from "../store/printer-store.ts";

// Module-level singletons so all callers share the same transport + printer instance
const transport = new WebBluetoothTransport();
Expand Down Expand Up @@ -46,6 +46,13 @@ export function useWebBluetooth() {
store.getState().setConnected(true);
store.getState().setConnecting(false);

const profile = findDeviceByName(peripheral.name);
if (profile) {
store.getState().setModelId(profile.modelId);
store.setState({ autoDetectedModelId: profile.modelId });
applyModelDefaults(profile.modelId);
}

try {
const battery = await printer.getBattery();
store.getState().setBattery(battery);
Expand Down
47 changes: 0 additions & 47 deletions packages/web/src/label/label-size-selector.tsx

This file was deleted.

22 changes: 21 additions & 1 deletion packages/web/src/label/label-sizes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { getDevice } from "@thermoprint/core";

export interface LabelSize {
name: string;
widthMm: number;
heightMm: number;
}

export const LABEL_SIZES: LabelSize[] = [
const FALLBACK_SIZES: LabelSize[] = [
{ name: "40 × 12 mm", widthMm: 40, heightMm: 12 },
{ name: "40 × 20 mm", widthMm: 40, heightMm: 20 },
{ name: "40 × 30 mm", widthMm: 40, heightMm: 30 },
Expand All @@ -15,3 +17,21 @@ export const LABEL_SIZES: LabelSize[] = [
{ name: "50 × 30 mm", widthMm: 50, heightMm: 30 },
{ name: "50 × 50 mm", widthMm: 50, heightMm: 50 },
];

export function getLabelSizes(
modelId: string | null,
paperType: "gap" | "continuous",
): LabelSize[] {
if (!modelId) return FALLBACK_SIZES;
const profile = getDevice(modelId);
const presets =
paperType === "gap"
? profile?.labelConfig?.gapSizes
: profile?.labelConfig?.continuousSizes;
if (!presets?.length) return FALLBACK_SIZES;
return presets.map((p) => ({
name: `${p.widthMm} × ${p.heightMm} mm`,
widthMm: p.widthMm,
heightMm: p.heightMm,
}));
}
Loading
Loading