Files
ARCG-Remote-Station-Software/plugins/rms.openwebrx.bandmap/index.js
OE6DXD e1a4ce0b8b initialize generic rms-software repository
Add the reusable RMS core application (server, web UI, plugins, tests, tools) with generic defaults, GPL licensing, and maintainer context documentation so deployments can consume this repo as software source independent of station-specific overlays.
2026-03-16 03:31:08 +01:00

312 lines
9.6 KiB
JavaScript

const fs = require("fs");
const path = require("path");
async function createPlugin(ctx) {
return {
async execute(action, input) {
if (action === "getBands") {
return getBands(ctx);
}
if (action === "setBand") {
return setBand(ctx, input || {});
}
if (action === "getState") {
return getState(ctx);
}
throw new Error(`Unknown action: ${action}`);
},
async getStatus() {
return getState(ctx);
},
async health() {
return { ok: true };
}
};
}
function getBands(ctx) {
const bands = readBandmap(ctx);
const state = readState(ctx);
return {
ok: true,
selectedBand: state.selectedBand || null,
selectedCenterFreqHz: state.centerFreqHz || null,
selectedSampRate: Number.isFinite(Number(state.sampRate)) ? Number(state.sampRate) : null,
selectedRfGain: normalizeRfGain(state.rfGain),
bands
};
}
async function setBand(ctx, input) {
const requested = String(input.band || input.setfreq || "").trim();
if (!requested) {
throw new Error("Band fehlt");
}
const bands = readBandmap(ctx);
const requestedKey = normalizeBandKey(requested);
const selected = bands.find((entry) => {
const bandKey = normalizeBandKey(entry.band);
const labelKey = normalizeBandKey(entry.label);
return bandKey === requestedKey || labelKey === requestedKey;
});
if (!selected) {
throw new Error(`Band nicht gefunden: ${requested}`);
}
const simulate = typeof ctx.simulateHardware === "boolean"
? ctx.simulateHardware
: !(process.platform === "linux" || String(ctx.env.ALLOW_NON_LINUX_CMDS || "false") === "true");
const setCommandTemplate = String(ctx.getSetting("setCommandTemplate", ctx.env.OPENWEBRX_BAND_SET_CMD_TEMPLATE || "")).trim();
const configFilePath = resolvePath(String(ctx.getSetting("configFilePath", ctx.env.OPENWEBRX_CONFIG_PATH || "")));
const timeoutMs = Number(ctx.getSetting("timeoutMs", ctx.env.OPENWEBRX_BAND_TIMEOUT_MS || 20000));
if (setCommandTemplate) {
const command = applyTemplate(setCommandTemplate, selected);
if (!simulate) {
const result = await ctx.commandRunner(command, { timeoutMs });
if (!result.ok) {
throw new Error(result.stderr || result.error || "OpenWebRX Bandwechsel fehlgeschlagen");
}
}
} else if (configFilePath) {
if (!simulate) {
if (fs.existsSync(configFilePath)) {
const raw = await fs.promises.readFile(configFilePath, "utf8");
let updated = raw;
updated = replaceScalarNumericPythonAssignment(updated, "start_freq", String(selected.centerFreqHz));
if (updated === raw && !/^[ \t]*start_freq\s*=/m.test(raw)) {
updated = `${raw.trimEnd()}\nstart_freq = ${selected.centerFreqHz}\n`;
}
if (Number.isFinite(selected.sampRate)) {
const beforeSampRate = updated;
updated = replaceScalarNumericPythonAssignment(updated, "samp_rate", String(selected.sampRate));
if (updated === beforeSampRate && !/^[ \t]*samp_rate\s*=/m.test(updated)) {
updated = `${updated.trimEnd()}\nsamp_rate = ${selected.sampRate}\n`;
}
}
updated = replaceScalarNumericPythonAssignment(updated, "center_freq", String(selected.centerFreqHz));
updated = replaceScalarNumericPythonAssignment(updated, "shown_center_freq", String(selected.centerFreqHz));
await fs.promises.writeFile(configFilePath, updated);
}
}
}
const state = {
selectedBand: selected.band,
selectedLabel: selected.label,
antennaRoute: selected.antennaRoute || null,
startMod: selected.startMod || null,
sampRate: selected.sampRate || null,
rfGain: normalizeRfGain(selected.rfGain),
centerFreqHz: selected.centerFreqHz,
inputMHz: selected.inputMHz,
updatedAt: new Date().toISOString(),
source: "rms.openwebrx.bandmap"
};
await writeState(ctx, state);
return {
ok: true,
...state,
skipped: simulate,
message: simulate
? `OpenWebRX Band ${selected.label} simuliert (${ctx.execMode || "dev"})`
: `OpenWebRX Band auf ${selected.label} gesetzt`
};
}
function getState(ctx) {
const bands = readBandmap(ctx);
const state = readState(ctx);
const selectedBand = state.selectedBand || null;
let antennaRoute = state.antennaRoute || null;
let startMod = state.startMod || null;
let sampRate = Number.isFinite(Number(state.sampRate)) ? Number(state.sampRate) : null;
let rfGain = normalizeRfGain(state.rfGain);
if (selectedBand && (!antennaRoute || !startMod || !sampRate || rfGain === null)) {
const selected = bands.find((entry) => String(entry.band) === String(selectedBand));
if (selected) {
if (!antennaRoute) {
antennaRoute = selected.antennaRoute || null;
}
if (!startMod) {
startMod = selected.startMod || null;
}
if (!sampRate && Number.isFinite(selected.sampRate)) {
sampRate = selected.sampRate;
}
if (rfGain === null) {
rfGain = normalizeRfGain(selected.rfGain);
}
}
}
return {
ok: true,
selectedBand,
selectedLabel: state.selectedLabel || null,
centerFreqHz: state.centerFreqHz || null,
antennaRoute,
startMod,
sampRate,
rfGain,
bands
};
}
function readBandmap(ctx) {
const csvPath = resolvePath(String(ctx.getSetting("csvPath", ctx.env.OPENWEBRX_BANDMAP_CSV_PATH || "")).trim());
const fallback = String(ctx.env.OPENWEBRX_BANDMAP || "").trim();
let raw = "";
if (csvPath && fs.existsSync(csvPath)) {
raw = fs.readFileSync(csvPath, "utf8");
} else if (fallback) {
raw = fallback.split(",").join("\n");
} else {
raw = "80;3650000;80m\n40;7150000;40m\n20;14300000;20m\n15;21250000;15m\n10;28400000;10m\n2;144384000;2m";
}
const entries = [];
const lines = raw.split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
const parts = trimmed.split(";").map((entry) => entry.trim());
if (parts.length < 2) {
continue;
}
const inputMHz = Number(parts[0]);
const centerFreqHz = Number(parts[1]);
if (!Number.isFinite(inputMHz) || !Number.isFinite(centerFreqHz)) {
continue;
}
const label = parts[2] || `${parts[0]} MHz`;
const parsedSampRate = Number(parts[5]);
const sampRate = Number.isFinite(parsedSampRate)
? parsedSampRate
: defaultSampleRateFor(centerFreqHz);
const rfGain = normalizeRfGain(parts[7]);
entries.push({
band: String(parts[0]),
inputMHz,
centerFreqHz,
label,
antennaRoute: parts[3] ? String(parts[3]).trim().toLowerCase() : "",
startMod: parts[4] ? String(parts[4]).trim().toLowerCase() : "",
sampRate,
rfGain
});
}
return entries;
}
function applyTemplate(template, selected) {
return String(template)
.replaceAll("{band}", selected.band)
.replaceAll("{inputMHz}", String(selected.inputMHz))
.replaceAll("{centerFreqHz}", String(selected.centerFreqHz))
.replaceAll("{sampRate}", String(selected.sampRate || ""))
.replaceAll("{rfGain}", selected.rfGain === null ? "" : String(selected.rfGain))
.replaceAll("{label}", selected.label)
.replaceAll("{antenna}", selected.antennaRoute || "")
.replaceAll("{antennaRoute}", selected.antennaRoute || "");
}
function normalizeRfGain(value) {
if (value === undefined || value === null) {
return null;
}
const text = String(value).trim();
if (!text) {
return null;
}
if (text.toLowerCase() === "auto") {
return "auto";
}
const numeric = Number(text);
return Number.isFinite(numeric) ? numeric : null;
}
function defaultSampleRateFor(centerFreqHz) {
const freq = Number(centerFreqHz);
if (!Number.isFinite(freq)) {
return 384000;
}
if (freq >= 88000000) {
return 768000;
}
if (freq >= 24000000) {
return 456000;
}
if (freq >= 14000000) {
return 384000;
}
if (freq >= 7000000) {
return 256000;
}
return 384000;
}
function normalizeBandKey(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/\s+/g, " ");
}
function replaceScalarNumericPythonAssignment(raw, key, valueLiteral) {
const escapedKey = escapeRegExp(key);
const linePattern = new RegExp(`^(\\s*${escapedKey}\\s*=\\s*)([^\\n#]*)(\\s*(?:#.*)?)$`, "m");
const match = raw.match(linePattern);
if (!match) {
return raw;
}
const rhs = String(match[2] || "").trim();
if (!rhs || rhs.startsWith("[")) {
return raw;
}
if (!/^[-+]?\d+(?:\.\d+)?$/.test(rhs)) {
return raw;
}
return raw.replace(linePattern, `$1${valueLiteral}$3`);
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function readState(ctx) {
const stateFilePath = resolvePath(String(ctx.getSetting("stateFilePath", ctx.env.OPENWEBRX_BAND_STATE_PATH || "./data/openwebrx-band-state.json")));
if (!stateFilePath || !fs.existsSync(stateFilePath)) {
return {};
}
try {
return JSON.parse(fs.readFileSync(stateFilePath, "utf8"));
} catch {
return {};
}
}
async function writeState(ctx, state) {
const stateFilePath = resolvePath(String(ctx.getSetting("stateFilePath", ctx.env.OPENWEBRX_BAND_STATE_PATH || "./data/openwebrx-band-state.json")));
if (!stateFilePath) {
return;
}
await fs.promises.mkdir(path.dirname(stateFilePath), { recursive: true });
await fs.promises.writeFile(stateFilePath, JSON.stringify(state, null, 2));
}
function resolvePath(value) {
const trimmed = String(value || "").trim();
if (!trimmed) return "";
if (path.isAbsolute(trimmed)) return trimmed;
return path.resolve(process.cwd(), trimmed);
}
module.exports = {
createPlugin
};