Refactors in SDS handling code

* SDS user defined data types now enums
* Improved state SubscriberRegistry
* Small printing and formatting fixes
This commit is contained in:
Wouter Bokslag
2026-03-10 18:32:52 +01:00
parent 59c5c28096
commit db9992f8af
11 changed files with 259 additions and 392 deletions

View File

@@ -1,17 +1,27 @@
use std::collections::{HashMap, HashSet};
use tetra_core::TimeslotAllocator;
#[derive(Debug, Clone)]
pub struct Subscriber {
pub issi: u32,
// Set of attached GSSIs
pub attached_groups: HashSet<u32>,
}
/// Centralized subscriber registry tracking locally registered ISSIs and their group affiliations.
#[derive(Debug, Clone)]
pub struct SubscriberRegistry {
/// Registered ISSIs → affiliated GSSIs
subscribers: HashMap<u32, HashSet<u32>>,
/// Registered ISSIs → Subscriber information
subscribers: HashMap<u32, Subscriber>,
/// Set of all GSSIs with at least one local affiliate
all_attached_groups: HashSet<u32>,
}
impl SubscriberRegistry {
pub fn new() -> Self {
Self {
subscribers: HashMap::new(),
all_attached_groups: HashSet::new(),
}
}
@@ -19,26 +29,62 @@ impl SubscriberRegistry {
self.subscribers.contains_key(&issi)
}
/// Tolerant registration; if ISSI already registered, we overwrite it with a fresh Subscriber struct
pub fn register(&mut self, issi: u32) {
self.subscribers.entry(issi).or_default();
self.deregister(issi); // Clean up any existing registration to prevent stale affiliations
self.subscribers.insert(
issi,
Subscriber {
issi,
attached_groups: HashSet::new(),
},
);
}
/// Gets mutable ref to subscriber. If not registered, a default Subscriber is inserted.
pub fn get_subscriber_mut(&mut self, issi: u32) -> &mut Subscriber {
self.subscribers.entry(issi).or_insert_with(|| Subscriber {
issi,
attached_groups: HashSet::new(),
})
}
/// Deregister an ISSI, removing it from the registry and cleaning up any group affiliations
pub fn deregister(&mut self, issi: u32) {
self.subscribers.remove(&issi);
}
pub fn affiliate(&mut self, issi: u32, gssi: u32) {
self.subscribers.entry(issi).or_default().insert(gssi);
}
pub fn deaffiliate(&mut self, issi: u32, gssi: u32) {
if let Some(groups) = self.subscribers.get_mut(&issi) {
groups.remove(&gssi);
if let Some(subscriber) = self.subscribers.remove(&issi) {
// Clean up global group affiliations for this subscriber
for gssi in &subscriber.attached_groups {
// Check if any other subscriber is still affiliated with this group
let still_has_members = self.subscribers.values().any(|s| s.attached_groups.contains(gssi));
if !still_has_members {
self.all_attached_groups.remove(gssi);
}
}
}
}
/// Add GSSI to subscriber's attached groups and global set
pub fn affiliate(&mut self, issi: u32, gssi: u32) {
let subscriber = self.get_subscriber_mut(issi);
subscriber.attached_groups.insert(gssi);
self.all_attached_groups.insert(gssi);
}
/// Remove GSSI from subscriber's attached groups. Update global set if no more subscribers are affiliated with this GSSI.
pub fn deaffiliate(&mut self, issi: u32, gssi: u32) {
let subscriber = self.get_subscriber_mut(issi);
if subscriber.attached_groups.remove(&gssi) {
// Check if any other subscriber is still affiliated with this group
let still_has_members = self.subscribers.values().any(|s| s.attached_groups.contains(&gssi));
if !still_has_members {
self.all_attached_groups.remove(&gssi);
}
}
}
/// Check if any subscriber is affiliated with the given GSSI
pub fn has_group_members(&self, gssi: u32) -> bool {
self.subscribers.values().any(|groups| groups.contains(&gssi))
self.all_attached_groups.contains(&gssi)
}
}
@@ -90,6 +136,14 @@ mod tests {
// Deaffiliate one, should still have members
reg.deaffiliate(1001, 100);
assert!(reg.has_group_members(100));
// Deregister a user, should still have members
reg.deregister(1002);
assert!(reg.has_group_members(100));
// Deregister last user, should have no members
reg.deregister(1003);
assert!(!reg.has_group_members(100));
}
#[test]
@@ -97,6 +151,20 @@ mod tests {
let reg = SubscriberRegistry::new();
assert!(!reg.has_group_members(999));
}
#[test]
fn test_register_overwrites_existing_subscriber() {
let mut reg = SubscriberRegistry::new();
reg.register(1001);
reg.affiliate(1001, 91);
assert!(reg.has_group_members(91));
reg.register(1001);
assert!(reg.is_registered(1001));
reg.deaffiliate(1001, 91);
assert!(!reg.has_group_members(91));
}
}
impl Default for StackState {

View File

@@ -5,6 +5,8 @@ use std::thread;
use std::time::{Duration, Instant};
use crossbeam_channel::{Receiver, Sender, unbounded};
use tetra_saps::control::enums::sds_user_data::SdsUserData;
use tetra_saps::control::sds::CmceSdsData;
use uuid::Uuid;
use crate::{MessageQueue, TetraEntityTrait};
@@ -1069,7 +1071,7 @@ impl BrewEntity {
);
// Brew always sends SDS Type 4 (variable length) per protocol spec
let short_data_type_identifier = 3;
let user_defined_data = SdsUserData::Type4(length_bits, data);
// Forward to CMCE SDS subentity for downlink delivery
queue.push_back(SapMsg {
@@ -1077,18 +1079,16 @@ impl BrewEntity {
src: TetraEntity::Brew,
dest: TetraEntity::Cmce,
dltime: self.dltime,
msg: SapMsgInner::CmceSdsData(tetra_saps::control::sds::CmceSdsData {
msg: SapMsgInner::CmceSdsData(CmceSdsData {
source_issi: source,
dest_issi: destination,
short_data_type_identifier,
data,
length_bits,
user_defined_data,
}),
});
}
/// Handle outgoing SDS from CMCE → Brew (local MS → network)
fn handle_sds_send(&self, sds: tetra_saps::control::sds::CmceSdsData) {
fn handle_sds_send(&self, sds: CmceSdsData) {
if !self.connected {
tracing::warn!(
"BrewEntity: not connected, dropping outgoing SDS {} -> {}",
@@ -1100,20 +1100,20 @@ impl BrewEntity {
let uuid = Uuid::new_v4();
tracing::info!(
"BrewEntity: sending SDS uuid={} src={} dst={} type={} {} bytes",
"BrewEntity: sending SDS uuid={} src={} dst={} type={} {} bits",
uuid,
sds.source_issi,
sds.dest_issi,
sds.short_data_type_identifier,
sds.data.len()
sds.user_defined_data.type_identifier(),
sds.user_defined_data.length_bits()
);
let _ = self.command_sender.send(BrewCommand::SendSds {
uuid,
source: sds.source_issi,
destination: sds.dest_issi,
data: sds.data,
length_bits: sds.length_bits,
data: sds.user_defined_data.to_arr(),
length_bits: sds.user_defined_data.length_bits(),
});
}
}

View File

@@ -404,7 +404,7 @@ impl CcBsSubentity {
let pdu = match USetup::from_bitbuf(&mut prim.sdu) {
Ok(pdu) => {
tracing::debug!("<- U-SETUP {:?}", pdu);
tracing::debug!("<- {:?}", pdu);
pdu
}
Err(e) => {
@@ -856,7 +856,7 @@ impl CcBsSubentity {
let pdu = match UTxCeased::from_bitbuf(&mut prim.sdu) {
Ok(pdu) => {
tracing::debug!("<- U-TX CEASED {:?}", pdu);
tracing::debug!("<- {:?}", pdu);
pdu
}
Err(e) => {
@@ -944,7 +944,7 @@ impl CcBsSubentity {
let pdu = match UTxDemand::from_bitbuf(&mut prim.sdu) {
Ok(pdu) => {
tracing::debug!("<- U-TX DEMAND {:?}", pdu);
tracing::debug!("<- {:?}", pdu);
pdu
}
Err(e) => {
@@ -1050,7 +1050,7 @@ impl CcBsSubentity {
let pdu = match URelease::from_bitbuf(&mut prim.sdu) {
Ok(pdu) => {
tracing::debug!("<- U-RELEASE {:?}", pdu);
tracing::debug!("<- {:?}", pdu);
pdu
}
Err(e) => {
@@ -1078,7 +1078,7 @@ impl CcBsSubentity {
let pdu = match UDisconnect::from_bitbuf(&mut prim.sdu) {
Ok(pdu) => {
tracing::debug!("<- U-DISCONNECT {:?}", pdu);
tracing::debug!("<- {:?}", pdu);
pdu
}
Err(e) => {

View File

@@ -1,5 +1,6 @@
use tetra_config::bluestation::SharedConfig;
use tetra_core::{BitBuffer, Sap, SsiType, TetraAddress, tetra_entities::TetraEntity, unimplemented_log};
use tetra_saps::control::enums::sds_user_data::SdsUserData;
use tetra_saps::control::sds::CmceSdsData;
use tetra_saps::lcmc::LcmcMleUnitdataReq;
use tetra_saps::{SapMsg, SapMsgInner};
@@ -34,7 +35,7 @@ impl SdsBsSubentity {
let pdu = match USdsData::from_bitbuf(&mut prim.sdu) {
Ok(pdu) => {
tracing::debug!("<- U-SDS-DATA {:?}", pdu);
tracing::debug!("<- {:?}", pdu);
pdu
}
Err(e) => {
@@ -57,66 +58,19 @@ impl SdsBsSubentity {
"SDS: U-SDS-DATA from ISSI {} to ISSI {}, type={}",
source_ssi,
dest_ssi,
pdu.short_data_type_identifier
pdu.user_defined_data.type_identifier()
);
// Extract SDS payload into a uniform representation
let (data, length_bits) = match pdu.short_data_type_identifier {
0 => {
// Type 1: 16 bits
let val = pdu.user_defined_data_1.unwrap_or(0);
(val.to_be_bytes()[6..8].to_vec(), 16u16)
}
1 => {
// Type 2: 32 bits
let val = pdu.user_defined_data_2.unwrap_or(0);
(val.to_be_bytes()[4..8].to_vec(), 32u16)
}
2 => {
// Type 3: 64 bits
let val = pdu.user_defined_data_3.unwrap_or(0);
(val.to_be_bytes().to_vec(), 64u16)
}
3 => {
// Type 4: variable length
let len_bits = pdu.length_indicator.unwrap_or(0) as u16;
let data = pdu.user_defined_data_4.unwrap_or_default();
(data, len_bits)
}
_ => {
tracing::warn!("SDS: invalid short_data_type_identifier={}", pdu.short_data_type_identifier);
return;
}
};
// Route: local delivery (ISSI or GSSI), Brew forward, or drop
let is_local_issi = self.config.state_read().subscribers.is_registered(dest_ssi);
let is_local_group = !is_local_issi && self.config.state_read().subscribers.has_group_members(dest_ssi);
if is_local_issi {
tracing::info!("SDS: local delivery: {} -> {}", source_ssi, dest_ssi);
self.send_d_sds_data(
queue,
message.dltime,
source_ssi,
dest_ssi,
SsiType::Issi,
pdu.short_data_type_identifier,
&data,
length_bits,
);
self.send_d_sds_data(queue, message.dltime, source_ssi, dest_ssi, SsiType::Issi, pdu.user_defined_data);
} else if is_local_group {
tracing::info!("SDS: group delivery: {} -> GSSI {}", source_ssi, dest_ssi);
self.send_d_sds_data(
queue,
message.dltime,
source_ssi,
dest_ssi,
SsiType::Gssi,
pdu.short_data_type_identifier,
&data,
length_bits,
);
self.send_d_sds_data(queue, message.dltime, source_ssi, dest_ssi, SsiType::Gssi, pdu.user_defined_data);
} else if brew::is_active(&self.config)
&& (brew::is_brew_issi_routable(&self.config, dest_ssi) || brew::is_tetrapack_sds_service_issi(&self.config, dest_ssi))
{
@@ -129,9 +83,7 @@ impl SdsBsSubentity {
msg: SapMsgInner::CmceSdsData(CmceSdsData {
source_issi: source_ssi,
dest_issi: dest_ssi,
short_data_type_identifier: pdu.short_data_type_identifier,
data,
length_bits,
user_defined_data: pdu.user_defined_data,
}),
});
} else {
@@ -149,8 +101,8 @@ impl SdsBsSubentity {
"SDS: received from Brew: {} -> {}, type={}, {} bits",
sds.source_issi,
sds.dest_issi,
sds.short_data_type_identifier,
sds.length_bits
sds.user_defined_data.type_identifier(),
sds.user_defined_data.length_bits()
);
if !self.config.state_read().subscribers.is_registered(sds.dest_issi) {
@@ -165,9 +117,7 @@ impl SdsBsSubentity {
sds.source_issi,
sds.dest_issi,
SsiType::Issi,
sds.short_data_type_identifier,
&sds.data,
sds.length_bits,
sds.user_defined_data,
);
}
@@ -182,7 +132,7 @@ impl SdsBsSubentity {
let pdu = match UStatus::from_bitbuf(&mut prim.sdu) {
Ok(pdu) => {
tracing::debug!("<- U-STATUS {:?}", pdu);
tracing::debug!("<- {:?}", pdu);
pdu
}
Err(e) => {
@@ -276,54 +226,13 @@ impl SdsBsSubentity {
source_issi: u32,
dest_issi: u32,
dest_ssi_type: SsiType,
short_data_type_identifier: u8,
data: &[u8],
length_bits: u16,
user_defined_data: SdsUserData,
) {
// Build D-SDS-DATA PDU
let (user_defined_data_1, user_defined_data_2, user_defined_data_3, length_indicator, user_defined_data_4) =
match short_data_type_identifier {
0 => {
let val = if data.len() >= 2 {
((data[0] as u64) << 8) | (data[1] as u64)
} else if data.len() == 1 {
(data[0] as u64) << 8
} else {
0
};
(Some(val), None, None, None, None)
}
1 => {
let mut val: u64 = 0;
for (i, &b) in data.iter().take(4).enumerate() {
val |= (b as u64) << (24 - i * 8);
}
(None, Some(val), None, None, None)
}
2 => {
let mut val: u64 = 0;
for (i, &b) in data.iter().take(8).enumerate() {
val |= (b as u64) << (56 - i * 8);
}
(None, None, Some(val), None, None)
}
3 => (None, None, None, Some(length_bits as u64), Some(data.to_vec())),
_ => {
tracing::warn!("SDS: invalid short_data_type_identifier={}", short_data_type_identifier);
return;
}
};
let pdu = DSdsData {
calling_party_type_identifier: PartyTypeIdentifier::Ssi,
calling_party_address_ssi: Some(source_issi as u64),
calling_party_extension: None,
short_data_type_identifier,
user_defined_data_1,
user_defined_data_2,
user_defined_data_3,
length_indicator,
user_defined_data_4,
user_defined_data,
external_subscriber_number: None,
dm_ms_address: None,
};

View File

@@ -65,7 +65,7 @@ impl BsDefrag {
tracing::debug!(
"defrag_buffer for ts {} ssi: {}, t: {}-{}, frags: {}: {}",
ts,
t.t,
buf.addr.ssi,
buf.t_first,
buf.t_last,

View File

@@ -8,6 +8,7 @@ use tetra_core::{BitBuffer, Sap, SsiType, TdmaTime, TetraAddress, debug};
use tetra_pdus::cmce::enums::party_type_identifier::PartyTypeIdentifier;
use tetra_pdus::cmce::pdus::u_sds_data::USdsData;
use tetra_pdus::cmce::pdus::u_status::UStatus;
use tetra_saps::control::enums::sds_user_data::SdsUserData;
use tetra_saps::control::sds::CmceSdsData;
use tetra_saps::lcmc::LcmcMleUnitdataInd;
use tetra_saps::sapmsg::{SapMsg, SapMsgInner};
@@ -21,7 +22,7 @@ fn register_subscriber(test: &mut ComponentTest, issi: u32) {
/// Helper: affiliate a subscriber with a GSSI in the StackState subscriber registry
fn affiliate_subscriber(test: &mut ComponentTest, issi: u32, gssi: u32) {
test.config.state_write().subscribers.affiliate(issi, gssi);
test.config.state_write().subscribers.affiliate(issi, gssi).unwrap();
}
/// Helper: build a U-SDS-DATA message from a source ISSI to a dest SSI with 16-bit payload
@@ -32,12 +33,7 @@ fn build_u_sds_data_msg(dltime: TdmaTime, source_issi: u32, dest_ssi: u32, paylo
called_party_short_number_address: None,
called_party_ssi: Some(dest_ssi as u64),
called_party_extension: None,
short_data_type_identifier: 0,
user_defined_data_1: Some(payload as u64),
user_defined_data_2: None,
user_defined_data_3: None,
length_indicator: None,
user_defined_data_4: None,
user_defined_data: SdsUserData::Type1(payload),
external_subscriber_number: None,
dm_ms_address: None,
};
@@ -169,9 +165,7 @@ fn test_sds_from_brew_to_local() {
msg: SapMsgInner::CmceSdsData(CmceSdsData {
source_issi: 3000001,
dest_issi: 2000001,
short_data_type_identifier: 0,
data: vec![0xCA, 0xFE],
length_bits: 16,
user_defined_data: SdsUserData::Type1(0xCAFE),
}),
};
test.submit_message(msg);
@@ -202,9 +196,7 @@ fn test_sds_from_brew_unregistered() {
msg: SapMsgInner::CmceSdsData(CmceSdsData {
source_issi: 3000001,
dest_issi: 9999999,
short_data_type_identifier: 0,
data: vec![0xDE, 0xAD],
length_bits: 16,
user_defined_data: SdsUserData::Type1(0xDEAD),
}),
};
test.submit_message(msg);

View File

@@ -3,6 +3,7 @@ use core::fmt;
use crate::cmce::enums::{cmce_pdu_type_dl::CmcePduTypeDl, party_type_identifier::PartyTypeIdentifier, type3_elem_id::CmceType3ElemId};
use tetra_core::typed_pdu_fields::*;
use tetra_core::{BitBuffer, expect_pdu_type, pdu_parse_error::PduParseErr};
use tetra_saps::control::enums::sds_user_data::SdsUserData;
/// Representation of the D-SDS-DATA PDU (Clause 14.7.1.10).
/// This PDU shall be for receiving user defined SDS data.
@@ -19,18 +20,8 @@ pub struct DSdsData {
pub calling_party_address_ssi: Option<u64>,
/// Conditional 24 bits, See note 1, condition: calling_party_type_identifier == 2
pub calling_party_extension: Option<u64>,
/// Type1, 2 bits, Short data type identifier
pub short_data_type_identifier: u8,
/// Conditional 16 bits, See note 2, condition: short_data_type_identifier == 0
pub user_defined_data_1: Option<u64>,
/// Conditional 32 bits, See note 2, condition: short_data_type_identifier == 1
pub user_defined_data_2: Option<u64>,
/// Conditional 64 bits, See note 2, condition: short_data_type_identifier == 2
pub user_defined_data_3: Option<u64>,
/// Conditional 11 bits, See note 2, condition: short_data_type_identifier == 3
pub length_indicator: Option<u64>,
/// Conditional See note 2, condition: short_data_type_identifier == 3
pub user_defined_data_4: Option<Vec<u8>>,
/// Either type1, type2, type3 or type4 user data field.
pub user_defined_data: SdsUserData,
/// Type3, External subscriber number
pub external_subscriber_number: Option<Type3FieldGeneric>,
/// Type3, DM-MS address
@@ -62,43 +53,25 @@ impl DSdsData {
} else {
None
};
// Type1
let short_data_type_identifier = buffer.read_field(2, "short_data_type_identifier")? as u8;
// Conditional
let user_defined_data_1 = if short_data_type_identifier == 0 {
Some(buffer.read_field(16, "short_data_type_identifier")?)
} else {
None
};
// Conditional
let user_defined_data_2 = if short_data_type_identifier == 1 {
Some(buffer.read_field(32, "user_defined_data_2")?)
} else {
None
};
// Conditional
let user_defined_data_3 = if short_data_type_identifier == 2 {
Some(buffer.read_field(64, "user_defined_data_3")?)
} else {
None
};
// Conditional
let length_indicator = if short_data_type_identifier == 3 {
Some(buffer.read_field(11, "length_indicator")?)
} else {
None
};
// Conditional
let user_defined_data_4 = if short_data_type_identifier == 3 {
let len_bits = length_indicator.unwrap() as usize;
let num_bytes = (len_bits + 7) / 8;
let mut data = vec![0u8; num_bytes];
buffer.read_bits_into_slice(len_bits, &mut data).ok_or(PduParseErr::BufferEnded {
field: Some("user_defined_data_4"),
})?;
Some(data)
} else {
None
let user_defined_data = match short_data_type_identifier {
0 => SdsUserData::Type1(buffer.read_field(16, "user_defined_data_1")? as u16),
1 => SdsUserData::Type2(buffer.read_field(32, "user_defined_data_2")? as u32),
2 => SdsUserData::Type3(buffer.read_field(64, "user_defined_data_3")?),
3 => {
let len_bits = buffer.read_field(11, "length_indicator")? as u16;
let num_bytes = (len_bits + 7) / 8;
let mut data = vec![0u8; num_bytes as usize];
buffer
.read_bits_into_slice(len_bits as usize, &mut data)
.ok_or(PduParseErr::BufferEnded {
field: Some("user_defined_data_4"),
})?;
SdsUserData::Type4(len_bits, data)
}
_ => unreachable!(),
};
// obit designates presence of any further type2, type3 or type4 fields
@@ -120,12 +93,7 @@ impl DSdsData {
calling_party_type_identifier,
calling_party_address_ssi,
calling_party_extension,
short_data_type_identifier,
user_defined_data_1,
user_defined_data_2,
user_defined_data_3,
length_indicator,
user_defined_data_4,
user_defined_data,
external_subscriber_number,
dm_ms_address,
})
@@ -145,34 +113,25 @@ impl DSdsData {
if let Some(ref value) = self.calling_party_extension {
buffer.write_bits(*value, 24);
}
// Type1
buffer.write_bits(self.short_data_type_identifier as u64, 2);
// Conditional
if let Some(ref value) = self.user_defined_data_1 {
buffer.write_bits(*value, 16);
}
// Conditional
if let Some(ref value) = self.user_defined_data_2 {
buffer.write_bits(*value, 32);
}
// Conditional
if let Some(ref value) = self.user_defined_data_3 {
buffer.write_bits(*value, 64);
}
// Conditional
if let Some(ref value) = self.length_indicator {
buffer.write_bits(*value, 11);
}
// Conditional
if let Some(ref data) = self.user_defined_data_4 {
let len_bits = self.length_indicator.unwrap() as usize;
let full_bytes = len_bits / 8;
let remaining_bits = len_bits % 8;
for i in 0..full_bytes {
buffer.write_bits(data[i] as u64, 8);
}
if remaining_bits > 0 {
buffer.write_bits((data[full_bytes] >> (8 - remaining_bits)) as u64, remaining_bits);
let short_data_type_identifier = self.user_defined_data.type_identifier();
buffer.write_bits(short_data_type_identifier as u64, 2);
match &self.user_defined_data {
SdsUserData::Type1(value) => buffer.write_bits(*value as u64, 16),
SdsUserData::Type2(value) => buffer.write_bits(*value as u64, 32),
SdsUserData::Type3(value) => buffer.write_bits(*value, 64),
SdsUserData::Type4(len_bits, data) => {
buffer.write_bits(*len_bits as u64, 11);
let full_bytes = (*len_bits as usize) / 8;
let remaining_bits = len_bits % 8;
for i in 0..full_bytes {
buffer.write_bits(data[i] as u64, 8);
}
if remaining_bits > 0 {
buffer.write_bits((data[full_bytes] >> (8 - remaining_bits)) as u64, remaining_bits as usize);
}
}
}
@@ -199,16 +158,11 @@ impl fmt::Display for DSdsData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"DSdsData {{ calling_party_type_identifier: {:?} calling_party_address_ssi: {:?} calling_party_extension: {:?} short_data_type_identifier: {:?} user_defined_data_1: {:?} user_defined_data_2: {:?} user_defined_data_3: {:?} length_indicator: {:?} user_defined_data_4: {:?} external_subscriber_number: {:?} dm_ms_address: {:?} }}",
"DSdsData {{ calling_party_type_identifier: {:?} calling_party_address_ssi: {:?} calling_party_extension: {:?} user_defined_data: {:?} external_subscriber_number: {:?} dm_ms_address: {:?} }}",
self.calling_party_type_identifier,
self.calling_party_address_ssi,
self.calling_party_extension,
self.short_data_type_identifier,
self.user_defined_data_1,
self.user_defined_data_2,
self.user_defined_data_3,
self.length_indicator,
self.user_defined_data_4,
self.user_defined_data,
self.external_subscriber_number,
self.dm_ms_address,
)
@@ -233,12 +187,7 @@ mod tests {
calling_party_type_identifier: PartyTypeIdentifier::Ssi,
calling_party_address_ssi: Some(1000001),
calling_party_extension: None,
short_data_type_identifier: 0,
user_defined_data_1: Some(0xABCD),
user_defined_data_2: None,
user_defined_data_3: None,
length_indicator: None,
user_defined_data_4: None,
user_defined_data: SdsUserData::Type1(0xABCD),
external_subscriber_number: None,
dm_ms_address: None,
};
@@ -246,8 +195,7 @@ mod tests {
assert_eq!(parsed.calling_party_type_identifier, PartyTypeIdentifier::Ssi);
assert_eq!(parsed.calling_party_address_ssi, Some(1000001));
assert_eq!(parsed.calling_party_extension, None);
assert_eq!(parsed.short_data_type_identifier, 0);
assert_eq!(parsed.user_defined_data_1, Some(0xABCD));
assert_eq!(parsed.user_defined_data, SdsUserData::Type1(0xABCD));
}
#[test]
@@ -257,21 +205,14 @@ mod tests {
calling_party_type_identifier: PartyTypeIdentifier::Ssi,
calling_party_address_ssi: Some(2000002),
calling_party_extension: None,
short_data_type_identifier: 3,
user_defined_data_1: None,
user_defined_data_2: None,
user_defined_data_3: None,
length_indicator: Some(40), // 5 bytes = 40 bits
user_defined_data_4: Some(payload.clone()),
user_defined_data: SdsUserData::Type4(40, payload.clone()), // 5 bytes = 40 bits
external_subscriber_number: None,
dm_ms_address: None,
};
let parsed = round_trip(&pdu);
assert_eq!(parsed.calling_party_type_identifier, PartyTypeIdentifier::Ssi);
assert_eq!(parsed.calling_party_address_ssi, Some(2000002));
assert_eq!(parsed.short_data_type_identifier, 3);
assert_eq!(parsed.length_indicator, Some(40));
assert_eq!(parsed.user_defined_data_4, Some(payload));
assert_eq!(parsed.user_defined_data, SdsUserData::Type4(40, payload));
}
#[test]
@@ -280,12 +221,7 @@ mod tests {
calling_party_type_identifier: PartyTypeIdentifier::Tsi,
calling_party_address_ssi: Some(3000003),
calling_party_extension: Some(0x123456),
short_data_type_identifier: 0,
user_defined_data_1: Some(0x1234),
user_defined_data_2: None,
user_defined_data_3: None,
length_indicator: None,
user_defined_data_4: None,
user_defined_data: SdsUserData::Type1(0x1234),
external_subscriber_number: None,
dm_ms_address: None,
};
@@ -293,8 +229,7 @@ mod tests {
assert_eq!(parsed.calling_party_type_identifier, PartyTypeIdentifier::Tsi);
assert_eq!(parsed.calling_party_address_ssi, Some(3000003));
assert_eq!(parsed.calling_party_extension, Some(0x123456));
assert_eq!(parsed.short_data_type_identifier, 0);
assert_eq!(parsed.user_defined_data_1, Some(0x1234));
assert_eq!(parsed.user_defined_data, SdsUserData::Type1(0x1234));
}
#[test]
@@ -303,12 +238,7 @@ mod tests {
calling_party_type_identifier: PartyTypeIdentifier::Sna,
calling_party_address_ssi: None,
calling_party_extension: None,
short_data_type_identifier: 1,
user_defined_data_1: None,
user_defined_data_2: Some(0xDEADBEEF),
user_defined_data_3: None,
length_indicator: None,
user_defined_data_4: None,
user_defined_data: SdsUserData::Type2(0xDEADBEEF),
external_subscriber_number: None,
dm_ms_address: None,
};
@@ -316,7 +246,6 @@ mod tests {
assert_eq!(parsed.calling_party_type_identifier, PartyTypeIdentifier::Sna);
assert_eq!(parsed.calling_party_address_ssi, None);
assert_eq!(parsed.calling_party_extension, None);
assert_eq!(parsed.short_data_type_identifier, 1);
assert_eq!(parsed.user_defined_data_2, Some(0xDEADBEEF));
assert_eq!(parsed.user_defined_data, SdsUserData::Type2(0xDEADBEEF));
}
}

View File

@@ -3,6 +3,7 @@ use core::fmt;
use crate::cmce::enums::{cmce_pdu_type_ul::CmcePduTypeUl, party_type_identifier::PartyTypeIdentifier, type3_elem_id::CmceType3ElemId};
use tetra_core::typed_pdu_fields::*;
use tetra_core::{BitBuffer, expect_pdu_type, pdu_parse_error::PduParseErr};
use tetra_saps::control::enums::sds_user_data::SdsUserData;
/// Representation of the U-SDS-DATA PDU (Clause 14.7.2.8).
/// This PDU shall be for sending user defined SDS data.
@@ -26,18 +27,8 @@ pub struct USdsData {
pub called_party_ssi: Option<u64>,
/// Conditional 24 bits, See note 2, condition: called_party_type_identifier == 2
pub called_party_extension: Option<u64>,
/// Type1, 2 bits, See note 4,
pub short_data_type_identifier: u8,
/// Conditional 16 bits, See note 2, condition: short_data_type_identifier == 0
pub user_defined_data_1: Option<u64>,
/// Conditional 32 bits, See note 2, condition: short_data_type_identifier == 1
pub user_defined_data_2: Option<u64>,
/// Conditional 64 bits, See note 2, condition: short_data_type_identifier == 2
pub user_defined_data_3: Option<u64>,
/// Conditional 11 bits, See note 2, condition: short_data_type_identifier == 3
pub length_indicator: Option<u64>,
/// Conditional See note 2, condition: short_data_type_identifier == 3
pub user_defined_data_4: Option<Vec<u8>>,
/// Either type1, type2, type3 or type4 user data field.
pub user_defined_data: SdsUserData,
/// Type3, External subscriber number
pub external_subscriber_number: Option<Type3FieldGeneric>,
/// Type3, DM-MS address
@@ -77,43 +68,25 @@ impl USdsData {
} else {
None
};
// Type1
let short_data_type_identifier = buffer.read_field(2, "short_data_type_identifier")? as u8;
// Conditional
let user_defined_data_1 = if short_data_type_identifier == 0 {
Some(buffer.read_field(16, "user_defined_data_1")?)
} else {
None
};
// Conditional
let user_defined_data_2 = if short_data_type_identifier == 1 {
Some(buffer.read_field(32, "user_defined_data_2")?)
} else {
None
};
// Conditional
let user_defined_data_3 = if short_data_type_identifier == 2 {
Some(buffer.read_field(64, "user_defined_data_3")?)
} else {
None
};
// Conditional
let length_indicator = if short_data_type_identifier == 3 {
Some(buffer.read_field(11, "length_indicator")?)
} else {
None
};
// Conditional
let user_defined_data_4 = if short_data_type_identifier == 3 {
let len_bits = length_indicator.unwrap() as usize;
let num_bytes = (len_bits + 7) / 8;
let mut data = vec![0u8; num_bytes];
buffer.read_bits_into_slice(len_bits, &mut data).ok_or(PduParseErr::BufferEnded {
field: Some("user_defined_data_4"),
})?;
Some(data)
} else {
None
let user_defined_data = match short_data_type_identifier {
0 => SdsUserData::Type1(buffer.read_field(16, "user_defined_data_1")? as u16),
1 => SdsUserData::Type2(buffer.read_field(32, "user_defined_data_2")? as u32),
2 => SdsUserData::Type3(buffer.read_field(64, "user_defined_data_3")?),
3 => {
let len_bits = buffer.read_field(11, "length_indicator")? as u16;
let num_bytes = (len_bits as usize + 7) / 8;
let mut data = vec![0u8; num_bytes];
buffer
.read_bits_into_slice(len_bits as usize, &mut data)
.ok_or(PduParseErr::BufferEnded {
field: Some("user_defined_data_4"),
})?;
SdsUserData::Type4(len_bits, data)
}
_ => unreachable!(),
};
// obit designates presence of any further type2, type3 or type4 fields
@@ -137,12 +110,7 @@ impl USdsData {
called_party_short_number_address,
called_party_ssi,
called_party_extension,
short_data_type_identifier,
user_defined_data_1,
user_defined_data_2,
user_defined_data_3,
length_indicator,
user_defined_data_4,
user_defined_data,
external_subscriber_number,
dm_ms_address,
})
@@ -168,34 +136,25 @@ impl USdsData {
if let Some(ref value) = self.called_party_extension {
buffer.write_bits(*value, 24);
}
// Type1
buffer.write_bits(self.short_data_type_identifier as u64, 2);
// Conditional
if let Some(ref value) = self.user_defined_data_1 {
buffer.write_bits(*value, 16);
}
// Conditional
if let Some(ref value) = self.user_defined_data_2 {
buffer.write_bits(*value, 32);
}
// Conditional
if let Some(ref value) = self.user_defined_data_3 {
buffer.write_bits(*value, 64);
}
// Conditional
if let Some(ref value) = self.length_indicator {
buffer.write_bits(*value, 11);
}
// Conditional
if let Some(ref data) = self.user_defined_data_4 {
let len_bits = self.length_indicator.unwrap() as usize;
let full_bytes = len_bits / 8;
let remaining_bits = len_bits % 8;
for i in 0..full_bytes {
buffer.write_bits(data[i] as u64, 8);
}
if remaining_bits > 0 {
buffer.write_bits((data[full_bytes] >> (8 - remaining_bits)) as u64, remaining_bits);
let short_data_type_identifier = self.user_defined_data.type_identifier();
buffer.write_bits(short_data_type_identifier as u64, 2);
match &self.user_defined_data {
SdsUserData::Type1(value) => buffer.write_bits(*value as u64, 16),
SdsUserData::Type2(value) => buffer.write_bits(*value as u64, 32),
SdsUserData::Type3(value) => buffer.write_bits(*value, 64),
SdsUserData::Type4(len_bits, data) => {
buffer.write_bits(*len_bits as u64, 11);
let full_bytes = (*len_bits as usize) / 8;
let remaining_bits = len_bits % 8;
for i in 0..full_bytes {
buffer.write_bits(data[i] as u64, 8);
}
if remaining_bits > 0 {
buffer.write_bits((data[full_bytes] >> (8 - remaining_bits)) as u64, remaining_bits as usize);
}
}
}
@@ -222,18 +181,13 @@ impl fmt::Display for USdsData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"USdsData {{ area_selection: {:?} called_party_type_identifier: {:?} called_party_short_number_address: {:?} called_party_ssi: {:?} called_party_extension: {:?} short_data_type_identifier: {:?} user_defined_data_1: {:?} user_defined_data_2: {:?} user_defined_data_3: {:?} length_indicator: {:?} user_defined_data_4: {:?} external_subscriber_number: {:?} dm_ms_address: {:?} }}",
"USdsData {{ area_selection: {:?} called_party_type_identifier: {:?} called_party_short_number_address: {:?} called_party_ssi: {:?} called_party_extension: {:?} user_defined_data: {:?} external_subscriber_number: {:?} dm_ms_address: {:?} }}",
self.area_selection,
self.called_party_type_identifier,
self.called_party_short_number_address,
self.called_party_ssi,
self.called_party_extension,
self.short_data_type_identifier,
self.user_defined_data_1,
self.user_defined_data_2,
self.user_defined_data_3,
self.length_indicator,
self.user_defined_data_4,
self.user_defined_data,
self.external_subscriber_number,
self.dm_ms_address,
)
@@ -260,12 +214,7 @@ mod tests {
called_party_short_number_address: None,
called_party_ssi: Some(1000001),
called_party_extension: None,
short_data_type_identifier: 0,
user_defined_data_1: Some(0xCAFE),
user_defined_data_2: None,
user_defined_data_3: None,
length_indicator: None,
user_defined_data_4: None,
user_defined_data: SdsUserData::Type1(0xCAFE),
external_subscriber_number: None,
dm_ms_address: None,
};
@@ -274,8 +223,7 @@ mod tests {
assert_eq!(parsed.called_party_type_identifier, PartyTypeIdentifier::Ssi);
assert_eq!(parsed.called_party_ssi, Some(1000001));
assert_eq!(parsed.called_party_extension, None);
assert_eq!(parsed.short_data_type_identifier, 0);
assert_eq!(parsed.user_defined_data_1, Some(0xCAFE));
assert_eq!(parsed.user_defined_data, SdsUserData::Type1(0xCAFE));
}
#[test]
@@ -287,21 +235,14 @@ mod tests {
called_party_short_number_address: None,
called_party_ssi: Some(2000002),
called_party_extension: None,
short_data_type_identifier: 3,
user_defined_data_1: None,
user_defined_data_2: None,
user_defined_data_3: None,
length_indicator: Some(24), // 3 bytes
user_defined_data_4: Some(payload.clone()),
user_defined_data: SdsUserData::Type4(24, payload.clone()),
external_subscriber_number: None,
dm_ms_address: None,
};
let parsed = round_trip(&pdu);
assert_eq!(parsed.area_selection, 5);
assert_eq!(parsed.called_party_ssi, Some(2000002));
assert_eq!(parsed.short_data_type_identifier, 3);
assert_eq!(parsed.length_indicator, Some(24));
assert_eq!(parsed.user_defined_data_4, Some(payload));
assert_eq!(parsed.user_defined_data, SdsUserData::Type4(24, payload));
}
#[test]
@@ -312,12 +253,7 @@ mod tests {
called_party_short_number_address: Some(42),
called_party_ssi: None,
called_party_extension: None,
short_data_type_identifier: 1,
user_defined_data_1: None,
user_defined_data_2: Some(0x12345678),
user_defined_data_3: None,
length_indicator: None,
user_defined_data_4: None,
user_defined_data: SdsUserData::Type2(0x12345678),
external_subscriber_number: None,
dm_ms_address: None,
};
@@ -325,7 +261,7 @@ mod tests {
assert_eq!(parsed.called_party_type_identifier, PartyTypeIdentifier::Sna);
assert_eq!(parsed.called_party_short_number_address, Some(42));
assert_eq!(parsed.called_party_ssi, None);
assert_eq!(parsed.user_defined_data_2, Some(0x12345678));
assert_eq!(parsed.user_defined_data, SdsUserData::Type2(0x12345678));
}
#[test]
@@ -336,12 +272,7 @@ mod tests {
called_party_short_number_address: None,
called_party_ssi: Some(3000003),
called_party_extension: Some(0xABCDEF),
short_data_type_identifier: 2,
user_defined_data_1: None,
user_defined_data_2: None,
user_defined_data_3: Some(0x0102030405060708),
length_indicator: None,
user_defined_data_4: None,
user_defined_data: SdsUserData::Type3(0x0102030405060708),
external_subscriber_number: None,
dm_ms_address: None,
};
@@ -349,7 +280,6 @@ mod tests {
assert_eq!(parsed.called_party_type_identifier, PartyTypeIdentifier::Tsi);
assert_eq!(parsed.called_party_ssi, Some(3000003));
assert_eq!(parsed.called_party_extension, Some(0xABCDEF));
assert_eq!(parsed.short_data_type_identifier, 2);
assert_eq!(parsed.user_defined_data_3, Some(0x0102030405060708));
assert_eq!(parsed.user_defined_data, SdsUserData::Type3(0x0102030405060708));
}
}

View File

@@ -1,2 +1,3 @@
pub mod circuit_mode_type;
pub mod communication_type;
pub mod sds_user_data;

View File

@@ -0,0 +1,40 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SdsUserData {
/// Type field 0, 16 bits, short_data_type_identifier == 0
Type1(u16),
/// Type field 1, 32 bits, short_data_type_identifier == 1
Type2(u32),
/// Type field 2, 64 bits, short_data_type_identifier == 2
Type3(u64),
/// Type field 3, variable length, short_data_type_identifier == 3
Type4(u16, Vec<u8>),
}
impl SdsUserData {
pub fn type_identifier(&self) -> u8 {
match self {
SdsUserData::Type1(_) => 0,
SdsUserData::Type2(_) => 1,
SdsUserData::Type3(_) => 2,
SdsUserData::Type4(_, _) => 3,
}
}
pub fn length_bits(&self) -> u16 {
match self {
SdsUserData::Type1(_) => 16,
SdsUserData::Type2(_) => 32,
SdsUserData::Type3(_) => 64,
SdsUserData::Type4(len_bits, _) => *len_bits,
}
}
pub fn to_arr(&self) -> Vec<u8> {
match self {
SdsUserData::Type1(value) => value.to_be_bytes().to_vec(),
SdsUserData::Type2(value) => value.to_be_bytes().to_vec(),
SdsUserData::Type3(value) => value.to_be_bytes().to_vec(),
SdsUserData::Type4(_, data) => data.clone(),
}
}
}

View File

@@ -1,3 +1,5 @@
use crate::control::enums::sds_user_data::SdsUserData;
/// SDS data routing between CMCE SDS subentity and Brew entity
#[derive(Debug, Clone)]
pub struct CmceSdsData {
@@ -5,10 +7,6 @@ pub struct CmceSdsData {
pub source_issi: u32,
/// Destination ISSI (called party)
pub dest_issi: u32,
/// Short data type identifier (0-3)
pub short_data_type_identifier: u8,
/// SDS payload data (raw bytes from user_defined_data_1/2/3/4)
pub data: Vec<u8>,
/// Length in bits (for type 4, from length_indicator)
pub length_bits: u16,
/// User-defined data (type1, type2, type3, or type4)
pub user_defined_data: SdsUserData,
}