S558 · SOURCE-BOUND GATE EVIDENCE
S558 · R1 modem: sesli arama durum makinesi modeli
tam S558 implementation modülü → Operations --test hedefi ile bağlı tam focused test → ayrı Operations kaydı Bu sayfa yalnız S558 kapısına aittir; komşu kapıların kaynakları bu kabulün içine katılmaz.
S558Focused kod testiOperations id exactsource SHA exacttest target exact
operation: g8l-s558-r1-voice-call-state-machine-model
uygulama/model · focused test · Operations · 3 exact excerpt
sequence-bound=true · implementation-bound=true
01 · Yürütme / doğrulama kodu
Kapının gerçek repository sözleşmesi
tam dosyaL1–L999
kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s558_r1_voice_call_state_machine_model.rs::S558 r1 voice call state machine model implementation
#![allow(unexpected_cfgs)]
//! S558 models the voice call control state machine of the R1 modem path as a
//! pure source/host model.
//!
//! The model covers `ATD<number>;` dialing with E.164-style number validation
//! (optional leading `+`, 3..=20 digits), `ATA` answer, `ATH`/`+CHUP` hangup,
//! `+CHLD`-style hold/resume, `+CLCC` list-current-calls parsing (`idx`, `dir`,
//! `stat` 0..5 active/held/dialing/alerting/incoming/waiting, `mode`, `mpty`,
//! quoted `number` plus type-of-address), `RING`/`+CLIP` unsolicited result
//! codes, `+VTS` DTMF tone validation, a bounded call table of at most two
//! calls (one active plus one held), bounded setup timeouts and checked call
//! duration ticks. The per-call state machine is
//! `Idle -> Dialing -> Alerting -> Active -> Disconnecting -> Idle` for mobile
//! originated calls and `Idle -> Incoming/Waiting -> Active` for mobile
//! terminated calls. Every accepted transition publishes one receipt into a
//! bounded ledger; every illegal transition, malformed line, invalid number,
//! invalid tone, out-of-order step or overflow fails closed without mutating
//! the state.
//!
//! S558 does not claim any modem hardware, AT transport, UART, audio path,
//! panel, board, power transition or runtime observation. The module has no
//! production callsite and is driven only by its focused host test. It
//! performs no device operation and does not rerun S540 or S543.
//! Predecessor: S557 (packet data PDP context / PPP frame model). Next gate:
//! S559 (audio route / PCM capability model).
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
pub const S558_SEQUENCE: usize = 558;
pub const S558_EXPECTED_PREDECESSOR: usize = 557;
pub const S558_R1_STAGE: u8 = 3;
pub const S558_R1_RANGE_FIRST: usize = 536;
pub const S558_R1_RANGE_LAST: usize = 568;
pub const S558_NUMBER_MIN_DIGITS: usize = 3;
pub const S558_NUMBER_MAX_DIGITS: usize = 20;
pub const S558_MAX_CALLS: usize = 2;
pub const S558_MAX_ACTIVE_CALLS: u8 = 1;
pub const S558_MAX_HELD_CALLS: u8 = 1;
pub const S558_CLCC_STAT_MAX: u8 = 5;
pub const S558_CLCC_MODE_VOICE: u8 = 0;
pub const S558_TOA_INTERNATIONAL: u8 = 145;
pub const S558_TOA_UNKNOWN: u8 = 129;
pub const S558_TOA_NATIONAL: u8 = 161;
pub const S558_DTMF_DURATION_MIN: u8 = 1;
pub const S558_DTMF_DURATION_MAX: u8 = 100;
pub const S558_DIAL_TIMEOUT_TICKS: u32 = 90;
pub const S558_RING_TIMEOUT_TICKS: u32 = 45;
pub const S558_MAX_TICK_STEP: u32 = 3_600;
pub const S558_MAX_CALL_DURATION_TICKS: u32 = 86_400;
pub const S558_MAX_TRANSITIONS: usize = 96;
pub const S558_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS: usize = 0;
pub const S558_PHYSICAL_OBSERVATIONS: usize = 0;
pub const S558_PHYSICAL_OR_DEVICE_OPERATIONS: usize = 0;
pub const S558_SD_WRITES: usize = 0;
pub const S558_UART_OPENS: usize = 0;
pub const S558_POWER_TRANSITIONS: usize = 0;
pub const S558_NEW_IMMUTABLE_RAW_CAPTURES: usize = 0;
pub const S558_S540_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S558_S543_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S558_AUTOMATIC_PROMOTION: bool = false;
pub const S558_BOOT_TO_UI_PHYSICALLY_OBSERVED: bool = false;
pub const S558_HARDWARE_PRESENT: bool = false;
pub const S558_R1_ACCEPTANCE_COMPLETE: bool = false;
pub const RUNBOOK_EXECUTED_IN_S558: bool = false;
/// Bounded E.164-style dial number: optional leading `+` and 3..=20 digits.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS558CallNumber {
digits: [u8; S558_NUMBER_MAX_DIGITS],
len: u8,
international: bool,
}
impl G8lS558CallNumber {
pub fn from_text(text: &str) -> Result<Self, G8lS558VoiceCallError> {
let raw = text.as_bytes();
if raw.is_empty() {
return Err(G8lS558VoiceCallError::NumberEmpty);
}
let (international, body) = match raw[0] {
b'+' => (true, &raw[1..]),
_ => (false, raw),
};
if body.len() < S558_NUMBER_MIN_DIGITS {
return Err(G8lS558VoiceCallError::NumberTooShort);
}
if body.len() > S558_NUMBER_MAX_DIGITS {
return Err(G8lS558VoiceCallError::NumberTooLong);
}
let mut digits = [0u8; S558_NUMBER_MAX_DIGITS];
for (slot, byte) in digits.iter_mut().zip(body) {
if *byte == b'+' {
return Err(G8lS558VoiceCallError::NumberMisplacedPlus);
}
if !byte.is_ascii_digit() {
return Err(G8lS558VoiceCallError::NumberInvalidCharacter);
}
*slot = *byte;
}
Ok(Self {
digits,
len: body.len() as u8,
international,
})
}
pub const fn len(&self) -> usize {
self.len as usize
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub const fn is_international(&self) -> bool {
self.international
}
pub fn digits(&self) -> &str {
core::str::from_utf8(&self.digits[..self.len as usize]).unwrap_or("")
}
pub const fn type_of_address(&self) -> u8 {
if self.international {
S558_TOA_INTERNATIONAL
} else {
S558_TOA_UNKNOWN
}
}
pub fn dial_string(&self) -> String {
if self.international {
format!("+{}", self.digits())
} else {
String::from(self.digits())
}
}
}
/// `+CLCC` `<dir>`: 0 mobile originated, 1 mobile terminated.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS558CallDirection {
MobileOriginated,
MobileTerminated,
}
impl G8lS558CallDirection {
pub const fn from_code(code: u8) -> Option<Self> {
match code {
0 => Some(Self::MobileOriginated),
1 => Some(Self::MobileTerminated),
_ => None,
}
}
pub const fn code(self) -> u8 {
match self {
Self::MobileOriginated => 0,
Self::MobileTerminated => 1,
}
}
}
/// `+CLCC` `<stat>` 0..5 mapping.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS558ClccStat {
Active,
Held,
Dialing,
Alerting,
Incoming,
Waiting,
}
impl G8lS558ClccStat {
pub const fn from_code(code: u8) -> Option<Self> {
match code {
0 => Some(Self::Active),
1 => Some(Self::Held),
2 => Some(Self::Dialing),
3 => Some(Self::Alerting),
4 => Some(Self::Incoming),
5 => Some(Self::Waiting),
_ => None,
}
}
pub const fn code(self) -> u8 {
match self {
Self::Active => 0,
Self::Held => 1,
Self::Dialing => 2,
Self::Alerting => 3,
Self::Incoming => 4,
Self::Waiting => 5,
}
}
pub const fn text(self) -> &'static str {
match self {
Self::Active => "active",
Self::Held => "held",
Self::Dialing => "dialing",
Self::Alerting => "alerting",
Self::Incoming => "incoming",
Self::Waiting => "waiting",
}
}
}
/// One parsed `+CLCC: <idx>,<dir>,<stat>,<mode>,<mpty>[,"<number>",<type>]` line.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS558ClccEntry {
pub idx: u8,
pub direction: G8lS558CallDirection,
pub stat: G8lS558ClccStat,
pub mode: u8,
pub multiparty: bool,
pub number: Option<G8lS558CallNumber>,
pub type_of_address: u8,
}
/// Per-call phase of the voice call state machine.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS558CallPhase {
Idle,
Dialing,
Alerting,
Active,
Held,
Incoming,
Waiting,
Disconnecting,
}
impl G8lS558CallPhase {
pub const fn clcc_stat(self) -> Option<G8lS558ClccStat> {
match self {
Self::Idle | Self::Disconnecting => None,
Self::Dialing => Some(G8lS558ClccStat::Dialing),
Self::Alerting => Some(G8lS558ClccStat::Alerting),
Self::Active => Some(G8lS558ClccStat::Active),
Self::Held => Some(G8lS558ClccStat::Held),
Self::Incoming => Some(G8lS558ClccStat::Incoming),
Self::Waiting => Some(G8lS558ClccStat::Waiting),
}
}
const fn is_setup(self) -> bool {
matches!(
self,
Self::Dialing | Self::Alerting | Self::Incoming | Self::Waiting | Self::Disconnecting
)
}
}
/// Events driving the state machine. Every event is copyable so that an exact
/// replay can be compared against the published receipt.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS558Event {
Dial(G8lS558CallNumber),
Ring { number: Option<G8lS558CallNumber> },
Answer,
Hangup { idx: u8 },
Hold { idx: u8 },
Resume { idx: u8 },
Dtmf { tone: u8, duration: u8 },
Tick(u32),
ClccReport(G8lS558ClccEntry),
Released { idx: u8 },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS558Call {
pub idx: u8,
pub direction: G8lS558CallDirection,
pub phase: G8lS558CallPhase,
pub number: Option<G8lS558CallNumber>,
pub setup_ticks: u32,
pub duration_ticks: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS558CallTransitionReceipt {
pub sequence: usize,
pub predecessor_sequence: usize,
pub step: usize,
pub event: G8lS558Event,
pub call_idx: u8,
pub from: G8lS558CallPhase,
pub to: G8lS558CallPhase,
pub active_calls: u8,
pub held_calls: u8,
pub occupied_slots: u8,
pub setup_ticks: u32,
pub duration_ticks: u32,
pub missed_calls: u32,
pub completed_calls: u32,
pub dtmf_tones_sent: u32,
pub hardware_present: bool,
pub physical_observations: usize,
pub runbook_executed: bool,
}
#[derive(Debug)]
pub struct G8lS558VoiceCallState {
calls: [Option<G8lS558Call>; S558_MAX_CALLS],
missed_calls: u32,
completed_calls: u32,
dtmf_tones_sent: u32,
ledger: Vec<G8lS558CallTransitionReceipt>,
}
impl G8lS558VoiceCallState {
pub const fn new() -> Self {
Self {
calls: [None; S558_MAX_CALLS],
missed_calls: 0,
completed_calls: 0,
dtmf_tones_sent: 0,
ledger: Vec::new(),
}
}
pub fn call(&self, idx: u8) -> Option<G8lS558Call> {
slot_index(idx).ok().and_then(|slot| self.calls[slot])
}
pub fn phase(&self, idx: u8) -> G8lS558CallPhase {
self.call(idx)
.map_or(G8lS558CallPhase::Idle, |call| call.phase)
}
pub fn active_calls(&self) -> u8 {
count_phase(&self.calls, G8lS558CallPhase::Active)
}
pub fn held_calls(&self) -> u8 {
count_phase(&self.calls, G8lS558CallPhase::Held)
}
pub fn occupied_slots(&self) -> u8 {
self.calls.iter().filter(|call| call.is_some()).count() as u8
}
pub const fn missed_calls(&self) -> u32 {
self.missed_calls
}
pub const fn completed_calls(&self) -> u32 {
self.completed_calls
}
pub const fn dtmf_tones_sent(&self) -> u32 {
self.dtmf_tones_sent
}
pub fn receipts(&self) -> &[G8lS558CallTransitionReceipt] {
&self.ledger
}
pub fn last_receipt(&self) -> Option<G8lS558CallTransitionReceipt> {
self.ledger.last().copied()
}
}
impl Default for G8lS558VoiceCallState {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS558VoiceCallOutcome {
TransitionPublished(G8lS558CallTransitionReceipt),
TransitionRetained(G8lS558CallTransitionReceipt),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS558VoiceCallError {
StepOutOfOrder,
PublishedStateDrift,
LedgerFull,
NumberEmpty,
NumberTooShort,
NumberTooLong,
NumberInvalidCharacter,
NumberMisplacedPlus,
InvalidClccLine,
InvalidClccIndex,
InvalidClccDirection,
InvalidClccStat,
InvalidClccMode,
InvalidClccMultiparty,
InvalidClccTypeOfAddress,
InvalidClipLine,
InvalidDtmfTone,
InvalidDtmfDuration,
NoCallSlot,
CallSetupAlreadyInProgress,
NoSuchCall,
NoIncomingCall,
IllegalTransition,
ActiveCallMustBeHeld,
HeldCallAlreadyPresent,
ActiveCallAlreadyPresent,
NoActiveCallForDtmf,
MultipartyUnsupported,
ClccDirectionMismatch,
ClccNumberMismatch,
TickZero,
TickTooLarge,
NoCallForTick,
DurationOverflow,
CounterOverflow,
}
impl G8lS558VoiceCallError {
pub const fn diagnostic_code(self) -> u64 {
match self {
Self::StepOutOfOrder => 1,
Self::PublishedStateDrift => 2,
Self::LedgerFull => 3,
Self::NumberEmpty => 4,
Self::NumberTooShort => 5,
Self::NumberTooLong => 6,
Self::NumberInvalidCharacter => 7,
Self::NumberMisplacedPlus => 8,
Self::InvalidClccLine => 9,
Self::InvalidClccIndex => 10,
Self::InvalidClccDirection => 11,
Self::InvalidClccStat => 12,
Self::InvalidClccMode => 13,
Self::InvalidClccMultiparty => 14,
Self::InvalidClccTypeOfAddress => 15,
Self::InvalidClipLine => 16,
Self::InvalidDtmfTone => 17,
Self::InvalidDtmfDuration => 18,
Self::NoCallSlot => 19,
Self::CallSetupAlreadyInProgress => 20,
Self::NoSuchCall => 21,
Self::NoIncomingCall => 22,
Self::IllegalTransition => 23,
Self::ActiveCallMustBeHeld => 24,
Self::HeldCallAlreadyPresent => 25,
Self::ActiveCallAlreadyPresent => 26,
Self::NoActiveCallForDtmf => 27,
Self::MultipartyUnsupported => 28,
Self::ClccDirectionMismatch => 29,
Self::ClccNumberMismatch => 30,
Self::TickZero => 31,
Self::TickTooLarge => 32,
Self::NoCallForTick => 33,
Self::DurationOverflow => 34,
Self::CounterOverflow => 35,
}
}
}
fn slot_index(idx: u8) -> Result<usize, G8lS558VoiceCallError> {
if idx == 0 || idx as usize > S558_MAX_CALLS {
return Err(G8lS558VoiceCallError::InvalidClccIndex);
}
Ok(idx as usize - 1)
}
fn count_phase(calls: &[Option<G8lS558Call>; S558_MAX_CALLS], phase: G8lS558CallPhase) -> u8 {
calls
.iter()
.flatten()
.filter(|call| call.phase == phase)
.count() as u8
}
fn parse_u8_field(field: &str, error: G8lS558VoiceCallError) -> Result<u8, G8lS558VoiceCallError> {
if field.is_empty() || field.len() > 3 {
return Err(error);
}
let mut value: u16 = 0;
for byte in field.bytes() {
if !byte.is_ascii_digit() {
return Err(error);
}
value = value * 10 + u16::from(byte - b'0');
}
u8::try_from(value).map_err(|_| error)
}
fn strip_quotes(field: &str, error: G8lS558VoiceCallError) -> Result<&str, G8lS558VoiceCallError> {
field
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
.ok_or(error)
}
/// Validates a DTMF tone byte (`0`..`9`, `*`, `#`, `A`..`D`).
pub const fn dtmf_tone_is_valid(tone: u8) -> bool {
matches!(tone, b'0'..=b'9' | b'*' | b'#' | b'A'..=b'D')
}
/// Validates the tone and the duration (in 1/10 s units) of one `+VTS` tone.
pub fn validate_dtmf(tone: u8, duration: u8) -> Result<(), G8lS558VoiceCallError> {
if !dtmf_tone_is_valid(tone) {
return Err(G8lS558VoiceCallError::InvalidDtmfTone);
}
if !(S558_DTMF_DURATION_MIN..=S558_DTMF_DURATION_MAX).contains(&duration) {
return Err(G8lS558VoiceCallError::InvalidDtmfDuration);
}
Ok(())
}
/// Parses one `+CLCC:` response line.
pub fn parse_clcc_line(line: &str) -> Result<G8lS558ClccEntry, G8lS558VoiceCallError> {
let body = line
.strip_prefix("+CLCC: ")
.ok_or(G8lS558VoiceCallError::InvalidClccLine)?;
let mut fields = [""; 7];
let mut count = 0usize;
for field in body.split(',') {
if count == fields.len() {
return Err(G8lS558VoiceCallError::InvalidClccLine);
}
fields[count] = field;
count += 1;
}
if count != 5 && count != 7 {
return Err(G8lS558VoiceCallError::InvalidClccLine);
}
let idx = parse_u8_field(fields[0], G8lS558VoiceCallError::InvalidClccIndex)?;
slot_index(idx)?;
let direction = parse_u8_field(fields[1], G8lS558VoiceCallError::InvalidClccDirection)
.and_then(|code| {
G8lS558CallDirection::from_code(code).ok_or(G8lS558VoiceCallError::InvalidClccDirection)
})?;
let stat =
parse_u8_field(fields[2], G8lS558VoiceCallError::InvalidClccStat).and_then(|code| {
G8lS558ClccStat::from_code(code).ok_or(G8lS558VoiceCallError::InvalidClccStat)
})?;
let mode = parse_u8_field(fields[3], G8lS558VoiceCallError::InvalidClccMode)?;
if mode != S558_CLCC_MODE_VOICE {
return Err(G8lS558VoiceCallError::InvalidClccMode);
}
let multiparty = match parse_u8_field(fields[4], G8lS558VoiceCallError::InvalidClccMultiparty)?
{
0 => false,
1 => true,
_ => return Err(G8lS558VoiceCallError::InvalidClccMultiparty),
};
let (number, type_of_address) = if count == 7 {
let text = strip_quotes(fields[5], G8lS558VoiceCallError::InvalidClccLine)?;
let number = G8lS558CallNumber::from_text(text)?;
let toa = parse_u8_field(fields[6], G8lS558VoiceCallError::InvalidClccTypeOfAddress)?;
let expected_international = toa == S558_TOA_INTERNATIONAL;
let known = matches!(
toa,
S558_TOA_INTERNATIONAL | S558_TOA_UNKNOWN | S558_TOA_NATIONAL
);
if !known || expected_international != number.is_international() {
return Err(G8lS558VoiceCallError::InvalidClccTypeOfAddress);
}
(Some(number), toa)
} else {
(None, 0)
};
Ok(G8lS558ClccEntry {
idx,
direction,
stat,
mode,
multiparty,
number,
type_of_address,
})
}
/// Parses one `+CLIP: "<number>",<type>[,...]` unsolicited result code.
pub fn parse_clip_line(line: &str) -> Result<G8lS558CallNumber, G8lS558VoiceCallError> {
let body = line
.strip_prefix("+CLIP: ")
.ok_or(G8lS558VoiceCallError::InvalidClipLine)?;
let mut fields = body.split(',');
let quoted = fields
.next()
.ok_or(G8lS558VoiceCallError::InvalidClipLine)?;
let toa_field = fields
.next()
.ok_or(G8lS558VoiceCallError::InvalidClipLine)?;
let text = strip_quotes(quoted, G8lS558VoiceCallError::InvalidClipLine)?;
let number = G8lS558CallNumber::from_text(text)?;
let toa = parse_u8_field(toa_field, G8lS558VoiceCallError::InvalidClipLine)?;
if (toa == S558_TOA_INTERNATIONAL) != number.is_international() {
return Err(G8lS558VoiceCallError::InvalidClipLine);
}
Ok(number)
}
/// `RING` unsolicited result code recogniser.
pub fn is_ring_urc(line: &str) -> bool {
line == "RING"
}
pub fn encode_atd(number: G8lS558CallNumber) -> String {
format!("ATD{};", number.dial_string())
}
pub const fn encode_ata() -> &'static str {
"ATA"
}
pub const fn encode_ath() -> &'static str {
"ATH"
}
pub const fn encode_chup() -> &'static str {
"AT+CHUP"
}
pub const fn encode_clcc_query() -> &'static str {
"AT+CLCC"
}
pub fn encode_vts(tone: u8, duration: u8) -> Result<String, G8lS558VoiceCallError> {
validate_dtmf(tone, duration)?;
Ok(format!("AT+VTS={},{}", tone as char, duration))
}
struct Next {
calls: [Option<G8lS558Call>; S558_MAX_CALLS],
call_idx: u8,
from: G8lS558CallPhase,
to: G8lS558CallPhase,
missed_calls: u32,
completed_calls: u32,
dtmf_tones_sent: u32,
}
fn free_slot(
calls: &[Option<G8lS558Call>; S558_MAX_CALLS],
) -> Result<usize, G8lS558VoiceCallError> {
calls
.iter()
.position(Option::is_none)
.ok_or(G8lS558VoiceCallError::NoCallSlot)
}
fn occupied_call(
calls: &[Option<G8lS558Call>; S558_MAX_CALLS],
idx: u8,
) -> Result<(usize, G8lS558Call), G8lS558VoiceCallError> {
let slot = slot_index(idx)?;
calls[slot]
.map(|call| (slot, call))
.ok_or(G8lS558VoiceCallError::NoSuchCall)
}
fn any_setup_in_progress(calls: &[Option<G8lS558Call>; S558_MAX_CALLS]) -> bool {
calls.iter().flatten().any(|call| call.phase.is_setup())
}
fn next_state(
state: &G8lS558VoiceCallState,
event: G8lS558Event,
) -> Result<Next, G8lS558VoiceCallError> {
let mut next = Next {
calls: state.calls,
call_idx: 0,
from: G8lS558CallPhase::Idle,
to: G8lS558CallPhase::Idle,
missed_calls: state.missed_calls,
completed_calls: state.completed_calls,
dtmf_tones_sent: state.dtmf_tones_sent,
};
match event {
G8lS558Event::Dial(number) => {
if any_setup_in_progress(&next.calls) {
return Err(G8lS558VoiceCallError::CallSetupAlreadyInProgress);
}
if count_phase(&next.calls, G8lS558CallPhase::Active) >= S558_MAX_ACTIVE_CALLS {
return Err(G8lS558VoiceCallError::ActiveCallMustBeHeld);
}
let slot = free_slot(&next.calls)?;
next.call_idx = slot as u8 + 1;
next.to = G8lS558CallPhase::Dialing;
next.calls[slot] = Some(G8lS558Call {
idx: next.call_idx,
direction: G8lS558CallDirection::MobileOriginated,
phase: G8lS558CallPhase::Dialing,
number: Some(number),
setup_ticks: 0,
duration_ticks: 0,
});
}
G8lS558Event::Ring { number } => {
if any_setup_in_progress(&next.calls) {
return Err(G8lS558VoiceCallError::CallSetupAlreadyInProgress);
}
let slot = free_slot(&next.calls)?;
let busy = count_phase(&next.calls, G8lS558CallPhase::Active)
+ count_phase(&next.calls, G8lS558CallPhase::Held);
next.call_idx = slot as u8 + 1;
next.to = if busy > 0 {
G8lS558CallPhase::Waiting
} else {
G8lS558CallPhase::Incoming
};
next.calls[slot] = Some(G8lS558Call {
idx: next.call_idx,
direction: G8lS558CallDirection::MobileTerminated,
phase: next.to,
number,
setup_ticks: 0,
duration_ticks: 0,
});
}
G8lS558Event::Answer => {
let (slot, call) = next
.calls
.iter()
.enumerate()
.find_map(|(slot, call)| {
call.filter(|call| {
matches!(
call.phase,
G8lS558CallPhase::Incoming | G8lS558CallPhase::Waiting
)
})
.map(|call| (slot, call))
})
.ok_or(G8lS558VoiceCallError::NoIncomingCall)?;
if count_phase(&next.calls, G8lS558CallPhase::Active) >= S558_MAX_ACTIVE_CALLS {
return Err(G8lS558VoiceCallError::ActiveCallMustBeHeld);
}
next.call_idx = call.idx;
next.from = call.phase;
next.to = G8lS558CallPhase::Active;
next.calls[slot] = Some(G8lS558Call {
phase: G8lS558CallPhase::Active,
..call
});
}
G8lS558Event::Hangup { idx } => {
let (slot, call) = occupied_call(&next.calls, idx)?;
if call.phase == G8lS558CallPhase::Disconnecting {
return Err(G8lS558VoiceCallError::IllegalTransition);
}
next.call_idx = idx;
next.from = call.phase;
next.to = G8lS558CallPhase::Disconnecting;
next.calls[slot] = Some(G8lS558Call {
phase: G8lS558CallPhase::Disconnecting,
..call
});
}
G8lS558Event::Released { idx } => {
let (slot, call) = occupied_call(&next.calls, idx)?;
if call.phase != G8lS558CallPhase::Disconnecting {
return Err(G8lS558VoiceCallError::IllegalTransition);
}
next.call_idx = idx;
next.from = call.phase;
next.to = G8lS558CallPhase::Idle;
next.completed_calls = next
.completed_calls
.checked_add(1)
.ok_or(G8lS558VoiceCallError::CounterOverflow)?;
next.calls[slot] = None;
}
G8lS558Event::Hold { idx } => {
let (slot, call) = occupied_call(&next.calls, idx)?;
if call.phase != G8lS558CallPhase::Active {
return Err(G8lS558VoiceCallError::IllegalTransition);
}
if count_phase(&next.calls, G8lS558CallPhase::Held) >= S558_MAX_HELD_CALLS {
return Err(G8lS558VoiceCallError::HeldCallAlreadyPresent);
}
next.call_idx = idx;
next.from = call.phase;
next.to = G8lS558CallPhase::Held;
next.calls[slot] = Some(G8lS558Call {
phase: G8lS558CallPhase::Held,
..call
});
}
G8lS558Event::Resume { idx } => {
let (slot, call) = occupied_call(&next.calls, idx)?;
if call.phase != G8lS558CallPhase::Held {
return Err(G8lS558VoiceCallError::IllegalTransition);
}
if count_phase(&next.calls, G8lS558CallPhase::Active) >= S558_MAX_ACTIVE_CALLS {
return Err(G8lS558VoiceCallError::ActiveCallAlreadyPresent);
}
next.call_idx = idx;
next.from = call.phase;
next.to = G8lS558CallPhase::Active;
next.calls[slot] = Some(G8lS558Call {
phase: G8lS558CallPhase::Active,
..call
});
}
G8lS558Event::Dtmf { tone, duration } => {
validate_dtmf(tone, duration)?;
let call = next
.calls
.iter()
.flatten()
.find(|call| call.phase == G8lS558CallPhase::Active)
.copied()
.ok_or(G8lS558VoiceCallError::NoActiveCallForDtmf)?;
next.call_idx = call.idx;
next.from = G8lS558CallPhase::Active;
next.to = G8lS558CallPhase::Active;
next.dtmf_tones_sent = next
.dtmf_tones_sent
.checked_add(1)
.ok_or(G8lS558VoiceCallError::CounterOverflow)?;
}
G8lS558Event::Tick(ticks) => {
if ticks == 0 {
return Err(G8lS558VoiceCallError::TickZero);
}
if ticks > S558_MAX_TICK_STEP {
return Err(G8lS558VoiceCallError::TickTooLarge);
}
let mut primary: Option<(u8, G8lS558CallPhase, G8lS558CallPhase)> = None;
for slot in 0..S558_MAX_CALLS {
let Some(call) = next.calls[slot] else {
continue;
};
let updated = tick_call(call, ticks)?;
if primary.is_none() {
primary = Some((
call.idx,
call.phase,
updated.map_or(G8lS558CallPhase::Idle, |call| call.phase),
));
}
if updated.is_none() {
next.missed_calls = next
.missed_calls
.checked_add(1)
.ok_or(G8lS558VoiceCallError::CounterOverflow)?;
}
next.calls[slot] = updated;
}
let (idx, from, to) = primary.ok_or(G8lS558VoiceCallError::NoCallForTick)?;
next.call_idx = idx;
next.from = from;
next.to = to;
}
G8lS558Event::ClccReport(entry) => {
if entry.mode != S558_CLCC_MODE_VOICE {
return Err(G8lS558VoiceCallError::InvalidClccMode);
}
if entry.multiparty {
return Err(G8lS558VoiceCallError::MultipartyUnsupported);
}
let (slot, call) = occupied_call(&next.calls, entry.idx)?;
if entry.direction != call.direction {
return Err(G8lS558VoiceCallError::ClccDirectionMismatch);
}
let number = match (call.number, entry.number) {
(Some(known), Some(reported)) if known != reported => {
return Err(G8lS558VoiceCallError::ClccNumberMismatch)
}
(Some(known), _) => Some(known),
(None, reported) => reported,
};
let to = match (call.phase, entry.stat) {
(G8lS558CallPhase::Dialing, G8lS558ClccStat::Dialing) => G8lS558CallPhase::Dialing,
(G8lS558CallPhase::Dialing, G8lS558ClccStat::Alerting)
| (G8lS558CallPhase::Alerting, G8lS558ClccStat::Alerting) => {
G8lS558CallPhase::Alerting
}
(G8lS558CallPhase::Alerting, G8lS558ClccStat::Active)
| (G8lS558CallPhase::Active, G8lS558ClccStat::Active) => G8lS558CallPhase::Active,
(G8lS558CallPhase::Held, G8lS558ClccStat::Held) => G8lS558CallPhase::Held,
(G8lS558CallPhase::Incoming, G8lS558ClccStat::Incoming) => {
G8lS558CallPhase::Incoming
}
(G8lS558CallPhase::Waiting, G8lS558ClccStat::Waiting) => G8lS558CallPhase::Waiting,
_ => return Err(G8lS558VoiceCallError::IllegalTransition),
};
next.call_idx = entry.idx;
next.from = call.phase;
next.to = to;
next.calls[slot] = Some(G8lS558Call {
phase: to,
number,
..call
});
}
}
Ok(next)
}
/// Advances one call by `ticks`. Returns `None` when an unanswered incoming
/// call times out and its slot is released as a missed call.
fn tick_call(call: G8lS558Call, ticks: u32) -> Result<Option<G8lS558Call>, G8lS558VoiceCallError> {
match call.phase {
G8lS558CallPhase::Active | G8lS558CallPhase::Held => {
let duration_ticks = call
.duration_ticks
.checked_add(ticks)
.filter(|total| *total <= S558_MAX_CALL_DURATION_TICKS)
.ok_or(G8lS558VoiceCallError::DurationOverflow)?;
Ok(Some(G8lS558Call {
duration_ticks,
..call
}))
}
G8lS558CallPhase::Dialing | G8lS558CallPhase::Alerting => {
let setup_ticks = call
.setup_ticks
.checked_add(ticks)
.ok_or(G8lS558VoiceCallError::DurationOverflow)?;
let phase = if setup_ticks > S558_DIAL_TIMEOUT_TICKS {
G8lS558CallPhase::Disconnecting
} else {
call.phase
};
Ok(Some(G8lS558Call {
phase,
setup_ticks,
..call
}))
}
G8lS558CallPhase::Incoming | G8lS558CallPhase::Waiting => {
let setup_ticks = call
.setup_ticks
.checked_add(ticks)
.ok_or(G8lS558VoiceCallError::DurationOverflow)?;
if setup_ticks > S558_RING_TIMEOUT_TICKS {
return Ok(None);
}
Ok(Some(G8lS558Call {
setup_ticks,
..call
}))
}
G8lS558CallPhase::Idle | G8lS558CallPhase::Disconnecting => Ok(Some(call)),
}
}
/// Applies one event at ledger position `step`. `step` must equal the number
/// of published receipts (a new transition) or address an already published
/// receipt with the exact same event (retained replay). Any other input
/// fails closed without mutating the state.
pub fn service_s558_model_voice_call_transition(
state: &mut G8lS558VoiceCallState,
step: usize,
event: G8lS558Event,
) -> Result<G8lS558VoiceCallOutcome, G8lS558VoiceCallError> {
if let Some(published) = state.ledger.get(step).copied() {
if published.event != event || published.step != step {
return Err(G8lS558VoiceCallError::PublishedStateDrift);
}
return Ok(G8lS558VoiceCallOutcome::TransitionRetained(published));
}
if step != state.ledger.len() {
return Err(G8lS558VoiceCallError::StepOutOfOrder);
}
if state.ledger.len() >= S558_MAX_TRANSITIONS {
return Err(G8lS558VoiceCallError::LedgerFull);
}
let next = next_state(state, event)?;
let touched = slot_index(next.call_idx)
.ok()
.and_then(|slot| next.calls[slot]);
let receipt = G8lS558CallTransitionReceipt {
sequence: S558_SEQUENCE,
predecessor_sequence: S558_EXPECTED_PREDECESSOR,
step,
event,
call_idx: next.call_idx,
from: next.from,
to: next.to,
active_calls: count_phase(&next.calls, G8lS558CallPhase::Active),
held_calls: count_phase(&next.calls, G8lS558CallPhase::Held),
occupied_slots: next.calls.iter().filter(|call| call.is_some()).count() as u8,
setup_ticks: touched.map_or(0, |call| call.setup_ticks),
duration_ticks: touched.map_or(0, |call| call.duration_ticks),
missed_calls: next.missed_calls,
completed_calls: next.completed_calls,
dtmf_tones_sent: next.dtmf_tones_sent,
hardware_present: S558_HARDWARE_PRESENT,
physical_observations: S558_PHYSICAL_OBSERVATIONS,
runbook_executed: RUNBOOK_EXECUTED_IN_S558,
};
state.calls = next.calls;
state.missed_calls = next.missed_calls;
state.completed_calls = next.completed_calls;
state.dtmf_tones_sent = next.dtmf_tones_sent;
state.ledger.push(receipt);
Ok(G8lS558VoiceCallOutcome::TransitionPublished(receipt))
}
snippet sha256: bae7dba15c24…file sha256: bae7dba15c24…
02 · Doğrulayan test kodu
Operations komutuna bağlı focused test
tam dosyaL1–L662
simulation/tests/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s558_r1_voice_call_state_machine_model.rs::S558 r1 voice call state machine model focused tests
use aselsan_microkernel_simulation::g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s558_r1_voice_call_state_machine_model::*;
use std::collections::BTreeSet;
const SOURCE: &str = include_str!(
"../../kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s558_r1_voice_call_state_machine_model.rs"
);
const MAIN: &str = include_str!("../../kernel/src/main.rs");
const SIMULATION_LIB: &str = include_str!("../src/lib.rs");
type Outcome = Result<G8lS558VoiceCallOutcome, G8lS558VoiceCallError>;
fn number(text: &str) -> G8lS558CallNumber {
G8lS558CallNumber::from_text(text).unwrap()
}
fn apply(state: &mut G8lS558VoiceCallState, step: usize, event: G8lS558Event) -> Outcome {
service_s558_model_voice_call_transition(state, step, event)
}
fn published(outcome: Outcome) -> G8lS558CallTransitionReceipt {
match outcome.unwrap() {
G8lS558VoiceCallOutcome::TransitionPublished(receipt) => receipt,
other => panic!("expected publication, got {other:?}"),
}
}
fn clcc(idx: u8, direction: u8, stat: u8, number: Option<G8lS558CallNumber>) -> G8lS558ClccEntry {
G8lS558ClccEntry {
idx,
direction: G8lS558CallDirection::from_code(direction).unwrap(),
stat: G8lS558ClccStat::from_code(stat).unwrap(),
mode: S558_CLCC_MODE_VOICE,
multiparty: false,
number,
type_of_address: number.map_or(0, |number| number.type_of_address()),
}
}
/// Drives a mobile-originated call to `Active` on idx 1 and returns the next step.
fn establish_outgoing(state: &mut G8lS558VoiceCallState, text: &str) -> usize {
let dialed = number(text);
published(apply(state, 0, G8lS558Event::Dial(dialed)));
published(apply(state, 1, G8lS558Event::ClccReport(clcc(1, 0, 3, Some(dialed)))));
published(apply(state, 2, G8lS558Event::ClccReport(clcc(1, 0, 0, Some(dialed)))));
assert_eq!(state.phase(1), G8lS558CallPhase::Active);
3
}
#[test]
fn sequence_scope_and_nonpromotion_are_exact() {
assert_eq!(S558_SEQUENCE, 558);
assert_eq!(S558_EXPECTED_PREDECESSOR, 557);
assert_eq!(S558_R1_STAGE, 3);
assert_eq!(S558_R1_RANGE_FIRST, 536);
assert_eq!(S558_R1_RANGE_LAST, 568);
assert_eq!(S558_NUMBER_MIN_DIGITS, 3);
assert_eq!(S558_NUMBER_MAX_DIGITS, 20);
assert_eq!(S558_MAX_CALLS, 2);
assert_eq!(S558_MAX_ACTIVE_CALLS, 1);
assert_eq!(S558_MAX_HELD_CALLS, 1);
assert_eq!(S558_CLCC_STAT_MAX, 5);
assert_eq!(S558_DTMF_DURATION_MIN, 1);
assert_eq!(S558_DTMF_DURATION_MAX, 100);
assert_eq!(S558_DIAL_TIMEOUT_TICKS, 90);
assert_eq!(S558_RING_TIMEOUT_TICKS, 45);
assert_eq!(S558_MAX_CALL_DURATION_TICKS, 86_400);
assert_eq!(S558_MAX_TRANSITIONS, 96);
assert_eq!(S558_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS, 0);
assert_eq!(S558_PHYSICAL_OBSERVATIONS, 0);
assert_eq!(S558_PHYSICAL_OR_DEVICE_OPERATIONS, 0);
assert_eq!(S558_SD_WRITES, 0);
assert_eq!(S558_UART_OPENS, 0);
assert_eq!(S558_POWER_TRANSITIONS, 0);
assert_eq!(S558_NEW_IMMUTABLE_RAW_CAPTURES, 0);
assert!(S558_S540_PHYSICAL_VERDICT_RETAINED_RED);
assert!(S558_S543_PHYSICAL_VERDICT_RETAINED_RED);
assert!(!S558_AUTOMATIC_PROMOTION);
assert!(!S558_BOOT_TO_UI_PHYSICALLY_OBSERVED);
assert!(!S558_HARDWARE_PRESENT);
assert!(!S558_R1_ACCEPTANCE_COMPLETE);
assert!(!RUNBOOK_EXECUTED_IN_S558);
}
#[test]
fn module_is_registered_in_kernel_and_simulation() {
let module = "g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s558_r1_voice_call_state_machine_model";
assert!(MAIN.contains(&format!("mod {module};")));
assert!(SIMULATION_LIB.contains(&format!("pub mod {module};")));
}
#[test]
fn source_has_no_device_execution_or_uart_emission_surface() {
for forbidden in [
"unsafe",
"asm!",
"write_volatile",
"crate::uart",
"crate::arch",
"#[no_mangle]",
"spin::",
"std::",
"std::process::Command",
"/dev/cu.",
"TIOCEXCL",
"crate::kprintln!",
] {
assert!(!SOURCE.contains(forbidden), "forbidden token: {forbidden}");
}
assert!(SOURCE.contains("performs no device operation"));
assert!(SOURCE.contains("does not rerun S540 or S543"));
assert!(SOURCE.contains("S558_HARDWARE_PRESENT: bool = false"));
assert!(SOURCE.contains("S558_R1_ACCEPTANCE_COMPLETE: bool = false"));
assert!(SOURCE.contains("RUNBOOK_EXECUTED_IN_S558: bool = false"));
}
#[test]
fn diagnostic_codes_are_nonzero_and_unique() {
let errors = [
G8lS558VoiceCallError::StepOutOfOrder,
G8lS558VoiceCallError::PublishedStateDrift,
G8lS558VoiceCallError::LedgerFull,
G8lS558VoiceCallError::NumberEmpty,
G8lS558VoiceCallError::NumberTooShort,
G8lS558VoiceCallError::NumberTooLong,
G8lS558VoiceCallError::NumberInvalidCharacter,
G8lS558VoiceCallError::NumberMisplacedPlus,
G8lS558VoiceCallError::InvalidClccLine,
G8lS558VoiceCallError::InvalidClccIndex,
G8lS558VoiceCallError::InvalidClccDirection,
G8lS558VoiceCallError::InvalidClccStat,
G8lS558VoiceCallError::InvalidClccMode,
G8lS558VoiceCallError::InvalidClccMultiparty,
G8lS558VoiceCallError::InvalidClccTypeOfAddress,
G8lS558VoiceCallError::InvalidClipLine,
G8lS558VoiceCallError::InvalidDtmfTone,
G8lS558VoiceCallError::InvalidDtmfDuration,
G8lS558VoiceCallError::NoCallSlot,
G8lS558VoiceCallError::CallSetupAlreadyInProgress,
G8lS558VoiceCallError::NoSuchCall,
G8lS558VoiceCallError::NoIncomingCall,
G8lS558VoiceCallError::IllegalTransition,
G8lS558VoiceCallError::ActiveCallMustBeHeld,
G8lS558VoiceCallError::HeldCallAlreadyPresent,
G8lS558VoiceCallError::ActiveCallAlreadyPresent,
G8lS558VoiceCallError::NoActiveCallForDtmf,
G8lS558VoiceCallError::MultipartyUnsupported,
G8lS558VoiceCallError::ClccDirectionMismatch,
G8lS558VoiceCallError::ClccNumberMismatch,
G8lS558VoiceCallError::TickZero,
G8lS558VoiceCallError::TickTooLarge,
G8lS558VoiceCallError::NoCallForTick,
G8lS558VoiceCallError::DurationOverflow,
G8lS558VoiceCallError::CounterOverflow,
];
let codes: BTreeSet<_> = errors
.into_iter()
.map(G8lS558VoiceCallError::diagnostic_code)
.collect();
assert_eq!(codes.len(), errors.len());
assert_eq!(codes.len(), 35);
assert!(!codes.contains(&0));
}
#[test]
fn exact_replay_retains_the_same_receipt() {
let mut state = G8lS558VoiceCallState::new();
let dialed = number("+905551234567");
let receipt = published(apply(&mut state, 0, G8lS558Event::Dial(dialed)));
assert_eq!(
apply(&mut state, 0, G8lS558Event::Dial(dialed)),
Ok(G8lS558VoiceCallOutcome::TransitionRetained(receipt))
);
assert_eq!(state.receipts().len(), 1);
assert_eq!(state.last_receipt(), Some(receipt));
assert_eq!(receipt.sequence, S558_SEQUENCE);
assert_eq!(receipt.predecessor_sequence, S558_EXPECTED_PREDECESSOR);
assert!(!receipt.hardware_present);
assert_eq!(receipt.physical_observations, 0);
assert!(!receipt.runbook_executed);
}
#[test]
fn divergent_input_after_publication_fails_closed() {
let mut state = G8lS558VoiceCallState::new();
published(apply(&mut state, 0, G8lS558Event::Dial(number("+905551234567"))));
assert_eq!(
apply(&mut state, 0, G8lS558Event::Dial(number("+905557654321"))),
Err(G8lS558VoiceCallError::PublishedStateDrift)
);
assert_eq!(
apply(&mut state, 0, G8lS558Event::Answer),
Err(G8lS558VoiceCallError::PublishedStateDrift)
);
assert_eq!(state.receipts().len(), 1);
assert_eq!(state.phase(1), G8lS558CallPhase::Dialing);
}
#[test]
fn number_validation_accepts_e164_and_rejects_malformed_input() {
let international = number("+905551234567");
assert!(international.is_international());
assert_eq!(international.digits(), "905551234567");
assert_eq!(international.len(), 12);
assert_eq!(international.type_of_address(), S558_TOA_INTERNATIONAL);
assert_eq!(international.dial_string(), "+905551234567");
let national = number("05551234567");
assert!(!national.is_international());
assert_eq!(national.type_of_address(), S558_TOA_UNKNOWN);
assert_eq!(national.dial_string(), "05551234567");
assert_eq!(number("112").len(), 3);
let twenty = "1".repeat(20);
assert_eq!(number(&twenty).len(), 20);
assert_eq!(number(&format!("+{twenty}")).len(), 20);
let twenty_one = "1".repeat(21);
assert_eq!(
G8lS558CallNumber::from_text(&twenty_one),
Err(G8lS558VoiceCallError::NumberTooLong)
);
assert_eq!(
G8lS558CallNumber::from_text(&format!("+{twenty_one}")),
Err(G8lS558VoiceCallError::NumberTooLong)
);
assert_eq!(
G8lS558CallNumber::from_text(""),
Err(G8lS558VoiceCallError::NumberEmpty)
);
assert_eq!(
G8lS558CallNumber::from_text("+"),
Err(G8lS558VoiceCallError::NumberTooShort)
);
assert_eq!(
G8lS558CallNumber::from_text("12"),
Err(G8lS558VoiceCallError::NumberTooShort)
);
assert_eq!(
G8lS558CallNumber::from_text("+90+555"),
Err(G8lS558VoiceCallError::NumberMisplacedPlus)
);
for malformed in ["555-1234", "555 1234", "555;1234", "ABC123", "+90555123456a"] {
assert_eq!(
G8lS558CallNumber::from_text(malformed),
Err(G8lS558VoiceCallError::NumberInvalidCharacter),
"{malformed}"
);
}
}
#[test]
fn mobile_originated_call_walks_dialing_alerting_active_disconnecting_idle() {
let mut state = G8lS558VoiceCallState::new();
let dialed = number("+905551234567");
let dial = published(apply(&mut state, 0, G8lS558Event::Dial(dialed)));
assert_eq!((dial.call_idx, dial.from, dial.to), (1, G8lS558CallPhase::Idle, G8lS558CallPhase::Dialing));
assert_eq!(dial.occupied_slots, 1);
let call = state.call(1).unwrap();
assert_eq!(call.direction, G8lS558CallDirection::MobileOriginated);
assert_eq!(call.number, Some(dialed));
let alerting = published(apply(&mut state, 1, G8lS558Event::ClccReport(clcc(1, 0, 3, Some(dialed)))));
assert_eq!((alerting.from, alerting.to), (G8lS558CallPhase::Dialing, G8lS558CallPhase::Alerting));
let active = published(apply(&mut state, 2, G8lS558Event::ClccReport(clcc(1, 0, 0, None))));
assert_eq!((active.from, active.to), (G8lS558CallPhase::Alerting, G8lS558CallPhase::Active));
assert_eq!(active.active_calls, 1);
assert_eq!(state.call(1).unwrap().number, Some(dialed));
let tick = published(apply(&mut state, 3, G8lS558Event::Tick(42)));
assert_eq!(tick.duration_ticks, 42);
let hangup = published(apply(&mut state, 4, G8lS558Event::Hangup { idx: 1 }));
assert_eq!((hangup.from, hangup.to), (G8lS558CallPhase::Active, G8lS558CallPhase::Disconnecting));
assert_eq!(hangup.active_calls, 0);
assert_eq!(hangup.duration_ticks, 42);
let released = published(apply(&mut state, 5, G8lS558Event::Released { idx: 1 }));
assert_eq!((released.from, released.to), (G8lS558CallPhase::Disconnecting, G8lS558CallPhase::Idle));
assert_eq!(released.occupied_slots, 0);
assert_eq!(released.completed_calls, 1);
assert_eq!(state.phase(1), G8lS558CallPhase::Idle);
assert_eq!(state.call(1), None);
assert_eq!(state.receipts().len(), 6);
for (position, receipt) in state.receipts().iter().enumerate() {
assert_eq!(receipt.step, position);
}
}
#[test]
fn incoming_ring_with_clip_answers_into_active() {
assert!(is_ring_urc("RING"));
assert!(!is_ring_urc("RINGING"));
let caller = parse_clip_line("+CLIP: \"+905559876543\",145").unwrap();
assert_eq!(caller, number("+905559876543"));
assert_eq!(parse_clip_line("+CLIP: \"05559876543\",129,,,,0").unwrap(), number("05559876543"));
assert_eq!(parse_clip_line("+CLIP: +905559876543,145"), Err(G8lS558VoiceCallError::InvalidClipLine));
assert_eq!(parse_clip_line("+CLIP: \"+905559876543\",129"), Err(G8lS558VoiceCallError::InvalidClipLine));
assert_eq!(parse_clip_line("+CLIP: \"+905559876543\""), Err(G8lS558VoiceCallError::InvalidClipLine));
assert_eq!(parse_clip_line("RING"), Err(G8lS558VoiceCallError::InvalidClipLine));
let mut state = G8lS558VoiceCallState::new();
let ring = published(apply(&mut state, 0, G8lS558Event::Ring { number: None }));
assert_eq!((ring.from, ring.to), (G8lS558CallPhase::Idle, G8lS558CallPhase::Incoming));
assert_eq!(state.call(1).unwrap().direction, G8lS558CallDirection::MobileTerminated);
let clip = published(apply(&mut state, 1, G8lS558Event::ClccReport(clcc(1, 1, 4, Some(caller)))));
assert_eq!((clip.from, clip.to), (G8lS558CallPhase::Incoming, G8lS558CallPhase::Incoming));
assert_eq!(state.call(1).unwrap().number, Some(caller));
published(apply(&mut state, 2, G8lS558Event::Tick(10)));
let answer = published(apply(&mut state, 3, G8lS558Event::Answer));
assert_eq!((answer.call_idx, answer.from, answer.to), (1, G8lS558CallPhase::Incoming, G8lS558CallPhase::Active));
assert_eq!(answer.setup_ticks, 10);
assert_eq!(answer.duration_ticks, 0);
assert_eq!(answer.active_calls, 1);
assert_eq!(
apply(&mut state, 4, G8lS558Event::Answer),
Err(G8lS558VoiceCallError::NoIncomingCall)
);
}
#[test]
fn clcc_line_parsing_maps_all_stat_codes_and_rejects_malformed_lines() {
for (code, stat, text) in [
(0, G8lS558ClccStat::Active, "active"),
(1, G8lS558ClccStat::Held, "held"),
(2, G8lS558ClccStat::Dialing, "dialing"),
(3, G8lS558ClccStat::Alerting, "alerting"),
(4, G8lS558ClccStat::Incoming, "incoming"),
(5, G8lS558ClccStat::Waiting, "waiting"),
] {
assert_eq!(G8lS558ClccStat::from_code(code), Some(stat));
assert_eq!(stat.code(), code);
assert_eq!(stat.text(), text);
let line = format!("+CLCC: 1,0,{code},0,0,\"+905551234567\",145");
let entry = parse_clcc_line(&line).unwrap();
assert_eq!(entry.stat, stat);
assert_eq!(entry.idx, 1);
assert_eq!(entry.direction, G8lS558CallDirection::MobileOriginated);
assert_eq!(entry.number, Some(number("+905551234567")));
assert_eq!(entry.type_of_address, 145);
}
assert_eq!(G8lS558ClccStat::from_code(6), None);
assert_eq!(G8lS558CallDirection::from_code(2), None);
let short = parse_clcc_line("+CLCC: 2,1,4,0,0").unwrap();
assert_eq!(short.idx, 2);
assert_eq!(short.direction, G8lS558CallDirection::MobileTerminated);
assert_eq!(short.number, None);
assert_eq!(short.type_of_address, 0);
assert!(parse_clcc_line("+CLCC: 1,0,0,0,1").unwrap().multiparty);
assert_eq!(parse_clcc_line("+CLCC: 1,0,0,0,0,\"05551234567\",161").unwrap().type_of_address, 161);
for (line, error) in [
("+CLCC:1,0,0,0,0", G8lS558VoiceCallError::InvalidClccLine),
("+CLCC: 1,0,0,0", G8lS558VoiceCallError::InvalidClccLine),
("+CLCC: 1,0,0,0,0,\"+905551234567\"", G8lS558VoiceCallError::InvalidClccLine),
("+CLCC: 1,0,0,0,0,\"+905551234567\",145,0,0", G8lS558VoiceCallError::InvalidClccLine),
("+CLCC: 1,0,0,0,0,+905551234567,145", G8lS558VoiceCallError::InvalidClccLine),
("+CLCC: 0,0,0,0,0", G8lS558VoiceCallError::InvalidClccIndex),
("+CLCC: 3,0,0,0,0", G8lS558VoiceCallError::InvalidClccIndex),
("+CLCC: 999,0,0,0,0", G8lS558VoiceCallError::InvalidClccIndex),
("+CLCC: x,0,0,0,0", G8lS558VoiceCallError::InvalidClccIndex),
("+CLCC: 1,2,0,0,0", G8lS558VoiceCallError::InvalidClccDirection),
("+CLCC: 1,0,6,0,0", G8lS558VoiceCallError::InvalidClccStat),
("+CLCC: 1,0,0,1,0", G8lS558VoiceCallError::InvalidClccMode),
("+CLCC: 1,0,0,0,2", G8lS558VoiceCallError::InvalidClccMultiparty),
("+CLCC: 1,0,0,0,0,\"+905551234567\",129", G8lS558VoiceCallError::InvalidClccTypeOfAddress),
("+CLCC: 1,0,0,0,0,\"05551234567\",145", G8lS558VoiceCallError::InvalidClccTypeOfAddress),
("+CLCC: 1,0,0,0,0,\"05551234567\",200", G8lS558VoiceCallError::InvalidClccTypeOfAddress),
("+CLCC: 1,0,0,0,0,\"555-1234\",129", G8lS558VoiceCallError::NumberInvalidCharacter),
("+CREG: 0,1", G8lS558VoiceCallError::InvalidClccLine),
] {
assert_eq!(parse_clcc_line(line), Err(error), "{line}");
}
}
#[test]
fn illegal_transitions_fail_closed_without_mutation() {
let mut state = G8lS558VoiceCallState::new();
let dialed = number("+905551234567");
assert_eq!(apply(&mut state, 0, G8lS558Event::Answer), Err(G8lS558VoiceCallError::NoIncomingCall));
assert_eq!(apply(&mut state, 0, G8lS558Event::Hangup { idx: 1 }), Err(G8lS558VoiceCallError::NoSuchCall));
assert_eq!(apply(&mut state, 0, G8lS558Event::Hangup { idx: 0 }), Err(G8lS558VoiceCallError::InvalidClccIndex));
assert_eq!(apply(&mut state, 0, G8lS558Event::Hangup { idx: 3 }), Err(G8lS558VoiceCallError::InvalidClccIndex));
assert_eq!(apply(&mut state, 0, G8lS558Event::Released { idx: 1 }), Err(G8lS558VoiceCallError::NoSuchCall));
assert_eq!(apply(&mut state, 0, G8lS558Event::Hold { idx: 1 }), Err(G8lS558VoiceCallError::NoSuchCall));
assert_eq!(apply(&mut state, 0, G8lS558Event::Tick(1)), Err(G8lS558VoiceCallError::NoCallForTick));
assert!(state.receipts().is_empty());
published(apply(&mut state, 0, G8lS558Event::Dial(dialed)));
// Dialing -> Active directly (skipping Alerting) is illegal.
assert_eq!(
apply(&mut state, 1, G8lS558Event::ClccReport(clcc(1, 0, 0, Some(dialed)))),
Err(G8lS558VoiceCallError::IllegalTransition)
);
// Dialing -> Held / Incoming / Waiting are illegal.
for stat in [1, 4, 5] {
assert_eq!(
apply(&mut state, 1, G8lS558Event::ClccReport(clcc(1, 0, stat, Some(dialed)))),
Err(G8lS558VoiceCallError::IllegalTransition)
);
}
assert_eq!(apply(&mut state, 1, G8lS558Event::Released { idx: 1 }), Err(G8lS558VoiceCallError::IllegalTransition));
assert_eq!(apply(&mut state, 1, G8lS558Event::Hold { idx: 1 }), Err(G8lS558VoiceCallError::IllegalTransition));
assert_eq!(apply(&mut state, 1, G8lS558Event::Resume { idx: 1 }), Err(G8lS558VoiceCallError::IllegalTransition));
assert_eq!(apply(&mut state, 1, G8lS558Event::Dial(dialed)), Err(G8lS558VoiceCallError::CallSetupAlreadyInProgress));
assert_eq!(apply(&mut state, 1, G8lS558Event::Ring { number: None }), Err(G8lS558VoiceCallError::CallSetupAlreadyInProgress));
assert_eq!(
apply(&mut state, 1, G8lS558Event::ClccReport(clcc(1, 1, 2, Some(dialed)))),
Err(G8lS558VoiceCallError::ClccDirectionMismatch)
);
assert_eq!(
apply(&mut state, 1, G8lS558Event::ClccReport(clcc(1, 0, 2, Some(number("+905550000000"))))),
Err(G8lS558VoiceCallError::ClccNumberMismatch)
);
assert_eq!(
apply(&mut state, 1, G8lS558Event::ClccReport(clcc(2, 0, 2, Some(dialed)))),
Err(G8lS558VoiceCallError::NoSuchCall)
);
let mut multiparty = clcc(1, 0, 2, Some(dialed));
multiparty.multiparty = true;
assert_eq!(
apply(&mut state, 1, G8lS558Event::ClccReport(multiparty)),
Err(G8lS558VoiceCallError::MultipartyUnsupported)
);
let mut data_mode = clcc(1, 0, 2, Some(dialed));
data_mode.mode = 1;
assert_eq!(
apply(&mut state, 1, G8lS558Event::ClccReport(data_mode)),
Err(G8lS558VoiceCallError::InvalidClccMode)
);
assert_eq!(state.receipts().len(), 1);
assert_eq!(state.phase(1), G8lS558CallPhase::Dialing);
assert_eq!(state.call(2), None);
// Disconnecting accepts only Released.
published(apply(&mut state, 1, G8lS558Event::Hangup { idx: 1 }));
assert_eq!(apply(&mut state, 2, G8lS558Event::Hangup { idx: 1 }), Err(G8lS558VoiceCallError::IllegalTransition));
assert_eq!(
apply(&mut state, 2, G8lS558Event::ClccReport(clcc(1, 0, 2, Some(dialed)))),
Err(G8lS558VoiceCallError::IllegalTransition)
);
assert_eq!(state.phase(1), G8lS558CallPhase::Disconnecting);
}
#[test]
fn bounded_concurrency_allows_one_active_plus_one_held_only() {
let mut state = G8lS558VoiceCallState::new();
let step = establish_outgoing(&mut state, "+905551234567");
let second = number("+905557654321");
assert_eq!(apply(&mut state, step, G8lS558Event::Dial(second)), Err(G8lS558VoiceCallError::ActiveCallMustBeHeld));
let hold = published(apply(&mut state, step, G8lS558Event::Hold { idx: 1 }));
assert_eq!((hold.from, hold.to, hold.active_calls, hold.held_calls), (G8lS558CallPhase::Active, G8lS558CallPhase::Held, 0, 1));
let dial = published(apply(&mut state, step + 1, G8lS558Event::Dial(second)));
assert_eq!((dial.call_idx, dial.to, dial.occupied_slots), (2, G8lS558CallPhase::Dialing, 2));
published(apply(&mut state, step + 2, G8lS558Event::ClccReport(clcc(2, 0, 3, Some(second)))));
let active = published(apply(&mut state, step + 3, G8lS558Event::ClccReport(clcc(2, 0, 0, Some(second)))));
assert_eq!((active.active_calls, active.held_calls, active.occupied_slots), (1, 1, 2));
assert_eq!(apply(&mut state, step + 4, G8lS558Event::Hold { idx: 2 }), Err(G8lS558VoiceCallError::HeldCallAlreadyPresent));
assert_eq!(apply(&mut state, step + 4, G8lS558Event::Resume { idx: 1 }), Err(G8lS558VoiceCallError::ActiveCallAlreadyPresent));
assert_eq!(apply(&mut state, step + 4, G8lS558Event::Ring { number: None }), Err(G8lS558VoiceCallError::NoCallSlot));
assert_eq!(apply(&mut state, step + 4, G8lS558Event::Dial(number("112"))), Err(G8lS558VoiceCallError::ActiveCallMustBeHeld));
let tick = published(apply(&mut state, step + 4, G8lS558Event::Tick(5)));
assert_eq!(tick.call_idx, 1);
assert_eq!(state.call(1).unwrap().duration_ticks, 5);
assert_eq!(state.call(2).unwrap().duration_ticks, 5);
published(apply(&mut state, step + 5, G8lS558Event::Hangup { idx: 2 }));
published(apply(&mut state, step + 6, G8lS558Event::Released { idx: 2 }));
let resume = published(apply(&mut state, step + 7, G8lS558Event::Resume { idx: 1 }));
assert_eq!((resume.from, resume.to, resume.active_calls, resume.held_calls), (G8lS558CallPhase::Held, G8lS558CallPhase::Active, 1, 0));
assert_eq!(state.completed_calls(), 1);
assert_eq!(state.occupied_slots(), 1);
}
#[test]
fn call_waiting_requires_hold_before_answer() {
let mut state = G8lS558VoiceCallState::new();
let step = establish_outgoing(&mut state, "+905551234567");
let waiting_number = number("+905559876543");
let ring = published(apply(&mut state, step, G8lS558Event::Ring { number: Some(waiting_number) }));
assert_eq!((ring.call_idx, ring.to), (2, G8lS558CallPhase::Waiting));
published(apply(&mut state, step + 1, G8lS558Event::ClccReport(clcc(2, 1, 5, Some(waiting_number)))));
assert_eq!(
apply(&mut state, step + 2, G8lS558Event::ClccReport(clcc(2, 1, 4, Some(waiting_number)))),
Err(G8lS558VoiceCallError::IllegalTransition)
);
assert_eq!(apply(&mut state, step + 2, G8lS558Event::Answer), Err(G8lS558VoiceCallError::ActiveCallMustBeHeld));
published(apply(&mut state, step + 2, G8lS558Event::Hold { idx: 1 }));
let answer = published(apply(&mut state, step + 3, G8lS558Event::Answer));
assert_eq!((answer.call_idx, answer.from, answer.to), (2, G8lS558CallPhase::Waiting, G8lS558CallPhase::Active));
assert_eq!((answer.active_calls, answer.held_calls), (1, 1));
// Rejecting a waiting call goes through Disconnecting like any other call.
let mut reject = G8lS558VoiceCallState::new();
let step = establish_outgoing(&mut reject, "+905551234567");
published(apply(&mut reject, step, G8lS558Event::Ring { number: None }));
let hangup = published(apply(&mut reject, step + 1, G8lS558Event::Hangup { idx: 2 }));
assert_eq!((hangup.from, hangup.to), (G8lS558CallPhase::Waiting, G8lS558CallPhase::Disconnecting));
published(apply(&mut reject, step + 2, G8lS558Event::Released { idx: 2 }));
assert_eq!(reject.phase(1), G8lS558CallPhase::Active);
assert_eq!(reject.phase(2), G8lS558CallPhase::Idle);
}
#[test]
fn dtmf_validation_requires_active_call_and_valid_tone() {
for tone in b"0123456789*#ABCD" {
assert!(dtmf_tone_is_valid(*tone), "{}", *tone as char);
assert_eq!(validate_dtmf(*tone, 1), Ok(()));
assert_eq!(validate_dtmf(*tone, 100), Ok(()));
}
for tone in b"abcdEF+, -" {
assert_eq!(validate_dtmf(*tone, 10), Err(G8lS558VoiceCallError::InvalidDtmfTone), "{}", *tone as char);
}
assert_eq!(validate_dtmf(b'5', 0), Err(G8lS558VoiceCallError::InvalidDtmfDuration));
assert_eq!(validate_dtmf(b'5', 101), Err(G8lS558VoiceCallError::InvalidDtmfDuration));
assert_eq!(validate_dtmf(b'5', 255), Err(G8lS558VoiceCallError::InvalidDtmfDuration));
assert_eq!(encode_vts(b'#', 10).unwrap(), "AT+VTS=#,10");
assert_eq!(encode_vts(b'x', 10), Err(G8lS558VoiceCallError::InvalidDtmfTone));
let mut state = G8lS558VoiceCallState::new();
assert_eq!(
apply(&mut state, 0, G8lS558Event::Dtmf { tone: b'1', duration: 10 }),
Err(G8lS558VoiceCallError::NoActiveCallForDtmf)
);
published(apply(&mut state, 0, G8lS558Event::Dial(number("+905551234567"))));
assert_eq!(
apply(&mut state, 1, G8lS558Event::Dtmf { tone: b'1', duration: 10 }),
Err(G8lS558VoiceCallError::NoActiveCallForDtmf)
);
let mut state = G8lS558VoiceCallState::new();
let step = establish_outgoing(&mut state, "+905551234567");
assert_eq!(
apply(&mut state, step, G8lS558Event::Dtmf { tone: b'E', duration: 10 }),
Err(G8lS558VoiceCallError::InvalidDtmfTone)
);
assert_eq!(
apply(&mut state, step, G8lS558Event::Dtmf { tone: b'1', duration: 0 }),
Err(G8lS558VoiceCallError::InvalidDtmfDuration)
);
let first = published(apply(&mut state, step, G8lS558Event::Dtmf { tone: b'1', duration: 10 }));
assert_eq!((first.call_idx, first.from, first.to, first.dtmf_tones_sent), (1, G8lS558CallPhase::Active, G8lS558CallPhase::Active, 1));
let second = published(apply(&mut state, step + 1, G8lS558Event::Dtmf { tone: b'#', duration: 100 }));
assert_eq!(second.dtmf_tones_sent, 2);
assert_eq!(state.dtmf_tones_sent(), 2);
published(apply(&mut state, step + 2, G8lS558Event::Hold { idx: 1 }));
assert_eq!(
apply(&mut state, step + 3, G8lS558Event::Dtmf { tone: b'1', duration: 10 }),
Err(G8lS558VoiceCallError::NoActiveCallForDtmf)
);
}
#[test]
fn call_duration_ticks_accumulate_and_overflow_fails_closed() {
let mut state = G8lS558VoiceCallState::new();
let step = establish_outgoing(&mut state, "+905551234567");
assert_eq!(apply(&mut state, step, G8lS558Event::Tick(0)), Err(G8lS558VoiceCallError::TickZero));
assert_eq!(apply(&mut state, step, G8lS558Event::Tick(S558_MAX_TICK_STEP + 1)), Err(G8lS558VoiceCallError::TickTooLarge));
let mut expected = 0u32;
let mut current = step;
while expected + S558_MAX_TICK_STEP <= S558_MAX_CALL_DURATION_TICKS {
let receipt = published(apply(&mut state, current, G8lS558Event::Tick(S558_MAX_TICK_STEP)));
expected += S558_MAX_TICK_STEP;
assert_eq!(receipt.duration_ticks, expected);
current += 1;
}
assert_eq!(expected, S558_MAX_CALL_DURATION_TICKS);
assert_eq!(state.call(1).unwrap().duration_ticks, S558_MAX_CALL_DURATION_TICKS);
assert_eq!(apply(&mut state, current, G8lS558Event::Tick(1)), Err(G8lS558VoiceCallError::DurationOverflow));
assert_eq!(state.call(1).unwrap().duration_ticks, S558_MAX_CALL_DURATION_TICKS);
assert_eq!(state.receipts().len(), current);
// Held calls keep accumulating duration; a boundary tick exactly at the maximum is accepted.
let mut held = G8lS558VoiceCallState::new();
let step = establish_outgoing(&mut held, "+905551234567");
published(apply(&mut held, step, G8lS558Event::Hold { idx: 1 }));
let tick = published(apply(&mut held, step + 1, G8lS558Event::Tick(7)));
assert_eq!((tick.from, tick.to, tick.duration_ticks), (G8lS558CallPhase::Held, G8lS558CallPhase::Held, 7));
}
#[test]
fn dial_and_ring_timeouts_bound_setup_phases() {
let mut state = G8lS558VoiceCallState::new();
let dialed = number("+905551234567");
published(apply(&mut state, 0, G8lS558Event::Dial(dialed)));
let boundary = published(apply(&mut state, 1, G8lS558Event::Tick(S558_DIAL_TIMEOUT_TICKS)));
assert_eq!((boundary.from, boundary.to, boundary.setup_ticks), (G8lS558CallPhase::Dialing, G8lS558CallPhase::Dialing, S558_DIAL_TIMEOUT_TICKS));
let timed_out = published(apply(&mut state, 2, G8lS558Event::Tick(1)));
assert_eq!((timed_out.from, timed_out.to), (G8lS558CallPhase::Dialing, G8lS558CallPhase::Disconnecting));
assert_eq!(timed_out.duration_ticks, 0);
published(apply(&mut state, 3, G8lS558Event::Released { idx: 1 }));
assert_eq!(state.completed_calls(), 1);
assert_eq!(state.missed_calls(), 0);
let mut alerting = G8lS558VoiceCallState::new();
published(apply(&mut alerting, 0, G8lS558Event::Dial(dialed)));
published(apply(&mut alerting, 1, G8lS558Event::Tick(30)));
published(apply(&mut alerting, 2, G8lS558Event::ClccReport(clcc(1, 0, 3, None))));
let timed_out = published(apply(&mut alerting, 3, G8lS558Event::Tick(61)));
assert_eq!((timed_out.from, timed_out.to, timed_out.setup_ticks), (G8lS558CallPhase::Alerting, G8lS558CallPhase::Disconnecting, 91));
let mut incoming = G8lS558VoiceCallState::new();
published(apply(&mut incoming, 0, G8lS558Event::Ring { number: Some(dialed) }));
let boundary = published(apply(&mut incoming, 1, G8lS558Event::Tick(S558_RING_TIMEOUT_TICKS)));
assert_eq!((boundary.to, boundary.setup_ticks), (G8lS558CallPhase::Incoming, S558_RING_TIMEOUT_TICKS));
let missed = published(apply(&mut incoming, 2, G8lS558Event::Tick(1)));
assert_eq!((missed.from, missed.to, missed.occupied_slots, missed.missed_calls), (G8lS558CallPhase::Incoming, G8lS558CallPhase::Idle, 0, 1));
assert_eq!(incoming.call(1), None);
assert_eq!(incoming.missed_calls(), 1);
assert_eq!(apply(&mut incoming, 3, G8lS558Event::Answer), Err(G8lS558VoiceCallError::NoIncomingCall));
// The freed slot can be reused by a new call.
let ring = published(apply(&mut incoming, 3, G8lS558Event::Ring { number: None }));
assert_eq!((ring.call_idx, ring.setup_ticks), (1, 0));
}
#[test]
fn step_out_of_order_and_ledger_bound_fail_closed() {
let mut state = G8lS558VoiceCallState::new();
let dialed = number("+905551234567");
assert_eq!(apply(&mut state, 1, G8lS558Event::Dial(dialed)), Err(G8lS558VoiceCallError::StepOutOfOrder));
assert!(state.receipts().is_empty());
let step = establish_outgoing(&mut state, "+905551234567");
assert_eq!(apply(&mut state, step + 1, G8lS558Event::Tick(1)), Err(G8lS558VoiceCallError::StepOutOfOrder));
assert_eq!(apply(&mut state, usize::MAX, G8lS558Event::Tick(1)), Err(G8lS558VoiceCallError::StepOutOfOrder));
let mut current = step;
while current < S558_MAX_TRANSITIONS {
published(apply(&mut state, current, G8lS558Event::Tick(1)));
current += 1;
}
assert_eq!(state.receipts().len(), S558_MAX_TRANSITIONS);
assert_eq!(apply(&mut state, current, G8lS558Event::Tick(1)), Err(G8lS558VoiceCallError::LedgerFull));
assert_eq!(apply(&mut state, current, G8lS558Event::Hangup { idx: 1 }), Err(G8lS558VoiceCallError::LedgerFull));
assert_eq!(state.receipts().len(), S558_MAX_TRANSITIONS);
assert_eq!(state.call(1).unwrap().duration_ticks, (S558_MAX_TRANSITIONS - step) as u32);
// Retained replay of an already published step still works when the ledger is full.
let first = state.receipts()[0];
assert_eq!(
apply(&mut state, 0, G8lS558Event::Dial(dialed)),
Ok(G8lS558VoiceCallOutcome::TransitionRetained(first))
);
}
#[test]
fn at_command_encoders_emit_exact_strings() {
assert_eq!(encode_atd(number("+905551234567")), "ATD+905551234567;");
assert_eq!(encode_atd(number("112")), "ATD112;");
assert_eq!(encode_ata(), "ATA");
assert_eq!(encode_ath(), "ATH");
assert_eq!(encode_chup(), "AT+CHUP");
assert_eq!(encode_clcc_query(), "AT+CLCC");
assert_eq!(encode_vts(b'5', 1).unwrap(), "AT+VTS=5,1");
assert_eq!(encode_vts(b'D', 100).unwrap(), "AT+VTS=D,100");
assert_eq!(encode_vts(b'*', 101), Err(G8lS558VoiceCallError::InvalidDtmfDuration));
for phase in [
G8lS558CallPhase::Dialing,
G8lS558CallPhase::Alerting,
G8lS558CallPhase::Active,
G8lS558CallPhase::Held,
G8lS558CallPhase::Incoming,
G8lS558CallPhase::Waiting,
] {
let stat = phase.clcc_stat().unwrap();
assert!(stat.code() <= S558_CLCC_STAT_MAX);
assert_eq!(G8lS558ClccStat::from_code(stat.code()), Some(stat));
}
assert_eq!(G8lS558CallPhase::Idle.clcc_stat(), None);
assert_eq!(G8lS558CallPhase::Disconnecting.clcc_stat(), None);
let state = G8lS558VoiceCallState::default();
assert_eq!(state.occupied_slots(), 0);
assert_eq!(state.active_calls(), 0);
assert_eq!(state.held_calls(), 0);
assert_eq!(state.phase(1), G8lS558CallPhase::Idle);
assert_eq!(state.phase(9), G8lS558CallPhase::Idle);
}
snippet sha256: ae6165f20622…file sha256: ae6165f20622…
03 · Kapı kimlik kaydı
Operations sıra, kimlik ve başlık bağı
tam Operations kaydıL2424–L2483
website/src/lib/operations.ts::g8l-s558-r1-voice-call-state-machine-model
{
id: "g8l-s558-r1-voice-call-state-machine-model",
date: "2026-08-30",
sequence: 558,
status: "passed",
umbrella_status: "partial",
title: "S558 · R1 modem: sesli arama durum makinesi modeli",
summary:
"S558 kaynak kapısı PASS'tir: R1 modem yolunun sesli arama kontrolü — ATD<numara>; çevirme ve E.164 tarzı numara doğrulama (opsiyonel + ve 3–20 rakam), ATA cevaplama, ATH/+CHUP kapatma, +CHLD tarzı hold/resume, +CLCC list-current-calls ayrıştırma (idx, dir, stat 0..5, mode, mpty, numara), RING/+CLIP URC işleme, +VTS DTMF doğrulama, en fazla 1 aktif + 1 held sınırlı eşzamanlı çağrı tablosu, kurulum zaman aşımı ve checked süre tick'leri — Idle→Dialing→Alerting→Active→Disconnecting→Idle ve Incoming/Waiting→Active geçişleriyle saf host modeli olarak modellendi. Her geçersiz geçiş, bozuk satır, geçersiz numara/ton, sıra dışı adım ve taşma state değiştirmeden fail-closed reddedilir; exact replay aynı receipt ile retained döner. Focused 18/18 PASS'tir. S540 ve S543 fiziksel verdict'leri değişmez RED kalır; hiçbir modem donanımı yoktur, physical observation=0 ve RUNBOOK_EXECUTED_IN_S558=NO'dur. S559 sesli aramanın ses yolunu bağlayan audio route / PCM capability model kapısıdır.",
evidence: [
"S558, S557'den ayrı source module, 18-test focused binary, proof, status manifest bloğu, Operations kaydı ve complete Code kartına sahiptir.",
"Dar S558 source-model status=PASS; R1 umbrella=PARTIAL ve S540/S543 physical gate status=RED olarak ayrı tutulur.",
"Numara doğrulama opsiyonel önde + ve 3..=20 ASCII rakam kabul eder; boş, kısa, 21+ rakam, yanlış yerde + ve rakam dışı karakter ayrı hata kodlarıyla fail-closed reddedilir.",
"ATD<numara>; yalnız doğrulanmış numaradan encode edilir; type-of-address uluslararası için 145, aksi halde 129'dur.",
"+CLCC satırı exact 5 veya 7 alanla ayrıştırılır: idx 1..=2, dir 0..1, stat 0..5 (active/held/dialing/alerting/incoming/waiting), voice mode 0, mpty 0..1 ve numara/type tutarlılığı zorunludur; on sekiz bozuk satır sınıfı testte ayrı ayrı reddedilir.",
"RING tanıyıcı ve +CLIP ayrıştırıcı gelen aramanın numarasını aynı tutarlılık kuralıyla üretir; Incoming/Waiting fazına CLCC raporu ile bağlanır.",
"Geçiş tablosu Idle→Dialing→Alerting→Active→Disconnecting→Idle ve Incoming/Waiting→Active'dir; Dialing'den doğrudan Active'e CLCC raporu, Disconnecting'e yeni rapor ve faz dışı her stat IllegalTransition ile reddedilir.",
"Eşzamanlılık sınırı 2 slot, en fazla 1 aktif + 1 held'dir: aktif çağrı varken Dial/Answer, held varken Hold, aktif varken Resume, üçüncü çağrı ve devam eden kurulum sırasında yeni kurulum fail-closed döner.",
"+VTS DTMF tonu 0-9/*/#/A-D ve süre 1..=100 ile doğrulanır; ton yalnız aktif çağrı varken gönderilir ve AT+VTS=<ton>,<süre> exact encode edilir.",
"Tick 1..=3600 sınırlıdır: aktif/held çağrılar checked toplama ile en fazla 86400 tick süre biriktirir, Dialing/Alerting 90 tick sonra Disconnecting'e, Incoming/Waiting 45 tick sonra missed-call olarak Idle'a düşer; çağrısız tick ve taşma fail-closed'dur.",
"Ledger en fazla 96 receipt tutar; exact replay TransitionRetained, publication sonrası sapan girdi PublishedStateDrift, sıra dışı adım StepOutOfOrder döner ve state değişmez.",
"Otuz beş hata varyantı 1..=35 nonzero benzersiz diagnostic kodu taşır; focused test benzersizliği BTreeSet ile doğrular.",
"Focused target CARGO_INCREMENTAL=0 ile 1 grup / 18 passed / 0 failed / 0 ignored / 0 filtered verdi.",
"Implementation 34362 B / bae7dba15c24acd69141a060eeadfbe1fe7dd68f3784e763de52bf7d1133552e; focused test 33580 B / ae6165f20622b01822762ad50e41b14ebf0e4e00d33b11f0468f718fdf0c2202 SHA-256'dır.",
"Proof 6211 B'dir ve model tablolarını, fail-closed koşullarını ve non-claim listesini içerir.",
"Modül hiçbir production callsite'a bağlanmadı; unsafe, asm!, write_volatile, crate::uart, crate::arch, spin:: ve #[no_mangle] yüzeyi yoktur ve focused test bunu source üzerinde doğrular.",
"Her receipt hardware_present=false, physical_observations=0 ve runbook_executed=false taşır.",
"RUNBOOK_EXECUTED_IN_S558=NO; supported-profile runtime observations=0, physical observations=0, SD/UART/power/new-raw=0/0/0/0, Boot-to-UI physically observed=false ve R1 acceptance=false'dur.",
"S540 ve S543 immutable fiziksel RED verdict'leri korunur; automatic promotion=false'dur ve S546 sonucu varsayılmaz.",
"S559 sesli aramanın earpiece/speaker/headset ses yolu ve PCM capability modelini host-only kuracaktır; aygıt veya fiziksel koşu yetkisi değildir.",
],
commands: [
"CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s558_r1_voice_call_state_machine_model -- --test-threads=1",
],
terminalSessions: [
{
id: "s558-focused",
title: "S558 sesli arama durum makinesi focused acceptance",
commandLines: [
"CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s558_r1_voice_call_state_machine_model -- --test-threads=1",
],
outputLines: [
"test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s",
"S558 focused=1 group / 18 passed / 0 failed",
"hardware=none physical=0 runbook=NO",
],
exitCode: 0,
outputMode: "complete",
},
],
terminalSessionsNote:
"S558 saf kaynak/host model kapısıdır; modem donanımı, AT transportu, UART veya fiziksel gözlem içermez. S540/S543 RED verdict'leri değişmez.",
limitations: [
"S558 yalnız kaynak/host modelidir; hiçbir donanım/panel/modem/board gözlemi yoktur ve gerçek modem üzerinde ATD/ATA/ATH/+CLCC davranışı gözlenmemiştir.",
"Modül production yoluna bağlanmadı; boot, IRQ, scheduler veya driver çağrısı yoktur.",
"S540 ve S543 fiziksel RED immutable kalır; automatic promotion yoktur ve S546 üçüncü fiziksel koşunun sonucu varsayılmaz.",
"Boot-to-UI physically observed=false ve R1 acceptance complete=false kalır.",
"S559 audio route / PCM capability modelini host-only kuracaktır; aygıt, ses veya fiziksel koşu yetkisi vermez.",
],
},snippet sha256: fa9cd0ca2b6d…file sha256: 9726dbf00f84…
Focused test komutu
CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s558_r1_voice_call_state_machine_model -- --test-threads=1proof: docs/M8.1-RPi5-G8l-S558-R1-Voice-Call-State-Machine-Model-Proof.md
Registry schema v5 · generator
website/scripts/generate-code-gates.mjs · Tam SHA-256: 3050638b71a684d8f8f947a8a6faa237a17fa8db5dc0db04fb207b668b462af9