Skip to content

Commit cbfd260

Browse files
committed
feat: IDF 连接模式 — 火焰按钮 Build+Flash+Monitor 一键烧录 (调 idf.py, 设置可配路径+自动探测, MCP 开放 esp_build_flash 工具)
1 parent 27c495a commit cbfd260

15 files changed

Lines changed: 865 additions & 23 deletions

File tree

src-tauri/src/esp_cmd.rs

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.

src-tauri/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ mod window_helper;
77
mod encoding_utils;
88
mod multi_string;
99
mod net_cmd;
10+
mod esp_cmd;
1011

12+
use esp_cmd::EspHandle;
1113
use state::{NetState, SerialState};
1214
use receive_buffer::ReceiveBuffer;
1315
use mcp_server::McpServerHandle;
@@ -80,6 +82,7 @@ pub fn run() {
8082
.plugin(tauri_plugin_window_state::Builder::default().build())
8183
.manage(serial)
8284
.manage(net)
85+
.manage(EspHandle::new())
8386
.manage(ReceiveBuffer::new())
8487
.manage(McpServerHandle::new())
8588
.manage(state::LocaleState::default())
@@ -99,6 +102,11 @@ pub fn run() {
99102
net_cmd::net_open,
100103
net_cmd::net_close,
101104
net_cmd::set_conn_mode,
105+
esp_cmd::set_esp_config,
106+
esp_cmd::esp_build_flash_start,
107+
esp_cmd::esp_flash_cancel,
108+
esp_cmd::esp_reset,
109+
esp_cmd::detect_esp_paths,
102110
multi_string::open_multi_string_window,
103111
multi_string::load_multi_strings,
104112
multi_string::save_multi_strings,

src-tauri/src/mcp_server.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ impl McpServerHandle {
3333
struct AppState {
3434
buffer: ReceiveBuffer,
3535
serial: SerialState,
36+
esp: crate::esp_cmd::EspHandle,
3637
app_handle: tauri::AppHandle,
3738
}
3839

@@ -41,6 +42,7 @@ impl Clone for AppState {
4142
Self {
4243
buffer: self.buffer.clone(),
4344
serial: self.serial.clone(),
45+
esp: self.esp.clone(),
4446
app_handle: self.app_handle.clone(),
4547
}
4648
}
@@ -180,6 +182,30 @@ async fn handle_tools_list(id: Value) -> (StatusCode, Json<Value>) {
180182
"type": "object",
181183
"properties": {}
182184
}
185+
},
186+
{
187+
"name": "esp_build_flash",
188+
"description": "对 ESP-IDF 项目执行 build + flash(等同于主界面的火焰按钮),完成后芯片自动复位运行",
189+
"inputSchema": {
190+
"type": "object",
191+
"properties": {
192+
"project_dir": {
193+
"type": "string",
194+
"description": "ESP-IDF 项目根目录(含 CMakeLists.txt)"
195+
},
196+
"port": {
197+
"type": "string",
198+
"description": "串口, 缺省用当前选中的串口",
199+
"default": ""
200+
},
201+
"baud": {
202+
"type": "integer",
203+
"description": "烧录波特率, 缺省用设置里的值",
204+
"default": 0
205+
}
206+
},
207+
"required": ["project_dir"]
208+
}
183209
}
184210
]
185211
}));
@@ -256,6 +282,82 @@ async fn handle_tools_call(state: &AppState, body: &Value, id: Value) -> (Status
256282
}));
257283
(StatusCode::OK, Json(resp))
258284
}
285+
"esp_build_flash" => {
286+
let project_dir = args.get("project_dir")
287+
.and_then(|v| v.as_str())
288+
.unwrap_or("")
289+
.to_string();
290+
let port = args.get("port")
291+
.and_then(|v| v.as_str())
292+
.unwrap_or("")
293+
.to_string();
294+
let baud = args.get("baud").and_then(|v| v.as_u64()).map(|v| v as u32);
295+
296+
if project_dir.is_empty() {
297+
let resp = jsonrpc_error(-32602, "project_dir cannot be empty");
298+
return (StatusCode::BAD_REQUEST, Json(resp));
299+
}
300+
301+
let cfg = {
302+
let c = state.esp.config.lock().unwrap_or_else(|e| e.into_inner());
303+
crate::esp_cmd::EspConfig {
304+
idf_path: c.idf_path.clone(),
305+
python_path: c.python_path.clone(),
306+
baud: baud.unwrap_or(c.baud),
307+
}
308+
};
309+
if cfg.idf_path.trim().is_empty() {
310+
let resp = jsonrpc_error(-32603, "ESP-IDF 路径未配置, 请先在系统设置里填写");
311+
return (StatusCode::INTERNAL_SERVER_ERROR, Json(resp));
312+
}
313+
314+
{
315+
let mut busy = state.esp.busy.lock().unwrap_or_else(|e| e.into_inner());
316+
if *busy {
317+
let resp = jsonrpc_error(-32603, "已有烧录任务在运行");
318+
return (StatusCode::INTERNAL_SERVER_ERROR, Json(resp));
319+
}
320+
*busy = true;
321+
}
322+
323+
let esp = state.esp.clone();
324+
let app_handle = state.app_handle.clone();
325+
let serial_name = state
326+
.serial
327+
.port_name
328+
.lock()
329+
.unwrap_or_else(|e| e.into_inner())
330+
.clone()
331+
.unwrap_or_default();
332+
let port = if port.is_empty() { serial_name } else { port };
333+
let result = tokio::task::spawn_blocking(move || {
334+
let r = crate::esp_cmd::run_worker(&esp, &app_handle, &cfg, &project_dir, &port);
335+
let mut busy = esp.busy.lock().unwrap_or_else(|e| e.into_inner());
336+
*busy = false;
337+
r
338+
})
339+
.await;
340+
341+
match result {
342+
Ok(Ok(())) => {
343+
let resp = jsonrpc_success(id, json!({
344+
"content": [{
345+
"type": "text",
346+
"text": "build + flash 成功, 芯片已复位运行"
347+
}]
348+
}));
349+
(StatusCode::OK, Json(resp))
350+
}
351+
Ok(Err(e)) => {
352+
let resp = jsonrpc_error(-32603, &format!("烧录失败: {}", e));
353+
(StatusCode::INTERNAL_SERVER_ERROR, Json(resp))
354+
}
355+
Err(e) => {
356+
let resp = jsonrpc_error(-32603, &format!("任务异常: {}", e));
357+
(StatusCode::INTERNAL_SERVER_ERROR, Json(resp))
358+
}
359+
}
360+
}
259361
_ => {
260362
let resp = jsonrpc_error(-32602, &format!("Tool not found: {}", tool_name));
261363
(StatusCode::NOT_FOUND, Json(resp))
@@ -296,6 +398,7 @@ pub async fn mcp_start(
296398
handle: tauri::State<'_, McpServerHandle>,
297399
buffer: tauri::State<'_, ReceiveBuffer>,
298400
serial: tauri::State<'_, SerialState>,
401+
esp: tauri::State<'_, crate::esp_cmd::EspHandle>,
299402
port: u16,
300403
) -> Result<(), String> {
301404
let mut running = handle.running.lock().await;
@@ -307,6 +410,7 @@ pub async fn mcp_start(
307410
let state = AppState {
308411
buffer: (*buffer).clone(),
309412
serial: (*serial).clone(),
413+
esp: (*esp).clone(),
310414
app_handle: app_handle.clone(),
311415
};
312416

src-tauri/src/net_cmd.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::state::{
2-
ConnState, NetIo, NetState, MODE_SERIAL, MODE_TCP_CLIENT, MODE_TCP_SERVER, MODE_UDP_CLIENT,
3-
MODE_UDP_SERVER,
2+
ConnState, NetIo, NetState, MODE_IDF, MODE_SERIAL, MODE_TCP_CLIENT, MODE_TCP_SERVER,
3+
MODE_UDP_CLIENT, MODE_UDP_SERVER,
44
};
55
use std::io::{Read, Write};
66
use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs, UdpSocket};
@@ -33,7 +33,7 @@ pub async fn set_conn_mode(
3333
mode: u8,
3434
) -> Result<(), String> {
3535
let state = state.inner();
36-
if !(MODE_SERIAL..=MODE_UDP_SERVER).contains(&mode) {
36+
if !(MODE_SERIAL..=MODE_IDF).contains(&mode) {
3737
return Err("Unknown connection mode".into());
3838
}
3939
state.mode.store(mode, Ordering::SeqCst);

src-tauri/src/state.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub const MODE_TCP_CLIENT: u8 = 1;
88
pub const MODE_TCP_SERVER: u8 = 2;
99
pub const MODE_UDP_CLIENT: u8 = 3;
1010
pub const MODE_UDP_SERVER: u8 = 4;
11+
pub const MODE_IDF: u8 = 5;
1112

1213
pub enum NetIo {
1314
Stream(std::net::TcpStream),

src/css/style.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,31 @@ select:focus, input:focus, textarea:focus {
742742
opacity: 0.7;
743743
}
744744

745+
/* ===== ESP build/flash dialog ===== */
746+
.esp-log {
747+
height: 320px;
748+
overflow-y: auto;
749+
background: var(--bg);
750+
color: var(--fg);
751+
border: 1px solid var(--border);
752+
border-radius: 4px;
753+
padding: 8px 10px;
754+
font-family: Consolas, 'Courier New', monospace;
755+
font-size: 12px;
756+
line-height: 1.5;
757+
white-space: pre-wrap;
758+
word-break: break-all;
759+
user-select: text;
760+
-webkit-user-select: text;
761+
}
762+
.esp-log .esp-stage {
763+
color: var(--accent);
764+
font-weight: 600;
765+
}
766+
.esp-log .esp-err {
767+
color: var(--danger);
768+
}
769+
745770
/* ===== Status Bar ===== */
746771
#status-bar {
747772
display: flex;

src/i18n/en.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"tcpServer": "TCP Server",
5252
"udpClient": "UDP Client",
5353
"udpServer": "UDP Server",
54+
"idf": "IDF",
5455
"remoteIp": "Remote IP",
5556
"remotePort": "Remote port",
5657
"localPort": "Local port"
@@ -123,7 +124,21 @@
123124
"reconnectInterval": "Interval:",
124125
"mcp": "MCP Service:",
125126
"enable": "Enable",
126-
"port": "Port:"
127+
"port": "Port:",
128+
"espIdf": "ESP-IDF path:",
129+
"espPython": "Python path (optional):",
130+
"espBaud": "Flash baud rate:",
131+
"espDetect": "Auto detect"
132+
},
133+
"esp": {
134+
"buildTitle": "ESP-IDF Build + Flash",
135+
"flashMonitor": "Flash+Monitor",
136+
"needConfig": "Configure ESP-IDF path in Settings first",
137+
"needPort": "Select a serial port first",
138+
"running": "Flashing...",
139+
"cancelling": "Cancelling...",
140+
"done": "Done, chip reset & running",
141+
"failed": "Failed: {stage}"
127142
},
128143
"serialSettings": {
129144
"title": "Serial Port Settings",

src/i18n/zh-CN.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"tcpServer": "TCP Server",
5252
"udpClient": "UDP Client",
5353
"udpServer": "UDP Server",
54+
"idf": "IDF",
5455
"remoteIp": "对方IP",
5556
"remotePort": "对方端口",
5657
"localPort": "本地端口"
@@ -123,7 +124,21 @@
123124
"reconnectInterval": "间隔:",
124125
"mcp": "MCP 服务:",
125126
"enable": "启用",
126-
"port": "端口:"
127+
"port": "端口:",
128+
"espIdf": "ESP-IDF 路径:",
129+
"espPython": "Python 路径(可空):",
130+
"espBaud": "烧录波特率:",
131+
"espDetect": "自动探测"
132+
},
133+
"esp": {
134+
"buildTitle": "ESP-IDF Build + Flash",
135+
"flashMonitor": "Flash+Monitor",
136+
"needConfig": "请先在系统设置里配置 ESP-IDF 路径",
137+
"needPort": "请先选择串口",
138+
"running": "烧录中...",
139+
"cancelling": "正在取消...",
140+
"done": "完成, 芯片已复位运行",
141+
"failed": "失败: {stage}"
127142
},
128143
"serialSettings": {
129144
"title": "串口详细设置",

src/index.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,22 @@
260260
<span class="label" style="margin-left:12px" data-i18n="settings.port">端口:</span>
261261
<input type="number" id="setting-mcp-port" value="9876" min="1024" max="65535" class="small-input" style="width:80px" />
262262
</div>
263+
<div class="setting-row">
264+
<label data-i18n="settings.espIdf">ESP-IDF 路径:</label>
265+
<input type="text" id="setting-esp-idf" class="file-path-input" list="esp-idf-datalist" spellcheck="false" placeholder="D:\...\esp-idf" />
266+
<datalist id="esp-idf-datalist"></datalist>
267+
<button id="btn-esp-detect" class="small-btn" data-i18n="settings.espDetect">自动探测</button>
268+
</div>
269+
<div class="setting-row">
270+
<label data-i18n="settings.espPython">Python 路径(可空):</label>
271+
<input type="text" id="setting-esp-python" class="file-path-input" list="esp-python-datalist" spellcheck="false" placeholder="...python_env\...\python.exe" />
272+
<datalist id="esp-python-datalist"></datalist>
273+
</div>
274+
<div class="setting-row">
275+
<label data-i18n="settings.espBaud">烧录波特率:</label>
276+
<input type="number" id="setting-esp-baud" class="small-input" style="width:90px" value="921600" min="0" max="3000000" />
277+
<span class="label" style="margin:0 4px" data-i18n="settings.ms">bps</span>
278+
</div>
263279
</div>
264280
<div class="dialog-footer">
265281
<button id="setting-apply" class="small-btn primary" data-i18n="common.apply">应用</button>
@@ -336,6 +352,23 @@
336352
</div>
337353
</div>
338354

355+
<div id="esp-flash-overlay" class="overlay hidden">
356+
<div id="esp-flash-dialog" class="dialog" style="width:640px;">
357+
<div class="dialog-titlebar">
358+
<span data-i18n="esp.buildTitle">ESP-IDF Build + Flash</span>
359+
<button class="dialog-close title-btn" id="btn-esp-dialog-close"><i data-lucide="x"></i></button>
360+
</div>
361+
<div class="dialog-body" style="padding:12px;">
362+
<pre id="esp-log" class="esp-log"></pre>
363+
</div>
364+
<div class="dialog-footer">
365+
<span id="esp-flash-status" class="label"></span>
366+
<div style="flex:1"></div>
367+
<button id="btn-esp-cancel" class="small-btn" data-i18n="common.cancel">取消</button>
368+
</div>
369+
</div>
370+
</div>
371+
339372
<script type="module" src="js/utils.js"></script>
340373
<script type="module" src="js/titlebar.js"></script>
341374
<script type="module" src="js/menu.js"></script>

src/js/app.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ document.addEventListener('DOMContentLoaded', async () => {
8181
document.addEventListener('clear-receive', clearReceive);
8282
listen('clear-receive', clearReceive);
8383

84+
document.addEventListener('open-monitor', async () => {
85+
const s = await getSettings();
86+
setTerminalMode(true);
87+
applyTerminalUI(true);
88+
createTerminal(s);
89+
await patchSettings({ mode: 'terminal' });
90+
});
91+
8492
document.addEventListener('hex-display-change', (e) => {
8593
setHexDisplay(e.detail.on);
8694
});

0 commit comments

Comments
 (0)