Skip to content

Commit 267dfe5

Browse files
committed
fix: auto-erase flash before upload on ESP8266/ESP32 boards
1 parent 81e6dd4 commit 267dfe5

6 files changed

Lines changed: 290 additions & 43 deletions

File tree

src-tauri/src/commands/platformio.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,32 @@ pub async fn upload_firmware(
341341
port: Option<String>,
342342
app: tauri::AppHandle,
343343
) -> Result<BuildResult, String> {
344+
// First, read platformio.ini to detect ESP boards
345+
let pio_ini_path = std::path::Path::new(&project_path).join("platformio.ini");
346+
let pio_ini_content = std::fs::read_to_string(&pio_ini_path).unwrap_or_default();
347+
let is_esp_board = pio_ini_content.contains("esp8266") || pio_ini_content.contains("esp32")
348+
|| pio_ini_content.contains("espressif8266") || pio_ini_content.contains("espressif32");
349+
350+
// For ESP boards, erase flash before upload to prevent conflicts with old firmware
351+
if is_esp_board {
352+
let _ = app.emit("build-output", "[UPLOAD] Erasing flash before upload (ESP board detected)...".to_string());
353+
let erase_args = vec![
354+
"run".to_string(),
355+
"--target".to_string(),
356+
"erase".to_string(),
357+
"-d".to_string(),
358+
project_path.clone(),
359+
];
360+
let erase_result = run_platformio_command(state.clone(), erase_args, app.clone()).await;
361+
if let Ok(result) = erase_result {
362+
if !result.success {
363+
let _ = app.emit("build-output", format!("[UPLOAD] Flash erase failed, continuing with upload: {}", result.stderr));
364+
} else {
365+
let _ = app.emit("build-output", "[UPLOAD] Flash erased successfully".to_string());
366+
}
367+
}
368+
}
369+
344370
let mut args = vec![
345371
"run".to_string(),
346372
"--target".to_string(),
@@ -355,6 +381,22 @@ pub async fn upload_firmware(
355381
run_platformio_command(state, args, app).await
356382
}
357383

384+
#[command]
385+
pub async fn erase_flash(
386+
state: tauri::State<'_, BuildState>,
387+
project_path: String,
388+
app: tauri::AppHandle,
389+
) -> Result<BuildResult, String> {
390+
let args = vec![
391+
"run".to_string(),
392+
"--target".to_string(),
393+
"erase".to_string(),
394+
"-d".to_string(),
395+
project_path,
396+
];
397+
run_platformio_command(state, args, app).await
398+
}
399+
358400
#[command]
359401
pub fn parse_build_errors(output: String) -> Vec<serde_json::Value> {
360402
let mut errors = Vec::new();

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub fn run() {
6969
commands::pty_kill,
7070
commands::start_watch,
7171
commands::stop_watch,
72+
commands::erase_flash,
7273
])
7374
.run(tauri::generate_context!())
7475
.unwrap_or_else(|e| {

src/hooks/useAI.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ragEngine } from '../lib/rag';
77
import { buildPlanContext } from './usePlanContext';
88
import { getPromptConfig } from '../lib/prompts';
99
import { getDebugToolDefinitions, executeDebugTool, type DebugToolCall } from '../lib/debug-tools';
10+
import { getPlanToolDefinitions, executePlanTool } from '../lib/plan-tools';
1011
import type { AIMode } from '../lib/ai-prompts';
1112

1213
interface ToolCall {
@@ -157,8 +158,8 @@ export function useAI() {
157158
];
158159

159160
const customEndpoint = getActiveEndpoint();
160-
const useTools = mode === 'debug';
161-
const toolDefs = useTools ? getDebugToolDefinitions() : undefined;
161+
const useTools = mode === 'debug' || mode === 'plan';
162+
const toolDefs = useTools ? (mode === 'plan' ? getPlanToolDefinitions() : getDebugToolDefinitions()) : undefined;
162163

163164
let response: AIResponse;
164165

@@ -191,7 +192,7 @@ export function useAI() {
191192
const toolCallsToExecute = parsedToolCalls.length > 0 ? parsedToolCalls : (response.tool_calls || []);
192193

193194
if (toolCallsToExecute.length > 0) {
194-
const debugToolCalls: Array<{ id: string; name: string; args: string; output: string; success: boolean; elapsedMs?: number }> = [];
195+
const planToolCalls: Array<{ id: string; name: string; args: string; output: string; success: boolean; elapsedMs?: number }> = [];
195196

196197
for (const tc of toolCallsToExecute) {
197198
let args: Record<string, unknown> = {};
@@ -206,10 +207,12 @@ export function useAI() {
206207
}
207208

208209
const startTime = Date.now();
209-
const result = await executeDebugTool(tc.id, tc.name, args);
210+
const result = mode === 'plan'
211+
? await executePlanTool(tc.id, tc.name, args)
212+
: await executeDebugTool(tc.id, tc.name, args);
210213
const elapsedMs = Date.now() - startTime;
211214

212-
debugToolCalls.push({
215+
planToolCalls.push({
213216
id: tc.id,
214217
name: tc.name,
215218
args: typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments),
@@ -242,7 +245,7 @@ export function useAI() {
242245
response.content = followUpResponse.content;
243246
response.usage = followUpResponse.usage;
244247

245-
addMessage({ role: 'assistant', content: response.content, usage: response.usage, toolCalls: debugToolCalls });
248+
addMessage({ role: 'assistant', content: response.content, usage: response.usage, toolCalls: planToolCalls });
246249
return response;
247250
}
248251

src/lib/plan-tools.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { invoke } from '@tauri-apps/api/core';
2+
import { useFileStore } from '../stores/fileStore';
3+
import type { ToolDefinition, ToolResult } from './agent-tools';
4+
5+
export function getPlanToolDefinitions(): ToolDefinition[] {
6+
return [
7+
{
8+
type: 'function',
9+
function: {
10+
name: 'web_search',
11+
description: 'Search the internet for information about boards, sensors, libraries, tutorials, datasheets, pinout diagrams, and best practices.',
12+
parameters: {
13+
type: 'object',
14+
properties: {
15+
query: { type: 'string', description: 'The search query. Be specific: include board name, component model, and what you need to know.' },
16+
},
17+
required: ['query'],
18+
},
19+
},
20+
},
21+
{
22+
type: 'function',
23+
function: {
24+
name: 'read_file',
25+
description: 'Read the contents of any file in the project.',
26+
parameters: {
27+
type: 'object',
28+
properties: {
29+
path: { type: 'string', description: 'Absolute path to the file' },
30+
},
31+
required: ['path'],
32+
},
33+
},
34+
},
35+
{
36+
type: 'function',
37+
function: {
38+
name: 'list_directory',
39+
description: 'List files and folders in a directory.',
40+
parameters: {
41+
type: 'object',
42+
properties: {
43+
path: { type: 'string', description: 'Directory path to list' },
44+
},
45+
required: ['path'],
46+
},
47+
},
48+
},
49+
{
50+
type: 'function',
51+
function: {
52+
name: 'get_directory_tree',
53+
description: 'Get the full directory tree structure.',
54+
parameters: {
55+
type: 'object',
56+
properties: {
57+
path: { type: 'string', description: 'Root directory path' },
58+
depth: { type: 'number', description: 'Maximum depth (default 3)' },
59+
},
60+
required: ['path'],
61+
},
62+
},
63+
},
64+
{
65+
type: 'function',
66+
function: {
67+
name: 'search_code',
68+
description: 'Search for text patterns in source files.',
69+
parameters: {
70+
type: 'object',
71+
properties: {
72+
path: { type: 'string', description: 'Root path to search in' },
73+
pattern: { type: 'string', description: 'Text pattern to find' },
74+
filePattern: { type: 'string', description: 'Optional file glob pattern (e.g. "*.cpp")' },
75+
},
76+
required: ['path', 'pattern'],
77+
},
78+
},
79+
},
80+
];
81+
}
82+
83+
export async function executePlanTool(callId: string, name: string, args: Record<string, unknown>): Promise<ToolResult> {
84+
try {
85+
switch (name) {
86+
case 'web_search': {
87+
const results = await invoke<Array<{ title: string; url: string; snippet: string }>>('web_search', {
88+
query: args.query as string,
89+
});
90+
const output = results.map((r, i) => `${i + 1}. **${r.title}**\n URL: ${r.url}\n ${r.snippet}`).join('\n\n');
91+
return { callId, toolCallId: callId, success: true, output: output || 'No search results found.' };
92+
}
93+
94+
case 'read_file': {
95+
const root = useFileStore.getState().rootPath;
96+
const content = await invoke<string>('read_file', { path: args.path as string, root });
97+
return { callId, toolCallId: callId, success: true, output: content };
98+
}
99+
100+
case 'list_directory': {
101+
const root = useFileStore.getState().rootPath;
102+
const entries = await invoke<Array<{ name: string; path: string; is_dir: boolean; is_file: boolean }>>('list_directory', { path: args.path as string, root });
103+
const output = entries.map(e => `${e.is_dir ? '[DIR]' : '[FILE]'} ${e.name}`).join('\n');
104+
return { callId, toolCallId: callId, success: true, output: output || 'Directory is empty.' };
105+
}
106+
107+
case 'get_directory_tree': {
108+
const root = useFileStore.getState().rootPath;
109+
const tree = await invoke<{ name: string; path: string; is_dir: boolean; children: unknown[] }>('get_directory_tree', {
110+
path: args.path as string,
111+
depth: (args.depth as number) || 3,
112+
root,
113+
});
114+
const formatTree = (node: typeof tree, indent = 0): string => {
115+
const prefix = ' '.repeat(indent);
116+
let result = `${prefix}${node.is_dir ? '[DIR]' : '[FILE]'} ${node.name}\n`;
117+
if (node.children && Array.isArray(node.children)) {
118+
for (const child of node.children) {
119+
result += formatTree(child as typeof tree, indent + 1);
120+
}
121+
}
122+
return result;
123+
};
124+
return { callId, toolCallId: callId, success: true, output: formatTree(tree) };
125+
}
126+
127+
case 'search_code': {
128+
const results = await invoke<Array<{ path: string; line_number: number; content: string }>>('grep_search', {
129+
root_path: args.path as string,
130+
pattern: args.pattern as string,
131+
file_pattern: args.filePattern as string | null,
132+
max_results: 50,
133+
});
134+
const output = results.map(r => `${r.path}:${r.line_number}: ${r.content}`).join('\n');
135+
return { callId, toolCallId: callId, success: true, output: output || 'No matches found.' };
136+
}
137+
138+
default:
139+
return { callId, toolCallId: callId, success: false, output: `Unknown tool: ${name}` };
140+
}
141+
} catch (err) {
142+
const msg = err instanceof Error ? err.message : String(err);
143+
return { callId, toolCallId: callId, success: false, output: `Error: ${msg}` };
144+
}
145+
}

src/lib/prompts/modes/agent.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ You operate in a single-threaded loop:
109109
- If build fails: read the FULL error output, identify root cause, fix, rebuild
110110
- **Never** interleave write → build → write → build (too slow, wastes iterations)
111111

112+
### Phase 4: Flash (Clean Upload)
113+
- When uploading to ESP8266/ESP32 boards, the system AUTOMATICALLY erases flash before upload
114+
- This prevents conflicts with old firmware (WiFi configs, pin states, partition data)
115+
- You do NOT need to manually erase — just run the upload command
116+
- For Arduino boards, no erase is needed (they don't have persistent flash conflicts)
117+
- After upload, the board will boot with ONLY the new firmware — no old code remains
118+
112119
---
113120

114121
## Build Error Resolution

0 commit comments

Comments
 (0)