This document defines the development plan for DASH. It establishes the versioning convention, the development eras, the feature sequence, and the milestone definitions. It is the authoritative reference for what gets built, in what order, and why.
This document is read alongside changelog.md, which records what actually happened during development — regressions, fixes, refinements, and lessons learned. Roadmap.md is the plan. Changelog.md is the reality.
Every DASH version is identified by three numbers in the format X.x.x.
The first number defines which deployment era DASH is in. Each era represents a fundamentally different target platform and capability level.
- 1.x.x — Tablet era. Development and refinement toward a working tablet installation.
- 2.x.x — Board era. SBC hardware with expanded features and refined experience.
- 3.x.x — Platform era. Advanced features, community tools, and capability expansion.
The first number increments when DASH moves from one era to the next — from tablet to board, from board to platform.
The second number increments each time a new major feature implementation begins. Each increment represents a meaningful new capability being added to DASH. Features are implemented in a defined sequence — see the era sections below for the ordered feature list.
The third number always follows this convention regardless of era or feature:
- .1 — Initial implementation. The feature exists for the first time. Core functionality is present and can be tested. May be rough or incomplete in edge cases.
- .2 onwards — Refinement. Each subsequent increment is a direct response to testing feedback — regressions fixed, behaviour improved, edge cases resolved. The number of refinement increments is not predefined. A feature advances when it is stable, not on a schedule.
The third number always resets to .1 when the second number increments. A new feature always starts at .1 regardless of how many refinements the previous feature required.
A version number tells a clear story at a glance.
- 1.3.1 — Tablet era, third major feature, initial implementation. Something new just arrived.
- 1.3.4 — Tablet era, third major feature, fourth refinement. Something has been worked on extensively.
- 2.1.1 — Board era just started, first major feature, initial implementation. A new era begins.
Every version increment — including third number refinements — must have a corresponding changelog.md entry before the version is considered complete. The changelog records what was implemented, what broke, what was fixed, and what remains outstanding. A version number without a changelog entry is incomplete.
Goal: A working DASH installation on a tablet that can be mounted in the X-Type and driven daily. Proves the core concept in real world conditions. Feature set is deliberately constrained — only what is needed for a meaningful and usable tablet head unit experience.
Target hardware: Any Android 7+ tablet with USB OTG host. Bronze tier.
Milestone definition: Version 1.x.x is complete when DASH can be mounted in a car on a tablet, display a running app in the viewport, show a functioning system bar with alerts and settings, connect and display at least one USB serial module in the module panel, and allow the user to launch apps from the launcher. It does not need to be beautiful. It needs to work.
What it is: The foundation of the entire DASH interface. Establishing the two-tier density and scale system before any UI is built on top of it.
What gets built:
- App density preset system — Compact, Normal, Comfortable, Large — applied to Android system density
- DASH UI scale preset system — Minimal, Compact, Balanced, Spacious — applied independently to DASH chrome
- DASH density override for its own windows, ensuring the two systems remain cleanly separated
- Basic Appearance settings page with density and scale selectors
- Verification that third party apps respond to density changes correctly
- Verification that DASH chrome responds to scale changes independently
Why first: Every subsequent feature is sized, positioned, and rendered using the density and scale system. Building anything before this is established means rebuilding it when this is added.
What it is: Registering DASH as the Android default home application and establishing correct boot and wake behaviour.
What gets built:
- Launcher manifest declaration — HOME and DEFAULT_HOME intent filters
- Default launcher prompt handling — guiding the user to set DASH as default
- Boot behaviour — DASH is the first thing seen on cold boot
- Screen wake behaviour — DASH foregrounds on ignition on system message
- Screen sleep behaviour — screen sleeps on ignition off system message
- Splash screen — displays on cold boot and screen wake, user-definable image or colour
- Android navigation bar suppression on supported devices
- Transient navigation bar fallback for back access
Why second: Every other feature assumes DASH is the launcher. Navigation, viewport control, persistent services — all depend on DASH owning the home position.
What it is: The only persistent interface element. The anchor of the entire DASH interface.
What gets built:
- System bar container — top or bottom position, user-defined height
- Single default zone spanning full bar width
- Mandatory alerts area element — placeholder initially, functional in 1.4.x when transport is live (superseded 2026-07-08 — the alerts area going functional, together with TRIGGER routing, was moved to the new 1.9.x Elements version as a deliberately low-priority nice-to-have. The placeholder remains from this version; it goes live in 1.9.x, not 1.4.x.)
- Mandatory settings button element — 48dp hard touch target floor enforced
- Basic element rendering within zones — left, centre, right snap positions
- Element vertical positioning within bar
- Bar height as master measurement — element sizing as a user-controlled dp value (Complete — 1.3.4)
- Element size control: 4dp steps, 24dp minimum ("min" label, − greyed), ceiling one step below bar height ("max" label, + greyed), auto-clamped when bar shrinks (Complete — 1.3.4)
- Zone splitting (up to three), zone width distribution presets, element packing layout, Spacer element type (Complete — 1.3.5)
- Edit mode scaffolding and zone divider dragging (Complete — 1.3.6) — edit mode entry from settings, bar visual state change, draggable zone dividers with detent snap at 1/4 / 1/3 / 1/2 / 2/3 / 3/4 of bar width. Edit mode originally planned as a single version; split into 1.3.6 (infrastructure + dividers) and 1.3.7 (element drag-and-drop) because drag-and-drop in Compose for a production-quality experience is a substantial undertaking on its own. Splitting keeps each version completable cleanly and ensures divider dragging can be tested before element repositioning is layered on top
- Edit mode — ruler and interaction model (Complete — 1.3.7) — the original plan for direct long-press-drag on the bar has been superseded by a design review. The new model: when edit mode is entered, a control strip appears a finger-width's gap from the bar on its inner side (below if top-docked, above if bottom-docked). The bar itself is never touched directly during editing — all interaction happens in the ruler. Zone dividers are represented in the ruler as arrow markers pointing back to their bar position. Elements are represented as footprint-sized boxes visible only in edit mode — no permanent boundary box exists in normal operation. The whole-bar border/tint treatment from 1.3.6 is removed — the ruler's presence alone is the unambiguous signal that edit mode is active; a second signal repeating the same information is redundant. Divider drag is free (no snapping) — snap detents from 1.3.6 were rebuilt into the ruler but removed after on-device testing; the snap threshold masked the gesture as non-response. Snap may return in a future version if there is genuine reason for it
- Edit mode — Save, Cancel, and edit workspace (Complete — 1.3.8) — DONE button replaced with separate SAVE and CANCEL actions centred in the main content area. SAVE commits the in-memory edited state to DataStore. CANCEL discards all changes and restores the bar to its last saved state with no write —
barConfigfrom DataStore is the implicit snapshot; no additional snapshot variable required - Edit mode — zone configuration relocation (Complete — 1.3.9) — zone count (1/2/3) moved from Settings into the edit workspace. Zone count is now part of the edit session — committed on SAVE, discarded on CANCEL. Distribution preset buttons removed entirely: they predate divider dragging and are redundant now that dragging achieves the same result directly. Settings System Bar section now contains only position toggle, bar height stepper, element height stepper, EDIT BAR LAYOUT, and RESET
- Edit mode — binding visual feedback (Complete — 1.3.10) — element boxes show a red 3dp tint on each bound edge (touching a zone boundary or adjacent element). Snap detents reintroduced at 1/4, 1/3, 1/2, 2/3, 3/4 with a 4dp threshold. Escape mechanic: picking up a divider already at a snap point enters free-move immediately; snap re-engages after 4dp of movement. Detent markers (tick marks) appear on the ruler during drag. Divider arrow turns red when settled at a snap point. Live edge highlight during element box drag previews the intended anchor in real time. Element box drop onto another element's footprint swaps their positions; drop into open space uses the existing thirds model
- Edit mode — drag gesture robustness, pick-up affordance, and ruler visual design (Complete — 1.3.11) — gesture events consumed unconditionally during drag rather than only on positionChanged, preventing cancellation when the touch point moves outside the original marker bounds. Divider arrow brightens to full opacity on press as immediate pick-up confirmation; element box stroke thickens from 1dp to 2dp and goes to full opacity on press — both before any drag movement. Ruler visual redesign: ruler is now transparent with a single 1dp centre track line in barAccent2; element boxes are stroke-only outlines (no fill); bound edge tints remain as coloured overlays on the outline. New barAccent2 theme token added for all ruler structural colours — sits between the invisible-dark barAccent and the content-bright barText, independently themeable in version 2
- Edit mode — element drag bounds crash (Complete — 1.3.12) — dragging an element box too far left (off the left screen edge) crashes the app. Root cause:
ElementBoxusesModifier.padding(start = xPx.toDp())andelementDragOffsetPxis unconstrained, so a sufficiently large leftward drag produces a negativexPx, which Compose rejects as invalid padding. Fix: clampcurrentXPxinEditRulerso the element box cannot be dragged to a position where its left edge would be off the ruler's left boundary. Apply the same clamp to the right edge for symmetry — an element box should not be draggable beyond the right edge of the ruler either - Edit mode — height controls relocation (Complete — 1.3.13) — move the bar height stepper and the element height stepper out of the main Settings panel and into the edit-mode workspace, alongside the zone count control already relocated there in 1.3.9. Both remain single global values exactly as before — bar height applies to the one bar, element height applies uniformly to every element on the bar. No per-element variation. While inside edit mode, adjusting either stepper resizes the bar or its elements in real time, so the user sees the result immediately rather than leaving settings to check
- Element height and width contract (→ 1.3.14) — finalises the SDK sizing contract, superseding the earlier draft. Height is decided entirely by DASH from the global element height setting and is never exposed to an element's implementation in any form — no height parameter, no height property, nothing in the API surface an element author sees or touches. Width is not negotiated by the element either. Each element has its own natural aspect ratio, defined purely by how the author designed its appearance. Given a height, DASH computes the corresponding width automatically by scaling the whole design proportionally — the way enlarging or shrinking a photograph preserves its shape rather than stretching it unevenly. By default, elements are rigid: at any given height, an element has exactly one natural size. It either fits in the remaining zone space or it doesn't. If it doesn't fit, DASH refuses to place it and shows the zone-overflow soft warning already specified — it never compresses, distorts, clips, or overlaps elements to force a fit. Making room is the user's responsibility, achieved by resizing the bar, rearranging zones, or removing something else. Documented future direction, not part of this version: an author may later choose to make their element compressible, defining multiple presentations for different amounts of available space — deferred until a real element complex enough to warrant it is built, which aligns with the additional elements already planned for version 2. Refactor both existing elements (Settings Button and Alerts Area) to conform fully — neither should reference height anywhere in its implementation, and each should render at a width derived purely from its natural aspect ratio and the given height. Confirm the Zone Layout computes total required width from natural element sizes and correctly refuses placement with the existing soft warning when there isn't enough room
- Spacer architecture correction (→ 1.3.15) — corrects the design decision made in 1.3.5 to handle the Spacer outside the ElementRegistry, on the reasoning that it was structural rather than a content element. That reasoning is superseded. The Spacer is not structural — it is a rigid element with no visible content, going through the exact same DashElement contract as Settings Button or Alerts Area, with its own fixed natural size and no special handling anywhere in the layout system. Flexible gap sizing is achieved by placing multiple spacer instances next to each other, not by giving the spacer its own resizable width control. Remove the special-case Spacer handling added in 1.3.5 from the Zone Layout. Building the actual Spacer element is deferred to version 2 alongside the other planned additional elements (Clock, Volume, Now Playing) — it does not need to exist for the remainder of 1.3.x. Leave or remove the partial ElementType.SPACER scaffolding from 1.3.5 as judged appropriate given its current state. Reaffirm: only Settings Button and Alerts Area are native, mandatory DASH elements. Every other element, including the Spacer once built, is SDK-pathway from the start — built using the same contract any third-party developer would use, even when DASH ships it by default
- Theme token system (Complete — 1.3.3, extended 1.3.11) — named colour tokens (barBackground, barAccent, barAccent2, barText) exposed via a
CompositionLocalrather than a plain singleton or object. Components read their theme from the current composition context; version 2 introduces full theming and presets by providing different token values at the top of the composition tree, with no rework to howSystemBaror anyDashElementreads its colours. This matches interface.md's requirement that elements consume theme tokens rather than hardcode values. One default token set. No user-facing theme switching — that is version 2 scope. Foundational requirement so the settings panel in 1.5.x can inherit its visual identity from the active system bar theme. barAccent2 added in 1.3.11 for ruler structural colours — a mid-tone between the invisible-dark barAccent and the content-bright barText
Why third: The system bar must exist before settings can live anywhere, before modules have a persistent UI reference, and before the viewport has a boundary to conform to.
What it is: The communication backbone that allows modules to connect, identify themselves, and send data to DASH.
Wire format note: This layer is built against the ratified module grammar in arduino/arduino.md (pipe-separated TYPE|id|…), which supersedes the older colon grammar (DASH:DISCOVER, SYSTEM:function:value, …) still shown in the body of transport.md. See the banner note at the top of transport.md (added 2026-07-01) for the full old→new mapping. The message names below have been updated to the current grammar.
Build order — one piece per version (1.4.1 → 1.4.16; 1.4.12–1.4.13 added 2026-07-07; on 2026-07-12 Bluetooth was inserted at 1.4.12 — bumping the mismatch and failure versions to 1.4.13/1.4.14 — and the module library and cleanup pass were added as 1.4.15/1.4.16):
-
1.4.1 — Transport interface (pluggable abstraction) + USB serial transport (usb-serial-for-android) + Serial Monitor. (Combined: a transport foundation must be visible to be verifiable. The Serial Monitor — a persistent dev instrument reached from settings — is the diagnostic surface every later piece is verified through.) (Complete — 1.4.1, bench-verified on Arduino Uno R4:
DISCOVER→HELLOround-trip confirmed.) -
1.4.2 — Discovery broadcast + response parsing (Complete — 1.4.2, bench-verified on the Arduino Uno R4: press DISCOVER → the module appears in the Module Management list by name). A user-driven DISCOVER button on a new Module Management screen (reached from settings, mirroring the Serial Monitor route) broadcasts
DISCOVERon all active transports; theHELLO|id|type|name|description|versionreplies are parsed and populate the discovered-module list, each press rebuilding it from scratch. (1.4.3 merged in — as 1.4.1 combined the transport and the monitor — because a discovery screen with a button but no populated list can't be verified as working. Two decisions taken here, ahead of the features that use them: (a) discovery is a user-driven method of installation, not reconnection — DASH broadcasts only on the button press, never on a timer; the automatic re-sweep of already-installed modules (arduino.md §6) belongs to reconnection/reconciliation, 1.4.6. (b) The message brain is a configurable crossroads, not a funnel — aDashControllerdispatcher (one inbox, sort by TYPE word, dispatch to a desk, one outbox) with only the discovery desk staffed today; the transport layer sends to and receives from all active transports. The single-device status prompt was removed from Module Management — that screen is about the whole bus of modules, not one device.) -
1.4.3 — (merged into 1.4.2 above — the outbound broadcast and the HELLO parsing were built together.)
-
1.4.4 — Installation handshake —
INSTALL|id→ type-specific declarations →INSTALL_END|id(Complete — 1.4.4, bench-verified on the Arduino Uno R4: DISCOVER → INSTALL → the pane runs a progress bar, turns green, and DETAILS shows the captured declarations.) All three declaration parsers built together — SYSTEM (SYSTEM_SIGNAL), LISTENER (SUBSCRIBE, all seven fields captured), ACCESSORY (MANIFEST+ length-prefixed CRC32-checked assetBLOCKs) — with Roger writing LISTENER and ACCESSORY reference modules to exercise each path. The install desk (Install) is the controller's first stateful, bidirectional desk: opened by DASH sendingINSTALL, fed by the declaration run, committed onINSTALL_ENDinto a session-onlyInstalledModule(disk is 1.4.5). Block framing is owned by the transport (a newFrameAssemblerswitches to a byte-count read synchronously on the IO thread); CRC validation and record assembly by the desk. (Deferred, with homes: install timeout + a designed fail-state visual to later 1.4.x failure work — only the unavoidable bad-CRC abort is handled now; ACCESSORY variables/controls declaration framing to be designed before the panel, 1.6.x, per arduino.md §10; ACTIVATE/live data/state store to 1.4.6+, so an installed module is dormant. arduino.md gained §4b state reporting and §5a controller architecture as groundwork for those, plussystem_commands.mdas the authoritative signal vocabulary.) -
1.4.5 — Module database — save installed modules to disk (Complete — 1.4.5, verified: install a module, relaunch DASH, the green card is back from disk with DETAILS reading the saved record.) New
ModuleDatabase— one folder per module underfilesDir/modules/, amodule.jsonrecord (the serialisedInstalledModule) beside anassets/folder holding each ACCESSORY block's raw bytes as plain files. The record is written last, so a folder interrupted mid-write has no record and is swept on the next load — never half-loaded. The split settled here: the database holds installed; the install desk holds only installing — a completed handshake commits into the database (payload bytes held in the session only for that hop, then written to disk, never kept in memory after). Module Management now merges two sources into one card per physical module: installed cards appear the moment the screen opens (no DISCOVER needed, plugged in or not), discovery adds INSTALL cards for the rest; DISCOVER can never remove an installed card. Uninstall is one recursive delete — no wire message, because the module persists nothing (arduino.md §6): DASH forgetting is the uninstall. (A failed disk write logs and degrades to 1.4.4's session-only behaviour rather than erroring. Asset bytes on disk are read by nothing yet — the panel, 1.6.x, is their consumer.) -
1.4.6 — Startup reconciliation — boot broadcast, Active/Dormant management,
ACTIVATE/DEACTIVATE+ROGERacknowledgement (Complete — 1.4.6, bench-verified on the Arduino Uno R4: launch DASH with the module plugged in → the green card turns ACTIVE within a sweep and the Serial Monitor shows DISCOVER → HELLO → ACTIVATE → ROGER; uninstall while active → DEACTIVATE → ROGER and the module's heartbeat stops.) The reconciliation desk (Reconciliation) is the controller's first desk with a clock: a persistentDISCOVERsweep — every 5 s for the first minute (the Uno Q ~20–30 s boot case), then every 30 s forever (crank-reset and hot-plug recovery) — matchesHELLOs against the module database andACTIVATEs the matches with ROGER-verified retries (3 tries, 2 s apart), pausing while an install handshake is in flight and sweeping immediately when a transport comes up. Installed cards wear an ACTIVE/DORMANT chip (absent after ~75 s unheard); discovered-but-not-installed cards now appear and age out on their own. Uninstall gained its wire half: an active module is sentDEACTIVATE(acked, retried), the record deleted immediately per arduino.md §6, with the disconnect-or-power-cycle warning raised if never confirmed. (Two decisions taken with Roger, 2026-07-06: (a) one DISCOVER, no separate reconnect message — a broadcast "reconnect" is impossible when modules don't know their own install state (§6), so the install/reconnect distinction lives in DASH's handling of the replies, not on the wire; a per-id ping and an optional HELLO state field were considered and parked (arduino.md Considered & parked). (b) The DISCOVER button became SYNC — with the sweep persistent, a dedicated install-discovery button is redundant; SYNC is §6's manual "check now", serving install and reconnection with one press. This amends 1.4.2's decision (a): discovery remains a method of installation, but "broadcasts only on the button press, never on a timer" was the rule only until reconciliation existed — the sweep 1.4.2 explicitly reserved for 1.4.6 now runs it. (c)ACTIVATEis re-asserted every sweep rather than only when DASH thinks a module is silent — a rebooted module's HELLO is indistinguishable from a healthy one's, so a fresh ROGER is the only proof of life; measured at ~1% of a 115200 wire for ten modules even with future §4b dumps, Roger accepted the chatter over changing the HELLO contract.) -
1.4.7 — System message routing —
BROADCAST|id|function|valueparsed and dispatched into the sourceless core (Complete — 1.4.7, verified on the tablet with the built-in simulator: SIMULATOR ON → install the two virtual modules → the store ticks, events fire, the pokes broadcast.) The controller's fourth desk (Broadcasts) is the §5 gatekeeper: sender must be installed and ACTIVE, then the id is consumed and the signal routed into the new sourceless core (SystemState: state store + event bus) by its §5a behaviour, looked up in the in-code copy ofsystem_commands.md(SystemCommands— the markdown stays authoritative). The DASH side of §4b came free by construction: the desk compares before it acts, so activation dumps and 5 s heartbeats are ordinary traffic — same value, nothing stirs; the store self-heals. Verification surface (agreed 2026-07-07): a State Inspector dev instrument (settings, beside the Serial Monitor) — store pane with value+age, timestamped event log — plus a simulated transport with two virtual modules kept to full firmware discipline (SILENT until ACTIVATE, honest path through the gatekeeper): a Sim Vehicle SYSTEM module (continuous speed/rpm stream, random door flips, §4b dump + heartbeat, poke buttons standing in for the pretend car's physical inputs) and a Sim Accessory (block-transfer install, REPORTs that correctly go unrouted until 1.4.9). (Decisions: the machinery is permanent, the inspector screen deliberately quick-and-dirty; unknown signals drop-and-log — the custom fallthrough and patch-bay stay future crossroads stages; live broadcast traffic feeds the reconciliation liveness clock so a streaming module can't age DORMANT mid-stream; the sim's two-modules-one-pipe is a deliberate shared-bus preview, contention design still parked at 1.4.10.) -
1.4.8 — LISTENER streams —
SUBSCRIBEat install,LISTENdelivery, with rate/threshold/gate evaluated in DASH (arduino.md §9) (Complete — 1.4.8, verified on the tablet with the built-in simulator: install Sim Vehicle + the new Sim Relay LISTENER → the vehicle's signals come straight back out asLISTENon the Serial Monitor, throttled at 1 Hz against the 2 Hz source, deadbanded, delivered on-change, on the 5 s heartbeat, dumped on activation, and valueless formedia_next.) The controller's fifth desk (Streams) — the outbound mirror of 1.4.7's broadcast desk. It watches the sourceless core rather than the wire (the crossroads' destination stays dumb; the desk holds the cleverness) and delivers subscribed signals back out, evaluating all four §9 controls in DASH: a leading-and-trailing rate throttle (the resting value of a stream converges within one window instead of waiting on the heartbeat), a numeric deadband, and a gate on another stored signal. (Two decisions taken with Roger, 2026-07-07→08: (a) defaults are the firmware library's job, not DASH's — the earlier plan for DASH to hold per-signal default rate/threshold was dropped; a module declares exactly what it wants and DASH honours the delivered line literally, holding no defaults and recording nothing extra. This keeps DASH out of the way (ethos) and the built-in/community playing field level (SDKable): the default-provider moved from DASH to the shared library the sketch author still doesn't touch. A blank optional field is legal and total (no cap / any change / always); a present-but-malformed field is logged and ignored, never guessed at — DASH assumes modules are written perfectly and drops what isn't, protecting its own integrity without accommodating the error. arduino.md §4c/§9 and system_commands.md reworded to match. (b) The vocabulary-as-editable-data idea was raised and parked indefinitely — the built-in signal set stays curated in code (type-safe, no runtime parse or ship-a-typo surface); user-added custom signals are the future patch-bay/custom-fallthrough stage's job, where a user-editable file is the right tool.) The gate path is built but was the one path left unexercised by the sim (its subscriptions are ungated for a clean "all signals" read) — recorded honestly as built-but-not-yet-watched; it reuses the verified store-watch and lookup patterns. -
1.4.9 — Module message routing —
REPORT|id|variable|valueparsed and dispatched (renders in the module panel once that exists in 1.6.x; dispatches to the monitor until then) (Complete — 1.4.9, verified on the tablet with the built-in simulator: install Sim Accessory → itstest_counterREPORTs, unrouted since 1.4.7, now land in the State Inspector's MODULE REPORTS pane; press ACTION ▸ →ACTION|…|sim_buttongoes out andbutton_pressesclimbs back in, the full round trip.) The controller's sixth desk (ModuleReports) is the specific column's inbound half — the private mirror of the 1.4.7 broadcast desk. The one architectural difference and the point of the version:REPORTis sourceful — the id is kept as the store key (temperaturefrom module X is that module's panel data, never merged), where a broadcast's id is consumed. It fillsModuleData, the per-module twin of the sourcelessSystemState. Built alongside it (Roger's call, so the 1.6.x panel front end is ready to just work):ACTION|id|control|valueout via a thinActionsdesk — a gatekept emit that stores nothing and deliberately does not validate the control id (the panel does that for free, only ever rendering real controls). With this the §4 2×2 is fully staffed: general column (BROADCAST/LISTEN) sourceless, specific column (REPORT/ACTION) sourceful. (Scope decision:TRIGGER, the third ACCESSORY-out message, was ruled out and given a home at the new 1.9.x Elements version — it lands in a shared alert store read by the alerts element, so it is element work, not private-panel work.) -
1.4.10 — Multi-device support & Devices view — the transport layer sees and addresses more than one physical device (today the USB transport grabs only the first device it finds); a Devices view of what is physically connected on each transport; and device selection in the Serial Monitor (a dropdown to pick which device to talk to). (Complete — 1.4.10, verified on the tablet with two real boards on a powered hub: an Arduino Uno R4 WiFi (CDC-ACM) and an ESP32 DevKitC (CP2102) held open at once — "2 devices @ 115200 8N1", the first run of the multi-device path.) Per-device
DeviceConnections each with their ownFrameAssembler(the hard requirement); inclusive per-device driver resolution so no ESP32 board is locked out; broadcast send / merged incoming / aggregated status + device list. The Devices view was dropped (Roger's call — Module Management already answers "what modules," a physical-device list is thin) and replaced by a Signal Monitor: a live board of every system message against its current value in the sourceless core — the surface the deleted State Inspector used to give, minus the sim. The Serial Monitor gained a "SEND TO" device dropdown (All devices = broadcast). The USB one-time permission grant was pulled forward from the pre-1.5 cleanup below (device_filter +USB_DEVICE_ATTACHEDintent-filter → "use by default", so a board authorised once stops re-prompting). Two new reference SYSTEM sketches (arduino.ino"Body",esp32.ino"Powertrain") exercise it on real copper, sharingambient_tempto prove the sourceless core takes redundant sources. Deferred: per-device labelling of inbound lines in the wire log (→ 1.5.x settings). (The session's dominant symptom — "second board kills the first" — was diagnosed as a power problem, not code: an unpowered hub browning out the tablet's whole USB bus; a powered hub fixed it, the transport was correct throughout.) The single-device prompt was already removed from Module Management in 1.4.2. Placed just before WiFi because WiFi is what really brings a crowd of devices — build the multi-device model and immediately stress it with WiFi's many clients. (Roger's call: progress the module-lifecycle sequence first; multi-device is not blocking — install addresses a module by id, so it works with one device meanwhile.) (Added 2026-07-07, from the what-happens-to-live-data-during-an-install discussion: (a) one FrameAssembler per device stream is a hard requirement of this version — today's single global assembler is correct only while there is a single device; kept global, a block transfer on device A would put device B's innocent live-data bytes into byte-count mode and corrupt the frame. Per-device assembly is what guarantees install data and live data never share a stream on any point-to-point transport — USB per-device, TCP per-socket. (b) Shared-bus contention is the one place that guarantee can't hold and is parked here with the rest of the shared-bus design: on RS485 multi-drop a block transfer must own the bus — DASH quiesces the other modules for the duration (temporary DEACTIVATE or a lighter hush, design TBD) — and the simultaneous-HELLO reply-jitter question parked in the 1.4.6 changelog lands in the same conversation.) -
1.4.11 — WiFi TCP transport — DASH TCP server, module client connections (proves the 1.4.1 abstraction is genuinely transport-agnostic) (Complete — 1.4.11, verified on real hardware: the Arduino Uno R4 WiFi running the new
arduino_wifi.ino"Body WiFi" reference module connected to DASH's TCP server over WiFi and came up as a module over the air — the first time a module has reached DASH over anything but a cable.) The secondDashTransportbehind the 1.4.1 contract, with the inversion that DASH is now the server and modules are the clients that connect in (vs USB, where DASH is the host that opens devices). A TCP server on port 3274 (D-A-S-H on a phone keypad; unprivileged, so it keeps the no-root constraint), oneFrameAssemblerper accepted socket (the 1.4.10 per-stream rule carried from cables to sockets), and — the whole point of the version — the only change above the transport layer was one line registering it inTransportManager; controller, core, install, database and reconciliation untouched. Also folded in: theINTERNETpermission; the tablet's LAN IP surfaced in the WiFi transport status; and the Serial Monitor switched to one status line per transport after the old merged line was found to mask the WiFi "Listening on " behind USB's. The in-car networking model was worked out and recorded (tablet-as-hotspot providing the module network while the SIM carries internet — one radio does both, and the tablet-as-AP gives a fixedDASH_HOST); the MODULE SETUP provisioning button was designed and deliberately parked to Version 2 (reading the device's own hotspot SSID/password needs system-app privilege Android denies a Bronze sideload — a natural capability-detection feature for the production hardware). -
1.4.12 — Bluetooth Classic (SPP) transport — a third
DashTransport, the wireless sibling of WiFi, further proving the 1.4.1 abstraction holds. (Complete — 1.4.12, verified on real hardware: an ESP32 DevKitC running the newesp32_bt.ino"Powertrain BT" module, paired once in Android settings, reached DASH over Bluetooth SPP and ran the fullDISCOVER → HELLO → INSTALL → ACTIVATE → ROGERhandshake and BROADCAST stream — giving the genuine three-transport bench, USB + WiFi + BT live at once. The one bench snag was an id collision in the reference sketch (it reused the WiFi Body's …EE07), not the transport, which logcat proved correct throughout; fixed by moving the BT sketch to …EE08.BLUETOOTH_SCANwas found unnecessary and not requested — bonding happens in Android settings, DASH never scans.) (Added 2026-07-12 — Roger can run SPP on the ESP32 while the Arduino stays on WiFi, giving a genuine three-transport bench: USB, WiFi and BT at once — the strongest proof yet that nothing above the transport layer cares which pipe carried a message.) Classic SPP, not BLE: a byte-stream that drops straight onto the line grammar and the per-deviceFrameAssemblermodel (BluetoothSerialon the ESP32 side). The transport shape is the familiar one — per-device RFCOMM socket, its own assembler, fan-out send / merged incoming — but BT brings three things WiFi didn't. Pairing: an SPP device must be bonded first, so DASH works with already-paired devices via standard Android Bluetooth settings (transport.md), no programmatic pairing. Runtime permissions:BLUETOOTH_CONNECT/BLUETOOTH_SCANon API 31+ are dangerous permissions needing a user grant — a capability-detection / graceful-degradation path (request; if denied, the transport reports unavailable and DASH carries on). Client model: DASH connects out to bonded devices as the RFCOMM client — more like USB's DASH-initiates than WiFi's module-connects-in. BLE is deliberately not this version — its GATT / characteristic / MTU-chunk model doesn't fit the line grammar and is its own later, more complex transport. (Placed right after WiFi because both are wireless hot-plug transports and doing them back-to-back keeps the machinery hot; it also gives 1.4.14's wired-vs-wireless fault visual a third transport type to test against.) -
1.4.13 — Firmware version mismatch — detect, reject, one-tap update. (Complete — 1.4.13, verified on real hardware: the
arduino_wifi.ino"Body WiFi" module reflashed v1.0 → v1.1 without uninstalling; on reconnect DASH recognised it as the installed module but held it DORMANT with an amber UPDATE chip, its live data refused at the Signal Monitor — then one tap of UPDATE re-ran the install and the module came back ACTIVE and streaming.) (Added 2026-07-07, prompted by the Test Accessory v2 reflash: DASH's stored record was captured from the old firmware and DASH silently didn't notice; renumbered 1.4.12 → 1.4.13 on 2026-07-12 when Bluetooth took 1.4.12.) The reconciliation desk already compares everyHELLOagainst the module database by id; this adds the version field to that comparison. A mismatch means the install-time contract DASH holds —SYSTEM_SIGNALdeclarations,SUBSCRIBErequests, MANIFEST assets — can no longer be trusted, so the module is quarantined, not merely flagged (Roger's call during the bench, 2026-07-12, overriding the original "surface but keep working" plan — the version is only worth tracking if a difference actually withholds trust): DASH will notACTIVATEa mismatched module, so it is heldDORMANTand all four §5 gatekeepers (broadcasts, reports, streams, actions) refuse its traffic in both directions — and because a compliant module boots SILENT and only speaks afterACTIVATE, it never transmits at all. The whole rejection is enforced in one place (the desk withholds activation); no gatekeeper needed changing. Surfaced with an amber UPDATE chip on the installed card and a DETAILS banner showing both stored and reporting versions, cleared by a one-tap update that re-runs the install handshake (uninstall + install back to back — a firmware update is a reinstall, because the module persists nothing, §6) and lifts the quarantine. Detection is difference only — the version field is free text with no ordering contract, and "different" is exactly the condition under which the stored record is untrustworthy; the flag self-heals when the module reports the new version that now matches the freshly stored record. The builder-facing rule (bumpversionwhenever the declared interface changes) is recorded in arduino.md §6. Deliberately excluded: auto-refresh. DASH re-running the handshake unprompted is deferred until the install failure work exists (timeout + designed fail state, deferred since 1.4.4 — now 1.4.14) — auto-triggering installs at boot with nobody watching invites the wedged-install-pauses-the-sweep problem. Revisit after 1.4.14 lands. -
1.4.14 — Designed install failure — timeout, fail state, recovery. (Complete — 1.4.14, all three failure paths verified on real hardware over USB via the new
test_accessory_big.ino"Big Test Accessory" (a ~100 KB icon-pack install, ~9 s on the wire): CANCEL mid-transfer reverts clean and frees the sweep; DISCONNECTED — a cable-pull fails the card at once; STALLED — aWEDGE_AFTER_BLOCKSfreeze trips the idle timeout.) (Added 2026-07-07 — not a new idea but the home for the failure work parked since 1.4.4, when "install timeout + a designed fail-state visual" was deferred with only the unavoidable bad-CRC abort built; renumbered 1.4.13 → 1.4.14 on 2026-07-12 when Bluetooth took 1.4.12.) An install that stalled mid-handshake used to leave the session open forever. This version closes that: an idle watchdog (10 s of silence, reset by every declaration/block) aborts a stalled session as STALLED; the card gets a designed fail state — a sealedInstallStateofInstalling | Failed(reason)withFailReasonSTALLED | DISCONNECTED | CORRUPT, a persistent badge with RETRY/DISMISS, never a silent reset (the old CRC abort was promoted from silent to a visible CORRUPT); and the 1.4.6 item is fixed —installBusyreads the live sessions map, not the badge projection, so a failed install can no longer pause the sweep. Beyond the roadmap sketch, worked out with Roger on the bench: (a) a CANCEL button — a deliberate stop reverts clean, no badge (a wire-levelINSTALL_ABORTdeferred to the 1.4.15 SDK lock); (b) Roger's physical-disconnect trip — every inbound frame now carries its origin (a newInboundFrametag+device envelope threaded through all three transports), so an install fails DISCONNECTED the instant its device leaves the bus, the idle timeout becoming the backstop for a link-holding wedge. The parked §6 absent-wired = fault vs absent-wireless = quiet visual landed viaDashTransport.wiredand a new orange NOT_RESPONDING state (an installed module seen this session then gone silent; a never-seen module stays calm DORMANT), the wired/wireless split wording the DETAILS reason. Bonus pass on Module Management (Roger's request): SYNC became REFRESH — beyond an immediate sweep it prunes discovered (not-installed) cards that no longer answer, while the sweep's aging turns silent installed modules orange. (Noted for later: the idle watchdog resets per completed block, not per raw byte, so a single block longer than the timeout would false-STALL — mitigated by the many-medium-blocks shape; leaning leave-it. And the failed-update edge — a failed reinstall leaves the module uninstalled with a retryable badge.) Completing this version unlocks the 1.4.13 auto-refresh revisit (still deferred). -
1.4.15 — The module firmware library — reconcile
arduino/arduino.md, lock the SDK, and write the shared Arduino library every module (built-in or community) is built from. (Complete — 1.4.15, SYSTEM + LISTENER library shipped; ACCESSORY deferred to after 1.6.x. Compile-verified on the R4 WiFi and a bare Uno R3; the refactored examples reproduce the hand-written reference sketches' wire output. Recorded unverified on hardware at Roger's instruction — he trusts the build and will surface any issue when he next flashes it, 2026-07-13.) (Added 2026-07-12 — Roger's call: with every transport and the failure work in, the SDK the reference sketches have been proving by hand is ready to become a real library.) Three acts, in order. (1) Reconcile arduino.md against everything actually built in 1.4.1–1.4.14 — walk each section against the changelog decisions, catch cheat-sheet-vs-body lag (e.g. theSUBSCRIBE-defaults cheat-sheet line still predates the 2026-07-08 defaults-in-the-library amendment in §4c), and add the module-facing wireless-connection facts a builder now needs (connect to port 3274 / bond over SPP, go SILENT on drop) — all as dated additive notes, never erasing the prior text. (2) Lock the SDK — with the spec current and true to the code, the rules are settled, which is the CLAUDE.md moment to consider promoting them out of the working record intotransport.mdor a dedicated module-rules document (only on Roger's express instruction). (3) Write the library — extract the discipline the reference sketches (arduino.ino,esp32.ino,arduino_wifi.ino, the LISTENER/ACCESSORY references) have carried by hand — framing, the HELLO / install handshake, the SILENT-until-ACTIVATElifecycle, the §4b dump + heartbeat,SUBSCRIBE-default filling, change detection — into one library so a builder writes only their own sensors and controls. The concrete proof of the SDKable principle: the library a community builder uses is the same one the built-in modules use. The ACCESSORY module type was deliberately split out (Roger's call, 2026-07-13): itsDashAccessorylibrary helper is deferred until after 1.6.x, because the ACCESSORY panel/layout spec (arduino.md §11) is the one part of the SDK not yet locked — it locks once 1.6.x actually renders a panel, and the helper falls out cleanly then. So 1.4.15 ships the two non-visual module types (SYSTEM, LISTENER); the visual one waits for the visual era. The library lives atarduino/DashModule/(a proper Arduino library —DashModulecore +DashSystem+DashListener, withdash_signals.hmirroringsystem_commands.mdforSUBSCRIBEdefaults), and the refactored reference modules are itsexamples/(BodySystem,LedListener).The ACCESSORY SDK — deferred here, lands after 1.6.x. (Given a number 2026-07-27: 1.6.6. Renumbered 2026-07-30 to 1.6.10, the last build version of 1.6.x — still immediately after the panel work, but by then the layout format has been proven by two real modules rather than rendered once. See 1.6.x.)
DashAccessory(chunked assetBLOCKstreaming from flash,REPORTout /ACTIONin,TRIGGER) plus locking the panel spec. Build it once the module panel (1.6.x) has rendered a real panel and the layout format is settled. (Amended 2026-08-12: the spec to lock ismodule-layout.md, promoted out of arduino.md §11 ahead of 1.6.5 — not §11 intomodule-sdk.mdas written here.) -
1.4.16 — Cleanup pass before 1.5.x — the short refinement pass that folds in the niceties deferred during the build. (Complete — 1.4.16, verified on real hardware 2026-07-16: a cold tablet reboot brings the modules alive with no delay, and the Serial Monitor reads correctly.) The headline: the "USB hot-plug reconnection reliability" item was diagnosed — with on-wire instrumentation — as not a transport bug at all, but a module-side ACTIVATE-idempotency bug. DASH re-asserts
ACTIVATEevery sweep (every 5 s in the 60 s fast phase) as a liveness ping; the module re-ran its whole activation on each one, resetting its own send-timers — so any signal changing slower than a sweep (gear 12 s, headlights 15 s, door 8–20 s) stayed frozen for the first ~60 s after every connect/cold-boot. Fixed in theDashModulelibrary —onActivated()now runs only on the real SILENT→ACTIVE transition; the module stillROGERs every re-assert. PluslinkLost()for wireless drops, and the four reference sketches rewritten onto the fixed library (arduino/current_sketches/; old ones →arduino/old/). The other two items shipped as planned: per-device labelling of inbound wire-log lines (and the direction arrows swapped — inbound →, outbound ←), and greet-on-device-count-change (a new device now wakes an immediateDISCOVERsweep). SDK-doc write-up of the idempotency rule +linkLost()deferred to the doc/README pass that follows. (1.4.x CLOSED — 2026-07-17. The deferred doc pass is done: theACTIVATE-idempotency rule and the wireless link-drop rule are written intomodule-sdk.md§6 andarduino.md§6, andarduino_secrets.h.exampleis committed beside BodyWifi. Nothing carries into 1.5.x.) Known items: USB hot-plug reconnection reliability (flagged 2026-07-12 — needs locking down tight) — a USB device sometimes fails to come back after an unplug/replug (or a module reboot / brown-out), staying absent until DASH is restarted. Intermittent, so it needs reproducing and then hardening the whole reconnection path inUsbSerialTransport: the re-sweep'sconnectAvailable(), the ATTACH/DETACH broadcast handling, thependingPermissionlinger/clear logic, and stale-DeviceConnectionpruning — a replugged board must always return on its own, no restart. Likely suspects: a missed DETACH leaving a dead connection in the map (socontainsKeyskips reopening), a reuseddeviceIdcolliding with a lingering permission flag, or an IO thread not fully torn down before the reopen. Per-device labelling of inbound lines in the Serial Monitor wire log (the SEND-TO dropdown targets sends, but inbound lines still merge under one transport tag — deferred here since 1.4.10); and greet-on-device-count-change (a second module connecting while another is already up currently waits out the reconciliation sweep rather than being greeted at once — noted in 1.4.11; a transport-agnostic fix that helps USB, WiFi and BT alike). The USB one-time permission grant this pass originally owned was already pulled forward into 1.4.10.
Cleanup before 1.5.x — promoted 2026-07-12 to an explicit numbered version, 1.4.16 above (alongside the module library at 1.4.15). Kept here for the record: the USB one-time permission grant this pass first listed was done early (pulled forward into 1.4.10 — a USB_DEVICE_ATTACHED intent-filter + device_filter.xml so a device is authorised once with "use by default"); the still-outstanding per-device wire-log labelling and greet-on-device-count-change items now live in 1.4.16.
Why fourth: The module panel in 1.5.x needs live data to be useful. Building transport first means module panel testing is immediately meaningful rather than working with dummy data.
What it is: The user-facing configuration interface. Minimal at this stage — only settings that support features already implemented. Grows as each subsequent feature is added.
The settings tree is defined in full in interface.md (see the 2026-07-20 reconciliation addendum there — this version implements that reconciled tree). Top-level categories: Appearance (the visual skin — Density, Splash, Colours, Fonts, Presets, Ambient Mode), Layout (the placeable surfaces — System Bar, Module Panel, App Launcher, Elements, Overlays), Modules, Transports, Vehicle, Audio, Notifications, Apps, System, and Developer.
The governing principle — a work-in-progress tab is a placeholder, not a version. The full navigation tree is built once, in the shell version (1.5.2): every category and subcategory present and navigable, the live ones empty-but-ready, the future ones showing an honest "arrives with vX.x" label. After that, numbered 1.5.x versions go only to tabs with a live feature to wire. Each placeholder lights up at its own feature's version — Module Panel at 1.6.x, Viewport at 1.7.x, App Launcher at 1.8.x, Elements at 1.9.x, the theming/overlays/audio/notifications/apps tabs across version 2, and the vehicle/CAN tabs in version 3. This is the roadmap's own rule ("only settings that support features already implemented; grows as each feature is added") made literal: the tree is complete and honest from 1.5.2, and each tab becomes functional when its feature ships. No reserved-empty slots — refinements slot in by renumbering, exactly as 1.4.x did when Bluetooth took 1.4.12.
Build order — one piece per version:
- 1.5.1 — Reconciliation & planning (no code). Update roadmap.md and interface.md: introduce the Layout top-level category and move System Bar, Module Panel, App Launcher, Elements and Overlays into it; split Overlays (appearance → Layout, trigger mapping → Notifications); correct the System Bar entry to Position + Edit Bar Layout entry (height/zones/element sizing live inside edit mode, per 1.3.9/1.3.13); drop the redundant Spacing subcategory; record the WIP-placeholder convention; resolve the four handoff observations. A Bible edit to two documents — the deliberate change interface.md requires, discussed and agreed before writing.
- 1.5.2 — Settings shell + full navigation tree. Full-screen panel below the system bar; three-level progressive navigation (major → subcategory → content) with the animated category slide; visual identity inherited from the system bar theme tokens; settings button on the bar wired to open it. Every category and subcategory in the tree present and navigable — live ones empty-but-ready, future ones honest WIP placeholders. Verified as: the tree navigates, the slide animates, the labels are correct. (Complete — 1.5.2, hardware-tested by Roger 2026-07-20.) Built to a two-pane navigation model rather than three-column (the cleaner one agreed during the build — main tree, then a subtree that replaces it, plus the content box; reconciled in interface.md's Settings Panel section). The panel rolls out from the bar like a blind (an explicitly animated height —
AnimatedVisibility'sexpandVerticallydoes not honour a duration when the container isfillMaxSize, which is why the first attempt ignored the speed setting), and a user-configurable transition length was added along the way (Appearance setting,LocalTransitionMillis, INSTANT → CINEMATIC; open and close symmetric). The theme token set was reconciled and extended —DashColors→DashTheme, nine tokens (background/text/icon/accent primary+secondary, plusfont) with a primary/secondary pairing rule, the old fourbar*tokens retired — and every DASH chrome string was moved onto thefonttoken so a single v2 setting will change all typography (the module-panel placeholder and the token default excepted). The settings button became a real vector gear filling its cell (tintediconColourPrimary) and now toggles open/closed. A throwaway module-panel placeholder (deleted at 1.6.x) lets the panel demonstrate conforming to the module panel — covering it minimised, yielding to it expanded. A temporary LEGACY SETTINGS bridge keeps the pre-1.5.2 flat controls reachable until they are rehomed (removed at 1.5.14). (The settings-panel landing looked bare, which sparked the settings hero design — a living silhouette/weather/god-ray scene — worked out this session and parked to version 2; see the v2 section.) - 1.5.3 — Appearance › Size & Scale (was "Density & Scale"). (Complete — 1.5.3, hardware-tested by Roger on phone and tablet 2026-07-22.) Became the DASH sizing hub: a DASH Scale section (system bar size, element size, app-favourites-bar placeholder, DASH text size — each a ± stepper writing its own surface's config) and an Android section (app density + font size, capability-gated to native controls on a privileged install, an honest "Android text & display size →" link on Bronze). DASH now owns its text sizing — a
dashTextScaleapplied at the composition root so DASH text ignores Android's font setting entirely; per-surface sizing plus a DASH-owned text size retires the old fluid global "DASH UI scale." Built alongside it, because making the first real tab feel right across a phone and a tablet demanded them: the reusable settings content scaffold (header + setting blocks + control vocabulary + live preview, so every later tab is built the same way), the adaptive settings layout (two-pane when wide, interface.md's original three-level drill-down when narrow — the panel reshapes to the space it has), and rotation persistence (MainActivityhandles config changes, so an open screen reflows instead of resetting to home). Both interface.md sections reconciled (dated 2026-07-22 addenda). Outstanding: only text is off Android (dp-renormalisation parked), the privileged font control is visual-only, and the EDIT BAR LAYOUT ruler still coexists with the new steppers — see the changelog. - 1.5.4 — Settings landing screen (interim). (Complete — 1.5.4, hardware-tested by Roger on the Pixel 8 Pro and the Galaxy Tab S9 Ultra, 2026-07-23.) The bare empty-state box now holds a layered weather scene — the version-2 hero brought forward, minus the vehicle silhouette and live-car interaction (those stay v2). Four layers, each user-replaceable and procedural if no art is supplied: a nine-state time-of-day skybox with the sun riding the right third, seven cloud levels driven by real cloud cover, three graded backgrounds (day / night-lit / snow), and procedural rain/snow/fog. "Frozen clock, living air" — the reading is a snapshot per open, but the air keeps moving. Live weather from Open-Meteo (keyless, CC-BY attributed) over a location cascade — manual pin → GPS (opt-in, never prompted) → IP geolocation → offline clock-only floor — with a new System › Location settings tab (device-location opt-in + manual city). The procedural render is a keeper look in its own right, not a placeholder; painted art is an optional override. Full art naming scheme and drop-folder instructions (
Android/data/com.dash.android/files/weather/,sky-<time>/clouds-<level>/background-<state>) are in the changelog. - 1.5.5 — Transitions & motion. (Complete — 1.5.5, hardware-verified by Roger 2026-07-23.) The single global transition length became a per-transition system under Appearance › Transitions (the open question resolved: it earned its own subcategory, not "Motion" — the tab holds transitions so it is named for them; interface.md 2026-07-23 addendum). The governing rule: if it's a transition, it goes in Transitions — every surface reveal/hide/move is user-controllable and breaks out to its own control; widget micro-animation stays fixed. A master pace (derived, greys to "Custom" when one diverges, re-tap re-syncs) sits over a registry-driven per-transition list that grows itself as surfaces ship. Six presets — INSTANT · FAST · NORMAL · SLOW · CINEMATIC · LABORIOUS (3000ms). Settings open/close split into independent durations; splash gained a real fade-in (fades here, dwell + artwork stay with the Splash tab). The parked legacy control was removed — this version is its rehome. Built alongside: a shrink-to-fit segmented control (
FitPresetSegment, measures its font down instead of side-scrolling) and weather pre-load so the settings landing opens on real weather, not the clock-only floor. Outstanding: copy/text polish deferred, and the stale WIP tree labels await the 1.5.14 sweep — see the changelog. - 1.5.6 — Appearance › Splash Screen. (Complete — 1.5.6, hardware-verified by Roger on the Pixel 8 Pro and Galaxy Tab S9 Ultra, 2026-07-23.) Grew past "image-or-colour + duration" into three source types — Colour / Image / Animation (a GIF or animated WebP that plays through once, no dwell) — over an independent BackgroundColourSplash (the splash's own colour, theme-token presets plus a custom HSV picker, persisting as the matte for all three types); a real-screen-shape preview with a Landscape/Portrait toggle (which also fixed a landscape-shows-portrait bug); and a per-orientation "Model B" crop editor — a fixed screen frame with the image panning/pinching behind it, a contain-floor to a standard-crop detent, two crops stored (portrait + landscape), driving the real boot splash. Display duration is a stepper for Colour/Image. Splash removed from the legacy settings bridge. Animation crop and true video deferred; see the changelog.
- 1.5.7 — Layout › System Bar. (Complete — 1.5.7, hardware-verified by Roger, 2026-07-23.) The box holds Position (live Bottom/Top toggle with a sliding "screen" preview), Zones (live 1/2/3 count), the Edit bar layout entry point, and Reset (tap-to-confirm). In the doing, edit mode was stripped to its irreducible job — the ruler beside the bar plus Save/Cancel — with its colours on the theme tokens; Bar Height and Element Size were deleted (they had lived in Appearance › Size & Scale since 1.5.3, clearing that duplication), the module panel and not-default-launcher banner now hide in edit mode, and Save/Cancel return to the System Bar tab instead of dropping to home. System Bar removed from the legacy flat panel. interface.md item 4 refined by a dated addendum (Position + Zones + entry + Reset). See the changelog.
- 1.5.8 — Modules › Module Management rehomed. (Complete — 1.5.8, hardware-verified by Roger, 2026-07-24.) The full 1.4.x Module Management screen (install/uninstall, fail badges, UPDATE, REFRESH, ACTIVE/DORMANT/NO REPLY chips) migrated into the Modules tab. Reached through a
LocalModuleDeskCompositionLocal (the four stateful managers live on the controller, so the tab reaches them rather than rebuilding them the way the stateless-prefs tabs do); a newfillsBoxtree flag lets the tab own the box height so its controls pin and only the cards scroll. *Planned as "no rebuild", but the migration turned into a card-interaction redesign on Roger's direction: the DETAILS dialog was dropped entirely (it carried no detail of real use), the card became a tap-to-select row with its action (INSTALL / UNINSTALL / UPDATE / progress) raised into the top bar beside REFRESH (REFRESH left, action right), a transport tag (USB/WIFI/BT) joined the chip row, identity lines scroll rather than wrap, and every control moved to the modern DASH button idiom on the settings-surface token. The unconfirmed-deactivation safety warning stayed. See the changelog. - 1.5.9 — Modules › Enable/disable cut, and single-sub categories open straight from the main tree. (Complete — 1.5.9, 2026-07-24, Roger's call.) *The version originally planned as a per-module user-facing disable (a stored intent withholding ACTIVATE, a persistent sibling of the 1.4.13 quarantine) plus transport assignment for a module on more than one pipe. On interrogation before any code, neither earned its place in v1, so both were cut: disable — you can already silence a board by unplugging or uninstalling it; the only case it uniquely serves (a board physically connected but software-muted, config kept) is thin, and the DASH ethos is not to build speculative user-intent layers before a felt need; transport assignment — a make-it-work-correctly concern that only bites once boards actually appear on two pipes at once, which hasn't happened. So 1.5.9 became the cut itself + its natural consequence: with Enable/Disable gone the Modules category has a single subcategory (Module Management), and a one-item submenu is pointless — the shell now opens a single-sub category's content straight from the main tree, no intermediate submenu. Made generic, so Transports (also one sub) collapses the same way. If a real need for disable or transport assignment surfaces, it returns as its own version, likely in v2. See the changelog.
- 1.5.10 — Modules › Transport Manager. (Complete — 1.5.10, hardware-verified by Roger on the Tab S9 Ultra with a WiFi R4, a USB R4 and a Bluetooth ESP32, 2026-07-27.) Planned as a generic transport list — enable/disable per transport, WiFi port/host, Bluetooth paired devices. What it became is a board-connection diagnostic, because the list as specified had nothing worth listing: enable/disable is the speculative user-intent layer 1.5.9 already cut, port is fixed at 3274 by transport.md, and pairing is Android's own screen that DASH deep-links out to rather than reimplements. So the card states the pipe's name, an address panel ("point your board at 192.168.50.4:3274", copyable) for any pipe a board must be aimed at, the pipe's own honest condition in one line, and then the boards on it — each with DATA and DASH lights and, where the card is wide enough, a plain sentence saying what they mean. It lists boards, not modules — a board is physical and may host many modules, which is exactly the line between this screen and Module Manager. Two supporting fixes came out of bench testing:
TransportStatusgained a structuredaddressfield (the UI was regex-scraping it out of a human-readable sentence), and the WiFi pipe gained an idle horizon — a powered-off TCP peer sends no FIN and no RST, so without asoTimeoutthe reader blocked for ever and the card claimed a link to a board lying unplugged on the bench. The Developer and Transports top-level categories were folded into Modules, and the planned CHECK NOW button was cut: the pipes greet an arriving board in ~100 ms, sweep every 30 s and close a dead client on the horizon, so a manual refresh on a self-refreshing surface only invites the user to press something before believing the screen. See the changelog. - 1.5.11 — The transport stack lives for the life of the process. (Complete — 1.5.11, verified 2026-07-27 by forcing two activity recreations with a dark-mode toggle: same PID throughout, no teardown, no reconnect.)
TransportManagerandDashControllermoved offMainScreen'sremember { }and onto a newDashApplication. Until now DASH's entire module bus was scoped to a composable, so any Android activity recreation tore it down and rebuilt it: every socket dropped, every module forced to reconnect,lastSeenwiped so the installed list fell back to DORMANT, and any install handshake in flight died mid-declaration.MainActivityhandles orientation and resize itself but notdensity,uiMode,fontScaleorlocale— so a dark-mode switch at dusk, a font-scale change, DASH's own App Density feature, or the system reclaiming the activity while the user is in Maps each triggered a silent, unannounced restart of the whole bus, mid-drive. Chosen over aViewModelbecause DASH is a launcher: opening an app in the viewport backgrounds it by design, and a ViewModel is cleared when the activity genuinely finishes — the bus must outlive the screen. The process is the honest scope; there is deliberately no teardown path, since the oldonDisposewas itself what let a transient UI event kill the bus. (Found on the bench 2026-07-27 as a duplicateclient c1in the WiFi log — a reset connection counter, which only a freshTransportManagercan produce. A foreground service is the next escalation if real hardware ever shows the process being killed mid-drive; not built on speculation.) - 1.5.12 — Modules › Serial Monitor + Signal Monitor rehomed, and the settings surface went dark. (Complete — 1.5.12, 2026-07-27.) Planned as a migration into a Developer tab; the Developer category was confirmed dead instead (Roger's call — the tabled question below is now settled), so both instruments landed under Modules beside the boards they watch. Nothing sits behind a safety gate: every instrument is a normal tab, per CLAUDE.md's no hidden menus, no barriers. Both were rebuilt on the settings surface rather than ported — the old screens were near-black wells with their own headers and CLOSE buttons, which suit a standalone route and not a tab. The Serial Monitor became a filterable grid: TIME · DIR · TRANSPORT · BOARD · MESSAGE · MODULE ID · PAYLOAD, with the filter for each column living in its header and its options built from what has actually been seen on the wire, so a message type invented by a community module appears the first time it is sent. Its per-pipe status lights were dropped — Transport Manager owns "what is connected and is it healthy", and saying it twice invites the two to disagree. Legacy panel cleanup: both standalone routes deleted, and the stale App Density block (a duplicate of the Appearance › Size & Scale deep-link since 1.5.3) removed. The theme moved too —
backgroundColourSecondary848482 → 2C2C2E andbackgroundColourPrimaryE5E5EA → D2D2D7 (withaccentColourPrimaryfollowing it to C2C2C7). The old mid-grey sat in the middle of the luminance range where nothing can separate from it: colour-coded text measured 1.06–1.28:1 and was unreadable whatever hue was tried. No palette fixes that; only the surface can. See the changelog. - 1.5.13 — Instrument polish: a DASH-native dropdown, measured columns, and a user-owned line buffer. (Complete — 1.5.13, 2026-07-27.) Originally cut earlier the same day — every part of the planned 1.5.13 had been rehomed or dropped by 1.5.10 and 1.5.12 (transport diagnostics are Transport Manager; the log viewer became Modules › Activity Log, deferred to v2; the safety-acknowledgement gate died with the Developer category). The number was then reclaimed the same day for the polish pass that came out of driving the new tabs on real hardware.
DashMenu— a DASH dropdown built onPopup, replacing Material's, which was the one thing on a DASH surface that could not be styled from its call site: DASH has never provided aMaterialTheme, deliberately, and every other Material component it uses takes an explicit colour at every call site, which is why the gap stayed invisible for eleven versions. Column widths are now measured, not constants — DASH lets the user change its text size, so any hardcodeddptruncates for somebody; each column measures its own header and a representative sample and re-measures when the font, density or text scale changes. The line buffer became the user's (50 · 200 · 500 · 1000 · 5000, persisted) because the right answer depends on how busy their bus is, which DASH cannot know — at twenty modules reporting ten times a second, 500 lines is two and a half seconds. Plus: the transport column reads Bluetooth rather than the internal tag; PAUSE green, CLEAR red, actions left and readout right; the send box returned as a COMMANDS drawer that opens on request and closes on send; the settings content box now aligns with the heading rather than the top of the tree, gaining the height it was missing; and the Signal Monitor's count toggles live-only. See the changelog. - 1.5.14 — System › Android Settings Links, About DASH, Licence and Power. (Complete — 1.5.14, hardware-verified by Roger on the Galaxy Tab S9 Ultra and installed on the Pixel 8 Pro, 2026-07-27.) Planned as "deep-links + an About surface (version, licence)"; it grew three ways on Roger's call during the build. (1) Android Settings Links — fourteen links in four sections, each capability-detected with fallbacks, a link nothing can handle simply not listed and a section left empty by that removed. DASH is the home screen, so Android's own Settings is installed, running and unreachable: without this tab a dropped WiFi connection cannot be fixed from inside DASH. (2) About and Licence split into two tabs — About answers who made this and where to find it (identity, the author, links as URL + QR, and a twelve-line device report with COPY REPORT); Licence carries the GPL-3.0 §5(d) notice DASH had never displayed anywhere, the full text read from an asset the build copies from the repo root, and the nine open-source components whose Apache 2.0 attribution was likewise not travelling with the binary. (3) System › Power, last under System, rehoming EXIT DASH with tap-to-confirm — named Power rather than Exit because on Silver/Gold it also holds restart and shut down, built privileged-only (
REBOOT/SHUTDOWNaresignature|privileged, ungrantable to a sideload, and root is forbidden) and therefore absent on Bronze rather than disabled.CHANGE LAUNCHER →also rehomed into the links tab as Default home app, leavingROTATIONas the last thing stranded in the legacy panel. Two things came out of the doing: a latent bug in the density probe — it reset the screen on privileged hardware, safe only while the density tab was its lone caller — now a true no-op and asked once per process; and the item is the control, Roger's call that a row with exactly one action needs no OPEN button at its end, which suits a moving vehicle better than a word at the far edge. (Power was built as a top-level category and moved under System the same day on Roger's call — a surface that restarts and shuts down the device is a system concern, not a peer of Appearance and Layout.) See the changelog. - 1.5.15 — Cleanup & polish — and the settings design language. (Complete — 1.5.15, hardware-verified by Roger throughout on the Tab S9 Ultra and Pixel 8 Pro, 2026-07-29.) Planned as "remove the old flat scaffold, confirm every 1.1.x–1.4.x feature has a home, confirm every WIP placeholder is honest." That part took an afternoon. The legacy panel is gone — the bridge open since 1.5.2, closed after thirteen versions of rehoming, its last live control becoming Layout › Rotation (all four orientations, and a glyph that is a miniature of the user's real layout with their system bar on the edge it will actually occupy, because a plain rectangle cannot show a 180° rotation). Also swept: the always-on DiagnosticOverlay pinned to the home screen since 1.1.x, and the dead
dashScaletoken,sendTo,formatDpi,TransitionId.hintandSettingToggle. What it became is the version where DASH stopped being a collection of settings pages and became one system. Going through every page in order, the same problem kept surfacing: seventeen font sizes across ninety-eight call sites, four different page margins, per-page spacing constants that no two pages shared, and controls that sized themselves to their own words. So: a five-tier type scale (HEADING · SUBHEADING · MAINBODY · BODY · TINY, inspso the user's text-size control still owns them), one spacing file, one status palette, and one control container — an 8% wash inside a hairline border, worn identically by every button, segment and stepper. With four rules recorded in the code rather than in anyone's head: a page is titled once; sections carry no rule; no help text under settings; and every control sits right, at one width, growing downwards rather than truncating. Then applied page by page, with About DASH and Licence deliberately left alone. Plus: the Location permission toggle became a selector (a toggle promises both directions and only Android can revoke a permission), Splash gained a None type that genuinely skips the splash, its colour picker collapsed behind a Custom swatch with a full hex/RGB/HSV editor, and the text scale widened to 0.4–2.0. Three regressions, all found by Roger looking at a page: the type sweep mis-ranked page titles, card names and column headers, because a value-based mapping cannot see a thing's rank; and controls squeezed their own content out below 0.5× because their touch targets do not shrink. See the changelog. Final version before 1.6.x — the 1.5.x Settings Panel era is closed.
(2026-07-27 move — Roger's call, later the same day. DASH Location Service leaves 1.5.x entirely and becomes 1.10.x, and Cleanup & polish shunts back down to 1.5.15, keeping the 1.5.x sequence continuous with no gap — the same reasoning that reclaimed 1.5.13 rather than skipping it. Reason: the hardware isn't here. The dev tablet has no GNSS and the USB receiver has not arrived, so building it now would mean writing a device-wide location provider blind and signing it off on trust, which is not how anything else in this project has been verified. It goes to the end of version 1, where the hardware will exist. It keeps its own major-feature number rather than joining a deferred bucket — it is a platform service that publishes to Android's own location provider and makes navigation possible, not a polish item, and numbering it beside a donations card would say those are the same size of thing. It stays put once moved, even if the receiver turns up early. The consequence, accepted: weather runs on the 1.5.4 IP-geolocation cascade for the whole of 1.6.x–1.9.x, which is a town or two out but works, and still degrades to the offline clock-only floor. Also decided: the deferred ACCESSORY SDK does not go to the end — it lands as 1.6.6, immediately after the module panel, because the half of it that locks arduino.md §11 into module-sdk.md wants doing while the panel's layout format is fresh, not months later reconstructing why each decision was made. See 1.10.x, 1.11.x and 1.6.6 below.)
(2026-07-27 renumber — Roger's call. A new 1.5.11 — transport stack lifetime was inserted, and everything from the old 1.5.11 onward shifted +1 (old 1.5.15 Cleanup → 1.5.16). Only version numbers moved — no content in the shifted versions changed. Reason for the early insert rather than appending it at the end: it is a foundation fix, not a settings tab, and every remaining 1.5.x version would otherwise be built on a bus that silently restarts itself. Found mid-bench while verifying 1.5.10, not planned. Also decided the same day: the Log Viewer of the then-1.5.13 is pulled out of Developer and becomes Modules › Activity Log, deferred to v2 — DASH's own decision log (the refused / dropped / left-dormant reasons that today reach only logcat) is real diagnostic value, but it needs its own in-app log sink rather than logcat, which no Bronze sideload has permission to read, and that is a v2-sized piece of plumbing rather than a 1.5.x rehome. Whether the Developer category survives at all — 1.5.10's working tree folds its instruments into Modules and drops the safety-acknowledgement gate as contrary to CLAUDE.md's no hidden menus, no barriers — was tabled to 1.5.12/1.5.13 — and settled at 1.5.12: the category is dead, its instruments live under Modules, and 1.5.13 was cut as a consequence.)
(2026-07-21 renumber — Roger's call. Two interim visual/motion versions were inserted early: 1.5.4 Settings landing screen and 1.5.5 Transitions, both new. Density & Scale holds at 1.5.3; everything from the old 1.5.4 Splash Screen onward shifted +2 (old 1.5.12 Cleanup → 1.5.14). Only version numbers moved — no content in the shifted versions changed. Reason for the early insert: the bare grey settings landing bothered Roger now, so the interim visual lands before the rest of the rehoming work rather than after it.)
(2026-07-24 shunt — Roger's call. DASH Location Service inserted as a new 1.5.14; Cleanup & polish shunted +1 to 1.5.15, keeping cleanup as the final 1.5.x version before 1.6.x. Reason: the dev tablet has no SIM and no GPS, and location needs a proper device-wide solution — GNSS ingest published to Android's system location so Google Maps and every viewport app get a live fix — rather than the interim weather-only cascade from 1.5.4. Only Cleanup's number moved; its content is unchanged.)
Why fifth: Users need to be able to configure what has been built before building more on top of it. The settings panel also needs to exist before module panel and launcher settings can be added in subsequent features.
What it is: The display area for installed accessory modules. The core DASH differentiator.
What gets built:
- Module panel container — docks to any of four edges
Automatic orientation — horizontal for top/bottom, vertical for left/right— superseded 2026-07-30: orientation is manual, the user's choice, not derived from the docked edge- Three sizes — Small one times, Medium two times, Large four times system bar height
- System bar relationship — module panel never overwrites system bar space
- Persistent mode — always visible
- Floating mode — hidden with peek strip, swipe to reveal, tracks gesture in real time
- Module layout slot rendering — h underscore and v underscore variants, light and dark
- Fallback behaviour — cascade to nearest available slot if requested slot undefined
Swipe to cycle between installed modules— dropped 2026-08-19: the swipe belongs to the module, not to DASH (1.6.8). Switching is the tab bar and themodule_panel_left/module_panel_rightsystem commands.Module Panel Reveal element added to element library— dropped 2026-07-30: hide-and-reveal is panel behaviour (1.6.9), not an element- Stacking rule enforcement — floating module panel and floating app launcher cannot share same edge (deferred 2026-07-30 to 1.8.x: the launcher does not exist until then)
- Module Panel settings added to Settings — Appearance tab, Panels section
The version sequence (Roger's call, 2026-07-30 — the feature list above is the inventory; this is the order it gets built in.)
-
1.6.1 — Documentation. Get the written record up to date before building, and draft the ACCESSORY panel/layout format — the grammar of what a module actually sends. Drafted here, not locked: arduino.md §11 has deliberately never been locked, and a spec is not locked before it has been built against. It locks at 1.6.10. (Discharged in full — 2026-08-05/12, ahead of 1.6.5 rather than at 1.6.1, exactly where 1.6.2's note said it should sit.) The layout format was designed across two sessions and promoted into its own document,
module-layout.md— the same move that producedmodule-sdk.mdat 1.6.10's ancestor, 1.4.15: the spec moves out, the reasoning stays in the working record. Every other document now points there rather than keeping its own copy, which is what closes the drift this version existed to fix. Not a numbered version and not in the changelog — nothing was built and nothing hardware-verified; the decisions and their reasoning are dated inside the specification itself. -
1.6.2 — Module home. The module panel itself, at the large size only, and one panel only. The first thing on screen. (Complete — 1.6.2, hardware-verified by Roger on the Tab S9 Ultra, 2026-07-30.) A plain box filled with
backgroundColourPrimary— no border, no radius, no content — because DASH draws the container and stops at its boundary. The large slot got the first real ratio of the twelve, 4 × 1.5, arrived at by eye across four attempts on hardware (3:1 → 4×3 → 4×2 → 4×1.5) and held as one constant since it stays provisional until the set locks at 1.6.10. New rule (Roger): the panel and the system bar never share an edge and never stack — where interface.md had the panel beginning where the bar ends, they now cannot meet at all, and DASH moves the panel out of the user's way when the bar arrives; at this version that resolves to the panel taking the edge opposite the bar. Settings can never cover the panel, so the blind insets around both edges and rolls out into the band between them. Swept: the 1.5.2 module-panel placeholder (deleted exactly as planned) and the not-default-launcher banner, which wanted the same edge — checked first that System › Android Settings Links already reaches Android's home-settings screen, so nobody is stranded. (1.6.1's documentation work is only part done — interface.md is reconciled, but the §11 layout-format draft and transport.md's pre-1.4.x text carry forward; neither blocks the container versions, and the draft is best sited just ahead of 1.6.5.) -
1.6.3 — Settings and docking. Move the panel around the screen and dock it to a chosen side, wired into the settings panel. (Complete — 1.6.3, hardware-verified by Roger, 2026-07-30.) All four edges, a persisted
ModulePanelConfig, and Layout › Module Panel live — ending the WIP placeholder open since 1.5.2, with four tiles built on the Rotation tab's glyph precedent (the real screen, the user's own bar on its actual edge, the panel where it would land). The long edge is the docked edge less what the bar has taken from it (Roger): full screen width on a horizontal edge, screen height less the bar on a vertical one — so changing bar height resizes a vertical panel. The vertical slot is the horizontal one stood on its end, 1.5 × 4, one constant serving both once expressed against the long edge; vertical is the shape that suits a landscape screen, giving a usable option in either orientation for the first time. Preference and effective edge are now separate — a collision with the bar displaces the panel for as long as it lasts and never rewrites what the user chose, so their edge returns when the bar moves away. The bar's own edge cannot be newly selected (Roger): greyed, unclickable, labelled with the reason, its glyph showing the bar alone — a deliberate exception to 1.5.15's no-dead-controls principle, since that edge is genuinely occupied rather than merely unwanted. Moving between edges registered asMODULE_PANEL_MOVEin the self-growing transition registry, with no settings rework. -
1.6.4 — The other panel sizes, and their integration into settings. (Complete — 1.6.4, hardware-verified by Roger, 2026-08-01.) All three sizes built, and the six layout slots finally have values — referred to since transport.md was written, undefined until now. Roger's call, in whole numbers: large 8 × 3, medium 16 × 3, small 16 × 1 horizontally, each vertical slot being its horizontal twin stood on its end (3 × 8, 3 × 16, 1 × 16) — so there are three shapes to learn rather than six. Long edge first, thickness second. Held DASH-side as integer
longUnits/thickUnitswith the aspect derived, so the ratio a module author reads and the number the layout maths uses are one fact that cannot drift apart. Chosen by eye across four builds on the Tab S9 Ultra rather than by reasoning, which is why they could not have been drafted at 1.6.1 as first planned. Layout › Module Panel became Size then Position (Roger) — size is the larger decision, and the position tiles draw the panel at whatever size is chosen above them, so the page reads top to bottom as one continuous answer. Every tile is a picture of the real result, with glyph thickness taken from the same ratio the real panel uses. Documentation: arduino.md §11's standing open item is closed — the six ratios are drafted there in full, and interface.md's "1× / 2× / 4× system bar height" is superseded (the impression was right; small does land near bar height on a wide screen, it just could not survive being tied to a measurement the user can move). Still a draft: nothing has been drawn to these shapes yet, and they lock at 1.6.10. -
1.6.5 — A test module on an ESP32, connected over WiFi. The first real hardware behind the panel. (Complete — 1.6.5, hardware-verified by Roger on the Tab S9 Ultra, 2026-08-12.) A Tank Gauge ACCESSORY (
arduino/current_sketches/GaugeWifi/) streams three blocks out of an ESP32's flash onINSTALL— the layout documenth_large_dayplus the two pieces of artwork it refers to — then reportstank_pressurefour times a second on a simulated sweep. 87,262 bytes arrived byte-for-byte identical, every CRC32 passing. The layout is the declaration (module-layout.md§9) made real:onInstall()is aMANIFESTand three blocks, because the layout already names every variable and control. TheDashAccessorylibrary helper stays deferred to 1.6.10 — the class here is the draft it gets extracted from, proven on hardware first, exactly as 1.4.15 did for SYSTEM and LISTENER. One long-standing defect fixed on the way:FrameAssemblercarried a 64 KB block limit whose comment deferred to an open item that had been closed the other way the same day (§2 — there are no asset caps), and an over-size block fell through into line framing, which would have shredded the PNG rather than failing honestly. Now an 8 MB sanity guard, with over-size blocks read-and-discarded and reported asFailReason.OVERSIZE. The panel still draws nothing — that is 1.6.6, and this version deliberately stops at the disk. -
1.6.6 — Redrawing that module's layout onto the panel. What the module sends is what the panel draws. (Complete — 1.6.6, hardware-verified by Roger on the Tab S9 Ultra over all three transports, 2026-08-13.) The twelve slot names became a derived type, closing the item 1.6.5 left open — and because the classification falls out of block names already recorded, the 1.6.5 install was understood without a reinstall. The SVG parser shipped out of the spike branch into
com.dash.android.panel,gradientTransformwas implemented (the one Open Item marked before 1.6.6 ships), and the layout engine went in: layers and bindings, theme tokens resolved in the frame being drawn, raster decoding that halves rather than refuses under memory pressure. §6's no layout, no panel was built — with nothing able to fill the slot the panel is absent from the screen's arithmetic, not merely skipped, superseding 1.6.2's empty-box tenancy. The Tank Gauge gained real bindings (a needle pivoted about a fraction of its own box, a clipped bar, a translating arrow head, a delegated theme token, a pulsing warning lamp, a text readout that reddens past 9 bar), and two more sketches —GaugeBtandGaugeUsb— carry the identical panel over Bluetooth and USB from one sharedassets/folder, byte-for-byte. Two 1.4-era defects surfaced and were fixed on the way:svg-subset.jsonwas never being copied into the APK, so no vector could draw on any build of main; and a Bluetooth board's name was captured once at connect and frozen, so a reflashed board kept its old name. -
1.6.7 — Data between the two devices. Buttons wired up, and both ends talking in the way they are meant to. (Complete — 1.6.7, hardware-verified by Roger on the Tab S9 Ultra over WiFi and Bluetooth, 2026-08-14.)
touchbindings, parsed since 1.6.6 and inert until now, are honoured across all three target routes — includinglayer#element, which meant walking the renderer's coordinate chain backwards to find a named path inside parsed artwork. The press area is the element's bounding box at its authored position: not its outline (a+drawn as two bars is a button, not two bars) and not where a binding has moved it to (a control that slid away from your finger would be indefensible). DASH imposes no minimum touch size — inflating a control the author drew small would be reaching inside the panel boundary to correct a design decision. Then the optimistic update and the timeout that keeps it honest (§8), with predictions held in a layer in front ofModuleDatarather than in it, because that store holds what modules said and a prediction is what DASH expects them to say next. The Tank Gauge became a thing you drive — the sweep is gone, because it proved the needle moved and could never prove why; three buttons cover both halves of §8, PLUS/MINUS momentary (nobody can name "one more than it is now" in advance) and ZERO carrying a literal, which is the only control on the panel that exercises optimism at all.module-layout.md§8 gained the rule it had always assumed — a touch binding'scontrolis the variable it predicts and itsvaluethe prediction — plus a correction to §9's table, which had claimed an omitted value sends an empty field when it sends no field at all. One defect found and fixed in-version, by Roger running exactly the test that was asked for: the coroutine expiring predictions was keyed only on the report map, so it completed at composition and no press ever restarted it — the optimistic write worked and the clock that makes it honest was never running. The escalation counter was dead for the same reason. See the changelog. Extended 2026-08-15 — the module's own message board, and the Climate panel. (Not a new version. Roger's call: this was written as firmware for a climate panel, and it is an extension of 1.6.7's press work rather than something of its own.) Designing the Climate panel broke 1.6.7's press model before a line of it was built — nine variables and thirteen controls where the Tank Gauge had one and three, and almost all of them steppers and toggles, none of which 1.6.7 could draw before the module answered, because "one more than whatever it is now" is not a value anyone can name in advance. Somodule-layout.mdgained §8a: a module declares, in its layout, the values each variable takes and in what order — the private twin ofsystem_commands.md, written by the module because the board is its own. A list, and no type system; a toggle is a two-element list that wraps, and the toggle problem disappeared into the stepper solution rather than needing a mechanism of its own. DASH now finds the current value, moves one place and sends what it landed on —ACTION|id|fan|4, neverfan_up— so a stepper predicts like anything else and every message on the wire is the same message. It does no arithmetic: it moves an index along a list the module wrote. This was not only firmware — it changedPanelVariable, the layout reader,TouchBinding.stepandPanelPress, which is why it is recorded here rather than left in the sketches. See the 1.6.7 changelog entry. -
1.6.8 — Multi-module. A second board over WiFi sending a different layout, and switching between the two accessory modules. The Climate panel built at 1.6.7 is the second panel this needs, and it looks nothing like the Tank Gauge, which is the point.
(Complete — 1.6.8, hardware-verified by Roger on the Tab S9 Ultra, 2026-08-26.) Two modules on two transports at once —
0000DA58AC05Climate over Bluetooth and0000DA58AC03Tank Gauge over USB — two tabs, and the cross-fade between two panels that look nothing like each other. The second board is an Arduino Uno R4 WiFi, not the ESP32 the sketch was written for:GaugeUsbcompiled for it unchanged and answeredDISCOVERfirst time, which is a portability result worth more than the tab it enabled. The bar also gained its colours and its size control in the same sitting — see below.The swipe is dropped, and the tab bar takes its place. (2026-08-19 — Roger's call.) The inventory above, and interface.md's Switching Between Modules section, had modules cycled by swiping inside the panel. They are not: the swipe belongs to the module. Why: the panel boundary is the module's box under the Module Mantra, and a single-finger gesture inside it is the module's input to claim — hold-to-repeat on a stepper, a drag slider, a swipe through presets. Once DASH takes that gesture it can never give it back without breaking every module built in the meantime, so it is not taken at all. The concrete problem it would have caused is already in the code:
PanelContent's press fires on touch-down and consumes the event, so a swipe starting on a control would send anACTIONbefore the finger moved and no drag detector would ever see the gesture.So DASH's control lives outside the box — a tab bar on the panel's inboard edge, the side facing the content area. DASH owns the walls; this makes one wall thick enough to touch. A tab per module that can fill the current slot, and tapping one goes straight to it — direct selection rather than cycling, so six modules are one tap apart rather than five. It is also where the which one am I on indicator honestly lives: DASH chrome on a DASH surface, with not a pixel of it inside the module's box. Tab labels come from the module's
HELLOname and need no SDK change.The same bar is 1.6.9's peek strip. The reveal handle and the switcher are one object and are not built twice — which is why its sizing and customisation are settled there rather than here. At 1.6.8 it exists with whatever defaults are needed to switch panels and no settings of its own.
The bar sits outside the panel and cuts into the viewport (Roger). Not out of the panel's own footprint: the module's box would then be thinner than the slot ratio it was authored to, and a module drawn for 8 × 3 must be drawn into 8 × 3. The panel keeps its exact shape and the cost lands on content area instead. Not overlaid on the panel either, which would be DASH drawing on the module's box.
The bar shows whenever the panel shows, including with a single module installed (Roger). Why: the viewport is then the same size on Monday and Friday — installing a module changes what is in the panel, not how much content area the screen has. It is also the reveal handle from 1.6.9, so a floating panel needs it whatever the module count, and one rule beats two. A single tab is a label rather than a dead control, so 1.5.15 does not object. With no module able to fill the slot there is no panel at all (§6), and therefore no bar.
Membership is simply: installed ACCESSORY, has a layout for the selected slot, gets a tab. SYSTEM and LISTENER modules never appear — they have no panel to draw. A silent module keeps its tab (Roger) — the install record is the tenancy, §6's rule is about layouts rather than liveness, and DASH does not annotate a tab with the state of its board. It also stops tabs appearing and vanishing every time a board brownouts or reconnects. Different id, different module, no exceptions (Roger):
GaugeWifiandGaugeBtare the same panel on two ids and get two identical tabs, and DASH neither disambiguates them nor comments on it.Load on demand, and no cache — measured rather than assumed, and this reversed the design. The Tank Gauge is a 1.4 KB layout, a 10.6 KB SVG and a 1600 × 600 PNG that decodes to 3.7 MB: roughly 20–40 ms, one to three frames. Climate is a 14 KB layout and one 10 KB vector with no raster at all — under 10 ms. There is no load to hide, so only the panel on screen is held decoded, memory is bounded at one panel however many modules are installed, and the LRU cache first proposed here was machinery for a cost that does not exist. (If a module ever ships a 4K photographic background and the panel stutters, §2's existing degradation — halve the decode rather than refuse — is already the answer, and it will be visible on hardware rather than guessed at.)
The switch is a cross-fade, with the outgoing panel held until the incoming one is ready so there is never an empty box. (Roger — "fine, but I might change it later", so it wants to be one constant.) Not a slide: sliding says Climate lives to the left of the Gauge, and tapping a tab directly can jump from the first to the fourth, so there is nothing meaningful to slide past. It is DASH's transition, so it takes the user's INSTANT–CINEMATIC setting — the opposite of anything inside a panel, where §5 gives the module's own durations and DASH's setting never enters.
No ordering at all in this version (Roger). Tabs sit in database order. The panel order, the dominant module and the dwell are one story and are told once, at 1.6.11, rather than a small version of it here and a real one later. (Amended 2026-08-27, roadmap 1.6.9: the dwell moved to 1.6.9 — Roger's call once he began working on the expansion model, where a panel that expands on a tap needs the timer that folds it back in the same version. The panel order and the dominant module are unaffected and still land at 1.6.11.) Which module was last shown survives a restart, because DASH restarts every ignition cycle — flagged at 1.6.11, where a dominant module may make that rule wrong.
Predictions stay keyed to the module, and switching away still retires them. (Corrected 2026-08-19, on reading the code: this was first written here as a regression to fix, and it is not one.
PanelPress.ktalready documents the behaviour as deliberate — "swapping panels retires the old one's outstanding presses with it, rather than leaving a prediction to expire against a module that is no longer on screen". That is right: a prediction exists so the panel does not look dead under a finger, so once the panel is off screen there is nothing to draw ahead of. What 1.6.8 changes is only that the case now actually happens.) The real structural change is thatrememberPanelPressesbecomes per-panel rather than per-screen — a cross-fade composes two panels at once, and one call at the screen could only ever describe one of them. Also expected and not a defect:GaugeWifiandGaugeBtare the same panel on two ids, so the bar shows two identical tabs. They are two modules. -
1.6.9 — The tab bar, and hiding and revealing the module panel. (Reveal requirement added 2026-08-12, out of the SVG parser spike. Tab bar scope added 2026-08-19 — Roger's call.) Whatever reveals the panel must itself be reachable when the panel is at its largest, on the smallest screen DASH supports. Found by walking into it: a large vertical slot on a phone in portrait leaves the settings panel almost no band to roll out into, since the blind insets around both the system bar and the panel — so the module panel's own setting became unreachable by the settings panel, with no way out, because DASH sets its own
requestedOrientationand no Android rotation control can get round it. A reveal control the panel can cover is not a reveal control. The combination that traps is one §6 ofmodule-layout.mdexplicitly permits a module to ship, so it is not an edge case to leave until the design is otherwise finished.The reachability rule is built — 2026-08-27, verified on the SM-X910, the SM-T710 and a Pixel phone. The answer is measured on the shape of the space left over rather than the size of what was taken: DASH knows the rectangle the settings blind would get, and tests its signed width ÷ height aspect against two bounds — no narrower than 0.48, no wider than 4.40 — plus the settings shell's own 302dp control-width floor as a hard backstop. Outside them the panel steps down to the largest smaller slot the module on screen actually ships, never crossing orientation, and retracts off its edge when there is no such slot. Retract is the guarantee; step-down is the courtesy (Roger) — the guarantee holds against any layout a module ships, while the courtesy is only available when the author drew more than one slot. This supersedes the 1.6.2 rule that settings never covers the module panel (Roger — "i think this is wrong"), and the stored preference is never rewritten, exactly as
effectiveEdgedoes not rewrite the user's edge. Tab membership is judged on the user's chosen slot rather than the yielded one, so DASH never puts a different module on screen. The tab bar never moves throughout — it is the one surface that must stay reachable. Full reasoning, the numbers and the open questions are in the changelog's 1.6.9 entry.The visibility and expansion model is built — 2026-08-27, verified on the SM-X910, the SM-T710 and a Pixel phone. The panel has four states, chosen as one setting: Off, Full, Retracted (rests off screen), Shrunk (rests at a smaller layout the author drew). The settings page is built from that choice rather than showing controls that are usually meaningless, and the resting-size constraint is structural — the full-size list is built from the rest size, so an illegal pairing cannot be expressed. The default is Off (Roger): DASH ships with no module panel until asked, which retires the question of a default size and gives the user the ability to turn the panel off at all. The viewport is laid out for the resting state and never the expanded one (Roger) — an expanded panel draws over the viewport rather than pushing it, so the running app never relayouts, and the user pays for the state the panel is usually in rather than the state it is briefly in. The bar lists what you can go to and is never filtered by what happens to be on screen. A tap shows a module at the largest slot it can fill, and tapping the one already on screen folds it away by hand. Any touch inside the panel restarts the dwell, observed through the existing press loop and consuming nothing — 1.6.8's rule that gestures inside the boundary belong to the module is untouched. Shrinking is the feature; compacting is the overflow mitigation, and the two are deliberately different words (Roger).
Compacting is built — 2026-09-14, hardware-verified on a Pixel 8 Pro in both docks. The overflow mitigation agreed on 2026-08-27, unchanged in the building: a panel that cannot fit at its true ratio scales down uniformly, keeps its aspect exactly, and centres on its edge (Roger) with
backgroundColourPrimaryeither side.ModulePanelSpec.boxForcaps thickness at what the screen has and lets the ratio pull the long edge in with it, so the author's shape is never distorted — a compacted panel is the drawing the module shipped, smaller. The band either side is DASH's own surface, not the module's box: a sibling of the panel rather than padding inside it, taking the panel's own offsets so it retracts with it.PanelYieldmeasures through the same function, so one place knows a panel can be capped rather than two that might drift.The finding worth carrying forward: this is not a head-unit curiosity. It was scoped as arithmetic for one bad pairing on a 1280 × 480 board, where a Large horizontal panel asks 119% of the band it has. But a phone held in landscape is head-unit shaped and overflows at default settings too — a Pixel 8 Pro is 997 × 448dp and asks 374dp of 372. The mitigation is reachable on ordinary hardware turned sideways. It is also the first place the panel stops being a wall and becomes an island on its edge, which is one built answer to the open question below — evidence for it, not a decision on it.
Verification, in brief: predicted and measured boxes agree to two decimal places in the horizontal dock (992.00 × 372.00dp, band 2.67dp each side, drawn ratio error 0.00000); the vertical dock was measured at two tab-bar thicknesses, compacting at 96dp and untouched at 32dp, giving a negative control. The differential between those two shots is the real proof (Roger's suggestion) — the artwork's height ratio measured 0.98094 against a predicted 0.98050, so the panel got shorter in proportion as it got thinner rather than being squashed. Full numbers in the changelog's 1.6.9 entry.
Still to build in this version: the tab bar's own Style, Spread, colour pairing and whether it shows at all.
The version number is now derived from changelog.md — 2026-09-11. (Roger's call, "it needs to not happen again". Not panel work; it surfaced on a catch-up read during this version and was fixed here.)
versionNamehad sat at 1.6.5 for four versions and a fortnight — wrong in About DASH, wrong in the copyableDeviceReport, and wrong in the title of every public nightly release. It had been bumped faithfully for twenty consecutive versions and broke at 1.6.6, in a commit that editedapp/build.gradle.ktsfor another reason and missed the line above. The bump is no longer a step anyone has to remember: Gradle reads the topmost## Version n.n.nheading out of changelog.md, andnightly.ymlreads the same source instead of grepping the build file. A missing heading fails the build rather than falling back. This is the disciplinecopyLicenceandcopySvgSubsetalready applied in that file — a list kept in two places diverges silently — applied to the one thing in it that was still kept twice.versionCodehas no source in the changelog and stays manual. Full account in the changelog's 1.6.9 entry.The tab bar and the peek strip are the same object, which is why the bar's own settings land here rather than at 1.6.8. The strip left on screen when a floating panel is hidden is the bar — so it is already the handle you pull, and it is already the thing that must stay reachable. Customising it once, after both of its jobs exist, beats customising it twice. (This scope was sited here rather than given a version of its own deliberately: inserting one would push the SDK lock to 1.6.11, and 1.6.10 is referenced 41 times across the document set and in ten source files, most of them in the changelog, which is a historical record and is not rewritten.)
Sizable and customisable (Roger). The bar is DASH's own surface, so unlike anything inside the panel it is entirely the user's to dress:
- Thickness —
its own stepper— landed early, at 1.6.8 (2026-08-26). Roger's call, and his reasoning, which is the reason it moved: asked how thick the bar should be, the honest answer is that it is not ours to choose. The cost lands on the content area, so the same 36dp is nearly free beside a large 8 × 3 panel and a real bite out of a 16 × 1 one — no single number serves both, and the person looking at the screen is the one who can see which case they are in. Giving them the tool was cheaper than picking for them. Settings → Layout → Module Panel → Selector, 24–96dp in steps of 4, default 36, stored inModulePanelConfigwith the panel's edge and size. Bounds and reasoning inModuleTabsSpec. (This bullet previously said "bound to the DASH UI Scale token" — corrected 2026-08-19; the fluidLocalDashScalewas removed at 1.5.15 and per-surface sizing replaced it at 1.5.3, which is the pattern this now joins.) No hard floor, deliberately: 24dp is small for a gloved hand, and interface.md reserves hard floors for safety-critical targets. The reachability question the bar really raises is the one below, and it is answered here rather than pre-empted with a floor. - Colour —
from the DASH theme tokens— already token-driven since 1.6.8, and re-paired on 2026-08-26 at Roger's direction. The bar took the secondary (dark) surface when first drawn; it now takes the primary (light) one, with the selected tab a darkbackgroundColourSecondarypill. Ink followsDashTheme's own stated rule rather than eye — primary surface carries black, secondary carries light grey — giving 13.9:1 unselected and 11.1:1 selected. The mid-grey accent was dropped from both roles: on a light bar it sits too close to the background to read at a glance, which is the trap 1.5.12 dug itself out of. What remains for this version is letting the user choose the pairing, rather than the pairing itself. - Tab style — pips, or the module's
HELLOname, or both. Icons are the open question, and it is time-sensitive:MANIFESTcarriesblocksandbytesand nothing else, so a module icon is an SDK change that must be agreed beforemodule-sdk.md's neighbours lock at 1.6.10, or it waits for version 3. - Alignment along the edge — full width, or gathered to one end.
- Whether it shows at all — and this one carries the version's own trap: hiding the only switch a user has strands them on one module with no way off it. Whatever answer is chosen, it is the same reachability rule this version exists for, applied to the bar rather than to the panel.
- Thickness —
-
1.6.10 — The module SDK — how to write a module. This is where
module-layout.mdlocks (amended 2026-08-12 — the panel spec was promoted out of arduino.md §11 into its own document ahead of 1.6.5, so it is that document which locks here, not §11 intomodule-sdk.mdas originally written), and where theDashAccessorylibrary helper is written (chunked assetBLOCKstreaming from flash,REPORTout andACTIONin,TRIGGER). (Moved here from 1.6.6, set 2026-07-27. The reason for siting it right after the panel work still holds — a spec lock wants doing while the layout format is fresh, not reconstructed months later — but by 1.6.10 that format has been proven by two real modules, bidirectional data and multi-module swiping, rather than rendered once. You cannot honestly lock a spec on a single sighting.) With this the SDKable principle is fully discharged: the library a community builder uses for a visual module is the same one DASH's own built-in modules use.Settled early, so it is not re-opened here:
translatestays. (2026-08-14, out of 1.6.7 — Roger's ruling.) §4.6 marks it a removal candidate "if still unused" at this lock, and the 1.6.7 redesign dropped the arrow head that was using it — so a careless reading of the working tree says it is unused. The test is whether it was ever proven, not whether anything happens to use it today, and it was proven at 1.6.6 against real artwork. Removing a working binding because this particular panel stopped needing it would decide on behalf of every author who has not built theirs yet. Also landing here:module-layout.md§8's control/variable rule and §9'stouchtable, both amended at 1.6.7 and both wanting a last read before the lock.Also here: the per-block retry, for USB. (Added 2026-08-13, out of 1.6.6 — Roger's call. 1.6.6's own scope was complete and verified; this is a transport problem it merely surfaced, and it belongs where the ACCESSORY SDK is written and where a considered
module-sdk.mdamendment can be made.) USB serial delivers large ACCESSORY payloads unreliably — an 88 KB panel arrives corrupt roughly two installs in five, alwayslengthOk=true crcOk=false: the right number of bytes and the wrong content. The identical bytes cross WiFi and Bluetooth perfectly, and both carry flow control and retransmission underneath where a bare UART has neither. XON/XOFF is ruled out permanently — a PNG contains0x11and0x13freely, so a software flow-control character cannot be distinguished from payload. 57600 baud was tried and put back: one failure in four against two in five is the same coin, so the cause is neither throughput nor bit time.The remedy agreed with Roger is a per-block retry. The per-block CRC already exists (
module-sdk.md§8) and is what catches this every time; what does not exist is a way to ask for one block again. The shape to design: a repair round afterINSTALL_END, not during — §8's install is explicitly a monologue ("no live data mid-handshake", and the module sits in a blocking loop streaming from flash), so DASH cannot interrupt it. Instead the monologue completes untouched, DASH notes which blocks failed their CRC rather than aborting on the first, and then asks for them with one new message (RESEND|id|name) while the module is back in its normal loop and listening. Bounded attempts, then fail as today.Two properties make it worth doing this way: §8's monologue rule does not change at all, only its abort-on-mismatch clause; and a module that never implements
RESENDsimply does not answer, so DASH times out and behaves exactly as it does now — purely additive, degrading to today's behaviour, which is the browser rule applied to the protocol. It also re-sends 8 KB rather than 88 KB, which is the whole argument for per-block over whole-install retry.module-sdk.mdis locked, so this is a considered amendment requiring Roger's express decision — nothing is to be changed there before that conversation. Until it lands, USB is sound for line traffic (SYSTEM / LISTENER) and a coin-toss for large panel payloads. -
1.6.11 — Cleanup and finalisation of 1.6.x.
Module panel behaviour, gathered in the Module Manager. (Note added 2026-08-19 — Roger's call. A marker for work to be done here, not a design.) By this point several modules are installed, the tab bar switches between them and the order is the user's — and the questions that were deferred while each piece was being built want answering together, in one place, rather than one per version. The Module Manager is where they belong, since that is already where a module's own record lives.
- A dominant module — the one that holds the panel by default, and the one DASH returns to.
- The panel order — begun at 1.6.8 as a plain reorder so the tabs have a sequence the user chose. It gets its proper treatment here, alongside the rest.
- How long another module stays up before the dominant one returns — a dwell, after which the panel hands itself back.
- How many layouts a module has, shown against the module's record. (Roger, 2026-08-19.) Written as the count and which of the twelve slots they are, since "three layouts" does not tell you whether you will see this module at small — the slot names do.
- And whatever else has accumulated by then. This list is expected to grow across 1.6.8–1.6.10 rather than to be complete now.
Why this is not the thing that was rejected the same day. A module count on the panel size tiles was proposed for 1.6.8 and ruled out — a module with no layout for the selected size simply is not drawn (
module-layout.md§6), that rule is the whole answer, and DASH annotating the size tiles would be DASH narrating a choice the user made and can unmake by changing the size back. The Module Manager entry is a different thing: it is a plain fact about the module, stated where the module's own record lives, in the place a user goes to find out what a module is. DASH is not explaining its own behaviour there — it is reporting what the author shipped. This also closesmodule-layout.md's open item "how DASH tells the user a module is hidden because it lacks the selected size": it doesn't.Module panel transition speed, in Transitions settings. (Note added 2026-08-19 — Roger's call.) Switching from one module's panel to another is DASH moving its own container's contents, so it is DASH's transition and takes the user's INSTANT–CINEMATIC setting — unlike anything inside a panel, which is the module's and never touches that setting (
module-layout.md§5). It wants its own speed control, alongsideMODULE_PANEL_MOVEwhich the docking work registered at 1.6.3. The registry is self-growing, so a transition built at 1.6.8 should surface here on its own — the work at 1.6.11 is likely to be checking it is present and sensibly named rather than building it. Sits next to, but is not part of, the transitions copy-and-ordering pass parked to just before v1 sign-off.Two interactions to settle when this is designed, both already live in earlier versions: 1.6.8 stores the last-shown module and restores it on restart — with a dominant module that rule may be wrong, or may be exactly what the dominant module means, and the two cannot both be the default. And
TRIGGERalready exists in the protocol and has never been built (module-sdk.md) — a module raising its own hand to come forward is the same mechanism as a dwell handing the panel back, seen from the other end. A reversing camera that takes the panel and returns it is the case that joins them.
(2026-07-30 — two items deferred out of the inventory above, Roger's call. The Module Panel Reveal element is dropped: hide-and-reveal is panel behaviour, built at 1.6.9, and is not an element — the line above predates that decision. Stacking rule enforcement cannot complete here either, since it governs the floating module panel against the floating app launcher, and the launcher does not exist until 1.8.x; it goes with the launcher. Also settled: there is no automatic orientation flip — panel orientation is the user's, set manually, not derived from which edge it is docked to.)
Why sixth: Transport is already live so modules can actually send data. The module panel can be tested with real hardware immediately.
What it is: Formal definition and control of the application display area.
What gets built:
- Viewport boundary calculation — remaining space after system bar and persistent panels
- App window constrained to viewport bounds
- Correct inset reporting to apps — apps know where interactive safe zones are
- Flush viewport mode — square edges, no overlap, default
- Dominant viewport mode — rounded corners, elevation shadow, viewport appears to float
- Passive viewport mode — viewport extends under floating bars, bars render as overlay layer
- Frosted glass effect for Passive mode — Android 12 and above, semi-transparent fallback for older versions
- Corner radius setting for Dominant mode
- Viewport mode selector added to Settings — Appearance tab, Viewport section
Why seventh: Viewport boundaries depend on knowing where all bars and panels are. Implementing this after the system bar and module panel ensures the calculation is complete and correct.
What it is: The full app library. Tray only at this stage — favourites bar deferred to version 2.
What gets built:
- Full screen launcher tray — app grid showing all installed apps
- Recently used apps surfaced at top of grid
- Search bar — filters apps and DASH functions simultaneously
- App launch on tap — app opens in viewport
- Long press app to pin — placeholder for favourites bar, functional in version 2
- App Launcher element added to element library — grid icon, taps to open tray
- Edge swipe to reveal tray — swipe from assigned edge
- Launcher settings added to Settings — Appearance tab, Panels section
Why last in version 1: The launcher is needed for the car test but is the least foundational of the version 1 features. Everything else must work before the launcher adds meaningful value.
What it is: Element work, gathered as the final major feature of version 1. (Added 2026-07-08.)
What gets built (outline — detail to be filled when this era approaches):
- Agnostic alerts — TRIGGER.
TRIGGER|id|namerouted into a shared, origin-aware alert store (a public blackboard, mirror of howBROADCASTfills the sourceless core — the alert is readable by anything; the alerts-area element just happens to be the thing that reads it), and the mandatory alerts-area element — a placeholder since 1.3.x — wired to read that store and render the module's shipped trigger icon. Origin-aware so an alert clears when its raising module goes DORMANT or is uninstalled. A deliberately low-priority nice-to-have, which is why it lands here at the end rather than in the transport era where TRIGGER's sibling messages were routed. - Further element work to be scoped when this version is reached.
Why here: These are nice-to-have refinements, not foundations — nothing else in version 1 depends on them, so they sit at the back of the sequence. (Amended 2026-07-27: no longer literally last in the era — 1.10.x and 1.11.x now follow. It remains the last of the interface features; what follows is a platform service that was moved back to meet its hardware, and the sign-off pass.)
What it is: DASH becomes the device's location provider. (Moved here from 1.5.15 on 2026-07-27 — see the 1.5.x renumber notes. Content unchanged; only its place in the sequence moved, because the hardware to verify it does not exist yet and this project does not sign off what it cannot see working.)
What gets built:
DASH ingests a live GNSS fix from the best available source — a USB serial NMEA receiver (read over the existing transport, no ESP32 needed), a Bluetooth GNSS puck, or a native board GNSS — and publishes it to Android's own system location provider, so every app in the viewport (Google Maps, Waze, anything that asks Android for a location) gets a live position, not just DASH. This is the piece that makes navigation possible; it is a device-wide platform service, not a corner of the weather code.
Capability-detected across the tiers: Bronze (a tablet with no GNSS) feeds Android via the mock/test location provider — a one-time Developer-Options setup selecting DASH as the mock app, and Play-Integrity-safe, so banking, Maps and the rest keep working; Silver/Gold (system-app privilege, or a board with native GNSS) injects the fix without the mock flag, or lets the board's own GNSS populate Android directly. One codebase, checkCapability() decides — the App Density pattern applied to location.
DASH's own weather/location cascade consumes the same live fix, retiring 1.5.4's stale last-known-network-fix + IP-geolocation approach (new cascade order: live GNSS → device fix → IP → clock-only floor). Builds on the System › Location tab (1.5.4), which gains source selection and live-fix status.
One thing to design before building: a GNSS receiver is not a DASH module. It speaks NMEA ($GPGGA), never says HELLO, and would arrive on the same USB transport the module bus uses — where FrameAssembler would try to parse it as DASH traffic, discovery would get nothing back, and the Serial Monitor would drown in ten sentences a second. So the transport stack likely needs a raw claim: a device handed over wholesale to a non-DASH reader, excluded from discovery and from the monitor's default view. Decide that before writing the ingest.
Why here: It was designed for 1.5.15 and moved back to meet its hardware. Nothing in 1.6.x–1.9.x depends on it, so the cost of moving it is only that weather stays on the IP cascade in the meantime — approximate, but working, and still offline-safe.
(Note: hardware.md should also gain a GNSS/GPS universal-peripheral entry — it is currently absent from the Bronze peripheral-solutions list.)
What it is: The things deliberately parked until the end, gathered in one place so none of them is quietly lost. (Added 2026-07-27.)
What gets built:
- Transitions copy and text polish, and the registry reorder — both deferred from 1.5.5 until the self-growing transition list is complete, which it only is once every surface in version 1 has shipped its transitions. Deliberately not done piecemeal.
- Donations — a Support DASH section on System › About DASH: GitHub Sponsors, one link, URL plus QR, no in-app payment of any kind. Left out of 1.5.14 entirely rather than stubbed. The rule it ships under: a donation never unlocks anything — no tier, no badge, no nag, no launch reminder, one section on one screen the user chose to open. That is CLAUDE.md's no locked features, no barriers applied to the single place most likely to erode it.
- A final pass against the Version 1 Milestone below.
Why last: None of it is a foundation, and two of the three cannot be finished until everything ahead of them has shipped.
1.x.x is complete when:
- DASH boots as the system launcher on a tablet
- Density and scale are configurable and work independently
- The system bar is present with alerts area and settings button always visible
- At least one USB serial module connects, installs, and displays in the module panel
- The viewport displays a running app correctly
- Apps can be launched from the launcher tray
- Basic settings are accessible and functional
- The whole system survives daily use in a moving vehicle without critical failures
Goal: DASH running on dedicated SBC hardware as Roger's personal daily driver. Refined, polished, and genuinely pleasurable to use every day. Everything from version 1 working correctly on proper hardware, plus the features that elevate it from prototype to finished personal system.
Target hardware: Orange Pi 5 or equivalent RK3588 board. Silver tier minimum.
Feature areas planned for version 2:
Theming — MaterialTheme token system, colour and font customisation, preset system with export and import, the three viewport mode presets as complete aesthetic packages.
Advanced UI scaling — per-element size controls, zone resizing, spacer element, element vertical positioning fine-tuning.
Fully integrated settings — all settings tabs complete and functional, three column navigation fully polished, Developer tab with all tools operational.
Settings hero — a living scene on the settings-panel landing (the empty state before a category is chosen), built as a stack of swappable, data-driven layers: a time-of-day + weather sky; a user-chosen vehicle silhouette; a semi-transparent headlight/tail-light glow overlay; procedurally-drawn art-deco god-ray beams; and a glanceable HUD of weather, temperature and time. Silhouettes ship as SDKable packs — a body layer, a lights overlay at partial opacity that fades in when lit, an orientation (side / front / rear, so the user matches their content box's aspect ratio), and lamp anchor points from which DASH draws the beams generically rather than baking them into the art (front = white beams, rear = red wash, side = the cinematic rake). The silhouette fits any box shape by preserving its aspect ratio, anchored to a ground line with the sky behind. Weather via Open-Meteo (free, no API key), location- and network-gated and degrading gracefully to time-of-day alone (which is offline and always works — DASH never hard-depends on connectivity). Staged: the offline, stylised, time-aware version first (silhouette + sky + god-rays + clock, zero dependencies), then the weather layer, then — the payoff — driving the lights from the real vehicle's headlights system message over the transport layer, so the settings hero reflects the actual car's state. Community can add silhouette packs and sky themes, exactly as with any other SDK surface. (Design worked out 2026-07-20 during the 1.5.2 shell build, when the empty settings landing looked bare; parked to v2, where weather, location and the live vehicle signal all belong.)
Module setup helper — a Developer/Serial Monitor button that emits a paste-ready arduino_secrets.h block (WiFi SSID/password, DASH host IP and port) so provisioning a WiFi module is copy-and-paste rather than hunting for the tablet's address. Capability-detected in the DASH way: the IP and port are always readable, but reading the device's own hotspot SSID/password needs a system-level permission Android denies a sideloaded (Bronze) app — so the credential auto-fill unlocks here on the system-app production hardware, degrading to placeholders where it can't. (Parked here from 1.4.11, when the idea came up during the first on-hardware WiFi module setup.)
App favourites bar — pinned app slots, icon count drives bar dimensions, empty slot placeholders, swipe from favourites to full tray.
Viewport Dominant and Passive modes — polished implementation, frosted glass effects, corner radius controls.
Advanced transparency effects — glass system bar, semi-transparent panels, the full Passive aesthetic.
Overlay system — transient and notification overlays, overlay SDK, notification interception via NotificationListenerService, overlay trigger mapping.
Now Playing element — MediaSession integration, full transport controls, album art.
Additional elements — clock and date variants, volume overlay trigger, connectivity status.
System message relay — modules subscribing to system messages, DASH relaying to subscribed modules.
Goal: Expanding DASH toward advanced automotive integration and community platform capability. Features that add depth and power over time rather than features needed for a working system. Polish and capability rather than foundation.
Feature areas planned for version 3:
Camera integration — reverse camera overlay, MIPI CSI pipeline on supported boards, surround view groundwork.
CAN integration — CAN sniffing service, OBD2 polling, standard signal slot population from vehicle data.
CAN learning tool — guided in-car signal identification, rolling CAN logger, community vehicle profile sharing.
CAN patch bay — user-configurable mapping of CAN signals to DASH system calls.
Element SDK — formal SDK extraction from built-in element codebase, documentation, examples, community element support.
Overlay SDK — formal SDK extraction, documentation, examples, community overlay support.
Advanced module features — HYBRID module type full implementation, source dominance for system messages, module version management.
Community infrastructure — vehicle profile database, theme sharing, element and overlay community library.
Advanced settings — ambient mode, granular notification control, driving mode rules, per-app audio permissions.
| Document | Purpose |
|---|---|
| hardware.md | Board selection, hardware tiers, peripheral requirements |
| transport.md | Module protocol, transport types, message definitions |
| interface.md | Interface architecture, elements, overlays, settings |
| changelog.md | Version-by-version record of what was built, what broke, what was fixed |
Every version increment requires a changelog entry. No version number advances without the corresponding changelog.md entry being written. This applies to third number refinements as much as major feature implementations.
Every built-in element and overlay must be built as if it were a community SDK component. No built-in component gets special internal access that a community developer could not have. If a built-in component needs internal DASH state that the SDK does not expose, that is a signal to extend the SDK interface — not a reason to make an exception. This discipline ensures that when the Element SDK and Overlay SDK are extracted in version 3, they are complete and genuinely capable rather than second class.
Features are considered complete when they are stable under real world use — not when they initially work on a bench. The third number refinement cycle exists precisely to close the gap between initial implementation and genuine stability. A feature is not done until daily driving confirms it.
This document is the authoritative development plan for DASH. Read alongside changelog.md for a complete picture of planned and actual progress.