From 9e117ee5c734bfd90579f797e74b531e4ef35860 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Tue, 7 Apr 2026 13:08:15 -0300 Subject: [PATCH] feat: enhance wire connection handling and GND checks for components --- .../components/simulator/SimulatorCanvas.tsx | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index eca592ba..53571699 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -633,27 +633,56 @@ export const SimulatorCanvas = () => { useEffect(() => { const unsubscribers: (() => void)[] = []; + // Returns true if the component has at least one wire connected to a board + // GND or power-rail pin (boardPinToNumber returns -1 for these). + // Used to block output components from activating without a ground connection. + const componentHasGndWire = (component: any): boolean => + wires.some(w => { + const isSelfStart = w.start.componentId === component.id; + const isSelfEnd = w.end.componentId === component.id; + if (!isSelfStart && !isSelfEnd) return false; + const otherEndpoint = isSelfStart ? w.end : w.start; + if (!isBoardComponent(otherEndpoint.componentId)) return false; + const boardInstance = boards.find(b => b.id === otherEndpoint.componentId); + const lookupKey = boardInstance ? boardInstance.boardKind : otherEndpoint.componentId; + return boardPinToNumber(lookupKey, otherEndpoint.pinName) === -1; + }); + // Helper to add subscription - const subscribeComponentToPin = (component: any, pin: number, componentPinName?: string) => { + // wireConnected: true when this call came from the wire-scanning path (not properties.pin). + const subscribeComponentToPin = ( + component: any, + pin: number, + componentPinName?: string, + wireConnected = false, + ) => { // Components with attachEvents in PartSimulationRegistry manage their own - // visual state (e.g. servo, buzzer). Skip generic digital/PWM updates for them - // to avoid flickering from raw PWM pulses being misinterpreted as on/off state. + // visual state (e.g. LED, servo, buzzer). Skip generic digital/PWM updates for + // them — they already handle GND logic internally via getArduinoPinHelper. const logic = PartSimulationRegistry.get(component.metadataId); const hasSelfManagedVisuals = !!(logic && logic.attachEvents); + // Generic GND check: for wire-connected output components that don't manage + // their own state, require at least one GND wire before activating. + // Skip the check for pin-property components (no GND wire to detect) and for + // self-managed components (they handle GND themselves via attachEvents). + const hasGnd = (!wireConnected || hasSelfManagedVisuals) + ? true + : componentHasGndWire(component); + const unsubscribe = pinManager.onPinChange( pin, (_pin, state) => { if (!hasSelfManagedVisuals) { - // 1. Update React state for standard properties (LEDs, buttons, etc.) - updateComponentState(component.id, state); + // Update React state — gate on GND for wire-connected components. + updateComponentState(component.id, hasGnd && state); } - // 2. Delegate to PartSimulationRegistry for custom visual updates + // Delegate to PartSimulationRegistry for custom visual updates if (logic && logic.onPinStateChange) { const el = document.getElementById(component.id); if (el) { - logic.onPinStateChange(componentPinName || 'A', state, el); + logic.onPinStateChange(componentPinName || 'A', hasGnd && state, el); } } } @@ -665,6 +694,7 @@ export const SimulatorCanvas = () => { // control signal, not a brightness value, so setting opacity would cause flicker. if (!hasSelfManagedVisuals) { const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => { + if (!hasGnd) return; // no GND → stay dark even under PWM const el = document.getElementById(component.id); if (el) el.style.opacity = duty > 0 ? String(duty) : ''; }); @@ -673,9 +703,9 @@ export const SimulatorCanvas = () => { }; components.forEach((component) => { - // 1. Subscribe by explicit pin property + // 1. Subscribe by explicit pin property (old-style, no wire needed) if (component.properties.pin !== undefined) { - subscribeComponentToPin(component, component.properties.pin as number, 'A'); + subscribeComponentToPin(component, component.properties.pin as number, 'A', false); } else { // 2. Subscribe by finding wires connected to arduino const connectedWires = wires.filter( @@ -699,7 +729,7 @@ export const SimulatorCanvas = () => { ` kind=${lookupKey} pinName=${otherEndpoint.pinName} → gpioPin=${pin}` ); if (pin !== null && pin >= 0) { - subscribeComponentToPin(component, pin, selfEndpoint.pinName); + subscribeComponentToPin(component, pin, selfEndpoint.pinName, true); } else if (pin === null) { console.warn(`[WirePin] Could not resolve pin "${otherEndpoint.pinName}" on ${lookupKey}`); }