You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A device reports which event firmware it runs; clients turn that into branding, event info, and lifecycle nudges from a shared, over-the-air metadata source. The identity is the proto enum MyNodeInfo.firmware_edition (FirmwareEdition: VANILLA, HAMVENTION, DEFCON, β¦). The display data for each edition β name, welcome message, dates, venue, links, accent color, icon β lives off-device in GET https://api.meshtastic.org/resource/eventFirmware, with a bundled JSON snapshot as the always-available offline fallback, so a new event ships without an app release. Given the edition, every client should: surface event branding (accent + icon on the main chrome, tap β an event info sheet), auto-suppress new-node notifications while on event firmware (event meshes churn hard) and restore them on return to vanilla, and once the event's end date has passed, nudge the user back to standard firmware. The proto enum is the stable key; the API/bundled metadata is the presentation layer keyed on it.
Tracking: design#120 Β· Proto (source of truth):FirmwareEdition + MyNodeInfo.firmware_edition (firmware-owned) Β· Metadata resource:api.meshtastic.org/resource/eventFirmware + bundled event_firmware.json Β· Reference build:Meshtastic-Android#6162 Β· Prior art: Meshtastic-Apple (enum + notifications, see below)
Two data surfaces (source of truth)
1. On-device identity β the proto enum.MyNodeInfo.firmware_edition is a FirmwareEdition enum reported at config handshake. Firmware owns the value set. The first 16 values are reserved for non-event special firmwares; events follow.
Value
Name
Kind
0
VANILLA
standard (not an event)
1
SMART_CITIZEN
special (reserved <16)
16
OPEN_SAUCE
event
17
DEFCON
event
18
BURNING_MAN
event
19
HAMVENTION
event
127
DIY_EDITION
special
"Is this an event?" = edition != VANILLA. Clients must default unknown/future enum values gracefully (treat as the reported name, no branding, still non-vanilla for notification purposes).
2. Off-device presentation β the metadata resource.GET https://api.meshtastic.org/resource/eventFirmware is live and returns an envelope (version 2) keyed by the enum name, sharing the {version, generatedAt, source, <payload>[]} shape used by deviceLinks. Verbatim shape of a live edition record (Hamvention, trimmed):
{
"version": 2,
"generatedAt": "2026-06-30T00:00:00Z",
"source": "bundled",
"editions": [
{
"edition": "HAMVENTION", // MUST equal the FirmwareEdition enum name (the join key)"displayName": "Dayton Hamvention 2026",
"welcomeMessage": "Welcome to Hamvention! ππ»",
"tag": "Hamvention", // short label"eventStart": "2026-05-15", // ISO YYYY-MM-DD, in `timeZone`"eventEnd": "2026-05-17",
"timeZone": "America/New_York", // IANA id; resolves the dates to local time"location": "Xenia, Ohio, USA",
"iconUrl": "https://api.meshtastic.org/resource/eventFirmware/hamvention.png", // now hosted"accentColor":"#BF1E2E", // #RRGGBB ambient brand color"domain": "hamvention.meshtastic.org", // per-event microsite"links": [ { "label": "Event Website", "url": "https://hamvention.org" } ],
"theme": { // richer branding than accentColor alone"name": "Radio Adventure",
"tagline": "Exploring the many avenues amateur radio has to offer.",
"colors": { "primary": "#BF1E2E", "secondary": null, "accent": null },
"palette": ["#BF1E2E"],
"fonts": null
},
"firmware": { // the event's actual firmware build"slug": "dayton2026",
"version": "2.7.23.07741e6",
"id": "v2.7.23.07741e6",
"title": "Meshtastic Firmware 2.7.23.07741e6",
"zipUrl": "https://github.com/meshtastic/meshtastic.github.io/raw/master/event/dayton2026/firmware-2.7.23.07741e6.zip",
"releaseNotes": "## Welcome to Dayton Hamvention 2026! β¦"
}
}
]
}
The edition string is the join key and MUST match the proto enum name exactly. Live editions today: HAMVENTION, OPEN_SAUCE, DEFCON, BURNING_MAN. Field coverage is uneven: firmware{} is populated for HAMVENTION/OPEN_SAUCE and null for DEFCON/BURNING_MAN; a hosted iconUrl currently exists only for HAMVENTION; theme.fonts/multi-color palette are populated for DEFCON but null/single-color elsewhere. Clients must treat every sub-field as independently optional.
Minimum viable field set every client should consume: edition, displayName, welcomeMessage, eventStart/eventEnd/timeZone, location, accentColor, iconUrl, links. The richer tag / domain / theme / firmware objects are optional/progressive β clients may ignore what they don't use, but should tolerate them (don't fail on unknown keys).
firmware{} is the notable new capability: it carries the event's own build (version + zipUrl + releaseNotes), so a client can offer/flash the event firmware directly and can compare it against the connected device's reported version. Out of MVP scope but worth designing around.
Every client bundles a snapshot of this payload and ships it as the offline seed; the network copy refreshes it. (source: "bundled" above reflects the server currently serving from the same bundled snapshot.)
No separate JSON schema is maintained: the live API is the source of truth and each client's typed models are its contract. (Android carried a schemas/event_firmware.schema.json copy but it was removed β nothing validated against it and it duplicated the API + models. If a formal schema is wanted, it should live once with the API/data publisher, not per-client β see notes.)
Behavior spec (all clients)
Offline-first / stale-while-revalidate. Seed from the bundled snapshot on first run (instant, works with no network). Refresh from /resource/eventFirmware in the background and cache the result across launches. Do not impose a short network deadline β api.meshtastic.org is regularly measured at 20β60 s; a tight timeout cancels every refresh and pins users to the bundled seed. Rely on the client's normal HTTP timeout/retry. An empty/failed response must leave the cache intact (never wipe to blank).
Resolve on connect. When connected and firmware_edition != VANILLA, look up the metadata record by enum name. No record found β still treat as an event (name-only, no branding) but don't block.
Notification default (event β quiet). On the first connect to any non-vanilla edition, auto-disable new-node/other high-churn notifications and set a one-shot latch. On the next connect to VANILLA, if the latch is set, re-enable and clear it. The user may re-enable manually at any time; the latch must not fight them. (Both Apple and Android already do this β codifying it.)
Post-event nudge (event over β return to standard). When the connected device is on an event edition whose eventEnd (evaluated in timeZone, falling back to device zone) is in the past, surface a persistent reminder to flash back to standard firmware, linking to the client's firmware-update flow. It clears automatically once the device is back on VANILLA. A missing/unparseable eventEnd is never treated as ended.
UX affordances (all clients)
Ambient branding on the main chrome. While an event edition is active, tint the primary app bar / header with accentColor (a subtle wash, not a full recolor β see below) and show the event icon (iconUrl when hosted, else a bundled drawable). Optionally render the app-wide typeface from theme.fonts β heading/body are Google Font family names, not URLs, so each client loads them via its own downloadable-font mechanism and degrades to the default typeface where unavailable (Android: Google-flavor only via the Play Fonts provider; F-Droid keeps the default). Tapping the branding opens the event info surface.
Make the ambient theme opt-out-able. The accent wash + fonts are a default-on convenience; offer a single "Use event theme" toggle (Android puts it in the event info sheet, backed by a persisted pref). The branding icon and info surface stay available even when opted out, so the user can still see the event and re-enable.
β οΈAccent wash + fonts only β do NOT recolor the whole app color scheme from the brand colors. There is no standard, contrast-correct way to derive a full M3 ColorScheme from an arbitrary seed color (Compose's dynamicColorScheme seeds only from the system wallpaper; dropping raw brand hex into primary/secondary breaks the tuned on-color contrast on every screen). Keep colors to the subtle app-bar/header wash. Fonts are the safe app-wide swap (event fonts skew legibility-first, e.g. Atkinson Hyperlegible, and degrade gracefully). Revisit a full seedβtonal-scheme only if a shared generator (e.g. material-color-utilities / MaterialKolor) is adopted.
Event info surface (sheet / screen): welcome message, venue/location, date range (eventStartβeventEnd), and the links list. Purely informational.
Post-event reminder surface: a banner/prompt (client-idiomatic) with a "return to standard firmware" action wired to the firmware-update flow.
β οΈLocalization tradeoff:welcomeMessage/displayName come from data (not per-client string catalogs), so they are not run through Crowdin. Clients that want a localized edition name may keep a localized enum-name map (as Apple does) and layer the API metadata on top for the rest. Flag if this is unacceptable β it's the central tradeoff of the data-driven approach.
Reference implementations & prior art
Meshtastic-Android β #6162 β reference for the metadata pipeline half.
ApiService.getEventFirmware() β resource/eventFirmware; offline-first repository seeded from bundled event_firmware.json, single-flight network refresh (1-day cache), DB-backed so it survives restarts; prunes editions dropped upstream.
Event branding on the app bar (accent wash + icon) β tap opens EventInfoSheet (welcome / location / dates / links).
App-wide ambient theme (#6163): accent wash andtheme.fonts applied over the whole M3 typescale, unified under one "Use event theme" opt-out toggle (Google-flavor fonts via downloadable Google Fonts). Implementation gotcha worth sharing: a downloadable font referenced only through the theme's Typography is not auto-fetched by Compose β it silently renders the fallback until you explicitly FontFamily.Resolver.preload() it. Verified on-device (DEFCON): headingβLato, bodyβAtkinson Hyperlegible.
Notification auto-disable on non-vanilla, restored on vanilla (applyEventFirmwareNotificationDefaults).
Post-event reminder banner on the Connections screen (hasEnded() = eventEnd past in timeZone).
Bundled asset kept current by a scheduled GitHub Action (hourly fetch/validate/diff into event_firmware.json).
Meshtastic-Apple β prior art (proto/enum + notifications half; no metadata API, branding, info screen, or post-event nudge yet).
Reads MyNodeInfo.firmwareEdition at handshake; stores on AccessoryManager.
Hand-written localized FirmwareEditions enum: name + description + isEvent, covering vanilla/smartCitizen/openSauce/defcon/burningMan/hamvention/diyEdition. Display data is hardcoded in-app (localized, but a new event needs an app release).
Surfaced as an orange edition-name label on the Connect screen only β no accent theming, welcome, info screen, or icons.
applyEventFirmwareNotificationDefaults auto-disables new-node notifications on non-vanilla, restores on vanilla (one-shot latch); covered by EventFirmwareNotificationTests.
β οΈKey alignment decision this spec forces: Apple sources edition display data from an in-app localized enum; Android sources it from the API/bundled metadata keyed on the enum name. The proposal is that the enum name stays the stable key and the API/bundled resource becomes the shared presentation layer (with the enum-name map as an optional localized fallback). Confirm this direction.
/resource/eventFirmware is live (returns version 2; editions currently HAMVENTION / OPEN_SAUCE / DEFCON / BURNING_MAN). Icons are hosted (api.meshtastic.org/resource/eventFirmware/<slug>.png) and event builds ship under meshtastic.github.io/event/<slug>/. Confirm who owns the publishing pipeline (who authors/edits editions, and whether the bundled snapshot in each app is regenerated from it).
Standardize one bundled filename (event_firmware.json) so the same authored payload seeds every app. A formal JSON schema is optional β if adopted, keep exactly one canonical copy with the API/data publisher (currently version 2, covering tag/domain/theme/firmware), not a drifting per-client copy. Android deliberately ships no schema file; its typed models are the contract.
Richer fields β partial adoption. The Android reference now consumes theme.fonts (heading/body) for the app-wide typeface. Still unconsumed anywhere: firmware{} (event build version + zipUrl + release notes β could drive in-app "flash the event firmware" / a version comparison), theme.palette, theme.tagline, domain, tag. Decide which are worth wiring cross-platform; firmware{} is the highest-value gap.
iconUrl is hosted but not consumed yet β clients still fall back to bundled per-edition drawables. Wiring remote icon loading is a follow-up (network image loading per platform).
Notification scope: confirm exactly which notification categories the event latch suppresses (new-node vs all) is consistent across clients β Apple gates newNodeNotifications; Android gates its node-events channel.
Post-event nudge is Android-only today. Decide whether it's in-scope for all clients (recommended) and what "return to standard firmware" links to on each platform.
πͺ Event firmware metadata β client spec
Important
A device reports which event firmware it runs; clients turn that into branding, event info, and lifecycle nudges from a shared, over-the-air metadata source. The identity is the proto enum
MyNodeInfo.firmware_edition(FirmwareEdition:VANILLA,HAMVENTION,DEFCON, β¦). The display data for each edition β name, welcome message, dates, venue, links, accent color, icon β lives off-device inGET https://api.meshtastic.org/resource/eventFirmware, with a bundled JSON snapshot as the always-available offline fallback, so a new event ships without an app release. Given the edition, every client should: surface event branding (accent + icon on the main chrome, tap β an event info sheet), auto-suppress new-node notifications while on event firmware (event meshes churn hard) and restore them on return to vanilla, and once the event's end date has passed, nudge the user back to standard firmware. The proto enum is the stable key; the API/bundled metadata is the presentation layer keyed on it.Tracking: design#120 Β· Proto (source of truth):
FirmwareEdition+MyNodeInfo.firmware_edition(firmware-owned) Β· Metadata resource:api.meshtastic.org/resource/eventFirmware+ bundledevent_firmware.jsonΒ· Reference build: Meshtastic-Android#6162 Β· Prior art: Meshtastic-Apple (enum + notifications, see below)Two data surfaces (source of truth)
1. On-device identity β the proto enum.
MyNodeInfo.firmware_editionis aFirmwareEditionenum reported at config handshake. Firmware owns the value set. The first 16 values are reserved for non-event special firmwares; events follow.VANILLASMART_CITIZENOPEN_SAUCEDEFCONBURNING_MANHAMVENTIONDIY_EDITION"Is this an event?" =
edition != VANILLA. Clients must default unknown/future enum values gracefully (treat as the reported name, no branding, still non-vanilla for notification purposes).2. Off-device presentation β the metadata resource.
GET https://api.meshtastic.org/resource/eventFirmwareis live and returns an envelope (version 2) keyed by the enum name, sharing the{version, generatedAt, source, <payload>[]}shape used bydeviceLinks. Verbatim shape of a live edition record (Hamvention, trimmed):{ "version": 2, "generatedAt": "2026-06-30T00:00:00Z", "source": "bundled", "editions": [ { "edition": "HAMVENTION", // MUST equal the FirmwareEdition enum name (the join key) "displayName": "Dayton Hamvention 2026", "welcomeMessage": "Welcome to Hamvention! ππ»", "tag": "Hamvention", // short label "eventStart": "2026-05-15", // ISO YYYY-MM-DD, in `timeZone` "eventEnd": "2026-05-17", "timeZone": "America/New_York", // IANA id; resolves the dates to local time "location": "Xenia, Ohio, USA", "iconUrl": "https://api.meshtastic.org/resource/eventFirmware/hamvention.png", // now hosted "accentColor":"#BF1E2E", // #RRGGBB ambient brand color "domain": "hamvention.meshtastic.org", // per-event microsite "links": [ { "label": "Event Website", "url": "https://hamvention.org" } ], "theme": { // richer branding than accentColor alone "name": "Radio Adventure", "tagline": "Exploring the many avenues amateur radio has to offer.", "colors": { "primary": "#BF1E2E", "secondary": null, "accent": null }, "palette": ["#BF1E2E"], "fonts": null }, "firmware": { // the event's actual firmware build "slug": "dayton2026", "version": "2.7.23.07741e6", "id": "v2.7.23.07741e6", "title": "Meshtastic Firmware 2.7.23.07741e6", "zipUrl": "https://github.com/meshtastic/meshtastic.github.io/raw/master/event/dayton2026/firmware-2.7.23.07741e6.zip", "releaseNotes": "## Welcome to Dayton Hamvention 2026! β¦" } } ] }editionstring is the join key and MUST match the proto enum name exactly. Live editions today:HAMVENTION,OPEN_SAUCE,DEFCON,BURNING_MAN. Field coverage is uneven:firmware{}is populated forHAMVENTION/OPEN_SAUCEandnullforDEFCON/BURNING_MAN; a hostediconUrlcurrently exists only forHAMVENTION;theme.fonts/multi-colorpaletteare populated forDEFCONbutnull/single-color elsewhere. Clients must treat every sub-field as independently optional.edition,displayName,welcomeMessage,eventStart/eventEnd/timeZone,location,accentColor,iconUrl,links. The richertag/domain/theme/firmwareobjects are optional/progressive β clients may ignore what they don't use, but should tolerate them (don't fail on unknown keys).firmware{}is the notable new capability: it carries the event's own build (version+zipUrl+releaseNotes), so a client can offer/flash the event firmware directly and can compare it against the connected device's reported version. Out of MVP scope but worth designing around.source: "bundled"above reflects the server currently serving from the same bundled snapshot.)schemas/event_firmware.schema.jsoncopy but it was removed β nothing validated against it and it duplicated the API + models. If a formal schema is wanted, it should live once with the API/data publisher, not per-client β see notes.)Behavior spec (all clients)
/resource/eventFirmwarein the background and cache the result across launches. Do not impose a short network deadline βapi.meshtastic.orgis regularly measured at 20β60 s; a tight timeout cancels every refresh and pins users to the bundled seed. Rely on the client's normal HTTP timeout/retry. An empty/failed response must leave the cache intact (never wipe to blank).firmware_edition != VANILLA, look up the metadata record by enum name. No record found β still treat as an event (name-only, no branding) but don't block.VANILLA, if the latch is set, re-enable and clear it. The user may re-enable manually at any time; the latch must not fight them. (Both Apple and Android already do this β codifying it.)eventEnd(evaluated intimeZone, falling back to device zone) is in the past, surface a persistent reminder to flash back to standard firmware, linking to the client's firmware-update flow. It clears automatically once the device is back onVANILLA. A missing/unparseableeventEndis never treated as ended.UX affordances (all clients)
accentColor(a subtle wash, not a full recolor β see below) and show the event icon (iconUrlwhen hosted, else a bundled drawable). Optionally render the app-wide typeface fromtheme.fontsβheading/bodyare Google Font family names, not URLs, so each client loads them via its own downloadable-font mechanism and degrades to the default typeface where unavailable (Android: Google-flavor only via the Play Fonts provider; F-Droid keeps the default). Tapping the branding opens the event info surface.ColorSchemefrom an arbitrary seed color (Compose'sdynamicColorSchemeseeds only from the system wallpaper; dropping raw brand hex intoprimary/secondarybreaks the tuned on-color contrast on every screen). Keep colors to the subtle app-bar/header wash. Fonts are the safe app-wide swap (event fonts skew legibility-first, e.g. Atkinson Hyperlegible, and degrade gracefully). Revisit a full seedβtonal-scheme only if a shared generator (e.g. material-color-utilities / MaterialKolor) is adopted.location, date range (eventStartβeventEnd), and thelinkslist. Purely informational.welcomeMessage/displayNamecome from data (not per-client string catalogs), so they are not run through Crowdin. Clients that want a localized edition name may keep a localized enum-name map (as Apple does) and layer the API metadata on top for the rest. Flag if this is unacceptable β it's the central tradeoff of the data-driven approach.Reference implementations & prior art
Meshtastic-Android β #6162 β reference for the metadata pipeline half.
ApiService.getEventFirmware()βresource/eventFirmware; offline-first repository seeded from bundledevent_firmware.json, single-flight network refresh (1-day cache), DB-backed so it survives restarts; prunes editions dropped upstream.EventInfoSheet(welcome / location / dates / links).theme.fontsapplied over the whole M3 typescale, unified under one "Use event theme" opt-out toggle (Google-flavor fonts via downloadable Google Fonts). Implementation gotcha worth sharing: a downloadable font referenced only through the theme'sTypographyis not auto-fetched by Compose β it silently renders the fallback until you explicitlyFontFamily.Resolver.preload()it. Verified on-device (DEFCON): headingβLato, bodyβAtkinson Hyperlegible.applyEventFirmwareNotificationDefaults).hasEnded()=eventEndpast intimeZone).event_firmware.json).Meshtastic-Apple β prior art (proto/enum + notifications half; no metadata API, branding, info screen, or post-event nudge yet).
MyNodeInfo.firmwareEditionat handshake; stores onAccessoryManager.FirmwareEditionsenum:name+description+isEvent, covering vanilla/smartCitizen/openSauce/defcon/burningMan/hamvention/diyEdition. Display data is hardcoded in-app (localized, but a new event needs an app release).applyEventFirmwareNotificationDefaultsauto-disables new-node notifications on non-vanilla, restores on vanilla (one-shot latch); covered byEventFirmwareNotificationTests.Per-client tracking (sub-issues)
FirmwareEditionvalues + publishing/ownership of/resource/eventFirmwareβ #TBDNotes / open items
/resource/eventFirmwareis live (returnsversion 2; editions currentlyHAMVENTION/OPEN_SAUCE/DEFCON/BURNING_MAN). Icons are hosted (api.meshtastic.org/resource/eventFirmware/<slug>.png) and event builds ship undermeshtastic.github.io/event/<slug>/. Confirm who owns the publishing pipeline (who authors/edits editions, and whether the bundled snapshot in each app is regenerated from it).event_firmware.json) so the same authored payload seeds every app. A formal JSON schema is optional β if adopted, keep exactly one canonical copy with the API/data publisher (currentlyversion 2, coveringtag/domain/theme/firmware), not a drifting per-client copy. Android deliberately ships no schema file; its typed models are the contract.theme.fonts(heading/body) for the app-wide typeface. Still unconsumed anywhere:firmware{}(event build version +zipUrl+ release notes β could drive in-app "flash the event firmware" / a version comparison),theme.palette,theme.tagline,domain,tag. Decide which are worth wiring cross-platform;firmware{}is the highest-value gap.iconUrlis hosted but not consumed yet β clients still fall back to bundled per-edition drawables. Wiring remote icon loading is a follow-up (network image loading per platform).newNodeNotifications; Android gates its node-events channel.