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
57 changes: 46 additions & 11 deletions packages/core/src/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,10 @@ export class Printer {
debugLog("PRINT", "done");
}

async getStatus(): Promise<PrinterStatus> {
async getStatus(timeoutMs = 5000): Promise<PrinterStatus> {
const cmd = this.protocol.buildStatusQuery();
await this.flowController.send(cmd.data);
const response = await this.waitForResponse("status");
const response = await this.waitForResponse("status", timeoutMs);
return {
status: (response?.value as string) ?? "unknown",
raw: response?.raw ?? new Uint8Array(),
Expand All @@ -170,8 +170,28 @@ export class Printer {
async getBattery(): Promise<number> {
const cmd = this.protocol.buildBatteryQuery();
await this.flowController.send(cmd.data);
const response = await this.waitForResponse("battery");
return (response?.value as number) ?? -1;
const response = await this.waitForResponse("battery", 3000);
// Battery is in response[1] as a raw byte (0-100)
if (response.value !== undefined) return response.value as number;
if (response.raw.length >= 2) return response.raw[1];
return response.raw[0] ?? -1;
}

async getModel(): Promise<string> {
const cmd = this.protocol.buildModelQuery();
await this.flowController.send(cmd.data);
const response = await this.waitForResponse("model", 3000);
if (typeof response.value === "string" && response.value) return response.value;
return new TextDecoder().decode(response.raw).replace(/\0/g, "").trim();
}

async getInfo(type: "firmware" | "serial" | "mac" | "bt-version" | "bt-name" | "speed"): Promise<string> {
const cmd = this.protocol.buildInfoQuery(type);
await this.flowController.send(cmd.data);
const response = await this.waitForResponse(type, 3000);
if (typeof response.value === "string" && response.value) return response.value;
if (typeof response.value === "number") return String(response.value);
return new TextDecoder().decode(response.raw).replace(/\0/g, "").trim();
}

async disconnect(): Promise<void> {
Expand Down Expand Up @@ -219,16 +239,31 @@ export class Printer {
private handleRxData(data: Uint8Array): void {
const response = this.protocol.parseResponse(data);
debugLog("RX", `${formatBytes(data)}${response ? ` → ${response.type}` : " (unknown)"}${response?.value !== undefined ? ` value=${response.value}` : ""}`);
if (!response) return;

if (response.type === "status") {
this.emit("status", { status: response.value as string, raw: data });
if (response) {
if (response.type === "status") {
this.emit("status", { status: response.value as string, raw: data });
}

// Resolve any pending waiters matching the parsed type
const idx = this.pendingResponses.findIndex((p) => p.type === response.type);
if (idx !== -1) {
this.pendingResponses.splice(idx, 1)[0].resolve(response);
return;
}
}

// Resolve any pending waiters
const idx = this.pendingResponses.findIndex((p) => p.type === response.type);
if (idx !== -1) {
this.pendingResponses.splice(idx, 1)[0].resolve(response);
// Unrecognized data — if there's a pending query waiter (battery/model/firmware),
// deliver the raw bytes to it. The Marklife printer returns query responses as
// raw data without echoing the command prefix.
if (!response && this.pendingResponses.length > 0) {
const waiter = this.pendingResponses[0];
const queryTypes = ["battery", "model", "firmware", "serial", "mac", "bt-version", "bt-name", "speed", "status"];
if (queryTypes.includes(waiter.type)) {
debugLog("RX", `routing raw data to pending "${waiter.type}" waiter`);
this.pendingResponses.splice(0, 1);
waiter.resolve({ type: waiter.type as PrinterResponse["type"], raw: data });
}
}
}

Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/protocol/l11/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,46 @@ export function getFirmware(): PrintCommand {
};
}

/** 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]),
};
}

/** Print self-test page: 1F 40 */
export function selfCheck(): PrintCommand {
return { label: "self-check", data: Uint8Array.from([0x1f, 0x40]) };
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/protocol/l11/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ export class L11Protocol implements PrinterProtocol {
return cmd.getBattery();
}

buildModelQuery(): PrintCommand {
return cmd.getModel();
}

buildInfoQuery(type: "firmware" | "serial" | "mac" | "bt-version" | "bt-name" | "speed"): PrintCommand {
switch (type) {
case "firmware": return cmd.getFirmware();
case "serial": return cmd.getSerial();
case "mac": return cmd.getMac();
case "bt-version": return cmd.getBtVersion();
case "bt-name": return cmd.getBtName();
case "speed": return cmd.getSpeed();
}
}

parseResponse(data: Uint8Array): PrinterResponse | null {
if (data.length < 1) return null;

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/protocol/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export type PrinterResponseType =
| "firmware"
| "serial"
| "mac"
| "bt-version"
| "bt-name"
| "speed"
| "credit"
| "mtu";

Expand Down Expand Up @@ -44,5 +47,7 @@ export interface PrinterProtocol {
buildWakeup(): PrintCommand[];
buildStatusQuery(): PrintCommand;
buildBatteryQuery(): PrintCommand;
buildModelQuery(): PrintCommand;
buildInfoQuery(type: "firmware" | "serial" | "mac" | "bt-version" | "bt-name" | "speed"): PrintCommand;
parseResponse(data: Uint8Array): PrinterResponse | null;
}
22 changes: 17 additions & 5 deletions packages/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,26 @@
/>
<script>
(function() {
var t = localStorage.getItem('thermoprint-theme');
if (t === 'dark' || (t !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
try {
var themes = ["cyan","amber","graphite","violet","forest","paper"];
var modes = ["dark","light"];
var t = localStorage.getItem("tp.theme.v1") || "cyan";
var m = localStorage.getItem("tp.mode.v1") || "dark";
if (themes.indexOf(t) < 0) t = "cyan";
if (modes.indexOf(m) < 0) m = "dark";
document.documentElement.setAttribute("data-theme", t);
document.documentElement.setAttribute("data-mode", m);
if (m === "dark") document.documentElement.classList.add("dark");
// UI scale
var s = parseFloat(localStorage.getItem("tp.uiScale.v1") || "1");
if (isFinite(s) && s >= 0.8 && s <= 1.4) {
document.documentElement.style.setProperty("--ui-scale", String(s));
}
} catch(e) {}
})();
</script>
</head>
<body class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100">
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
5 changes: 4 additions & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-konva": "^19.2.3",
"zustand": "^5.0.11"
"zundo": "^2.3.0",
"zustand": "^5.0.11",
"@fontsource/inter": "^5.2.5",
"@fontsource-variable/jetbrains-mono": "^5.2.5"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LabelEditor } from "./editor/label-editor.tsx";
import { Editor } from "./editor/editor.tsx";

function BluetoothUnsupportedBanner() {
return (
Expand All @@ -25,7 +25,7 @@ export function App() {
return (
<>
{!isBluetoothSupported && <BluetoothUnsupportedBanner />}
<LabelEditor />
<Editor />
</>
);
}
9 changes: 9 additions & 0 deletions packages/web/src/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/web/src/components/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck — legacy v1 component, not used by the redesigned editor
import { useState, useRef, type ReactNode } from "react";
import { createPortal } from "react-dom";

Expand Down
Loading
Loading