Skip to content

Commit f7fbfea

Browse files
committed
Add ham license requirement for amateur radio devices and related UI updates
1 parent 1b88586 commit f7fbfea

6 files changed

Lines changed: 147 additions & 4 deletions

File tree

components/DeviceDetail.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232
class="h-6 m-1 pb-1"
3333
alt="Meshtastic UI"
3434
>
35+
<img
36+
v-if="requiresHamLicense(props.device)"
37+
src="@/assets/img/hamvention.svg"
38+
class="h-6 m-1 pb-1"
39+
:title="$t('device.ham_license_required')"
40+
:alt="$t('device.ham_license_required')"
41+
>
3542
</div>
3643
<div
3744
v-if="props.device.images && isSupporterDevice(props.device)"
@@ -74,6 +81,7 @@
7481
<script lang="ts" setup>
7582
import type { DeviceHardware } from '~/types/api'
7683
import { supportedVendorDeviceTags } from '~/types/resources'
84+
import { requiresHamLicense } from '~/utils/deviceBadges'
7785
import { useFirmwareStore } from '../stores/firmwareStore'
7886
import { computed } from 'vue'
7987

i18n/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"subheading": "If your connected device already has Meshtastic installed, you can automatically detect it:",
3333
"auto_detect": "Auto-detect",
3434
"supported_devices": "Supported Devices",
35-
"diy_devices": "Community Supported Devices"
35+
"diy_devices": "Community Supported Devices",
36+
"ham_license_required": "Amateur radio band — an amateur radio license is required to transmit"
3637
},
3738
"firmware": {
3839
"title": "Firmware",

stores/deviceStore.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { mande } from 'mande'
22
import { defineStore } from 'pinia'
33
import {
4+
eventMode,
45
vendorCobrandingTag,
56
} from '~/types/resources'
7+
import { applyEventDeviceOverrides } from '~/utils/eventDevices'
68

79
import { MeshDevice } from '@meshtastic/core'
810
import { TransportWebSerial } from '@meshtastic/transport-web-serial'
@@ -37,7 +39,7 @@ export const useDeviceStore = defineStore('device', {
3739
selectedDevice: <DeviceHardware | undefined>undefined,
3840
selectedTarget: <DeviceHardware | undefined>undefined,
3941
tag: <string | undefined>undefined,
40-
targets: <DeviceHardware[]>[],
42+
apiTargets: <DeviceHardware[]>[],
4143
isConnecting: false,
4244
abortController: <AbortController | undefined>undefined,
4345
readerClosed: <Promise<any> | undefined>undefined,
@@ -46,6 +48,17 @@ export const useDeviceStore = defineStore('device', {
4648
}
4749
},
4850
getters: {
51+
/**
52+
* The API device list plus any devices only the active event's firmware
53+
* build ships. Derived (not baked into state) so it stays correct however
54+
* event mode and the device fetch interleave — plugins/eventMode.client.ts
55+
* can still re-resolve the edition from the live API after mount.
56+
*/
57+
targets(): DeviceHardware[] {
58+
// Co-branded builds are pinned to one vendor's devices; never widen them.
59+
if (vendorCobrandingTag.length > 0) return this.apiTargets
60+
return applyEventDeviceOverrides(this.apiTargets, eventMode.enabled ? eventMode.eventTag : undefined)
61+
},
4962
filteredDevices(): DeviceHardware[] {
5063
if (this.tag) {
5164
return this.targets.filter(t => t.tags?.includes(this.tag ?? '') || t.architecture === this.tag)
@@ -124,14 +137,14 @@ export const useDeviceStore = defineStore('device', {
124137
},
125138
setTargetsList(targets: DeviceHardware[]) {
126139
if (vendorCobrandingTag.length > 0) {
127-
this.targets = targets.filter(
140+
this.apiTargets = targets.filter(
128141
(t: DeviceHardware) => t.activelySupported
129142
&& !t.architecture.toLowerCase().startsWith('portduino')
130143
&& t.tags?.includes(vendorCobrandingTag),
131144
)
132145
}
133146
else {
134-
this.targets = targets.filter(
147+
this.apiTargets = targets.filter(
135148
(t: DeviceHardware) => t.activelySupported
136149
&& !t.architecture.toLowerCase().startsWith('portduino'),
137150
)

utils/deviceBadges.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { DeviceHardware } from '~/types/api'
2+
3+
/**
4+
* Devices that transmit in an amateur radio band rather than an ISM band — the
5+
* T-Beam BPF is a 144–148 MHz (2 m) ~5 W board — so operating one legally needs
6+
* an amateur radio licence. Matched on platformioTarget so the badge survives
7+
* the device graduating from the event overlay (utils/eventDevices.ts) to the
8+
* published API device list.
9+
*/
10+
const hamBandTargets = new Set([
11+
't-beam-bpf',
12+
])
13+
14+
export function requiresHamLicense(device: DeviceHardware): boolean {
15+
return hamBandTargets.has(device.platformioTarget)
16+
}

utils/eventDevices.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { applyEventDeviceOverrides, eventOnlyDevices } from './eventDevices'
3+
import type { DeviceHardware } from '~/types/api'
4+
5+
const tbeam1w: DeviceHardware = {
6+
hwModel: 122,
7+
hwModelSlug: 'TBEAM_1_WATT',
8+
platformioTarget: 't-beam-1w',
9+
architecture: 'esp32-s3',
10+
activelySupported: true,
11+
displayName: 'LilyGo T-Beam 1W',
12+
supportLevel: 1,
13+
tags: ['LilyGo'],
14+
}
15+
16+
const apiList: DeviceHardware[] = [tbeam1w]
17+
18+
describe('applyEventDeviceOverrides', () => {
19+
it('adds the T-Beam BPF in DEF CON mode', () => {
20+
const targets = applyEventDeviceOverrides(apiList, 'DEFCON')
21+
const bpf = targets.find(t => t.platformioTarget === 't-beam-bpf')
22+
expect(bpf).toBeDefined()
23+
expect(bpf?.activelySupported).toBe(true)
24+
expect(bpf?.hwModel).toBe(124)
25+
expect(bpf?.architecture).toBe('esp32-s3')
26+
// 16MB drives the OTA/spiffs offsets of the legacy clean-install path.
27+
expect(bpf?.partitionScheme).toBe('16MB')
28+
expect(bpf?.images).toEqual(['tbeam-bpf.svg'])
29+
})
30+
31+
it('leaves the list untouched off the DEF CON domain', () => {
32+
expect(applyEventDeviceOverrides(apiList, undefined)).toBe(apiList)
33+
expect(applyEventDeviceOverrides(apiList, 'Hamvention')).toBe(apiList)
34+
expect(applyEventDeviceOverrides(apiList, 'Burning Man')).toBe(apiList)
35+
})
36+
37+
it('does not duplicate a device the API already publishes', () => {
38+
const published = [...apiList, ...eventOnlyDevices.DEFCON]
39+
const targets = applyEventDeviceOverrides(published, 'DEFCON')
40+
expect(targets).toBe(published)
41+
expect(targets.filter(t => t.platformioTarget === 't-beam-bpf')).toHaveLength(1)
42+
})
43+
44+
it('does not mutate the source list', () => {
45+
const source = [...apiList]
46+
applyEventDeviceOverrides(source, 'DEFCON')
47+
expect(source).toEqual(apiList)
48+
})
49+
})

utils/eventDevices.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { DeviceHardware } from '~/types/api'
2+
3+
/**
4+
* Devices an event's custom firmware build ships but that
5+
* api.meshtastic.org/resource/deviceHardware does not publish as actively
6+
* supported yet. Keyed by the edition's event tag (EventModeConfig.eventTag),
7+
* so an entry only ever reaches the device picker on that event's domain.
8+
*
9+
* Each entry mirrors the custom_meshtastic_* metadata in the firmware repo's
10+
* variants/<arch>/<env>/platformio.ini — the same source the API generates from
11+
* — so the UI doesn't shift once the API catches up. Entries are inert (not
12+
* duplicated) after that happens, but delete them anyway to keep this honest.
13+
*/
14+
export const eventOnlyDevices: Record<string, DeviceHardware[]> = {
15+
// DEF CON 34's build ships firmware-t-beam-bpf-*, but hwModel 124 is still
16+
// absent from the API device list.
17+
// https://github.com/meshtastic/firmware/blob/master/variants/esp32s3/t-beam-bpf/platformio.ini
18+
DEFCON: [
19+
{
20+
hwModel: 124,
21+
hwModelSlug: 'TBEAM_BPF',
22+
platformioTarget: 't-beam-bpf',
23+
// The API normalises the platformio `esp32s3` to a hyphenated arch, which
24+
// is what the flash flow matches on (see components/targets/Esp32.vue).
25+
architecture: 'esp32-s3',
26+
activelySupported: true,
27+
displayName: 'LilyGo T-Beam BPF',
28+
supportLevel: 3,
29+
tags: ['LilyGo'],
30+
images: ['tbeam-bpf.svg'],
31+
// board_build.partitions = default_16MB.csv
32+
partitionScheme: '16MB',
33+
hasMui: false,
34+
hasInkHud: false,
35+
},
36+
],
37+
}
38+
39+
/**
40+
* Overlay the active event's extra devices onto the API device list. A no-op
41+
* when no event is active, the event has no extras, or the API already
42+
* publishes the device — so this only ever adds entries on an event domain.
43+
*/
44+
export function applyEventDeviceOverrides(
45+
targets: DeviceHardware[],
46+
eventTag?: string,
47+
): DeviceHardware[] {
48+
const extras = eventTag ? eventOnlyDevices[eventTag] : undefined
49+
if (!extras?.length) return targets
50+
51+
const missing = extras.filter(
52+
extra => !targets.some(t => t.platformioTarget === extra.platformioTarget),
53+
)
54+
if (!missing.length) return targets
55+
return [...targets, ...missing]
56+
}

0 commit comments

Comments
 (0)