diff --git a/packages/core/src/device/profiles/p12.ts b/packages/core/src/device/profiles/p12.ts index eca426c..e865543 100644 --- a/packages/core/src/device/profiles/p12.ts +++ b/packages/core/src/device/profiles/p12.ts @@ -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", @@ -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 }, + }, }; diff --git a/packages/core/src/device/profiles/p15.ts b/packages/core/src/device/profiles/p15.ts index ce56956..ea62c37 100644 --- a/packages/core/src/device/profiles/p15.ts +++ b/packages/core/src/device/profiles/p15.ts @@ -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", @@ -12,8 +25,6 @@ export const p15Profile: DeviceProfile = { packetSize: 95, flowControl: { initialCredits: 4, - starvationTimeoutMs: 1000, - timerIntervalMs: 30, }, defaults: { density: 2, paperType: "gap" }, namePrefixes: [ @@ -30,4 +41,11 @@ export const p15Profile: DeviceProfile = { "P1s", "LPC74", ], + labelConfig: { + supportedPaperTypes: ["gap"], + defaultPaperType: "gap", + gapSizes, + continuousSizes: [], + defaultSize: { widthMm: 40, heightMm: 12 }, + }, }; diff --git a/packages/core/src/device/types.ts b/packages/core/src/device/types.ts index cfdf11b..f1c3e4f 100644 --- a/packages/core/src/device/types.ts +++ b/packages/core/src/device/types.ts @@ -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 { @@ -13,6 +27,7 @@ export interface DeviceProfile { flowControl: Partial; defaults: { density: number; paperType: "gap" | "continuous" }; namePrefixes: string[]; + labelConfig?: DeviceLabelConfig; } export interface PrintOptions { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b663cd6..af86253 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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, diff --git a/packages/core/src/printer.ts b/packages/core/src/printer.ts index 6214733..d43a928 100644 --- a/packages/core/src/printer.ts +++ b/packages/core/src/printer.ts @@ -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); + } } } diff --git a/packages/core/src/transport/flow-control.ts b/packages/core/src/transport/flow-control.ts index 47cea02..0b7651b 100644 --- a/packages/core/src/transport/flow-control.ts +++ b/packages/core/src/transport/flow-control.ts @@ -4,24 +4,31 @@ 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, ) { + 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(); @@ -29,7 +36,10 @@ export class FlowController { /** * 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, @@ -66,7 +76,10 @@ 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(); @@ -74,8 +87,8 @@ export class FlowController { 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, diff --git a/packages/web/src/editor/toolbar/toolbar.tsx b/packages/web/src/editor/toolbar/toolbar.tsx index cafbc28..a9b3c4a 100644 --- a/packages/web/src/editor/toolbar/toolbar.tsx +++ b/packages/web/src/editor/toolbar/toolbar.tsx @@ -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"; @@ -185,10 +184,6 @@ export function Toolbar() { -
- - -
diff --git a/packages/web/src/hooks/use-web-bluetooth.ts b/packages/web/src/hooks/use-web-bluetooth.ts index 568ed42..02f6276 100644 --- a/packages/web/src/hooks/use-web-bluetooth.ts +++ b/packages/web/src/hooks/use-web-bluetooth.ts @@ -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(); @@ -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); diff --git a/packages/web/src/label/label-size-selector.tsx b/packages/web/src/label/label-size-selector.tsx deleted file mode 100644 index 5351342..0000000 --- a/packages/web/src/label/label-size-selector.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useState } from "react"; -import { useEditorStore } from "../store/editor-store.ts"; -import { LABEL_SIZES } from "./label-sizes.ts"; - -export function LabelSizeSelector() { - const labelConfig = useEditorStore((s) => s.labelConfig); - const setLabelConfig = useEditorStore((s) => s.setLabelConfig); - const [custom, setCustom] = useState(false); - - const currentPreset = LABEL_SIZES.find( - (s) => s.widthMm === labelConfig.widthMm && s.heightMm === labelConfig.heightMm, - ); - - if (custom) { - return ( -
- Label: - setLabelConfig(Number(e.target.value), labelConfig.heightMm)} - className="w-14 px-1.5 py-0.5 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700" /> - x - setLabelConfig(labelConfig.widthMm, Number(e.target.value))} - className="w-14 px-1.5 py-0.5 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700" /> - mm - -
- ); - } - - return ( -
- Label: - -
- ); -} diff --git a/packages/web/src/label/label-sizes.ts b/packages/web/src/label/label-sizes.ts index b2f7019..166cc3d 100644 --- a/packages/web/src/label/label-sizes.ts +++ b/packages/web/src/label/label-sizes.ts @@ -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 }, @@ -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, + })); +} diff --git a/packages/web/src/printer/print-settings-panel.tsx b/packages/web/src/printer/print-settings-panel.tsx index b081b85..14abc7f 100644 --- a/packages/web/src/printer/print-settings-panel.tsx +++ b/packages/web/src/printer/print-settings-panel.tsx @@ -1,4 +1,8 @@ -import { usePrinterStore } from "../store/printer-store.ts"; +import { useState } from "react"; +import { usePrinterStore, applyModelDefaults } from "../store/printer-store.ts"; +import { useEditorStore } from "../store/editor-store.ts"; +import { getDevice, getRegisteredDevices } from "@thermoprint/core"; +import { getLabelSizes } from "../label/label-sizes.ts"; import { Tooltip } from "../components/tooltip.tsx"; import { Info } from "lucide-react"; import type { DitherMode } from "@thermoprint/core"; @@ -11,48 +15,179 @@ function InfoTip({ text }: { text: string }) { ); } +const selectClass = + "w-full mt-0.5 px-2 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700"; + export function PrintSettingsPanel() { const settings = usePrinterStore((s) => s.settings); const updateSettings = usePrinterStore((s) => s.updateSettings); + const modelId = usePrinterStore((s) => s.modelId); + const autoDetectedModelId = usePrinterStore((s) => s.autoDetectedModelId); + const setModelId = usePrinterStore((s) => s.setModelId); + + const labelConfig = useEditorStore((s) => s.labelConfig); + const setLabelConfig = useEditorStore((s) => s.setLabelConfig); + + const [customSize, setCustomSize] = useState(false); + + const devices = getRegisteredDevices(); + const profile = modelId ? getDevice(modelId) : null; + const lc = profile?.labelConfig; + + const supportedPaperTypes = lc?.supportedPaperTypes ?? ["gap", "continuous"]; + const sizes = getLabelSizes(modelId, settings.paperType); + + const currentPreset = sizes.find( + (s) => s.widthMm === labelConfig.widthMm && s.heightMm === labelConfig.heightMm, + ); + + const handleModelChange = (newModelId: string) => { + setModelId(newModelId); + applyModelDefaults(newModelId); + setCustomSize(false); + }; + + const handlePaperTypeChange = (paperType: "gap" | "continuous") => { + updateSettings({ paperType }); + // Reset label size if current size isn't available for the new paper type + const newSizes = getLabelSizes(modelId, paperType); + const stillValid = newSizes.some( + (s) => s.widthMm === labelConfig.widthMm && s.heightMm === labelConfig.heightMm, + ); + if (!stillValid && newSizes.length > 0) { + setLabelConfig(newSizes[0].widthMm, newSizes[0].heightMm); + } + }; return (

Print Settings

+ + {/* Printer Model */}
- handleModelChange(e.target.value)} + className={selectClass} + > + {devices.map((d) => ( + + ))}
+ + {/* Paper Type */}
- handlePaperTypeChange(e.target.value as "gap" | "continuous")} + className={selectClass} + > + {supportedPaperTypes.includes("gap") && ( + + )} + {supportedPaperTypes.includes("continuous") && ( + + )} + +
+ + {/* Label Size */} +
+ + {customSize ? ( +
+ setLabelConfig(Number(e.target.value), labelConfig.heightMm)} + className="w-16 px-1.5 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700" + /> + × + setLabelConfig(labelConfig.widthMm, Number(e.target.value))} + className="w-16 px-1.5 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700" + /> + mm + +
+ ) : ( + + )} +
+ + {/* Density */} +
+ +
+ + {/* Dither Mode */}
+ + {/* Threshold */}