fob export fixes
This commit is contained in:
Generated
+1
-1
@@ -444,7 +444,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kat"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
|
||||
@@ -119,11 +119,12 @@ Press `Enter` on a capture to open the action menu. When using RTL-SDR (receive-
|
||||
|
||||
When exporting to `.fob`, a metadata form collects filename and optional vehicle info:
|
||||
|
||||
- **File** — output filename (extension added by format)
|
||||
- **File** — output filename (extension added by format). For unknown protocol, a unique 8-character hex suffix (e.g. `A1B2C3D4`) is shown in the field and appended to the filename so each export has a distinct file.
|
||||
- **Year** — vehicle model year
|
||||
- **Make** — manufacturer (auto-suggested from protocol)
|
||||
- **Model** — vehicle model
|
||||
- **Region** — region/market
|
||||
- **Command** — button/command label (e.g. Unlock, Lock, Trunk, Panic); used in the default filename for unknown protocol and stored in the .fob vehicle section.
|
||||
- **Notes** — free-form notes
|
||||
|
||||
The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) containing:
|
||||
@@ -153,17 +154,22 @@ The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) c
|
||||
"year": 2023,
|
||||
"make": "Kia",
|
||||
"model": "Sportage",
|
||||
"region": "",
|
||||
"region": "NA",
|
||||
"command": "Lock",
|
||||
"notes": ""
|
||||
},
|
||||
"capture": {
|
||||
"timestamp": "2026-02-07T12:00:00Z",
|
||||
"raw_data_hex": "0x...",
|
||||
"raw_pair_count": 0,
|
||||
"raw_pairs": [{"level": true, "duration_us": 400}, {"level": false, "duration_us": 800}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`rf_modulation` is AM, FM, or AM/FM per protocol (from ProtoPirate). Raw pairs are included when config `include_raw_pairs` is true.
|
||||
- **vehicle.command** — optional; user-editable command label (e.g. Unlock, Lock). Set in the export form or via **i** (capture metadata). Used for unknown-protocol export filenames (`Year_Make_Model_Region_Command_8HEX.fob`).
|
||||
- **rf_modulation** — AM, FM, or AM/FM per protocol (from ProtoPirate). Omitted when unknown.
|
||||
- **capture** — `raw_data_hex`, `raw_pair_count`, and optionally `raw_pairs` when config `include_raw_pairs` is true.
|
||||
|
||||
### VIM-Style Commands
|
||||
|
||||
@@ -213,7 +219,7 @@ include_raw_pairs = true
|
||||
```
|
||||
|
||||
- **import_directory** — directory scanned at startup for .fob and .sub files to import (default `~/.config/KAT/import`). Exports are still saved to **export_directory**.
|
||||
- **research_mode** — when `false` (default), only successfully decoded signals appear in the list; when `true`, unknown (unidentified) signals are also shown.
|
||||
- **research_mode** — when `true` (default), unknown (unidentified) signals are shown in addition to decoded ones; when `false`, only successfully decoded signals appear.
|
||||
- **include_raw_pairs** — when `true`, .fob exports include raw level/duration pairs for replay.
|
||||
|
||||
## Supported Protocols
|
||||
@@ -268,7 +274,7 @@ The **AM/OOK** demodulator turns IQ samples into level/duration pairs for protoc
|
||||
- **Exponential moving average** — magnitude smoothing
|
||||
- **Schmitt trigger hysteresis** — reduces chattering at the decision boundary
|
||||
- **Debounce** — 40µs minimum pulse width to reject noise spikes
|
||||
- **Gap detection** — 20 ms gap treated as end of signal
|
||||
- **Gap detection** — 80 ms gap treated as end of signal (multi-burst keyfob presses stay one capture)
|
||||
|
||||
## IMPORTS folder
|
||||
|
||||
@@ -314,6 +320,17 @@ src/
|
||||
└── status_bar.rs # Status bar
|
||||
```
|
||||
|
||||
## Call for researchers
|
||||
|
||||
**We need your help.** KAT’s protocol decoders and future analysis depend on real-world keyfob captures. If you use KAT for security research, authorized testing, or protocol development, please consider:
|
||||
|
||||
- **Contributing to protocol analysis** — share timing, encoding, or decoder feedback; report bugs or suggest new protocols.
|
||||
- **Contributing captures to the research library** — add your `.fob` files to the **[FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library)** so others can use them for decoder development, CVE validation, and protocol studies.
|
||||
|
||||
The [FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library) is a community collection of KAT `.fob` captures organized by manufacturer. Your submissions (with proper metadata: year, make, model, region, command) help grow test vectors and support the wider keyfob research community.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
KAT is developed by **Kara Zajac (.leviathan)**. KAT would not be possible without [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate)—the protocol decoders, reference implementations, and community work are the foundation this tool is built on. Truly standing on the shoulders of giants.
|
||||
|
||||
+54
-63
@@ -1307,52 +1307,52 @@ impl App {
|
||||
.unwrap_or_else(|_| path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// For unknown protocol: Year_Make_Model_Region_Command (user can edit; we append _<8 hex> on save).
|
||||
/// For known protocol: protocol_serial as before.
|
||||
/// Default export filename for .fob: Year_Make_Model_Region_Command (same format for all captures).
|
||||
/// Uses capture metadata when set; fallbacks: make from protocol for known, command from button_name(), else "Unknown".
|
||||
fn default_export_filename(capture: &Capture) -> String {
|
||||
if capture.protocol_name().eq_ignore_ascii_case("unknown") {
|
||||
let year = capture
|
||||
.year
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let make = capture
|
||||
.make
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let model = capture
|
||||
.model
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let region = capture
|
||||
.region
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let cmd_str = capture
|
||||
.command
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| capture.button_name())
|
||||
.trim();
|
||||
let command = if cmd_str.is_empty() || cmd_str == "-" {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
cmd_str.replace(' ', "_")
|
||||
};
|
||||
format!("{}_{}_{}_{}_{}", year, make, model, region, command)
|
||||
let year = capture
|
||||
.year
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let make = capture
|
||||
.make
|
||||
.as_deref()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().replace(' ', "_"))
|
||||
.unwrap_or_else(|| {
|
||||
if capture.protocol_name().eq_ignore_ascii_case("unknown") {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
Self::get_make_for_protocol(capture.protocol_name())
|
||||
.trim()
|
||||
.replace(' ', "_")
|
||||
}
|
||||
});
|
||||
let model = capture
|
||||
.model
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let region = capture
|
||||
.region
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
.trim()
|
||||
.replace(' ', "_");
|
||||
let cmd_str = capture
|
||||
.command
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| capture.button_name())
|
||||
.trim();
|
||||
let command = if cmd_str.is_empty() || cmd_str == "-" {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}_{}",
|
||||
capture.protocol_name().replace(' ', "_").to_lowercase(),
|
||||
capture.serial_hex()
|
||||
)
|
||||
}
|
||||
cmd_str.replace(' ', "_")
|
||||
};
|
||||
format!("{}_{}_{}_{}_{}", year, make, model, region, command)
|
||||
}
|
||||
|
||||
/// Start .fob export by entering filename input mode
|
||||
@@ -1362,21 +1362,17 @@ impl App {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Pre-fill filename from protocol + serial; for unknown captures include 8-hex suffix so user sees it
|
||||
// Pre-fill filename: Year_Make_Model_Region_Command_8HEX for all .fob exports
|
||||
let capture = self.captures.iter().find(|c| c.id == id);
|
||||
let default_name = capture
|
||||
.map(|c| Self::default_export_filename(c))
|
||||
.unwrap_or_else(|| format!("capture_{}", id));
|
||||
self.export_filename = if capture.map(|c| c.protocol.is_none()).unwrap_or(false) {
|
||||
let suffix_nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32;
|
||||
format!("{}_{:08X}", default_name, suffix)
|
||||
} else {
|
||||
default_name.clone()
|
||||
};
|
||||
let suffix_nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32;
|
||||
self.export_filename = format!("{}_{:08X}", default_name, suffix);
|
||||
|
||||
// Pre-fill metadata from capture if set, otherwise make from protocol
|
||||
let make = capture
|
||||
@@ -1449,18 +1445,15 @@ impl App {
|
||||
notes: self.fob_meta_notes.clone(),
|
||||
};
|
||||
|
||||
// Unknown captures or user-edited vehicle-style filenames always get 8-hex suffix to avoid overwrites.
|
||||
let use_hex_suffix = capture.protocol.is_none()
|
||||
|| self.export_filename.ends_with("_Unknown")
|
||||
|| self.export_filename == "Unknown";
|
||||
// All .fob exports use Year_Make_Model_Region_Command_8HEX; append 8-hex if user removed it
|
||||
let already_has_8hex = self.export_filename.len() >= 9
|
||||
&& self.export_filename.as_bytes()[self.export_filename.len() - 9] == b'_'
|
||||
&& self.export_filename[self.export_filename.len() - 8..]
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_hexdigit());
|
||||
let filename = if use_hex_suffix && already_has_8hex {
|
||||
let filename = if already_has_8hex {
|
||||
format!("{}.fob", self.export_filename.trim())
|
||||
} else if use_hex_suffix {
|
||||
} else {
|
||||
let base = self.export_filename.trim();
|
||||
let suffix_nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -1473,8 +1466,6 @@ impl App {
|
||||
} else {
|
||||
format!("{}_{}.fob", base, hex_suffix)
|
||||
}
|
||||
} else {
|
||||
format!("{}.fob", self.export_filename)
|
||||
};
|
||||
let path = export_dir.join(&filename);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user