From b2474bf5d1b5c2f34807da22b446f5aa805dab00 Mon Sep 17 00:00:00 2001 From: David Montero Date: Tue, 26 May 2026 18:54:17 +0200 Subject: [PATCH] fix(stop): preserve display state on Stop, only blank on Reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reporter feedback after 7aca3db: pressing Stop on the uno-7segment example turned the 7-segment off, and pressing Start again left random segments lit / no number at all. The previous fix made resetPinStates() notify every listener with (pin, false) on both Stop and Reset, which was right for Reset (full reboot) but wrong for Stop: - On Stop the AVR CPU is just paused. Internally it still has PORTD=0xFF (or whatever the last drive was). - resetPinStates blanked the pinStates cache + fan-out LOW notifications. Display turns off, fine. - On Start the CPU resumes from where it paused. avr8js's port listener fires only for bits that CHANGED relative to its OWN oldValue (which still holds the pre-stop value). If oldValue matches the live register, no pinChange event fires for that bit, and the display has no signal telling it to come back on. Split the API into two methods: resetPinStates() — soft cleanup, drops outputPins only. Used by stopBoard. Cached pinStates and visual state stay so the resume picks up where it left off. hardResetPinStates() — full cleanup, drops outputPins + pinStates and fan-outs (pin, false) to listeners. Used by resetBoard (CPU starts at PC=0, firmware re-drives every pin from setup()). Updated the test helper clearAllPinManagerState to call hardResetPinStates between tests so the same-state short-circuit in triggerPinChange doesn't suppress fresh events. All 32 vitest tests pass (AVRSimulator, interconnect-routing, dual-arduino-software-serial, pin-position-rotation). --- .../src/__tests__/helpers/multiBoardSetup.ts | 11 +++++- frontend/src/simulation/PinManager.ts | 36 +++++++++++-------- frontend/src/store/useSimulatorStore.ts | 18 +++++----- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/frontend/src/__tests__/helpers/multiBoardSetup.ts b/frontend/src/__tests__/helpers/multiBoardSetup.ts index 49b6397f..930164d0 100644 --- a/frontend/src/__tests__/helpers/multiBoardSetup.ts +++ b/frontend/src/__tests__/helpers/multiBoardSetup.ts @@ -44,6 +44,11 @@ export function resetStore(useSimulatorStore: any): void { * current board list. Required between tests so that the same-state * short-circuit in PinManager.triggerPinChange doesn't suppress fresh * events. + * + * Uses hardResetPinStates (clear cache + classifications), not the + * stopBoard-flavored resetPinStates (classifications only) which + * leaves cached pin states intact so the display can resume after a + * pause. */ export function clearAllPinManagerState( useSimulatorStore: any, @@ -52,7 +57,11 @@ export function clearAllPinManagerState( const state = useSimulatorStore.getState(); for (const b of state.boards ?? []) { const pm = getBoardPinManager(b.id); - pm?.resetPinStates?.(); + if (pm?.hardResetPinStates) { + pm.hardResetPinStates(); + } else { + pm?.resetPinStates?.(); + } } } diff --git a/frontend/src/simulation/PinManager.ts b/frontend/src/simulation/PinManager.ts index 38728243..4d159270 100644 --- a/frontend/src/simulation/PinManager.ts +++ b/frontend/src/simulation/PinManager.ts @@ -139,22 +139,30 @@ export class PinManager { } /** - * Clear cached pin states + output-pin classifications. Called by - * stopBoard / resetBoard so the next Run starts without stale output - * classifications from a previous session forcing premature V-source - * emission. - * - * Also notifies every listener that its pin returned to LOW when the - * previously-cached state was HIGH. Without this notification, - * stateful display components (7-segment, dot-matrix, NeoPixel, LCD - * backlight) keep the last lit pattern frozen on screen after the - * user presses Reset / Stop — their visual state only updates when a - * pinChange callback fires, and resetPinStates was clearing the - * cache silently. Listeners that ignore the synthetic LOW (analog - * sensors, buttons) are unaffected; their next external write - * overrides the false anyway. + * Soft cleanup for stopBoard: drop the MCU-output classification so + * the next Run doesn't keep emitting stale V-sources on the SPICE + * side from a pin the previous program had driven. The cached pin + * STATES (`pinStates`) stay put — components that hold visual state + * (7-segment, NeoPixel, LCD, dot-matrix) keep showing the last + * pattern, which is what users expect from a pause/stop. On resume, + * avr8js's port-listener fires only for bits that CHANGED relative + * to its internal oldValue, so blanking the cache here would race: + * if the sketch's port register matches what it was pre-stop, no + * pinChange fires and the display would never recover. */ resetPinStates(): void { + this.outputPins.clear(); + } + + /** + * Hard reset for resetBoard / firmware reload: wipe every cached + * state AND notify listeners that previously-HIGH pins are now LOW, + * so stateful displays redraw cleanly to all-off. Reset implies the + * MCU is restarting from 0 — there's no "resume" race to worry + * about; the firmware will re-drive every pin from setup() once it + * boots. + */ + hardResetPinStates(): void { const wereHigh: number[] = []; for (const [pin, state] of this.pinStates) { if (state) wereHigh.push(pin); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index e7aaf700..611f76d6 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -1629,14 +1629,16 @@ export const useSimulatorStore = create((set, get) => { const sim = getBoardSimulator(boardId); if (sim) { sim.reset(); - // Drop MCU-output classification so the new program starts - // clean — no stale V-sources from the previous run. The - // resetPinStates call also notifies listeners that pins which - // were HIGH are now LOW, so visual components (7-segment, - // NeoPixel, LCD backlight) clear their stale state instead of - // freezing on whatever pattern they had at the moment of - // Reset. - getBoardPinManager(boardId)?.resetPinStates(); + // Reset is a hard reboot — the CPU starts at PC=0, every pin + // goes back to floating, every output classification is + // dropped. The hardResetPinStates call also notifies + // listeners that previously-HIGH pins are now LOW, so visual + // components (7-segment, NeoPixel, LCD) clear their stale + // pattern instead of freezing on whatever was lit at the + // moment of Reset. The Stop path uses the lighter + // resetPinStates() that ONLY drops the output classification + // (preserves the display so it can resume) — see PinManager. + getBoardPinManager(boardId)?.hardResetPinStates(); // NOTE: do NOT reassign sim.onSerialData here. sim.reset() // recreates the USART but the new usart.onByteTransmit // already chains through `this.onSerialData`, which is the