mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-07-05 09:11:00 +02:00
Decode advert node type as enum; add sensor type; drop empty packets
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -266,6 +266,7 @@ export default function ApiDocsPage() {
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">is_repeater</code> - Whether the node acts as a repeater (0/1)</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">is_chat_node</code> - Whether the node supports chat (0/1)</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">is_room_server</code> - Whether the node is a room server (0/1)</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">is_sensor</code> - Whether the node is a sensor (0/1)</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">has_name</code> - Whether the node has a name (0/1)</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">last_seen</code> - Most recent activity timestamp</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">first_seen</code> - First time this node was observed</li>
|
||||
@@ -280,7 +281,7 @@ export default function ApiDocsPage() {
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">advert_count</code> - Number of adverts in this group</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">earliest_timestamp/latest_timestamp</code> - Time range for this advert group</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">latitude/longitude</code> - Position at time of advert</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">is_repeater/is_chat_node/is_room_server/has_location</code> - Node capabilities at time of advert</li>
|
||||
<li><code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded">is_repeater/is_chat_node/is_room_server/is_sensor/has_location</code> - Node capabilities at time of advert</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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
|
||||
</span>
|
||||
) || null}
|
||||
{!node.is_repeater && !node.is_chat_node && !node.is_room_server && (
|
||||
{node.is_sensor && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200">
|
||||
Sensor
|
||||
</span>
|
||||
) || null}
|
||||
{!node.is_repeater && !node.is_chat_node && !node.is_room_server && !node.is_sensor && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Unknown
|
||||
</span>
|
||||
@@ -472,6 +478,11 @@ export default function MeshcoreNodePage() {
|
||||
Room
|
||||
</span>
|
||||
) || null}
|
||||
{neighbor.is_sensor && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200">
|
||||
Sensor
|
||||
</span>
|
||||
) || null}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||
|
||||
@@ -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
|
||||
</span>
|
||||
) || null}
|
||||
{advert.is_sensor && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200">
|
||||
Se
|
||||
</span>
|
||||
) || null}
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${
|
||||
@@ -158,7 +164,12 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai
|
||||
Room Server
|
||||
</span>
|
||||
) || null}
|
||||
{!advert.is_repeater && !advert.is_chat_node && !advert.is_room_server && (
|
||||
{advert.is_sensor && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200">
|
||||
Sensor
|
||||
</span>
|
||||
) || null}
|
||||
{!advert.is_repeater && !advert.is_chat_node && !advert.is_room_server && !advert.is_sensor && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Unknown
|
||||
</span>
|
||||
|
||||
@@ -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 (
|
||||
<Link
|
||||
@@ -62,6 +64,12 @@ export default function NodeCard({ node, className = "", showTopicInfo = true }:
|
||||
Room Server
|
||||
</span>
|
||||
)}
|
||||
{isSensor && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200">
|
||||
<SignalIcon className="h-3 w-3 mr-1" />
|
||||
Sensor
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -219,6 +219,7 @@ export function createMeshcoreAdvertsStreamerConfig(
|
||||
is_repeater,
|
||||
is_chat_node,
|
||||
is_room_server,
|
||||
is_sensor,
|
||||
has_name,
|
||||
broker,
|
||||
topic,
|
||||
|
||||
Reference in New Issue
Block a user