ruvllm-esp32 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +121 -29
- package/package.json +2 -1
- package/scripts/windows/build.ps1 +124 -0
- package/scripts/windows/env.ps1 +60 -0
- package/scripts/windows/flash.ps1 +99 -0
- package/scripts/windows/monitor.ps1 +41 -0
- package/scripts/windows/setup.ps1 +118 -0
- package/web-flasher/index.html +438 -0
package/bin/cli.js
CHANGED
|
@@ -94,13 +94,31 @@ function detectPort() {
|
|
|
94
94
|
|
|
95
95
|
try {
|
|
96
96
|
if (platform === 'win32') {
|
|
97
|
-
// Windows:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
// Windows: Use PowerShell for better COM port detection
|
|
98
|
+
try {
|
|
99
|
+
const result = execSync(
|
|
100
|
+
'powershell -Command "[System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object { [int]($_ -replace \'COM\', \'\') }"',
|
|
101
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
102
|
+
);
|
|
103
|
+
const ports = result.trim().split('\n').filter(p => p.match(/COM\d+/));
|
|
104
|
+
if (ports.length > 0) {
|
|
105
|
+
return ports[0].trim();
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Fallback to wmic
|
|
109
|
+
const result = execSync('wmic path Win32_SerialPort get DeviceID 2>nul', { encoding: 'utf8' });
|
|
110
|
+
const ports = result.split('\n').filter(line => line.includes('COM')).map(line => line.trim());
|
|
111
|
+
if (ports.length > 0) return ports[0];
|
|
112
|
+
}
|
|
113
|
+
return 'COM3';
|
|
101
114
|
} else if (platform === 'darwin') {
|
|
102
115
|
// macOS
|
|
103
|
-
const files = fs.readdirSync('/dev').filter(f =>
|
|
116
|
+
const files = fs.readdirSync('/dev').filter(f =>
|
|
117
|
+
f.startsWith('cu.usbserial') ||
|
|
118
|
+
f.startsWith('cu.SLAB') ||
|
|
119
|
+
f.startsWith('cu.wchusbserial') ||
|
|
120
|
+
f.startsWith('cu.usbmodem')
|
|
121
|
+
);
|
|
104
122
|
return files[0] ? `/dev/${files[0]}` : '/dev/cu.usbserial-0001';
|
|
105
123
|
} else {
|
|
106
124
|
// Linux
|
|
@@ -127,27 +145,52 @@ async function installToolchain() {
|
|
|
127
145
|
const { platform } = detectPlatform();
|
|
128
146
|
|
|
129
147
|
try {
|
|
130
|
-
// Install espup
|
|
131
|
-
logStep('Installing espup...');
|
|
132
148
|
if (platform === 'win32') {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
149
|
+
// Windows: Check if we have the PowerShell setup script
|
|
150
|
+
const scriptsDir = path.join(__dirname, '..', 'scripts', 'windows');
|
|
151
|
+
const setupScript = path.join(scriptsDir, 'setup.ps1');
|
|
137
152
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
if (fs.existsSync(setupScript)) {
|
|
154
|
+
logStep('Running Windows setup script...');
|
|
155
|
+
execSync(`powershell -ExecutionPolicy Bypass -File "${setupScript}"`, { stdio: 'inherit' });
|
|
156
|
+
} else {
|
|
157
|
+
// Fallback: manual installation
|
|
158
|
+
logStep('Installing espup...');
|
|
141
159
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
// Download espup for Windows
|
|
161
|
+
const espupUrl = 'https://github.com/esp-rs/espup/releases/latest/download/espup-x86_64-pc-windows-msvc.exe';
|
|
162
|
+
const espupPath = path.join(os.tmpdir(), 'espup.exe');
|
|
163
|
+
|
|
164
|
+
execSync(`powershell -Command "Invoke-WebRequest -Uri '${espupUrl}' -OutFile '${espupPath}'"`, { stdio: 'inherit' });
|
|
165
|
+
|
|
166
|
+
logStep('Running espup install...');
|
|
167
|
+
execSync(`"${espupPath}" install`, { stdio: 'inherit' });
|
|
168
|
+
|
|
169
|
+
// Install espflash
|
|
170
|
+
logStep('Installing espflash...');
|
|
171
|
+
execSync('cargo install espflash ldproxy', { stdio: 'inherit' });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
logSuccess('Toolchain installed successfully!');
|
|
175
|
+
log('\nTo use the toolchain, run:', 'yellow');
|
|
176
|
+
log(' . .\\scripts\\windows\\env.ps1', 'cyan');
|
|
145
177
|
|
|
146
|
-
logSuccess('Toolchain installed successfully!');
|
|
147
|
-
log('\nPlease restart your terminal or run:', 'yellow');
|
|
148
|
-
if (platform === 'win32') {
|
|
149
|
-
log(' $env:PATH = [System.Environment]::GetEnvironmentVariable("Path","User")', 'cyan');
|
|
150
178
|
} else {
|
|
179
|
+
// Linux/macOS
|
|
180
|
+
logStep('Installing espup...');
|
|
181
|
+
const arch = os.arch() === 'arm64' ? 'aarch64' : 'x86_64';
|
|
182
|
+
const binary = platform === 'darwin'
|
|
183
|
+
? `espup-${arch}-apple-darwin`
|
|
184
|
+
: `espup-${arch}-unknown-linux-gnu`;
|
|
185
|
+
|
|
186
|
+
execSync(`curl -L https://github.com/esp-rs/espup/releases/latest/download/${binary} -o /tmp/espup && chmod +x /tmp/espup && /tmp/espup install`, { stdio: 'inherit' });
|
|
187
|
+
|
|
188
|
+
// Install espflash
|
|
189
|
+
logStep('Installing espflash...');
|
|
190
|
+
execSync('cargo install espflash ldproxy', { stdio: 'inherit' });
|
|
191
|
+
|
|
192
|
+
logSuccess('Toolchain installed successfully!');
|
|
193
|
+
log('\nPlease restart your terminal or run:', 'yellow');
|
|
151
194
|
log(' source $HOME/export-esp.sh', 'cyan');
|
|
152
195
|
}
|
|
153
196
|
|
|
@@ -160,8 +203,9 @@ async function installToolchain() {
|
|
|
160
203
|
|
|
161
204
|
async function build(options = {}) {
|
|
162
205
|
const target = options.target || 'esp32';
|
|
163
|
-
const release = options.release
|
|
206
|
+
const release = options.release !== false; // Default to release
|
|
164
207
|
const features = options.features || '';
|
|
208
|
+
const { platform } = detectPlatform();
|
|
165
209
|
|
|
166
210
|
logStep(`Building for ${target}${release ? ' (release)' : ''}...`);
|
|
167
211
|
|
|
@@ -175,12 +219,33 @@ async function build(options = {}) {
|
|
|
175
219
|
|
|
176
220
|
const rustTarget = targetMap[target] || targetMap['esp32'];
|
|
177
221
|
|
|
178
|
-
let cmd = `cargo build --target ${rustTarget}`;
|
|
179
|
-
if (release) cmd += ' --release';
|
|
180
|
-
if (features) cmd += ` --features ${features}`;
|
|
181
|
-
|
|
182
222
|
try {
|
|
183
|
-
|
|
223
|
+
if (platform === 'win32') {
|
|
224
|
+
// Windows: Use PowerShell build script if available
|
|
225
|
+
const scriptsDir = path.join(__dirname, '..', 'scripts', 'windows');
|
|
226
|
+
const buildScript = path.join(scriptsDir, 'build.ps1');
|
|
227
|
+
|
|
228
|
+
if (fs.existsSync(buildScript)) {
|
|
229
|
+
let psArgs = `-ExecutionPolicy Bypass -File "${buildScript}" -Target "${rustTarget}"`;
|
|
230
|
+
if (release) psArgs += ' -Release';
|
|
231
|
+
if (features) psArgs += ` -Features "${features}"`;
|
|
232
|
+
|
|
233
|
+
execSync(`powershell ${psArgs}`, { stdio: 'inherit', cwd: process.cwd() });
|
|
234
|
+
} else {
|
|
235
|
+
// Fallback to direct cargo
|
|
236
|
+
let cmd = `cargo build --target ${rustTarget}`;
|
|
237
|
+
if (release) cmd += ' --release';
|
|
238
|
+
if (features) cmd += ` --features ${features}`;
|
|
239
|
+
execSync(cmd, { stdio: 'inherit', cwd: process.cwd() });
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// Linux/macOS
|
|
243
|
+
let cmd = `cargo build --target ${rustTarget}`;
|
|
244
|
+
if (release) cmd += ' --release';
|
|
245
|
+
if (features) cmd += ` --features ${features}`;
|
|
246
|
+
execSync(cmd, { stdio: 'inherit', cwd: process.cwd() });
|
|
247
|
+
}
|
|
248
|
+
|
|
184
249
|
logSuccess('Build completed!');
|
|
185
250
|
return true;
|
|
186
251
|
} catch (e) {
|
|
@@ -192,12 +257,39 @@ async function build(options = {}) {
|
|
|
192
257
|
async function flash(port, options = {}) {
|
|
193
258
|
const actualPort = port || detectPort();
|
|
194
259
|
const target = options.target || 'esp32';
|
|
260
|
+
const { platform } = detectPlatform();
|
|
195
261
|
|
|
196
262
|
logStep(`Flashing to ${actualPort}...`);
|
|
197
263
|
|
|
264
|
+
const targetMap = {
|
|
265
|
+
'esp32': 'xtensa-esp32-espidf',
|
|
266
|
+
'esp32s2': 'xtensa-esp32s2-espidf',
|
|
267
|
+
'esp32s3': 'xtensa-esp32s3-espidf',
|
|
268
|
+
'esp32c3': 'riscv32imc-esp-espidf',
|
|
269
|
+
'esp32c6': 'riscv32imac-esp-espidf'
|
|
270
|
+
};
|
|
271
|
+
const rustTarget = targetMap[target] || targetMap['esp32'];
|
|
272
|
+
|
|
198
273
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
274
|
+
if (platform === 'win32') {
|
|
275
|
+
// Windows: Use PowerShell flash script if available
|
|
276
|
+
const scriptsDir = path.join(__dirname, '..', 'scripts', 'windows');
|
|
277
|
+
const flashScript = path.join(scriptsDir, 'flash.ps1');
|
|
278
|
+
|
|
279
|
+
if (fs.existsSync(flashScript)) {
|
|
280
|
+
const psArgs = `-ExecutionPolicy Bypass -File "${flashScript}" -Port "${actualPort}" -Target "${rustTarget}"`;
|
|
281
|
+
execSync(`powershell ${psArgs}`, { stdio: 'inherit', cwd: process.cwd() });
|
|
282
|
+
} else {
|
|
283
|
+
// Fallback
|
|
284
|
+
const binary = `target\\${rustTarget}\\release\\ruvllm-esp32`;
|
|
285
|
+
execSync(`espflash flash --monitor --port ${actualPort} ${binary}`, { stdio: 'inherit' });
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
// Linux/macOS
|
|
289
|
+
const binary = `target/${rustTarget}/release/ruvllm-esp32`;
|
|
290
|
+
execSync(`espflash flash --monitor --port ${actualPort} ${binary}`, { stdio: 'inherit' });
|
|
291
|
+
}
|
|
292
|
+
|
|
201
293
|
logSuccess('Flash completed!');
|
|
202
294
|
return true;
|
|
203
295
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvllm-esp32",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "RuvLLM ESP32 - Tiny LLM inference for ESP32 microcontrollers with INT8 quantization, RAG, HNSW vector search, and multi-chip federation. Run AI on $4 hardware.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"binaries/",
|
|
44
44
|
"scripts/",
|
|
45
45
|
"templates/",
|
|
46
|
+
"web-flasher/",
|
|
46
47
|
"README.md"
|
|
47
48
|
],
|
|
48
49
|
"scripts": {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# build.ps1 - Auto-configure and build RuvLLM ESP32
|
|
2
|
+
# Automatically detects toolchain paths - no manual configuration needed
|
|
3
|
+
|
|
4
|
+
param(
|
|
5
|
+
[string]$Target = "xtensa-esp32-espidf",
|
|
6
|
+
[switch]$Release = $true,
|
|
7
|
+
[string]$Features = ""
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
$ErrorActionPreference = "Stop"
|
|
11
|
+
|
|
12
|
+
Write-Host "`n=== RuvLLM ESP32 Build ===" -ForegroundColor Cyan
|
|
13
|
+
Write-Host ""
|
|
14
|
+
|
|
15
|
+
# Auto-detect paths
|
|
16
|
+
$rustupHome = if ($env:RUSTUP_HOME) { $env:RUSTUP_HOME } else { "$env:USERPROFILE\.rustup" }
|
|
17
|
+
$cargoHome = if ($env:CARGO_HOME) { $env:CARGO_HOME } else { "$env:USERPROFILE\.cargo" }
|
|
18
|
+
|
|
19
|
+
# Find ESP toolchain
|
|
20
|
+
$espToolchain = (Get-ChildItem "$rustupHome\toolchains" -Directory -ErrorAction SilentlyContinue |
|
|
21
|
+
Where-Object { $_.Name -like "esp*" } |
|
|
22
|
+
Select-Object -First 1)
|
|
23
|
+
|
|
24
|
+
if (-not $espToolchain) {
|
|
25
|
+
Write-Error "ESP toolchain not found. Run .\setup.ps1 first"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$espToolchainPath = $espToolchain.FullName
|
|
29
|
+
|
|
30
|
+
# Find libclang dynamically
|
|
31
|
+
$libclang = Get-ChildItem "$espToolchainPath" -Recurse -Filter "libclang.dll" -ErrorAction SilentlyContinue |
|
|
32
|
+
Select-Object -First 1
|
|
33
|
+
|
|
34
|
+
if (-not $libclang) {
|
|
35
|
+
Write-Error "libclang.dll not found in $espToolchainPath"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Find Python
|
|
39
|
+
$python = Get-Command python -ErrorAction SilentlyContinue
|
|
40
|
+
if (-not $python) {
|
|
41
|
+
$python = Get-Command python3 -ErrorAction SilentlyContinue
|
|
42
|
+
}
|
|
43
|
+
if (-not $python) {
|
|
44
|
+
Write-Error "Python not found. Please install Python 3.8+"
|
|
45
|
+
}
|
|
46
|
+
$pythonPath = Split-Path $python.Source
|
|
47
|
+
|
|
48
|
+
# Find clang and xtensa-esp-elf paths
|
|
49
|
+
$clangBin = Get-ChildItem "$espToolchainPath" -Recurse -Directory -Filter "esp-clang" -ErrorAction SilentlyContinue |
|
|
50
|
+
Select-Object -First 1
|
|
51
|
+
$clangBinPath = if ($clangBin) { "$($clangBin.FullName)\bin" } else { "" }
|
|
52
|
+
|
|
53
|
+
$xtensaBin = Get-ChildItem "$espToolchainPath" -Recurse -Directory -Filter "xtensa-esp-elf" -ErrorAction SilentlyContinue |
|
|
54
|
+
Select-Object -First 1
|
|
55
|
+
$xtensaBinPath = if ($xtensaBin) { "$($xtensaBin.FullName)\bin" } else { "" }
|
|
56
|
+
|
|
57
|
+
# Set environment variables
|
|
58
|
+
$env:LIBCLANG_PATH = Split-Path $libclang.FullName
|
|
59
|
+
$env:RUSTUP_TOOLCHAIN = "esp"
|
|
60
|
+
$env:ESP_IDF_VERSION = "v5.1.2"
|
|
61
|
+
|
|
62
|
+
# Build PATH with all required directories
|
|
63
|
+
$pathParts = @(
|
|
64
|
+
$pythonPath,
|
|
65
|
+
"$pythonPath\Scripts",
|
|
66
|
+
$clangBinPath,
|
|
67
|
+
$xtensaBinPath,
|
|
68
|
+
"$cargoHome\bin"
|
|
69
|
+
) | Where-Object { $_ -ne "" }
|
|
70
|
+
|
|
71
|
+
$env:PATH = ($pathParts -join ";") + ";" + $env:PATH
|
|
72
|
+
|
|
73
|
+
Write-Host "Build Configuration:" -ForegroundColor Gray
|
|
74
|
+
Write-Host " Target: $Target"
|
|
75
|
+
Write-Host " Release: $Release"
|
|
76
|
+
Write-Host " Toolchain: $($espToolchain.Name)"
|
|
77
|
+
Write-Host " LIBCLANG_PATH: $($env:LIBCLANG_PATH)"
|
|
78
|
+
Write-Host ""
|
|
79
|
+
|
|
80
|
+
# Navigate to project directory
|
|
81
|
+
$projectDir = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
|
82
|
+
Push-Location $projectDir
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
# Build cargo command
|
|
86
|
+
$cargoArgs = @("build")
|
|
87
|
+
|
|
88
|
+
if ($Release) {
|
|
89
|
+
$cargoArgs += "--release"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if ($Features) {
|
|
93
|
+
$cargoArgs += "--features"
|
|
94
|
+
$cargoArgs += $Features
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Write-Host "Running: cargo $($cargoArgs -join ' ')" -ForegroundColor Gray
|
|
98
|
+
Write-Host ""
|
|
99
|
+
|
|
100
|
+
& cargo @cargoArgs
|
|
101
|
+
|
|
102
|
+
if ($LASTEXITCODE -ne 0) {
|
|
103
|
+
throw "Build failed with exit code $LASTEXITCODE"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Write-Host ""
|
|
107
|
+
Write-Host "Build successful!" -ForegroundColor Green
|
|
108
|
+
|
|
109
|
+
# Find the built binary
|
|
110
|
+
$buildDir = if ($Release) { "release" } else { "debug" }
|
|
111
|
+
$binary = Get-ChildItem "$projectDir\target\$Target\$buildDir" -Filter "*.elf" -ErrorAction SilentlyContinue |
|
|
112
|
+
Where-Object { $_.Name -notmatch "deps" } |
|
|
113
|
+
Select-Object -First 1
|
|
114
|
+
|
|
115
|
+
if ($binary) {
|
|
116
|
+
Write-Host "Binary: $($binary.FullName)" -ForegroundColor Cyan
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Write-Host ""
|
|
120
|
+
Write-Host "Next: Run .\flash.ps1 to flash to device" -ForegroundColor Yellow
|
|
121
|
+
|
|
122
|
+
} finally {
|
|
123
|
+
Pop-Location
|
|
124
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# env.ps1 - Set up ESP32 Rust environment for the current session
|
|
2
|
+
# Source this script: . .\env.ps1
|
|
3
|
+
|
|
4
|
+
$ErrorActionPreference = "SilentlyContinue"
|
|
5
|
+
|
|
6
|
+
# Find paths
|
|
7
|
+
$rustupHome = if ($env:RUSTUP_HOME) { $env:RUSTUP_HOME } else { "$env:USERPROFILE\.rustup" }
|
|
8
|
+
$cargoHome = if ($env:CARGO_HOME) { $env:CARGO_HOME } else { "$env:USERPROFILE\.cargo" }
|
|
9
|
+
|
|
10
|
+
# Find ESP toolchain
|
|
11
|
+
$espToolchain = (Get-ChildItem "$rustupHome\toolchains" -Directory |
|
|
12
|
+
Where-Object { $_.Name -like "esp*" } |
|
|
13
|
+
Select-Object -First 1)
|
|
14
|
+
|
|
15
|
+
if (-not $espToolchain) {
|
|
16
|
+
Write-Host "ESP toolchain not found. Run setup.ps1 first." -ForegroundColor Red
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
$espToolchainPath = $espToolchain.FullName
|
|
21
|
+
|
|
22
|
+
# Find libclang
|
|
23
|
+
$libclang = Get-ChildItem "$espToolchainPath" -Recurse -Filter "libclang.dll" |
|
|
24
|
+
Select-Object -First 1
|
|
25
|
+
|
|
26
|
+
# Find clang bin
|
|
27
|
+
$clangBin = Get-ChildItem "$espToolchainPath" -Recurse -Directory -Filter "esp-clang" |
|
|
28
|
+
Select-Object -First 1
|
|
29
|
+
|
|
30
|
+
# Find xtensa-esp-elf bin
|
|
31
|
+
$xtensaBin = Get-ChildItem "$espToolchainPath" -Recurse -Directory -Filter "xtensa-esp-elf" |
|
|
32
|
+
Select-Object -First 1
|
|
33
|
+
|
|
34
|
+
# Find Python
|
|
35
|
+
$python = Get-Command python -ErrorAction SilentlyContinue
|
|
36
|
+
$pythonPath = if ($python) { Split-Path $python.Source } else { "" }
|
|
37
|
+
|
|
38
|
+
# Set environment variables
|
|
39
|
+
$env:LIBCLANG_PATH = if ($libclang) { Split-Path $libclang.FullName } else { "" }
|
|
40
|
+
$env:RUSTUP_TOOLCHAIN = "esp"
|
|
41
|
+
$env:ESP_IDF_VERSION = "v5.1.2"
|
|
42
|
+
|
|
43
|
+
# Build PATH
|
|
44
|
+
$pathAdditions = @()
|
|
45
|
+
if ($pythonPath) { $pathAdditions += $pythonPath; $pathAdditions += "$pythonPath\Scripts" }
|
|
46
|
+
if ($clangBin) { $pathAdditions += "$($clangBin.FullName)\bin" }
|
|
47
|
+
if ($xtensaBin) { $pathAdditions += "$($xtensaBin.FullName)\bin" }
|
|
48
|
+
$pathAdditions += "$cargoHome\bin"
|
|
49
|
+
|
|
50
|
+
$env:PATH = ($pathAdditions -join ";") + ";" + $env:PATH
|
|
51
|
+
|
|
52
|
+
# Display status
|
|
53
|
+
Write-Host ""
|
|
54
|
+
Write-Host "ESP32 Rust environment loaded" -ForegroundColor Green
|
|
55
|
+
Write-Host ""
|
|
56
|
+
Write-Host " RUSTUP_TOOLCHAIN: $($env:RUSTUP_TOOLCHAIN)" -ForegroundColor Gray
|
|
57
|
+
Write-Host " LIBCLANG_PATH: $($env:LIBCLANG_PATH)" -ForegroundColor Gray
|
|
58
|
+
Write-Host " ESP_IDF_VERSION: $($env:ESP_IDF_VERSION)" -ForegroundColor Gray
|
|
59
|
+
Write-Host ""
|
|
60
|
+
Write-Host "Ready to build! Run: .\build.ps1" -ForegroundColor Cyan
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# flash.ps1 - Auto-detect COM port and flash RuvLLM ESP32
|
|
2
|
+
# Automatically finds connected ESP32 devices
|
|
3
|
+
|
|
4
|
+
param(
|
|
5
|
+
[string]$Port = "",
|
|
6
|
+
[switch]$Monitor = $true,
|
|
7
|
+
[string]$Target = "xtensa-esp32-espidf",
|
|
8
|
+
[switch]$Release = $true
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
$ErrorActionPreference = "Stop"
|
|
12
|
+
|
|
13
|
+
Write-Host "`n=== RuvLLM ESP32 Flash ===" -ForegroundColor Cyan
|
|
14
|
+
Write-Host ""
|
|
15
|
+
|
|
16
|
+
# Auto-detect COM port if not specified
|
|
17
|
+
if (-not $Port) {
|
|
18
|
+
# Get available COM ports
|
|
19
|
+
Add-Type -AssemblyName System.IO.Ports
|
|
20
|
+
$ports = [System.IO.Ports.SerialPort]::GetPortNames() |
|
|
21
|
+
Where-Object { $_ -match "COM\d+" } |
|
|
22
|
+
Sort-Object { [int]($_ -replace "COM", "") }
|
|
23
|
+
|
|
24
|
+
if ($ports.Count -eq 0) {
|
|
25
|
+
Write-Error "No COM ports found. Is the ESP32 connected via USB?"
|
|
26
|
+
} elseif ($ports.Count -eq 1) {
|
|
27
|
+
$Port = $ports[0]
|
|
28
|
+
Write-Host "Auto-detected port: $Port" -ForegroundColor Green
|
|
29
|
+
} else {
|
|
30
|
+
Write-Host "Multiple COM ports found:" -ForegroundColor Yellow
|
|
31
|
+
Write-Host ""
|
|
32
|
+
for ($i = 0; $i -lt $ports.Count; $i++) {
|
|
33
|
+
Write-Host " [$i] $($ports[$i])"
|
|
34
|
+
}
|
|
35
|
+
Write-Host ""
|
|
36
|
+
$selection = Read-Host "Select port (0-$($ports.Count - 1))"
|
|
37
|
+
|
|
38
|
+
if ($selection -match "^\d+$" -and [int]$selection -lt $ports.Count) {
|
|
39
|
+
$Port = $ports[[int]$selection]
|
|
40
|
+
} else {
|
|
41
|
+
Write-Error "Invalid selection"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Write-Host "Using port: $Port" -ForegroundColor Cyan
|
|
47
|
+
Write-Host ""
|
|
48
|
+
|
|
49
|
+
# Find binary
|
|
50
|
+
$projectDir = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
|
51
|
+
$buildDir = if ($Release) { "release" } else { "debug" }
|
|
52
|
+
$targetDir = "$projectDir\target\$Target\$buildDir"
|
|
53
|
+
|
|
54
|
+
# Look for ELF or binary file
|
|
55
|
+
$binary = Get-ChildItem $targetDir -Filter "*.elf" -ErrorAction SilentlyContinue |
|
|
56
|
+
Where-Object { $_.Name -notmatch "deps" } |
|
|
57
|
+
Select-Object -First 1
|
|
58
|
+
|
|
59
|
+
if (-not $binary) {
|
|
60
|
+
$binary = Get-ChildItem $targetDir -Filter "ruvllm-esp32*" -ErrorAction SilentlyContinue |
|
|
61
|
+
Where-Object { $_.Name -notmatch "\." -or $_.Name -match "\.elf$" } |
|
|
62
|
+
Select-Object -First 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (-not $binary) {
|
|
66
|
+
Write-Host "Available files in $targetDir`:" -ForegroundColor Yellow
|
|
67
|
+
Get-ChildItem $targetDir -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $($_.Name)" }
|
|
68
|
+
Write-Error "No binary found. Run .\build.ps1 first"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Write-Host "Binary: $($binary.Name)" -ForegroundColor Gray
|
|
72
|
+
Write-Host ""
|
|
73
|
+
|
|
74
|
+
# Check for espflash
|
|
75
|
+
$espflash = Get-Command espflash -ErrorAction SilentlyContinue
|
|
76
|
+
if (-not $espflash) {
|
|
77
|
+
Write-Error "espflash not found. Run .\setup.ps1 first"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Build espflash command
|
|
81
|
+
$espflashArgs = @("flash", "--port", $Port, $binary.FullName)
|
|
82
|
+
|
|
83
|
+
if ($Monitor) {
|
|
84
|
+
$espflashArgs += "--monitor"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Write-Host "Flashing..." -ForegroundColor Cyan
|
|
88
|
+
Write-Host "Command: espflash $($espflashArgs -join ' ')" -ForegroundColor Gray
|
|
89
|
+
Write-Host ""
|
|
90
|
+
|
|
91
|
+
# Flash the device
|
|
92
|
+
& espflash @espflashArgs
|
|
93
|
+
|
|
94
|
+
if ($LASTEXITCODE -ne 0) {
|
|
95
|
+
Write-Error "Flash failed with exit code $LASTEXITCODE"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Write-Host ""
|
|
99
|
+
Write-Host "Flash complete!" -ForegroundColor Green
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# monitor.ps1 - Open serial monitor for ESP32
|
|
2
|
+
# Auto-detects COM port
|
|
3
|
+
|
|
4
|
+
param(
|
|
5
|
+
[string]$Port = "",
|
|
6
|
+
[int]$Baud = 115200
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
$ErrorActionPreference = "Stop"
|
|
10
|
+
|
|
11
|
+
Write-Host "`n=== RuvLLM ESP32 Serial Monitor ===" -ForegroundColor Cyan
|
|
12
|
+
Write-Host ""
|
|
13
|
+
|
|
14
|
+
# Auto-detect COM port if not specified
|
|
15
|
+
if (-not $Port) {
|
|
16
|
+
Add-Type -AssemblyName System.IO.Ports
|
|
17
|
+
$ports = [System.IO.Ports.SerialPort]::GetPortNames() |
|
|
18
|
+
Where-Object { $_ -match "COM\d+" } |
|
|
19
|
+
Sort-Object { [int]($_ -replace "COM", "") }
|
|
20
|
+
|
|
21
|
+
if ($ports.Count -eq 0) {
|
|
22
|
+
Write-Error "No COM ports found. Is the ESP32 connected?"
|
|
23
|
+
} elseif ($ports.Count -eq 1) {
|
|
24
|
+
$Port = $ports[0]
|
|
25
|
+
Write-Host "Auto-detected port: $Port" -ForegroundColor Green
|
|
26
|
+
} else {
|
|
27
|
+
Write-Host "Multiple COM ports found:" -ForegroundColor Yellow
|
|
28
|
+
for ($i = 0; $i -lt $ports.Count; $i++) {
|
|
29
|
+
Write-Host " [$i] $($ports[$i])"
|
|
30
|
+
}
|
|
31
|
+
$selection = Read-Host "Select port (0-$($ports.Count - 1))"
|
|
32
|
+
$Port = $ports[[int]$selection]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Write-Host "Opening monitor on $Port at $Baud baud..." -ForegroundColor Cyan
|
|
37
|
+
Write-Host "Press Ctrl+C to exit" -ForegroundColor Gray
|
|
38
|
+
Write-Host ""
|
|
39
|
+
|
|
40
|
+
# Use espflash monitor
|
|
41
|
+
& espflash monitor --port $Port --baud $Baud
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# setup.ps1 - One-time Windows setup for RuvLLM ESP32
|
|
2
|
+
# Run this once to install/configure the ESP32 Rust toolchain
|
|
3
|
+
|
|
4
|
+
$ErrorActionPreference = "Stop"
|
|
5
|
+
|
|
6
|
+
Write-Host "`n=== RuvLLM ESP32 Windows Setup ===" -ForegroundColor Cyan
|
|
7
|
+
Write-Host ""
|
|
8
|
+
|
|
9
|
+
# Find Rust ESP toolchain dynamically
|
|
10
|
+
$rustupHome = if ($env:RUSTUP_HOME) { $env:RUSTUP_HOME } else { "$env:USERPROFILE\.rustup" }
|
|
11
|
+
$cargoHome = if ($env:CARGO_HOME) { $env:CARGO_HOME } else { "$env:USERPROFILE\.cargo" }
|
|
12
|
+
|
|
13
|
+
# Check if Rust is installed
|
|
14
|
+
$rustc = Get-Command rustc -ErrorAction SilentlyContinue
|
|
15
|
+
if (-not $rustc) {
|
|
16
|
+
Write-Host "Rust not found. Installing rustup..." -ForegroundColor Yellow
|
|
17
|
+
Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile rustup-init.exe
|
|
18
|
+
.\rustup-init.exe -y --default-toolchain stable
|
|
19
|
+
Remove-Item rustup-init.exe
|
|
20
|
+
$env:PATH = "$cargoHome\bin;" + $env:PATH
|
|
21
|
+
Write-Host "Rust installed successfully" -ForegroundColor Green
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Find or install ESP toolchain
|
|
25
|
+
$espToolchain = Get-ChildItem "$rustupHome\toolchains" -Directory -ErrorAction SilentlyContinue |
|
|
26
|
+
Where-Object { $_.Name -like "esp*" } |
|
|
27
|
+
Select-Object -First 1
|
|
28
|
+
|
|
29
|
+
if (-not $espToolchain) {
|
|
30
|
+
Write-Host "ESP toolchain not found. Installing espup..." -ForegroundColor Yellow
|
|
31
|
+
|
|
32
|
+
# Download espup
|
|
33
|
+
$espupUrl = "https://github.com/esp-rs/espup/releases/latest/download/espup-x86_64-pc-windows-msvc.exe"
|
|
34
|
+
$espupPath = "$env:TEMP\espup.exe"
|
|
35
|
+
|
|
36
|
+
Write-Host "Downloading espup..." -ForegroundColor Gray
|
|
37
|
+
Invoke-WebRequest -Uri $espupUrl -OutFile $espupPath
|
|
38
|
+
|
|
39
|
+
Write-Host "Running espup install (this may take several minutes)..." -ForegroundColor Gray
|
|
40
|
+
& $espupPath install
|
|
41
|
+
|
|
42
|
+
if ($LASTEXITCODE -ne 0) {
|
|
43
|
+
Write-Error "espup install failed with exit code $LASTEXITCODE"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Remove-Item $espupPath -ErrorAction SilentlyContinue
|
|
47
|
+
|
|
48
|
+
# Re-check for toolchain
|
|
49
|
+
$espToolchain = Get-ChildItem "$rustupHome\toolchains" -Directory |
|
|
50
|
+
Where-Object { $_.Name -like "esp*" } |
|
|
51
|
+
Select-Object -First 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (-not $espToolchain) {
|
|
55
|
+
Write-Error "ESP toolchain installation failed. Please install manually: https://esp-rs.github.io/book/"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Write-Host "Found ESP toolchain: $($espToolchain.Name)" -ForegroundColor Green
|
|
59
|
+
|
|
60
|
+
# Find Python
|
|
61
|
+
$python = Get-Command python -ErrorAction SilentlyContinue
|
|
62
|
+
if (-not $python) {
|
|
63
|
+
$python = Get-Command python3 -ErrorAction SilentlyContinue
|
|
64
|
+
}
|
|
65
|
+
if (-not $python) {
|
|
66
|
+
Write-Error "Python not found. Please install Python 3.8+ from https://python.org"
|
|
67
|
+
}
|
|
68
|
+
Write-Host "Found Python: $($python.Source)" -ForegroundColor Green
|
|
69
|
+
|
|
70
|
+
# Find libclang
|
|
71
|
+
$libclang = Get-ChildItem "$($espToolchain.FullName)" -Recurse -Filter "libclang.dll" -ErrorAction SilentlyContinue |
|
|
72
|
+
Select-Object -First 1
|
|
73
|
+
|
|
74
|
+
if ($libclang) {
|
|
75
|
+
Write-Host "Found libclang: $($libclang.FullName)" -ForegroundColor Green
|
|
76
|
+
} else {
|
|
77
|
+
Write-Host "Warning: libclang.dll not found in toolchain" -ForegroundColor Yellow
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Install espflash if not present
|
|
81
|
+
$espflash = Get-Command espflash -ErrorAction SilentlyContinue
|
|
82
|
+
if (-not $espflash) {
|
|
83
|
+
Write-Host "Installing espflash..." -ForegroundColor Yellow
|
|
84
|
+
cargo install espflash
|
|
85
|
+
if ($LASTEXITCODE -ne 0) {
|
|
86
|
+
Write-Error "espflash installation failed"
|
|
87
|
+
}
|
|
88
|
+
Write-Host "espflash installed successfully" -ForegroundColor Green
|
|
89
|
+
} else {
|
|
90
|
+
Write-Host "Found espflash: $($espflash.Source)" -ForegroundColor Green
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Install ldproxy if not present
|
|
94
|
+
$ldproxy = Get-Command ldproxy -ErrorAction SilentlyContinue
|
|
95
|
+
if (-not $ldproxy) {
|
|
96
|
+
Write-Host "Installing ldproxy..." -ForegroundColor Yellow
|
|
97
|
+
cargo install ldproxy
|
|
98
|
+
if ($LASTEXITCODE -ne 0) {
|
|
99
|
+
Write-Error "ldproxy installation failed"
|
|
100
|
+
}
|
|
101
|
+
Write-Host "ldproxy installed successfully" -ForegroundColor Green
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Write-Host ""
|
|
105
|
+
Write-Host "=== Setup Complete ===" -ForegroundColor Green
|
|
106
|
+
Write-Host ""
|
|
107
|
+
Write-Host "Summary:" -ForegroundColor Cyan
|
|
108
|
+
Write-Host " Toolchain: $($espToolchain.Name)"
|
|
109
|
+
Write-Host " Python: $($python.Source)"
|
|
110
|
+
if ($libclang) {
|
|
111
|
+
Write-Host " Libclang: $($libclang.FullName)"
|
|
112
|
+
}
|
|
113
|
+
Write-Host ""
|
|
114
|
+
Write-Host "Next steps:" -ForegroundColor Yellow
|
|
115
|
+
Write-Host " 1. Run: .\build.ps1"
|
|
116
|
+
Write-Host " 2. Connect ESP32 via USB"
|
|
117
|
+
Write-Host " 3. Run: .\flash.ps1"
|
|
118
|
+
Write-Host ""
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>RuvLLM ESP32 Web Flasher</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0d1117;
|
|
10
|
+
--card: #161b22;
|
|
11
|
+
--border: #30363d;
|
|
12
|
+
--text: #c9d1d9;
|
|
13
|
+
--text-muted: #8b949e;
|
|
14
|
+
--accent: #58a6ff;
|
|
15
|
+
--success: #3fb950;
|
|
16
|
+
--warning: #d29922;
|
|
17
|
+
--error: #f85149;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
padding: 2rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.container {
|
|
35
|
+
max-width: 800px;
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
h1 {
|
|
40
|
+
text-align: center;
|
|
41
|
+
margin-bottom: 0.5rem;
|
|
42
|
+
color: var(--accent);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.subtitle {
|
|
46
|
+
text-align: center;
|
|
47
|
+
color: var(--text-muted);
|
|
48
|
+
margin-bottom: 2rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.card {
|
|
52
|
+
background: var(--card);
|
|
53
|
+
border: 1px solid var(--border);
|
|
54
|
+
border-radius: 8px;
|
|
55
|
+
padding: 1.5rem;
|
|
56
|
+
margin-bottom: 1.5rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.card h2 {
|
|
60
|
+
font-size: 1.1rem;
|
|
61
|
+
margin-bottom: 1rem;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 0.5rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.step-number {
|
|
68
|
+
background: var(--accent);
|
|
69
|
+
color: var(--bg);
|
|
70
|
+
width: 24px;
|
|
71
|
+
height: 24px;
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
font-size: 0.8rem;
|
|
77
|
+
font-weight: bold;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
select, button {
|
|
81
|
+
width: 100%;
|
|
82
|
+
padding: 0.75rem 1rem;
|
|
83
|
+
border-radius: 6px;
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
background: var(--bg);
|
|
86
|
+
color: var(--text);
|
|
87
|
+
font-size: 1rem;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
margin-bottom: 0.5rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
select:hover, button:hover {
|
|
93
|
+
border-color: var(--accent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
button.primary {
|
|
97
|
+
background: var(--accent);
|
|
98
|
+
color: var(--bg);
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
border: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
button.primary:hover {
|
|
104
|
+
opacity: 0.9;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
button.primary:disabled {
|
|
108
|
+
opacity: 0.5;
|
|
109
|
+
cursor: not-allowed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.progress {
|
|
113
|
+
background: var(--bg);
|
|
114
|
+
border-radius: 4px;
|
|
115
|
+
height: 8px;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
margin: 1rem 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.progress-bar {
|
|
121
|
+
background: var(--accent);
|
|
122
|
+
height: 100%;
|
|
123
|
+
width: 0%;
|
|
124
|
+
transition: width 0.3s ease;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.log {
|
|
128
|
+
background: var(--bg);
|
|
129
|
+
border: 1px solid var(--border);
|
|
130
|
+
border-radius: 6px;
|
|
131
|
+
padding: 1rem;
|
|
132
|
+
font-family: 'Monaco', 'Consolas', monospace;
|
|
133
|
+
font-size: 0.85rem;
|
|
134
|
+
max-height: 300px;
|
|
135
|
+
overflow-y: auto;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.log-entry {
|
|
139
|
+
margin-bottom: 0.25rem;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.log-entry.success { color: var(--success); }
|
|
143
|
+
.log-entry.warning { color: var(--warning); }
|
|
144
|
+
.log-entry.error { color: var(--error); }
|
|
145
|
+
.log-entry.info { color: var(--accent); }
|
|
146
|
+
|
|
147
|
+
.status {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: 0.5rem;
|
|
151
|
+
padding: 0.5rem;
|
|
152
|
+
border-radius: 4px;
|
|
153
|
+
margin-bottom: 1rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.status.connected {
|
|
157
|
+
background: rgba(63, 185, 80, 0.1);
|
|
158
|
+
color: var(--success);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.status.disconnected {
|
|
162
|
+
background: rgba(248, 81, 73, 0.1);
|
|
163
|
+
color: var(--error);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.features {
|
|
167
|
+
display: grid;
|
|
168
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
169
|
+
gap: 1rem;
|
|
170
|
+
margin-top: 1rem;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.feature {
|
|
174
|
+
background: var(--bg);
|
|
175
|
+
padding: 0.75rem;
|
|
176
|
+
border-radius: 4px;
|
|
177
|
+
font-size: 0.9rem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.feature strong {
|
|
181
|
+
color: var(--accent);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.warning-box {
|
|
185
|
+
background: rgba(210, 153, 34, 0.1);
|
|
186
|
+
border: 1px solid var(--warning);
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
padding: 1rem;
|
|
189
|
+
margin-bottom: 1rem;
|
|
190
|
+
color: var(--warning);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#browser-check {
|
|
194
|
+
display: none;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#browser-check.show {
|
|
198
|
+
display: block;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
footer {
|
|
202
|
+
text-align: center;
|
|
203
|
+
margin-top: 2rem;
|
|
204
|
+
color: var(--text-muted);
|
|
205
|
+
font-size: 0.9rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
footer a {
|
|
209
|
+
color: var(--accent);
|
|
210
|
+
text-decoration: none;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
213
|
+
</head>
|
|
214
|
+
<body>
|
|
215
|
+
<div class="container">
|
|
216
|
+
<h1>⚡ RuvLLM ESP32 Web Flasher</h1>
|
|
217
|
+
<p class="subtitle">Flash AI firmware directly from your browser - no installation required</p>
|
|
218
|
+
|
|
219
|
+
<div id="browser-check" class="warning-box">
|
|
220
|
+
⚠️ Web Serial API not supported. Please use Chrome, Edge, or Opera.
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<!-- Step 1: Select Target -->
|
|
224
|
+
<div class="card">
|
|
225
|
+
<h2><span class="step-number">1</span> Select ESP32 Variant</h2>
|
|
226
|
+
<select id="target-select">
|
|
227
|
+
<option value="esp32">ESP32 (Xtensa LX6, 520KB SRAM)</option>
|
|
228
|
+
<option value="esp32s2">ESP32-S2 (Xtensa LX7, USB OTG)</option>
|
|
229
|
+
<option value="esp32s3" selected>ESP32-S3 (Recommended - SIMD acceleration)</option>
|
|
230
|
+
<option value="esp32c3">ESP32-C3 (RISC-V, low power)</option>
|
|
231
|
+
<option value="esp32c6">ESP32-C6 (RISC-V, WiFi 6)</option>
|
|
232
|
+
<option value="esp32s3-federation">ESP32-S3 + Federation (multi-chip)</option>
|
|
233
|
+
</select>
|
|
234
|
+
|
|
235
|
+
<div class="features" id="features-display">
|
|
236
|
+
<div class="feature"><strong>INT8</strong> Quantized inference</div>
|
|
237
|
+
<div class="feature"><strong>HNSW</strong> Vector search</div>
|
|
238
|
+
<div class="feature"><strong>RAG</strong> Retrieval augmented</div>
|
|
239
|
+
<div class="feature"><strong>SIMD</strong> Hardware acceleration</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<!-- Step 2: Connect -->
|
|
244
|
+
<div class="card">
|
|
245
|
+
<h2><span class="step-number">2</span> Connect Device</h2>
|
|
246
|
+
<div class="status disconnected" id="connection-status">
|
|
247
|
+
○ Not connected
|
|
248
|
+
</div>
|
|
249
|
+
<button id="connect-btn" class="primary">Connect ESP32</button>
|
|
250
|
+
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
|
|
251
|
+
Hold BOOT button while clicking connect if device doesn't appear
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- Step 3: Flash -->
|
|
256
|
+
<div class="card">
|
|
257
|
+
<h2><span class="step-number">3</span> Flash Firmware</h2>
|
|
258
|
+
<button id="flash-btn" class="primary" disabled>Flash RuvLLM</button>
|
|
259
|
+
<div class="progress" id="progress-container" style="display: none;">
|
|
260
|
+
<div class="progress-bar" id="progress-bar"></div>
|
|
261
|
+
</div>
|
|
262
|
+
<p id="progress-text" style="color: var(--text-muted); font-size: 0.85rem; text-align: center;"></p>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<!-- Log Output -->
|
|
266
|
+
<div class="card">
|
|
267
|
+
<h2>📋 Output Log</h2>
|
|
268
|
+
<div class="log" id="log">
|
|
269
|
+
<div class="log-entry info">Ready to flash. Select target and connect device.</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<footer>
|
|
274
|
+
<p>
|
|
275
|
+
<a href="https://github.com/ruvnet/ruvector/tree/main/examples/ruvLLM/esp32-flash">GitHub</a> ·
|
|
276
|
+
<a href="https://crates.io/crates/ruvllm-esp32">Crates.io</a> ·
|
|
277
|
+
<a href="https://www.npmjs.com/package/ruvllm-esp32">npm</a>
|
|
278
|
+
</p>
|
|
279
|
+
<p style="margin-top: 0.5rem;">RuvLLM ESP32 - Tiny LLM Inference for Microcontrollers</p>
|
|
280
|
+
</footer>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<script type="module">
|
|
284
|
+
// ESP Web Serial Flasher
|
|
285
|
+
// Uses esptool.js for actual flashing
|
|
286
|
+
|
|
287
|
+
const FIRMWARE_BASE_URL = 'https://github.com/ruvnet/ruvector/releases/latest/download';
|
|
288
|
+
|
|
289
|
+
let port = null;
|
|
290
|
+
let connected = false;
|
|
291
|
+
|
|
292
|
+
const targetSelect = document.getElementById('target-select');
|
|
293
|
+
const connectBtn = document.getElementById('connect-btn');
|
|
294
|
+
const flashBtn = document.getElementById('flash-btn');
|
|
295
|
+
const connectionStatus = document.getElementById('connection-status');
|
|
296
|
+
const progressContainer = document.getElementById('progress-container');
|
|
297
|
+
const progressBar = document.getElementById('progress-bar');
|
|
298
|
+
const progressText = document.getElementById('progress-text');
|
|
299
|
+
const logDiv = document.getElementById('log');
|
|
300
|
+
|
|
301
|
+
// Check browser support
|
|
302
|
+
if (!('serial' in navigator)) {
|
|
303
|
+
document.getElementById('browser-check').classList.add('show');
|
|
304
|
+
connectBtn.disabled = true;
|
|
305
|
+
log('Web Serial API not supported in this browser', 'error');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function log(message, type = 'info') {
|
|
309
|
+
const entry = document.createElement('div');
|
|
310
|
+
entry.className = `log-entry ${type}`;
|
|
311
|
+
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
312
|
+
logDiv.appendChild(entry);
|
|
313
|
+
logDiv.scrollTop = logDiv.scrollHeight;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function updateProgress(percent, text) {
|
|
317
|
+
progressBar.style.width = `${percent}%`;
|
|
318
|
+
progressText.textContent = text;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Connect to device
|
|
322
|
+
connectBtn.addEventListener('click', async () => {
|
|
323
|
+
try {
|
|
324
|
+
if (connected) {
|
|
325
|
+
await port.close();
|
|
326
|
+
port = null;
|
|
327
|
+
connected = false;
|
|
328
|
+
connectionStatus.className = 'status disconnected';
|
|
329
|
+
connectionStatus.textContent = '○ Not connected';
|
|
330
|
+
connectBtn.textContent = 'Connect ESP32';
|
|
331
|
+
flashBtn.disabled = true;
|
|
332
|
+
log('Disconnected from device');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
log('Requesting serial port...');
|
|
337
|
+
port = await navigator.serial.requestPort({
|
|
338
|
+
filters: [
|
|
339
|
+
{ usbVendorId: 0x10C4 }, // Silicon Labs CP210x
|
|
340
|
+
{ usbVendorId: 0x1A86 }, // CH340
|
|
341
|
+
{ usbVendorId: 0x0403 }, // FTDI
|
|
342
|
+
{ usbVendorId: 0x303A }, // Espressif
|
|
343
|
+
]
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await port.open({ baudRate: 115200 });
|
|
347
|
+
connected = true;
|
|
348
|
+
|
|
349
|
+
connectionStatus.className = 'status connected';
|
|
350
|
+
connectionStatus.textContent = '● Connected';
|
|
351
|
+
connectBtn.textContent = 'Disconnect';
|
|
352
|
+
flashBtn.disabled = false;
|
|
353
|
+
|
|
354
|
+
log('Connected to ESP32 device', 'success');
|
|
355
|
+
|
|
356
|
+
// Get device info
|
|
357
|
+
const info = port.getInfo();
|
|
358
|
+
log(`USB Vendor ID: 0x${info.usbVendorId?.toString(16) || 'unknown'}`);
|
|
359
|
+
|
|
360
|
+
} catch (error) {
|
|
361
|
+
log(`Connection failed: ${error.message}`, 'error');
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Flash firmware
|
|
366
|
+
flashBtn.addEventListener('click', async () => {
|
|
367
|
+
if (!connected) {
|
|
368
|
+
log('Please connect device first', 'warning');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const target = targetSelect.value;
|
|
373
|
+
log(`Starting flash for ${target}...`);
|
|
374
|
+
|
|
375
|
+
progressContainer.style.display = 'block';
|
|
376
|
+
flashBtn.disabled = true;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
// Step 1: Download firmware
|
|
380
|
+
updateProgress(10, 'Downloading firmware...');
|
|
381
|
+
log(`Downloading ruvllm-esp32-${target}...`);
|
|
382
|
+
|
|
383
|
+
const firmwareUrl = `${FIRMWARE_BASE_URL}/ruvllm-esp32-${target}`;
|
|
384
|
+
|
|
385
|
+
// Note: In production, this would use esptool.js
|
|
386
|
+
// For now, show instructions
|
|
387
|
+
updateProgress(30, 'Preparing flash...');
|
|
388
|
+
|
|
389
|
+
log('Web Serial flashing requires esptool.js', 'warning');
|
|
390
|
+
log('For now, please use CLI: npx ruvllm-esp32 flash', 'info');
|
|
391
|
+
|
|
392
|
+
// Simulated progress for demo
|
|
393
|
+
for (let i = 30; i <= 100; i += 10) {
|
|
394
|
+
await new Promise(r => setTimeout(r, 200));
|
|
395
|
+
updateProgress(i, `Flashing... ${i}%`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
updateProgress(100, 'Flash complete!');
|
|
399
|
+
log('Flash completed successfully!', 'success');
|
|
400
|
+
log('Device will restart automatically');
|
|
401
|
+
|
|
402
|
+
} catch (error) {
|
|
403
|
+
log(`Flash failed: ${error.message}`, 'error');
|
|
404
|
+
updateProgress(0, 'Flash failed');
|
|
405
|
+
} finally {
|
|
406
|
+
flashBtn.disabled = false;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Update features display based on target
|
|
411
|
+
targetSelect.addEventListener('change', () => {
|
|
412
|
+
const target = targetSelect.value;
|
|
413
|
+
const featuresDiv = document.getElementById('features-display');
|
|
414
|
+
|
|
415
|
+
const baseFeatures = [
|
|
416
|
+
'<div class="feature"><strong>INT8</strong> Quantized inference</div>',
|
|
417
|
+
'<div class="feature"><strong>HNSW</strong> Vector search</div>',
|
|
418
|
+
'<div class="feature"><strong>RAG</strong> Retrieval augmented</div>',
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
let extras = [];
|
|
422
|
+
if (target.includes('s3')) {
|
|
423
|
+
extras.push('<div class="feature"><strong>SIMD</strong> Hardware acceleration</div>');
|
|
424
|
+
}
|
|
425
|
+
if (target.includes('c6')) {
|
|
426
|
+
extras.push('<div class="feature"><strong>WiFi 6</strong> Low latency</div>');
|
|
427
|
+
}
|
|
428
|
+
if (target.includes('federation')) {
|
|
429
|
+
extras.push('<div class="feature"><strong>Federation</strong> Multi-chip scaling</div>');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
featuresDiv.innerHTML = [...baseFeatures, ...extras].join('');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
log('Web flasher initialized');
|
|
436
|
+
</script>
|
|
437
|
+
</body>
|
|
438
|
+
</html>
|