* fix(connections): connect to the just-added connection via the live store addConnectionAndConnect() adds a connection and then connects to it in the same tick, but connect() resolved the id against the memoized `connections` closure, which is stale until the hook re-renders. The just-added id was therefore reported as an unknown connection id and Save silently never connected any HTTP/Serial/Bluetooth device. Read savedConnections from useDeviceStore.getState() so the lookup always sees the live store. * test(e2e): real-device Playwright messaging suite Drives the actual web app in Chromium against real meshtasticd firmware over the HTTP phone API and verifies text messaging in both directions across a two-node mesh. Nodes mesh over the firmware's built-in UDP multicast (224.0.0.69) with no MQTT/relay; distinct node numbers, real encryption. - Default backend: two Docker meshtasticd sim nodes (daily-debian). The same specs run against physical hardware via E2E_DEVICE_MODE=hardware. - An off-browser Python meshtastic peer (e2e/peer/peer.py) drives/asserts the non-browser node over the TCP phone API, mirroring firmware mcp-server tests. - Coverage: connect over HTTPS, mesh->web receive, web->mesh send. Direct messages are fixme'd (see below). CI workflow runs it on Linux. Bugs surfaced by the suite: - Fixed (prior commit): connect-on-save never connected (stale-closure id lookup in useConnections). - Not fixed: apps/web/src/core/subscriptions.ts throws 'ReferenceError: nodeDB is not defined' on every device-metrics telemetry packet (the #1050 migration removed that store); caught per-packet, so messaging still works. - Not fixed: direct messages are blocked by a PKI 'Keys Mismatch' (the SDK's stored peer public key != the key presented during NodeInfo exchange), seen even with fresh sim nodes. * test(e2e): address Copilot review feedback - waitForTcp(): destroy the probe socket on the error path so repeated connection failures don't accumulate sockets/FDs across the retry loop. - Don't remove the mesh containers in Playwright globalTeardown in CI — it raced the workflow's failure log capture. Teardown is now gated on E2E_DOCKER_DOWN only; CI dumps device logs on failure and tears the mesh down in a final always() workflow step. * fix(sdk): fold device-metrics telemetry into nodes apps/web/src/core/subscriptions.ts called nodeDB.addDeviceMetrics() on every device-metrics telemetry packet, but the #1050 migration removed that store — so it threw 'ReferenceError: nodeDB is not defined' on each telemetry packet (caught per-packet by the SDK's HandleFromRadio, so messaging still worked but the error spammed the console). Route device metrics into the SDK NodesClient via onTelemetryPacket instead — mirroring the existing position handler; the Node domain already carries a deviceMetrics field — and drop the dead app-side handler. Adds a NodesClient test covering the fold. * docs(e2e): accurate DM root cause + bug status The direct-message fixme is a simulator limitation, not a web-app bug: the keyless meshtasticd sim nodes NAK a DM with NO_CHANNEL (routing error 6) — no Curve25519 keypair is provisioned/shared, and current firmware can't deliver a direct message without a per-node key / decryptable channel. The app surfaces this correctly (key-refresh dialog). Re-enable against hardware or once the sim provisions keys. Also: mark the nodeDB telemetry bug fixed and note the CI teardown change. * docs(e2e): precise DM root cause (firmware/sim PKI) Followed up on the suggestion to provision keys in config.security: the keys ARE settable and persist (verified via admin), but on the native meshtasticd sim they don't sync to the node's owner / NodeInfo key — owner.public_key stays empty and the node keeps its MAC-derived num — so the two nodes never exchange keys. Combined with the firmware refusing non-PKI DMs ('Unknown public key for destination ... refusing to send legacy DM'), the DM is NAK'd with NO_CHANNEL. A firmware/sim limitation; DMs work on real hardware. Spec stays fixme. * docs(e2e): definitive DM root cause (SimRadio PKC payload limit) Per the steer to research the firmware: PKI keygen is gated on a set LoRa region (NodeDB.cpp:3051) and the sim boots region-UNSET — setting lora.region via admin DOES make the nodes generate and exchange keys (verified both ways). But a PKI-encrypted DM still can't traverse the SimRadio: the PKC overhead exceeds its payload limit ('Payload size larger than compressed message allows! Send empty payload'), so the packet is truncated and the receiver NAKs NO_CHANNEL ('No suitable channel found for decoding, hash 0x0'). The firmware skips PKC under --sim (Router.cpp:730) for exactly this reason, but --sim also disables the config-file loading the web app needs, so they're mutually exclusive. DMs work on real hardware; spec stays fixme with this detail. --------- Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
import { useNewNodeNum } from "@core/hooks/useNewNodeNum";
|
|
import { type Device } from "@core/stores";
|
|
import { type MeshDevice, Protobuf } from "@meshtastic/sdk";
|
|
|
|
/**
|
|
* Wires up the legacy MeshDevice event stream into the web's Zustand stores.
|
|
*
|
|
* Note: the SDK now owns chat persistence (via SqlocalMessageRepository) and
|
|
* the entire NodesClient surface — node info, user, position, lastHeard /
|
|
* snr, favourite / ignored flags, and PKI-error tracking. This handler no
|
|
* longer mirrors any of that into the legacy stores; what remains is
|
|
* device-store-only state (waypoints, traceroutes, neighbour info, dialog
|
|
* open triggers, unread counts).
|
|
*/
|
|
export const subscribeAll = (device: Device, connection: MeshDevice) => {
|
|
connection.events.onDeviceMetadataPacket.subscribe((metadataPacket) => {
|
|
device.addMetadata(metadataPacket.from, metadataPacket.data);
|
|
});
|
|
|
|
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
|
switch (routingPacket.data.variant.case) {
|
|
case "errorReason": {
|
|
if (
|
|
routingPacket.data.variant.value === Protobuf.Mesh.Routing_Error.NONE
|
|
) {
|
|
return;
|
|
}
|
|
console.info(`Routing Error: ${routingPacket.data.variant.value}`);
|
|
break;
|
|
}
|
|
case "routeReply": {
|
|
console.info(`Route Reply: ${routingPacket.data.variant.value}`);
|
|
break;
|
|
}
|
|
case "routeRequest": {
|
|
console.info(`Route Request: ${routingPacket.data.variant.value}`);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
connection.events.onDeviceStatus.subscribe((status) => {
|
|
device.setStatus(status);
|
|
});
|
|
|
|
connection.events.onWaypointPacket.subscribe((waypoint) => {
|
|
const { data, channel, from, rxTime } = waypoint;
|
|
device.addWaypoint(data, channel, from, rxTime);
|
|
});
|
|
|
|
connection.events.onMyNodeInfo.subscribe((nodeInfo) => {
|
|
useNewNodeNum(device.id, nodeInfo);
|
|
});
|
|
|
|
// onUserPacket / onPositionPacket / onNodeInfoPacket and device-metrics
|
|
// telemetry (battery / channel utilisation / voltage) are folded into nodes by
|
|
// the SDK NodesClient (see packages/sdk/src/features/nodes/NodesClient.ts).
|
|
|
|
connection.events.onChannelPacket.subscribe((channel) => {
|
|
device.addChannel(channel);
|
|
});
|
|
connection.events.onConfigPacket.subscribe((config) => {
|
|
device.setConfig(config);
|
|
});
|
|
connection.events.onModuleConfigPacket.subscribe((moduleConfig) => {
|
|
device.setModuleConfig(moduleConfig);
|
|
});
|
|
|
|
// Inbound message handling (persistence, unread counts) lives entirely on
|
|
// the SDK ChatClient now — see ChatClient + chat.unread.
|
|
|
|
connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => {
|
|
device.addTraceRoute({
|
|
...traceRoutePacket,
|
|
});
|
|
});
|
|
|
|
connection.events.onPendingSettingsChange.subscribe((state) => {
|
|
device.setPendingSettingsChanges(state);
|
|
});
|
|
|
|
// onMeshPacket → lastHeard / snr per-node updates are handled by the SDK
|
|
// NodesClient.
|
|
|
|
connection.events.onClientNotificationPacket.subscribe(
|
|
(clientNotificationPacket) => {
|
|
device.addClientNotification(clientNotificationPacket);
|
|
device.setDialogOpen("clientNotification", true);
|
|
},
|
|
);
|
|
|
|
connection.events.onNeighborInfoPacket.subscribe((neighborInfo) => {
|
|
device.addNeighborInfo(neighborInfo.from, neighborInfo.data);
|
|
});
|
|
|
|
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
|
if (routingPacket.data.variant.case === "errorReason") {
|
|
switch (routingPacket.data.variant.value) {
|
|
case Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT:
|
|
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
|
|
break;
|
|
case Protobuf.Mesh.Routing_Error.NO_CHANNEL:
|
|
case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY:
|
|
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
|
|
// Per-node error tracking lives on the SDK NodesClient
|
|
// (client.nodes.errors); the dialog open trigger stays here so the
|
|
// legacy device-store-driven dialog manager keeps working.
|
|
device.setDialogOpen("refreshKeys", true);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
};
|