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 §11 intomodule-sdk.md. Build it once the module panel (1.6.x) has rendered a real panel and the layout format is settled. -
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 — horizontal swipe for horizontal panel, vertical for vertical
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.
- 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.
- 1.6.6 — Redrawing that module's layout onto the panel. What the module sends is what the panel draws.
- 1.6.7 — Data between the two devices. Buttons wired up, and both ends talking in the way they are meant to.
- 1.6.8 — Multi-module. A second board over WiFi sending a different layout, and swiping between the two accessory modules.
- 1.6.9 — Hiding and revealing the module panel.
- 1.6.10 — The module SDK — how to write a module. This is where arduino.md §11 locks into
module-sdk.md, 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. - 1.6.11 — Cleanup and finalisation of 1.6.x.
(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.