Skip to content

Commit 86755d4

Browse files
authored
Pull device filters from build API at startup (betaflight#5043)
1 parent 5c21287 commit 86755d4

3 files changed

Lines changed: 107 additions & 14 deletions

File tree

src/js/BuildApi.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,21 @@ export default class BuildApi {
222222
}
223223

224224
async loadConfiguratorRelease(type) {
225-
const url = `${this._url}/api/configurator/releases/${type}`;
225+
const url = `${this._url}/api/app/releases/${type}`;
226226
return await this.fetchJson(url);
227227
}
228228

229+
async loadDeviceFilters() {
230+
try {
231+
return await this.fetchJson(`${this._url}/api/app/devices`);
232+
} catch {
233+
// offline or network error — caller falls back to cache
234+
return null;
235+
}
236+
}
237+
229238
async loadSponsorTile(mode, page) {
230-
const url = `${this._url}/api/configurator/sponsors/${mode}/${page}`;
239+
const url = `${this._url}/api/app/sponsors/${mode}/${page}`;
231240
return await this.fetchText(url);
232241
}
233242
}

src/js/main.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import NotificationManager from "./utils/notifications.js";
1616
import { Capacitor } from "@capacitor/core";
1717
import loginManager from "./LoginManager.js";
1818
import { enableDevelopmentOptions } from "./utils/developmentOptions.js";
19+
import { loadDeviceFilters } from "./protocols/devices.js";
1920

2021
// Silence Capacitor bridge debug spam on native platforms
2122
if (Capacitor?.isNativePlatform?.() && typeof Capacitor.isLoggingEnabled === "boolean") {
@@ -70,6 +71,10 @@ function appReady() {
7071

7172
cleanupLocalStorage();
7273

74+
loadDeviceFilters().catch((err) => {
75+
console.warn("Failed to load device filters, using defaults:", err);
76+
});
77+
7378
i18n.init(async function () {
7479
await startProcess();
7580

src/js/protocols/devices.js

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
export const bluetoothDevices = [
1+
import BuildApi from "../BuildApi.js";
2+
import { get as getConfig, set as setConfig } from "../ConfigStorage.js";
3+
4+
const STORAGE_KEY = "device-filters";
5+
6+
const defaultBluetoothDevices = [
27
{
38
name: "CC2541",
49
serviceUuid: "0000ffe0-0000-1000-8000-00805f9b34fb",
@@ -50,7 +55,7 @@ export const bluetoothDevices = [
5055
},
5156
];
5257

53-
export const serialDevices = [
58+
const defaultSerialDevices = [
5459
{ vendorId: 1027, productId: 24577 }, // FT232R USB UART
5560
{ vendorId: 1155, productId: 12886 }, // STM32 in HID mode
5661
{ vendorId: 1155, productId: 14158 }, // 0483:374e STM Electronics STLink Virtual COM Port (NUCLEO boards)
@@ -64,17 +69,15 @@ export const serialDevices = [
6469
{ vendorId: 11914, productId: 9 }, // Raspberry Pi Pico VCP
6570
];
6671

67-
export const usbDevices = {
68-
filters: [
69-
{ vendorId: 1155, productId: 57105 }, // STM Device in DFU Mode || Digital Radio in USB mode
70-
{ vendorId: 10473, productId: 393 }, // GD32 DFU Bootloader
71-
{ vendorId: 11836, productId: 57105 }, // AT32F435 DFU Bootloader
72-
{ vendorId: 12619, productId: 262 }, // APM32 DFU Bootloader
73-
{ vendorId: 11914, productId: 15 }, // Raspberry Pi Pico in Bootloader mode
74-
],
75-
};
72+
const defaultUsbFilters = [
73+
{ vendorId: 1155, productId: 57105 }, // STM Device in DFU Mode || Digital Radio in USB mode
74+
{ vendorId: 10473, productId: 393 }, // GD32 DFU Bootloader
75+
{ vendorId: 11836, productId: 57105 }, // AT32F435 DFU Bootloader
76+
{ vendorId: 12619, productId: 262 }, // APM32 DFU Bootloader
77+
{ vendorId: 11914, productId: 15 }, // Raspberry Pi Pico in Bootloader mode
78+
];
7679

77-
export const vendorIdNames = {
80+
const defaultVendorIdNames = {
7881
1027: "FTDI",
7982
1155: "STM Electronics",
8083
4292: "Silicon Labs",
@@ -83,7 +86,83 @@ export const vendorIdNames = {
8386
11914: "Raspberry Pi Pico",
8487
};
8588

89+
export const bluetoothDevices = [...defaultBluetoothDevices];
90+
export const serialDevices = [...defaultSerialDevices];
91+
export const usbDevices = { filters: [...defaultUsbFilters] };
92+
export const vendorIdNames = { ...defaultVendorIdNames };
8693
export const webSerialDevices = serialDevices.map(({ vendorId, productId }) => ({
8794
usbVendorId: vendorId,
8895
usbProductId: productId,
8996
}));
97+
98+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
99+
100+
function isPlainObject(value) {
101+
return value !== null && typeof value === "object" && !Array.isArray(value);
102+
}
103+
104+
function sanitizeVidPidEntries(arr) {
105+
return arr.filter(
106+
(entry) => isPlainObject(entry) && typeof entry.vendorId === "number" && typeof entry.productId === "number",
107+
);
108+
}
109+
110+
function applyFilters(data) {
111+
if (Array.isArray(data?.bluetoothDevices)) {
112+
const sanitized = data.bluetoothDevices.filter((d) => isPlainObject(d) && typeof d.serviceUuid === "string");
113+
bluetoothDevices.splice(0, bluetoothDevices.length, ...sanitized);
114+
}
115+
if (Array.isArray(data?.serialDevices)) {
116+
const sanitized = sanitizeVidPidEntries(data.serialDevices);
117+
serialDevices.splice(0, serialDevices.length, ...sanitized);
118+
webSerialDevices.splice(
119+
0,
120+
webSerialDevices.length,
121+
...sanitized.map(({ vendorId, productId }) => ({
122+
usbVendorId: vendorId,
123+
usbProductId: productId,
124+
})),
125+
);
126+
}
127+
if (Array.isArray(data?.usbDevices?.filters)) {
128+
const sanitized = sanitizeVidPidEntries(data.usbDevices.filters);
129+
usbDevices.filters.splice(0, usbDevices.filters.length, ...sanitized);
130+
}
131+
if (isPlainObject(data?.vendorIdNames)) {
132+
for (const key of Object.keys(vendorIdNames)) {
133+
delete vendorIdNames[key];
134+
}
135+
for (const [key, value] of Object.entries(data.vendorIdNames)) {
136+
if (UNSAFE_KEYS.has(key) || typeof value !== "string") {
137+
continue;
138+
}
139+
vendorIdNames[key] = value;
140+
}
141+
}
142+
}
143+
144+
function isValidPayload(data) {
145+
if (!isPlainObject(data)) {
146+
return false;
147+
}
148+
return (
149+
Array.isArray(data.bluetoothDevices) ||
150+
Array.isArray(data.serialDevices) ||
151+
Array.isArray(data.usbDevices?.filters) ||
152+
isPlainObject(data.vendorIdNames)
153+
);
154+
}
155+
156+
export async function loadDeviceFilters(buildApi = new BuildApi()) {
157+
const remote = await buildApi.loadDeviceFilters();
158+
if (isValidPayload(remote)) {
159+
applyFilters(remote);
160+
setConfig({ [STORAGE_KEY]: remote });
161+
return;
162+
}
163+
164+
const cached = getConfig(STORAGE_KEY)?.[STORAGE_KEY];
165+
if (cached) {
166+
applyFilters(cached);
167+
}
168+
}

0 commit comments

Comments
 (0)