Working changes

This commit is contained in:
leviathan
2026-02-13 18:10:29 -05:00
parent 9e2565c1fc
commit 04b1d91d78
15 changed files with 587 additions and 162 deletions
+107 -3
View File
@@ -1,12 +1,12 @@
//! Flipper Zero .sub export format.
//!
//! Outputs files in the Flipper SubGhz RAW format with alternating
//! Read/write files in the Flipper SubGhz RAW format with alternating
//! positive (high) and negative (low) durations in microseconds.
use anyhow::Result;
use anyhow::{Context, Result};
use std::path::Path;
use crate::capture::Capture;
use crate::capture::{Capture, CaptureStatus, StoredLevelDuration};
/// Export a capture to Flipper Zero .sub RAW format
pub fn export_flipper_sub(capture: &Capture, path: &Path) -> Result<()> {
@@ -47,3 +47,107 @@ pub fn export_flipper_sub(capture: &Capture, path: &Path) -> Result<()> {
tracing::info!("Exported Flipper .sub to {:?}", path);
Ok(())
}
/// Scan a directory for Flipper .sub files (same format we export).
pub fn scan_sub_files(dir: &Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return out;
};
for e in entries.flatten() {
let p = e.path();
if p.extension().map_or(false, |e| e == "sub") {
out.push(p);
}
}
out.sort();
out
}
/// Gap duration (µs) used to split a .sub stream into separate transmissions.
/// Keyfobs typically use 1025 ms between button pushes; in-frame gaps are &lt; 1 ms.
pub const SUB_INTER_BURST_GAP_US: u32 = 10_000;
/// Split raw level/duration pairs into segments at long gaps (e.g. between keyfob button pushes).
/// Any pulse (HIGH or LOW) with duration >= `gap_threshold_us` starts a new segment; the long pulse is not included in any segment.
pub fn split_raw_pairs_by_gap(
pairs: &[StoredLevelDuration],
gap_threshold_us: u32,
) -> Vec<Vec<StoredLevelDuration>> {
let mut segments = Vec::new();
let mut current = Vec::new();
for p in pairs {
if p.duration_us >= gap_threshold_us {
if !current.is_empty() {
segments.push(std::mem::take(&mut current));
}
} else {
current.push(*p);
}
}
if !current.is_empty() {
segments.push(current);
}
segments
}
/// Parse a Flipper SubGhz RAW .sub file and return one Capture per transmission.
/// The stream is split at long gaps (see `split_raw_pairs_by_gap`) so multiple button pushes become separate captures.
/// Positive values = HIGH, negative = LOW; duration in microseconds.
pub fn import_sub(path: &Path, next_id: u32) -> Result<Vec<Capture>> {
let s = std::fs::read_to_string(path)
.with_context(|| format!("Read .sub file: {:?}", path))?;
let mut frequency_hz: Option<u32> = None;
let mut raw_data = Vec::new();
for line in s.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(rest) = line.strip_prefix("Frequency:") {
let n: u32 = rest.trim().parse().context("Parse Frequency in .sub")?;
frequency_hz = Some(n);
continue;
}
if let Some(rest) = line.strip_prefix("RAW_Data:") {
for word in rest.split_whitespace() {
let value: i64 = word.parse().with_context(|| format!("Parse RAW_Data value: {:?}", word))?;
raw_data.push(value);
}
}
}
let frequency = frequency_hz.unwrap_or(433_920_000);
let raw_pairs: Vec<StoredLevelDuration> = raw_data
.into_iter()
.map(|v| {
let duration_us = v.unsigned_abs() as u32;
let level = v >= 0;
StoredLevelDuration { level, duration_us }
})
.collect();
if raw_pairs.is_empty() {
anyhow::bail!("No RAW_Data in .sub file");
}
let segments = split_raw_pairs_by_gap(&raw_pairs, SUB_INTER_BURST_GAP_US);
let captures: Vec<Capture> = segments
.into_iter()
.enumerate()
.map(|(i, pairs)| {
let cap = Capture::from_pairs_with_rf(next_id + i as u32, frequency, pairs, None);
Capture {
status: CaptureStatus::Unknown,
..cap
}
})
.collect();
Ok(captures)
}
+2
View File
@@ -300,6 +300,7 @@ fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result<Capture> {
data_count_bit: sig.data_bits,
raw_pairs,
status,
received_rf: None,
})
}
@@ -364,6 +365,7 @@ fn import_fob_v1(fob: &FobFileV1, next_id: u32) -> Result<Capture> {
data_count_bit: cap.data_bits,
raw_pairs,
status,
received_rf: None,
})
}