From d645f74c4fb15de6a2c8b41eab0c19d982e5f9de Mon Sep 17 00:00:00 2001 From: misadeks Date: Tue, 24 Feb 2026 16:25:54 +0100 Subject: [PATCH] implemented prototype neighbor cell broadcast --- crates/tetra-config/src/stack_config.rs | 125 +++++++++++ crates/tetra-config/src/toml_config.rs | 105 ++++++++- crates/tetra-entities/src/mle/mle_bs_ms.rs | 45 +++- crates/tetra-pdus/src/mle/fields/mod.rs | 1 + .../neighbour_cell_information_for_ca.rs | 202 ++++++++++++++++++ .../src/mle/pdus/d_nwrk_broadcast.rs | 62 ++++-- example_config/config.toml | 18 ++ 7 files changed, 540 insertions(+), 18 deletions(-) create mode 100644 crates/tetra-pdus/src/mle/fields/neighbour_cell_information_for_ca.rs diff --git a/crates/tetra-config/src/stack_config.rs b/crates/tetra-config/src/stack_config.rs index 4e9acf1..2e5c65a 100644 --- a/crates/tetra-config/src/stack_config.rs +++ b/crates/tetra-config/src/stack_config.rs @@ -60,6 +60,67 @@ pub struct CfgNetInfo { pub mnc: u16, } +#[derive(Debug, Clone, Default, Deserialize)] +pub struct CfgBsServiceDetails { + #[serde(default)] + pub registration: bool, + #[serde(default)] + pub deregistration: bool, + #[serde(default)] + pub priority_cell: bool, + #[serde(default)] + pub no_minimum_mode: bool, + #[serde(default)] + pub migration: bool, + #[serde(default)] + pub system_wide_services: bool, + #[serde(default)] + pub voice_service: bool, + #[serde(default)] + pub circuit_mode_data_service: bool, + #[serde(default)] + pub sndcp_service: bool, + #[serde(default)] + pub aie_service: bool, + #[serde(default)] + pub advanced_link: bool, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CfgNeighborCellCa { + /// Type1, 5 bits + pub cell_identifier_ca: u8, + /// Type1, 2 bits + pub cell_reselection_types_supported: u8, + /// Type1, 1 bit + pub neighbor_cell_synchronized: bool, + /// Type1, 2 bits + pub cell_load_ca: u8, + /// Type1, 12 bits + pub main_carrier_number: u16, + + /// Type2, 10 bits + pub main_carrier_number_extension: Option, + /// Type2, 10 bits + pub mcc: Option, + /// Type2, 14 bits + pub mnc: Option, + /// Type2, 14 bits + pub location_area: Option, + /// Type2, 3 bits + pub maximum_ms_transmit_power: Option, + /// Type2, 4 bits + pub minimum_rx_access_level: Option, + /// Type2, 16 bits + pub subscriber_class: Option, + /// Type2, 12 bits + pub bs_service_details: Option, + /// Type2, 5 bits + pub timeshare_cell_information_or_security_parameters: Option, + /// Type2, 6 bits + pub tdma_frame_offset: Option, +} + #[derive(Debug, Clone, Deserialize)] pub struct CfgCellInfo { // 2 bits, from 18.4.2.1 D-MLE-SYNC @@ -137,6 +198,10 @@ pub struct CfgCellInfo { pub u_plane_dtx: bool, #[serde(default)] pub frame_18_ext: bool, + + /// Neighbour cell list for D-NWRK-BROADCAST (CA) + #[serde(default)] + pub neighbor_cells_ca: Vec, } impl Default for CfgCellInfo { @@ -172,6 +237,8 @@ impl Default for CfgCellInfo { ts_reserved_frames: 0, u_plane_dtx: false, frame_18_ext: false, + + neighbor_cells_ca: Vec::new(), } } } @@ -288,6 +355,64 @@ impl StackConfig { }; } + if self.cell.neighbor_cells_ca.len() > 7 { + return Err("cell_info.neighbor_cells_ca supports at most 7 entries"); + } + for cell in &self.cell.neighbor_cells_ca { + if cell.cell_identifier_ca > 0x1F { + return Err("cell_info.neighbor_cells_ca.cell_identifier_ca must be <= 31"); + } + if cell.cell_reselection_types_supported > 0x3 { + return Err("cell_info.neighbor_cells_ca.cell_reselection_types_supported must be <= 3"); + } + if cell.cell_load_ca > 0x3 { + return Err("cell_info.neighbor_cells_ca.cell_load_ca must be <= 3"); + } + if cell.main_carrier_number > 0x0FFF { + return Err("cell_info.neighbor_cells_ca.main_carrier_number must be <= 4095"); + } + if let Some(v) = cell.main_carrier_number_extension { + if v > 0x03FF { + return Err("cell_info.neighbor_cells_ca.main_carrier_number_extension must be <= 1023"); + } + } + if let Some(v) = cell.mcc { + if v > 0x03FF { + return Err("cell_info.neighbor_cells_ca.mcc must be <= 1023"); + } + } + if let Some(v) = cell.mnc { + if v > 0x3FFF { + return Err("cell_info.neighbor_cells_ca.mnc must be <= 16383"); + } + } + if let Some(v) = cell.location_area { + if v > 0x3FFF { + return Err("cell_info.neighbor_cells_ca.location_area must be <= 16383"); + } + } + if let Some(v) = cell.maximum_ms_transmit_power { + if v > 0x7 { + return Err("cell_info.neighbor_cells_ca.maximum_ms_transmit_power must be <= 7"); + } + } + if let Some(v) = cell.minimum_rx_access_level { + if v > 0xF { + return Err("cell_info.neighbor_cells_ca.minimum_rx_access_level must be <= 15"); + } + } + if let Some(v) = cell.timeshare_cell_information_or_security_parameters { + if v > 0x1F { + return Err("cell_info.neighbor_cells_ca.timeshare_cell_information_or_security_parameters must be <= 31"); + } + } + if let Some(v) = cell.tdma_frame_offset { + if v > 0x3F { + return Err("cell_info.neighbor_cells_ca.tdma_frame_offset must be <= 63"); + } + } + } + Ok(()) } } diff --git a/crates/tetra-config/src/toml_config.rs b/crates/tetra-config/src/toml_config.rs index 1955aa4..0d1e611 100644 --- a/crates/tetra-config/src/toml_config.rs +++ b/crates/tetra-config/src/toml_config.rs @@ -8,7 +8,18 @@ use toml::Value; use super::stack_config_brew::{CfgBrewDto, apply_brew_patch}; -use super::stack_config::{CfgCellInfo, CfgNetInfo, CfgPhyIo, PhyBackend, SharedConfig, StackConfig, StackMode, StackState}; +use super::stack_config::{ + CfgBsServiceDetails, + CfgCellInfo, + CfgNeighborCellCa, + CfgNetInfo, + CfgPhyIo, + PhyBackend, + SharedConfig, + StackConfig, + StackMode, + StackState, +}; use super::stack_config_soapy::{CfgSoapySdr, LimeSdrCfg, SXceiverCfg, UsrpB2xxCfg}; /// Build `SharedConfig` from a TOML configuration file @@ -237,6 +248,9 @@ fn apply_cell_info_patch(dst: &mut CfgCellInfo, ci: CellInfoDto) { if let Some(v) = ci.frame_18_ext { dst.frame_18_ext = v; } + if let Some(v) = ci.neighbor_cells_ca { + dst.neighbor_cells_ca = v.into_iter().map(Into::into).collect(); + } } fn sorted_keys(map: &HashMap) -> Vec<&str> { @@ -382,6 +396,8 @@ struct CellInfoDto { pub u_plane_dtx: Option, pub frame_18_ext: Option, + pub neighbor_cells_ca: Option>, + #[serde(flatten)] extra: HashMap, } @@ -393,3 +409,90 @@ struct StackStatePatch { #[serde(flatten)] extra: HashMap, } + +#[derive(Deserialize)] +struct NeighborCellCaDto { + pub cell_identifier_ca: u8, + pub cell_reselection_types_supported: u8, + pub neighbor_cell_synchronized: bool, + pub cell_load_ca: u8, + pub main_carrier_number: u16, + + pub main_carrier_number_extension: Option, + pub mcc: Option, + pub mnc: Option, + pub location_area: Option, + pub maximum_ms_transmit_power: Option, + pub minimum_rx_access_level: Option, + pub subscriber_class: Option, + pub bs_service_details: Option, + pub timeshare_cell_information_or_security_parameters: Option, + pub tdma_frame_offset: Option, +} + +#[derive(Default, Deserialize)] +struct BsServiceDetailsDto { + #[serde(default)] + pub registration: bool, + #[serde(default)] + pub deregistration: bool, + #[serde(default)] + pub priority_cell: bool, + #[serde(default)] + pub no_minimum_mode: bool, + #[serde(default)] + pub migration: bool, + #[serde(default)] + pub system_wide_services: bool, + #[serde(default)] + pub voice_service: bool, + #[serde(default)] + pub circuit_mode_data_service: bool, + #[serde(default)] + pub sndcp_service: bool, + #[serde(default)] + pub aie_service: bool, + #[serde(default)] + pub advanced_link: bool, +} + +impl From for CfgNeighborCellCa { + fn from(dto: NeighborCellCaDto) -> Self { + Self { + cell_identifier_ca: dto.cell_identifier_ca, + cell_reselection_types_supported: dto.cell_reselection_types_supported, + neighbor_cell_synchronized: dto.neighbor_cell_synchronized, + cell_load_ca: dto.cell_load_ca, + main_carrier_number: dto.main_carrier_number, + main_carrier_number_extension: dto.main_carrier_number_extension, + mcc: dto.mcc, + mnc: dto.mnc, + location_area: dto.location_area, + maximum_ms_transmit_power: dto.maximum_ms_transmit_power, + minimum_rx_access_level: dto.minimum_rx_access_level, + subscriber_class: dto.subscriber_class, + bs_service_details: dto.bs_service_details.map(Into::into), + timeshare_cell_information_or_security_parameters: dto + .timeshare_cell_information_or_security_parameters, + tdma_frame_offset: dto.tdma_frame_offset, + } + } +} + +impl From for CfgBsServiceDetails { + fn from(dto: BsServiceDetailsDto) -> Self { + Self { + registration: dto.registration, + deregistration: dto.deregistration, + priority_cell: dto.priority_cell, + no_minimum_mode: dto.no_minimum_mode, + migration: dto.migration, + system_wide_services: dto.system_wide_services, + voice_service: dto.voice_service, + circuit_mode_data_service: dto.circuit_mode_data_service, + sndcp_service: dto.sndcp_service, + aie_service: dto.aie_service, + advanced_link: dto.advanced_link, + } + } +} diff --git a/crates/tetra-entities/src/mle/mle_bs_ms.rs b/crates/tetra-entities/src/mle/mle_bs_ms.rs index 3825b84..e102ac4 100644 --- a/crates/tetra-entities/src/mle/mle_bs_ms.rs +++ b/crates/tetra-entities/src/mle/mle_bs_ms.rs @@ -12,6 +12,8 @@ use tetra_saps::{SapMsg, SapMsgInner}; use tetra_pdus::mle::enums::mle_pdu_type_dl::MlePduTypeDl; use tetra_pdus::mle::enums::mle_protocol_discriminator::MleProtocolDiscriminator; +use tetra_pdus::mle::fields::bs_service_details::BsServiceDetails; +use tetra_pdus::mle::fields::neighbour_cell_information_for_ca::NeighbourCellInformationForCa; use tetra_pdus::mle::pdus::d_mle_sync::DMleSync; use tetra_pdus::mle::pdus::d_mle_sysinfo::DMleSysinfo; use tetra_pdus::mle::pdus::d_nwrk_broadcast::DNwrkBroadcast; @@ -627,13 +629,50 @@ impl Mle { let tetra_network_time = Self::encode_tetra_network_time(local_now); let cell_load_ca = self.config.state_read().cell_load_ca & 0x3; + let neighbour_cells: Vec = self + .config + .config() + .cell + .neighbor_cells_ca + .iter() + .map(|cfg| NeighbourCellInformationForCa { + cell_identifier_ca: cfg.cell_identifier_ca, + cell_reselection_types_supported: cfg.cell_reselection_types_supported, + neighbour_cell_synchronized: cfg.neighbor_cell_synchronized, + cell_load_ca: cfg.cell_load_ca, + main_carrier_number: cfg.main_carrier_number, + main_carrier_number_extension: cfg.main_carrier_number_extension, + mcc: cfg.mcc, + mnc: cfg.mnc, + location_area: cfg.location_area, + maximum_ms_transmit_power: cfg.maximum_ms_transmit_power, + minimum_rx_access_level: cfg.minimum_rx_access_level, + subscriber_class: cfg.subscriber_class, + bs_service_details: cfg.bs_service_details.as_ref().map(|bs| BsServiceDetails { + registration: bs.registration, + deregistration: bs.deregistration, + priority_cell: bs.priority_cell, + no_minimum_mode: bs.no_minimum_mode, + migration: bs.migration, + system_wide_services: bs.system_wide_services, + voice_service: bs.voice_service, + circuit_mode_data_service: bs.circuit_mode_data_service, + sndcp_service: bs.sndcp_service, + aie_service: bs.aie_service, + advanced_link: bs.advanced_link, + }), + timeshare_cell_information_or_security_parameters: cfg + .timeshare_cell_information_or_security_parameters, + tdma_frame_offset: cfg.tdma_frame_offset, + }) + .collect(); let pdu = DNwrkBroadcast { cell_re_select_parameters: 0, cell_load_ca, tetra_network_time: Some(tetra_network_time), - // Explicitly signal "no neighbour cell information" (P-bit set, value 0). - number_of_ca_neighbour_cells: Some(0), - neighbour_cell_information_for_ca: None, + // Explicitly signal "no neighbour cell information" when empty. + number_of_ca_neighbour_cells: Some(neighbour_cells.len() as u8), + neighbour_cell_information_for_ca: neighbour_cells, }; tracing::debug!("DNwrkBroadcast {:?}", &pdu); diff --git a/crates/tetra-pdus/src/mle/fields/mod.rs b/crates/tetra-pdus/src/mle/fields/mod.rs index b4ccb0c..1a0c4e9 100644 --- a/crates/tetra-pdus/src/mle/fields/mod.rs +++ b/crates/tetra-pdus/src/mle/fields/mod.rs @@ -1 +1,2 @@ pub mod bs_service_details; +pub mod neighbour_cell_information_for_ca; diff --git a/crates/tetra-pdus/src/mle/fields/neighbour_cell_information_for_ca.rs b/crates/tetra-pdus/src/mle/fields/neighbour_cell_information_for_ca.rs new file mode 100644 index 0000000..70cbf4c --- /dev/null +++ b/crates/tetra-pdus/src/mle/fields/neighbour_cell_information_for_ca.rs @@ -0,0 +1,202 @@ +use core::fmt; + +use tetra_core::typed_pdu_fields::typed; +use tetra_core::{BitBuffer, pdu_parse_error::PduParseErr}; + +use crate::mle::fields::bs_service_details::BsServiceDetails; + +/// Clause 18.5.17: D-NWRK-BROADCAST Neighbor cell information for CA element (Table 18.64). +#[derive(Debug, Clone)] +pub struct NeighbourCellInformationForCa { + /// Type1, 5 bits + pub cell_identifier_ca: u8, + /// Type1, 2 bits + pub cell_reselection_types_supported: u8, + /// Type1, 1 bit + pub neighbour_cell_synchronized: bool, + /// Type1, 2 bits + pub cell_load_ca: u8, + /// Type1, 12 bits + pub main_carrier_number: u16, + /// Type2, 10 bits + pub main_carrier_number_extension: Option, + /// Type2, 10 bits + pub mcc: Option, + /// Type2, 14 bits + pub mnc: Option, + /// Type2, 14 bits + pub location_area: Option, + /// Type2, 3 bits + pub maximum_ms_transmit_power: Option, + /// Type2, 4 bits + pub minimum_rx_access_level: Option, + /// Type2, 16 bits + pub subscriber_class: Option, + /// Type2, 12 bits + pub bs_service_details: Option, + /// Type2, 5 bits + pub timeshare_cell_information_or_security_parameters: Option, + /// Type2, 6 bits + pub tdma_frame_offset: Option, +} + +impl NeighbourCellInformationForCa { + pub fn from_bitbuf(buf: &mut BitBuffer) -> Result { + let cell_identifier_ca = buf.read_field(5, "cell_identifier_ca")? as u8; + let cell_reselection_types_supported = buf.read_field(2, "cell_reselection_types_supported")? as u8; + let neighbour_cell_synchronized = buf.read_field(1, "neighbour_cell_synchronized")? != 0; + let cell_load_ca = buf.read_field(2, "cell_load_ca")? as u8; + let main_carrier_number = buf.read_field(12, "main_carrier_number")? as u16; + + // For this element, each optional Type2 field is preceded by a P-bit. + let main_carrier_number_extension = + typed::parse_type2_generic(true, buf, 10, "main_carrier_number_extension")?.map(|v| v as u16); + let mcc = typed::parse_type2_generic(true, buf, 10, "mcc")?.map(|v| v as u16); + let mnc = typed::parse_type2_generic(true, buf, 14, "mnc")?.map(|v| v as u16); + let location_area = typed::parse_type2_generic(true, buf, 14, "location_area")?.map(|v| v as u16); + let maximum_ms_transmit_power = + typed::parse_type2_generic(true, buf, 3, "maximum_ms_transmit_power")?.map(|v| v as u8); + let minimum_rx_access_level = + typed::parse_type2_generic(true, buf, 4, "minimum_rx_access_level")?.map(|v| v as u8); + let subscriber_class = typed::parse_type2_generic(true, buf, 16, "subscriber_class")?.map(|v| v as u16); + let bs_service_details = typed::parse_type2_struct(true, buf, BsServiceDetails::from_bitbuf)?; + let timeshare_cell_information_or_security_parameters = + typed::parse_type2_generic(true, buf, 5, "timeshare_cell_information_or_security_parameters")?.map(|v| v as u8); + let tdma_frame_offset = typed::parse_type2_generic(true, buf, 6, "tdma_frame_offset")?.map(|v| v as u8); + + Ok(NeighbourCellInformationForCa { + cell_identifier_ca, + cell_reselection_types_supported, + neighbour_cell_synchronized, + cell_load_ca, + main_carrier_number, + main_carrier_number_extension, + mcc, + mnc, + location_area, + maximum_ms_transmit_power, + minimum_rx_access_level, + subscriber_class, + bs_service_details, + timeshare_cell_information_or_security_parameters, + tdma_frame_offset, + }) + } + + pub fn to_bitbuf(&self, buf: &mut BitBuffer) -> Result<(), PduParseErr> { + self.ensure_bits(self.cell_identifier_ca as u64, 5, "cell_identifier_ca")?; + self.ensure_bits( + self.cell_reselection_types_supported as u64, + 2, + "cell_reselection_types_supported", + )?; + self.ensure_bits(self.cell_load_ca as u64, 2, "cell_load_ca")?; + self.ensure_bits(self.main_carrier_number as u64, 12, "main_carrier_number")?; + self.ensure_opt_bits( + self.main_carrier_number_extension.map(|v| v as u64), + 10, + "main_carrier_number_extension", + )?; + self.ensure_opt_bits(self.mcc.map(|v| v as u64), 10, "mcc")?; + self.ensure_opt_bits(self.mnc.map(|v| v as u64), 14, "mnc")?; + self.ensure_opt_bits(self.location_area.map(|v| v as u64), 14, "location_area")?; + self.ensure_opt_bits( + self.maximum_ms_transmit_power.map(|v| v as u64), + 3, + "maximum_ms_transmit_power", + )?; + self.ensure_opt_bits( + self.minimum_rx_access_level.map(|v| v as u64), + 4, + "minimum_rx_access_level", + )?; + self.ensure_opt_bits(self.subscriber_class.map(|v| v as u64), 16, "subscriber_class")?; + self.ensure_opt_bits( + self.timeshare_cell_information_or_security_parameters.map(|v| v as u64), + 5, + "timeshare_cell_information_or_security_parameters", + )?; + self.ensure_opt_bits(self.tdma_frame_offset.map(|v| v as u64), 6, "tdma_frame_offset")?; + + buf.write_bits(self.cell_identifier_ca as u64, 5); + buf.write_bits(self.cell_reselection_types_supported as u64, 2); + buf.write_bits(self.neighbour_cell_synchronized as u64, 1); + buf.write_bits(self.cell_load_ca as u64, 2); + buf.write_bits(self.main_carrier_number as u64, 12); + + typed::write_type2_generic( + true, + buf, + self.main_carrier_number_extension.map(|v| v as u64), + 10, + ); + typed::write_type2_generic(true, buf, self.mcc.map(|v| v as u64), 10); + typed::write_type2_generic(true, buf, self.mnc.map(|v| v as u64), 14); + typed::write_type2_generic(true, buf, self.location_area.map(|v| v as u64), 14); + typed::write_type2_generic( + true, + buf, + self.maximum_ms_transmit_power.map(|v| v as u64), + 3, + ); + typed::write_type2_generic( + true, + buf, + self.minimum_rx_access_level.map(|v| v as u64), + 4, + ); + typed::write_type2_generic(true, buf, self.subscriber_class.map(|v| v as u64), 16); + typed::write_type2_struct(true, buf, &self.bs_service_details, |val, inner| { + val.to_bitbuf(inner); + Ok(()) + })?; + typed::write_type2_generic( + true, + buf, + self.timeshare_cell_information_or_security_parameters.map(|v| v as u64), + 5, + ); + typed::write_type2_generic(true, buf, self.tdma_frame_offset.map(|v| v as u64), 6); + + Ok(()) + } + + fn ensure_bits(&self, value: u64, bits: u8, field: &'static str) -> Result<(), PduParseErr> { + let max = if bits >= 64 { u64::MAX } else { (1u64 << bits) - 1 }; + if value > max { + return Err(PduParseErr::InvalidValue { field, value }); + } + Ok(()) + } + + fn ensure_opt_bits(&self, value: Option, bits: u8, field: &'static str) -> Result<(), PduParseErr> { + if let Some(v) = value { + self.ensure_bits(v, bits, field)?; + } + Ok(()) + } +} + +impl fmt::Display for NeighbourCellInformationForCa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "NeighbourCellInformationForCa {{ cell_identifier_ca: {:?} cell_reselection_types_supported: {:?} neighbour_cell_synchronized: {:?} cell_load_ca: {:?} main_carrier_number: {:?} main_carrier_number_extension: {:?} mcc: {:?} mnc: {:?} location_area: {:?} maximum_ms_transmit_power: {:?} minimum_rx_access_level: {:?} subscriber_class: {:?} bs_service_details: {:?} timeshare_cell_information_or_security_parameters: {:?} tdma_frame_offset: {:?} }}", + self.cell_identifier_ca, + self.cell_reselection_types_supported, + self.neighbour_cell_synchronized, + self.cell_load_ca, + self.main_carrier_number, + self.main_carrier_number_extension, + self.mcc, + self.mnc, + self.location_area, + self.maximum_ms_transmit_power, + self.minimum_rx_access_level, + self.subscriber_class, + self.bs_service_details, + self.timeshare_cell_information_or_security_parameters, + self.tdma_frame_offset, + ) + } +} diff --git a/crates/tetra-pdus/src/mle/pdus/d_nwrk_broadcast.rs b/crates/tetra-pdus/src/mle/pdus/d_nwrk_broadcast.rs index 084e7a5..3d44601 100644 --- a/crates/tetra-pdus/src/mle/pdus/d_nwrk_broadcast.rs +++ b/crates/tetra-pdus/src/mle/pdus/d_nwrk_broadcast.rs @@ -4,6 +4,7 @@ use tetra_core::typed_pdu_fields::*; use tetra_core::{BitBuffer, expect_pdu_type, pdu_parse_error::PduParseErr}; use crate::mle::enums::mle_pdu_type_dl::MlePduTypeDl; +use crate::mle::fields::neighbour_cell_information_for_ca::NeighbourCellInformationForCa; /// Representation of the D-NWRK-BROADCAST PDU (Clause 18.4.1.4.1). /// Upon receipt from the SwMI, the message shall inform the MS-MLE about parameters for the CA serving cell and parameters for one or more CA neighbour cells. @@ -22,12 +23,11 @@ pub struct DNwrkBroadcast { /// Type2, 48 bits, TETRA network time pub tetra_network_time: Option, /// Type2, 3 bits, See note 2, - pub number_of_ca_neighbour_cells: Option, + pub number_of_ca_neighbour_cells: Option, /// Conditional See note 3, condition: number_of_ca_neighbour_cells > Some(0) - pub neighbour_cell_information_for_ca: Option, + pub neighbour_cell_information_for_ca: Vec, } -#[allow(unreachable_code)] // TODO FIXME review, finalize and remove this impl DNwrkBroadcast { /// Parse from BitBuffer pub fn from_bitbuf(buffer: &mut BitBuffer) -> Result { @@ -45,15 +45,22 @@ impl DNwrkBroadcast { // Type2 let tetra_network_time = typed::parse_type2_generic(obit, buffer, 48, "tetra_network_time")?; // Type2 - let number_of_ca_neighbour_cells = typed::parse_type2_generic(obit, buffer, 3, "number_of_ca_neighbour_cells")?; + let number_of_ca_neighbour_cells = + typed::parse_type2_generic(obit, buffer, 3, "number_of_ca_neighbour_cells")?.map(|v| v as u8); // Conditional - let neighbour_cell_information_for_ca = if obit && number_of_ca_neighbour_cells > Some(0) { - unimplemented!(); - Some(buffer.read_field(999, "neighbour_cell_information_for_ca")?) - } else { - None - }; + let mut neighbour_cell_information_for_ca = Vec::new(); + if let Some(count) = number_of_ca_neighbour_cells { + if count > 7 { + return Err(PduParseErr::InvalidValue { + field: "number_of_ca_neighbour_cells", + value: count as u64, + }); + } + for _ in 0..count { + neighbour_cell_information_for_ca.push(NeighbourCellInformationForCa::from_bitbuf(buffer)?); + } + } // Read trailing obit (if not previously encountered) obit = if obit { buffer.read_field(1, "trailing_obit")? == 1 } else { obit }; @@ -72,6 +79,27 @@ impl DNwrkBroadcast { /// Serialize this PDU into the given BitBuffer. pub fn to_bitbuf(&self, buffer: &mut BitBuffer) -> Result<(), PduParseErr> { + let neighbour_count = self.neighbour_cell_information_for_ca.len(); + if let Some(count) = self.number_of_ca_neighbour_cells { + if count as usize != neighbour_count { + return Err(PduParseErr::Inconsistency { + field: "number_of_ca_neighbour_cells", + reason: "count does not match neighbour_cell_information_for_ca length", + }); + } + if count > 7 { + return Err(PduParseErr::InvalidValue { + field: "number_of_ca_neighbour_cells", + value: count as u64, + }); + } + } else if neighbour_count > 0 { + return Err(PduParseErr::Inconsistency { + field: "neighbour_cell_information_for_ca", + reason: "missing number_of_ca_neighbour_cells", + }); + } + // PDU Type buffer.write_bits(MlePduTypeDl::DNwrkBroadcast.into_raw(), 3); // Type1 @@ -90,12 +118,18 @@ impl DNwrkBroadcast { typed::write_type2_generic(obit, buffer, self.tetra_network_time, 48); // Type2 - typed::write_type2_generic(obit, buffer, self.number_of_ca_neighbour_cells, 3); + typed::write_type2_generic( + obit, + buffer, + self.number_of_ca_neighbour_cells.map(|v| v as u64), + 3, + ); // Conditional - if let Some(ref _value) = self.neighbour_cell_information_for_ca { - unimplemented!(); - buffer.write_bits(*_value, 999); + if self.number_of_ca_neighbour_cells.unwrap_or(0) > 0 { + for neighbour in &self.neighbour_cell_information_for_ca { + neighbour.to_bitbuf(buffer)?; + } } // Write terminating m-bit delimiters::write_mbit(buffer, 0); diff --git a/example_config/config.toml b/example_config/config.toml index 32217b1..d76c590 100644 --- a/example_config/config.toml +++ b/example_config/config.toml @@ -105,6 +105,24 @@ colour_code = 1 # Neighbor cell broadcast settings neighbor_cell_broadcast = 2 +# Neighbor cells (CA) broadcast list for D-NWRK-BROADCAST +# [[cell_info.neighbor_cells_ca]] +# cell_identifier_ca = 1 +# cell_reselection_types_supported = 0 +# neighbor_cell_synchronized = true +# cell_load_ca = 0 +# main_carrier_number = 1521 +# main_carrier_number_extension = 0 +# mcc = 204 +# mnc = 1337 +# location_area = 2 +# maximum_ms_transmit_power = 5 +# minimum_rx_access_level = 3 +# subscriber_class = 0xFFFF +# bs_service_details = { registration = true, deregistration = true, priority_cell = false, no_minimum_mode = false, migration = false, system_wide_services = true, voice_service = true, circuit_mode_data_service = false, sndcp_service = false, aie_service = false, advanced_link = false } +# timeshare_cell_information_or_security_parameters = 0 +# tdma_frame_offset = 0 + # Cell load (Channel Allocation) - current load # cell_load_ca = 0