Skip to main content

fdomain_uart_driver_lib/
serial.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//! Low-level serial I/O traits, device discovery, handshake, frame reader, and writer task.
6//!
7//! This module encapsulates all direct interaction with the serial byte stream
8//! below the Go-Back-N `ResendSP` state machines:
9//! - **RX (`SerialReader`)**: Rather than spawning a separate `reader_task`,
10//!   incoming bytes are parsed by [`SerialReader`], which is borrowed directly
11//!   by the receiver task (`reader.next_frame().await`). This allows the caller
12//!   to recover any unconsumed trailing bytes via [`SerialReader::take_unconsumed`]
13//!   when a session ends or renegotiates.
14//! - **TX (`writer_task`)**: Because multiple producers emit frames concurrently
15//!   (outgoing data/close frames, duplicate handshake replies, and priority
16//!   cumulative ACKs via [`AckTracker`]), outgoing serial writes are handled by
17//!   a dedicated [`writer_task`] that prioritizes ACKs and coalesces 4 KiB
18//!   frame batches.
19
20use crate::error::{Result, TargetDriverError};
21use fidl_fuchsia_hardware_serial::{
22    CharacterWidth, Class, Config, DeviceMarker, DeviceProxy, DeviceProxy_Marker, FlowControl,
23    Parity, StopWidth,
24};
25use futures::channel::mpsc;
26use futures::prelude::*;
27use futures::stream::FusedStream;
28use log::{error, info, warn};
29use std::time::Duration;
30use uart_fpl::{
31    AckTracker, CONTROL_CHANNEL_ID, FrameParser, FrameType, ProtocolId, TargetHandshake,
32    encode_frame,
33};
34
35const SERIAL_DEV_DIR: &str = "/dev/class/serial";
36const MAX_WRITE_ATTEMPTS: usize = 5;
37const WRITE_RETRY_DELAY: Duration = Duration::from_millis(50);
38const WRITE_BATCH_CAPACITY: usize = 4096;
39
40/// Asynchronous byte reader abstraction over a target serial device.
41#[allow(async_fn_in_trait)]
42pub trait SerialRead {
43    /// Reads the next chunk of raw bytes from the serial device.
44    ///
45    /// Returns an empty `Vec` on EOF.
46    async fn serial_read(&mut self) -> Result<Vec<u8>>;
47}
48
49/// Asynchronous byte writer abstraction over a target serial device.
50#[allow(async_fn_in_trait)]
51pub trait SerialWrite {
52    /// Writes the full slice of bytes to the serial device.
53    async fn serial_write(&mut self, data: &[u8]) -> Result<()>;
54}
55
56impl SerialRead for DeviceProxy {
57    async fn serial_read(&mut self) -> Result<Vec<u8>> {
58        let res = self.read().await?;
59        res.map_err(TargetDriverError::DriverStatus)
60    }
61}
62
63impl SerialWrite for DeviceProxy {
64    async fn serial_write(&mut self, data: &[u8]) -> Result<()> {
65        let res = self.write(data).await?;
66        res.map_err(TargetDriverError::DriverStatus)?;
67        Ok(())
68    }
69}
70
71async fn handle_handshake_frame<W: SerialWrite>(
72    handshake: &TargetHandshake,
73    frame: &uart_fpl::Frame,
74    device: &mut W,
75) -> Result<ProtocolId> {
76    let (selected, resp_payload) = handshake.handle_request(&frame.payload)?;
77    let resp_frame = encode_frame(
78        frame.session_id,
79        CONTROL_CHANNEL_ID,
80        1,
81        FrameType::NegotiateResp,
82        &resp_payload,
83    )?;
84    write_frame_to_device(device, &resp_frame).await?;
85    if let Some(proto) = selected {
86        Ok(proto)
87    } else {
88        warn!("Handshake negotiation failed: no common protocol");
89        Err(TargetDriverError::NoCommonProtocol)
90    }
91}
92
93/// Runs the target-side framing protocol handshake over `device`.
94///
95/// Waits for a valid `NegotiateReq` frame on [`CONTROL_CHANNEL_ID`], sends a
96/// `NegotiateResp`, and returns the negotiated [`ProtocolId`], session ID, and
97/// any unconsumed bytes read past the handshake frame.
98///
99/// # Errors
100///
101/// Returns [`TargetDriverError::EmptyRead`] if the serial device returns EOF
102/// before negotiation completes, or an I/O/framing error if reading or writing
103/// to the serial device fails.
104pub async fn run_target_handshake<S: SerialRead + SerialWrite>(
105    device: &mut S,
106) -> Result<(ProtocolId, u32, Vec<u8>)> {
107    run_target_handshake_with_initial_data(device, &[]).await
108}
109
110/// Runs the target-side framing protocol handshake seeded with `initial_data`.
111///
112/// Feeds `initial_data` into the frame parser before reading additional bytes
113/// from `device`, allowing carry-over bytes from a previous session to be
114/// processed without loss. Invalid or unsupported `NegotiateReq` payloads are
115/// rejected in-band while continuing to wait for a valid handshake on `device`.
116///
117/// # Errors
118///
119/// Returns [`TargetDriverError::EmptyRead`] if the serial device returns EOF
120/// before negotiation completes, or an I/O/framing error if reading or writing
121/// to the serial device fails.
122pub async fn run_target_handshake_with_initial_data<S: SerialRead + SerialWrite>(
123    device: &mut S,
124    initial_data: &[u8],
125) -> Result<(ProtocolId, u32, Vec<u8>)> {
126    let handshake = TargetHandshake::new(vec![ProtocolId::ResendSP]);
127    let mut parser = FrameParser::new();
128    if !initial_data.is_empty() {
129        parser.feed(initial_data);
130    }
131    loop {
132        while let Some(frame) = parser.next_frame() {
133            if frame.channel_id == CONTROL_CHANNEL_ID && frame.frame_type == FrameType::NegotiateReq
134            {
135                if frame.session_id == 0 {
136                    warn!("Ignoring NegotiateReq with invalid session_id 0");
137                    continue;
138                }
139                match handle_handshake_frame(&handshake, &frame, device).await {
140                    Ok(proto) => return Ok((proto, frame.session_id, parser.take_unconsumed())),
141                    Err(
142                        e @ (TargetDriverError::NoCommonProtocol | TargetDriverError::Handshake(_)),
143                    ) => {
144                        warn!(
145                            "Ignoring rejected NegotiateReq ({:?}); waiting for next handshake",
146                            e
147                        );
148                        continue;
149                    }
150                    Err(e) => return Err(e),
151                }
152            }
153        }
154        let bytes = device.serial_read().await?;
155        if bytes.is_empty() {
156            return Err(TargetDriverError::EmptyRead);
157        }
158        parser.feed(&bytes);
159    }
160}
161
162/// Buffered UART protocol frame reader wrapping a [`SerialRead`] implementation.
163///
164/// Serves as the RX counterpart to [`writer_task`]. It is structured as a
165/// borrowed reader rather than a standalone background task so that unconsumed
166/// trailing bytes in the [`FrameParser`] can be recovered via
167/// [`Self::take_unconsumed`] when a session renegotiates.
168#[derive(Debug)]
169pub struct SerialReader<R> {
170    client: R,
171    parser: FrameParser,
172}
173
174impl<R: SerialRead> SerialReader<R> {
175    /// Creates a new [`SerialReader`] with an empty parser buffer.
176    pub fn new(client: R) -> Self {
177        Self::with_initial_data(client, &[])
178    }
179
180    /// Creates a new [`SerialReader`] pre-populated with `initial_data`.
181    pub fn with_initial_data(client: R, initial_data: &[u8]) -> Self {
182        let mut parser = FrameParser::new();
183        if !initial_data.is_empty() {
184            parser.feed(initial_data);
185        }
186        Self { client, parser }
187    }
188
189    /// Drains and returns all unconsumed raw bytes currently buffered in the parser.
190    pub fn take_unconsumed(&mut self) -> Vec<u8> {
191        self.parser.take_unconsumed()
192    }
193
194    /// Re-encodes `frame` and prepends it ahead of any unconsumed bytes in the parser buffer.
195    pub fn requeue_frame(&mut self, frame: &uart_fpl::Frame) {
196        match encode_frame(
197            frame.session_id,
198            frame.channel_id,
199            frame.seq,
200            frame.frame_type,
201            &frame.payload,
202        ) {
203            Ok(raw) => {
204                let trailing = self.parser.take_unconsumed();
205                self.parser.feed(&raw);
206                if !trailing.is_empty() {
207                    self.parser.feed(&trailing);
208                }
209            }
210            Err(e) => {
211                warn!("Failed to re-encode frame for requeue: {:?}", e);
212            }
213        }
214    }
215
216    /// Reads from the underlying serial device until a complete, valid [`uart_fpl::Frame`] is parsed.
217    ///
218    /// # Errors
219    ///
220    /// Returns [`TargetDriverError::Eof`] if the underlying reader returns an empty read,
221    /// or propagates any error from [`SerialRead::serial_read`].
222    pub async fn next_frame(&mut self) -> Result<uart_fpl::Frame> {
223        loop {
224            if let Some(frame) = self.parser.next_frame() {
225                return Ok(frame);
226            }
227            let data = self.client.serial_read().await?;
228            if data.is_empty() {
229                return Err(TargetDriverError::Eof);
230            }
231            self.parser.feed(&data);
232        }
233    }
234}
235
236async fn probe_serial_path(path: &str) -> Option<(DeviceProxy, Class)> {
237    // In `fuchsia.hardware.serial`, `/dev/class/serial` nodes serve the `@discoverable`
238    // `DeviceProxy` connector protocol (generated by `fidlgen_rust` as `DeviceProxy_Marker`
239    // to avoid colliding with `Device`'s `DeviceProxy` struct), which requires calling
240    // `get_channel` to bind a `fuchsia.hardware.serial/Device` channel.
241    let proxy_client =
242        match fuchsia_component::client::connect_to_protocol_at_path::<DeviceProxy_Marker>(path) {
243            Ok(proxy) => proxy,
244            Err(e) => {
245                warn!("Failed to connect to DeviceProxy at {}: {:?}", path, e);
246                return None;
247            }
248        };
249    let (client, server) = fidl::endpoints::create_proxy::<DeviceMarker>();
250    if let Err(e) = proxy_client.get_channel(server) {
251        warn!("Failed to call get_channel for serial device at {}: {:?}", path, e);
252        return None;
253    }
254    match client.get_class().await {
255        Ok(class) => Some((client, class)),
256        Err(e) => {
257            warn!("Failed to get_class for serial device at {}: {:?}", path, e);
258            None
259        }
260    }
261}
262
263fn collect_sorted_serial_paths() -> Result<Vec<String>> {
264    let mut paths = Vec::new();
265    for entry in std::fs::read_dir(SERIAL_DEV_DIR)? {
266        let path_buf = entry?.path();
267        let path = path_buf.to_str().ok_or(TargetDriverError::NonUtf8Path)?.to_owned();
268        paths.push(path);
269    }
270    paths.sort();
271    Ok(paths)
272}
273
274async fn discover_serial_port() -> Result<(DeviceProxy, String)> {
275    let paths = collect_sorted_serial_paths()?;
276    let mut console_port = None;
277    for path in paths {
278        if let Some((client, class)) = probe_serial_path(&path).await {
279            // Prefer a dedicated generic UART over one attached to an interactive console, and
280            // ignore internal board UARTs (BluetoothHci, KernelDebug, Mcu).
281            // Note: In practice, the Class::Console branch may currently be a no-op because no
282            // Fuchsia serial driver reports Class::Console, even when used as a console port.
283            match class {
284                Class::Generic => return Ok((client, path)),
285                Class::Console => {
286                    console_port.get_or_insert((client, path));
287                }
288                _ => {}
289            }
290        }
291    }
292    console_port.ok_or(TargetDriverError::NoSerialPorts)
293}
294
295/// Discovers a serial device under `/dev/class/serial` and configures it for 8N1 at `baud_rate`.
296///
297/// Prefers `Class::Generic` ports over `Class::Console` ports.
298///
299/// # Errors
300///
301/// Returns [`TargetDriverError::NoSerialPorts`] if no usable port is found, or
302/// propagates filesystem and FIDL errors encountered during discovery.
303pub async fn open_serial_device(baud_rate: u32) -> Result<DeviceProxy> {
304    let (client, path) = discover_serial_port().await?;
305    let config = Config {
306        character_width: CharacterWidth::Bits8,
307        stop_width: StopWidth::Bits1,
308        parity: Parity::None,
309        control_flow: FlowControl::None,
310        baud_rate,
311    };
312    let status = client.set_config(&config).await?;
313    if status != 0 {
314        // Virtual serial drivers such as uart16550 on QEMU reject SetConfig while enabled or when
315        // baud_rate exceeds 115200, even though the underlying channel operates normally.
316        warn!("SetConfig on {} returned non-OK status: {}", path, status);
317    }
318    info!("Successfully opened and configured serial device at {}", path);
319    Ok(client)
320}
321
322async fn write_frame_to_device<W: SerialWrite>(device: &mut W, frame: &[u8]) -> Result<()> {
323    let mut attempts = 0;
324    loop {
325        match device.serial_write(frame).await {
326            Ok(()) => return Ok(()),
327            Err(e) => error!("Serial write error: {:?}, attempt={}", e, attempts),
328        }
329        attempts += 1;
330        if attempts >= MAX_WRITE_ATTEMPTS {
331            return Err(TargetDriverError::WriteRetriesExhausted);
332        }
333        fuchsia_async::Timer::new(WRITE_RETRY_DELAY).await;
334    }
335}
336
337async fn write_ack_frame<W: SerialWrite>(device: &mut W, session_id: u32, seq: u8) -> Result<()> {
338    let ack_frame = encode_frame(session_id, CONTROL_CHANNEL_ID, seq, FrameType::Ack, &[])?;
339    write_frame_to_device(device, &ack_frame).await
340}
341
342async fn flush_frame_batch<W: SerialWrite>(
343    device: &mut W,
344    first_frame: &[u8],
345    data_rx: &mut mpsc::Receiver<Vec<u8>>,
346    batch_buf: &mut Vec<u8>,
347) -> Result<()> {
348    batch_buf.clear();
349    batch_buf.extend_from_slice(first_frame);
350    while let Ok(next_frame) = data_rx.try_recv() {
351        batch_buf.extend_from_slice(&next_frame);
352        if batch_buf.len() >= WRITE_BATCH_CAPACITY {
353            break;
354        }
355    }
356    write_frame_to_device(device, batch_buf).await
357}
358
359/// Runs the serial frame writer loop, prioritizing pending ACKs from `ack_tracker`
360/// ahead of coalesced outgoing data frames from `data_rx`.
361///
362/// # Errors
363///
364/// Returns [`TargetDriverError::WriteRetriesExhausted`] if writing to `device`
365/// fails repeatedly, or [`TargetDriverError::Frame`] if encoding an ACK frame fails.
366pub async fn writer_task<W: SerialWrite>(
367    mut device: W,
368    ack_tracker: AckTracker,
369    mut data_rx: mpsc::Receiver<Vec<u8>>,
370) -> Result<()> {
371    let mut batch_buf = Vec::with_capacity(WRITE_BATCH_CAPACITY);
372    loop {
373        if let Some((session_id, seq)) = ack_tracker.take_ack() {
374            write_ack_frame(&mut device, session_id, seq).await?;
375            continue;
376        }
377        if data_rx.is_terminated() {
378            break;
379        }
380        futures::select_biased! {
381            (session_id, seq) = ack_tracker.wait_ack().fuse() => {
382                write_ack_frame(&mut device, session_id, seq).await?;
383            }
384            frame = data_rx.next() => match frame {
385                Some(first_frame) => {
386                    flush_frame_batch(&mut device, &first_frame, &mut data_rx, &mut batch_buf)
387                        .await?;
388                }
389                None => break,
390            },
391            complete => break,
392        }
393    }
394    Ok(())
395}
396
397#[cfg(test)]
398pub(crate) struct MockSerial {
399    pub(crate) read_data: Vec<Vec<u8>>,
400    pub(crate) write_data: Vec<Vec<u8>>,
401}
402
403#[cfg(test)]
404impl SerialRead for MockSerial {
405    async fn serial_read(&mut self) -> Result<Vec<u8>> {
406        if self.read_data.is_empty() {
407            return Ok(Vec::new());
408        }
409        Ok(self.read_data.remove(0))
410    }
411}
412
413#[cfg(test)]
414impl SerialWrite for MockSerial {
415    async fn serial_write(&mut self, data: &[u8]) -> Result<()> {
416        self.write_data.push(data.to_vec());
417        Ok(())
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use std::cell::RefCell;
425    use std::rc::Rc;
426    use uart_fpl::{HandshakeResponse, HandshakeStatus, HostHandshake};
427
428    struct SharedWriteData {
429        data: RefCell<Vec<Vec<u8>>>,
430    }
431
432    #[derive(Clone)]
433    struct MockSerialWriter {
434        shared: Rc<SharedWriteData>,
435    }
436
437    impl SerialWrite for MockSerialWriter {
438        async fn serial_write(&mut self, data: &[u8]) -> Result<()> {
439            self.shared.data.borrow_mut().push(data.to_vec());
440            Ok(())
441        }
442    }
443
444    #[fuchsia::test]
445    async fn test_writer_task_prioritizes_acks() {
446        let shared = Rc::new(SharedWriteData { data: RefCell::new(Vec::new()) });
447        let writer = MockSerialWriter { shared: shared.clone() };
448        let ack_tracker = AckTracker::new();
449        let (mut data_tx, data_rx) = mpsc::channel(10);
450        let session_id = 12345u32;
451        let data_frame_1 = encode_frame(session_id, 1, 0, FrameType::Data, &[1]).unwrap();
452        let data_frame_2 = encode_frame(session_id, 1, 1, FrameType::Data, &[2]).unwrap();
453        let ack_frame =
454            encode_frame(session_id, CONTROL_CHANNEL_ID, 0, FrameType::Ack, &[]).unwrap();
455        data_tx.send(data_frame_1.clone()).await.unwrap();
456        data_tx.send(data_frame_2.clone()).await.unwrap();
457        ack_tracker.set_ack(session_id, 0);
458        let writer_handle =
459            fuchsia_async::Task::local(
460                async move { writer_task(writer, ack_tracker, data_rx).await },
461            );
462        drop(data_tx);
463        writer_handle.await.unwrap();
464        let written = shared.data.borrow().clone();
465        let expected_data = [data_frame_1, data_frame_2].concat();
466        assert_eq!(written, vec![ack_frame, expected_data]);
467    }
468
469    #[fuchsia::test]
470    async fn test_run_target_handshake_mock_serial() {
471        let session_id = 0xCAFEBABE;
472        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
473        let (frame_type, payload) = host.start().unwrap();
474        let req_frame =
475            encode_frame(session_id, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
476        let mut mock_serial = MockSerial { read_data: vec![req_frame], write_data: Vec::new() };
477        let (proto, negotiated_sid, unconsumed) =
478            run_target_handshake(&mut mock_serial).await.expect("handshake should succeed");
479        assert_eq!(proto, ProtocolId::ResendSP);
480        assert_eq!(negotiated_sid, session_id);
481        assert!(unconsumed.is_empty());
482        assert_eq!(mock_serial.write_data.len(), 1);
483        let mut parser = FrameParser::new();
484        parser.feed(&mock_serial.write_data[0]);
485        let resp_frame = parser.next_frame().expect("valid response frame");
486        assert_eq!(resp_frame.session_id, session_id);
487        assert_eq!(resp_frame.channel_id, CONTROL_CHANNEL_ID);
488        assert_eq!(resp_frame.seq, 1);
489        assert_eq!(resp_frame.frame_type, FrameType::NegotiateResp);
490        let resp = HandshakeResponse::try_from(resp_frame.payload.as_slice()).unwrap();
491        assert_eq!(resp.status, HandshakeStatus::Success);
492        assert_eq!(resp.selected, Some(ProtocolId::ResendSP));
493    }
494
495    #[fuchsia::test]
496    async fn test_run_target_handshake_ignores_zero_session_id() {
497        let valid_sid = 0xCAFEBABE;
498        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
499        let (frame_type, payload) = host.start().unwrap();
500        let zero_req = encode_frame(0, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
501        let valid_req =
502            encode_frame(valid_sid, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
503        let mut mock_serial =
504            MockSerial { read_data: vec![zero_req, valid_req], write_data: Vec::new() };
505        let (proto, negotiated_sid, _) =
506            run_target_handshake(&mut mock_serial).await.expect("handshake should succeed");
507        assert_eq!(proto, ProtocolId::ResendSP);
508        assert_eq!(negotiated_sid, valid_sid);
509        assert_eq!(mock_serial.write_data.len(), 1);
510    }
511
512    #[fuchsia::test]
513    async fn test_serial_reader_with_initial_data() {
514        let session_id = 12345u32;
515        let frame_bytes =
516            encode_frame(session_id, 42, 0, FrameType::Data, b"buffered_stream_data").unwrap();
517        let mock_serial = MockSerial { read_data: Vec::new(), write_data: Vec::new() };
518        let mut reader = SerialReader::with_initial_data(mock_serial, &frame_bytes);
519        let frame = reader.next_frame().await.expect("Should parse frame from initial_data");
520        assert_eq!(frame.session_id, session_id);
521        assert_eq!(frame.channel_id, 42);
522        assert_eq!(frame.payload, b"buffered_stream_data");
523    }
524
525    #[fuchsia::test]
526    async fn test_serial_reader_requeue_frame_and_take_unconsumed() {
527        let session_id = 12345u32;
528        let frame1_bytes = encode_frame(session_id, 1, 0, FrameType::Data, b"first").unwrap();
529        let frame2_bytes = encode_frame(session_id, 1, 1, FrameType::Data, b"second").unwrap();
530        let mock_serial = MockSerial {
531            read_data: vec![[frame1_bytes.clone(), frame2_bytes.clone()].concat()],
532            write_data: Vec::new(),
533        };
534        let mut reader = SerialReader::new(mock_serial);
535        let parsed_frame1 = reader.next_frame().await.unwrap();
536        reader.requeue_frame(&parsed_frame1);
537        let unconsumed = reader.take_unconsumed();
538        assert_eq!(unconsumed, [frame1_bytes, frame2_bytes].concat());
539    }
540
541    #[fuchsia::test]
542    async fn test_run_target_handshake_retains_unconsumed_data() {
543        let session_id = 0x12345678;
544        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
545        let (frame_type, payload) = host.start().unwrap();
546        let req_frame =
547            encode_frame(session_id, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
548        let trailing = encode_frame(session_id, 1, 0, FrameType::Data, b"initial payload").unwrap();
549        let combined = [req_frame, trailing.clone()].concat();
550        let mut mock_serial = MockSerial { read_data: vec![combined], write_data: Vec::new() };
551        let (proto, negotiated_sid, unconsumed) =
552            run_target_handshake(&mut mock_serial).await.expect("handshake should succeed");
553        assert_eq!(proto, ProtocolId::ResendSP);
554        assert_eq!(negotiated_sid, session_id);
555        assert_eq!(unconsumed, trailing);
556        let mut reader = SerialReader::with_initial_data(mock_serial, &unconsumed);
557        let parsed = reader.next_frame().await.expect("Should parse unconsumed frame");
558        assert_eq!(parsed.channel_id, 1);
559        assert_eq!(parsed.payload, b"initial payload");
560    }
561
562    #[fuchsia::test]
563    async fn test_run_target_handshake_ignores_rejected_requests_and_retains_trailing() {
564        let unsupported_req = encode_frame(
565            0x22334455,
566            CONTROL_CHANNEL_ID,
567            0,
568            FrameType::NegotiateReq,
569            &[1, 0, 0, 0, 99],
570        )
571        .unwrap();
572        let malformed_req =
573            encode_frame(0x22334456, CONTROL_CHANNEL_ID, 0, FrameType::NegotiateReq, &[1]).unwrap();
574        let valid_sid = 0x22334457;
575        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
576        let (frame_type, payload) = host.start().unwrap();
577        let valid_req =
578            encode_frame(valid_sid, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
579        // Coalesce unsupported, malformed, and valid requests into a single chunk to verify
580        // that rejected NegotiateReq frames do not abort the loop or discard trailing bytes.
581        let combined = [unsupported_req, malformed_req, valid_req].concat();
582        let mut mock_serial = MockSerial { read_data: vec![combined], write_data: Vec::new() };
583        let (proto, negotiated_sid, unconsumed) =
584            run_target_handshake(&mut mock_serial).await.expect("handshake should succeed");
585        assert_eq!(proto, ProtocolId::ResendSP);
586        assert_eq!(negotiated_sid, valid_sid);
587        assert!(unconsumed.is_empty());
588        // One NoCommonProtocol NegotiateResp + one Success NegotiateResp.
589        assert_eq!(mock_serial.write_data.len(), 2);
590    }
591
592    #[fuchsia::test]
593    async fn test_run_target_handshake_retries_transient_write_error() {
594        struct FlakyWriteSerial {
595            read_data: Vec<Vec<u8>>,
596            write_data: Vec<Vec<u8>>,
597            remaining_write_failures: usize,
598        }
599
600        impl SerialRead for FlakyWriteSerial {
601            async fn serial_read(&mut self) -> Result<Vec<u8>> {
602                if self.read_data.is_empty() {
603                    return Ok(Vec::new());
604                }
605                Ok(self.read_data.remove(0))
606            }
607        }
608
609        impl SerialWrite for FlakyWriteSerial {
610            async fn serial_write(&mut self, data: &[u8]) -> Result<()> {
611                if self.remaining_write_failures > 0 {
612                    self.remaining_write_failures -= 1;
613                    return Err(TargetDriverError::DriverStatus(-27));
614                }
615                self.write_data.push(data.to_vec());
616                Ok(())
617            }
618        }
619
620        let session_id = 0x11223344;
621        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
622        let (frame_type, payload) = host.start().unwrap();
623        let req_frame =
624            encode_frame(session_id, CONTROL_CHANNEL_ID, 0, frame_type, &payload).unwrap();
625        let mut flaky = FlakyWriteSerial {
626            read_data: vec![req_frame],
627            write_data: Vec::new(),
628            remaining_write_failures: 2,
629        };
630        let (proto, negotiated_sid, _) =
631            run_target_handshake(&mut flaky).await.expect("handshake should succeed after retry");
632        assert_eq!(proto, ProtocolId::ResendSP);
633        assert_eq!(negotiated_sid, session_id);
634        assert_eq!(flaky.remaining_write_failures, 0);
635        assert_eq!(flaky.write_data.len(), 1);
636    }
637}