feat(mesh): Meshtastic RSSI/SNR + peer-location map wiring (backlog #14/#15, part 1)

Backend: parse_mesh_packet now decodes MeshPacket.rx_snr (field 8, float) and
rx_rssi (field 12, int32), and a new POSITION_APP branch decodes Position.
latitude_i/longitude_i (fields 1/2, sfixed32) -- all field numbers confirmed
against the canonical meshtastic/protobufs mesh.proto, not guessed. Threaded
through ParsedContact -> refresh_contacts -> MeshPeer (mirroring how
pkc_capable was wired for #17), so mesh.peers now surfaces real rssi/snr/lat/
lon instead of always-null. Fixed a real bug found along the way:
update_node_info's unconditional contact replace would have silently wiped
any already-tracked signal/position data on the next NodeInfo packet -- now
preserves it.

Frontend: mesh.ts's updateNodePositionsFromPeers() feeds real position data
into the SAME nodePositions map MeshMap.vue already renders from (parallel to
the existing Coordinate/Alert-message path) -- MeshMap.vue itself needed zero
changes, it was already built for this.

105/105 mesh tests pass (4 new: rx_snr/rx_rssi decode, position decode +
incomplete-field handling, full packet_to_inbound_frame integration).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-30 22:52:42 -04:00
co-authored by Claude Sonnet 5
parent dfca007949
commit 02b6b52a8c
8 changed files with 247 additions and 29 deletions
+26
View File
@@ -38,6 +38,10 @@ export interface MeshPeer {
hops: number
last_advert?: number
reachable?: boolean
/** Last known position (degrees), from a Meshtastic POSITION_APP broadcast.
* Absent until the peer has shared one. */
lat?: number | null
lon?: number | null
}
export interface MeshChannel {
@@ -215,6 +219,7 @@ export const useMeshStore = defineStore('mesh', () => {
method: 'mesh.peers',
})
peers.value = res.peers
updateNodePositionsFromPeers(res.peers)
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh peers'
}
@@ -298,6 +303,27 @@ export const useMeshStore = defineStore('mesh', () => {
}
}
/** Feed real Meshtastic POSITION_APP broadcasts into the same map the
* manually-shared Coordinate/Alert messages already populate — MeshMap.vue
* needs no changes, it already renders from `nodePositions`. Uses
* `last_heard` as the freshness timestamp, same comparison pattern as
* `updateNodePositionsFromMessages`, so a peer position never clobbers a
* more-recently-shared coordinate message and vice versa. */
function updateNodePositionsFromPeers(peerList: MeshPeer[]) {
for (const peer of peerList) {
if (typeof peer.lat !== 'number' || typeof peer.lon !== 'number') continue
const existing = nodePositions.value.get(peer.contact_id)
if (!existing || peer.last_heard > existing.timestamp) {
nodePositions.value.set(peer.contact_id, {
lat: peer.lat,
lng: peer.lon,
label: peer.advert_name,
timestamp: peer.last_heard,
})
}
}
}
function getNodePositions(): Map<number, NodePosition> {
return nodePositions.value
}