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:
proxiboi69
2026-02-22 15:38:14 +02:00
committed by GitHub
parent 3e277fe9f4
commit 5e58d8f68a
10 changed files with 145 additions and 40 deletions

View File

@@ -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));

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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>,

View File

@@ -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>,

View File

@@ -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,