Fix PEN encoding and improve UI layout

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>
This commit is contained in:
Jan-Henrik 2025-12-06 12:56:27 +01:00
parent bf20c2b378
commit d0a8273fee
14 changed files with 1304 additions and 485 deletions

View file

@ -0,0 +1,173 @@
---
name: ui-designer
description: Expert visual designer specializing in creating intuitive, beautiful, and accessible user interfaces. Masters design systems, interaction patterns, and visual hierarchy to craft exceptional user experiences that balance aesthetics with functionality.
tools: Read, Write, Edit, Bash, Glob, Grep
---
You are a senior UI designer with expertise in visual design, interaction design, and design systems. Your focus spans creating beautiful, functional interfaces that delight users while maintaining consistency, accessibility, and brand alignment across all touchpoints.
## Communication Protocol
### Required Initial Step: Design Context Gathering
Always begin by requesting design context from the context-manager. This step is mandatory to understand the existing design landscape and requirements.
Send this context request:
```json
{
"requesting_agent": "ui-designer",
"request_type": "get_design_context",
"payload": {
"query": "Design context needed: brand guidelines, existing design system, component libraries, visual patterns, accessibility requirements, and target user demographics."
}
}
```
## Execution Flow
Follow this structured approach for all UI design tasks:
### 1. Context Discovery
Begin by querying the context-manager to understand the design landscape. This prevents inconsistent designs and ensures brand alignment.
Context areas to explore:
- Brand guidelines and visual identity
- Existing design system components
- Current design patterns in use
- Accessibility requirements
- Performance constraints
Smart questioning approach:
- Leverage context data before asking users
- Focus on specific design decisions
- Validate brand alignment
- Request only critical missing details
### 2. Design Execution
Transform requirements into polished designs while maintaining communication.
Active design includes:
- Creating visual concepts and variations
- Building component systems
- Defining interaction patterns
- Documenting design decisions
- Preparing developer handoff
Status updates during work:
```json
{
"agent": "ui-designer",
"update_type": "progress",
"current_task": "Component design",
"completed_items": ["Visual exploration", "Component structure", "State variations"],
"next_steps": ["Motion design", "Documentation"]
}
```
### 3. Handoff and Documentation
Complete the delivery cycle with comprehensive documentation and specifications.
Final delivery includes:
- Notify context-manager of all design deliverables
- Document component specifications
- Provide implementation guidelines
- Include accessibility annotations
- Share design tokens and assets
Completion message format:
"UI design completed successfully. Delivered comprehensive design system with 47 components, full responsive layouts, and dark mode support. Includes Figma component library, design tokens, and developer handoff documentation. Accessibility validated at WCAG 2.1 AA level."
Design critique process:
- Self-review checklist
- Peer feedback
- Stakeholder review
- User testing
- Iteration cycles
- Final approval
- Version control
- Change documentation
Performance considerations:
- Asset optimization
- Loading strategies
- Animation performance
- Render efficiency
- Memory usage
- Battery impact
- Network requests
- Bundle size
Motion design:
- Animation principles
- Timing functions
- Duration standards
- Sequencing patterns
- Performance budget
- Accessibility options
- Platform conventions
- Implementation specs
Dark mode design:
- Color adaptation
- Contrast adjustment
- Shadow alternatives
- Image treatment
- System integration
- Toggle mechanics
- Transition handling
- Testing matrix
Cross-platform consistency:
- Web standards
- iOS guidelines
- Android patterns
- Desktop conventions
- Responsive behavior
- Native patterns
- Progressive enhancement
- Graceful degradation
Design documentation:
- Component specs
- Interaction notes
- Animation details
- Accessibility requirements
- Implementation guides
- Design rationale
- Update logs
- Migration paths
Quality assurance:
- Design review
- Consistency check
- Accessibility audit
- Performance validation
- Browser testing
- Device verification
- User feedback
- Iteration planning
Deliverables organized by type:
- Design files with component libraries
- Style guide documentation
- Design token exports
- Asset packages
- Prototype links
- Specification documents
- Handoff annotations
- Implementation notes
Integration with other agents:
- Collaborate with ux-researcher on user insights
- Provide specs to frontend-developer
- Work with accessibility-tester on compliance
- Support product-manager on feature design
- Guide backend-developer on data visualization
- Partner with content-marketer on visual content
- Assist qa-expert with visual testing
- Coordinate with performance-engineer on optimization
Always prioritize user needs, maintain design consistency, and ensure accessibility while creating beautiful, functional interfaces that enhance the user experience.

173
UI_IMPROVEMENTS.md Normal file
View file

@ -0,0 +1,173 @@
# UI/UX Improvements for Brother Embroidery Machine Controller
## Overview
This document outlines the UI/UX improvements made to enhance usability for non-technical users.
## Key Improvements
### 1. Workflow Stepper Component
**File:** `src/components/WorkflowStepper.tsx`
A visual progress indicator showing all 7 steps of the embroidery workflow:
1. Connect to Machine
2. Load Pattern
3. Upload Pattern
4. Mask Trace
5. Start Sewing
6. Monitor Progress
7. Complete
**Features:**
- Clear visual indication of current step (highlighted in blue with ring)
- Completed steps marked with green checkmarks
- Future steps shown in gray
- Progress bar connecting all steps
- Step descriptions for context
### 2. Next Step Guide Component
**File:** `src/components/NextStepGuide.tsx`
Context-sensitive guidance that shows users exactly what to do next:
**Features:**
- Clear instruction cards with icons
- Step-by-step bullet points
- Color-coded by urgency:
- Blue: Informational/next action
- Yellow: Waiting for user/machine action
- Green: Success/ready states
- Red: Errors
- Tailored messages for each machine state
- Non-technical language
### 3. Pattern Upload Lock
**Modified:** `src/components/FileUpload.tsx`
Prevents users from accidentally changing the pattern after upload:
**Features:**
- Pattern file selection disabled after successful upload
- Clear notification explaining pattern is locked
- Users must complete or delete current pattern before uploading new one
- Prevents confusion and potential errors
### 4. Simplified Information Display
**Modified Components:**
- `MachineConnection.tsx`: Reduced from 5 details to 2 essential ones
- `FileUpload.tsx`: Added filename, reformatted with better visual hierarchy
- `ProgressMonitor.tsx`: Simplified time display, removed technical coordinates
- All components use consistent card-style layouts with gray backgrounds
**Changes:**
- Removed technical details (MAC address, serial number, raw coordinates)
- Added thousand separators for numbers (e.g., "12,345 stitches")
- Changed time format from "5:30" to "5 min 30 sec" for clarity
- Larger progress percentage display (2xl font)
- Better visual grouping of related information
### 5. Contextual UI Visibility
**Modified:** `src/App.tsx`
Sections now show/hide based on workflow state:
**Visibility Rules:**
- **Workflow Stepper**: Only visible when connected
- **Next Step Guide**: Always visible, content changes based on state
- **Machine Connection**: Always visible
- **Pattern File**: Only visible when connected
- **Sewing Progress**: Only visible when pattern is uploaded
- **Pattern Preview**: Shows placeholder when no pattern loaded
### 6. Enhanced Visual Design
**Changes:**
- New gradient blue header with tagline
- Gray background for better card contrast
- Consistent rounded corners and shadows
- Better spacing and padding
- Color-coded status indicators throughout
- Improved typography hierarchy
### 7. Better Error Handling
**Features:**
- Errors displayed prominently at top of page
- Clear error messages with left border highlighting
- Error guidance in Next Step Guide
- Distinct error states in workflow
## User Experience Flow
### Before Improvements:
1. All panels visible at once
2. No clear indication of what to do next
3. Technical information overwhelming
4. Could change pattern after upload
5. No visual workflow guidance
### After Improvements:
1. Clear step-by-step progression shown at top
2. Next Step Guide tells users exactly what to do
3. Only relevant sections visible for current step
4. Pattern locked after upload (prevents mistakes)
5. Simple, non-technical language throughout
6. Visual feedback at every step
## Technical Implementation
### New Files Created:
- `src/components/WorkflowStepper.tsx`
- `src/components/NextStepGuide.tsx`
### Modified Files:
- `src/App.tsx` - Main layout and state management
- `src/components/FileUpload.tsx` - Added pattern lock
- `src/components/MachineConnection.tsx` - Simplified display
- `src/components/ProgressMonitor.tsx` - Improved readability
- `src/utils/errorCodeHelpers.ts` - Fixed TypeScript compatibility
### State Management:
- Added `patternUploaded` state to track upload status
- Pattern lock prevents re-upload without delete
- Automatic state detection from machine status
- Proper cleanup on disconnect/delete
## Design Principles Applied
1. **Progressive Disclosure**: Show only what's needed for current step
2. **Clarity Over Completeness**: Hide technical details, show user-friendly info
3. **Visual Hierarchy**: Use size, color, and spacing to guide attention
4. **Feedback**: Always show current state and next action
5. **Error Prevention**: Lock pattern after upload, confirm destructive actions
6. **Consistency**: Unified visual language across all components
## Accessibility Considerations
- Clear visual indicators with icons
- Color not the only differentiator (icons + text)
- Large touch targets for buttons
- Readable font sizes
- Semantic HTML structure
- Clear labels and descriptions
## Testing Recommendations
1. Test complete workflow from connect to complete
2. Verify pattern cannot be changed after upload
3. Check all machine states show correct guidance
4. Test error scenarios display properly
5. Verify responsiveness on different screen sizes
6. Test with actual embroidery machine if possible
## Future Enhancement Opportunities
1. Add estimated time remaining during sewing
2. Add pattern preview thumbnails in stepper
3. Add sound notifications for state changes
4. Add pattern history/favorites
5. Add tutorial mode for first-time users
6. Add keyboard shortcuts for power users
7. Add offline mode indicators
8. Add pattern size validation warnings

View file

@ -4,8 +4,12 @@ import { MachineConnection } from './components/MachineConnection';
import { FileUpload } from './components/FileUpload'; import { FileUpload } from './components/FileUpload';
import { PatternCanvas } from './components/PatternCanvas'; import { PatternCanvas } from './components/PatternCanvas';
import { ProgressMonitor } from './components/ProgressMonitor'; import { ProgressMonitor } from './components/ProgressMonitor';
import { WorkflowStepper } from './components/WorkflowStepper';
import { NextStepGuide } from './components/NextStepGuide';
import type { PesPatternData } from './utils/pystitchConverter'; import type { PesPatternData } from './utils/pystitchConverter';
import { pyodideLoader } from './utils/pyodideLoader'; import { pyodideLoader } from './utils/pyodideLoader';
import { MachineStatus } from './types/machine';
import { hasError } from './utils/errorCodeHelpers';
import './App.css'; import './App.css';
function App() { function App() {
@ -14,6 +18,7 @@ function App() {
const [pyodideReady, setPyodideReady] = useState(false); const [pyodideReady, setPyodideReady] = useState(false);
const [pyodideError, setPyodideError] = useState<string | null>(null); const [pyodideError, setPyodideError] = useState<string | null>(null);
const [patternOffset, setPatternOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); const [patternOffset, setPatternOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const [patternUploaded, setPatternUploaded] = useState(false);
// Initialize Pyodide on mount // Initialize Pyodide on mount
useEffect(() => { useEffect(() => {
@ -45,6 +50,7 @@ function App() {
setPesData(data); setPesData(data);
// Reset pattern offset when new pattern is loaded // Reset pattern offset when new pattern is loaded
setPatternOffset({ x: 0, y: 0 }); setPatternOffset({ x: 0, y: 0 });
setPatternUploaded(false);
}, []); }, []);
const handlePatternOffsetChange = useCallback((offsetX: number, offsetY: number) => { const handlePatternOffsetChange = useCallback((offsetX: number, offsetY: number) => {
@ -52,23 +58,86 @@ function App() {
console.log('[App] Pattern offset changed:', { x: offsetX, y: offsetY }); console.log('[App] Pattern offset changed:', { x: offsetX, y: offsetY });
}, []); }, []);
const handleUpload = useCallback(async (penData: Uint8Array, pesData: PesPatternData, fileName: string, patternOffset?: { x: number; y: number }) => {
await machine.uploadPattern(penData, pesData, fileName, patternOffset);
setPatternUploaded(true);
}, [machine]);
const handleDeletePattern = useCallback(async () => {
await machine.deletePattern();
setPatternUploaded(false);
setPesData(null);
}, [machine]);
// Track pattern uploaded state based on machine status
useEffect(() => {
if (!machine.isConnected) {
setPatternUploaded(false);
return;
}
// Pattern is uploaded if machine has pattern info
if (machine.patternInfo !== null) {
setPatternUploaded(true);
} else {
// No pattern info means no pattern on machine
setPatternUploaded(false);
}
}, [machine.machineStatus, machine.patternInfo, machine.isConnected]);
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col bg-gray-50">
<header className="bg-white px-8 py-6 border-b border-gray-300 shadow-md"> <header className="bg-gradient-to-r from-blue-600 to-blue-700 px-8 py-3 shadow-lg">
<h1 className="text-3xl font-semibold mb-2">Brother Embroidery Machine Controller</h1> <div className="max-w-[1600px] mx-auto flex items-center gap-8">
{machine.error && ( <h1 className="text-xl font-bold text-white whitespace-nowrap">SKiTCH Controller</h1>
<div className="bg-red-100 text-red-900 px-4 py-3 rounded border border-red-200 mt-4">{machine.error}</div>
)} {/* Workflow Stepper - Integrated in header when connected */}
{pyodideError && ( {machine.isConnected && (
<div className="bg-red-100 text-red-900 px-4 py-3 rounded border border-red-200 mt-4">Python Error: {pyodideError}</div> <div className="flex-1">
)} <WorkflowStepper
{!pyodideReady && !pyodideError && ( machineStatus={machine.machineStatus}
<div className="bg-blue-100 text-blue-900 px-4 py-3 rounded border border-blue-200 mt-4">Initializing Python environment...</div> isConnected={machine.isConnected}
hasPattern={pesData !== null}
patternUploaded={patternUploaded}
/>
</div>
)} )}
</div>
</header> </header>
<div className="flex-1 grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-6 p-6 max-w-[1600px] w-full mx-auto"> <div className="flex-1 p-6 max-w-[1600px] w-full mx-auto">
{/* Global errors */}
{machine.error && (
<div className="bg-red-100 text-red-900 px-6 py-4 rounded-lg border-l-4 border-red-600 mb-6 shadow-md">
<strong>Error:</strong> {machine.error}
</div>
)}
{pyodideError && (
<div className="bg-red-100 text-red-900 px-6 py-4 rounded-lg border-l-4 border-red-600 mb-6 shadow-md">
<strong>Python Error:</strong> {pyodideError}
</div>
)}
{!pyodideReady && !pyodideError && (
<div className="bg-blue-100 text-blue-900 px-6 py-4 rounded-lg border-l-4 border-blue-600 mb-6 shadow-md">
Initializing Python environment...
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-6">
{/* Left Column - Controls */}
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Next Step Guide - Always visible */}
<NextStepGuide
machineStatus={machine.machineStatus}
isConnected={machine.isConnected}
hasPattern={pesData !== null}
patternUploaded={patternUploaded}
hasError={hasError(machine.machineError)}
errorMessage={machine.error || undefined}
errorCode={machine.machineError}
/>
{/* Machine Connection - Always visible */}
<MachineConnection <MachineConnection
isConnected={machine.isConnected} isConnected={machine.isConnected}
machineInfo={machine.machineInfo} machineInfo={machine.machineInfo}
@ -76,23 +145,56 @@ function App() {
machineStatusName={machine.machineStatusName} machineStatusName={machine.machineStatusName}
machineError={machine.machineError} machineError={machine.machineError}
isPolling={machine.isPolling} isPolling={machine.isPolling}
resumeAvailable={machine.resumeAvailable}
resumeFileName={machine.resumeFileName}
onConnect={machine.connect} onConnect={machine.connect}
onDisconnect={machine.disconnect} onDisconnect={machine.disconnect}
onRefresh={machine.refreshStatus} onRefresh={machine.refreshStatus}
/> />
{/* Pattern File - Only show when connected */}
{machine.isConnected && (
<FileUpload <FileUpload
isConnected={machine.isConnected} isConnected={machine.isConnected}
machineStatus={machine.machineStatus} machineStatus={machine.machineStatus}
uploadProgress={machine.uploadProgress} uploadProgress={machine.uploadProgress}
onPatternLoaded={handlePatternLoaded} onPatternLoaded={handlePatternLoaded}
onUpload={machine.uploadPattern} onUpload={handleUpload}
pyodideReady={pyodideReady} pyodideReady={pyodideReady}
patternOffset={patternOffset} patternOffset={patternOffset}
patternUploaded={patternUploaded}
resumeAvailable={machine.resumeAvailable}
resumeFileName={machine.resumeFileName}
pesData={pesData}
/> />
)}
</div>
{/* Right Column - Pattern Preview */}
<div className="flex flex-col gap-6">
{pesData ? (
<PatternCanvas
pesData={pesData}
sewingProgress={machine.sewingProgress}
machineInfo={machine.machineInfo}
initialPatternOffset={patternOffset}
onPatternOffsetChange={handlePatternOffsetChange}
/>
) : (
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern Preview</h2>
<div className="flex items-center justify-center h-[600px] bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
<div className="text-center">
<svg className="w-24 h-24 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<p className="text-gray-600 text-lg mb-2">No Pattern Loaded</p>
<p className="text-gray-500 text-sm">Connect to your machine and choose a PES file to begin</p>
</div>
</div>
</div>
)}
{/* Progress Monitor - Wide section below pattern preview */}
{machine.isConnected && patternUploaded && (
<ProgressMonitor <ProgressMonitor
machineStatus={machine.machineStatus} machineStatus={machine.machineStatus}
patternInfo={machine.patternInfo} patternInfo={machine.patternInfo}
@ -101,18 +203,10 @@ function App() {
onStartMaskTrace={machine.startMaskTrace} onStartMaskTrace={machine.startMaskTrace}
onStartSewing={machine.startSewing} onStartSewing={machine.startSewing}
onResumeSewing={machine.resumeSewing} onResumeSewing={machine.resumeSewing}
onDeletePattern={machine.deletePattern} onDeletePattern={handleDeletePattern}
/> />
)}
</div> </div>
<div className="flex flex-col">
<PatternCanvas
pesData={pesData}
sewingProgress={machine.sewingProgress}
machineInfo={machine.machineInfo}
initialPatternOffset={patternOffset}
onPatternOffsetChange={handlePatternOffsetChange}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -55,14 +55,14 @@ export function ConfirmDialog({
<div className="p-4 px-6 flex gap-3 justify-end border-t border-gray-300"> <div className="p-4 px-6 flex gap-3 justify-end border-t border-gray-300">
<button <button
onClick={onCancel} onClick={onCancel}
className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]" className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3] cursor-pointer"
autoFocus autoFocus
> >
{cancelText} {cancelText}
</button> </button>
<button <button
onClick={onConfirm} onClick={onConfirm}
className={variant === 'danger' ? 'px-6 py-3 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]' : 'px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]'} className={variant === 'danger' ? 'px-6 py-3 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3] cursor-pointer' : 'px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3] cursor-pointer'}
> >
{confirmText} {confirmText}
</button> </button>

View file

@ -11,6 +11,10 @@ interface FileUploadProps {
onUpload: (penData: Uint8Array, pesData: PesPatternData, fileName: string, patternOffset?: { x: number; y: number }) => void; onUpload: (penData: Uint8Array, pesData: PesPatternData, fileName: string, patternOffset?: { x: number; y: number }) => void;
pyodideReady: boolean; pyodideReady: boolean;
patternOffset: { x: number; y: number }; patternOffset: { x: number; y: number };
patternUploaded: boolean;
resumeAvailable: boolean;
resumeFileName: string | null;
pesData: PesPatternData | null;
} }
export function FileUpload({ export function FileUpload({
@ -21,9 +25,17 @@ export function FileUpload({
onUpload, onUpload,
pyodideReady, pyodideReady,
patternOffset, patternOffset,
patternUploaded,
resumeAvailable,
resumeFileName,
pesData: pesDataProp,
}: FileUploadProps) { }: FileUploadProps) {
const [pesData, setPesData] = useState<PesPatternData | null>(null); const [localPesData, setLocalPesData] = useState<PesPatternData | null>(null);
const [fileName, setFileName] = useState<string>(''); const [fileName, setFileName] = useState<string>('');
// Use prop pesData if available (from cached pattern), otherwise use local state
const pesData = pesDataProp || localPesData;
const displayFileName = resumeFileName || fileName;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleFileChange = useCallback( const handleFileChange = useCallback(
@ -39,7 +51,7 @@ export function FileUpload({
setIsLoading(true); setIsLoading(true);
try { try {
const data = await convertPesToPen(file); const data = await convertPesToPen(file);
setPesData(data); setLocalPesData(data);
setFileName(file.name); setFileName(file.name);
onPatternLoaded(data); onPatternLoaded(data);
} catch (err) { } catch (err) {
@ -56,52 +68,68 @@ export function FileUpload({
); );
const handleUpload = useCallback(() => { const handleUpload = useCallback(() => {
if (pesData && fileName) { if (pesData && displayFileName) {
onUpload(pesData.penData, pesData, fileName, patternOffset); onUpload(pesData.penData, pesData, displayFileName, patternOffset);
} }
}, [pesData, fileName, onUpload, patternOffset]); }, [pesData, displayFileName, onUpload, patternOffset]);
return ( return (
<div className="bg-white p-6 rounded-lg shadow-md"> <div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern File</h2> <h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern File</h2>
<div> <div>
{resumeAvailable && resumeFileName && (
<div className="bg-green-50 border border-green-200 px-4 py-3 rounded mb-4">
<p className="text-sm text-green-800">
<strong>Loaded cached pattern:</strong> "{resumeFileName}"
</p>
</div>
)}
{patternUploaded && (
<div className="bg-blue-50 border border-blue-200 px-4 py-3 rounded mb-4">
<p className="text-sm text-blue-800">
<strong>Pattern uploaded successfully!</strong> The pattern is now locked and cannot be changed.
To upload a different pattern, you must first complete or delete the current one.
</p>
</div>
)}
<input <input
type="file" type="file"
accept=".pes" accept=".pes"
onChange={handleFileChange} onChange={handleFileChange}
id="file-input" id="file-input"
className="hidden" className="hidden"
disabled={!pyodideReady || isLoading} disabled={!pyodideReady || isLoading || patternUploaded}
/> />
<label htmlFor="file-input" className={`inline-block px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm cursor-pointer transition-all ${!pyodideReady || isLoading ? 'opacity-50 cursor-not-allowed grayscale-[0.3]' : 'hover:bg-gray-700 hover:shadow-md'}`}> <label htmlFor="file-input" className={`inline-block px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm transition-all ${!pyodideReady || isLoading || patternUploaded ? 'opacity-50 cursor-not-allowed grayscale-[0.3]' : 'cursor-pointer hover:bg-gray-700 hover:shadow-md'}`}>
{isLoading ? 'Loading...' : !pyodideReady ? 'Initializing...' : 'Choose PES File'} {isLoading ? 'Loading...' : !pyodideReady ? 'Initializing...' : patternUploaded ? 'Pattern Locked' : 'Choose PES File'}
</label> </label>
{pesData && ( {pesData && (
<div className="mt-4"> <div className="mt-4">
<h3 className="text-base font-semibold my-4">Pattern Details</h3> <h3 className="text-base font-semibold my-4">Pattern Information</h3>
<div className="flex justify-between py-2 border-b border-gray-300"> <div className="bg-gray-50 p-4 rounded-lg space-y-3">
<span className="font-medium text-gray-600">Total Stitches:</span> <div className="flex justify-between">
<span className="font-semibold">{pesData.stitchCount}</span> <span className="font-medium text-gray-700">File Name:</span>
<span className="font-semibold text-gray-900">{displayFileName}</span>
</div> </div>
<div className="flex justify-between py-2 border-b border-gray-300"> <div className="flex justify-between">
<span className="font-medium text-gray-600">Colors:</span> <span className="font-medium text-gray-700">Pattern Size:</span>
<span className="font-semibold">{pesData.colorCount}</span> <span className="font-semibold text-gray-900">
</div>
<div className="flex justify-between py-2 border-b border-gray-300">
<span className="font-medium text-gray-600">Size:</span>
<span className="font-semibold">
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '} {((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</span> </span>
</div> </div>
<div className="flex justify-between py-2"> <div className="flex justify-between">
<span className="font-medium text-gray-600">Bounds:</span> <span className="font-medium text-gray-700">Thread Colors:</span>
<span className="font-semibold"> <span className="font-semibold text-gray-900">{pesData.colorCount}</span>
({pesData.bounds.minX}, {pesData.bounds.minY}) to ( </div>
{pesData.bounds.maxX}, {pesData.bounds.maxY}) <div className="flex justify-between">
</span> <span className="font-medium text-gray-700">Total Stitches:</span>
<span className="font-semibold text-gray-900">{pesData.stitchCount.toLocaleString()}</span>
</div>
</div> </div>
</div> </div>
)} )}
@ -110,7 +138,7 @@ export function FileUpload({
<button <button
onClick={handleUpload} onClick={handleUpload}
disabled={!isConnected || uploadProgress > 0} disabled={!isConnected || uploadProgress > 0}
className="mt-4 px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]" className="mt-4 px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3] cursor-pointer"
> >
{uploadProgress > 0 {uploadProgress > 0
? `Uploading... ${uploadProgress.toFixed(0)}%` ? `Uploading... ${uploadProgress.toFixed(0)}%`

View file

@ -1,9 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { InformationCircleIcon } from '@heroicons/react/24/solid';
import type { MachineInfo } from '../types/machine'; import type { MachineInfo } from '../types/machine';
import { MachineStatus } from '../types/machine'; import { MachineStatus } from '../types/machine';
import { ConfirmDialog } from './ConfirmDialog'; import { ConfirmDialog } from './ConfirmDialog';
import { shouldConfirmDisconnect, getStateVisualInfo } from '../utils/machineStateHelpers'; import { shouldConfirmDisconnect, getStateVisualInfo } from '../utils/machineStateHelpers';
import { hasError, getErrorMessage } from '../utils/errorCodeHelpers'; import { hasError, getErrorDetails } from '../utils/errorCodeHelpers';
interface MachineConnectionProps { interface MachineConnectionProps {
isConnected: boolean; isConnected: boolean;
@ -12,8 +13,6 @@ interface MachineConnectionProps {
machineStatusName: string; machineStatusName: string;
machineError: number; machineError: number;
isPolling: boolean; isPolling: boolean;
resumeAvailable: boolean;
resumeFileName: string | null;
onConnect: () => void; onConnect: () => void;
onDisconnect: () => void; onDisconnect: () => void;
onRefresh: () => void; onRefresh: () => void;
@ -26,11 +25,8 @@ export function MachineConnection({
machineStatusName, machineStatusName,
machineError, machineError,
isPolling, isPolling,
resumeAvailable,
resumeFileName,
onConnect, onConnect,
onDisconnect, onDisconnect,
onRefresh,
}: MachineConnectionProps) { }: MachineConnectionProps) {
const [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false); const [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false);
@ -62,70 +58,87 @@ export function MachineConnection({
danger: 'bg-red-100 text-red-800 border-red-200', danger: 'bg-red-100 text-red-800 border-red-200',
}; };
// Only show error info when connected AND there's an actual error
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
return ( return (
<div className="bg-white p-6 rounded-lg shadow-md"> <div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Machine Connection</h2> <div className="flex items-center justify-between mb-4 pb-2 border-b-2 border-gray-300">
<h2 className="text-xl font-semibold">Machine Connection</h2>
{isConnected && isPolling && (
<span className="flex items-center gap-2 text-xs text-gray-500">
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></span>
Auto-refreshing
</span>
)}
</div>
{!isConnected ? ( {!isConnected ? (
<div className="flex gap-3 mt-4 flex-wrap"> <div className="flex gap-3 mt-4 flex-wrap">
<button onClick={onConnect} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]"> <button onClick={onConnect} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3] cursor-pointer">
Connect to Machine Connect to Machine
</button> </button>
</div> </div>
) : ( ) : (
<div> <div>
<div className="flex items-center gap-4 mb-4 p-3 bg-gray-100 rounded"> {/* Error/Info Display */}
<span className={`flex items-center gap-2 px-4 py-2 rounded font-semibold text-sm border ${statusBadgeColors[stateVisual.color as keyof typeof statusBadgeColors] || statusBadgeColors.info}`}> {errorInfo && (
<span className="text-lg leading-none">{stateVisual.icon}</span> errorInfo.isInformational ? (
<span className="uppercase tracking-wide">{machineStatusName}</span> // Informational messages (like initialization steps)
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start gap-2">
<InformationCircleIcon className="w-5 h-5 text-blue-600 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-semibold text-blue-900 text-sm">{errorInfo.title}</div>
</div>
</div>
</div>
) : (
// Regular errors shown as errors
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-2">
<span className="text-red-600 text-lg flex-shrink-0"></span>
<div className="flex-1 min-w-0">
<div className="font-semibold text-red-900 text-sm mb-1">{errorInfo.title}</div>
<div className="text-xs text-red-700 font-mono">
Error Code: 0x{machineError.toString(16).toUpperCase().padStart(2, '0')}
</div>
</div>
</div>
</div>
)
)}
{/* Machine Status */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-600">Status:</span>
<span className={`flex items-center gap-2 px-3 py-1.5 rounded-lg font-semibold text-sm ${statusBadgeColors[stateVisual.color as keyof typeof statusBadgeColors] || statusBadgeColors.info}`}>
<span className="text-base leading-none">{stateVisual.icon}</span>
<span>{machineStatusName}</span>
</span> </span>
{isPolling && ( </div>
<span className="text-blue-600 text-xs animate-pulse" title="Polling machine status"></span>
)}
{hasError(machineError) && (
<span className="bg-red-100 text-red-900 px-4 py-2 rounded font-semibold text-sm">{getErrorMessage(machineError)}</span>
)}
</div> </div>
{/* Machine Info */}
{machineInfo && ( {machineInfo && (
<div> <div className="bg-gray-50 p-4 rounded-lg space-y-2 mb-4">
<div className="flex justify-between py-2 border-b border-gray-300"> <div className="flex justify-between text-sm">
<span className="font-medium text-gray-600">Model:</span> <span className="font-medium text-gray-600">Model:</span>
<span className="font-semibold">{machineInfo.modelNumber}</span> <span className="font-semibold text-gray-900">{machineInfo.modelNumber}</span>
</div> </div>
<div className="flex justify-between py-2 border-b border-gray-300"> <div className="flex justify-between text-sm">
<span className="font-medium text-gray-600">Serial:</span>
<span className="font-semibold">{machineInfo.serialNumber}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-300">
<span className="font-medium text-gray-600">Software:</span>
<span className="font-semibold">{machineInfo.softwareVersion}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-300">
<span className="font-medium text-gray-600">Max Area:</span> <span className="font-medium text-gray-600">Max Area:</span>
<span className="font-semibold"> <span className="font-semibold text-gray-900">
{(machineInfo.maxWidth / 10).toFixed(1)} x{' '} {(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
{(machineInfo.maxHeight / 10).toFixed(1)} mm
</span> </span>
</div> </div>
<div className="flex justify-between py-2">
<span className="font-medium text-gray-600">MAC:</span>
<span className="font-semibold">{machineInfo.macAddress}</span>
</div>
</div> </div>
)} )}
{resumeAvailable && resumeFileName && ( {/* Disconnect Button */}
<div className="bg-green-100 text-green-800 px-4 py-3 rounded border border-green-200 my-4 font-medium"> <div className="flex gap-3 mt-4">
Loaded cached pattern: "{resumeFileName}" <button onClick={handleDisconnectClick} className="w-full px-6 py-3 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md cursor-pointer">
</div>
)}
<div className="flex gap-3 mt-4 flex-wrap">
<button onClick={onRefresh} className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Refresh Status
</button>
<button onClick={handleDisconnectClick} className="px-6 py-3 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Disconnect Disconnect
</button> </button>
</div> </div>

View file

@ -0,0 +1,304 @@
import { InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/solid';
import { MachineStatus } from '../types/machine';
import { getErrorDetails } from '../utils/errorCodeHelpers';
interface NextStepGuideProps {
machineStatus: MachineStatus;
isConnected: boolean;
hasPattern: boolean;
patternUploaded: boolean;
hasError: boolean;
errorMessage?: string;
errorCode?: number;
}
export function NextStepGuide({
machineStatus,
isConnected,
hasPattern,
patternUploaded,
hasError,
errorMessage,
errorCode
}: NextStepGuideProps) {
// Don't show if there's an error - show detailed error guidance instead
if (hasError) {
const errorDetails = getErrorDetails(errorCode);
// Check if this is informational (like initialization steps) vs a real error
if (errorDetails?.isInformational) {
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">
{errorDetails.title}
</h3>
<p className="text-blue-800 mb-3">
{errorDetails.description}
</p>
{errorDetails.solutions && errorDetails.solutions.length > 0 && (
<>
<h4 className="font-semibold text-blue-900 mb-2">Steps:</h4>
<ol className="list-decimal list-inside text-sm text-blue-700 space-y-2">
{errorDetails.solutions.map((solution, index) => (
<li key={index} className="pl-2">{solution}</li>
))}
</ol>
</>
)}
</div>
</div>
</div>
);
}
// Regular error display for actual errors
return (
<div className="bg-red-50 border-l-4 border-red-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<ExclamationTriangleIcon className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-red-900 mb-2">
{errorDetails?.title || 'Error Occurred'}
</h3>
<p className="text-red-800 mb-3">
{errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.'}
</p>
{errorDetails?.solutions && errorDetails.solutions.length > 0 && (
<>
<h4 className="font-semibold text-red-900 mb-2">How to Fix:</h4>
<ol className="list-decimal list-inside text-sm text-red-700 space-y-2">
{errorDetails.solutions.map((solution, index) => (
<li key={index} className="pl-2">{solution}</li>
))}
</ol>
</>
)}
{errorCode !== undefined && (
<p className="text-xs text-red-600 mt-4 font-mono">
Error Code: 0x{errorCode.toString(16).toUpperCase().padStart(2, '0')}
</p>
)}
</div>
</div>
</div>
);
}
// Determine what to show based on current state
if (!isConnected) {
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 1: Connect to Machine</h3>
<p className="text-blue-800 mb-3">To get started, connect to your Brother embroidery machine via Bluetooth.</p>
<ul className="list-disc list-inside text-sm text-blue-700 space-y-1">
<li>Make sure your machine is powered on</li>
<li>Enable Bluetooth on your machine</li>
<li>Click the "Connect to Machine" button below</li>
</ul>
</div>
</div>
</div>
);
}
if (!hasPattern) {
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 2: Load Your Pattern</h3>
<p className="text-blue-800 mb-3">Choose a PES embroidery file from your computer to preview and upload.</p>
<ul className="list-disc list-inside text-sm text-blue-700 space-y-1">
<li>Click "Choose PES File" in the Pattern File section</li>
<li>Select your embroidery design (.pes file)</li>
<li>Review the pattern preview on the right</li>
<li>You can drag the pattern to adjust its position</li>
</ul>
</div>
</div>
</div>
);
}
if (!patternUploaded) {
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 3: Upload Pattern to Machine</h3>
<p className="text-blue-800 mb-3">Send your pattern to the embroidery machine to prepare for sewing.</p>
<ul className="list-disc list-inside text-sm text-blue-700 space-y-1">
<li>Review the pattern preview to ensure it's positioned correctly</li>
<li>Check the pattern size matches your hoop</li>
<li>Click "Upload to Machine" when ready</li>
<li>Wait for the upload to complete (this may take a minute)</li>
</ul>
</div>
</div>
</div>
);
}
// Pattern is uploaded, guide based on machine status
switch (machineStatus) {
case MachineStatus.IDLE:
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 4: Start Mask Trace</h3>
<p className="text-blue-800 mb-3">The mask trace helps the machine understand the pattern boundaries.</p>
<ul className="list-disc list-inside text-sm text-blue-700 space-y-1">
<li>Click "Start Mask Trace" button in the Sewing Progress section</li>
<li>The machine will trace the pattern outline</li>
<li>This ensures the hoop is positioned correctly</li>
</ul>
</div>
</div>
</div>
);
case MachineStatus.MASK_TRACE_LOCK_WAIT:
return (
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Machine Action Required</h3>
<p className="text-yellow-800 mb-3">The machine is ready to trace the pattern outline.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 space-y-1">
<li><strong>Press the button on your machine</strong> to confirm and start the mask trace</li>
<li>Ensure the hoop is properly attached</li>
<li>Make sure the needle area is clear</li>
</ul>
</div>
</div>
</div>
);
case MachineStatus.MASK_TRACING:
return (
<div className="bg-cyan-50 border-l-4 border-cyan-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-cyan-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-cyan-900 mb-2">Mask Trace In Progress</h3>
<p className="text-cyan-800 mb-3">The machine is tracing the pattern boundary. Please wait...</p>
<ul className="list-disc list-inside text-sm text-cyan-700 space-y-1">
<li>Watch the machine trace the outline</li>
<li>Verify the pattern fits within your hoop</li>
<li>Do not interrupt the machine</li>
</ul>
</div>
</div>
</div>
);
case MachineStatus.MASK_TRACE_COMPLETE:
case MachineStatus.SEWING_WAIT:
return (
<div className="bg-green-50 border-l-4 border-green-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-green-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-green-900 mb-2">Step 5: Ready to Sew!</h3>
<p className="text-green-800 mb-3">The machine is ready to begin embroidering your pattern.</p>
<ul className="list-disc list-inside text-sm text-green-700 space-y-1">
<li>Verify your thread colors are correct</li>
<li>Ensure the fabric is properly hooped</li>
<li>Click "Start Sewing" when ready</li>
</ul>
</div>
</div>
</div>
);
case MachineStatus.SEWING:
return (
<div className="bg-cyan-50 border-l-4 border-cyan-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-cyan-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-cyan-900 mb-2">Step 6: Sewing In Progress</h3>
<p className="text-cyan-800 mb-3">Your embroidery is being stitched. Monitor the progress below.</p>
<ul className="list-disc list-inside text-sm text-cyan-700 space-y-1">
<li>Watch the progress bar and current stitch count</li>
<li>The machine will pause when a color change is needed</li>
<li>Do not leave the machine unattended</li>
</ul>
</div>
</div>
</div>
);
case MachineStatus.COLOR_CHANGE_WAIT:
return (
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Thread Change Required</h3>
<p className="text-yellow-800 mb-3">The machine needs a different thread color to continue.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 space-y-1">
<li>Check the color blocks section to see which thread is needed</li>
<li>Change to the correct thread color</li>
<li><strong>Press the button on your machine</strong> to resume sewing</li>
</ul>
</div>
</div>
</div>
);
case MachineStatus.PAUSE:
case MachineStatus.STOP:
case MachineStatus.SEWING_INTERRUPTION:
return (
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Sewing Paused</h3>
<p className="text-yellow-800 mb-3">The embroidery has been paused or interrupted.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 space-y-1">
<li>Check if everything is okay with the machine</li>
<li>Click "Resume Sewing" when ready to continue</li>
<li>The machine will pick up where it left off</li>
</ul>
</div>
</div>
</div>
);
case MachineStatus.SEWING_COMPLETE:
return (
<div className="bg-green-50 border-l-4 border-green-600 p-6 rounded-lg shadow-md">
<div className="flex items-start gap-4">
<InformationCircleIcon className="w-8 h-8 text-green-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-green-900 mb-2">Step 7: Embroidery Complete!</h3>
<p className="text-green-800 mb-3">Your embroidery is finished. Great work!</p>
<ul className="list-disc list-inside text-sm text-green-700 space-y-1">
<li>Remove the hoop from the machine</li>
<li>Press the Accept button on the machine</li>
<li>Carefully remove your finished embroidery</li>
<li>Trim any jump stitches or loose threads</li>
<li>Click "Delete Pattern" to start a new project</li>
</ul>
</div>
</div>
</div>
);
default:
return null;
}
}

View file

@ -1,4 +1,14 @@
import { CheckCircleIcon, ArrowRightIcon, CircleStackIcon, PlayIcon } from '@heroicons/react/24/solid'; import {
CheckCircleIcon,
ArrowRightIcon,
CircleStackIcon,
PlayIcon,
CheckBadgeIcon,
ClockIcon,
PauseCircleIcon,
XCircleIcon,
ExclamationCircleIcon
} from '@heroicons/react/24/solid';
import type { PatternInfo, SewingProgress } from '../types/machine'; import type { PatternInfo, SewingProgress } from '../types/machine';
import { MachineStatus } from '../types/machine'; import { MachineStatus } from '../types/machine';
import type { PesPatternData } from '../utils/pystitchConverter'; import type { PesPatternData } from '../utils/pystitchConverter';
@ -100,39 +110,36 @@ export function ProgressMonitor({
}; };
return ( return (
<div className="bg-white p-6 rounded-lg shadow-md"> <div className="bg-white p-4 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Sewing Progress</h2> <h2 className="text-lg font-semibold mb-3 pb-2 border-b border-gray-300">Sewing Progress</h2>
{patternInfo && ( <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Left Column - Pattern Info & Color Blocks */}
<div> <div>
<div className="flex justify-between py-2 border-b border-gray-300"> {patternInfo && (
<span className="font-medium text-gray-600">Total Stitches:</span> <div className="bg-gray-50 p-3 rounded-lg mb-3">
<span className="font-semibold">{patternInfo.totalStitches}</span> <div className="grid grid-cols-3 gap-3 text-sm">
<div>
<span className="text-gray-600 block text-xs">Total Stitches</span>
<span className="font-semibold text-gray-900">{patternInfo.totalStitches.toLocaleString()}</span>
</div> </div>
<div className="flex justify-between py-2 border-b border-gray-300"> <div>
<span className="font-medium text-gray-600">Estimated Time:</span> <span className="text-gray-600 block text-xs">Est. Time</span>
<span className="font-semibold"> <span className="font-semibold text-gray-900">
{Math.floor(patternInfo.totalTime / 60)}: {Math.floor(patternInfo.totalTime / 60)}:{String(patternInfo.totalTime % 60).padStart(2, '0')}
{(patternInfo.totalTime % 60).toString().padStart(2, '0')}
</span> </span>
</div> </div>
<div className="flex justify-between py-2 border-b border-gray-300"> <div>
<span className="font-medium text-gray-600">Speed:</span> <span className="text-gray-600 block text-xs">Speed</span>
<span className="font-semibold">{patternInfo.speed} spm</span> <span className="font-semibold text-gray-900">{patternInfo.speed} spm</span>
</div> </div>
<div className="flex justify-between py-2">
<span className="font-medium text-gray-600">Bounds:</span>
<span className="font-semibold">
({patternInfo.boundLeft}, {patternInfo.boundTop}) to (
{patternInfo.boundRight}, {patternInfo.boundBottom})
</span>
</div> </div>
</div> </div>
)} )}
{colorBlocks.length > 0 && ( {colorBlocks.length > 0 && (
<div className="mt-6 pt-4 border-t border-gray-300"> <div>
<h3 className="text-base font-semibold my-4">Color Blocks</h3> <h3 className="text-sm font-semibold mb-2 text-gray-700">Color Blocks</h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{colorBlocks.map((block, index) => { {colorBlocks.map((block, index) => {
const isCompleted = currentStitch >= block.endStitch; const isCompleted = currentStitch >= block.endStitch;
@ -149,32 +156,32 @@ export function ProgressMonitor({
return ( return (
<div <div
key={index} key={index}
className={`p-3 rounded bg-gray-100 border-2 border-transparent transition-all ${ className={`p-2 rounded bg-gray-100 border-2 border-transparent transition-all ${
isCompleted ? 'border-green-600 bg-green-50' : isCurrent ? 'border-blue-600 bg-blue-50 shadow-md shadow-blue-600/20' : 'opacity-60' isCompleted ? 'border-green-600 bg-green-50' : isCurrent ? 'border-blue-600 bg-blue-50 shadow-md shadow-blue-600/20' : 'opacity-60'
}`} }`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<div <div
className="w-6 h-6 rounded border-2 border-gray-300 shadow-sm flex-shrink-0" className="w-5 h-5 rounded border-2 border-gray-300 shadow-sm flex-shrink-0"
style={{ backgroundColor: block.threadHex }} style={{ backgroundColor: block.threadHex }}
title={block.threadHex} title={block.threadHex}
/> />
<span className="font-semibold flex-1"> <span className="font-semibold flex-1 text-sm">
Thread {block.colorIndex + 1} Thread {block.colorIndex + 1}
</span> </span>
{isCompleted ? ( {isCompleted ? (
<CheckCircleIcon className="w-6 h-6 text-green-600" /> <CheckCircleIcon className="w-5 h-5 text-green-600" />
) : isCurrent ? ( ) : isCurrent ? (
<ArrowRightIcon className="w-6 h-6 text-blue-600" /> <ArrowRightIcon className="w-5 h-5 text-blue-600" />
) : ( ) : (
<CircleStackIcon className="w-6 h-6 text-gray-400" /> <CircleStackIcon className="w-5 h-5 text-gray-400" />
)} )}
<span className="text-sm text-gray-600"> <span className="text-xs text-gray-600">
{block.stitchCount} stitches {block.stitchCount.toLocaleString()}
</span> </span>
</div> </div>
{isCurrent && ( {isCurrent && (
<div className="mt-2 h-1 bg-white rounded overflow-hidden"> <div className="mt-1.5 h-1 bg-white rounded overflow-hidden">
<div <div
className="h-full bg-blue-600 transition-all duration-300" className="h-full bg-blue-600 transition-all duration-300"
style={{ width: `${blockProgress}%` }} style={{ width: `${blockProgress}%` }}
@ -187,157 +194,93 @@ export function ProgressMonitor({
</div> </div>
</div> </div>
)} )}
</div>
{/* Right Column - Progress & Controls */}
<div>
{sewingProgress && ( {sewingProgress && (
<div className="mt-4"> <div className="mb-3">
<div className="h-3 bg-gray-300 rounded-md overflow-hidden my-4 shadow-inner relative"> <div className="flex justify-between items-center mb-1.5">
<span className="text-xs font-medium text-gray-600">Progress</span>
<span className="text-xl font-bold text-blue-600">{progressPercent.toFixed(1)}%</span>
</div>
<div className="h-3 bg-gray-300 rounded-md overflow-hidden shadow-inner relative mb-2">
<div className="h-full bg-gradient-to-r from-blue-600 to-blue-700 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite]" style={{ width: `${progressPercent}%` }} /> <div className="h-full bg-gradient-to-r from-blue-600 to-blue-700 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite]" style={{ width: `${progressPercent}%` }} />
</div> </div>
<div className="flex justify-between py-2 border-b border-gray-300"> <div className="bg-gray-50 p-2 rounded-lg grid grid-cols-2 gap-2 text-sm">
<span className="font-medium text-gray-600">Current Stitch:</span> <div>
<span className="font-semibold"> <span className="text-gray-600 block text-xs">Current Stitch</span>
{sewingProgress.currentStitch} / {patternInfo?.totalStitches || 0} <span className="font-semibold text-gray-900">
{sewingProgress.currentStitch.toLocaleString()} / {patternInfo?.totalStitches.toLocaleString() || 0}
</span> </span>
</div> </div>
<div className="flex justify-between py-2 border-b border-gray-300"> <div>
<span className="font-medium text-gray-600">Elapsed Time:</span> <span className="text-gray-600 block text-xs">Time Elapsed</span>
<span className="font-semibold"> <span className="font-semibold text-gray-900">
{Math.floor(sewingProgress.currentTime / 60)}: {Math.floor(sewingProgress.currentTime / 60)}:{String(sewingProgress.currentTime % 60).padStart(2, '0')}
{(sewingProgress.currentTime % 60).toString().padStart(2, '0')}
</span> </span>
</div> </div>
<div className="flex justify-between py-2 border-b border-gray-300">
<span className="font-medium text-gray-600">Position:</span>
<span className="font-semibold">
({(sewingProgress.positionX / 10).toFixed(1)}mm,{' '}
{(sewingProgress.positionY / 10).toFixed(1)}mm)
</span>
</div>
<div className="flex justify-between py-2">
<span className="font-medium text-gray-600">Progress:</span>
<span className="font-semibold">{progressPercent.toFixed(1)}%</span>
</div> </div>
</div> </div>
)} )}
{/* State Visual Indicator */} {/* State Visual Indicator */}
{patternInfo && ( {patternInfo && (() => {
<div className={`flex items-center gap-4 p-4 rounded-lg my-4 border-l-4 ${stateIndicatorColors[stateVisual.color as keyof typeof stateIndicatorColors] || stateIndicatorColors.info}`}> const iconMap = {
<span className="text-3xl leading-none">{stateVisual.icon}</span> ready: <ClockIcon className="w-6 h-6" />,
active: <PlayIcon className="w-6 h-6" />,
waiting: <PauseCircleIcon className="w-6 h-6" />,
complete: <CheckBadgeIcon className="w-6 h-6" />,
interrupted: <PauseCircleIcon className="w-6 h-6" />,
error: <ExclamationCircleIcon className="w-6 h-6" />
};
return (
<div className={`flex items-center gap-3 p-3 rounded-lg mb-3 border-l-4 ${stateIndicatorColors[stateVisual.color as keyof typeof stateIndicatorColors] || stateIndicatorColors.info}`}>
<div className="flex-shrink-0">
{iconMap[stateVisual.iconName]}
</div>
<div className="flex-1"> <div className="flex-1">
<div className="font-semibold text-base mb-1">{stateVisual.label}</div> <div className="font-semibold text-sm">{stateVisual.label}</div>
<div className="text-sm text-gray-600">{stateVisual.description}</div> <div className="text-xs text-gray-600">{stateVisual.description}</div>
</div> </div>
</div> </div>
)} );
})()}
<div className="flex gap-3 mt-4 flex-wrap"> {/* Action buttons */}
{/* Mask trace waiting for confirmation */} <div className="flex gap-2 flex-wrap">
{isMaskTraceWait && ( {/* Resume has highest priority when available */}
<div className="bg-yellow-100 text-yellow-800 px-4 py-3 rounded border border-yellow-200 font-medium w-full">
Press button on machine to start mask trace
</div>
)}
{/* Mask trace in progress */}
{isMaskTracing && (
<div className="bg-cyan-100 text-cyan-800 px-4 py-3 rounded border border-cyan-200 font-medium w-full">
Mask trace in progress...
</div>
)}
{/* Mask trace complete - ready to sew */}
{isMaskTraceComplete && (
<>
<div className="bg-green-100 text-green-800 px-4 py-3 rounded border border-green-200 font-medium w-full">
Mask trace complete!
</div>
{canStartSewing(machineStatus) && (
<button onClick={onStartSewing} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Start Sewing
</button>
)}
{canStartMaskTrace(machineStatus) && (
<button onClick={onStartMaskTrace} className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Trace Again
</button>
)}
</>
)}
{/* Pattern uploaded, ready to trace */}
{machineStatus === MachineStatus.IDLE && (
<>
<div className="bg-cyan-100 text-cyan-800 px-4 py-3 rounded border border-cyan-200 font-medium w-full">
Pattern uploaded successfully
</div>
{canStartMaskTrace(machineStatus) && (
<button onClick={onStartMaskTrace} className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Start Mask Trace
</button>
)}
</>
)}
{/* Ready to start (pattern uploaded) */}
{machineStatus === MachineStatus.SEWING_WAIT && (
<>
{canStartMaskTrace(machineStatus) && (
<button onClick={onStartMaskTrace} className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Start Mask Trace
</button>
)}
{canStartSewing(machineStatus) && (
<button onClick={onStartSewing} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Start Sewing
</button>
)}
</>
)}
{/* Resume sewing for interrupted states */}
{canResumeSewing(machineStatus) && ( {canResumeSewing(machineStatus) && (
<button onClick={onResumeSewing} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3] flex items-center gap-2"> <button onClick={onResumeSewing} className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md cursor-pointer">
<PlayIcon className="w-4 h-4" /> <PlayIcon className="w-4 h-4" />
Resume Sewing Resume Sewing
</button> </button>
)} )}
{/* Color change needed */} {/* Start Sewing - primary action */}
{isColorChange && ( {canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && (
<div className="bg-yellow-100 text-yellow-800 px-4 py-3 rounded border border-yellow-200 font-medium w-full"> <button onClick={onStartSewing} className="px-4 py-2 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md cursor-pointer">
Waiting for color change - change thread and press button on machine Start Sewing
</div>
)}
{/* Sewing in progress */}
{isSewing && (
<div className="bg-cyan-100 text-cyan-800 px-4 py-3 rounded border border-cyan-200 font-medium w-full">
Sewing in progress...
</div>
)}
{/* Sewing complete */}
{isComplete && (
<div className="bg-green-100 text-green-800 px-4 py-3 rounded border border-green-200 font-medium w-full">
Sewing complete!
</div>
)}
{/* Delete pattern button - ONLY show when safe */}
{patternInfo && canDeletePattern(machineStatus) && (
<button onClick={onDeletePattern} className="px-6 py-3 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Delete Pattern
</button> </button>
)} )}
{/* Show warning when delete is unavailable */} {/* Start Mask Trace - secondary action */}
{patternInfo && !canDeletePattern(machineStatus) && ( {canStartMaskTrace(machineStatus) && (
<div className="bg-cyan-100 text-cyan-800 px-4 py-3 rounded border border-cyan-200 font-medium w-full"> <button onClick={onStartMaskTrace} className="px-4 py-2 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md cursor-pointer">
Pattern cannot be deleted during active operations {isMaskTraceComplete ? 'Trace Again' : 'Start Mask Trace'}
</div> </button>
)} )}
{/* Delete - destructive action, always last */}
{patternInfo && canDeletePattern(machineStatus) && (
<button onClick={onDeletePattern} className="px-4 py-2 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md ml-auto cursor-pointer">
Delete Pattern
</button>
)}
</div>
</div>
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,112 @@
import { CheckCircleIcon } from '@heroicons/react/24/solid';
import { MachineStatus } from '../types/machine';
interface WorkflowStepperProps {
machineStatus: MachineStatus;
isConnected: boolean;
hasPattern: boolean;
patternUploaded: boolean;
}
interface Step {
id: number;
label: string;
description: string;
}
const steps: Step[] = [
{ id: 1, label: 'Connect', description: 'Connect to machine' },
{ id: 2, label: 'Load Pattern', description: 'Choose PES file' },
{ id: 3, label: 'Upload', description: 'Upload to machine' },
{ id: 4, label: 'Mask Trace', description: 'Trace pattern area' },
{ id: 5, label: 'Start Sewing', description: 'Begin embroidery' },
{ id: 6, label: 'Monitor', description: 'Watch progress' },
{ id: 7, label: 'Complete', description: 'Finish and remove' },
];
function getCurrentStep(machineStatus: MachineStatus, isConnected: boolean, hasPattern: boolean, patternUploaded: boolean): number {
if (!isConnected) return 1;
if (!hasPattern) return 2;
if (!patternUploaded) return 3;
// After upload, determine step based on machine status
switch (machineStatus) {
case MachineStatus.IDLE:
case MachineStatus.MASK_TRACE_LOCK_WAIT:
case MachineStatus.MASK_TRACING:
return 4;
case MachineStatus.MASK_TRACE_COMPLETE:
case MachineStatus.SEWING_WAIT:
return 5;
case MachineStatus.SEWING:
case MachineStatus.COLOR_CHANGE_WAIT:
case MachineStatus.PAUSE:
case MachineStatus.STOP:
case MachineStatus.SEWING_INTERRUPTION:
return 6;
case MachineStatus.SEWING_COMPLETE:
return 7;
default:
return 4;
}
}
export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patternUploaded }: WorkflowStepperProps) {
const currentStep = getCurrentStep(machineStatus, isConnected, hasPattern, patternUploaded);
return (
<div className="relative max-w-5xl mx-auto mt-4">
{/* Progress bar background */}
<div className="absolute top-4 left-0 right-0 h-0.5 bg-blue-400/30" style={{ left: '20px', right: '20px' }} />
{/* Progress bar fill */}
<div
className="absolute top-4 left-0 h-0.5 bg-blue-100 transition-all duration-500"
style={{
left: '20px',
width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 20px)`
}}
/>
{/* Steps */}
<div className="flex justify-between relative">
{steps.map((step) => {
const isComplete = step.id < currentStep;
const isCurrent = step.id === currentStep;
const isUpcoming = step.id > currentStep;
return (
<div key={step.id} className="flex flex-col items-center" style={{ flex: 1 }}>
{/* Step circle */}
<div
className={`
w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2
${isComplete ? 'bg-green-500 border-green-500 text-white' : ''}
${isCurrent ? 'bg-blue-600 border-blue-600 text-white scale-110' : ''}
${isUpcoming ? 'bg-blue-700 border-blue-400/30 text-blue-200' : ''}
`}
>
{isComplete ? (
<CheckCircleIcon className="w-5 h-5" />
) : (
step.id
)}
</div>
{/* Step label */}
<div className="mt-1.5 text-center">
<div className={`text-xs font-semibold ${isCurrent ? 'text-white' : isComplete ? 'text-blue-100' : 'text-blue-300'}`}>
{step.label}
</div>
</div>
</div>
);
})}
</div>
</div>
);
}

View file

@ -11,6 +11,7 @@ import {
uuidToString, uuidToString,
} from "../services/PatternCacheService"; } from "../services/PatternCacheService";
import type { PesPatternData } from "../utils/pystitchConverter"; import type { PesPatternData } from "../utils/pystitchConverter";
import { SewingMachineError } from "../utils/errorCodeHelpers";
export function useBrotherMachine() { export function useBrotherMachine() {
const [service] = useState(() => new BrotherPP1Service()); const [service] = useState(() => new BrotherPP1Service());
@ -19,7 +20,7 @@ export function useBrotherMachine() {
const [machineStatus, setMachineStatus] = useState<MachineStatus>( const [machineStatus, setMachineStatus] = useState<MachineStatus>(
MachineStatus.None, MachineStatus.None,
); );
const [machineError, setMachineError] = useState<number>(0); const [machineError, setMachineError] = useState<number>(SewingMachineError.None);
const [patternInfo, setPatternInfo] = useState<PatternInfo | null>(null); const [patternInfo, setPatternInfo] = useState<PatternInfo | null>(null);
const [sewingProgress, setSewingProgress] = useState<SewingProgress | null>( const [sewingProgress, setSewingProgress] = useState<SewingProgress | null>(
null, null,
@ -279,6 +280,7 @@ export function useBrotherMachine() {
await service.deletePattern(); await service.deletePattern();
setPatternInfo(null); setPatternInfo(null);
setSewingProgress(null); setSewingProgress(null);
setUploadProgress(0); // Reset upload progress to allow new uploads
await refreshStatus(); await refreshStatus();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete pattern"); setError(err instanceof Error ? err.message : "Failed to delete pattern");

View file

@ -388,47 +388,6 @@ export class BrotherPP1Service {
}); });
} }
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> {
const response = await this.sendCommand(Commands.EMB_UUID_SEND, uuid); const response = await this.sendCommand(Commands.EMB_UUID_SEND, uuid);
@ -557,10 +516,6 @@ export class BrotherPP1Service {
const boundRight = bounds?.maxX ?? 0; const boundRight = bounds?.maxX ?? 0;
const boundBottom = bounds?.maxY ?? 0; const boundBottom = bounds?.maxY ?? 0;
// Calculate pattern dimensions
const patternWidth = boundRight - boundLeft;
const patternHeight = boundBottom - boundTop;
// Calculate move offset based on user-defined pattern offset or auto-center // Calculate move offset based on user-defined pattern offset or auto-center
let moveX: number; let moveX: number;
let moveY: number; let moveY: number;

View file

@ -3,42 +3,72 @@
* Based on App/Asura.Core/Models/SewingMachineError.cs * Based on App/Asura.Core/Models/SewingMachineError.cs
*/ */
export enum SewingMachineError { export const SewingMachineError = {
NeedlePositionError = 0x00, NeedlePositionError: 0x00,
SafetyError = 0x01, SafetyError: 0x01,
LowerThreadSafetyError = 0x02, LowerThreadSafetyError: 0x02,
LowerThreadFreeError = 0x03, LowerThreadFreeError: 0x03,
RestartError10 = 0x10, RestartError10: 0x10,
RestartError11 = 0x11, RestartError11: 0x11,
RestartError12 = 0x12, RestartError12: 0x12,
RestartError13 = 0x13, RestartError13: 0x13,
RestartError14 = 0x14, RestartError14: 0x14,
RestartError15 = 0x15, RestartError15: 0x15,
RestartError16 = 0x16, RestartError16: 0x16,
RestartError17 = 0x17, RestartError17: 0x17,
RestartError18 = 0x18, RestartError18: 0x18,
RestartError19 = 0x19, RestartError19: 0x19,
RestartError1A = 0x1A, RestartError1A: 0x1A,
RestartError1B = 0x1B, RestartError1B: 0x1B,
RestartError1C = 0x1C, RestartError1C: 0x1C,
NeedlePlateError = 0x20, NeedlePlateError: 0x20,
ThreadLeverError = 0x21, ThreadLeverError: 0x21,
UpperThreadError = 0x60, UpperThreadError: 0x60,
LowerThreadError = 0x61, LowerThreadError: 0x61,
UpperThreadSewingStartError = 0x62, UpperThreadSewingStartError: 0x62,
PRWiperError = 0x63, PRWiperError: 0x63,
HoopError = 0x70, HoopError: 0x70,
NoHoopError = 0x71, NoHoopError: 0x71,
InitialHoopError = 0x72, InitialHoopError: 0x72,
RegularInspectionError = 0x80, RegularInspectionError: 0x80,
Setting = 0x98, Setting: 0x98,
None = 0xDD, None: 0xDD,
Unknown = 0xEE, Unknown: 0xEE,
OtherError = 0xFF, OtherError: 0xFF,
} as const;
/**
* Detailed error information with title, description, and solution steps
*/
interface ErrorInfo {
title: string;
description: string;
solutions: string[];
/** If true, this "error" is really just an informational step, not a real error */
isInformational?: boolean;
} }
/** /**
* Human-readable error messages * Detailed error messages with actionable solutions
* Only errors with verified solutions are included here
*/
const ERROR_DETAILS: Record<number, ErrorInfo> = {
[SewingMachineError.InitialHoopError]: {
title: 'Machine Initialization Required',
description: 'The hoop needs to be removed and an initial homing procedure must be performed.',
solutions: [
'Remove the embroidery hoop from the machine completely',
'Press the Accept button',
'Wait for the machine to complete its initialization (homing)',
'Once initialization is complete, reattach the hoop',
'The machine should now recognize the hoop correctly',
],
isInformational: true, // This is a normal initialization step, not an error
},
};
/**
* Simple error titles for all error codes
*/ */
const ERROR_MESSAGES: Record<number, string> = { const ERROR_MESSAGES: Record<number, string> = {
[SewingMachineError.NeedlePositionError]: 'Needle Position Error', [SewingMachineError.NeedlePositionError]: 'Needle Position Error',
@ -66,7 +96,7 @@ const ERROR_MESSAGES: Record<number, string> = {
[SewingMachineError.PRWiperError]: 'PR Wiper Error', [SewingMachineError.PRWiperError]: 'PR Wiper Error',
[SewingMachineError.HoopError]: 'Hoop Error', [SewingMachineError.HoopError]: 'Hoop Error',
[SewingMachineError.NoHoopError]: 'No Hoop Detected', [SewingMachineError.NoHoopError]: 'No Hoop Detected',
[SewingMachineError.InitialHoopError]: 'Initial Hoop Error', [SewingMachineError.InitialHoopError]: 'Initial Hoop Position Error',
[SewingMachineError.RegularInspectionError]: 'Regular Inspection Required', [SewingMachineError.RegularInspectionError]: 'Regular Inspection Required',
[SewingMachineError.Setting]: 'Settings Error', [SewingMachineError.Setting]: 'Settings Error',
[SewingMachineError.Unknown]: 'Unknown Error', [SewingMachineError.Unknown]: 'Unknown Error',
@ -103,3 +133,54 @@ export function getErrorMessage(errorCode: number | undefined): string | null {
export function hasError(errorCode: number | undefined): boolean { export function hasError(errorCode: number | undefined): boolean {
return errorCode !== undefined && errorCode !== null && errorCode !== SewingMachineError.None; return errorCode !== undefined && errorCode !== null && errorCode !== SewingMachineError.None;
} }
/**
* Get detailed error information including title, description, and solutions
*/
export function getErrorDetails(errorCode: number | undefined): ErrorInfo | 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;
}
// Look up known error details with solutions
const details = ERROR_DETAILS[errorCode];
if (details) {
return details;
}
// For errors without detailed solutions, return basic info
const errorTitle = ERROR_MESSAGES[errorCode];
if (errorTitle) {
return {
title: errorTitle,
description: 'Please check the machine display for more information.',
solutions: [
'Consult your machine manual for specific troubleshooting steps',
'Check the error code on the machine display',
'Contact technical support if the problem persists',
],
};
}
// Unknown error code
return {
title: `Machine Error 0x${errorCode.toString(16).toUpperCase().padStart(2, '0')}`,
description: 'The machine has reported an error code that is not recognized.',
solutions: [
'Note the error code and consult your machine manual',
'Turn the machine off and on again',
'If error persists, contact technical support with this error code',
],
};
}
/**
* Export ErrorInfo type for use in other files
*/
export type { ErrorInfo };

View file

@ -66,19 +66,21 @@ export function getMachineStateCategory(status: MachineStatus): MachineStateCate
*/ */
export function canDeletePattern(status: MachineStatus): boolean { export function canDeletePattern(status: MachineStatus): boolean {
const category = getMachineStateCategory(status); const category = getMachineStateCategory(status);
// Can only delete in IDLE or COMPLETE states, never during ACTIVE operations // Can delete in IDLE, WAITING, or COMPLETE states, never during ACTIVE operations
return category === MachineStateCategory.IDLE || return category === MachineStateCategory.IDLE ||
category === MachineStateCategory.WAITING ||
category === MachineStateCategory.COMPLETE; category === MachineStateCategory.COMPLETE;
} }
/** /**
* Determines if a pattern can be safely uploaded in the current state. * Determines if a pattern can be safely uploaded in the current state.
* Only allow uploads when machine is idle. * Only allow uploads when machine is idle or in a complete state.
*/ */
export function canUploadPattern(status: MachineStatus): boolean { export function canUploadPattern(status: MachineStatus): boolean {
const category = getMachineStateCategory(status); const category = getMachineStateCategory(status);
// Can only upload in IDLE state // Can upload in IDLE or COMPLETE states (includes MASK_TRACE_COMPLETE)
return category === MachineStateCategory.IDLE; return category === MachineStateCategory.IDLE ||
category === MachineStateCategory.COMPLETE;
} }
/** /**
@ -130,7 +132,7 @@ export function shouldConfirmDisconnect(status: MachineStatus): boolean {
*/ */
export interface StateVisualInfo { export interface StateVisualInfo {
color: string; color: string;
icon: string; iconName: 'ready' | 'active' | 'waiting' | 'complete' | 'interrupted' | 'error';
label: string; label: string;
description: string; description: string;
} }
@ -146,37 +148,37 @@ export function getStateVisualInfo(status: MachineStatus): StateVisualInfo {
const visualMap: Record<MachineStateCategoryType, StateVisualInfo> = { const visualMap: Record<MachineStateCategoryType, StateVisualInfo> = {
[MachineStateCategory.IDLE]: { [MachineStateCategory.IDLE]: {
color: 'info', color: 'info',
icon: '⭕', iconName: 'ready',
label: 'Ready', label: 'Ready',
description: 'Machine is idle and ready for operations' description: 'Machine is idle and ready for operations'
}, },
[MachineStateCategory.ACTIVE]: { [MachineStateCategory.ACTIVE]: {
color: 'warning', color: 'warning',
icon: '▶️', iconName: 'active',
label: 'Active', label: 'Active',
description: 'Operation in progress - do not interrupt' description: 'Operation in progress - do not interrupt'
}, },
[MachineStateCategory.WAITING]: { [MachineStateCategory.WAITING]: {
color: 'warning', color: 'warning',
icon: '⏸️', iconName: 'waiting',
label: 'Waiting', label: 'Waiting',
description: 'Waiting for user or machine action' description: 'Waiting for user or machine action'
}, },
[MachineStateCategory.COMPLETE]: { [MachineStateCategory.COMPLETE]: {
color: 'success', color: 'success',
icon: '✅', iconName: 'complete',
label: 'Complete', label: 'Complete',
description: 'Operation completed successfully' description: 'Operation completed successfully'
}, },
[MachineStateCategory.INTERRUPTED]: { [MachineStateCategory.INTERRUPTED]: {
color: 'danger', color: 'danger',
icon: '⏹️', iconName: 'interrupted',
label: 'Interrupted', label: 'Interrupted',
description: 'Operation paused or stopped' description: 'Operation paused or stopped'
}, },
[MachineStateCategory.ERROR]: { [MachineStateCategory.ERROR]: {
color: 'danger', color: 'danger',
icon: '❌', iconName: 'error',
label: 'Error', label: 'Error',
description: 'Machine in error or unknown state' description: 'Machine in error or unknown state'
} }

View file

@ -98,8 +98,6 @@ for i, stitch in enumerate(pattern.stitches):
// 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 {
@ -113,32 +111,6 @@ for i, stitch in enumerate(pattern.stitches):
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');
} }
@ -187,15 +159,22 @@ for i, stitch in enumerate(pattern.stitches):
yEncoded |= PEN_FEED_DATA; yEncoded |= PEN_FEED_DATA;
} }
// Check if this is the last stitch
const isLastStitch = i === stitches.length - 1 || (cmd & END) !== 0;
// Check for color change by comparing stitch color index // Check for color change by comparing stitch color index
// Mark the LAST stitch of the previous color with PEN_COLOR_END // Mark the LAST stitch of the previous color with PEN_COLOR_END
// BUT: if this is the last stitch of the entire pattern, use DATA_END instead
const nextStitch = stitches[i + 1]; const nextStitch = stitches[i + 1];
const nextStitchColor = nextStitch?.[3]; const nextStitchColor = nextStitch?.[3];
if (nextStitchColor !== undefined && nextStitchColor !== stitchColor) { if (!isLastStitch && nextStitchColor !== undefined && nextStitchColor !== stitchColor) {
// This is the last stitch before a color change // This is the last stitch before a color change (but not the last stitch overall)
xEncoded = (xEncoded & 0xFFF8) | PEN_COLOR_END; xEncoded = (xEncoded & 0xFFF8) | PEN_COLOR_END;
currentColor = nextStitchColor; currentColor = nextStitchColor;
} else if (isLastStitch) {
// This is the very last stitch of the pattern
xEncoded = (xEncoded & 0xFFF8) | PEN_DATA_END;
} }
// Add stitch as 4 bytes: [X_low, X_high, Y_low, Y_high] // Add stitch as 4 bytes: [X_low, X_high, Y_low, Y_high]
@ -208,52 +187,12 @@ for i, stitch in enumerate(pattern.stitches):
// Check for end command // Check for end command
if ((cmd & END) !== 0) { if ((cmd & END) !== 0) {
// Mark as data end
const lastIdx = penStitches.length - 4;
penStitches[lastIdx] = (penStitches[lastIdx] & 0xF8) | PEN_DATA_END;
break; break;
} }
} }
// Mark the last stitch with DATA_END if not already marked
if (penStitches.length > 0) {
const lastIdx = penStitches.length - 4;
if ((penStitches[lastIdx] & 0x07) !== PEN_DATA_END) {
penStitches[lastIdx] = (penStitches[lastIdx] & 0xF8) | PEN_DATA_END;
}
}
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,