<ahref="https://github.com/meshcore-dev/meshcore/edit/main/docs/development/roadmap.md"title="Edit this page"class="md-content__button md-icon"rel="edit">
<h3id="mark-all-read-at-type-level">✅ Mark-all-read at type level</h3>
<p>Hold Enter on the MESSAGE mode-select screen (DM / Channels / Rooms) opens a 1-item context menu "Mark all read". Acts on the currently highlighted mode and shows a brief confirmation alert.</p>
<p>Implementation:
- New <code>UITask::clearAllDMUnread()</code> — <code>memset</code> over <code>_dm_unread_table</code>
then recent DM contacts deduped; selecting a contact that's already pinned
elsewhere moves it to the new slot)</p>
<p><strong>Follow-up done</strong>: OLED unread-badge overlap fixed — badge and name share the
same baseline, drawTextEllipsized's max width subtracts badge width + a 3 px
gap so names shorten to "Nam…" before the digit.</p>
<p>A 2×3 grid (six slots) of pinned contacts on its own home page, between Clock and Messages. Joystick picks a tile, Enter opens the existing DM conversation or sends a pre-set quick reply.</p>
<p>Data model:
- New field in NodePrefs: <code>uint8_t favourite_contacts[6][6]</code> — first 6 bytes of each contact's <code>pub_key</code> (enough to disambiguate locally)
- Lookup at render time: walk contacts, match prefix, render name + unread badge
- Empty slot renders as "+" placeholder; Enter on empty opens a contact picker (existing UI)</p>
<p>Pinning UX:
- In QuickMsg DM list, long-press on a contact → context menu → "Pin to dial" → asks which of the 6 slots
- Unpin via the same menu (only shown when contact is already pinned)</p>
<p>Schema bump: add <code>favourite_contacts</code> to NodePrefs, bump <code>SCHEMA_SENTINEL</code> low byte.</p>
<p>Render layout (250×122 landscape e-ink):</p>
<pre><code>╔══════════════════════════════╗
║ Favourites ║
╠══════════════════════════════╣
║ ┌──────┐ ┌──────┐ ┌──────┐ ║
║ │Alice │ │Bob 3 │ │ + │ ║
║ └──────┘ └──────┘ └──────┘ ║
║ ┌──────┐ ┌──────┐ ┌──────┐ ║
║ │Carol │ │ + │ │ + │ ║
║ └──────┘ └──────┘ └──────┘ ║
╚══════════════════════════════╝
</code></pre>
<p>Joystick navigation is natural with 6 tiles (UP/DOWN between rows, LEFT/RIGHT within row).</p>
<h3id="gps-trail-renamed-from-breadcrumb">✅ GPS trail (renamed from breadcrumb)</h3>
<p>Phase 1 ✅ (storage + sampling + Summary view + G indicator in status bar)
Export saved entries. Single flash slot at /trail (binary header with
magic+version+count+accumulated_ms then raw TrailPoint records). GPX 1.1
dump goes over USB Serial; "Export GPX" streams the live RAM ring,
"Export saved" streams the flash file straight to USB without touching
the live ring. Segments respect SEG_START boundaries. Alert reflects
BLE-app-collision state)
Phase 5 ✅ (Settings + actions consolidated into a single Hold-Enter popup —
Min dist + Units cycled with LEFT/RIGHT (popup stays open), plus
Start/Stop tracking and Reset action items. Short Enter never toggles —
both start and stop go through the popup, so a stray tap can never change
tracking state. View counter (N/3) lives in the title bar; the bottom hint
row is gone, content fills the freed space. Sampling cadence fixed at 1 s,
GPS upd setting also removed; both rely on the sensor manager's defaults)
Polish ✅ (map view: filled/open dot markers around segment breaks;
"Waiting for GPS fix" status when started without a lock;
capacity bumped to 512 points; elapsed/avg-speed run on millis() instead
of RTC so they tick even before GPS time is synced)</p>
<p>Tools › Breadcrumb. Periodically samples <code>(lat, lon, ts)</code> into a RAM ring buffer; user explicitly saves snapshots to flash.</p>
<p><strong>Logging is a runtime state</strong>, not a settings value. User starts/stops from the
Tools › Breadcrumb screen. Once active, sampling continues in the background
regardless of which screen is shown, and a <code>G</code> indicator appears in the status
bar (analogous to <code>A</code> for auto-advert). A reboot resets the active state to
off; the RAM trail is also lost on reboot unless saved to a flash slot first.</p>
<p>Settings only control sampling cadence and the min-distance gate — they don't
enable/disable the feature.</p>
<p><strong>Storage model — RAM ring with explicit save</strong></p>
<p>Rationale: auto-off only blanks the display, the firmware keeps running, so the RAM trail survives every idle scenario. Typical use is a single trip start→stop while wearing the device; persisting across reboots is rarely wanted. RAM-only avoids ~1400 flash writes/day and the LittleFS wear that comes with continuous logging.</p>
<ul>
<li>Live ring: <code>BreadcrumbEntry[BC_RAM_CAP]</code> in <code>UITask</code> (or a dedicated component). Each entry <code>int32_t lat_1e6, int32_t lon_1e6, uint32_t ts</code> = 12 B. Cap = 256 → 3 KB RAM. nRF52840 (256 KB RAM) has plenty of headroom.</li>
<li>Wrap-on-write: oldest entry replaced when buffer full.</li>
<li>Reboot wipes the live trail (intentional; matches the "this trip" model).</li>
</ul>
<p><strong>Snapshot slots on flash</strong> (user-initiated only):
- <code>/breadcrumb.0</code>, <code>/breadcrumb.1</code>, <code>/breadcrumb.2</code> — three named slots
- Each file: small header (count, start_ts, end_ts, total_distance_m) + entry array
- Written only on explicit "Save trail" action — zero background writes, zero wear concern
- Optional: auto-save to slot 0 on detected low-battery shutdown (single write before going dark)</p>
<p>UI screens (LEFT/RIGHT cycles):
1. <strong>Summary</strong> — total distance (km), elapsed time (h:mm), point count, current speed (from last 2 samples), GPS fix indicator
2. <strong>Trail map</strong> — ASCII bounding-box plot. Auto-fit the polygon, current position marked <code>X</code>, start marked <code>*</code>. UP/DOWN zoom, LEFT/RIGHT pan when zoomed.
3. <strong>Last N entries list</strong> — scroll through recent points with timestamp + delta from previous.</p>
<p>Joystick actions:
- Enter → toggle live logging on/off (status bar shows <code>*</code> when active, like auto-advert <code>A</code>)
<p>Schema impact: new prefs fields <code>uint8_t breadcrumb_interval_idx</code>, <code>uint8_t breadcrumb_min_delta_idx</code>. Sentinel bump. The slot files are separate from prefs.</p>
<p>Edge cases:
- No GPS fix: skip sampling, status indicator dims
- Low-batt shutdown: optional auto-save to slot 0 (one write) before powerdown
- Memory: 3 KB RAM is negligible on this MCU; if RAM ever tightens, drop to 128 entries</p>
<h3id="waypoints-navigation-cluster-mark-a-spot-navigate-back">✅ Waypoints + navigation cluster — mark a spot, navigate back</h3>
<p><strong>✅ Shipped</strong> (branch <code>feat/waypoints-nav</code>). The whole navigation suite landed. Notable deltas from the original spec that follows:</p>
<ul>
<li><strong>Waypoints</strong> — Mark here / list / Rename / Delete / <strong>Clear waypoints</strong> / <strong>Send</strong>; stored in <code>/waypoints</code> (16 max), independent of trail recording and kept across Reset trail.</li>
<li><strong>Map</strong> — waypoint marker shows the <strong>first two</strong> label chars (not one), placed edge-aware so it stays on-map. With a trail the view frames the recorded route and clamps far waypoints to the nearest edge; with no trail it auto-fits to waypoints + live position. Degenerate single-point case handled.</li>
<li><strong>Shared NavView</strong> — one <code>navview::draw(...)</code> reused by waypoints, Trail-start backtrack, Nearby-node nav and message-location nav. Shows distance + <code>To:</code> + <code>Hdg:</code> (two absolute bearings), honouring the global Units setting.</li>
<li><strong>COG ring in UITask</strong> — heading source decoupled from trail logging, time-sampled with gross-error rejection + min-displacement gate; restarts after a >15 s GPS gap so a reacquired fix can't imply a teleport heading.</li>
<li><strong>Standalone Compass</strong> (Tools › Compass) — heading-up <strong>scrolling tape</strong> with a fixed travel-direction pointer + large degrees/cardinal readout. (A north-up circular dial was tried first and dropped — only a few-px needle fits the OLED's vertical space.)</li>
<li><strong>Global Units</strong> (Settings › System: Metric/Imperial) — drives every distance/speed in Tools, the min-distance gate, and the map scale-bar. New <code>units_imperial</code> + <code>trail_show_pace</code> prefs (schema 0xC0DE0006); the old combined <code>trail_units_idx</code> retired.</li>
<li><strong>Location over mesh</strong> — Waypoints list → <strong>Send</strong> shares <code>[WAY]lat,lon label</code>; a received location ({loc} text or a [WAY] share) offers <strong>Navigate / Save waypoint</strong> from both the message list row and the fullscreen view. Backed by a shared <code>geo::parseLatLon</code>.</li>
</ul>
<p>Shared helpers extracted: <code>geo::</code> (haversineKm/bearingDeg/bearingCardinal/fmtDist/parseLatLon) in <code>GeoUtils.h</code>; 1-px <code>gfx::drawLine/drawCircle</code> in <code>GfxUtils.h</code>; one <code>UITask::currentLocation()</code> GPS accessor.</p>
<p>The original design spec is kept below as a record.</p>
<hr/>
<p>Turns Solo from a comms device into a basic GPS navigator. Mark the current
position with a short label, then later get bearing + distance back to it —
ideal for off-grid use (car, camp, trailhead, water source).</p>
<p><strong>Storage</strong> — dedicated flash file <code>/waypoints</code> (separate from prefs, like
<code>/trail</code>). Fixed table, no schema-sentinel impact:</p>
<pre><code>struct Waypoint {
int32_t lat_1e6, lon_1e6; // saved fix
uint32_t ts; // when marked (RTC)
char label[12]; // short name, NUL-terminated ("CAR", "CAMP", "H2O"…)
};
static const int WAYPOINT_MAX = 16; // 16 × 24 B = 384 B file
</code></pre>
<p><strong>Marking</strong> — from the GPS or Trail screen: Hold Enter → "Mark here".
- Requires a GPS fix; otherwise alert "No GPS fix".
- Opens the existing <code>KeyboardWidget</code> to type the label (≤11 chars). Empty
input auto-labels <code>WP<n></code>.
- Saves the current fix + label, appends to the file.</p>
<p><strong>Visible on the trail Map</strong> — waypoints render on the existing Map view as a
distinct marker (e.g. a hollow diamond or a small flag) so they show in
context with the recorded track:
- Fold waypoint coords into the map's bounding box so off-track waypoints
stay in frame. Today <code>renderMap()</code> derives the box from
<code>TrailStore::boundingBox()</code>; extend it to also span the waypoint table
(and handle the "waypoints but empty trail" case — map still renders).
- Generalise the <code>project()</code> lambda to take raw (lat, lon) instead of a
<code>TrailPoint&</code> so the same projection draws both track points and waypoints.
- Marker shows the label's first character beside it when there's room
(122 px is tight with many waypoints); the full label lives in the list /
nav view.</p>
<p><strong>Trail workflow integration</strong> — waypoints live <em>inside</em> the Trail screen, not
a separate Tools entry, because marking points of interest happens while you
are recording:
- <strong>Mark</strong>: Trail → Hold Enter → "Mark here" (new action-menu row). Opens the
keyboard for the label, saves the current fix. Works whether or not tracking
is active — a waypoint is independent of trail recording state.
- <strong>Manage / navigate</strong>: Trail → Hold Enter → "Waypoints" → a PopupMenu list
of saved waypoints (label + distance). Selecting one:
- Enter → fullscreen nav (see below).
- Hold Enter → Rename / Delete (later: <strong>Share over mesh</strong>).
<code>0xC0DE000F</code> (suppress-dup), <code>0xC0DE0010</code> (radio profile), with stray-byte
clamps for upgraders.</li>
<li><strong>Open question:</strong> the app-side dedicated-band gate in <code>CMD_SET_RADIO_PARAMS</code>
was commented out (not deleted) to match the on-device toggle's any-frequency
behaviour — undecided whether that gate was UX-only or regulatory.</li>
<li><strong>Not done:</strong> live two-device mesh verification (A→repeater→C); counters make
it observable but don't replace the field test.</li>
</ul>
<h3id="sos-broadcast">SOS broadcast</h3>
<p>Configurable in Settings › System › SOS:
- Target: channel index or DM contact
- Message template (uses placeholders)</p>
<p>Trigger: Hold Back + Hold Enter for 3 s on any screen → confirmation popup ("Send SOS?") → Enter to send. Sends with <code>{loc}</code> and <code>{batt}</code> filled. 30 s cooldown.</p>
<h3id="range-test-shipped-as-nearby-nodes-ping">✅ Range test — shipped as Nearby Nodes ping</h3>
<p>The practical need is covered by <strong>ping</strong> rather than a dedicated screen: in
Nearby Nodes, a node's detail view → <strong>Hold Enter → Ping</strong> sends a direct mesh
ping and shows RTT + SNR (own and remote), repeatable on demand. Available from
both the stored-node detail and the active-discovery detail.</p>
<p>The original idea below (a Tools › Range Test screen with continuous 5 s
pinging and a 30-sample sparkline) was <strong>not</strong> built — kept as a possible
future enhancement on top of the existing ping.</p>
<p>Tools › Range Test:
- Pick a node from contacts/nearby
- Enter starts pinging every 5 s, logs RTT + RSSI + SNR (ring ~30)
- Display shows current values + 30-sample sparkline (block characters)
- Enter stops; Hold Enter for context menu (reset, change target)</p>
<h3id="quiet-hours">Quiet hours</h3>
<p>Settings › Sound › Quiet Hours:
- Enable on/off
- Start HH (LEFT/RIGHT to change, 24 h)
- End HH</p>
<p>When within window: buzzer set to "off" (overrides setting), display brightness → 0. Restores prefs values when window ends. Time source: rtc_clock.</p>
<h3id="channel-scanner-home-page">Channel scanner home page</h3>
<p>Toggleable in Settings › Home Pages. Lists channels with: name, unread count, last message age. Enter opens the channel. Sort by recency by default; LEFT/RIGHT toggles to alphabetical.</p>
<p>QuickMsg DM list: a 4-th sort mode (currently sorted by message count). LEFT/RIGHT on the list header cycles: name | message-count | recency | distance. Distance uses GPS pos from contact's last advert.</p>
<p>Read-only. UP/DOWN switches between metrics. Bottom shows current value as text.</p>
<h3id="auto-reply-query-commands-with-live-data">✅ Auto-reply query commands with live data</h3>
<p>Realised as a <strong>command bot</strong> rather than a trigger/reply table: with <strong>Commands</strong> ON, a DM is scanned for <code>!word</code> tokens and answered with live node data via <code>expandMsg</code> — <code>!batt</code>/<code>!loc</code>/<code>!time</code>/<code>!temp</code>/<code>!status</code>/<code>!ping</code>/<code>!help</code>, plus <code>!hops</code> (per-message hop count via <code>getPathHashCount()</code>, <code>direct</code> if heard directly). Multiple commands in one message are merged into a single <code>|</code>-joined reply (one transmission/throttle/counter tick) via the shared <code>botScanCommands</code>. Works in DMs (per-contact throttle, ignores quiet hours — a pull) and on the bot's <strong>monitored channel</strong> (broadcast: per-channel cooldown, respects quiet hours). Toggled independently of the trigger bot. See <ahref="examples/companion_radio/MyMeshBot.h"><code>MyMeshBot.h</code></a><code>tryBotCommand</code> / <code>tryBotChannelCommand</code> / <code>botCommandReply</code>.</p>
<p>Settings › System › Batt Calibration: edit 5 voltage breakpoints used to convert mV → %. UP/DOWN selects breakpoint, LEFT/RIGHT changes voltage in 50 mV steps. Helps users with non-standard LiPos report accurate %.</p>
<h3id="mark-read-at-type-level-done-see-mark-all-read-at-type-level-above">✅ Mark-read at type level — done (see "Mark-all-read at type level" above)</h3>
<h3id="display-test-pattern">Display test pattern</h3>
<p>Tools › Display Test: full-screen grid + bars + Lemon glyph dump. Useful for verifying driver/font changes after flashing.</p>
<p>From channel view, Hold Enter → "Who's online?". Sends 0-hop discovery to channel members, collects responses for 10 s, shows a list with RSSI. Similar to existing Nearby active discovery but scoped to a channel.</p>
<li><strong>Favourites dial</strong> — new home page + new prefs field; touches familiar areas (QuickMsg context menu, NodePrefs, schema sentinel bump)</li>
<li><strong>GPS breadcrumb</strong> — largest of the three; introduces a new flash file and a Tools sub-screen with multiple views</li>
</ol>
<p>After #3, re-prioritise the backlog with the user.</p>
<hr/>
<h1id="code-audit-known-bugs-hardening-backlog">Code audit — known bugs / hardening backlog</h1>
<p>Pass through wio-unified after commit <code>321d769e</code>. Grouped by severity. Listed but <strong>not yet fixed</strong>.</p>
<h2id="critical">Critical</h2>
<h3id="onchannelmessagerecv-onchanneldatarecv-guard-for-findchannelidx-1">✅ <code>onChannelMessageRecv</code> / <code>onChannelDataRecv</code> — guard for <code>findChannelIdx == -1</code></h3>
<p>Defensive <code>if (ch_idx >= MAX_GROUP_CHANNELS) return;</code> at function entry — prevents ring-buffer pollution in case any future caller forgets the upstream guard. With C1 fixed this should never trigger, but the cost is zero.</p>
<h2id="high">High</h2>
<h3id="findchannelidx-scans-all-zero-secret-in-uninitialised-slots">✅ <code>findChannelIdx</code> scans all-zero secret in uninitialised slots</h3>
<p>Fixed (local override): <code>findChannelIdx()</code> now returns <code>-1</code> immediately when the queried secret is all-zero, so a corrupted/empty channel can't match an unused all-zero slot. Complements the load-side skip already in <code>loadChannels()</code>.</p>
<h3id="savechannels-writes-all-40-slots-to-channels2">✅ <code>saveChannels</code> writes all 40 slots to <code>/channels2</code></h3>
<p>Fixed: the save loop now skips unused slots (all-zero secret) instead of writing every slot up to <code>MAX_GROUP_CHANNELS</code>, so the file holds only the channels actually configured (was always ~2.7 KB). <code>loadChannels()</code> already compacted empty entries on read, so the loaded result is unchanged — only on-flash size and write wear drop.</p>
<h3id="msgread0-wipes-the-whole-dm-unread-table">📋 <code>msgRead(0)</code> wipes the whole DM unread table</h3>
<p>When the companion app reads the last message from the offline queue, all on-device badges disappear. Previously discussed and a fix was reverted as "intended sync behaviour" — keep as known limitation; document or restrict to "Favourites Dial badges only".</p>
<h3id="message-buffers-sized-below-the-protocol-maximum-clipped-long-messages">✅ Message buffers sized below the protocol maximum (clipped long messages)</h3>
<p><code>ChHistEntry::text</code> was 140 B and <code>DmHistEntry::text</code> only 80 B, while the
keyboard capped input at 139 B — all below MeshCore's <code>MAX_TEXT_LEN</code> (160 B).
Channel messages embed the sender as <code>"Name: body"</code> in the payload, so the
prefix ate into the 140 and clipped the tail; DMs over ~80 B were cut outright;
and Polish text (2 bytes per accented char) roughly halved the visible limit.
Fixed: history + fullscreen/preview copies sized to <code>MAX_TEXT_LEN + 1</code>, keyboard
cap raised to 160 with per-field maxima kept on the smaller stores (custom_msgs,
bot reply). Full-length messages now compose, send, store and display intact.</p>
<h3id="loadprefsint-scopes-trail_units_idx-reset-to-the-0xc0de0003-jump">✅ <code>loadPrefsInt</code> scopes <code>trail_units_idx</code> reset to the 0xC0DE0003 jump</h3>
<p>The reset is now gated on <code>sentinel == 0xC0DE0003</code> so newer mismatches (e.g. 0xC0DE0004 → 0xC0DE0005, which both saved the field correctly) no longer clobber the user's choice.</p>
<h2id="medium">Medium</h2>
<h3id="cmd_set_default_flood_scope-off-by-one-not-a-bug">❌ <code>CMD_SET_DEFAULT_FLOOD_SCOPE</code> off-by-one — not a bug</h3>
<p>Re-checked: <code>default_scope_name</code> is declared <code>char[31]</code> (not 32), so <code>n < 31</code> correctly admits the maximum 30-character string + NUL. The audit entry was a misread.</p>
<h3id="strlen-on-cmd_frame-without-null-termination-replaced-with-strnlen">✅ <code>strlen</code> on <code>cmd_frame</code> without null-termination — replaced with <code>strnlen</code></h3>
<p>The 31-byte name slot in <code>CMD_SET_DEFAULT_FLOOD_SCOPE</code> doesn't have to be NUL-terminated by the sender. Switched to <code>strnlen(…, 31)</code> so the search can't run past the field into the 16-byte key (or beyond the frame).</p>
<h3id="popupmenu_cap-updated-only-in-render">📋 <code>PopupMenu._cap</code> updated only in <code>render()</code></h3>
<p>Left as-is: the framework always renders before forwarding input, so the fragile invariant doesn't fire in practice. Worth a refactor only if the call order ever changes.</p>
<h3id="scan-detail-guarded-against-narrow-displays">✅ Scan detail guarded against narrow displays</h3>
<p>Pub-key line is skipped entirely when <code>max_chars < 4</code> instead of feeding a negative length to <code>strncpy</code>. (Lived in <code>renderDiscoverDetail</code> before the one-list refactor; now in <code>renderScanDetail</code>.)</p>
<h3id="expandmsg-gps-validity-test-treats-0-0-as-invalid">📋 <code>expandMsg</code> GPS validity test treats (0, 0) as invalid</h3>
<p>Scan detail view (<code>SNR: %.1f dB</code>, <code>Rem: %.1f dB</code>) and the ping popup keep the 0.25 dB resolution. (After the one-list refactor the scan list cards show <strong>RSSI</strong> in the right column, not SNR.)</p>
<h3id="trail-_count-cast-to-uint16_t">✅ Trail <code>_count</code> cast to <code>uint16_t</code></h3>
<p>A <code>static_assert(CAPACITY <= 0xFFFF, …)</code> next to the CAPACITY definition now fails the build if it is ever grown past what the uint16_t save-header count can hold, instead of silently truncating. Safe today (CAPACITY=512).</p>
<h3id="bot-strstr-on-truncated-199-char-buffer-not-reachable">✅ Bot <code>strstr</code> on truncated 199-char buffer — not reachable</h3>
<p>Re-checked: <code>BOT_SCRATCH</code> is 200 and <code>MAX_TEXT_LEN</code> is 160, so an incoming message never reaches the 199-char truncation point — the scratch buffer (used by the centralised <code>botTriggerMatches()</code>) always holds the whole message. No fix needed.</p>
<h3id="strncpy-buf-sizeofbuf-replaced-with-strcpy">✅ <code>strncpy("?", buf, sizeof(buf))</code> replaced with <code>strcpy</code></h3>
<p>Re-checked: <code>rlen</code> is clamped to 20 before building <code>"RE:" + nick</code>, so the title is ≤23 chars and fits <code>title[24]</code> with no overflow. A nick longer than 20 chars is shown truncated, but that's an intentional fit-to-header limit (the OLED header only fits ~21 chars anyway), not a bug.</p>
<h2id="priority-for-merge">Priority for merge</h2>
<p>Fix status after this pass:</p>
<ul>
<li>✅ C1 + C2 — <code>findChannelIdx == -1</code> guarded at both channel-recv paths; <code>addChannelMsg</code> defends against bogus index</li>
<li>✅ H4 — <code>trail_units_idx</code> reset scoped to the 0xC0DE0003 jump</li>
<li>✅ M2 — <code>strnlen</code> instead of <code>strlen</code> on default scope name</li>
<li>✅ M4 — <code>renderDiscoverDetail</code> skips pub-key line on very narrow displays</li>
<li>✅ L1 — SNR shown with 0.25 dB precision everywhere</li>
<li>✅ L4 — fallback <code>"?"</code> sender no longer memsets through <code>strncpy</code></li>
<li>✅ Trail map grid silent loss — superseded by the Trail refactor: <code>renderGrid</code> now picks a round labelled step (<code>1m…100km</code> / <code>10ft…100mi</code>) nearest ~1/3 of the shorter side and enforces a <code>MIN_GRID_PX</code> floor, so the grid can never silently vanish on an elongated trail. <ahref="examples/companion_radio/ui-new/TrailScreen.h#L635"><code>TrailScreen.h:635</code></a></li>
<li>❌ M1 — re-checked, not a bug (<code>default_scope_name[31]</code>)</li>
<li>📋 H1 + H2 — still open; need coordinated fix in upstream <code>BaseChatMesh</code> (<code>findChannelIdx</code> should iterate <code>num_channels</code>, not <code>MAX_GROUP_CHANNELS</code>; <code>saveChannels</code> should stop at the first uninitialised slot) or a local override</li>
<li>📋 H3 — left as known limitation pending UX call</li>
<li>📋 M3, M5, L2, L3, L6 — minor or stylistic; left in backlog</li>
<scriptid="__config"type="application/json">{"annotate":null,"base":"../..","features":["content.action.edit","content.code.copy","search.highlight","search.suggest"],"search":"../../assets/javascripts/workers/search.2c215733.min.js","tags":null,"translations":{"clipboard.copied":"Copied to clipboard","clipboard.copy":"Copy to clipboard","search.result.more.one":"1 more on this page","search.result.more.other":"# more on this page","search.result.none":"No matching documents","search.result.one":"1 matching document","search.result.other":"# matching documents","search.result.placeholder":"Type to start searching","search.result.term.missing":"Missing","select.version":"Select version"},"version":null}</script>