Commit Graph

286 Commits

Author SHA1 Message Date
David Montero Crespo d4ec4fe04d fix(ntc): the temperature sensor read wrong on 3.3V boards, and three examples decoded backwards
Two independent defects, both visible as "the NTC reports the wrong
temperature" (issue #233).

1. The part built its divider against a hard-coded 5 V rail, while the SPICE
   path used the board's real rail. They only agreed on AVR. On a 3.3 V board
   the injected node sat above the ADC's full scale: the slider at 25 C read
   ~1.4 C, and every slider value below ~11 C saturated to the same number, so
   -40, 0 and 10 C were indistinguishable. The rail now comes from the board
   (analogRailVolts), matching setAdcVoltage's own AVR/RP2040/ESP32 split.

2. The divider convention was corrected in 608c2538 (VCC -> R_pull -> OUT ->
   R_NTC -> GND) but data/examples.ts was not updated with it, so three of the
   four bundled NTC sketches still decoded the OTHER topology. Their readings
   came out mirrored about 25 C: the uno-ntc example printed -3.42 C with the
   slider at 60 C - the exact symptom dd22bcfe had recorded as the bug. Fixed
   in uno-ntc, pico-ntc and attiny85-ntc-sensor; examples-circuits.ts was
   already right.

The sibling analog sensors (gas, flame, heart-beat, sound) map their sliders
0..1023 onto the same hard-coded 5 V and will saturate the same way on 3.3 V
boards. Left alone here: that is not what #233 reports, and each one needs its
examples re-checked before changing.
2026-08-03 07:33:20 +02:00
David Montero Crespo 6877099e78 fix(micropython): the \r poke must not fire once the prompt is already up
Stage 1 arms a \r at banner+800ms to flush QEMU's held ">>> " prompt, and
stage 2 arms Ctrl-A at prompt+200ms. Nothing kept the first from firing
after the second: if the prompt ever arrives on its own within 600ms, the
\r lands inside the pasted source, and MicroPython reads a lone \r as a
newline - it cuts a source line in half with no diagnostic of its own.

Here that is still latent; the in-browser JS engines, which deliver serial
per byte, hit it on every single run.
2026-08-03 05:48:40 +02:00
David Montero Crespo cd2dc2f54b Merge remote-tracking branch 'origin/master' into v3.2-worktree 2026-08-01 04:23:55 +02:00
David Montero Crespo ab7949502d feat(sim): seated sockets are electrical, and capability is probed
Three generic seams, none naming any board:

The pin tracer gains a socket hop: a component that declares boardSocket
with a board actually seated on it resolves a pad to the SAME-NAMED pin
of that board — seating IS the connection, like a XIAO pushed into a
shield header or a HAT on the 40-pin. Until now a seated shield's
buttons and LEDs were dead unless the user drew wires the real stack
does not have. Wires keep working exactly as before for every other
host (Uno, Mega, Pi, STM32, ...).

setMicrophoneSource now RETURNS whether a bridge actually took the
source, so a part can state that this host has no I2S instead of
pretending to stream into a void — capability reported, never invented.

createNeopixelDecoder is exported: boards are not the only things
carrying an addressable LED, and a part with one on board needs the
same decode rather than a drifting copy.
2026-07-31 22:32:47 +02:00
David Montero eb3b04efe3 spice: seed INPUT_PULLUP pins to their resting level on pull enable
Deterministic repro (emulation-gaps F4): after pinMode(2, INPUT_PULLUP)
with a correctly-wired button-to-GND, all 8 setup()-time digitalReads
and the first two loop() passes returned 0 — the input register only
got its level from the first SPICE solve (~400 ms). That window is what
fired phantom emergency-stop latches and auto-started state machines in
audited sessions (and an unwired INPUT_PULLUP pin on the qemu bridges
never got driven at all: no net in pinNetMap, connector skips it, guest
read 0 forever).

Fix: PinManager.onPullChange fires on pull transitions; AVRSimulator
seeds its port input to the resting level instantly (real silicon does
this in ns), and makePinPullHandler does the same for the qemu-bridge
boards via the shim's setPinState. Sourced nets are still overridden by
the very next solve, so the 'mis-wired button still works' regression
guarded by the June work cannot return.
2026-07-31 20:51:42 +02:00
David Montero fc048ff941 Merge branch 'feat/circuit-safety-p1' 2026-07-31 20:43:10 +02:00
David Montero c97a43fd7c verify: catch unpowered nets, open source loops, and relay coil voltage mismatch
Audited circuits (2026-07) passed pre-flight with zero findings while being
electrically dead or mis-supplied: a battery with one pole wired, a MOSFET
switching a rail nothing powers, a 12V-coil relay fed from 5V, and VCC pins
tied together with no source. Three new graph-based rules (no solve needed,
so they fire even when ngspice cannot converge; all non-blocking warnings):

- unpowered-net: power-input pins (VCC/VIN/V+/COIL+/rated supply pins) and
  whole conduction regions that no battery, power supply, or board pin
  reaches. Ground and source-negative nets do not bridge power; MOSFET/BJT
  conduct only through their channel; a relay coil is isolated from its
  contacts; custom chips and boards are self-powered.
- no-return-path: a source with a single pole wired, or whose + side never
  reconnects to its - side (return analysis joins all pins, so voltage-driven
  gates/bases are not false positives).
- voltage-mismatch: relay coil_voltage vs the nominal supply on the coil
  nets (region fallback for coils fed through a switch/transistor): below
  the 60% pull-in threshold it never actuates, above 1.5x it burns out.

verifyFromStore now also verifies circuits whose only source is a
power-supply component. Gallery sweep extended to fail on the new codes:
zero findings across all 228 wired examples.
2026-07-31 20:38:23 +02:00
David Montero 1ad1fd4137 feat(components): DS3231 RTC module + GPS NEO-6M NMEA talker
DS3231 (I2C 0x68):
- New velxio-ds3231 Web Component (4-pin module: GND/VCC/SDA/SCL) with
  pinInfo per CLAUDE.md 6a; registered in elements-register + main.tsx.
- Catalog entry in scripts/component-overrides.json (_customComponents)
  mirrored into components-metadata.json exactly as the generator emits it
  (verified by cloning wokwi-elements and running generate:metadata: zero drift).
- The existing VirtualDS3231 device sim (DS1307-compatible time regs
  0x00-0x06 + control 0x0E / status 0x0F / temperature 0x11-0x12 at 0.25 C
  steps) is now user-reachable from the picker; the AVR/RP2040 attach path
  gains live temperature updates via the SensorControlPanel (new ds3231
  slider in sensorControlConfig).

GPS NEO-6M (UART talker, 9600 baud):
- New velxio-gps-neo6m Web Component (VCC/RX/TX/GND, pulsing PPS LED).
- New part sim GpsParts.ts: emits a valid GPGGA+GPRMC cycle per second
  (correct XOR checksums, ddmm.mmmmm coords, UTC clock advancing 1 s/cycle)
  with lat/lng/altitude/speed configurable via property dialog and live
  via a new SensorControlPanel entry. Defaults: Madrid 40.4168 N 3.7038 W.
- Injection routes, chosen by classifying the wired board pin:
  1) hardware UART RX -> new uniform sim.feedUart(uart, data) seam
     (AVR USART0, RP2040 uart0/1 via rp2040js feedByte, ESP32 uart0/2 and
     STM32 USART1/2 via their bridge shims); byte feed is paced in 8-byte
     wall-clock chunks so 32-byte RX FIFOs never overflow;
  2) any other digital pin on a cycle-accurate sim -> real 8N1 bit-banged
     waveform via schedulePinChange, so SoftwareSerial decodes it;
  3) feedUart rejection (e.g. Mega USART1-3, not modelled by avr8js)
     degrades permanently to the bit-banged path.
- Interconnect already probed sim.feedUart; implementing it also upgrades
  cross-board UART byte shortcuts for RP2040 uart1.

Tests: new gps-neo6m.test.ts (16 tests: NMEA builders, byte injection on
Uno RX0 / ESP32 RX2, Mega RX1 fallback, waveform decode back to $GPGGA,
live position updates, PPS pulse) + 3 new ds3231 AVR-path tests in
protocol-parts.test.ts. Full frontend suite: 2294 passed, 2 pre-existing
Galaksija Z80 timeouts that pass in isolation. npx tsc --noEmit clean.
2026-07-31 20:38:06 +02:00
David Montero 986b532922 spice: stamp STM32 output pins as V sources + STM32 board pin groups (3.3V)
The STM32 branch of collectPinStates deliberately skipped outputs from
the netlist — a workaround from when pinNameToArduinoPin couldn't map
'PC13' at all. With the mapping unified, emit digital outputs like every
other board so wired LEDs light from the solve (emulation-gaps F3).
Adds the missing BOARD_PIN_GROUPS entries for the STM32 family (3.3V
logic, suffixed GND/3V3 silks) so sources stamp at the right voltage.
2026-07-31 19:36:21 +02:00
David Montero a18964b401 spice: unify silkscreen->GPIO mapping on boardPinToNumber (fixes STM32 outputs, nano-esp32 buttons, esp32-family SPICE analog)
Three parallel half-implementations of the pin-name mapping each broke a
different board family (2026-07 emulation-gaps audit):

- collectPinStates.pinNameToArduinoPin returned -1 for STM32 'PC13' (no
  V source stamped: LED dark with correct firmware) and for nano-esp32
  'D2'/'A0' (no pull resistor stamped for INPUT_PULLUP buttons).
- connectDigitalInputsToMcu.gpioFromPinName only knew digits/GPIO/GP, so
  labeled pins were never driven from the solve (buttons stuck LOW).
- connectAnalogInputsToMcu's ADC_PIN_MAP used 'GPIO32'-style lookup keys
  that never matched the real bare-number wire pin names, so the
  SPICE->ADC path skipped every esp32-family pin; its per-kind GPIO
  converters also mapped nano-esp32 A-pins with the AVR convention.

All three now delegate to utils/boardPinMapping.boardPinToNumber (with
the previous behavior as fallback for unknown names). Also adds the
ESP32-C3 branch to the store's adcChannelForPin (GPIO0-5 -> CH0-5,
verified against the qemu SARADC channel layout).
2026-07-31 19:20:59 +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 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 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 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 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 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 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 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
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 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 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 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 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 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 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 Crespo 6307aed007 feat: breadboard (full 830-point) + mini breadboard (170-point) parts
Two velxio-native passive parts, rendered as web components with
programmatic SVG + precomputed pinInfo (velxio-breadboard 830 holes,
velxio-breadboard-mini 170). Pin names follow the Wokwi convention
(holes `18t.d` / `17b.i`, rails `tp/tn/bp/bn.N`) and the metadata ids
are `breadboard` / `breadboard-mini`, so wokwi diagram.json zips
import/export with no aliasing.

Internal connectivity (5-hole column strips, full-length power rails)
is centralized in utils/breadboardNets.ts and wired into every net
consumer:
- NetlistBuilder: unionBreadboardGroups joins wired holes per group at
  the union-find level in buildNetlist, buildWireNetMap and
  buildBoardPinNetMap — SPICE, the circuit verifier and the voltage
  overlay all see one net per strip/rail with no extra cards.
- DynamicComponent.traceDetailed: the digital trace hops through every
  other wired hole of the entered group, so parts wired through a
  breadboard still resolve their board pin (2-terminal
  PASSIVE_PIN_PAIRS could not express N-hole groups).

Verified end-to-end in the app: Uno pin 8 -> full-board column ->
resistor -> mini-board column -> LED -> ground rail -> GND lights the
LED, and the HUD shows the 3 collapsed SPICE nets. 8 new unit tests
(breadboard-nets.test.ts); netlist-builder + circuit-verifier suites
stay green.
2026-07-15 21:50:36 +02:00
David Montero d7f4e8e966 feat(ssd1306): add the 4-pin I2C OLED module + examples on Uno/ESP32/Pico/STM32
Adds `velxio-ssd1306-i2c-4pin`, a native 4-pin SSD1306 OLED module
(GND/VCC/SCL/SDA) — the cheap 0.96" I2C board most beginners actually have,
matching Wokwi's board-ssd1306. The 8-pin `wokwi-ssd1306` breakout stays; this
is the distinct 4-pin part (issue #215). Same SSD1306Core render pipeline
(imageData/redraw) so the display paints identically; I2C-only, address via the
i2cAddress property (default 0x3C). Styled after the existing 8-pin element
(blue PCB, dark screen, corner holes, star).

Ships four "SSD1306 OLED (4-pin I2C)" gallery examples wiring it over I2C on
Arduino Uno (A4/A5), ESP32 (21/22), Raspberry Pi Pico (GP4/GP5) and STM32 Blue
Pill (PB7/PB6).
2026-07-09 23:13:50 +02:00
David Montero b51bbcf06d refactor(ssd1306): drop the i2c/spi aliases; CS-only auto-detect + protocol pin
Follow-up to the SSD1306 picker consolidation. All 68 saved projects that used
the retired ssd1306-i2c / ssd1306-spi ids have been migrated to the single
`ssd1306` (metadataId rewritten, protocol pinned), so the simulation aliases
are no longer needed and are removed.

- Auto-detect refined to CS-only: chip-select is the SPI-exclusive signal;
  DC does NOT imply SPI (on the 8-pin module DC doubles as the I2C address /
  SA0 line, so many I2C circuits wire it). Fixes false-SPI on those circuits.
- The `ssd1306` part honors an explicit `protocol` property when present
  (migrated legacy projects carry it) and auto-detects otherwise.
- loadProjectState normalizes any lingering ssd1306-i2c/spi ids (old .vlx
  files, pre-migration snapshots) to `ssd1306` + the matching protocol, so
  removing the aliases can never blank an old import.
2026-07-09 22:13:31 +02:00
David Montero b6dd2f5201 feat(ssd1306): one auto-detecting OLED part (merge the I2C/SPI picker entries)
The SSD1306 was three picker entries — a generic `ssd1306` with a protocol
selector plus `ssd1306-i2c` / `ssd1306-spi` shortcuts (issue #101) — all the
same 8-pin wokwi-ssd1306 element. That is confusing for one physical module
(issue #215). Wokwi ships a single I2C-only part; this goes one better: a
single part that auto-detects the protocol from the wiring, like a real
breadboard — CS or DC wired to a GPIO means SPI, otherwise I2C. No protocol
switch to set, just wire it up.

Works on every board with an I2C/SPI bus (AVR, RP2040, ESP32 Xtensa, STM32).
The ssd1306-i2c / ssd1306-spi ids stay as backward-compat simulation aliases
for projects saved before the merge, but are removed from the picker. Adds an
i2cAddress property (0x3c/0x3d) matching the real module and Wokwi.

Note: ESP32-C3, Raspberry Pi 3 and the bare RISC-V board do not emulate I2C/SPI
peripherals, so no I2C/SPI device (this or any other) attaches there yet.
2026-07-09 21:46:40 +02:00
David Montero 061bb91da1 fix(esp32-c3): drive digital inputs from the SPICE solve (spiceDrivenInputs)
Esp32C3Simulator already had the GPIO_IN plumbing (setPinState -> gpioIn ->
GPIO_IN_REG read) but never opted into connectDigitalInputsToMcu, so a pin
wired to a switch/button was never fed the solved circuit voltage and
digitalRead() ignored the real wiring. Enabling the flag (as AVRSimulator and
RP2040Simulator already do) completes the issue #247 fix: the ESP32-C3 now
reads GPIO2 from the SPICE solve, so toggling the slide switch flips the LED.
BasicParts' button/slide-switch seed already yields to spiceDriven(), so there
is no double-drive.
2026-07-09 19:16:37 +02:00
David Montero f5c1887869 fix(sim): model slide-switch as a real SPDT + tie ESP32 dual 3V3/5V pins to the rail
The slide-switch SPICE model only wired pin 1 <-> pin 2 (an SPST), ignoring
pin 3. The part is really an SPDT whose common wiper (pin 2) selects pin 1 at
value=0 or pin 3 at value=1, so a switch wired GND-1 / signal-2 / VCC-3 (the
natural Wokwi hookup) could never pull its signal high. Fixes the reported
ESP32-C3 case (issue #247) where only the green LED lit and the switch never
toggled the red one.

Second cause on that board: the ESP32-C3-DevKitM-1 exposes its supply as
3V3.1/3V3.2 and 5V.1/5V.2 (there is no bare 3V3/5V pin). VCC_PIN_RE has no
numeric-suffix branch on purpose (a dual-supply pin such as L293D VCC2 must
not collapse onto the shared logic rail), so those numbered pins floated at
0 V and the switch's HIGH side was dead. List them in boardPinGroups for
esp32-c3 / esp32-s3 / esp32-cam.

- componentToSpice: SPDT emission (both throws, complementary 0.01/1e9 R).
- digitalGateEngine: both driveSwitch paths (all-digital + mixed) made SPDT to
  match, so the pure-digital paint and the ngspice solve agree.
- examples-digital / examples-circuits: rewire every slide-switch so the rail
  feeds pin 3 and pin 1 is the value=0 throw, preserving value=ON=HIGH.
- spice-slide-switch-spdt-repro test reproduces issue #247 at the netlist level.
2026-07-09 18:58:12 +02:00
David Montero a1137f1929 fix(sim): re-solve SPICE on ESP32/STM32/Pi output pin edges
WebSocket-backed boards (ESP32, STM32, Raspberry Pi) reach the electrical
simulation only through PinManager.triggerPinChange, which updated the pin
state + notified listeners but never requested an electrical re-solve. AVR
and RP2040 already resolve at their own toggle sites. As a result an analog
part on an MCU-driven net (e.g. a resistor-less LED whose brightness comes
from the SPICE solve) stayed at its first solved value until unrelated
activity (such as serial output) forced a solve — so an ESP32 blink with no
Serial in loop() left the LED stuck on.

Request an electrical re-solve after an 'mcu'-sourced pin edge, in one place
(triggerPinChange), covering all WS boards. Gated to source==='mcu' so the
solver's own input feedback (triggerPinChange with the default 'external'
source) can't loop; requestElectricalResolve coalesces overlapping ticks so
a per-edge call is cheap.
2026-07-02 22:33:46 +02:00
David Montero ed132afb91 sim: drive RP2040 + STM32 digital inputs from the real circuit
Extend the spice-driven input path (already live for AVR/ESP32) to RP2040 and
STM32 so digitalRead() of an INPUT pin reflects the actual wiring: a pin tied
to a rail reads that rail, and an INPUT_PULLUP button-to-GND reads idle-HIGH /
pressed-LOW instead of floating or inverted.

RP2040 (rp2040js, frontend-only): the GPIO listener now splits input vs output
mode. Input pins report their pad pull (InputPullUp/Down) via setPinPull and
seed the pull's idle level (rp2040js does not auto-apply the pad pull to the
readable input register); the SPICE solve then overrides via connectDigital-
InputsToMcu when the net is actually sourced. Output pins drive as before.
spiceDrivenInputs = true.

STM32 (backend QEMU): the worker now forwards a new gpio_pull event (from the
libqemu-arm picsimlab_pull_pin callback) so the netlist stamps the matching
weak resistor; Stm32Bridge surfaces it, Stm32BridgeShim opts into
spiceDrivenInputs, and collectPinStates maps PA0/PC13 names to the linear pin
so the pull is read. STM32 outputs stay on the part layer (unchanged).

Event-driven parts with no SPICE model (rotary encoder, keypad) remain
protected by the existing sourcedNets gate in the connector.
2026-06-26 22:05:24 +02:00
David Montero 48099c0bda feat(avr): drive inputs from real circuit, modeling the internal pull-up
Re-do the AVR spice-driven digital inputs (reverted in c11c195) the right way so
INPUT_PULLUP buttons keep working. PinManager.updatePort now detects the AVR
internal pull-up (input DDR bit + PORT bit high) and sets the pin pull, so the
netlist stamps the 45k pull-up and an INPUT_PULLUP input reads HIGH at idle.
connectDigitalInputsToMcu drives a pin from the solve only when its net is
source-backed by a RAIL or a COMPONENT card (button switch, divider, cross-board
output) — NOT by the internal pull alone — so INPUT_PULLUP pins wired to
event-driven parts with no SPICE model (rotary encoder, keypad) are left to the
part layer and never clobbered. AVR only; RP2040/STM32 stay on the part-seed
until their pulls are modeled.
2026-06-26 20:18:41 +02:00
David Montero c11c1954e1 revert(boards): undo spice-driven digital inputs for AVR/RP2040/STM32
The spiceDrivenInputs change (e81450e + f4401cc) fixed plain-INPUT-wired-to-rail
reads but BROKE the far more common INPUT_PULLUP + button-to-GND pattern: the
internal pull-up is not modeled in the netlist, so the input floated LOW and read
as permanently pressed (verified live on the stm32-bluepill-button example).
Revert all the spice-driven-input changes to the pre-fix part-seed behaviour,
which handles INPUT_PULLUP correctly. Proper fix (model the internal pull-up per
board so BOTH patterns work) is a follow-up. Keeps the Pi LED fix.
2026-06-26 18:51:06 +02:00
David Montero f4401cc2dd fix(rp2040,stm32): drive digital inputs from the real circuit, like AVR
Extend the source-backed SPICE-driven input fix to the Pico (RP2040) and STM32:
a GP/PA pin wired to a rail or button now reads the right level from the solve,
while floating event-part nets (encoder/keypad/dialer/dip/stepper) stay on the
part layer. RP2040 just opts in (spiceDrivenInputs); STM32 opts in via the
Stm32BridgeShim and connectDigitalInputsToMcu maps PA0/PC13 names to the linear
pin setPinState expects (stm32PinNameToLinear).
2026-06-26 18:34:54 +02:00
David Montero e81450e348 fix(avr): drive digital inputs from the real circuit (SPICE), not a fake seed
An Arduino input wired to a power rail read the wrong level: a pin tied to 5V
read LOW, and a button-to-5V read idle-HIGH / pressed-LOW. AVR inputs were never
fed the solved circuit voltage (only the ESP32 had spiceDrivenInputs), so a
bare-rail input had no driver and buttons fell back to a hardcoded active-low
pull-up seed that ignored the wiring.

Enable spiceDrivenInputs on AVRSimulator, and gate connectDigitalInputsToMcu on
a new NetlistBuilder sourcedNets set (rails, GPIO V-sources, pulls, and any net
a component card touches). Only source-backed input pins are driven from the
solve; floating nets are left to the part layer, so event-driven parts with no
SPICE model (rotary encoder, keypad, dialer, dip-switch, stepper) keep driving
their own pins instead of being forced LOW.
2026-06-26 18:00:03 +02:00
David Montero 7fb2ee3de9 feat(esp32): drive digital inputs from the solved circuit (real-wiring fidelity)
ESP32 digitalRead now reflects the actual circuit instead of a part-level
seed, so a button behaves like hardware — including breaking when it's
mis-wired.

- connectDigitalInputsToMcu: after each SPICE solve, threshold every ESP32
  input pin's net voltage (3.3 V LVCMOS, hysteresis) and push the level into
  QEMU. Only pins the MCU isn't driving as outputs are injected.
- Esp32BridgeShim advertises spiceDrivenInputs; the pushbutton / 6mm-button /
  slide-switch parts skip their direct setPinState seed for such boards and
  only flip the component property (pressed/value), which re-solves the
  circuit. The connector then decides the level from the real wiring.
- makePinPullHandler no longer seeds the pin; it only records the pull
  (netlist resistor) + requests a re-solve, so the read stays circuit-driven.
- GROUND_PIN_RE now matches bare numbered grounds (GND2, GND3) — the ESP32
  DevKit element labels its second pad 'GND2', which previously floated.

Net effect: a correctly-wired INPUT_PULLUP button idles HIGH and reads LOW
pressed; a button mis-wired with GND on the wrong terminal reads stuck-LOW,
matching real silicon. AVR / RP2040 keep the legacy part-seed path.
2026-06-24 22:52:52 +02:00
David Montero df9e06c99a feat(esp32): emulate internal pull-up/pull-down for GPIO inputs
INPUT_PULLUP / INPUT_PULLDOWN had no effect in simulation: the ESP32's
internal pull resistors live inside QEMU and were invisible to the SPICE
solver, so an input wired to a button-to-GND floated to 0 V and read LOW
even at idle. The canonical active-low button never worked.

Read the pull config straight out of the running guest: the IO_MUX
register (FUN_PU bit 8 / FUN_PD bit 7) is already exposed read-only via
qemu_picsimlab_get_internals(3), so no QEMU rebuild is needed. The worker
scans it on the 100 ms poll thread and emits gpio_pull; the bridge feeds
it to PinManager; the netlist stamps a weak 45k resistor to the rail so
idle inputs read the correct level. 45k matches the real internal pull
and is weak enough that any external driver/pull dominates.

Verified with ngspice: idle ~3.3 V (HIGH), pressed ~0 V (LOW).
2026-06-24 03:32:30 +02:00
David Montero d8b6c77335 fix(spice): model pushbutton as a real 4-pin tactile switch
The pushbutton was modelled as a switch between only 1.l and 2.l; the
other two legs (1.r, 2.r) connected to nothing. Wiring GND/GPIO to those
legs silently produced a dead button, and the failure was invisible.

Model it like hardware: 1.l is internally shorted to 1.r and 2.l to 2.r,
and pressing bridges terminal 1 to terminal 2. Wiring to any leg now
works, and putting GPIO and GND on the same terminal is a dead short,
exactly as on a real tactile switch. Back-compat A/B variant preserved.
2026-06-24 03:32:17 +02:00
David Montero 03339a132a feat(pi): gpiozero core + boot feedback, terminal and upload UX
- boot_images manifest: bump arm64 rootfs (gpiozero/colorzero baked in,
  hostname applied at boot, reworded MOTD)
- RaspberryPi3Bridge: onBooted shell-ready detector + sendAndWaitForPrompt
  flow control (resets on disconnect)
- RaspberryPiWorkspace: distinct Booting overlay + piBooted-driven status,
  inline SVG icons replacing emoji glyphs
- SerialMonitor: strip CSI/DSR escapes so the dumb console no longer shows
  a literal [6n next to the prompt
- VirtualFileSystem: upload auto-starts the Pi and waits for the shell, then
  flow-controls each command (no more dropped lines on large files)
- i18n: bootingTitle/bootingNote + reworded offlineNote2 across 9 locales
2026-06-22 20:21:06 +02:00
David Montero 8c32b16589 fix(sim): SSD1306 page-addressing mode (Tiny4kOLED/U8g2 OLED garbled)
SSD1306Core only handled horizontal/vertical addressing (0x20/0x21/0x22) and
defaulted memMode to horizontal. Page-mode drivers (Tiny4kOLED on ATtiny85,
U8g2 page buffer, classic SSD1306 libs) position the cursor with the single-byte
commands 0xB0-0xB7 (page) and 0x00-0x0F / 0x10-0x1F (column nibbles) and rely on
the SSD1306 power-on default of PAGE addressing — they never send 0x20. velxio
ignored those cursor commands and advanced in horizontal mode, so every setCursor
was a no-op and the hatching/border/text piled onto wrong rows -> garbled display.
Fix: default memMode=2 (datasheet power-on) and handle the page/column-set
commands. Adafruit_SSD1306 still works (it sends 0x20,0x00 + 0x21/0x22 explicitly).
Verified: decoded the real ATTinyCore Tiny4kOLED I2C stream renders a clean
border + '128x64'. Adds a page-addressing render test.
2026-06-19 18:08:01 +02:00
David Montero 37b3fac978 feat(sim): ATtiny85 USI I2C — drive I2C devices (SSD1306 OLED) via TinyWireM
The ATtiny85 has no hardware TWI; TinyWireM/Tiny4kOLED drive I2C through the USI
peripheral (SDA=PB0, SCL=PB2). avr8js ships AVRUSI but velxio never instantiated
it, so the I2C bus had no master on the ATtiny85 and devices (e.g. SSD1306 OLED)
got no data — the display stayed blank (and wokwi-ssd1306 threw putImageData with
an empty framebuffer). New UsiI2cBridge instantiates AVRUSI and sniffs the SDA/SCL
lines, replaying START/STOP + 8-bit bytes onto the shared I2C bus as
start/connectToSlave/writeByte/stop (the same calls AVRTWI makes for the Uno).
Validated against real ATTinyCore Tiny4kOLED firmware: decodes addr 0x3C + SSD1306
init/data stream. Firmware tolerates NACK so the sniffer is passive.
2026-06-19 07:44:18 +02:00