Merge pull request #51 from ajvpot/ajvpot/add-cascadiamesh-broker

Accept numeric SNR/len/RSSI in MeshCore packets messages
This commit is contained in:
Alex Vanderpot
2026-06-19 03:08:06 -04:00
committed by GitHub
2 changed files with 94 additions and 17 deletions
+43 -17
View File
@@ -177,25 +177,51 @@ func parseMeshCoreRawMessage(payload []byte) (origin string, originPubkey []byte
return cleanOrigin, originPubkeyBytes, meshTimestamp, packet, nil
}
// flexString unmarshals a JSON value that may be encoded as either a string or
// a number into a string. Depending on gateway firmware, the numeric metric
// fields below — SNR, len, etc. — arrive as JSON numbers (including negative
// and fractional values) rather than strings; without this, a single numeric
// field would fail the whole packet's unmarshal and drop the message, including
// the raw packet bytes we actually need.
type flexString string
func (f *flexString) UnmarshalJSON(data []byte) error {
if len(data) == 0 || string(data) == "null" {
*f = ""
return nil
}
if data[0] == '"' {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
*f = flexString(s)
return nil
}
// Non-string (number, bool): keep the raw JSON token as its text form.
*f = flexString(data)
return nil
}
func parseMeshCorePacketsMessage(payload []byte) (origin string, originPubkey []byte, meshTimestamp time.Time, packet []byte, err error) {
type PacketMessage struct {
Origin string `json:"origin"`
OriginID string `json:"origin_id"`
Timestamp string `json:"timestamp"`
Type string `json:"type"`
Direction string `json:"direction"`
Time string `json:"time"`
Date string `json:"date"`
Len string `json:"len"`
PacketType string `json:"packet_type"`
Route string `json:"route"`
PayloadLen string `json:"payload_len"`
Raw string `json:"raw"`
SNR string `json:"SNR"`
RSSI string `json:"RSSI"`
Score string `json:"score"`
Duration string `json:"duration"`
Hash string `json:"hash"`
Origin string `json:"origin"`
OriginID string `json:"origin_id"`
Timestamp string `json:"timestamp"`
Type string `json:"type"`
Direction string `json:"direction"`
Time string `json:"time"`
Date string `json:"date"`
Len flexString `json:"len"`
PacketType flexString `json:"packet_type"`
Route string `json:"route"`
PayloadLen flexString `json:"payload_len"`
Raw string `json:"raw"`
SNR flexString `json:"SNR"`
RSSI flexString `json:"RSSI"`
Score flexString `json:"score"`
Duration flexString `json:"duration"`
Hash string `json:"hash"`
}
var pkt PacketMessage
if err = json.Unmarshal(payload, &pkt); err != nil {
+51
View File
@@ -265,3 +265,54 @@ func TestExtractGatewayID(t *testing.T) {
})
}
}
// Some gateways encode the numeric metric fields — SNR, RSSI, len, etc. — as
// bare JSON numbers, including negative and fractional values, rather than
// strings. These must parse rather than dropping the whole packet (and the raw
// bytes we need with it).
func TestParseMeshCorePacketsMessage_NumericMetrics(t *testing.T) {
payload := []byte(`{
"timestamp": "2026-06-19T07:03:43.000000",
"origin": "PDX Gateway Bridge 14",
"origin_id": "FACE69B85A0D184C7D122164BDFADE87F25069455C85DD2824CAD3F6AA0B1929",
"type": "PACKET", "direction": "rx", "len": 38, "payload_len": 20,
"packet_type": 1, "route": "F",
"raw": "05481D6B54CA61006000AEE498916968452A7994F827AFB6CE312721FFBE377BA3D113F924C6",
"SNR": -9.25, "RSSI": -95, "score": 1000, "duration": 0
}`)
origin, originPubkey, meshTimestamp, packet, err := parseMeshCorePacketsMessage(payload)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if origin != "PDX Gateway Bridge 14" {
t.Errorf("unexpected origin: %s", origin)
}
if hex.EncodeToString(originPubkey) != strings.ToLower("FACE69B85A0D184C7D122164BDFADE87F25069455C85DD2824CAD3F6AA0B1929") {
t.Errorf("unexpected origin_pubkey: %s", hex.EncodeToString(originPubkey))
}
if meshTimestamp.Format(time.RFC3339Nano) != "2026-06-19T07:03:43Z" {
t.Errorf("unexpected meshTimestamp: %s", meshTimestamp)
}
if len(packet) == 0 {
t.Error("packet should not be empty")
}
}
// The same fields quoted as strings (other gateways) must keep working too.
func TestParseMeshCorePacketsMessage_StringMetrics(t *testing.T) {
payload := []byte(`{
"timestamp": "2026-06-19T07:03:43.000000",
"origin": "PDX Gateway Bridge 14",
"origin_id": "FACE69B85A0D184C7D122164BDFADE87F25069455C85DD2824CAD3F6AA0B1929",
"type": "PACKET", "direction": "rx", "len": "38", "payload_len": "20",
"raw": "05481D6B54CA61006000AEE498916968452A7994F827AFB6CE312721FFBE377BA3D113F924C6",
"SNR": "11.8", "RSSI": "-38"
}`)
_, _, _, packet, err := parseMeshCorePacketsMessage(payload)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(packet) == 0 {
t.Error("packet should not be empty")
}
}