[feat](trx-rs): expose marine mode and ft8 live bar
Add an FT8 live overlay bar, align APRS top controls with the other decoder tabs, advertise MARINE in the SoapySDR mode list, and make the VDES decoder emit raw unsynced diagnostic frames instead of dropping weak bursts outright. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -118,7 +118,7 @@ impl VdesDecoder {
|
||||
return None;
|
||||
}
|
||||
|
||||
let framed = extract_candidate_frame(&symbols)?;
|
||||
let framed = extract_candidate_frame(&symbols).unwrap_or_else(|| fallback_frame_slice(&symbols));
|
||||
let rms = burst_rms(&samples);
|
||||
let mode = classify_vdes_burst(framed.symbols.len());
|
||||
let payload_symbols = framed.payload_symbols();
|
||||
@@ -129,7 +129,7 @@ impl VdesDecoder {
|
||||
&framed,
|
||||
&mode,
|
||||
rms,
|
||||
&deinterleaved,
|
||||
&framed.symbols,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ impl VdesDecoder {
|
||||
&framed,
|
||||
&mode,
|
||||
rms,
|
||||
&deinterleaved,
|
||||
&framed.symbols,
|
||||
));
|
||||
}
|
||||
let parsed = parse_vdes_payload(&decoded_bits);
|
||||
@@ -345,14 +345,25 @@ fn extract_candidate_frame(symbols: &[u8]) -> Option<FrameSlice> {
|
||||
})
|
||||
}
|
||||
|
||||
fn fallback_frame_slice(symbols: &[u8]) -> FrameSlice {
|
||||
let take = symbols.len().min(TER_MCS1_100_BURST_SYMBOLS);
|
||||
FrameSlice {
|
||||
start_offset: 0,
|
||||
sync_score: 0.0,
|
||||
sync_errors: (TER_MCS1_100_SYNC_SYMBOLS * 2) as u8,
|
||||
phase_rotation: 0,
|
||||
symbols: symbols[..take].to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_unsynced_message(
|
||||
channel: &str,
|
||||
framed: &FrameSlice,
|
||||
mode: &BurstMode<'_>,
|
||||
rms: f32,
|
||||
deinterleaved: &[u8],
|
||||
raw_symbols: &[u8],
|
||||
) -> VdesMessage {
|
||||
let raw_bytes = pack_dibits_msb(deinterleaved);
|
||||
let raw_bytes = pack_dibits_msb(raw_symbols);
|
||||
let sync_pct = framed.sync_score * 100.0;
|
||||
VdesMessage {
|
||||
ts_ms: None,
|
||||
@@ -361,7 +372,7 @@ fn build_unsynced_message(
|
||||
repeat: 0,
|
||||
mmsi: 0,
|
||||
crc_ok: false,
|
||||
bit_len: deinterleaved.len() * 2,
|
||||
bit_len: raw_symbols.len() * 2,
|
||||
raw_bytes,
|
||||
lat: None,
|
||||
lon: None,
|
||||
|
||||
@@ -2062,6 +2062,7 @@ function render(update) {
|
||||
"Connected, listening for packets",
|
||||
);
|
||||
if (window.updateAprsBar) window.updateAprsBar();
|
||||
if (window.updateFt8Bar) window.updateFt8Bar();
|
||||
if (cwStatus && modeUpper !== "CW" && modeUpper !== "CWR" && cwStatus.textContent === "Receiving") {
|
||||
cwStatus.textContent = "Connected, listening for packets";
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<div id="rds-ps-overlay" aria-live="polite" aria-label="RDS station name"></div>
|
||||
<div id="ais-bar-overlay" aria-live="polite" aria-label="Recent AIS messages"></div>
|
||||
<div id="vdes-bar-overlay" aria-live="polite" aria-label="Recent VDES bursts"></div>
|
||||
<div id="ft8-bar-overlay" aria-live="polite" aria-label="Recent FT8 decodes"></div>
|
||||
<div id="aprs-bar-overlay" aria-live="polite" aria-label="Recent APRS frames"></div>
|
||||
</div>
|
||||
<div id="spectrum-bookmark-axis"></div>
|
||||
@@ -473,7 +474,7 @@
|
||||
<div id="vdes-messages"></div>
|
||||
</div>
|
||||
<div id="subtab-aprs" class="sub-tab-panel" style="display:none;">
|
||||
<div class="aprs-controls">
|
||||
<div class="ft8-controls aprs-controls">
|
||||
<button id="aprs-pause-btn" type="button">Pause</button>
|
||||
<button id="aprs-clear-btn" type="button">Clear</button>
|
||||
<input id="aprs-filter" class="ft8-filter" type="text" placeholder="Filter (e.g. SP2, beacon)" />
|
||||
|
||||
@@ -3,9 +3,12 @@ const ft8Status = document.getElementById("ft8-status");
|
||||
const ft8PeriodEl = document.getElementById("ft8-period");
|
||||
const ft8MessagesEl = document.getElementById("ft8-messages");
|
||||
const ft8FilterInput = document.getElementById("ft8-filter");
|
||||
const ft8BarOverlay = document.getElementById("ft8-bar-overlay");
|
||||
const FT8_MAX_MESSAGES = 200;
|
||||
const FT8_BAR_WINDOW_MS = 15 * 60 * 1000;
|
||||
const FT8_PERIOD_SECONDS = 15;
|
||||
let ft8FilterText = "";
|
||||
let ft8MessageHistory = [];
|
||||
|
||||
function fmtTime(tsMs) {
|
||||
if (!tsMs) return "--:--:--";
|
||||
@@ -40,12 +43,51 @@ function renderFt8Row(msg) {
|
||||
}
|
||||
|
||||
function addFt8Message(msg) {
|
||||
ft8MessageHistory.unshift(msg);
|
||||
if (ft8MessageHistory.length > FT8_MAX_MESSAGES) ft8MessageHistory.length = FT8_MAX_MESSAGES;
|
||||
updateFt8Bar();
|
||||
ft8MessagesEl.prepend(renderFt8Row(msg));
|
||||
while (ft8MessagesEl.children.length > FT8_MAX_MESSAGES) {
|
||||
ft8MessagesEl.removeChild(ft8MessagesEl.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function ft8BarRfText(msg) {
|
||||
const baseHz = Number.isFinite(window.ft8BaseHz) ? window.ft8BaseHz : null;
|
||||
if (!Number.isFinite(msg.freq_hz) || !Number.isFinite(baseHz)) return null;
|
||||
return `${(baseHz + msg.freq_hz).toFixed(0)} Hz`;
|
||||
}
|
||||
|
||||
function updateFt8Bar() {
|
||||
if (!ft8BarOverlay) return;
|
||||
const modeUpper = (document.getElementById("mode")?.value || "").toUpperCase();
|
||||
const isFt8Mode = modeUpper === "DIG" || modeUpper === "USB";
|
||||
const cutoffMs = Date.now() - FT8_BAR_WINDOW_MS;
|
||||
const messages = ft8MessageHistory.filter((msg) => Number(msg.ts_ms) >= cutoffMs).slice(0, 8);
|
||||
if (!isFt8Mode || messages.length === 0) {
|
||||
ft8BarOverlay.style.display = "none";
|
||||
ft8BarOverlay.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">FT8</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearFt8Bar()" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();window.clearFt8Bar();}" aria-label="Clear FT8 overlay">Clear</span></span><span class="aprs-bar-window">Last 15 minutes</span></div>';
|
||||
for (const msg of messages) {
|
||||
const ts = msg.ts_ms ? `<span class="aprs-bar-time">${fmtTime(msg.ts_ms)}</span>` : "";
|
||||
const snr = Number.isFinite(msg.snr_db) ? `${msg.snr_db.toFixed(1)} dB` : "-- dB";
|
||||
const dt = Number.isFinite(msg.dt_s) ? `dt ${msg.dt_s.toFixed(2)}` : null;
|
||||
const rf = ft8BarRfText(msg);
|
||||
const detail = [snr, dt, rf].filter(Boolean).join(" · ");
|
||||
const text = escapeHtml((msg.message || "").toString());
|
||||
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${ts}<span class="aprs-bar-call">${text}</span>${detail ? ` · ${detail}` : ""}</div></div>`;
|
||||
}
|
||||
ft8BarOverlay.innerHTML = html;
|
||||
ft8BarOverlay.style.display = "flex";
|
||||
}
|
||||
window.updateFt8Bar = updateFt8Bar;
|
||||
window.clearFt8Bar = function() {
|
||||
document.getElementById("ft8-clear-btn")?.click();
|
||||
};
|
||||
|
||||
function renderFt8Message(message) {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
@@ -145,10 +187,13 @@ function updateFt8RowRf(row) {
|
||||
window.updateFt8RfDisplay = function() {
|
||||
const rows = ft8MessagesEl.querySelectorAll(".ft8-row");
|
||||
rows.forEach((row) => updateFt8RowRf(row));
|
||||
updateFt8Bar();
|
||||
};
|
||||
|
||||
window.resetFt8HistoryView = function() {
|
||||
ft8MessagesEl.innerHTML = "";
|
||||
ft8MessageHistory = [];
|
||||
updateFt8Bar();
|
||||
if (window.clearMapMarkersByType) window.clearMapMarkersByType("ft8");
|
||||
};
|
||||
|
||||
|
||||
@@ -631,7 +631,8 @@ small { color: var(--text-muted); }
|
||||
}
|
||||
#aprs-bar-overlay,
|
||||
#ais-bar-overlay,
|
||||
#vdes-bar-overlay {
|
||||
#vdes-bar-overlay,
|
||||
#ft8-bar-overlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -1100,22 +1101,6 @@ small { color: var(--text-muted); }
|
||||
border-radius: 6px;
|
||||
}
|
||||
.aprs-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
|
||||
#subtab-aprs .aprs-controls > button {
|
||||
min-height: 1.65rem;
|
||||
padding: 0.08rem 0.5rem;
|
||||
border-radius: 0.4rem;
|
||||
border: 1px solid var(--filter-border);
|
||||
background: var(--filter-bg);
|
||||
color: var(--filter-fg);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
cursor: pointer;
|
||||
}
|
||||
#subtab-aprs .aprs-controls > button:hover {
|
||||
border-color: var(--accent-green);
|
||||
color: var(--text);
|
||||
}
|
||||
.aprs-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
||||
@@ -194,6 +194,7 @@ impl SoapySdrRig {
|
||||
RigMode::FM,
|
||||
RigMode::AIS,
|
||||
RigMode::VDES,
|
||||
RigMode::MARINE,
|
||||
RigMode::DIG,
|
||||
RigMode::PKT,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user