Skip to content

Commit 51ea338

Browse files
authored
feat: detect serial hotplug state changes (#31)
1 parent 066603c commit 51ea338

14 files changed

Lines changed: 521 additions & 69 deletions

File tree

scripts/i18n-check.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
66
const projectRoot = path.resolve(scriptDir, '..');
77
const srcDir = path.join(projectRoot, 'src');
88
const localesDir = path.join(srcDir, 'i18n', 'locales');
9+
const DEFAULT_LOCALE = 'zh';
910

1011
function readJson(filePath) {
1112
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -76,11 +77,16 @@ for (const file of files) {
7677
}
7778
}
7879

79-
const missingByLocale = {};
80+
const errorMissingByLocale = {};
81+
const warningMissingByLocale = {};
8082
for (const [locale, keys] of Object.entries(locales)) {
8183
const missing = Array.from(usedKeys).filter(key => !keys.has(key)).sort();
8284
if (missing.length > 0) {
83-
missingByLocale[locale] = missing;
85+
if (locale === DEFAULT_LOCALE) {
86+
errorMissingByLocale[locale] = missing;
87+
} else {
88+
warningMissingByLocale[locale] = missing;
89+
}
8490
}
8591
}
8692

@@ -90,13 +96,24 @@ if (dynamicKeys.size > 0) {
9096
console.log(`Dynamic template keys skipped: ${dynamicKeys.size}`);
9197
}
9298

93-
if (Object.keys(missingByLocale).length === 0) {
99+
if (Object.keys(errorMissingByLocale).length === 0 && Object.keys(warningMissingByLocale).length === 0) {
94100
console.log('No missing keys detected for string-literal usages.');
95101
process.exit(0);
96102
}
97103

98-
for (const [locale, missing] of Object.entries(missingByLocale)) {
99-
console.log(`\nMissing in ${locale} (${missing.length}):`);
104+
for (const [locale, missing] of Object.entries(warningMissingByLocale)) {
105+
console.log(`\nMissing in ${locale} (${missing.length}) [fallback -> ${DEFAULT_LOCALE}]:`);
106+
for (const key of missing) {
107+
console.log(` - ${key}`);
108+
}
109+
}
110+
111+
if (Object.keys(errorMissingByLocale).length === 0) {
112+
process.exit(0);
113+
}
114+
115+
for (const [locale, missing] of Object.entries(errorMissingByLocale)) {
116+
console.log(`\nMissing in ${locale} (${missing.length}) [required]:`);
100117
for (const key of missing) {
101118
console.log(` - ${key}`);
102119
}

src-tauri/Cargo.lock

Lines changed: 56 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ serde_json = "1"
2626
tauri-plugin-store = "2"
2727
tauri-plugin-updater = "2"
2828
serialport = "4.2"
29+
nusb = "0.2.3"
30+
futures-lite = "2"
2931
sftool-lib = "0.2.1"
3032
tauri-plugin-dialog = "2"
3133
tauri-plugin-fs = "2"

src-tauri/src/app.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::commands::*;
22
use crate::state::AppState;
3+
use crate::utils::spawn_serial_hotplug_watcher;
34
use std::sync::Mutex;
45
use std::time::Duration;
56
use tauri::Manager;
@@ -47,6 +48,7 @@ pub fn run() {
4748
.plugin(tauri_plugin_process::init())
4849
.setup(|app| {
4950
app.manage(Mutex::new(AppState::default()));
51+
spawn_serial_hotplug_watcher(app.handle().clone());
5052
Ok(())
5153
})
5254
.invoke_handler(tauri::generate_handler![

src-tauri/src/commands/device.rs

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,14 @@
11
use crate::progress::TauriProgressCallback;
22
use crate::state::AppState;
33
use crate::types::{DeviceConfig, PortInfo};
4-
use crate::utils::create_tool_instance_with_progress;
4+
use crate::utils::{create_tool_instance_with_progress, list_serial_ports};
55
use sftool_lib::progress::ProgressSinkArc;
66
use std::sync::{Arc, Mutex};
77
use tauri::{AppHandle, State};
88

99
#[tauri::command]
1010
pub fn get_serial_ports() -> Result<Vec<PortInfo>, String> {
11-
let ports = match serialport::available_ports() {
12-
Ok(ports) => ports,
13-
Err(e) => return Err(format!("无法获取串口列表: {}", e)),
14-
};
15-
16-
let port_infos = ports
17-
.into_iter()
18-
.filter(|_p| {
19-
// 在 macOS 下过滤掉 /dev/tty* 开头的串口,只保留 /dev/cu* 的
20-
#[cfg(target_os = "macos")]
21-
{
22-
if _p.port_name.starts_with("/dev/tty") {
23-
return false;
24-
}
25-
}
26-
true
27-
})
28-
.map(|p| {
29-
let port_type = match p.port_type {
30-
serialport::SerialPortType::UsbPort(info) => {
31-
format!("USB ({:04x}:{:04x})", info.vid, info.pid)
32-
}
33-
serialport::SerialPortType::BluetoothPort => "蓝牙".to_string(),
34-
serialport::SerialPortType::PciPort => "PCI".to_string(),
35-
_ => "未知".to_string(),
36-
};
37-
38-
PortInfo {
39-
name: p.port_name,
40-
port_type,
41-
}
42-
})
43-
.collect();
44-
45-
Ok(port_infos)
11+
list_serial_ports()
4612
}
4713

4814
#[tauri::command]
@@ -86,8 +52,7 @@ pub async fn connect_device(
8652
#[tauri::command]
8753
pub fn disconnect_device(state: State<'_, Mutex<AppState>>) -> Result<(), String> {
8854
let mut app_state = state.lock().unwrap();
89-
app_state.device_config = None;
90-
app_state.sftool = None;
55+
app_state.clear_device_connection();
9156
Ok(())
9257
}
9358

src-tauri/src/state/app_state.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ impl Default for AppState {
2121
}
2222

2323
impl AppState {
24+
pub fn clear_device_connection(&mut self) {
25+
self.device_config = None;
26+
self.sftool = None;
27+
}
28+
2429
pub fn register_temp_dir(&mut self, path: PathBuf) {
2530
self.retained_temp_dirs.push(path);
2631
}

src-tauri/src/types/device.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
use serde::{Deserialize, Serialize};
22

3-
#[derive(Debug, Serialize, Deserialize)]
3+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
4+
pub struct UsbInfo {
5+
pub vid: u16,
6+
pub pid: u16,
7+
#[serde(skip_serializing_if = "Option::is_none")]
8+
pub serial_number: Option<String>,
9+
#[serde(skip_serializing_if = "Option::is_none")]
10+
pub manufacturer: Option<String>,
11+
#[serde(skip_serializing_if = "Option::is_none")]
12+
pub product: Option<String>,
13+
}
14+
15+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
416
pub struct PortInfo {
517
pub name: String,
618
pub port_type: String,
19+
#[serde(skip_serializing_if = "Option::is_none")]
20+
pub usb_info: Option<UsbInfo>,
21+
}
22+
23+
#[derive(Debug, Serialize, Deserialize, Clone)]
24+
pub struct SerialPortsChangedEvent {
25+
pub ports: Vec<PortInfo>,
726
}
827

928
#[derive(Debug, Serialize, Deserialize, Clone)]

src-tauri/src/utils/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
pub mod serial_ports;
12
pub mod stub_ops;
23
pub mod tool_factory;
34
pub mod validator;
45

6+
pub use serial_ports::*;
57
pub use tool_factory::*;
68
pub use validator::*;

0 commit comments

Comments
 (0)