Commit Graph

1337 Commits

Author SHA1 Message Date
David Montero Crespo bad2a03c4f fix(sim): import the seat check the socket hop relies on 2026-07-31 23:11:50 +02:00
David Montero Crespo 8bb9d4e85b ui(picker): reSpeaker ONLINE card matches the functional component (sensors, vertical art) 2026-07-31 22:37:07 +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 Crespo 8ff8fdb292 feat(sim): board-agnostic speaker seam — audio out plays on the host
setSpeakerMonitor mirrors setMicrophoneSource: a part that models a
board with an on-board speaker/jack (reSpeaker Lite) gets the peak of
each block the guest played, so it can light a meter, while the audio
itself goes to the host's sound card via the engine bridge. Nothing is
wired for it on the canvas — that is the point: a real board plugged
into your speakers has no wire to draw. No-op on bridges without an
audio path, which reads as a silent speaker.
2026-07-31 17:29:52 +02:00
David Montero Crespo e0a69fecae fix(canvas): apply the drag rank on the group, not inside it
Drag-to-front lifted boards but never parts: a component's z lives
inside .component-interactive-group, which is itself a stacking
context, so the 10+rank set on the inner wrapper was clamped to the
group's own level (1/2) while boards compete one level up as direct
children of .canvas-world. Once any board had been dragged it sat at
10+rank and no component could ever climb back over it — drag an LED
onto the Arduino and the board swallowed it, exactly as reported.

The rank now lands on the group itself (both branches: instruments and
ordinary parts), so parts and boards share ONE rank space and the last
thing dragged really is the thing on top. The inner wrapper keeps its
local 1/5 for selection order within the group.
2026-07-31 16:40:01 +02:00
David Montero Crespo cb26e36b28 feat(canvas): drag-to-front — whatever you dragged last paints on top
An unseated board dropped over a component was painted underneath it
(boards z 0, components z 1) and became impossible to grab back; the
earlier blanket z bump for boards broke the opposite case, hiding LEDs
and transistors behind every board. Neither static order can win both.

The rule is now the user's own gesture: the FIRST movement of a drag
raises that item (board or part, symmetrically) to the top of a
monotonic dragged-stack, so a board dropped over an LED sits visibly on
top — and dragging the LED afterwards wins the stack right back.
Click-select alone never raises: selection must not reshuffle a scene
you arranged. Untouched items keep the static layering (components
above unseated boards, seated boards above their socket), and the rank
map is ephemeral — never saved with the project.
2026-07-31 16:02:26 +02:00
David Montero Crespo 94bfe627d3 ui(examples): reSpeaker glyph at the true 178x437 scale 2026-07-31 15:18:30 +02:00
David Montero Crespo efd6765c4c ui(examples): reSpeaker gallery glyph matches the new vertical board art 2026-07-31 08:21:58 +02:00
David Montero Crespo 159417c927 fix(examples): direct pro-example URLs no longer hang on Loading example
The load effect listed the overlay's `settled` flag as a dependency. On
a direct /example/<pro-id> URL the load can begin the moment the
overlay registers the example — one microtask before the overlay's
import promise settles. The flip then re-fired the effect mid-load: the
cleanup cancelled it (setReady skipped), the re-run hit the loadedIdRef
guard and returned, and the page hung on "Loading example…" forever.

The effect now depends only on the example itself; the 404-vs-still-
loading decision moved into the render, where reading `settled` cancels
nothing. A load cancelled mid-flight also resets the loadedIdRef guard
so a genuine re-run reloads instead of early-returning.
2026-07-31 04:22:52 +02:00
David Montero Crespo 08c5626b79 feat(sim): board-agnostic microphone seam + reSpeaker gallery glyph
setMicrophoneSource on the simulator shim mirrors addI2CDevice: a part
that produces audio hands one 16-bit sample per call to whatever bridge
implements the method (the in-browser JS-engine bridges); everywhere
else the optional chain makes it a silent mic. No board-kind checks in
components. Plus the flat reSpeaker Lite glyph for gallery cards.
2026-07-31 03:30:13 +02:00
David Montero Crespo e9053f73fe ui(editor): one control height and one blue across the toolbar strip
Every control in the unified strip is now 28px tall: tb-btn (was 30),
Libraries (was 26), the C++/MicroPython select (was ~22), the board
selector, Serial/Scope, canvas icon buttons, zoom and Add (all were 32).
The explorer toggle moves inside the Code/Both/Circuit segmented group
as its first segment, and every active-segment blue is the Libraries
blue (var(--color-action-primary)) instead of the hardcoded #0e639c.

The editor zone's overlap floor becomes min-width: max-content — the
hardcoded 280px sat below the real content width, letting the console
button paint over the board selector when the docked chat narrowed the
bar. Now the zone can never shrink under its buttons; flex-wrap moves
the canvas controls to a second line instead. Also closes the gap
between the logo and the File menu (header gap 16->6, menubar
margin-left 14->2).
2026-07-31 00:46:08 +02:00
David Montero Crespo 697a9de4f4 fix(i18n): deep-merge locale files — shallow spread was eating sections
The common namespace was assembled with a shallow spread, so any
top-level section present in two locale files had the later file clobber
the earlier one WHOLESALE. Two live casualties:

- header: adding header.shareProject to common2 (yesterday's raw-string
  cleanup) erased common.json's whole header — header.auth.* and
  header.nav.* — so the account menu rendered "header.auth.signOut" as
  its label. My regression, caught verifying the account-menu fixes.
- editor.share.*: common2's editor section has been clobbering
  common.json's since forever — the Share modal's seven visibility
  labels were silently lost in every locale. Pre-existing.

Locale files merge by key now (deepMerge, both the synchronous en init
and the dynamic per-locale loader), which ends the failure class instead
of the instance; shareProject also moves to common.json's header where
it belonged. Verified: all header.auth/nav and editor.share leaves
resolve again in all nine locales.
2026-07-31 00:30:34 +02:00
David Montero Crespo 155513a493 fix(editor): account footer is the account alone; language rides elsewhere
The bottom-left block read wrong: a globe crowding the account button for
no gain — language already lives in the menubar's Language menu for
everyone, and (for signed-in users) inside the account menu. The footer
now holds only the account button.

Width-aware, as requested: when the explorer is dragged narrow (footer
under 150px, via container query) the username hides and the avatar
alone identifies the account; with the explorer collapsed, the floating
fallback box shows just the circle.

Adds the OSS half of the crash fix: LocaleSync listens for
'velxio-locale-switch' window events and performs the locale change as a
normal SPA navigation. The pro account menu is injected into its own
React root OUTSIDE the Router, so it cannot navigate itself — its
language rows dispatch this event instead.
2026-07-30 22:56:19 +02:00
David Montero Crespo e17f6fd6cf i18n(editor): translate the remaining raw UI strings
The explorer toggle tooltips (Hide/Show file explorer) and the header's
Share-project title were raw English — now keyed and translated in all
nine locales, same common2.json home as the menubar strings.
2026-07-30 21:18:37 +02:00
David Montero Crespo db632832b4 i18n(editor): translate the new menubar in all nine locales
Every menu we added lived on t() English fallbacks — the keys did not
exist in ANY locale file, so a Spanish or Chinese user got an English
menubar. 29 keys (editor.menu.* + the toolbar labels the File menu
reuses) now exist in en/es/pt-br/it/fr/zh-cn/de/ja/ru.

Two entries needed their own leaf keys: editor.toolbar.compile and .run
are nested OBJECTS (title/options/... sub-keys), so t() on them cannot
resolve to a string — the menu now uses editor.menu.compile / .run,
which also avoids double shortcuts ("Compile (Ctrl+B)" plus the menu's
own Ctrl+B column).

Verified mechanically: all 38 keys the menubar references resolve to
strings in all nine locales.
2026-07-30 21:12:28 +02:00
David Montero Crespo d233b66cc9 feat(editor): View and Language menus — full desktop-menu parity
The menubar now matches the desktop app's native menu set: File, Edit,
View, Language, Help.

View collects what the user asked to reach from a menu: Compile (Ctrl+B),
Run, Stop, Reset up top; the panel toggles — File Explorer, Output
Console, Serial Monitor, Oscilloscope/Logic Analyzer — in the middle
(store-backed ones render a live check, like a real desktop menu); and
the canvas view actions (center, zoom) move here from Edit, which goes
back to being undo/redo only, as menus have always worked.

Language lists the nine locales with the current one checked, switching
through the same switchLocale path the header globe uses — so language is
reachable from the menubar regardless of the account state, on top of
living in the signed-in account menu.

Run/Stop/Compile/Reset and the console toggle register through the
editorCommands seam from EditorToolbar (their handlers close over its
state); the explorer toggle from EditorPage; serial and scope call their
stores directly, same as undo/redo.
2026-07-30 21:05:55 +02:00
David Montero Crespo 3b7abfc4f7 feat(editor): account block fuses into the explorer footer
The floating corner box sat ON TOP of the file tree — create enough files
and the last rows scrolled underneath it. The account + language block is
now the explorer panel's footer (VS Code style): the tree scrolls above
it, always. When the explorer is collapsed the block falls back to the
small fixed corner box, so account and language stay reachable.

The standalone language globe becomes a logged-out affordance: once the
pro auth slot renders its signed-in marker (data-auth-user), a :has()
rule hides the globe — signed-in users change language inside the
account menu, where the pro overlay now hosts it.
2026-07-30 20:54:24 +02:00
David Montero Crespo 57d7c93c90 fix(editor): corner-box dropdowns open upward
The language menu and the account menu were written for the top bar and
open downward; from the bottom-left corner box that means straight off
the bottom of the viewport. The language menu flips via a scoped CSS
rule; the account menu (pro) now anchors bottom-up whenever its trigger
sits in the lower half of the screen.
2026-07-30 20:25:52 +02:00
David Montero Crespo 341bebeb39 fix(editor): corner box escapes the header's containing block
The header's backdrop-filter makes it a containing block for fixed
descendants, so the corner box's bottom:8px resolved against the 45px
header — measured at y=-4, sitting on the logo. Portaled to <body>, where
fixed means the viewport. The auth slot keeps working: pro's injector
finds it by attribute wherever it lives.
2026-07-30 20:09:30 +02:00
David Montero Crespo 3100d453c2 feat(editor): language + account move to a bottom-left corner box
IDE-style: a small fixed box in the explorer's quiet bottom corner holds
the language switcher and the account button (pro keeps injecting it into
the same data-velxio-slot, just relocated). The autosave dot rides next
to the menus, and the header's Share button is gone — File > Share/Embed
already covers it.

The point is width: emptying the header's right side gives the toolbar
the last ~150px it needed, so the single-row layout now holds down to
~1400px WITH the chat docked — which is exactly the reported 1440x900
case. Tight-fallback thresholds drop accordingly (1400/1060/1040).

Desktop-editor variant only; marketing pages and mobile keep their
header untouched.
2026-07-30 19:52:58 +02:00
David Montero Crespo fff5823543 fix(editor): no awkward middle state for the in-header strip
At 1440x900 with the chat docked, the merged header hit an ugly in-between:
the wrapped canvas controls floated as an orphan group inside a tall
transparent header, next to a dead gap before the language/user controls.
Reported by the user as "se ve raro" — wider screens looked fine.

Below the width where one row genuinely fits (thresholds account for the
docked chat's 380px / collapsed 36px reservation), the strip now falls
back to being its own full-width second bar with its old background and
border — the exact familiar pre-merge layout. Wide screens keep the
single 44px row; tight screens get the classic two bars; the broken-
looking limbo between them no longer exists.
2026-07-30 19:43:31 +02:00
David Montero Crespo 8868a3c585 feat(editor): fold the "..." menu into File — with its PRO pills
The toolbar's overflow button held four leftovers (Share/Embed, Sync to
GitHub, Upload firmware, Record simulation). They move into the File
menu, which is where a session-frequency action belongs, and the strip
loses one more button. The PRO pill travels with them — same style the
overflow used (now .emb-pro) — and the two File items that were always
premium (BOM, schematic image) finally show it too: users should know an
item is premium BEFORE clicking, not via a surprise upgrade prompt.

The pro actions keep firing the same window events the overflow items
fired (share/github-sync/record prompts), so the overlay's listeners are
untouched and OSS builds keep their silent no-op. Dead overflow CSS
removed with the button.
2026-07-30 19:18:37 +02:00
David Montero Crespo ffb4fc0159 feat(editor): toolbar rides inside the header — one row, not two
With the marketing nav gone from the editor header, its middle was ~700px
of dead space at 1280 while the toolbar occupied a whole second 38px bar.
The unified toolbar strip now fills that middle through a new AppHeader
editorToolbar slot: one 44px row where there used to be 44+38 — a full
row returned to the code and the canvas.

The strip keeps its own class names, so everything that keyed on them
keeps working untouched: the container queries, the docked-chat
padding-right, and the internal flex-wrap. When it truly cannot fit, the
strip wraps and the header grows (height: auto on the modifier class)
instead of clipping or overlapping; brand and the right-side controls
stay pinned to the first line. Inside the header the strip drops its own
background and border so it reads as one bar, not a box within a bar.

Mobile keeps the previous layout (no strip; the mobile tab bar remains).
2026-07-30 18:56:24 +02:00
David Montero Crespo b70bb8ab83 refactor(routes): OSS is the editor; marketing moves to the overlay
The open-source build now ships exactly its product surface: /editor, the
examples gallery (/examples, /examples/:id) and the per-example editor
(/example/:id). Landing, about, pricing, docs, the 14 keyword-targeted
simulator landings and the v2/v2.5/v3 showcases move to the private
overlay, registered through the same registerProRoutes seam that already
carries login/admin/classroom.

Root behaves per build: an overlay that registers an index route claims
'/' (velxio.dev keeps its landing); otherwise '/' redirects to /editor.
The redirect waits for the overlay import to settle — same contract as
markProExamplesSettled — so a velxio.dev visitor is never bounced into
the editor because the landing was 300ms away from registering.

Prerender and sitemap follow the split: entry-server pulls the marketing
page map from '@pro/pages/marketing' behind a VITE_PRO_BUILD-gated dynamic
import (the proven main.tsx pattern, with an OSS stub for tsc), and
generate-sitemap lists only served routes in OSS builds (2 URLs) while
pro builds stay byte-identical — verified against a pre-migration
baseline: sitemap (37 URLs) and prerendered /, /about, /docs,
/arduino-simulator, /esp32-simulator, /v3 all identical modulo hashed
asset names. OSS prerender drops exactly the 32 marketing pages (348→316).

The OSS header slims down to match: Editor, Examples, GitHub, Discord —
the marketing links only render in pro builds, where their routes exist.
The editor Help menu links those pages absolutely (velxio.dev) in OSS,
exactly like the desktop app's Help menu.
2026-07-30 17:50:14 +02:00
David Montero Crespo 7d78f87bf0 feat(editor): menu Help, homogeneo con la app de escritorio
Mismo contenido y orden que el Help del menu nativo de escritorio
(pro/desktop menu.rs): Documentation, Examples, Pricing | Velxio Home,
Blog, About Velxio | Discord Community, GitHub Repository. Los enlaces que
el header del editor dejo de mostrar como nav de marketing recuperan aqui
un sitio ordenado, y todo abre en pestana nueva para que el editor (y el
trabajo sin guardar) se quede donde esta — el mismo criterio que la app
nativa, que abre el navegador del sistema.
2026-07-30 17:31:31 +02:00
David Montero Crespo 3cc1510cae fix(examples): el enlace directo a un ejemplo pro ya no parpadea un 404
Reportado en /example/pi5-opencv-vision: al recargar aparecia un instante
el 404 con el header completo (menus incluidos) y despues cargaba el
editor. La carrera: los ejemplos pro se registran cuando aterriza el
import dinamico del overlay, y la pagina resolvia la galeria ANTES,
concluia "no existe", pintaba el 404 y al llegar el registro re-renderizaba
al editor.

"No esta en la galeria" y "no esta TODAVIA" son respuestas distintas, asi
que el registro gana una senal de asentado: main.tsx la activa cuando el
import del overlay resuelve (o falla — finally), e inmediatamente en la
build OSS, donde no viene overlay y un 404 debe ser instantaneo. La pagina
se queda en "Loading example..." hasta que el registro asienta y solo
entonces un id ausente es de verdad un 404.

markProExamplesSettled es idempotente y solo notifica una vez, con test —
el sintoma de romper eso seria una tormenta de re-renders por hot-reload
del overlay.
2026-07-30 17:06:05 +02:00
David Montero Crespo 03652c0748 fix(editor): la fila envuelve en vez de estrujar botones
Con la ventana estrecha y el chat acoplado, a la fila unificada le faltan
~60px y flexbox se los quitaba al toggle Code/Both/Circuit: de 101px a 32,
con dos de sus tres botones recortados a esquirlas inalcanzables bajo el
overflow hidden. Medido en staging a 1024px.

El toggle deja de encogerse (flex-shrink 0) y la fila hace wrap: cuando de
verdad no cabe, los controles del canvas bajan a una segunda linea corta.
Dos filas breves ganan a botones invisibles.
2026-07-30 16:43:46 +02:00
David Montero Crespo 03afa31b5f feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.

Tres piezas:

1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
   estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
   AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
   build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
   File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
   autoguardado, Share y usuario se quedan.

2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
   boton; lo que se usa unas veces por sesion va al menu. File: nuevo
   workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
   imagen del esquema. Edit: undo/redo (con su estado real del historial),
   centrar vista, zoom. Las acciones viven en cierres de cuatro
   componentes distintos, asi que hay un registro id->handler
   (lib/editorCommands, mismo patron de seam que registerProExamples o
   registerBoardBuiltins): cada dueno registra al montar y el menu invoca
   por id; un item sin dueno montado se pinta deshabilitado, que ademas es
   la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
   viejo no borra al sucesor) y tiene test de eso.

3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
   botones inline de Import/Export y sus gemelos responsivos del More
   (~70px recuperados); el More queda solo con los extras pro (firmware,
   GitHub, share, grabacion). El CSS responsivo de los botones retirados
   se va con ellos.

Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 16:26:36 +02:00
David Montero Crespo fdcf0d19d2 feat(canvas): ver donde cae lo que anades, y clicks con sentido
Tres cambios que van juntos porque atacan la misma queja: "anado un
elemento y a veces ni veo donde se anadio".

1. Donde cae. Ya se anclaba a la esquina visible, pero la cascada que evita
   que se apilen iba indexada por components.length: seguia avanzando
   aunque movieras o borraras piezas, asi que las caidas se alejaban cada
   vez mas de donde estabas mirando. Ahora toma el primer hueco LIBRE desde
   la esquina, bajando en diagonal; si apartas la ultima, la siguiente
   recupera su sitio. Extraido a utils/dropSlot con 8 tests, incluido el
   caso de "la apartaron" y el tope para no salirse de la vista.

2. Que se vea. El recien anadido queda seleccionado, y la seleccion pasa de
   un borde discontinuo quieto a un caminito de hormigas. El movimiento es
   lo que capta el ojo en un canvas lleno; un borde fijo se pierde. Va en
   un pseudo-elemento por fuera del cuerpo, sin robar clicks ni tapar el
   dibujo, y se queda quieto si el sistema pide menos animacion.

3. Clicks. El izquierdo SELECCIONA y ya esta; antes abria el panel de
   propiedades, o sea que no podias ni senalar una pieza sin comerte un
   popup que luego habia que cerrar. Propiedades y pines pasan al click
   derecho, que es donde va lo deliberado. En tactil se mantiene tocar ->
   panel, que ahi no hay boton derecho.
2026-07-30 15:40:52 +02:00
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 b7a191eaab feat(pi): techo de capacidad para los guests QEMU (multiusuario)
Cada guest es un proceso QEMU con su propia RAM (1-2 GB segun placa) y
sus hilos de vCPU, asi que el limite lo pone la maquina, no el codigo.
No habia ningun tope: el usuario N simplemente empujaba la caja a swap y
la sesion de TODOS se volvia lenta, que es peor que decirle al usuario N
que espere un minuto.

  VELXIO_PI_MAX_INSTANCES   (6)  guests simultaneos en la maquina
  VELXIO_PI_MAX_PER_OWNER   (2)  guests por persona

El "owner" es el hash de la cookie de sesion: sirve solo para contar, no
se guarda ni se lee de vuelta, y cae al host del cliente cuando no hay
cookie (sidecar de escritorio, tests). Sin identidad solo aplica el tope
global.

El rechazo llega como un mensaje que dice que hacer ("prueba en un
minuto" / "para una de tus sesiones"), no como un fallo mudo, y se
comprueba ANTES de lanzar el proceso.
2026-07-29 21:15: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 84e32ef1f2 feat(qemu): optional restricted guest egress via a single guestfwd tunnel
Guests stay on '-nic none' unless a profile opts in, and opting in does
NOT mean internet: the NIC is user,restrict=on (no route out, no route
to the host LAN) with one guestfwd to whatever command the overlay
configures — a filtering proxy in practice. Keeps 'user code never gets
a raw socket outside' true by construction.
2026-07-29 04:39:03 +02:00
David Montero Crespo 8d4f298f68 feat(qemu): session ceiling for guest instances
A forgotten tab pinned a QEMU process and its RAM for as long as the
browser stayed open. Instances now shut themselves down after
MAX_SESSION_SECONDS (2 h default, env-tunable) and say so on the serial
line instead of vanishing.
2026-07-29 04:33:12 +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