Material 3 blue seed, light, drawn at 390×844 against the real widgets. Wire: 0xC5 as of meshcore-firmware 3e788e2a (#1801, branch epic/1788-client-settings, running on QCC2), read 2026-10-25 in OffbandConfigProtocol.h; badge screens read in BadgeScreens.cpp, BadgeLayout.h, TimeZones.h, BatteryGauge.h at the same commit. Supersedes the #970 slot mockup sent 2026-10-24. Values are examples; the bullets are the behavior to build. Built under #970, #971, #986 and #987. APPROVED 2026-10-25
| Badge screen and row | Wire (0xC5 sub-code, caps byte 2) | App before | App after this |
|---|---|---|---|
| Tones › Sound (All, Self, None) | 0x01/0x02, 0x01 | Buzzer and tones | unchanged (frame 2) |
| Tones › DM, Channel, Room, New contact, Ack, Sent | 0x0F/0x10 EVTONE, 0x10 | missing | six rows + picker (frames 2, 4) |
| Each picker: Silent, Chime, Kerplop, Tick, Bloop, Drop, Knock, filled slots | same "picker's rule" on every set | missing | one shared picker (frames 4, 7) |
| Picker plays each tune as you move | 0x11 TUNE_PLAY, 0x10 | missing | play button per row (frames 3, 4, 5) |
| Tones › Custom: all eight slots, named or "(empty)" | 0x09 SLOT_GET, 0x10 | missing | slot list (frame 3) |
| Custom › edit a slot (keyboard) | 0x12 SLOT_READ + 0x0A SLOT_SET, 0x10 | missing | editor, prefilled (frame 6) |
| Channel's own tone, with Default | 0x0B/0x0C CHTUNE, 0x10 | codec built, no screen | "Radio tone" in the channel sheet (frame 7) |
| Settings › Nav text, Message text | 0x13 GET, 0x14 TEXT_SET, 0x40 | missing | Screen page (frame 8) |
| Settings › Screen off (15 s, 30 s, 1 m, 2 m, 5 m) | 0x15 SCROFF_SET, 0x40 | missing | Screen page (frame 8) |
| Settings › Time zone (40 zones plus "not set", which shows ages) | 0x16 TZ_SET, 0x40 | missing | zone list (frame 9) |
| Settings › Battery: 100% point, Set 100% here, Back to auto | 0x17 BATT_SET, 0x40 | missing | Battery page (frames 10, 11) |
| Settings › GPS | 0x0D/0x0E, 0x20 | Location (#977) | unchanged |
| Settings › Hibernate, Device pages | not settings (AmberForge 1350): an action and a navigation | n/a | n/a |
One caps change to carry into the code already written: 0x10 is now TUNES (slots, channel tone, event tones, preview), not channel tone alone, and the slot sub-codes moved from the buzzer bit to it. A radio with a buzzer but no tune engine advertises 0x01 without 0x10 and keeps today's scope-only screen.
The badge's own hardware settings sit with Button actions, gated on 0x40. A radio without it shows today's card.
0x40. Today that means firmware built with the Offband screen set; a display fitted to a pyramid badge with that firmware gets them.SCREEN_GET reply: the 100% point and where it came from, in the badge's words (default, learned, set).The badge's Tones screen, in its order: Sound first, because nothing below can sound while it says None.
kToneEventRows): DM, Channel, Room, New contact, Ack, Sent. DM is one tone for every direct message; there is no per-contact choice (#983 withdrawn).effective byte from EVTONE_GET. When stored is 0xFF it adds "(radio default)", so a choice and a default never look alike.0x10. With 0x01 alone the screen is today's scope rows.Scrolled down. All eight slots are rows, as on the badge: an empty slot is where you go to fill one.
SLOT_GET replies, one per slot, read when the screen opens.TUNE_PLAY with the slot's id (8 + slot). Empty slots have none: there is nothing to hear.One picker for all six events, with the badge's list. Play auditions on the radio; tapping the name chooses it.
EVTONE_SET; the dialog closes and the row changes only on the radio's reply, which carries stored and effective.The radio says why it refused. The app says it in words, as the badge does with "Buzzer: OFF".
TUNE_PLAY result 2 (sound off): this message, which stays until OK.The editor reads the stored tune back first, so a filled slot opens with its text and can be changed, not only replaced.
SLOT_READ at offset 0, then again at offset + bytes while more is 1. The field is read-only and shows this bar until the read completes.rtttlProblem() and its words ("b= must be 1 or more"); Save stays disabled while there is a problem. Saving is the chunked SLOT_SET, then a re-read.The channel sheet's "Radio tone" picker (#971), now able to say what Default plays, the way the badge shows "Default Kerplop".
effective tune from EVTONE_GET ev 1, so it names a real tune rather than "follows the radio".3e788e2a), which settles the first #971 question.The badge's Nav text, Message text and Screen off rows, with the same choices and no others.
SCREEN_GET reply; each change sends its SET and the controls redraw from the 14-byte re-read reply, never from the tap.scr_stored is 0 (compiled default), no segment is selected and the line reads "Board default: 1 m" from scr_secs; picking a step replaces it and there is no way back to 0, on the badge or here.The badge's 40 zones, its "not set", and a suggestion: the badge suggests from GPS, the app from the phone.
TimeZones.h kZones, in its order. AmberForge confirms it is append-only, so the index is stable; an index the app does not know is shown by number.TZ_SET like any row.The badge's Battery screen: where 100% is, how it got there, and its two actions.
batt_stored, batt_full and batt_pinned bytes; the reading and percent are the battery report the app already receives.BATT_SET 1: the radio pins its own current reading, as the badge does. A confirm says "Only do this with the radio fully charged."BATT_SET 0, shown only when something is learned or pinned.The radio refuses a reading that is not a plausible full cell, and says which way it is wrong.
ERR_BATT_NOT_CHARGED (9): this message. ERR_BATT_TOO_HIGH (8): "The reading is higher than a full cell, so it was not used."_ErrorBanner: it stays until dismissed.They are this radio's own hardware settings, beside Button actions, and they appear only on radios that have them. The rejected alternative was a new top-level "Device display" category, which would be empty on most radios.
The badge auditions as you move because moving and choosing are separate keys. On a phone, a tap is the choice, so play is its own button and the name chooses. A preview never changes the setting.
Firmware refuses 0xFF on an event because the badge's picker has no Default row for events, and Screen off accepts only its five steps. The app matches that. A way back would be a firmware change.
The badge says "Nav text" and "Message text", which fit 21 columns. The app has room to say what each governs.
Every reply on this list re-reads after the write: EVTONE and CHTUNE carry stored and effective, SCREEN carries the whole set. The app draws only from replies, stores none of it, and so has nothing per radio to isolate (#853) or to drift.
Firmware refuses anything its own screens would not offer. Building each list from the same rule (named built-ins plus filled slots, five Screen off steps, three sizes, the zone table) means the app never offers a choice the radio will reject.