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
22 changes: 22 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"name": "thermoprint",
"private": true,
"workspaces": ["packages/*"]
"workspaces": [
"packages/*"
],
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@
"devDependencies": {
"typescript": "^5.7.0",
"@types/bun": "latest"
},
"dependencies": {
"fflate": "^0.8.2"
}
}
42 changes: 42 additions & 0 deletions packages/core/src/device/profiles/m60.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { DeviceProfile, LabelSizePreset } from "../types.js";

const gapSizes: LabelSizePreset[] = [
{ widthMm: 20, heightMm: 10 },
{ widthMm: 30, heightMm: 15 },
{ widthMm: 40, heightMm: 12 },
{ widthMm: 40, heightMm: 20 },
{ widthMm: 40, heightMm: 30 },
{ widthMm: 50, heightMm: 30 },
{ widthMm: 50, heightMm: 40 },
];

const continuousSizes: LabelSizePreset[] = [
{ widthMm: 30, heightMm: 15 },
{ widthMm: 40, heightMm: 20 },
{ widthMm: 40, heightMm: 30 },
{ widthMm: 50, heightMm: 30 },
{ widthMm: 50, heightMm: 40 },
];

export const m60Profile: DeviceProfile = {
modelId: "m60",
protocolId: "x2",
serviceUuid: "0000ff00-0000-1000-8000-00805f9b34fb",
characteristics: {
tx: "0000ff02-0000-1000-8000-00805f9b34fb",
rx: "0000ff01-0000-1000-8000-00805f9b34fb",
cx: "0000ff03-0000-1000-8000-00805f9b34fb",
},
flowControl: {
packetDelayMs: 1,
},
defaults: { density: 2, paperType: "gap" },
namePrefixes: ["M60", "X2"],
labelConfig: {
supportedPaperTypes: ["gap", "continuous"],
defaultPaperType: "gap",
gapSizes,
continuousSizes,
defaultSize: { widthMm: 50, heightMm: 30 },
},
};
2 changes: 2 additions & 0 deletions packages/core/src/device/registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DeviceProfile } from "./types.js";
import { p15Profile } from "./profiles/p15.js";
import { p12Profile } from "./profiles/p12.js";
import { m60Profile } from "./profiles/m60.js";

const devices: DeviceProfile[] = [];

Expand Down Expand Up @@ -30,3 +31,4 @@ export function getRegisteredDevices(): DeviceProfile[] {
// Register built-in devices
registerDevice(p15Profile);
registerDevice(p12Profile);
registerDevice(m60Profile);
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type {
} from "./protocol/types.js";
export { registerProtocol, getProtocol } from "./protocol/registry.js";
export { L11Protocol } from "./protocol/l11/protocol.js";
export { X2Protocol } from "./protocol/x2/protocol.js";

// Device types & registry (for adding new printer models)
export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/protocol/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { PrinterProtocol } from "./types.js";
import { L11Protocol } from "./l11/protocol.js";
import { X2Protocol } from "./x2/protocol.js";
import { ThermoprintError, ErrorCode } from "../errors.js";

type ProtocolFactory = () => PrinterProtocol;
Expand Down Expand Up @@ -27,3 +28,4 @@ export function getRegisteredProtocolIds(): string[] {

// Register built-in protocols
registerProtocol("l11", () => new L11Protocol());
registerProtocol("x2", () => new X2Protocol());
159 changes: 159 additions & 0 deletions packages/core/src/protocol/x2/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { zlibSync } from "fflate";
import type { ImageBitmap1bpp, PrintCommand } from "../types.js";

/** 6 zero bytes to wake the printer */
export function wakeup(): PrintCommand {
return { label: "wakeup", data: new Uint8Array(6) };
}

/** Start print job: 1F C0 01 00 */
export function enable(): PrintCommand {
return { label: "enable", data: Uint8Array.from([0x1f, 0xc0, 0x01, 0x00]) };
}

/** End print job: 1F C0 01 01 */
export function stop(): PrintCommand {
return { label: "stop", data: Uint8Array.from([0x1f, 0xc0, 0x01, 0x01]) };
}

/**
* Set density: 1F 70 02 <value>
* Density mapping: 1→3, 2→8, 3→14
*/
export function setDensity(density: number): PrintCommand {
const DENSITY_MAP: Record<number, number> = { 1: 3, 2: 8, 3: 14 };
const value = DENSITY_MAP[density] ?? 8;
return {
label: "set-density",
data: Uint8Array.from([0x1f, 0x70, 0x02, value & 0xff]),
};
}

/** Set paper type to gap: 1F 80 02 20 */
export function setPaperTypeGap(): PrintCommand {
return {
label: "set-paper-type",
data: Uint8Array.from([0x1f, 0x80, 0x02, 0x20]),
};
}

/** Feed n dots: 1B 4A <lo> <hi> 00 */
export function feedDots(dots: number): PrintCommand {
return {
label: "feed-dots",
data: Uint8Array.from([0x1b, 0x4a, dots & 0xff, (dots >> 8) & 0xff, 0x00]),
};
}

/** Adjust position auto: 1F 11 <param> */
export function adjustPositionAuto(param: number): PrintCommand {
return {
label: "adjust-position",
data: Uint8Array.from([0x1f, 0x11, param & 0xff]),
};
}

/** Printer location: 1F 12 <x> <y> */
export function printerLocation(x: number, y: number): PrintCommand {
return {
label: "printer-location",
data: Uint8Array.from([0x1f, 0x12, x & 0xff, y & 0xff]),
};
}

/**
* Build a compressed bitmap command: 1F 10 <wh> <wl> <hh> <hl> <len4> + zlib data
* Compression: standard zlib compress() with default parameters (level 6).
*/
export function printBitmap(image: ImageBitmap1bpp): PrintCommand {
const { data: pixels, bytesPerRow, height } = image;
const compressed = zlibSync(pixels, { level: 6 });

const header = Uint8Array.from([
0x1f,
0x10,
(bytesPerRow >> 8) & 0xff,
bytesPerRow & 0xff,
(height >> 8) & 0xff,
height & 0xff,
(compressed.length >> 24) & 0xff,
(compressed.length >> 16) & 0xff,
(compressed.length >> 8) & 0xff,
compressed.length & 0xff,
]);

const command = new Uint8Array(header.length + compressed.length);
command.set(header, 0);
command.set(compressed, header.length);

return { label: "print-bitmap", data: command, bulk: true };
}

/** Query battery level: 10 FF 50 F1 */
export function getBattery(): PrintCommand {
return {
label: "get-battery",
data: Uint8Array.from([0x10, 0xff, 0x50, 0xf1]),
};
}

/** Query printer status: 10 FF 40 */
export function getStatus(): PrintCommand {
return { label: "get-status", data: Uint8Array.from([0x10, 0xff, 0x40]) };
}

/** Query model string: 10 FF 20 F0 */
export function getModel(): PrintCommand {
return {
label: "get-model",
data: Uint8Array.from([0x10, 0xff, 0x20, 0xf0]),
};
}

/** Query firmware version: 10 FF 20 F1 */
export function getFirmware(): PrintCommand {
return {
label: "get-firmware",
data: Uint8Array.from([0x10, 0xff, 0x20, 0xf1]),
};
}

/** Query serial number: 10 FF 20 F2 */
export function getSerial(): PrintCommand {
return {
label: "get-serial",
data: Uint8Array.from([0x10, 0xff, 0x20, 0xf2]),
};
}

/** Query Bluetooth MAC address: 10 FF 20 F3 */
export function getMac(): PrintCommand {
return {
label: "get-mac",
data: Uint8Array.from([0x10, 0xff, 0x20, 0xf3]),
};
}

/** Query BT module version: 10 FF 30 10 */
export function getBtVersion(): PrintCommand {
return {
label: "get-bt-version",
data: Uint8Array.from([0x10, 0xff, 0x30, 0x10]),
};
}

/** Query BT device name: 10 FF 30 11 */
export function getBtName(): PrintCommand {
return {
label: "get-bt-name",
data: Uint8Array.from([0x10, 0xff, 0x30, 0x11]),
};
}

/** Query print speed: 1F 60 00 */
export function getSpeed(): PrintCommand {
return {
label: "get-speed",
data: Uint8Array.from([0x1f, 0x60, 0x00]),
};
}
Loading
Loading