Skip to main content

twine_ctl/shell/
serial.rs

1// Copyright (c) 2026 Jake Swensen
2// SPDX-License-Identifier: MPL-2.0
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
8use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf};
9use tokio::time::Duration;
10use tokio_serial::{SerialPortBuilderExt, SerialStream};
11use twine_rs_macros::TwineShell;
12
13use crate::error::TwineCtlError;
14
15use super::{SkipResultRead, TwineCtlShell};
16
17#[derive(TwineShell)]
18#[twine_shell(crate_path = "crate")]
19pub struct TwineCtlSerialShell {
20    prompt: Option<&'static str>,
21    lines: Lines<BufReader<ReadHalf<SerialStream>>>,
22    writer: WriteHalf<SerialStream>,
23    timeout_duration: Duration,
24}
25
26impl TwineCtlSerialShell {
27    /// Open a serial connection to a Thread device
28    pub async fn open(path: &str, baud: u32) -> Result<Self, TwineCtlError> {
29        let port = tokio_serial::new(path, baud).open_native_async()?;
30
31        let (reader, writer) = tokio::io::split(port);
32        let lines = BufReader::new(reader).lines();
33
34        let mut shell = TwineCtlSerialShell {
35            prompt: Some(">"),
36            lines,
37            writer,
38            timeout_duration: Duration::from_millis(1_000),
39        };
40
41        shell.enter_and_wait_for_prompt().await?;
42
43        Ok(shell)
44    }
45
46    /// Send enter key sequences and wait for the prompt
47    async fn enter_and_wait_for_prompt(&mut self) -> Result<(), TwineCtlError> {
48        self.writer.write_all(b"\r\n\r\n").await?;
49        self.writer.flush().await?;
50        self.wait_for_prompt(self.timeout_duration).await
51    }
52}
53
54#[async_trait::async_trait]
55impl TwineCtlShell for TwineCtlSerialShell {
56    fn cmd_timeout_duration(&self) -> Duration {
57        self.timeout_duration
58    }
59
60    fn prompt(&self) -> Option<&'static str> {
61        self.prompt
62    }
63
64    async fn next_line(&mut self) -> Result<Option<String>, TwineCtlError> {
65        let line = self.lines.next_line().await?;
66        Ok(line)
67    }
68
69    async fn run(
70        &mut self,
71        cmd: &str,
72        timeout_duration: Duration,
73        skip_result_read: SkipResultRead,
74    ) -> Result<Vec<String>, TwineCtlError> {
75        self.writer.write_all(cmd.as_bytes()).await?;
76        self.writer.write_all(b"\r\n").await?;
77        self.writer.flush().await?;
78
79        match skip_result_read {
80            SkipResultRead::True => {
81                self.enter_and_wait_for_prompt().await?;
82                Ok(Vec::new())
83            }
84            SkipResultRead::False => self.read_result(cmd, timeout_duration).await,
85        }
86    }
87}