[feat](trx-rs): add FT2 decoder support (wired to FT4)
Mirrors the FT4 implementation across the full stack. The trx-ft8 crate wires Ft8Decoder::new_ft2() to FTX_PROTOCOL_FT4 as a placeholder pending a dedicated FT2 implementation. Changes: - trx-ft8: Ft8Decoder::new_ft2() delegates to with_protocol(Ft4) - trx-core: DecodedMessage::Ft2, AUDIO_MSG_FT2_DECODE (0x15), ft2_decode_enabled/ft2_decode_reset_seq state, SetFt2DecodeEnabled/ ResetFt2Decoder commands, protocol mapping - trx-server: DecoderHistories::ft2, run_ft2_decoder (7.5s slots), run_background_ft2_decoder, history push/replay, decoder task spawn - trx-frontend-http: ft2_history in FrontendRuntimeContext, toggle/clear endpoints, /ft2.js route, bookmark/scheduler/background decode support, DecodeHistoryPayload ft2 field - web: ft2.js plugin (3.75s period timer), FT2 subtab in index.html, FT2 map source (distinct hue), app.js dispatch, decode-history-worker HISTORY_GROUP_KEYS, bookmarks read/write/apply Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -110,6 +110,11 @@ impl Ft8Decoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_ft2(sample_rate: u32) -> Result<Self, String> {
|
||||||
|
// Wired to FT4 protocol pending a dedicated FT2 implementation.
|
||||||
|
Self::new_ft4(sample_rate)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn block_size(&self) -> usize {
|
pub fn block_size(&self) -> usize {
|
||||||
self.block_size
|
self.block_size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ use uuid::Uuid;
|
|||||||
use trx_core::audio::{
|
use trx_core::audio::{
|
||||||
parse_vchan_audio_frame, parse_vchan_uuid_msg, read_audio_msg, write_audio_msg,
|
parse_vchan_audio_frame, parse_vchan_uuid_msg, read_audio_msg, write_audio_msg,
|
||||||
write_vchan_uuid_msg, AudioStreamInfo, AUDIO_MSG_AIS_DECODE, AUDIO_MSG_APRS_DECODE,
|
write_vchan_uuid_msg, AudioStreamInfo, AUDIO_MSG_AIS_DECODE, AUDIO_MSG_APRS_DECODE,
|
||||||
AUDIO_MSG_CW_DECODE, AUDIO_MSG_FT4_DECODE, AUDIO_MSG_FT8_DECODE, AUDIO_MSG_HF_APRS_DECODE,
|
AUDIO_MSG_CW_DECODE, AUDIO_MSG_FT2_DECODE, AUDIO_MSG_FT4_DECODE, AUDIO_MSG_FT8_DECODE,
|
||||||
|
AUDIO_MSG_HF_APRS_DECODE,
|
||||||
AUDIO_MSG_HISTORY_COMPRESSED, AUDIO_MSG_RX_FRAME, AUDIO_MSG_RX_FRAME_CH,
|
AUDIO_MSG_HISTORY_COMPRESSED, AUDIO_MSG_RX_FRAME, AUDIO_MSG_RX_FRAME_CH,
|
||||||
AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_VCHAN_ALLOCATED, AUDIO_MSG_VCHAN_BW,
|
AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_VCHAN_ALLOCATED, AUDIO_MSG_VCHAN_BW,
|
||||||
AUDIO_MSG_VCHAN_DESTROYED, AUDIO_MSG_VCHAN_FREQ, AUDIO_MSG_VCHAN_MODE, AUDIO_MSG_VCHAN_REMOVE,
|
AUDIO_MSG_VCHAN_DESTROYED, AUDIO_MSG_VCHAN_FREQ, AUDIO_MSG_VCHAN_MODE, AUDIO_MSG_VCHAN_REMOVE,
|
||||||
@@ -288,6 +289,7 @@ async fn handle_audio_connection(
|
|||||||
| AUDIO_MSG_CW_DECODE
|
| AUDIO_MSG_CW_DECODE
|
||||||
| AUDIO_MSG_FT8_DECODE
|
| AUDIO_MSG_FT8_DECODE
|
||||||
| AUDIO_MSG_FT4_DECODE
|
| AUDIO_MSG_FT4_DECODE
|
||||||
|
| AUDIO_MSG_FT2_DECODE
|
||||||
| AUDIO_MSG_WSPR_DECODE,
|
| AUDIO_MSG_WSPR_DECODE,
|
||||||
payload,
|
payload,
|
||||||
)) => {
|
)) => {
|
||||||
|
|||||||
@@ -366,6 +366,9 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
DecodedMessage::Ft4(_) => {
|
DecodedMessage::Ft4(_) => {
|
||||||
// FT4 history is managed by the frontend HTTP audio collector
|
// FT4 history is managed by the frontend HTTP audio collector
|
||||||
}
|
}
|
||||||
|
DecodedMessage::Ft2(_) => {
|
||||||
|
// FT2 history is managed by the frontend HTTP audio collector
|
||||||
|
}
|
||||||
DecodedMessage::Wspr(message) => {
|
DecodedMessage::Wspr(message) => {
|
||||||
if let Ok(mut history) = wspr_history.lock() {
|
if let Ok(mut history) = wspr_history.lock() {
|
||||||
history.push_back((now, message));
|
history.push_back((now, message));
|
||||||
|
|||||||
@@ -777,6 +777,7 @@ mod tests {
|
|||||||
cw_decode_enabled: false,
|
cw_decode_enabled: false,
|
||||||
ft8_decode_enabled: false,
|
ft8_decode_enabled: false,
|
||||||
ft4_decode_enabled: false,
|
ft4_decode_enabled: false,
|
||||||
|
ft2_decode_enabled: false,
|
||||||
wspr_decode_enabled: false,
|
wspr_decode_enabled: false,
|
||||||
cw_auto: true,
|
cw_auto: true,
|
||||||
cw_wpm: 15,
|
cw_wpm: 15,
|
||||||
|
|||||||
@@ -204,6 +204,8 @@ pub struct FrontendRuntimeContext {
|
|||||||
pub ft8_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
|
pub ft8_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
|
||||||
/// FT4 decode history (timestamp, message)
|
/// FT4 decode history (timestamp, message)
|
||||||
pub ft4_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
|
pub ft4_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
|
||||||
|
/// FT2 decode history (timestamp, message)
|
||||||
|
pub ft2_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
|
||||||
/// WSPR decode history (timestamp, message)
|
/// WSPR decode history (timestamp, message)
|
||||||
pub wspr_history: Arc<Mutex<VecDeque<(Instant, WsprMessage)>>>,
|
pub wspr_history: Arc<Mutex<VecDeque<(Instant, WsprMessage)>>>,
|
||||||
/// Authentication tokens for HTTP-JSON frontend
|
/// Authentication tokens for HTTP-JSON frontend
|
||||||
@@ -286,6 +288,7 @@ impl FrontendRuntimeContext {
|
|||||||
cw_history: Arc::new(Mutex::new(VecDeque::new())),
|
cw_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
ft8_history: Arc::new(Mutex::new(VecDeque::new())),
|
ft8_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
ft4_history: Arc::new(Mutex::new(VecDeque::new())),
|
ft4_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
|
ft2_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
wspr_history: Arc::new(Mutex::new(VecDeque::new())),
|
wspr_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
auth_tokens: HashSet::new(),
|
auth_tokens: HashSet::new(),
|
||||||
sse_clients: Arc::new(AtomicUsize::new(0)),
|
sse_clients: Arc::new(AtomicUsize::new(0)),
|
||||||
|
|||||||
@@ -215,12 +215,14 @@ function applyAuthRestrictions() {
|
|||||||
"vdes-clear-btn",
|
"vdes-clear-btn",
|
||||||
"ft8-decode-toggle-btn",
|
"ft8-decode-toggle-btn",
|
||||||
"ft4-decode-toggle-btn",
|
"ft4-decode-toggle-btn",
|
||||||
|
"ft2-decode-toggle-btn",
|
||||||
"wspr-decode-toggle-btn",
|
"wspr-decode-toggle-btn",
|
||||||
"hf-aprs-decode-toggle-btn",
|
"hf-aprs-decode-toggle-btn",
|
||||||
"cw-auto",
|
"cw-auto",
|
||||||
"aprs-clear-btn",
|
"aprs-clear-btn",
|
||||||
"ft8-clear-btn",
|
"ft8-clear-btn",
|
||||||
"ft4-clear-btn",
|
"ft4-clear-btn",
|
||||||
|
"ft2-clear-btn",
|
||||||
"wspr-clear-btn",
|
"wspr-clear-btn",
|
||||||
"cw-clear-btn"
|
"cw-clear-btn"
|
||||||
];
|
];
|
||||||
@@ -2856,6 +2858,13 @@ function render(update) {
|
|||||||
ft4ToggleBtn.style.borderColor = ft4On ? "#00d17f" : "";
|
ft4ToggleBtn.style.borderColor = ft4On ? "#00d17f" : "";
|
||||||
ft4ToggleBtn.style.color = ft4On ? "#00d17f" : "";
|
ft4ToggleBtn.style.color = ft4On ? "#00d17f" : "";
|
||||||
}
|
}
|
||||||
|
const ft2ToggleBtn = document.getElementById("ft2-decode-toggle-btn");
|
||||||
|
if (ft2ToggleBtn) {
|
||||||
|
const ft2On = !!update.ft2_decode_enabled;
|
||||||
|
ft2ToggleBtn.textContent = ft2On ? "Disable FT2" : "Enable FT2";
|
||||||
|
ft2ToggleBtn.style.borderColor = ft2On ? "#00d17f" : "";
|
||||||
|
ft2ToggleBtn.style.color = ft2On ? "#00d17f" : "";
|
||||||
|
}
|
||||||
const wsprToggleBtn = document.getElementById("wspr-decode-toggle-btn");
|
const wsprToggleBtn = document.getElementById("wspr-decode-toggle-btn");
|
||||||
if (wsprToggleBtn) {
|
if (wsprToggleBtn) {
|
||||||
const wsprOn = !!update.wspr_decode_enabled;
|
const wsprOn = !!update.wspr_decode_enabled;
|
||||||
@@ -3986,7 +3995,7 @@ const locatorMarkers = new Map();
|
|||||||
const decodeContactPaths = new Map();
|
const decodeContactPaths = new Map();
|
||||||
let selectedMapQsoKey = null;
|
let selectedMapQsoKey = null;
|
||||||
const mapMarkers = new Set();
|
const mapMarkers = new Set();
|
||||||
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, wspr: true };
|
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true };
|
||||||
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
|
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
|
||||||
const mapLocatorFilter = { phase: "band", bands: new Set() };
|
const mapLocatorFilter = { phase: "band", bands: new Set() };
|
||||||
let mapSearchFilter = "";
|
let mapSearchFilter = "";
|
||||||
@@ -4163,7 +4172,7 @@ function ensureVdesMarker(key, entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureDecodeLocatorMarker(entry) {
|
function ensureDecodeLocatorMarker(entry) {
|
||||||
if (!aprsMap || !entry || entry.marker || !entry.grid || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "wspr")) return;
|
if (!aprsMap || !entry || entry.marker || !entry.grid || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr")) return;
|
||||||
const bounds = maidenheadToBounds(entry.grid);
|
const bounds = maidenheadToBounds(entry.grid);
|
||||||
if (!bounds) return;
|
if (!bounds) return;
|
||||||
const count = Math.max(entry.stationDetails?.size || 0, entry.stations?.size || 0, 1);
|
const count = Math.max(entry.stationDetails?.size || 0, entry.stations?.size || 0, 1);
|
||||||
@@ -4245,7 +4254,7 @@ function pruneAisEntry(key, entry, cutoffMs) {
|
|||||||
|
|
||||||
function pruneLocatorEntry(key, entry, cutoffMs) {
|
function pruneLocatorEntry(key, entry, cutoffMs) {
|
||||||
const canRenderMap = !!aprsMap && !decodeHistoryReplayActive;
|
const canRenderMap = !!aprsMap && !decodeHistoryReplayActive;
|
||||||
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "wspr")) return true;
|
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr")) return true;
|
||||||
if (!(entry.allStationDetails instanceof Map)) {
|
if (!(entry.allStationDetails instanceof Map)) {
|
||||||
entry.allStationDetails = entry.stationDetails instanceof Map
|
entry.allStationDetails = entry.stationDetails instanceof Map
|
||||||
? new Map(entry.stationDetails)
|
? new Map(entry.stationDetails)
|
||||||
@@ -4331,6 +4340,7 @@ function locatorSourceLabel(type) {
|
|||||||
if (type === "bookmark") return "Bookmarks";
|
if (type === "bookmark") return "Bookmarks";
|
||||||
if (type === "wspr") return "WSPR";
|
if (type === "wspr") return "WSPR";
|
||||||
if (type === "ft4") return "FT4";
|
if (type === "ft4") return "FT4";
|
||||||
|
if (type === "ft2") return "FT2";
|
||||||
return "FT8";
|
return "FT8";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4346,7 +4356,7 @@ function locatorFilterColor(type) {
|
|||||||
const light = lightTheme ? 42 : 56;
|
const light = lightTheme ? 42 : 56;
|
||||||
const hue = type === "bookmark"
|
const hue = type === "bookmark"
|
||||||
? hues.bookmark
|
? hues.bookmark
|
||||||
: (type === "wspr" ? hues.wspr : (type === "ft4" ? hues.ft4 : hues.ft8));
|
: (type === "wspr" ? hues.wspr : (type === "ft4" ? hues.ft4 : (type === "ft2" ? hues.ft2 : hues.ft8)));
|
||||||
return `hsl(${hue.toFixed(1)} ${sat}% ${light}%)`;
|
return `hsl(${hue.toFixed(1)} ${sat}% ${light}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4472,6 +4482,7 @@ function locatorThemeHues() {
|
|||||||
bookmark: wrapHue(baseHue),
|
bookmark: wrapHue(baseHue),
|
||||||
ft8: wrapHue(peakHue),
|
ft8: wrapHue(peakHue),
|
||||||
ft4: wrapHue(peakHue + 30),
|
ft4: wrapHue(peakHue + 30),
|
||||||
|
ft2: wrapHue(peakHue + 60),
|
||||||
wspr: wrapHue((waveHue + baseHue) / 2),
|
wspr: wrapHue((waveHue + baseHue) / 2),
|
||||||
bandBase: wrapHue((baseHue * 0.65) + (peakHue * 0.35)),
|
bandBase: wrapHue((baseHue * 0.65) + (peakHue * 0.35)),
|
||||||
};
|
};
|
||||||
@@ -4522,6 +4533,7 @@ function locatorHueForEntry(entry) {
|
|||||||
if (entry?.sourceType === "bookmark") return hues.bookmark;
|
if (entry?.sourceType === "bookmark") return hues.bookmark;
|
||||||
if (entry?.sourceType === "wspr") return hues.wspr;
|
if (entry?.sourceType === "wspr") return hues.wspr;
|
||||||
if (entry?.sourceType === "ft4") return hues.ft4;
|
if (entry?.sourceType === "ft4") return hues.ft4;
|
||||||
|
if (entry?.sourceType === "ft2") return hues.ft2;
|
||||||
return hues.ft8;
|
return hues.ft8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4788,7 +4800,7 @@ function setSelectedLocatorMarker(marker) {
|
|||||||
|
|
||||||
function isLocatorOverlay(marker) {
|
function isLocatorOverlay(marker) {
|
||||||
const type = marker?.__trxType;
|
const type = marker?.__trxType;
|
||||||
return type === "bookmark" || type === "ft8" || type === "ft4" || type === "wspr";
|
return type === "bookmark" || type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr";
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendLocatorOverlayToBack(marker) {
|
function sendLocatorOverlayToBack(marker) {
|
||||||
@@ -4919,7 +4931,7 @@ function rebuildMapLocatorFilters() {
|
|||||||
for (const entry of locatorMarkers.values()) {
|
for (const entry of locatorMarkers.values()) {
|
||||||
const sourceType = entry?.sourceType;
|
const sourceType = entry?.sourceType;
|
||||||
if (!sourceType) continue;
|
if (!sourceType) continue;
|
||||||
if ((sourceType === "ft8" || sourceType === "ft4" || sourceType === "wspr") && !entry?.visibleInHistoryWindow) continue;
|
if ((sourceType === "ft8" || sourceType === "ft4" || sourceType === "ft2" || sourceType === "wspr") && !entry?.visibleInHistoryWindow) continue;
|
||||||
availableSources.add(sourceType);
|
availableSources.add(sourceType);
|
||||||
const meta = entry?.bandMeta instanceof Map ? entry.bandMeta : new Map();
|
const meta = entry?.bandMeta instanceof Map ? entry.bandMeta : new Map();
|
||||||
for (const [label, hz] of meta.entries()) {
|
for (const [label, hz] of meta.entries()) {
|
||||||
@@ -4947,7 +4959,7 @@ function rebuildMapLocatorFilters() {
|
|||||||
if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
|
if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceItems = ["ais", "vdes", "aprs", "bookmark", "ft8", "ft4", "wspr"]
|
const sourceItems = ["ais", "vdes", "aprs", "bookmark", "ft8", "ft4", "ft2", "wspr"]
|
||||||
.filter((key) => availableSources.has(key))
|
.filter((key) => availableSources.has(key))
|
||||||
.map((key) => ({
|
.map((key) => ({
|
||||||
key,
|
key,
|
||||||
@@ -4989,7 +5001,7 @@ function markerPassesLocatorFilters(marker) {
|
|||||||
|
|
||||||
function markerSearchText(marker) {
|
function markerSearchText(marker) {
|
||||||
const type = marker?.__trxType;
|
const type = marker?.__trxType;
|
||||||
if (type === "bookmark" || type === "ft8" || type === "ft4" || type === "wspr") {
|
if (type === "bookmark" || type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr") {
|
||||||
const entry = locatorEntryForMarker(marker);
|
const entry = locatorEntryForMarker(marker);
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (entry?.grid) parts.push(entry.grid);
|
if (entry?.grid) parts.push(entry.grid);
|
||||||
@@ -5135,7 +5147,7 @@ window.clearMapMarkersByType = function(type) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "ft8" || type === "ft4" || type === "wspr") {
|
if (type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr") {
|
||||||
const prefix = `${type}:`;
|
const prefix = `${type}:`;
|
||||||
for (const [key, entry] of locatorMarkers.entries()) {
|
for (const [key, entry] of locatorMarkers.entries()) {
|
||||||
if (!key.startsWith(prefix)) continue;
|
if (!key.startsWith(prefix)) continue;
|
||||||
@@ -5382,7 +5394,7 @@ function initAprsMap() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (marker.__trxType === "bookmark" || marker.__trxType === "ft8" || marker.__trxType === "ft4" || marker.__trxType === "wspr") {
|
if (marker.__trxType === "bookmark" || marker.__trxType === "ft8" || marker.__trxType === "ft4" || marker.__trxType === "ft2" || marker.__trxType === "wspr") {
|
||||||
const center = locatorMarkerCenter(marker);
|
const center = locatorMarkerCenter(marker);
|
||||||
if (center) {
|
if (center) {
|
||||||
setSelectedLocatorMarker(marker);
|
setSelectedLocatorMarker(marker);
|
||||||
@@ -5621,10 +5633,10 @@ window.navigateToMapLocator = function(grid, preferredType = null) {
|
|||||||
sizeAprsMapToViewport();
|
sizeAprsMapToViewport();
|
||||||
if (!aprsMap) return false;
|
if (!aprsMap) return false;
|
||||||
|
|
||||||
const pref = preferredType === "wspr" ? "wspr" : (preferredType === "ft4" ? "ft4" : (preferredType === "ft8" ? "ft8" : null));
|
const pref = preferredType === "wspr" ? "wspr" : (preferredType === "ft4" ? "ft4" : (preferredType === "ft2" ? "ft2" : (preferredType === "ft8" ? "ft8" : null)));
|
||||||
const keys = pref
|
const keys = pref
|
||||||
? [`${pref}:${normalizedGrid}`, `ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`]
|
? [`${pref}:${normalizedGrid}`, `ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `ft2:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`]
|
||||||
: [`ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`];
|
: [`ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `ft2:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`];
|
||||||
let entry = null;
|
let entry = null;
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
entry = locatorMarkers.get(key);
|
entry = locatorMarkers.get(key);
|
||||||
@@ -6164,6 +6176,7 @@ function applyMapFilter() {
|
|||||||
(type === "aprs" && mapFilter.aprs) ||
|
(type === "aprs" && mapFilter.aprs) ||
|
||||||
(type === "ft8" && mapFilter.ft8) ||
|
(type === "ft8" && mapFilter.ft8) ||
|
||||||
(type === "ft4" && mapFilter.ft4) ||
|
(type === "ft4" && mapFilter.ft4) ||
|
||||||
|
(type === "ft2" && mapFilter.ft2) ||
|
||||||
(type === "wspr" && mapFilter.wspr)
|
(type === "wspr" && mapFilter.wspr)
|
||||||
);
|
);
|
||||||
const onMap = aprsMap.hasLayer(marker);
|
const onMap = aprsMap.hasLayer(marker);
|
||||||
@@ -6275,7 +6288,7 @@ function rebuildDecodeContactPaths() {
|
|||||||
const stationLocators = new Map();
|
const stationLocators = new Map();
|
||||||
const directedMessages = [];
|
const directedMessages = [];
|
||||||
for (const entry of locatorMarkers.values()) {
|
for (const entry of locatorMarkers.values()) {
|
||||||
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "wspr")) continue;
|
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr")) continue;
|
||||||
const grid = String(entry.grid || "").trim().toUpperCase();
|
const grid = String(entry.grid || "").trim().toUpperCase();
|
||||||
if (!grid || !(entry.stationDetails instanceof Map)) continue;
|
if (!grid || !(entry.stationDetails instanceof Map)) continue;
|
||||||
for (const detail of entry.stationDetails.values()) {
|
for (const detail of entry.stationDetails.values()) {
|
||||||
@@ -6524,7 +6537,7 @@ window.syncBookmarkMapLocators = function(bookmarks) {
|
|||||||
|
|
||||||
window.mapAddLocator = function(message, grids, type = "ft8", station = null, details = null) {
|
window.mapAddLocator = function(message, grids, type = "ft8", station = null, details = null) {
|
||||||
if (!Array.isArray(grids) || grids.length === 0) return;
|
if (!Array.isArray(grids) || grids.length === 0) return;
|
||||||
const markerType = type === "wspr" ? "wspr" : (type === "ft4" ? "ft4" : "ft8");
|
const markerType = type === "wspr" ? "wspr" : (type === "ft4" ? "ft4" : (type === "ft2" ? "ft2" : "ft8"));
|
||||||
const unique = [...new Set(grids.map((g) => String(g).toUpperCase()))];
|
const unique = [...new Set(grids.map((g) => String(g).toUpperCase()))];
|
||||||
const stationId = station && String(station).trim() ? String(station).trim().toUpperCase() : "";
|
const stationId = station && String(station).trim() ? String(station).trim().toUpperCase() : "";
|
||||||
const locatorDetails = new Map();
|
const locatorDetails = new Map();
|
||||||
@@ -7326,6 +7339,7 @@ function updateDecodeStatus(text) {
|
|||||||
const cw = document.getElementById("cw-status");
|
const cw = document.getElementById("cw-status");
|
||||||
const ft8 = document.getElementById("ft8-status");
|
const ft8 = document.getElementById("ft8-status");
|
||||||
const ft4 = document.getElementById("ft4-status");
|
const ft4 = document.getElementById("ft4-status");
|
||||||
|
const ft2 = document.getElementById("ft2-status");
|
||||||
setModeBoundDecodeStatus(ais, ["AIS"], "Select AIS mode to decode", text);
|
setModeBoundDecodeStatus(ais, ["AIS"], "Select AIS mode to decode", text);
|
||||||
const vdesText = text === "Connected, listening for packets" ? "Connected, listening for bursts" : text;
|
const vdesText = text === "Connected, listening for packets" ? "Connected, listening for bursts" : text;
|
||||||
setModeBoundDecodeStatus(vdes, ["VDES"], "Select VDES mode to decode", vdesText);
|
setModeBoundDecodeStatus(vdes, ["VDES"], "Select VDES mode to decode", vdesText);
|
||||||
@@ -7334,6 +7348,7 @@ function updateDecodeStatus(text) {
|
|||||||
setModeBoundDecodeStatus(cw, ["CW", "CWR"], "Select CW mode to decode", cwText);
|
setModeBoundDecodeStatus(cw, ["CW", "CWR"], "Select CW mode to decode", cwText);
|
||||||
if (ft8 && ft8.textContent !== "Receiving") ft8.textContent = text;
|
if (ft8 && ft8.textContent !== "Receiving") ft8.textContent = text;
|
||||||
if (ft4 && ft4.textContent !== "Receiving") ft4.textContent = text;
|
if (ft4 && ft4.textContent !== "Receiving") ft4.textContent = text;
|
||||||
|
if (ft2 && ft2.textContent !== "Receiving") ft2.textContent = text;
|
||||||
}
|
}
|
||||||
function dispatchDecodeMessage(msg) {
|
function dispatchDecodeMessage(msg) {
|
||||||
if (msg.type === "ais" && window.onServerAis) window.onServerAis(msg);
|
if (msg.type === "ais" && window.onServerAis) window.onServerAis(msg);
|
||||||
@@ -7343,6 +7358,7 @@ function dispatchDecodeMessage(msg) {
|
|||||||
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
|
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
|
||||||
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
|
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
|
||||||
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
|
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
|
||||||
|
if (msg.type === "ft2" && window.onServerFt2) window.onServerFt2(msg);
|
||||||
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
|
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7375,6 +7391,10 @@ function dispatchDecodeBatch(batch) {
|
|||||||
window.onServerFt4Batch(batch);
|
window.onServerFt4Batch(batch);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (type === "ft2" && window.onServerFt2Batch) {
|
||||||
|
window.onServerFt2Batch(batch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (type === "wspr" && window.onServerWsprBatch) {
|
if (type === "wspr" && window.onServerWsprBatch) {
|
||||||
window.onServerWsprBatch(batch);
|
window.onServerWsprBatch(batch);
|
||||||
return;
|
return;
|
||||||
@@ -7449,6 +7469,10 @@ function restoreDecodeHistoryGroup(kind, messages) {
|
|||||||
window.restoreFt4History(messages);
|
window.restoreFt4History(messages);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (kind === "ft2" && window.restoreFt2History) {
|
||||||
|
window.restoreFt2History(messages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (kind === "wspr" && window.restoreWsprHistory) {
|
if (kind === "wspr" && window.restoreWsprHistory) {
|
||||||
window.restoreWsprHistory(messages);
|
window.restoreWsprHistory(messages);
|
||||||
return;
|
return;
|
||||||
@@ -7537,7 +7561,7 @@ function connectDecode() {
|
|||||||
|
|
||||||
function totalDecodeHistoryMessages(groups) {
|
function totalDecodeHistoryMessages(groups) {
|
||||||
if (!groups || typeof groups !== "object") return 0;
|
if (!groups || typeof groups !== "object") return 0;
|
||||||
return ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "wspr"]
|
return ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr"]
|
||||||
.reduce((sum, key) => sum + (Array.isArray(groups[key]) ? groups[key].length : 0), 0);
|
.reduce((sum, key) => sum + (Array.isArray(groups[key]) ? groups[key].length : 0), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7548,7 +7572,7 @@ function connectDecode() {
|
|||||||
setDecodeHistoryReplayActive(true);
|
setDecodeHistoryReplayActive(true);
|
||||||
updateHistoryReplayOverlay();
|
updateHistoryReplayOverlay();
|
||||||
}
|
}
|
||||||
for (const kind of ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "wspr"]) {
|
for (const kind of ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr"]) {
|
||||||
const messages = groups && Array.isArray(groups[kind]) ? groups[kind] : [];
|
const messages = groups && Array.isArray(groups[kind]) ? groups[kind] : [];
|
||||||
if (messages.length === 0) continue;
|
if (messages.length === 0) continue;
|
||||||
for (let index = 0; index < messages.length; index += DECODE_HISTORY_WORKER_GROUP_LIMIT) {
|
for (let index = 0; index < messages.length; index += DECODE_HISTORY_WORKER_GROUP_LIMIT) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
||||||
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "wspr"];
|
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr"];
|
||||||
|
|
||||||
function decodeCborUint(view, bytes, state, additional) {
|
function decodeCborUint(view, bytes, state, additional) {
|
||||||
const offset = state.offset;
|
const offset = state.offset;
|
||||||
|
|||||||
@@ -375,6 +375,7 @@
|
|||||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ais" value="ais" /> AIS</label>
|
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ais" value="ais" /> AIS</label>
|
||||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ft8" value="ft8" /> FT8</label>
|
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ft8" value="ft8" /> FT8</label>
|
||||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ft4" value="ft4" /> FT4</label>
|
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ft4" value="ft4" /> FT4</label>
|
||||||
|
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ft2" value="ft2" /> FT2</label>
|
||||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-wspr" value="wspr" /> WSPR</label>
|
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-wspr" value="wspr" /> WSPR</label>
|
||||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-hf-aprs" value="hf-aprs" /> HF APRS</label>
|
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-hf-aprs" value="hf-aprs" /> HF APRS</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -427,6 +428,7 @@
|
|||||||
<button class="sub-tab" data-subtab="cw">CW</button>
|
<button class="sub-tab" data-subtab="cw">CW</button>
|
||||||
<button class="sub-tab" data-subtab="ft8">FT8</button>
|
<button class="sub-tab" data-subtab="ft8">FT8</button>
|
||||||
<button class="sub-tab" data-subtab="ft4">FT4</button>
|
<button class="sub-tab" data-subtab="ft4">FT4</button>
|
||||||
|
<button class="sub-tab" data-subtab="ft2">FT2</button>
|
||||||
<button class="sub-tab" data-subtab="wspr">WSPR</button>
|
<button class="sub-tab" data-subtab="wspr">WSPR</button>
|
||||||
<button class="sub-tab" data-subtab="rds">RDS</button>
|
<button class="sub-tab" data-subtab="rds">RDS</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -467,6 +469,12 @@
|
|||||||
Decodes FT4 messages from RX audio (DIG/USB only, toggle required). 7.5-second slots.
|
Decodes FT4 messages from RX audio (DIG/USB only, toggle required). 7.5-second slots.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="plugin-item">
|
||||||
|
<strong>FT2 Decoder</strong>
|
||||||
|
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
||||||
|
Decodes FT2 messages from RX audio (DIG/USB only, toggle required). 3.75-second slots.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="plugin-item">
|
<div class="plugin-item">
|
||||||
<strong>WSPR Decoder</strong>
|
<strong>WSPR Decoder</strong>
|
||||||
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
||||||
@@ -660,6 +668,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="ft4-messages"></div>
|
<div id="ft4-messages"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="subtab-ft2" class="sub-tab-panel" style="display:none;">
|
||||||
|
<div class="ft8-controls">
|
||||||
|
<button id="ft2-decode-toggle-btn" type="button">Enable FT2</button>
|
||||||
|
<button id="ft2-pause-btn" type="button">Pause</button>
|
||||||
|
<button id="ft2-clear-btn" type="button">Clear</button>
|
||||||
|
<input id="ft2-filter" class="ft8-filter" type="text" placeholder="Filter (e.g. CQ, DL4)" />
|
||||||
|
<small id="ft2-status" style="color:var(--text-muted);">Waiting for server decode</small>
|
||||||
|
<small id="ft2-period" style="color:var(--text-muted);">Next slot --s</small>
|
||||||
|
</div>
|
||||||
|
<div class="ft8-header">
|
||||||
|
<span class="ft8-time">Time</span>
|
||||||
|
<span class="ft8-snr">SNR</span>
|
||||||
|
<span class="ft8-dt">DT</span>
|
||||||
|
<span class="ft8-freq">RF</span>
|
||||||
|
<span class="ft8-msg">Message</span>
|
||||||
|
</div>
|
||||||
|
<div id="ft2-messages"></div>
|
||||||
|
</div>
|
||||||
<div id="subtab-wspr" class="sub-tab-panel" style="display:none;">
|
<div id="subtab-wspr" class="sub-tab-panel" style="display:none;">
|
||||||
<div class="ft8-controls">
|
<div class="ft8-controls">
|
||||||
<button id="wspr-decode-toggle-btn" type="button">Enable WSPR</button>
|
<button id="wspr-decode-toggle-btn" type="button">Enable WSPR</button>
|
||||||
@@ -937,6 +963,7 @@
|
|||||||
<script src="/hf-aprs.js"></script>
|
<script src="/hf-aprs.js"></script>
|
||||||
<script src="/ft8.js"></script>
|
<script src="/ft8.js"></script>
|
||||||
<script src="/ft4.js"></script>
|
<script src="/ft4.js"></script>
|
||||||
|
<script src="/ft2.js"></script>
|
||||||
<script src="/wspr.js"></script>
|
<script src="/wspr.js"></script>
|
||||||
<script src="/cw.js"></script>
|
<script src="/cw.js"></script>
|
||||||
<script src="/bookmarks.js"></script>
|
<script src="/bookmarks.js"></script>
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ function bmReadDecoders() {
|
|||||||
if (document.getElementById("bm-dec-ais").checked) decoders.push("ais");
|
if (document.getElementById("bm-dec-ais").checked) decoders.push("ais");
|
||||||
if (document.getElementById("bm-dec-ft8").checked) decoders.push("ft8");
|
if (document.getElementById("bm-dec-ft8").checked) decoders.push("ft8");
|
||||||
if (document.getElementById("bm-dec-ft4").checked) decoders.push("ft4");
|
if (document.getElementById("bm-dec-ft4").checked) decoders.push("ft4");
|
||||||
|
if (document.getElementById("bm-dec-ft2").checked) decoders.push("ft2");
|
||||||
if (document.getElementById("bm-dec-wspr").checked) decoders.push("wspr");
|
if (document.getElementById("bm-dec-wspr").checked) decoders.push("wspr");
|
||||||
if (document.getElementById("bm-dec-hf-aprs").checked) decoders.push("hf-aprs");
|
if (document.getElementById("bm-dec-hf-aprs").checked) decoders.push("hf-aprs");
|
||||||
return decoders;
|
return decoders;
|
||||||
@@ -195,6 +196,7 @@ function bmWriteDecoders(decoders) {
|
|||||||
document.getElementById("bm-dec-ais").checked = list.includes("ais");
|
document.getElementById("bm-dec-ais").checked = list.includes("ais");
|
||||||
document.getElementById("bm-dec-ft8").checked = list.includes("ft8");
|
document.getElementById("bm-dec-ft8").checked = list.includes("ft8");
|
||||||
document.getElementById("bm-dec-ft4").checked = list.includes("ft4");
|
document.getElementById("bm-dec-ft4").checked = list.includes("ft4");
|
||||||
|
document.getElementById("bm-dec-ft2").checked = list.includes("ft2");
|
||||||
document.getElementById("bm-dec-wspr").checked = list.includes("wspr");
|
document.getElementById("bm-dec-wspr").checked = list.includes("wspr");
|
||||||
document.getElementById("bm-dec-hf-aprs").checked = list.includes("hf-aprs");
|
document.getElementById("bm-dec-hf-aprs").checked = list.includes("hf-aprs");
|
||||||
}
|
}
|
||||||
@@ -358,6 +360,10 @@ async function bmApply(bm) {
|
|||||||
if (wantFt4 !== !!st.ft4_decode_enabled) {
|
if (wantFt4 !== !!st.ft4_decode_enabled) {
|
||||||
await postPath("/toggle_ft4_decode");
|
await postPath("/toggle_ft4_decode");
|
||||||
}
|
}
|
||||||
|
const wantFt2 = bm.decoders.includes("ft2");
|
||||||
|
if (wantFt2 !== !!st.ft2_decode_enabled) {
|
||||||
|
await postPath("/toggle_ft2_decode");
|
||||||
|
}
|
||||||
const wantWspr = bm.decoders.includes("wspr");
|
const wantWspr = bm.decoders.includes("wspr");
|
||||||
if (wantWspr !== !!st.wspr_decode_enabled) {
|
if (wantWspr !== !!st.wspr_decode_enabled) {
|
||||||
await postPath("/toggle_wspr_decode");
|
await postPath("/toggle_wspr_decode");
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
// --- FT2 Decoder Plugin (server-side decode) ---
|
||||||
|
// SPDX-FileCopyrightText: 2026 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||||
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
function ft8RenderMessageFt2(message) {
|
||||||
|
if (typeof renderFt8Message === "function") return renderFt8Message(message);
|
||||||
|
if (typeof ft8EscapeHtml === "function") return ft8EscapeHtml(message);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ft2Status = document.getElementById("ft2-status");
|
||||||
|
const ft2PeriodEl = document.getElementById("ft2-period");
|
||||||
|
const ft2MessagesEl = document.getElementById("ft2-messages");
|
||||||
|
const ft2FilterInput = document.getElementById("ft2-filter");
|
||||||
|
const ft2PauseBtn = document.getElementById("ft2-pause-btn");
|
||||||
|
const FT2_PERIOD_MS = 3750;
|
||||||
|
let ft2FilterText = "";
|
||||||
|
let ft2MessageHistory = [];
|
||||||
|
let ft2Paused = false;
|
||||||
|
let ft2BufferedWhilePaused = 0;
|
||||||
|
|
||||||
|
function currentFt2HistoryRetentionMs() {
|
||||||
|
return typeof window.getDecodeHistoryRetentionMs === "function"
|
||||||
|
? window.getDecodeHistoryRetentionMs()
|
||||||
|
: 24 * 60 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneFt2MessageHistory() {
|
||||||
|
const cutoffMs = Date.now() - currentFt2HistoryRetentionMs();
|
||||||
|
ft2MessageHistory = ft2MessageHistory.filter((msg) => Number(msg?._tsMs ?? msg?.ts_ms) >= cutoffMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFt2Ui(key, job) {
|
||||||
|
if (typeof window.trxScheduleUiFrameJob === "function") {
|
||||||
|
window.trxScheduleUiFrameJob(key, job);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
job();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFt2HistoryRender() { scheduleFt2Ui("ft2-history", () => renderFt2History()); }
|
||||||
|
|
||||||
|
function normalizeFt2DisplayFreqHz(freqHz) {
|
||||||
|
const rawHz = Number(freqHz);
|
||||||
|
if (!Number.isFinite(rawHz)) return null;
|
||||||
|
const baseHz = Number.isFinite(window.ft8BaseHz) ? Number(window.ft8BaseHz) : null;
|
||||||
|
if (Number.isFinite(baseHz) && baseHz > 0 && rawHz >= 0 && rawHz < 100000) {
|
||||||
|
return baseHz + rawHz;
|
||||||
|
}
|
||||||
|
return rawHz;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFt2PeriodTimer() {
|
||||||
|
if (!ft2PeriodEl) return;
|
||||||
|
const nowMs = Date.now();
|
||||||
|
const remaining = (FT2_PERIOD_MS - nowMs % FT2_PERIOD_MS) / 1000;
|
||||||
|
ft2PeriodEl.textContent = `Next slot ${remaining.toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFt2PeriodTimer();
|
||||||
|
setInterval(updateFt2PeriodTimer, 250);
|
||||||
|
|
||||||
|
function renderFt2Row(msg) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "ft8-row";
|
||||||
|
const rawMessage = (msg.message || "").toString();
|
||||||
|
row.dataset.message = rawMessage.toUpperCase();
|
||||||
|
row.dataset.decoder = "ft2";
|
||||||
|
row.dataset.storedFreqHz = Number.isFinite(msg.freq_hz) ? String(msg.freq_hz) : "";
|
||||||
|
const snr = Number.isFinite(msg.snr_db) ? msg.snr_db.toFixed(1) : "--";
|
||||||
|
const dt = Number.isFinite(msg.dt_s) ? msg.dt_s.toFixed(2) : "--";
|
||||||
|
const displayFreqHz = normalizeFt2DisplayFreqHz(msg.freq_hz);
|
||||||
|
const freq = Number.isFinite(displayFreqHz) ? displayFreqHz.toFixed(0) : "--";
|
||||||
|
const renderedMessage = ft8RenderMessageFt2(rawMessage);
|
||||||
|
const tsMs = msg._tsMs ?? msg.ts_ms;
|
||||||
|
const timeStr = tsMs ? new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }) : "--:--:--";
|
||||||
|
row.innerHTML = `<span class="ft8-time">${timeStr}</span><span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${renderedMessage}</span>`;
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFt2PauseUi() {
|
||||||
|
if (!ft2PauseBtn) return;
|
||||||
|
ft2PauseBtn.textContent = ft2Paused ? "Resume" : "Pause";
|
||||||
|
ft2PauseBtn.classList.toggle("active", ft2Paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFt2History() {
|
||||||
|
pruneFt2MessageHistory();
|
||||||
|
if (!ft2MessagesEl || ft2Paused) { updateFt2PauseUi(); return; }
|
||||||
|
const filter = ft2FilterText;
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
for (let i = 0; i < ft2MessageHistory.length; i++) {
|
||||||
|
const msg = ft2MessageHistory[i];
|
||||||
|
if (filter && !(msg.message || "").toString().toUpperCase().includes(filter)) continue;
|
||||||
|
fragment.appendChild(renderFt2Row(msg));
|
||||||
|
}
|
||||||
|
ft2MessagesEl.replaceChildren(fragment);
|
||||||
|
updateFt2PauseUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFt2Message(msg) {
|
||||||
|
msg._tsMs = Number.isFinite(msg?.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
||||||
|
ft2MessageHistory.unshift(msg);
|
||||||
|
pruneFt2MessageHistory();
|
||||||
|
if (ft2Paused) { ft2BufferedWhilePaused += 1; updateFt2PauseUi(); return; }
|
||||||
|
scheduleFt2HistoryRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeServerFt2Message(msg) {
|
||||||
|
const raw = (msg.message || "").toString();
|
||||||
|
const locatorDetails = typeof ft8ExtractLocatorDetails === "function" ? ft8ExtractLocatorDetails(raw) : [];
|
||||||
|
const grids = locatorDetails.length > 0
|
||||||
|
? locatorDetails.map((d) => d.grid)
|
||||||
|
: (typeof ft8ExtractAllGrids === "function" ? ft8ExtractAllGrids(raw) : []);
|
||||||
|
const station = typeof ft8ExtractLikelyCallsign === "function" ? ft8ExtractLikelyCallsign(raw) : null;
|
||||||
|
const rfHz = normalizeFt2DisplayFreqHz(msg.freq_hz);
|
||||||
|
return {
|
||||||
|
raw, grids, station, rfHz, locatorDetails,
|
||||||
|
history: {
|
||||||
|
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
|
||||||
|
ts_ms: msg.ts_ms, snr_db: msg.snr_db, dt_s: msg.dt_s,
|
||||||
|
freq_hz: Number.isFinite(rfHz) ? rfHz : msg.freq_hz,
|
||||||
|
message: msg.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onServerFt2Batch = function(messages) {
|
||||||
|
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
|
if (ft2Status) ft2Status.textContent = ft2Paused ? "Paused" : "Receiving";
|
||||||
|
const normalized = [];
|
||||||
|
for (const msg of messages) {
|
||||||
|
const next = normalizeServerFt2Message(msg);
|
||||||
|
if (next.grids.length > 0 && window.mapAddLocator) {
|
||||||
|
window.mapAddLocator(next.raw, next.grids, "ft2", next.station, { ...msg, freq_hz: next.rfHz, locator_details: next.locatorDetails });
|
||||||
|
}
|
||||||
|
next.history._tsMs = Number.isFinite(next.history?.ts_ms) ? Number(next.history.ts_ms) : Date.now();
|
||||||
|
normalized.push(next.history);
|
||||||
|
}
|
||||||
|
normalized.reverse();
|
||||||
|
ft2MessageHistory = normalized.concat(ft2MessageHistory);
|
||||||
|
pruneFt2MessageHistory();
|
||||||
|
if (ft2Paused) { ft2BufferedWhilePaused += messages.length; updateFt2PauseUi(); return; }
|
||||||
|
scheduleFt2HistoryRender();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.restoreFt2History = function(messages) { window.onServerFt2Batch(messages); };
|
||||||
|
window.pruneFt2HistoryView = function() { pruneFt2MessageHistory(); renderFt2History(); };
|
||||||
|
|
||||||
|
window.resetFt2HistoryView = function() {
|
||||||
|
if (ft2MessagesEl) ft2MessagesEl.innerHTML = "";
|
||||||
|
ft2MessageHistory = [];
|
||||||
|
ft2BufferedWhilePaused = 0;
|
||||||
|
renderFt2History();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ft2FilterInput) {
|
||||||
|
ft2FilterInput.addEventListener("input", () => {
|
||||||
|
ft2FilterText = ft2FilterInput.value.trim().toUpperCase();
|
||||||
|
renderFt2History();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ft2PauseBtn) {
|
||||||
|
ft2PauseBtn.addEventListener("click", () => {
|
||||||
|
ft2Paused = !ft2Paused;
|
||||||
|
if (!ft2Paused) { ft2BufferedWhilePaused = 0; renderFt2History(); } else { updateFt2PauseUi(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("ft2-decode-toggle-btn")?.addEventListener("click", async () => {
|
||||||
|
try { await postPath("/toggle_ft2_decode"); } catch (e) { console.error("FT2 toggle failed", e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("ft2-clear-btn")?.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await postPath("/clear_ft2_decode");
|
||||||
|
window.resetFt2HistoryView();
|
||||||
|
} catch (e) { console.error("FT2 clear failed", e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onServerFt2 = function(msg) {
|
||||||
|
if (ft2Status) ft2Status.textContent = ft2Paused ? "Paused" : "Receiving";
|
||||||
|
const next = normalizeServerFt2Message(msg);
|
||||||
|
if (next.grids.length > 0 && window.mapAddLocator) {
|
||||||
|
window.mapAddLocator(next.raw, next.grids, "ft2", next.station, { ...msg, freq_hz: next.rfHz, locator_details: next.locatorDetails });
|
||||||
|
}
|
||||||
|
addFt2Message(next.history);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateFt2PauseUi();
|
||||||
@@ -451,6 +451,7 @@ struct DecodeHistoryPayload {
|
|||||||
cw: Vec<trx_core::decode::CwEvent>,
|
cw: Vec<trx_core::decode::CwEvent>,
|
||||||
ft8: Vec<trx_core::decode::Ft8Message>,
|
ft8: Vec<trx_core::decode::Ft8Message>,
|
||||||
ft4: Vec<trx_core::decode::Ft8Message>,
|
ft4: Vec<trx_core::decode::Ft8Message>,
|
||||||
|
ft2: Vec<trx_core::decode::Ft8Message>,
|
||||||
wspr: Vec<trx_core::decode::WsprMessage>,
|
wspr: Vec<trx_core::decode::WsprMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +464,7 @@ impl DecodeHistoryPayload {
|
|||||||
+ self.cw.len()
|
+ self.cw.len()
|
||||||
+ self.ft8.len()
|
+ self.ft8.len()
|
||||||
+ self.ft4.len()
|
+ self.ft4.len()
|
||||||
|
+ self.ft2.len()
|
||||||
+ self.wspr.len()
|
+ self.wspr.len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,6 +479,7 @@ fn collect_decode_history(context: &FrontendRuntimeContext) -> DecodeHistoryPayl
|
|||||||
cw: crate::server::audio::snapshot_cw_history(context),
|
cw: crate::server::audio::snapshot_cw_history(context),
|
||||||
ft8: crate::server::audio::snapshot_ft8_history(context),
|
ft8: crate::server::audio::snapshot_ft8_history(context),
|
||||||
ft4: crate::server::audio::snapshot_ft4_history(context),
|
ft4: crate::server::audio::snapshot_ft4_history(context),
|
||||||
|
ft2: crate::server::audio::snapshot_ft2_history(context),
|
||||||
wspr: crate::server::audio::snapshot_wspr_history(context),
|
wspr: crate::server::audio::snapshot_wspr_history(context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1014,6 +1017,15 @@ pub async fn toggle_ft4_decode(
|
|||||||
send_command(&rig_tx, RigCommand::SetFt4DecodeEnabled(!enabled)).await
|
send_command(&rig_tx, RigCommand::SetFt4DecodeEnabled(!enabled)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/toggle_ft2_decode")]
|
||||||
|
pub async fn toggle_ft2_decode(
|
||||||
|
state: web::Data<watch::Receiver<RigState>>,
|
||||||
|
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let enabled = state.get_ref().borrow().ft2_decode_enabled;
|
||||||
|
send_command(&rig_tx, RigCommand::SetFt2DecodeEnabled(!enabled)).await
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/toggle_wspr_decode")]
|
#[post("/toggle_wspr_decode")]
|
||||||
pub async fn toggle_wspr_decode(
|
pub async fn toggle_wspr_decode(
|
||||||
state: web::Data<watch::Receiver<RigState>>,
|
state: web::Data<watch::Receiver<RigState>>,
|
||||||
@@ -1041,6 +1053,15 @@ pub async fn clear_ft4_decode(
|
|||||||
send_command(&rig_tx, RigCommand::ResetFt4Decoder).await
|
send_command(&rig_tx, RigCommand::ResetFt4Decoder).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/clear_ft2_decode")]
|
||||||
|
pub async fn clear_ft2_decode(
|
||||||
|
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||||
|
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
crate::server::audio::clear_ft2_history(context.get_ref());
|
||||||
|
send_command(&rig_tx, RigCommand::ResetFt2Decoder).await
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/clear_wspr_decode")]
|
#[post("/clear_wspr_decode")]
|
||||||
pub async fn clear_wspr_decode(
|
pub async fn clear_wspr_decode(
|
||||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||||
@@ -1524,6 +1545,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(set_cw_tone)
|
.service(set_cw_tone)
|
||||||
.service(toggle_ft8_decode)
|
.service(toggle_ft8_decode)
|
||||||
.service(toggle_ft4_decode)
|
.service(toggle_ft4_decode)
|
||||||
|
.service(toggle_ft2_decode)
|
||||||
.service(toggle_wspr_decode)
|
.service(toggle_wspr_decode)
|
||||||
.service(clear_ais_decode)
|
.service(clear_ais_decode)
|
||||||
.service(clear_vdes_decode)
|
.service(clear_vdes_decode)
|
||||||
@@ -1532,6 +1554,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(clear_cw_decode)
|
.service(clear_cw_decode)
|
||||||
.service(clear_ft8_decode)
|
.service(clear_ft8_decode)
|
||||||
.service(clear_ft4_decode)
|
.service(clear_ft4_decode)
|
||||||
|
.service(clear_ft2_decode)
|
||||||
.service(clear_wspr_decode)
|
.service(clear_wspr_decode)
|
||||||
.service(select_rig)
|
.service(select_rig)
|
||||||
// Bookmark CRUD
|
// Bookmark CRUD
|
||||||
@@ -1566,6 +1589,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(hf_aprs_js)
|
.service(hf_aprs_js)
|
||||||
.service(ft8_js)
|
.service(ft8_js)
|
||||||
.service(ft4_js)
|
.service(ft4_js)
|
||||||
|
.service(ft2_js)
|
||||||
.service(wspr_js)
|
.service(wspr_js)
|
||||||
.service(cw_js)
|
.service(cw_js)
|
||||||
.service(bookmarks_js)
|
.service(bookmarks_js)
|
||||||
@@ -1746,6 +1770,13 @@ async fn ft4_js() -> impl Responder {
|
|||||||
.body(status::FT4_JS)
|
.body(status::FT4_JS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/ft2.js")]
|
||||||
|
async fn ft2_js() -> impl Responder {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.insert_header((header::CONTENT_TYPE, "application/javascript; charset=utf-8"))
|
||||||
|
.body(status::FT2_JS)
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/wspr.js")]
|
#[get("/wspr.js")]
|
||||||
async fn wspr_js() -> impl Responder {
|
async fn wspr_js() -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
@@ -1879,11 +1910,12 @@ async fn send_command_to_rig(
|
|||||||
|
|
||||||
fn bookmark_decoder_state(
|
fn bookmark_decoder_state(
|
||||||
bookmark: &crate::server::bookmarks::Bookmark,
|
bookmark: &crate::server::bookmarks::Bookmark,
|
||||||
) -> (bool, bool, bool, bool, bool) {
|
) -> (bool, bool, bool, bool, bool, bool) {
|
||||||
let mut want_aprs = bookmark.mode.trim().eq_ignore_ascii_case("PKT");
|
let mut want_aprs = bookmark.mode.trim().eq_ignore_ascii_case("PKT");
|
||||||
let mut want_hf_aprs = false;
|
let mut want_hf_aprs = false;
|
||||||
let mut want_ft8 = false;
|
let mut want_ft8 = false;
|
||||||
let mut want_ft4 = false;
|
let mut want_ft4 = false;
|
||||||
|
let mut want_ft2 = false;
|
||||||
let mut want_wspr = false;
|
let mut want_wspr = false;
|
||||||
|
|
||||||
for decoder in bookmark
|
for decoder in bookmark
|
||||||
@@ -1896,12 +1928,13 @@ fn bookmark_decoder_state(
|
|||||||
"hf-aprs" => want_hf_aprs = true,
|
"hf-aprs" => want_hf_aprs = true,
|
||||||
"ft8" => want_ft8 = true,
|
"ft8" => want_ft8 = true,
|
||||||
"ft4" => want_ft4 = true,
|
"ft4" => want_ft4 = true,
|
||||||
|
"ft2" => want_ft2 = true,
|
||||||
"wspr" => want_wspr = true,
|
"wspr" => want_wspr = true,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(want_aprs, want_hf_aprs, want_ft8, want_ft4, want_wspr)
|
(want_aprs, want_hf_aprs, want_ft8, want_ft4, want_ft2, want_wspr)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bookmark_decoder_kinds(bookmark: &crate::server::bookmarks::Bookmark) -> Vec<String> {
|
fn bookmark_decoder_kinds(bookmark: &crate::server::bookmarks::Bookmark) -> Vec<String> {
|
||||||
@@ -1913,7 +1946,7 @@ fn bookmark_decoder_kinds(bookmark: &crate::server::bookmarks::Bookmark) -> Vec<
|
|||||||
{
|
{
|
||||||
if matches!(
|
if matches!(
|
||||||
decoder.as_str(),
|
decoder.as_str(),
|
||||||
"aprs" | "ais" | "ft8" | "ft4" | "wspr" | "hf-aprs"
|
"aprs" | "ais" | "ft8" | "ft4" | "ft2" | "wspr" | "hf-aprs"
|
||||||
) && !out.iter().any(|existing| existing == &decoder)
|
) && !out.iter().any(|existing| existing == &decoder)
|
||||||
{
|
{
|
||||||
out.push(decoder);
|
out.push(decoder);
|
||||||
@@ -1968,12 +2001,13 @@ async fn apply_selected_channel(
|
|||||||
let Some(bookmark) = bookmark_store.get(bookmark_id) else {
|
let Some(bookmark) = bookmark_store.get(bookmark_id) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let (want_aprs, want_hf_aprs, want_ft8, want_ft4, want_wspr) = bookmark_decoder_state(&bookmark);
|
let (want_aprs, want_hf_aprs, want_ft8, want_ft4, want_ft2, want_wspr) = bookmark_decoder_state(&bookmark);
|
||||||
let desired = [
|
let desired = [
|
||||||
RigCommand::SetAprsDecodeEnabled(want_aprs),
|
RigCommand::SetAprsDecodeEnabled(want_aprs),
|
||||||
RigCommand::SetHfAprsDecodeEnabled(want_hf_aprs),
|
RigCommand::SetHfAprsDecodeEnabled(want_hf_aprs),
|
||||||
RigCommand::SetFt8DecodeEnabled(want_ft8),
|
RigCommand::SetFt8DecodeEnabled(want_ft8),
|
||||||
RigCommand::SetFt4DecodeEnabled(want_ft4),
|
RigCommand::SetFt4DecodeEnabled(want_ft4),
|
||||||
|
RigCommand::SetFt2DecodeEnabled(want_ft2),
|
||||||
RigCommand::SetWsprDecodeEnabled(want_wspr),
|
RigCommand::SetWsprDecodeEnabled(want_wspr),
|
||||||
];
|
];
|
||||||
for cmd in desired {
|
for cmd in desired {
|
||||||
@@ -2022,6 +2056,7 @@ async fn wait_for_view(mut rx: watch::Receiver<RigState>) -> Result<RigSnapshot,
|
|||||||
cw_tone_hz: state.cw_tone_hz,
|
cw_tone_hz: state.cw_tone_hz,
|
||||||
ft8_decode_enabled: state.ft8_decode_enabled,
|
ft8_decode_enabled: state.ft8_decode_enabled,
|
||||||
ft4_decode_enabled: state.ft4_decode_enabled,
|
ft4_decode_enabled: state.ft4_decode_enabled,
|
||||||
|
ft2_decode_enabled: state.ft2_decode_enabled,
|
||||||
wspr_decode_enabled: state.wspr_decode_enabled,
|
wspr_decode_enabled: state.wspr_decode_enabled,
|
||||||
filter: state.filter.clone(),
|
filter: state.filter.clone(),
|
||||||
spectrum: None,
|
spectrum: None,
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ fn prune_ft4_history(context: &FrontendRuntimeContext, history: &mut VecDeque<(I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prune_ft2_history(context: &FrontendRuntimeContext, history: &mut VecDeque<(Instant, Ft8Message)>) {
|
||||||
|
let cutoff = decode_history_cutoff(context);
|
||||||
|
while let Some((ts, _)) = history.front() {
|
||||||
|
if *ts >= cutoff {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
history.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn prune_wspr_history(
|
fn prune_wspr_history(
|
||||||
context: &FrontendRuntimeContext,
|
context: &FrontendRuntimeContext,
|
||||||
history: &mut VecDeque<(Instant, WsprMessage)>,
|
history: &mut VecDeque<(Instant, WsprMessage)>,
|
||||||
@@ -221,6 +231,15 @@ fn record_ft4(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
|||||||
prune_ft4_history(context, &mut history);
|
prune_ft4_history(context, &mut history);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_ft2(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||||
|
let mut history = context
|
||||||
|
.ft2_history
|
||||||
|
.lock()
|
||||||
|
.expect("ft2 history mutex poisoned");
|
||||||
|
history.push_back((Instant::now(), msg));
|
||||||
|
prune_ft2_history(context, &mut history);
|
||||||
|
}
|
||||||
|
|
||||||
fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
|
fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
|
||||||
let mut history = context
|
let mut history = context
|
||||||
.wspr_history
|
.wspr_history
|
||||||
@@ -308,6 +327,15 @@ pub fn snapshot_ft4_history(context: &FrontendRuntimeContext) -> Vec<Ft8Message>
|
|||||||
history.iter().map(|(_, msg)| msg.clone()).collect()
|
history.iter().map(|(_, msg)| msg.clone()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn snapshot_ft2_history(context: &FrontendRuntimeContext) -> Vec<Ft8Message> {
|
||||||
|
let mut history = context
|
||||||
|
.ft2_history
|
||||||
|
.lock()
|
||||||
|
.expect("ft2 history mutex poisoned");
|
||||||
|
prune_ft2_history(context, &mut history);
|
||||||
|
history.iter().map(|(_, msg)| msg.clone()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn snapshot_wspr_history(context: &FrontendRuntimeContext) -> Vec<WsprMessage> {
|
pub fn snapshot_wspr_history(context: &FrontendRuntimeContext) -> Vec<WsprMessage> {
|
||||||
let mut history = context
|
let mut history = context
|
||||||
.wspr_history
|
.wspr_history
|
||||||
@@ -373,6 +401,14 @@ pub fn clear_ft4_history(context: &FrontendRuntimeContext) {
|
|||||||
history.clear();
|
history.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_ft2_history(context: &FrontendRuntimeContext) {
|
||||||
|
let mut history = context
|
||||||
|
.ft2_history
|
||||||
|
.lock()
|
||||||
|
.expect("ft2 history mutex poisoned");
|
||||||
|
history.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_wspr_history(context: &FrontendRuntimeContext) {
|
pub fn clear_wspr_history(context: &FrontendRuntimeContext) {
|
||||||
let mut history = context
|
let mut history = context
|
||||||
.wspr_history
|
.wspr_history
|
||||||
@@ -411,6 +447,7 @@ pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
|
|||||||
DecodedMessage::Cw(evt) => record_cw(&context, evt),
|
DecodedMessage::Cw(evt) => record_cw(&context, evt),
|
||||||
DecodedMessage::Ft8(msg) => record_ft8(&context, msg),
|
DecodedMessage::Ft8(msg) => record_ft8(&context, msg),
|
||||||
DecodedMessage::Ft4(msg) => record_ft4(&context, msg),
|
DecodedMessage::Ft4(msg) => record_ft4(&context, msg),
|
||||||
|
DecodedMessage::Ft2(msg) => record_ft2(&context, msg),
|
||||||
DecodedMessage::Wspr(msg) => record_wspr(&context, msg),
|
DecodedMessage::Wspr(msg) => record_wspr(&context, msg),
|
||||||
},
|
},
|
||||||
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::server::bookmarks::{Bookmark, BookmarkStore};
|
|||||||
use crate::server::scheduler::{SchedulerStatusMap, SharedSchedulerControlManager};
|
use crate::server::scheduler::{SchedulerStatusMap, SharedSchedulerControlManager};
|
||||||
use crate::server::vchan::{ClientChannel, ClientChannelManager};
|
use crate::server::vchan::{ClientChannel, ClientChannelManager};
|
||||||
|
|
||||||
const SUPPORTED_DECODER_KINDS: &[&str] = &["aprs", "ais", "ft8", "ft4", "wspr", "hf-aprs"];
|
const SUPPORTED_DECODER_KINDS: &[&str] = &["aprs", "ais", "ft8", "ft4", "ft2", "wspr", "hf-aprs"];
|
||||||
const CHANNEL_KIND_NAME: &str = "VirtualBackgroundDecodeChannel";
|
const CHANNEL_KIND_NAME: &str = "VirtualBackgroundDecodeChannel";
|
||||||
const VISIBLE_CHANNEL_KIND_NAME: &str = "VirtualChannel";
|
const VISIBLE_CHANNEL_KIND_NAME: &str = "VirtualChannel";
|
||||||
|
|
||||||
|
|||||||
@@ -673,6 +673,7 @@ async fn apply_scheduler_decoders(
|
|||||||
let mut want_hf_aprs = false;
|
let mut want_hf_aprs = false;
|
||||||
let mut want_ft8 = false;
|
let mut want_ft8 = false;
|
||||||
let mut want_ft4 = false;
|
let mut want_ft4 = false;
|
||||||
|
let mut want_ft2 = false;
|
||||||
let mut want_wspr = false;
|
let mut want_wspr = false;
|
||||||
|
|
||||||
let mut update_from = |bm: &crate::server::bookmarks::Bookmark| {
|
let mut update_from = |bm: &crate::server::bookmarks::Bookmark| {
|
||||||
@@ -682,6 +683,7 @@ async fn apply_scheduler_decoders(
|
|||||||
"hf-aprs" => want_hf_aprs = true,
|
"hf-aprs" => want_hf_aprs = true,
|
||||||
"ft8" => want_ft8 = true,
|
"ft8" => want_ft8 = true,
|
||||||
"ft4" => want_ft4 = true,
|
"ft4" => want_ft4 = true,
|
||||||
|
"ft2" => want_ft2 = true,
|
||||||
"wspr" => want_wspr = true,
|
"wspr" => want_wspr = true,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -698,6 +700,7 @@ async fn apply_scheduler_decoders(
|
|||||||
("HF APRS", RigCommand::SetHfAprsDecodeEnabled(want_hf_aprs)),
|
("HF APRS", RigCommand::SetHfAprsDecodeEnabled(want_hf_aprs)),
|
||||||
("FT8", RigCommand::SetFt8DecodeEnabled(want_ft8)),
|
("FT8", RigCommand::SetFt8DecodeEnabled(want_ft8)),
|
||||||
("FT4", RigCommand::SetFt4DecodeEnabled(want_ft4)),
|
("FT4", RigCommand::SetFt4DecodeEnabled(want_ft4)),
|
||||||
|
("FT2", RigCommand::SetFt2DecodeEnabled(want_ft2)),
|
||||||
("WSPR", RigCommand::SetWsprDecodeEnabled(want_wspr)),
|
("WSPR", RigCommand::SetWsprDecodeEnabled(want_wspr)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub const APRS_JS: &str = include_str!("../assets/web/plugins/aprs.js");
|
|||||||
pub const HF_APRS_JS: &str = include_str!("../assets/web/plugins/hf-aprs.js");
|
pub const HF_APRS_JS: &str = include_str!("../assets/web/plugins/hf-aprs.js");
|
||||||
pub const FT8_JS: &str = include_str!("../assets/web/plugins/ft8.js");
|
pub const FT8_JS: &str = include_str!("../assets/web/plugins/ft8.js");
|
||||||
pub const FT4_JS: &str = include_str!("../assets/web/plugins/ft4.js");
|
pub const FT4_JS: &str = include_str!("../assets/web/plugins/ft4.js");
|
||||||
|
pub const FT2_JS: &str = include_str!("../assets/web/plugins/ft2.js");
|
||||||
pub const WSPR_JS: &str = include_str!("../assets/web/plugins/wspr.js");
|
pub const WSPR_JS: &str = include_str!("../assets/web/plugins/wspr.js");
|
||||||
pub const CW_JS: &str = include_str!("../assets/web/plugins/cw.js");
|
pub const CW_JS: &str = include_str!("../assets/web/plugins/cw.js");
|
||||||
pub const BOOKMARKS_JS: &str = include_str!("../assets/web/plugins/bookmarks.js");
|
pub const BOOKMARKS_JS: &str = include_str!("../assets/web/plugins/bookmarks.js");
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ pub const AUDIO_MSG_VCHAN_DESTROYED: u8 = 0x12;
|
|||||||
pub const AUDIO_MSG_VCHAN_BW: u8 = 0x13;
|
pub const AUDIO_MSG_VCHAN_BW: u8 = 0x13;
|
||||||
/// Server → client: FT4 decoded message (JSON `DecodedMessage::Ft4`).
|
/// Server → client: FT4 decoded message (JSON `DecodedMessage::Ft4`).
|
||||||
pub const AUDIO_MSG_FT4_DECODE: u8 = 0x14;
|
pub const AUDIO_MSG_FT4_DECODE: u8 = 0x14;
|
||||||
|
/// Server → client: FT2 decoded message (JSON `DecodedMessage::Ft2`).
|
||||||
|
pub const AUDIO_MSG_FT2_DECODE: u8 = 0x15;
|
||||||
|
|
||||||
/// Maximum payload size for normal messages (1 MB).
|
/// Maximum payload size for normal messages (1 MB).
|
||||||
const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
|
const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub enum DecodedMessage {
|
|||||||
Ft8(Ft8Message),
|
Ft8(Ft8Message),
|
||||||
#[serde(rename = "ft4")]
|
#[serde(rename = "ft4")]
|
||||||
Ft4(Ft8Message),
|
Ft4(Ft8Message),
|
||||||
|
#[serde(rename = "ft2")]
|
||||||
|
Ft2(Ft8Message),
|
||||||
#[serde(rename = "wspr")]
|
#[serde(rename = "wspr")]
|
||||||
Wspr(WsprMessage),
|
Wspr(WsprMessage),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ pub enum RigCommand {
|
|||||||
SetCwToneHz(u32),
|
SetCwToneHz(u32),
|
||||||
SetFt8DecodeEnabled(bool),
|
SetFt8DecodeEnabled(bool),
|
||||||
SetFt4DecodeEnabled(bool),
|
SetFt4DecodeEnabled(bool),
|
||||||
|
SetFt2DecodeEnabled(bool),
|
||||||
SetWsprDecodeEnabled(bool),
|
SetWsprDecodeEnabled(bool),
|
||||||
ResetAprsDecoder,
|
ResetAprsDecoder,
|
||||||
ResetHfAprsDecoder,
|
ResetHfAprsDecoder,
|
||||||
ResetCwDecoder,
|
ResetCwDecoder,
|
||||||
ResetFt8Decoder,
|
ResetFt8Decoder,
|
||||||
ResetFt4Decoder,
|
ResetFt4Decoder,
|
||||||
|
ResetFt2Decoder,
|
||||||
ResetWsprDecoder,
|
ResetWsprDecoder,
|
||||||
SetBandwidth(u32),
|
SetBandwidth(u32),
|
||||||
SetFirTaps(u32),
|
SetFirTaps(u32),
|
||||||
|
|||||||
@@ -510,6 +510,7 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
|
|||||||
| RigCommand::SetCwToneHz(_)
|
| RigCommand::SetCwToneHz(_)
|
||||||
| RigCommand::SetFt8DecodeEnabled(_)
|
| RigCommand::SetFt8DecodeEnabled(_)
|
||||||
| RigCommand::SetFt4DecodeEnabled(_)
|
| RigCommand::SetFt4DecodeEnabled(_)
|
||||||
|
| RigCommand::SetFt2DecodeEnabled(_)
|
||||||
| RigCommand::SetWsprDecodeEnabled(_)
|
| RigCommand::SetWsprDecodeEnabled(_)
|
||||||
| RigCommand::SetHfAprsDecodeEnabled(_)
|
| RigCommand::SetHfAprsDecodeEnabled(_)
|
||||||
| RigCommand::ResetHfAprsDecoder
|
| RigCommand::ResetHfAprsDecoder
|
||||||
@@ -517,6 +518,7 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
|
|||||||
| RigCommand::ResetCwDecoder
|
| RigCommand::ResetCwDecoder
|
||||||
| RigCommand::ResetFt8Decoder
|
| RigCommand::ResetFt8Decoder
|
||||||
| RigCommand::ResetFt4Decoder
|
| RigCommand::ResetFt4Decoder
|
||||||
|
| RigCommand::ResetFt2Decoder
|
||||||
| RigCommand::ResetWsprDecoder
|
| RigCommand::ResetWsprDecoder
|
||||||
| RigCommand::SetBandwidth(_)
|
| RigCommand::SetBandwidth(_)
|
||||||
| RigCommand::SetFirTaps(_)
|
| RigCommand::SetFirTaps(_)
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ pub struct RigState {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ft4_decode_enabled: bool,
|
pub ft4_decode_enabled: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub ft2_decode_enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub wspr_decode_enabled: bool,
|
pub wspr_decode_enabled: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cw_auto: bool,
|
pub cw_auto: bool,
|
||||||
@@ -70,6 +72,8 @@ pub struct RigState {
|
|||||||
#[serde(default, skip_serializing)]
|
#[serde(default, skip_serializing)]
|
||||||
pub ft4_decode_reset_seq: u64,
|
pub ft4_decode_reset_seq: u64,
|
||||||
#[serde(default, skip_serializing)]
|
#[serde(default, skip_serializing)]
|
||||||
|
pub ft2_decode_reset_seq: u64,
|
||||||
|
#[serde(default, skip_serializing)]
|
||||||
pub wspr_decode_reset_seq: u64,
|
pub wspr_decode_reset_seq: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +152,7 @@ impl RigState {
|
|||||||
cw_decode_enabled: true,
|
cw_decode_enabled: true,
|
||||||
ft8_decode_enabled: false,
|
ft8_decode_enabled: false,
|
||||||
ft4_decode_enabled: false,
|
ft4_decode_enabled: false,
|
||||||
|
ft2_decode_enabled: false,
|
||||||
wspr_decode_enabled: false,
|
wspr_decode_enabled: false,
|
||||||
cw_auto: true,
|
cw_auto: true,
|
||||||
cw_wpm: 15,
|
cw_wpm: 15,
|
||||||
@@ -160,6 +165,7 @@ impl RigState {
|
|||||||
cw_decode_reset_seq: 0,
|
cw_decode_reset_seq: 0,
|
||||||
ft8_decode_reset_seq: 0,
|
ft8_decode_reset_seq: 0,
|
||||||
ft4_decode_reset_seq: 0,
|
ft4_decode_reset_seq: 0,
|
||||||
|
ft2_decode_reset_seq: 0,
|
||||||
wspr_decode_reset_seq: 0,
|
wspr_decode_reset_seq: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,6 +223,7 @@ impl RigState {
|
|||||||
cw_tone_hz: snapshot.cw_tone_hz,
|
cw_tone_hz: snapshot.cw_tone_hz,
|
||||||
ft8_decode_enabled: snapshot.ft8_decode_enabled,
|
ft8_decode_enabled: snapshot.ft8_decode_enabled,
|
||||||
ft4_decode_enabled: snapshot.ft4_decode_enabled,
|
ft4_decode_enabled: snapshot.ft4_decode_enabled,
|
||||||
|
ft2_decode_enabled: snapshot.ft2_decode_enabled,
|
||||||
wspr_decode_enabled: snapshot.wspr_decode_enabled,
|
wspr_decode_enabled: snapshot.wspr_decode_enabled,
|
||||||
filter: snapshot.filter,
|
filter: snapshot.filter,
|
||||||
spectrum: None, // spectrum flows through /api/spectrum, not persistent state
|
spectrum: None, // spectrum flows through /api/spectrum, not persistent state
|
||||||
@@ -226,6 +233,7 @@ impl RigState {
|
|||||||
cw_decode_reset_seq: 0,
|
cw_decode_reset_seq: 0,
|
||||||
ft8_decode_reset_seq: 0,
|
ft8_decode_reset_seq: 0,
|
||||||
ft4_decode_reset_seq: 0,
|
ft4_decode_reset_seq: 0,
|
||||||
|
ft2_decode_reset_seq: 0,
|
||||||
wspr_decode_reset_seq: 0,
|
wspr_decode_reset_seq: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,6 +269,7 @@ impl RigState {
|
|||||||
cw_tone_hz: self.cw_tone_hz,
|
cw_tone_hz: self.cw_tone_hz,
|
||||||
ft8_decode_enabled: self.ft8_decode_enabled,
|
ft8_decode_enabled: self.ft8_decode_enabled,
|
||||||
ft4_decode_enabled: self.ft4_decode_enabled,
|
ft4_decode_enabled: self.ft4_decode_enabled,
|
||||||
|
ft2_decode_enabled: self.ft2_decode_enabled,
|
||||||
wspr_decode_enabled: self.wspr_decode_enabled,
|
wspr_decode_enabled: self.wspr_decode_enabled,
|
||||||
filter: self.filter.clone(),
|
filter: self.filter.clone(),
|
||||||
spectrum: self.spectrum.clone(),
|
spectrum: self.spectrum.clone(),
|
||||||
@@ -427,6 +436,8 @@ pub struct RigSnapshot {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ft4_decode_enabled: bool,
|
pub ft4_decode_enabled: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub ft2_decode_enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub wspr_decode_enabled: bool,
|
pub wspr_decode_enabled: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cw_auto: bool,
|
pub cw_auto: bool,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand {
|
|||||||
ClientCommand::SetCwToneHz { tone_hz } => RigCommand::SetCwToneHz(tone_hz),
|
ClientCommand::SetCwToneHz { tone_hz } => RigCommand::SetCwToneHz(tone_hz),
|
||||||
ClientCommand::SetFt8DecodeEnabled { enabled } => RigCommand::SetFt8DecodeEnabled(enabled),
|
ClientCommand::SetFt8DecodeEnabled { enabled } => RigCommand::SetFt8DecodeEnabled(enabled),
|
||||||
ClientCommand::SetFt4DecodeEnabled { enabled } => RigCommand::SetFt4DecodeEnabled(enabled),
|
ClientCommand::SetFt4DecodeEnabled { enabled } => RigCommand::SetFt4DecodeEnabled(enabled),
|
||||||
|
ClientCommand::SetFt2DecodeEnabled { enabled } => RigCommand::SetFt2DecodeEnabled(enabled),
|
||||||
ClientCommand::SetWsprDecodeEnabled { enabled } => {
|
ClientCommand::SetWsprDecodeEnabled { enabled } => {
|
||||||
RigCommand::SetWsprDecodeEnabled(enabled)
|
RigCommand::SetWsprDecodeEnabled(enabled)
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,7 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand {
|
|||||||
ClientCommand::ResetCwDecoder => RigCommand::ResetCwDecoder,
|
ClientCommand::ResetCwDecoder => RigCommand::ResetCwDecoder,
|
||||||
ClientCommand::ResetFt8Decoder => RigCommand::ResetFt8Decoder,
|
ClientCommand::ResetFt8Decoder => RigCommand::ResetFt8Decoder,
|
||||||
ClientCommand::ResetFt4Decoder => RigCommand::ResetFt4Decoder,
|
ClientCommand::ResetFt4Decoder => RigCommand::ResetFt4Decoder,
|
||||||
|
ClientCommand::ResetFt2Decoder => RigCommand::ResetFt2Decoder,
|
||||||
ClientCommand::ResetWsprDecoder => RigCommand::ResetWsprDecoder,
|
ClientCommand::ResetWsprDecoder => RigCommand::ResetWsprDecoder,
|
||||||
ClientCommand::SetBandwidth { bandwidth_hz } => RigCommand::SetBandwidth(bandwidth_hz),
|
ClientCommand::SetBandwidth { bandwidth_hz } => RigCommand::SetBandwidth(bandwidth_hz),
|
||||||
ClientCommand::SetFirTaps { taps } => RigCommand::SetFirTaps(taps),
|
ClientCommand::SetFirTaps { taps } => RigCommand::SetFirTaps(taps),
|
||||||
@@ -103,6 +105,7 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand {
|
|||||||
RigCommand::SetCwToneHz(tone_hz) => ClientCommand::SetCwToneHz { tone_hz },
|
RigCommand::SetCwToneHz(tone_hz) => ClientCommand::SetCwToneHz { tone_hz },
|
||||||
RigCommand::SetFt8DecodeEnabled(enabled) => ClientCommand::SetFt8DecodeEnabled { enabled },
|
RigCommand::SetFt8DecodeEnabled(enabled) => ClientCommand::SetFt8DecodeEnabled { enabled },
|
||||||
RigCommand::SetFt4DecodeEnabled(enabled) => ClientCommand::SetFt4DecodeEnabled { enabled },
|
RigCommand::SetFt4DecodeEnabled(enabled) => ClientCommand::SetFt4DecodeEnabled { enabled },
|
||||||
|
RigCommand::SetFt2DecodeEnabled(enabled) => ClientCommand::SetFt2DecodeEnabled { enabled },
|
||||||
RigCommand::SetWsprDecodeEnabled(enabled) => {
|
RigCommand::SetWsprDecodeEnabled(enabled) => {
|
||||||
ClientCommand::SetWsprDecodeEnabled { enabled }
|
ClientCommand::SetWsprDecodeEnabled { enabled }
|
||||||
}
|
}
|
||||||
@@ -111,6 +114,7 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand {
|
|||||||
RigCommand::ResetCwDecoder => ClientCommand::ResetCwDecoder,
|
RigCommand::ResetCwDecoder => ClientCommand::ResetCwDecoder,
|
||||||
RigCommand::ResetFt8Decoder => ClientCommand::ResetFt8Decoder,
|
RigCommand::ResetFt8Decoder => ClientCommand::ResetFt8Decoder,
|
||||||
RigCommand::ResetFt4Decoder => ClientCommand::ResetFt4Decoder,
|
RigCommand::ResetFt4Decoder => ClientCommand::ResetFt4Decoder,
|
||||||
|
RigCommand::ResetFt2Decoder => ClientCommand::ResetFt2Decoder,
|
||||||
RigCommand::ResetWsprDecoder => ClientCommand::ResetWsprDecoder,
|
RigCommand::ResetWsprDecoder => ClientCommand::ResetWsprDecoder,
|
||||||
RigCommand::SetBandwidth(bandwidth_hz) => ClientCommand::SetBandwidth { bandwidth_hz },
|
RigCommand::SetBandwidth(bandwidth_hz) => ClientCommand::SetBandwidth { bandwidth_hz },
|
||||||
RigCommand::SetFirTaps(taps) => ClientCommand::SetFirTaps { taps },
|
RigCommand::SetFirTaps(taps) => ClientCommand::SetFirTaps { taps },
|
||||||
|
|||||||
@@ -34,12 +34,14 @@ pub enum ClientCommand {
|
|||||||
SetCwToneHz { tone_hz: u32 },
|
SetCwToneHz { tone_hz: u32 },
|
||||||
SetFt8DecodeEnabled { enabled: bool },
|
SetFt8DecodeEnabled { enabled: bool },
|
||||||
SetFt4DecodeEnabled { enabled: bool },
|
SetFt4DecodeEnabled { enabled: bool },
|
||||||
|
SetFt2DecodeEnabled { enabled: bool },
|
||||||
SetWsprDecodeEnabled { enabled: bool },
|
SetWsprDecodeEnabled { enabled: bool },
|
||||||
ResetAprsDecoder,
|
ResetAprsDecoder,
|
||||||
ResetHfAprsDecoder,
|
ResetHfAprsDecoder,
|
||||||
ResetCwDecoder,
|
ResetCwDecoder,
|
||||||
ResetFt8Decoder,
|
ResetFt8Decoder,
|
||||||
ResetFt4Decoder,
|
ResetFt4Decoder,
|
||||||
|
ResetFt2Decoder,
|
||||||
ResetWsprDecoder,
|
ResetWsprDecoder,
|
||||||
SetBandwidth { bandwidth_hz: u32 },
|
SetBandwidth { bandwidth_hz: u32 },
|
||||||
SetFirTaps { taps: u32 },
|
SetFirTaps { taps: u32 },
|
||||||
|
|||||||
+269
-2
@@ -24,7 +24,8 @@ use trx_aprs::AprsDecoder;
|
|||||||
use trx_core::audio::{
|
use trx_core::audio::{
|
||||||
parse_vchan_uuid_msg, read_audio_msg, write_audio_msg, write_vchan_audio_frame,
|
parse_vchan_uuid_msg, read_audio_msg, write_audio_msg, write_vchan_audio_frame,
|
||||||
write_vchan_uuid_msg, AudioStreamInfo,
|
write_vchan_uuid_msg, AudioStreamInfo,
|
||||||
AUDIO_MSG_AIS_DECODE, AUDIO_MSG_APRS_DECODE, AUDIO_MSG_CW_DECODE, AUDIO_MSG_FT4_DECODE,
|
AUDIO_MSG_AIS_DECODE, AUDIO_MSG_APRS_DECODE, AUDIO_MSG_CW_DECODE, AUDIO_MSG_FT2_DECODE,
|
||||||
|
AUDIO_MSG_FT4_DECODE,
|
||||||
AUDIO_MSG_FT8_DECODE, AUDIO_MSG_HF_APRS_DECODE, AUDIO_MSG_HISTORY_COMPRESSED,
|
AUDIO_MSG_FT8_DECODE, AUDIO_MSG_HF_APRS_DECODE, AUDIO_MSG_HISTORY_COMPRESSED,
|
||||||
AUDIO_MSG_RX_FRAME, AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_VCHAN_ALLOCATED,
|
AUDIO_MSG_RX_FRAME, AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_VCHAN_ALLOCATED,
|
||||||
AUDIO_MSG_VCHAN_BW, AUDIO_MSG_VCHAN_DESTROYED, AUDIO_MSG_VCHAN_FREQ, AUDIO_MSG_VCHAN_MODE,
|
AUDIO_MSG_VCHAN_BW, AUDIO_MSG_VCHAN_DESTROYED, AUDIO_MSG_VCHAN_FREQ, AUDIO_MSG_VCHAN_MODE,
|
||||||
@@ -158,6 +159,7 @@ pub struct DecoderHistories {
|
|||||||
pub cw: Mutex<VecDeque<(Instant, CwEvent)>>,
|
pub cw: Mutex<VecDeque<(Instant, CwEvent)>>,
|
||||||
pub ft8: Mutex<VecDeque<(Instant, Ft8Message)>>,
|
pub ft8: Mutex<VecDeque<(Instant, Ft8Message)>>,
|
||||||
pub ft4: Mutex<VecDeque<(Instant, Ft8Message)>>,
|
pub ft4: Mutex<VecDeque<(Instant, Ft8Message)>>,
|
||||||
|
pub ft2: Mutex<VecDeque<(Instant, Ft8Message)>>,
|
||||||
pub wspr: Mutex<VecDeque<(Instant, WsprMessage)>>,
|
pub wspr: Mutex<VecDeque<(Instant, WsprMessage)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +173,7 @@ impl DecoderHistories {
|
|||||||
cw: Mutex::new(VecDeque::new()),
|
cw: Mutex::new(VecDeque::new()),
|
||||||
ft8: Mutex::new(VecDeque::new()),
|
ft8: Mutex::new(VecDeque::new()),
|
||||||
ft4: Mutex::new(VecDeque::new()),
|
ft4: Mutex::new(VecDeque::new()),
|
||||||
|
ft2: Mutex::new(VecDeque::new()),
|
||||||
wspr: Mutex::new(VecDeque::new()),
|
wspr: Mutex::new(VecDeque::new()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -394,6 +397,35 @@ impl DecoderHistories {
|
|||||||
self.ft4.lock().expect("ft4 history mutex poisoned").clear();
|
self.ft4.lock().expect("ft4 history mutex poisoned").clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- FT2 ---
|
||||||
|
|
||||||
|
fn prune_ft2(history: &mut VecDeque<(Instant, Ft8Message)>) {
|
||||||
|
let cutoff = Instant::now() - FT8_HISTORY_RETENTION;
|
||||||
|
while let Some((ts, _)) = history.front() {
|
||||||
|
if *ts < cutoff {
|
||||||
|
history.pop_front();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_ft2_message(&self, msg: Ft8Message) {
|
||||||
|
let mut h = self.ft2.lock().expect("ft2 history mutex poisoned");
|
||||||
|
h.push_back((Instant::now(), msg));
|
||||||
|
Self::prune_ft2(&mut h);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot_ft2_history(&self) -> Vec<Ft8Message> {
|
||||||
|
let mut h = self.ft2.lock().expect("ft2 history mutex poisoned");
|
||||||
|
Self::prune_ft2(&mut h);
|
||||||
|
h.iter().map(|(_, msg)| msg.clone()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_ft2_history(&self) {
|
||||||
|
self.ft2.lock().expect("ft2 history mutex poisoned").clear();
|
||||||
|
}
|
||||||
|
|
||||||
// --- WSPR ---
|
// --- WSPR ---
|
||||||
|
|
||||||
fn prune_wspr(history: &mut VecDeque<(Instant, WsprMessage)>) {
|
fn prune_wspr(history: &mut VecDeque<(Instant, WsprMessage)>) {
|
||||||
@@ -436,8 +468,9 @@ impl DecoderHistories {
|
|||||||
let cw = self.cw.lock().map(|h| h.len()).unwrap_or(0);
|
let cw = self.cw.lock().map(|h| h.len()).unwrap_or(0);
|
||||||
let ft8 = self.ft8.lock().map(|h| h.len()).unwrap_or(0);
|
let ft8 = self.ft8.lock().map(|h| h.len()).unwrap_or(0);
|
||||||
let ft4 = self.ft4.lock().map(|h| h.len()).unwrap_or(0);
|
let ft4 = self.ft4.lock().map(|h| h.len()).unwrap_or(0);
|
||||||
|
let ft2 = self.ft2.lock().map(|h| h.len()).unwrap_or(0);
|
||||||
let wspr = self.wspr.lock().map(|h| h.len()).unwrap_or(0);
|
let wspr = self.wspr.lock().map(|h| h.len()).unwrap_or(0);
|
||||||
ais + vdes + aprs + hf_aprs + cw + ft8 + ft4 + wspr
|
ais + vdes + aprs + hf_aprs + cw + ft8 + ft4 + ft2 + wspr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1820,6 +1853,148 @@ pub async fn run_ft4_decoder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the FT2 decoder task. Mirrors FT4 but uses FT2 protocol (7.5s slots for now).
|
||||||
|
pub async fn run_ft2_decoder(
|
||||||
|
sample_rate: u32,
|
||||||
|
channels: u16,
|
||||||
|
mut pcm_rx: broadcast::Receiver<Vec<f32>>,
|
||||||
|
mut state_rx: watch::Receiver<RigState>,
|
||||||
|
decode_tx: broadcast::Sender<DecodedMessage>,
|
||||||
|
histories: Arc<DecoderHistories>,
|
||||||
|
) {
|
||||||
|
info!("FT2 decoder started ({}Hz, {} ch)", sample_rate, channels);
|
||||||
|
let mut decoder = match Ft8Decoder::new_ft2(FT8_SAMPLE_RATE) {
|
||||||
|
Ok(decoder) => decoder,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("FT2 decoder init failed: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut last_reset_seq: u64 = 0;
|
||||||
|
let mut active = state_rx.borrow().ft2_decode_enabled
|
||||||
|
&& matches!(state_rx.borrow().status.mode, RigMode::DIG | RigMode::USB);
|
||||||
|
let mut ft2_buf: Vec<f32> = Vec::new();
|
||||||
|
let mut last_slot: i64 = -1;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !active {
|
||||||
|
match state_rx.changed().await {
|
||||||
|
Ok(()) => {
|
||||||
|
let state = state_rx.borrow();
|
||||||
|
active = state.ft2_decode_enabled
|
||||||
|
&& matches!(state.status.mode, RigMode::DIG | RigMode::USB);
|
||||||
|
if active {
|
||||||
|
pcm_rx = pcm_rx.resubscribe();
|
||||||
|
}
|
||||||
|
if state.ft2_decode_reset_seq != last_reset_seq {
|
||||||
|
last_reset_seq = state.ft2_decode_reset_seq;
|
||||||
|
decoder.reset();
|
||||||
|
ft2_buf.clear();
|
||||||
|
}
|
||||||
|
last_slot = -1;
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
recv = pcm_rx.recv() => {
|
||||||
|
match recv {
|
||||||
|
Ok(frame) => {
|
||||||
|
let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||||
|
Ok(dur) => dur.as_secs() as i64,
|
||||||
|
Err(_) => 0,
|
||||||
|
};
|
||||||
|
// FT2 slot period is 7.5s (same as FT4 for now); use now * 2 / 15
|
||||||
|
let slot = now * 2 / 15;
|
||||||
|
if slot != last_slot {
|
||||||
|
last_slot = slot;
|
||||||
|
decoder.reset();
|
||||||
|
ft2_buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let reset_seq = {
|
||||||
|
let state = state_rx.borrow();
|
||||||
|
state.ft2_decode_reset_seq
|
||||||
|
};
|
||||||
|
if reset_seq != last_reset_seq {
|
||||||
|
last_reset_seq = reset_seq;
|
||||||
|
decoder.reset();
|
||||||
|
ft2_buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mono = downmix_mono(frame, channels);
|
||||||
|
apply_decode_audio_gate(&mut mono);
|
||||||
|
let Some(resampled) = resample_to_12k(&mono, sample_rate) else {
|
||||||
|
warn!("FT2 decoder: unsupported sample rate {}", sample_rate);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
ft2_buf.extend_from_slice(&resampled);
|
||||||
|
|
||||||
|
while ft2_buf.len() >= decoder.block_size() {
|
||||||
|
let block: Vec<f32> = ft2_buf.drain(..decoder.block_size()).collect();
|
||||||
|
let results = tokio::task::block_in_place(|| {
|
||||||
|
decoder.process_block(&block);
|
||||||
|
decoder.decode_if_ready(100)
|
||||||
|
});
|
||||||
|
if !results.is_empty() {
|
||||||
|
for res in results {
|
||||||
|
let ts_ms = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||||
|
Ok(dur) => dur.as_millis() as i64,
|
||||||
|
Err(_) => 0,
|
||||||
|
};
|
||||||
|
let base_freq_hz = state_rx.borrow().status.freq.hz as f64;
|
||||||
|
let abs_freq_hz = base_freq_hz + res.freq_hz as f64;
|
||||||
|
let msg = Ft8Message {
|
||||||
|
ts_ms,
|
||||||
|
snr_db: res.snr_db,
|
||||||
|
dt_s: res.dt_s,
|
||||||
|
freq_hz: if abs_freq_hz.is_finite() && abs_freq_hz > 0.0 {
|
||||||
|
abs_freq_hz as f32
|
||||||
|
} else {
|
||||||
|
res.freq_hz
|
||||||
|
},
|
||||||
|
message: res.text,
|
||||||
|
};
|
||||||
|
histories.record_ft2_message(msg.clone());
|
||||||
|
let _ = decode_tx.send(DecodedMessage::Ft2(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
|
warn!("FT2 decoder: dropped {} PCM frames", n);
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Closed) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed = state_rx.changed() => {
|
||||||
|
match changed {
|
||||||
|
Ok(()) => {
|
||||||
|
let state = state_rx.borrow();
|
||||||
|
active = state.ft2_decode_enabled
|
||||||
|
&& matches!(state.status.mode, RigMode::DIG | RigMode::USB);
|
||||||
|
if state.ft2_decode_reset_seq != last_reset_seq {
|
||||||
|
last_reset_seq = state.ft2_decode_reset_seq;
|
||||||
|
decoder.reset();
|
||||||
|
ft2_buf.clear();
|
||||||
|
}
|
||||||
|
if !active {
|
||||||
|
decoder.reset();
|
||||||
|
ft2_buf.clear();
|
||||||
|
last_slot = -1;
|
||||||
|
} else {
|
||||||
|
pcm_rx = pcm_rx.resubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the WSPR decoder task. Mirrors FT8 lifecycle/slot behavior.
|
/// Run the WSPR decoder task. Mirrors FT8 lifecycle/slot behavior.
|
||||||
///
|
///
|
||||||
/// Note: decoding engine integration is intentionally staged; this task already
|
/// Note: decoding engine integration is intentionally staged; this task already
|
||||||
@@ -2250,6 +2425,85 @@ async fn run_background_ft4_decoder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn run_background_ft2_decoder(
|
||||||
|
sample_rate: u32,
|
||||||
|
channels: u16,
|
||||||
|
mut pcm_rx: broadcast::Receiver<Vec<f32>>,
|
||||||
|
base_freq_hz: u64,
|
||||||
|
decode_tx: broadcast::Sender<DecodedMessage>,
|
||||||
|
) {
|
||||||
|
info!(
|
||||||
|
"Background FT2 decoder started ({}Hz, {} ch @ {} Hz)",
|
||||||
|
sample_rate, channels, base_freq_hz
|
||||||
|
);
|
||||||
|
let mut decoder = match Ft8Decoder::new_ft2(FT8_SAMPLE_RATE) {
|
||||||
|
Ok(decoder) => decoder,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Background FT2 decoder init failed: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut ft2_buf: Vec<f32> = Vec::new();
|
||||||
|
let mut last_slot: i64 = -1;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match pcm_rx.recv().await {
|
||||||
|
Ok(frame) => {
|
||||||
|
let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
|
||||||
|
{
|
||||||
|
Ok(dur) => dur.as_secs() as i64,
|
||||||
|
Err(_) => 0,
|
||||||
|
};
|
||||||
|
// FT2 slot period is 7.5s (same as FT4 for now); use now * 2 / 15
|
||||||
|
let slot = now * 2 / 15;
|
||||||
|
if slot != last_slot {
|
||||||
|
last_slot = slot;
|
||||||
|
decoder.reset();
|
||||||
|
ft2_buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mono = downmix_mono(frame, channels);
|
||||||
|
apply_decode_audio_gate(&mut mono);
|
||||||
|
let Some(resampled) = resample_to_12k(&mono, sample_rate) else {
|
||||||
|
warn!(
|
||||||
|
"Background FT2 decoder: unsupported sample rate {}",
|
||||||
|
sample_rate
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
ft2_buf.extend_from_slice(&resampled);
|
||||||
|
|
||||||
|
while ft2_buf.len() >= decoder.block_size() {
|
||||||
|
let block: Vec<f32> = ft2_buf.drain(..decoder.block_size()).collect();
|
||||||
|
let results = tokio::task::block_in_place(|| {
|
||||||
|
decoder.process_block(&block);
|
||||||
|
decoder.decode_if_ready(100)
|
||||||
|
});
|
||||||
|
for res in results {
|
||||||
|
let abs_freq_hz = base_freq_hz as f64 + res.freq_hz as f64;
|
||||||
|
let msg = Ft8Message {
|
||||||
|
ts_ms: current_timestamp_ms(),
|
||||||
|
snr_db: res.snr_db,
|
||||||
|
dt_s: res.dt_s,
|
||||||
|
freq_hz: if abs_freq_hz.is_finite() && abs_freq_hz > 0.0 {
|
||||||
|
abs_freq_hz as f32
|
||||||
|
} else {
|
||||||
|
res.freq_hz
|
||||||
|
},
|
||||||
|
message: res.text,
|
||||||
|
};
|
||||||
|
let _ = decode_tx.send(DecodedMessage::Ft2(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
|
warn!("Background FT2 decoder: dropped {} PCM frames", n);
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Closed) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_background_wspr_decoder(
|
async fn run_background_wspr_decoder(
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
channels: u16,
|
channels: u16,
|
||||||
@@ -2452,6 +2706,7 @@ async fn handle_audio_client(
|
|||||||
push_history!(histories.snapshot_hf_aprs_history(), DecodedMessage::HfAprs, AUDIO_MSG_HF_APRS_DECODE);
|
push_history!(histories.snapshot_hf_aprs_history(), DecodedMessage::HfAprs, AUDIO_MSG_HF_APRS_DECODE);
|
||||||
push_history!(histories.snapshot_ft8_history(), DecodedMessage::Ft8, AUDIO_MSG_FT8_DECODE);
|
push_history!(histories.snapshot_ft8_history(), DecodedMessage::Ft8, AUDIO_MSG_FT8_DECODE);
|
||||||
push_history!(histories.snapshot_ft4_history(), DecodedMessage::Ft4, AUDIO_MSG_FT4_DECODE);
|
push_history!(histories.snapshot_ft4_history(), DecodedMessage::Ft4, AUDIO_MSG_FT4_DECODE);
|
||||||
|
push_history!(histories.snapshot_ft2_history(), DecodedMessage::Ft2, AUDIO_MSG_FT2_DECODE);
|
||||||
push_history!(histories.snapshot_wspr_history(), DecodedMessage::Wspr, AUDIO_MSG_WSPR_DECODE);
|
push_history!(histories.snapshot_wspr_history(), DecodedMessage::Wspr, AUDIO_MSG_WSPR_DECODE);
|
||||||
push_history!(histories.snapshot_cw_history(), DecodedMessage::Cw, AUDIO_MSG_CW_DECODE);
|
push_history!(histories.snapshot_cw_history(), DecodedMessage::Cw, AUDIO_MSG_CW_DECODE);
|
||||||
|
|
||||||
@@ -2538,6 +2793,7 @@ async fn handle_audio_client(
|
|||||||
DecodedMessage::Cw(_) => AUDIO_MSG_CW_DECODE,
|
DecodedMessage::Cw(_) => AUDIO_MSG_CW_DECODE,
|
||||||
DecodedMessage::Ft8(_) => AUDIO_MSG_FT8_DECODE,
|
DecodedMessage::Ft8(_) => AUDIO_MSG_FT8_DECODE,
|
||||||
DecodedMessage::Ft4(_) => AUDIO_MSG_FT4_DECODE,
|
DecodedMessage::Ft4(_) => AUDIO_MSG_FT4_DECODE,
|
||||||
|
DecodedMessage::Ft2(_) => AUDIO_MSG_FT2_DECODE,
|
||||||
DecodedMessage::Wspr(_) => AUDIO_MSG_WSPR_DECODE,
|
DecodedMessage::Wspr(_) => AUDIO_MSG_WSPR_DECODE,
|
||||||
};
|
};
|
||||||
if let Ok(json) = serde_json::to_vec(&msg) {
|
if let Ok(json) = serde_json::to_vec(&msg) {
|
||||||
@@ -2564,6 +2820,7 @@ async fn handle_audio_client(
|
|||||||
DecodedMessage::Cw(_) => AUDIO_MSG_CW_DECODE,
|
DecodedMessage::Cw(_) => AUDIO_MSG_CW_DECODE,
|
||||||
DecodedMessage::Ft8(_) => AUDIO_MSG_FT8_DECODE,
|
DecodedMessage::Ft8(_) => AUDIO_MSG_FT8_DECODE,
|
||||||
DecodedMessage::Ft4(_) => AUDIO_MSG_FT4_DECODE,
|
DecodedMessage::Ft4(_) => AUDIO_MSG_FT4_DECODE,
|
||||||
|
DecodedMessage::Ft2(_) => AUDIO_MSG_FT2_DECODE,
|
||||||
DecodedMessage::Wspr(_) => AUDIO_MSG_WSPR_DECODE,
|
DecodedMessage::Wspr(_) => AUDIO_MSG_WSPR_DECODE,
|
||||||
};
|
};
|
||||||
if let Ok(json) = serde_json::to_vec(&msg) {
|
if let Ok(json) = serde_json::to_vec(&msg) {
|
||||||
@@ -2704,6 +2961,16 @@ async fn handle_audio_client(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}),
|
}),
|
||||||
|
"ft2" => tokio::spawn(async move {
|
||||||
|
run_background_ft2_decoder(
|
||||||
|
sr,
|
||||||
|
ch_count,
|
||||||
|
task_rx,
|
||||||
|
base_freq_hz,
|
||||||
|
decode_tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}),
|
||||||
"wspr" => tokio::spawn(async move {
|
"wspr" => tokio::spawn(async move {
|
||||||
run_background_wspr_decoder(
|
run_background_wspr_decoder(
|
||||||
sr,
|
sr,
|
||||||
|
|||||||
@@ -741,6 +741,21 @@ fn spawn_rig_audio_stack(
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Spawn FT2 decoder task
|
||||||
|
let ft2_pcm_rx = pcm_tx.subscribe();
|
||||||
|
let ft2_state_rx = state_rx.clone();
|
||||||
|
let ft2_decode_tx = decode_tx.clone();
|
||||||
|
let ft2_sr = rig_cfg.audio.sample_rate;
|
||||||
|
let ft2_ch = rig_cfg.audio.channels;
|
||||||
|
let ft2_shutdown_rx = shutdown_rx.clone();
|
||||||
|
let ft2_histories = histories.clone();
|
||||||
|
handles.push(tokio::spawn(async move {
|
||||||
|
tokio::select! {
|
||||||
|
_ = audio::run_ft2_decoder(ft2_sr, ft2_ch as u16, ft2_pcm_rx, ft2_state_rx, ft2_decode_tx, ft2_histories) => {}
|
||||||
|
_ = wait_for_shutdown(ft2_shutdown_rx) => {}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// Spawn WSPR decoder task
|
// Spawn WSPR decoder task
|
||||||
let wspr_pcm_rx = pcm_tx.subscribe();
|
let wspr_pcm_rx = pcm_tx.subscribe();
|
||||||
let wspr_state_rx = state_rx.clone();
|
let wspr_state_rx = state_rx.clone();
|
||||||
|
|||||||
@@ -476,6 +476,12 @@ async fn process_command(
|
|||||||
let _ = ctx.state_tx.send(ctx.state.clone());
|
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||||
return snapshot_from(ctx.state);
|
return snapshot_from(ctx.state);
|
||||||
}
|
}
|
||||||
|
RigCommand::SetFt2DecodeEnabled(en) => {
|
||||||
|
ctx.state.ft2_decode_enabled = en;
|
||||||
|
info!("FT2 decode {}", if en { "enabled" } else { "disabled" });
|
||||||
|
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||||
|
return snapshot_from(ctx.state);
|
||||||
|
}
|
||||||
RigCommand::SetWsprDecodeEnabled(en) => {
|
RigCommand::SetWsprDecodeEnabled(en) => {
|
||||||
ctx.state.wspr_decode_enabled = en;
|
ctx.state.wspr_decode_enabled = en;
|
||||||
info!("WSPR decode {}", if en { "enabled" } else { "disabled" });
|
info!("WSPR decode {}", if en { "enabled" } else { "disabled" });
|
||||||
@@ -517,6 +523,12 @@ async fn process_command(
|
|||||||
let _ = ctx.state_tx.send(ctx.state.clone());
|
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||||
return snapshot_from(ctx.state);
|
return snapshot_from(ctx.state);
|
||||||
}
|
}
|
||||||
|
RigCommand::ResetFt2Decoder => {
|
||||||
|
ctx.histories.clear_ft2_history();
|
||||||
|
ctx.state.ft2_decode_reset_seq += 1;
|
||||||
|
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||||
|
return snapshot_from(ctx.state);
|
||||||
|
}
|
||||||
RigCommand::ResetWsprDecoder => {
|
RigCommand::ResetWsprDecoder => {
|
||||||
ctx.histories.clear_wspr_history();
|
ctx.histories.clear_wspr_history();
|
||||||
ctx.state.wspr_decode_reset_seq += 1;
|
ctx.state.wspr_decode_reset_seq += 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user