ASELSANMicrokernel
S555 · SOURCE-BOUND GATE EVIDENCE

S555 · R1 modem: SIM kayıt durum makinesi modeli

tam S555 implementation modülü → Operations --test hedefi ile bağlı tam focused test → ayrı Operations kaydı Bu sayfa yalnız S555 kapısına aittir; komşu kapıların kaynakları bu kabulün içine katılmaz.

S555Focused kod testiOperations id exactsource SHA exacttest target exact

operation: g8l-s555-r1-sim-registration-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–L682
kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s555_r1_sim_registration_state_machine_model.rs::S555 r1 sim registration state machine model implementation
#![allow(unexpected_cfgs)]

//! S555 models the SIM and network registration state machine of the R1 modem
//! path as a pure source/host model.
//!
//! The model covers `+CPIN` states (`READY`, `SIM PIN`, `SIM PUK`,
//! `NOT INSERTED`), three PIN attempts before PUK escalation, ten PUK attempts
//! before the SIM is blocked, the `+CREG`/`+CEREG` `<stat>` 0..5 mapping, the
//! `+COPS` operator string as a bounded buffer, `+CSQ` RSSI 0..31/99 to bars
//! 0..5, and a registration state machine whose search phase is bounded by
//! ticks.  Every accepted transition publishes one receipt into a bounded
//! ledger; every invalid transition (for example a registration report while
//! no SIM is READY) fails closed without mutating the state.
//!
//! S555 does not claim any modem hardware, AT transport, UART, 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: S554 (modem AT command transport framing model).  Next gate:
//! S556 (SMS PDU encode/decode model).

use alloc::vec::Vec;

pub const S555_SEQUENCE: usize = 555;
pub const S555_EXPECTED_PREDECESSOR: usize = 554;
pub const S555_R1_STAGE: u8 = 3;
pub const S555_R1_RANGE_FIRST: usize = 536;
pub const S555_R1_RANGE_LAST: usize = 568;
pub const S555_PIN_ATTEMPTS: u8 = 3;
pub const S555_PUK_ATTEMPTS: u8 = 10;
pub const S555_SEARCH_TIMEOUT_TICKS: u32 = 60;
pub const S555_CSQ_RSSI_MAX: u8 = 31;
pub const S555_CSQ_RSSI_UNKNOWN: u8 = 99;
pub const S555_SIGNAL_BARS_MAX: u8 = 5;
pub const S555_REGISTRATION_STAT_MAX: u8 = 5;
pub const S555_OPERATOR_NAME_MAX_LEN: usize = 16;
pub const S555_MAX_TRANSITIONS: usize = 64;
pub const S555_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS: usize = 0;
pub const S555_PHYSICAL_OBSERVATIONS: usize = 0;
pub const S555_PHYSICAL_OR_DEVICE_OPERATIONS: usize = 0;
pub const S555_SD_WRITES: usize = 0;
pub const S555_UART_OPENS: usize = 0;
pub const S555_POWER_TRANSITIONS: usize = 0;
pub const S555_NEW_IMMUTABLE_RAW_CAPTURES: usize = 0;
pub const S555_S540_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S555_S543_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S555_AUTOMATIC_PROMOTION: bool = false;
pub const S555_BOOT_TO_UI_PHYSICALLY_OBSERVED: bool = false;
pub const S555_HARDWARE_PRESENT: bool = false;
pub const S555_R1_ACCEPTANCE_COMPLETE: bool = false;
pub const RUNBOOK_EXECUTED_IN_S555: bool = false;

/// `+CPIN: <code>` states that the model accepts.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS555CpinState {
    NotInserted,
    SimPin,
    SimPuk,
    Ready,
}

impl G8lS555CpinState {
    pub const fn text(self) -> &'static str {
        match self {
            Self::NotInserted => "NOT INSERTED",
            Self::SimPin => "SIM PIN",
            Self::SimPuk => "SIM PUK",
            Self::Ready => "READY",
        }
    }
}

/// `+CREG`/`+CEREG` `<stat>` 0..5 mapping.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS555RegistrationStat {
    NotRegistered,
    Home,
    Searching,
    Denied,
    Unknown,
    Roaming,
}

impl G8lS555RegistrationStat {
    pub const fn from_code(code: u8) -> Option<Self> {
        match code {
            0 => Some(Self::NotRegistered),
            1 => Some(Self::Home),
            2 => Some(Self::Searching),
            3 => Some(Self::Denied),
            4 => Some(Self::Unknown),
            5 => Some(Self::Roaming),
            _ => None,
        }
    }

    pub const fn code(self) -> u8 {
        match self {
            Self::NotRegistered => 0,
            Self::Home => 1,
            Self::Searching => 2,
            Self::Denied => 3,
            Self::Unknown => 4,
            Self::Roaming => 5,
        }
    }

    pub const fn is_registered(self) -> bool {
        matches!(self, Self::Home | Self::Roaming)
    }
}

/// Bounded, printable-ASCII operator name (`+COPS` long alphanumeric form).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS555OperatorName {
    bytes: [u8; S555_OPERATOR_NAME_MAX_LEN],
    len: u8,
}

impl G8lS555OperatorName {
    pub const fn empty() -> Self {
        Self {
            bytes: [0; S555_OPERATOR_NAME_MAX_LEN],
            len: 0,
        }
    }

    pub fn from_text(text: &str) -> Result<Self, G8lS555SimRegistrationError> {
        let raw = text.as_bytes();
        if raw.is_empty() {
            return Err(G8lS555SimRegistrationError::OperatorNameEmpty);
        }
        if raw.len() > S555_OPERATOR_NAME_MAX_LEN {
            return Err(G8lS555SimRegistrationError::OperatorNameTooLong);
        }
        let mut bytes = [0u8; S555_OPERATOR_NAME_MAX_LEN];
        for (slot, byte) in bytes.iter_mut().zip(raw) {
            if !(0x20..=0x7e).contains(byte) || *byte == b'"' {
                return Err(G8lS555SimRegistrationError::OperatorNameNotPrintable);
            }
            *slot = *byte;
        }
        Ok(Self {
            bytes,
            len: raw.len() as u8,
        })
    }

    pub fn as_str(&self) -> &str {
        core::str::from_utf8(&self.bytes[..self.len as usize]).unwrap_or("")
    }

    pub const fn len(&self) -> usize {
        self.len as usize
    }

    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }
}

/// Registration state machine phases.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS555Phase {
    NoSim,
    PinRequired,
    PukRequired,
    SimBlocked,
    SimReady,
    Searching,
    Registered { roaming: bool },
    Denied,
    SearchTimedOut,
}

/// 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 G8lS555Event {
    CpinReport(G8lS555CpinState),
    PinEntry { correct: bool },
    PukEntry { correct: bool },
    StartSearch,
    Tick(u32),
    RegistrationReport(u8),
    OperatorReport(G8lS555OperatorName),
    SignalReport(u8),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS555TransitionReceipt {
    pub sequence: usize,
    pub predecessor_sequence: usize,
    pub step: usize,
    pub event: G8lS555Event,
    pub from: G8lS555Phase,
    pub to: G8lS555Phase,
    pub pin_attempts_remaining: u8,
    pub puk_attempts_remaining: u8,
    pub search_ticks_elapsed: u32,
    pub registration_stat: u8,
    pub operator: G8lS555OperatorName,
    pub rssi: u8,
    pub signal_bars: u8,
    pub hardware_present: bool,
    pub physical_observations: usize,
    pub runbook_executed: bool,
}

#[derive(Debug)]
pub struct G8lS555SimRegistrationState {
    phase: G8lS555Phase,
    pin_attempts_remaining: u8,
    puk_attempts_remaining: u8,
    search_ticks_elapsed: u32,
    registration_stat: u8,
    operator: G8lS555OperatorName,
    rssi: u8,
    signal_bars: u8,
    ledger: Vec<G8lS555TransitionReceipt>,
}

impl G8lS555SimRegistrationState {
    pub const fn new() -> Self {
        Self {
            phase: G8lS555Phase::NoSim,
            pin_attempts_remaining: S555_PIN_ATTEMPTS,
            puk_attempts_remaining: S555_PUK_ATTEMPTS,
            search_ticks_elapsed: 0,
            registration_stat: 0,
            operator: G8lS555OperatorName::empty(),
            rssi: S555_CSQ_RSSI_UNKNOWN,
            signal_bars: 0,
            ledger: Vec::new(),
        }
    }

    pub const fn phase(&self) -> G8lS555Phase {
        self.phase
    }

    pub const fn pin_attempts_remaining(&self) -> u8 {
        self.pin_attempts_remaining
    }

    pub const fn puk_attempts_remaining(&self) -> u8 {
        self.puk_attempts_remaining
    }

    pub const fn search_ticks_elapsed(&self) -> u32 {
        self.search_ticks_elapsed
    }

    pub const fn registration_stat(&self) -> u8 {
        self.registration_stat
    }

    pub const fn operator(&self) -> G8lS555OperatorName {
        self.operator
    }

    pub const fn signal_bars(&self) -> u8 {
        self.signal_bars
    }

    pub fn receipts(&self) -> &[G8lS555TransitionReceipt] {
        &self.ledger
    }

    pub fn last_receipt(&self) -> Option<G8lS555TransitionReceipt> {
        self.ledger.last().copied()
    }
}

impl Default for G8lS555SimRegistrationState {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS555SimRegistrationOutcome {
    TransitionPublished(G8lS555TransitionReceipt),
    TransitionRetained(G8lS555TransitionReceipt),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS555SimRegistrationError {
    StepOutOfOrder,
    PublishedStateDrift,
    LedgerFull,
    InvalidCpinLine,
    RedundantCpinReport,
    CpinRegression,
    ReadyWithoutPinEntry,
    PinEntryWithoutPinRequest,
    PukEntryWithoutPukRequest,
    SimBlocked,
    InvalidRegistrationStat,
    InvalidRegistrationLine,
    RegistrationWithoutSimReady,
    RegistrationWithoutSearch,
    SearchWithoutSimReady,
    SearchWhileRegistered,
    TickOutsideSearch,
    TickOverflow,
    OperatorNameEmpty,
    OperatorNameTooLong,
    OperatorNameNotPrintable,
    OperatorWithoutRegistration,
    InvalidOperatorLine,
    InvalidRssi,
    InvalidSignalLine,
    SignalWithoutSim,
}

impl G8lS555SimRegistrationError {
    pub const fn diagnostic_code(self) -> u64 {
        match self {
            Self::StepOutOfOrder => 1,
            Self::PublishedStateDrift => 2,
            Self::LedgerFull => 3,
            Self::InvalidCpinLine => 4,
            Self::RedundantCpinReport => 5,
            Self::CpinRegression => 6,
            Self::ReadyWithoutPinEntry => 7,
            Self::PinEntryWithoutPinRequest => 8,
            Self::PukEntryWithoutPukRequest => 9,
            Self::SimBlocked => 10,
            Self::InvalidRegistrationStat => 11,
            Self::InvalidRegistrationLine => 12,
            Self::RegistrationWithoutSimReady => 13,
            Self::RegistrationWithoutSearch => 14,
            Self::SearchWithoutSimReady => 15,
            Self::SearchWhileRegistered => 16,
            Self::TickOutsideSearch => 17,
            Self::TickOverflow => 18,
            Self::OperatorNameEmpty => 19,
            Self::OperatorNameTooLong => 20,
            Self::OperatorNameNotPrintable => 21,
            Self::OperatorWithoutRegistration => 22,
            Self::InvalidOperatorLine => 23,
            Self::InvalidRssi => 24,
            Self::InvalidSignalLine => 25,
            Self::SignalWithoutSim => 26,
        }
    }
}

/// `+CSQ` RSSI (0..31, 99 = unknown) to bars 0..5.  Any other RSSI is invalid.
pub const fn csq_rssi_to_bars(rssi: u8) -> Result<u8, G8lS555SimRegistrationError> {
    match rssi {
        S555_CSQ_RSSI_UNKNOWN => Ok(0),
        0..=1 => Ok(0),
        2..=7 => Ok(1),
        8..=13 => Ok(2),
        14..=19 => Ok(3),
        20..=25 => Ok(4),
        26..=31 => Ok(5),
        _ => Err(G8lS555SimRegistrationError::InvalidRssi),
    }
}

fn strip_prefix_trimmed<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
    let body = line.trim_end_matches(['\r', '\n']);
    body.strip_prefix(prefix).map(str::trim)
}

fn parse_decimal_u8(field: &str) -> Option<u8> {
    let field = field.trim();
    if field.is_empty() || field.len() > 3 || !field.bytes().all(|b| b.is_ascii_digit()) {
        return None;
    }
    field
        .bytes()
        .try_fold(0u8, |acc, b| acc.checked_mul(10)?.checked_add(b - b'0'))
}

/// Decodes `+CPIN: READY` / `+CPIN: SIM PIN` / `+CPIN: SIM PUK` /
/// `+CPIN: NOT INSERTED` (with optional trailing CR/LF).
pub fn parse_cpin_line(line: &str) -> Result<G8lS555CpinState, G8lS555SimRegistrationError> {
    let body =
        strip_prefix_trimmed(line, "+CPIN:").ok_or(G8lS555SimRegistrationError::InvalidCpinLine)?;
    match body {
        "READY" => Ok(G8lS555CpinState::Ready),
        "SIM PIN" => Ok(G8lS555CpinState::SimPin),
        "SIM PUK" => Ok(G8lS555CpinState::SimPuk),
        "NOT INSERTED" => Ok(G8lS555CpinState::NotInserted),
        _ => Err(G8lS555SimRegistrationError::InvalidCpinLine),
    }
}

/// Decodes `+CREG: <n>,<stat>` or `+CEREG: <n>,<stat>` (extra location
/// fields are ignored) and returns the mapped `<stat>`.
pub fn parse_registration_line(
    line: &str,
) -> Result<G8lS555RegistrationStat, G8lS555SimRegistrationError> {
    let body = strip_prefix_trimmed(line, "+CREG:")
        .or_else(|| strip_prefix_trimmed(line, "+CEREG:"))
        .ok_or(G8lS555SimRegistrationError::InvalidRegistrationLine)?;
    let mut fields = body.split(',');
    let mode = fields
        .next()
        .and_then(parse_decimal_u8)
        .ok_or(G8lS555SimRegistrationError::InvalidRegistrationLine)?;
    if mode > 2 {
        return Err(G8lS555SimRegistrationError::InvalidRegistrationLine);
    }
    let stat = fields
        .next()
        .and_then(parse_decimal_u8)
        .ok_or(G8lS555SimRegistrationError::InvalidRegistrationLine)?;
    G8lS555RegistrationStat::from_code(stat)
        .ok_or(G8lS555SimRegistrationError::InvalidRegistrationStat)
}

/// Decodes `+COPS: <mode>,<format>,"<oper>"[,<act>]` and returns the bounded
/// operator name.
pub fn parse_cops_line(line: &str) -> Result<G8lS555OperatorName, G8lS555SimRegistrationError> {
    let body = strip_prefix_trimmed(line, "+COPS:")
        .ok_or(G8lS555SimRegistrationError::InvalidOperatorLine)?;
    let open = body
        .find('"')
        .ok_or(G8lS555SimRegistrationError::InvalidOperatorLine)?;
    let rest = &body[open + 1..];
    let close = rest
        .find('"')
        .ok_or(G8lS555SimRegistrationError::InvalidOperatorLine)?;
    let head = &body[..open];
    let mut fields = head.split(',');
    let mode = fields
        .next()
        .and_then(parse_decimal_u8)
        .ok_or(G8lS555SimRegistrationError::InvalidOperatorLine)?;
    let format = fields
        .next()
        .and_then(parse_decimal_u8)
        .ok_or(G8lS555SimRegistrationError::InvalidOperatorLine)?;
    if mode > 4 || format > 2 || fields.next().is_some_and(|f| !f.trim().is_empty()) {
        return Err(G8lS555SimRegistrationError::InvalidOperatorLine);
    }
    G8lS555OperatorName::from_text(&rest[..close])
}

/// Decodes `+CSQ: <rssi>,<ber>` and returns `(rssi, bars)`.
pub fn parse_csq_line(line: &str) -> Result<(u8, u8), G8lS555SimRegistrationError> {
    let body = strip_prefix_trimmed(line, "+CSQ:")
        .ok_or(G8lS555SimRegistrationError::InvalidSignalLine)?;
    let mut fields = body.split(',');
    let rssi = fields
        .next()
        .and_then(parse_decimal_u8)
        .ok_or(G8lS555SimRegistrationError::InvalidSignalLine)?;
    let ber = fields
        .next()
        .and_then(parse_decimal_u8)
        .ok_or(G8lS555SimRegistrationError::InvalidSignalLine)?;
    if fields.next().is_some() || (ber > 7 && ber != 99) {
        return Err(G8lS555SimRegistrationError::InvalidSignalLine);
    }
    Ok((rssi, csq_rssi_to_bars(rssi)?))
}

struct Next {
    phase: G8lS555Phase,
    pin: u8,
    puk: u8,
    ticks: u32,
    stat: u8,
    operator: G8lS555OperatorName,
    rssi: u8,
    bars: u8,
}

fn next_state(
    state: &G8lS555SimRegistrationState,
    event: G8lS555Event,
) -> Result<Next, G8lS555SimRegistrationError> {
    use G8lS555Event as E;
    use G8lS555Phase as P;
    use G8lS555SimRegistrationError as Err;

    let mut next = Next {
        phase: state.phase,
        pin: state.pin_attempts_remaining,
        puk: state.puk_attempts_remaining,
        ticks: state.search_ticks_elapsed,
        stat: state.registration_stat,
        operator: state.operator,
        rssi: state.rssi,
        bars: state.signal_bars,
    };

    // SIM removal is the only event a blocked SIM accepts; it resets everything.
    if let E::CpinReport(G8lS555CpinState::NotInserted) = event {
        if state.phase == P::NoSim {
            return Err(Err::RedundantCpinReport);
        }
        let fresh = G8lS555SimRegistrationState::new();
        next.phase = P::NoSim;
        next.pin = fresh.pin_attempts_remaining;
        next.puk = fresh.puk_attempts_remaining;
        next.ticks = 0;
        next.stat = 0;
        next.operator = fresh.operator;
        next.rssi = fresh.rssi;
        next.bars = 0;
        return Ok(next);
    }
    if state.phase == P::SimBlocked {
        return Err(Err::SimBlocked);
    }

    match event {
        E::CpinReport(cpin) => {
            if state.phase != P::NoSim {
                return Err(match (state.phase, cpin) {
                    (P::PinRequired | P::PukRequired, G8lS555CpinState::Ready) => {
                        Err::ReadyWithoutPinEntry
                    }
                    _ => Err::CpinRegression,
                });
            }
            next.phase = match cpin {
                G8lS555CpinState::SimPin => P::PinRequired,
                G8lS555CpinState::SimPuk => P::PukRequired,
                G8lS555CpinState::Ready => P::SimReady,
                G8lS555CpinState::NotInserted => return Err(Err::RedundantCpinReport),
            };
        }
        E::PinEntry { correct } => {
            if state.phase != P::PinRequired {
                return Err(Err::PinEntryWithoutPinRequest);
            }
            if correct {
                next.phase = P::SimReady;
                next.pin = S555_PIN_ATTEMPTS;
            } else {
                next.pin = state.pin_attempts_remaining.saturating_sub(1);
                if next.pin == 0 {
                    next.phase = P::PukRequired;
                }
            }
        }
        E::PukEntry { correct } => {
            if state.phase != P::PukRequired {
                return Err(Err::PukEntryWithoutPukRequest);
            }
            if correct {
                next.phase = P::SimReady;
                next.pin = S555_PIN_ATTEMPTS;
                next.puk = S555_PUK_ATTEMPTS;
            } else {
                next.puk = state.puk_attempts_remaining.saturating_sub(1);
                if next.puk == 0 {
                    next.phase = P::SimBlocked;
                }
            }
        }
        E::StartSearch => match state.phase {
            P::SimReady | P::Denied | P::SearchTimedOut => {
                next.phase = P::Searching;
                next.ticks = 0;
                next.stat = G8lS555RegistrationStat::Searching.code();
                next.operator = G8lS555OperatorName::empty();
            }
            P::Searching | P::Registered { .. } => return Err(Err::SearchWhileRegistered),
            _ => return Err(Err::SearchWithoutSimReady),
        },
        E::Tick(delta) => {
            if state.phase != P::Searching {
                return Err(Err::TickOutsideSearch);
            }
            next.ticks = state
                .search_ticks_elapsed
                .checked_add(delta)
                .ok_or(Err::TickOverflow)?;
            if next.ticks > S555_SEARCH_TIMEOUT_TICKS {
                next.phase = P::SearchTimedOut;
                next.stat = G8lS555RegistrationStat::NotRegistered.code();
            }
        }
        E::RegistrationReport(code) => {
            let stat =
                G8lS555RegistrationStat::from_code(code).ok_or(Err::InvalidRegistrationStat)?;
            match state.phase {
                P::Searching | P::Registered { .. } => {}
                P::SimReady | P::Denied | P::SearchTimedOut => {
                    return Err(Err::RegistrationWithoutSearch)
                }
                _ => return Err(Err::RegistrationWithoutSimReady),
            }
            next.stat = stat.code();
            next.phase = match stat {
                G8lS555RegistrationStat::Home => P::Registered { roaming: false },
                G8lS555RegistrationStat::Roaming => P::Registered { roaming: true },
                G8lS555RegistrationStat::Denied => P::Denied,
                G8lS555RegistrationStat::NotRegistered => P::SimReady,
                G8lS555RegistrationStat::Searching | G8lS555RegistrationStat::Unknown => {
                    P::Searching
                }
            };
            if !stat.is_registered() {
                next.operator = G8lS555OperatorName::empty();
            }
            if next.phase == P::Searching && !matches!(state.phase, P::Searching) {
                next.ticks = 0;
            }
        }
        E::OperatorReport(name) => {
            if name.is_empty() {
                return Err(Err::OperatorNameEmpty);
            }
            if !matches!(state.phase, P::Registered { .. }) {
                return Err(Err::OperatorWithoutRegistration);
            }
            next.operator = name;
        }
        E::SignalReport(rssi) => {
            let bars = csq_rssi_to_bars(rssi)?;
            if state.phase == P::NoSim {
                return Err(Err::SignalWithoutSim);
            }
            next.rssi = rssi;
            next.bars = bars;
        }
    }
    Ok(next)
}

/// 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_s555_model_sim_registration_transition(
    state: &mut G8lS555SimRegistrationState,
    step: usize,
    event: G8lS555Event,
) -> Result<G8lS555SimRegistrationOutcome, G8lS555SimRegistrationError> {
    if let Some(published) = state.ledger.get(step).copied() {
        if published.event != event || published.step != step {
            return Err(G8lS555SimRegistrationError::PublishedStateDrift);
        }
        return Ok(G8lS555SimRegistrationOutcome::TransitionRetained(published));
    }
    if step != state.ledger.len() {
        return Err(G8lS555SimRegistrationError::StepOutOfOrder);
    }
    if state.ledger.len() >= S555_MAX_TRANSITIONS {
        return Err(G8lS555SimRegistrationError::LedgerFull);
    }
    let next = next_state(state, event)?;
    let receipt = G8lS555TransitionReceipt {
        sequence: S555_SEQUENCE,
        predecessor_sequence: S555_EXPECTED_PREDECESSOR,
        step,
        event,
        from: state.phase,
        to: next.phase,
        pin_attempts_remaining: next.pin,
        puk_attempts_remaining: next.puk,
        search_ticks_elapsed: next.ticks,
        registration_stat: next.stat,
        operator: next.operator,
        rssi: next.rssi,
        signal_bars: next.bars,
        hardware_present: S555_HARDWARE_PRESENT,
        physical_observations: S555_PHYSICAL_OBSERVATIONS,
        runbook_executed: RUNBOOK_EXECUTED_IN_S555,
    };
    state.phase = next.phase;
    state.pin_attempts_remaining = next.pin;
    state.puk_attempts_remaining = next.puk;
    state.search_ticks_elapsed = next.ticks;
    state.registration_stat = next.stat;
    state.operator = next.operator;
    state.rssi = next.rssi;
    state.signal_bars = next.bars;
    state.ledger.push(receipt);
    Ok(G8lS555SimRegistrationOutcome::TransitionPublished(receipt))
}
snippet sha256: 06eeaf6a2088file sha256: 06eeaf6a2088
02 · Doğrulayan test kodu

Operations komutuna bağlı focused test

tam dosyaL1–L623
simulation/tests/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s555_r1_sim_registration_state_machine_model.rs::S555 r1 sim registration state machine model focused tests
use aselsan_microkernel_simulation::g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s555_r1_sim_registration_state_machine_model::*;
use std::collections::BTreeSet;

const SOURCE: &str = include_str!(
    "../../kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s555_r1_sim_registration_state_machine_model.rs"
);
const MAIN: &str = include_str!("../../kernel/src/main.rs");
const SIMULATION_LIB: &str = include_str!("../src/lib.rs");

type State = G8lS555SimRegistrationState;
type Event = G8lS555Event;
type Phase = G8lS555Phase;
type Outcome = G8lS555SimRegistrationOutcome;
type Error = G8lS555SimRegistrationError;

fn step(state: &mut State, event: Event) -> Result<Outcome, Error> {
    let step = state.receipts().len();
    service_s555_model_sim_registration_transition(state, step, event)
}

fn publish(state: &mut State, event: Event) -> G8lS555TransitionReceipt {
    match step(state, event) {
        Ok(Outcome::TransitionPublished(receipt)) => receipt,
        other => panic!("expected publication for {event:?}, got {other:?}"),
    }
}

fn operator(name: &str) -> G8lS555OperatorName {
    G8lS555OperatorName::from_text(name).unwrap()
}

fn ready_state() -> State {
    let mut state = State::new();
    publish(&mut state, Event::CpinReport(G8lS555CpinState::Ready));
    state
}

fn registered_state() -> State {
    let mut state = ready_state();
    publish(&mut state, Event::StartSearch);
    publish(&mut state, Event::RegistrationReport(1));
    state
}

#[test]
fn sequence_scope_and_nonpromotion_are_exact() {
    assert_eq!(S555_SEQUENCE, 555);
    assert_eq!(S555_EXPECTED_PREDECESSOR, 554);
    assert_eq!(S555_R1_STAGE, 3);
    assert_eq!(S555_R1_RANGE_FIRST, 536);
    assert_eq!(S555_R1_RANGE_LAST, 568);
    assert_eq!(S555_PIN_ATTEMPTS, 3);
    assert_eq!(S555_PUK_ATTEMPTS, 10);
    assert_eq!(S555_SEARCH_TIMEOUT_TICKS, 60);
    assert_eq!(S555_CSQ_RSSI_MAX, 31);
    assert_eq!(S555_CSQ_RSSI_UNKNOWN, 99);
    assert_eq!(S555_SIGNAL_BARS_MAX, 5);
    assert_eq!(S555_REGISTRATION_STAT_MAX, 5);
    assert_eq!(S555_OPERATOR_NAME_MAX_LEN, 16);
    assert_eq!(S555_MAX_TRANSITIONS, 64);
    assert_eq!(S555_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS, 0);
    assert_eq!(S555_PHYSICAL_OBSERVATIONS, 0);
    assert_eq!(S555_PHYSICAL_OR_DEVICE_OPERATIONS, 0);
    assert_eq!(S555_SD_WRITES, 0);
    assert_eq!(S555_UART_OPENS, 0);
    assert_eq!(S555_POWER_TRANSITIONS, 0);
    assert_eq!(S555_NEW_IMMUTABLE_RAW_CAPTURES, 0);
    assert!(S555_S540_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(S555_S543_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(!S555_AUTOMATIC_PROMOTION);
    assert!(!S555_BOOT_TO_UI_PHYSICALLY_OBSERVED);
    assert!(!S555_HARDWARE_PRESENT);
    assert!(!S555_R1_ACCEPTANCE_COMPLETE);
    assert!(!RUNBOOK_EXECUTED_IN_S555);
}

#[test]
fn module_is_registered_in_kernel_and_simulation() {
    let module = "g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s555_r1_sim_registration_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::",
        "crate::kprintln!",
        "/dev/cu.",
        "TIOCEXCL",
    ] {
        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("no production callsite"));
}

#[test]
fn diagnostic_codes_are_nonzero_and_unique() {
    let errors = [
        Error::StepOutOfOrder,
        Error::PublishedStateDrift,
        Error::LedgerFull,
        Error::InvalidCpinLine,
        Error::RedundantCpinReport,
        Error::CpinRegression,
        Error::ReadyWithoutPinEntry,
        Error::PinEntryWithoutPinRequest,
        Error::PukEntryWithoutPukRequest,
        Error::SimBlocked,
        Error::InvalidRegistrationStat,
        Error::InvalidRegistrationLine,
        Error::RegistrationWithoutSimReady,
        Error::RegistrationWithoutSearch,
        Error::SearchWithoutSimReady,
        Error::SearchWhileRegistered,
        Error::TickOutsideSearch,
        Error::TickOverflow,
        Error::OperatorNameEmpty,
        Error::OperatorNameTooLong,
        Error::OperatorNameNotPrintable,
        Error::OperatorWithoutRegistration,
        Error::InvalidOperatorLine,
        Error::InvalidRssi,
        Error::InvalidSignalLine,
        Error::SignalWithoutSim,
    ];
    let codes: BTreeSet<_> = errors.into_iter().map(Error::diagnostic_code).collect();
    assert_eq!(codes.len(), errors.len());
    assert_eq!(codes.len(), 26);
    assert!(!codes.contains(&0));
}

#[test]
fn exact_replay_retains_the_same_receipt() {
    let mut state = State::new();
    let receipt = publish(&mut state, Event::CpinReport(G8lS555CpinState::SimPin));
    assert_eq!(
        service_s555_model_sim_registration_transition(
            &mut state,
            0,
            Event::CpinReport(G8lS555CpinState::SimPin)
        ),
        Ok(Outcome::TransitionRetained(receipt))
    );
    assert_eq!(state.receipts().len(), 1);
    assert_eq!(state.phase(), Phase::PinRequired);
    let second = publish(&mut state, Event::PinEntry { correct: true });
    assert_eq!(
        service_s555_model_sim_registration_transition(
            &mut state,
            0,
            Event::CpinReport(G8lS555CpinState::SimPin)
        ),
        Ok(Outcome::TransitionRetained(receipt))
    );
    assert_eq!(
        service_s555_model_sim_registration_transition(
            &mut state,
            1,
            Event::PinEntry { correct: true }
        ),
        Ok(Outcome::TransitionRetained(second))
    );
    assert_eq!(state.receipts().len(), 2);
}

#[test]
fn divergent_input_after_publication_fails_closed() {
    let mut state = State::new();
    publish(&mut state, Event::CpinReport(G8lS555CpinState::SimPin));
    publish(&mut state, Event::PinEntry { correct: false });
    assert_eq!(
        service_s555_model_sim_registration_transition(
            &mut state,
            1,
            Event::PinEntry { correct: true }
        ),
        Err(Error::PublishedStateDrift)
    );
    assert_eq!(
        service_s555_model_sim_registration_transition(
            &mut state,
            0,
            Event::CpinReport(G8lS555CpinState::Ready)
        ),
        Err(Error::PublishedStateDrift)
    );
    assert_eq!(state.receipts().len(), 2);
    assert_eq!(state.phase(), Phase::PinRequired);
    assert_eq!(state.pin_attempts_remaining(), 2);
}

#[test]
fn happy_path_pin_search_home_registration_operator_and_signal() {
    let mut state = State::new();
    let r0 = publish(&mut state, Event::CpinReport(G8lS555CpinState::SimPin));
    assert_eq!((r0.from, r0.to), (Phase::NoSim, Phase::PinRequired));
    assert_eq!(r0.sequence, S555_SEQUENCE);
    assert_eq!(r0.predecessor_sequence, S555_EXPECTED_PREDECESSOR);
    assert!(!r0.hardware_present);
    assert_eq!(r0.physical_observations, 0);
    assert!(!r0.runbook_executed);
    let r1 = publish(&mut state, Event::PinEntry { correct: true });
    assert_eq!((r1.from, r1.to), (Phase::PinRequired, Phase::SimReady));
    assert_eq!(r1.pin_attempts_remaining, 3);
    let r2 = publish(&mut state, Event::StartSearch);
    assert_eq!(r2.to, Phase::Searching);
    assert_eq!(r2.registration_stat, 2);
    let r3 = publish(&mut state, Event::Tick(10));
    assert_eq!((r3.to, r3.search_ticks_elapsed), (Phase::Searching, 10));
    let r4 = publish(&mut state, Event::RegistrationReport(1));
    assert_eq!(r4.to, Phase::Registered { roaming: false });
    assert_eq!(r4.registration_stat, 1);
    let r5 = publish(&mut state, Event::OperatorReport(operator("Turkcell")));
    assert_eq!(r5.operator.as_str(), "Turkcell");
    assert_eq!(r5.to, Phase::Registered { roaming: false });
    let r6 = publish(&mut state, Event::SignalReport(20));
    assert_eq!((r6.rssi, r6.signal_bars), (20, 4));
    assert_eq!(state.receipts().len(), 7);
    assert_eq!(state.last_receipt(), Some(r6));
    assert_eq!(state.operator().as_str(), "Turkcell");
    assert_eq!(state.signal_bars(), 4);
    for (index, receipt) in state.receipts().iter().enumerate() {
        assert_eq!(receipt.step, index);
    }
}

#[test]
fn three_wrong_pins_escalate_to_puk_and_puk_exhaustion_blocks_sim() {
    let mut state = State::new();
    publish(&mut state, Event::CpinReport(G8lS555CpinState::SimPin));
    let a = publish(&mut state, Event::PinEntry { correct: false });
    assert_eq!((a.to, a.pin_attempts_remaining), (Phase::PinRequired, 2));
    let b = publish(&mut state, Event::PinEntry { correct: false });
    assert_eq!((b.to, b.pin_attempts_remaining), (Phase::PinRequired, 1));
    let c = publish(&mut state, Event::PinEntry { correct: false });
    assert_eq!((c.to, c.pin_attempts_remaining), (Phase::PukRequired, 0));
    assert_eq!(
        step(&mut state, Event::PinEntry { correct: true }),
        Err(Error::PinEntryWithoutPinRequest)
    );
    for remaining in (1..S555_PUK_ATTEMPTS).rev() {
        let r = publish(&mut state, Event::PukEntry { correct: false });
        assert_eq!((r.to, r.puk_attempts_remaining), (Phase::PukRequired, remaining));
    }
    let blocked = publish(&mut state, Event::PukEntry { correct: false });
    assert_eq!((blocked.to, blocked.puk_attempts_remaining), (Phase::SimBlocked, 0));
    assert_eq!(state.phase(), Phase::SimBlocked);
}

#[test]
fn correct_puk_restores_sim_ready_with_fresh_counters() {
    let mut state = State::new();
    publish(&mut state, Event::CpinReport(G8lS555CpinState::SimPuk));
    assert_eq!(state.phase(), Phase::PukRequired);
    publish(&mut state, Event::PukEntry { correct: false });
    let r = publish(&mut state, Event::PukEntry { correct: true });
    assert_eq!(r.to, Phase::SimReady);
    assert_eq!(r.pin_attempts_remaining, S555_PIN_ATTEMPTS);
    assert_eq!(r.puk_attempts_remaining, S555_PUK_ATTEMPTS);
    assert_eq!(
        step(&mut state, Event::PukEntry { correct: true }),
        Err(Error::PukEntryWithoutPukRequest)
    );
}

#[test]
fn blocked_sim_accepts_only_removal_and_removal_resets_everything() {
    let mut state = State::new();
    publish(&mut state, Event::CpinReport(G8lS555CpinState::SimPuk));
    for _ in 0..S555_PUK_ATTEMPTS {
        publish(&mut state, Event::PukEntry { correct: false });
    }
    assert_eq!(state.phase(), Phase::SimBlocked);
    for event in [
        Event::PukEntry { correct: true },
        Event::PinEntry { correct: true },
        Event::StartSearch,
        Event::Tick(1),
        Event::RegistrationReport(1),
        Event::SignalReport(10),
        Event::CpinReport(G8lS555CpinState::Ready),
    ] {
        assert_eq!(step(&mut state, event), Err(Error::SimBlocked), "{event:?}");
    }
    let removed = publish(&mut state, Event::CpinReport(G8lS555CpinState::NotInserted));
    assert_eq!(removed.to, Phase::NoSim);
    assert_eq!(removed.pin_attempts_remaining, S555_PIN_ATTEMPTS);
    assert_eq!(removed.puk_attempts_remaining, S555_PUK_ATTEMPTS);
    assert_eq!(removed.rssi, S555_CSQ_RSSI_UNKNOWN);
    assert!(removed.operator.is_empty());
    assert_eq!(
        step(&mut state, Event::CpinReport(G8lS555CpinState::NotInserted)),
        Err(Error::RedundantCpinReport)
    );
    let mut registered = registered_state();
    publish(&mut registered, Event::OperatorReport(operator("Vodafone TR")));
    let removed = publish(
        &mut registered,
        Event::CpinReport(G8lS555CpinState::NotInserted),
    );
    assert_eq!(removed.from, Phase::Registered { roaming: false });
    assert_eq!(removed.registration_stat, 0);
    assert!(removed.operator.is_empty());
}

#[test]
fn cpin_line_parser_maps_all_states_and_rejects_malformed() {
    assert_eq!(parse_cpin_line("+CPIN: READY\r\n"), Ok(G8lS555CpinState::Ready));
    assert_eq!(parse_cpin_line("+CPIN: SIM PIN"), Ok(G8lS555CpinState::SimPin));
    assert_eq!(parse_cpin_line("+CPIN:SIM PUK\r"), Ok(G8lS555CpinState::SimPuk));
    assert_eq!(
        parse_cpin_line("+CPIN: NOT INSERTED"),
        Ok(G8lS555CpinState::NotInserted)
    );
    for bad in ["+CPIN: SIM PIN2", "+CPIN:", "CPIN: READY", "+CPIN: ready", "", "+CREG: 0,1"] {
        assert_eq!(parse_cpin_line(bad), Err(Error::InvalidCpinLine), "{bad:?}");
    }
    for state in [
        G8lS555CpinState::Ready,
        G8lS555CpinState::SimPin,
        G8lS555CpinState::SimPuk,
        G8lS555CpinState::NotInserted,
    ] {
        assert_eq!(parse_cpin_line(&format!("+CPIN: {}", state.text())), Ok(state));
    }
}

#[test]
fn cpin_reports_cannot_skip_pin_entry_or_regress() {
    let mut state = State::new();
    publish(&mut state, Event::CpinReport(G8lS555CpinState::SimPin));
    assert_eq!(
        step(&mut state, Event::CpinReport(G8lS555CpinState::Ready)),
        Err(Error::ReadyWithoutPinEntry)
    );
    assert_eq!(
        step(&mut state, Event::CpinReport(G8lS555CpinState::SimPuk)),
        Err(Error::CpinRegression)
    );
    let mut ready = ready_state();
    assert_eq!(
        step(&mut ready, Event::CpinReport(G8lS555CpinState::SimPin)),
        Err(Error::CpinRegression)
    );
    assert_eq!(
        step(&mut ready, Event::CpinReport(G8lS555CpinState::Ready)),
        Err(Error::CpinRegression)
    );
    assert_eq!(ready.receipts().len(), 1);
}

#[test]
fn registration_stat_mapping_covers_0_to_5_and_rejects_others() {
    let expected = [
        (0, G8lS555RegistrationStat::NotRegistered, false),
        (1, G8lS555RegistrationStat::Home, true),
        (2, G8lS555RegistrationStat::Searching, false),
        (3, G8lS555RegistrationStat::Denied, false),
        (4, G8lS555RegistrationStat::Unknown, false),
        (5, G8lS555RegistrationStat::Roaming, true),
    ];
    for (code, stat, registered) in expected {
        assert_eq!(G8lS555RegistrationStat::from_code(code), Some(stat));
        assert_eq!(stat.code(), code);
        assert_eq!(stat.is_registered(), registered);
        assert_eq!(parse_registration_line(&format!("+CREG: 0,{code}")), Ok(stat));
        assert_eq!(parse_registration_line(&format!("+CEREG: 2,{code},\"1A2B\",\"01C3D4E5\",7\r\n")), Ok(stat));
    }
    for code in [6u8, 7, 99, 255] {
        assert_eq!(G8lS555RegistrationStat::from_code(code), None);
        assert_eq!(
            parse_registration_line(&format!("+CREG: 0,{code}")),
            Err(Error::InvalidRegistrationStat)
        );
    }
    for bad in ["+CREG: 1", "+CREG: 3,1", "+CREG: x,1", "+CSQ: 0,1", "+CREG:", "+CREG: 0,"] {
        assert_eq!(parse_registration_line(bad), Err(Error::InvalidRegistrationLine), "{bad:?}");
    }
    let mut state = registered_state();
    assert_eq!(step(&mut state, Event::RegistrationReport(6)), Err(Error::InvalidRegistrationStat));
}

#[test]
fn csq_rssi_to_bars_table_boundaries_and_99_unknown() {
    let table = [
        (0, 0), (1, 0), (2, 1), (7, 1), (8, 2), (13, 2), (14, 3), (19, 3), (20, 4),
        (25, 4), (26, 5), (31, 5), (99, 0),
    ];
    for (rssi, bars) in table {
        assert_eq!(csq_rssi_to_bars(rssi), Ok(bars), "rssi {rssi}");
        assert!(bars <= S555_SIGNAL_BARS_MAX);
        assert_eq!(parse_csq_line(&format!("+CSQ: {rssi},99")), Ok((rssi, bars)));
    }
    for rssi in [32u8, 33, 50, 98, 100, 255] {
        assert_eq!(csq_rssi_to_bars(rssi), Err(Error::InvalidRssi), "rssi {rssi}");
        assert_eq!(parse_csq_line(&format!("+CSQ: {rssi},0")), Err(Error::InvalidRssi));
    }
    assert_eq!(parse_csq_line("+CSQ: 31,7\r\n"), Ok((31, 5)));
    for bad in ["+CSQ: 31", "+CSQ: 31,8", "+CSQ: 31,0,1", "+CSQ: -1,0", "+CSQ: 1000,0", "+COPS: 0"] {
        assert_eq!(parse_csq_line(bad), Err(Error::InvalidSignalLine), "{bad:?}");
    }
    let mut state = State::new();
    assert_eq!(step(&mut state, Event::SignalReport(10)), Err(Error::SignalWithoutSim));
    assert_eq!(step(&mut state, Event::SignalReport(32)), Err(Error::InvalidRssi));
    let mut ready = ready_state();
    let unknown = publish(&mut ready, Event::SignalReport(99));
    assert_eq!((unknown.rssi, unknown.signal_bars, unknown.to), (99, 0, Phase::SimReady));
    assert_eq!(step(&mut ready, Event::SignalReport(32)), Err(Error::InvalidRssi));
    assert_eq!(ready.receipts().len(), 2);
}

#[test]
fn search_is_bounded_by_ticks_and_tick_overflow_fails_closed() {
    let mut state = ready_state();
    assert_eq!(step(&mut state, Event::Tick(1)), Err(Error::TickOutsideSearch));
    publish(&mut state, Event::StartSearch);
    let exact = publish(&mut state, Event::Tick(S555_SEARCH_TIMEOUT_TICKS));
    assert_eq!((exact.to, exact.search_ticks_elapsed), (Phase::Searching, 60));
    assert_eq!(step(&mut state, Event::Tick(u32::MAX)), Err(Error::TickOverflow));
    assert_eq!(state.search_ticks_elapsed(), 60);
    let timed_out = publish(&mut state, Event::Tick(1));
    assert_eq!((timed_out.to, timed_out.search_ticks_elapsed), (Phase::SearchTimedOut, 61));
    assert_eq!(timed_out.registration_stat, 0);
    assert_eq!(step(&mut state, Event::Tick(1)), Err(Error::TickOutsideSearch));
    assert_eq!(step(&mut state, Event::RegistrationReport(1)), Err(Error::RegistrationWithoutSearch));
    let retry = publish(&mut state, Event::StartSearch);
    assert_eq!((retry.from, retry.to, retry.search_ticks_elapsed), (Phase::SearchTimedOut, Phase::Searching, 0));
    assert_eq!(step(&mut state, Event::StartSearch), Err(Error::SearchWhileRegistered));
}

#[test]
fn registration_without_sim_ready_fails_closed() {
    let mut no_sim = State::new();
    for code in 0..=S555_REGISTRATION_STAT_MAX {
        assert_eq!(
            step(&mut no_sim, Event::RegistrationReport(code)),
            Err(Error::RegistrationWithoutSimReady)
        );
    }
    assert_eq!(step(&mut no_sim, Event::StartSearch), Err(Error::SearchWithoutSimReady));
    assert_eq!(step(&mut no_sim, Event::PinEntry { correct: true }), Err(Error::PinEntryWithoutPinRequest));
    assert!(no_sim.receipts().is_empty());
    let mut pin = State::new();
    publish(&mut pin, Event::CpinReport(G8lS555CpinState::SimPin));
    assert_eq!(step(&mut pin, Event::RegistrationReport(1)), Err(Error::RegistrationWithoutSimReady));
    assert_eq!(step(&mut pin, Event::StartSearch), Err(Error::SearchWithoutSimReady));
    let mut ready = ready_state();
    assert_eq!(step(&mut ready, Event::RegistrationReport(1)), Err(Error::RegistrationWithoutSearch));
    assert_eq!(step(&mut ready, Event::RegistrationReport(5)), Err(Error::RegistrationWithoutSearch));
    assert_eq!(ready.phase(), Phase::SimReady);
    assert_eq!(ready.receipts().len(), 1);
}

#[test]
fn roaming_denied_and_loss_transitions_follow_the_stat_table() {
    let mut state = ready_state();
    publish(&mut state, Event::StartSearch);
    let unknown = publish(&mut state, Event::RegistrationReport(4));
    assert_eq!((unknown.to, unknown.registration_stat), (Phase::Searching, 4));
    let roaming = publish(&mut state, Event::RegistrationReport(5));
    assert_eq!(roaming.to, Phase::Registered { roaming: true });
    publish(&mut state, Event::OperatorReport(operator("Roam Net")));
    let home = publish(&mut state, Event::RegistrationReport(1));
    assert_eq!(home.to, Phase::Registered { roaming: false });
    assert_eq!(home.operator.as_str(), "Roam Net");
    let lost = publish(&mut state, Event::RegistrationReport(2));
    assert_eq!((lost.to, lost.search_ticks_elapsed), (Phase::Searching, 0));
    assert!(lost.operator.is_empty());
    let denied = publish(&mut state, Event::RegistrationReport(3));
    assert_eq!((denied.to, denied.registration_stat), (Phase::Denied, 3));
    assert_eq!(step(&mut state, Event::RegistrationReport(1)), Err(Error::RegistrationWithoutSearch));
    assert_eq!(step(&mut state, Event::OperatorReport(operator("X"))), Err(Error::OperatorWithoutRegistration));
    let retry = publish(&mut state, Event::StartSearch);
    assert_eq!(retry.from, Phase::Denied);
    let dropped = publish(&mut state, Event::RegistrationReport(0));
    assert_eq!((dropped.to, dropped.registration_stat), (Phase::SimReady, 0));
    assert_eq!(step(&mut state, Event::RegistrationReport(1)), Err(Error::RegistrationWithoutSearch));
}

#[test]
fn operator_report_requires_registration_and_bounded_printable_name() {
    assert_eq!(
        G8lS555OperatorName::from_text(""),
        Err(Error::OperatorNameEmpty)
    );
    assert_eq!(
        G8lS555OperatorName::from_text("ABCDEFGHIJKLMNOPQ"),
        Err(Error::OperatorNameTooLong)
    );
    assert_eq!(
        G8lS555OperatorName::from_text("Türk Telekom"),
        Err(Error::OperatorNameNotPrintable)
    );
    assert_eq!(
        G8lS555OperatorName::from_text("A\"B"),
        Err(Error::OperatorNameNotPrintable)
    );
    assert_eq!(
        G8lS555OperatorName::from_text("A\rB"),
        Err(Error::OperatorNameNotPrintable)
    );
    let max = G8lS555OperatorName::from_text("ABCDEFGHIJKLMNOP").unwrap();
    assert_eq!(max.len(), S555_OPERATOR_NAME_MAX_LEN);
    assert_eq!(max.as_str(), "ABCDEFGHIJKLMNOP");
    assert_eq!(G8lS555OperatorName::empty().as_str(), "");
    assert_eq!(parse_cops_line("+COPS: 0,0,\"Turkcell\",7\r\n"), Ok(operator("Turkcell")));
    assert_eq!(parse_cops_line("+COPS: 1,2,\"28601\""), Ok(operator("28601")));
    for bad in [
        "+COPS: 0",
        "+COPS: 0,0,Turkcell",
        "+COPS: 5,0,\"Turkcell\"",
        "+COPS: 0,3,\"Turkcell\"",
        "+COPS: 0,0,1,\"Turkcell\"",
        "+CSQ: 0,0,\"Turkcell\"",
    ] {
        assert_eq!(parse_cops_line(bad), Err(Error::InvalidOperatorLine), "{bad:?}");
    }
    assert_eq!(parse_cops_line("+COPS: 0,0,\"\""), Err(Error::OperatorNameEmpty));
    assert_eq!(
        parse_cops_line("+COPS: 0,0,\"ABCDEFGHIJKLMNOPQ\""),
        Err(Error::OperatorNameTooLong)
    );
    let mut ready = ready_state();
    assert_eq!(
        step(&mut ready, Event::OperatorReport(operator("Turkcell"))),
        Err(Error::OperatorWithoutRegistration)
    );
    let mut searching = ready_state();
    publish(&mut searching, Event::StartSearch);
    assert_eq!(
        step(&mut searching, Event::OperatorReport(operator("Turkcell"))),
        Err(Error::OperatorWithoutRegistration)
    );
    let mut registered = registered_state();
    assert_eq!(
        step(&mut registered, Event::OperatorReport(G8lS555OperatorName::empty())),
        Err(Error::OperatorNameEmpty)
    );
    let r = publish(&mut registered, Event::OperatorReport(operator("Turkcell")));
    assert_eq!(state_operator(&registered), "Turkcell");
    assert_eq!(r.operator, operator("Turkcell"));
}

fn state_operator(state: &State) -> &str {
    state.receipts().last().map(|r| r.operator.as_str()).unwrap_or("")
}

#[test]
fn step_out_of_order_and_ledger_full_fail_closed() {
    let mut state = State::new();
    assert_eq!(
        service_s555_model_sim_registration_transition(
            &mut state,
            1,
            Event::CpinReport(G8lS555CpinState::Ready)
        ),
        Err(Error::StepOutOfOrder)
    );
    assert!(state.receipts().is_empty());
    publish(&mut state, Event::CpinReport(G8lS555CpinState::Ready));
    assert_eq!(
        service_s555_model_sim_registration_transition(&mut state, 5, Event::StartSearch),
        Err(Error::StepOutOfOrder)
    );
    publish(&mut state, Event::StartSearch);
    let mut published = 2;
    while published < S555_MAX_TRANSITIONS {
        let r = publish(&mut state, Event::SignalReport((published % 32) as u8));
        assert_eq!(r.step, published);
        published += 1;
    }
    assert_eq!(state.receipts().len(), S555_MAX_TRANSITIONS);
    assert_eq!(step(&mut state, Event::SignalReport(1)), Err(Error::LedgerFull));
    assert_eq!(state.receipts().len(), S555_MAX_TRANSITIONS);
    assert_eq!(state.phase(), Phase::Searching);
    let first = state.receipts()[0];
    assert_eq!(
        service_s555_model_sim_registration_transition(
            &mut state,
            0,
            Event::CpinReport(G8lS555CpinState::Ready)
        ),
        Ok(Outcome::TransitionRetained(first))
    );
}

#[test]
fn default_state_is_no_sim_with_unknown_signal_and_empty_ledger() {
    let state = State::default();
    assert_eq!(state.phase(), Phase::NoSim);
    assert_eq!(state.pin_attempts_remaining(), S555_PIN_ATTEMPTS);
    assert_eq!(state.puk_attempts_remaining(), S555_PUK_ATTEMPTS);
    assert_eq!(state.search_ticks_elapsed(), 0);
    assert_eq!(state.registration_stat(), 0);
    assert!(state.operator().is_empty());
    assert_eq!(state.signal_bars(), 0);
    assert!(state.receipts().is_empty());
    assert_eq!(state.last_receipt(), None);
}

#[test]
fn source_only_gate_keeps_runtime_physical_and_r1_claims_zero() {
    assert!(SOURCE.contains("S555_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS: usize = 0"));
    assert!(SOURCE.contains("S555_PHYSICAL_OBSERVATIONS: usize = 0"));
    assert!(SOURCE.contains("S555_PHYSICAL_OR_DEVICE_OPERATIONS: usize = 0"));
    assert!(SOURCE.contains("S555_HARDWARE_PRESENT: bool = false"));
    assert!(SOURCE.contains("S555_R1_ACCEPTANCE_COMPLETE: bool = false"));
    assert!(SOURCE.contains("RUNBOOK_EXECUTED_IN_S555: bool = false"));
    assert!(SOURCE.contains("S555_R1_STAGE: u8 = 3"));
    assert!(!SOURCE.contains("static S555"));
    assert!(!SOURCE.contains("Mutex"));
}
snippet sha256: baf39799f25afile sha256: baf39799f25a
03 · Kapı kimlik kaydı

Operations sıra, kimlik ve başlık bağı

tam Operations kaydıL2601–L2659
website/src/lib/operations.ts::g8l-s555-r1-sim-registration-state-machine-model
  {
    id: "g8l-s555-r1-sim-registration-state-machine-model",
    date: "2026-08-30",
    sequence: 555,
    status: "passed",
    umbrella_status: "partial",
    title: "S555 · R1 modem: SIM kayıt durum makinesi modeli",
    summary:
      "S555 kaynak/host model kapısı PASS'tir: R1 3. aşama (modem, veri, arama ve ses) için SIM ve şebeke kayıt durum makinesi saf bir model olarak yazıldı. Model +CPIN durumlarını (READY, SIM PIN, SIM PUK, NOT INSERTED), 3 PIN denemesi sonrası PUK ve 10 PUK denemesi sonrası SIM bloklanmasını, +CREG/+CEREG <stat> 0..5 eşlemesini, sınırlı +COPS operatör adını, +CSQ RSSI 0..31/99 → 0..5 bar tablosunu ve 60 tick ile sınırlı aramayı kapsar; her geçiş bir receipt üretir ve geçersiz geçişler (örneğin SIM READY olmadan kayıt) fail-closed reddedilir. Focused 21/21 PASS'tir. S540 ve S543 fiziksel raw/verdict değişmez RED kalır; hiçbir modem, SIM, UART, panel veya board yoktur; physical observation=0, SD/UART/power/new-raw=0/0/0/0, Boot-to-UI=false ve R1 acceptance=false'dur. RUNBOOK_EXECUTED_IN_S555=NO. S556 host-only SMS PDU encode/decode modeli kapısıdır.",
    evidence: [
      "S555, S554'ten ayrı saf model kernel modülü, 21-test focused binary, proof, status manifest, Operations kaydı ve complete Code kartına sahiptir; production callsite yoktur ve modül hiçbir boot, IRQ, scheduler veya driver yoluna bağlı değildir.",
      "Dar S555 source/host status=PASS; R1 umbrella=PARTIAL ve S540/S543 physical gate status=RED olarak ayrı tutulur.",
      "+CPIN çözücü READY, SIM PIN, SIM PUK ve NOT INSERTED metinlerini G8lS555CpinState'e eşler; diğer her metin InvalidCpinLine ile reddedilir.",
      "+CREG/+CEREG <stat> 0..5 sırasıyla NotRegistered, Home, Searching, Denied, Unknown, Roaming olarak eşlenir; 6..255 InvalidRegistrationStat, <n>>2 veya eksik alan InvalidRegistrationLine verir.",
      "+COPS operatör adı 1..16 bayt yazdırılabilir ASCII ile sınırlıdır; boş, uzun, yazdırılamayan veya tırnak içeren adlar ayrı diagnostic kodlarıyla fail-closed döner.",
      "+CSQ RSSI→bar tablosu 0..1→0, 2..7→1, 8..13→2, 14..19→3, 20..25→4, 26..31→5 ve 99→0 (unknown) biçimindedir; 32..98 ve 100..255 InvalidRssi ile reddedilir.",
      "Durum makinesi NoSim, PinRequired, PukRequired, SimBlocked, SimReady, Searching, Registered{roaming}, Denied ve SearchTimedOut fazlarını taşır; 3 yanlış PIN PUK'a, 10 yanlış PUK SimBlocked'a götürür ve doğru PUK sayaçları sıfırlar.",
      "Arama 60 tick ile sınırlıdır: toplam 60'ı aşan tick SearchTimedOut geçişi üretir, tick toplamı checked_add ile yapılır ve taşma TickOverflow ile reddedilir.",
      "SIM READY olmadan kayıt raporu RegistrationWithoutSimReady, aktif arama olmadan kayıt raporu RegistrationWithoutSearch, PIN/PUK beklenirken +CPIN: READY ReadyWithoutPinEntry ve bloklu SIM'de kaldırma dışındaki her olay SimBlocked ile fail-closed reddedilir; durum değişmez.",
      "Her kabul edilen geçiş from/to faz, olay, kalan PIN/PUK denemesi, geçen tick, <stat>, operatör, RSSI ve bar içeren bir receipt üretir; ledger en fazla 64 receipt tutar ve dolduğunda LedgerFull döner.",
      "Aynı adımın exact replay'i TransitionRetained ile aynı receipt'i döndürür; yayınlanmış bir adımda farklı olay PublishedStateDrift, sıra dışı adım StepOutOfOrder verir.",
      "26 hata kodu sıfırdan farklı ve tekildir; kaynakta unsafe, asm!, write_volatile, crate::uart, crate::arch, #[no_mangle] ve spin:: yüzeyi yoktur.",
      "Focused target 1 grup / 21 passed / 0 failed / 0 ignored / 0 filtered verdi.",
      "Implementation 23004 B / 06eeaf6a208804842f0c620557d346b1445fcbda40a1948754f958a2aa7d38b2; focused test 25597 B / baf39799f25a301472df7a69603cca11e03e16320162908199da4efd32bb08c9 SHA-256'dır.",
      "Proof 4710 B'dir.",
      "S540 immutable raw 20525 B ve S543 immutable raw 20509 B fiziksel RED olarak byte-exact korunur; automatic promotion=false ve rerun=false'dur.",
      "S555 sırasında modem, SIM, AT transport, SD write/read-back/eject, UART open/capture, power transition, fiziksel koşu veya yeni immutable raw üretimi yapılmadı.",
      "RUNBOOK_EXECUTED_IN_S555=NO; supported-profile runtime observations=0, physical observations=0, hardware present=false, Boot-to-UI physically observed=false ve R1 acceptance=false'dur.",
      "S556 yalnız host üzerinde SMS PDU encode/decode modelini yazacaktı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_s555_r1_sim_registration_state_machine_model -- --test-threads=1",
    ],
    terminalSessions: [
      {
        id: "s555-focused",
        title: "S555 SIM kayıt durum makinesi modeli focused",
        commandLines: [
          "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s555_r1_sim_registration_state_machine_model -- --test-threads=1",
        ],
        outputLines: [
          "test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s",
          "S555 focused=1 group / 21 passed / 0 failed",
          "hardware=none physical=0 runbook=NO",
        ],
        exitCode: 0,
        outputMode: "complete",
      },
    ],
    terminalSessionsNote:
      "S555 kaynak/host model PASS'tir; supported-profile runtime, modem veya fiziksel PASS değildir. S540 ve S543 RED raw ve kararları değişmez.",
    limitations: [
      "S555 saf bir kaynak/host modelidir; hiçbir donanım/panel/modem/board gözlemi yoktur ve gerçek bir SIM veya şebeke ile hiçbir kayıt gözlenmemiştir.",
      "Modülün production callsite'ı yoktur; gerçek AT transport, modem sürücüsü ve UART bağlantısı bu kapının dışındadır.",
      "S540 ve S543 fiziksel RED immutable kalır; S546 fiziksel koşusunun kararı bu kapıda varsayılmaz.",
      "BOOT_TO_UI_READY gerçek UART'ta görülmedi; Boot-to-UI ve R1 acceptance false kalır.",
      "S556 host-only SMS PDU encode/decode modelidir; yeni SD/UART/power koşusu ayrı kapı, fresh target revalidation, açık operatör yetkisi ve yeni immutable raw ister.",
    ],
  },
snippet sha256: ff84919bfae6file 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_s555_r1_sim_registration_state_machine_model -- --test-threads=1
proof: docs/M8.1-RPi5-G8l-S555-R1-SIM-Registration-State-Machine-Model-Proof.md
Registry schema v5 · generator website/scripts/generate-code-gates.mjs · Tam SHA-256: 91d38c7b6222f0b4c117be786454853543da55a160e543d9b951057cc20dcc06