diff --git a/src/components/ProgressMonitor.tsx b/src/components/ProgressMonitor.tsx index 67b944a..1effe9d 100644 --- a/src/components/ProgressMonitor.tsx +++ b/src/components/ProgressMonitor.tsx @@ -230,15 +230,17 @@ export function ProgressMonitor({ )} - {/* Mask trace complete - waiting for confirmation */} + {/* Mask trace complete - ready to sew */} {isMaskTraceComplete && ( <>
Mask trace complete!
-
- Press button on machine to confirm (or trace again) -
+ {canStartSewing(machineStatus) && ( + + )} {canStartMaskTrace(machineStatus) && ( + )} + + )} + {/* Ready to start (pattern uploaded) */} {machineStatus === MachineStatus.SEWING_WAIT && ( <> diff --git a/src/hooks/useBrotherMachine.ts b/src/hooks/useBrotherMachine.ts index 350a856..f2e57b3 100644 --- a/src/hooks/useBrotherMachine.ts +++ b/src/hooks/useBrotherMachine.ts @@ -202,9 +202,13 @@ export function useBrotherMachine() { try { setError(null); setUploadProgress(0); - const uuid = await service.uploadPattern(penData, (progress) => { - setUploadProgress(progress); - }); + const uuid = await service.uploadPattern( + penData, + (progress) => { + setUploadProgress(progress); + }, + pesData.bounds, + ); setUploadProgress(100); // Cache the pattern with its UUID diff --git a/src/services/BrotherPP1Service.ts b/src/services/BrotherPP1Service.ts index 6bfe4ba..21c87c4 100644 --- a/src/services/BrotherPP1Service.ts +++ b/src/services/BrotherPP1Service.ts @@ -15,6 +15,7 @@ const Commands = { MACHINE_INFO: 0x0000, MACHINE_STATE: 0x0001, SERVICE_COUNT: 0x0100, + REGULAR_INSPECTION: 0x0103, PATTERN_UUID_REQUEST: 0x0702, MASK_TRACE: 0x0704, LAYOUT_SEND: 0x0705, @@ -25,13 +26,18 @@ const Commands = { 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 { @@ -132,6 +138,38 @@ export class BrotherPP1Service { }); } + private getCommandName(cmdId: number): string { + const names: Record = { + [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(), @@ -148,30 +186,41 @@ export class BrotherPP1Service { 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( - "Sending command:", - Array.from(command) - .map((b) => b.toString(16).padStart(2, "0")) - .join(" "), + `[TX] ${this.getCommandName(cmdId)} (0x${cmdId.toString(16).padStart(4, "0")}):`, + hexData, ); - console.log("Sending command"); - // Write command and immediately read response - await this.writeCharacteristic.writeValueWithoutResponse(command); - console.log("delay"); - // Small delay to ensure response is ready + // Write command + await this.writeCharacteristic.writeValueWithResponse(command); + + // Longer delay to allow machine to prepare response await new Promise((resolve) => setTimeout(resolve, 50)); - console.log("reading response"); + // Read response const responseData = await this.readCharacteristic.readValue(); const response = new Uint8Array(responseData.buffer); - console.log( - "Received response:", - Array.from(response) - .map((b) => b.toString(16).padStart(2, "0")) - .join(" "), - ); + 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; }); @@ -289,7 +338,7 @@ export class BrotherPP1Service { } } - async sendDataChunk(offset: number, data: Uint8Array): Promise { + async sendDataChunk(offset: number, data: Uint8Array): Promise { const checksum = data.reduce((sum, byte) => (sum + byte) & 0xff, 0); const payload = new Uint8Array(4 + data.length + 1); @@ -303,10 +352,81 @@ export class BrotherPP1Service { payload.set(data, 4); payload[4 + data.length] = checksum; - const response = await this.sendCommand(Commands.SEND_DATA, payload); + // Official app approach: Send chunk without waiting for response + await this.sendCommandNoResponse(Commands.SEND_DATA, payload); + } - // 0x00 = complete, 0x02 = continue - return response[2] === 0x00; + private async sendCommandNoResponse( + cmdId: number, + data: Uint8Array = new Uint8Array(), + ): Promise { + 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)); + }); + } + + private async pollForTransferComplete(): Promise { + if (!this.readCharacteristic) { + throw new Error("Not connected"); + } + + // Poll until transfer is complete + while (true) { + const responseData = await this.readCharacteristic.readValue(); + const response = new Uint8Array(responseData.buffer); + + console.log( + "Poll response:", + Array.from(response) + .map((b) => b.toString(16).padStart(2, "0")) + .join(" "), + ); + + // Check response format: [CMD_HIGH, CMD_LOW, STATUS] + if (response.length < 3) { + throw new Error("Invalid response length"); + } + + const status = response[2]; + + if (status === 0x01) { + // Error + throw new Error("Transfer failed"); + } else if (status === 0x00) { + // Complete + console.log("Transfer complete"); + break; + } else if (status === 0x02) { + // Continue - wait 1 second and poll again (as per official app) + console.log("Transfer in progress, waiting..."); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + throw new Error(`Unknown transfer status: 0x${status.toString(16)}`); + } + } } async sendUUID(uuid: Uint8Array): Promise { @@ -325,14 +445,19 @@ export class BrotherPP1Service { rotate: number, flip: number, frame: number, + boundLeft: number, + boundTop: number, + boundRight: number, + boundBottom: number, ): Promise { - const payload = new Uint8Array(12); + 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); @@ -341,11 +466,41 @@ export class BrotherPP1Service { 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 { + return await this.sendCommand(Commands.MACHINE_SETTING_INFO); + } + async startMaskTrace(): Promise { - const payload = new Uint8Array([0x01]); + // 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); } @@ -362,6 +517,7 @@ export class BrotherPP1Service { async uploadPattern( data: Uint8Array, onProgress?: (progress: number) => void, + bounds?: { minX: number; maxX: number; minY: number; maxY: number }, ): Promise { // Calculate checksum const checksum = data.reduce((sum, byte) => sum + byte, 0) & 0xffff; @@ -376,34 +532,64 @@ export class BrotherPP1Service { 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), ); - const isComplete = await this.sendDataChunk(offset, chunk); + await this.sendDataChunk(offset, chunk); offset += chunk.length; if (onProgress) { onProgress((offset / data.length) * 100); } - - if (isComplete) { - break; - } - - // Small delay between chunks - await new Promise((resolve) => setTimeout(resolve, 10)); } + // 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 pattern dimensions + const patternWidth = boundRight - boundLeft; + const patternHeight = boundBottom - boundTop; + + // Calculate center offset to position pattern at machine center + // Machine embroidery area center is at (0, 0) + // Pattern center should align with machine center + const patternCenterX = (boundLeft + boundRight) / 2; + const patternCenterY = (boundTop + boundBottom) / 2; + + // moveX/moveY shift the pattern so its center aligns with origin + const moveX = -patternCenterX; + const moveY = -patternCenterY; + + // Send layout with actual pattern bounds + // sizeX/sizeY are scaling factors (100 = 100% = no scaling) + await this.sendLayout( + Math.round(moveX), // moveX - center the pattern + Math.round(moveY), // moveY - center 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); - // Send default layout (no transformation) - await this.sendLayout(0, 0, 0, 0, 0, 0, 0); - console.log( "Pattern uploaded successfully with UUID:", Array.from(uuid) diff --git a/src/utils/errorCodeHelpers.ts b/src/utils/errorCodeHelpers.ts index 989be88..5a734ea 100644 --- a/src/utils/errorCodeHelpers.ts +++ b/src/utils/errorCodeHelpers.ts @@ -76,7 +76,12 @@ const ERROR_MESSAGES: Record = { /** * Get human-readable error message for an error code */ -export function getErrorMessage(errorCode: number): string | null { +export function getErrorMessage(errorCode: number | undefined): string | null { + // Handle undefined or null + if (errorCode === undefined || errorCode === null) { + return null; + } + // 0xDD (221) is the default "no error" value if (errorCode === SewingMachineError.None) { return null; // No error to display @@ -95,6 +100,6 @@ export function getErrorMessage(errorCode: number): string | null { /** * Check if error code represents an actual error condition */ -export function hasError(errorCode: number): boolean { - return errorCode !== SewingMachineError.None; +export function hasError(errorCode: number | undefined): boolean { + return errorCode !== undefined && errorCode !== null && errorCode !== SewingMachineError.None; } diff --git a/src/utils/machineStateHelpers.ts b/src/utils/machineStateHelpers.ts index b1e8fc2..a1897a9 100644 --- a/src/utils/machineStateHelpers.ts +++ b/src/utils/machineStateHelpers.ts @@ -88,6 +88,7 @@ export function canUploadPattern(status: MachineStatus): boolean { export function canStartSewing(status: MachineStatus): boolean { // Only in specific ready states return status === MachineStatus.SEWING_WAIT || + status === MachineStatus.MASK_TRACE_COMPLETE || status === MachineStatus.PAUSE || status === MachineStatus.STOP || status === MachineStatus.SEWING_INTERRUPTION; @@ -97,8 +98,9 @@ export function canStartSewing(status: MachineStatus): boolean { * Determines if mask trace can be started in the current state. */ export function canStartMaskTrace(status: MachineStatus): boolean { - // Only when ready or after previous trace - return status === MachineStatus.SEWING_WAIT || + // Can start mask trace when IDLE (after upload), SEWING_WAIT, or after previous trace + return status === MachineStatus.IDLE || + status === MachineStatus.SEWING_WAIT || status === MachineStatus.MASK_TRACE_COMPLETE; } diff --git a/src/utils/pystitchConverter.ts b/src/utils/pystitchConverter.ts index 4c31af8..3c07be3 100644 --- a/src/utils/pystitchConverter.ts +++ b/src/utils/pystitchConverter.ts @@ -46,45 +46,38 @@ export async function convertPesToPen(file: File): Promise { // Read the pattern using PyStitch const result = await pyodide.runPythonAsync(` import pystitch +from pystitch.EmbConstant import STITCH, JUMP, TRIM, STOP, END, COLOR_CHANGE # Read the PES file pattern = pystitch.read('${filename}') -# PyStitch groups stitches by color blocks using get_as_stitchblock -# This returns tuples of (thread, stitches_list) for each color block +# Use the raw stitches list which preserves command flags +# Each stitch in pattern.stitches is [x, y, cmd] +# We need to assign color indices based on COLOR_CHANGE commands +# and filter out COLOR_CHANGE and STOP commands (they're not actual stitches) + stitches_with_colors = [] -block_index = 0 +current_color = 0 -# Iterate through stitch blocks -# Each block is a tuple containing (thread, stitch_list) -for block in pattern.get_as_stitchblock(): - if isinstance(block, tuple): - # Extract thread and stitch list from tuple - thread_obj = None - stitches_list = None +for i, stitch in enumerate(pattern.stitches): + x, y, cmd = stitch - for elem in block: - # Check if this is the thread object (has color or hex_color attributes) - if hasattr(elem, 'color') or hasattr(elem, 'hex_color'): - thread_obj = elem - # Check if this is the stitch list - elif isinstance(elem, list) and len(elem) > 0 and isinstance(elem[0], list): - stitches_list = elem + # Check for color change command - increment color but don't add stitch + if cmd == COLOR_CHANGE: + current_color += 1 + continue - if stitches_list: - # Find the index of this thread in the threadlist - thread_index = block_index - if thread_obj and hasattr(pattern, 'threadlist'): - for i, t in enumerate(pattern.threadlist): - if t is thread_obj: - thread_index = i - break + # Check for stop command - skip it + if cmd == STOP: + continue - for stitch in stitches_list: - # stitch is [x, y, command] - stitches_with_colors.append([stitch[0], stitch[1], stitch[2], thread_index]) + # Check for standalone END command (no stitch data) + if cmd == END: + continue - block_index += 1 + # Add actual stitch with color index and command + # Keep JUMP/TRIM flags as they indicate jump stitches + stitches_with_colors.append([x, y, cmd, current_color]) # Convert to JSON-serializable format { @@ -98,13 +91,16 @@ for block in pattern.get_as_stitchblock(): ], 'thread_count': len(pattern.threadlist), 'stitch_count': len(stitches_with_colors), - 'block_count': block_index + 'color_changes': current_color } `); // Convert Python result to JavaScript const data = result.toJs({ dict_converter: Object.fromEntries }); + console.log('[DEBUG] PyStitch stitch_count:', data.stitch_count); + console.log('[DEBUG] PyStitch color_changes:', data.color_changes); + // Clean up virtual file try { pyodide.FS.unlink(filename); @@ -117,6 +113,32 @@ for block in pattern.get_as_stitchblock(): Array.from(stitch) as number[] ); + console.log('[DEBUG] JavaScript stitches.length:', stitches.length); + console.log('[DEBUG] First 5 stitches:', stitches.slice(0, 5)); + console.log('[DEBUG] Middle 5 stitches:', stitches.slice(Math.floor(stitches.length / 2), Math.floor(stitches.length / 2) + 5)); + console.log('[DEBUG] Last 5 stitches:', stitches.slice(-5)); + + // Count stitch types (PyStitch constants: STITCH=0, JUMP=1, TRIM=2) + let jumpCount = 0, normalCount = 0; + for (let i = 0; i < stitches.length; i++) { + const cmd = stitches[i][2]; + if (cmd === 1 || cmd === 2) jumpCount++; // JUMP or TRIM + else normalCount++; // STITCH (0) + } + console.log('[DEBUG] Stitch types: normal=' + normalCount + ', jump/trim=' + jumpCount); + + // Calculate min/max of raw stitch values to understand the data + let rawMinX = Infinity, rawMaxX = -Infinity, rawMinY = Infinity, rawMaxY = -Infinity; + for (let i = 0; i < stitches.length; i++) { + const x = stitches[i][0]; + const y = stitches[i][1]; + rawMinX = Math.min(rawMinX, x); + rawMaxX = Math.max(rawMaxX, x); + rawMinY = Math.min(rawMinY, y); + rawMaxY = Math.max(rawMaxY, y); + } + console.log('[DEBUG] Raw stitch value ranges:', { rawMinX, rawMaxX, rawMinY, rawMaxY }); + if (!stitches || stitches.length === 0) { throw new Error('Invalid PES file or no stitches found'); } @@ -133,32 +155,35 @@ for block in pattern.get_as_stitchblock(): let minY = Infinity; let maxY = -Infinity; - // Convert to PEN format + // PyStitch returns ABSOLUTE coordinates + // PEN format uses absolute coordinates, shifted left by 3 bits (as per official app line 780) const penStitches: number[] = []; let currentColor = stitches[0]?.[3] ?? 0; // Track current color using stitch color index for (let i = 0; i < stitches.length; i++) { const stitch = stitches[i]; - const x = Math.round(stitch[0]); - const y = Math.round(stitch[1]); + const absX = Math.round(stitch[0]); + const absY = Math.round(stitch[1]); const cmd = stitch[2]; const stitchColor = stitch[3]; // Color index from PyStitch - // Track bounds for non-jump stitches - if ((cmd & MOVE) === 0) { - minX = Math.min(minX, x); - maxX = Math.max(maxX, x); - minY = Math.min(minY, y); - maxY = Math.max(maxY, y); + // Track bounds for non-jump stitches (cmd=0 is STITCH) + if (cmd === 0) { + minX = Math.min(minX, absX); + maxX = Math.max(maxX, absX); + minY = Math.min(minY, absY); + maxY = Math.max(maxY, absY); } - // Encode coordinates with flags in low 3 bits + // Encode absolute coordinates with flags in low 3 bits // Shift coordinates left by 3 bits to make room for flags - let xEncoded = (x << 3) & 0xFFFF; - let yEncoded = (y << 3) & 0xFFFF; + // As per official app line 780: buffer[index64] = (byte) ((int) numArray4[index64 / 4, 0] << 3 & (int) byte.MaxValue); + let xEncoded = (absX << 3) & 0xFFFF; + let yEncoded = (absY << 3) & 0xFFFF; - // Add jump flag if this is a move command - if ((cmd & MOVE) !== 0) { + // Add jump flag if this is a JUMP (1) or TRIM (2) command + // PyStitch constants: STITCH=0, JUMP=1, TRIM=2 + if (cmd === 1 || cmd === 2) { yEncoded |= PEN_FEED_DATA; } @@ -200,6 +225,35 @@ for block in pattern.get_as_stitchblock(): const penData = new Uint8Array(penStitches); + console.log('[DEBUG] PEN data size:', penData.length, 'bytes'); + console.log('[DEBUG] Encoded stitch count:', penData.length / 4); + console.log('[DEBUG] Expected vs Actual:', data.stitch_count, 'vs', penData.length / 4); + console.log('[DEBUG] First 20 bytes (5 stitches):', + Array.from(penData.slice(0, 20)) + .map(b => b.toString(16).padStart(2, '0')) + .join(' ')); + console.log('[DEBUG] Last 20 bytes (5 stitches):', + Array.from(penData.slice(-20)) + .map(b => b.toString(16).padStart(2, '0')) + .join(' ')); + console.log('[DEBUG] Calculated bounds from stitches:', { + minX, + maxX, + minY, + maxY, + }); + + // Check for color change markers and end marker + let colorChangeCount = 0; + let hasEndMarker = false; + for (let i = 0; i < penData.length; i += 4) { + const xLow = penData[i]; + const yLow = penData[i + 2]; + if ((xLow & 0x07) === PEN_COLOR_END) colorChangeCount++; + if ((xLow & 0x07) === PEN_DATA_END) hasEndMarker = true; + } + console.log('[DEBUG] Color changes found:', colorChangeCount, '| Has END marker:', hasEndMarker); + return { stitches, threads,