#[cfg(feature = "native")]
use std::io::Write as _;
#[cfg(feature = "native")]
pub struct MonitorSession {
port: Box<dyn serialport::SerialPort>,
}
#[cfg(feature = "native")]
impl MonitorSession {
pub fn open(port_name: &str, baud_rate: u32) -> crate::Result<Self> {
let port = serialport::new(port_name, baud_rate)
.timeout(std::time::Duration::from_millis(50))
.open()?;
Ok(Self { port })
}
pub fn try_clone_reader(&self) -> crate::Result<Box<dyn serialport::SerialPort>> {
Ok(self
.port
.try_clone()?)
}
pub fn write_bytes(&mut self, data: &[u8]) -> crate::Result<()> {
self.port
.write_all(data)?;
Ok(())
}
pub fn set_data_terminal_ready(&mut self, enabled: bool) -> crate::Result<()> {
self.port
.write_data_terminal_ready(enabled)?;
Ok(())
}
pub fn set_request_to_send(&mut self, enabled: bool) -> crate::Result<()> {
self.port
.write_request_to_send(enabled)?;
Ok(())
}
}
#[cfg(not(feature = "native"))]
pub struct MonitorSession;
#[cfg(not(feature = "native"))]
impl MonitorSession {
pub fn open(_port_name: &str, _baud_rate: u32) -> crate::Result<Self> {
Err(crate::Error::Unsupported(
"Serial monitor is only available with native feature".to_string(),
))
}
}
pub fn split_utf8(bytes: &[u8]) -> (&str, &[u8]) {
match std::str::from_utf8(bytes) {
Ok(s) => (s, &[]),
Err(e) => {
let valid_up_to = e.valid_up_to();
let valid = std::str::from_utf8(&bytes[..valid_up_to]).unwrap_or_default();
(valid, &bytes[valid_up_to..])
},
}
}
pub fn drain_utf8_lossy(buffer: &mut Vec<u8>) -> String {
let mut output = String::new();
loop {
match std::str::from_utf8(buffer) {
Ok(valid) => {
output.push_str(valid);
buffer.clear();
break;
},
Err(err) => {
let valid_up_to = err.valid_up_to();
if valid_up_to > 0 {
if let Ok(valid) = std::str::from_utf8(&buffer[..valid_up_to]) {
output.push_str(valid);
}
}
match err.error_len() {
Some(invalid_len) => {
output.push('�');
let drain_to = valid_up_to
.saturating_add(invalid_len)
.min(buffer.len());
buffer.drain(..drain_to);
},
None => {
if valid_up_to > 0 {
buffer.drain(..valid_up_to);
}
break;
},
}
},
}
}
output
}
pub fn clean_monitor_text(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\n' | '\t' => out.push(ch),
'\r' => out.push('\n'),
_ if ch.is_control() => {},
_ => out.push(ch),
}
}
out
}
pub fn format_monitor_output(text: &str, timestamp: bool, at_line_start: &mut bool) -> String {
let normalized = text
.replace("\r\n", "\n")
.replace('\r', "\n");
if !timestamp {
let mut out = String::with_capacity(normalized.len() * 2);
for c in normalized.chars() {
match c {
'\n' => {
out.push_str("\r\n");
*at_line_start = true;
},
_ => {
out.push(c);
*at_line_start = false;
},
}
}
return out;
}
let mut out = String::with_capacity(normalized.len() + 128);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let total_secs = now.as_secs();
let millis = now.subsec_millis();
let hours = (total_secs / 3600) % 24;
let minutes = (total_secs / 60) % 60;
let seconds = total_secs % 60;
for c in normalized.chars() {
match c {
'\n' => {
out.push_str("\r\n");
*at_line_start = true;
},
_ => {
if *at_line_start {
use std::fmt::Write;
let _ = write!(
out,
"\x1b[90m[{hours:02}:{minutes:02}:{seconds:02}.{millis:03}]\x1b[0m "
);
*at_line_start = false;
}
out.push(c);
},
}
}
out
}
#[cfg(test)]
mod tests {
use super::{clean_monitor_text, drain_utf8_lossy, format_monitor_output};
#[test]
fn test_drain_utf8_lossy_replaces_invalid_bytes_and_continues() {
let mut buf = vec![0xFF, b'A', 0xFE, b'B'];
let out = drain_utf8_lossy(&mut buf);
assert_eq!(out, "�A�B");
assert!(buf.is_empty());
}
#[test]
fn test_drain_utf8_lossy_keeps_incomplete_suffix() {
let mut buf = vec![0xE4, 0xBD]; let out = drain_utf8_lossy(&mut buf);
assert_eq!(out, "");
assert_eq!(buf, vec![0xE4, 0xBD]);
buf.push(0xA0);
let out2 = drain_utf8_lossy(&mut buf);
assert_eq!(out2, "你");
assert!(buf.is_empty());
}
#[test]
fn test_clean_monitor_text_filters_control_chars() {
let text = "A\x07B\x1BC\tD\nE\rF";
let cleaned = clean_monitor_text(text);
assert_eq!(cleaned, "ABC\tD\nE\nF");
}
#[test]
fn test_format_output_normalizes_standalone_cr_to_newline() {
let mut at_line_start = true;
let result = format_monitor_output("abc\rdef", false, &mut at_line_start);
assert_eq!(result, "abc\r\ndef");
}
#[test]
fn test_format_output_no_timestamp_updates_line_state() {
let mut at_line_start = true;
let result = format_monitor_output("abc", false, &mut at_line_start);
assert_eq!(result, "abc");
assert!(!at_line_start);
let result2 = format_monitor_output("\n", false, &mut at_line_start);
assert_eq!(result2, "\r\n");
assert!(at_line_start);
}
}