From 0265a900a0c7404b62ee9325b7ee2a4bab6cbaac Mon Sep 17 00:00:00 2001 From: Alex Vanderpot Date: Fri, 19 Jun 2026 02:03:00 -0400 Subject: [PATCH] Decode advert node type as enum; add sensor type; drop empty packets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The advert app_data flags byte (Core Protocol §2.8.3) uses split encoding: the low 4 bits are an integer node-type enum (1=chat/companion, 2=repeater, 3=room server, 4=sensor) and the high 4 bits are independent presence flags. The decode treated the type as independent bit flags, so a room server (type 3 = 0b0011) matched is_chat_node, is_repeater AND is_room_server at once, and a sensor (type 4) matched none and rendered as an untyped node. Migration 008 fixes the derivation in the meshcore_adverts / meshcore_adverts_latest views (read-time only, retroactive over all history), adds is_sensor and a node_type column, and carries neighbor_is_sensor onto the direct-neighbor graph. The UI gains a sensor badge across the node page, node cards, and advert details, and search/streaming/node queries expose is_sensor. Ingest: drop envelopes whose decoded packet has zero bytes instead of storing them. They decode to an empty payload and a degenerate packet_hash and carry no signal; dropped rows are counted and logged. Co-Authored-By: Claude Opus 4.8 (1M context) --- ingest/cmd/meshcoreingest/main.go | 16 + ingest/cmd/meshcoreingest/main_test.go | 27 ++ .../migrations/008_advert_node_type_enum.sql | 285 ++++++++++++++++++ meshexplorer/src/app/(app)/api-docs/page.tsx | 5 +- .../(app)/meshcore/node/[publicKey]/page.tsx | 15 +- meshexplorer/src/components/AdvertDetails.tsx | 13 +- meshexplorer/src/components/NodeCard.tsx | 10 +- meshexplorer/src/hooks/useMeshcoreSearch.ts | 1 + meshexplorer/src/hooks/useNeighbors.ts | 1 + meshexplorer/src/hooks/useNodeData.ts | 2 + meshexplorer/src/lib/clickhouse/actions.ts | 9 + meshexplorer/src/lib/clickhouse/streaming.ts | 1 + 12 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 ingest/migrations/008_advert_node_type_enum.sql diff --git a/ingest/cmd/meshcoreingest/main.go b/ingest/cmd/meshcoreingest/main.go index 755e784..470deed 100644 --- a/ingest/cmd/meshcoreingest/main.go +++ b/ingest/cmd/meshcoreingest/main.go @@ -54,10 +54,23 @@ var packetRows chan meshcorePacketRow // or unreachable). The writer logs and resets it on each flush. var droppedRows uint64 +// droppedEmpty counts rows dropped because the decoded packet was empty. A +// growing number of upstream publishers emit an otherwise-valid envelope +// (origin/origin_id present) with no packet bytes; storing those produces rows +// that decode to an empty payload and a degenerate packet_hash and carry no +// signal. The writer logs and resets this on each flush. +var droppedEmpty uint64 + // enqueuePacket hands a row to the batch writer without ever blocking the // caller. Blocking here would defeat the whole purpose — it runs on paho's // inbound goroutine. If the buffer is full we drop and count instead. func enqueuePacket(row meshcorePacketRow) { + // Skip envelopes that carry no packet bytes: there is nothing to decode and + // they only add noise (empty payload, degenerate hash). Counted, not stored. + if len(row.packet) == 0 { + atomic.AddUint64(&droppedEmpty, 1) + return + } select { case packetRows <- row: default: @@ -78,6 +91,9 @@ func runBatchWriter(d *ingestcommon.Daemon, flushInterval time.Duration, maxRows if dropped := atomic.SwapUint64(&droppedRows, 0); dropped > 0 { zap.L().Warn("Dropped MeshCore packets: insert buffer full", zap.Uint64("dropped", dropped)) } + if dropped := atomic.SwapUint64(&droppedEmpty, 0); dropped > 0 { + zap.L().Info("Dropped MeshCore packets: empty packet (no bytes to decode)", zap.Uint64("dropped", dropped)) + } if len(batch) == 0 { return } diff --git a/ingest/cmd/meshcoreingest/main_test.go b/ingest/cmd/meshcoreingest/main_test.go index f7bdcea..e82c6bc 100644 --- a/ingest/cmd/meshcoreingest/main_test.go +++ b/ingest/cmd/meshcoreingest/main_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/hex" "strings" + "sync/atomic" "testing" "time" ) @@ -95,6 +96,32 @@ func TestParseMeshCoreRawMessage_WithOriginID(t *testing.T) { } } +func TestEnqueuePacket_DropsEmpty(t *testing.T) { + // Use a real buffered channel so a successful enqueue is observable. + packetRows = make(chan meshcorePacketRow, 4) + t.Cleanup(func() { packetRows = nil }) + atomic.StoreUint64(&droppedEmpty, 0) + atomic.StoreUint64(&droppedRows, 0) + + // Empty packet: dropped and counted, never enqueued. + enqueuePacket(meshcorePacketRow{origin: "node", originPubkey: "key", packet: ""}) + if got := atomic.LoadUint64(&droppedEmpty); got != 1 { + t.Errorf("droppedEmpty = %d, want 1", got) + } + if len(packetRows) != 0 { + t.Errorf("empty packet should not be enqueued, queue len = %d", len(packetRows)) + } + + // Non-empty packet: enqueued, not counted as empty. + enqueuePacket(meshcorePacketRow{origin: "node", originPubkey: "key", packet: "\x01\x02"}) + if got := atomic.LoadUint64(&droppedEmpty); got != 1 { + t.Errorf("droppedEmpty = %d after non-empty enqueue, want 1", got) + } + if len(packetRows) != 1 { + t.Errorf("non-empty packet should be enqueued, queue len = %d", len(packetRows)) + } +} + func TestExtractBaseTopic(t *testing.T) { tests := []struct { name string diff --git a/ingest/migrations/008_advert_node_type_enum.sql b/ingest/migrations/008_advert_node_type_enum.sql new file mode 100644 index 0000000..54f6c4b --- /dev/null +++ b/ingest/migrations/008_advert_node_type_enum.sql @@ -0,0 +1,285 @@ +-- +goose Up +-- Decode the advert (payload_type=4) node TYPE correctly. +-- +-- The advert app_data flags byte (payload offset 101) uses split encoding per the Core Protocol +-- spec (§2.8.3): the LOW 4 bits are an integer node-type ENUM, the HIGH 4 bits are independent +-- presence flags. +-- node type (bits 0-3, 0x0F): 0=NONE, 1=CHAT/companion, 2=REPEATER, 3=ROOM server, 4=SENSOR +-- presence (bits 4-7): 0x10 has_location, 0x20 feature1, 0x40 feature2, 0x80 has_name +-- +-- The original decode treated the type as independent bit flags: +-- is_chat_node = flags & 0x01, is_repeater = flags & 0x02, is_room_server = flags & 0x03. +-- Because a ROOM server is type 3 (binary 0011) it matched ALL THREE tests at once (shown as +-- repeater + companion + room simultaneously), and a SENSOR (type 4) matched NONE (shown as an +-- untyped/unknown node). This fixes the derivation to the enum, adds is_sensor, and exposes a +-- node_type column. The presence-flag (has_*) decoding was already correct and is unchanged. +-- +-- These are read-time VIEW changes over already-stored data (appdata_flags / payload), so the fix +-- applies retroactively to all history with no reingest or backfill. The is_chat_node/is_repeater/ +-- is_room_server AggregateFunction columns in meshcore_adverts_latest_state predate this fix and +-- held the old bitmask values; the read view below now derives the booleans from the (correctly +-- stored) appdata_flags instead, so those state columns are intentionally bypassed. + +-- meshcore_adverts: enum-based node type + is_sensor + node_type (otherwise identical to migration 007). +-- +goose StatementBegin +CREATE OR REPLACE VIEW meshcore_adverts AS +SELECT + ingest_timestamp, + origin, + origin_pubkey, + mesh_timestamp, + packet, + path_len, + path, + hash_size, + broker, + topic, + multiIf(lower(topic) IN ('meshcore','meshcore/salish'), 'SEA', match(splitByChar('/', lower(topic))[2], '^[a-z]{3}$'), upper(splitByChar('/', lower(topic))[2]), '') AS region, + hex(substring(payload, 1, 32)) AS public_key, + reinterpretAsUInt32(substring(payload, 33, 4)) AS adv_timestamp, + hex(substring(payload, 37, 64)) AS signature, + reinterpretAsUInt8(substring(payload, 101, 1)) AS appdata_flags, + bitAnd(appdata_flags, 0x0F) AS node_type, + node_type = 1 AS is_chat_node, + node_type = 2 AS is_repeater, + node_type = 3 AS is_room_server, + node_type = 4 AS is_sensor, + bitAnd(appdata_flags, 0x10) = 0x10 AS has_location, + bitAnd(appdata_flags, 0x20) = 0x20 AS has_feature1, + bitAnd(appdata_flags, 0x40) = 0x40 AS has_feature2, + bitAnd(appdata_flags, 0x80) = 0x80 AS has_name, + CASE WHEN bitAnd(appdata_flags, 0x10) = 0x10 + THEN reinterpretAsInt32(substring(payload, 102, 4)) + ELSE NULL + END AS latitude_i, + CASE WHEN bitAnd(appdata_flags, 0x10) = 0x10 + THEN reinterpretAsInt32(substring(payload, 106, 4)) + ELSE NULL + END AS longitude_i, + latitude_i * 1e-6 AS latitude, + longitude_i * 1e-6 AS longitude, + substring( + payload, + 102 + + multiIf(bitAnd(appdata_flags, 0x10) = 0x10, 8, 0) + ) AS node_name, + hex(substring(payload, 1, 1)) AS node_hash, + packet_hash +FROM meshcore_packets +WHERE payload_type = 4; +-- +goose StatementEnd + +-- meshcore_adverts_latest: derive the type booleans + node_type from the merged appdata_flags +-- (bypassing the legacy bitmask state columns). Same column contract as migration 005 plus the +-- new is_sensor / node_type columns. +-- +goose StatementBegin +CREATE OR REPLACE VIEW meshcore_adverts_latest AS +SELECT + public_key, + minMerge(first_heard) AS first_heard, + maxMerge(last_seen) AS last_seen, + argMaxMerge(broker) AS broker, + argMaxMerge(topic) AS topic, + argMaxMerge(region) AS region, + argMaxMerge(origin) AS origin, + argMaxMerge(mesh_timestamp) AS mesh_timestamp, + argMaxMerge(packet) AS packet, + argMaxMerge(path_len) AS path_len, + argMaxMerge(path) AS path, + argMaxMerge(adv_timestamp) AS adv_timestamp, + argMaxMerge(signature) AS signature, + argMaxMerge(appdata_flags) AS appdata_flags, + bitAnd(appdata_flags, 0x0F) AS node_type, + toUInt8(node_type = 1) AS is_chat_node, + toUInt8(node_type = 2) AS is_repeater, + toUInt8(node_type = 3) AS is_room_server, + toUInt8(node_type = 4) AS is_sensor, + argMaxMerge(has_location) AS has_location, + argMaxMerge(has_feature1) AS has_feature1, + argMaxMerge(has_feature2) AS has_feature2, + argMaxMerge(has_name) AS has_name, + argMaxMerge(latitude_i) AS latitude_i, + argMaxMerge(longitude_i) AS longitude_i, + argMaxMerge(latitude) AS latitude, + argMaxMerge(longitude) AS longitude, + argMaxMerge(node_name) AS node_name, + argMaxMerge(node_hash) AS node_hash, + argMaxMerge(packet_hash) AS packet_hash +FROM meshcore_adverts_latest_state +GROUP BY public_key +ORDER BY last_seen DESC; +-- +goose StatementEnd + +-- Carry the corrected type onto the per-node direct-neighbor graph: the existing +-- neighbor_is_repeater/is_chat_node/is_room_server already self-correct (they read from +-- meshcore_adverts_latest), but a neighbor_is_sensor column is added so sensor neighbors render too. +DROP VIEW IF EXISTS meshcore_node_direct_neighbors; +-- +goose StatementBegin +CREATE MATERIALIZED VIEW IF NOT EXISTS meshcore_node_direct_neighbors +REFRESH EVERY 1 HOUR +ENGINE = MergeTree +ORDER BY (node_public_key) +AS +WITH + node_details AS ( + SELECT public_key, node_name, latitude, longitude, has_location, + is_repeater, is_chat_node, is_room_server, is_sensor, has_name, last_seen + FROM meshcore_adverts_latest + ), + directions AS ( + SELECT DISTINCT hex(origin_pubkey) AS node_public_key, public_key AS neighbor_public_key, 'incoming' AS direction + FROM meshcore_adverts + WHERE path_len = 0 AND hex(origin_pubkey) != public_key AND ingest_timestamp >= now() - INTERVAL 7 DAY + UNION ALL + SELECT DISTINCT public_key AS node_public_key, hex(origin_pubkey) AS neighbor_public_key, 'outgoing' AS direction + FROM meshcore_adverts + WHERE path_len = 0 AND hex(origin_pubkey) != public_key AND ingest_timestamp >= now() - INTERVAL 7 DAY + ) +SELECT + d.node_public_key AS node_public_key, + d.neighbor_public_key AS neighbor_public_key, + d.direction AS direction, + nd.node_name AS neighbor_name, + nd.latitude AS neighbor_latitude, + nd.longitude AS neighbor_longitude, + nd.has_location AS neighbor_has_location, + nd.is_repeater AS neighbor_is_repeater, + nd.is_chat_node AS neighbor_is_chat_node, + nd.is_room_server AS neighbor_is_room_server, + nd.is_sensor AS neighbor_is_sensor, + nd.has_name AS neighbor_has_name, + nd.last_seen AS neighbor_last_seen +FROM directions AS d +INNER JOIN node_details AS nd ON d.neighbor_public_key = nd.public_key; +-- +goose StatementEnd + +-- Apply the corrected derivation immediately (these REFRESH MVs otherwise recompute hourly). +SYSTEM REFRESH VIEW meshcore_node_direct_neighbors; +SYSTEM REFRESH VIEW meshcore_all_neighbor_edges; + + +-- +goose Down +-- Restore the migration-007 adverts view (bitmask flags, no is_sensor/node_type). +-- +goose StatementBegin +CREATE OR REPLACE VIEW meshcore_adverts AS +SELECT + ingest_timestamp, + origin, + origin_pubkey, + mesh_timestamp, + packet, + path_len, + path, + hash_size, + broker, + topic, + multiIf(lower(topic) IN ('meshcore','meshcore/salish'), 'SEA', match(splitByChar('/', lower(topic))[2], '^[a-z]{3}$'), upper(splitByChar('/', lower(topic))[2]), '') AS region, + hex(substring(payload, 1, 32)) AS public_key, + reinterpretAsUInt32(substring(payload, 33, 4)) AS adv_timestamp, + hex(substring(payload, 37, 64)) AS signature, + reinterpretAsUInt8(substring(payload, 101, 1)) AS appdata_flags, + bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x01) = 0x01 AS is_chat_node, + bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x02) = 0x02 AS is_repeater, + bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x03) = 0x03 AS is_room_server, + bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x10) = 0x10 AS has_location, + bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x20) = 0x20 AS has_feature1, + bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x40) = 0x40 AS has_feature2, + bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x80) = 0x80 AS has_name, + CASE WHEN bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x10) = 0x10 + THEN reinterpretAsInt32(substring(payload, 102, 4)) + ELSE NULL + END AS latitude_i, + CASE WHEN bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x10) = 0x10 + THEN reinterpretAsInt32(substring(payload, 106, 4)) + ELSE NULL + END AS longitude_i, + latitude_i * 1e-6 AS latitude, + longitude_i * 1e-6 AS longitude, + substring( + payload, + 102 + + multiIf(bitAnd(reinterpretAsUInt8(substring(payload, 101, 1)), 0x10) = 0x10, 8, 0) + ) AS node_name, + hex(substring(payload, 1, 1)) AS node_hash, + packet_hash +FROM meshcore_packets +WHERE payload_type = 4; +-- +goose StatementEnd + +-- Restore the migration-005 latest view (reads the bitmask state columns, no is_sensor/node_type). +-- +goose StatementBegin +CREATE OR REPLACE VIEW meshcore_adverts_latest AS +SELECT + public_key, + minMerge(first_heard) AS first_heard, + maxMerge(last_seen) AS last_seen, + argMaxMerge(broker) AS broker, + argMaxMerge(topic) AS topic, + argMaxMerge(region) AS region, + argMaxMerge(origin) AS origin, + argMaxMerge(mesh_timestamp) AS mesh_timestamp, + argMaxMerge(packet) AS packet, + argMaxMerge(path_len) AS path_len, + argMaxMerge(path) AS path, + argMaxMerge(adv_timestamp) AS adv_timestamp, + argMaxMerge(signature) AS signature, + argMaxMerge(appdata_flags) AS appdata_flags, + argMaxMerge(is_chat_node) AS is_chat_node, + argMaxMerge(is_repeater) AS is_repeater, + argMaxMerge(is_room_server) AS is_room_server, + argMaxMerge(has_location) AS has_location, + argMaxMerge(has_feature1) AS has_feature1, + argMaxMerge(has_feature2) AS has_feature2, + argMaxMerge(has_name) AS has_name, + argMaxMerge(latitude_i) AS latitude_i, + argMaxMerge(longitude_i) AS longitude_i, + argMaxMerge(latitude) AS latitude, + argMaxMerge(longitude) AS longitude, + argMaxMerge(node_name) AS node_name, + argMaxMerge(node_hash) AS node_hash, + argMaxMerge(packet_hash) AS packet_hash +FROM meshcore_adverts_latest_state +GROUP BY public_key +ORDER BY last_seen DESC; +-- +goose StatementEnd + +-- Restore the migration-003 direct-neighbor MV (no neighbor_is_sensor). +DROP VIEW IF EXISTS meshcore_node_direct_neighbors; +-- +goose StatementBegin +CREATE MATERIALIZED VIEW IF NOT EXISTS meshcore_node_direct_neighbors +REFRESH EVERY 1 HOUR +ENGINE = MergeTree +ORDER BY (node_public_key) +AS +WITH + node_details AS ( + SELECT public_key, node_name, latitude, longitude, has_location, + is_repeater, is_chat_node, is_room_server, has_name, last_seen + FROM meshcore_adverts_latest + ), + directions AS ( + SELECT DISTINCT hex(origin_pubkey) AS node_public_key, public_key AS neighbor_public_key, 'incoming' AS direction + FROM meshcore_adverts + WHERE path_len = 0 AND hex(origin_pubkey) != public_key AND ingest_timestamp >= now() - INTERVAL 7 DAY + UNION ALL + SELECT DISTINCT public_key AS node_public_key, hex(origin_pubkey) AS neighbor_public_key, 'outgoing' AS direction + FROM meshcore_adverts + WHERE path_len = 0 AND hex(origin_pubkey) != public_key AND ingest_timestamp >= now() - INTERVAL 7 DAY + ) +SELECT + d.node_public_key AS node_public_key, + d.neighbor_public_key AS neighbor_public_key, + d.direction AS direction, + nd.node_name AS neighbor_name, + nd.latitude AS neighbor_latitude, + nd.longitude AS neighbor_longitude, + nd.has_location AS neighbor_has_location, + nd.is_repeater AS neighbor_is_repeater, + nd.is_chat_node AS neighbor_is_chat_node, + nd.is_room_server AS neighbor_is_room_server, + nd.has_name AS neighbor_has_name, + nd.last_seen AS neighbor_last_seen +FROM directions AS d +INNER JOIN node_details AS nd ON d.neighbor_public_key = nd.public_key; +-- +goose StatementEnd +SYSTEM REFRESH VIEW meshcore_node_direct_neighbors; diff --git a/meshexplorer/src/app/(app)/api-docs/page.tsx b/meshexplorer/src/app/(app)/api-docs/page.tsx index a57b82e..11ecb8e 100644 --- a/meshexplorer/src/app/(app)/api-docs/page.tsx +++ b/meshexplorer/src/app/(app)/api-docs/page.tsx @@ -266,6 +266,7 @@ export default function ApiDocsPage() {
  • is_repeater - Whether the node acts as a repeater (0/1)
  • is_chat_node - Whether the node supports chat (0/1)
  • is_room_server - Whether the node is a room server (0/1)
  • +
  • is_sensor - Whether the node is a sensor (0/1)
  • has_name - Whether the node has a name (0/1)
  • last_seen - Most recent activity timestamp
  • first_seen - First time this node was observed
  • @@ -280,7 +281,7 @@ export default function ApiDocsPage() {
  • advert_count - Number of adverts in this group
  • earliest_timestamp/latest_timestamp - Time range for this advert group
  • latitude/longitude - Position at time of advert
  • -
  • is_repeater/is_chat_node/is_room_server/has_location - Node capabilities at time of advert
  • +
  • is_repeater/is_chat_node/is_room_server/is_sensor/has_location - Node capabilities at time of advert
  • @@ -325,6 +326,7 @@ export default function ApiDocsPage() { "is_repeater": 1, "is_chat_node": 1, "is_room_server": 0, + "is_sensor": 0, "has_name": 1, "last_seen": "2025-09-07T00:59:18", "first_seen": "2025-09-01T10:00:00" @@ -343,6 +345,7 @@ export default function ApiDocsPage() { "is_repeater": 1, "is_chat_node": 1, "is_room_server": 0, + "is_sensor": 0, "has_location": 1 } ], diff --git a/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx b/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx index f4aaa8f..1a328c9 100644 --- a/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx +++ b/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx @@ -25,7 +25,8 @@ function getNodeType(node: NodeInfo): number { if (node.is_chat_node) return 1; // companion if (node.is_repeater) return 2; // repeater if (node.is_room_server) return 3; // room - return 4; // sensor (default for standard nodes) + if (node.is_sensor) return 4; // sensor + return 1; // default to companion for QR contact import when the type is unadvertised } export default function MeshcoreNodePage() { @@ -242,7 +243,12 @@ export default function MeshcoreNodePage() { Room Server ) || null} - {!node.is_repeater && !node.is_chat_node && !node.is_room_server && ( + {node.is_sensor && ( + + Sensor + + ) || null} + {!node.is_repeater && !node.is_chat_node && !node.is_room_server && !node.is_sensor && ( Unknown @@ -472,6 +478,11 @@ export default function MeshcoreNodePage() { Room ) || null} + {neighbor.is_sensor && ( + + Sensor + + ) || null}
    diff --git a/meshexplorer/src/components/AdvertDetails.tsx b/meshexplorer/src/components/AdvertDetails.tsx index a0cefda..c01ecb5 100644 --- a/meshexplorer/src/components/AdvertDetails.tsx +++ b/meshexplorer/src/components/AdvertDetails.tsx @@ -17,6 +17,7 @@ interface AdvertDetailsProps { is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; has_location: number; packet_hash: string; hash_size?: number; // bytes per path hop (1/2/3); used to split path into hops @@ -66,6 +67,11 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai S ) || null} + {advert.is_sensor && ( + + Se + + ) || null}
    ) || null} - {!advert.is_repeater && !advert.is_chat_node && !advert.is_room_server && ( + {advert.is_sensor && ( + + Sensor + + ) || null} + {!advert.is_repeater && !advert.is_chat_node && !advert.is_room_server && !advert.is_sensor && ( Unknown diff --git a/meshexplorer/src/components/NodeCard.tsx b/meshexplorer/src/components/NodeCard.tsx index 72cce0f..6e18d02 100644 --- a/meshexplorer/src/components/NodeCard.tsx +++ b/meshexplorer/src/components/NodeCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { MapPinIcon, WifiIcon, ChatBubbleLeftRightIcon, ServerIcon } from '@heroicons/react/24/outline'; +import { MapPinIcon, WifiIcon, ChatBubbleLeftRightIcon, ServerIcon, SignalIcon } from '@heroicons/react/24/outline'; import Link from 'next/link'; import moment from 'moment'; import { formatPublicKey } from '@/lib/meshcore'; @@ -15,6 +15,7 @@ export interface NodeCardData { is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; last_seen: string; topic?: string; broker?: string; @@ -31,6 +32,7 @@ export default function NodeCard({ node, className = "", showTopicInfo = true }: const isRepeater = node.is_repeater === 1; const isChatNode = node.is_chat_node === 1; const isRoomServer = node.is_room_server === 1; + const isSensor = node.is_sensor === 1; return ( )} + {isSensor && ( + + + Sensor + + )} diff --git a/meshexplorer/src/hooks/useMeshcoreSearch.ts b/meshexplorer/src/hooks/useMeshcoreSearch.ts index 067f35a..2a09d49 100644 --- a/meshexplorer/src/hooks/useMeshcoreSearch.ts +++ b/meshexplorer/src/hooks/useMeshcoreSearch.ts @@ -12,6 +12,7 @@ export interface MeshcoreSearchResult { is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; has_name: number; first_heard: string; last_seen: string; diff --git a/meshexplorer/src/hooks/useNeighbors.ts b/meshexplorer/src/hooks/useNeighbors.ts index 8cf12bd..8e313cc 100644 --- a/meshexplorer/src/hooks/useNeighbors.ts +++ b/meshexplorer/src/hooks/useNeighbors.ts @@ -10,6 +10,7 @@ export interface Neighbor { is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; has_name: number; directions: string[]; } diff --git a/meshexplorer/src/hooks/useNodeData.ts b/meshexplorer/src/hooks/useNodeData.ts index 4cae070..68c0535 100644 --- a/meshexplorer/src/hooks/useNodeData.ts +++ b/meshexplorer/src/hooks/useNodeData.ts @@ -10,6 +10,7 @@ export interface NodeInfo { is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; has_name: number; broker: string | null; topic: string | null; @@ -28,6 +29,7 @@ export interface Advert { is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; has_location: number; packet_hash: string; hash_size?: number; // bytes per path hop (1/2/3); used to split path into hops diff --git a/meshexplorer/src/lib/clickhouse/actions.ts b/meshexplorer/src/lib/clickhouse/actions.ts index ee40168..1599858 100644 --- a/meshexplorer/src/lib/clickhouse/actions.ts +++ b/meshexplorer/src/lib/clickhouse/actions.ts @@ -134,6 +134,7 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50) argMax(is_repeater, ingest_timestamp) as is_repeater, argMax(is_chat_node, ingest_timestamp) as is_chat_node, argMax(is_room_server, ingest_timestamp) as is_room_server, + argMax(is_sensor, ingest_timestamp) as is_sensor, argMax(has_name, ingest_timestamp) as has_name, argMax(broker, ingest_timestamp) as broker, argMax(topic, ingest_timestamp) as topic, @@ -159,6 +160,7 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50) is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; has_name: number; broker: string | null; topic: string | null; @@ -188,6 +190,7 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50) argMax(is_repeater, ingest_timestamp) as is_repeater, argMax(is_chat_node, ingest_timestamp) as is_chat_node, argMax(is_room_server, ingest_timestamp) as is_room_server, + argMax(is_sensor, ingest_timestamp) as is_sensor, argMax(has_location, ingest_timestamp) as has_location, any(hash_size) as hash_size, packet_hash @@ -204,6 +207,7 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50) is_repeater, is_chat_node, is_room_server, + is_sensor, has_location, hex(origin_pubkey) as origin_pubkey, origin, @@ -425,6 +429,7 @@ export async function getMeshcoreNodeNeighbors(publicKey: string, lastSeen: stri any(neighbor_is_repeater) AS is_repeater, any(neighbor_is_chat_node) AS is_chat_node, any(neighbor_is_room_server) AS is_room_server, + any(neighbor_is_sensor) AS is_sensor, any(neighbor_has_name) AS has_name, groupUniqArray(direction) AS directions FROM meshcore_node_direct_neighbors @@ -449,6 +454,7 @@ export async function getMeshcoreNodeNeighbors(publicKey: string, lastSeen: stri is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; has_name: number; directions: string[]; }>; @@ -587,6 +593,7 @@ export async function searchMeshcoreNodes(searchParams: SearchQuery | SearchQuer is_repeater, is_chat_node, is_room_server, + is_sensor, has_name, first_heard, last_seen, @@ -607,6 +614,7 @@ export async function searchMeshcoreNodes(searchParams: SearchQuery | SearchQuer is_repeater, is_chat_node, is_room_server, + is_sensor, has_name, first_heard, last_seen, @@ -639,6 +647,7 @@ export async function searchMeshcoreNodes(searchParams: SearchQuery | SearchQuer is_repeater: number; is_chat_node: number; is_room_server: number; + is_sensor: number; has_name: number; first_heard: string; last_seen: string; diff --git a/meshexplorer/src/lib/clickhouse/streaming.ts b/meshexplorer/src/lib/clickhouse/streaming.ts index c224722..93ad432 100644 --- a/meshexplorer/src/lib/clickhouse/streaming.ts +++ b/meshexplorer/src/lib/clickhouse/streaming.ts @@ -219,6 +219,7 @@ export function createMeshcoreAdvertsStreamerConfig( is_repeater, is_chat_node, is_room_server, + is_sensor, has_name, broker, topic,