mirror of
https://github.com/MidnightBlueLabs/tetra-bluestation.git
synced 2026-03-29 05:09:51 +00:00
Massive restructuring and refactor
Although functional changes are limited, I've massively refactored and restructured the codebase into several separate crates. - tetra-core features functionality and data that are used throughout the stack - tetra-saps contains definitions for all SAP message types - tetra-config contains everything that has to do with toml-based config file parsing and conversion to the internally-used datastructures - tetra-pdus features entity-related PDU definitions, including field struct types and enums - tetra-entities features implementations for TETRA entities, split between BS and MS use cases. It also holds subcomponents like schedulers and decoding tools. A separate bins folder is made, which may hold any current and future binary targets we want to compile. Right now we have: - tetra-bluestation-bs, which compiles the core base station binary - pdu-tool, which is a crude, buggy, ad hoc tool to parse certain pdu types for stack debugging purposes
This commit is contained in:
19
bins/bluestation-bs/Cargo.toml
Normal file
19
bins/bluestation-bs/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "bluestation-bs"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "tetra-bluestation"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tetra-core = { workspace = true }
|
||||
tetra-config = { workspace = true }
|
||||
tetra-saps = { workspace = true }
|
||||
tetra-entities = { workspace = true }
|
||||
|
||||
clap = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
103
bins/bluestation-bs/src/main.rs
Normal file
103
bins/bluestation-bs/src/main.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use clap::Parser;
|
||||
|
||||
use tetra_config::{PhyBackend, SharedConfig, StackMode, toml_config};
|
||||
use tetra_core::{TdmaTime, debug};
|
||||
use tetra_entities::{cmce::cmce_bs::CmceBs, llc::llc_bs_ms::Llc, lmac::lmac_bs::LmacBs, mle::mle_bs_ms::Mle, mm::mm_bs::MmBs, phy::{components::soapy_dev::RxTxDevSoapySdr, phy_bs::PhyBs}, sndcp::sndcp_bs::Sndcp, umac::umac_bs::UmacBs};
|
||||
use tetra_entities::MessageRouter;
|
||||
|
||||
|
||||
/// Load configuration file
|
||||
fn load_config_from_toml(cfg_path: &str) -> SharedConfig {
|
||||
match toml_config::from_file(cfg_path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!("Failed to load configuration from {}: {}", cfg_path, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start base station stack
|
||||
fn build_bs_stack(cfg: &mut SharedConfig) -> MessageRouter {
|
||||
|
||||
let mut router = MessageRouter::new(cfg.clone());
|
||||
|
||||
// Add suitable Phy component based on PhyIo type
|
||||
match cfg.config().phy_io.backend {
|
||||
PhyBackend::SoapySdr => {
|
||||
let rxdev = RxTxDevSoapySdr::new(cfg);
|
||||
let phy = PhyBs::new(cfg.clone(), rxdev);
|
||||
router.register_entity(Box::new(phy));
|
||||
}
|
||||
_ => {
|
||||
panic!("Unsupported PhyIo type: {:?}", cfg.config().phy_io.backend);
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining components
|
||||
let lmac = LmacBs::new(cfg.clone());
|
||||
let umac = UmacBs::new(cfg.clone());
|
||||
let llc = Llc::new(cfg.clone());
|
||||
let mle = Mle::new(cfg.clone());
|
||||
let mm = MmBs::new(cfg.clone());
|
||||
let sndcp = Sndcp::new(cfg.clone());
|
||||
let cmce = CmceBs::new(cfg.clone());
|
||||
router.register_entity(Box::new(lmac));
|
||||
router.register_entity(Box::new(umac));
|
||||
router.register_entity(Box::new(llc));
|
||||
router.register_entity(Box::new(mle));
|
||||
router.register_entity(Box::new(mm));
|
||||
router.register_entity(Box::new(sndcp));
|
||||
router.register_entity(Box::new(cmce));
|
||||
|
||||
// Init network time
|
||||
router.set_dl_time(TdmaTime::default());
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
author,
|
||||
version,
|
||||
about = "TETRA BlueStation Stack",
|
||||
long_about = "Runs the TETRA BlueStation stack using the provided TOML configuration files"
|
||||
)]
|
||||
|
||||
|
||||
struct Args {
|
||||
/// Config file (required)
|
||||
#[arg(
|
||||
help = "TOML config with network/cell parameters",
|
||||
)]
|
||||
config: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
eprintln!("░▀█▀░█▀▀░▀█▀░█▀▄░█▀█░░░░░█▀▄░█░░░█░█░█▀▀░█▀▀░▀█▀░█▀█░▀█▀░▀█▀░█▀█░█▀█");
|
||||
eprintln!("░░█░░█▀▀░░█░░█▀▄░█▀█░▄▄▄░█▀▄░█░░░█░█░█▀▀░▀▀█░░█░░█▀█░░█░░░█░░█░█░█░█");
|
||||
eprintln!("░░▀░░▀▀▀░░▀░░▀░▀░▀░▀░░░░░▀▀░░▀▀▀░▀▀▀░▀▀▀░▀▀▀░░▀░░▀░▀░░▀░░▀▀▀░▀▀▀░▀░▀\n");
|
||||
eprintln!(" Wouter Bokslag / Midnight Blue");
|
||||
eprintln!(" -> https://github.com/MidnightBlueLabs/tetra-bluestation");
|
||||
eprintln!(" -> https://midnightblue.nl\n");
|
||||
|
||||
let args = Args::parse();
|
||||
let mut cfg = load_config_from_toml(&args.config);
|
||||
let _log_guard = debug::setup_logging_default(cfg.config().debug_log.clone());
|
||||
|
||||
let mut router = match cfg.config().stack_mode {
|
||||
StackMode::Mon => {
|
||||
unimplemented!("Monitor mode is not implemented");
|
||||
},
|
||||
StackMode::Ms => {
|
||||
unimplemented!("MS mode is not implemented");
|
||||
},
|
||||
StackMode::Bs => {
|
||||
build_bs_stack(&mut cfg)
|
||||
}
|
||||
};
|
||||
|
||||
router.run_stack(None);
|
||||
}
|
||||
19
bins/pdu-tool/Cargo.toml
Normal file
19
bins/pdu-tool/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "pdu-tool"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "pdu-tool"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tetra-core = { workspace = true }
|
||||
tetra-config = { workspace = true }
|
||||
tetra-saps = { workspace = true }
|
||||
tetra-pdus = { workspace = true }
|
||||
|
||||
clap = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
1
bins/pdu-tool/src/entities/mod.rs
Normal file
1
bins/pdu-tool/src/entities/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod umac;
|
||||
698
bins/pdu-tool/src/entities/umac.rs
Normal file
698
bins/pdu-tool/src/entities/umac.rs
Normal file
@@ -0,0 +1,698 @@
|
||||
use tetra_core::BitBuffer;
|
||||
use tetra_saps::tmv::enums::logical_chans::LogicalChannel;
|
||||
use tetra_pdus::umac::{
|
||||
enums::{
|
||||
broadcast_type::BroadcastType,
|
||||
mac_pdu_type::MacPduType,
|
||||
},
|
||||
pdus::{
|
||||
// Uplink PDUs
|
||||
mac_access::MacAccess,
|
||||
mac_data::MacData,
|
||||
mac_end_hu::MacEndHu,
|
||||
mac_end_ul::MacEndUl,
|
||||
mac_frag_ul::MacFragUl,
|
||||
mac_u_blck::MacUBlck,
|
||||
mac_u_signal::MacUSignal,
|
||||
// Downlink PDUs
|
||||
mac_resource::MacResource,
|
||||
mac_end_dl::MacEndDl,
|
||||
mac_frag_dl::MacFragDl,
|
||||
mac_d_blck::MacDBlck,
|
||||
mac_sysinfo::MacSysinfo,
|
||||
mac_sync::MacSync,
|
||||
access_define::AccessDefine,
|
||||
},
|
||||
};
|
||||
|
||||
/// Result of length_ind interpretation
|
||||
#[derive(Debug)]
|
||||
pub struct LengthIndInfo {
|
||||
/// PDU payload length in bits (0 for null PDU or fragmentation)
|
||||
pub pdu_len_bits: usize,
|
||||
/// Whether this is a null PDU
|
||||
pub is_null_pdu: bool,
|
||||
/// Whether this is fragmentation start (no length known)
|
||||
pub is_frag_start: bool,
|
||||
/// Whether second half slot is stolen (STCH)
|
||||
pub second_half_stolen: bool,
|
||||
}
|
||||
|
||||
/// UMAC parser for standalone PDU debugging
|
||||
pub struct UmacParser;
|
||||
|
||||
impl UmacParser {
|
||||
|
||||
/// Parse an uplink MAC PDU and print the result
|
||||
/// Follows the structure of UmacBs::rx_tmv_unitdata_ind and rx_tmv_sch
|
||||
pub fn parse_ul(mut pdu: BitBuffer, logical_channel: LogicalChannel) {
|
||||
println!("=== UMAC UL Parser ===");
|
||||
println!("Logical channel: {:?}", logical_channel);
|
||||
println!("Input bits: {}", pdu.dump_bin());
|
||||
println!();
|
||||
|
||||
// Iterate until no more messages left in mac block
|
||||
loop {
|
||||
let Some(bits) = pdu.peek_bits(3) else {
|
||||
println!("[!] Insufficient bits remaining: {}", pdu.dump_bin());
|
||||
return;
|
||||
};
|
||||
let orig_start = pdu.get_raw_start();
|
||||
|
||||
// Clause 21.4.1; handling differs between SCH_HU and others
|
||||
match logical_channel {
|
||||
LogicalChannel::SchF | LogicalChannel::Stch => {
|
||||
// First two bits are MAC PDU type
|
||||
let Ok(pdu_type) = MacPduType::try_from(bits >> 1) else {
|
||||
println!("[!] Invalid PDU type: {}", bits >> 1);
|
||||
return;
|
||||
};
|
||||
println!("MAC PDU Type: {:?} ({})", pdu_type, bits >> 1);
|
||||
|
||||
match pdu_type {
|
||||
MacPduType::MacResourceMacData => {
|
||||
// On uplink this is MAC-DATA
|
||||
Self::parse_mac_data(&mut pdu);
|
||||
}
|
||||
MacPduType::MacFragMacEnd => {
|
||||
// Third bit distinguishes mac-frag (0) from mac-end (1)
|
||||
if bits & 1 == 0 {
|
||||
Self::parse_mac_frag_ul(&mut pdu);
|
||||
} else {
|
||||
Self::parse_mac_end_ul(&mut pdu);
|
||||
}
|
||||
}
|
||||
MacPduType::SuppMacUSignal => {
|
||||
// STCH determines which subtype is relevant
|
||||
if logical_channel == LogicalChannel::Stch {
|
||||
Self::parse_mac_u_signal(&mut pdu);
|
||||
} else {
|
||||
// Supplementary MAC PDU - third bit distinguishes
|
||||
if bits & 1 == 0 {
|
||||
Self::parse_mac_u_blck(&mut pdu);
|
||||
} else {
|
||||
println!("[!] Unexpected supplementary PDU subtype");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
MacPduType::Broadcast => {
|
||||
println!("[!] Broadcast PDU not expected on uplink");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
LogicalChannel::SchHu => {
|
||||
// Only 1 bit needed for subtype distinction on SCH/HU
|
||||
let pdu_type = (bits >> 2) & 1;
|
||||
println!("SCH/HU PDU Type: {} ({})",
|
||||
if pdu_type == 0 { "MAC-ACCESS" } else { "MAC-END-HU" },
|
||||
pdu_type);
|
||||
|
||||
match pdu_type {
|
||||
0 => Self::parse_mac_access(&mut pdu),
|
||||
1 => Self::parse_mac_end_hu(&mut pdu),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("[!] Unknown/unsupported logical channel for UL: {:?}", logical_channel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if more PDUs remain in MAC block
|
||||
if !Self::check_continue(&pdu, orig_start) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a downlink MAC PDU and print the result
|
||||
/// Follows the structure of UmacMs::rx_tmv_unitdata_ind and rx_tmv_sch
|
||||
pub fn parse_dl(mut pdu: BitBuffer, logical_channel: LogicalChannel) {
|
||||
println!("=== UMAC DL Parser ===");
|
||||
println!("Logical channel: {:?}", logical_channel);
|
||||
println!("Input bits: {}", pdu.dump_bin());
|
||||
println!();
|
||||
|
||||
// Handle special channels first
|
||||
match logical_channel {
|
||||
LogicalChannel::Aach => {
|
||||
println!("--- AACH (Access Assignment Channel) ---");
|
||||
println!("[!] AACH parsing not implemented in standalone tool");
|
||||
return;
|
||||
}
|
||||
LogicalChannel::Bsch => {
|
||||
Self::parse_mac_sync(&mut pdu);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
assert!(logical_channel == LogicalChannel::SchF
|
||||
|| logical_channel == LogicalChannel::SchHd,
|
||||
"Unsupported logical channel for DL: {:?}", logical_channel);
|
||||
|
||||
// Iterate until no more messages left in mac block
|
||||
loop {
|
||||
let Some(bits) = pdu.peek_bits(3) else {
|
||||
println!("[!] Insufficient bits remaining: {}", pdu.dump_bin());
|
||||
return;
|
||||
};
|
||||
let orig_start = pdu.get_raw_start();
|
||||
|
||||
// First two bits are MAC PDU type
|
||||
let Ok(pdu_type) = MacPduType::try_from(bits >> 1) else {
|
||||
println!("[!] Invalid PDU type: {}", bits >> 1);
|
||||
return;
|
||||
};
|
||||
println!("MAC PDU Type: {:?} ({})", pdu_type, bits >> 1);
|
||||
|
||||
match pdu_type {
|
||||
MacPduType::MacResourceMacData => {
|
||||
// On downlink this is MAC-RESOURCE
|
||||
Self::parse_mac_resource(&mut pdu);
|
||||
}
|
||||
MacPduType::MacFragMacEnd => {
|
||||
// Third bit distinguishes mac-frag (0) from mac-end (1)
|
||||
if bits & 1 == 0 {
|
||||
Self::parse_mac_frag_dl(&mut pdu);
|
||||
} else {
|
||||
Self::parse_mac_end_dl(&mut pdu);
|
||||
}
|
||||
}
|
||||
MacPduType::Broadcast => {
|
||||
Self::parse_broadcast(&mut pdu);
|
||||
}
|
||||
MacPduType::SuppMacUSignal => {
|
||||
if logical_channel == LogicalChannel::Stch {
|
||||
// U-SIGNAL on stealing channel
|
||||
Self::parse_mac_u_signal(&mut pdu);
|
||||
} else {
|
||||
// Supplementary PDU - third bit distinguishes
|
||||
if bits & 1 == 0 {
|
||||
Self::parse_mac_d_blck(&mut pdu);
|
||||
} else {
|
||||
println!("[!] Unexpected supplementary PDU subtype on DL");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if more PDUs remain in MAC block
|
||||
if !Self::check_continue(&pdu, orig_start) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_data(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-DATA ---");
|
||||
match MacData::from_bitbuf(pdu) {
|
||||
Ok(mac_data) => {
|
||||
println!("{:#?}", mac_data);
|
||||
|
||||
// Print SDU preview if we have a length
|
||||
if let Some(len) = mac_data.length_ind {
|
||||
let info = Self::interpret_length_ind(len, pdu.get_len_remaining());
|
||||
if !info.is_null_pdu && !info.is_frag_start && !info.second_half_stolen && info.pdu_len_bits > 0 {
|
||||
Self::print_sdu(pdu, info.pdu_len_bits.min(pdu.get_len_remaining()), "TM-SDU");
|
||||
} else if info.is_frag_start {
|
||||
println!("Fragment data: {} bits remaining", pdu.get_len_remaining());
|
||||
}
|
||||
}
|
||||
|
||||
// Apply PDU association
|
||||
Self::apply_pdu_association(pdu, mac_data.length_ind, mac_data.fill_bits);
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-DATA: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_access(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-ACCESS ---");
|
||||
match MacAccess::from_bitbuf(pdu) {
|
||||
Ok(mac_access) => {
|
||||
println!("{:#?}", mac_access);
|
||||
|
||||
// Print SDU preview if we have a length
|
||||
if let Some(len) = mac_access.length_ind {
|
||||
let info = Self::interpret_length_ind(len, pdu.get_len_remaining());
|
||||
if !info.is_null_pdu && !info.is_frag_start && !info.second_half_stolen && info.pdu_len_bits > 0 {
|
||||
Self::print_sdu(pdu, info.pdu_len_bits.min(pdu.get_len_remaining()), "TM-SDU");
|
||||
} else if info.is_frag_start {
|
||||
println!("Fragment data: {} bits remaining", pdu.get_len_remaining());
|
||||
}
|
||||
}
|
||||
|
||||
// Apply PDU association
|
||||
Self::apply_pdu_association(pdu, mac_access.length_ind, mac_access.fill_bits);
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-ACCESS: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_frag_ul(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-FRAG (UL) ---");
|
||||
match MacFragUl::from_bitbuf(pdu) {
|
||||
Ok(mac_frag) => {
|
||||
println!("{:#?}", mac_frag);
|
||||
let remaining = pdu.get_len_remaining();
|
||||
println!("TM-SDU fragment: {} bits remaining", remaining);
|
||||
if remaining > 0 && remaining <= 64 {
|
||||
if let Some(frag_bits) = pdu.peek_bits(remaining) {
|
||||
println!("Fragment data: {:0width$b}", frag_bits, width = remaining);
|
||||
}
|
||||
}
|
||||
println!("BitBuffer: {}", pdu.dump_bin());
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-FRAG: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_end_ul(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-END (UL) ---");
|
||||
match MacEndUl::from_bitbuf(pdu) {
|
||||
Ok(mac_end) => {
|
||||
println!("{:#?}", mac_end);
|
||||
|
||||
// Print SDU preview if we have a length
|
||||
if let Some(len) = mac_end.length_ind {
|
||||
let info = Self::interpret_length_ind(len, pdu.get_len_remaining());
|
||||
if !info.is_null_pdu && !info.is_frag_start && !info.second_half_stolen && info.pdu_len_bits > 0 {
|
||||
Self::print_sdu(pdu, info.pdu_len_bits.min(pdu.get_len_remaining()), "TM-SDU");
|
||||
} else if info.is_frag_start {
|
||||
println!("Fragment data: {} bits remaining", pdu.get_len_remaining());
|
||||
}
|
||||
}
|
||||
|
||||
// Apply PDU association
|
||||
Self::apply_pdu_association(pdu, mac_end.length_ind, mac_end.fill_bits);
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-END: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_end_hu(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-END-HU ---");
|
||||
match MacEndHu::from_bitbuf(pdu) {
|
||||
Ok(mac_end) => {
|
||||
println!("{:#?}", mac_end);
|
||||
|
||||
// Print SDU preview if we have a length
|
||||
if let Some(len) = mac_end.length_ind {
|
||||
let info = Self::interpret_length_ind(len, pdu.get_len_remaining());
|
||||
if !info.is_null_pdu && !info.is_frag_start && !info.second_half_stolen && info.pdu_len_bits > 0 {
|
||||
Self::print_sdu(pdu, info.pdu_len_bits.min(pdu.get_len_remaining()), "TM-SDU");
|
||||
} else if info.is_frag_start {
|
||||
println!("Fragment data: {} bits remaining", pdu.get_len_remaining());
|
||||
}
|
||||
}
|
||||
|
||||
// Apply PDU association
|
||||
Self::apply_pdu_association(pdu, mac_end.length_ind, mac_end.fill_bits);
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-END-HU: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_u_blck(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-U-BLCK ---");
|
||||
match MacUBlck::from_bitbuf(pdu) {
|
||||
Ok(mac_u_blck) => {
|
||||
println!("{:#?}", mac_u_blck);
|
||||
let remaining = pdu.get_len_remaining();
|
||||
println!("TM-SDU: {} bits remaining", remaining);
|
||||
println!("BitBuffer: {}", pdu.dump_bin());
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-U-BLCK: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_u_signal(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-U-SIGNAL ---");
|
||||
match MacUSignal::from_bitbuf(pdu) {
|
||||
Ok(mac_u_signal) => {
|
||||
println!("{:#?}", mac_u_signal);
|
||||
println!("BitBuffer: {}", pdu.dump_bin());
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-U-SIGNAL: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DOWNLINK PDU PARSERS
|
||||
// ========================================================================
|
||||
|
||||
fn parse_mac_resource(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-RESOURCE ---");
|
||||
match MacResource::from_bitbuf(pdu) {
|
||||
Ok(mac_res) => {
|
||||
println!("{:#?}", mac_res);
|
||||
|
||||
// Print SDU preview
|
||||
let info = Self::interpret_length_ind(mac_res.length_ind, pdu.get_len_remaining());
|
||||
if !info.is_null_pdu && !info.is_frag_start && !info.second_half_stolen && info.pdu_len_bits > 0 {
|
||||
Self::print_sdu(pdu, info.pdu_len_bits.min(pdu.get_len_remaining()), "TM-SDU");
|
||||
} else if info.is_frag_start {
|
||||
let remaining = pdu.get_len_remaining();
|
||||
println!("Fragment data: {} bits remaining", remaining);
|
||||
}
|
||||
|
||||
// Apply PDU association
|
||||
Self::apply_pdu_association(pdu, Some(mac_res.length_ind), mac_res.fill_bits);
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-RESOURCE: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_frag_dl(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-FRAG (DL) ---");
|
||||
match MacFragDl::from_bitbuf(pdu) {
|
||||
Ok(mac_frag) => {
|
||||
println!("{:#?}", mac_frag);
|
||||
let remaining = pdu.get_len_remaining();
|
||||
println!("TM-SDU fragment: {} bits remaining", remaining);
|
||||
println!("BitBuffer: {}", pdu.dump_bin());
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-FRAG (DL): {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_end_dl(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-END (DL) ---");
|
||||
match MacEndDl::from_bitbuf(pdu) {
|
||||
Ok(mac_end) => {
|
||||
println!("{:#?}", mac_end);
|
||||
|
||||
// Print SDU preview
|
||||
let info = Self::interpret_length_ind(mac_end.length_ind, pdu.get_len_remaining());
|
||||
if !info.is_null_pdu && !info.is_frag_start && !info.second_half_stolen && info.pdu_len_bits > 0 {
|
||||
Self::print_sdu(pdu, info.pdu_len_bits.min(pdu.get_len_remaining()), "TM-SDU");
|
||||
} else if info.is_frag_start {
|
||||
println!("Fragment data: {} bits remaining", pdu.get_len_remaining());
|
||||
}
|
||||
|
||||
// Apply PDU association
|
||||
Self::apply_pdu_association(pdu, Some(mac_end.length_ind), mac_end.fill_bits);
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-END (DL): {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_d_blck(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-D-BLCK ---");
|
||||
match MacDBlck::from_bitbuf(pdu) {
|
||||
Ok(mac_d_blck) => {
|
||||
println!("{:#?}", mac_d_blck);
|
||||
let remaining = pdu.get_len_remaining();
|
||||
println!("TM-SDU: {} bits remaining", remaining);
|
||||
println!("BitBuffer: {}", pdu.dump_bin());
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-D-BLCK: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// BROADCAST PDU PARSERS
|
||||
// ========================================================================
|
||||
|
||||
fn parse_broadcast(pdu: &mut BitBuffer) {
|
||||
// Peek broadcast type (bits 2-3 after MAC PDU type)
|
||||
let Some(bits) = pdu.peek_bits_posoffset(2, 2) else {
|
||||
println!("[!] Insufficient bits for broadcast type");
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(bcast_type) = BroadcastType::try_from(bits) else {
|
||||
println!("[!] Invalid broadcast type: {}", bits);
|
||||
return;
|
||||
};
|
||||
println!("Broadcast Type: {:?}", bcast_type);
|
||||
|
||||
match bcast_type {
|
||||
BroadcastType::Sysinfo => Self::parse_mac_sysinfo(pdu),
|
||||
BroadcastType::AccessDefine => Self::parse_access_define(pdu),
|
||||
BroadcastType::SysinfoDa => {
|
||||
println!("[!] SYSINFO-DA parsing not implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_sync(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-SYNC (BSCH) ---");
|
||||
match MacSync::from_bitbuf(pdu) {
|
||||
Ok(mac_sync) => {
|
||||
println!("{:#?}", mac_sync);
|
||||
println!("BitBuffer: {}", pdu.dump_bin());
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-SYNC: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mac_sysinfo(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing MAC-SYSINFO ---");
|
||||
match MacSysinfo::from_bitbuf(pdu) {
|
||||
Ok(mac_sysinfo) => {
|
||||
println!("{:#?}", mac_sysinfo);
|
||||
let remaining = pdu.get_len_remaining();
|
||||
if remaining > 0 {
|
||||
println!("MLE PDU follows: {} bits remaining", remaining);
|
||||
}
|
||||
println!("BitBuffer: {}", pdu.dump_bin());
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse MAC-SYSINFO: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_access_define(pdu: &mut BitBuffer) {
|
||||
println!("--- Parsing ACCESS-DEFINE ---");
|
||||
match AccessDefine::from_bitbuf(pdu) {
|
||||
Ok(access_def) => {
|
||||
println!("{:#?}", access_def);
|
||||
println!("BitBuffer: {}", pdu.dump_bin());
|
||||
}
|
||||
Err(e) => println!("[!] Failed to parse ACCESS-DEFINE: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ========================================================================
|
||||
|
||||
/// Interpret length_ind value according to ETSI EN 300 392-2 clause 21.4.3.1
|
||||
pub fn interpret_length_ind(length_ind: u8, remaining_bits: usize) -> LengthIndInfo {
|
||||
match length_ind {
|
||||
0b000000 => {
|
||||
// Null PDU
|
||||
LengthIndInfo {
|
||||
pdu_len_bits: 0,
|
||||
is_null_pdu: true,
|
||||
is_frag_start: false,
|
||||
second_half_stolen: false,
|
||||
}
|
||||
}
|
||||
0b000001 => {
|
||||
// Reserved
|
||||
println!("[!] Reserved length_ind value: 1");
|
||||
LengthIndInfo {
|
||||
pdu_len_bits: 0,
|
||||
is_null_pdu: false,
|
||||
is_frag_start: false,
|
||||
second_half_stolen: false,
|
||||
}
|
||||
}
|
||||
0b000010..=0b111001 => {
|
||||
// Valid length: 2-57 octets (16-456 bits)
|
||||
let len_bits = length_ind as usize * 8;
|
||||
LengthIndInfo {
|
||||
pdu_len_bits: len_bits.min(remaining_bits),
|
||||
is_null_pdu: false,
|
||||
is_frag_start: false,
|
||||
second_half_stolen: false,
|
||||
}
|
||||
}
|
||||
0b111010..=0b111101 => {
|
||||
// Reserved
|
||||
println!("[!] Reserved length_ind value: {}", length_ind);
|
||||
LengthIndInfo {
|
||||
pdu_len_bits: 0,
|
||||
is_null_pdu: false,
|
||||
is_frag_start: false,
|
||||
second_half_stolen: false,
|
||||
}
|
||||
}
|
||||
0b111110 => {
|
||||
// Second half slot stolen (STCH)
|
||||
LengthIndInfo {
|
||||
pdu_len_bits: remaining_bits,
|
||||
is_null_pdu: false,
|
||||
is_frag_start: false,
|
||||
second_half_stolen: true,
|
||||
}
|
||||
}
|
||||
0b111111 => {
|
||||
// Start of TL-SDU which extends in one or more subsequent MAC PDUs (fragmentation)
|
||||
LengthIndInfo {
|
||||
pdu_len_bits: remaining_bits,
|
||||
is_null_pdu: false,
|
||||
is_frag_start: true,
|
||||
second_half_stolen: false,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Should not happen for 6-bit value
|
||||
println!("[!] Invalid length_ind value: {}", length_ind);
|
||||
LengthIndInfo {
|
||||
pdu_len_bits: 0,
|
||||
is_null_pdu: false,
|
||||
is_frag_start: false,
|
||||
second_half_stolen: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Count fill bits at the end of a PDU segment.
|
||||
/// Fill bits are: a single '1' followed by zero or more '0' bits.
|
||||
/// Returns the number of fill bits (including the leading '1').
|
||||
pub fn count_fill_bits(pdu: &BitBuffer, pdu_len_bits: usize) -> usize {
|
||||
let start = pdu.get_raw_start();
|
||||
let mut index = pdu_len_bits as isize - 1;
|
||||
|
||||
// Walk backwards from end looking for the '1' that marks start of fill
|
||||
while index >= 0 {
|
||||
let bit = pdu.peek_bits_startoffset(start + index as usize, 1);
|
||||
if let Some(bit) = bit {
|
||||
if bit == 0 {
|
||||
index -= 1;
|
||||
} else {
|
||||
// Found the '1' bit - fill bits are from here to end
|
||||
return (pdu_len_bits as isize - index) as usize;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill bits found (all zeros or empty)
|
||||
0
|
||||
}
|
||||
|
||||
/// Apply PDU association: truncate buffer to PDU boundary, strip fill bits,
|
||||
/// and check for remaining data that could be another PDU.
|
||||
///
|
||||
/// Returns the remaining bits string if there's a "next block", None otherwise.
|
||||
pub fn apply_pdu_association(
|
||||
pdu: &mut BitBuffer,
|
||||
length_ind: Option<u8>,
|
||||
has_fill_bits: bool,
|
||||
) -> Option<String> {
|
||||
let Some(length_ind) = length_ind else {
|
||||
println!(" [No length_ind present - cannot apply PDU association]");
|
||||
return None;
|
||||
};
|
||||
|
||||
let remaining_bits = pdu.get_len_remaining();
|
||||
let info = Self::interpret_length_ind(length_ind, remaining_bits);
|
||||
|
||||
println!();
|
||||
println!("=== PDU Association ===");
|
||||
println!("length_ind: {} (0b{:06b})", length_ind, length_ind);
|
||||
|
||||
if info.is_null_pdu {
|
||||
println!(" Null PDU");
|
||||
return None;
|
||||
}
|
||||
|
||||
if info.is_frag_start {
|
||||
println!(" Fragmentation start (TL-SDU extends to subsequent MAC PDUs)");
|
||||
println!(" Remaining {} bits are fragment data", remaining_bits);
|
||||
return None;
|
||||
}
|
||||
|
||||
if info.second_half_stolen {
|
||||
println!(" Second half slot stolen (STCH signalling)");
|
||||
return None;
|
||||
}
|
||||
|
||||
let pdu_len_bits = info.pdu_len_bits;
|
||||
println!(" TM-SDU length: {} bits ({} bytes)", pdu_len_bits, length_ind);
|
||||
|
||||
// Calculate fill bits if requested
|
||||
let fill_bits = if has_fill_bits {
|
||||
let fb = Self::count_fill_bits(pdu, pdu_len_bits);
|
||||
if fb > 0 {
|
||||
println!(" Fill bits detected: {} bits", fb);
|
||||
}
|
||||
fb
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let sdu_len_bits = pdu_len_bits.saturating_sub(fill_bits);
|
||||
println!(" Effective SDU length: {} bits", sdu_len_bits);
|
||||
|
||||
// Check what's left after this PDU
|
||||
let orig_end = pdu.get_raw_end();
|
||||
let pdu_start = pdu.get_raw_start();
|
||||
let next_pdu_start = pdu_start + pdu_len_bits;
|
||||
let remaining_after_pdu = orig_end.saturating_sub(next_pdu_start);
|
||||
|
||||
if remaining_after_pdu >= 16 {
|
||||
// Minimum MAC PDU is ~16 bits (null PDU), so there could be another
|
||||
println!();
|
||||
println!("=== Next Block Available ===");
|
||||
println!(" {} bits remaining after this PDU", remaining_after_pdu);
|
||||
|
||||
// Extract the remaining bits as a string
|
||||
let mut next_block = String::new();
|
||||
for i in 0..remaining_after_pdu {
|
||||
if let Some(bit) = pdu.peek_bits_startoffset(next_pdu_start + i, 1) {
|
||||
next_block.push(if bit == 1 { '1' } else { '0' });
|
||||
}
|
||||
}
|
||||
|
||||
println!(" Next block: {}", next_block);
|
||||
println!();
|
||||
println!("To decode next PDU, run:");
|
||||
println!(" pdu-tool <direction> tmv umac \"{}\"", next_block);
|
||||
|
||||
return Some(next_block);
|
||||
} else if remaining_after_pdu > 0 {
|
||||
println!();
|
||||
println!(" {} bits remaining (too few for another PDU)", remaining_after_pdu);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if we should continue parsing more PDUs in this MAC block
|
||||
fn check_continue(pdu: &BitBuffer, orig_start: usize) -> bool {
|
||||
// If start was not updated, we also consider it end of message
|
||||
// If 16 or more bits remain (len of null pdu), we continue parsing
|
||||
if pdu.get_raw_start() != orig_start && pdu.get_len() >= 16 {
|
||||
println!();
|
||||
println!("--- Remaining {} bits, continuing parse ---", pdu.get_len_remaining());
|
||||
println!("Remaining: {}", pdu.dump_bin_full(true));
|
||||
println!();
|
||||
true
|
||||
} else {
|
||||
println!();
|
||||
println!("=== End of MAC block ===");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Print SDU data if available
|
||||
fn print_sdu(pdu: &mut BitBuffer, bit_len: usize, label: &str) {
|
||||
println!("{} length: {} bits ({} bytes)", label, bit_len, bit_len / 8);
|
||||
|
||||
if let Some(sdu_bits) = pdu.peek_bits(bit_len) {
|
||||
println!("{}: {:0width$b}", label, sdu_bits, width = bit_len);
|
||||
}
|
||||
pdu.read_bits(bit_len); // Advance past SDU
|
||||
}
|
||||
}
|
||||
96
bins/pdu-tool/src/main.rs
Normal file
96
bins/pdu-tool/src/main.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use clap::Parser;
|
||||
|
||||
use tetra_core::BitBuffer;
|
||||
use tetra_saps::tmv::enums::logical_chans::LogicalChannel;
|
||||
|
||||
mod entities;
|
||||
use entities::umac::UmacParser;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
author,
|
||||
version,
|
||||
about = "TETRA Raw PDU Decoder",
|
||||
long_about = "Decodes a raw bitstring as a PDU for the specified SAP and destination component"
|
||||
)]
|
||||
struct Args {
|
||||
/// Direction: uplink or downlink
|
||||
#[arg(
|
||||
help = "Direction: [ ul | dl ]"
|
||||
)]
|
||||
direction: String,
|
||||
|
||||
/// SAP (Service Access Point) name
|
||||
#[arg(
|
||||
help = "SAP name: [ tmv ]"
|
||||
)]
|
||||
sap: String,
|
||||
|
||||
/// Destination component name
|
||||
#[arg(
|
||||
help = "Destination component: [ umac ]"
|
||||
)]
|
||||
destination: String,
|
||||
|
||||
/// Raw bitstring to decode
|
||||
#[arg(
|
||||
help = "Raw bitstring (binary representation) to parse as PDU"
|
||||
)]
|
||||
bitstring: String,
|
||||
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "channel",
|
||||
default_value = "schf",
|
||||
help = "Logical channel (for tmv sap): [ schf | schhu | schhd | stch | bnch | bsch | aach ]"
|
||||
)]
|
||||
channel: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
eprintln!("[+] TETRA PDU Decoding tool");
|
||||
eprintln!(" Wouter Bokslag / Midnight Blue");
|
||||
eprintln!(" * This tool is a MESS and is for testing only *");
|
||||
eprintln!(" * There be bugs.. *");
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let logical_channel = match args.channel.to_lowercase().as_str() {
|
||||
"schf" | "sch_f" | "sch/f" => LogicalChannel::SchF,
|
||||
"schhu" | "sch_hu" | "sch/hu" => LogicalChannel::SchHu,
|
||||
"schhd" | "sch_hd" | "sch/hd" => LogicalChannel::SchHd,
|
||||
"stch" => LogicalChannel::Stch,
|
||||
"bnch" => LogicalChannel::Bnch,
|
||||
"bsch" => LogicalChannel::Bsch,
|
||||
"aach" => LogicalChannel::Aach,
|
||||
_ => {
|
||||
eprintln!("Error: Unsupported logical channel '{}'. Use: schf, schhu, schhd, stch, bnch, bsch, aach", args.channel);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let is_downlink = match args.direction.to_lowercase().as_str() {
|
||||
"ul" | "uplink" => false,
|
||||
"dl" | "downlink" => true,
|
||||
_ => {
|
||||
eprintln!("Error: Unsupported direction '{}'. Use: ul, dl", args.direction);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match (args.sap.to_lowercase().as_str(), args.destination.to_lowercase().as_str()) {
|
||||
("tmv", "umac") => {
|
||||
let pdu = BitBuffer::from_bitstr(args.bitstring.as_str());
|
||||
if is_downlink {
|
||||
UmacParser::parse_dl(pdu, logical_channel);
|
||||
} else {
|
||||
UmacParser::parse_ul(pdu, logical_channel);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Error: Unsupported SAP '{}' or destination '{}'", args.sap, args.destination);
|
||||
eprintln!("Supported: tmv umac");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user