Skip to main content

ax_runtime/serial/
mod.rs

1//! UART runtime ownership and task-context data service.
2//!
3//! Each UART has one CPU-affine maintenance task. Sleepable TTY output and
4//! non-blocking per-CPU log records use separate bounded queues; only the IRQ
5//! endpoint, maintenance task, and emergency endpoint touch UART registers.
6
7mod control;
8mod ingress;
9mod log_mailbox;
10pub(crate) mod spsc;
11mod state;
12mod worker;
13
14use alloc::{boxed::Box, sync::Arc, vec::Vec};
15use core::{
16    fmt::{self, Write},
17    sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
18};
19
20use ax_driver::serial::SerialDevice;
21pub use ax_driver::serial::SerialDeviceInfo;
22use ax_lazyinit::OnceLock;
23use ax_sync::Mutex;
24use ax_task::{AxCpuMask, IrqNotify, TaskInner, WaitQueue};
25use axpoll::{IoEvents, PollSet};
26pub use rdif_serial::{Config, ConfigError, DataBits, Parity, RxFlag, StopBits};
27
28pub(crate) use self::log_mailbox::{LogRecord, LogRecordKind};
29use self::{
30    control::{ControlOp, ControlQueue},
31    ingress::TxIngress,
32    log_mailbox::{LogMailbox, LogRecordMeta},
33    spsc::{Consumer as SpscConsumer, Producer as SpscProducer},
34    state::{SerialIrqLatch, SerialStatsAtomic},
35    worker::SerialWorker,
36};
37use crate::{RuntimeError, RuntimeResult, sync::SpinLock};
38
39const NO_ACTIVE_CONSOLE: usize = usize::MAX;
40const IRQ_RX_CAPACITY: usize = 16_384;
41const SUBSCRIPTION_RX_CAPACITY: usize = 4_096;
42const LOG_SUBSCRIPTION_CAPACITY: usize = 64;
43
44static SERIAL_RUNTIMES: OnceLock<Box<[SerialRuntimeHandle]>> = OnceLock::new();
45static LOG_MAILBOX: OnceLock<Arc<LogMailbox>> = OnceLock::new();
46static ACTIVE_CONSOLE: AtomicUsize = AtomicUsize::new(NO_ACTIVE_CONSOLE);
47
48const RUNTIME_DORMANT: u8 = 0;
49const RUNTIME_STARTED: u8 = 1;
50const RUNTIME_FAILED_CLOSED: u8 = 2;
51
52struct RuntimeLifecycle(AtomicU8);
53
54impl RuntimeLifecycle {
55    const fn new() -> Self {
56        Self(AtomicU8::new(RUNTIME_DORMANT))
57    }
58
59    fn started(&self) -> bool {
60        self.0.load(Ordering::Acquire) == RUNTIME_STARTED
61    }
62
63    fn ensure_available(&self) -> RuntimeResult {
64        (self.0.load(Ordering::Acquire) != RUNTIME_FAILED_CLOSED)
65            .then_some(())
66            .ok_or(RuntimeError::ConsoleFailedClosed)
67    }
68
69    fn ensure_started(&self) -> RuntimeResult {
70        match self.0.load(Ordering::Acquire) {
71            RUNTIME_STARTED => Ok(()),
72            RUNTIME_FAILED_CLOSED => Err(RuntimeError::ConsoleFailedClosed),
73            RUNTIME_DORMANT => Err(RuntimeError::SerialNotStarted),
74            _ => unreachable!(),
75        }
76    }
77
78    fn set_started(&self, started: bool) {
79        let next = if started {
80            RUNTIME_STARTED
81        } else {
82            RUNTIME_DORMANT
83        };
84        let _ = self
85            .0
86            .try_update(Ordering::AcqRel, Ordering::Acquire, |state| {
87                (state != RUNTIME_FAILED_CLOSED).then_some(next)
88            });
89    }
90
91    fn fail_closed(&self) {
92        self.0.store(RUNTIME_FAILED_CLOSED, Ordering::Release);
93    }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum RxItem {
98    Byte { byte: u8, flag: RxFlag },
99    Overrun,
100}
101
102impl Default for RxItem {
103    fn default() -> Self {
104        Self::Byte {
105            byte: 0,
106            flag: RxFlag::Normal,
107        }
108    }
109}
110
111struct RuntimeIrqBridge {
112    latch: SerialIrqLatch,
113    rx_overflow: AtomicBool,
114    notify: IrqNotify,
115}
116
117impl RuntimeIrqBridge {
118    const fn new() -> Self {
119        Self {
120            latch: SerialIrqLatch::new(),
121            rx_overflow: AtomicBool::new(false),
122            notify: IrqNotify::new(),
123        }
124    }
125}
126
127struct RuntimeShared {
128    index: usize,
129    info: SerialDeviceInfo,
130    owner_cpu: usize,
131    polling: bool,
132    port: SpinLock<Box<dyn rdif_serial::UartPort>>,
133    register_gate: Arc<rdif_serial::UartRegisterGate<dyn rdif_serial::UartEmergencyTx>>,
134    ingress: TxIngress,
135    log_mailbox: Arc<LogMailbox>,
136    rx_subscription: SpinLock<Option<SpscConsumer<RxItem>>>,
137    log_subscription: SpinLock<Option<SpscConsumer<LogRecord>>>,
138    log_subscription_gate: SpinLock<()>,
139    log_subscription_active: AtomicBool,
140    log_subscription_dropped_records: AtomicUsize,
141    log_subscription_dropped_bytes: AtomicUsize,
142    control: ControlQueue,
143    bridge: Arc<RuntimeIrqBridge>,
144    stats: Arc<SerialStatsAtomic>,
145    rx_source: Arc<PollSet>,
146    tx_source: Arc<PollSet>,
147    rx_progress: WaitQueue,
148    console_progress: WaitQueue,
149    tx_progress: WaitQueue,
150    tty_output_lock: Mutex<()>,
151    log_barriers: AtomicUsize,
152    lifecycle: RuntimeLifecycle,
153    irq_handle: OnceLock<ax_hal::irq::IrqHandle>,
154}
155
156impl RuntimeShared {
157    /// Runs one task-context register transaction with local IRQ delivery
158    /// excluded and all cross-CPU aliases serialized by the UART gate.
159    fn with_port<R>(&self, access: impl FnOnce(&mut dyn rdif_serial::UartPort) -> R) -> Option<R> {
160        let mut port = self.port.lock_irqsave();
161        let _register_access = loop {
162            if self.register_gate.emergency_active() {
163                return None;
164            }
165            if let Some(access) = self.register_gate.try_enter() {
166                break access;
167            }
168            core::hint::spin_loop();
169        };
170        Some(access(&mut **port))
171    }
172
173    fn started(&self) -> bool {
174        self.lifecycle.started()
175    }
176
177    fn ensure_started(&self) -> RuntimeResult {
178        self.lifecycle.ensure_started()
179    }
180
181    fn set_started(&self, started: bool) {
182        self.lifecycle.set_started(started);
183        if !started {
184            self.rx_progress.notify_all(true);
185            self.console_progress.notify_all(true);
186            self.tx_progress.notify_all(true);
187        }
188    }
189
190    fn fail_closed(&self) {
191        self.lifecycle.fail_closed();
192        self.disable_irq();
193        // `FailedClosed` is not merely an API state. Terminally claim the
194        // register gate so a final in-flight worker or IRQ transaction cannot
195        // hand the UART back to a normal endpoint afterward. The lifecycle
196        // publication and disabled IRQ prevent new contenders; an existing
197        // bounded register transaction is allowed to finish.
198        while !self.register_gate.emergency_active() {
199            if let Some(access) = self.register_gate.try_begin_emergency() {
200                drop(access);
201                break;
202            }
203            core::hint::spin_loop();
204        }
205        self.ingress.stop_and_discard();
206        self.rx_progress.notify_all(true);
207        self.console_progress.notify_all(true);
208        self.tx_progress.notify_all(true);
209    }
210
211    fn publish_tx_space(&self) {
212        self.tx_progress.notify_all(true);
213        // SAFETY: the maintenance task publishes queue space before waking
214        // task-context poll waiters.
215        unsafe { self.tx_source.wake(IoEvents::OUT) };
216    }
217
218    fn enable_irq(&self) -> RuntimeResult {
219        let Some(handle) = self.irq_handle.get().copied() else {
220            return Ok(());
221        };
222        ax_hal::irq::enable_irq(handle).map_err(|error| {
223            warn!(
224                "failed to enable serial IRQ for {}: {error:?}",
225                self.info.name
226            );
227            RuntimeError::from(error)
228        })
229    }
230
231    fn disable_irq(&self) {
232        let Some(handle) = self.irq_handle.get().copied() else {
233            return;
234        };
235        if let Err(err) = ax_hal::irq::disable_irq(handle) {
236            warn!(
237                "failed to disable serial IRQ for {}: {err:?}",
238                self.info.name
239            );
240        }
241    }
242}
243
244/// Cloneable OS-facing façade for one UART runtime.
245#[derive(Clone)]
246pub struct SerialRuntimeHandle {
247    shared: Arc<RuntimeShared>,
248}
249
250impl SerialRuntimeHandle {
251    pub fn info(&self) -> &SerialDeviceInfo {
252        &self.shared.info
253    }
254
255    /// Leases the only RX subscription.
256    ///
257    /// Dropping the subscription returns the consumer to this runtime so a
258    /// failed owner initialization does not permanently consume the RX path.
259    pub fn take_rx_subscription(&self) -> Option<SerialRxSubscription> {
260        self.shared.lifecycle.ensure_available().ok()?;
261        let consumer = self.shared.rx_subscription.lock_irqsave().take()?;
262        Some(SerialRxSubscription {
263            consumer: SpinLock::new(Some(consumer)),
264            shared: self.shared.clone(),
265        })
266    }
267
268    pub(crate) fn take_log_subscription(&self) -> Option<SerialLogSubscription> {
269        self.shared.lifecycle.ensure_available().ok()?;
270        let _route = self.shared.log_subscription_gate.lock_irqsave();
271        if self.shared.log_subscription_active.load(Ordering::Acquire) {
272            return None;
273        }
274        let mut available = self.shared.log_subscription.lock_irqsave();
275        let mut consumer = available.take()?;
276        consumer.clear();
277        self.shared
278            .log_subscription_dropped_records
279            .store(0, Ordering::Release);
280        self.shared
281            .log_subscription_dropped_bytes
282            .store(0, Ordering::Release);
283        self.shared
284            .log_subscription_active
285            .store(true, Ordering::Release);
286        Some(SerialLogSubscription {
287            consumer: SpinLock::new(Some(consumer)),
288            shared: self.shared.clone(),
289        })
290    }
291
292    /// Returns a cloneable task-context output capability for this UART.
293    ///
294    /// This per-port API is used by operating systems that expose non-console
295    /// serial devices. Physical-console consumers should use
296    /// [`crate::console::output`] so raw-HAL fallback and failed-closed state
297    /// remain hidden behind the console boundary.
298    pub fn task_output(&self) -> SerialTaskOutput {
299        SerialTaskOutput {
300            shared: self.shared.clone(),
301        }
302    }
303
304    pub fn start(&self, config: Config) -> RuntimeResult {
305        self.shared.lifecycle.ensure_available()?;
306        self.shared
307            .control
308            .submit(ControlOp::Start(config), &self.shared.bridge.notify)
309    }
310
311    pub fn shutdown(&self) -> RuntimeResult {
312        let result = self
313            .shared
314            .control
315            .submit(ControlOp::Shutdown, &self.shared.bridge.notify);
316        if result.is_ok() {
317            deactivate_console(&self.shared);
318        }
319        result
320    }
321
322    pub fn set_config(&self, config: Config) -> RuntimeResult {
323        self.output_barrier()?.set_config(config)
324    }
325
326    /// Pauses extraction of new log records until the returned guard drops.
327    pub(crate) fn output_barrier(&self) -> RuntimeResult<SerialOutputBarrier> {
328        self.shared.ensure_started()?;
329        Ok(SerialOutputBarrier::new(self.shared.clone()))
330    }
331
332    /// Blocks new early-console register access before runtime configuration.
333    pub(crate) fn begin_console_handoff(&self) -> RuntimeResult {
334        ax_hal::console::begin_runtime_handoff()?;
335        Ok(())
336    }
337
338    /// Adopts the already-running firmware console while the platform path is
339    /// in `Preparing`.
340    ///
341    /// The worker preserves the firmware line/FIFO configuration and only
342    /// masks device-local sources before enabling its registered IRQ action.
343    /// The console coordinator owns the surrounding handoff transaction and
344    /// closes the early path if this operation fails.
345    pub(crate) fn adopt_prepared_console(&self) -> RuntimeResult {
346        self.shared
347            .control
348            .submit(ControlOp::AdoptFirmwareConsole, &self.shared.bridge.notify)
349    }
350
351    /// Permanently rejects task, IRQ-consumer, and per-port use after a
352    /// selected console handoff becomes untrustworthy.
353    pub(crate) fn fail_console_closed(&self) {
354        self.shared.fail_closed();
355    }
356
357    /// Publishes runtime log routing and completes the platform handoff.
358    pub(crate) fn commit_console_handoff(&self) -> RuntimeResult {
359        self.shared.ensure_started()?;
360        // Reserve log routing before publishing either console-owner state.
361        // Once the platform transition is committed there is no safe early
362        // owner to roll back to, so every remaining operation must be
363        // infallible.
364        if !self.shared.log_mailbox.claim(self.shared.index) {
365            let _ = self.shutdown();
366            return Err(RuntimeError::SerialConsoleBusy);
367        }
368        if ACTIVE_CONSOLE
369            .compare_exchange(
370                NO_ACTIVE_CONSOLE,
371                self.shared.index,
372                Ordering::AcqRel,
373                Ordering::Acquire,
374            )
375            .is_err()
376        {
377            self.shared.log_mailbox.release(self.shared.index);
378            let _ = self.shutdown();
379            return Err(RuntimeError::SerialConsoleBusy);
380        }
381        if let Err(error) = ax_hal::console::commit_runtime_handoff() {
382            let _ = ACTIVE_CONSOLE.compare_exchange(
383                self.shared.index,
384                NO_ACTIVE_CONSOLE,
385                Ordering::AcqRel,
386                Ordering::Acquire,
387            );
388            self.shared.log_mailbox.release(self.shared.index);
389            let _ = self.shutdown();
390            return Err(error.into());
391        }
392        self.shared.bridge.notify.notify();
393        Ok(())
394    }
395}
396
397/// Cloneable, bounded MPSC submission façade. It never accesses UART registers.
398#[derive(Clone)]
399pub(crate) struct SerialTxSender {
400    shared: Arc<RuntimeShared>,
401}
402
403impl SerialTxSender {
404    pub fn try_write(&self, bytes: &[u8]) -> RuntimeResult<usize> {
405        if bytes.is_empty() {
406            return Ok(0);
407        }
408        self.shared.ensure_started()?;
409        let accepted = self
410            .shared
411            .ingress
412            .try_write(bytes, &self.shared.bridge.notify);
413        if accepted == 0 {
414            Err(RuntimeError::WouldBlock)
415        } else {
416            Ok(accepted)
417        }
418    }
419
420    pub fn wait_writable(&self) -> RuntimeResult {
421        self.shared.ensure_started()?;
422        self.shared
423            .tx_progress
424            .wait_until(|| self.shared.ingress.write_room() > 0 || !self.shared.started());
425        self.shared
426            .started()
427            .then_some(())
428            .ok_or(RuntimeError::SerialNotStarted)
429    }
430
431    /// Writes every raw byte, sleeping only when the bounded runtime queue is full.
432    pub fn write_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
433        self.write_all_with(bytes, |shared, remaining| {
434            shared.ingress.try_write(remaining, &shared.bridge.notify)
435        })
436    }
437
438    /// Writes every text byte while expanding line feeds to CRLF.
439    pub fn write_text_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
440        self.write_all_with(bytes, |shared, remaining| {
441            shared
442                .ingress
443                .try_write_text(remaining, &shared.bridge.notify)
444        })
445    }
446
447    fn write_all_with(
448        &self,
449        bytes: &[u8],
450        submit: impl Fn(&RuntimeShared, &[u8]) -> usize,
451    ) -> RuntimeResult<usize> {
452        let mut written = 0;
453        while written < bytes.len() {
454            self.shared.ensure_started()?;
455            let accepted = submit(&self.shared, &bytes[written..]);
456            if accepted == 0 {
457                self.wait_writable()?;
458            } else {
459                written += accepted;
460            }
461        }
462        Ok(written)
463    }
464}
465
466/// Sleepable TTY/configuration transaction which excludes new log extraction.
467pub(crate) struct SerialOutputBarrier {
468    shared: Arc<RuntimeShared>,
469}
470
471impl SerialOutputBarrier {
472    fn new(shared: Arc<RuntimeShared>) -> Self {
473        shared.log_barriers.fetch_add(1, Ordering::AcqRel);
474        shared.bridge.notify.notify();
475        Self { shared }
476    }
477
478    /// Waits for queued TTY bytes, the current log record, and UART hardware
479    /// to become idle. New log records remain paused after this method returns.
480    pub fn wait_idle(&self) -> RuntimeResult {
481        self.shared.ensure_started()?;
482        self.shared.control.submit_drain(&self.shared.bridge.notify)
483    }
484
485    /// Applies configuration before allowing worker log extraction to resume.
486    pub fn set_config(&self, config: Config) -> RuntimeResult {
487        self.shared.ensure_started()?;
488        self.shared
489            .control
490            .submit(ControlOp::SetConfig(config), &self.shared.bridge.notify)
491    }
492}
493
494impl Drop for SerialOutputBarrier {
495    fn drop(&mut self) {
496        self.shared.log_barriers.fetch_sub(1, Ordering::AcqRel);
497        self.shared.bridge.notify.notify();
498    }
499}
500
501/// The unique RX consumer for one UART runtime.
502pub struct SerialRxSubscription {
503    consumer: SpinLock<Option<SpscConsumer<RxItem>>>,
504    shared: Arc<RuntimeShared>,
505}
506
507/// Internal complete-record consumer re-exported through `ax_runtime::console`.
508pub(crate) struct SerialLogSubscription {
509    consumer: SpinLock<Option<SpscConsumer<LogRecord>>>,
510    shared: Arc<RuntimeShared>,
511}
512
513impl SerialLogSubscription {
514    pub(crate) fn try_read(&self) -> Option<LogRecord> {
515        self.consumer.lock_irqsave().as_mut()?.pop()
516    }
517
518    pub(crate) fn dropped(&self) -> (usize, usize) {
519        (
520            self.shared
521                .log_subscription_dropped_records
522                .swap(0, Ordering::AcqRel),
523            self.shared
524                .log_subscription_dropped_bytes
525                .swap(0, Ordering::AcqRel),
526        )
527    }
528
529    pub(crate) fn wait_readable(&self) -> RuntimeResult {
530        self.shared.ensure_started()?;
531        self.shared.console_progress.wait_until(|| {
532            self.has_pending()
533                || !self.shared.log_subscription_active.load(Ordering::Acquire)
534                || !self.shared.started()
535        });
536        self.has_pending()
537            .then_some(())
538            .ok_or(RuntimeError::SerialNotStarted)
539    }
540
541    pub(crate) fn has_pending(&self) -> bool {
542        self.shared
543            .log_subscription_dropped_records
544            .load(Ordering::Acquire)
545            != 0
546            || self
547                .consumer
548                .lock_irqsave()
549                .as_ref()
550                .is_some_and(|consumer| !consumer.is_empty())
551    }
552}
553
554impl Drop for SerialLogSubscription {
555    fn drop(&mut self) {
556        let _route = self.shared.log_subscription_gate.lock_irqsave();
557        self.shared
558            .log_subscription_active
559            .store(false, Ordering::Release);
560        let Some(mut consumer) = self.consumer.get_mut().take() else {
561            return;
562        };
563        consumer.clear();
564        let mut available = self.shared.log_subscription.lock_irqsave();
565        debug_assert!(
566            available.is_none(),
567            "serial runtime cannot have two log consumers"
568        );
569        if available.is_none() {
570            *available = Some(consumer);
571        }
572        self.shared.console_progress.notify_all(true);
573        self.shared.bridge.notify.notify();
574    }
575}
576
577/// Cloneable task-context output capability for one runtime UART.
578#[derive(Clone)]
579pub struct SerialTaskOutput {
580    shared: Arc<RuntimeShared>,
581}
582
583impl SerialTaskOutput {
584    pub fn try_write(&self, bytes: &[u8]) -> RuntimeResult<usize> {
585        let Some(_output) = self.shared.tty_output_lock.try_lock() else {
586            return Err(RuntimeError::WouldBlock);
587        };
588        SerialTxSender {
589            shared: self.shared.clone(),
590        }
591        .try_write(bytes)
592    }
593
594    pub fn write_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
595        let _output = self.shared.tty_output_lock.lock();
596        SerialTxSender {
597            shared: self.shared.clone(),
598        }
599        .write_all(bytes)
600    }
601
602    pub fn write_text_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
603        let _output = self.shared.tty_output_lock.lock();
604        SerialTxSender {
605            shared: self.shared.clone(),
606        }
607        .write_text_all(bytes)
608    }
609
610    pub fn write_fmt(&self, args: fmt::Arguments<'_>) -> fmt::Result {
611        let _output = self.shared.tty_output_lock.lock();
612        let mut writer = ActiveConsoleWriter {
613            sender: SerialTxSender {
614                shared: self.shared.clone(),
615            },
616        };
617        writer.write_fmt(args)
618    }
619
620    pub fn wait_idle(&self) -> RuntimeResult {
621        let _output = self.shared.tty_output_lock.lock();
622        SerialOutputBarrier::new(self.shared.clone()).wait_idle()
623    }
624
625    pub fn discard_pending(&self) -> RuntimeResult {
626        let _output = self.shared.tty_output_lock.lock();
627        self.shared.ensure_started()?;
628        self.shared
629            .control
630            .submit(ControlOp::DiscardTx, &self.shared.bridge.notify)
631    }
632
633    pub fn reconfigure(
634        &self,
635        config: Option<Config>,
636        drain: bool,
637        publish: impl FnOnce(),
638    ) -> RuntimeResult {
639        let _output = self.shared.tty_output_lock.lock();
640        let barrier = SerialOutputBarrier::new(self.shared.clone());
641        if drain {
642            barrier.wait_idle()?;
643        }
644        if let Some(config) = config {
645            barrier.set_config(config)?;
646        }
647        publish();
648        Ok(())
649    }
650
651    pub fn poll_source(&self) -> Arc<PollSet> {
652        self.shared.tx_source.clone()
653    }
654}
655
656impl SerialRxSubscription {
657    pub fn drain(&self, out: &mut [RxItem]) -> usize {
658        let count = {
659            let mut subscription = self.consumer.lock_irqsave();
660            // `None` is only observable from `Drop`, which requires exclusive
661            // access. Keep the runtime boundary non-panicking if that invariant
662            // is changed by a future ownership refactor.
663            let Some(consumer) = subscription.as_mut() else {
664                return 0;
665            };
666            consumer.drain(out)
667        };
668        notify_drained_space(count, || self.shared.bridge.notify.notify());
669        count
670    }
671
672    /// Blocks until RX data is available or the runtime stops.
673    pub fn wait_readable(&self) -> RuntimeResult {
674        self.shared.ensure_started()?;
675        self.shared.rx_progress.wait_until(|| {
676            self.consumer
677                .lock_irqsave()
678                .as_ref()
679                .is_some_and(|consumer| !consumer.is_empty())
680                || !self.shared.started()
681        });
682        self.consumer
683            .lock_irqsave()
684            .as_ref()
685            .is_some_and(|consumer| !consumer.is_empty())
686            .then_some(())
687            .ok_or(RuntimeError::SerialNotStarted)
688    }
689
690    pub fn discard_pending(&self) -> RuntimeResult {
691        self.shared.ensure_started()?;
692        self.clear_pending();
693        let result = self
694            .shared
695            .control
696            .submit(ControlOp::DiscardRx, &self.shared.bridge.notify);
697        self.clear_pending();
698        result
699    }
700
701    pub fn poll_source(&self) -> Arc<PollSet> {
702        self.shared.rx_source.clone()
703    }
704
705    pub(crate) fn wait_console_event(&self, logs: &SerialLogSubscription) -> RuntimeResult {
706        if !Arc::ptr_eq(&self.shared, &logs.shared) {
707            return Err(RuntimeError::OperationNotSupported);
708        }
709        self.shared.ensure_started()?;
710        self.shared
711            .console_progress
712            .wait_until(|| self.has_pending() || logs.has_pending() || !self.shared.started());
713        (self.has_pending() || logs.has_pending())
714            .then_some(())
715            .ok_or(RuntimeError::SerialNotStarted)
716    }
717
718    fn has_pending(&self) -> bool {
719        self.consumer
720            .lock_irqsave()
721            .as_ref()
722            .is_some_and(|consumer| !consumer.is_empty())
723    }
724
725    fn clear_pending(&self) {
726        if let Some(consumer) = self.consumer.lock_irqsave().as_mut() {
727            consumer.clear();
728        }
729        self.shared.bridge.notify.notify();
730    }
731}
732
733impl Drop for SerialRxSubscription {
734    fn drop(&mut self) {
735        let Some(consumer) = self.consumer.get_mut().take() else {
736            return;
737        };
738        let mut available = self.shared.rx_subscription.lock_irqsave();
739        debug_assert!(
740            available.is_none(),
741            "serial runtime cannot have two RX consumers"
742        );
743        if available.is_none() {
744            *available = Some(consumer);
745        }
746    }
747}
748
749fn notify_drained_space(count: usize, notify_space: impl FnOnce()) {
750    if count != 0 {
751        notify_space();
752    }
753}
754
755pub fn runtimes() -> &'static [SerialRuntimeHandle] {
756    SERIAL_RUNTIMES.get().map_or(&[], Box::as_ref)
757}
758
759pub(crate) fn active_console() -> Option<&'static SerialRuntimeHandle> {
760    runtimes().get(ACTIVE_CONSOLE.load(Ordering::Acquire))
761}
762
763pub(crate) fn init(primary_cpu: usize) {
764    let log_mailbox = LOG_MAILBOX
765        .call_once(|| Arc::new(LogMailbox::new(ax_hal::cpu_num().max(1))))
766        .clone();
767    // `rust_main` initializes the primary scheduler and IPI/IRQ framework
768    // before serial discovery, so task-context doorbells are safe on this CPU.
769    log_mailbox.mark_wake_ready(primary_cpu);
770    let mut handles = Vec::new();
771    for serial in ax_driver::serial::take_serial_devices() {
772        match build_runtime(handles.len(), primary_cpu, serial, log_mailbox.clone()) {
773            Ok(handle) => handles.push(handle),
774            Err(err) => warn!("failed to initialize serial runtime: {err:?}"),
775        }
776    }
777    SERIAL_RUNTIMES.call_once(|| handles.into_boxed_slice());
778}
779
780#[cfg(feature = "smp")]
781pub(crate) fn mark_log_wake_ready(cpu_id: usize) {
782    if let Some(log_mailbox) = LOG_MAILBOX.get() {
783        log_mailbox.mark_wake_ready(cpu_id);
784    }
785}
786
787fn build_runtime(
788    index: usize,
789    primary_cpu: usize,
790    serial: SerialDevice,
791    log_mailbox: Arc<LogMailbox>,
792) -> RuntimeResult<SerialRuntimeHandle> {
793    let SerialDevice {
794        info,
795        port,
796        mut irq,
797        register_gate,
798    } = serial;
799    let polling = info.irq.is_none();
800    let bridge = Arc::new(RuntimeIrqBridge::new());
801    let stats = Arc::new(SerialStatsAtomic::new());
802    let register_gate: Arc<rdif_serial::UartRegisterGate<dyn rdif_serial::UartEmergencyTx>> =
803        Arc::from(register_gate);
804    let (irq_rx_producer, irq_rx_consumer) = spsc::channel(IRQ_RX_CAPACITY);
805    let (rx_output_producer, rx_output_consumer) = spsc::channel(SUBSCRIPTION_RX_CAPACITY);
806    let (log_subscription_producer, log_subscription_consumer) =
807        spsc::channel(LOG_SUBSCRIPTION_CAPACITY);
808    let shared = Arc::new(RuntimeShared {
809        index,
810        info,
811        owner_cpu: primary_cpu,
812        polling,
813        port: SpinLock::new(port),
814        register_gate: register_gate.clone(),
815        ingress: TxIngress::new(),
816        log_mailbox,
817        rx_subscription: SpinLock::new(Some(rx_output_consumer)),
818        log_subscription: SpinLock::new(Some(log_subscription_consumer)),
819        log_subscription_gate: SpinLock::new(()),
820        log_subscription_active: AtomicBool::new(false),
821        log_subscription_dropped_records: AtomicUsize::new(0),
822        log_subscription_dropped_bytes: AtomicUsize::new(0),
823        control: ControlQueue::new(),
824        bridge: bridge.clone(),
825        stats: stats.clone(),
826        rx_source: Arc::new(PollSet::new()),
827        tx_source: Arc::new(PollSet::new()),
828        rx_progress: WaitQueue::new(),
829        console_progress: WaitQueue::new(),
830        tx_progress: WaitQueue::new(),
831        tty_output_lock: Mutex::new(()),
832        log_barriers: AtomicUsize::new(0),
833        lifecycle: RuntimeLifecycle::new(),
834        irq_handle: OnceLock::new(),
835    });
836
837    let worker = SerialWorker::new(
838        shared.clone(),
839        irq_rx_consumer,
840        rx_output_producer,
841        log_subscription_producer,
842    );
843    let task = TaskInner::new(
844        move || worker.run(),
845        alloc::format!("serial{index}-maint"),
846        ax_task::default_task_stack_size(),
847    );
848    task.set_cpumask(AxCpuMask::one_shot(primary_cpu));
849
850    if let Some(binding) = shared.info.irq.clone() {
851        let irq_id = crate::irq::resolve_binding_irq(binding).map_err(|error| {
852            warn!(
853                "failed to resolve serial IRQ for {}: {error:?}",
854                shared.info.name
855            );
856            RuntimeError::from(error)
857        })?;
858        let callback_bridge = bridge;
859        let callback_stats = stats;
860        let mut callback_rx = RuntimeIrqPublisher {
861            producer: irq_rx_producer,
862            bridge: callback_bridge.clone(),
863            stats: callback_stats.clone(),
864        };
865        let callback_gate = register_gate;
866        let request = serial_irq_request(
867            ax_hal::irq::IrqRequest::new(move |_| {
868                let Some(_register_access) = callback_gate.try_enter() else {
869                    return ax_hal::irq::IrqReturn::Unhandled;
870                };
871                let Some(report) = irq.handle() else {
872                    callback_stats.spurious_irq();
873                    return ax_hal::irq::IrqReturn::Unhandled;
874                };
875                let event = callback_rx.publish(report);
876                mask_deferred_irq_rx(&mut *irq, event);
877                callback_stats.handled_irq(event);
878                callback_bridge.latch.publish(event);
879                callback_bridge.notify.notify_irq();
880                ax_hal::irq::IrqReturn::Handled
881            }),
882            primary_cpu,
883        );
884        let handle = ax_hal::irq::request_irq(irq_id, request).map_err(|error| {
885            warn!(
886                "failed to register serial IRQ for {}: {error:?}",
887                shared.info.name
888            );
889            RuntimeError::from(error)
890        })?;
891        shared.irq_handle.call_once(|| handle);
892    }
893
894    ax_task::spawn_task(task);
895    info!(
896        "serial runtime {} ready: cpu={}, irq={:?}, polling={}",
897        shared.info.name, shared.owner_cpu, shared.info.irq, shared.polling
898    );
899    Ok(SerialRuntimeHandle { shared })
900}
901
902fn serial_irq_request(
903    request: ax_hal::irq::IrqRequest,
904    primary_cpu: usize,
905) -> ax_hal::irq::IrqRequest {
906    request
907        .share_mode(ax_hal::irq::ShareMode::Shared)
908        .affinity(ax_hal::irq::IrqAffinity::Fixed(ax_hal::irq::CpuId(
909            primary_cpu,
910        )))
911        .auto_enable(ax_hal::irq::AutoEnable::No)
912}
913
914struct RuntimeIrqPublisher {
915    producer: SpscProducer<rdif_serial::RxSample>,
916    bridge: Arc<RuntimeIrqBridge>,
917    stats: Arc<SerialStatsAtomic>,
918}
919
920impl RuntimeIrqPublisher {
921    fn publish(&mut self, mut report: rdif_serial::SerialIrqReport) -> rdif_serial::SerialIrqEvent {
922        // Preserve the driver's bounded-IRQ decision. A fully drained UART
923        // must remain armed while the owner transports its samples; masking a
924        // small FIFO until task context runs can overflow at line rate.
925        for &sample in report.rx.as_slice() {
926            if self.producer.push(sample).is_err() {
927                self.stats.add_rx_dropped(1);
928                self.bridge.rx_overflow.store(true, Ordering::Release);
929                report.event.rx_errors |= rdif_serial::RxErrorFlags::OVERRUN;
930                report.event.rearm |= rdif_serial::SerialEventSet::RX;
931            }
932        }
933        report.event
934    }
935}
936
937fn mask_deferred_irq_rx(irq: &mut dyn rdif_serial::UartIrq, event: rdif_serial::SerialIrqEvent) {
938    if event.rearm.intersects(rdif_serial::SerialEventSet::RX) {
939        irq.mask(rdif_serial::SerialEventSet::RX);
940    }
941}
942
943/// Publishes one complete ordinary record without waiting for UART progress.
944pub(crate) fn try_publish_record(
945    meta: ax_log::RecordMeta,
946    args: fmt::Arguments<'_>,
947) -> Option<ax_log::PublishStatus> {
948    let index = ACTIVE_CONSOLE.load(Ordering::Acquire);
949    let runtime = runtimes().get(index)?;
950    let guard = ax_task::sync::PreemptIrqSaveGuard::new();
951    // SAFETY: `guard` prevents task migration and local IRQ re-entry for the
952    // whole callback; runtime CPU-local state is installed before handoff.
953    let (outcome, log_wake_ready) = unsafe {
954        ax_hal::percpu::with_cpu_pin(|pin| {
955            let cpu_id = ax_hal::percpu::this_cpu_id_pinned(pin);
956            let current = ax_task::current_may_uninit();
957            let task_id = current.as_ref().map(|task| task.id().as_u64());
958            let timestamp_nanos = ax_hal::time::monotonic_time().as_nanos() as u64;
959            let record_meta = match meta.kind() {
960                ax_log::RecordKind::Print => LogRecordMeta::print(timestamp_nanos, task_id),
961                ax_log::RecordKind::Log => LogRecordMeta::log(timestamp_nanos, task_id),
962            };
963            (
964                runtime
965                    .shared
966                    .log_mailbox
967                    .try_publish(cpu_id, record_meta, args),
968                runtime.shared.log_mailbox.wake_ready(cpu_id),
969            )
970        })
971    }
972    .unwrap_or_else(|_| (log_mailbox::PublishOutcome::dropped(0), false));
973    drop(guard);
974    runtime
975        .shared
976        .stats
977        .add_log_dropped(outcome.dropped_source_bytes());
978    runtime
979        .shared
980        .stats
981        .add_log_dropped_records(outcome.dropped_records());
982    match record_wake_context(
983        outcome.published(),
984        ax_hal::irq::in_irq_context(),
985        log_wake_ready,
986    ) {
987        RecordWakeContext::Interrupt => {
988            runtime.shared.bridge.notify.notify_irq();
989        }
990        RecordWakeContext::Task => {
991            runtime.shared.bridge.notify.notify();
992        }
993        RecordWakeContext::None => {}
994    }
995    Some(if !outcome.published() {
996        ax_log::PublishStatus::Dropped
997    } else if outcome.truncated() {
998        ax_log::PublishStatus::Truncated
999    } else {
1000        ax_log::PublishStatus::Published
1001    })
1002}
1003
1004#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1005enum RecordWakeContext {
1006    None,
1007    Interrupt,
1008    Task,
1009}
1010
1011const fn record_wake_context(
1012    published: bool,
1013    in_irq_context: bool,
1014    log_wake_ready: bool,
1015) -> RecordWakeContext {
1016    if !published || !log_wake_ready {
1017        RecordWakeContext::None
1018    } else if in_irq_context {
1019        RecordWakeContext::Interrupt
1020    } else {
1021        RecordWakeContext::Task
1022    }
1023}
1024
1025/// Synchronously streams one emergency record without the log mailbox.
1026pub(crate) fn emergency_write(args: fmt::Arguments<'_>) -> Option<usize> {
1027    let index = ACTIVE_CONSOLE.load(Ordering::Acquire);
1028    let runtime = runtimes().get(index)?;
1029    let Some(_formatting) = EmergencyFormatting::try_enter() else {
1030        runtime.shared.stats.add_log_dropped_records(1);
1031        return Some(0);
1032    };
1033    let Some(register_access) = claim_emergency_registers(&runtime.shared.register_gate) else {
1034        runtime.shared.stats.add_log_dropped_records(1);
1035        return Some(0);
1036    };
1037    let mut writer = EmergencyWriter::new(register_access);
1038    if writer.write_fmt(args).is_err() {
1039        runtime.shared.stats.add_log_dropped_records(1);
1040    }
1041    Some(writer.source_written)
1042}
1043
1044const EMERGENCY_CLAIM_ATTEMPTS: usize = 4096;
1045static EMERGENCY_FORMATTING: AtomicBool = AtomicBool::new(false);
1046
1047fn claim_emergency_registers(
1048    gate: &rdif_serial::UartRegisterGate<dyn rdif_serial::UartEmergencyTx>,
1049) -> Option<rdif_serial::UartEmergencyAccess<'_, dyn rdif_serial::UartEmergencyTx>> {
1050    for _ in 0..EMERGENCY_CLAIM_ATTEMPTS {
1051        if let Some(access) = gate.try_begin_emergency() {
1052            return Some(access);
1053        }
1054        core::hint::spin_loop();
1055    }
1056    None
1057}
1058
1059struct EmergencyFormatting;
1060
1061impl EmergencyFormatting {
1062    fn try_enter() -> Option<Self> {
1063        EMERGENCY_FORMATTING
1064            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
1065            .ok()
1066            .map(|_| Self)
1067    }
1068}
1069
1070impl Drop for EmergencyFormatting {
1071    fn drop(&mut self) {
1072        EMERGENCY_FORMATTING.store(false, Ordering::Release);
1073    }
1074}
1075
1076struct EmergencyWriter<'a, E: rdif_serial::UartEmergencyTx + ?Sized> {
1077    access: rdif_serial::UartEmergencyAccess<'a, E>,
1078    source_written: usize,
1079}
1080
1081impl<'a, E: rdif_serial::UartEmergencyTx + ?Sized> EmergencyWriter<'a, E> {
1082    const fn new(access: rdif_serial::UartEmergencyAccess<'a, E>) -> Self {
1083        Self {
1084            access,
1085            source_written: 0,
1086        }
1087    }
1088
1089    fn write_all_blocking(&self, mut bytes: &[u8]) {
1090        while !bytes.is_empty() {
1091            let written = self.access.try_write(bytes).min(bytes.len());
1092            if written == 0 {
1093                core::hint::spin_loop();
1094            } else {
1095                bytes = &bytes[written..];
1096            }
1097        }
1098    }
1099}
1100
1101impl<E: rdif_serial::UartEmergencyTx + ?Sized> Write for EmergencyWriter<'_, E> {
1102    fn write_str(&mut self, text: &str) -> fmt::Result {
1103        let mut remaining = text.as_bytes();
1104        while let Some(newline) = remaining.iter().position(|&byte| byte == b'\n') {
1105            self.write_all_blocking(&remaining[..newline]);
1106            self.write_all_blocking(b"\r\n");
1107            remaining = &remaining[newline + 1..];
1108        }
1109        self.write_all_blocking(remaining);
1110        self.source_written = self.source_written.saturating_add(text.len());
1111        Ok(())
1112    }
1113}
1114
1115fn deactivate_console(shared: &RuntimeShared) {
1116    if ACTIVE_CONSOLE
1117        .compare_exchange(
1118            shared.index,
1119            NO_ACTIVE_CONSOLE,
1120            Ordering::AcqRel,
1121            Ordering::Acquire,
1122        )
1123        .is_ok()
1124    {
1125        shared.log_mailbox.release(shared.index);
1126        shared.bridge.notify.notify();
1127    }
1128}
1129
1130struct ActiveConsoleWriter {
1131    sender: SerialTxSender,
1132}
1133
1134impl Write for ActiveConsoleWriter {
1135    fn write_str(&mut self, text: &str) -> fmt::Result {
1136        self.sender
1137            .write_text_all(text.as_bytes())
1138            .map(|_| ())
1139            .map_err(|_| fmt::Error)
1140    }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145    use super::*;
1146
1147    struct ChunkedEmergencyTx(&'static AtomicUsize);
1148
1149    impl rdif_serial::UartEmergencyTx for ChunkedEmergencyTx {
1150        unsafe fn mask_interrupts_unlocked(&self) {}
1151
1152        unsafe fn try_write_unlocked(&self, bytes: &[u8]) -> usize {
1153            let written = bytes.len().min(7);
1154            self.0.fetch_add(written, Ordering::Relaxed);
1155            written
1156        }
1157    }
1158
1159    struct RecordingIrq {
1160        masked: rdif_serial::SerialEventSet,
1161    }
1162
1163    #[test]
1164    fn failed_closed_runtime_cannot_return_to_dormant_or_started() {
1165        let lifecycle = RuntimeLifecycle::new();
1166
1167        assert_eq!(
1168            lifecycle.ensure_started(),
1169            Err(RuntimeError::SerialNotStarted)
1170        );
1171        lifecycle.set_started(true);
1172        assert!(lifecycle.ensure_started().is_ok());
1173
1174        lifecycle.fail_closed();
1175        assert_eq!(
1176            lifecycle.ensure_available(),
1177            Err(RuntimeError::ConsoleFailedClosed)
1178        );
1179        assert_eq!(
1180            lifecycle.ensure_started(),
1181            Err(RuntimeError::ConsoleFailedClosed)
1182        );
1183
1184        lifecycle.set_started(false);
1185        lifecycle.set_started(true);
1186        assert_eq!(
1187            lifecycle.ensure_started(),
1188            Err(RuntimeError::ConsoleFailedClosed)
1189        );
1190    }
1191
1192    impl rdif_serial::UartIrq for RecordingIrq {
1193        fn mask(&mut self, sources: rdif_serial::SerialEventSet) {
1194            self.masked |= sources;
1195        }
1196
1197        fn handle(&mut self) -> Option<rdif_serial::SerialIrqReport> {
1198            None
1199        }
1200    }
1201
1202    #[test]
1203    fn emergency_writer_streams_a_record_larger_than_the_former_buffer() {
1204        static HARDWARE_BYTES: AtomicUsize = AtomicUsize::new(0);
1205
1206        HARDWARE_BYTES.store(0, Ordering::Relaxed);
1207        let gate = rdif_serial::UartRegisterGate::new(ChunkedEmergencyTx(&HARDWARE_BYTES));
1208        let access = gate.try_begin_emergency().expect("emergency takeover");
1209        let mut writer = EmergencyWriter::new(access);
1210        let payload = "x".repeat(2_048);
1211
1212        writer.write_str(&payload).unwrap();
1213        writer.write_str("\nBACKTRACE_END").unwrap();
1214
1215        assert_eq!(writer.source_written, payload.len() + 14);
1216        assert_eq!(HARDWARE_BYTES.load(Ordering::Relaxed), payload.len() + 15);
1217        assert!(gate.try_enter().is_none());
1218    }
1219
1220    #[test]
1221    fn irq_report_drops_only_after_the_preallocated_ring_is_full() {
1222        let bridge = Arc::new(RuntimeIrqBridge::new());
1223        let stats = Arc::new(SerialStatsAtomic::new());
1224        let (producer, mut consumer) = spsc::channel(2);
1225        let mut publisher = RuntimeIrqPublisher {
1226            producer,
1227            bridge: bridge.clone(),
1228            stats: stats.clone(),
1229        };
1230        let samples = [
1231            rdif_serial::RxSample {
1232                byte: Some(1),
1233                ..rdif_serial::RxSample::default()
1234            },
1235            rdif_serial::RxSample {
1236                byte: Some(2),
1237                ..rdif_serial::RxSample::default()
1238            },
1239            rdif_serial::RxSample {
1240                byte: Some(3),
1241                ..rdif_serial::RxSample::default()
1242            },
1243        ];
1244        let mut batch = rdif_serial::IrqRxBatch::new();
1245        for sample in samples {
1246            batch.try_push(sample).unwrap();
1247        }
1248        let event = publisher.publish(rdif_serial::SerialIrqReport::new(
1249            rdif_serial::SerialIrqEvent::default(),
1250            batch,
1251        ));
1252
1253        assert_eq!(consumer.pop().and_then(|sample| sample.byte), Some(1));
1254        assert_eq!(consumer.pop().and_then(|sample| sample.byte), Some(2));
1255        assert!(consumer.pop().is_none());
1256        assert_eq!(stats.snapshot().rx_dropped, 1);
1257        assert!(bridge.rx_overflow.load(Ordering::Acquire));
1258        assert!(event.rx_errors.contains(rdif_serial::RxErrorFlags::OVERRUN));
1259        assert!(event.rearm.contains(rdif_serial::SerialEventSet::RX));
1260    }
1261
1262    #[test]
1263    fn fully_drained_rx_irq_keeps_hardware_source_armed() {
1264        let bridge = Arc::new(RuntimeIrqBridge::new());
1265        let stats = Arc::new(SerialStatsAtomic::new());
1266        let (producer, mut consumer) = spsc::channel(2);
1267        let mut publisher = RuntimeIrqPublisher {
1268            producer,
1269            bridge,
1270            stats,
1271        };
1272        let mut batch = rdif_serial::IrqRxBatch::new();
1273        batch
1274            .try_push(rdif_serial::RxSample {
1275                byte: Some(b'x'),
1276                ..rdif_serial::RxSample::default()
1277            })
1278            .unwrap();
1279
1280        let event = publisher.publish(rdif_serial::SerialIrqReport::new(
1281            rdif_serial::SerialIrqEvent {
1282                events: rdif_serial::SerialEventSet::RX_DATA,
1283                ..rdif_serial::SerialIrqEvent::default()
1284            },
1285            batch,
1286        ));
1287
1288        assert_eq!(consumer.pop().and_then(|sample| sample.byte), Some(b'x'));
1289        assert!(
1290            !event.rearm.contains(rdif_serial::SerialEventSet::RX),
1291            "a drained IRQ must not leave a small UART FIFO masked until the owner task runs"
1292        );
1293    }
1294
1295    #[test]
1296    fn deferred_rx_masks_only_the_uart_source() {
1297        let mut irq = RecordingIrq {
1298            masked: rdif_serial::SerialEventSet::empty(),
1299        };
1300        mask_deferred_irq_rx(
1301            &mut irq,
1302            rdif_serial::SerialIrqEvent {
1303                rearm: rdif_serial::SerialEventSet::RX | rdif_serial::SerialEventSet::TX_SPACE,
1304                ..rdif_serial::SerialIrqEvent::default()
1305            },
1306        );
1307
1308        assert_eq!(irq.masked, rdif_serial::SerialEventSet::RX);
1309    }
1310
1311    #[test]
1312    fn subscription_drain_notifies_a_worker_waiting_for_output_space() {
1313        let (mut producer, consumer) = spsc::channel(1);
1314        producer.push(RxItem::Overrun).unwrap();
1315        let mut consumer = consumer;
1316        let mut item = [RxItem::default()];
1317        let mut notify_count = 0;
1318
1319        let count = consumer.drain(&mut item);
1320        notify_drained_space(count, || notify_count += 1);
1321        assert_eq!(count, 1);
1322        assert_eq!(item, [RxItem::Overrun]);
1323        assert_eq!(notify_count, 1);
1324    }
1325
1326    #[test]
1327    fn serial_irq_stays_disabled_until_the_worker_starts_the_port() {
1328        let request = serial_irq_request(
1329            ax_hal::irq::IrqRequest::new(|_| ax_hal::irq::IrqReturn::Handled),
1330            0,
1331        );
1332
1333        assert_eq!(
1334            request.auto_enable_mode(),
1335            ax_hal::irq::AutoEnable::No,
1336            "the IRQ action must not run before the worker has configured the UART"
1337        );
1338    }
1339
1340    #[test]
1341    fn absent_runtime_console_preserves_early_publication_fallback() {
1342        ACTIVE_CONSOLE.store(NO_ACTIVE_CONSOLE, Ordering::Release);
1343        assert_eq!(
1344            try_publish_record(ax_log::RecordMeta::print(), format_args!("fallback")),
1345            None
1346        );
1347    }
1348
1349    #[test]
1350    fn early_secondary_log_does_not_wake_before_log_wake_ready() {
1351        assert_eq!(
1352            record_wake_context(true, false, false),
1353            RecordWakeContext::None
1354        );
1355        assert_eq!(
1356            record_wake_context(true, false, true),
1357            RecordWakeContext::Task
1358        );
1359        assert_eq!(
1360            record_wake_context(true, true, false),
1361            RecordWakeContext::None
1362        );
1363        assert_eq!(
1364            record_wake_context(true, true, true),
1365            RecordWakeContext::Interrupt
1366        );
1367    }
1368
1369    #[test]
1370    fn wake_ready_transition_preserves_early_secondary_records() {
1371        const OWNER: usize = 7;
1372        let mailbox = Arc::new(LogMailbox::new(2));
1373        assert!(mailbox.claim(OWNER));
1374
1375        let early = mailbox.try_publish(
1376            1,
1377            LogRecordMeta::log(1, None),
1378            format_args!("secondary started\n"),
1379        );
1380        assert!(early.published());
1381        assert_eq!(
1382            record_wake_context(early.published(), false, mailbox.wake_ready(1)),
1383            RecordWakeContext::None
1384        );
1385
1386        mailbox.mark_wake_ready(1);
1387        let ready = mailbox.try_publish(
1388            1,
1389            LogRecordMeta::log(2, Some(8)),
1390            format_args!("secondary init OK\n"),
1391        );
1392        assert!(ready.published());
1393        assert_eq!(
1394            record_wake_context(ready.published(), false, mailbox.wake_ready(1)),
1395            RecordWakeContext::Task
1396        );
1397
1398        let mut reader = mailbox.reader();
1399        assert!(
1400            reader
1401                .take(OWNER)
1402                .is_some_and(|record| record.record.bytes().ends_with(b"secondary started\r\n"))
1403        );
1404        assert!(
1405            reader
1406                .take(OWNER)
1407                .is_some_and(|record| record.record.bytes().ends_with(b"secondary init OK\r\n"))
1408        );
1409        assert!(reader.take(OWNER).is_none());
1410    }
1411}