Compare commits
3 Commits
2bcf6ab1e5
...
fix/wavelo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
061045999b | ||
| 4ced7418cd | |||
|
|
0488f6e25d |
@@ -86,3 +86,12 @@ MICROHAM_PTT_DOWN_CMD=
|
|||||||
MICROHAM_PTT_UP_CMD=
|
MICROHAM_PTT_UP_CMD=
|
||||||
OPENWEBRX_ACCESS_POLICY_FILE=./data/openwebrx-access-policy.txt
|
OPENWEBRX_ACCESS_POLICY_FILE=./data/openwebrx-access-policy.txt
|
||||||
OPENWEBRX_PERSISTENT_USERS_FILE=./data/openwebrx-persistent-users.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
|
||||||
|
# Min interval between live-tuning pushes per operator (ms, trailing-debounced).
|
||||||
|
WAVELOG_MIN_INTERVAL_MS=3000
|
||||||
|
|||||||
@@ -18,3 +18,4 @@
|
|||||||
- `rms-vswr-report-reader.md`
|
- `rms-vswr-report-reader.md`
|
||||||
- `rms-vswr-nanovna.md`
|
- `rms-vswr-nanovna.md`
|
||||||
- `rms-station-shell.md`
|
- `rms-station-shell.md`
|
||||||
|
- `rms-wavelog-cat.md`
|
||||||
|
|||||||
22
docs/plugins/rms-wavelog-cat.md
Normal file
22
docs/plugins/rms-wavelog-cat.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# rms.wavelog.cat
|
||||||
|
|
||||||
|
- Zweck: Aktuelle Stationsfrequenz/-mode an ein Wavelog-Logbuch melden (CAT Radio API).
|
||||||
|
Pushed `POST <baseUrl>/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:
|
||||||
|
- **Bandwechsel** (`/v1/openwebrx/bands/select`): sofortiger Push der Band-Mittenfrequenz
|
||||||
|
(`centerFreqHz` + `startMod`).
|
||||||
|
- **Live-Tuning in OpenWebRX** (`/v1/openwebrx/plugin/live-state`): Push der tatsaechlich getunten
|
||||||
|
Frequenz/Mode des aktiven Operators, **gedrosselt** (Trailing-Debounce, `WAVELOG_MIN_INTERVAL_MS`,
|
||||||
|
Default 3000 ms) damit Feintunen die API nicht flutet.
|
||||||
|
- Beides best-effort: Fehler brechen den jeweiligen Flow 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).
|
||||||
116
plugins/rms.wavelog.cat/index.js
Normal file
116
plugins/rms.wavelog.cat/index.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// rms.wavelog.cat
|
||||||
|
//
|
||||||
|
// Publishes the station's current frequency/mode to a Wavelog logbook via its
|
||||||
|
// CAT radio API (`POST <baseUrl>/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
|
||||||
|
};
|
||||||
18
plugins/rms.wavelog.cat/manifest.json
Normal file
18
plugins/rms.wavelog.cat/manifest.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,6 +135,9 @@ const els = {
|
|||||||
openwebrxSessionLink: document.getElementById("openwebrxSessionLink"),
|
openwebrxSessionLink: document.getElementById("openwebrxSessionLink"),
|
||||||
openwebrxCopyLinkBtn: document.getElementById("openwebrxCopyLinkBtn"),
|
openwebrxCopyLinkBtn: document.getElementById("openwebrxCopyLinkBtn"),
|
||||||
openwebrxSessionTicket: document.getElementById("openwebrxSessionTicket"),
|
openwebrxSessionTicket: document.getElementById("openwebrxSessionTicket"),
|
||||||
|
wavelogKeyInput: document.getElementById("wavelogKeyInput"),
|
||||||
|
wavelogKeySaveBtn: document.getElementById("wavelogKeySaveBtn"),
|
||||||
|
wavelogKeyMessage: document.getElementById("wavelogKeyMessage"),
|
||||||
openwebrxTxStatePill: document.getElementById("openwebrxTxStatePill"),
|
openwebrxTxStatePill: document.getElementById("openwebrxTxStatePill"),
|
||||||
rotorCurrent: document.getElementById("rotorCurrent"),
|
rotorCurrent: document.getElementById("rotorCurrent"),
|
||||||
rotorCompass: document.getElementById("rotorCompass"),
|
rotorCompass: document.getElementById("rotorCompass"),
|
||||||
@@ -690,6 +693,11 @@ function bindEvents() {
|
|||||||
await copyOpenWebRxLink();
|
await copyOpenWebRxLink();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (els.wavelogKeySaveBtn) {
|
||||||
|
els.wavelogKeySaveBtn.addEventListener("click", async () => {
|
||||||
|
await saveWavelogKey();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (els.openwebrxEnableTxBtn) {
|
if (els.openwebrxEnableTxBtn) {
|
||||||
els.openwebrxEnableTxBtn.addEventListener("click", async (event) => {
|
els.openwebrxEnableTxBtn.addEventListener("click", async (event) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
@@ -1496,6 +1504,8 @@ async function refreshCurrentUser() {
|
|||||||
try {
|
try {
|
||||||
const result = await api("/v1/me", { authRequired: true });
|
const result = await api("/v1/me", { authRequired: true });
|
||||||
state.user = result.user;
|
state.user = result.user;
|
||||||
|
state.wavelogApiKey = typeof result.wavelogApiKey === "string" ? result.wavelogApiKey : "";
|
||||||
|
applyWavelogKeyToInput();
|
||||||
renderUserSettingsAuthMethods();
|
renderUserSettingsAuthMethods();
|
||||||
await ensureLanguageFromUserPreference();
|
await ensureLanguageFromUserPreference();
|
||||||
} catch {
|
} 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() {
|
async function openOpenWebRxSession() {
|
||||||
clearMessages("status");
|
clearMessages("status");
|
||||||
setOpenWebRxBusy(true);
|
setOpenWebRxBusy(true);
|
||||||
try {
|
try {
|
||||||
const result = await requestOpenWebRxSessionWithRetry();
|
const result = await requestOpenWebRxSessionWithRetry();
|
||||||
const session = result && result.session ? result.session : null;
|
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 sessionTicket = session && session.ticket ? String(session.ticket) : "";
|
||||||
state.openWebRx.sessionUrl = buildOpenWebRxSessionUrl(session && session.iframeUrl ? session.iframeUrl : "", sessionTicket);
|
state.openWebRx.sessionUrl = buildOpenWebRxSessionUrl(session && session.iframeUrl ? session.iframeUrl : "", sessionTicket);
|
||||||
state.openWebRx.sessionTicket = sessionTicket;
|
state.openWebRx.sessionTicket = sessionTicket;
|
||||||
@@ -1715,6 +1750,10 @@ async function openOpenWebRxExternal() {
|
|||||||
try {
|
try {
|
||||||
const result = await requestOpenWebRxSessionWithRetry();
|
const result = await requestOpenWebRxSessionWithRetry();
|
||||||
const session = result && result.session ? result.session : null;
|
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 sessionTicket = session && session.ticket ? String(session.ticket) : "";
|
||||||
const sessionUrl = buildOpenWebRxSessionUrl(session && session.iframeUrl ? String(session.iframeUrl) : "", sessionTicket);
|
const sessionUrl = buildOpenWebRxSessionUrl(session && session.iframeUrl ? String(session.iframeUrl) : "", sessionTicket);
|
||||||
if (!sessionUrl) {
|
if (!sessionUrl) {
|
||||||
|
|||||||
@@ -52,6 +52,10 @@
|
|||||||
"OpenWebRX laden": "OpenWebRX laden",
|
"OpenWebRX laden": "OpenWebRX laden",
|
||||||
"SDR oeffnen": "SDR oeffnen",
|
"SDR oeffnen": "SDR oeffnen",
|
||||||
"Session Key (Ticket):": "Session Key (Ticket):",
|
"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",
|
"Reservierungen": "Reservierungen",
|
||||||
"Naechsten Slot reservieren": "Naechsten Slot reservieren",
|
"Naechsten Slot reservieren": "Naechsten Slot reservieren",
|
||||||
"SWR Uebersicht": "SWR Uebersicht",
|
"SWR Uebersicht": "SWR Uebersicht",
|
||||||
|
|||||||
@@ -52,6 +52,10 @@
|
|||||||
"OpenWebRX laden": "Load OpenWebRX",
|
"OpenWebRX laden": "Load OpenWebRX",
|
||||||
"SDR oeffnen": "Open SDR",
|
"SDR oeffnen": "Open SDR",
|
||||||
"Session Key (Ticket):": "Session key (ticket):",
|
"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",
|
"Reservierungen": "Reservations",
|
||||||
"Naechsten Slot reservieren": "Reserve next slot",
|
"Naechsten Slot reservieren": "Reserve next slot",
|
||||||
"SWR Uebersicht": "SWR Overview",
|
"SWR Uebersicht": "SWR Overview",
|
||||||
|
|||||||
@@ -166,6 +166,15 @@
|
|||||||
<div id="openwebrxSessionAccess" class="stack" hidden>
|
<div id="openwebrxSessionAccess" class="stack" hidden>
|
||||||
<p class="muted">Session Key (Ticket): <code id="openwebrxSessionTicket">-</code></p>
|
<p class="muted">Session Key (Ticket): <code id="openwebrxSessionTicket">-</code></p>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="wavelogKeyBox" class="stack">
|
||||||
|
<label for="wavelogKeyInput">Wavelog API Key (Rig-Key)</label>
|
||||||
|
<input id="wavelogKeyInput" type="text" autocomplete="off" spellcheck="false" placeholder="Wavelog API Key" />
|
||||||
|
<div class="actions">
|
||||||
|
<button id="wavelogKeySaveBtn" type="button" class="ghost-btn">Key speichern</button>
|
||||||
|
</div>
|
||||||
|
<p id="wavelogKeyMessage" class="message"></p>
|
||||||
|
<p class="muted">Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="reservationPanel" class="links-wrap" hidden>
|
<section id="reservationPanel" class="links-wrap" hidden>
|
||||||
|
|||||||
124
server/index.js
124
server/index.js
@@ -44,6 +44,7 @@ const config = {
|
|||||||
actionRateLimit: Number(process.env.ACTION_RATE_LIMIT || 30),
|
actionRateLimit: Number(process.env.ACTION_RATE_LIMIT || 30),
|
||||||
openWebRxPttLiveFreqToleranceHz: Number(process.env.OPENWEBRX_PTT_LIVE_FREQ_TOLERANCE_HZ || 50),
|
openWebRxPttLiveFreqToleranceHz: Number(process.env.OPENWEBRX_PTT_LIVE_FREQ_TOLERANCE_HZ || 50),
|
||||||
openWebRxLiveStateTtlMs: Number(process.env.OPENWEBRX_LIVE_STATE_TTL_MS || 10000),
|
openWebRxLiveStateTtlMs: Number(process.env.OPENWEBRX_LIVE_STATE_TTL_MS || 10000),
|
||||||
|
wavelogMinIntervalMs: Number(process.env.WAVELOG_MIN_INTERVAL_MS || 3000),
|
||||||
openWebRxPttBlockedBandConfigIdsRaw: String(process.env.OPENWEBRX_PTT_BLOCKED_BAND_CONFIG_IDS || "27,6,24"),
|
openWebRxPttBlockedBandConfigIdsRaw: String(process.env.OPENWEBRX_PTT_BLOCKED_BAND_CONFIG_IDS || "27,6,24"),
|
||||||
openWebRxTxPollMs: Number(process.env.OPENWEBRX_TX_POLL_MS || 3000),
|
openWebRxTxPollMs: Number(process.env.OPENWEBRX_TX_POLL_MS || 3000),
|
||||||
txAudioEnabled: String(process.env.TX_AUDIO_ENABLED || "true").trim().toLowerCase(),
|
txAudioEnabled: String(process.env.TX_AUDIO_ENABLED || "true").trim().toLowerCase(),
|
||||||
@@ -196,6 +197,7 @@ const runtime = {
|
|||||||
lastEnsureSdrAtMs: 0
|
lastEnsureSdrAtMs: 0
|
||||||
},
|
},
|
||||||
openWebRxLiveStateByUserId: {},
|
openWebRxLiveStateByUserId: {},
|
||||||
|
wavelogCatByUserId: {},
|
||||||
rotor: {
|
rotor: {
|
||||||
azimuth: null,
|
azimuth: null,
|
||||||
rawAzimuth: null,
|
rawAzimuth: null,
|
||||||
@@ -358,7 +360,7 @@ async function routeRequest(req, res) {
|
|||||||
if (method === "GET" && url.pathname === "/v1/me") {
|
if (method === "GET" && url.pathname === "/v1/me") {
|
||||||
const auth = requireAuth(req, res);
|
const auth = requireAuth(req, res);
|
||||||
if (!auth) return;
|
if (!auth) return;
|
||||||
return sendJson(res, 200, { user: sanitizeUser(auth.user), capabilities: userCapabilities(auth.user) });
|
return sendJson(res, 200, { user: sanitizeUser(auth.user), wavelogApiKey: auth.user.wavelogApiKey || "", capabilities: userCapabilities(auth.user) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === "GET" && url.pathname === "/v1/auth/session/diag") {
|
if (method === "GET" && url.pathname === "/v1/auth/session/diag") {
|
||||||
@@ -387,6 +389,13 @@ async function routeRequest(req, res) {
|
|||||||
return handleSelfLanguageUpdate(res, auth.user, body);
|
return handleSelfLanguageUpdate(res, auth.user, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === "PUT" && url.pathname === "/v1/me/wavelog-key") {
|
||||||
|
const auth = requireAuth(req, res);
|
||||||
|
if (!auth) return;
|
||||||
|
const body = await readJsonBody(req);
|
||||||
|
return handleSelfWavelogKeyUpdate(res, auth.user, body);
|
||||||
|
}
|
||||||
|
|
||||||
if (method === "GET" && url.pathname === "/v1/station/status") {
|
if (method === "GET" && url.pathname === "/v1/station/status") {
|
||||||
const auth = requireAuth(req, res);
|
const auth = requireAuth(req, res);
|
||||||
if (!auth) return;
|
if (!auth) return;
|
||||||
@@ -1547,6 +1556,22 @@ async function handleSelfLanguageUpdate(res, user, body) {
|
|||||||
return sendJson(res, 200, { ok: true, user: sanitizeUser(user) });
|
return sendJson(res, 200, { ok: true, user: sanitizeUser(user) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSelfWavelogKeyUpdate(res, user, body) {
|
||||||
|
const raw = body && typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
||||||
|
if (raw.length > 128) {
|
||||||
|
return sendError(res, 400, "user.wavelog_key.invalid", "API Key zu lang");
|
||||||
|
}
|
||||||
|
// Allow empty (clears the key). Otherwise keep it to a conservative token charset.
|
||||||
|
if (raw && !/^[A-Za-z0-9._-]+$/.test(raw)) {
|
||||||
|
return sendError(res, 400, "user.wavelog_key.invalid", "API Key enthaelt ungueltige Zeichen");
|
||||||
|
}
|
||||||
|
user.wavelogApiKey = raw;
|
||||||
|
await writeJson(files.users, runtime.users);
|
||||||
|
// Audit the change WITHOUT storing the key value itself.
|
||||||
|
await appendAudit("user.wavelog_key.update", user, { set: Boolean(raw) });
|
||||||
|
return sendJson(res, 200, { ok: true, wavelogApiKey: raw });
|
||||||
|
}
|
||||||
|
|
||||||
async function handleAdminUserAuthMethodsUpdate(res, actor, pathname, body) {
|
async function handleAdminUserAuthMethodsUpdate(res, actor, pathname, body) {
|
||||||
const match = pathname.match(/^\/v1\/admin\/users\/([^/]+)\/auth-methods$/);
|
const match = pathname.match(/^\/v1\/admin\/users\/([^/]+)\/auth-methods$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
@@ -3615,7 +3640,8 @@ async function handleOpenWebRxSessionIssue(res, user) {
|
|||||||
|
|
||||||
return sendJson(res, 200, {
|
return sendJson(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
session: result
|
session: result,
|
||||||
|
wavelogApiKey: user.wavelogApiKey || ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3682,6 +3708,7 @@ async function handleOpenWebRxBandSelect(res, user, body) {
|
|||||||
runtime.txFollowRoute = antennaRoute;
|
runtime.txFollowRoute = antennaRoute;
|
||||||
runtime.openWebRxAntennaRoute = antennaRoute;
|
runtime.openWebRxAntennaRoute = antennaRoute;
|
||||||
await appendAudit("openwebrx.band.set", user, { band, result: { centerFreqHz: result.centerFreqHz || null, skipped: Boolean(result.skipped) } });
|
await appendAudit("openwebrx.band.set", user, { band, result: { centerFreqHz: result.centerFreqHz || null, skipped: Boolean(result.skipped) } });
|
||||||
|
await maybePublishWavelogCat(user, result);
|
||||||
return sendJson(res, 200, { ok: true, result });
|
return sendJson(res, 200, { ok: true, result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = String(error && error.message ? error.message : error);
|
const message = String(error && error.message ? error.message : error);
|
||||||
@@ -3692,6 +3719,98 @@ async function handleOpenWebRxBandSelect(res, user, body) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push a frequency/mode to the active operator's Wavelog logbook (CAT API).
|
||||||
|
// Best-effort: never throws into the caller's flow.
|
||||||
|
async function sendWavelogCat(user, frequencyHz, mode) {
|
||||||
|
try {
|
||||||
|
if (!user || !user.wavelogApiKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!runtime.pluginState.enabled["rms.wavelog.cat"]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!runtime.pluginState.providers["logging.cat.publish"]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const freq = Number(frequencyHz);
|
||||||
|
if (!Number.isFinite(freq) || freq <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const outcome = await executeCapability("logging.cat.publish", "publish", {
|
||||||
|
frequencyHz: freq,
|
||||||
|
mode: mode || null,
|
||||||
|
apiKey: user.wavelogApiKey
|
||||||
|
}, { user, skipTxSafety: true });
|
||||||
|
if (!outcome || outcome.ok !== true) {
|
||||||
|
console.warn("wavelog.cat publish not ok", {
|
||||||
|
userId: user.id,
|
||||||
|
reason: outcome && (outcome.reason || outcome.error || outcome.status)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("wavelog.cat publish error", {
|
||||||
|
userId: user.id,
|
||||||
|
error: String(error && error.message ? error.message : error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Band select: push the band center immediately (best-effort).
|
||||||
|
async function maybePublishWavelogCat(user, bandResult) {
|
||||||
|
await sendWavelogCat(user, bandResult && bandResult.centerFreqHz, bandResult && bandResult.startMod ? bandResult.startMod : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live tuning in OpenWebRX: push the actual tuned frequency/mode, throttled so
|
||||||
|
// fine-tuning doesn't flood the Wavelog API. Trailing-edge: the latest value is
|
||||||
|
// always sent once the throttle window elapses.
|
||||||
|
function scheduleWavelogCatLive(user, frequencyHz, mode) {
|
||||||
|
try {
|
||||||
|
if (!user || !user.wavelogApiKey || !runtime.pluginState.enabled["rms.wavelog.cat"]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const freq = Number(frequencyHz);
|
||||||
|
if (!Number.isFinite(freq) || freq <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = String(user.id || "");
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const minIntervalMs = Number.isFinite(config.wavelogMinIntervalMs) ? Math.max(500, config.wavelogMinIntervalMs) : 3000;
|
||||||
|
const rec = runtime.wavelogCatByUserId[key] || { lastFreq: null, lastMode: null, lastPushMs: 0, timer: null };
|
||||||
|
runtime.wavelogCatByUserId[key] = rec;
|
||||||
|
rec.pendingFreq = freq;
|
||||||
|
rec.pendingMode = mode || null;
|
||||||
|
rec.pendingUser = user;
|
||||||
|
if (rec.lastFreq === freq && rec.lastMode === (mode || null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fire = () => {
|
||||||
|
rec.timer = null;
|
||||||
|
rec.lastFreq = rec.pendingFreq;
|
||||||
|
rec.lastMode = rec.pendingMode;
|
||||||
|
rec.lastPushMs = Date.now();
|
||||||
|
sendWavelogCat(rec.pendingUser, rec.pendingFreq, rec.pendingMode);
|
||||||
|
};
|
||||||
|
const elapsed = Date.now() - (rec.lastPushMs || 0);
|
||||||
|
if (!rec.timer && elapsed >= minIntervalMs) {
|
||||||
|
fire();
|
||||||
|
} else if (!rec.timer) {
|
||||||
|
rec.timer = setTimeout(fire, Math.max(250, minIntervalMs - elapsed));
|
||||||
|
if (rec.timer && typeof rec.timer.unref === "function") {
|
||||||
|
rec.timer.unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If a timer is already pending, pendingFreq/Mode were updated above so the
|
||||||
|
// trailing push sends the latest tuned frequency.
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("wavelog.cat live schedule error", {
|
||||||
|
userId: user && user.id,
|
||||||
|
error: String(error && error.message ? error.message : error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleOpenWebRxTxEnable(res, user) {
|
async function handleOpenWebRxTxEnable(res, user) {
|
||||||
if (!runtime.station.isInUse || runtime.station.activeByUserId !== user.id) {
|
if (!runtime.station.isInUse || runtime.station.activeByUserId !== user.id) {
|
||||||
return sendError(res, 403, "openwebrx.not_owner", "OpenWebRX TX nur fuer aktiven Stationsbenutzer");
|
return sendError(res, 403, "openwebrx.not_owner", "OpenWebRX TX nur fuer aktiven Stationsbenutzer");
|
||||||
@@ -4972,6 +5091,7 @@ async function handleOpenWebRxPluginLiveState(req, res, url, body) {
|
|||||||
updatedAtMs: Date.now(),
|
updatedAtMs: Date.now(),
|
||||||
source: "plugin"
|
source: "plugin"
|
||||||
});
|
});
|
||||||
|
scheduleWavelogCatLive(user, parsed.frequencyHz, parsed.mode);
|
||||||
return sendJson(res, 200, {
|
return sendJson(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
liveState: {
|
liveState: {
|
||||||
|
|||||||
29
test/wavelog-cat.test.js
Normal file
29
test/wavelog-cat.test.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
|
||||||
|
const { toAdifMode } = require("../plugins/rms.wavelog.cat/index.js");
|
||||||
|
|
||||||
|
test("toAdifMode maps phone submodes to SSB", () => {
|
||||||
|
assert.strictEqual(toAdifMode("usb"), "SSB");
|
||||||
|
assert.strictEqual(toAdifMode("LSB"), "SSB");
|
||||||
|
assert.strictEqual(toAdifMode("ssb"), "SSB");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("toAdifMode maps cw/fm/am variants", () => {
|
||||||
|
assert.strictEqual(toAdifMode("cw"), "CW");
|
||||||
|
assert.strictEqual(toAdifMode("cwr"), "CW");
|
||||||
|
assert.strictEqual(toAdifMode("nfm"), "FM");
|
||||||
|
assert.strictEqual(toAdifMode("fm"), "FM");
|
||||||
|
assert.strictEqual(toAdifMode("am"), "AM");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("toAdifMode passes unknown modes through in upper case", () => {
|
||||||
|
assert.strictEqual(toAdifMode("ft8"), "FT8");
|
||||||
|
assert.strictEqual(toAdifMode("c4fm"), "C4FM");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("toAdifMode returns empty string for empty input", () => {
|
||||||
|
assert.strictEqual(toAdifMode(""), "");
|
||||||
|
assert.strictEqual(toAdifMode(null), "");
|
||||||
|
assert.strictEqual(toAdifMode(undefined), "");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user