Skip to content

Commit 9e55cdb

Browse files
jamesarichclaude
andauthored
feat: wire up event-firmware metadata API (offline-first) + post-event firmware reminder (#6162)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8e8cc5b commit 9e55cdb

23 files changed

Lines changed: 2537 additions & 199 deletions

File tree

.github/workflows/scheduled-updates.yml

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,35 @@ jobs:
7979
fi
8080
fi
8181
82+
- name: Update event firmware metadata
83+
id: event_firmware
84+
run: |
85+
event_firmware_file_path="androidApp/src/main/assets/event_firmware.json"
86+
temp_event_firmware_file="/tmp/new_event_firmware.json"
87+
88+
echo "Fetching latest event firmware metadata..."
89+
http_code=$(curl -s --max-time 90 -o "$temp_event_firmware_file" -w '%{http_code}' https://api.meshtastic.org/resource/eventFirmware || true)
90+
http_code="${http_code:-0}"
91+
92+
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
93+
echo "::warning::Event firmware API returned HTTP $http_code. Skipping event firmware update."
94+
echo "status=error" >> "$GITHUB_OUTPUT"
95+
echo "detail=HTTP $http_code from event firmware API" >> "$GITHUB_OUTPUT"
96+
elif ! jq empty "$temp_event_firmware_file" 2>/dev/null; then
97+
echo "::warning::Event firmware API returned invalid JSON data. Skipping event firmware update."
98+
echo "status=error" >> "$GITHUB_OUTPUT"
99+
echo "detail=Invalid JSON response from event firmware API" >> "$GITHUB_OUTPUT"
100+
else
101+
if [ ! -f "$event_firmware_file_path" ] || ! jq --sort-keys . "$temp_event_firmware_file" | diff -q - <(jq --sort-keys . "$event_firmware_file_path"); then
102+
echo "Changes detected in event firmware metadata or local file missing. Updating $event_firmware_file_path."
103+
cp "$temp_event_firmware_file" "$event_firmware_file_path"
104+
echo "status=updated" >> "$GITHUB_OUTPUT"
105+
else
106+
echo "No changes detected in event firmware metadata."
107+
echo "status=unchanged" >> "$GITHUB_OUTPUT"
108+
fi
109+
fi
110+
82111
- name: Sync with Crowdin
83112
uses: crowdin/github-action@v2
84113
with:
@@ -113,11 +142,22 @@ jobs:
113142

114143
- name: Build PR body
115144
id: pr_body
145+
env:
146+
# Hoisted into env: (not interpolated directly in the script) so no step output can inject shell — see
147+
# zizmor template-injection. Values are fixed strings today, but detail may carry API text in future.
148+
FIRMWARE_STATUS: ${{ steps.firmware.outputs.status }}
149+
FIRMWARE_DETAIL: ${{ steps.firmware.outputs.detail }}
150+
HARDWARE_STATUS: ${{ steps.hardware.outputs.status }}
151+
HARDWARE_DETAIL: ${{ steps.hardware.outputs.detail }}
152+
EVENT_FIRMWARE_STATUS: ${{ steps.event_firmware.outputs.status }}
153+
EVENT_FIRMWARE_DETAIL: ${{ steps.event_firmware.outputs.detail }}
116154
run: |
117-
firmware_status="${{ steps.firmware.outputs.status }}"
118-
firmware_detail="${{ steps.firmware.outputs.detail }}"
119-
hardware_status="${{ steps.hardware.outputs.status }}"
120-
hardware_detail="${{ steps.hardware.outputs.detail }}"
155+
firmware_status="$FIRMWARE_STATUS"
156+
firmware_detail="$FIRMWARE_DETAIL"
157+
hardware_status="$HARDWARE_STATUS"
158+
hardware_detail="$HARDWARE_DETAIL"
159+
event_firmware_status="$EVENT_FIRMWARE_STATUS"
160+
event_firmware_detail="$EVENT_FIRMWARE_DETAIL"
121161
122162
body="This PR includes automated updates from the scheduled workflow:"
123163
body+=$'\n'
@@ -138,6 +178,14 @@ jobs:
138178
*) body+=$'\n'"- ❓ \`device_hardware.json\` — unknown status." ;;
139179
esac
140180
181+
# Event firmware status
182+
case "$event_firmware_status" in
183+
updated) body+=$'\n'"- ✅ \`event_firmware.json\` updated from the Meshtastic API." ;;
184+
unchanged) body+=$'\n'"- ✔️ \`event_firmware.json\` checked — no changes detected." ;;
185+
error) body+=$'\n'"- ⚠️ \`event_firmware.json\` skipped — ${event_firmware_detail}." ;;
186+
*) body+=$'\n'"- ❓ \`event_firmware.json\` — unknown status." ;;
187+
esac
188+
141189
# Crowdin (always attempted)
142190
body+=$'\n'"- Source strings were uploaded to Crowdin."
143191
body+=$'\n'"- Latest translations were downloaded from Crowdin (if available)."
@@ -161,6 +209,7 @@ jobs:
161209
Automated updates for:
162210
- Firmware releases list
163211
- Device hardware list
212+
- Event firmware metadata
164213
- Crowdin source string uploads
165214
- Crowdin translation downloads
166215
title: 'chore: Scheduled updates (Firmware, Hardware, Translations)'
@@ -171,6 +220,7 @@ jobs:
171220
add-paths: |
172221
androidApp/src/main/assets/firmware_releases.json
173222
androidApp/src/main/assets/device_hardware.json
223+
androidApp/src/main/assets/event_firmware.json
174224
fastlane/metadata/android/**
175225
**/strings.xml
176226
docs/**/*.md

.skills/compose-ui/strings-index.txt

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.meshtastic.app.di
1818

1919
import org.koin.core.annotation.Module
2020
import org.koin.core.annotation.Single
21+
import org.meshtastic.core.model.EventFirmwareResponse
2122
import org.meshtastic.core.model.NetworkDeviceHardware
2223
import org.meshtastic.core.model.NetworkDeviceLinksResponse
2324
import org.meshtastic.core.model.NetworkFirmwareReleases
@@ -42,5 +43,8 @@ class FDroidNetworkModule {
4243

4344
override suspend fun getFirmwareReleases(): NetworkFirmwareReleases =
4445
throw UnsupportedOperationException("getFirmwareReleases is not supported on F-Droid builds.")
46+
47+
override suspend fun getEventFirmware(): EventFirmwareResponse =
48+
throw UnsupportedOperationException("getEventFirmware is not supported on F-Droid builds.")
4549
}
4650
}
Lines changed: 141 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,183 @@
11
{
2-
"version": 1,
3-
"generatedAt": "2026-06-23T00:00:00Z",
2+
"version": 2,
3+
"generatedAt": "2026-06-30T00:00:00Z",
44
"source": "bundled",
55
"editions": [
66
{
77
"edition": "HAMVENTION",
8-
"displayName": "Hamvention",
8+
"displayName": "Dayton Hamvention 2026",
99
"welcomeMessage": "Welcome to Hamvention! 🍖📻",
10+
"tag": "Hamvention",
1011
"eventStart": "2026-05-15",
1112
"eventEnd": "2026-05-17",
1213
"timeZone": "America/New_York",
1314
"location": "Xenia, Ohio, USA",
14-
"iconUrl": null,
15-
"accentColor": "#005DAA",
15+
"iconUrl": "https://api.meshtastic.org/resource/eventFirmware/hamvention.png",
16+
"accentColor": "#BF1E2E",
17+
"domain": "hamvention.meshtastic.org",
1618
"links": [
17-
{ "label": "Event Website", "url": "https://hamvention.org" }
18-
]
19+
{
20+
"label": "Event Website",
21+
"url": "https://hamvention.org"
22+
}
23+
],
24+
"theme": {
25+
"name": "Radio Adventure",
26+
"tagline": "Exploring the many avenues amateur radio has to offer.",
27+
"colors": {
28+
"primary": "#BF1E2E",
29+
"secondary": null,
30+
"accent": null
31+
},
32+
"palette": [
33+
"#BF1E2E"
34+
],
35+
"fonts": null
36+
},
37+
"firmware": {
38+
"slug": "dayton2026",
39+
"version": "2.7.23.07741e6",
40+
"id": "v2.7.23.07741e6",
41+
"title": "Meshtastic Firmware 2.7.23.07741e6",
42+
"zipUrl": "https://github.com/meshtastic/meshtastic.github.io/raw/master/event/dayton2026/firmware-2.7.23.07741e6.zip",
43+
"releaseNotes": "## Welcome to Dayton Hamvention 2026!\n\nThis firmware has been customized for Hamvention with factory default configurations.\n\n### ⚠️ Important: Backup Before Flashing\n\nIf your device has existing settings or encryption keys, **backup your keys / configurations** before proceeding. Flashing will reset your device to factory settings for the event.\n\n### Quick Start\n\n1. Ensure a **data-capable USB cable** is connected\n2. Select your device type\n3. Choose \"Full Erase and Install\"\n4. After flashing, download the Meshtastic app and pair via Bluetooth\n5. If you updated from a previous version or installed a UF2 on an NRF52 device, you will need to perform a factory reset on the device to activate the Hamvention mode.\n\n**73 and happy meshing from Dayton!**"
44+
}
1945
},
2046
{
2147
"edition": "OPEN_SAUCE",
22-
"displayName": "Open Sauce",
48+
"displayName": "Open Sauce 2026",
2349
"welcomeMessage": "Welcome to Open Sauce! 🔧",
24-
"eventStart": "2026-07-18",
50+
"tag": "Open Sauce",
51+
"eventStart": "2026-07-17",
2552
"eventEnd": "2026-07-19",
2653
"timeZone": "America/Los_Angeles",
2754
"location": "San Mateo, California, USA",
2855
"iconUrl": null,
2956
"accentColor": "#E94F1D",
57+
"domain": "opensauce.meshtastic.org",
3058
"links": [
31-
{ "label": "Event Website", "url": "https://opensauce.com" }
32-
]
59+
{
60+
"label": "Event Website",
61+
"url": "https://opensauce.com"
62+
}
63+
],
64+
"theme": {
65+
"name": "Makers & Mesh",
66+
"tagline": "Where makers, science, and mesh collide.",
67+
"colors": {
68+
"primary": "#E94F1D",
69+
"secondary": null,
70+
"accent": null
71+
},
72+
"palette": [
73+
"#E94F1D"
74+
],
75+
"fonts": null
76+
},
77+
"firmware": {
78+
"slug": "opensauce2026",
79+
"version": "2.7.26.004b486",
80+
"id": "v2.7.26.004b486",
81+
"title": "Meshtastic Firmware 2.7.26.004b486",
82+
"zipUrl": "https://github.com/meshtastic/meshtastic.github.io/raw/master/event/opensauce2026/firmware-2.7.26.004b486.zip",
83+
"releaseNotes": "## Welcome to Open Sauce 2026! 🔧\n\nThis firmware has been customized for Open Sauce with factory default configurations.\n\n### ⚠️ Important: Backup Before Flashing\n\nIf your device has existing settings or encryption keys, **backup your keys / configurations** before proceeding. Flashing will reset your device to factory settings for the event.\n\n### Quick Start\n\n1. Ensure a **data-capable USB cable** is connected\n2. Select your device type\n3. Choose \"Full Erase and Install\"\n4. After flashing, download the Meshtastic app and pair via Bluetooth\n5. If you updated from a previous version or installed a UF2 on an NRF52 device, you will need to perform a factory reset on the device to activate the Open Sauce mode.\n\n**Happy meshing from San Mateo!**"
84+
}
3385
},
3486
{
3587
"edition": "DEFCON",
36-
"displayName": "DEFCON",
37-
"welcomeMessage": "Welcome to DEFCON! 💀",
88+
"displayName": "DEF CON 34",
89+
"welcomeMessage": "Welcome to DEF CON 34! 💀",
90+
"tag": "DEFCON",
3891
"eventStart": "2026-08-06",
3992
"eventEnd": "2026-08-09",
4093
"timeZone": "America/Los_Angeles",
41-
"location": "Las Vegas, Nevada, USA",
94+
"location": "Las Vegas Convention Center, Las Vegas, Nevada, USA",
4295
"iconUrl": null,
43-
"accentColor": "#C0392B",
96+
"accentColor": "#0D294A",
97+
"domain": "defcon.meshtastic.org",
4498
"links": [
45-
{ "label": "Event Website", "url": "https://defcon.org" }
46-
]
99+
{
100+
"label": "Event Website",
101+
"url": "https://defcon.org"
102+
},
103+
{
104+
"label": "DEF CON 34 Theme",
105+
"url": "https://defcon.org/html/defcon-34/dc-34-theme.html"
106+
},
107+
{
108+
"label": "Mastodon",
109+
"url": "https://defcon.social"
110+
}
111+
],
112+
"theme": {
113+
"name": "Agency",
114+
"tagline": "Agency is self-determination. It's about making choices that increase yours, AND helping others to control theirs.",
115+
"colors": {
116+
"primary": "#0D294A",
117+
"secondary": "#017FA4",
118+
"accent": "#E0004E"
119+
},
120+
"palette": [
121+
"#0D294A",
122+
"#017FA4",
123+
"#105F66",
124+
"#6CCDB8",
125+
"#F1B435",
126+
"#E0004E"
127+
],
128+
"fonts": {
129+
"heading": "Lato",
130+
"body": "Atkinson Hyperlegible"
131+
}
132+
},
133+
"firmware": {
134+
"slug": "defcon2026",
135+
"version": null,
136+
"id": null,
137+
"title": null,
138+
"zipUrl": null,
139+
"releaseNotes": null
140+
}
47141
},
48142
{
49143
"edition": "BURNING_MAN",
50-
"displayName": "Burning Man",
144+
"displayName": "Burning Man 2026",
51145
"welcomeMessage": "Welcome to Burning Man! 🔥",
146+
"tag": "Burning Man",
52147
"eventStart": "2026-08-30",
53148
"eventEnd": "2026-09-07",
54149
"timeZone": "America/Los_Angeles",
55150
"location": "Black Rock City, Nevada, USA",
56151
"iconUrl": null,
57-
"accentColor": "#E8552D",
152+
"accentColor": "#EC8819",
153+
"domain": "burningman.meshtastic.org",
58154
"links": [
59-
{ "label": "Event Website", "url": "https://burningman.org" }
60-
]
155+
{
156+
"label": "Event Website",
157+
"url": "https://burningman.org"
158+
}
159+
],
160+
"theme": {
161+
"name": "Axis Mundi",
162+
"tagline": "The axis of the world — a celestial column connecting earth, sky, and the realms between.",
163+
"colors": {
164+
"primary": "#EC8819",
165+
"secondary": null,
166+
"accent": null
167+
},
168+
"palette": [
169+
"#EC8819"
170+
],
171+
"fonts": null
172+
},
173+
"firmware": {
174+
"slug": "burningman2026",
175+
"version": null,
176+
"id": null,
177+
"title": null,
178+
"zipUrl": null,
179+
"releaseNotes": null
180+
}
61181
}
62182
]
63183
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (c) 2026 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package org.meshtastic.core.data.datasource
18+
19+
import kotlinx.coroutines.withContext
20+
import org.koin.core.annotation.Single
21+
import org.meshtastic.core.database.DatabaseProvider
22+
import org.meshtastic.core.database.entity.EventFirmwareEditionEntity
23+
import org.meshtastic.core.di.CoroutineDispatchers
24+
25+
@Single
26+
class EventFirmwareEditionLocalDataSource(
27+
private val dbManager: DatabaseProvider,
28+
private val dispatchers: CoroutineDispatchers,
29+
) {
30+
private val dao
31+
get() = dbManager.currentDb.value.eventFirmwareEditionDao()
32+
33+
suspend fun getByEdition(edition: String): EventFirmwareEditionEntity? =
34+
withContext(dispatchers.io) { dao.getByEdition(edition) }
35+
36+
suspend fun upsertAll(editions: List<EventFirmwareEditionEntity>) =
37+
withContext(dispatchers.io) { dao.upsertAll(editions) }
38+
39+
// No-op on empty: `NOT IN ()` is always true in SQLite, so forwarding an empty list would wipe the whole table.
40+
// Callers only prune against a non-empty upstream payload; an empty list means "keep everything".
41+
suspend fun deleteNotIn(keep: List<String>) {
42+
if (keep.isEmpty()) return
43+
withContext(dispatchers.io) { dao.deleteNotIn(keep) }
44+
}
45+
46+
suspend fun count(): Int = withContext(dispatchers.io) { dao.count() }
47+
}

0 commit comments

Comments
 (0)