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 };