import { forActiveRig, isActiveRigDecode } from "./chunk-S57W63QN.js"; import { hostCore } from "./chunk-KL66PICH.js"; // src/plugins/ftx-family.ts var bridge = window; function formatBarTime(timestampMs) { if (!timestampMs) return "--:--:--"; return new Date(timestampMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function finiteNumber(value) { const number = typeof value === "number" ? value : Number(value); return Number.isFinite(number) ? number : null; } function isAlphaNumeric(value) { return value !== void 0 && /[A-Za-z0-9]/.test(value); } function isGrid(value) { const normalized = value.trim().toUpperCase(); return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR"; } function escapeFtxHtml(input) { return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); } function extractFtxGrids(message) { return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))]; } function tokenize(message) { return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean); } function isCallsign(token) { return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token); } function extractFtxLocatorDetails(message) { const tokens = tokenize(message); const grids = extractFtxGrids(message); const gridIndex = tokens.findIndex(isGrid); const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign); const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? ""); const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null; const target = directed ? callsigns[0] ?? null : null; return grids.map((grid) => ({ grid, station: source, source, target })); } function extractFtxCallsign(message) { return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null; } function renderFtxMessage(message) { let html = ""; let index = 0; while (index < message.length) { if (!isAlphaNumeric(message[index])) { html += escapeFtxHtml(message[index] ?? ""); index += 1; continue; } let end = index + 1; while (end < message.length && isAlphaNumeric(message[end])) end += 1; const token = message.slice(index, end); const grid = token.toUpperCase(); html += isGrid(grid) ? `${grid}` : escapeFtxHtml(token); index = end; } return html; } function installFtxCompatibilityHelpers() { bridge.renderFt8Message = renderFtxMessage; bridge.ft8EscapeHtml = escapeFtxHtml; bridge.ft8ExtractLocatorDetails = extractFtxLocatorDetails; bridge.ft8ExtractAllGrids = extractFtxGrids; bridge.ft8ExtractLikelyCallsign = extractFtxCallsign; } function initializeFt8FamilyBar() { const labels = { ft8: "FT8", ft4: "FT4", ft2: "FT2" }; const builders = {}; const dismissed = { ft8: 0, ft4: 0, ft2: 0 }; const overlay = document.getElementById("ft8-bar-overlay"); let active = "ft8"; const update = () => { if (!overlay) return; const mode = (document.getElementById("mode")?.value ?? "").toUpperCase(); const result = builders[active]?.(); if (mode !== "DIG" && mode !== "USB" || !result || result.count === 0 || result.newestTsMs <= dismissed[active]) { overlay.style.display = "none"; overlay.innerHTML = ""; return; } const label = labels[active]; overlay.innerHTML = `
${result.html}`; overlay.style.display = "flex"; }; bridge.registerFt8FamilyBarRenderer = (decoder, builder) => { builders[decoder] = builder; }; bridge.setFt8FamilyBarDecoder = (decoder) => { active = decoder; update(); }; bridge.updateFt8Bar = update; bridge.clearFt8Bar = () => { bridge.trxPluginRuntime.reset(active); }; bridge.closeFt8Bar = () => { dismissed[active] = Date.now(); update(); }; } function initializeFtxDecoder(config) { const { id, label, periodMs, periodDigits = 1 } = config; const status = document.getElementById(`${id}-status`); const period = document.getElementById(`${id}-period`); const messagesElement = document.getElementById(`${id}-messages`); const filterInput = document.getElementById(`${id}-filter`); let filterText = ""; let history = []; const renderMessage = (message) => { return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message); }; const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5; const prune = () => { const cutoff = Date.now() - retentionMs(); history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff); }; const schedule = (job) => { if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job); else job(); }; const displayFrequency = (value) => { const raw = finiteNumber(value); if (raw === null) return null; const base = finiteNumber(bridge.ft8BaseHz); return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw; }; const renderRow = (message) => { const row = document.createElement("div"); row.className = "ft8-row"; const raw = message.message ?? ""; row.dataset.message = raw.toUpperCase(); row.dataset.decoder = id; const storedFrequency = finiteNumber(message.freq_hz); row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency); const snr = finiteNumber(message.snr_db); const delta = finiteNumber(message.dt_s); const frequency = displayFrequency(message.freq_hz); const timestamp = finiteNumber(message._tsMs ?? message.ts_ms); const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); row.innerHTML = `${time}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${frequency?.toFixed(0) ?? "--"}${renderMessage(raw)}`; const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw); if (station) { const log = document.createElement("button"); log.type = "button"; log.className = "ft8-log-btn"; log.textContent = "Log"; log.title = `Start a log entry for ${station}`; log.addEventListener("click", (event) => { event.stopPropagation(); const details = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw); bridge.logContact?.({ call: station, gridsquare: details.find((detail) => detail.station === station)?.grid, decoder: id }); }); row.appendChild(log); } return row; }; const render = () => { prune(); if (!messagesElement) return; const rigMessages = forActiveRig(history); const fragment = document.createDocumentFragment(); let count = 0; for (const message of rigMessages) { if (count >= 200) break; if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue; fragment.appendChild(renderRow(message)); count += 1; } messagesElement.replaceChildren(fragment); }; const plotLocator = (message) => { const raw = message.message ?? ""; const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw); const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw); if (grids.length === 0) return; const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw); const frequency = displayFrequency(message.freq_hz); bridge.mapAddLocator?.(raw, grids, id, station, { ...message, freq_hz: frequency ?? message.freq_hz, locator_details: locatorDetails }); }; const normalize = (message) => { const frequency = displayFrequency(message.freq_hz); plotLocator(message); return { // The rig that heard it, kept so the mini view can tell a decode of the // rig on screen from one a background rig made on another band. rig_id: message.rig_id ?? null, receiver: bridge.getDecodeRigMeta?.() ?? null, ts_ms: message.ts_ms, snr_db: message.snr_db, dt_s: message.dt_s, freq_hz: frequency ?? message.freq_hz, message: message.message, _tsMs: finiteNumber(message.ts_ms) ?? Date.now() }; }; const receiveBatch = (messages) => { if (messages.length === 0) return; if (status && messages.some((message) => isActiveRigDecode(message.rig_id))) { status.textContent = "Receiving"; } history = messages.map(normalize).reverse().concat(history); prune(); bridge.setFt8FamilyBarDecoder?.(id); bridge.updateFt8Bar?.(); schedule(render); }; const reset = () => { history = []; bridge.updateFt8Bar?.(); render(); bridge.clearMapMarkersByType?.(id); }; const barFrames = () => { const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5 && isActiveRigDecode(message.rig_id)).slice(0, 8); let html = ""; for (const message of recent) { const timestamp = finiteNumber(message._tsMs ?? message.ts_ms); const time = timestamp === null ? "" : ``; const snr = finiteNumber(message.snr_db); const delta = finiteNumber(message.dt_s); const frequency = displayFrequency(message.freq_hz); const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · "); html += ``; } return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html }; }; bridge.trxPluginRuntime.registerDecoder({ id, onMessage: (message) => { receiveBatch([message]); }, onBatch: receiveBatch, restore: receiveBatch, prune: () => { prune(); render(); }, reset, rerender: () => { bridge.updateFt8Bar?.(); render(); }, // Oldest first, so the map builds the grids up in the order they were heard. syncMap: () => { for (const message of [...history].reverse()) plotLocator(message); } }); bridge.registerFt8FamilyBarRenderer?.(id, barFrames); const updatePeriod = () => { if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`; }; updatePeriod(); window.setInterval(updatePeriod, 250); filterInput?.addEventListener("input", () => { filterText = filterInput.value.trim().toUpperCase(); render(); }); messagesElement?.addEventListener("click", (event) => { if (!(event.target instanceof Element)) return; const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid; if (grid) { bridge.navigateToMapLocator?.(grid.toUpperCase(), id); event.preventDefault(); } }); const toggle = document.getElementById(`${id}-decode-toggle-btn`); toggle?.addEventListener("click", () => { void (async () => { try { await bridge.takeSchedulerControlForDecoderDisable?.(toggle); await hostCore.postPath(`/toggle_${id}_decode`); } catch (error) { console.error(`${label} toggle failed`, error); } })(); }); document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => { void (async () => { if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return; try { await hostCore.postPath(`/clear_${id}_decode`); reset(); } catch (error) { console.error(`${label} history clear failed`, error); } })(); }); } export { installFtxCompatibilityHelpers, initializeFt8FamilyBar, initializeFtxDecoder };