mirror of
https://github.com/MidnightBlueLabs/tetra-bluestation.git
synced 2026-03-29 05:09:51 +00:00
Fix CMCE floor-control signaling and LLC framing for group calls (#22)
- Reduce initial D-SETUP burst from 4 frames to 1+1 backup - Fix transmission_request_permission polarity (0 = allowed) - Send individual D-TX GRANTED then group D-TX GRANTED to GSSI - Update cached D-SETUP transmission_grant for current call state - Use BL-UDATA for GSSI-addressed and STCH messages in LLC - Send BL-ACK via FACCH on traffic timeslots - Implement STCH second-half-stolen signaling (UMAC → LMAC) - Drain stale signaling from DL queue when leaving hangtime
This commit is contained in:
@@ -311,8 +311,9 @@ impl CircuitMgr {
|
||||
if let Some(circuit) = circuit {
|
||||
let age = circuit.ts_created.age(dltime);
|
||||
|
||||
// Send D-SETUP for first 4 frames after circuit creation
|
||||
if age < 4 * 4 {
|
||||
// Send D-SETUP for the initial frame + 1 backup frame after circuit creation.
|
||||
// Matches ETSI Annex D Figure D.2: 1 initial + 1 back-up on MCCH.
|
||||
if age < 1 * 4 {
|
||||
tasks
|
||||
.get_or_insert_with(Vec::new)
|
||||
.push(CircuitMgrCmd::SendDSetup(circuit.call_id, circuit.usage, circuit.ts));
|
||||
|
||||
@@ -675,10 +675,20 @@ impl CcBsSubentity {
|
||||
match task {
|
||||
CircuitMgrCmd::SendDSetup(call_id, usage, ts) => {
|
||||
// Get our cached D-SETUP, build a prim and send it down the stack
|
||||
let Some((pdu, dest_addr)) = self.cached_setups.get(&call_id) else {
|
||||
let Some((pdu, dest_addr)) = self.cached_setups.get_mut(&call_id) else {
|
||||
tracing::error!("No cached D-SETUP for call id {}", call_id);
|
||||
return;
|
||||
};
|
||||
// Update transmission_grant based on current call state:
|
||||
// During hangtime (nobody transmitting), use NotGranted;
|
||||
// during active TX, use GrantedToOtherUser.
|
||||
if let Some(active) = self.active_calls.get(&call_id) {
|
||||
pdu.transmission_grant = if active.tx_active {
|
||||
TransmissionGrant::GrantedToOtherUser
|
||||
} else {
|
||||
TransmissionGrant::NotGranted
|
||||
};
|
||||
}
|
||||
let dest_addr = *dest_addr;
|
||||
let (sdu, chan_alloc) = Self::build_d_setup_prim(pdu, usage, ts, UlDlAssignment::Both);
|
||||
let prim = Self::build_sapmsg(sdu, Some(chan_alloc), self.dltime, dest_addr);
|
||||
@@ -889,7 +899,7 @@ impl CcBsSubentity {
|
||||
// Send D-TX CEASED via FACCH (stealing) to all group members
|
||||
let d_tx_ceased = DTxCeased {
|
||||
call_identifier: call_id,
|
||||
transmission_request_permission: true, // Allow other MSs to request the floor
|
||||
transmission_request_permission: false, // ETSI 14.8.43: 0 = allowed to request transmission
|
||||
notification_indicator: None,
|
||||
facility: None,
|
||||
dm_ms_address: None,
|
||||
@@ -974,8 +984,8 @@ impl CcBsSubentity {
|
||||
};
|
||||
let dest_addr = *dest_addr;
|
||||
|
||||
// Send D-TX GRANTED via FACCH
|
||||
let d_tx_granted = DTxGranted {
|
||||
// ETSI 14.5.2.2.1 b): Send individual D-TX GRANTED (Granted) to requesting MS FIRST
|
||||
let d_tx_granted_individual = DTxGranted {
|
||||
call_identifier: call_id,
|
||||
transmission_grant: TransmissionGrant::Granted.into_raw() as u8,
|
||||
transmission_request_permission: false,
|
||||
@@ -991,14 +1001,18 @@ impl CcBsSubentity {
|
||||
proprietary: None,
|
||||
};
|
||||
|
||||
tracing::info!("-> {:?}", d_tx_granted);
|
||||
tracing::info!("-> D-TX GRANTED (individual, Granted) {:?}", d_tx_granted_individual);
|
||||
let mut sdu = BitBuffer::new_autoexpand(50);
|
||||
d_tx_granted.to_bitbuf(&mut sdu).expect("Failed to serialize DTxGranted");
|
||||
d_tx_granted_individual.to_bitbuf(&mut sdu).expect("Failed to serialize DTxGranted");
|
||||
sdu.seek(0);
|
||||
|
||||
let msg = Self::build_sapmsg_stealing(sdu, self.dltime, dest_addr, ts);
|
||||
let requesting_addr = TetraAddress::new(requesting_party.ssi, SsiType::Issi);
|
||||
let msg = Self::build_sapmsg_stealing(sdu, self.dltime, requesting_addr, ts);
|
||||
queue.push_back(msg);
|
||||
|
||||
// ETSI 14.5.2.2.1 b): Send group D-TX GRANTED (GrantedToOtherUser) to GSSI
|
||||
self.send_d_tx_granted_facch(queue, call_id, requesting_party.ssi, dest_addr.ssi, ts);
|
||||
|
||||
// Notify UMAC to resume traffic mode (exit hangtime) for this timeslot.
|
||||
queue.push_back(SapMsg {
|
||||
sap: Sap::Control,
|
||||
@@ -1375,7 +1389,7 @@ impl CcBsSubentity {
|
||||
fn send_d_tx_ceased_facch(&mut self, queue: &mut MessageQueue, call_id: u16, dest_gssi: u32, ts: u8) {
|
||||
let pdu = DTxCeased {
|
||||
call_identifier: call_id,
|
||||
transmission_request_permission: true,
|
||||
transmission_request_permission: false, // ETSI 14.8.43: 0 = allowed to request transmission
|
||||
notification_indicator: None,
|
||||
facility: None,
|
||||
dm_ms_address: None,
|
||||
|
||||
@@ -4,7 +4,10 @@ use std::panic;
|
||||
use crate::{MessageQueue, TetraEntityTrait};
|
||||
use tetra_config::SharedConfig;
|
||||
use tetra_core::tetra_entities::TetraEntity;
|
||||
use tetra_core::{BitBuffer, Sap, TdmaTime, TetraAddress, unimplemented_log};
|
||||
use tetra_core::{BitBuffer, Sap, SsiType, TdmaTime, TetraAddress, unimplemented_log};
|
||||
use tetra_saps::lcmc::enums::alloc_type::ChanAllocType;
|
||||
use tetra_saps::lcmc::enums::ul_dl_assignment::UlDlAssignment;
|
||||
use tetra_saps::lcmc::fields::chan_alloc_req::CmceChanAllocReq;
|
||||
use tetra_saps::tla::{TlaTlDataIndBl, TlaTlUnitdataIndBl};
|
||||
use tetra_saps::tma::TmaUnitdataReq;
|
||||
use tetra_saps::{SapMsg, SapMsgInner};
|
||||
@@ -20,6 +23,8 @@ pub struct AckData {
|
||||
pub addr: TetraAddress,
|
||||
pub t_start: TdmaTime,
|
||||
pub n: u8,
|
||||
/// Timeslot on which the original message was received
|
||||
pub ts: u8,
|
||||
}
|
||||
|
||||
pub struct Llc {
|
||||
@@ -46,8 +51,13 @@ impl Llc {
|
||||
}
|
||||
|
||||
/// Schedule an ACK to be sent at a later time
|
||||
pub fn schedule_outgoing_ack(&mut self, t: TdmaTime, addr: TetraAddress, n: u8) {
|
||||
self.scheduled_out_acks.push(AckData { t_start: t, n, addr });
|
||||
pub fn schedule_outgoing_ack(&mut self, dltime: TdmaTime, addr: TetraAddress, ns: u8) {
|
||||
self.scheduled_out_acks.push(AckData {
|
||||
t_start: dltime,
|
||||
n: ns,
|
||||
addr,
|
||||
ts: dltime.t,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns details for outstanding to-be-sent ACK, if any. Returned u8 is the sequence number
|
||||
@@ -73,7 +83,12 @@ impl Llc {
|
||||
|
||||
/// Register that we expect an ACK for this link (acknowledged mode only)
|
||||
fn register_expected_ack(&mut self, t: TdmaTime, addr: TetraAddress, n: u8) {
|
||||
self.expected_in_acks.push(AckData { t_start: t, n, addr });
|
||||
self.expected_in_acks.push(AckData {
|
||||
t_start: t,
|
||||
n,
|
||||
addr,
|
||||
ts: t.t,
|
||||
});
|
||||
}
|
||||
|
||||
fn format_ack_list(ack_list: &Vec<AckData>) -> String {
|
||||
@@ -135,6 +150,44 @@ impl Llc {
|
||||
panic!()
|
||||
};
|
||||
|
||||
// Use unacknowledged mode (BL-UDATA) when:
|
||||
// 1. STCH (stolen half-slot) — no established LLC link on STCH.
|
||||
// 2. GSSI-addressed messages — per ETSI EN 300 392-2, group-addressed
|
||||
// signaling on SCH/F must use BL-UDATA because there is no
|
||||
// established LLC link with individual group members. Using BL-DATA
|
||||
// causes radios (e.g. Sepura) to silently discard frames when the
|
||||
// ns sequence number doesn't match their V(R).
|
||||
if prim.stealing_permission || prim.main_address.ssi_type == SsiType::Gssi {
|
||||
let mut pdu_buf = BitBuffer::new_autoexpand(32);
|
||||
let pdu = BlUdata { has_fcs: false };
|
||||
pdu.to_bitbuf(&mut pdu_buf);
|
||||
let sdu_len = prim.tl_sdu.get_len_remaining();
|
||||
pdu_buf.copy_bits(&mut prim.tl_sdu, sdu_len);
|
||||
pdu_buf.seek(0);
|
||||
tracing::debug!("-> {:?} sdu {}", pdu, pdu_buf.dump_bin());
|
||||
|
||||
let sapmsg = SapMsg {
|
||||
sap: Sap::TmaSap,
|
||||
src: self.entity(),
|
||||
dest: TetraEntity::Umac,
|
||||
dltime: message.dltime,
|
||||
msg: SapMsgInner::TmaUnitdataReq(TmaUnitdataReq {
|
||||
req_handle: prim.req_handle,
|
||||
pdu: pdu_buf,
|
||||
main_address: prim.main_address,
|
||||
endpoint_id: prim.endpoint_id,
|
||||
stealing_permission: prim.stealing_permission,
|
||||
subscriber_class: prim.subscriber_class,
|
||||
air_interface_encryption: prim.air_interface_encryption,
|
||||
stealing_repeats_flag: prim.stealing_repeats_flag,
|
||||
data_category: prim.data_class_info,
|
||||
chan_alloc: prim.chan_alloc,
|
||||
}),
|
||||
};
|
||||
queue.push_back(sapmsg);
|
||||
return;
|
||||
}
|
||||
|
||||
// If an ack still needs to be sent, get the relevant expected sequence number
|
||||
let out_ack_n = self.get_out_ack_n_if_any(message.dltime.t, prim.main_address);
|
||||
|
||||
@@ -461,7 +514,11 @@ impl TetraEntityTrait for Llc {
|
||||
// Take oldest element from scheduled_out_acks, and remove it from the list
|
||||
let ret = !self.scheduled_out_acks.is_empty();
|
||||
while let Some(ack) = self.scheduled_out_acks.first() {
|
||||
tracing::debug!("tick_end: auto-ack for ssi: {}, n: {}", ack.addr.ssi, ack.n);
|
||||
tracing::debug!("tick_end: auto-ack for ssi: {}, n: {}, ts: {}", ack.addr.ssi, ack.n, ack.ts);
|
||||
|
||||
// Send BL-ACK via FACCH (stealing) on the traffic timeslot if the original
|
||||
// message arrived on a traffic channel (TS2-4), otherwise via MCCH (TS1).
|
||||
let steal = matches!(ack.ts, 2..=4);
|
||||
|
||||
let mut pdu_buf = BitBuffer::new_autoexpand(5);
|
||||
let pdu = BlAck { has_fcs: false, nr: ack.n };
|
||||
@@ -484,13 +541,25 @@ impl TetraEntityTrait for Llc {
|
||||
pdu: pdu_buf,
|
||||
main_address: ack.addr,
|
||||
// scrambling_code: self.config.config().scrambling_code(),
|
||||
endpoint_id: 0, // todo fixme
|
||||
stealing_permission: false, // TODO FIXME
|
||||
endpoint_id: 0, // todo fixme
|
||||
stealing_permission: steal,
|
||||
subscriber_class: 0, // TODO FIXME
|
||||
air_interface_encryption: None, // TODO FIXME
|
||||
stealing_repeats_flag: None, // TODO FIXME
|
||||
data_category: None, // TODO FIXME
|
||||
chan_alloc: None, // TODO FIXME
|
||||
chan_alloc: if steal {
|
||||
let mut timeslots = [false; 4];
|
||||
timeslots[(ack.ts - 1) as usize] = true;
|
||||
Some(CmceChanAllocReq {
|
||||
usage: None,
|
||||
timeslots,
|
||||
alloc_type: ChanAllocType::Replace,
|
||||
ul_dl_assigned: UlDlAssignment::Both,
|
||||
carrier: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}),
|
||||
};
|
||||
queue.push_back(sapmsg);
|
||||
|
||||
@@ -292,11 +292,14 @@ impl LmacBs {
|
||||
}
|
||||
}
|
||||
|
||||
// fn rx_tmv_configure_req(&mut self, _queue: &mut MessageQueue, mut message: SapMsg) {
|
||||
// tracing::trace!("rx_tmv_configure_req");
|
||||
// let SapMsgInner::TmvConfigureReq(_prim) = &mut message.msg else {panic!()};
|
||||
// unimplemented_log!("rx_tmv_configure_req");
|
||||
// }
|
||||
fn rx_tmv_configure_req(&mut self, _queue: &mut MessageQueue, message: SapMsg) {
|
||||
let SapMsgInner::TmvConfigureReq(prim) = &message.msg else {
|
||||
panic!()
|
||||
};
|
||||
if let Some(stolen) = prim.second_half_stolen {
|
||||
self.second_block_stolen = stolen;
|
||||
}
|
||||
}
|
||||
|
||||
/// Request from Umac to transmit a message
|
||||
fn rx_tmv_unitdata_req_slot(&mut self, queue: &mut MessageQueue, mut message: SapMsg) {
|
||||
@@ -382,9 +385,9 @@ impl LmacBs {
|
||||
tracing::trace!("rx_tmv_prim");
|
||||
|
||||
match message.msg {
|
||||
// SapMsgInner::TmvConfigureReq(_) => {
|
||||
// self.rx_tmv_configure_req(queue, message);
|
||||
// }
|
||||
SapMsgInner::TmvConfigureReq(_) => {
|
||||
self.rx_tmv_configure_req(queue, message);
|
||||
}
|
||||
SapMsgInner::TmvUnitdataReq(_) => {
|
||||
self.rx_tmv_unitdata_req_slot(queue, message);
|
||||
}
|
||||
|
||||
@@ -142,6 +142,13 @@ impl BsChannelScheduler {
|
||||
self.hangtime[idx] = active;
|
||||
self.hangtime_guard[idx] = if active { 1 } else { 0 };
|
||||
|
||||
// When leaving hangtime, drain stale signaling items that can only be consumed
|
||||
// in signaling mode. Keep Stealing items — they carry D-TX GRANTED/CEASED
|
||||
// that still need FACCH delivery.
|
||||
if !active {
|
||||
self.dltx_queues[idx].retain(|e| matches!(e, DlSchedElem::Stealing(_)));
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"BsChannelScheduler: hangtime {} for ts {} (guard={})",
|
||||
if active { "ENABLED" } else { "DISABLED" },
|
||||
@@ -1050,12 +1057,12 @@ impl BsChannelScheduler {
|
||||
if hang_effective && (dl_traffic_usage.is_some() || ul_traffic_usage.is_some()) {
|
||||
aach.dl_usage = AccessAssignDlUsage::AssignedControl;
|
||||
aach.ul_usage = AccessAssignUlUsage::CommonAndAssigned;
|
||||
// ACCESS-ASSIGN header=1 requires an access field for both UL subslots.
|
||||
// Keep it consistent with TS1 defaults.
|
||||
aach.f2_af = Some(AccessField {
|
||||
access_code: 0,
|
||||
base_frame_len: 4,
|
||||
});
|
||||
// ACCESS-ASSIGN header=1 requires an access field for both UL subslots.
|
||||
// Keep it consistent with TS1 defaults.
|
||||
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)
|
||||
|
||||
@@ -27,13 +27,14 @@ use tetra_saps::lcmc::enums::alloc_type::ChanAllocType;
|
||||
use tetra_saps::lcmc::enums::ul_dl_assignment::UlDlAssignment;
|
||||
use tetra_saps::lcmc::fields::chan_alloc_req::CmceChanAllocReq;
|
||||
use tetra_saps::tma::{TmaReport, TmaReportInd, TmaUnitdataInd};
|
||||
use tetra_saps::tmv::TmvConfigureReq;
|
||||
use tetra_saps::tmv::enums::logical_chans::LogicalChannel;
|
||||
use tetra_saps::{SapMsg, SapMsgInner};
|
||||
|
||||
use crate::lmac::components::scrambler;
|
||||
use crate::umac::subcomp::bs_sched::{BsChannelScheduler, PrecomputedUmacPdus, TCH_S_CAP};
|
||||
use crate::umac::subcomp::fillbits;
|
||||
use crate::{MessageQueue, TetraEntityTrait};
|
||||
use crate::{MessagePrio, MessageQueue, TetraEntityTrait};
|
||||
|
||||
use super::subcomp::bs_defrag::BsDefrag;
|
||||
|
||||
@@ -393,7 +394,6 @@ impl UmacBs {
|
||||
}
|
||||
0b111110 => {
|
||||
// Second half slot stolen in STCH
|
||||
unimplemented_log!("rx_mac_data: SECOND HALF SLOT STOLEN IN STCH but signal not implemented");
|
||||
(prim.pdu.get_len(), false, true, false)
|
||||
}
|
||||
0b111111 => {
|
||||
@@ -469,8 +469,19 @@ impl UmacBs {
|
||||
// Fragmentation start, add to defragmenter
|
||||
self.defrag.insert_first(&mut prim.pdu, message.dltime, addr, None);
|
||||
} else if second_half_stolen {
|
||||
// TODO FIXME maybe not elif here
|
||||
tracing::warn!("rx_mac_data: SECOND HALF SLOT STOLEN IN STCH but not implemented");
|
||||
// Signal LMAC that Block2 is also stolen (STCH, not TCH).
|
||||
// Must be Immediate priority so LMAC sees it before processing Block2.
|
||||
let m = SapMsg {
|
||||
sap: Sap::TmvSap,
|
||||
src: self.self_component,
|
||||
dest: TetraEntity::Lmac,
|
||||
dltime: message.dltime,
|
||||
msg: SapMsgInner::TmvConfigureReq(TmvConfigureReq {
|
||||
second_half_stolen: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
queue.push_prio(m, MessagePrio::Immediate);
|
||||
} else {
|
||||
// Pass directly to LLC
|
||||
let sdu = {
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct DCallRestore {
|
||||
/// Type1, 2 bits, Transmission grant
|
||||
pub transmission_grant: u8,
|
||||
/// Type1, 1 bits, Transmission request permission
|
||||
/// Set to true to signal MSes they are allowed to send a U-TX DEMAND
|
||||
/// ETSI 14.8.43: 0 = allowed to request transmission, 1 = not allowed.
|
||||
pub transmission_request_permission: bool,
|
||||
/// Type1, 1 bits, Reset call time-out timer (T310)
|
||||
pub reset_call_time_out_timer_t310_: bool,
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct DTxCeased {
|
||||
/// Type1, 14 bits, Call identifier
|
||||
pub call_identifier: u16,
|
||||
/// Type1, 1 bits, Transmission request permission
|
||||
/// Set to true to signal MSes they are allowed to send a U-TX DEMAND
|
||||
/// ETSI 14.8.43: 0 = allowed to request transmission, 1 = not allowed.
|
||||
pub transmission_request_permission: bool,
|
||||
/// Type2, 6 bits, Notification indicator
|
||||
pub notification_indicator: Option<u64>,
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct DTxContinue {
|
||||
/// Type1, 1 bits, do_Continue
|
||||
pub do_continue: bool,
|
||||
/// Type1, 1 bits, Transmission request permission
|
||||
/// Set to true to signal MSes they are allowed to send a U-TX DEMAND
|
||||
/// ETSI 14.8.43: 0 = allowed to request transmission, 1 = not allowed.
|
||||
pub transmission_request_permission: bool,
|
||||
/// Type2, 6 bits, Notification indicator
|
||||
pub notification_indicator: Option<u64>,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub struct DTxGranted {
|
||||
/// Type1, 2 bits, Transmission grant
|
||||
pub transmission_grant: u8,
|
||||
/// Type1, 1 bits, Transmission request permission
|
||||
/// Set to true to signal MSes they are allowed to send a U-TX DEMAND
|
||||
/// ETSI 14.8.43: 0 = allowed to request transmission, 1 = not allowed.
|
||||
pub transmission_request_permission: bool,
|
||||
/// Type1, 1 bits, Encryption control
|
||||
pub encryption_control: bool,
|
||||
|
||||
Reference in New Issue
Block a user