mirror of
https://github.com/meshtastic/web.git
synced 2026-07-30 22:46:30 -04:00
fix(web/connections): probe = "online" not "configured"; flip status to "configuring" before DB open; await transport disconnect
Three reconnect-flow bugs: 1. probeConnection returned "configured" for serial / bluetooth when the browser had stored permission for the device — but permission ≠ a configured Meshtastic device. The card showed "connected" and a Disconnect button before the user had ever clicked Connect. Probe now returns "online" (= "available, click to connect"), matching the HTTP path. "configured" is reserved for the onConfigComplete callback. 2. setupMeshDevice flipped status from "connecting" → "configuring" AFTER awaiting buildMeshDevice, which opens the OPFS DB. If the DB open stalled (multi-tab contention, slow first-time init), the card sat at "connecting" indefinitely. Move the status flip ahead of the persistence await — UI shows "configuring" while persistence is spinning up. 3. teardown's `device?.connection?.disconnect()` returned a Promise that was never awaited, so the underlying transport's port.close() could race the next port.open() on a fast disconnect → reconnect. Make teardown async, await disconnect, propagate via async removeConnection / disconnect callers. Plus: connect() catch block logs the underlying error before turning it into a status update, so the actual failure shows up in console even when the toast text is generic.
This commit is contained in:
@@ -188,7 +188,11 @@ export async function probeConnection(
|
||||
}
|
||||
).getDevices?.();
|
||||
const hasPermission = known?.some((d: BluetoothDevice) => d.id === conn.deviceId);
|
||||
return hasPermission ? "configured" : "disconnected";
|
||||
// Permission granted ≠ device configured. The card surfaces "online"
|
||||
// (i.e. "available, click to connect") so the user explicitly opts
|
||||
// into the configure handshake. "configured" is reserved for when
|
||||
// the firmware has actually replied with config-complete.
|
||||
return hasPermission ? "online" : "disconnected";
|
||||
} catch {
|
||||
return "disconnected";
|
||||
}
|
||||
@@ -210,7 +214,7 @@ export async function probeConnection(
|
||||
).getInfo?.() ?? {};
|
||||
return info.usbVendorId === conn.usbVendorId && info.usbProductId === conn.usbProductId;
|
||||
});
|
||||
return hasPermission ? "configured" : "disconnected";
|
||||
return hasPermission ? "online" : "disconnected";
|
||||
} catch {
|
||||
return "disconnected";
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useConnections() {
|
||||
[updateSavedConnection],
|
||||
);
|
||||
|
||||
const teardown = useCallback((id: ConnectionId, conn?: Connection) => {
|
||||
const teardown = useCallback(async (id: ConnectionId, conn?: Connection) => {
|
||||
stopHeartbeat(id);
|
||||
configSubscriptions.get(id)?.();
|
||||
configSubscriptions.delete(id);
|
||||
@@ -57,7 +57,10 @@ export function useConnections() {
|
||||
if (conn?.meshDeviceId) {
|
||||
const device = useDeviceStore.getState().getDevice(conn.meshDeviceId);
|
||||
try {
|
||||
device?.connection?.disconnect();
|
||||
// Await the underlying transport's close so the port is fully
|
||||
// released before the user clicks reconnect — otherwise port.open()
|
||||
// can race the close and throw "port already open".
|
||||
await device?.connection?.disconnect();
|
||||
} catch {}
|
||||
}
|
||||
closeTransport(cachedTransports.get(id));
|
||||
@@ -65,9 +68,9 @@ export function useConnections() {
|
||||
}, []);
|
||||
|
||||
const removeConnection = useCallback(
|
||||
(id: ConnectionId) => {
|
||||
async (id: ConnectionId) => {
|
||||
const conn = connections.find((c) => c.id === id);
|
||||
teardown(id, conn);
|
||||
await teardown(id, conn);
|
||||
if (conn?.meshDeviceId) {
|
||||
try {
|
||||
useDeviceStore.getState().removeDevice(conn.meshDeviceId);
|
||||
@@ -104,6 +107,14 @@ export function useConnections() {
|
||||
deviceId = deviceId ?? randId();
|
||||
|
||||
const device = addDevice(deviceId);
|
||||
|
||||
// Flip status to "configuring" up front. buildMeshDevice awaits the
|
||||
// OPFS DB open which can take a beat (or stall under multi-tab
|
||||
// contention); we don't want the UI looking stuck at "connecting"
|
||||
// while persistence is spinning up.
|
||||
device.setConnectionPhase("configuring");
|
||||
updateStatus(id, "configuring");
|
||||
|
||||
const meshDevice = await buildMeshDevice(id, deviceId, transport);
|
||||
|
||||
if (!meshRegistry.has(id)) {
|
||||
@@ -128,9 +139,6 @@ export function useConnections() {
|
||||
});
|
||||
configSubscriptions.set(id, unsubConfigComplete);
|
||||
|
||||
device.setConnectionPhase("configuring");
|
||||
updateStatus(id, "configuring");
|
||||
|
||||
meshDevice
|
||||
.configure()
|
||||
.then(() => meshDevice.heartbeat().then(() => startConfigHeartbeat(id, meshDevice)))
|
||||
@@ -175,6 +183,7 @@ export function useConnections() {
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("[useConnections] connect failed:", err);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
updateStatus(id, "error", message);
|
||||
return false;
|
||||
@@ -188,7 +197,7 @@ export function useConnections() {
|
||||
const conn = connections.find((c) => c.id === id);
|
||||
if (!conn) return;
|
||||
try {
|
||||
teardown(id, conn);
|
||||
await teardown(id, conn);
|
||||
if (conn.meshDeviceId) {
|
||||
const device = useDeviceStore.getState().getDevice(conn.meshDeviceId);
|
||||
if (device) {
|
||||
|
||||
Reference in New Issue
Block a user