[feat](trx-wefax): save images as YYYY-MM-DD_HH-MM-SS-freq_kHz_MODE.png

Include rig dial frequency and mode in WEFAX image filenames, matching
fldigi's approach of capturing tuning at save time. Images are saved to
~/.cache/trx-rs/wefax/. Server passes current rig state to the decoder
via set_tuning() before each processing block.

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-04 08:00:21 +02:00
parent 69e00a8245
commit 036442d0ed
3 changed files with 28 additions and 8 deletions
+13 -1
View File
@@ -80,6 +80,10 @@ pub struct WefaxDecoder {
signal_detect_count: u32, signal_detect_count: u32,
/// Accumulator for computing luminance variance within the current window. /// Accumulator for computing luminance variance within the current window.
signal_detect_buf: Vec<f32>, signal_detect_buf: Vec<f32>,
/// Current rig dial frequency in Hz (for image filenames).
freq_hz: u64,
/// Current rig mode name (for image filenames).
mode: String,
} }
impl WefaxDecoder { impl WefaxDecoder {
@@ -102,6 +106,8 @@ impl WefaxDecoder {
sent_idle_event: false, sent_idle_event: false,
signal_detect_count: 0, signal_detect_count: 0,
signal_detect_buf: Vec::with_capacity(INTERNAL_RATE as usize / 2), signal_detect_buf: Vec::with_capacity(INTERNAL_RATE as usize / 2),
freq_hz: 0,
mode: String::new(),
} }
} }
@@ -319,6 +325,12 @@ impl WefaxDecoder {
events events
} }
/// Update the current rig tuning (used for image filenames).
pub fn set_tuning(&mut self, freq_hz: u64, mode: &str) {
self.freq_hz = freq_hz;
self.mode = mode.to_string();
}
/// Check if the decoder is currently receiving an image. /// Check if the decoder is currently receiving an image.
pub fn is_receiving(&self) -> bool { pub fn is_receiving(&self) -> bool {
matches!( matches!(
@@ -400,7 +412,7 @@ impl WefaxDecoder {
// Save PNG if output directory is configured. // Save PNG if output directory is configured.
if let Some(ref dir) = self.config.output_dir { if let Some(ref dir) = self.config.output_dir {
let output_path = PathBuf::from(dir); let output_path = PathBuf::from(dir);
match image.save_png(&output_path, ioc, lpm) { match image.save_png(&output_path, self.freq_hz, &self.mode) {
Ok(p) => { Ok(p) => {
path_str = Some(p.to_string_lossy().into_owned()); path_str = Some(p.to_string_lossy().into_owned());
} }
+8 -7
View File
@@ -43,8 +43,8 @@ impl ImageAssembler {
pub fn save_png( pub fn save_png(
&self, &self,
output_dir: &Path, output_dir: &Path,
ioc: u16, freq_hz: u64,
lpm: u16, mode: &str,
) -> Result<PathBuf, String> { ) -> Result<PathBuf, String> {
if self.lines.is_empty() { if self.lines.is_empty() {
return Err("no image lines to save".into()); return Err("no image lines to save".into());
@@ -53,7 +53,7 @@ impl ImageAssembler {
std::fs::create_dir_all(output_dir) std::fs::create_dir_all(output_dir)
.map_err(|e| format!("create output dir: {}", e))?; .map_err(|e| format!("create output dir: {}", e))?;
let filename = generate_filename(ioc, lpm); let filename = generate_filename(freq_hz, mode);
let path = output_dir.join(&filename); let path = output_dir.join(&filename);
let file = std::fs::File::create(&path) let file = std::fs::File::create(&path)
@@ -89,7 +89,7 @@ impl ImageAssembler {
} }
} }
fn generate_filename(ioc: u16, lpm: u16) -> String { fn generate_filename(freq_hz: u64, mode: &str) -> String {
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default(); .unwrap_or_default();
@@ -97,10 +97,11 @@ fn generate_filename(ioc: u16, lpm: u16) -> String {
// Convert to UTC datetime components manually (avoid chrono dependency). // Convert to UTC datetime components manually (avoid chrono dependency).
let (year, month, day, hour, min, sec) = unix_to_utc(secs); let (year, month, day, hour, min, sec) = unix_to_utc(secs);
let freq_khz = freq_hz / 1000;
format!( format!(
"WEFAX-{:04}-{:02}-{:02}T{:02}{:02}{:02}-IOC{}-{}lpm.png", "{:04}-{:02}-{:02}_{:02}-{:02}-{:02}-{}_kHz_{}.png",
year, month, day, hour, min, sec, ioc, lpm year, month, day, hour, min, sec, freq_khz, mode
) )
} }
@@ -179,7 +180,7 @@ mod tests {
} }
let dir = std::env::temp_dir().join("trx-wefax-test"); let dir = std::env::temp_dir().join("trx-wefax-test");
let result = asm.save_png(&dir, 576, 120); let result = asm.save_png(&dir, 7880000, "USB");
assert!(result.is_ok(), "save_png failed: {:?}", result.err()); assert!(result.is_ok(), "save_png failed: {:?}", result.err());
let path = result.unwrap(); let path = result.unwrap();
assert!(path.exists()); assert!(path.exists());
+7
View File
@@ -2780,6 +2780,13 @@ pub async fn run_wefax_decoder(
}; };
was_active = true; was_active = true;
{
let state = state_rx.borrow();
decoder.set_tuning(
state.status.freq.hz,
&format!("{:?}", state.status.mode),
);
}
let events = tokio::task::block_in_place(|| { let events = tokio::task::block_in_place(|| {
let _span = info_span!("wefax_decode").entered(); let _span = info_span!("wefax_decode").entered();
decoder.process_samples(&mono) decoder.process_samples(&mono)