Skip to main content

fdomain_uart_driver_lib/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Target-side FDomain UART driver daemon for Fuchsia devices.
6//!
7//! This component acts as the target-side transport bridge between a physical or
8//! virtual serial device (`/dev/class/serial`) and the Remote Control Service
9//! (`fuchsia.developer.remotecontrol.connector.Connector`).
10//!
11//! # Architecture
12//!
13//! 1. **Boot Argument Check**: Checks `fuchsia.boot.Arguments` for `dev.fdomain.uart`
14//!    (defaults to disabled; opt-in via `dev.fdomain.uart=true`) and `dev.fdomain.uart.baud`
15//!    (defaults to `1000000`).
16//! 2. **Device Discovery**: Scans `/dev/class/serial`, preferring `Class::Generic`
17//!    ports over `Class::Console` ports, and configures 8N1 at the target baud rate.
18//! 3. **Handshake**: Runs `TargetHandshake` supporting `ProtocolId::ResendSP`
19//!    (Go-Back-N sliding-window framing) and preserves any trailing bytes read
20//!    past the handshake frame.
21//! 4. **Bridge Tasks**: Spawns four concurrent tasks (`writer_task`, `receiver_task`,
22//!    `sender_task`, and `coordinator_task`) that multiplex FDomain channels over
23//!    UART frames and connect each channel to RCS via `fdomain_toolbox_socket`.
24
25pub mod coordinator;
26pub mod error;
27pub mod receiver;
28pub mod sender;
29pub mod serial;
30
31use crate::coordinator::{
32    DEFAULT_CHANNEL_CAPACITY, DEFAULT_CLIENT_DATA_CAPACITY, coordinator_task,
33};
34use crate::error::TargetDriverError;
35use crate::receiver::receiver_task;
36use crate::sender::sender_task;
37use crate::serial::{
38    SerialRead, SerialReader, SerialWrite, open_serial_device,
39    run_target_handshake_with_initial_data, writer_task,
40};
41pub use error::Result;
42use fidl_fuchsia_developer_remotecontrol_connector::{ConnectorMarker, ConnectorProxy};
43use fidl_fuchsia_hardware_serial::DeviceProxy;
44use fuchsia_component::client::connect_to_protocol;
45use futures::channel::mpsc;
46use log::{error, info, warn};
47use std::time::Duration;
48use uart_fpl::{AckTracker, CONTROL_CHANNEL_ID, FrameType, ProtocolId, encode_frame};
49
50/// Boot argument key controlling whether the target FDomain UART driver is enabled.
51const BOOT_ARG_ENABLED: &str = "dev.fdomain.uart";
52
53/// Boot argument key specifying an optional UART baud rate override.
54const BOOT_ARG_BAUD_RATE: &str = "dev.fdomain.uart.baud";
55
56/// Default UART baud rate (1,000,000 bps) used when [`BOOT_ARG_BAUD_RATE`] is unset or invalid.
57const DEFAULT_BAUD_RATE: u32 = 1_000_000;
58
59/// Delay before attempting to reopen the serial device after an I/O error or disconnect.
60const RETRY_DELAY: Duration = Duration::from_secs(1);
61
62/// Queries `fuchsia.boot.Arguments` to check whether the driver is enabled via
63/// [`BOOT_ARG_ENABLED`], defaulting to `false` when unset or if the protocol is unavailable.
64async fn is_enabled() -> bool {
65    if let Ok(args) = connect_to_protocol::<fidl_fuchsia_boot::ArgumentsMarker>() {
66        match args.get_bool(BOOT_ARG_ENABLED, false).await {
67            Ok(val) => val,
68            Err(e) => {
69                warn!("Failed to get {} boot arg: {:?}", BOOT_ARG_ENABLED, e);
70                false
71            }
72        }
73    } else {
74        warn!("Failed to connect to fuchsia.boot.Arguments");
75        false
76    }
77}
78
79/// Reads the configured UART baud rate from `fuchsia.boot.Arguments` ([`BOOT_ARG_BAUD_RATE`]),
80/// falling back to [`DEFAULT_BAUD_RATE`] when unset, zero, or unparseable.
81async fn read_baud_rate() -> u32 {
82    let Ok(args) = connect_to_protocol::<fidl_fuchsia_boot::ArgumentsMarker>() else {
83        warn!("Failed to connect to fuchsia.boot.Arguments to get baud rate");
84        return DEFAULT_BAUD_RATE;
85    };
86    match args.get_string(BOOT_ARG_BAUD_RATE).await {
87        Ok(Some(value)) => match value.parse::<u32>() {
88            Ok(baud) if baud > 0 => baud,
89            _ => {
90                warn!("Failed to parse {} '{}', using default", BOOT_ARG_BAUD_RATE, value);
91                DEFAULT_BAUD_RATE
92            }
93        },
94        Ok(None) => DEFAULT_BAUD_RATE,
95        Err(e) => {
96            warn!("Failed to get {} boot arg: {:?}", BOOT_ARG_BAUD_RATE, e);
97            DEFAULT_BAUD_RATE
98        }
99    }
100}
101
102/// Performs the Channel 0 protocol negotiation handshake on `device`, seeded with any
103/// `initial_data` carried over from a previous session.
104///
105/// On the first invocation after daemon startup (`*is_initial_start == true`), emits a
106/// cold-start `Reset(0)` frame before listening for the host's `NegotiateReq`.
107async fn negotiate_session<S: SerialRead + SerialWrite + Clone>(
108    device: &S,
109    is_initial_start: &mut bool,
110    initial_data: &[u8],
111) -> Result<(ProtocolId, u32, Vec<u8>)> {
112    if *is_initial_start {
113        // On cold start, emit an initial Reset frame with sentinel session_id = 0 to
114        // instruct any listening host to flush stale in-flight state. Once written
115        // successfully, `is_initial_start` is cleared for the lifetime of the daemon
116        // to avoid spurious unsolicited resets on subsequent device reopens.
117        let reset_frame = encode_frame(0, CONTROL_CHANNEL_ID, 0, FrameType::Reset, &[])?;
118        let mut device_writer = device.clone();
119        if let Err(e) = device_writer.serial_write(&reset_frame).await {
120            warn!("Failed to send reset frame: {:?}", e);
121        } else {
122            *is_initial_start = false;
123        }
124    }
125    let mut device_reader = device.clone();
126    run_target_handshake_with_initial_data(&mut device_reader, initial_data).await
127}
128
129/// Spawns and drives the four concurrent UART bridge tasks ([`writer_task`],
130/// [`receiver_task`], [`sender_task`], and [`coordinator_task`]) for an active `session_id`.
131///
132/// Returns the joint task completion result along with any unconsumed trailing bytes
133/// buffered in [`SerialReader`] when the session terminates (for example, bytes from a
134/// coalesced `NegotiateReq` immediately following a `Reset` frame).
135async fn run_bridge_session<S: SerialRead + SerialWrite + Clone>(
136    device: S,
137    session_id: u32,
138    unconsumed: &[u8],
139    rcs_connector: ConnectorProxy,
140) -> (Result<((), (), (), ())>, Vec<u8>) {
141    let ack_tracker = AckTracker::new();
142    let (data_tx, data_rx) = mpsc::channel(DEFAULT_CHANNEL_CAPACITY);
143    let (ack_tx, ack_rx) = mpsc::channel(DEFAULT_CLIENT_DATA_CAPACITY);
144    let (sender_tx, sender_rx) = mpsc::channel(DEFAULT_CHANNEL_CAPACITY);
145    let (serial_tx, serial_rx) = mpsc::channel(DEFAULT_CLIENT_DATA_CAPACITY);
146    let mut reader = SerialReader::with_initial_data(device.clone(), unconsumed);
147    let res = {
148        let writer = writer_task(device, ack_tracker.clone(), data_rx);
149        let receiver =
150            receiver_task(&mut reader, serial_tx, ack_tracker, ack_tx, session_id, data_tx.clone());
151        let sender = sender_task(sender_rx, data_tx, ack_rx, session_id);
152        let coordinator = coordinator_task(serial_rx, sender_tx, rcs_connector);
153        info!("Bridge tasks started");
154        futures::try_join!(writer, receiver, sender, coordinator)
155    };
156    (res, reader.take_unconsumed())
157}
158
159/// Runs consecutive protocol handshakes and FDomain bridge sessions over an open `device`.
160///
161/// In-band session transitions ([`TargetDriverError::SessionIdChanged`] or
162/// [`TargetDriverError::ResetRequested`]) renegotiate immediately on the open `device`
163/// while preserving trailing bytes in `carry_over`. Transport-level errors or EOF
164/// explicitly drop `device` before waiting [`RETRY_DELAY`] so the underlying serial
165/// driver can complete asynchronous teardown before the port is reopened.
166async fn run_device_sessions<S: SerialRead + SerialWrite + Clone>(
167    device: S,
168    rcs_connector: &ConnectorProxy,
169    is_initial_start: &mut bool,
170) {
171    let mut carry_over = Vec::new();
172    loop {
173        let (proto, session_id, unconsumed) =
174            match negotiate_session(&device, is_initial_start, &carry_over).await {
175                Ok(negotiated) => negotiated,
176                Err(e) => {
177                    error!("Handshake failed ({:?}); reopening serial device in 1s...", e);
178                    drop(device);
179                    fuchsia_async::Timer::new(RETRY_DELAY).await;
180                    return;
181                }
182            };
183        info!("Handshake succeeded. Negotiated protocol: {:?}", proto);
184        let (res, remaining) =
185            run_bridge_session(device.clone(), session_id, &unconsumed, rcs_connector.clone())
186                .await;
187        match res {
188            Err(TargetDriverError::SessionIdChanged) => {
189                info!("Session ID changed; renegotiating on existing serial device");
190                carry_over = remaining;
191            }
192            Err(TargetDriverError::ResetRequested) => {
193                info!("Session reset requested; renegotiating on existing serial device");
194                carry_over = remaining;
195            }
196            Err(e) => {
197                warn!(
198                    "Bridge session ended with error ({:?}); reopening serial device in 1s...",
199                    e
200                );
201                drop(device);
202                fuchsia_async::Timer::new(RETRY_DELAY).await;
203                return;
204            }
205            Ok(_) => {
206                drop(device);
207                fuchsia_async::Timer::new(RETRY_DELAY).await;
208                return;
209            }
210        }
211    }
212}
213
214/// Executes a single device-discovery and session lifecycle iteration.
215///
216/// Attempts to open and configure a target UART device at `baud_rate` and run
217/// [`run_device_sessions`], or backs off for [`RETRY_DELAY`] if no serial port is
218/// ready yet.
219async fn run_driver_iteration(
220    baud_rate: u32,
221    rcs_connector: &ConnectorProxy,
222    is_initial_start: &mut bool,
223) -> Result<()> {
224    let device: DeviceProxy = match open_serial_device(baud_rate).await {
225        Ok(device) => device,
226        // Silence NoSerialPorts and early devfs Io errors to prevent Archivist log
227        // buffer flooding on targets without populated UART controllers or during
228        // asynchronous driver discovery.
229        Err(TargetDriverError::NoSerialPorts | TargetDriverError::Io(_)) => {
230            fuchsia_async::Timer::new(RETRY_DELAY).await;
231            return Ok(());
232        }
233        Err(e) => {
234            warn!("Failed to open serial device: {:?}. Retrying in 1s...", e);
235            fuchsia_async::Timer::new(RETRY_DELAY).await;
236            return Ok(());
237        }
238    };
239    run_device_sessions(device, rcs_connector, is_initial_start).await;
240    Ok(())
241}
242
243/// Runs the target-side FDomain UART driver daemon.
244///
245/// # Errors
246///
247/// Returns [`TargetDriverError::RcsConnect`] if connecting to the
248/// `fuchsia.developer.remotecontrol.connector.Connector` protocol fails.
249pub async fn run_driver() -> Result<()> {
250    info!("Starting FDomain UART Driver");
251    if !is_enabled().await {
252        info!(
253            "FDomain UART Driver is disabled by boot argument '{}'. Sleeping forever.",
254            BOOT_ARG_ENABLED
255        );
256        futures::future::pending::<()>().await;
257    }
258    let baud_rate = read_baud_rate().await;
259    let rcs_connector = connect_to_protocol::<ConnectorMarker>()
260        .map_err(|e| TargetDriverError::RcsConnect(format!("{e:#}")))?;
261    let mut is_initial_start = true;
262    loop {
263        run_driver_iteration(baud_rate, &rcs_connector, &mut is_initial_start).await?;
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::serial::MockSerial;
271    use std::cell::RefCell;
272    use std::rc::Rc;
273    use uart_fpl::{FrameParser, HostHandshake};
274
275    #[derive(Clone)]
276    struct SharedMockSerial(Rc<RefCell<MockSerial>>);
277
278    impl SerialRead for SharedMockSerial {
279        async fn serial_read(&mut self) -> Result<Vec<u8>> {
280            let mut inner = self.0.borrow_mut();
281            if inner.read_data.is_empty() {
282                return Ok(Vec::new());
283            }
284            Ok(inner.read_data.remove(0))
285        }
286    }
287
288    impl SerialWrite for SharedMockSerial {
289        async fn serial_write(&mut self, data: &[u8]) -> Result<()> {
290            self.0.borrow_mut().write_data.push(data.to_vec());
291            Ok(())
292        }
293    }
294
295    #[fuchsia::test]
296    async fn test_negotiate_session_initial_start_sends_reset() {
297        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
298        let (frame_type, payload) = host.start().unwrap();
299        let req_1 = encode_frame(111, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
300        let req_2 = encode_frame(222, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
301        let mock = SharedMockSerial(Rc::new(RefCell::new(MockSerial {
302            read_data: vec![req_1],
303            write_data: Vec::new(),
304        })));
305        let mut is_initial_start = true;
306        let (proto_1, sid_1, _) =
307            negotiate_session(&mock, &mut is_initial_start, &[]).await.unwrap();
308        assert!(!is_initial_start);
309        assert_eq!(proto_1, ProtocolId::ResendSP);
310        assert_eq!(sid_1, 111);
311        let mut parser = FrameParser::new();
312        for chunk in &mock.0.borrow().write_data {
313            parser.feed(chunk);
314        }
315        let first_written = parser.next_frame().unwrap();
316        assert_eq!(first_written.frame_type, FrameType::Reset);
317        let second_written = parser.next_frame().unwrap();
318        assert_eq!(second_written.frame_type, FrameType::NegotiateResp);
319
320        mock.0.borrow_mut().write_data.clear();
321        let (proto_2, sid_2, _) =
322            negotiate_session(&mock, &mut is_initial_start, &req_2).await.unwrap();
323        assert_eq!(proto_2, ProtocolId::ResendSP);
324        assert_eq!(sid_2, 222);
325        let mut parser_2 = FrameParser::new();
326        for chunk in &mock.0.borrow().write_data {
327            parser_2.feed(chunk);
328        }
329        let only_written = parser_2.next_frame().unwrap();
330        assert_eq!(only_written.frame_type, FrameType::NegotiateResp);
331        assert!(parser_2.next_frame().is_none());
332    }
333
334    #[fuchsia::test]
335    async fn test_run_bridge_session_preserves_remaining_bytes_on_reset() {
336        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
337        let (frame_type, payload) = host.start().unwrap();
338        let session_1 = 111u32;
339        let session_2 = 222u32;
340        let reset_1 =
341            encode_frame(session_1, CONTROL_CHANNEL_ID, 0, FrameType::Reset, &[]).unwrap();
342        let req_2 = encode_frame(session_2, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
343        // Coalesce `Reset` for session 1 and `NegotiateReq` for session 2 into a single read chunk.
344        let coalesced = [reset_1, req_2.clone()].concat();
345        let mock = SharedMockSerial(Rc::new(RefCell::new(MockSerial {
346            read_data: vec![coalesced],
347            write_data: Vec::new(),
348        })));
349        let (connector_proxy, _connector_stream) =
350            fidl::endpoints::create_proxy_and_stream::<ConnectorMarker>();
351        let (res, remaining) =
352            run_bridge_session(mock.clone(), session_1, &[], connector_proxy).await;
353        assert!(matches!(res, Err(TargetDriverError::ResetRequested)));
354        assert_eq!(remaining, req_2);
355
356        // Passing `remaining` as `carry_over` into `negotiate_session` immediately negotiates session 2.
357        let mut is_initial_start = false;
358        let (proto, sid, _) =
359            negotiate_session(&mock, &mut is_initial_start, &remaining).await.unwrap();
360        assert_eq!(proto, ProtocolId::ResendSP);
361        assert_eq!(sid, session_2);
362    }
363}