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