[feat](trx-rs): server-side decode and aprs byte rendering
Co-authored-by: Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -70,8 +70,6 @@ let supportedModes = [];
|
|||||||
let supportedBands = [];
|
let supportedBands = [];
|
||||||
let freqDirty = false;
|
let freqDirty = false;
|
||||||
let modeDirty = false;
|
let modeDirty = false;
|
||||||
let aprsAutoToggleInFlight = false;
|
|
||||||
let cwAutoToggleInFlight = false;
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let lastEventAt = Date.now();
|
let lastEventAt = Date.now();
|
||||||
let es;
|
let es;
|
||||||
@@ -247,24 +245,6 @@ function render(update) {
|
|||||||
const mode = normalizeMode(update.status.mode);
|
const mode = normalizeMode(update.status.mode);
|
||||||
modeEl.value = mode ? mode.toUpperCase() : "";
|
modeEl.value = mode ? mode.toUpperCase() : "";
|
||||||
}
|
}
|
||||||
const currentMode = update.status && update.status.mode ? normalizeMode(update.status.mode) : "";
|
|
||||||
const modeUpper = currentMode ? currentMode.toUpperCase() : "";
|
|
||||||
const aprsDesired = modeUpper === "PKT";
|
|
||||||
const cwDesired = modeUpper === "CW" || modeUpper === "CWR";
|
|
||||||
const aprsEnabled = !!update.aprs_decode_enabled;
|
|
||||||
const cwEnabled = !!update.cw_decode_enabled;
|
|
||||||
if (aprsDesired !== aprsEnabled && !aprsAutoToggleInFlight) {
|
|
||||||
aprsAutoToggleInFlight = true;
|
|
||||||
postPath("/toggle_aprs_decode")
|
|
||||||
.catch((e) => console.error("APRS auto-toggle failed", e))
|
|
||||||
.finally(() => { aprsAutoToggleInFlight = false; });
|
|
||||||
}
|
|
||||||
if (cwDesired !== cwEnabled && !cwAutoToggleInFlight) {
|
|
||||||
cwAutoToggleInFlight = true;
|
|
||||||
postPath("/toggle_cw_decode")
|
|
||||||
.catch((e) => console.error("CW auto-toggle failed", e))
|
|
||||||
.finally(() => { cwAutoToggleInFlight = false; });
|
|
||||||
}
|
|
||||||
if (update.status && typeof update.status.tx_en === "boolean") {
|
if (update.status && typeof update.status.tx_en === "boolean") {
|
||||||
lastTxEn = update.status.tx_en;
|
lastTxEn = update.status.tx_en;
|
||||||
pttBtn.textContent = update.status.tx_en ? "PTT On" : "PTT Off";
|
pttBtn.textContent = update.status.tx_en ? "PTT On" : "PTT Off";
|
||||||
|
|||||||
@@ -6,7 +6,27 @@ const APRS_MAX_PACKETS = 100;
|
|||||||
// Persistent packet history
|
// Persistent packet history
|
||||||
let aprsPacketHistory = loadSetting("aprsPackets", []);
|
let aprsPacketHistory = loadSetting("aprsPackets", []);
|
||||||
|
|
||||||
function escapeAprsInfo(str) {
|
function renderAprsInfo(pkt) {
|
||||||
|
const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null;
|
||||||
|
if (bytes && bytes.length > 0) {
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
const b = bytes[i];
|
||||||
|
if (b >= 0x20 && b <= 0x7e) {
|
||||||
|
const ch = String.fromCharCode(b);
|
||||||
|
if (ch === "<") out += "<";
|
||||||
|
else if (ch === ">") out += ">";
|
||||||
|
else if (ch === "&") out += "&";
|
||||||
|
else if (ch === '"') out += """;
|
||||||
|
else out += ch;
|
||||||
|
} else {
|
||||||
|
const hex = b.toString(16).toUpperCase().padStart(2, "0");
|
||||||
|
out += `<span class="aprs-byte">0x${hex}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const str = pkt.info || "";
|
||||||
let out = "";
|
let out = "";
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
const code = str.charCodeAt(i);
|
const code = str.charCodeAt(i);
|
||||||
@@ -19,7 +39,7 @@ function escapeAprsInfo(str) {
|
|||||||
else out += ch;
|
else out += ch;
|
||||||
} else {
|
} else {
|
||||||
const hex = code.toString(16).toUpperCase().padStart(2, "0");
|
const hex = code.toString(16).toUpperCase().padStart(2, "0");
|
||||||
out += `<span style="color:var(--accent-yellow);">[0x${hex}]</span>`;
|
out += `<span class="aprs-byte">0x${hex}</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -46,7 +66,7 @@ function renderAprsRow(pkt) {
|
|||||||
const osmUrl = `https://www.openstreetmap.org/?mlat=${pkt.lat}&mlon=${pkt.lon}#map=15/${pkt.lat}/${pkt.lon}`;
|
const osmUrl = `https://www.openstreetmap.org/?mlat=${pkt.lat}&mlon=${pkt.lon}#map=15/${pkt.lat}/${pkt.lon}`;
|
||||||
posHtml = ` <a class="aprs-pos" href="${osmUrl}" target="_blank">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>`;
|
posHtml = ` <a class="aprs-pos" href="${osmUrl}" target="_blank">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>`;
|
||||||
}
|
}
|
||||||
row.innerHTML = `<span class="aprs-time">${ts}</span>${symbolHtml}<span class="aprs-call">${pkt.srcCall}</span>>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: <span title="${pkt.type}">${escapeAprsInfo(pkt.info)}</span>${posHtml}${crcTag}`;
|
row.innerHTML = `<span class="aprs-time">${ts}</span>${symbolHtml}<span class="aprs-call">${pkt.srcCall}</span>>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: <span title="${pkt.type}">${renderAprsInfo(pkt)}</span>${posHtml}${crcTag}`;
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +116,7 @@ window.onServerAprs = function(pkt) {
|
|||||||
destCall: pkt.dest_call,
|
destCall: pkt.dest_call,
|
||||||
path: pkt.path,
|
path: pkt.path,
|
||||||
info: pkt.info,
|
info: pkt.info,
|
||||||
|
info_bytes: pkt.info_bytes,
|
||||||
type: pkt.packet_type,
|
type: pkt.packet_type,
|
||||||
crcOk: pkt.crc_ok,
|
crcOk: pkt.crc_ok,
|
||||||
lat: pkt.lat,
|
lat: pkt.lat,
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ small { color: var(--text-muted); }
|
|||||||
.aprs-symbol { display: inline-block; width: 24px; height: 24px; background-size: 384px 192px; vertical-align: middle; margin-right: 0.3rem; }
|
.aprs-symbol { display: inline-block; width: 24px; height: 24px; background-size: 384px 192px; vertical-align: middle; margin-right: 0.3rem; }
|
||||||
.aprs-pos { color: var(--accent-green); text-decoration: none; margin-left: 0.3rem; font-size: 0.8rem; }
|
.aprs-pos { color: var(--accent-green); text-decoration: none; margin-left: 0.3rem; font-size: 0.8rem; }
|
||||||
.aprs-pos:hover { text-decoration: underline; }
|
.aprs-pos:hover { text-decoration: underline; }
|
||||||
|
.aprs-byte { color: var(--accent-yellow); background: rgba(255, 214, 0, 0.12); border: 1px solid rgba(255, 214, 0, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-size: 0.78em; }
|
||||||
|
|
||||||
.cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
|
.cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
|
||||||
.cw-config { display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; margin-bottom: 0.75rem; }
|
.cw-config { display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; margin-bottom: 0.75rem; }
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub struct AprsPacket {
|
|||||||
pub dest_call: String,
|
pub dest_call: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub info: String,
|
pub info: String,
|
||||||
|
pub info_bytes: Vec<u8>,
|
||||||
pub packet_type: String,
|
pub packet_type: String,
|
||||||
pub crc_ok: bool,
|
pub crc_ok: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|||||||
@@ -328,8 +328,7 @@ pub async fn run_aprs_decoder(
|
|||||||
Ok(frame) => {
|
Ok(frame) => {
|
||||||
let state = state_rx.borrow().clone();
|
let state = state_rx.borrow().clone();
|
||||||
let mode = &state.status.mode;
|
let mode = &state.status.mode;
|
||||||
let enabled = state.aprs_decode_enabled;
|
let active = matches!(mode, RigMode::PKT);
|
||||||
let active = enabled && matches!(mode, RigMode::PKT);
|
|
||||||
|
|
||||||
// Check for reset request
|
// Check for reset request
|
||||||
if state.aprs_decode_reset_seq != last_reset_seq {
|
if state.aprs_decode_reset_seq != last_reset_seq {
|
||||||
@@ -390,8 +389,7 @@ pub async fn run_cw_decoder(
|
|||||||
Ok(frame) => {
|
Ok(frame) => {
|
||||||
let state = state_rx.borrow().clone();
|
let state = state_rx.borrow().clone();
|
||||||
let mode = &state.status.mode;
|
let mode = &state.status.mode;
|
||||||
let enabled = state.cw_decode_enabled;
|
let active = matches!(mode, RigMode::CW | RigMode::CWR);
|
||||||
let active = enabled && matches!(mode, RigMode::CW | RigMode::CWR);
|
|
||||||
|
|
||||||
// Check for reset request
|
// Check for reset request
|
||||||
if state.cw_decode_reset_seq != last_reset_seq {
|
if state.cw_decode_reset_seq != last_reset_seq {
|
||||||
|
|||||||
@@ -421,6 +421,7 @@ fn parse_aprs(ax25: &Ax25Frame) -> AprsPacket {
|
|||||||
dest_call,
|
dest_call,
|
||||||
path,
|
path,
|
||||||
info: info_str,
|
info: info_str,
|
||||||
|
info_bytes: info.to_vec(),
|
||||||
packet_type: packet_type.to_string(),
|
packet_type: packet_type.to_string(),
|
||||||
crc_ok: false, // set by caller
|
crc_ok: false, // set by caller
|
||||||
lat,
|
lat,
|
||||||
|
|||||||
Reference in New Issue
Block a user