mirror of
https://github.com/jhbruhn/respira.git
synced 2026-04-28 01:55:45 +00:00
feature: Add stitch step control for error recovery and manual positioning
Implements automatic stitch rollback on thread errors (upper thread: -6, lower thread: -2, sewing start: -21) and manual step controls to adjust stitch position when machine is paused/stopped/interrupted. Adds UI with step buttons (-100/-10/-1/+1/+10/+100), thread start jump, and current stitch reset. Uses existing NEEDLE_MODE_INSTRUCTIONS (0x0709) BLE command. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f45f26942
commit
7250e0e586
7 changed files with 425 additions and 13 deletions
|
|
@ -17,6 +17,7 @@ interface ProgressActionsProps {
|
||||||
machineStatus: MachineStatus;
|
machineStatus: MachineStatus;
|
||||||
isDeleting: boolean;
|
isDeleting: boolean;
|
||||||
isMaskTraceComplete: boolean;
|
isMaskTraceComplete: boolean;
|
||||||
|
hasSewingProgress: boolean;
|
||||||
onResumeSewing: () => void;
|
onResumeSewing: () => void;
|
||||||
onStartSewing: () => void;
|
onStartSewing: () => void;
|
||||||
onStartMaskTrace: () => void;
|
onStartMaskTrace: () => void;
|
||||||
|
|
@ -26,6 +27,7 @@ export function ProgressActions({
|
||||||
machineStatus,
|
machineStatus,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
isMaskTraceComplete,
|
isMaskTraceComplete,
|
||||||
|
hasSewingProgress,
|
||||||
onResumeSewing,
|
onResumeSewing,
|
||||||
onStartSewing,
|
onStartSewing,
|
||||||
onStartMaskTrace,
|
onStartMaskTrace,
|
||||||
|
|
@ -59,7 +61,7 @@ export function ProgressActions({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Start Mask Trace - secondary action */}
|
{/* Start Mask Trace - secondary action */}
|
||||||
{canStartMaskTrace(machineStatus) && (
|
{canStartMaskTrace(machineStatus, hasSewingProgress) && (
|
||||||
<Button
|
<Button
|
||||||
onClick={onStartMaskTrace}
|
onClick={onStartMaskTrace}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,12 @@ import {
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { canShowStepControl } from "../../utils/machineStateHelpers";
|
||||||
import { ProgressStats } from "./ProgressStats";
|
import { ProgressStats } from "./ProgressStats";
|
||||||
import { ProgressSection } from "./ProgressSection";
|
import { ProgressSection } from "./ProgressSection";
|
||||||
import { ColorBlockList } from "./ColorBlockList";
|
import { ColorBlockList } from "./ColorBlockList";
|
||||||
import { ProgressActions } from "./ProgressActions";
|
import { ProgressActions } from "./ProgressActions";
|
||||||
|
import { StitchStepControl } from "./StitchStepControl";
|
||||||
|
|
||||||
export function ProgressMonitor() {
|
export function ProgressMonitor() {
|
||||||
// Machine store
|
// Machine store
|
||||||
|
|
@ -35,18 +37,28 @@ export function ProgressMonitor() {
|
||||||
patternInfo,
|
patternInfo,
|
||||||
sewingProgress,
|
sewingProgress,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
|
adjustedStitchIndex,
|
||||||
|
lastRolledBackError,
|
||||||
|
pausedStitchIndex,
|
||||||
startMaskTrace,
|
startMaskTrace,
|
||||||
startSewing,
|
startSewing,
|
||||||
resumeSewing,
|
resumeSewing,
|
||||||
|
adjustStitchPosition,
|
||||||
|
setStitchPosition,
|
||||||
} = useMachineStore(
|
} = useMachineStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
machineStatus: state.machineStatus,
|
machineStatus: state.machineStatus,
|
||||||
patternInfo: state.patternInfo,
|
patternInfo: state.patternInfo,
|
||||||
sewingProgress: state.sewingProgress,
|
sewingProgress: state.sewingProgress,
|
||||||
isDeleting: state.isDeleting,
|
isDeleting: state.isDeleting,
|
||||||
|
adjustedStitchIndex: state.adjustedStitchIndex,
|
||||||
|
lastRolledBackError: state.lastRolledBackError,
|
||||||
|
pausedStitchIndex: state.pausedStitchIndex,
|
||||||
startMaskTrace: state.startMaskTrace,
|
startMaskTrace: state.startMaskTrace,
|
||||||
startSewing: state.startSewing,
|
startSewing: state.startSewing,
|
||||||
resumeSewing: state.resumeSewing,
|
resumeSewing: state.resumeSewing,
|
||||||
|
adjustStitchPosition: state.adjustStitchPosition,
|
||||||
|
setStitchPosition: state.setStitchPosition,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -140,11 +152,26 @@ export function ProgressMonitor() {
|
||||||
currentBlockRef={currentBlockRef}
|
currentBlockRef={currentBlockRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Step control for paused/stopped/error states */}
|
||||||
|
{canShowStepControl(machineStatus, currentStitch > 0) && (
|
||||||
|
<StitchStepControl
|
||||||
|
currentStitch={currentStitch}
|
||||||
|
adjustedStitchIndex={adjustedStitchIndex}
|
||||||
|
pausedStitchIndex={pausedStitchIndex}
|
||||||
|
totalStitches={totalStitches}
|
||||||
|
lastRolledBackError={lastRolledBackError}
|
||||||
|
colorBlocks={colorBlocks}
|
||||||
|
onAdjustPosition={adjustStitchPosition}
|
||||||
|
onSetPosition={setStitchPosition}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<ProgressActions
|
<ProgressActions
|
||||||
machineStatus={machineStatus}
|
machineStatus={machineStatus}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
isMaskTraceComplete={isMaskTraceComplete}
|
isMaskTraceComplete={isMaskTraceComplete}
|
||||||
|
hasSewingProgress={currentStitch > 0}
|
||||||
onResumeSewing={resumeSewing}
|
onResumeSewing={resumeSewing}
|
||||||
onStartSewing={startSewing}
|
onStartSewing={startSewing}
|
||||||
onStartMaskTrace={startMaskTrace}
|
onStartMaskTrace={startMaskTrace}
|
||||||
|
|
|
||||||
187
src/components/ProgressMonitor/StitchStepControl.tsx
Normal file
187
src/components/ProgressMonitor/StitchStepControl.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
/**
|
||||||
|
* StitchStepControl Component
|
||||||
|
*
|
||||||
|
* Manual stitch position control shown when machine is paused/stopped/interrupted.
|
||||||
|
* Allows stepping forward/backward by 1, 10, or 100 stitches,
|
||||||
|
* jumping to thread color boundaries, and resetting to current position.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
ChevronDoubleLeftIcon,
|
||||||
|
ChevronDoubleRightIcon,
|
||||||
|
SwatchIcon,
|
||||||
|
ArrowUturnLeftIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { getErrorStitchRollback, getErrorMessage } from "../../utils/errorCodeHelpers";
|
||||||
|
import type { ColorBlock } from "./types";
|
||||||
|
import { findCurrentBlockIndex } from "../../utils/colorBlockHelpers";
|
||||||
|
|
||||||
|
interface StitchStepControlProps {
|
||||||
|
currentStitch: number;
|
||||||
|
adjustedStitchIndex: number | null;
|
||||||
|
pausedStitchIndex: number | null;
|
||||||
|
totalStitches: number;
|
||||||
|
lastRolledBackError: number | null;
|
||||||
|
colorBlocks: ColorBlock[];
|
||||||
|
onAdjustPosition: (offset: number) => void;
|
||||||
|
onSetPosition: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StitchStepControl({
|
||||||
|
currentStitch,
|
||||||
|
adjustedStitchIndex,
|
||||||
|
pausedStitchIndex,
|
||||||
|
totalStitches,
|
||||||
|
lastRolledBackError,
|
||||||
|
colorBlocks,
|
||||||
|
onAdjustPosition,
|
||||||
|
onSetPosition,
|
||||||
|
}: StitchStepControlProps) {
|
||||||
|
const displayStitch = adjustedStitchIndex ?? currentStitch;
|
||||||
|
|
||||||
|
// Find the start of the current thread color block
|
||||||
|
const handleGoToThreadStart = () => {
|
||||||
|
const blockIndex = findCurrentBlockIndex(colorBlocks, displayStitch);
|
||||||
|
if (blockIndex >= 0) {
|
||||||
|
onSetPosition(colorBlocks[blockIndex].startStitch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset to the position when the machine was paused (after auto-rollback, before manual adjustments)
|
||||||
|
const handleGoToPausedStitch = () => {
|
||||||
|
if (pausedStitchIndex !== null) {
|
||||||
|
onSetPosition(pausedStitchIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rollback info text
|
||||||
|
const rollbackAmount = lastRolledBackError
|
||||||
|
? getErrorStitchRollback(lastRolledBackError)
|
||||||
|
: null;
|
||||||
|
const rollbackErrorName = lastRolledBackError
|
||||||
|
? getErrorMessage(lastRolledBackError)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3 bg-gray-200 dark:bg-gray-700/50 p-3 rounded-lg">
|
||||||
|
<div className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
Stitch Position
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current position display */}
|
||||||
|
<div className="text-center mb-2">
|
||||||
|
<span className="text-lg font-bold text-gray-900 dark:text-gray-100 tabular-nums">
|
||||||
|
{displayStitch.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
|
||||||
|
/ {totalStitches.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rollback info */}
|
||||||
|
{rollbackAmount !== null && rollbackErrorName && (
|
||||||
|
<div className="text-xs text-amber-600 dark:text-amber-400 text-center mb-2">
|
||||||
|
Moved back {rollbackAmount} stitches due to {rollbackErrorName.toLowerCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step buttons */}
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onAdjustPosition(-100)}
|
||||||
|
disabled={displayStitch <= 0}
|
||||||
|
aria-label="Back 100 stitches"
|
||||||
|
title="-100"
|
||||||
|
>
|
||||||
|
<ChevronDoubleLeftIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onAdjustPosition(-10)}
|
||||||
|
disabled={displayStitch <= 0}
|
||||||
|
aria-label="Back 10 stitches"
|
||||||
|
title="-10"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="w-4 h-4 -mr-1" />
|
||||||
|
<ChevronLeftIcon className="w-4 h-4 -ml-1" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onAdjustPosition(-1)}
|
||||||
|
disabled={displayStitch <= 0}
|
||||||
|
aria-label="Back 1 stitch"
|
||||||
|
title="-1"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onAdjustPosition(1)}
|
||||||
|
disabled={displayStitch >= totalStitches}
|
||||||
|
aria-label="Forward 1 stitch"
|
||||||
|
title="+1"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onAdjustPosition(10)}
|
||||||
|
disabled={displayStitch >= totalStitches}
|
||||||
|
aria-label="Forward 10 stitches"
|
||||||
|
title="+10"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="w-4 h-4 -mr-1" />
|
||||||
|
<ChevronRightIcon className="w-4 h-4 -ml-1" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onAdjustPosition(100)}
|
||||||
|
disabled={displayStitch >= totalStitches}
|
||||||
|
aria-label="Forward 100 stitches"
|
||||||
|
title="+100"
|
||||||
|
>
|
||||||
|
<ChevronDoubleRightIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation buttons */}
|
||||||
|
<div className="flex items-center justify-center gap-1 mt-2">
|
||||||
|
{colorBlocks.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleGoToThreadStart}
|
||||||
|
title="Go to the beginning of the selected thread color"
|
||||||
|
aria-label="Go to the beginning of the selected thread color"
|
||||||
|
>
|
||||||
|
<SwatchIcon className="w-3.5 h-3.5" />
|
||||||
|
Thread Start
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{pausedStitchIndex !== null && displayStitch !== pausedStitchIndex && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleGoToPausedStitch}
|
||||||
|
title="Go to the current stitch"
|
||||||
|
aria-label="Go to the current stitch"
|
||||||
|
>
|
||||||
|
<ArrowUturnLeftIcon className="w-3.5 h-3.5" />
|
||||||
|
Current Stitch
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -626,6 +626,13 @@ export class BrotherPP1Service {
|
||||||
await this.sendCommand(Commands.MASK_TRACE, payload);
|
await this.sendCommand(Commands.MASK_TRACE, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setStitchIndex(stitchIndex: number): Promise<void> {
|
||||||
|
const payload = new Uint8Array(2);
|
||||||
|
payload[0] = stitchIndex & 0xff; // Low byte
|
||||||
|
payload[1] = (stitchIndex >> 8) & 0xff; // High byte
|
||||||
|
await this.sendCommand(Commands.NEEDLE_MODE_INSTRUCTIONS, payload);
|
||||||
|
}
|
||||||
|
|
||||||
async startSewing(): Promise<void> {
|
async startSewing(): Promise<void> {
|
||||||
await this.sendCommand(Commands.START_SEWING);
|
await this.sendCommand(Commands.START_SEWING);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ import type {
|
||||||
SewingProgress,
|
SewingProgress,
|
||||||
} from "../types/machine";
|
} from "../types/machine";
|
||||||
import { MachineStatus, MachineStatusNames } from "../types/machine";
|
import { MachineStatus, MachineStatusNames } from "../types/machine";
|
||||||
import { SewingMachineError } from "../utils/errorCodeHelpers";
|
import {
|
||||||
|
SewingMachineError,
|
||||||
|
getErrorStitchRollback,
|
||||||
|
} from "../utils/errorCodeHelpers";
|
||||||
|
import { getMachineStateCategory } from "../utils/machineStateHelpers";
|
||||||
import { uuidToString } from "../services/PatternCacheService";
|
import { uuidToString } from "../services/PatternCacheService";
|
||||||
import { createStorageService } from "../platform";
|
import { createStorageService } from "../platform";
|
||||||
import type { IStorageService } from "../platform/interfaces/IStorageService";
|
import type { IStorageService } from "../platform/interfaces/IStorageService";
|
||||||
|
|
@ -41,6 +45,11 @@ interface MachineState {
|
||||||
isCommunicating: boolean;
|
isCommunicating: boolean;
|
||||||
isDeleting: boolean;
|
isDeleting: boolean;
|
||||||
|
|
||||||
|
// Step control state
|
||||||
|
adjustedStitchIndex: number | null;
|
||||||
|
lastRolledBackError: number | null;
|
||||||
|
pausedStitchIndex: number | null; // Position snapshot after pause + auto-rollback, before manual adjustments
|
||||||
|
|
||||||
// Polling control
|
// Polling control
|
||||||
pollIntervalId: NodeJS.Timeout | null;
|
pollIntervalId: NodeJS.Timeout | null;
|
||||||
serviceCountIntervalId: NodeJS.Timeout | null;
|
serviceCountIntervalId: NodeJS.Timeout | null;
|
||||||
|
|
@ -56,6 +65,8 @@ interface MachineState {
|
||||||
startSewing: () => Promise<void>;
|
startSewing: () => Promise<void>;
|
||||||
resumeSewing: () => Promise<void>;
|
resumeSewing: () => Promise<void>;
|
||||||
deletePattern: () => Promise<void>;
|
deletePattern: () => Promise<void>;
|
||||||
|
setStitchPosition: (index: number) => Promise<void>;
|
||||||
|
adjustStitchPosition: (offset: number) => Promise<void>;
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
initialize: () => void;
|
initialize: () => void;
|
||||||
|
|
@ -64,6 +75,7 @@ interface MachineState {
|
||||||
_setupSubscriptions: () => void;
|
_setupSubscriptions: () => void;
|
||||||
_startPolling: () => void;
|
_startPolling: () => void;
|
||||||
_stopPolling: () => void;
|
_stopPolling: () => void;
|
||||||
|
_handleErrorStitchRollback: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMachineStore = create<MachineState>((set, get) => ({
|
export const useMachineStore = create<MachineState>((set, get) => ({
|
||||||
|
|
@ -81,6 +93,9 @@ export const useMachineStore = create<MachineState>((set, get) => ({
|
||||||
isPairingError: false,
|
isPairingError: false,
|
||||||
isCommunicating: false,
|
isCommunicating: false,
|
||||||
isDeleting: false,
|
isDeleting: false,
|
||||||
|
adjustedStitchIndex: null,
|
||||||
|
lastRolledBackError: null,
|
||||||
|
pausedStitchIndex: null,
|
||||||
pollIntervalId: null,
|
pollIntervalId: null,
|
||||||
serviceCountIntervalId: null,
|
serviceCountIntervalId: null,
|
||||||
|
|
||||||
|
|
@ -104,6 +119,14 @@ export const useMachineStore = create<MachineState>((set, get) => ({
|
||||||
machineError: state.error,
|
machineError: state.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch sewing progress so we know if sewing was in progress before reconnect
|
||||||
|
try {
|
||||||
|
const progress = await service.getSewingProgress();
|
||||||
|
set({ sewingProgress: progress });
|
||||||
|
} catch {
|
||||||
|
// Not critical - polling will pick it up
|
||||||
|
}
|
||||||
|
|
||||||
// Check for resume possibility using cache store
|
// Check for resume possibility using cache store
|
||||||
const { useMachineCacheStore } = await import("./useMachineCacheStore");
|
const { useMachineCacheStore } = await import("./useMachineCacheStore");
|
||||||
await useMachineCacheStore.getState().checkResume();
|
await useMachineCacheStore.getState().checkResume();
|
||||||
|
|
@ -237,7 +260,12 @@ export const useMachineStore = create<MachineState>((set, get) => ({
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
set({ error: null });
|
set({
|
||||||
|
error: null,
|
||||||
|
adjustedStitchIndex: null,
|
||||||
|
lastRolledBackError: null,
|
||||||
|
pausedStitchIndex: null,
|
||||||
|
});
|
||||||
await service.startSewing();
|
await service.startSewing();
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -253,7 +281,12 @@ export const useMachineStore = create<MachineState>((set, get) => ({
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
set({ error: null });
|
set({
|
||||||
|
error: null,
|
||||||
|
adjustedStitchIndex: null,
|
||||||
|
lastRolledBackError: null,
|
||||||
|
pausedStitchIndex: null,
|
||||||
|
});
|
||||||
await service.resumeSewing();
|
await service.resumeSewing();
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -304,6 +337,64 @@ export const useMachineStore = create<MachineState>((set, get) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Set stitch position to an absolute index
|
||||||
|
setStitchPosition: async (index: number) => {
|
||||||
|
const { isConnected, service, patternInfo } = get();
|
||||||
|
if (!isConnected) return;
|
||||||
|
|
||||||
|
const totalStitches = patternInfo?.totalStitches || 0;
|
||||||
|
const clamped = Math.max(0, Math.min(index, totalStitches));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.setStitchIndex(clamped);
|
||||||
|
set({ adjustedStitchIndex: clamped });
|
||||||
|
// Refresh progress so UI reflects the new position
|
||||||
|
await get().refreshProgress();
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
error:
|
||||||
|
err instanceof Error ? err.message : "Failed to set stitch position",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Adjust stitch position by a relative offset
|
||||||
|
adjustStitchPosition: async (offset: number) => {
|
||||||
|
const { sewingProgress, adjustedStitchIndex } = get();
|
||||||
|
const currentIndex =
|
||||||
|
adjustedStitchIndex ?? sewingProgress?.currentStitch ?? 0;
|
||||||
|
await get().setStitchPosition(currentIndex + offset);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle automatic stitch rollback for thread errors
|
||||||
|
_handleErrorStitchRollback: async () => {
|
||||||
|
const { machineError, sewingProgress, service, lastRolledBackError } =
|
||||||
|
get();
|
||||||
|
|
||||||
|
const rollback = getErrorStitchRollback(machineError);
|
||||||
|
if (rollback === null) return;
|
||||||
|
if (machineError === lastRolledBackError) return;
|
||||||
|
|
||||||
|
const currentStitch = sewingProgress?.currentStitch ?? 0;
|
||||||
|
const newIndex = Math.max(0, currentStitch - rollback);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[StepControl] Auto-rollback: stitch ${currentStitch} -> ${newIndex} (error 0x${machineError.toString(16)}, rollback ${rollback})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.setStitchIndex(newIndex);
|
||||||
|
set({
|
||||||
|
adjustedStitchIndex: newIndex,
|
||||||
|
lastRolledBackError: machineError,
|
||||||
|
});
|
||||||
|
// Immediately refresh progress so subsequent polls see consistent state
|
||||||
|
await get().refreshProgress();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[StepControl] Failed to rollback stitch position:", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Initialize the store (call once from App component)
|
// Initialize the store (call once from App component)
|
||||||
initialize: () => {
|
initialize: () => {
|
||||||
get()._setupSubscriptions();
|
get()._setupSubscriptions();
|
||||||
|
|
@ -359,7 +450,7 @@ export const useMachineStore = create<MachineState>((set, get) => ({
|
||||||
status === MachineStatus.MASK_TRACING ||
|
status === MachineStatus.MASK_TRACING ||
|
||||||
status === MachineStatus.SEWING_DATA_RECEIVE
|
status === MachineStatus.SEWING_DATA_RECEIVE
|
||||||
) {
|
) {
|
||||||
return 500;
|
return 1000;
|
||||||
} else if (
|
} else if (
|
||||||
status === MachineStatus.COLOR_CHANGE_WAIT ||
|
status === MachineStatus.COLOR_CHANGE_WAIT ||
|
||||||
status === MachineStatus.MASK_TRACE_LOCK_WAIT ||
|
status === MachineStatus.MASK_TRACE_LOCK_WAIT ||
|
||||||
|
|
@ -374,11 +465,55 @@ export const useMachineStore = create<MachineState>((set, get) => ({
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
|
|
||||||
|
const currentState = get();
|
||||||
|
const category = getMachineStateCategory(currentState.machineStatus);
|
||||||
|
|
||||||
// Refresh progress during sewing
|
// Refresh progress during sewing
|
||||||
if (get().machineStatus === MachineStatus.SEWING) {
|
if (currentState.machineStatus === MachineStatus.SEWING) {
|
||||||
await refreshProgress();
|
await refreshProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset step control state when machine is actively sewing
|
||||||
|
if (category === "active") {
|
||||||
|
if (
|
||||||
|
currentState.adjustedStitchIndex !== null ||
|
||||||
|
currentState.lastRolledBackError !== null ||
|
||||||
|
currentState.pausedStitchIndex !== null
|
||||||
|
) {
|
||||||
|
set({
|
||||||
|
adjustedStitchIndex: null,
|
||||||
|
lastRolledBackError: null,
|
||||||
|
pausedStitchIndex: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reset rollback tracking when error clears while still paused
|
||||||
|
else if (
|
||||||
|
currentState.lastRolledBackError !== null &&
|
||||||
|
currentState.machineError === SewingMachineError.None
|
||||||
|
) {
|
||||||
|
set({ lastRolledBackError: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-rollback for thread errors when machine is interrupted or paused mid-sew
|
||||||
|
// Only runs once on entering paused state (when pausedStitchIndex is not yet set)
|
||||||
|
const isInterruptedOrPausedMidSew =
|
||||||
|
category === "interrupted" ||
|
||||||
|
(currentState.machineStatus === MachineStatus.SEWING_WAIT &&
|
||||||
|
(currentState.sewingProgress?.currentStitch ?? 0) > 0);
|
||||||
|
if (isInterruptedOrPausedMidSew && get().pausedStitchIndex === null) {
|
||||||
|
// Refresh progress so rollback has accurate current stitch
|
||||||
|
await get().refreshProgress();
|
||||||
|
await get()._handleErrorStitchRollback();
|
||||||
|
|
||||||
|
// Snapshot the paused position (after rollback, before manual adjustments)
|
||||||
|
const postRollbackStitch =
|
||||||
|
get().adjustedStitchIndex ??
|
||||||
|
get().sewingProgress?.currentStitch ??
|
||||||
|
0;
|
||||||
|
set({ pausedStitchIndex: postRollbackStitch });
|
||||||
|
}
|
||||||
|
|
||||||
// follows the apps logic:
|
// follows the apps logic:
|
||||||
// Check if we have a cached pattern and pattern info needs refreshing
|
// Check if we have a cached pattern and pattern info needs refreshing
|
||||||
const { useMachineCacheStore } = await import("./useMachineCacheStore");
|
const { useMachineCacheStore } = await import("./useMachineCacheStore");
|
||||||
|
|
@ -434,6 +569,12 @@ export const usePatternInfo = () =>
|
||||||
useMachineStore((state) => state.patternInfo);
|
useMachineStore((state) => state.patternInfo);
|
||||||
export const useSewingProgress = () =>
|
export const useSewingProgress = () =>
|
||||||
useMachineStore((state) => state.sewingProgress);
|
useMachineStore((state) => state.sewingProgress);
|
||||||
|
export const useAdjustedStitchIndex = () =>
|
||||||
|
useMachineStore((state) => state.adjustedStitchIndex);
|
||||||
|
export const useLastRolledBackError = () =>
|
||||||
|
useMachineStore((state) => state.lastRolledBackError);
|
||||||
|
export const usePausedStitchIndex = () =>
|
||||||
|
useMachineStore((state) => state.pausedStitchIndex);
|
||||||
// Derived state: pattern is uploaded if machine has pattern info
|
// Derived state: pattern is uploaded if machine has pattern info
|
||||||
export const usePatternUploaded = () =>
|
export const usePatternUploaded = () =>
|
||||||
useMachineStore((state) => state.patternInfo !== null);
|
useMachineStore((state) => state.patternInfo !== null);
|
||||||
|
|
|
||||||
|
|
@ -412,6 +412,24 @@ export function getErrorDetails(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of stitches to roll back for a given error code.
|
||||||
|
* Returns null if the error does not trigger automatic rollback.
|
||||||
|
* Based on Artspira ChangeStitchForError logic.
|
||||||
|
*/
|
||||||
|
export function getErrorStitchRollback(errorCode: number): number | null {
|
||||||
|
switch (errorCode) {
|
||||||
|
case SewingMachineError.UpperThreadError:
|
||||||
|
return 6;
|
||||||
|
case SewingMachineError.LowerThreadError:
|
||||||
|
return 2;
|
||||||
|
case SewingMachineError.UpperThreadSewingStartError:
|
||||||
|
return 21;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export ErrorInfo type for use in other files
|
* Export ErrorInfo type for use in other files
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -107,14 +107,21 @@ 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.
|
||||||
|
* When hasSewingProgress is true, SEWING_WAIT means the machine is paused mid-sew,
|
||||||
|
* not waiting for initial start - mask trace should not be offered.
|
||||||
*/
|
*/
|
||||||
export function canStartMaskTrace(status: MachineStatus): boolean {
|
export function canStartMaskTrace(
|
||||||
// Can start mask trace when IDLE (after upload), SEWING_WAIT, or after previous trace
|
status: MachineStatus,
|
||||||
return (
|
hasSewingProgress = false,
|
||||||
status === MachineStatus.IDLE ||
|
): boolean {
|
||||||
status === MachineStatus.SEWING_WAIT ||
|
if (status === MachineStatus.IDLE || status === MachineStatus.MASK_TRACE_COMPLETE) {
|
||||||
status === MachineStatus.MASK_TRACE_COMPLETE
|
return true;
|
||||||
);
|
}
|
||||||
|
// Only allow mask trace in SEWING_WAIT if sewing hasn't started yet
|
||||||
|
if (status === MachineStatus.SEWING_WAIT && !hasSewingProgress) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -127,6 +134,29 @@ export function canResumeSewing(status: MachineStatus): boolean {
|
||||||
return category === MachineStateCategory.INTERRUPTED;
|
return category === MachineStateCategory.INTERRUPTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the step control UI should be shown.
|
||||||
|
* Allows manual stitch position adjustment when machine is paused/stopped/interrupted,
|
||||||
|
* or in SEWING_WAIT if sewing has already started (currentStitch > 0).
|
||||||
|
*/
|
||||||
|
export function canShowStepControl(
|
||||||
|
status: MachineStatus,
|
||||||
|
hasSewingProgress: boolean,
|
||||||
|
): boolean {
|
||||||
|
if (
|
||||||
|
status === MachineStatus.PAUSE ||
|
||||||
|
status === MachineStatus.STOP ||
|
||||||
|
status === MachineStatus.SEWING_INTERRUPTION
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// SEWING_WAIT is also the paused state; show controls only if sewing already started
|
||||||
|
if (status === MachineStatus.SEWING_WAIT && hasSewingProgress) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if disconnect should show a confirmation dialog.
|
* Determines if disconnect should show a confirmation dialog.
|
||||||
* Confirms if disconnecting during active operation or while waiting.
|
* Confirms if disconnecting during active operation or while waiting.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue