Improvements/Refactoring around AACH (#82)

* Add: Refactor AACH behaviour.

Main changes here are to split a lot of the integer typed fields out into enums and structs, and make invalid state of the AccessAssign PDU unrepresentable through typing.

Studied the BaseFrameLength behaviour and standardised on a sensible set of defaults throughout for that.

Other changes include advertising CLCH opportunities via BaseFrameLength::CLCHSubslot where expected and a relaxing of how often SYNC bursts are sent. Using Air Interface monitoring on a few radios indicated that SYNC bursts appearing in non-mandated slots caused decode errors and radio downlink counter decrements. 

* Fix: Indicate reserved uplink subslots in ACCESS-ASSIGN.

This caused issues with multi-fragment messages. Seems like some radios do indeed check the AACH during a multi-slot grant and if they don't see Reserved Subslots (00) abort and re-attempt random-access again.

* Refactor: One place to set the default access frame marker in bs_sched

* Add: Tests for reserved & CLCH sched subslots
This commit is contained in:
Ret Folf
2026-07-09 23:35:48 +02:00
committed by wbokslag
parent 1a1c8eb739
commit 5346d66186
11 changed files with 746 additions and 349 deletions
@@ -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);
}
}
}
+2 -1
View File
@@ -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) => {
-1
View File
@@ -1,5 +1,4 @@
#![allow(dead_code)]
pub mod cmce;
pub mod llc;
pub mod mle;
@@ -0,0 +1,46 @@
use core::fmt;
use std::fmt::Display;
#[derive(Debug, Clone, Copy)]
pub enum AccessCode {
AccessCodeA,
AccessCodeB,
AccessCodeC,
AccessCodeD
}
impl TryFrom<u64> for AccessCode {
type Error = ();
fn try_from(value: u64) -> Result<Self, Self::Error> {
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"),
}
}
}
+1 -1
View File
@@ -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;
+172 -151
View File
@@ -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<AccessField>,
// pub access_field1: Option<AccessField>,
// pub access_field2: Option<AccessField>,
/// Populated when header == 0
/// Provides access rights on UL subslot 1
pub f1_af1: Option<AccessField>,
// pub f1_dl_um: Option<AccessAssignDlUsage>,
/// Populated when header == 0
/// Provides access rights on UL subslot 2
pub f2_af2: Option<AccessField>,
// 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<AccessField>,
// pub f2_ul_um: Option<AccessAssignUlUsage>,
}
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<Self, PduParseErr> {
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)
}
}
}
}
@@ -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<AccessField>,
// 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<AccessAssignUlUsage>,
// 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<AccessField>,
// 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<AccessField>,
// pub f2_ul_um: Option<AccessAssignUlUsage>,
}
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<Self, PduParseErr> {
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)
}
}
}
}
@@ -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<u64> for AccessField {
type Error = ();
fn try_from(value: u64) -> Result<Self, Self::Error> {
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)
}
}
@@ -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<u64> for BaseFrameLength {
type Error = ();
fn try_from(value: u64) -> Result<Self, Self::Error> {
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")
}
}
}
@@ -1 +1,3 @@
pub mod umac_circuit;
pub mod access_field;
pub mod base_frame_length;
@@ -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)
}
}