Files
edgetx/web/scripts/gen-radios-json.js
2026-08-03 16:37:20 +08:00

181 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
//
// Generate web/public/radios.json from the authoritative hardware definition
// JSON files in radio/src/boards/hw_defs/.
//
// Usage: node web/scripts/gen-radios-json.js [flavour1 flavour2 ...]
//
// Flavour names match the hw_defs filenames (e.g. "tx16s", "x9d+2019").
// If no flavours are specified, generates entries for all hw_defs files.
//
// The key left/right side mapping matches Companion's radioKeyDefinitions
// table in companion/src/simulation/simulateduiwidget.cpp.
import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..', '..');
const HW_DEFS = join(ROOT, 'radio', 'src', 'boards', 'hw_defs');
const OUTPUT = join(ROOT, 'web', 'public', 'radios.json');
// Key side and order — matches Companion's radioKeyDefinitions table
// (companion/src/simulation/simulateduiwidget.cpp)
const KEY_LAYOUT = {
KEY_SYS: { side: 'L', row: 0 },
KEY_MODEL: { side: 'R', row: 0 },
KEY_PAGEUP: { side: 'L', row: 1 },
KEY_PAGEDN: { side: 'R', row: 1 },
KEY_UP: { side: 'L', row: 2 },
KEY_DOWN: { side: 'R', row: 2 },
KEY_LEFT: { side: 'L', row: 3 },
KEY_RIGHT: { side: 'R', row: 3 },
KEY_MINUS: { side: 'L', row: 4 },
KEY_PLUS: { side: 'R', row: 4 },
KEY_TELE: { side: 'R', row: 5 },
KEY_MENU: { side: 'L', row: 6 },
KEY_SHIFT: { side: 'R', row: 6 },
KEY_EXIT: { side: 'L', row: 7 },
KEY_ENTER: { side: 'R', row: 7 },
};
// Pretty labels for keys (unicode symbols instead of plain text)
const KEY_LABELS = {
KEY_PAGEUP: 'PAGE\u25C0', // PAGE◀
KEY_PAGEDN: 'PAGE\u25B6', // PAGE▶
KEY_UP: '\u25B2', // ▲
KEY_DOWN: '\u25BC', // ▼
KEY_LEFT: '\u25C0', // ◀
KEY_RIGHT: '\u25B6', // ▶
KEY_ENTER: 'Enter \u23CE', // Enter ⏎
};
/** Build display name lookup from fw.json (the authoritative radio name list). */
function loadDisplayNames() {
try {
const fw = JSON.parse(readFileSync(join(ROOT, 'fw.json'), 'utf-8'));
const map = {};
for (const [name, prefix] of fw.targets) {
// prefix is e.g. "tx16s-", "x9dp2019-" — strip trailing dash
const key = prefix.replace(/-$/, '');
map[key] = name;
}
return map;
} catch {
return {};
}
}
const DISPLAY_NAMES = loadDisplayNames();
function getDisplayName(flavour) {
// fw.json uses build target names (x9dp2019), hw_defs uses flavour (x9d+2019)
// Try both the flavour and the p-substituted form
if (DISPLAY_NAMES[flavour]) return DISPLAY_NAMES[flavour];
const pForm = flavour.replace('+', 'p');
if (DISPLAY_NAMES[pForm]) return DISPLAY_NAMES[pForm];
return flavour.toUpperCase();
}
function processFlavour(flavour) {
let jsonFile;
try {
jsonFile = readFileSync(join(HW_DEFS, `${flavour}.json`), 'utf-8');
} catch {
console.warn(` ⚠ Skipping ${flavour}: no hw_defs JSON found`);
return null;
}
const hw = JSON.parse(jsonFile);
// Inputs: sticks + flex (skip VBAT, RTC_BAT, etc.)
const inputs = (hw.adc_inputs?.inputs ?? [])
.filter(i => i.type === 'STICK' || i.type === 'FLEX')
.map(i => {
const entry = { name: i.name, type: i.type };
if (i.label) entry.label = i.label;
else entry.label = i.name;
if (i.default) entry.default = i.default;
return entry;
});
// Switches: hardware type 'ADC' means 3POS (analog-read switch)
const switches = (hw.switches ?? []).map(s => {
const swType = (s.type === '3POS' || s.type === 'ADC') ? '3POS' : '2POS';
return { name: s.name, type: swType, default: s.default || swType };
});
// Trims
const trims = (hw.trims ?? []).map(t => ({ name: t.name }));
// Keys with side mapping, pretty labels, sorted by Companion grid row
const keys = (hw.keys ?? []).map(k => ({
key: k.key,
label: KEY_LABELS[k.key] || k.label || k.name,
side: (KEY_LAYOUT[k.key] || {}).side || 'R',
})).sort((a, b) => {
const ra = (KEY_LAYOUT[a.key] || {}).row ?? 99;
const rb = (KEY_LAYOUT[b.key] || {}).row ?? 99;
return ra - rb;
});
// Display / LCD info
const disp = hw.display ?? {};
const display = {
w: disp.w ?? 480,
h: disp.h ?? 272,
depth: disp.depth ?? 16,
};
// WASM filename uses the flavour name directly
return {
name: getDisplayName(flavour),
wasm: `edgetx-${flavour}-simulator.wasm`,
display,
inputs,
switches,
trims,
keys,
};
}
// Determine which flavours to generate: args, or fw.json targets (supported radios only)
let flavours = process.argv.slice(2);
if (flavours.length === 0) {
// Use fw.json as the source of supported targets
// The prefix in fw.json is the build target name (e.g. "x9dp2019-")
// which may differ from the hw_defs flavour (e.g. "x9d+2019")
const fw = JSON.parse(readFileSync(join(ROOT, 'fw.json'), 'utf-8'));
flavours = fw.targets.map(([, prefix]) => {
const buildTarget = prefix.replace(/-$/, '');
// Check if hw_defs file exists under this name; if not, try + substitution
try {
readFileSync(join(HW_DEFS, `${buildTarget}.json`));
return buildTarget;
} catch {
const plusForm = buildTarget.replace('dp', 'd+');
try {
readFileSync(join(HW_DEFS, `${plusForm}.json`));
return plusForm;
} catch {
return buildTarget; // will be skipped later
}
}
});
}
console.log(`Generating radios.json for ${flavours.length} flavours...`);
const radios = [];
for (const flavour of flavours) {
const entry = processFlavour(flavour);
if (entry) {
radios.push(entry);
console.log(` ✓ ${flavour}${entry.name}`);
}
}
writeFileSync(OUTPUT, JSON.stringify(radios, null, 2) + '\n');
console.log(`\nWrote ${radios.length} entries to ${OUTPUT}`);