Commit Graph

930 Commits

Author SHA1 Message Date
David Montero Crespo b567ba2faf i18n(examples): la galeria, toda en ingles
Velxio es internacional y su galeria estaba mezclada. Tres focos:

- examples-robot-desktop: el codigo que el ejemplo ENTREGA al usuario
  llevaba 33 comentarios en castellano ("Tiempo del ultimo movimiento
  detectado", "Cola de estados") y 7 cadenas que el sketch imprime por
  serie ("INICIO DE LA LECTURA DE SENSORES", "Movimiento DETECTADO").
  Traducido todo y reescrito en ASCII, como el resto de la galeria.
- examples.ts: siete comentarios mios en castellano, de cuando anadi las
  resistencias en serie a los LEDs. El resto del fichero estaba en ingles;
  los deje incoherentes.

Sin cambios de comportamiento: solo texto. Los tests de galeria siguen en
verde (167).
2026-07-30 15:22:05 +02:00
David Montero Crespo 31c722b593 feat(pi): salida de pantalla para las Pi + arreglo del guard de connect
Seam nuevo registerBoardBuiltins/getBoardBuiltins: una placa que el arbol
OSS ya dibuja (la familia Pi) puede recibir perifericos del overlay sin
registrar un ProBoardDef, que es lo que secuestraba el arte de la placa y
dejaba los cables en la esquina. El overlay lo usa para llevar los frames
del guest (cv2.imshow) al panel cableado en el canvas.

Arregla ademas el guard de connect() del bridge: comparaba readyState con
WebSocket.OPEN leido del global, asi que cuando esas constantes no estaban
`undefined === undefined` era cierto con socket a null y connect() volvia
sin abrir nada — el bridge se quedaba muerto. Ahora comprueba que el socket
exista y usa el valor numerico. Recupera los 12 tests de
multi-board-integration que esto habia roto.

El test del UART Pi->Uno pasa a comprobar el contrato vigente: por el cable
va onUartTx (el UART del header), no la consola del guest; y se anade el
caso que fija que la charla de arranque NO se filtra al vecino.
2026-07-30 01:09:04 +02:00
David Montero Crespo a0ba1fbd28 fix(uart): el enganche de serie sobrevive a recrear el simulador
El fan-out del Interconnect envuelve el onSerialData de la INSTANCIA y
la marca con un flag. Compilar, resetear o cambiar de motor crea una
instancia nueva: sin flag y sin envoltorio, asi que la placa dejaba de
oirse por el cable a mitad de sesion. Sintoma real: la Pi en modo Linux
encendia el LED del Arduino (Pi -> Uno), pero la respuesta del Arduino
no volvia nunca (Uno -> Pi), porque el Uno habia recreado su simulador
al compilar despues de que se construyeran las rutas.

Se vuelve a enganchar en cada simulatorMap.set (siete puntos: AVR,
RP2040, RISC-V, ESP32, STM32 y los shims).
2026-07-29 20:55:05 +02:00
David Montero Crespo 3f867f7234 chore(pi): traza de que motor toma cada arranque
Una linea por start con el motor elegido, si estaba fijado y el motivo.
Sin ella, una placa que no arranca NADA (motor equivocado, bridge
ausente) es indistinguible de una que arranco bien: no hay error, no hay
proceso y la barra dice lo mismo. El toolbar ya deja su traza
[handleRun]; esta cubre el camino del boton de modo Linux.
2026-07-29 20:33:36 +02:00
David Montero Crespo d98617f1d2 fix(pi): arrancar sin bridge deja de fallar en silencio + test del reinicio
startBoard llamaba `getBoardBridge(id)?.connect()`: si la placa no tenia
bridge, el encadenamiento opcional se lo tragaba y el usuario se quedaba
sin guest, sin error y con la barra mostrando Stop. Ahora avisa por
consola y baja el flag de running, que es lo unico honesto que se puede
hacer ahi.

Test nuevo del camino que usa el boton de modo Linux: fijar el modo,
parar y arrancar tiene que abrir el WebSocket del guest, dejar
engineMode en linux y running en true; y un socket en CLOSING no puede
impedir la reconexion.
2026-07-29 20:10:57 +02:00
David Montero Crespo 6bfeaf1b15 fix(pi): reiniciar en modo Linux vuelve a arrancar el guest
connect() se rendia si el socket no estaba CLOSED, y uno en CLOSING pasa
esa prueba: pulsar "Linux terminal" justo despues de una ejecucion
cerraba el socket y el arranque siguiente no hacia nada -- ni guest, ni
error, y la barra seguia mostrando Stop. Ahora solo se rinde con OPEN o
CONNECTING y descarta el que se esta cerrando.

startBoard marca ademas running en la rama Pi: el boton de Linux
reinicia la placa por su cuenta, sin pasar por la barra, asi que el flag
se quedaba con lo que hubiera dejado la ejecucion anterior.
2026-07-29 19:33:14 +02:00
David Montero Crespo c55e399055 fix(uart): el hook de TX de la Pi se reinstala cuando nace el bridge
Las rutas serie se construyen al cargar la pagina, pero el bridge de una
placa QEMU-Linux nace al pulsar Run: ensureSerialHook encontraba bridge
nulo, hacia no-op y nadie volvia a intentarlo — los bytes que el guest
transmitia por el header salian del backend (uart_tx) y morian en un
onUartTx sin instalar. reensureSerialHooks(boardId) repite el enganche
(idempotente por el flag) y el store lo llama al crear el bridge.
2026-07-29 17:26:02 +02:00
David Montero Crespo 93388c675a fix(pi): perifericos completos en la familia Pi — entradas, buses y pines
Auditoria de "la Pi tiene todo lo de la placa real" con cuatro huecos
encontrados y cerrados:

1) GPIO de entrada en modo Linux: GPIO_IN respondia VAL 0 fijo (stub de
   la fase 2), asi que GPIO.input() leia 0 eternamente aunque el canvas
   empujara el nivel. El backend guarda ahora el ultimo nivel por pin
   (set_pin_state lo escribe) y GPIO_IN contesta de ahi. Los flancos
   (SET) siguen llegando al guest como antes.

2) UART del header hacia otra placa: el shim del rootfs ya hablaba
   `UART <port> TX <hex>` / RX_REQ, pero sin modelo de esclavo el
   backend tragaba los bytes. Ahora TX sin esclavo se emite al canvas
   (uart_tx) y RX_REQ sin esclavo drena la cola que llena pi_uart_rx —
   el mismo protocolo de siempre, sin ops nuevas.

3) El escaner de esclavos I2C/SPI/UART estaba doblemente muerto:
   clasificaba por numero fisico de pin ('3','5','19'...) cuando el
   elemento expone GPIOxx, y su unico llamador era RaspberryPiWorkspace,
   que el terminal unificado reemplazo. Acepta ambos nombres y corre en
   onBooted del store.

4) boardPinToNumber solo mapeaba los pines de la 3/4/5; la Zero, 1B+ y
   2B (mismo header de 40 pines, mismo elemento) se quedaban sin mapa.
2026-07-29 17:05:39 +02:00
David Montero Crespo 508d2e141e feat(uart): el puerto serie del header tambien en modo Linux
La placa QEMU-Linux tiene DOS flujos serie y hasta ahora el cableado
usaba el equivocado: el enrutado entregaba los bytes del vecino a la
consola (el shell) y sacaba al cable la cháchara del arranque. El
header, que es lo que el usuario cablea, no existia.

Ahora el canal de protocolo lleva dos ops nuevas:

  UARTTX <b64>   el guest transmitio por el header -> al canvas
  UARTRX         el guest pregunta que le llego -> UART_RXQ <b64>

El backend guarda una cola por instancia (acotada a 64 KB, que un script
que no lee nunca no la haga crecer) y el websocket acepta `pi_uart_rx`
con los bytes que el vecino manda. En el frontend el bridge gana
onUartTx / sendUartBytes y el Interconnect engancha ESE flujo en vez de
la consola para las placas Pi.

Con esto el mismo script -- import serial, escribir, dormir, leer --
funciona en los dos motores.
2026-07-29 16:53:45 +02:00
David Montero Crespo 2e0be83f23 fix(examples): la resistencia declarada como `resistance` no se aplicaba
El constructor de netlist lee properties.value y cae a 1 kohm si no esta;
once ejemplos declaraban `resistance: '220'`, asi que sus LEDs lucian a
un quinto de lo previsto (brillo 0,16 en vez de 0,72 medido en el
pi-to-arduino-led-control). Se normaliza el nombre de la propiedad.
2026-07-29 08:06:04 +02:00
David Montero Crespo 9160624da4 fix(uart): la Pi puede hablar por serie con la placa de al lado
Dos piezas que faltaban para que pi-to-arduino-led-control fuera algo
mas que un guion imprimiendo lo que "habria enviado".

1) classifyPin no reconocia los pads del header por su nombre. Se llaman
   GPIO14 / GPIO15 en el dibujo de la placa y en todos los cables de los
   ejemplos, pero solo se aceptaban numeros fisicos: parseInt('GPIO14')
   daba NaN, el pin no clasificaba como nada y el Interconnect nunca
   construia la ruta. Ahora se acepta el prefijo GPIO/BCM y la numeracion
   fisica sigue funcionando.

2) Seam de serie para placas que no tienen ni simulador ni bridge: el
   motor de navegador corre el Python de la Pi en la propia pestana.
   registerSerialSink(placa, fn) recibe los bytes que le llegan y
   feedBoardSerialOut(placa, ch) anuncia los que envia, que es lo que el
   enrutado por cables ya sabia repartir.
2026-07-29 07:40:13 +02:00
David Montero Crespo 8e1001740a fix(boards): el overlay ya no secuestra el dibujo de las Raspberry Pi
Dos fallos que salieron probando pi-to-arduino-led-control y
pi5-pir-motion-alarm.

1) BoardOnCanvas mira getProBoard() ANTES del switch OSS para decidir si
   dibuja un elemento del overlay. El overlay registraba un def minimo
   para las seis Pi solo para llevar una linea de setup del guest, y eso
   basto para cambiarles el render: en vez de la ilustracion
   Raspberry_Pi_3_illustration.svg salia la caja esquematica, con otras
   coordenadas de pines, y los cables quedaban colgando en la esquina.
   Ahora hay un registro aparte, registerGuestSetup(kind, linea), que
   lleva la cadena y nada mas; getGuestSetup() la resuelve dando
   prioridad al def del overlay si existe.

2) Las partes de entrada (PIR, botones, sensores) avisan con
   simulator.setPinState(pin, nivel). En una placa QEMU-Linux no hay
   simulador de MCU -- el CPU es el guest -- asi que la llamada acababa
   en la instancia AVR heredada y se perdia: pulsar el sensor no hacia
   nada. traceDetailed devuelve ahora tambien la placa a la que llega el
   pin, y si es de la familia Pi la parte recibe un simulador que empuja
   el nivel al bridge (gpio_in para el guest, el valor pin<N> que leen
   los shims del motor de navegador) y al PinManager de esa placa.
2026-07-29 07:34:01 +02:00
David Montero Crespo 5a79836fdc test(examples): ningun LED de la galeria puede colgar directo de un pin
Guarda de regresion para lo que se acaba de arreglar. Recorre el grafo de
cableado (no solo el vecino inmediato del LED, porque la resistencia
protege igual desde el catodo o desde el otro lado de un transistor o un
rele) y falla si algun LED llega a un pin sin limitar la corriente.

Es el peor tipo de ejemplo roto: el sketch imprime "LED ON", el runtime
quema el LED y no aparece ningun error, solo un LED oscuro.
2026-07-29 07:24:25 +02:00
David Montero Crespo 27774bb3e1 fix(examples): resistencia en serie en todos los LED de la galeria
El modelo electrico es honesto: un LED colgado directo de un pin a 3,3 V
o 5 V pide una corriente absurda, el simulador lo quema y se queda
oscuro aunque el sketch imprima "LED ON". Ya se habia arreglado en los
ejemplos de Raspberry Pi con un solo LED, pero quedaban 24 sin proteger
-- sobre todo los de Pico (i2c-scanner, spi-loopback, adc-read,
multi-protocol, serial-echo, eeprom, rtc) y todos los RGB, que necesitan
una resistencia POR CANAL.

Se anaden 36 resistencias de 220 ohm en serie, cada una entre el pin y
el anodo, colocadas al lado de su LED. Los ejemplos que ya llevaban la
resistencia en el catodo se dejan como estan: protege igual.

Con esto la galeria pasa de 24 LEDs que se queman al pulsar Play a 0.
2026-07-29 07:16:34 +02:00
David Montero Crespo 5e6e76e1da fix(examples): series resistors for the Raspberry Pi gallery LEDs
Every Pi example hung its LED straight off a GPIO pin, so the electrical
model solved 2.4e29 A and burnt it out: the script printed LED ON while
the canvas stayed dark. Written before the SPICE engine landed, and
nobody noticed because reaching that point took a 90 s boot. 220R in
series, like the ESP32 examples already do.
2026-07-29 05:03:02 +02:00
David Montero Crespo b622cad80a feat(metrics): run events carry which engine served them
Without this we cannot tell whether the in-browser path is actually
displacing guest boots — the whole point of the dual engine is a number
(% of runs that never touched the backend), so the event has to say.
2026-07-29 04:41:52 +02:00
David Montero Crespo a334cd3089 feat(serial): serial-actions slot in the monitor toolbar
Generic overlay mount point for per-board terminal actions.
2026-07-29 04:32:39 +02:00
David Montero Crespo 78f0181435 feat(qemu): per-session extra drives + start_pi payload passthrough
A profile's extra_drive is the same file for everyone; an overlay may
need a disk built for THIS session (what the project declared). New
seam set_pi_extra_drive_resolver(fn) receives the client id, the board
and the start_pi payload and returns raw images, mounted read-only after
the profile's own. The WS route forwards msg_data and the bridge gained
startPayload so a client can declare it. Generic: no package manager,
no OS knowledge in the OSS tree.
2026-07-29 04:31:21 +02:00
David Montero Crespo 72e48073bd feat(canvas): board-status slot next to the board selector
Generic overlay mount point for per-board status (empty in OSS).
2026-07-29 04:22:35 +02:00
David Montero Crespo 43ef6c9f1d feat(seams): instant-engine registry for QEMU-Linux boards
Generic seam only — no board, OS or runtime specifics in the OSS tree:
an overlay may register an engine that decides, per board, whether a run
needs the Linux guest or can happen in the browser. The decision is data
(engine + reason + where) so the UI can explain a 90 s boot instead of
just taking it, and BoardInstance carries engineMode / enginePinned so
the user's choice is predictable and the terminal panel knows whether an
interactive shell exists. Nothing registers in OSS: the QEMU path is
untouched.
2026-07-29 04:20:07 +02:00
David Montero Crespo 8163869d31 fix(spice): map micro:bit-style P<n> pad names to pin numbers
The netlist collector only understood GPIO<n>/GP<n>/bare digits, so a
wire on a QEMU-Linux board's P24 Gravity pad never got its V-source
stamped and the LED stayed dark while the guest toggled the pin.
2026-07-28 22:35:46 +02:00
David Montero Crespo 7da01c6a91 fix(toolbar): Run follows the disabled-while-running convention on QEMU-Linux boards; Reset is the fast re-run
Keeping Play enabled while the guest ran broke the convention every
other board follows (owner report). Run now disables as usual; RESET on
a booted guest re-uploads the edited files and re-runs the script
without the ~45 s reboot, matching Reset's restart-the-program meaning.
2026-07-28 22:05:36 +02:00
David Montero Crespo 330b9ed1b2 feat(pi-family): quietBoot — hide the shared rootfs' branded boot chatter
The generic arm64 image prints another product's banner/motd/login line
during boot, before guestSetup can re-brand the guest. Boards with
quietBoot show a neutral '[Velxio] Booting <label> (Linux guest)...'
progress line (dots every 4 s) while boot detection and the prompt-gated
upload still run underneath; the shell is revealed (already re-branded)
right before the auto-run command, so the user's first visible output is
their own program.
2026-07-28 21:07:21 +02:00
David Montero Crespo a766d0cde3 fix(pi-family): per-tab client_id for the QEMU-Linux WebSocket
With the bare boardId as client_id, two tabs (or two users) on the same
example shared one QEMU instance: serial output went to whichever socket
connected last, keystrokes interleaved and one tab's stop killed the
other's guest. Suffix the tab session id so each tab gets its own
instance (stopped on its own disconnect, like the ESP32 workers).
2026-07-28 20:44:51 +02:00
David Montero Crespo cdf4aad4ae fix(editor): Pi kinds end in digits — test the full board id before stripping the instance suffix
'raspberry-pi-3' minus the numeric suffix is 'raspberry-pi', which
matches no kind, so freshly added Pis regressed to a sketch.ino group.
2026-07-28 20:15:56 +02:00
David Montero Crespo 248d8b5e97 fix(toolbar): Run stays enabled on running QEMU-Linux boards — re-run without reboot was unreachable 2026-07-28 20:01:39 +02:00
David Montero Crespo bbafe66ee7 feat(pi-family): unified editor UX — Monaco + explorer + bottom xterm, one file surface
QEMU-Linux boards used three competing file surfaces (workspace group
with a meaningless sketch.ino/libraries.json, the VFS panel with its
Upload button, and the Pi workspace's own editor). Now they behave like
every other board:

- the editor file group defaults to script.py for ANY Pi-family kind
  (kind-based check via isPiBoardKind, not the old raspberry-pi- string)
- the libraries.json manifest row is hidden for Pi boards
- EditorPage always renders Monaco; the RaspberryPiWorkspace swap is
  gone
- the bottom serial panel renders the interactive xterm (PiTerminal,
  now seeded with session history) for running Pi boards
- example vfsFiles load into the editor group (single source of truth);
  the run path uploads the group into the guest home
- Run on a booted guest re-runs without the 45 s reboot (Ctrl-C +
  re-upload + run); starting a Pi board pops the terminal open
- piSyncAndRunScript/piRerunScript exported from the store; auto-run
  now applies to the whole family (guestHome/autoRun overridable)
2026-07-28 19:43:20 +02:00
David Montero Crespo dd86020343 feat(pi-family): one-click run UX — autoRun + guestHome + clean serial mirror
- ProBoardDef.autoRun: after boot (+guestSetup) the VFS uploads itself
  and the command runs, so a single Run click boots, uploads and starts
  the user's script (same UX as compiled boards)
- ProBoardDef.guestHome: VFS home dir override ('/root' for guests that
  log in as root); those boards drop the historic hello.sh sample
- upload sequence extracted to utils/piUpload (shared by the VFS panel
  button and autoRun)
- serial monitor strips DEL/C0 control echoes (backspace showed tofu)
2026-07-28 16:28:02 +02:00
David Montero Crespo ebe2ab9d5a merge: trabajo pi-family paralelo + fix de swipe tactil (ramas concurrentes del submodulo) 2026-07-28 16:09:57 +02:00
David Montero Crespo 4ec34c2a5f fix(canvas): un swipe sobre una pantalla tactil en Run ya no panea el canvas
El caso ownsPointer retornaba sin stopPropagation y el mousedown llegaba al
fondo del canvas, cuyo convenio arrastre-izquierdo-panea movia el mundo
entero bajo el dedo a mitad de swipe (solo el tap funcionaba). El modelo
tactil escucha POINTER events — stream aparte — asi que cortar el mousedown
(y el touchstart movil) no le quita nada. Los knobs wokwi conservan el
pass-through de siempre.
2026-07-28 16:09:27 +02:00
David Montero Crespo 3803a41a07 fix(canvas): attachBuiltins keyed on a stable run signature, not boards identity
The boards array is replaced on every serial batch, so the attach effect
detached/re-attached built-in peripherals at up to 60 Hz while output
flowed; events landing inside the 500 ms re-attach window were lost.
One-shot streams (a guest display list) never recover — the repainting
SPI LCD decoders masked this for ESP32 boards.
2026-07-28 15:50:49 +02:00
David Montero Crespo 0203b19ee5 fix(canvas): el chequeo de asiento se repite tras montar — el primer render lo congelaba
isBoardSeated lee pinInfo/boardSocket del DOM; en el primer render de un
ejemplo que ABRE con la placa ya posada ni la placa ni el zocalo estan
montados, el memo devolvia 'no posada' y no recalculaba nunca (z=0 pese a
asiento exacto medido). Re-chequeo al siguiente frame y a los 400ms.
2026-07-28 15:49:25 +02:00
David Montero Crespo 3ca8bf0e6e feat(canvas): contrato ownsPointer — tocar una pantalla tactil en Run no la arrastra
La whitelist de tags que poseen el puntero durante la simulacion gana una
salida generica regla-6a: el elemento declara `get ownsPointer() { return
true }` y el wrapper no inicia el drag mientras corre. El cristal del Round
Display pintaba el punto verde Y arrastraba el shield por el canvas a la vez.
2026-07-28 15:33:24 +02:00
David Montero Crespo 98df5134cb fix(canvas): las placas solo pintan encima cuando estan POSADAS en un zocalo
El bump global a zIndex 3 (efecto iman del Round Display) puso TODAS las
placas por encima de TODOS los componentes: una resistencia al lado de un
Arduino quedaba oculta tras la placa en cualquier ejemplo normal (se veian
los cables, no el cuerpo). Ahora isBoardSeated (misma matematica del snap,
umbral 0.5px) decide: posada -> z3 encima de su zocalo, como el XIAO fisico
apilado en el shield; libre -> z0 debajo de los componentes, como siempre.
El iman coloca la placa exactamente en el asiento, asi que al capturarla
salta al frente y al arrancarla vuelve abajo.
2026-07-28 15:22:56 +02:00
David Montero Crespo 1cdcb5a967 feat(pi-family): built-in peripheral plumbing for overlay QEMU-Linux boards
- profile key extra_drive: optional read-only second virtio-blk so an
  overlay can ship guest-side shim libraries (/dev/vdb)
- SENS <name> protocol op: canvas-fed named values (built-in sensors /
  buttons) served from PiInstance.sensor_state, pushed by the frontend
  via the new pi_sensor_state WS message
- DISP <b64> protocol op: guest display commands forwarded to the
  frontend as 'display' events (built-in screens)
- RaspberryPi3Bridge: onDisplay / onGpioPwm callbacks + setSensorState
- SimulatorCanvas hands piFamily boards their Pi bridge in
  attachBuiltins (was ESP32-only)
2026-07-28 15:06:25 +02:00
David Montero Crespo 55dd25eeba feat(pi-family): guestSetup seam + board-branded workspace texts
Overlay QEMU-Linux boards are not Raspberry Pis: ProBoardDef.guestSetup
lets a board send one shell line at the boot prompt (hostname/PS1/clear)
to de-brand the generic image, sent before piBooted flips so uploads
cannot interleave; the workspace start button and power-on title carry
the board's own label for non raspberry-pi kinds; the compile console
line uses the board label instead of 'Raspberry Pi 3B'.
2026-07-28 14:36:13 +02:00
David Montero Crespo 7bb54e8d32 fix(toolbar): Run on QEMU-Linux boards boots directly — no bogus firmware error
The QEMU branch of handleRun required compiledProgram, but Pi-family
boards never have one (handleCompile early-returns 'no compilation
needed' without producing firmware), so the toolbar Run always surfaced
'Compilation produced no firmware' instead of powering the board on.
Start them directly, same as the workspace Start button.
2026-07-28 14:13:03 +02:00
David Montero Crespo 6babaf08e1 feat(seams): overlay-registered QEMU-Linux board kinds + backend profile registry
registerPiFamilyKind + ProBoardDef.piFamily route an overlay board through
the existing Raspberry Pi bridge path (WebSocket qemu, VFS panel, boot
terminal); register_pi_board_profile lets the overlay add the matching
PI_CONFIGS entry (cpu/image_set/...) at registration time.
2026-07-28 08:45:56 +02:00
David Montero Crespo 6a78268a0b chore(picker): coma doble en ONLINE_ONLY_COMPONENT_ADS — elemento hueco fuera 2026-07-28 08:25:48 +02:00
David Montero Crespo a419b13cb1 fix(esp32): el mapa GPIO->canal ADC del shim pregunta al puente
setAdcVoltage/setAdcWaveform solo conocian el mapa del ESP32 clasico
(GPIO32..39): en un S3 devolvian false para TODOS los pines y el part no
podia alimentar el ADC. Ahora el shim consulta bridge.adcChannelForGpio si
existe (los puentes js exponen el mapa de su chip) y cae al mapa clasico
solo si no.
2026-07-28 04:15:04 +02:00
David Montero Crespo 0b7c9e1dd6 fix(esp32): completeTransfer del shim SPI ya no es un no-op — el MISO de los parts llega al guest
El adaptador SPI del Esp32BridgeShim descartaba el MISO de los parts ('el worker
lo lleva por _spi_response'), cierto solo en la era QEMU: cualquier part SPI que
RESPONDE (una SD contestando CMD0) hablaba con nadie en modo js — medido como
SD.begin()=0 con sd_diskio reintentando CMD0 para siempre en el Round Display.

Ahora reenvia a bridge.setSpiResponse, que todos los puentes tienen: el de QEMU
lo manda al worker, y los motores js fijan el byte que su SpiForwarder devuelve
para ESTA transferencia (toda la cadena onByte corre sincrona dentro del
transfer del motor). El reposo lo restaura el decodificador compartido, que ya
completa cada byte ajeno con 0xff.
2026-07-28 02:01:15 +02:00
David Montero Crespo a7015239b3 fix(camera): la animacion inline pisaba las hormigas de la clase
El boton conservaba animation:'none' inline fuera del estado requesting, y el
estilo inline gana a la clase: .velxio-ants quedaba con animationName none —
medido con getComputedStyle en staging. Inline solo mientras solicita permiso;
en el resto, undefined para que la clase anime.
2026-07-27 19:40:54 +02:00
David Montero Crespo 9d8cc0976f fix(examples): la ILI9341 del preview de camara, girada 90 y sin cables sobre el cristal
El modulo es retrato con la botonera de pines en el borde INFERIOR y el sketch
dibuja en setRotation(1): segun el mapeo MADCTL del propio modelo (writePixel:
el borde superior del contenido rot-1 cae sobre el borde IZQUIERDO del canvas),
verlo derecho exige girar el modulo un cuarto horario — igual que en el banco
fisico. El mismo giro lleva los pines al borde izquierdo, de cara a la placa,
asi que los cables van rectos entre ambos en vez de por encima del cristal.
2026-07-27 18:31:16 +02:00
David Montero Crespo 126ff4fafc feat(camera): el boton de camara pide el click con borde de hormigas en marcha
Cuatro tiras de gradiente repetido, una por borde, deslizando un periodo de
guion por ciclo — fondo y no border porque un borde CSS no puede animar su
dash-offset. currentColor hereda el color de estado (gris en reposo, rojo en
error). Se apaga en streaming/solicitando, y con prefers-reduced-motion.

Ojo del gato encerrado: el estilo inline usaba el shorthand background, que
pisa el background-image de la clase — pasa a backgroundColor.
2026-07-27 18:27:30 +02:00
David Montero Crespo 5177b82791 feat(camera): la webcam arranca sola al ejecutar en una ESP32-CAM
Un sketch de ESP32-CAM existe para capturar, asi que esperar el click en el
toggle se lee como 'la camara esta rota': el guest arranca, esp_camera_init
completa contra el OV2640 modelado, y cam_hal agota el tiempo eternamente
esperando fotogramas que nadie envia — sin pedir siquiera el permiso.

Ahora la webcam se solicita al iniciar la ejecucion; el prompt del navegador ES
el consentimiento, y el toggle queda como apagador manual. Solo una vez por
ejecucion: parar el stream a mano no lo re-dispara.
2026-07-27 18:13:21 +02:00
David Montero Crespo 1e6b7efddc fix(canvas): las placas pintan por encima de los componentes
Estaban a zIndex 0 con los componentes a 1: cualquier solape las escondia. El
caso que lo decide es el apilado — una XIAO posada en el zocalo del Round
Display debe ser lo que se ve, como la placa recien soltada queda arriba del
monton. Los montajes lado a lado no solapan, asi que nada mas cambia.
2026-07-27 18:03:43 +02:00
David Montero Crespo 24d25966a8 feat(canvas): iman de zocalo — una placa se posa sobre un shield que la acepta
Como el iman de la breadboard pero para PLACAS: un componente puede declarar en
su elemento (estilo regla 6a, igual que pinInfo) que lleva un zocalo:

    get boardSocket(): { anchorPin, accepts }

y una placa cuyo boardKind case con accepts, arrastrada cerca, se posa de golpe
con su pad anchorPin sobre el homonimo del zocalo. Un solo ancla basta porque
ambas rejillas comparten paso — esa es la gracia de un zocalo. Arrastrarla mas
alla de la tolerancia la suelta, sin estado que recordar.

Enganchado en los dos caminos de arrastre de placas (raton y tactil). El
overlay privado declara el zocalo sin que este repo sepa que existe.
2026-07-27 17:36:52 +02:00
David Montero Crespo a13eb3e7e2 fix(adc,examples): mapa ADC por chip, entradas flotantes y una auditoria de la galeria
Tres cosas que salieron al tirar del hilo de los pull-ups.

1) ESP32_ADC_PIN_MAP era la tabla del ESP32 CLASICO aplicada a toda la familia, y
   fallaba de dos maneras:
     - S3: sus pines ADC son GPIO1..20, ninguno esta en esa tabla, asi que la
       busqueda devolvia undefined y el listener del potenciometro no llegaba a
       engancharse. El mando no hacia nada.
     - C3/C6: GPIO0 SI esta en la tabla clasica, como ADC2_CH1 -> canal 9. Pero en
       esos chips GPIO0 es ADC1_CH0 y sus motores toman un indice de canal de ADC1
       (0..4). El valor se empujaba al canal 9, fuera de rango, descartado en
       silencio mientras el firmware leia el 0. Una respuesta equivocada en vez de
       ninguna, que es peor.
   adcPinMapFor(boardKind) devuelve la tabla del chip.

2) El ejemplo de las gafas OLED en MicroPython declaraba sus botones con Pin.IN a
   secas. Van entre 3V3 y el pin, asi que sin pull-down el pin queda FLOTANDO con
   el boton abierto. El propio ejemplo ya lo avisaba en un comentario ("Add
   Pin.PULL_DOWN if the pin floats") — ahora lo hace. Es un fallo de hardware de
   verdad, no un artefacto del emulador.

3) gallery-run-gate.audit.test.ts: EditorToolbar bloquea el Run cuando el
   verificador de circuito saca errores, asi que un circuito invalido no es
   cosmetico, es la diferencia entre un ejemplo que arranca y uno que parece
   muerto. Y esos defectos se esconden hasta que el circuito resuelve DE PUNTA A
   PUNTA: c3-button llevaba un LED de 506 mA que nadie veia porque sus cables
   apuntaban a pines inexistentes.

   El test reproduce la puerta del Run tal cual (mismo snapshot de peor caso,
   mismo buildInputFromStore, mismo verifyCircuit, ngspice de verdad) sobre los
   227 ejemplos. Encontro tres mas sin resistencia en serie —nano-button-led,
   mega-led-chase y mega-serial-control, 17 LEDs en total— y ahora quedan 0.
   A partir de aqui, un ejemplo nuevo con un LED colgado del GPIO salta en CI.
2026-07-27 05:01:49 +02:00
David Montero Crespo 3d29288764 fix(examples): resistencia en serie para el LED de c3-button y pico-button-led
Los dos colgaban el LED directo del GPIO. Con los cables del pulsador arreglados
el circuito por fin resuelve entero, y el verificador saca lo que llevaba ahi
desde siempre: 506,9 mA por un LED de 20 mA. Como un error de circuito bloquea la
ejecucion (EditorToolbar checkOrBlock devuelve false y saca el modal de
"ejecutar de todos modos"), el ejemplo se quedaba sin arrancar.

O sea que no era un fallo nuevo, era uno viejo que hasta ahora nadie podia ver
porque el circuito no llegaba a resolverse. 220 ohm en serie, igual que el resto
de ejemplos con LED (esp32-blink-led y esp32-doom ya lo hacian bien).
2026-07-27 01:53:42 +02:00
David Montero Crespo 715e93c610 fix(esp32): el boton activo a nivel bajo — que lo decida el circuito, no una constante
El canvas reenviaba la pulsacion de un pulsador cableado como
sendPinEvent(gpio, true), o sea "pulsado = ALTO". Para el idiom canonico de
Arduino — pin -> pulsador -> GND con INPUT_PULLUP, activo a nivel BAJO, que usan
13 de los 18 ejemplos con entrada de usuario — eso esta al reves: pulsar conducia
el pin a su nivel de REPOSO y soltarlo al ACTIVO. Ademas escribia directamente en
el latch de entrada del emulador por detras de connectDigitalInputsToMcu,
desincronizando la cache lastLevel de la que ese fichero se declara unico
escritor. La victima visible era esp32-doom: cuatro botones que hacian lo
contrario de lo que pulsabas.

No lo sustituyo por la polaridad opuesta, que seria la misma adivinanza al reves:
cuando el simulador resuelve entradas por el circuito (spiceDrivenInputs), el
nivel sale del propio cableado. El INPUT_PULLUP del guest se reporta ahora como
gpio_pull, estampa una resistencia de 45k al riel de 3V3, y cerrar el pulsador
cortocircuita el nodo contra la pata que tenga al otro lado. Sale bien tanto para
un boton a GND como para uno a 3V3, sin que nadie asuma nada. El atajo se queda
solo para simuladores que se salgan del modelo electrico.

Aparte, dos arreglos de cableado:

  - boardPinToNumber devuelve -1 (no null) para pines de alimentacion y masa, y
    la guarda comprobaba `=== null`. Asi que la pata de GND de cada boton colaba
    y registraba un SEGUNDO par de listeners apuntando al pin -1.

  - c3-button y pico-button-led cableaban sus pulsadores a los pines '1a' y '1b',
    que NO EXISTEN: el wokwi-pushbutton expone 1.l, 2.l, 1.r y 2.r. Esos dos
    cables llevaban colgando desde siempre, asi que esos botones no han
    funcionado nunca. Pasan a 1.l / 2.l, como el resto de ejemplos.
2026-07-26 23:31:47 +02:00
David Montero Crespo a698534b7b fix(pins): la cabecera del M5 Cardputer ya resuelve a numeros de GPIO
boardPinToNumber no tenia rama para 'cardputer-adv', asi que caia al return
null del final: cada cable a su cabecera EXT o al Grove Port A se conectaba a
nada. La pieza quedaba en el lienzo, el sketch leia un pin muerto y nadie
avisaba de nada.

La rama compartida de esp32 tampoco habria servido: recorta en 39 y este board
saca G40 en la cabecera. El S3 tiene 48 GPIOs.
2026-07-26 02:47:05 +02:00
David Montero Crespo 0f68037c35 feat(canvas): fuera el minimapa
Se anclaba a la esquina inferior derecha del lienzo y tapaba una parte del area
util del circuito, que es justo donde se trabaja. Quien pana y hace zoom no lo
necesitaba para orientarse, asi que se retira entero en vez de esconderlo tras
un interruptor mas.

Se van el componente y su CSS (no los usaba nadie mas) y la linea del listado de
novedades que lo anunciaba, que ya no seria cierta.
2026-07-26 00:59:58 +02:00
David Montero Crespo fd6829f645 merge: master (produccion) dentro de v3.2
Las dos ramas habian divergido: master llevaba el modo lenguaje ESP-IDF puro
(#139) y v3.2 la ruta de compilacion IDF v5.5 para toda la familia ESP32 mas
los arreglos de venv/toolchain. Ambas tocaban espidf_compiler.py.

Los dos lados son ejes ORTOGONALES y se conservan enteros:

  - use_idf5 / arduino_mode (v3.2): que arbol IDF usa el build (5.5 vs 4.4) y
    si cabe Arduino-como-componente.
  - pure_idf (master): el modo LENGUAJE que elige el usuario; sus ficheros son
    las fuentes del componente main con su propio app_main().

Resolucion:
  - _build_env acepta los tres. Un build IDF puro fuerza arduino_mode a falso:
    la plantilla CMake mete el componente arduino-esp32 en cuanto existe
    ARDUINO_ESP32_PATH, asi que dejarlo puesto compilaba el core de Arduino en
    un build que no tiene sketch. Lo cazaron los tests de master.
  - VELXIO_PURE_SKETCH solo con pure_idf, nunca con arduino_mode a falso a
    secas: un target sin core arduino-esp32 sigue entregando un SKETCH al
    traductor legacy y no debe tomar la rama del glob puro.
  - La identidad del build-dir suma los dos tokens (|idf:N|ard:N y |lang:pure):
    ningun par de esas combinaciones puede compartir un build/ configurado.
  - La cadena de escritura de fuentes queda pure_idf -> arduino_mode -> legacy.
  - sdkconfig: render de v3.2 (con target/use_idf5) mas el filtrado de simbolos
    CONFIG_ARDUINO* de master cuando el build es puro.

test/backend/unit/test_espidf_compiler.py: los 7 tests que ya estaban rotos en
v3.2 (AttributeError: idf5_path, fixture sin actualizar desde que se anadio la
seleccion de IDF) vuelven a pasar.

Verificado: backend 293 pasan / 0 fallan (v3.2 traia 7 rotos); frontend 2268
pasan / 0 fallan en los dos shards.
2026-07-25 21:52:00 +02:00
velxio-deploy a79d8fd563 chore(examples): refresh 1 thumb file(s) [auto] 2026-07-24 07:36:40 +02:00
David Montero 4ff281bd40 fix(spice): MCU-edge listeners detach on resubscription for QEMU boards (frozen LEDs)
connectMcuEdgesToService resubscribes its per-pin listeners whenever
pinNetMap changes. The subscription swept pin NUMBERS 0..63 and
reverse-mapped them to names ('GPIO2' on ESP32, 'GPIO17' on Pi) to match
against pinNetMap's keys — but those keys are the WIRE pin names ('2',
'4', 'A0'), so after the first solve the match failed for every pin and
the resubscription attached nothing. Any mid-run pinNetMap update then
silently killed the MCU-edge → SPICE path and the canvas froze at the
last solved state while the firmware kept toggling.

Masked until now because nothing perturbed pinNetMap mid-run on the
blink examples; pure ESP-IDF mode (#139) unmasked it — gpio_reset_pin()
leaves the internal pull-up enabled, the worker reports gpio_pull, the
handler requests an electrical resolve, pinNetMap gets a new identity,
and the ESP-IDF blink example's LED froze ON.

Fix: subscribe FROM the pinNetMap names, mapped to PinManager pins with
the same pinNameToArduinoPin the netlist collector uses (STM32 via
stm32PinNameToLinear), and hand schedulePin the netlist name so
handleMcuEdge's v_<board>_<pin> lookup hits the fast alterSource path
instead of a full rebuild per edge. The 0..63 sweep remains as the
pre-first-solve fallback. Also fixed pinNameToArduinoPin's dead 'GPIO'
branch ('GP' tested first turned 'GPIO32' into parseInt('IO32') = NaN).
2026-07-24 07:26:09 +02:00
David Montero 734b7d0487 feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.

Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
  compile paths and folded into the dedup job key (language='arduino'
  and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
  as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
  resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
  and VELXIO_PURE_SKETCH raised so the template CMake compiles the
  user's own sources via a glob branch. Pure builds get their own
  persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
  detection sets has_wifi, and literal #define SSID/PASS plus
  wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
  mode (the symbols don't exist without the arduino component).

Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
  family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
  driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
  (the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).

Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 06:37:01 +02:00
David Montero Crespo 57ffc5c085 feat(spice): registerSpiceMapper seam for overlay-registered components
componentToSpice/isSpiceMapped now consult a pro-registered mapper table after
the static MAPPERS, so a private build can give its closed components a SPICE
model (e.g. the DFRobot Gravity analog sensors emit a voltage source at AOUT).
Empty in a pure OSS build. Mapper type exported.
2026-07-24 03:56:15 +02:00
David Montero Crespo a9fcf69035 feat(picker): ONLINE ad cards for the Seeed components (CP4)
Round Display, Grove Gesture and ReSpeaker Lite now advertise themselves in
the OSS picker like the M5Stack matrices; they auto-hide in any build whose
overlay merges the real components (registry.getById).
2026-07-24 01:45:47 +02:00
David Montero Crespo 4392a91539 fix(boards): isBoardComponent recognizes overlay-registered boards (CP3)
A wire to a pro board's Dx pad (XIAO 'D2', etc.) did nothing because
isBoardComponent only knew the OSS board-id list, so the electrical/sensor
chain skipped the board endpoint. It now also returns true for any
proBoardRegistry kind, so boardPinToNumber (which already consults the pro
def's pinToNumber) resolves the Dx name. Fixes the numeric-alias-only
workaround in the XIAO examples.
2026-07-24 01:43:15 +02:00
David Montero Crespo c819fea74b feat(picker): UNIHIKER M10 ONLINE ad card (DFRobot SBC) 2026-07-23 21:52:24 +02:00
David Montero Crespo 116afcab98 feat(sensors): runtime seam for overlay-registered sensor controls
registerSensorControls() lets a private build add SensorControlDef entries for
sensors it ships outside the OSS tree (e.g. the DFRobot Gravity analog family)
so they get the live slider panel; every SENSOR_CONTROLS lookup now goes
through getSensorControl(id) which falls back to the registered map. Dead code
in a pure OSS build, same contract as proBoardRegistry / registerComponentDoc.
2026-07-23 21:32:14 +02:00
David Montero Crespo 69819ee614 feat(picker): ONLINE ad cards for the Seeed Studio XIAO trio
xiao-esp32s3-sense / xiao-esp32c6 / xiao-rp2040 advertise themselves in the
OSS picker like the other hosted-only boards; the ads auto-hide in any build
whose overlay registers the real kinds (same BOARD_KIND_LABELS contract).
2026-07-23 08:26:33 +02:00
David Montero Crespo c6ec887a21 fix(picker): declare the version hooks BEFORE the memos that list them as deps
filteredComponents' new registryVersion dep evaluated in its useMemo deps
array while the const was still in the temporal dead zone (declared further
down the component) — 'Cannot access N before initialization', white screen
on every page. Hooks moved up next to the registry declaration.
2026-07-23 07:20:34 +02:00
David Montero Crespo 5beccf94a6 fix(picker): late overlay registration re-renders the picker — no more reload races
The @pro overlay import is dynamic, so board/component registration can land
AFTER the picker mounted and memoized its lists. allBoards had frozen deps —
if the picker rendered first, overlay boards (M5Stack Core, Cardputer,
Pimoroni, C6) vanished for the whole session while their ONLINE ads were
already hidden; the component grid + ONLINE component ads flipped between ad
cards and real entries depending on who won the reload race (different SVG,
ONLINE badge appearing and disappearing, wrong hover thumbnail).

proBoardRegistry and ComponentRegistry.mergeComponents now bump a version and
notify subscribers; the picker's allBoards / filteredComponents /
visibleComponentAds memos key off those via useSyncExternalStore — same
contract as proRoutes and registerProExamples.
2026-07-23 06:59:46 +02:00
David Montero Crespo 75680a408c fix(picker): variant-true previews + datasheet popover above the modal
- The card preview creates the live element but only forwarded
  defaultValues.value, so variants sharing a tag rendered identically (both
  M5Stack Chain matrices showed the dark RGB housing — the mono flag never
  reached the element). Forward every defaultValues entry, matching what
  DynamicComponent assigns at placement.
- The hover datasheet panel sat at z-index 2000 while the picker overlay was
  raised to 9000 (above the AI chat), hiding the popover behind the very
  modal that summons it. Raise it to 9100.
2026-07-23 05:15:31 +02:00
David Montero 4766789632 feat(landing): hero shows the weather-station circuit gif running
Replaces the static hero-editor screenshot with the animated
estacion-meteorologica-esp32.gif (whole circuit executing). contain +
matching background so the full circuit stays visible instead of
cover-cropping it.
2026-07-22 22:56:32 +02:00
David Montero Crespo 78f5f5ebc3 feat(proBoardRegistry): attachBuiltins — overlay-owned built-in peripheral wiring
Boards with built-in hardware (LCD on the element's own canvas, speaker,
on-board buttons/keyboard) need run-time wiring between the DOM element and
the board's simulator shim / ESP32 bridge. One generic SimulatorCanvas effect
now hands those handles to the overlay's attachBuiltins shortly after run
start and runs its cleanup on stop — no board names in the OSS tree.
2026-07-22 22:33:46 +02:00
David Montero Crespo d90e738995 fix(examples): example pages re-render when the overlay registers late examples
registerProExamples() now notifies useSyncExternalStore subscribers (same
contract as proRoutes): a direct URL to an overlay-registered example resolved
before the dynamic @pro import landed and stuck on the 404 branch. The three
example pages subscribe to the version counter.
2026-07-22 21:43:03 +02:00
David Montero Crespo a7c36f7a7b fix(proBoardRegistry): loadFirmware receives the store's compiled artifact as a string
compiledProgram is the base64/hex string the backend returned (what
RP2040Simulator.loadBinary consumes) — not raw bytes.
2026-07-22 21:22:40 +02:00
David Montero Crespo 32958640a3 feat(boards): runtime board-registration seam for private overlays
registerProBoards() lets a hosted overlay ship boards outside the OSS tree as
data-only definitions: registration patches the exported BoardKind maps
(labels / FQBN / MicroPython) so every existing read site keeps working, and
the sites a map can't cover consult the registry — canvas render (custom
element or overlay render fn), BOARD_SIZE, pin-name mapping, picker list +
descriptions + tag, ESP32 family routing, in-browser simulator construction
and firmware load (structural ProBoardSimulator contract, duck-typed PIO
attach/detach), built-in bridge sensors, and a CS-gated built-in microSD
(sdCsPin -> sd_card.cs_pin worker config). Esp32Bridge additionally gains the
esp32-c6 machine type + TX pin (public chip knowledge — the C6 compile path
already ships) and a generic sendKey() for built-in matrix keyboards.
registerProExamples() appends gallery examples at runtime; the board ONLINE
ads recompute at render so registration hides them. OSS behavior without an
overlay is unchanged — the registry is dead code, same as the other seams.
2026-07-22 21:21:21 +02:00
David Montero Crespo 9e8381e032 feat(picker): online-only COMPONENT ad cards + overlay datasheet seam
Components that only exist in the hosted editor now advertise themselves in
the picker exactly like the online-only boards do: ONLINE_ONLY_COMPONENT_ADS
(same module as the board ads) renders an ONLINE-badged card that links to
velxio.com, auto-hidden in any build whose ComponentRegistry has the real
component (the hosted overlay merges it in — no per-build switches). First
entries: the M5Stack Chain RGB/Mono 8x8 matrices. componentDocs gains
registerComponentDoc(id, raw) so an overlay can supply the hover datasheet
for components it injects at runtime.
2026-07-22 20:39:54 +02:00
David Montero 249fb24ebb docs(public): add estacion-meteorologica-esp32 running-circuit demo gif 2026-07-22 19:38:45 +02:00
David Montero Crespo 9b512e110f fix(registry): resolve brand-prefixed metadata ids (wokwi-lcd2004 -> lcd2004)
Gallery templates and agent-loaded projects can store element tag names
("wokwi-lcd2004") where the registry keys by the bare id ("lcd2004").
getById now falls back to the stripped id, so such a component renders
instead of sitting invisible in the store — a real agent session shipped
an LCD counter whose LCD existed in the store, was wired and validated,
and simply never appeared on the canvas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:06:15 +02:00
David Montero Crespo 8c1bbbc1a7 fix(7segment): rebuild per-element sim state when the digit count changes
Root cause of "el agente construye el reloj, dice que funciona, pero el
display queda en blanco hasta recargar la página" — diagnosed by driving
the live agent end-to-end and instrumenting the element:

The 7-segment part simulator caches its state (segments, digitValues,
digitEnabled) in a WeakMap keyed by the DOM element, sizing it from
element.digits at FIRST access. The agent builds incrementally: it adds
the display with the default digits=1 and only then sets digits=4 — so
the cached state was born in single-digit mode. Every later attachEvents
(compile bumps hexEpoch → re-attach with the finished wiring) kept
consulting the stale state: it subscribed COM.1/COM.2 (which don't exist
on a 4-digit part) instead of DIG1..DIG4, and because those resolvers DID
attach, the all-digits-on fallback never kicked in either. Result: no
digit ever enabled, no flush ever ran, values stayed a frozen 8-zero
array. A page reload "fixed" it because the fresh element mounted with
digits already 4.

get7SegState now compares the cached digit count against the element's
current value and rebuilds the state when they differ, so any re-attach
after a digits change subscribes the right pins.

Test: attach with digits=1 (COM subscribed), set digits=4, re-attach →
DIG1..4 subscribed, and a segment+digit pulse actually lights values[0]
in the 32-slot array.

Verified live: the exact agent prompt that produced a permanently blank
display now shows the multiplexed digits + blinking colon in-session,
no reload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:16:56 +02:00
David Montero Crespo fce1cdafdc fix(esp32): force clean reconnect in Esp32Bridge.connect() — the real run-after-agent fix
The stop-first guard in the Run button only fires when board.running is
true, but the Run button is DISABLED while a board runs — so by the time the
user can actually click Run, the board has already disconnected
(running=false) and the guard is a no-op. The failure lives one level down:
Esp32Bridge.connect() early-returned whenever a socket lingered in ANY
non-CLOSED state (CONNECTING/OPEN/CLOSING). The agent's run_simulation
leaves such a socket; when its backend QEMU session ends but the frontend
socket is still zombie, the user's Run → startBoard → connect() did nothing.
A page reload "fixed" it only by constructing a fresh bridge.

connect() now tears down any lingering socket (detaching handlers + close)
and opens a new one to the same session key — exactly what the reload does,
which is why the reload always worked. The backend already handles a new WS
replacing an existing session, so no reload is needed.

Test: connect() on an OPEN socket closes the old one and boots a fresh
start_esp32 (esp32-dht22-flow).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 07:01:05 +02:00
David Montero Crespo 9c26174c93 fix(sim): clean restart on Run after agent + display body occludes crossing wires
Two issues from a real ESP32 7-segment clock the agent built.

Run after the agent didn't work until a page reload
---------------------------------------------------
The agent's run_simulation leaves the ESP32 board RUNNING (live QEMU
WebSocket). Esp32Bridge.connect() is a no-op while the socket is non-CLOSED,
so the user's subsequent Run click called startBoard() → connect() → did
NOTHING. And if the backend QEMU session had since died while the frontend
socket lingered (CONNECTING/OPEN/CLOSING), the user saw a dead sim that only
a reload cleared — exactly the "di Run y no funcionó; recargué y sí" report.
The Arduino/C++ QEMU path now stops a running board first (closing the WS),
waits for it to settle, then boots fresh — the MicroPython path already did
this for the same reason.

Wires painted over the 7-segment digits
----------------------------------------
The agent bridges each segment strip to its resistor from a breadboard hole
that is physically UNDER the seated display; on the flat canvas those wires
(wire layer z 35) painted over the digits (component z 1) — "casi ni se ven
los dígitos". A large-bodied display seated on a breadboard now renders
ABOVE the wire layer, so its face occludes the wires crossing it exactly as
the real part's body would (the wire passes behind it to reach the hole).
Scoped to display bodies (7segment, matrix, oled, lcd, ili9341, led-ring…)
and only when actually seated; thin parts and free-floating displays are
untouched. The pin overlay + seated-pin markers share the display's stacking
group, so they rise with it and wiring still works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 06:46:57 +02:00
David Montero Crespo d726139946 feat(breadboard): one-hole-one-wire selection rules + jumper colors
Fixes the reported breadboard wiring UX ("requerimos un vocabulario"):

Vocabulary implemented (breadboardOccupancy.ts, pure + unit-tested):
  - 1 hole = 1 wire. A hole already holding a visible wire can't start a
    new one — clicking it SELECTS that wire. This is the core fix: wires
    running hole-to-hole across the board were impossible to select
    because the pin overlays swallowed every click and silently started a
    new wire (so the top horizontal rail wire was un-deletable).
  - Same 5-hole strip / rail = one net. When a new wire end lands in an
    occupied hole (a seated leg or another wire), it shifts to the
    NEAREST FREE hole of the same group — electrically identical, the
    real-world "bridge to the next hole in the row". Never crosses strips.

Two selection bugs behind the symptom:
  - Click on a wire lying over the breadboard BODY now selects the wire
    instead of opening the breadboard's 830-hole property dialog (that
    list popping over everything was the "se sobrepone la lista de todos
    los puntos" report). Guarded so the bubbled canvas click doesn't
    re-toggle the fresh selection.
  - Click on a hole occupied by a wire selects the wire (handlePinClick),
    so wires anchored in holes are reachable at all.

Jumper colors (like a real kit — a board of identical green wires is
unreadable, "se ven todos verdes"):
  - Power-rail holes mandate red (tp./bp. = +) / black (tn./bn. = −).
  - Other breadboard holes get a random jumper-palette color on manual
    draw; red and black are reserved for rails.
  - jumperColorForId gives agent/deterministic callers a stable per-wire
    color across reloads.

Tests: breadboard-occupancy.test.ts (12) — findWireAtHole (skips seating
wires, topmost wins), resolveFreeHole (same-strip shift, no cross-strip,
rail shift, passthrough), color policy (rails, palette determinism,
red/black reserved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 05:49:23 +02:00
David Montero Crespo c02547049d feat: ESP32 bridge seams, picker datasheets, S3/C3 examples + online-only board showcase
Generic platform work ported from the internal line:
- Esp32BridgeFactory seam + rebuildEsp32Bridge + sync-I2C seam: a
  substitute simulation bridge (e.g. the hosted editor's in-browser JS
  emulators) can be installed without touching OSS code
- Component datasheets: hover popover (ComponentInfoPanel) + markdown
  docs for common parts
- Per-chip S3/C3 basics examples for the gallery
- .gitignore: never allow pro emulator mask ROMs into the OSS repo

New: online-only board showcase. Boards implemented by the hosted editor
(ESP32-C6, M5Stack Core, Cardputer ADV, Pimoroni RP2350 family) appear
in the picker as advertisement cards with an ONLINE badge linking to
velxio.com, where they are free to use. Ads auto-hide in any build that
registers the real BoardKind.
2026-07-21 00:22:19 -03:00
David Montero Crespo 701042fa22 fix(perf): un-freeze the editor during fast-toggling simulations (ESP32 clock)
Running a multiplexed 4-digit 7-segment clock on ESP32/QEMU froze the
browser for minutes after Run — evaluate probes waited 40-90 s, and before
the first fixes the sim WebSocket eventually died (code 1006) with the page
never recovering. CPU-profiled on staging; four compounding per-GPIO-edge
costs, in profile order:

updateComponentState minted a new components array per edge
------------------------------------------------------------
The store setter rebuilt `components` (and one properties object) on EVERY
edge even when the state didn't change. The breadboard is direct-wired to
13 board pins, so segment toggles produced thousands of store sets per
second; every subscriber re-rendered each time, and the canvas subscription
effect (deps: [components, ...]) re-subscribed all pin listeners in a loop.
Now a no-op guard returns prevState unchanged, and breadboards are treated
as self-managed (they have no visual on/off state to echo).

CompilationConsole re-rendered every log line per editor render
----------------------------------------------------------------
The post-compile console holds hundreds of lines; each render called
Date.toLocaleTimeString per line (~0.2 ms each — it builds a fresh Intl
formatter every call). Profile: 162 s of self time in LogLine over a 337 s
window, in ~150 ms tasks. LogLine is now memoized (entries are immutable),
timestamps go through one shared Intl.DateTimeFormat, and the console
itself is React.memo'd against parent re-renders.

Per-edge full SPICE re-solves
------------------------------
PinManager requested a FULL netlist rebuild+solve on every 'mcu' edge.
Now only the edge that newly classifies a pin as MCU-output triggers the
rebuild (that's what emits the pin's V-source); steady-state updates flow
through connectMcuEdgesToService's per-pin coalesced alterSource path.
The start.ts resolve hook is trailing-throttled (33 ms) for the other
per-edge callers (RP2040, custom chips), the service's pending-edge queue
drains on a 33 ms gap timer instead of replaying back-to-back, and new
edges arriving inside the gap queue instead of soloing a solve.
STM32 / Pi reverse pin-name mappings added to connectMcuEdgesToService so
those boards keep fine-grained updates now that the full-tick storm is
gone (PA0/PC13-style and GPIO-style names never matched before).

wokwi-7segment re-rendered per segment write
---------------------------------------------
element.values now flushes at most every 8 ms per display (trailing write
guaranteed), instead of re-rendering the 32-shape SVG per edge.

Also: CLN (colon) pin support for 7-segment clock faces — wired CLN now
drives colon/colonValue in both the attachEvents path and the QEMU
onPinStateChange path; it was silently ignored, so clock colons never lit.

Verified on staging with the failing project: main-thread probes drop from
40-90 s waits (324 long tasks, 52.6 s blocked in 150 s) to 5-11 ms
(2 long tasks, 179 ms), display shows 12:00 with the colon blinking at
1 Hz from the first seconds after Run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 01:15:59 +02:00
David Montero a000fba154 fix(picker): Pi family component cards — full illustrations + PRO badge
The registry's Pi Zero/1/2/3/4/5 component entries rendered the live
velxio-raspberry-pi-* element clipped to a sliver and carried no PRO
marker. Reuse the board illustrations keyed by tagName (Zero/1/2
intentionally share the Pi 3 art) and show the shared gold PRO pill on
any component whose id is a pro board kind (Pi Linux family + STM32).
2026-07-21 00:19:01 +02:00
David Montero 1ce0bad5c8 fix(picker): Pi 4/5 board thumbnails as full illustrations + clearer PRO badge
The Pi 4/5 cards instantiated their live custom element at natural size
with a CSS scale; the transform keeps the unscaled layout box, so the
100px thumbnail clipped the board to a narrow vertical sliver. Use the
existing board illustration PNGs with objectFit contain, same as Pi 3.
The PRO badge on gated boards grows to a readable pill with a drop
shadow.
2026-07-20 23:47:50 +02:00
David Montero Crespo 0635e15e7a fix(router): escape corridors for endpoint-in-obstacle + checked-elbow parity
Three router bugs found by replaying a real agent session (reloj_3333) where
wires ran straight across a seated 4-digit display. Each fix is covered by a
regression test built from the failing geometry.

Endpoint inside an obstacle no longer drops the whole obstacle
--------------------------------------------------------------
Breadboard strips under a seated display start INSIDE its inflated bbox, so
the "rects containing an endpoint are dropped" rule deleted the display as
an obstacle for every wire leaving those strips — 15 wires crossed it end to
end. The rect is now carved instead: an escape corridor (ROUTE_MARGIN wide)
from the endpoint to the chosen edge, with the rest of the body still
blocking. Side blocks overlap the endpoint's row by 1px, or the strict
segment-hit test leaves the row as a free seam straight across the body.

Overlapping rects escape in ONE shared direction
------------------------------------------------
Seated resistors overlap heavily (19px pitch, ~66px inflated boxes). When
each containing rect picked its own nearest edge, the corridors pointed
different ways and walled each other off — A* found no exit, fell back to
the direct elbow, and the wire crossed the display anyway. The escape
direction is now chosen once against the UNION of containing rects and
every carve uses it, so the corridors chain into a continuous exit.

Null route materialises the CHECKED elbow
------------------------------------------
routeAroundObstacles returns null when the PREVIEW elbow (longer-axis-first)
is clear — but the re-route pass stored empty waypoints, which the renderer
expands as the horizontal-first corner: a DIFFERENT elbow the router never
validated. Three wires shipped crossing a display whose checked route was
clean. The pass now materialises previewElbow explicitly, exactly like
finishWireCreation always did.

Verified E2E: the same agent prompt that produced 15 crossings now builds
the ESP32 clock with ZERO wire segments crossing the display body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 21:35:58 +02:00
David Montero Crespo 651161559a feat(wires): avoid other wires + live routed preview + system-owned shapes
Extends the existing component-avoiding A* (wireAutoRoute.ts) into the full
auto-router the canvas was missing. Three pieces:

Wire avoidance with soft costs
------------------------------
Component bodies stay hard-blocked, but wires get graded costs: running
parallel on top of another wire (within an 8px corridor) is charged per px,
a perpendicular crossing costs a small fixed amount, and bends keep their
existing penalty. Crossings must stay possible — hard-blocking wires makes
dense boards unroutable and everything would degrade to the default elbow.

The compressed grid gains "corridor" coordinates 8px to each side of every
wire segment, so the router actually has a lane to run BESIDE a wire; that
is also what lays multi-wire runs out as a tidy side-by-side bus, since
each new wire routes seeing the previous ones. Wires sharing an endpoint
with the route are exempt (wires meeting on a pin must touch there), and
only wires within 120px of the route's bbox participate, keeping the grid
under the coordinate cap on dense canvases.

autoRouted: the system owns the shape until the user takes it
-------------------------------------------------------------
New Wire flag, set by pin-to-pin creation and by agent add_wire. Every
shape-editing gesture (segment drag, waypoint drag, waypoint insert — five
call sites) clears it: from that moment the wire is hand-authored and is
NEVER re-shaped, exactly where the user put it. Wires from older projects
have no flag and are treated as hand-authored.

recalculateAllWirePositions re-routes flagged wires after endpoints move
(component drag end, agent batches, mount settle — never per drag frame).
This is also what routes agent wires at all: they are created before their
elements mount and before pin coords are final, so creation-time routing
is impossible; the settle-timer recalc routes them once geometry is real.

Live routed preview
-------------------
updateWireInProgress routes start->cursor (throttled to 40ms) and the
preview renders that path, so the wire dodges components and wires AS THE
MOUSE MOVES instead of snapping into shape on the final click. Hand-guided
previews (user-placed waypoints) keep the classic path untouched.

Verified in the live app: an agent-built breadboard circuit shows 0 wire
overlap px and 0 body crossings across all wires, and a hand-started wire
aimed collinear with an existing run previews 21px beside it, overlap 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:08:25 +02:00
David Montero Crespo 3ac00ae5ff fix(trace): recognise runtime boards and same-hole junctions in pin tracing
An ESP32 clock built by the agent stayed dark while QEMU was verifiably
emitting hundreds of GPIO edges per second (437/pin measured on the live
websocket). Reload did not help — this was not the seating race. Two
independent tracing bugs, reproduced from the real project circuit (fixture
included) and each sufficient to kill the display:

Boards added at runtime were invisible
--------------------------------------
isBoardComponent matches static id prefixes ('arduino-uno', ...), which only
covers the default board. Every board added at runtime gets a minted UUID id
— the agent's add_board always does — so traceDetailed treated the board
endpoint as an unknown component and resolved null, and SimulatorCanvas's
direct-wire subscription path skipped it entirely. Every Uno project happened
to work because they reuse the default board whose instance id IS the literal
'arduino-uno'. Both sites now consult the live boards list first, keeping
isBoardComponent as the legacy-id fallback.

Strip walking missed wires stacked on one hole
----------------------------------------------
The breadboard group walk continued the trace from every OTHER wired hole of
the strip, excluding the arrival hole by name. But two wires may legitimately
share one hole — the agent bridges strips straight into the seat hole (8 of
this circuit's 9 bridges land exactly on a resistor's own hole), which is
electrically identical to using a free hole of the strip. The name exclusion
made those junctions dead ends. Exclusion is now by incoming WIRE id, so
same-hole connections resolve; the depth bound already prevents ping-ponging
between two wires of one net.

With both fixes the exact saved circuit resolves every display pin to its
GPIO (A..DP -> 32,33,25,26,27,14,12,13; DIG1..4 -> 15,2,4,5; COM -> GND) and
the live project now shows 12:00 on the real QEMU simulation. traceDetailed
is exported for the regression test, which drives the real store with the
real circuit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:46:23 +02:00
David Montero Crespo 91965e9824 fix(examples): declare Adafruit BusIO in ESP32 GFX-based examples
The ESP-IDF compile path stages exactly the libraries declared in the
example (no transitive resolution), so Adafruit_GFX.h failing to find
Adafruit_I2CDevice.h broke esp32s3-ili9341-hello and esp32-oled-4pin-i2c
with 'Compilation produced no firmware'. The other ESP32 GFX examples
(esp32-oled, esp32-bmp280) already declare BusIO explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:36:36 +02:00
David Montero Crespo 9e5ae8baf6 fix(breadboard): derive seating at element mount — closes run-before-seating race
A part can land in the store at its FINAL position before its element
mounts: the agent streams add_component and the seating move in one batch,
and updateComponent's reseat then finds no DOM (computeSeating null) and
keeps the empty seating. Nothing re-derived it afterwards — the agent-side
seat correction skips when the position needs no nudge, and 'pininfo-change'
only fires on pin-SET swaps, not on plain init. Meanwhile run_simulation
executes right after the SSE round, before the correction's animation frame.

Net effect, reported by a user as a suspicion that turned out exactly right:
a clock the agent built and ran in one turn showed a dead display, while
reloading the project and running it worked — bb seating wires are persisted,
so on reload they exist before Run is pressed.

DynamicComponent now reseats once the element's pinInfo first becomes
measurable (same polling cadence as the pinInfo-ready effect), which closes
the hole for every path that stores a final position before mount: agent
batches, project load, undo. To keep that free on load,
reseatComponentOnBreadboard skips the store write when there is nothing
seated and nothing to clear — otherwise every off-board part would churn the
wires array identity once per mount.

Verified live end-to-end: agent adds + seats + wires + compiles + RUNS in a
single turn; the seated LED blinks immediately (4 transitions sampled), with
all 4 seated-pin markers present — no reload needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:39:29 +02:00
David Montero Crespo d612c3bae7 feat(breadboard): green dots on pins plugged into a breadboard
Seating is otherwise invisible — a seated pin connects to its hole through a
zero-length `bb` wire that never renders — so a user couldn't tell a part
that merely sits ON the board from one whose pins are actually connected.
This was reported after placing parts that looked seated but gave no signal
they were wired in.

SeatedPinMarkers draws a small always-on green dot (Wokwi-style) on each pin
that has a `bb` wire, derived once per render from the store's wires
(component pin = wire start). Non-interactive layer below the wire-target
hit boxes; only breadboard-seated pins light up, so board-wired builtins stay
unmarked — exactly the "seated vs connected" distinction that was missing.

The per-pin rotation math (rotate about the wrapper centre, which the overlay
layers live outside of) is extracted from PinOverlay into a shared
`rotatePinLocal`, so the dots and the wire-target boxes can never drift apart
under rotation. A test asserts rotatePinLocal agrees with calculatePinPosition
at 0/90/180/270°.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 04:48:16 +02:00
David Montero Crespo bbd025d1c4 fix(breadboard): land agent-seated parts exactly under rotation
The agent computes exact hole assignments server-side but can only send an
approximate canvas x/y, because the rotation pivot is the DOM wrapper centre
and the wrapper includes a text label the server cannot measure. Under
rotation that left seated parts off by up to ~4 px — enough that a diode
(pins 7.5 pitches apart) half-seated: computeSeating found no hole for the
far pin and it went electrically dead.

resolveSeatPosition corrects it in the browser by pure translation: read
where the anchor pin actually is from live DOM geometry (real pivot), read
where the solver put it, shift the whole part by the difference. Every other
pin follows because pin-to-pin offsets are pivot-free. It never re-solves, so
it cannot slide the part to different holes and the validated netlist holds.

The anchor target is the solver's anchor position in breadboard-element
space, WITH its sub-pitch centroid translation — not the hole centre.
Targeting the centre would re-break the diode (far pin 4.8 px out). Verified
against real rendered geometry in a browser: resistor and diode at 90° both
seat within the intrinsic lattice residual (0.6 / 2.4 px).

Applied via a `seat` payload on the move_component effect (velxio-prod
overlay); this commit is the resolver + tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 22:01:41 +02:00
David Montero Crespo 66c6c7d813 feat(breadboard): hover-gated labels + full-footprint seating solver
Three changes, all driven by a real project where a 4-digit 7-segment clock
was unreadable and half its parts were not actually seated.

Labels on hover only
--------------------
Eight vertical resistors at 19 px pitch rendered eight 93 px "Resistor 220 Ω"
labels on top of each other, hiding the parts and the breadboard holes; the
SPICE overlay added ~40 more `0uV` pills. Both are now revealed on hover:
hovering a part also lights up the voltages of every wire touching it.

The label is hidden with OPACITY and stays in flow. pinPositionCalculator
derives the rotation pivot from wrapper.offsetHeight, so taking it out of
flow would move the pins of every rotated component in every saved project.

Seat-on-drop
------------
The drag-time magnet only aligned the anchor pin and assumed the rest
followed, which is how parts ended up HALF-seated: some pins in holes, the
rest dead in the air. It looks mounted in a screenshot and silently breaks
the circuit. On release we now re-solve properly — nearest position where
EVERY pin is in a free hole, sliding past occupied columns — via the new
solvePlacement/seatOnDrop. Geometry comes from the element's own pinInfo,
so there is no part whitelist.

Sub-pitch translation
---------------------
solvePlacement first assigned pins to holes at half-pitch, then translates
by the centroid of the residuals before judging fit. Pinning the anchor dead
centre refused every off-lattice footprint: a diode spans 7.5 pitches, so
one leg landed 4.8 px out. Shifted 2.4 px, BOTH legs sit inside tolerance —
what bending the leads does on a real board. Measured over the catalog this
takes seatable parts from 87 to 125 of 152; diodes, transistors, regulators,
optocouplers and flip-flops are rescued with no artwork change.

Staying under SEAT_TOLERANCE (< half pitch) keeps each pin's nearest hole
unambiguous, so computeSeating resolves the same holes and the netlist is
unaffected by the small offset.

Also: refuse a placement that would put two of a part's own pins in one
strip. A column strip — and far worse, a power rail — is a single net, so
such a seating shorts the part to itself. Without it a 7-segment happily
lays its pins across a rail. And deduplicate pin names before solving:
calculatePinPosition resolves by name and returns the first match, so a
board carrying GND x5 collided with itself and was refused outright.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 05:28:40 +02:00
David Montero 206cda78af fix(components): type-coerce string properties + reseat on pininfo-change
Root cause of the 'digits=4 display seated with the 1-digit COM pinout'
bug: property values arrive as STRINGS (agent set_component_property,
the property dialog's text inputs) and were assigned to the web
component verbatim — wokwi's 7segment does switch(this.digits) with
numeric cases, so el.digits='4' silently fell back to the 1-digit
pinout (and 'false' stayed truthy for boolean props like colon).

- DynamicComponent now coerces string values to the TYPE of the
  metadata default for that key (number/boolean) before assigning.
- New pininfo-change listener: when a property swaps the element's pin
  set (digits, flip, pins edge), the elements announce it — re-derive
  the breadboard seating then, with the fresh pinout, instead of never.
2026-07-18 19:18:27 +02:00
David Montero aab5e1be08 feat(canvas): resistors default to vertical on add
Every resistor variant ('resistor' + 'resistor-<value>') now lands on
the canvas rotated 90 degrees: reads better, takes less horizontal
space, and drops straight into breadboard columns. Explicit rotations
in metadata defaults are respected. The breadboard auto-vertical drag
check widens from the two-entry set to the same prefix predicate, so
preconfigured variants (resistor-330 etc.) rotate on the board too.
2026-07-18 18:02:41 +02:00
David Montero f98c3268fa feat(breadboard): Wokwi-style parts-on-breadboard — hole snapping + invisible seating wires
Parts now plug INTO the breadboard instead of using it as a junction box:

- Drag magnetism: while dragging, the part's anchor pin snaps to the
  nearest hole center (9 px range, 9.6 px grid) so parts land perfectly
  aligned, like Wokwi.
- Seating: every pin within 4 px of a hole gets an invisible zero-length
  wire (Wire.bb) from pin to hole — the exact model Wokwi persists as
  ["r1:1","bb1:6t.b","",["$bb"]]. Electrically they are ordinary
  wires, so the netlist builder, digital trace and SPICE need zero
  changes; they are simply not rendered and not hit-testable. Seating
  re-computes on every move/rotation (updateComponent), and moving the
  breadboard carries its seated parts along.
- Resistors auto-rotate to vertical when dragged over a breadboard
  (their 58.8 px pin span bridges the center trench rows b-f exactly).
- Seat tolerance 4 px: absorbs the worst element pin-spacing residual
  (~1.6 px) while staying under half the hole pitch, so a pin is never
  ambiguous between holes.

Wokwi interchange fixes that fell out of the diagram.json research:
- import maps the top-level rotate attr onto properties.rotation
  (previously every rotated part imported flat) and export emits it
  back as rotate instead of leaking it into attrs;
- $bb / empty-color connections import as bb seating wires and export
  back as ["$bb"] entries, so parts-on-breadboard projects round-trip;
- wokwi-breadboard-half aliases to the full breadboard (hole names are
  a strict superset, so every connection stays valid).

Breadboard elements now export their pure hole grids and import cleanly
without a DOM (node tests); geometry + store seating covered by
breadboard-snap.test.ts and breadboard-seating.test.ts.
2026-07-18 08:47:25 +02:00
David Montero 8e33088752 fix(editor): sync URL after New workspace + sever project identity on .vlx import
Three stale-project-identity fixes from reviewing the New-workspace flow:

- New workspace (web): handleNewClick cleared the workspace and the
  current project but left the browser on the old /user/slug URL — a
  refresh (or back-button pop) silently reloaded the OLD project over
  the fresh unsaved workspace. Now replaceState's to the localized
  /editor (replace, not push, so no back-entry points at the stale
  project route).
- New workspace (desktop menu): same URL fix for the newProject menu
  action, which cleared identity but never left the project route.
- .vlx import: importVlxFile mutated the stores WITHOUT clearing
  currentProject — with a saved project open, autosave saw the
  imported content as dirty edits on the old projectId and silently
  PUT the .vlx contents over the user's saved project (and pushed the
  clobber to GitHub on linked projects). Now severs identity first,
  same guard loadExample.ts already documents.
2026-07-18 08:24:06 +02:00
David Montero 30882f3930 refactor(verify): extract store-driven pre-flight verification into verifyFromStore
verifyCircuitFromStore() builds the worst-case snapshot (every wired
digital pin driven HIGH) and solves it — extracted verbatim from
EditorToolbar's runVerification so programmatic runners (editor
extensions, agents) can gate their own run paths on the same rules.
No behavior change for the Run button.
2026-07-18 06:44:38 +02:00
David Montero 7ed9c51bd3 feat(wires): first-time auto-routing around components
Creating a wire with a direct pin-to-pin click (no user waypoints) now
routes around other components' bounding boxes instead of crossing
them. Routing happens exactly once, at creation: the routed corners are
stored as ordinary waypoints, so every later manual edit stays where
the user puts it — never re-routed.

Router (utils/wireAutoRoute.ts):
- tries the preview elbow first (clear -> keep existing behavior and
  the WYSIWYG shape), then the opposite elbow, then A* over the
  compressed grid spanned by pin coordinates and obstacle edges
  inflated by an 8 px clearance, with a 40 px per-bend penalty so
  straighter routes win
- obstacles are component boxes only (never boards — pins sit on both
  board edges and detouring around a board produces absurd routes),
  excluding the wire's own endpoint components, measured from the
  rendered DOM; rects containing an endpoint are dropped
- any failure (walled-off target, oversized grid, no DOM) falls back
  to the previous direct-elbow behavior
2026-07-18 05:59:45 +02:00
David Montero abbbbad559 feat(wires): fuse sub-pixel jogs + snap segment drags to the wire's own runs
Hand-aligning a dragged segment could leave two parallel runs a pixel
or two apart, joined by a tiny perpendicular step, because alignment
snapping only ever targeted OTHER wires' geometry.

- Segment and bend-point drags now also snap (6 px threshold) against
  the dragged wire's own points — excluding the ones being dragged —
  so a run clicks into line with its neighbour and the exact
  simplification fuses them into one segment on commit.
- fuseMicroJogs: parallel runs offset by under 2 px joined by a tiny
  step are aligned automatically (the run not anchored to a wire
  endpoint moves; shorter run yields when both are free). Applied at
  render time and in renderedToWaypoints/normalizeWireWaypoints, so
  already-saved crooked wires display straight without touching data.
2026-07-18 05:39:37 +02:00
David Montero 152f9e4ce0 feat(wires): wokwi-style rounded bends + degenerate path cleanup
Three wiring quality fixes:

- Rounded corners: every bend now renders as a quadratic curve
  (radius 7, clamped to half the shorter adjacent segment), with
  round line caps/joins. Segment/waypoint drag previews and the
  in-progress preview use the same path builder so the look is
  consistent everywhere.

- Degenerate geometry cleanup at render time: the expanded polyline
  is simplified (duplicates, collinear runs, U-turns) before the
  path is emitted, so wires saved with junk waypoints no longer
  render on top of themselves. Stored data is untouched until the
  user edits the wire.

- WYSIWYG commit: finishWireCreation materialises the final-leg
  elbow exactly as the live preview drew it (longer axis first) and
  normalises the stored waypoints. Previously the committed wire
  fell back to horizontal-first and visibly changed shape on click.

simplifyOrthogonalPath moved to wireUtils (re-exported from
wireHitDetection for existing imports); the duplicated inline
expansions in SimulatorCanvas now use the shared helper. Waypoint
dots on idle wires removed (visual noise); endpoint dots stay.
2026-07-18 05:02:47 +02:00
David Montero 2e5ac20eba fix(simulator): don't fire key-bound buttons while typing in Monaco
Monaco's focus sink is a plain div (.native-edit-context under the
EditContext API), neither an input tag nor contentEditable, so the
typing guard missed it and a mapped letter typed into the code editor
pressed the button. Treat any keydown originating inside .monaco-editor
as typing.
2026-07-18 04:02:01 +02:00
David Montero 5218f7314b feat(simulator): map pushbuttons to keyboard keys
Any pushbutton (pushbutton / pushbutton-6mm) can now be driven from the
keyboard. Assign a key from the component property dialog — a keycap
control captures the next keypress (Escape cancels, modifiers alone are
rejected) — and a keycap badge next to the component label shows the
mapping on the canvas. Several buttons may share one key on purpose;
the dialog shows a hint when that happens.

At runtime a global bridge translates keydown/keyup into the same
button-press / button-release DOM events the mouse fires on the wokwi
element, so every simulation path (avr8js pin logic, SPICE-driven
inputs, the QEMU GPIO bridge, the pressed visual) behaves identically
to a mouse click. Guards: ignored while typing in inputs or the code
editor, ignored with Ctrl/Alt/Meta held, auto-repeat collapses into one
long press, and window blur releases everything so no button sticks
after Alt-Tab.

The binding is stored as the component's 'key' property, so it
round-trips through project saves and .vlx exports and is undoable like
any other property edit. Strings added to all 9 locales.
2026-07-18 04:02:01 +02:00
David Montero Crespo e72413a13f feat(ui): replace native confirm() dialogs with reusable modal
Convert the remaining window.confirm() call sites to the in-app
MessageDialogHost, extended with a new confirm mode (Cancel + Confirm
buttons, optional danger styling) via showConfirmDialog().

Sites converted:
- New workspace (EditorPage)
- Load project / delete file (FileExplorer)
- Overwrite SPIFFS file (BoardOptionsModal)
- Delete VFS node (VirtualFileSystem)

All dialog strings are internationalized across the 9 supported locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru); the two previously
English-only modals now pull from i18n too.
2026-07-18 01:59:18 +02:00