Skip to content

Commit 648a9c3

Browse files
danditomasoCopilot
andauthored
refactor: device connection logic, added nonce to get config only (#946)
* refactor: device connection logic, added nonce to get config only on connect. * Update packages/web/src/core/services/MeshService.ts Co-authored-by: Copilot <[email protected]> * Update packages/web/src/pages/Connections/useConnections.ts Co-authored-by: Copilot <[email protected]> * code review fixes * fixes from code review * ui fixes * refactored meshService, moved code into deviceStore. Fixed some connnection issues * formatting fixes --------- Co-authored-by: Copilot <[email protected]>
1 parent 7f21b3b commit 648a9c3

File tree

17 files changed

+658
-209
lines changed

17 files changed

+658
-209
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ __screenshots__*
99
npm/
1010
.idea
1111
**/LICENSE
12+
.DS_Store
1213

1314
packages/protobufs/packages/ts/dist
1415

packages/core/src/utils/eventSystem.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,12 @@ export class EventSystem {
387387
*/
388388
public readonly onQueueStatus: SimpleEventDispatcher<Protobuf.Mesh.QueueStatus> =
389389
new SimpleEventDispatcher<Protobuf.Mesh.QueueStatus>();
390+
391+
/**
392+
* Fires when a configCompleteId message is received from the device
393+
*
394+
* @event onConfigComplete
395+
*/
396+
public readonly onConfigComplete: SimpleEventDispatcher<number> =
397+
new SimpleEventDispatcher<number>();
390398
}

packages/core/src/utils/transform/decodePacket.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,21 +135,27 @@ export const decodePacket = (device: MeshDevice) =>
135135
}
136136

137137
case "configCompleteId": {
138-
if (decodedMessage.payloadVariant.value !== device.configId) {
139-
device.log.error(
140-
Types.Emitter[Types.Emitter.HandleFromRadio],
141-
`❌ Invalid config id received from device, expected ${device.configId} but received ${decodedMessage.payloadVariant.value}`,
142-
);
143-
}
144-
145138
device.log.info(
146139
Types.Emitter[Types.Emitter.HandleFromRadio],
147-
`⚙️ Valid config id received from device: ${device.configId}`,
140+
`⚙️ Received config complete id: ${decodedMessage.payloadVariant.value}`,
148141
);
149142

150-
device.updateDeviceStatus(
151-
Types.DeviceStatusEnum.DeviceConfigured,
143+
// Emit the configCompleteId event for MeshService to handle two-stage flow
144+
device.events.onConfigComplete.dispatch(
145+
decodedMessage.payloadVariant.value,
152146
);
147+
148+
// For backward compatibility: if configId matches, update device status
149+
// MeshService will override this behavior for two-stage flow
150+
if (decodedMessage.payloadVariant.value === device.configId) {
151+
device.log.info(
152+
Types.Emitter[Types.Emitter.HandleFromRadio],
153+
`⚙️ Config id matches device.configId: ${device.configId}`,
154+
);
155+
device.updateDeviceStatus(
156+
Types.DeviceStatusEnum.DeviceConfigured,
157+
);
158+
}
153159
break;
154160
}
155161

packages/web/src/components/DeviceInfoPanel.tsx

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface DeviceInfoPanelProps {
2626
isCollapsed: boolean;
2727
deviceMetrics: DeviceMetrics;
2828
firmwareVersion: string;
29-
user: Protobuf.Mesh.User;
29+
user?: Protobuf.Mesh.User;
3030
setDialogOpen: () => void;
3131
setCommandPaletteOpen: () => void;
3232
disableHover?: boolean;
@@ -70,8 +70,12 @@ export const DeviceInfoPanel = ({
7070
}
7171
switch (status) {
7272
case "connected":
73+
case "configured":
74+
case "online":
7375
return "bg-emerald-500";
7476
case "connecting":
77+
case "configuring":
78+
case "disconnecting":
7579
return "bg-amber-500";
7680
case "error":
7781
return "bg-red-500";
@@ -84,6 +88,10 @@ export const DeviceInfoPanel = ({
8488
if (!status) {
8589
return t("unknown.notAvailable", "N/A");
8690
}
91+
// Show "connected" for configured state
92+
if (status === "configured") {
93+
return t("toasts.connected", { ns: "connections" });
94+
}
8795
return status;
8896
};
8997

@@ -135,28 +143,30 @@ export const DeviceInfoPanel = ({
135143

136144
return (
137145
<>
138-
<div
139-
className={cn(
140-
"flex items-center gap-3 p-1 flex-shrink-0",
141-
isCollapsed && "justify-center",
142-
)}
143-
>
144-
<Avatar
145-
nodeNum={parseInt(user.id.slice(1), 16)}
146-
className={cn("flex-shrink-0", isCollapsed && "")}
147-
size="sm"
148-
/>
149-
{!isCollapsed && (
150-
<p
151-
className={cn(
152-
"text-sm font-medium text-gray-800 dark:text-gray-200",
153-
"transition-opacity duration-300 ease-in-out truncate",
154-
)}
155-
>
156-
{user.longName}
157-
</p>
158-
)}
159-
</div>
146+
{user && (
147+
<div
148+
className={cn(
149+
"flex items-center gap-3 p-1 flex-shrink-0",
150+
isCollapsed && "justify-center",
151+
)}
152+
>
153+
<Avatar
154+
nodeNum={parseInt(user.id.slice(1), 16)}
155+
className={cn("flex-shrink-0", isCollapsed && "")}
156+
size="sm"
157+
/>
158+
{!isCollapsed && (
159+
<p
160+
className={cn(
161+
"text-sm font-medium text-gray-800 dark:text-gray-200",
162+
"transition-opacity duration-300 ease-in-out truncate",
163+
)}
164+
>
165+
{user.longName}
166+
</p>
167+
)}
168+
</div>
169+
)}
160170

161171
{connectionStatus && (
162172
<button

packages/web/src/components/Dialog/AddConnectionDialog/AddConnectionDialog.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { SupportBadge } from "@app/components/Badge/SupportedBadge.tsx";
22
import { Switch } from "@app/components/UI/Switch.tsx";
3-
import type { NewConnection } from "@app/core/stores/deviceStore/types.ts";
3+
import type {
4+
ConnectionType,
5+
NewConnection,
6+
} from "@app/core/stores/deviceStore/types.ts";
47
import { testHttpReachable } from "@app/pages/Connections/utils";
58
import { Button } from "@components/UI/Button.tsx";
69
import { Input } from "@components/UI/Input.tsx";
@@ -34,7 +37,7 @@ import { Trans, useTranslation } from "react-i18next";
3437
import { DialogWrapper } from "../DialogWrapper.tsx";
3538
import { urlOrIpv4Schema } from "./validation.ts";
3639

37-
type TabKey = "http" | "bluetooth" | "serial";
40+
type TabKey = ConnectionType;
3841
type TestingStatus = "idle" | "testing" | "success" | "failure";
3942
type DialogState = {
4043
tab: TabKey;
@@ -390,12 +393,6 @@ export default function AddConnectionDialog({
390393
const reachable = await testHttpReachable(validatedURL.data);
391394
if (reachable) {
392395
dispatch({ type: "SET_TEST_STATUS", payload: "success" });
393-
toast({
394-
title: t("addConnection.httpConnection.connectionTest.success.title"),
395-
description: t(
396-
"addConnection.httpConnection.connectionTest.success.description",
397-
),
398-
});
399396
} else {
400397
dispatch({ type: "SET_TEST_STATUS", payload: "failure" });
401398
toast({

packages/web/src/components/PageComponents/Connections/ConnectionStatusBadge.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ export function ConnectionStatusBadge({
77
status: Connection["status"];
88
}) {
99
let color = "";
10+
let displayStatus = status;
1011

1112
switch (status) {
1213
case "connected":
14+
case "configured":
1315
color = "bg-emerald-500";
16+
displayStatus = "connected";
1417
break;
1518
case "connecting":
19+
case "configuring":
1620
color = "bg-amber-500";
1721
break;
1822
case "online":
@@ -31,7 +35,7 @@ export function ConnectionStatusBadge({
3135
aria-hidden="true"
3236
/>
3337
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">
34-
{status}
38+
{displayStatus}
3539
</span>
3640
</Button>
3741
);

packages/web/src/components/PageComponents/Map/Markers/NodeMarker.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export const NodeMarker = memo(function NodeMarker({
1616
id,
1717
lng,
1818
lat,
19-
label,
2019
longLabel,
2120
tooltipLabel,
2221
hasError,

packages/web/src/components/Sidebar.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import { useFirstSavedConnection } from "@app/core/stores/deviceStore/selectors.ts";
12
import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
23
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
34
import { Spinner } from "@components/UI/Spinner.tsx";
45
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
56
import {
67
type Page,
8+
useActiveConnection,
79
useAppStore,
10+
useDefaultConnection,
811
useDevice,
9-
useDeviceStore,
1012
useNodeDB,
1113
useSidebar,
1214
} from "@core/stores";
@@ -71,17 +73,18 @@ export const Sidebar = ({ children }: SidebarProps) => {
7173
const { hardware, metadata, unreadCounts, setDialogOpen } = useDevice();
7274
const { getNode, getNodesLength } = useNodeDB();
7375
const { setCommandPaletteOpen } = useAppStore();
74-
const savedConnections = useDeviceStore((s) => s.savedConnections);
7576
const myNode = getNode(hardware.myNodeNum);
7677
const { isCollapsed } = useSidebar();
7778
const { t } = useTranslation("ui");
7879
const navigate = useNavigate({ from: "/" });
7980

80-
// Get the active connection (connected > default > first)
81+
// Get the active connection from selector (connected > default > first)
8182
const activeConnection =
82-
savedConnections.find((c) => c.status === "connected") ||
83-
savedConnections.find((c) => c.isDefault) ||
84-
savedConnections[0];
83+
useActiveConnection() ||
84+
// biome-ignore lint/correctness/useHookAtTopLevel: not a react hook
85+
useDefaultConnection() ||
86+
// biome-ignore lint/correctness/useHookAtTopLevel: not a hook
87+
useFirstSavedConnection();
8588

8689
const pathname = useLocation({
8790
select: (location) => location.pathname.replace(/^\//, ""),

packages/web/src/core/stores/deviceStore/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,17 @@ type DeviceData = {
5252
waypoints: WaypointWithMetadata[];
5353
neighborInfo: Map<number, Protobuf.Mesh.NeighborInfo>;
5454
};
55+
export type ConnectionPhase =
56+
| "disconnected"
57+
| "connecting"
58+
| "configuring"
59+
| "configured";
60+
5561
export interface Device extends DeviceData {
5662
// Ephemeral state (not persisted)
5763
status: Types.DeviceStatusEnum;
64+
connectionPhase: ConnectionPhase;
65+
connectionId: ConnectionId | null;
5866
channels: Map<Types.ChannelNumber, Protobuf.Channel.Channel>;
5967
config: Protobuf.LocalOnly.LocalConfig;
6068
moduleConfig: Protobuf.LocalOnly.LocalModuleConfig;
@@ -70,6 +78,8 @@ export interface Device extends DeviceData {
7078
clientNotifications: Protobuf.Mesh.ClientNotification[];
7179

7280
setStatus: (status: Types.DeviceStatusEnum) => void;
81+
setConnectionPhase: (phase: ConnectionPhase) => void;
82+
setConnectionId: (id: ConnectionId | null) => void;
7383
setConfig: (config: Protobuf.Config.Config) => void;
7484
setModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => void;
7585
getEffectiveConfig<K extends ValidConfigType>(
@@ -153,6 +163,16 @@ export interface deviceState {
153163
) => void;
154164
removeSavedConnection: (id: ConnectionId) => void;
155165
getSavedConnections: () => Connection[];
166+
167+
// Active connection tracking
168+
activeConnectionId: ConnectionId | null;
169+
setActiveConnectionId: (id: ConnectionId | null) => void;
170+
getActiveConnectionId: () => ConnectionId | null;
171+
172+
// Helper selectors for connection ↔ device relationships
173+
getActiveConnection: () => Connection | undefined;
174+
getDeviceForConnection: (id: ConnectionId) => Device | undefined;
175+
getConnectionForDevice: (deviceId: number) => Connection | undefined;
156176
}
157177

158178
interface PrivateDeviceState extends deviceState {
@@ -185,6 +205,8 @@ function deviceFactory(
185205
neighborInfo,
186206

187207
status: Types.DeviceStatusEnum.DeviceDisconnected,
208+
connectionPhase: "disconnected",
209+
connectionId: null,
188210
channels: new Map(),
189211
config: create(Protobuf.LocalOnly.LocalConfigSchema),
190212
moduleConfig: create(Protobuf.LocalOnly.LocalModuleConfigSchema),
@@ -227,6 +249,26 @@ function deviceFactory(
227249
}),
228250
);
229251
},
252+
setConnectionPhase: (phase: ConnectionPhase) => {
253+
set(
254+
produce<PrivateDeviceState>((draft) => {
255+
const device = draft.devices.get(id);
256+
if (device) {
257+
device.connectionPhase = phase;
258+
}
259+
}),
260+
);
261+
},
262+
setConnectionId: (connectionId: ConnectionId | null) => {
263+
set(
264+
produce<PrivateDeviceState>((draft) => {
265+
const device = draft.devices.get(id);
266+
if (device) {
267+
device.connectionId = connectionId;
268+
}
269+
}),
270+
);
271+
},
230272
setConfig: (config: Protobuf.Config.Config) => {
231273
set(
232274
produce<PrivateDeviceState>((draft) => {
@@ -907,6 +949,7 @@ export const deviceStoreInitializer: StateCreator<PrivateDeviceState> = (
907949
) => ({
908950
devices: new Map(),
909951
savedConnections: [],
952+
activeConnectionId: null,
910953

911954
addDevice: (id) => {
912955
const existing = get().devices.get(id);
@@ -972,6 +1015,33 @@ export const deviceStoreInitializer: StateCreator<PrivateDeviceState> = (
9721015
);
9731016
},
9741017
getSavedConnections: () => get().savedConnections,
1018+
1019+
setActiveConnectionId: (id) => {
1020+
set(
1021+
produce<PrivateDeviceState>((draft) => {
1022+
draft.activeConnectionId = id;
1023+
}),
1024+
);
1025+
},
1026+
getActiveConnectionId: () => get().activeConnectionId,
1027+
1028+
getActiveConnection: () => {
1029+
const activeId = get().activeConnectionId;
1030+
if (!activeId) {
1031+
return undefined;
1032+
}
1033+
return get().savedConnections.find((c) => c.id === activeId);
1034+
},
1035+
getDeviceForConnection: (id) => {
1036+
const connection = get().savedConnections.find((c) => c.id === id);
1037+
if (!connection?.meshDeviceId) {
1038+
return undefined;
1039+
}
1040+
return get().devices.get(connection.meshDeviceId);
1041+
},
1042+
getConnectionForDevice: (deviceId) => {
1043+
return get().savedConnections.find((c) => c.meshDeviceId === deviceId);
1044+
},
9751045
});
9761046

9771047
const persistOptions: PersistOptions<PrivateDeviceState, DevicePersisted> = {

0 commit comments

Comments
 (0)