diff --git a/frontend/src/__tests__/esp32-integration.test.ts b/frontend/src/__tests__/esp32-integration.test.ts index e5c754f..215d272 100644 --- a/frontend/src/__tests__/esp32-integration.test.ts +++ b/frontend/src/__tests__/esp32-integration.test.ts @@ -314,10 +314,11 @@ describe('useSimulatorStore — ESP32 boards', () => { ); }); - it('addBoard("esp32") creates an Esp32Bridge, not a simulator', () => { + it('addBoard("esp32") creates an Esp32Bridge + a shim in simulatorMap', () => { const { addBoard } = useSimulatorStore.getState(); const id = addBoard('esp32', 300, 100); - expect(getBoardSimulator(id)).toBeUndefined(); + // Esp32BridgeShim is stored in simulatorMap so PartSimulationRegistry components work + expect(getBoardSimulator(id)).toBeDefined(); expect(getEsp32Bridge(id)).toBeDefined(); expect(getEsp32Bridge(id)?.boardKind).toBe('esp32'); }); @@ -328,11 +329,13 @@ describe('useSimulatorStore — ESP32 boards', () => { expect(getEsp32Bridge(id)?.boardKind).toBe('esp32-s3'); }); - it('addBoard("esp32-c3") creates an Esp32C3Simulator, not an Esp32Bridge', () => { + it('addBoard("esp32-c3") uses QEMU Esp32Bridge (full ESP-IDF support)', () => { const { addBoard } = useSimulatorStore.getState(); const id = addBoard('esp32-c3', 300, 100); - // ESP32-C3 uses the browser-side RV32IMC emulator — no QEMU bridge - expect(getEsp32Bridge(id)).toBeUndefined(); + // ESP32-C3 uses QEMU backend via Esp32Bridge for full ESP-IDF ROM compatibility + expect(getEsp32Bridge(id)).toBeDefined(); + expect(getEsp32Bridge(id)?.boardKind).toBe('esp32-c3'); + // Esp32BridgeShim is also present in simulatorMap for component compatibility expect(getBoardSimulator(id)).toBeDefined(); }); diff --git a/frontend/src/__tests__/simulation-parts.test.ts b/frontend/src/__tests__/simulation-parts.test.ts index c4d371a..f6de6d6 100644 --- a/frontend/src/__tests__/simulation-parts.test.ts +++ b/frontend/src/__tests__/simulation-parts.test.ts @@ -687,30 +687,32 @@ describe('Servo — attachEvents', () => { }); it('calculates 90° when OCR1A = ICR1/2 (servo midpoint)', () => { - // ICR1 = 20000 (50 Hz at prescaler 8, 16 MHz) - // OCR1A = 10000 → pulseUs = 1000 + (10000/20000)*1000 = 1500 µs → 90° + // Real Arduino Servo.h: prescaler=8, 16 MHz → 0.5 µs/tick + // ICR1 = 40000 ticks = 20 ms period (50 Hz) + // 90° midpoint: 1472 µs → OCR1A = 1472 / 0.5 = 2944 + // pulseUs = (2944/40000)*20000 = 1472 µs → angle = (1472-544)/1856*180 = 90° const logic = PartSimulationRegistry.get('servo')!; const el = makeElement({ angle: -1 }); const sim = makeSimulator(); // ICR1L=0x86, ICR1H=0x87; OCR1AL=0x88, OCR1AH=0x89 - sim.cpu.data[0x88] = 10000 & 0xFF; // OCR1AL - sim.cpu.data[0x89] = (10000 >> 8) & 0xFF; // OCR1AH - sim.cpu.data[0x86] = 20000 & 0xFF; // ICR1L - sim.cpu.data[0x87] = (20000 >> 8) & 0xFF; // ICR1H + sim.cpu.data[0x88] = 2944 & 0xFF; // OCR1AL + sim.cpu.data[0x89] = (2944 >> 8) & 0xFF; // OCR1AH + sim.cpu.data[0x86] = 40000 & 0xFF; // ICR1L + sim.cpu.data[0x87] = (40000 >> 8) & 0xFF; // ICR1H logic.attachEvents!(el, sim as any, noPins); expect((el as any).angle).toBe(90); }); it('calculates 0° when OCR1A = 0 (minimum pulse)', () => { - // OCR1A=0, ICR1=20000 → pulseUs = 1000 + 0 = 1000 µs → 0° + // OCR1A=0 → pulseUs=0 → clamped to MIN_PULSE_US=544 µs → 0° const logic = PartSimulationRegistry.get('servo')!; const el = makeElement({ angle: -1 }); const sim = makeSimulator(); - sim.cpu.data[0x86] = 20000 & 0xFF; - sim.cpu.data[0x87] = (20000 >> 8) & 0xFF; + sim.cpu.data[0x86] = 40000 & 0xFF; + sim.cpu.data[0x87] = (40000 >> 8) & 0xFF; // OCR1A = 0 (default) logic.attachEvents!(el, sim as any, noPins); @@ -718,15 +720,16 @@ describe('Servo — attachEvents', () => { }); it('calculates 180° when OCR1A = ICR1 (maximum pulse)', () => { - // OCR1A=20000, ICR1=20000 → pulseUs = 1000 + 1000 = 2000 µs → 180° + // 180° maximum: 2400 µs → OCR1A = 2400 / 0.5 = 4800 (ICR1=40000, 50 Hz) + // pulseUs = (4800/40000)*20000 = 2400 µs → angle = (2400-544)/1856*180 = 180° const logic = PartSimulationRegistry.get('servo')!; const el = makeElement({ angle: -1 }); const sim = makeSimulator(); - sim.cpu.data[0x88] = 20000 & 0xFF; - sim.cpu.data[0x89] = (20000 >> 8) & 0xFF; - sim.cpu.data[0x86] = 20000 & 0xFF; - sim.cpu.data[0x87] = (20000 >> 8) & 0xFF; + sim.cpu.data[0x88] = 4800 & 0xFF; + sim.cpu.data[0x89] = (4800 >> 8) & 0xFF; + sim.cpu.data[0x86] = 40000 & 0xFF; + sim.cpu.data[0x87] = (40000 >> 8) & 0xFF; logic.attachEvents!(el, sim as any, noPins); expect((el as any).angle).toBe(180); diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 98606f7..d2f03f8 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -450,9 +450,9 @@ export const LandingPage: React.FC = () => {

- Simulate Arduino,
+ Emulate Arduino,
ESP32 & Raspberry Pi.
- And 16 more boards in your browser.. + in your browser.

Write code, compile, and run on 19 real boards — Arduino Uno, ESP32, ESP32-C3, diff --git a/frontend/src/simulation/Esp32Bridge.ts b/frontend/src/simulation/Esp32Bridge.ts index 6f250b4..0e1ffac 100644 --- a/frontend/src/simulation/Esp32Bridge.ts +++ b/frontend/src/simulation/Esp32Bridge.ts @@ -47,6 +47,8 @@ const API_BASE = (): string => /** Returns a stable UUID for this browser tab (persists across reloads, resets on new tab). */ function getTabSessionId(): string { + // sessionStorage is not available in Node/test environments + if (typeof sessionStorage === 'undefined') return crypto.randomUUID(); const KEY = 'velxio-tab-id'; let id = sessionStorage.getItem(KEY); if (!id) { @@ -187,7 +189,7 @@ export class Esp32Bridge { }; socket.onclose = (ev) => { - console.log(`[Esp32Bridge:${this.boardId}] WebSocket closed (code=${ev.code})`); + console.log(`[Esp32Bridge:${this.boardId}] WebSocket closed (code=${ev?.code ?? '?'})`); this._connected = false; this.socket = null; this.onDisconnected?.();