Skip to content

Commit 121d596

Browse files
committed
refactor: 合并重复实现精简代码 (emitter.rs 统一 emit_serial_data 三份, conn.rs 泛型 pump_read_loop 合并串口/TCP 读循环, spawn_idf_bat 收敛 ESP 子进程样板并补 CREATE_NO_WINDOW 修取消烧录闪黑窗, ConnCore::set/is_reconnecting 收敛状态样板, utils.js copyText/textToUtf8Bytes/textToHex/pad2/fileStamp 收敛前端重复工具, menu.js 下拉样板 helpers 收敛清选中与键盘导航)
1 parent d7f93b7 commit 121d596

15 files changed

Lines changed: 285 additions & 292 deletions

src-tauri/src/conn.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,80 @@ pub fn validate_mode(mode: u8) -> bool {
1717

1818
const IO_STATS_INTERVAL: Duration = Duration::from_millis(250);
1919

20+
/// 读取间隔静默判定:空闲超过该时长即把已累积的数据作为一帧推出。
21+
/// 串口 / TCP / UDP / SSH 四条兄弟管线共用同一阈值。
22+
pub const GAP_TIMEOUT: Duration = Duration::from_millis(5);
23+
24+
/// 读取泵的退出原因(兄弟管线共用,见 [`pump_read_loop`])。
25+
#[derive(Clone, Copy, PartialEq)]
26+
pub enum ReadLoopEnd {
27+
/// 外部请求停止(关闭端口 / 代次过期)。
28+
Stopped,
29+
/// 读错误或对端关闭,连接已不可用。
30+
DeviceLost,
31+
}
32+
33+
/// 统一的接收读取泵:所有兄弟连接(串口 / TCP)共用同一套成帧逻辑——
34+
/// 累积到 4096 字节立即成帧,或空闲超过 [`GAP_TIMEOUT`] 后把残留作为一帧推出。
35+
///
36+
/// 停止/让位判定与 EOF 行为各不相同,通过参数注入:
37+
/// - `label`: 日志上下文("Serial" / "TCP" …)
38+
/// - `should_stop`: 请求停止或代次过期
39+
/// - `is_parked`: 工具占用管线时让位(烧录 / 监控期间不读)
40+
/// - `on_eof`: 读到 EOF 时的处理;串口无 EOF 语义传 `None`
41+
pub fn pump_read_loop<R: Read>(
42+
reader: R,
43+
meter: Arc<Meter>,
44+
app: &tauri::AppHandle,
45+
label: &str,
46+
should_stop: impl Fn() -> bool,
47+
is_parked: impl Fn() -> bool,
48+
on_eof: Option<Box<dyn Fn() -> ReadLoopEnd>>,
49+
) -> ReadLoopEnd {
50+
let mut reader = MeteredReader::new(reader, meter);
51+
let mut buf = [0u8; 4096];
52+
let mut acc: Vec<u8> = Vec::new();
53+
let mut last_time = Instant::now();
54+
55+
loop {
56+
if should_stop() {
57+
if !acc.is_empty() {
58+
crate::emitter::emit_serial_data(app, acc.clone(), true);
59+
}
60+
return ReadLoopEnd::Stopped;
61+
}
62+
if is_parked() {
63+
std::thread::sleep(Duration::from_millis(10));
64+
continue;
65+
}
66+
match reader.read(&mut buf) {
67+
Ok(0) => {
68+
if let Some(end) = on_eof {
69+
return end();
70+
}
71+
}
72+
Ok(n) if n > 0 => {
73+
acc.extend_from_slice(&buf[..n]);
74+
last_time = Instant::now();
75+
if acc.len() >= 4096 {
76+
crate::emitter::emit_serial_data(app, std::mem::take(&mut acc), false);
77+
}
78+
}
79+
Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {
80+
if !acc.is_empty() && last_time.elapsed() >= GAP_TIMEOUT {
81+
crate::emitter::emit_serial_data(app, std::mem::take(&mut acc), true);
82+
}
83+
std::thread::sleep(Duration::from_millis(10));
84+
}
85+
Err(e) => {
86+
log::warn!("{} read error: {}", label, e);
87+
return ReadLoopEnd::DeviceLost;
88+
}
89+
_ => {}
90+
}
91+
}
92+
}
93+
2094
/// 收发字节计数:连接管线上的非侵入装饰(计数 + 节流 io-stats 事件内聚)。
2195
/// 兄弟连接(串口/TCP/UDP/未来 SSH)共享同一实例;`info()` 与前端状态栏
2296
/// 均从它取值,清空按钮经 `reset_and_emit` 联动归零。

src-tauri/src/emitter.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//! 前端接收帧的统一出口。
2+
//!
3+
//! 串口 / TCP / UDP / SSH 四条兄弟管线把数据以同一个 `serial-data` 事件
4+
//! 推给前端,emit 逻辑因此收敛到一处:各 cmd 模块只管读,不管怎么发。
5+
6+
use tauri::Emitter;
7+
8+
/// 推送一帧接收数据。`frame_end` 标记该帧是否完整(用于前端帧聚合)。
9+
pub(crate) fn emit_serial_data(app: &tauri::AppHandle, data: Vec<u8>, frame_end: bool) {
10+
let _ = app.emit(
11+
"serial-data",
12+
serde_json::json!({ "bytes": data, "frameEnd": frame_end }),
13+
);
14+
}

src-tauri/src/encoding_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ pub(crate) fn get_port_description(name: &str) -> Option<String> {
174174
#[cfg(windows)]
175175
{
176176
use std::os::windows::process::CommandExt;
177+
// CREATE_NO_WINDOW:wmic 只是查数据,不该闪出控制台窗口。
177178
cmd.creation_flags(0x08000000);
178179
}
179180
let output = cmd

src-tauri/src/esp_cmd.rs

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,36 @@ fn write_step_bat(script: &str) -> Result<PathBuf, String> {
268268
Ok(bat)
269269
}
270270

271+
/// CREATE_NO_WINDOW:所有子进程都不该弹出控制台窗口(烧录、环境探测、
272+
/// 取消时的 taskkill 同理),否则取消烧录会闪出黑窗。
273+
const CREATE_NO_WINDOW: u32 = 0x08000000;
274+
275+
/// 统一的 IDF 子进程启动样板:写临时 bat → 剥离 MSYS 变量 → 双管道 spawn。
276+
/// 错误以原始 `io::Error` 返回,由调用方按其上下文格式化文案。
277+
fn spawn_idf_bat(cwd: &str, script: &str) -> Result<Child, std::io::Error> {
278+
let bat = write_step_bat(script).map_err(std::io::Error::other)?;
279+
let bat_path = bat.to_string_lossy().to_string();
280+
let mut cmd = Command::new("cmd");
281+
cmd.arg("/C")
282+
.arg(&bat_path)
283+
.current_dir(cwd)
284+
.env("PYTHONUNBUFFERED", "1")
285+
// 从 Git Bash 启动时这些变量会让 export/activate 脚本拒绝执行。
286+
.env_remove("MSYSTEM")
287+
.env_remove("MSYS")
288+
.env_remove("MINGW_PREFIX")
289+
.env_remove("MINGW_CHOST")
290+
.env_remove("MSYSTEM_CHOST")
291+
.stdout(Stdio::piped())
292+
.stderr(Stdio::piped());
293+
#[cfg(windows)]
294+
{
295+
use std::os::windows::process::CommandExt;
296+
cmd.creation_flags(CREATE_NO_WINDOW);
297+
}
298+
cmd.spawn()
299+
}
300+
271301
/// Runs one raw cmd script (blocking). Returns:
272302
/// - Ok(true) success (exit 0)
273303
/// - Ok(false) non-zero exit
@@ -285,21 +315,7 @@ fn run_cmd(
285315
script: &str,
286316
cwd: &str,
287317
) -> Result<bool, String> {
288-
let bat = write_step_bat(script)?;
289-
let bat_path = bat.to_string_lossy().to_string();
290-
let mut child = Command::new("cmd")
291-
.arg("/C")
292-
.arg(&bat_path)
293-
.current_dir(cwd)
294-
.env("PYTHONUNBUFFERED", "1")
295-
.env_remove("MSYSTEM")
296-
.env_remove("MSYS")
297-
.env_remove("MINGW_PREFIX")
298-
.env_remove("MINGW_CHOST")
299-
.env_remove("MSYSTEM_CHOST")
300-
.stdout(Stdio::piped())
301-
.stderr(Stdio::piped())
302-
.spawn()
318+
let mut child = spawn_idf_bat(cwd, script)
303319
.map_err(|e| format!("Failed to start {}: {}", stage, e))?;
304320

305321
let stdout = child.stdout.take();
@@ -538,9 +554,14 @@ pub async fn esp_flash_cancel(handle: tauri::State<'_, EspHandle>) -> Result<(),
538554
if let Some(mut c) = child {
539555
let pid = c.id();
540556
let _ = c.kill();
541-
let _ = Command::new("taskkill")
542-
.args(["/PID", &pid.to_string(), "/T", "/F"])
543-
.output();
557+
let mut cmd = Command::new("taskkill");
558+
cmd.args(["/PID", &pid.to_string(), "/T", "/F"]);
559+
#[cfg(windows)]
560+
{
561+
use std::os::windows::process::CommandExt;
562+
cmd.creation_flags(CREATE_NO_WINDOW);
563+
}
564+
let _ = cmd.output();
544565
}
545566
Ok(())
546567
}
@@ -738,25 +759,7 @@ fn run_probe(python: &str, activate: &Path, idf_path: &str) -> (bool, String) {
738759
python,
739760
activate.to_string_lossy()
740761
);
741-
let bat = match write_step_bat(&script) {
742-
Ok(b) => b,
743-
Err(e) => return (false, e),
744-
};
745-
let bat_path = bat.to_string_lossy().to_string();
746-
let mut child = match Command::new("cmd")
747-
.arg("/C")
748-
.arg(&bat_path)
749-
.current_dir(idf_path)
750-
.env("PYTHONUNBUFFERED", "1")
751-
.env_remove("MSYSTEM")
752-
.env_remove("MSYS")
753-
.env_remove("MINGW_PREFIX")
754-
.env_remove("MINGW_CHOST")
755-
.env_remove("MSYSTEM_CHOST")
756-
.stdout(Stdio::piped())
757-
.stderr(Stdio::piped())
758-
.spawn()
759-
{
762+
let mut child = match spawn_idf_bat(idf_path, &script) {
760763
Ok(c) => c,
761764
Err(e) => return (false, format!("启动失败: {}", e)),
762765
};

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod serial_cmd;
22
mod checksum;
33
mod state;
44
mod conn;
5+
mod emitter;
56
mod receive_buffer;
67
mod mcp_server;
78
mod window_helper;

0 commit comments

Comments
 (0)