mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Major fixes: - Fix PEN data encoding to properly mark color changes and end markers - COLOR_END (0x03) for intermediate color blocks - DATA_END (0x05) for the final stitch only - Machine now correctly reads total stitch count across all color blocks - Reset uploadProgress when pattern is deleted to re-enable upload button - Allow pattern deletion during WAITING states - Allow pattern upload in COMPLETE states - Fix pattern state tracking to reset when patternInfo is null UI improvements: - Integrate workflow stepper into compact header - Change app title to "SKiTCH Controller" - Reduce header size from ~200px to ~70px - Make Sewing Progress section more compact with two-column layout - Replace emojis with Heroicons throughout - Reorganize action buttons with better visual hierarchy - Add cursor-pointer to all buttons for better UX - Fix cached pattern not showing info in Pattern File box - Remove duplicate status messages (keep only state visual indicator) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
616 lines
18 KiB
TypeScript
616 lines
18 KiB
TypeScript
import type {
|
|
MachineInfo,
|
|
PatternInfo,
|
|
SewingProgress,
|
|
} from "../types/machine";
|
|
import { MachineStatus } from "../types/machine";
|
|
|
|
// BLE Service and Characteristic UUIDs
|
|
const SERVICE_UUID = "a76eb9e0-f3ac-4990-84cf-3a94d2426b2b";
|
|
const WRITE_CHAR_UUID = "a76eb9e2-f3ac-4990-84cf-3a94d2426b2b";
|
|
const READ_CHAR_UUID = "a76eb9e1-f3ac-4990-84cf-3a94d2426b2b";
|
|
|
|
// Command IDs (big-endian)
|
|
const Commands = {
|
|
MACHINE_INFO: 0x0000,
|
|
MACHINE_STATE: 0x0001,
|
|
SERVICE_COUNT: 0x0100,
|
|
REGULAR_INSPECTION: 0x0103,
|
|
PATTERN_UUID_REQUEST: 0x0702,
|
|
MASK_TRACE: 0x0704,
|
|
LAYOUT_SEND: 0x0705,
|
|
EMB_SEWING_INFO_REQUEST: 0x0706,
|
|
PATTERN_SEWING_INFO: 0x0707,
|
|
EMB_SEWING_DATA_DELETE: 0x0708,
|
|
NEEDLE_MODE_INSTRUCTIONS: 0x0709,
|
|
EMB_UUID_SEND: 0x070a,
|
|
RESUME_FLAG_REQUEST: 0x070b,
|
|
RESUME: 0x070c,
|
|
HOOP_AVOIDANCE: 0x070f,
|
|
START_SEWING: 0x070e,
|
|
MASK_TRACE_1: 0x0710,
|
|
EMB_ORG_POINT: 0x0800,
|
|
FIRM_UPDATE_START: 0x0b00,
|
|
SET_SETTING_REST: 0x0c00,
|
|
SET_SETTING_SEND: 0x0c01,
|
|
MACHINE_SETTING_INFO: 0x0c02,
|
|
SEND_DATA_INFO: 0x1200,
|
|
SEND_DATA: 0x1201,
|
|
CLEAR_ERROR: 0x1300,
|
|
ERROR_LOG_REPLY: 0x1301,
|
|
};
|
|
|
|
export class BrotherPP1Service {
|
|
private device: BluetoothDevice | null = null;
|
|
private server: BluetoothRemoteGATTServer | null = null;
|
|
private writeCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
|
|
private readCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
|
|
private commandQueue: Array<() => Promise<void>> = [];
|
|
private isProcessingQueue = false;
|
|
|
|
async connect(): Promise<void> {
|
|
this.device = await navigator.bluetooth.requestDevice({
|
|
filters: [{ services: [SERVICE_UUID] }],
|
|
});
|
|
|
|
if (!this.device.gatt) {
|
|
throw new Error("GATT not available");
|
|
}
|
|
console.log("Connecting");
|
|
this.server = await this.device.gatt.connect();
|
|
console.log("Connected");
|
|
const service = await this.server.getPrimaryService(SERVICE_UUID);
|
|
console.log("Got primary service");
|
|
|
|
this.writeCharacteristic = await service.getCharacteristic(WRITE_CHAR_UUID);
|
|
this.readCharacteristic = await service.getCharacteristic(READ_CHAR_UUID);
|
|
|
|
console.log("Connected to Brother PP1 machine");
|
|
|
|
console.log("Send dummy command");
|
|
try {
|
|
await this.getMachineInfo();
|
|
console.log("Dummy command success");
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
async disconnect(): Promise<void> {
|
|
// Clear any pending commands
|
|
this.commandQueue = [];
|
|
this.isProcessingQueue = false;
|
|
|
|
if (this.server) {
|
|
this.server.disconnect();
|
|
}
|
|
this.device = null;
|
|
this.server = null;
|
|
this.writeCharacteristic = null;
|
|
this.readCharacteristic = null;
|
|
}
|
|
|
|
isConnected(): boolean {
|
|
return this.server?.connected ?? false;
|
|
}
|
|
|
|
/**
|
|
* Process the command queue sequentially
|
|
*/
|
|
private async processQueue(): Promise<void> {
|
|
if (this.isProcessingQueue || this.commandQueue.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.isProcessingQueue = true;
|
|
|
|
while (this.commandQueue.length > 0) {
|
|
const command = this.commandQueue.shift();
|
|
if (command) {
|
|
try {
|
|
await command();
|
|
} catch (err) {
|
|
console.error("Command queue error:", err);
|
|
// Continue processing queue even if one command fails
|
|
}
|
|
}
|
|
}
|
|
|
|
this.isProcessingQueue = false;
|
|
}
|
|
|
|
/**
|
|
* Enqueue a Bluetooth command to be executed sequentially
|
|
*/
|
|
private async enqueueCommand<T>(operation: () => Promise<T>): Promise<T> {
|
|
return new Promise<T>((resolve, reject) => {
|
|
this.commandQueue.push(async () => {
|
|
try {
|
|
const result = await operation();
|
|
resolve(result);
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
|
|
// Start processing the queue
|
|
this.processQueue();
|
|
});
|
|
}
|
|
|
|
private getCommandName(cmdId: number): string {
|
|
const names: Record<number, string> = {
|
|
[Commands.MACHINE_INFO]: "MACHINE_INFO",
|
|
[Commands.MACHINE_STATE]: "MACHINE_STATE",
|
|
[Commands.SERVICE_COUNT]: "SERVICE_COUNT",
|
|
[Commands.REGULAR_INSPECTION]: "REGULAR_INSPECTION",
|
|
[Commands.PATTERN_UUID_REQUEST]: "PATTERN_UUID_REQUEST",
|
|
[Commands.MASK_TRACE]: "MASK_TRACE",
|
|
[Commands.LAYOUT_SEND]: "LAYOUT_SEND",
|
|
[Commands.EMB_SEWING_INFO_REQUEST]: "EMB_SEWING_INFO_REQUEST",
|
|
[Commands.PATTERN_SEWING_INFO]: "PATTERN_SEWING_INFO",
|
|
[Commands.EMB_SEWING_DATA_DELETE]: "EMB_SEWING_DATA_DELETE",
|
|
[Commands.NEEDLE_MODE_INSTRUCTIONS]: "NEEDLE_MODE_INSTRUCTIONS",
|
|
[Commands.EMB_UUID_SEND]: "EMB_UUID_SEND",
|
|
[Commands.RESUME_FLAG_REQUEST]: "RESUME_FLAG_REQUEST",
|
|
[Commands.RESUME]: "RESUME",
|
|
[Commands.HOOP_AVOIDANCE]: "HOOP_AVOIDANCE",
|
|
[Commands.START_SEWING]: "START_SEWING",
|
|
[Commands.MASK_TRACE_1]: "MASK_TRACE_1",
|
|
[Commands.EMB_ORG_POINT]: "EMB_ORG_POINT",
|
|
[Commands.FIRM_UPDATE_START]: "FIRM_UPDATE_START",
|
|
[Commands.SET_SETTING_REST]: "SET_SETTING_REST",
|
|
[Commands.SET_SETTING_SEND]: "SET_SETTING_SEND",
|
|
[Commands.MACHINE_SETTING_INFO]: "MACHINE_SETTING_INFO",
|
|
[Commands.SEND_DATA_INFO]: "SEND_DATA_INFO",
|
|
[Commands.SEND_DATA]: "SEND_DATA",
|
|
[Commands.CLEAR_ERROR]: "CLEAR_ERROR",
|
|
[Commands.ERROR_LOG_REPLY]: "ERROR_LOG_REPLY",
|
|
};
|
|
return names[cmdId] || `UNKNOWN(0x${cmdId.toString(16).padStart(4, "0")})`;
|
|
}
|
|
|
|
private async sendCommand(
|
|
cmdId: number,
|
|
data: Uint8Array = new Uint8Array(),
|
|
): Promise<Uint8Array> {
|
|
// Enqueue the command to ensure sequential execution
|
|
return this.enqueueCommand(async () => {
|
|
if (!this.writeCharacteristic || !this.readCharacteristic) {
|
|
throw new Error("Not connected");
|
|
}
|
|
|
|
// Build command with big-endian command ID
|
|
const command = new Uint8Array(2 + data.length);
|
|
command[0] = (cmdId >> 8) & 0xff; // High byte
|
|
command[1] = cmdId & 0xff; // Low byte
|
|
command.set(data, 2);
|
|
|
|
const hexData = Array.from(command)
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join(" ");
|
|
|
|
console.log(
|
|
`[TX] ${this.getCommandName(cmdId)} (0x${cmdId.toString(16).padStart(4, "0")}):`,
|
|
hexData,
|
|
);
|
|
|
|
// Write command
|
|
await this.writeCharacteristic.writeValueWithResponse(command);
|
|
|
|
// Longer delay to allow machine to prepare response
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
|
|
// Read response
|
|
const responseData = await this.readCharacteristic.readValue();
|
|
const response = new Uint8Array(responseData.buffer);
|
|
|
|
const hexResponse = Array.from(response)
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join(" ");
|
|
|
|
// Parse response
|
|
let parsed = "";
|
|
if (response.length >= 3) {
|
|
const respCmdId = (response[0] << 8) | response[1];
|
|
const status = response[2];
|
|
parsed = ` | Status: 0x${status.toString(16).padStart(2, "0")}`;
|
|
if (respCmdId !== cmdId) {
|
|
parsed += ` | WARNING: Response cmd 0x${respCmdId.toString(16).padStart(4, "0")} != request cmd`;
|
|
}
|
|
}
|
|
|
|
console.log(`[RX] ${this.getCommandName(cmdId)}:`, hexResponse, parsed);
|
|
|
|
return response;
|
|
});
|
|
}
|
|
|
|
async getMachineInfo(): Promise<MachineInfo> {
|
|
const response = await this.sendCommand(Commands.MACHINE_INFO);
|
|
|
|
// Skip 2-byte command header
|
|
const data = response.slice(2);
|
|
|
|
const decoder = new TextDecoder("ascii");
|
|
const serialNumber = decoder.decode(data.slice(2, 11)).replace(/\0/g, "");
|
|
const modelCode = decoder.decode(data.slice(39, 50)).replace(/\0/g, "");
|
|
|
|
// Software version (big-endian int16)
|
|
const swVersion = (data[0] << 8) | data[1];
|
|
|
|
// BT version (big-endian int16)
|
|
const btVersion = (data[24] << 8) | data[25];
|
|
|
|
// Max dimensions (little-endian int16)
|
|
const maxWidth = data[29] | (data[30] << 8);
|
|
const maxHeight = data[31] | (data[32] << 8);
|
|
|
|
// MAC address
|
|
const macAddress = Array.from(data.slice(16, 22))
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join(":")
|
|
.toUpperCase();
|
|
|
|
return {
|
|
serialNumber,
|
|
modelNumber: modelCode,
|
|
softwareVersion: `${(swVersion / 100).toFixed(2)}.${data[35]}`,
|
|
bluetoothVersion: btVersion,
|
|
maxWidth,
|
|
maxHeight,
|
|
macAddress,
|
|
};
|
|
}
|
|
|
|
async getMachineState(): Promise<{ status: MachineStatus; error: number }> {
|
|
const response = await this.sendCommand(Commands.MACHINE_STATE);
|
|
|
|
return {
|
|
status: response[2] as MachineStatus,
|
|
error: response[4],
|
|
};
|
|
}
|
|
|
|
async getPatternInfo(): Promise<PatternInfo> {
|
|
const response = await this.sendCommand(Commands.EMB_SEWING_INFO_REQUEST);
|
|
const data = response.slice(2);
|
|
|
|
const readInt16LE = (offset: number) =>
|
|
data[offset] | (data[offset + 1] << 8);
|
|
const readUInt16LE = (offset: number) =>
|
|
data[offset] | (data[offset + 1] << 8);
|
|
|
|
return {
|
|
boundLeft: readInt16LE(0),
|
|
boundTop: readInt16LE(2),
|
|
boundRight: readInt16LE(4),
|
|
boundBottom: readInt16LE(6),
|
|
totalTime: readUInt16LE(8),
|
|
totalStitches: readUInt16LE(10),
|
|
speed: readUInt16LE(12),
|
|
};
|
|
}
|
|
|
|
async getSewingProgress(): Promise<SewingProgress> {
|
|
const response = await this.sendCommand(Commands.PATTERN_SEWING_INFO);
|
|
const data = response.slice(2);
|
|
|
|
const readInt16LE = (offset: number) => {
|
|
const value = data[offset] | (data[offset + 1] << 8);
|
|
// Convert to signed 16-bit integer
|
|
return value > 0x7fff ? value - 0x10000 : value;
|
|
};
|
|
const readUInt16LE = (offset: number) =>
|
|
data[offset] | (data[offset + 1] << 8);
|
|
|
|
return {
|
|
currentStitch: readUInt16LE(0),
|
|
currentTime: readInt16LE(2),
|
|
stopTime: readInt16LE(4),
|
|
positionX: readInt16LE(6),
|
|
positionY: readInt16LE(8),
|
|
};
|
|
}
|
|
|
|
async deletePattern(): Promise<void> {
|
|
await this.sendCommand(Commands.EMB_SEWING_DATA_DELETE);
|
|
}
|
|
|
|
async sendDataInfo(length: number, checksum: number): Promise<void> {
|
|
const payload = new Uint8Array(7);
|
|
payload[0] = 0x03; // Type
|
|
|
|
// Length (little-endian uint32)
|
|
payload[1] = length & 0xff;
|
|
payload[2] = (length >> 8) & 0xff;
|
|
payload[3] = (length >> 16) & 0xff;
|
|
payload[4] = (length >> 24) & 0xff;
|
|
|
|
// Checksum (little-endian uint16)
|
|
payload[5] = checksum & 0xff;
|
|
payload[6] = (checksum >> 8) & 0xff;
|
|
|
|
const response = await this.sendCommand(Commands.SEND_DATA_INFO, payload);
|
|
|
|
if (response[2] !== 0x00) {
|
|
throw new Error("Data info rejected");
|
|
}
|
|
}
|
|
|
|
async sendDataChunk(offset: number, data: Uint8Array): Promise<void> {
|
|
const checksum = data.reduce((sum, byte) => (sum + byte) & 0xff, 0);
|
|
|
|
const payload = new Uint8Array(4 + data.length + 1);
|
|
|
|
// Offset (little-endian uint32)
|
|
payload[0] = offset & 0xff;
|
|
payload[1] = (offset >> 8) & 0xff;
|
|
payload[2] = (offset >> 16) & 0xff;
|
|
payload[3] = (offset >> 24) & 0xff;
|
|
|
|
payload.set(data, 4);
|
|
payload[4 + data.length] = checksum;
|
|
|
|
// Official app approach: Send chunk without waiting for response
|
|
await this.sendCommandNoResponse(Commands.SEND_DATA, payload);
|
|
}
|
|
|
|
private async sendCommandNoResponse(
|
|
cmdId: number,
|
|
data: Uint8Array = new Uint8Array(),
|
|
): Promise<void> {
|
|
return this.enqueueCommand(async () => {
|
|
if (!this.writeCharacteristic) {
|
|
throw new Error("Not connected");
|
|
}
|
|
|
|
// Build command with big-endian command ID
|
|
const command = new Uint8Array(2 + data.length);
|
|
command[0] = (cmdId >> 8) & 0xff; // High byte
|
|
command[1] = cmdId & 0xff; // Low byte
|
|
command.set(data, 2);
|
|
|
|
const hexData = Array.from(command)
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join(" ");
|
|
|
|
console.log(
|
|
`[TX-NoResp] ${this.getCommandName(cmdId)} (0x${cmdId.toString(16).padStart(4, "0")}):`,
|
|
hexData.substring(0, 100) + (hexData.length > 100 ? "..." : ""),
|
|
);
|
|
|
|
// Write without reading response
|
|
await this.writeCharacteristic.writeValueWithResponse(command);
|
|
|
|
// Small delay to allow BLE buffer to clear
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
});
|
|
}
|
|
|
|
async sendUUID(uuid: Uint8Array): Promise<void> {
|
|
const response = await this.sendCommand(Commands.EMB_UUID_SEND, uuid);
|
|
|
|
if (response[2] !== 0x00) {
|
|
throw new Error("UUID rejected");
|
|
}
|
|
}
|
|
|
|
async sendLayout(
|
|
moveX: number,
|
|
moveY: number,
|
|
sizeX: number,
|
|
sizeY: number,
|
|
rotate: number,
|
|
flip: number,
|
|
frame: number,
|
|
boundLeft: number,
|
|
boundTop: number,
|
|
boundRight: number,
|
|
boundBottom: number,
|
|
): Promise<void> {
|
|
const payload = new Uint8Array(26);
|
|
|
|
const writeInt16LE = (offset: number, value: number) => {
|
|
payload[offset] = value & 0xff;
|
|
payload[offset + 1] = (value >> 8) & 0xff;
|
|
};
|
|
|
|
// Position/transformation parameters (12 bytes)
|
|
writeInt16LE(0, moveX);
|
|
writeInt16LE(2, moveY);
|
|
writeInt16LE(4, sizeX);
|
|
writeInt16LE(6, sizeY);
|
|
writeInt16LE(8, rotate);
|
|
payload[10] = flip;
|
|
payload[11] = frame;
|
|
|
|
// Pattern bounds (8 bytes)
|
|
writeInt16LE(12, boundLeft);
|
|
writeInt16LE(14, boundTop);
|
|
writeInt16LE(16, boundRight);
|
|
writeInt16LE(18, boundBottom);
|
|
|
|
// Repeat moveX and moveY at the end (6 bytes)
|
|
writeInt16LE(20, moveX);
|
|
writeInt16LE(22, moveY);
|
|
payload[24] = flip;
|
|
payload[25] = frame;
|
|
|
|
console.log('[DEBUG] Layout bounds:', {
|
|
boundLeft,
|
|
boundTop,
|
|
boundRight,
|
|
boundBottom,
|
|
moveX,
|
|
moveY,
|
|
sizeX,
|
|
sizeY,
|
|
});
|
|
|
|
await this.sendCommand(Commands.LAYOUT_SEND, payload);
|
|
}
|
|
|
|
async getMachineSettings(): Promise<Uint8Array> {
|
|
return await this.sendCommand(Commands.MACHINE_SETTING_INFO);
|
|
}
|
|
|
|
async startMaskTrace(): Promise<void> {
|
|
// Query machine settings before starting mask trace (as per official app)
|
|
await this.getMachineSettings();
|
|
|
|
const payload = new Uint8Array([0x00]);
|
|
await this.sendCommand(Commands.MASK_TRACE, payload);
|
|
}
|
|
|
|
async startSewing(): Promise<void> {
|
|
await this.sendCommand(Commands.START_SEWING);
|
|
}
|
|
|
|
async resumeSewing(): Promise<void> {
|
|
// Resume uses the same START_SEWING command as initial start
|
|
// The machine tracks current position and resumes from there
|
|
await this.sendCommand(Commands.START_SEWING);
|
|
}
|
|
|
|
async uploadPattern(
|
|
data: Uint8Array,
|
|
onProgress?: (progress: number) => void,
|
|
bounds?: { minX: number; maxX: number; minY: number; maxY: number },
|
|
patternOffset?: { x: number; y: number },
|
|
): Promise<Uint8Array> {
|
|
// Calculate checksum
|
|
const checksum = data.reduce((sum, byte) => sum + byte, 0) & 0xffff;
|
|
|
|
// Delete existing pattern
|
|
await this.deletePattern();
|
|
|
|
// Send data info
|
|
await this.sendDataInfo(data.length, checksum);
|
|
|
|
// Send data in chunks (max chunk size ~500 bytes to be safe with BLE MTU)
|
|
const chunkSize = 500;
|
|
let offset = 0;
|
|
|
|
// Send all chunks without waiting for responses (official app approach)
|
|
while (offset < data.length) {
|
|
const chunk = data.slice(
|
|
offset,
|
|
Math.min(offset + chunkSize, data.length),
|
|
);
|
|
|
|
await this.sendDataChunk(offset, chunk);
|
|
offset += chunk.length;
|
|
|
|
if (onProgress) {
|
|
onProgress((offset / data.length) * 100);
|
|
}
|
|
}
|
|
|
|
// Wait a bit for machine to finish processing chunks
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
|
|
// Use provided bounds or default to 0
|
|
const boundLeft = bounds?.minX ?? 0;
|
|
const boundTop = bounds?.minY ?? 0;
|
|
const boundRight = bounds?.maxX ?? 0;
|
|
const boundBottom = bounds?.maxY ?? 0;
|
|
|
|
// Calculate move offset based on user-defined pattern offset or auto-center
|
|
let moveX: number;
|
|
let moveY: number;
|
|
|
|
if (patternOffset) {
|
|
// Use user-defined offset from canvas dragging
|
|
// Pattern offset is in canvas coordinates (0,0 at hoop center)
|
|
// We need to calculate the move that positions pattern's center at the offset position
|
|
const patternCenterX = (boundLeft + boundRight) / 2;
|
|
const patternCenterY = (boundTop + boundBottom) / 2;
|
|
|
|
// moveX/moveY define where the pattern center should be
|
|
// offset.x/y is where user dragged the pattern to (relative to hoop center)
|
|
moveX = patternOffset.x - patternCenterX;
|
|
moveY = patternOffset.y - patternCenterY;
|
|
|
|
console.log('[LAYOUT] Using user-defined offset:', {
|
|
patternOffset,
|
|
patternCenter: { x: patternCenterX, y: patternCenterY },
|
|
moveX,
|
|
moveY,
|
|
});
|
|
} else {
|
|
// Auto-center: position pattern center at machine center (0, 0)
|
|
const patternCenterX = (boundLeft + boundRight) / 2;
|
|
const patternCenterY = (boundTop + boundBottom) / 2;
|
|
moveX = -patternCenterX;
|
|
moveY = -patternCenterY;
|
|
|
|
console.log('[LAYOUT] Auto-centering pattern:', { moveX, moveY });
|
|
}
|
|
|
|
// Send layout with actual pattern bounds
|
|
// sizeX/sizeY are scaling factors (100 = 100% = no scaling)
|
|
await this.sendLayout(
|
|
Math.round(moveX), // moveX - position the pattern
|
|
Math.round(moveY), // moveY - position the pattern
|
|
100, // sizeX (100% - no scaling)
|
|
100, // sizeY (100% - no scaling)
|
|
0, // rotate
|
|
0, // flip
|
|
1, // frame
|
|
boundLeft,
|
|
boundTop,
|
|
boundRight,
|
|
boundBottom,
|
|
);
|
|
|
|
// Generate random UUID
|
|
const uuid = crypto.getRandomValues(new Uint8Array(16));
|
|
await this.sendUUID(uuid);
|
|
|
|
console.log(
|
|
"Pattern uploaded successfully with UUID:",
|
|
Array.from(uuid)
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join(""),
|
|
);
|
|
|
|
// Return UUID for caching
|
|
return uuid;
|
|
}
|
|
|
|
/**
|
|
* Request the UUID of the pattern currently loaded on the machine
|
|
*/
|
|
async getPatternUUID(): Promise<Uint8Array | null> {
|
|
try {
|
|
const response = await this.sendCommand(Commands.PATTERN_UUID_REQUEST);
|
|
|
|
// Response format: [cmd_high, cmd_low, uuid_bytes...]
|
|
// UUID starts at index 2 (16 bytes)
|
|
if (response.length < 18) {
|
|
// Not enough data for UUID
|
|
console.log(
|
|
"[BrotherPP1] Response too short for UUID:",
|
|
response.length,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// Extract UUID (16 bytes starting at index 2)
|
|
const uuid = response.slice(2, 18);
|
|
|
|
// Check if UUID is all zeros (no pattern loaded)
|
|
const allZeros = uuid.every((byte) => byte === 0);
|
|
if (allZeros) {
|
|
console.log("[BrotherPP1] UUID is all zeros, no pattern loaded");
|
|
return null;
|
|
}
|
|
|
|
return uuid;
|
|
} catch (err) {
|
|
console.error("[BrotherPP1] Failed to get pattern UUID:", err);
|
|
return null;
|
|
}
|
|
}
|
|
}
|