diff --git a/crates/tetra-entities/src/umac/subcomp/bs_sched.rs b/crates/tetra-entities/src/umac/subcomp/bs_sched.rs index b0d6b9d..dd40a82 100644 --- a/crates/tetra-entities/src/umac/subcomp/bs_sched.rs +++ b/crates/tetra-entities/src/umac/subcomp/bs_sched.rs @@ -14,7 +14,7 @@ use tetra_pdus::{ }, fields::basic_slotgrant::BasicSlotgrant, pdus::{ - access_assign::{AccessAssign, AccessField}, + access_assign::{AccessAssign}, access_assign_fr18::AccessAssignFr18, mac_resource::MacResource, mac_sync::MacSync, @@ -22,7 +22,9 @@ use tetra_pdus::{ }, }, }; - +use tetra_pdus::umac::enums::access_code::AccessCode; +use tetra_pdus::umac::structs::access_field::AccessField; +use tetra_pdus::umac::structs::base_frame_length::BaseFrameLength; use crate::{ lmac::components::scrambler, umac::subcomp::{bs_frag::BsFragger, circuit_mgr::CircuitMgr}, @@ -40,6 +42,9 @@ pub const SCH_HD_CAP: usize = 124; pub const SCH_F_CAP: usize = 268; pub const TCH_S_CAP: usize = 274; +// The default access frame marker used in access fields +const DEFAULT_ACCESS_FRAME_MARKER: BaseFrameLength = BaseFrameLength::Subslots2; + /// Number of timeslots the scheduler operates on. May become larger when secondary carriers are supported. pub const NUM_TIMESLOTS: usize = 4; @@ -1077,6 +1082,7 @@ impl BsChannelScheduler { } fn generate_bbk_block(&self, ts: TdmaTime) -> TmvUnitdataReq { + let (ul_traffic_usage, dl_traffic_usage) = if ts.f == 18 { (None, None) } else { @@ -1088,80 +1094,121 @@ impl BsChannelScheduler { // Generate BBK block let mut aach_bb = BitBuffer::new(14); + if ts.f != 18 { - let mut aach = AccessAssign::default(); - match ts.t { + let aach = match ts.t { + + // MCCH (TS1) 1 => { + + // 23.3.1.1.2 + // "During normal mode operation, it shall always be assumed that slot 1 on the + // downlink is for common control as part of the MCCH." assert!(dl_traffic_usage.is_none(), "DL ts 1 can't be traffic"); - assert!(ul_traffic_usage.is_none(), "UL ts 1 can't be traffic (is this allowed?"); // TODO FIXME check spec - // Always CommonOnly on TS1 (MCCH). Per ETSI 23.5.2.2.2, the MS - // with a grant transmits in granted slots without checking the AACH. - aach.dl_usage = AccessAssignDlUsage::CommonControl; - aach.ul_usage = AccessAssignUlUsage::CommonOnly; - aach.f1_af1 = Some(AccessField { - access_code: 0, - base_frame_len: 4, - }); - aach.f2_af2 = Some(AccessField { - access_code: 0, - base_frame_len: 4, - }); - } - 2..=4 => { - // Additional channels (TS2..TS4). - // Normal operation: Traffic(usage) when a circuit is active, else Unallocated. - // Hangtime: immediately switch AACH to AssignedControl so radios - // detect the end of traffic in the same frame as D-TX CEASED. - // The timeslot may still be in traffic mode (for STCH delivery) but - // the AACH reflects the new channel state. - let in_hangtime = (2..=4).contains(&ts.t) && self.hangtime[ts.t as usize - 1]; + // TODO FIXME: It *is* possible for UL TS1 to carry traffic. + // + // 23.3.4 Independent allocation of uplink and downlink + // "A BS may allocate uplink and downlink channels for different purposes. Some examples are listed below:" + // "[...] common control on downlink MCCH (slot 1); uplink slot 1 of main carrier allocated for a circuit mode call;" + // + // That said, it's not something that tetra-bluestation does right now, so this assert is still sensible. + assert!(ul_traffic_usage.is_none(), "UL TS 1 can't currently be traffic"); - if in_hangtime && (dl_traffic_usage.is_some() || ul_traffic_usage.is_some()) { - aach.dl_usage = AccessAssignDlUsage::AssignedControl; - // AssignedOnly (Header 2) allows random access for MSs on - // the assigned channel while blocking common control MSs. - aach.ul_usage = AccessAssignUlUsage::AssignedOnly; - aach.f2_af = Some(AccessField { - access_code: 0, - base_frame_len: 4, - }); - } else { - aach.dl_usage = if let Some(usage) = dl_traffic_usage { - AccessAssignDlUsage::Traffic(usage) - } else { - AccessAssignDlUsage::Unallocated - }; - aach.ul_usage = if let Some(usage) = ul_traffic_usage { - AccessAssignUlUsage::Traffic(usage) - } else { - AccessAssignUlUsage::Unallocated - }; + // Indicate any reserved slots in the uplink with base_frame_len=ReservedSubslot + AccessAssign::DownlinkCommonControlUplinkCommonOnly { + access_field_1: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: if self.ul_get_slot_owner(ts, PhyBlockNum::Block1).is_some() { + BaseFrameLength::ReservedSubslot + } else { + DEFAULT_ACCESS_FRAME_MARKER + } + }, + access_field_2: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: if self.ul_get_slot_owner(ts, PhyBlockNum::Block2).is_some() { + BaseFrameLength::ReservedSubslot + } else { + DEFAULT_ACCESS_FRAME_MARKER + } + }, } - } - _ => panic!("finalize_ts_for_tick: invalid timeslot {}", ts.t), - } + + }, + + // Additional channels (TS2..TS4) + 2..=4 => { + + if self.is_hangtime(ts.t) && (dl_traffic_usage.is_some() || ul_traffic_usage.is_some()) { + + // Hangtime: immediately switch AACH to AssignedControl so radios + // detect the end of traffic in the same frame as D-TX CEASED. + // The timeslot may still be in traffic mode (for STCH delivery) but + // the AACH reflects the new channel state. + AccessAssign::DownlinkDefinedUplinkAssignedOnly { + downlink_usage_marker: AccessAssignDlUsage::AssignedControl, + access_field: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: DEFAULT_ACCESS_FRAME_MARKER, + }, + } + + } else { + + // Normal operation: Traffic(usage) when a circuit is active, else Unallocated + AccessAssign::DownlinkDefinedUplinkDefined { + downlink_usage_marker: if let Some(usage) = dl_traffic_usage { + AccessAssignDlUsage::Traffic(usage) + } else { + AccessAssignDlUsage::Unallocated + }, + uplink_usage_marker: if let Some(usage) = ul_traffic_usage { + AccessAssignUlUsage::Traffic(usage) + } else { + AccessAssignUlUsage::Unallocated + } + } + + } + }, + + _ => panic!("finalize_ts_for_tick: invalid timeslot {}", ts.t) + }; aach.to_bitbuf(&mut aach_bb); + } else { - // Fr18 + + // Frame 18 is the Control Frame, which cannot contain traffic assert!(ul_traffic_usage.is_none() && dl_traffic_usage.is_none()); - let aach = AccessAssignFr18 { - ul_usage: AccessAssignUlUsage::CommonOnly, - f1_af1: Some(AccessField { - access_code: 0, - base_frame_len: 1, - }), - f2_af2: Some(AccessField { - access_code: 0, - base_frame_len: 0, - }), - ..Default::default() + + let aach = AccessAssignFr18::UplinkCommonOnly { + access_field_1: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: if self.ul_get_slot_owner(ts, PhyBlockNum::Block1).is_some() { + // Subslot is reserved in the uplink + BaseFrameLength::ReservedSubslot + } else if ts.is_mandatory_clch() { + // CLCH opportunity (which is always in SSN1, see EN 300 392 ยง9.5.1 Table 9.27) + BaseFrameLength::CLCHSubslot + } else { + DEFAULT_ACCESS_FRAME_MARKER + } + }, + access_field_2: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: if self.ul_get_slot_owner(ts, PhyBlockNum::Block2).is_some() { + BaseFrameLength::ReservedSubslot + } else { + DEFAULT_ACCESS_FRAME_MARKER + } + }, }; - // TODO FIXME: Access field defaults are possibly not great + aach.to_bitbuf(&mut aach_bb); - } + }; TmvUnitdataReq { logical_channel: LogicalChannel::Aach, @@ -1530,4 +1577,101 @@ mod tests { assert!(sched.dltx_queues[ts.t as usize - 1].len() == 1); } + + #[test] + fn test_dl_indicates_reserved_subslots() { + + let mut sched = get_testing_slotter(); + let mut ts = TdmaTime::default(); + + // Add a reservation for TS1, both subslots in the third occurrence of TS1 + sched.ulsched[0][2] = TimeslotSchedule { + ul1: Some(1), + ul2: Some(1), + }; + + // Generate the next 4 frames to see how the reserved subslots are indicated in the BBK block + for i in 0..4 { + let bbk = sched.generate_bbk_block(ts); + let mut aach_buf = bbk.mac_block.clone(); + aach_buf.seek(0); + + // Decode the AACH (which in this test will always be the F1-17 format) + let access_assign = AccessAssign::from_bitbuf(&mut aach_buf) + .expect("Failed to decode AACH block"); + tracing::debug!("Decoded AACH: {:?}", access_assign); + + // Third occurrence should have both slots reserved + let expected_subslot_bfl = if i == 2 { + BaseFrameLength::ReservedSubslot + } else { + DEFAULT_ACCESS_FRAME_MARKER + }; + + match access_assign { + AccessAssign::DownlinkCommonControlUplinkCommonOnly { access_field_1, access_field_2 } => { + assert_eq!( + access_field_1.base_frame_len, expected_subslot_bfl, + "Unexpected base frame length for access field 1 on TS1 occurrence {}", + i + 1 + ); + assert_eq!( + access_field_2.base_frame_len, expected_subslot_bfl, + "Unexpected base frame length for access field 2 on TS1 occurrence {}", + i + 1 + ); + }, + _ => panic!("Expected DownlinkCommonControlUplinkCommonOnly format for TS1"), + } + + // Move on to the next occurrence of TS1 + ts = ts.add_timeslots(4); + } + } + + #[test] + fn test_dl_indicates_clch_opportunities() { + let sched = get_testing_slotter(); + + // Frame 18 + let mut ts = TdmaTime { + t: 1, + f: 18, + m: 1, + h: 0, + }; + + // Generate the next 4 frames to make sure CLCH is correctly indicated + for _ in 0..4 { + + let bbk = sched.generate_bbk_block(ts); + let mut aach_buf = bbk.mac_block.clone(); + aach_buf.seek(0); + + // Decode the AACH (which in this test will always be the frame 18 format) + let access_assign = AccessAssignFr18::from_bitbuf(&mut aach_buf) + .expect("Failed to decode AACH block"); + tracing::debug!("Decoded AACH: {:?}", access_assign); + + // For MN=1, F=18, T=2, SSN1 should be CLCH (when F == 18 and T == 4 - ((M + 1) % 4), otherwise default + let expected_subslot_bfl = if ts.t == 2 { + BaseFrameLength::CLCHSubslot + } else { + DEFAULT_ACCESS_FRAME_MARKER + }; + + match access_assign { + AccessAssignFr18::UplinkCommonOnly { access_field_1, .. } => { + assert_eq!( + access_field_1.base_frame_len, expected_subslot_bfl, + "Unexpected base frame length for access field 1 on frame 18" + ); + }, + _ => panic!("Expected AccessAssignFr18::UplinkCommonOnly format for frame 18"), + } + + ts = ts.add_timeslots(1); + } + } + } diff --git a/crates/tetra-entities/src/umac/umac_ms.rs b/crates/tetra-entities/src/umac/umac_ms.rs index e602fca..6892595 100644 --- a/crates/tetra-entities/src/umac/umac_ms.rs +++ b/crates/tetra-entities/src/umac/umac_ms.rs @@ -568,7 +568,8 @@ impl UmacMs { } }; - pdu.dl_usage.is_traffic() + pdu.dl_is_traffic() + } else { let _pdu = match AccessAssignFr18::from_bitbuf(&mut prim.pdu) { Ok(pdu) => { diff --git a/crates/tetra-pdus/src/lib.rs b/crates/tetra-pdus/src/lib.rs index 894d5a8..537b305 100644 --- a/crates/tetra-pdus/src/lib.rs +++ b/crates/tetra-pdus/src/lib.rs @@ -1,5 +1,4 @@ #![allow(dead_code)] - pub mod cmce; pub mod llc; pub mod mle; diff --git a/crates/tetra-pdus/src/umac/enums/access_code.rs b/crates/tetra-pdus/src/umac/enums/access_code.rs new file mode 100644 index 0000000..63eacc1 --- /dev/null +++ b/crates/tetra-pdus/src/umac/enums/access_code.rs @@ -0,0 +1,46 @@ +use core::fmt; +use std::fmt::Display; + +#[derive(Debug, Clone, Copy)] +pub enum AccessCode { + AccessCodeA, + AccessCodeB, + AccessCodeC, + AccessCodeD +} + +impl TryFrom for AccessCode { + type Error = (); + + fn try_from(value: u64) -> Result { + match value { + 0b00 => Ok(AccessCode::AccessCodeA), + 0b01 => Ok(AccessCode::AccessCodeB), + 0b10 => Ok(AccessCode::AccessCodeC), + 0b11 => Ok(AccessCode::AccessCodeD), + _ => Err(()), + } + } +} + +impl AccessCode { + pub fn into_raw(self) -> u64 { + match self { + AccessCode::AccessCodeA => 0b00, + AccessCode::AccessCodeB => 0b01, + AccessCode::AccessCodeC => 0b10, + AccessCode::AccessCodeD => 0b11, + } + } +} + +impl Display for AccessCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AccessCode::AccessCodeA => write!(f, "Access Code A"), + AccessCode::AccessCodeB => write!(f, "Access Code B"), + AccessCode::AccessCodeC => write!(f, "Access Code C"), + AccessCode::AccessCodeD => write!(f, "Access Code D"), + } + } +} diff --git a/crates/tetra-pdus/src/umac/enums/mod.rs b/crates/tetra-pdus/src/umac/enums/mod.rs index c8f22fd..b25def8 100644 --- a/crates/tetra-pdus/src/umac/enums/mod.rs +++ b/crates/tetra-pdus/src/umac/enums/mod.rs @@ -5,6 +5,6 @@ pub mod mac_pdu_type; pub mod mac_resource_addr_type; pub mod reservation_requirement; pub mod sysinfo_opt_field_flag; - pub mod access_assign_dl_usage; pub mod access_assign_ul_usage; +pub mod access_code; diff --git a/crates/tetra-pdus/src/umac/pdus/access_assign.rs b/crates/tetra-pdus/src/umac/pdus/access_assign.rs index 89c30af..9cfebdb 100644 --- a/crates/tetra-pdus/src/umac/pdus/access_assign.rs +++ b/crates/tetra-pdus/src/umac/pdus/access_assign.rs @@ -1,197 +1,218 @@ use core::fmt; use std::panic; - use tetra_core::{BitBuffer, pdu_parse_error::PduParseErr}; - use crate::umac::enums::{access_assign_dl_usage::AccessAssignDlUsage, access_assign_ul_usage::AccessAssignUlUsage}; - -#[derive(Debug, Clone, Copy)] -pub struct AccessField { - // 2 - pub access_code: u8, - // 4 - pub base_frame_len: u8, -} +pub(crate) use crate::umac::enums::access_code::AccessCode; +pub(crate) use crate::umac::structs::access_field::AccessField; +pub(crate) use crate::umac::structs::base_frame_length::BaseFrameLength; /// Clause 21.4.7.2 ACCESS-ASSIGN -/// TODO FIXME technically not part of this SAP, but part of the MAC #[derive(Debug)] -pub struct AccessAssign { - // 2, kept for debugging purposes - pub _header: u8, - // 6 - pub dl_usage: AccessAssignDlUsage, - pub ul_usage: AccessAssignUlUsage, +pub enum AccessAssign { - // Three valid combinations: - // - Only access_field (applies for both subslots) - // - Both access_field1 and access_field2 - // - None (when dl and ul usage need to be sent) - // pub access_field: Option, - // pub access_field1: Option, - // pub access_field2: Option, - /// Populated when header == 0 - /// Provides access rights on UL subslot 1 - pub f1_af1: Option, - // pub f1_dl_um: Option, - /// Populated when header == 0 - /// Provides access rights on UL subslot 2 - pub f2_af2: Option, + // Header = 00 + // Downlink usage - common control + // Uplink access rights - common only + DownlinkCommonControlUplinkCommonOnly { + access_field_1: AccessField, + access_field_2: AccessField + }, + + // Header = 01 + // Downlink usage - defined by field 1 + // Uplink access rights - common and assigned + DownlinkDefinedUplinkCommonAndAssigned { + downlink_usage_marker: AccessAssignDlUsage, + access_field: AccessField + }, + + // Header = 10 + // Downlink usage - defined by field 1 + // Uplink access rights - assigned only + DownlinkDefinedUplinkAssignedOnly { + downlink_usage_marker: AccessAssignDlUsage, + access_field: AccessField + }, + + // Header = 11 + // Downlink usage - defined by field 1 + // Uplink access rights - defined by field 2 + DownlinkDefinedUplinkDefined { + downlink_usage_marker: AccessAssignDlUsage, + uplink_usage_marker: AccessAssignUlUsage + } - /// Populated when header == 1 or 2 - /// Provides access rights on both UL subslots - pub f2_af: Option, - // pub f2_ul_um: Option, } impl Default for AccessAssign { fn default() -> Self { - AccessAssign { - _header: 0, - dl_usage: AccessAssignDlUsage::CommonControl, - ul_usage: AccessAssignUlUsage::CommonOnly, - f1_af1: None, - // f1_dl_um: None, - f2_af2: None, - f2_af: None, - // f2_ul_um: None + AccessAssign::DownlinkCommonControlUplinkCommonOnly { + access_field_1: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: BaseFrameLength::Subslots4 + }, + access_field_2: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: BaseFrameLength::Subslots4 + } } } } impl AccessAssign { + + pub fn dl_is_traffic(&self) -> bool { + match self { + AccessAssign::DownlinkDefinedUplinkCommonAndAssigned { downlink_usage_marker, .. } | + AccessAssign::DownlinkDefinedUplinkAssignedOnly { downlink_usage_marker, .. } | + AccessAssign::DownlinkDefinedUplinkDefined { downlink_usage_marker, .. } => { + downlink_usage_marker.is_traffic() + }, + _ => false + } + } + pub fn from_bitbuf(buf: &mut BitBuffer) -> Result { - let mut s = AccessAssign { - _header: buf.read_field(2, "_header")? as u8, - ..Default::default() - }; - let field1 = buf.read_field(6, "field1")? as u8; - let field2 = buf.read_field(6, "field2")? as u8; + let header = buf.read_field(2, "_header")?; + let field1 = buf.read_field(6, "field1")?; + let field2 = buf.read_field(6, "field2")?; - match s._header { - 0 => { - // DL common control - // UL access rights - common only - s.dl_usage = AccessAssignDlUsage::CommonControl; - s.ul_usage = AccessAssignUlUsage::CommonOnly; + match header { - s.f1_af1 = Some(AccessField { - access_code: (field1 >> 4) & 0x3, - base_frame_len: field1 & 0xF, - }); - s.f2_af2 = Some(AccessField { - access_code: (field2 >> 4) & 0x3, - base_frame_len: field2 & 0xF, - }); + 0b00 => { + // Downlink usage - common control + // Uplink access rights - common only + Ok(AccessAssign::DownlinkCommonControlUplinkCommonOnly { + access_field_1: field1.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field_1", value: field1 })?, + access_field_2: field2.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field_2", value: field2 })?, + }) } - 1 => { - // DL defined by field1 usage marker - // UL access rights - common and assigned - s.dl_usage = AccessAssignDlUsage::from_usage_marker(field1); - s.ul_usage = AccessAssignUlUsage::CommonAndAssigned; - s.f2_af = Some(AccessField { - access_code: (field2 >> 4) & 0x3, - base_frame_len: field2 & 0xF, - }); + + 0b01 => { + // Downlink usage - defined by field 1 + // Uplink access rights - common and assigned + Ok(AccessAssign::DownlinkDefinedUplinkCommonAndAssigned { + downlink_usage_marker: AccessAssignDlUsage::from_usage_marker(field1 as u8), + access_field: field2.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field", value: field2 })?, + }) } - 2 => { - // DL defined by field1 usage marker - // UL access rights - assigned only - s.dl_usage = AccessAssignDlUsage::from_usage_marker(field1); - s.ul_usage = AccessAssignUlUsage::AssignedOnly; - s.f2_af = Some(AccessField { - access_code: (field2 >> 4) & 0x3, - base_frame_len: field2 & 0xF, - }); + + 0b10 => { + // Downlink usage - defined by field 1 + // Uplink access rights - assigned only + Ok(AccessAssign::DownlinkDefinedUplinkAssignedOnly { + downlink_usage_marker: AccessAssignDlUsage::from_usage_marker(field1 as u8), + access_field: field2.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field", value: field2 })?, + }) } - 3 => { - // DL defined by field1 usage marker - // UL defined by field2 usage marker - s.dl_usage = AccessAssignDlUsage::from_usage_marker(field1); - let ul_usage = AccessAssignUlUsage::from_usage_marker(field2); - s.ul_usage = ul_usage.ok_or(PduParseErr::InvalidValue { - field: "ul_usage", - value: field2 as u64, - })?; + + 0b11 => { + // Downlink usage - defined by field 1 + // Uplink access rights - defined by field 2 + Ok(AccessAssign::DownlinkDefinedUplinkDefined { + downlink_usage_marker: AccessAssignDlUsage::from_usage_marker(field1 as u8), + uplink_usage_marker: AccessAssignUlUsage::from_usage_marker(field2 as u8).unwrap(), + }) } + _ => { - panic!() + panic!("Invalid header value for ACCESS-ASSIGN: {}", header); } } - - Ok(s) } pub fn to_bitbuf(&self, buf: &mut BitBuffer) { - // Safe fallback when a caller sets UL usage to CommonAndAssigned / AssignedOnly - // but forgets to provide field2 (access field). Scheduler should still - // set f2_af explicitly; this just prevents runtime panics. - const DEFAULT_AF: AccessField = AccessField { - access_code: 0, - base_frame_len: 4, - }; - if self.dl_usage == AccessAssignDlUsage::CommonControl && self.ul_usage == AccessAssignUlUsage::CommonOnly { - assert!( - self.f1_af1.is_some() && self.f2_af2.is_some(), - "AccessAssign with CommonControl and CommonOnly must have both access fields defined" - ); - assert!( - self.f2_af.is_none(), - "AccessAssign with CommonControl and CommonOnly must not have f2_af defined" - ); + match self { - let header = 0; - buf.write_bits(header as u64, 2); - buf.write_bits(self.f1_af1.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f1_af1.as_ref().unwrap().base_frame_len as u64, 4); - buf.write_bits(self.f2_af2.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f2_af2.as_ref().unwrap().base_frame_len as u64, 4); - } else if self.ul_usage == AccessAssignUlUsage::CommonAndAssigned { - let header = 1; - buf.write_bits(header as u64, 2); + AccessAssign::DownlinkCommonControlUplinkCommonOnly { + access_field_1, + access_field_2 + } => { - let dl_usage = self.dl_usage.to_usage_marker(); - buf.write_bits(dl_usage as u64, 6); - let af = self.f2_af.unwrap_or(DEFAULT_AF); - buf.write_bits(af.access_code as u64, 2); - buf.write_bits(af.base_frame_len as u64, 4); - } else if self.ul_usage == AccessAssignUlUsage::AssignedOnly { - let header = 2; - buf.write_bits(header as u64, 2); - let dl_usage = self.dl_usage.to_usage_marker(); - buf.write_bits(dl_usage as u64, 6); - let af = self.f2_af.unwrap_or(DEFAULT_AF); - buf.write_bits(af.access_code as u64, 2); - buf.write_bits(af.base_frame_len as u64, 4); - } else { - // Both DL and UL usage given by usage markers - let header = 3; - buf.write_bits(header as u64, 2); + // Header = 00 + buf.write_bits(0b00, 2); - let dl_usage = self.dl_usage.to_usage_marker(); - let ul_usage = self.ul_usage.to_usage_marker().unwrap(); + // Access field 1 + buf.write_bits(access_field_1.into_raw(), 6); - buf.write_bits(dl_usage as u64, 6); - buf.write_bits(ul_usage as u64, 6); + // Access field 2 + buf.write_bits(access_field_2.into_raw(), 6); + + }, + + AccessAssign::DownlinkDefinedUplinkCommonAndAssigned { + downlink_usage_marker, + access_field + } => { + + // Header = 01 + buf.write_bits(0b01, 2); + + // Downlink usage marker + buf.write_bits(downlink_usage_marker.to_usage_marker() as u64, 6); + + // Access field (both subslots) + buf.write_bits(access_field.into_raw(), 6); + + }, + + AccessAssign::DownlinkDefinedUplinkAssignedOnly { + downlink_usage_marker, + access_field + } => { + + // Header = 10 + buf.write_bits(0b10, 2); + + // Downlink usage marker + buf.write_bits(downlink_usage_marker.to_usage_marker() as u64, 6); + + // Access field (both subslots) + buf.write_bits(access_field.into_raw(), 6); + + }, + + AccessAssign::DownlinkDefinedUplinkDefined { + downlink_usage_marker, + uplink_usage_marker + } => { + + // Header = 11 + buf.write_bits(0b11, 2); + + // Downlink usage marker + buf.write_bits(downlink_usage_marker.to_usage_marker() as u64, 6); + + // Uplink usage marker + buf.write_bits(uplink_usage_marker.to_usage_marker().unwrap() as u64, 6); + + } } } } impl fmt::Display for AccessAssign { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "access_assign {{ dl_usage: {}, ul_usage: {}", self.dl_usage, self.ul_usage)?; - if let Some(af) = &self.f2_af { - write!(f, " access_field code: {} base_frame_len {}", af.access_code, af.base_frame_len)?; - }; - if let Some(af1) = &self.f1_af1 { - write!(f, " access_field1 code: {} base_frame_len {}", af1.access_code, af1.base_frame_len)?; - }; - if let Some(af2) = &self.f2_af2 { - write!(f, " access_field2 code: {} base_frame_len {}", af2.access_code, af2.base_frame_len)?; - }; - write!(f, " }}") + match self { + AccessAssign::DownlinkCommonControlUplinkCommonOnly { access_field_1, access_field_2 } => { + write!(f, "AccessAssign {{ DL: Common Ctrl, UL: Common Only, af1: {}, af2: {} }}", access_field_1, access_field_2) + }, + AccessAssign::DownlinkDefinedUplinkCommonAndAssigned { downlink_usage_marker, access_field } => { + write!(f, "AccessAssign {{ DL: {}, UL: Common & Assigned, af: {} }}", downlink_usage_marker, access_field) + }, + AccessAssign::DownlinkDefinedUplinkAssignedOnly { downlink_usage_marker, access_field } => { + write!(f, "AccessAssign {{ DL: {}, UL: Assigned Only, af: {} }}", downlink_usage_marker, access_field) + }, + AccessAssign::DownlinkDefinedUplinkDefined { downlink_usage_marker, uplink_usage_marker } => { + write!(f, "AccessAssign {{ DL: {}, UL: {} }}", downlink_usage_marker, uplink_usage_marker) + } + } } } diff --git a/crates/tetra-pdus/src/umac/pdus/access_assign_fr18.rs b/crates/tetra-pdus/src/umac/pdus/access_assign_fr18.rs index 534fd10..fb11e0f 100644 --- a/crates/tetra-pdus/src/umac/pdus/access_assign_fr18.rs +++ b/crates/tetra-pdus/src/umac/pdus/access_assign_fr18.rs @@ -1,171 +1,199 @@ use core::fmt; use std::panic; - use tetra_core::{BitBuffer, pdu_parse_error::PduParseErr}; - use crate::umac::{enums::access_assign_ul_usage::AccessAssignUlUsage, pdus::access_assign::AccessField}; +use crate::umac::pdus::access_assign::{AccessCode, BaseFrameLength}; /// Clause 21.4.7.2 ACCESS-ASSIGN -/// TODO FIXME technically not part of this SAP, but part of the MAC #[derive(Debug)] -pub struct AccessAssignFr18 { - // 2, kept for debugging purposes - pub _header: u8, - // 6 - // pub dl_usage: AccessAssignDlUsage, - pub ul_usage: AccessAssignUlUsage, +pub enum AccessAssignFr18 { - /// Populated when header == 0, 1 or 2 - /// Provides access rights on UL subslot 1 - pub f1_af1: Option, + // Header = 00 + // Uplink access rights - common only + UplinkCommonOnly { + access_field_1: AccessField, + access_field_2: AccessField + }, - /// Populated when header == 3 - pub f1_traf_um: Option, + // Header = 01 + // Uplink access rights - common and assigned + UplinkCommonAndAssigned { + access_field_1: AccessField, + access_field_2: AccessField + }, - /// Populated when header == 0, 1 or 2 - /// Provides access rights on UL subslot 2 - pub f2_af2: Option, + // Header = 10 + // Uplink access rights - assigned only + UplinkAssignedOnly { + access_field_1: AccessField, + access_field_2: AccessField + }, + + // Header = 11 + // Uplink access rights - common and assigned, but with traffic usage marker (UMt) instead of AF1 + UplinkCommonAndAssignedTraffic { + uplink_usage_marker: AccessAssignUlUsage, + access_field: AccessField + } - /// Populated when header == 3 - /// Provides access rights on both UL subslots - pub f2_af: Option, - // pub f2_ul_um: Option, } impl Default for AccessAssignFr18 { fn default() -> Self { - AccessAssignFr18 { - _header: 0, - - ul_usage: AccessAssignUlUsage::CommonOnly, - - f1_af1: None, - f1_traf_um: None, - f2_af2: None, - f2_af: None, + AccessAssignFr18::UplinkCommonOnly { + access_field_1: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: BaseFrameLength::ReservedSubslot, + }, + access_field_2: AccessField { + access_code: AccessCode::AccessCodeA, + base_frame_len: BaseFrameLength::ReservedSubslot, + }, } } } impl AccessAssignFr18 { + pub fn from_bitbuf(buf: &mut BitBuffer) -> Result { - let mut s = AccessAssignFr18 { - _header: buf.read_field(2, "_header")? as u8, - ..Default::default() - }; - let field1 = buf.read_field(6, "field1")? as u8; - let field2 = buf.read_field(6, "field2")? as u8; + let header = buf.read_field(2, "_header")?; + let field1 = buf.read_field(6, "field1")? ; + let field2 = buf.read_field(6, "field2")?; - match s._header { - 0 => { - s.ul_usage = AccessAssignUlUsage::CommonOnly; - s.f1_af1 = Some(AccessField { - access_code: (field1 >> 4) & 0x3, - base_frame_len: field1 & 0xF, - }); - s.f2_af2 = Some(AccessField { - access_code: (field2 >> 4) & 0x3, - base_frame_len: field2 & 0xF, - }); - } - 1 => { - s.ul_usage = AccessAssignUlUsage::CommonAndAssigned; - s.f1_af1 = Some(AccessField { - access_code: (field1 >> 4) & 0x3, - base_frame_len: field1 & 0xF, - }); - s.f2_af2 = Some(AccessField { - access_code: (field2 >> 4) & 0x3, - base_frame_len: field2 & 0xF, - }); - } - 2 => { - s.ul_usage = AccessAssignUlUsage::AssignedOnly; - s.f1_af1 = Some(AccessField { - access_code: (field1 >> 4) & 0x3, - base_frame_len: field1 & 0xF, - }); - s.f2_af2 = Some(AccessField { - access_code: (field2 >> 4) & 0x3, - base_frame_len: field2 & 0xF, - }); - } - 3 => { - // UL usage counts as CommonAndAssigned, but with traffic marker - let ul_usage = AccessAssignUlUsage::from_usage_marker(field1); - s.ul_usage = ul_usage.ok_or(PduParseErr::InvalidValue { - field: "ul_usage", - value: field1 as u64, - })?; - assert!(ul_usage.unwrap().is_traffic()); + match header { - s.f2_af = Some(AccessField { - access_code: (field2 >> 4) & 0x3, - base_frame_len: field2 & 0xF, - }); + 0b00 => { + // Uplink access rights - common only + Ok(AccessAssignFr18::UplinkCommonOnly { + access_field_1: field1.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field_1", value: field1 })?, + access_field_2: field2.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field_2", value: field2 })?, + }) } + + 0b01 => { + // Uplink access rights - common and assigned + Ok(AccessAssignFr18::UplinkCommonAndAssigned { + access_field_1: field1.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field_1", value: field1 })?, + access_field_2: field2.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field_2", value: field2 })?, + }) + } + + 0b10 => { + // Uplink access rights - assigned only + Ok(AccessAssignFr18::UplinkAssignedOnly { + access_field_1: field1.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field_1", value: field1 })?, + access_field_2: field2.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field_2", value: field2 })?, + }) + } + + 0b11 => { + // Uplink access rights - common and assigned, but with traffic usage marker (UMt) instead of AF1 + Ok(AccessAssignFr18::UplinkCommonAndAssignedTraffic { + uplink_usage_marker: AccessAssignUlUsage::from_usage_marker(field1 as u8) + .ok_or(PduParseErr::InvalidValue { field: "uplink_usage_marker", value: field1 })?, + access_field: field2.try_into() + .map_err(|_| PduParseErr::InvalidValue { field: "access_field", value: field2 })?, + }) + } + _ => { - panic!() + panic!("Invalid header value for Frame 18 ACCESS-ASSIGN: {}", header); } } - - Ok(s) } pub fn to_bitbuf(&self, buf: &mut BitBuffer) { - if self.ul_usage == AccessAssignUlUsage::CommonOnly { - let header = 0; - buf.write_bits(header as u64, 2); - buf.write_bits(self.f1_af1.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f1_af1.as_ref().unwrap().base_frame_len as u64, 4); - buf.write_bits(self.f2_af2.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f2_af2.as_ref().unwrap().base_frame_len as u64, 4); - assert!(self.f2_af.is_none()); - } else if self.ul_usage == AccessAssignUlUsage::CommonAndAssigned { - let header = 1; - buf.write_bits(header as u64, 2); - buf.write_bits(self.f1_af1.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f1_af1.as_ref().unwrap().base_frame_len as u64, 4); - buf.write_bits(self.f2_af2.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f2_af2.as_ref().unwrap().base_frame_len as u64, 4); - assert!(self.f2_af.is_none()); - } else if self.ul_usage == AccessAssignUlUsage::AssignedOnly { - let header = 2; - buf.write_bits(header as u64, 2); - buf.write_bits(self.f1_af1.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f1_af1.as_ref().unwrap().base_frame_len as u64, 4); - buf.write_bits(self.f2_af2.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f2_af2.as_ref().unwrap().base_frame_len as u64, 4); - assert!(self.f2_af.is_none()); - } else if self.ul_usage.is_traffic() { - // UL usage counts as common and assigned, but with traffic marker - let header = 3; - buf.write_bits(header as u64, 2); - let ul_usage = self.ul_usage.to_usage_marker().unwrap(); - buf.write_bits(ul_usage as u64, 6); - buf.write_bits(self.f2_af.as_ref().unwrap().access_code as u64, 2); - buf.write_bits(self.f2_af.as_ref().unwrap().base_frame_len as u64, 4); - assert!(self.f1_af1.is_none()); - assert!(self.f2_af2.is_none()); - } else { - unimplemented!("AccessAssign::to_bitbuf_fr18 for other cases"); + + match self { + + AccessAssignFr18::UplinkCommonOnly { + access_field_1, + access_field_2 + } => { + + // Header = 00 + buf.write_bits(0b00, 2); + + // Access field 1 + buf.write_bits(access_field_1.into_raw(), 6); + + // Access field 2 + buf.write_bits(access_field_2.into_raw(), 6); + + }, + + AccessAssignFr18::UplinkCommonAndAssigned { + access_field_1, + access_field_2 + } => { + + // Header = 01 + buf.write_bits(0b01, 2); + + // Access field 1 + buf.write_bits(access_field_1.into_raw(), 6); + + // Access field 2 + buf.write_bits(access_field_2.into_raw(), 6); + + }, + + AccessAssignFr18::UplinkAssignedOnly { + access_field_1, + access_field_2 + } => { + + // Header = 10 + buf.write_bits(0b10, 2); + + // Access field 1 + buf.write_bits(access_field_1.into_raw(), 6); + + // Access field 2 + buf.write_bits(access_field_2.into_raw(), 6); + + }, + + AccessAssignFr18::UplinkCommonAndAssignedTraffic { + uplink_usage_marker, + access_field + } => { + + // Header = 11 + buf.write_bits(0b11, 2); + + // Uplink usage marker + buf.write_bits(uplink_usage_marker.to_usage_marker().unwrap() as u64, 6); + + // Access field (both subslots) + buf.write_bits(access_field.into_raw(), 6);} + } } } impl fmt::Display for AccessAssignFr18 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "access_assign {{ ul_usage: {}", self.ul_usage)?; - if let Some(af) = &self.f2_af { - write!(f, " AF {}/{}", af.access_code, af.base_frame_len)?; - }; - if let Some(af) = &self.f1_af1 { - write!(f, " AF1 {}/{}", af.access_code, af.base_frame_len)?; - }; - if let Some(af) = &self.f2_af2 { - write!(f, " AF2 {}/{}", af.access_code, af.base_frame_len)?; - }; - write!(f, " }}") + match self { + AccessAssignFr18::UplinkCommonOnly { access_field_1, access_field_2 } => { + write!(f, "AccessAssignFr18 {{ UplinkCommonOnly: af1: {}, af2: {} }}", access_field_1, access_field_2) + }, + AccessAssignFr18::UplinkCommonAndAssigned { access_field_1, access_field_2 } => { + write!(f, "AccessAssignFr18 {{ UplinkCommonAndAssigned: af1: {}, af2: {} }}", access_field_1, access_field_2) + }, + AccessAssignFr18::UplinkAssignedOnly { access_field_1, access_field_2 } => { + write!(f, "AccessAssignFr18 {{ UplinkAssignedOnly: af1: {}, af2: {} }}", access_field_1, access_field_2) + }, + AccessAssignFr18::UplinkCommonAndAssignedTraffic { uplink_usage_marker, access_field } => { + write!(f, "AccessAssignFr18 {{ UplinkCommonAndAssignedTraffic: uum: {}, af: {} }}", uplink_usage_marker, access_field) + } + } } } diff --git a/crates/tetra-pdus/src/umac/structs/access_field.rs b/crates/tetra-pdus/src/umac/structs/access_field.rs new file mode 100644 index 0000000..71a8a6a --- /dev/null +++ b/crates/tetra-pdus/src/umac/structs/access_field.rs @@ -0,0 +1,33 @@ +use core::fmt; +use crate::umac::enums::access_code::AccessCode; +use crate::umac::structs::base_frame_length::BaseFrameLength; + +#[derive(Debug, Clone, Copy)] +pub struct AccessField { + pub access_code: AccessCode, + pub base_frame_len: BaseFrameLength, +} + +impl TryFrom for AccessField { + type Error = (); + + fn try_from(value: u64) -> Result { + Ok(AccessField { + access_code: ((value >> 4) & 0b11).try_into()?, + base_frame_len: (value & 0b1111).try_into()?, + }) + } +} + +impl AccessField { + pub fn into_raw(self) -> u64 { + (self.access_code.into_raw() << 4) | self.base_frame_len.into_raw() + } +} + +impl fmt::Display for AccessField { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Access Code: {}, Base Frame Length: {}", self.access_code, self.base_frame_len) + } +} + diff --git a/crates/tetra-pdus/src/umac/structs/base_frame_length.rs b/crates/tetra-pdus/src/umac/structs/base_frame_length.rs new file mode 100644 index 0000000..0da950c --- /dev/null +++ b/crates/tetra-pdus/src/umac/structs/base_frame_length.rs @@ -0,0 +1,100 @@ +use core::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BaseFrameLength { + + /// Essentially "already assigned" - Access Code is meaningless in this Access Field + ReservedSubslot, + + /// CLCH opportunity - Access Code is meaningless in this Access Field (common access for linearisation only) + CLCHSubslot, + + /// Ongoing Frame, a continuation of the ongoing access frame + OngoingFrame, + + // The following options indicate the start of a new access frame + Subslots1, + Subslots2, + Subslots3, + Subslots4, + Subslots5, + Subslots6, + Subslots8, + Subslots10, + Subslots12, + Subslots16, + Subslots20, + Subslots24, + Subslots32 +} + +impl TryFrom for BaseFrameLength { + type Error = (); + fn try_from(value: u64) -> Result { + match value { + 0b0000 => Ok(BaseFrameLength::ReservedSubslot), + 0b0001 => Ok(BaseFrameLength::CLCHSubslot), + 0b0010 => Ok(BaseFrameLength::OngoingFrame), + 0b0011 => Ok(BaseFrameLength::Subslots1), + 0b0100 => Ok(BaseFrameLength::Subslots2), + 0b0101 => Ok(BaseFrameLength::Subslots3), + 0b0110 => Ok(BaseFrameLength::Subslots4), + 0b0111 => Ok(BaseFrameLength::Subslots5), + 0b1000 => Ok(BaseFrameLength::Subslots6), + 0b1001 => Ok(BaseFrameLength::Subslots8), + 0b1010 => Ok(BaseFrameLength::Subslots10), + 0b1011 => Ok(BaseFrameLength::Subslots12), + 0b1100 => Ok(BaseFrameLength::Subslots16), + 0b1101 => Ok(BaseFrameLength::Subslots20), + 0b1110 => Ok(BaseFrameLength::Subslots24), + 0b1111 => Ok(BaseFrameLength::Subslots32), + _ => Err(()), + } + } +} + +impl BaseFrameLength { + pub fn into_raw(self) -> u64 { + match self { + BaseFrameLength::ReservedSubslot => 0b0000, + BaseFrameLength::CLCHSubslot => 0b0001, + BaseFrameLength::OngoingFrame => 0b0010, + BaseFrameLength::Subslots1 => 0b0011, + BaseFrameLength::Subslots2 => 0b0100, + BaseFrameLength::Subslots3 => 0b0101, + BaseFrameLength::Subslots4 => 0b0110, + BaseFrameLength::Subslots5 => 0b0111, + BaseFrameLength::Subslots6 => 0b1000, + BaseFrameLength::Subslots8 => 0b1001, + BaseFrameLength::Subslots10 => 0b1010, + BaseFrameLength::Subslots12 => 0b1011, + BaseFrameLength::Subslots16 => 0b1100, + BaseFrameLength::Subslots20 => 0b1101, + BaseFrameLength::Subslots24 => 0b1110, + BaseFrameLength::Subslots32 => 0b1111 + } + } +} + +impl fmt::Display for BaseFrameLength { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + BaseFrameLength::ReservedSubslot => write!(f, "Reserved Subslot"), + BaseFrameLength::CLCHSubslot => write!(f, "CLCH Opportunity"), + BaseFrameLength::OngoingFrame => write!(f, "Ongoing Frame"), + BaseFrameLength::Subslots1 => write!(f, "1 Subslot"), + BaseFrameLength::Subslots2 => write!(f, "2 Subslots"), + BaseFrameLength::Subslots3 => write!(f, "3 Subslots"), + BaseFrameLength::Subslots4 => write!(f, "4 Subslots"), + BaseFrameLength::Subslots5 => write!(f, "5 Subslots"), + BaseFrameLength::Subslots6 => write!(f, "6 Subslots"), + BaseFrameLength::Subslots8 => write!(f, "8 Subslots"), + BaseFrameLength::Subslots10 => write!(f, "10 Subslots"), + BaseFrameLength::Subslots12 => write!(f, "12 Subslots"), + BaseFrameLength::Subslots16 => write!(f, "16 Subslots"), + BaseFrameLength::Subslots20 => write!(f, "20 Subslots"), + BaseFrameLength::Subslots24 => write!(f, "24 Subslots"), + BaseFrameLength::Subslots32 => write!(f, "32 Subslots") + } + } +} \ No newline at end of file diff --git a/crates/tetra-pdus/src/umac/structs/mod.rs b/crates/tetra-pdus/src/umac/structs/mod.rs index 3a29132..2a350aa 100644 --- a/crates/tetra-pdus/src/umac/structs/mod.rs +++ b/crates/tetra-pdus/src/umac/structs/mod.rs @@ -1 +1,3 @@ pub mod umac_circuit; +pub mod access_field; +pub mod base_frame_length; diff --git a/crates/tetra-saps/src/tmv/enums/logical_chans.rs b/crates/tetra-saps/src/tmv/enums/logical_chans.rs index 8b1c4b3..f66183b 100644 --- a/crates/tetra-saps/src/tmv/enums/logical_chans.rs +++ b/crates/tetra-saps/src/tmv/enums/logical_chans.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// Logical channels as defined in the standard #[derive(Debug, Clone, Copy, PartialEq)] pub enum LogicalChannel { @@ -94,3 +96,24 @@ impl LogicalChannel { } } } + +impl fmt::Display for LogicalChannel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let name = match self { + LogicalChannel::Aach => "AACH", + LogicalChannel::SchHd => "SCH (half slot, downlink)", + LogicalChannel::SchF => "SCH (full slot)", + LogicalChannel::Stch => "STCH", + LogicalChannel::SchHu => "SCH (half slot, uplink)", + LogicalChannel::TchS => "TCH (Voice)", + LogicalChannel::Tch24 => "TCH (2.4 kbps)", + LogicalChannel::Tch48 => "TCH (4.8 kbps)", + LogicalChannel::Tch72 => "TCH (7.2 kbps)", + LogicalChannel::Bsch => "BSCH", + LogicalChannel::Bnch => "BNCH", + LogicalChannel::Blch => "BLCH", + LogicalChannel::Clch => "CLCH", + }; + write!(f, "{}", name) + } +}