1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use std::sync::atomic::Ordering;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::time::sleep;
use super::x714::X714;
impl X714 {
/// Full Serial connection + reconnection loop with VID/PID auto-detection.
/// Mirrors Python's `connect_serial()` from `SerialProtocol`.
/// Runs forever until `self.shared.running` is set to `false`.
pub(crate) async fn run_serial_loop(&self) {
loop {
if !self.shared.running.load(Ordering::Relaxed) {
break;
}
// ── Port selection ─────────────────────────────────────────────────
let port_name = if self.config.serial.port.to_uppercase() == "AUTO" {
match detect_serial_port(self.config.serial.vid, self.config.serial.pid) {
Some(p) => {
eprintln!(
"[{}] 🔍 Auto-detected port: {} (VID={:#06x} PID={:#06x})",
self.config.name, p, self.config.serial.vid, self.config.serial.pid
);
p
}
None => {
eprintln!(
"[{}] ⚠️ No serial port with VID={:#06x} PID={:#06x} found – retrying in {}s…",
self.config.name,
self.config.serial.vid,
self.config.serial.pid,
self.config.reconnection_time
);
sleep(Duration::from_secs(self.config.reconnection_time)).await;
continue;
}
}
} else {
self.config.serial.port.clone()
};
eprintln!(
"[{}] 🔌 Connecting to {} @ {} bps…",
self.config.name, port_name, self.config.serial.baudrate
);
// ── Open port ─────────────────────────────────────────────────────
let builder = tokio_serial::new(&port_name, self.config.serial.baudrate);
match tokio_serial::SerialStream::open(&builder) {
Ok(stream) => {
eprintln!(
"[{}] ✅ Serial connected on {}",
self.config.name, port_name
);
let (read_half, write_half) = tokio::io::split(stream);
*self.shared.writer.lock().await =
Some(Box::new(write_half) as Box<dyn tokio::io::AsyncWrite + Send + Unpin>);
self.on_connected().await;
// ── Read loop ──────────────────────────────────────────────
let recv_self = self.clone();
let recv_task = tokio::spawn(async move {
recv_self.serial_receive_loop(read_half).await;
});
recv_task.await.ok();
// ── Cleanup ────────────────────────────────────────────────
*self.shared.writer.lock().await = None;
self.on_disconnected();
eprintln!(
"[{}] 🔌 Serial disconnected, reconnecting…",
self.config.name
);
}
Err(e) => {
eprintln!(
"[{}] ❌ Serial open error on {}: {}",
self.config.name, port_name, e
);
}
}
if !self.shared.running.load(Ordering::Relaxed) {
break;
}
sleep(Duration::from_secs(self.config.reconnection_time)).await;
}
}
async fn serial_receive_loop(&self, reader: tokio::io::ReadHalf<tokio_serial::SerialStream>) {
let mut buf_reader = BufReader::new(reader);
let mut line = String::new();
loop {
if !self.shared.is_connected.load(Ordering::Relaxed) {
break;
}
line.clear();
match buf_reader.read_line(&mut line).await {
Ok(0) => {
// EOF – port disconnected
self.shared.is_connected.store(false, Ordering::Relaxed);
break;
}
Ok(_) => {
let trimmed = line.trim();
if !trimmed.is_empty() {
self.on_receive(trimmed);
}
}
Err(e) => {
eprintln!("[{}] Serial read error: {}", self.config.name, e);
self.shared.is_connected.store(false, Ordering::Relaxed);
break;
}
}
}
}
}
/// Enumerate available serial ports and return the device path matching the
/// given USB VID+PID, or `None` if not found.
fn detect_serial_port(vid: u16, pid: u16) -> Option<String> {
let ports = serialport::available_ports().ok()?;
for port in ports {
if let serialport::SerialPortType::UsbPort(info) = port.port_type {
if info.vid == vid && info.pid == pid {
return Some(port.port_name);
}
}
}
None
}