diff --git a/.env.example b/.env.example index 7c315ab..8e17525 100644 --- a/.env.example +++ b/.env.example @@ -86,3 +86,10 @@ MICROHAM_PTT_DOWN_CMD= MICROHAM_PTT_UP_CMD= OPENWEBRX_ACCESS_POLICY_FILE=./data/openwebrx-access-policy.txt OPENWEBRX_PERSISTENT_USERS_FILE=./data/openwebrx-persistent-users.txt +# rms.wavelog.cat: push current frequency/mode to Wavelog (CAT radio API). +# Base URL of the Wavelog instance (no trailing slash). Leave empty to disable. +WAVELOG_BASE_URL= +# Radio name shown in Wavelog's Radios list. +WAVELOG_RADIO_NAME=RMS Remote +# HTTP timeout for the Wavelog API call (ms). +WAVELOG_TIMEOUT_MS=8000 diff --git a/docs/plugins/README.md b/docs/plugins/README.md index 6b52e29..87d1c18 100644 --- a/docs/plugins/README.md +++ b/docs/plugins/README.md @@ -18,3 +18,4 @@ - `rms-vswr-report-reader.md` - `rms-vswr-nanovna.md` - `rms-station-shell.md` +- `rms-wavelog-cat.md` diff --git a/docs/plugins/rms-wavelog-cat.md b/docs/plugins/rms-wavelog-cat.md new file mode 100644 index 0000000..ace5a9b --- /dev/null +++ b/docs/plugins/rms-wavelog-cat.md @@ -0,0 +1,18 @@ +# rms.wavelog.cat + +- Zweck: Aktuelle Stationsfrequenz/-mode an ein Wavelog-Logbuch melden (CAT Radio API). + Pushed `POST /index.php/api/radio` mit dem **per-Operator** Wavelog-API-Key, sodass die + Frequenz im persoenlichen Wavelog-Konto des aktiven Operators landet (`cat.user_id` = Key-Besitzer). +- Capabilities: `logging.cat.publish`. +- Aktionen: `publish` mit Input `{ frequencyHz, mode, apiKey, baseUrl?, radioName? }`. +- Wichtige Settings/ENV: + - `baseUrl` / `WAVELOG_BASE_URL` – URL der Wavelog-Instanz (ohne Slash am Ende). Leer = deaktiviert. + - `radioName` / `WAVELOG_RADIO_NAME` – Anzeigename in Wavelog (Default `RMS Remote`). + - `timeoutMs` / `WAVELOG_TIMEOUT_MS` – HTTP-Timeout (Default 8000). +- Ausloeser: Der Core ruft die Capability beim Bandwechsel (`/v1/openwebrx/bands/select`) auf und + uebergibt `centerFreqHz` + `startMod` sowie den Wavelog-Key des aktiven Operators. Best-effort: + Fehler brechen den Bandwechsel nie ab. +- Key-Ablage: pro Benutzer in `users.json` (`wavelogApiKey`), setzbar via `PUT /v1/me/wavelog-key` + und sichtbar im OpenWebRX-Panel. **Kein** Key im Repo – der Key ist Laufzeit-/Benutzerdatum. +- Mode-Mapping: `usb|lsb|ssb → SSB`, `cw|cwr → CW`, `fm|nfm|wfm → FM`, `am|sam → AM`, sonst Rohwert + in Grossbuchstaben (Wavelog mappt gaengige Rig-Aliase selbst). diff --git a/plugins/rms.wavelog.cat/index.js b/plugins/rms.wavelog.cat/index.js new file mode 100644 index 0000000..1af4b67 --- /dev/null +++ b/plugins/rms.wavelog.cat/index.js @@ -0,0 +1,116 @@ +// rms.wavelog.cat +// +// Publishes the station's current frequency/mode to a Wavelog logbook via its +// CAT radio API (`POST /index.php/api/radio`). The caller passes the +// per-operator Wavelog API key, so each operator's frequency lands in their own +// Wavelog account (cat.user_id = key owner). +// +// Generic by design (public repo): the Wavelog base URL and radio name come from +// plugin settings or runtime env, never hardcoded. No secrets are stored here. + +const ADIF_MODE_BY_STARTMOD = { + usb: "SSB", + lsb: "SSB", + ssb: "SSB", + cw: "CW", + cwr: "CW", + fm: "FM", + nfm: "FM", + wfm: "FM", + am: "AM", + sam: "AM", + dmr: "DMR", + rtty: "RTTY", + ft8: "FT8", + ft4: "FT4", + psk: "PSK", + psk31: "PSK" +}; + +function toAdifMode(mode) { + const key = String(mode || "").trim().toLowerCase(); + if (!key) { + return ""; + } + if (ADIF_MODE_BY_STARTMOD[key]) { + return ADIF_MODE_BY_STARTMOD[key]; + } + // Fall back to the raw value in upper case; Wavelog maps common rig aliases itself. + return String(mode).trim().toUpperCase(); +} + +async function createPlugin(ctx) { + return { + async execute(action, input) { + if (action !== "publish") { + throw new Error(`Unknown action: ${action}`); + } + return publish(ctx, input || {}); + }, + async health() { + const baseUrl = resolveBaseUrl(ctx, {}); + return { ok: true, configured: Boolean(baseUrl) }; + } + }; +} + +function resolveBaseUrl(ctx, input) { + return String(input.baseUrl || ctx.getSetting("baseUrl", ctx.env.WAVELOG_BASE_URL || "")) + .trim() + .replace(/\/+$/, ""); +} + +async function publish(ctx, input) { + const baseUrl = resolveBaseUrl(ctx, input); + const apiKey = String(input.apiKey || "").trim(); + const radio = String(input.radioName || ctx.getSetting("radioName", ctx.env.WAVELOG_RADIO_NAME || "RMS Remote")).trim(); + const frequencyHz = Math.round(Number(input.frequencyHz)); + const mode = toAdifMode(input.mode); + + if (!baseUrl) { + return { ok: false, skipped: true, reason: "wavelog.baseUrl_missing" }; + } + if (!apiKey) { + return { ok: false, skipped: true, reason: "wavelog.apiKey_missing" }; + } + if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) { + return { ok: false, skipped: true, reason: "wavelog.frequency_missing" }; + } + + const payload = { + key: apiKey, + radio: radio || "RMS Remote", + frequency: frequencyHz + }; + if (mode) { + payload.mode = mode; + } + + const timeoutMs = Number(ctx.getSetting("timeoutMs", ctx.env.WAVELOG_TIMEOUT_MS || 8000)); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), Number.isFinite(timeoutMs) ? timeoutMs : 8000); + try { + const resp = await fetch(`${baseUrl}/index.php/api/radio`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal: controller.signal + }); + const text = await resp.text().catch(() => ""); + if (!resp.ok) { + // Never surface the API key; only status + a short body excerpt. + return { ok: false, status: resp.status, body: String(text).slice(0, 200) }; + } + return { ok: true, status: resp.status, radio: payload.radio, frequency: frequencyHz, mode: mode || null }; + } catch (error) { + return { ok: false, error: String(error && error.message ? error.message : error) }; + } finally { + clearTimeout(timer); + } +} + +module.exports = { + createPlugin, + // exported for unit tests + toAdifMode +}; diff --git a/plugins/rms.wavelog.cat/manifest.json b/plugins/rms.wavelog.cat/manifest.json new file mode 100644 index 0000000..c858c6f --- /dev/null +++ b/plugins/rms.wavelog.cat/manifest.json @@ -0,0 +1,18 @@ +{ + "id": "rms.wavelog.cat", + "name": "Wavelog CAT", + "version": "1.0.0", + "apiVersion": "1.0", + "capabilities": [ + "logging.cat.publish" + ], + "settingsSchema": { + "type": "object", + "properties": { + "baseUrl": { "type": "string" }, + "radioName": { "type": "string" }, + "timeoutMs": { "type": "number", "minimum": 1000, "maximum": 60000 } + }, + "additionalProperties": false + } +} diff --git a/public/app.js b/public/app.js index 50368b0..bd0f3ea 100644 --- a/public/app.js +++ b/public/app.js @@ -135,6 +135,9 @@ const els = { openwebrxSessionLink: document.getElementById("openwebrxSessionLink"), openwebrxCopyLinkBtn: document.getElementById("openwebrxCopyLinkBtn"), openwebrxSessionTicket: document.getElementById("openwebrxSessionTicket"), + wavelogKeyInput: document.getElementById("wavelogKeyInput"), + wavelogKeySaveBtn: document.getElementById("wavelogKeySaveBtn"), + wavelogKeyMessage: document.getElementById("wavelogKeyMessage"), openwebrxTxStatePill: document.getElementById("openwebrxTxStatePill"), rotorCurrent: document.getElementById("rotorCurrent"), rotorCompass: document.getElementById("rotorCompass"), @@ -690,6 +693,11 @@ function bindEvents() { await copyOpenWebRxLink(); }); } + if (els.wavelogKeySaveBtn) { + els.wavelogKeySaveBtn.addEventListener("click", async () => { + await saveWavelogKey(); + }); + } if (els.openwebrxEnableTxBtn) { els.openwebrxEnableTxBtn.addEventListener("click", async (event) => { if (event) { @@ -1496,6 +1504,8 @@ async function refreshCurrentUser() { try { const result = await api("/v1/me", { authRequired: true }); state.user = result.user; + state.wavelogApiKey = typeof result.wavelogApiKey === "string" ? result.wavelogApiKey : ""; + applyWavelogKeyToInput(); renderUserSettingsAuthMethods(); await ensureLanguageFromUserPreference(); } catch { @@ -1684,12 +1694,37 @@ async function cancelReservationByUserId(userId) { } } +function applyWavelogKeyToInput() { + if (els.wavelogKeyInput) { + els.wavelogKeyInput.value = String(state.wavelogApiKey || ""); + } +} + +async function saveWavelogKey() { + if (!els.wavelogKeyInput) { + return; + } + const apiKey = String(els.wavelogKeyInput.value || "").trim(); + try { + const result = await api("/v1/me/wavelog-key", { method: "PUT", body: { apiKey } }); + state.wavelogApiKey = result && typeof result.wavelogApiKey === "string" ? result.wavelogApiKey : apiKey; + applyWavelogKeyToInput(); + renderMessage(els.wavelogKeyMessage, translateLiteral("Wavelog API Key gespeichert."), false); + } catch (error) { + renderMessage(els.wavelogKeyMessage, error.message, true); + } +} + async function openOpenWebRxSession() { clearMessages("status"); setOpenWebRxBusy(true); try { const result = await requestOpenWebRxSessionWithRetry(); const session = result && result.session ? result.session : null; + if (result && typeof result.wavelogApiKey === "string") { + state.wavelogApiKey = result.wavelogApiKey; + applyWavelogKeyToInput(); + } const sessionTicket = session && session.ticket ? String(session.ticket) : ""; state.openWebRx.sessionUrl = buildOpenWebRxSessionUrl(session && session.iframeUrl ? session.iframeUrl : "", sessionTicket); state.openWebRx.sessionTicket = sessionTicket; @@ -1715,6 +1750,10 @@ async function openOpenWebRxExternal() { try { const result = await requestOpenWebRxSessionWithRetry(); const session = result && result.session ? result.session : null; + if (result && typeof result.wavelogApiKey === "string") { + state.wavelogApiKey = result.wavelogApiKey; + applyWavelogKeyToInput(); + } const sessionTicket = session && session.ticket ? String(session.ticket) : ""; const sessionUrl = buildOpenWebRxSessionUrl(session && session.iframeUrl ? String(session.iframeUrl) : "", sessionTicket); if (!sessionUrl) { diff --git a/public/i18n/de.json b/public/i18n/de.json index 616bdee..30fd217 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -52,6 +52,10 @@ "OpenWebRX laden": "OpenWebRX laden", "SDR oeffnen": "SDR oeffnen", "Session Key (Ticket):": "Session Key (Ticket):", + "Wavelog API Key (Rig-Key)": "Wavelog API Key (Rig-Key)", + "Key speichern": "Key speichern", + "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.", + "Wavelog API Key gespeichert.": "Wavelog API Key gespeichert.", "Reservierungen": "Reservierungen", "Naechsten Slot reservieren": "Naechsten Slot reservieren", "SWR Uebersicht": "SWR Uebersicht", diff --git a/public/i18n/en.json b/public/i18n/en.json index d5dc831..6f5e4e7 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -52,6 +52,10 @@ "OpenWebRX laden": "Load OpenWebRX", "SDR oeffnen": "Open SDR", "Session Key (Ticket):": "Session key (ticket):", + "Wavelog API Key (Rig-Key)": "Wavelog API Key (Rig Key)", + "Key speichern": "Save key", + "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "The station's frequency and mode are sent to your Wavelog logbook on band change.", + "Wavelog API Key gespeichert.": "Wavelog API key saved.", "Reservierungen": "Reservations", "Naechsten Slot reservieren": "Reserve next slot", "SWR Uebersicht": "SWR Overview", diff --git a/public/index.html b/public/index.html index e2c8bb8..a26bf3a 100644 --- a/public/index.html +++ b/public/index.html @@ -166,6 +166,15 @@ +
+ + +
+ +
+

+

Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.

+