[fix](trx-rs): transfer WEFAX PNG data from server to client for remote image serving

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-04-20 00:28:55 +02:00
parent 5de972dd61
commit c92428b78b
7 changed files with 63 additions and 2 deletions
Generated
+2
View File
@@ -3178,6 +3178,7 @@ version = "0.1.0"
dependencies = [
"actix-web",
"actix-ws",
"base64",
"brotli 7.0.0",
"bytes",
"dirs",
@@ -3261,6 +3262,7 @@ dependencies = [
name = "trx-server"
version = "0.1.0"
dependencies = [
"base64",
"bytes",
"chrono",
"clap",
+12
View File
@@ -574,12 +574,23 @@ impl WefaxDecoder {
let ppl = WefaxConfig::pixels_per_line(ioc);
let mut path_str = None;
let mut png_data = None;
// Save PNG if output directory is configured.
if let Some(ref dir) = self.config.output_dir {
let output_path = PathBuf::from(dir);
match image.save_png(&output_path, self.freq_hz, &self.mode) {
Ok(p) => {
// Read back the PNG bytes for remote client transfer.
match std::fs::read(&p) {
Ok(bytes) => {
png_data =
Some(base64::engine::general_purpose::STANDARD.encode(&bytes));
}
Err(e) => {
eprintln!("WEFAX: failed to read PNG for transfer: {}", e);
}
}
path_str = Some(p.to_string_lossy().into_owned());
}
Err(e) => {
@@ -597,6 +608,7 @@ impl WefaxDecoder {
ioc,
pixels_per_line: ppl,
path: path_str,
png_data,
complete: true,
}));
}
@@ -16,6 +16,7 @@ tokio = { workspace = true, features = ["full"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tracing = { workspace = true }
base64 = "0.22"
actix-web = "4.4"
actix-ws = "0.3"
tokio-stream = { version = "0.1", features = ["sync"] }
@@ -16,6 +16,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use actix_web::{get, web, Error, HttpRequest, HttpResponse};
use actix_ws::Message;
use base64::Engine as _;
use bytes::Bytes;
use serde::Deserialize;
use tokio::sync::broadcast;
@@ -297,7 +298,30 @@ fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
prune_wspr_history(context, &mut history);
}
fn record_wefax(context: &FrontendRuntimeContext, msg: WefaxMessage) {
fn record_wefax(context: &FrontendRuntimeContext, mut msg: WefaxMessage) {
// If the server sent PNG data, save it to the local cache so the
// `/images/` endpoint can serve it.
if let Some(ref data) = msg.png_data {
if let Some(ref path) = msg.path {
if let Some(filename) = std::path::Path::new(path).file_name() {
let dir = dirs::cache_dir()
.unwrap_or_else(|| std::path::PathBuf::from(".cache"))
.join("trx-rs")
.join("wefax");
if std::fs::create_dir_all(&dir).is_ok() {
if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(data) {
let local_path = dir.join(filename);
if let Err(e) = std::fs::write(&local_path, &bytes) {
tracing::warn!("WEFAX: failed to save local image: {}", e);
}
}
}
}
}
}
// Strip bulk data before storing in memory.
msg.png_data = None;
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.decode_history
+4
View File
@@ -291,6 +291,10 @@ pub struct WefaxMessage {
/// Filesystem path to saved PNG (set on completion).
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// Base64-encoded PNG data for transfer to remote clients.
/// Populated by the server when sending, stripped before storing in history.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub png_data: Option<String>,
/// True when image is complete (stop tone received).
pub complete: bool,
}
+1
View File
@@ -14,6 +14,7 @@ ft2 = ["trx-ftx/ft2", "trx-protocol/ft2"]
soapysdr = ["trx-backend/soapysdr"]
[dependencies]
base64 = "0.22"
flate2 = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-serial = { workspace = true }
+18 -1
View File
@@ -12,6 +12,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use base64::Engine as _;
use bytes::Bytes;
use flate2::write::GzEncoder;
use flate2::Compression;
@@ -709,6 +710,8 @@ impl DecoderHistories {
if msg.ts_ms.is_none() {
msg.ts_ms = Some(current_timestamp_ms());
}
// Strip bulk PNG data before storing in memory/persistence.
msg.png_data = None;
let mut h = lock_or_recover(&self.wefax, "wefax_history");
let before = h.len();
h.push_back((Instant::now(), msg));
@@ -722,7 +725,21 @@ impl DecoderHistories {
let before = h.len();
Self::prune_wefax(&mut h);
self.adjust_total_count(before, h.len());
h.iter().map(|(_, msg)| msg.clone()).collect()
h.iter()
.map(|(_, msg)| {
let mut m = msg.clone();
// Re-read PNG from disk so remote clients can save a local copy.
if m.png_data.is_none() {
if let Some(ref path) = m.path {
if let Ok(bytes) = std::fs::read(path) {
m.png_data =
Some(base64::engine::general_purpose::STANDARD.encode(&bytes));
}
}
}
m
})
.collect()
}
pub fn clear_wefax_history(&self) {