The emulated BMI270 and battery gauge were faithful and inert: with nothing
driving them the IMU reported the board lying flat for the whole run and the
gauge reported the single voltage it was seeded with, so a sketch that reacts
to tilt or warns on low charge could not be exercised at all.
A Sensors popover on the canvas header supplies the missing half. Tilt is
entered as roll/pitch on a drag pad rather than three raw axes — what a user
means is "lean it left" — and gravity in the board's frame is that rotation
applied to (0, 0, 1) g, which is also why the vector stays at 1 g whatever the
attitude. Turning the board spins the gyro for as long as the drag lasts and
parks it back at zero shortly after, matching a real part: a held tilt reads no
rotation. Battery is a slider over the LiPo working range.
Gated per board by builtInImu / builtInBattery on the pro board def, the same
way the Mic toggle is gated, and each section renders only if its flag is set.
The microphone stream also learns to resample. The engine now tells the source
which rate the guest programmed, so a sketch recording at 8 kHz consumes two
captured samples per read instead of playing back at half speed.
Authors paste screenshot URLs without knowing the  syntax; GFM
autolinks them into plain <a> tags, so videos previewed but images did
not. An autolinked href ending in an image extension now renders as the
image itself; explicit [label](...png) links stay links. All images get
loading=lazy.
useProjectStore.currentExampleId — stamped by loadExample, dropped the
moment a real project (or a blank workspace) takes over. The compile
request and the run event carry it as example_id, analytics only, so
'which examples get compiled and run most' becomes answerable.
setComponents/setWires (project load, example load, .vlx/wokwi import,
clear) bump a counter so observers that diff components/wires between
updates can tell hydration from a user deliberately adding one part.
Canvas edits (addComponent/addWire) don't touch it.
Second half of the deep-link race: an overlay board kind added BEFORE
the overlay registers makes isEsp32Kind() false, so addBoard wires the
board as a generic AVR — and the parts on the canvas attach their I2C
device models to THAT simulator (measured live: the gesture's device
sat on an AVRSimulator while the C6 engine booted with an empty bus).
The overlay's rebuild then swapped simulatorMap but left the flat
`simulator` field stale, so DynamicComponent's [simulator] effect never
re-attached anything. Re-syncing the flat field when the rebuilt board
is the active one makes every part re-attach against the real shim —
sensors, displays, mic sources, the lot.
The pro overlay loads async, so a deep-linked example attaches its part
device models to the shim wired around the STOCK QEMU bridge — whose
attachSyncI2cDevice is a silent optional no-op — and only then does the
overlay's factory rebuild the board with the js-engine bridge. The
rebuild made a brand-new shim; the parts on the canvas hold the old one
in their attach closures and never re-attach. Result: the engine booted
with an EMPTY I2C bus and arduino's i2c-ng driver failed every
transaction with ESP_ERR_INVALID_STATE (the seated C6 gesture example —
measured live with the bus device table at Map(0)). Whether it broke
was a chunk-load race; the pro bundle's growth turned it into a sure
loss.
The shim now records every sync-attached device and a rebuilt shim
adopts its predecessor's — through addI2CDevice, so the new Delegating
bridge buffers them pre-connect and its start() replays them into the
engine after reset, as it always did for attaches it actually heard.
Several boards share one FQBN (the Pimoroni RP2350 trio all compile as
rpipico2), so FQBN-keyed metrics can't tell them apart. The compile
request and the run event now carry board_kind — analytics only, rides
into the metric hook's extra.
suppressOnlineOnlyAds(): overlay seam so the hosted build can silence
'available in the online editor' ad cards for items it manages — an
embargoed item must vanish, not advertise the site the user is on.
Pure OSS builds never call it; ads behave exactly as before.
The whenDefined recalc was necessary but not sufficient — measured on
staging, the example route loses BOTH ways:
- the element side: every settle timer can fire before the part is
findable in the DOM. The pinInfo-ready poll in DynamicComponent now
also re-derives wire endpoints the first time the part's pinInfo is
actually readable, and waits up to 10s (the 2s cap was the corner-
wire bug again on slow connections).
- the wire side: loadExample stores wires with zeroed endpoints BEHIND
awaits (library installs), potentially after every recalc has come
and gone. A canvas effect now spots endpoints still at (0,0) and
runs one deferred recalc; real coordinates land, the condition
clears, no loop.
Both Buy renderers (picker hover panel + Part Inspector) now append
utm_source=velxio&utm_medium=simulator&utm_campaign=datasheet with the
component id in utm_content, so vendors can attribute the referral.
trackProductPageClick fires the GA4 event and dispatches a
'velxio:product-page-click' CustomEvent — an overlay build can persist
the click server-side without this tree knowing about it.
The canvas re-derives wire endpoints on load-settle timers (100/300/
500ms), but an overlay-defined custom element can upgrade after the
last one — its chunk is large — and calculatePinPosition then finds no
pinInfo, so every wire to the part falls back to its corner and floats
there until something moves it (field report: the Grove gesture module
with all four Grove wires dangling on bare PCB). A customElements
.whenDefined hook now recalculates when the tag actually defines,
however late that is.
tailAfterStream sliced result.stdout at the live-buffer offset, but the
buffer interleaves stdout+stderr in arrival order while result.stdout is
stdout-only plus a joining newline — offsets never line up, so the flush
could start mid-line ('f/esptool_py && ...'). Instead, the poll loop now
emits one final onProgress(state:'done') with the completed buffer and
the existing delta logic flushes the tail perfectly aligned. tailAfterStream
is gone.
With the success-path reprint gone, the lines that land between the
final running-poll and job completion (esptool image creation, the
binary-size summary) silently vanished. tailAfterStream() appends
result.stdout past the streamed offset, noise-filtered, before the
structured analysis — skipped once the live buffer hits its 256 KB
tail-trim cap, where offsets stop lining up.
ESP-IDF builds sprayed the compilation console with configure chatter
(-- cmake status, git-metadata probes that 'fatal' on the synthesized
project tree, IDF component-manager NOTEs, Python env checks) and then
reprinted the entire stdout a second time when parseCompileResult ran
over the final result after the live stream had already shown it.
- isNoiseBuildLine(): pattern list for build-system chatter, applied to
the live stream deltas and to the final-result parsing.
- parseCompileResult(streamedLive): skips the stdout reprint on success;
on failure re-emits only error/warning lines so they still get the
red/yellow markers the plain live stream lacked.
Compiler errors, FAILED blocks, ninja [N/M] progress, esptool output and
binary-size summaries all still show. The full log stays available
server-side.
The chat reserves its width as padding-right on .app-container, but an
absolute inset-0 layer spans the padding box, so the picker's right
column and its scrollbar landed under the panel. The screen now gives
up the same width through the same CSS variable, with the same 0.12s
ease the padding uses, and falls back to zero in builds without the
overlay.
A modal over a pristine /editor dims an interface that is empty anyway
and offers a Cancel whose only destination is a dead workspace. The
picker now takes over the content area (StarterScreen): no veil, no
Cancel, the grid gets the room the 840px dialog never had, and the
examples gallery link is the secondary path out. The modal stays for
"New workspace" over existing work, where the replace-everything
warning and Cancel mean something — both shells share one card grid.
The details that make the takeover behave like a page: it sits at
z-index 90 (above the canvas' floating zoom pill, below the header's
stacking context so the menu dropdowns stay clickable; the fixed
account corner box is render-gated instead), the panes underneath are
inert while it is up, focus lands on the heading on mount and parks on
the workspace container on dismissal, and any path that fills the
workspace meanwhile — the File-menu modal, Open project…, a dropped
.vlx — dismisses it rather than letting its cards silently wipe the
loaded work. Full editor.newProject i18n for all nine locales (German
in Sie-form), heading outline h1→h2 / h3→h4, token focus ring, and
reduced-motion respected.
The component grid and the category filter now follow a fixed
maker-first category order (sensors, outputs/LEDs, displays, input,
motors, communication, electromech, other… then passive, analog, logic):
Velxio's audience reaches for a DHT22 or an LED far more often than a
bare transistor or a 74HC gate. Stable sort — order within a category is
untouched, and search results group the same way.
The account options that only lived in the bottom-left user button
(subscription, licenses, simulation history, replays, privacy) are now
reachable from the header too: a new Account menu hosts the pro items via
the SAME data-velxio-slot="user-menu" the corner dropdown uses — one
overlay renderer, two hosts, empty in OSS builds. Language moves from its
own top-level trigger into a section of that menu, so the menubar stays at
five triggers and frees header room for when the AI chat docks.
The canvas header runs out of room when the AI chat panel docks on the
right — the board selector clipped and the zoom cluster was squeezed out
entirely. The −/%/+ cluster now floats on the circuit canvas' bottom-right
corner (always free), with stopPropagation so a click never starts a pan.
Component picker: registry entries that ARE boards (the injected Raspberry
Pi family) no longer render as component cards — clicking one dropped a
dead 40-pin canvas prop that never boots (that is how "add a Pi 4" looked
like it placed a Pi 3). The boards row is the only path now, and it gains
Raspberry Pi Zero / 1 / 2 cards (Pi-3 illustration, PRO pill, same gate):
their QEMU profiles already existed in qemu_manager; the canvas renders
them through the Pi-3 element and BoardOnCanvas/store route them like any
Pi kind.
Part inspector: a board whose canvas node is NOT a registered custom
element (the Pi family's plain <img> wrapper) now clones the live node for
the preview instead of giving up — the Pi 3/4/5 pin map was completely
missing before. Wide boards with 40+ pins (Mega, Pi family) render
ROTATED 90° so their long pin rows become side columns with vertical room
(new PINS_ART_ROTATED_H box), and pinInspectorLayout splits a dual-line
header pair to OPPOSITE gutters the way printed pinout posters do — 40
interleaved labels never fit one gutter. Existing 18 layout tests pass.
esp-insights/esp-rainmaker print their version banner to stderr during the
CMake configure; without git metadata in the component tree it reads
"Project commit: HEAD-HASH-NOTFOUND" and the stderr classifier's default
painted it as a red error. Users read it as a failed build (it is not).
Classify "project commit:" lines as info.
The header spoke two languages: the ESP32-CAM/Sense board toggle says
"Camera" while component-owned webcams printed their registry label
("HuskyLens cam", "Gesture cam") — which read as a different control.
The button now says "Camera" everywhere; the component identity stays in
the tooltip, where it disambiguates when several cameras coexist.
Cards drop to a 178px grid (dialog 840px) with tighter type so more
starters fit per row; the Blank card's plus glyph goes accent-blue at
64px. Arduino Mega (mega-blink) joins the Arduino section next to the
Uno. Recaptured xiao-rp2040-blink.webp now that the board art is
silk-screened "XIAO-RP2040" (was "XIAO-ESP32-C3").
Boards with a camera surface their webcam state in the header
(CameraToggle); components that ARE cameras — vision sensors whose
element captures the webcam itself — had no header presence, so users
who learned "the header icon says my webcam is in use" read its absence
as a broken feature. A tiny registry seam lets any component register
its camera (label, live/error/off, reason, toggle); the header renders
one CameraToggle-styled button per entry and nothing when none.
The starter cards now carry the same circuit thumbnails the examples
gallery uses (ExampleThumbnail → /examples-thumbs/<id>.webp with the
CircuitPreview SVG fallback): boards with a Blink example reuse its thumb,
the rest get freshly captured starter-<kind>.webp shots in the same folder
(XIAO S3/C3, Netduino 2, Pi 4/5; plus the missing xiao-esp32c6-blink and
xiao-rp2040-blink example thumbs).
The ESP32 section is now one card per chip generation with the Seeed XIAO
variant preferred (DevKit V1 / XIAO S3 / XIAO C3 / XIAO C6), and XIAO
RP2040 joins the Raspberry Pi section — replacing the previous
every-variant list and the separate XIAO section.
Fixes: a pristine /editor visit clears the hardcoded Uno starter BEFORE
showing the dialog (empty canvas behind; cancel now leaves a blank
workspace), and clearing the last board re-points the editor at a
nonexistent group so Monaco no longer keeps showing the deleted board's
sketch (removeBoard only re-points when another board remains).
New NewProjectDialog: a bare /editor visit (and the New workspace button)
offers a blank workspace or a ready-to-run Blink starter per board family
instead of silently dropping the user into the hardcoded Uno + LED starter.
Boards with a gallery Blink example load it fully wired; the rest get a
fresh board with the on-board-LED sketch. Pro-gated boards carry the PRO
pill and go through the boardGateDecision seam. Shows once per page load;
en/es strings (the other locales fall back to English for now).
The word "Buy" on a solid button read as an ad inside a work dialog. The
link is really a pointer to the part's product page, so it now says that —
localized in all nine languages — with an external-link icon instead of a
shopping cart. Same URL, same placement, in both the part inspector and the
picker hover card.
The part inspector (right-click dialog) now lists the gallery examples that
use the part at the bottom of the Properties tab — matched by metadata id
against boardType/boardFilter/boards[].boardKind/components[].type, so it
works for components and boards alike, including overlay-registered ones
(re-derived via the pro-examples version subscription). Each link opens the
live editor (/example/:id) in a new tab. i18n key in all 9 locales.
The /examples board filter combo was a fixed list, so examples for
runtime-registered boards (XIAO, M5Stack, Raspberry Pi, ...) were reachable
only under "All". The gallery now appends a tab per board kind found in the
registered examples, labelled from BOARD_KIND_LABELS.
e-paper RAM persists between refreshes, but the decoder reset the accumulated
window union on every 0x20 MASTER_ACTIVATION. A PARTIAL refresh (BadgeOS redraws
only a strip when you navigate) then set a small window, so composeFrame emitted
just that strip, placed top-left with the rest black — reading as a 90°-rotated
screen with a black band down the side. Keep the union (and the greyscale flag)
across refreshes so it stays at the full-panel extent set by the first full
refresh; every frame now composes the whole persistent RAM and partial updates
land in place. Cleared only by a real RST (0x12). Paged full refreshes (GxEPD2)
re-cover the panel each frame so they are unchanged; the BadgeOS menu render is
byte-identical and all 23 decoder/epaper tests pass.
A panel driven with a Y-decrement entry mode through the rotation-1 transpose
(BadgeOS on the Badger 2350) rendered upside-down: its text read vertically
flipped. The transpose was calibrated against the standard Y-increment
(GxEPD2 / raw-SSD1680) path, where the source index maps to display row via
Wn-1-x; under Y-decrement the source axis ends up flipped, so map it straight
(x) to the display row for that case. Gated on the same yTopIsHigh signal
already used for the RAM read, so Y-increment panels are byte-for-byte
unchanged. Verified: the BadgeOS factory menu now renders upright ("BadgeOS"
title + app icons readable). 23 decoder/epaper tests still pass.
Panels that upload a custom waveform LUT (0x32) AND write both RAM planes
(0x24 + 0x26) in a frame drive 4-level greyscale: each pixel is 2 bits (one
per plane) composited by the custom LUT into 4 grey levels. Auto-detect this
from the command stream (custom LUT + red-plane written; never key off a panel
name) and expose an optional Frame.grey (0..3) alongside the existing 1-bit
`pixels` (kept as a level>=2 -> white projection so 1-bit and BWR consumers -
EPaperPart, GxEPD2 - are byte-identical). composeFrame builds a parallel grey
plane and rotates it through the same transpose as the palette plane.
The combo (bw<<1 | red) -> level map is [3,2,1,0] (white/light/dark/black):
bw is the dominant luminance bit (bw=0 -> light pair, bw=1 -> dark pair), red
refines one step within each half; verified against the captured menu's plane
populations. Exported DEFAULT_GREY_COMBO_TO_LEVEL + an optional greyComboToLevel
option (tunes ordering only, never forces greyscale on).
Standard SSD1680 paths (raw badge examples, GxEPD2 full refresh) load the OTP
LUT, never send 0x32, never write 0x26 -> unchanged. 23 unit tests pass
(ssd168x-decoder + epaper-part-integration, incl. new greyscale + no-regression
cases).
Panels that upload their own B/W waveform LUT via 0x32 (WRITE_LUT) instead of
loading the OTP/built-in waveform (0x22 with the 0x10 load bit) redefine the
generic SSD168x B/W convention: on the OTP waveform RAM bit 1 = white, but a
custom-waveform panel in our catalog inverts it (bit 1 = black). Auto-detect
the custom LUT from the command stream (never key off a panel name) and XOR the
B/W polarity; when set, compose from the 0x24 plane only (the 0x26 plane is the
differential "old" framebuffer, not a mono mirror). Every standard path we
drive (GxEPD2 full refresh, the raw SSD1680 badge examples) loads the OTP LUT
and never sends 0x32, so their polarity is unchanged.
Also fix descending RAM ranges (X-/Y-decrement entry mode, e.g. yrange=[263,0]):
normalize window bounds and the write-cursor wrap with min/max (verbatim [0] as
the low bound collapsed every scanline onto one row -> garbled/blank frame), and
read the visual-top row from the high RAM address under Y-decrement. Byte-
identical for ascending ranges. Removes the dead ramTarget field.
20/20 unit tests pass (ssd168x-decoder + epaper-part-integration).
The in-browser MicroPython run path required `sim instanceof RP2040Simulator`,
so an overlay-registered pro simulator (e.g. the RP2350's ARM MicroPython)
could never use it. Accept any simulator exposing loadMicroPython(files)
instead - generic plumbing, no board-specific reference; a sim without the
method is a no-op exactly as before. RP2040 behavior is unchanged.
Replaces the inline swap-the-action-bar confirmation (buttons jumped
around) with showConfirmDialog, same pattern as FileExplorer. While the
modal is open the dialog's Escape/click-outside close handlers stand
down. Boards keep their own remove-board modal via skipDeleteConfirm.
NewsMarkdown (shared renderer): YouTube links render as a thumbnail +
play badge that swaps to the youtube-nocookie iframe on click (nothing
loads from the player until the user opts in); images render inline;
all links open in a new tab. Product decision supersedes the alt-text
image stripping: posts embed screenshots/demos, and the README now
documents that embedded media loads in the viewer's browser.
Security review follow-ups (all LOW):
- proxy: negative cache on upstream failure (no outbound fetch per
request while velxio.dev is unreachable) and a 2MB body cap via
streaming read (httpx timeouts are per read op, not per body)
- NewsModal: OSS builds render markdown images as alt text so viewers'
browsers never contact remote hosts; pro cloud builds show them
Editor shows scheduled product-news posts as a markdown modal: max one
per day, oldest first, each post only ever once (marked seen at
delivery). OSS default source proxies velxio.dev's public feed through
the local backend (cached ~6h, anonymous, VELXIO_NEWS=off to disable)
with seen-state in localStorage; a pro overlay can register a
server-backed per-user source via lib/newsSource.ts.
File names may now carry '/' paths; the explorer renders each board group
as a collapsible folder tree (folders first, files in group order, CSS-var
indentation because .fe-file-item pins padding with !important). New-folder
buttons on the board header and per-folder hover actions (new file here,
new subfolder, delete with confirm when files are inside; a folder holding
every file of the group cannot be deleted - groups keep >= 1 file).
Model: names are the source of truth; only EMPTY folders are tracked
(folderGroups per group) so they survive until a file lands in them, then
materialise into the name prefix. Round-trips through .vlx (optional
folderGroups field, laxly validated), the auth stash draft, and
loadProjectState. Examples gain boards[].files so a single board can ship
a full multi-file tree (first entry = main sketch).
Backend: the arduino-cli sketch writer now creates subdirectories for
'/'-pathed names and REJECTS names that resolve outside the sketch dir
('..' traversal) - previously it crashed on the former and wrote outside
the tmp dir on the latter.
- examples-robot-desktop: Serial messages, comments and identifiers
(right/left servos, light threshold naming, START state) across the
embedded .ino and headers
- examples-100-days: PULL_DOWN explainer comment in the MicroPython menu day
On real hardware your code lives in flash, not on the microSD; a sketch
listing the card should see its data (csv, json, txt, wav...), not its
own source. buildProjectSdImage now skips .ino/.h/.hpp/.c/.cpp/.py from
the workspace auto-copy. Explicit uploads are exempt - an upload always
lands whatever its extension. Panel hint updated; tests cover both the
exclusion and the upload exemption (15 green).
- The live card file action now says Download (it was Save).
- Right-click opens the board inspector in EVERY mode: the contextmenu
handler moves from the drag overlay (hidden during simulation) to the
board wrapper, so the SD panel is reachable while the sketch runs and
the board's buttons stay interactive.
- Buy button in the inspector action bar goes solid brand blue #0071e3,
matching Velxio's solid-button language.
The SD panel only knew about uploads; everything the GUEST wrote to the
FAT card (photos, logs) stayed invisible inside the bridge. New
readFat16Image() parses the card image back (BPB-driven, real cluster
chains, VFAT long names, root dir - the inverse of buildFat16Image but
general enough for FatFs-written cards), and the panel gains a live
'Card contents' section reading the bridge's readSdImage() with a
per-file Save (download) button and a Refresh. Round-trip covered in
fat-image.test.ts (16 green).
Real webcams hit the 23000-byte bound, but the S3 esp32-camera build
allocates width*height/5 = 15360 bytes for a QVGA JPEG frame buffer and
stops copying one 1 KiB DMA half-buffer short of it, so oversized frames
lost their tail - including the EOI marker cam_hal scans for (the NO-EOI
storm on staging; the small canvas-fake frames masked it). The bounded
encoder now takes a budget (14000 for the Sense, default 23000 for the
QEMU ESP32-CAM path unchanged), the downscale fallback re-runs its own
quality ladder, and a frame that cannot fit is skipped instead of sent
truncated.
The toggle's attention border reused the .velxio-ants class name, which
is ALSO the canvas selection outline whose ::after is position:absolute
- on the non-positioned button it anchored to the toolbar and drew a
marching-ant line across its full width. Renamed the button variant to
.velxio-btn-ants. The ants also showed while the simulation was stopped;
now they only run when the sketch is running and the camera is off (the
only moment the button is actually asking for a click).
New canvas-header Mic toggle, shown for boards whose ProBoardDef declares
builtInMicrophone (their bridge implements setMicrophoneSource). Capture
runs at the AudioContext's native rate, is decimated to 16 kHz and ring-
buffered (2 s, drop-oldest on overflow, silence on underflow - the engine
paces pulls in simulated cycles, up to 4x real time during boot boost).
No auto-start: with the toggle off the bridge's 440 Hz test tone keeps
mic sketches alive, so taking the user's microphone stays an explicit
click. The source is re-applied on every run start to survive bridge
rebuilds.
The Camera button was hard-gated to the esp32-cam board kind. The Sense now
runs its camera through the S3 engine's LCD_CAM model, fed by the same
useWebcamFrames -> sendCameraFrame path, so the toggle shows for it as well.
Three-part fix for the library-contamination class of ESP32 compile
failures (a user's project kept pulling unrelated libraries because 79%
of projects compile with an empty manifest, forcing scan-all resolution
over ~1000 shared libraries):
1. Name-affinity ranking in _find_library_for_header: the provider whose
name matches the header stem wins (exact normalized match > esp32
arch declaration > contains-stem, alphabetical only as tie-break).
Previously first-alphabetical won, so any stray library shipping a
generic header (Servo.h has 6 providers) could hijack the build.
2. Manifest auto-migration: scan-all compiles now report the libraries
the build really used (manifest_incomplete +
manifest_suggested_libraries on the compile result); the editor folds
single-candidate suggestions into the board's declared manifest after
a green build, so the NEXT compile runs scoped. One green build = one
migrated project; empty-manifest scan-all behaviour is untouched.
3. Install-declares: the Wokwi import library modal records successful
installs in the active board's manifest, and .vlx export/import now
round-trips board.libraries.
Tests: test_espidf_library_ranking.py (9) + library-manifest.test.ts (19).
A ProBoardDef can declare builtInSdCsPin, and the store already built a FAT
card from the project files for it - but there was no way to put YOUR files
on that card: the upload panel only existed for the microsd-card component
and for parts with metadata.sdSlot, and boards have no properties bag.
Boards now carry sdFiles (same shape the SD panel persists everywhere else):
- BoardInstance.sdFiles, consumed by the store's Run path (card component
still wins when one is on the canvas);
- the board inspector shows the SD Card panel whenever the board kind
declares builtInSdCsPin, and routes the panel's onChange to updateBoard;
- .vlx round-trips it (serialisableBoard + payload type), and gallery
examples can pre-load the slot via ExampleBoard.sdFiles, applied by
loadExample the way the panel would.
First user: the XIAO ESP32S3 Sense (CS=GPIO21), whose board def and example
land in the pro overlay.
- Rotate/Delete (and the board actions) moved OUT of the left column into a
bar across the bottom of the dialog, so they are in the same place on both
tabs. Buy joins them, right-aligned: it used to be the last thing in the
datasheet body, which meant scrolling that pane to the end to reach it, and
it did not exist at all on the Properties tab.
- The datasheet scrolled sideways because the doc tables have a nowrap first
column (the pin name) and wide pinouts pushed past the pane. The dialog is
wider (720px, still clamped to the viewport so it cannot run off the right
edge) and the doc now wraps: fixed table layout, wrapping first column,
pre-wrap on code blocks, overflow-x hidden.
Switching Properties <-> Datasheet resized the whole dialog: each tab sized
to its own content, so a short property list shrank it and a long datasheet
grew it. The tab pane is now a fixed 360px (capped at 46vh) and each tab
scrolls inside it, so the dialog holds still. The mobile full-sheet keeps
flowing as one column.
Rotate and Delete were outline buttons; the brand kit says interactive
surfaces are solid accent blue (--blue-600 #0071e3, hover --blue-500) and
destructive ones solid danger red (--red-500 #ff453a), both with white text.
They use the app's own tokens rather than hardcoded hex.
The preview effect still depended on componentMetadata BY IDENTITY. For
components that is stable (the registry hands back the same object), but for
BOARDS the canvas builds the metadata inline per render, so every parent
re-render - which a pointer moving over the canvas causes - tore the preview
element down and rebuilt it. The dialog visibly resized and snapped back.
It now keys on a digest of the metadata (id, tagName, defaults) and reads the
object through a ref, joining propsKey and pinsKey which were fixed the same
way earlier. Nothing rebuilds unless something real changed.
91 of the 156 catalogue parts have neither an authored doc nor a metadata
description, so the Datasheet tab rendered nothing but tag chips and read as
broken. It now states plainly that no datasheet is written yet, and points at
the pin map, which IS the useful content for those parts. The picker hover
card gets the same line, since both render one body.
The Arduino Uno came out wrong: 18 pins on the top header, 13 on the bottom,
and SIX x values shared between the two. The column detector accepted any two
pads sharing an x, so those pairs were read as columns and most of the board
was thrown into the side gutters with leader lines fanning across the art.
A column now needs three pads AND small gaps between them (no more than 40%
of the body), which is what a real header looks like; rows get the same guard
on x. The Uno lands as two headers, top and bottom, with no side gutters at
all - covered by a test using its real 31-pin table.
Three things from using it:
- The pin map is why the dialog gets opened, so hiding it behind a checkbox
meant every visit started with a click. Pins are always drawn now and the
toggle is gone (with its translation key).
- The dialog sat at z-index 1300 and the pro agent chat panel docks over the
canvas at 8000, so on a wide layout the dialog opened UNDERNEATH the chat -
invisible, on the very area the chat covers. It now sits at 8500: above the
chat, still below the component picker.
- On a 32-pin board a name in the gutter could not be matched to a pad by
eye. Hovering (or focusing) either the label or the dot now lights the
whole run - label, leader line and pad together.
Right-clicking a board opened a small bespoke menu (Board Options... / Flash
to real board / Remove board) with its styling inline, while components got
the inspector. Boards now open the SAME dialog: the live board art with every
pin labelled at its real position, and an actions column that carries Board
Options and Flash instead of Rotate (boards do not rotate), with the delete
button reading "Remove board" and the wire count moved into the confirmation.
The dialog grew four optional props to make that possible without a second
implementation: extraActions, deleteLabel/deleteHint, and
previewTagName/previewAttributes - boards select their art through a
board-kind ATTRIBUTE rather than a property, so the preview needs to set it.
Also adds the boardOptions and flashToBoard strings to the nine locales: the
old menu hardcoded them in English, so those two items never translated.
205 lines of inline-styled menu are gone.
The ReSpeaker's left side carries 13 labels: at the minimum spacing that is
208px against a 190px body, so the column ran past both ends and the first
and last labels were cut off by the preview's clip. The gutters now grow to
whatever the labels actually occupy - the box follows the content.
Three things the dialog got wrong in use:
1. Missing pins. Classification was nearest-edge with an "interior" bucket,
and interior pins printed no label - only a dot with a tooltip. The
ReSpeaker Lite showed it: its XIAO socket is two INSET columns (x=50.5 and
126.5 on a 178-wide body) plus two breakout columns, so most of its pins
were called interior and the part looked like it was missing them. The
classifier now finds the LINES first - pins sharing an x are a column,
pins sharing a y a row - assigns each line to the side of the body it sits
on however far inset it is, and joins label to dot with a leader. Only a
lone pad falls back to nearest-edge. Every pin gets a printed label; that
is now the contract, covered by a ReSpeaker test with its real table.
2. The panel was cut off on the Datasheet tab until some unrelated event
re-rendered it. The datasheet loads asynchronously, so the one-shot
measure placed the dialog while it was still short and it then grew off
screen. A ResizeObserver repositions on any size change, and the tab body
caps its own height so long markdown scrolls inside the tab.
3. Flicker when the pointer left and re-entered. The preview effect depended
on componentProperties and pinInfo by identity - both are rebuilt on every
parent render (pinInfo is a getter) - so the element was torn down and
remounted constantly. It now depends on stable digests of them.
An SSD1306 has 8 pins along a 240px header. Horizontal labels need ~36px
each = 288px, so they overflowed the art box and rendered outside the dialog,
over the header and the canvas. Top/bottom labels are now vertical text - what
printed pinout diagrams do - which needs only the side spacing, and the gutter
grows to fit. The preview also clips, so nothing can escape again.
The right-click dialog was a 250px column: every pin as a text row (an ESP32
means 30 rows of scrolling), a few editors, Rotate/Delete. The picker's info
card (datasheet, brand, Buy) was a separate surface you could only reach by
hovering the catalogue. One dialog now serves both:
- ~560px, two panes: live preview + actions on the left, Properties/Datasheet
tabs on the right. The Datasheet tab IS the picker card's body -
ComponentInfoPanel was split into an embeddable ComponentInfoBody (+
useComponentDoc, and its .cip-* styles moved to their own stylesheet) so
both surfaces render one implementation.
- "Show pins" (off by default): the preview is the REAL web component,
mounted and scaled, with pin markers at their true pinInfo positions and
labels flanking the side each pin lives on. pinInspectorLayout is pure
geometry - nearest-edge classification, per-side label stacking with a
minimum spacing and leader lines when nudged, interior pins as
dot+tooltip - driven only by pinInfo plus the measured element size, so
every part and board, present or future, works with no per-part data.
Dots are tinted by the pin's declared signal (i2c/spi/power/...).
- Clicking a pin keeps the old contract (onPinSelect -> handlePinClick), so
wires start with the same undo, colors and running-state gating.
- The two open gestures finally agree on coordinates: right-click always
passed viewport coords while touch passed canvas-local ones; the dialog is
now portaled to body in viewport space and the touch site converts.
- Keybind capture, the SD Card panel, the two-step delete, Escape and
click-outside all migrate unchanged. Mobile keeps the full-sheet layout.
Geometry covered by pin-inspector-layout.test.ts with coordinate tables
copied verbatim from the shipped parts (ESP32 two-column board, LED, an
interior-pin case).
The "SD Card" upload panel in the property dialog was keyed to one component
id, microsd-card, so a part that CARRIES a slot of its own (the Seeed Round
Display shield) had a working card electrically but no way to put files on
it - the user saw the slot mount and could never feed it.
New metadata flag sdSlot: the dialog shows the same panel for any part that
declares it, persisting uploads as properties.sdFiles exactly like the
standalone card. The part's simulation is responsible for reading them when
it builds its card image.
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.
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.
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.
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.
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.
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.
Lets metrics overlays distinguish agent-triggered compiles from manual
ones (the AI assistant's compiles were previously invisible in project
counters). Loose optional field — omitted means manual.
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.
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).
The GPIO->channel map only covered the classic ESP32 (GPIO 36-39/32-35),
so on esp32-s3 / xiao-esp32-s3 / arduino-nano-esp32 every SPICE-driven
analog value was dropped and analogRead saw 0 (when it didn't hang —
fixed machine-side in libqemu 1.2.5's SENS stub). S3 family: ADC1 =
GPIO 1-10 -> CH0-9, ADC2 = GPIO 11-20 -> channel index 10-19, matching
the machine's channel layout.
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.
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.
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.
The overlay's auto-save implementation arrives via a dynamic import that
races the first React commit. A hook whose mount effect ran before the
overlay chunk evaluated saw installedImpl === null and stayed idle for
the whole life of the tab: no debounced saves, no beforeunload flush,
with no visible symptom. Any tab that hard-loaded straight into the
editor and never remounted it silently lost every edit made after the
project was bound.
installAutoSaveImpl now wakes hooks that mounted before it ran, so the
implementation starts as soon as it exists. Swapping a live impl at
runtime remains unsupported.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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)
- 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)
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.
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.
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.
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.
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.
- 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)
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'.