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.
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.
Installing ArduinoJson made the project stop compiling, with an error that
never mentioned ArduinoJson:
CMake Error at user_libs/user_libs_all/CMakeLists.txt:15
(target_compile_definitions):
target_compile_definitions may only set INTERFACE properties on
INTERFACE targets
-- Configuring incomplete, errors occurred!
ArduinoJson ships headers only. With no sources, idf_component_register()
builds the merged user-libs component as an INTERFACE library, and PRIVATE
scope on an INTERFACE target is a hard CMake error - so the configure step
died before a single file was compiled. The user saw only a failing build
and reported it as "ArduinoJson can't be found even after it has been
installed" (#124).
Those definitions map alloca/memcpy_P/memcmp_P for library C sources that
use them undeclared; with no sources there is nothing to define, so they are
emitted only when the component actually compiles something. A library that
carries its own .cpp is unchanged - which is why LiquidCrystal_I2C (#257)
worked and hid this.
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.