diff --git a/Cargo.lock b/Cargo.lock index 108d00c..62da909 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/src/decoders/trx-wefax/src/decoder.rs b/src/decoders/trx-wefax/src/decoder.rs index b0f6c4f..60090a5 100644 --- a/src/decoders/trx-wefax/src/decoder.rs +++ b/src/decoders/trx-wefax/src/decoder.rs @@ -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, })); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml b/src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml index 5073797..93ee1af 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml +++ b/src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml @@ -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"] } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs index 48d5091..13bcad9 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs @@ -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 diff --git a/src/trx-core/src/decode.rs b/src/trx-core/src/decode.rs index deb6e90..9ee5748 100644 --- a/src/trx-core/src/decode.rs +++ b/src/trx-core/src/decode.rs @@ -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, + /// 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, /// True when image is complete (stop tone received). pub complete: bool, } diff --git a/src/trx-server/Cargo.toml b/src/trx-server/Cargo.toml index 7a20c2c..bbe208e 100644 --- a/src/trx-server/Cargo.toml +++ b/src/trx-server/Cargo.toml @@ -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 } diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 66a7783..dc8e89f 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -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) {