mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
Fix PES to PEN conversion and protocol implementation
- Use PyStitch raw stitches with proper command flag handling - Import constants from pystitch.EmbConstant (STITCH, JUMP, TRIM, etc.) - Filter COLOR_CHANGE, STOP, and END command-only stitches - Properly encode jump/trim stitches with PEN_FEED_DATA flag - Add pattern centering with moveX/moveY in layout - Fix color change detection and PEN_COLOR_END marking - Add comprehensive debug logging for pattern analysis - Fix machine state helpers for IDLE and MASK_TRACE_COMPLETE states - Update ProgressMonitor UI for proper button visibility - Add error handling for undefined error codes Machine now successfully uploads patterns, completes mask trace, and transitions to sewing mode. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
acdf87b237
commit
e0fadf69da
6 changed files with 356 additions and 89 deletions
|
|
@ -230,15 +230,17 @@ export function ProgressMonitor({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mask trace complete - waiting for confirmation */}
|
{/* Mask trace complete - ready to sew */}
|
||||||
{isMaskTraceComplete && (
|
{isMaskTraceComplete && (
|
||||||
<>
|
<>
|
||||||
<div className="status-message success">
|
<div className="status-message success">
|
||||||
Mask trace complete!
|
Mask trace complete!
|
||||||
</div>
|
</div>
|
||||||
<div className="status-message warning">
|
{canStartSewing(machineStatus) && (
|
||||||
Press button on machine to confirm (or trace again)
|
<button onClick={onStartSewing} className="btn-primary">
|
||||||
</div>
|
Start Sewing
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{canStartMaskTrace(machineStatus) && (
|
{canStartMaskTrace(machineStatus) && (
|
||||||
<button onClick={onStartMaskTrace} className="btn-secondary">
|
<button onClick={onStartMaskTrace} className="btn-secondary">
|
||||||
Trace Again
|
Trace Again
|
||||||
|
|
@ -247,6 +249,20 @@ export function ProgressMonitor({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Pattern uploaded, ready to trace */}
|
||||||
|
{machineStatus === MachineStatus.IDLE && (
|
||||||
|
<>
|
||||||
|
<div className="status-message info">
|
||||||
|
Pattern uploaded successfully
|
||||||
|
</div>
|
||||||
|
{canStartMaskTrace(machineStatus) && (
|
||||||
|
<button onClick={onStartMaskTrace} className="btn-secondary">
|
||||||
|
Start Mask Trace
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Ready to start (pattern uploaded) */}
|
{/* Ready to start (pattern uploaded) */}
|
||||||
{machineStatus === MachineStatus.SEWING_WAIT && (
|
{machineStatus === MachineStatus.SEWING_WAIT && (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -202,9 +202,13 @@ export function useBrotherMachine() {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
const uuid = await service.uploadPattern(penData, (progress) => {
|
const uuid = await service.uploadPattern(
|
||||||
|
penData,
|
||||||
|
(progress) => {
|
||||||
setUploadProgress(progress);
|
setUploadProgress(progress);
|
||||||
});
|
},
|
||||||
|
pesData.bounds,
|
||||||
|
);
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
|
|
||||||
// Cache the pattern with its UUID
|
// Cache the pattern with its UUID
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const Commands = {
|
||||||
MACHINE_INFO: 0x0000,
|
MACHINE_INFO: 0x0000,
|
||||||
MACHINE_STATE: 0x0001,
|
MACHINE_STATE: 0x0001,
|
||||||
SERVICE_COUNT: 0x0100,
|
SERVICE_COUNT: 0x0100,
|
||||||
|
REGULAR_INSPECTION: 0x0103,
|
||||||
PATTERN_UUID_REQUEST: 0x0702,
|
PATTERN_UUID_REQUEST: 0x0702,
|
||||||
MASK_TRACE: 0x0704,
|
MASK_TRACE: 0x0704,
|
||||||
LAYOUT_SEND: 0x0705,
|
LAYOUT_SEND: 0x0705,
|
||||||
|
|
@ -25,13 +26,18 @@ const Commands = {
|
||||||
EMB_UUID_SEND: 0x070a,
|
EMB_UUID_SEND: 0x070a,
|
||||||
RESUME_FLAG_REQUEST: 0x070b,
|
RESUME_FLAG_REQUEST: 0x070b,
|
||||||
RESUME: 0x070c,
|
RESUME: 0x070c,
|
||||||
|
HOOP_AVOIDANCE: 0x070f,
|
||||||
START_SEWING: 0x070e,
|
START_SEWING: 0x070e,
|
||||||
MASK_TRACE_1: 0x0710,
|
MASK_TRACE_1: 0x0710,
|
||||||
EMB_ORG_POINT: 0x0800,
|
EMB_ORG_POINT: 0x0800,
|
||||||
|
FIRM_UPDATE_START: 0x0b00,
|
||||||
|
SET_SETTING_REST: 0x0c00,
|
||||||
|
SET_SETTING_SEND: 0x0c01,
|
||||||
MACHINE_SETTING_INFO: 0x0c02,
|
MACHINE_SETTING_INFO: 0x0c02,
|
||||||
SEND_DATA_INFO: 0x1200,
|
SEND_DATA_INFO: 0x1200,
|
||||||
SEND_DATA: 0x1201,
|
SEND_DATA: 0x1201,
|
||||||
CLEAR_ERROR: 0x1300,
|
CLEAR_ERROR: 0x1300,
|
||||||
|
ERROR_LOG_REPLY: 0x1301,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class BrotherPP1Service {
|
export class BrotherPP1Service {
|
||||||
|
|
@ -132,6 +138,38 @@ export class BrotherPP1Service {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
private async sendCommand(
|
||||||
cmdId: number,
|
cmdId: number,
|
||||||
data: Uint8Array = new Uint8Array(),
|
data: Uint8Array = new Uint8Array(),
|
||||||
|
|
@ -148,30 +186,41 @@ export class BrotherPP1Service {
|
||||||
command[1] = cmdId & 0xff; // Low byte
|
command[1] = cmdId & 0xff; // Low byte
|
||||||
command.set(data, 2);
|
command.set(data, 2);
|
||||||
|
|
||||||
console.log(
|
const hexData = Array.from(command)
|
||||||
"Sending command:",
|
|
||||||
Array.from(command)
|
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join(" "),
|
.join(" ");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[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");
|
// Write command
|
||||||
// Small delay to ensure response is ready
|
await this.writeCharacteristic.writeValueWithResponse(command);
|
||||||
|
|
||||||
|
// Longer delay to allow machine to prepare response
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
console.log("reading response");
|
|
||||||
|
|
||||||
|
// Read response
|
||||||
const responseData = await this.readCharacteristic.readValue();
|
const responseData = await this.readCharacteristic.readValue();
|
||||||
const response = new Uint8Array(responseData.buffer);
|
const response = new Uint8Array(responseData.buffer);
|
||||||
|
|
||||||
console.log(
|
const hexResponse = Array.from(response)
|
||||||
"Received response:",
|
|
||||||
Array.from(response)
|
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join(" "),
|
.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;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
@ -289,7 +338,7 @@ export class BrotherPP1Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendDataChunk(offset: number, data: Uint8Array): Promise<boolean> {
|
async sendDataChunk(offset: number, data: Uint8Array): Promise<void> {
|
||||||
const checksum = data.reduce((sum, byte) => (sum + byte) & 0xff, 0);
|
const checksum = data.reduce((sum, byte) => (sum + byte) & 0xff, 0);
|
||||||
|
|
||||||
const payload = new Uint8Array(4 + data.length + 1);
|
const payload = new Uint8Array(4 + data.length + 1);
|
||||||
|
|
@ -303,10 +352,81 @@ export class BrotherPP1Service {
|
||||||
payload.set(data, 4);
|
payload.set(data, 4);
|
||||||
payload[4 + data.length] = checksum;
|
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
|
private async sendCommandNoResponse(
|
||||||
return response[2] === 0x00;
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pollForTransferComplete(): Promise<void> {
|
||||||
|
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<void> {
|
async sendUUID(uuid: Uint8Array): Promise<void> {
|
||||||
|
|
@ -325,14 +445,19 @@ export class BrotherPP1Service {
|
||||||
rotate: number,
|
rotate: number,
|
||||||
flip: number,
|
flip: number,
|
||||||
frame: number,
|
frame: number,
|
||||||
|
boundLeft: number,
|
||||||
|
boundTop: number,
|
||||||
|
boundRight: number,
|
||||||
|
boundBottom: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const payload = new Uint8Array(12);
|
const payload = new Uint8Array(26);
|
||||||
|
|
||||||
const writeInt16LE = (offset: number, value: number) => {
|
const writeInt16LE = (offset: number, value: number) => {
|
||||||
payload[offset] = value & 0xff;
|
payload[offset] = value & 0xff;
|
||||||
payload[offset + 1] = (value >> 8) & 0xff;
|
payload[offset + 1] = (value >> 8) & 0xff;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Position/transformation parameters (12 bytes)
|
||||||
writeInt16LE(0, moveX);
|
writeInt16LE(0, moveX);
|
||||||
writeInt16LE(2, moveY);
|
writeInt16LE(2, moveY);
|
||||||
writeInt16LE(4, sizeX);
|
writeInt16LE(4, sizeX);
|
||||||
|
|
@ -341,11 +466,41 @@ export class BrotherPP1Service {
|
||||||
payload[10] = flip;
|
payload[10] = flip;
|
||||||
payload[11] = frame;
|
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);
|
await this.sendCommand(Commands.LAYOUT_SEND, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMachineSettings(): Promise<Uint8Array> {
|
||||||
|
return await this.sendCommand(Commands.MACHINE_SETTING_INFO);
|
||||||
|
}
|
||||||
|
|
||||||
async startMaskTrace(): Promise<void> {
|
async startMaskTrace(): Promise<void> {
|
||||||
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);
|
await this.sendCommand(Commands.MASK_TRACE, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,6 +517,7 @@ export class BrotherPP1Service {
|
||||||
async uploadPattern(
|
async uploadPattern(
|
||||||
data: Uint8Array,
|
data: Uint8Array,
|
||||||
onProgress?: (progress: number) => void,
|
onProgress?: (progress: number) => void,
|
||||||
|
bounds?: { minX: number; maxX: number; minY: number; maxY: number },
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
// Calculate checksum
|
// Calculate checksum
|
||||||
const checksum = data.reduce((sum, byte) => sum + byte, 0) & 0xffff;
|
const checksum = data.reduce((sum, byte) => sum + byte, 0) & 0xffff;
|
||||||
|
|
@ -376,34 +532,64 @@ export class BrotherPP1Service {
|
||||||
const chunkSize = 500;
|
const chunkSize = 500;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
||||||
|
// Send all chunks without waiting for responses (official app approach)
|
||||||
while (offset < data.length) {
|
while (offset < data.length) {
|
||||||
const chunk = data.slice(
|
const chunk = data.slice(
|
||||||
offset,
|
offset,
|
||||||
Math.min(offset + chunkSize, data.length),
|
Math.min(offset + chunkSize, data.length),
|
||||||
);
|
);
|
||||||
const isComplete = await this.sendDataChunk(offset, chunk);
|
|
||||||
|
|
||||||
|
await this.sendDataChunk(offset, chunk);
|
||||||
offset += chunk.length;
|
offset += chunk.length;
|
||||||
|
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
onProgress((offset / data.length) * 100);
|
onProgress((offset / data.length) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isComplete) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay between chunks
|
// Wait a bit for machine to finish processing chunks
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
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
|
// Generate random UUID
|
||||||
const uuid = crypto.getRandomValues(new Uint8Array(16));
|
const uuid = crypto.getRandomValues(new Uint8Array(16));
|
||||||
await this.sendUUID(uuid);
|
await this.sendUUID(uuid);
|
||||||
|
|
||||||
// Send default layout (no transformation)
|
|
||||||
await this.sendLayout(0, 0, 0, 0, 0, 0, 0);
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Pattern uploaded successfully with UUID:",
|
"Pattern uploaded successfully with UUID:",
|
||||||
Array.from(uuid)
|
Array.from(uuid)
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,12 @@ const ERROR_MESSAGES: Record<number, string> = {
|
||||||
/**
|
/**
|
||||||
* Get human-readable error message for an error code
|
* 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
|
// 0xDD (221) is the default "no error" value
|
||||||
if (errorCode === SewingMachineError.None) {
|
if (errorCode === SewingMachineError.None) {
|
||||||
return null; // No error to display
|
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
|
* Check if error code represents an actual error condition
|
||||||
*/
|
*/
|
||||||
export function hasError(errorCode: number): boolean {
|
export function hasError(errorCode: number | undefined): boolean {
|
||||||
return errorCode !== SewingMachineError.None;
|
return errorCode !== undefined && errorCode !== null && errorCode !== SewingMachineError.None;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ export function canUploadPattern(status: MachineStatus): boolean {
|
||||||
export function canStartSewing(status: MachineStatus): boolean {
|
export function canStartSewing(status: MachineStatus): boolean {
|
||||||
// Only in specific ready states
|
// Only in specific ready states
|
||||||
return status === MachineStatus.SEWING_WAIT ||
|
return status === MachineStatus.SEWING_WAIT ||
|
||||||
|
status === MachineStatus.MASK_TRACE_COMPLETE ||
|
||||||
status === MachineStatus.PAUSE ||
|
status === MachineStatus.PAUSE ||
|
||||||
status === MachineStatus.STOP ||
|
status === MachineStatus.STOP ||
|
||||||
status === MachineStatus.SEWING_INTERRUPTION;
|
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.
|
* Determines if mask trace can be started in the current state.
|
||||||
*/
|
*/
|
||||||
export function canStartMaskTrace(status: MachineStatus): boolean {
|
export function canStartMaskTrace(status: MachineStatus): boolean {
|
||||||
// Only when ready or after previous trace
|
// Can start mask trace when IDLE (after upload), SEWING_WAIT, or after previous trace
|
||||||
return status === MachineStatus.SEWING_WAIT ||
|
return status === MachineStatus.IDLE ||
|
||||||
|
status === MachineStatus.SEWING_WAIT ||
|
||||||
status === MachineStatus.MASK_TRACE_COMPLETE;
|
status === MachineStatus.MASK_TRACE_COMPLETE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,45 +46,38 @@ export async function convertPesToPen(file: File): Promise<PesPatternData> {
|
||||||
// Read the pattern using PyStitch
|
// Read the pattern using PyStitch
|
||||||
const result = await pyodide.runPythonAsync(`
|
const result = await pyodide.runPythonAsync(`
|
||||||
import pystitch
|
import pystitch
|
||||||
|
from pystitch.EmbConstant import STITCH, JUMP, TRIM, STOP, END, COLOR_CHANGE
|
||||||
|
|
||||||
# Read the PES file
|
# Read the PES file
|
||||||
pattern = pystitch.read('${filename}')
|
pattern = pystitch.read('${filename}')
|
||||||
|
|
||||||
# PyStitch groups stitches by color blocks using get_as_stitchblock
|
# Use the raw stitches list which preserves command flags
|
||||||
# This returns tuples of (thread, stitches_list) for each color block
|
# 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 = []
|
stitches_with_colors = []
|
||||||
block_index = 0
|
current_color = 0
|
||||||
|
|
||||||
# Iterate through stitch blocks
|
for i, stitch in enumerate(pattern.stitches):
|
||||||
# Each block is a tuple containing (thread, stitch_list)
|
x, y, cmd = stitch
|
||||||
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 elem in block:
|
# Check for color change command - increment color but don't add stitch
|
||||||
# Check if this is the thread object (has color or hex_color attributes)
|
if cmd == COLOR_CHANGE:
|
||||||
if hasattr(elem, 'color') or hasattr(elem, 'hex_color'):
|
current_color += 1
|
||||||
thread_obj = elem
|
continue
|
||||||
# Check if this is the stitch list
|
|
||||||
elif isinstance(elem, list) and len(elem) > 0 and isinstance(elem[0], list):
|
|
||||||
stitches_list = elem
|
|
||||||
|
|
||||||
if stitches_list:
|
# Check for stop command - skip it
|
||||||
# Find the index of this thread in the threadlist
|
if cmd == STOP:
|
||||||
thread_index = block_index
|
continue
|
||||||
if thread_obj and hasattr(pattern, 'threadlist'):
|
|
||||||
for i, t in enumerate(pattern.threadlist):
|
|
||||||
if t is thread_obj:
|
|
||||||
thread_index = i
|
|
||||||
break
|
|
||||||
|
|
||||||
for stitch in stitches_list:
|
# Check for standalone END command (no stitch data)
|
||||||
# stitch is [x, y, command]
|
if cmd == END:
|
||||||
stitches_with_colors.append([stitch[0], stitch[1], stitch[2], thread_index])
|
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
|
# Convert to JSON-serializable format
|
||||||
{
|
{
|
||||||
|
|
@ -98,13 +91,16 @@ for block in pattern.get_as_stitchblock():
|
||||||
],
|
],
|
||||||
'thread_count': len(pattern.threadlist),
|
'thread_count': len(pattern.threadlist),
|
||||||
'stitch_count': len(stitches_with_colors),
|
'stitch_count': len(stitches_with_colors),
|
||||||
'block_count': block_index
|
'color_changes': current_color
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Convert Python result to JavaScript
|
// Convert Python result to JavaScript
|
||||||
const data = result.toJs({ dict_converter: Object.fromEntries });
|
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
|
// Clean up virtual file
|
||||||
try {
|
try {
|
||||||
pyodide.FS.unlink(filename);
|
pyodide.FS.unlink(filename);
|
||||||
|
|
@ -117,6 +113,32 @@ for block in pattern.get_as_stitchblock():
|
||||||
Array.from(stitch) as number[]
|
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) {
|
if (!stitches || stitches.length === 0) {
|
||||||
throw new Error('Invalid PES file or no stitches found');
|
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 minY = Infinity;
|
||||||
let maxY = -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[] = [];
|
const penStitches: number[] = [];
|
||||||
let currentColor = stitches[0]?.[3] ?? 0; // Track current color using stitch color index
|
let currentColor = stitches[0]?.[3] ?? 0; // Track current color using stitch color index
|
||||||
|
|
||||||
for (let i = 0; i < stitches.length; i++) {
|
for (let i = 0; i < stitches.length; i++) {
|
||||||
const stitch = stitches[i];
|
const stitch = stitches[i];
|
||||||
const x = Math.round(stitch[0]);
|
const absX = Math.round(stitch[0]);
|
||||||
const y = Math.round(stitch[1]);
|
const absY = Math.round(stitch[1]);
|
||||||
const cmd = stitch[2];
|
const cmd = stitch[2];
|
||||||
const stitchColor = stitch[3]; // Color index from PyStitch
|
const stitchColor = stitch[3]; // Color index from PyStitch
|
||||||
|
|
||||||
// Track bounds for non-jump stitches
|
// Track bounds for non-jump stitches (cmd=0 is STITCH)
|
||||||
if ((cmd & MOVE) === 0) {
|
if (cmd === 0) {
|
||||||
minX = Math.min(minX, x);
|
minX = Math.min(minX, absX);
|
||||||
maxX = Math.max(maxX, x);
|
maxX = Math.max(maxX, absX);
|
||||||
minY = Math.min(minY, y);
|
minY = Math.min(minY, absY);
|
||||||
maxY = Math.max(maxY, y);
|
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
|
// Shift coordinates left by 3 bits to make room for flags
|
||||||
let xEncoded = (x << 3) & 0xFFFF;
|
// As per official app line 780: buffer[index64] = (byte) ((int) numArray4[index64 / 4, 0] << 3 & (int) byte.MaxValue);
|
||||||
let yEncoded = (y << 3) & 0xFFFF;
|
let xEncoded = (absX << 3) & 0xFFFF;
|
||||||
|
let yEncoded = (absY << 3) & 0xFFFF;
|
||||||
|
|
||||||
// Add jump flag if this is a move command
|
// Add jump flag if this is a JUMP (1) or TRIM (2) command
|
||||||
if ((cmd & MOVE) !== 0) {
|
// PyStitch constants: STITCH=0, JUMP=1, TRIM=2
|
||||||
|
if (cmd === 1 || cmd === 2) {
|
||||||
yEncoded |= PEN_FEED_DATA;
|
yEncoded |= PEN_FEED_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,6 +225,35 @@ for block in pattern.get_as_stitchblock():
|
||||||
|
|
||||||
const penData = new Uint8Array(penStitches);
|
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 {
|
return {
|
||||||
stitches,
|
stitches,
|
||||||
threads,
|
threads,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue