Skip to content

Commit eea123a

Browse files
feat(device): auto-detect ESP32 serial port in HSM setup
1 parent 28a4214 commit eea123a

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

crates/heartwood-device/src/web.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,6 +1851,32 @@ async fn api_hsm_pin(
18511851
}
18521852
}
18531853

1854+
/// `GET /api/hsm/detect` — scan for ESP32 serial devices.
1855+
///
1856+
/// Returns a JSON array of detected serial ports that look like Espressif
1857+
/// devices (vendor ID 0x303a). Used by the setup UI to auto-populate the
1858+
/// serial port field.
1859+
async fn api_hsm_detect() -> impl IntoResponse {
1860+
let ports = serialport::available_ports().unwrap_or_default();
1861+
let espressif: Vec<serde_json::Value> = ports
1862+
.iter()
1863+
.filter_map(|p| {
1864+
if let serialport::SerialPortType::UsbPort(usb) = &p.port_type {
1865+
// Espressif vendor ID is 0x303a
1866+
if usb.vid == 0x303a {
1867+
return Some(json!({
1868+
"port": p.port_name,
1869+
"product": usb.product.as_deref().unwrap_or("ESP32"),
1870+
"serial": usb.serial_number.as_deref().unwrap_or(""),
1871+
}));
1872+
}
1873+
}
1874+
None
1875+
})
1876+
.collect();
1877+
(StatusCode::OK, axum::Json(json!(espressif)))
1878+
}
1879+
18541880
/// Build and return the application router.
18551881
///
18561882
/// No CORS layer is applied — the web UI uses same-origin requests.
@@ -1881,6 +1907,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
18811907
.route("/api/client-keys", get(api_list_client_keys))
18821908
.route("/api/derive-client-key", post(api_derive_client_key))
18831909
.route("/api/hsm/pin", post(api_hsm_pin))
1910+
.route("/api/hsm/detect", get(api_hsm_detect))
18841911
.layer(middleware::from_fn_with_state(state.clone(), lock_middleware))
18851912
.layer(middleware::from_fn_with_state(state.clone(), auth_middleware))
18861913
.layer(DefaultBodyLimit::max(65536))

web/index.html

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,12 @@ <h3>Hardware HSM <span class="badge advanced">advanced</span></h3>
257257
<div id="fields-hsm" class="fields hidden">
258258
<div class="field">
259259
<label>Serial port path</label>
260-
<input type="text" id="hsm-serial-port" placeholder="/dev/ttyUSB0" autocomplete="off" spellcheck="false" style="width:100%; padding:0.65rem; background:#111; border:1px solid #333; border-radius:6px; color:#e0e0e0; font-family:monospace; font-size:0.9rem;">
260+
<div style="display:flex; gap:0.5rem; align-items:center;">
261+
<input type="text" id="hsm-serial-port" placeholder="/dev/ttyACM0" autocomplete="off" spellcheck="false" style="flex:1; padding:0.65rem; background:#111; border:1px solid #333; border-radius:6px; color:#e0e0e0; font-family:monospace; font-size:0.9rem;">
262+
<button type="button" onclick="detectEsp32()" style="padding:0.65rem 1rem; background:#2a2a2a; border:1px solid #444; border-radius:6px; color:#e0e0e0; cursor:pointer; white-space:nowrap; font-size:0.85rem;">Detect</button>
263+
</div>
264+
<p id="hsm-detect-msg" style="font-size:0.78rem; opacity:0.6; margin-top:0.25rem;"></p>
261265
</div>
262-
<p style="font-size:0.78rem; opacity:0.45; margin-top:0.25rem;">The ESP32 must be connected to this port before starting the bridge binary. Common paths: <code>/dev/ttyUSB0</code>, <code>/dev/ttyACM0</code>.</p>
263266
</div>
264267

265268
<!-- PIN for encryption at rest -->
@@ -454,6 +457,26 @@ <h2>Device Password</h2>
454457
return { focus: () => input.focus(), reset: () => { input.value = ''; suggestions.classList.remove('visible'); } };
455458
}
456459

460+
async function detectEsp32() {
461+
const msg = document.getElementById('hsm-detect-msg');
462+
msg.textContent = 'Scanning...';
463+
try {
464+
const r = await fetch('/api/hsm/detect');
465+
const devices = await r.json();
466+
if (devices.length === 0) {
467+
msg.textContent = 'No ESP32 devices found. Check the USB connection.';
468+
} else if (devices.length === 1) {
469+
document.getElementById('hsm-serial-port').value = devices[0].port;
470+
msg.textContent = 'Found: ' + devices[0].product + ' at ' + devices[0].port;
471+
} else {
472+
document.getElementById('hsm-serial-port').value = devices[0].port;
473+
msg.textContent = 'Found ' + devices.length + ' devices. Using ' + devices[0].port;
474+
}
475+
} catch (e) {
476+
msg.textContent = 'Detection failed: ' + e.message;
477+
}
478+
}
479+
457480
function selectMode(mode) {
458481
currentMode = mode;
459482
document.querySelectorAll('.mode-card').forEach(c =>

0 commit comments

Comments
 (0)