velxio/frontend/src/store/useSimulatorStore.ts

570 lines
17 KiB
TypeScript

import { create } from 'zustand';
import { AVRSimulator } from '../simulation/AVRSimulator';
import { RP2040Simulator } from '../simulation/RP2040Simulator';
import { PinManager } from '../simulation/PinManager';
import { VirtualDS1307, VirtualTempSensor, I2CMemoryDevice } from '../simulation/I2CBusManager';
import type { RP2040I2CDevice } from '../simulation/RP2040Simulator';
import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
import { calculatePinPosition } from '../utils/pinPositionCalculator';
export type BoardType = 'arduino-uno' | 'arduino-nano' | 'arduino-mega' | 'raspberry-pi-pico';
export const BOARD_FQBN: Record<BoardType, string> = {
'arduino-uno': 'arduino:avr:uno',
'arduino-nano': 'arduino:avr:nano:cpu=atmega328',
'arduino-mega': 'arduino:avr:mega',
'raspberry-pi-pico': 'rp2040:rp2040:rpipico',
};
export const BOARD_LABELS: Record<BoardType, string> = {
'arduino-uno': 'Arduino Uno',
'arduino-nano': 'Arduino Nano',
'arduino-mega': 'Arduino Mega 2560',
'raspberry-pi-pico': 'Raspberry Pi Pico',
};
// Default position for the Arduino board
export const DEFAULT_BOARD_POSITION = { x: 50, y: 50 };
// Keep legacy export alias for any remaining references
export const ARDUINO_POSITION = DEFAULT_BOARD_POSITION;
interface Component {
id: string;
metadataId: string; // References ComponentMetadata by ID (e.g., 'led', 'dht22')
x: number;
y: number;
properties: Record<string, unknown>; // Flexible properties for any component type
}
interface SimulatorState {
// Board selection
boardType: BoardType;
setBoardType: (type: BoardType) => void;
// Board position (mutable — allows dragging)
boardPosition: { x: number; y: number };
setBoardPosition: (pos: { x: number; y: number }) => void;
// Simulation state
simulator: AVRSimulator | RP2040Simulator | null;
pinManager: PinManager;
running: boolean;
compiledHex: string | null;
/** Increments each time a new hex/binary is loaded — used to re-attach
* virtual devices (SSD1306, etc.) to the fresh I2C bus without toggling
* on every play/stop cycle. */
hexEpoch: number;
// Components
components: Component[];
// Wire state (Phase 1)
wires: Wire[];
selectedWireId: string | null;
wireInProgress: WireInProgress | null;
// Serial monitor state
serialOutput: string;
serialBaudRate: number;
serialMonitorOpen: boolean;
// Actions
initSimulator: () => void;
loadHex: (hex: string) => void;
loadBinary: (base64: string) => void;
startSimulation: () => void;
stopSimulation: () => void;
resetSimulation: () => void;
setCompiledHex: (hex: string) => void;
setCompiledBinary: (base64: string) => void;
setRunning: (running: boolean) => void;
// Component management
addComponent: (component: Component) => void;
removeComponent: (id: string) => void;
updateComponent: (id: string, updates: Partial<Component>) => void;
updateComponentState: (id: string, state: boolean) => void;
handleComponentEvent: (componentId: string, eventName: string, data?: any) => void;
setComponents: (components: Component[]) => void;
// Wire management (Phase 1)
addWire: (wire: Wire) => void;
removeWire: (wireId: string) => void;
updateWire: (wireId: string, updates: Partial<Wire>) => void;
setSelectedWire: (wireId: string | null) => void;
setWires: (wires: Wire[]) => void;
// Wire creation (Phase 2)
startWireCreation: (endpoint: WireEndpoint) => void;
updateWireInProgress: (x: number, y: number) => void;
finishWireCreation: (endpoint: WireEndpoint) => void;
cancelWireCreation: () => void;
// Wire position updates (auto-update when components move)
updateWirePositions: (componentId: string) => void;
recalculateAllWirePositions: () => void;
// Serial monitor
toggleSerialMonitor: () => void;
serialWrite: (text: string) => void;
clearSerialOutput: () => void;
}
export const useSimulatorStore = create<SimulatorState>((set, get) => {
// Create PinManager instance
const pinManager = new PinManager();
return {
boardType: 'arduino-uno' as BoardType,
boardPosition: { ...DEFAULT_BOARD_POSITION },
simulator: null,
pinManager,
running: false,
compiledHex: null,
hexEpoch: 0,
components: [
{
id: 'led-builtin',
metadataId: 'led',
x: 350,
y: 100,
properties: {
color: 'red',
pin: 13,
state: false,
},
},
],
// Wire state with test wires (Phase 1 - Testing)
// Positions will be recalculated dynamically after DOM mount
wires: [
{
id: 'wire-test-1',
start: {
componentId: 'arduino-uno',
pinName: 'GND.1',
x: 0,
y: 0,
},
end: {
componentId: 'led-builtin',
pinName: 'A',
x: 0,
y: 0,
},
controlPoints: [],
color: '#000000', // Black for GND
signalType: 'power-gnd',
isValid: true,
},
{
id: 'wire-test-2',
start: {
componentId: 'arduino-uno',
pinName: '13',
x: 0,
y: 0,
},
end: {
componentId: 'led-builtin',
pinName: 'C',
x: 0,
y: 0,
},
controlPoints: [],
color: '#00ff00', // Green for digital
signalType: 'digital',
isValid: true,
},
],
selectedWireId: null,
wireInProgress: null,
serialOutput: '',
serialBaudRate: 0,
serialMonitorOpen: false,
setBoardPosition: (pos) => {
set({ boardPosition: pos });
},
setBoardType: (type: BoardType) => {
const { running } = get();
if (running) {
get().stopSimulation();
}
const simulator = (type === 'arduino-uno' || type === 'arduino-nano' || type === 'arduino-mega')
? new AVRSimulator(pinManager, type === 'arduino-mega' ? 'mega' : 'uno')
: new RP2040Simulator(pinManager);
// Wire serial output callback for both simulator types
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
if (simulator instanceof AVRSimulator) {
simulator.onBaudRateChange = (baudRate: number) => set({ serialBaudRate: baudRate });
}
set({ boardType: type, simulator, compiledHex: null, serialOutput: '', serialBaudRate: 0 });
console.log(`Board switched to: ${type}`);
},
initSimulator: () => {
const { boardType } = get();
const simulator = (boardType === 'arduino-uno' || boardType === 'arduino-nano' || boardType === 'arduino-mega')
? new AVRSimulator(pinManager, boardType === 'arduino-mega' ? 'mega' : 'uno')
: new RP2040Simulator(pinManager);
// Wire serial output callback for both simulator types
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
if (simulator instanceof AVRSimulator) {
simulator.onBaudRateChange = (baudRate: number) => set({ serialBaudRate: baudRate });
}
set({ simulator, serialOutput: '', serialBaudRate: 0 });
console.log(`Simulator initialized: ${boardType}`);
},
loadHex: (hex: string) => {
const { simulator } = get();
if (simulator && simulator instanceof AVRSimulator) {
try {
simulator.loadHex(hex);
// Re-register background I2C devices on the fresh bus created by loadHex
simulator.addI2CDevice(new VirtualDS1307());
simulator.addI2CDevice(new VirtualTempSensor());
simulator.addI2CDevice(new I2CMemoryDevice(0x50));
set((s) => ({ compiledHex: hex, hexEpoch: s.hexEpoch + 1 }));
console.log('HEX file loaded successfully');
} catch (error) {
console.error('Failed to load HEX:', error);
}
} else {
console.warn('loadHex: simulator not initialized or wrong board type');
}
},
loadBinary: (base64: string) => {
const { simulator } = get();
if (simulator && simulator instanceof RP2040Simulator) {
try {
simulator.loadBinary(base64);
// Re-register background I2C devices on the fresh bus
simulator.addI2CDevice(new VirtualDS1307() as RP2040I2CDevice);
simulator.addI2CDevice(new VirtualTempSensor() as RP2040I2CDevice);
simulator.addI2CDevice(new I2CMemoryDevice(0x50) as RP2040I2CDevice);
set((s) => ({ compiledHex: base64, hexEpoch: s.hexEpoch + 1 }));
console.log('Binary loaded into RP2040 successfully');
} catch (error) {
console.error('Failed to load binary:', error);
}
} else {
console.warn('loadBinary: simulator not initialized or wrong board type');
}
},
startSimulation: () => {
const { simulator } = get();
if (simulator) {
// Background I2C devices are registered in loadHex/loadBinary,
// so we just need to start the CPU loop here.
simulator.start();
set({ running: true, serialMonitorOpen: true });
}
},
stopSimulation: () => {
const { simulator } = get();
if (simulator) {
simulator.stop();
set({ running: false });
}
},
resetSimulation: () => {
const { simulator } = get();
if (simulator) {
simulator.reset();
// Re-wire serial callback after reset (both simulator types)
simulator.onSerialData = (char: string) => {
set((s) => ({ serialOutput: s.serialOutput + char }));
};
if (simulator instanceof AVRSimulator) {
simulator.onBaudRateChange = (baudRate: number) => set({ serialBaudRate: baudRate });
}
set({ running: false, serialOutput: '', serialBaudRate: 0 });
}
},
setCompiledHex: (hex: string) => {
set({ compiledHex: hex });
get().loadHex(hex);
},
setCompiledBinary: (base64: string) => {
set({ compiledHex: base64 }); // use compiledHex as "program ready" flag
get().loadBinary(base64);
},
setRunning: (running: boolean) => set({ running }),
addComponent: (component) => {
set((state) => ({
components: [...state.components, component],
}));
},
removeComponent: (id) => {
set((state) => ({
components: state.components.filter((c) => c.id !== id),
// Also remove wires connected to this component
wires: state.wires.filter((w) =>
w.start.componentId !== id && w.end.componentId !== id
),
}));
},
updateComponent: (id, updates) => {
set((state) => ({
components: state.components.map((c) =>
c.id === id ? { ...c, ...updates } : c
),
}));
// Update wire positions if component moved
if (updates.x !== undefined || updates.y !== undefined) {
get().updateWirePositions(id);
}
},
updateComponentState: (id, state) => {
set((prevState) => ({
components: prevState.components.map((c) =>
c.id === id ? { ...c, properties: { ...c.properties, state, value: state } } : c
),
}));
},
handleComponentEvent: (_componentId, _eventName, _data) => {
// Legacy UI-based handling can be placed here if needed
// but device simulation events are now in DynamicComponent via PartSimulationRegistry
},
setComponents: (components) => {
set({ components });
},
// Wire management actions
addWire: (wire) => {
set((state) => ({
wires: [...state.wires, wire],
}));
},
removeWire: (wireId) => {
set((state) => ({
wires: state.wires.filter((w) => w.id !== wireId),
selectedWireId: state.selectedWireId === wireId ? null : state.selectedWireId,
}));
},
updateWire: (wireId, updates) => {
set((state) => ({
wires: state.wires.map((w) =>
w.id === wireId ? { ...w, ...updates } : w
),
}));
},
setSelectedWire: (wireId) => {
set({ selectedWireId: wireId });
},
setWires: (wires) => {
set({ wires });
},
// Wire creation actions (Phase 2)
startWireCreation: (endpoint) => {
set({
wireInProgress: {
startEndpoint: endpoint,
currentX: endpoint.x,
currentY: endpoint.y,
},
});
},
updateWireInProgress: (x, y) => {
set((state) => {
if (!state.wireInProgress) return state;
return {
wireInProgress: {
...state.wireInProgress,
currentX: x,
currentY: y,
},
};
});
},
finishWireCreation: (endpoint) => {
const state = get();
if (!state.wireInProgress) return;
const { startEndpoint } = state.wireInProgress;
// Calculate midpoint for control point
const midX = (startEndpoint.x + endpoint.x) / 2;
const midY = (startEndpoint.y + endpoint.y) / 2;
const newWire: Wire = {
id: `wire-${Date.now()}`,
start: startEndpoint,
end: endpoint,
controlPoints: [
{
id: `cp-${Date.now()}`,
x: midX,
y: midY,
},
],
color: '#00ff00', // Default green, will be calculated based on signal type
signalType: 'digital',
isValid: true,
};
set((state) => ({
wires: [...state.wires, newWire],
wireInProgress: null,
}));
},
cancelWireCreation: () => {
set({ wireInProgress: null });
},
// Update wire positions when component moves
updateWirePositions: (componentId) => {
set((state) => {
const component = state.components.find((c) => c.id === componentId);
// For the board, use boardPosition from state
const bp = state.boardPosition;
const compX = component ? component.x : bp.x;
const compY = component ? component.y : bp.y;
const updatedWires = state.wires.map((wire) => {
const updated = { ...wire };
if (wire.start.componentId === componentId) {
const pos = calculatePinPosition(
componentId,
wire.start.pinName,
compX,
compY
);
if (pos) {
updated.start = { ...wire.start, x: pos.x, y: pos.y };
}
}
// Update end endpoint if it belongs to this component
if (wire.end.componentId === componentId) {
const pos = calculatePinPosition(
componentId,
wire.end.pinName,
compX,
compY
);
if (pos) {
updated.end = { ...wire.end, x: pos.x, y: pos.y };
}
}
return updated;
});
return { wires: updatedWires };
});
},
// Recalculate all wire positions from actual DOM pinInfo
recalculateAllWirePositions: () => {
const state = get();
const updatedWires = state.wires.map((wire) => {
const updated = { ...wire };
const startComp = state.components.find((c) => c.id === wire.start.componentId);
const bp = state.boardPosition;
const startX = startComp ? startComp.x : bp.x;
const startY = startComp ? startComp.y : bp.y;
const startPos = calculatePinPosition(
wire.start.componentId,
wire.start.pinName,
startX,
startY
);
if (startPos) {
updated.start = { ...wire.start, x: startPos.x, y: startPos.y };
} else {
// Pin name not found in element's pinInfo (e.g. board type mismatch).
// Fall back to the component/board position so the wire renders near
// its endpoint rather than at the canvas origin (0,0).
updated.start = { ...wire.start, x: startX, y: startY };
}
// Resolve end component position
const endComp = state.components.find((c) => c.id === wire.end.componentId);
const endX = endComp ? endComp.x : bp.x;
const endY = endComp ? endComp.y : bp.y;
const endPos = calculatePinPosition(
wire.end.componentId,
wire.end.pinName,
endX,
endY
);
if (endPos) {
updated.end = { ...wire.end, x: endPos.x, y: endPos.y };
} else {
updated.end = { ...wire.end, x: endX, y: endY };
}
// Auto-generate control points for wires that have none
// (e.g., wires loaded from examples or old saved projects).
// This ensures the rendered path and interactive segments use the
// same Z-shape routing, enabling proper segment dragging.
if (
updated.controlPoints.length === 0 &&
(updated.start.x !== 0 || updated.start.y !== 0) &&
(updated.end.x !== 0 || updated.end.y !== 0)
) {
const midX = updated.start.x + (updated.end.x - updated.start.x) / 2;
updated.controlPoints = [
{ id: `cp-${wire.id}-0`, x: midX, y: updated.start.y },
{ id: `cp-${wire.id}-1`, x: midX, y: updated.end.y },
];
}
return updated;
});
set({ wires: updatedWires });
},
// Serial monitor actions
toggleSerialMonitor: () => {
set((s) => ({ serialMonitorOpen: !s.serialMonitorOpen }));
},
serialWrite: (text: string) => {
const { simulator } = get();
if (simulator) {
simulator.serialWrite(text);
}
},
clearSerialOutput: () => {
set({ serialOutput: '' });
},
};
});