diff --git a/README.md b/README.md
index 9801ce1..f051be3 100644
--- a/README.md
+++ b/README.md
@@ -116,8 +116,12 @@ go run .
```text
GET /api/health
-GET /api/nodes
-GET /api/nodes/:id
+GET /api/nodeinfo
+GET /api/nodeinfo/:id
+GET /api/map-reports
+GET /api/map-reports/:id
+GET /api/nodes # /api/nodeinfo 的兼容别名
+GET /api/nodes/:id # /api/nodeinfo/:id 的兼容别名
GET /api/text-messages
GET /api/positions
GET /api/telemetry
@@ -145,21 +149,23 @@ meshtastic:
程序默认启用 SQLite,数据库表迁移和操作由 GORM 执行,并持久化以下数据:
-- `nodeinfo_map`:融合 `type == "nodeinfo"` 和 `type == "map_report"` 的节点信息
+- `nodeinfo`:保存 `type == "nodeinfo"` 的节点身份和设备信息
+- `map_report`:保存 `type == "map_report"` 的地图报告信息,前端地图从该表读取
- `text_message`:追加保存 `type == "text_message"` 的聊天消息
- `position`:追加保存 `type == "position"` 的位置包
- `telemetry`:追加保存 `type == "telemetry"` 的遥测包
- `routing`:追加保存 `type == "routing"` 的路由控制包
- `traceroute`:追加保存 `type == "traceroute"` 的路径追踪包
-`nodeinfo_map` 规则:
+`nodeinfo` / `map_report` 规则:
-- `nodeinfo` 表不再使用;如果旧数据库中已经存在该表,程序不会自动删除它
-- 同一节点以 `node_id`(即解析结果中的 `from`,例如 `!a8dfd867`)作为主键
-- 重复收到同一节点时不会插入重复行,只更新 `updated_at`、`content_json`、`latest_type` 和本次记录中有值的字段
-- `nodeinfo` 独有字段和 `map_report` 独有字段会互相保留;例如后续 `map_report` 不会清空已有的 `public_key`
+- 两张表都以 `node_id`(即解析结果中的 `from`,例如 `!a8dfd867`)作为主键
+- `nodeinfo` 只保存节点身份和设备字段,例如 `user_id`、名称、硬件型号、角色、授权状态和公钥
+- `map_report` 只保存地图报告字段,例如名称、硬件型号、角色、固件版本、区域、调制预设、经纬度、海拔、位置精度和在线节点数
+- 重复收到同一节点时不会插入重复行,只更新 `updated_at`、`content_json` 和本次记录中有值的字段
- `first_seen_at` 保留第一次写入时间
-- `content_json` 保存最新一次 `nodeinfo` 或 `map_report` 的完整解析结果 JSON
+- `content_json` 分别保存最新一次 `nodeinfo` 或 `map_report` 的完整解析结果 JSON
+- 旧版本创建的 `nodeinfo_map` 融合表不会被自动删除,新版本不再写入该表;新表会从新收到的数据开始填充
`text_message` 规则:
diff --git a/db.go b/db.go
index 45ee6a2..49b3984 100644
--- a/db.go
+++ b/db.go
@@ -62,33 +62,48 @@ type MQTTClientRecordFields struct {
MQTTRemotePort *string `gorm:"column:mqtt_remote_port"`
}
-type nodeInfoMapRecord struct {
+type nodeInfoRecord struct {
+ NodeID string `gorm:"column:node_id;primaryKey;not null"`
+ NodeNum int64 `gorm:"column:node_num;not null;index"`
+ UserID *string `gorm:"column:user_id"`
+ LongName *string `gorm:"column:long_name"`
+ ShortName *string `gorm:"column:short_name"`
+ HWModel *string `gorm:"column:hw_model"`
+ Role *string `gorm:"column:role"`
+ IsLicensed *bool `gorm:"column:is_licensed"`
+ PublicKey *string `gorm:"column:public_key"`
+ ContentJSON string `gorm:"column:content_json;not null"`
+ FirstSeenAt time.Time `gorm:"column:first_seen_at;autoCreateTime"`
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index"`
+}
+
+func (nodeInfoRecord) TableName() string {
+ return "nodeinfo"
+}
+
+type mapReportRecord struct {
NodeID string `gorm:"column:node_id;primaryKey;not null"`
- NodeNum int64 `gorm:"column:node_num;not null"`
- LatestType string `gorm:"column:latest_type;not null"`
- UserID *string `gorm:"column:user_id"`
+ NodeNum int64 `gorm:"column:node_num;not null;index"`
LongName *string `gorm:"column:long_name"`
ShortName *string `gorm:"column:short_name"`
HWModel *string `gorm:"column:hw_model"`
Role *string `gorm:"column:role"`
- IsLicensed *bool `gorm:"column:is_licensed"`
- PublicKey *string `gorm:"column:public_key"`
FirmwareVersion *string `gorm:"column:firmware_version"`
Region *string `gorm:"column:region"`
ModemPreset *string `gorm:"column:modem_preset"`
- Latitude *float64 `gorm:"column:latitude"`
- Longitude *float64 `gorm:"column:longitude"`
+ Latitude *float64 `gorm:"column:latitude;index"`
+ Longitude *float64 `gorm:"column:longitude;index"`
Altitude *int64 `gorm:"column:altitude"`
PositionPrecision *int64 `gorm:"column:position_precision"`
NumOnlineLocalNodes *int64 `gorm:"column:num_online_local_nodes"`
HasOptedReportLocation *bool `gorm:"column:has_opted_report_location"`
ContentJSON string `gorm:"column:content_json;not null"`
FirstSeenAt time.Time `gorm:"column:first_seen_at;autoCreateTime"`
- UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index"`
}
-func (nodeInfoMapRecord) TableName() string {
- return "nodeinfo_map"
+func (mapReportRecord) TableName() string {
+ return "map_report"
}
type textMessageRecord struct {
@@ -239,7 +254,8 @@ func (s *store) migrate() error {
label string
model any
}{
- {label: "nodeinfo_map", model: &nodeInfoMapRecord{}},
+ {label: "nodeinfo", model: &nodeInfoRecord{}},
+ {label: "map_report", model: &mapReportRecord{}},
{label: "text_message", model: &textMessageRecord{}},
{label: "position", model: &positionRecord{}},
{label: "telemetry", model: &telemetryRecord{}},
@@ -278,43 +294,75 @@ func createMissingIndexes(migrator gorm.Migrator, model any, label string, index
return nil
}
-func (s *store) UpsertNodeInfoMap(record map[string]any) error {
- node, err := nodeInfoMapFromRecord(record)
+func (s *store) UpsertNodeInfo(record map[string]any) error {
+ node, err := nodeInfoFromRecord(record)
if err != nil {
return err
}
- if err := s.upsertNodeInfoMapRecord(node); err != nil {
- return fmt.Errorf("upsert nodeinfo_map %s: %w", node.NodeID, err)
+ if err := s.upsertNodeInfoRecord(node); err != nil {
+ return fmt.Errorf("upsert nodeinfo %s: %w", node.NodeID, err)
}
return nil
}
-func (s *store) upsertNodeInfoMapRecord(node *nodeInfoMapRecord) error {
+func (s *store) UpsertMapReport(record map[string]any) error {
+ report, err := mapReportFromRecord(record)
+ if err != nil {
+ return err
+ }
+ if err := s.upsertMapReportRecord(report); err != nil {
+ return fmt.Errorf("upsert map_report %s: %w", report.NodeID, err)
+ }
+ return nil
+}
+
+func (s *store) upsertNodeInfoRecord(node *nodeInfoRecord) error {
return s.db.Transaction(func(tx *gorm.DB) error {
- var existing nodeInfoMapRecord
+ var existing nodeInfoRecord
err := tx.Where("node_id = ?", node.NodeID).Take(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
if err := tx.Create(node).Error; err != nil {
- return s.updateNodeInfoMapRecord(tx, node)
+ return s.updateNodeInfoRecord(tx, node)
}
return nil
}
if err != nil {
return err
}
- return s.updateNodeInfoMapRecord(tx, node)
+ return s.updateNodeInfoRecord(tx, node)
})
}
-func (s *store) updateNodeInfoMapRecord(tx *gorm.DB, node *nodeInfoMapRecord) error {
- updates := nodeInfoMapUpdates(node)
- return tx.Model(&nodeInfoMapRecord{}).Where("node_id = ?", node.NodeID).Updates(updates).Error
+func (s *store) upsertMapReportRecord(report *mapReportRecord) error {
+ return s.db.Transaction(func(tx *gorm.DB) error {
+ var existing mapReportRecord
+ err := tx.Where("node_id = ?", report.NodeID).Take(&existing).Error
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ if err := tx.Create(report).Error; err != nil {
+ return s.updateMapReportRecord(tx, report)
+ }
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ return s.updateMapReportRecord(tx, report)
+ })
}
-func nodeInfoMapUpdates(node *nodeInfoMapRecord) map[string]any {
+func (s *store) updateNodeInfoRecord(tx *gorm.DB, node *nodeInfoRecord) error {
+ updates := nodeInfoUpdates(node)
+ return tx.Model(&nodeInfoRecord{}).Where("node_id = ?", node.NodeID).Updates(updates).Error
+}
+
+func (s *store) updateMapReportRecord(tx *gorm.DB, report *mapReportRecord) error {
+ updates := mapReportUpdates(report)
+ return tx.Model(&mapReportRecord{}).Where("node_id = ?", report.NodeID).Updates(updates).Error
+}
+
+func nodeInfoUpdates(node *nodeInfoRecord) map[string]any {
updates := map[string]any{
"node_num": node.NodeNum,
- "latest_type": node.LatestType,
"content_json": node.ContentJSON,
"updated_at": time.Now(),
}
@@ -325,15 +373,28 @@ func nodeInfoMapUpdates(node *nodeInfoMapRecord) map[string]any {
addStringUpdate(updates, "role", node.Role)
addBoolUpdate(updates, "is_licensed", node.IsLicensed)
addStringUpdate(updates, "public_key", node.PublicKey)
- addStringUpdate(updates, "firmware_version", node.FirmwareVersion)
- addStringUpdate(updates, "region", node.Region)
- addStringUpdate(updates, "modem_preset", node.ModemPreset)
- addFloat64Update(updates, "latitude", node.Latitude)
- addFloat64Update(updates, "longitude", node.Longitude)
- addInt64Update(updates, "altitude", node.Altitude)
- addInt64Update(updates, "position_precision", node.PositionPrecision)
- addInt64Update(updates, "num_online_local_nodes", node.NumOnlineLocalNodes)
- addBoolUpdate(updates, "has_opted_report_location", node.HasOptedReportLocation)
+ return updates
+}
+
+func mapReportUpdates(report *mapReportRecord) map[string]any {
+ updates := map[string]any{
+ "node_num": report.NodeNum,
+ "content_json": report.ContentJSON,
+ "updated_at": time.Now(),
+ }
+ addStringUpdate(updates, "long_name", report.LongName)
+ addStringUpdate(updates, "short_name", report.ShortName)
+ addStringUpdate(updates, "hw_model", report.HWModel)
+ addStringUpdate(updates, "role", report.Role)
+ addStringUpdate(updates, "firmware_version", report.FirmwareVersion)
+ addStringUpdate(updates, "region", report.Region)
+ addStringUpdate(updates, "modem_preset", report.ModemPreset)
+ addFloat64Update(updates, "latitude", report.Latitude)
+ addFloat64Update(updates, "longitude", report.Longitude)
+ addInt64Update(updates, "altitude", report.Altitude)
+ addInt64Update(updates, "position_precision", report.PositionPrecision)
+ addInt64Update(updates, "num_online_local_nodes", report.NumOnlineLocalNodes)
+ addBoolUpdate(updates, "has_opted_report_location", report.HasOptedReportLocation)
return updates
}
@@ -392,35 +453,47 @@ func (s *store) InsertTraceroute(record map[string]any, clientInfo mqttClientInf
return nil
}
-func nodeInfoMapFromRecord(record map[string]any) (*nodeInfoMapRecord, error) {
- latestType, ok := record["type"].(string)
- if !ok || (latestType != "nodeinfo" && latestType != "map_report") {
- return nil, fmt.Errorf("record type %v is not nodeinfo or map_report", record["type"])
+func nodeInfoFromRecord(record map[string]any) (*nodeInfoRecord, error) {
+ recordType, ok := record["type"].(string)
+ if !ok || recordType != "nodeinfo" {
+ return nil, fmt.Errorf("record type %v is not nodeinfo", record["type"])
}
- nodeID, ok := record["from"].(string)
- if !ok || nodeID == "" {
- return nil, fmt.Errorf("nodeinfo_map missing from")
- }
- nodeNum, err := int64FromAny(record["from_num"])
+ nodeID, nodeNum, contentJSON, err := nodeRecordBase(record, "nodeinfo")
if err != nil {
- return nil, fmt.Errorf("nodeinfo_map from_num: %w", err)
- }
- contentJSON, err := json.Marshal(record)
- if err != nil {
- return nil, fmt.Errorf("encode nodeinfo_map content_json: %w", err)
+ return nil, err
}
- return &nodeInfoMapRecord{
+ return &nodeInfoRecord{
+ NodeID: nodeID,
+ NodeNum: nodeNum,
+ UserID: nullableString(record["user_id"]),
+ LongName: nullableString(record["long_name"]),
+ ShortName: nullableString(record["short_name"]),
+ HWModel: nullableString(record["hw_model"]),
+ Role: nullableString(record["role"]),
+ IsLicensed: nullableBool(record["is_licensed"]),
+ PublicKey: nullableString(record["public_key"]),
+ ContentJSON: contentJSON,
+ }, nil
+}
+
+func mapReportFromRecord(record map[string]any) (*mapReportRecord, error) {
+ recordType, ok := record["type"].(string)
+ if !ok || recordType != "map_report" {
+ return nil, fmt.Errorf("record type %v is not map_report", record["type"])
+ }
+ nodeID, nodeNum, contentJSON, err := nodeRecordBase(record, "map_report")
+ if err != nil {
+ return nil, err
+ }
+
+ return &mapReportRecord{
NodeID: nodeID,
NodeNum: nodeNum,
- LatestType: latestType,
- UserID: nullableString(record["user_id"]),
LongName: nullableString(record["long_name"]),
ShortName: nullableString(record["short_name"]),
HWModel: nullableString(record["hw_model"]),
Role: nullableString(record["role"]),
- IsLicensed: nullableBool(record["is_licensed"]),
- PublicKey: nullableString(record["public_key"]),
FirmwareVersion: nullableString(record["firmware_version"]),
Region: nullableString(record["region"]),
ModemPreset: nullableString(record["modem_preset"]),
@@ -430,10 +503,26 @@ func nodeInfoMapFromRecord(record map[string]any) (*nodeInfoMapRecord, error) {
PositionPrecision: nullableInt64(record["position_precision"]),
NumOnlineLocalNodes: nullableInt64(record["num_online_local_nodes"]),
HasOptedReportLocation: nullableBool(record["has_opted_report_location"]),
- ContentJSON: string(contentJSON),
+ ContentJSON: contentJSON,
}, nil
}
+func nodeRecordBase(record map[string]any, label string) (string, int64, string, error) {
+ nodeID, ok := record["from"].(string)
+ if !ok || nodeID == "" {
+ return "", 0, "", fmt.Errorf("%s missing from", label)
+ }
+ nodeNum, err := int64FromAny(record["from_num"])
+ if err != nil {
+ return "", 0, "", fmt.Errorf("%s from_num: %w", label, err)
+ }
+ contentJSON, err := json.Marshal(record)
+ if err != nil {
+ return "", 0, "", fmt.Errorf("encode %s content_json: %w", label, err)
+ }
+ return nodeID, nodeNum, string(contentJSON), nil
+}
+
func textMessageFromRecord(record map[string]any, clientInfo mqttClientInfo) (*textMessageRecord, error) {
recordType, ok := record["type"].(string)
if !ok || recordType != "text_message" {
diff --git a/db_test.go b/db_test.go
index e616b7f..7244ea6 100644
--- a/db_test.go
+++ b/db_test.go
@@ -11,7 +11,7 @@ func TestOpenStoreCreatesTables(t *testing.T) {
st := openTestStore(t)
defer st.Close()
- for _, table := range []string{"nodeinfo_map", "text_message", "position", "telemetry", "routing", "traceroute"} {
+ for _, table := range []string{"nodeinfo", "map_report", "text_message", "position", "telemetry", "routing", "traceroute"} {
var name string
if err := rawTestDB(t, st).QueryRow("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table).Scan(&name); err != nil {
t.Fatalf("%s table missing: %v", table, err)
@@ -22,160 +22,164 @@ func TestOpenStoreCreatesTables(t *testing.T) {
}
var oldCount int
- if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'nodeinfo'").Scan(&oldCount); err != nil {
+ if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'nodeinfo_map'").Scan(&oldCount); err != nil {
t.Fatal(err)
}
if oldCount != 0 {
- t.Fatalf("old nodeinfo table count = %d, want 0", oldCount)
+ t.Fatalf("nodeinfo_map table count = %d, want 0", oldCount)
}
}
-func TestUpsertNodeInfoMapInsertsAndUpdatesSameNode(t *testing.T) {
+func TestUpsertNodeInfoInsertsAndUpdatesSameNode(t *testing.T) {
st := openTestStore(t)
defer st.Close()
- first := nodeInfoRecord("first name")
- if err := st.UpsertNodeInfoMap(first); err != nil {
- t.Fatalf("first UpsertNodeInfoMap() error = %v", err)
+ first := nodeInfoTestRecord("first name")
+ if err := st.UpsertNodeInfo(first); err != nil {
+ t.Fatalf("first UpsertNodeInfo() error = %v", err)
}
- second := nodeInfoRecord("second name")
+ second := nodeInfoTestRecord("second name")
second["short_name"] = "snd"
- if err := st.UpsertNodeInfoMap(second); err != nil {
- t.Fatalf("second UpsertNodeInfoMap() error = %v", err)
+ if err := st.UpsertNodeInfo(second); err != nil {
+ t.Fatalf("second UpsertNodeInfo() error = %v", err)
}
var count int
- if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM nodeinfo_map WHERE node_id = ?", "!12345678").Scan(&count); err != nil {
+ if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&count); err != nil {
t.Fatal(err)
}
if count != 1 {
- t.Fatalf("node row count = %d, want 1", count)
+ t.Fatalf("nodeinfo row count = %d, want 1", count)
}
- var latestType, longName, content string
- if err := rawTestDB(t, st).QueryRow("SELECT latest_type, long_name, content_json FROM nodeinfo_map WHERE node_id = ?", "!12345678").Scan(&latestType, &longName, &content); err != nil {
+ var longName, shortName, content string
+ if err := rawTestDB(t, st).QueryRow("SELECT long_name, short_name, content_json FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&longName, &shortName, &content); err != nil {
t.Fatal(err)
}
- if latestType != "nodeinfo" {
- t.Fatalf("latest_type = %q, want nodeinfo", latestType)
- }
- if longName != "second name" {
- t.Fatalf("long_name = %q, want second name", longName)
+ if longName != "second name" || shortName != "snd" {
+ t.Fatalf("nodeinfo names = %q/%q, want second name/snd", longName, shortName)
}
if !strings.Contains(content, "second name") {
t.Fatalf("content_json = %q, want updated content", content)
}
}
-func TestUpsertNodeInfoMapMergesNodeInfoThenMapReport(t *testing.T) {
+func TestUpsertMapReportInsertsAndUpdatesSameNode(t *testing.T) {
st := openTestStore(t)
defer st.Close()
- if err := st.UpsertNodeInfoMap(nodeInfoRecord("node name")); err != nil {
- t.Fatalf("nodeinfo UpsertNodeInfoMap() error = %v", err)
+ first := mapReportTestRecord("first map")
+ if err := st.UpsertMapReport(first); err != nil {
+ t.Fatalf("first UpsertMapReport() error = %v", err)
}
- if err := st.UpsertNodeInfoMap(mapReportRecord("map name")); err != nil {
- t.Fatalf("map_report UpsertNodeInfoMap() error = %v", err)
+
+ second := mapReportTestRecord("second map")
+ second["latitude"] = 43.5
+ if err := st.UpsertMapReport(second); err != nil {
+ t.Fatalf("second UpsertMapReport() error = %v", err)
}
var count int
- if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM nodeinfo_map WHERE node_id = ?", "!12345678").Scan(&count); err != nil {
+ if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM map_report WHERE node_id = ?", "!12345678").Scan(&count); err != nil {
t.Fatal(err)
}
if count != 1 {
- t.Fatalf("node row count = %d, want 1", count)
+ t.Fatalf("map_report row count = %d, want 1", count)
}
- var latestType, userID, publicKey, longName, firmware, content string
+ var longName string
var latitude float64
var opted sql.NullBool
- if err := rawTestDB(t, st).QueryRow("SELECT latest_type, user_id, public_key, long_name, firmware_version, latitude, has_opted_report_location, content_json FROM nodeinfo_map WHERE node_id = ?", "!12345678").Scan(&latestType, &userID, &publicKey, &longName, &firmware, &latitude, &opted, &content); err != nil {
+ if err := rawTestDB(t, st).QueryRow("SELECT long_name, latitude, has_opted_report_location FROM map_report WHERE node_id = ?", "!12345678").Scan(&longName, &latitude, &opted); err != nil {
t.Fatal(err)
}
- if latestType != "map_report" {
- t.Fatalf("latest_type = %q, want map_report", latestType)
- }
- if userID != "!12345678" || publicKey != "abcd" {
- t.Fatalf("nodeinfo fields not preserved: user_id=%q public_key=%q", userID, publicKey)
- }
- if longName != "map name" {
- t.Fatalf("long_name = %q, want map name", longName)
- }
- if firmware != "1.2.3" {
- t.Fatalf("firmware = %q, want 1.2.3", firmware)
- }
- if latitude != 42.5 {
- t.Fatalf("latitude = %v, want 42.5", latitude)
+ if longName != "second map" || latitude != 43.5 {
+ t.Fatalf("map_report row = %q/%v, want second map/43.5", longName, latitude)
}
if !opted.Valid || opted.Bool {
t.Fatalf("has_opted_report_location = %+v, want valid false", opted)
}
- if !strings.Contains(content, "map_report") {
- t.Fatalf("content_json = %q, want latest map_report content", content)
- }
}
-func TestUpsertNodeInfoMapMergesMapReportThenNodeInfo(t *testing.T) {
+func TestNodeInfoAndMapReportAreStoredSeparately(t *testing.T) {
st := openTestStore(t)
defer st.Close()
- if err := st.UpsertNodeInfoMap(mapReportRecord("map name")); err != nil {
- t.Fatalf("map_report UpsertNodeInfoMap() error = %v", err)
+ if err := st.UpsertNodeInfo(nodeInfoTestRecord("node name")); err != nil {
+ t.Fatalf("UpsertNodeInfo() error = %v", err)
}
- if err := st.UpsertNodeInfoMap(nodeInfoRecord("node name")); err != nil {
- t.Fatalf("nodeinfo UpsertNodeInfoMap() error = %v", err)
+ if err := st.UpsertMapReport(mapReportTestRecord("map name")); err != nil {
+ t.Fatalf("UpsertMapReport() error = %v", err)
}
- var latestType, userID, longName, firmware string
- var latitude float64
- if err := rawTestDB(t, st).QueryRow("SELECT latest_type, user_id, long_name, firmware_version, latitude FROM nodeinfo_map WHERE node_id = ?", "!12345678").Scan(&latestType, &userID, &longName, &firmware, &latitude); err != nil {
+ var nodeLongName, userID, publicKey string
+ if err := rawTestDB(t, st).QueryRow("SELECT long_name, user_id, public_key FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&nodeLongName, &userID, &publicKey); err != nil {
t.Fatal(err)
}
- if latestType != "nodeinfo" {
- t.Fatalf("latest_type = %q, want nodeinfo", latestType)
+ if nodeLongName != "node name" || userID != "!12345678" || publicKey != "abcd" {
+ t.Fatalf("nodeinfo row = %q/%q/%q, want node fields", nodeLongName, userID, publicKey)
}
- if userID != "!12345678" {
- t.Fatalf("user_id = %q, want !12345678", userID)
+
+ var mapLongName, firmware string
+ var latitude float64
+ if err := rawTestDB(t, st).QueryRow("SELECT long_name, firmware_version, latitude FROM map_report WHERE node_id = ?", "!12345678").Scan(&mapLongName, &firmware, &latitude); err != nil {
+ t.Fatal(err)
}
- if longName != "node name" {
- t.Fatalf("long_name = %q, want node name", longName)
- }
- if firmware != "1.2.3" || latitude != 42.5 {
- t.Fatalf("map fields not preserved: firmware=%q latitude=%v", firmware, latitude)
+ if mapLongName != "map name" || firmware != "1.2.3" || latitude != 42.5 {
+ t.Fatalf("map_report row = %q/%q/%v, want map fields", mapLongName, firmware, latitude)
}
}
-func TestUpsertNodeInfoMapRequiresNodeFields(t *testing.T) {
+func TestUpsertNodeInfoRequiresNodeFields(t *testing.T) {
st := openTestStore(t)
defer st.Close()
- if err := st.UpsertNodeInfoMap(map[string]any{"type": "nodeinfo", "from_num": 1}); err == nil || !strings.Contains(err.Error(), "from") {
+ if err := st.UpsertNodeInfo(map[string]any{"type": "nodeinfo", "from_num": 1}); err == nil || !strings.Contains(err.Error(), "from") {
t.Fatalf("missing from error = %v, want from error", err)
}
- if err := st.UpsertNodeInfoMap(map[string]any{"type": "nodeinfo", "from": "!00000001"}); err == nil || !strings.Contains(err.Error(), "from_num") {
+ if err := st.UpsertNodeInfo(map[string]any{"type": "nodeinfo", "from": "!00000001"}); err == nil || !strings.Contains(err.Error(), "from_num") {
t.Fatalf("missing from_num error = %v, want from_num error", err)
}
}
-func TestNodeInfoMapFromRecordRejectsWrongType(t *testing.T) {
- _, err := nodeInfoMapFromRecord(map[string]any{"type": "text_message"})
- if err == nil {
- t.Fatalf("nodeInfoMapFromRecord() error = nil, want error")
+func TestUpsertMapReportRequiresNodeFields(t *testing.T) {
+ st := openTestStore(t)
+ defer st.Close()
+
+ if err := st.UpsertMapReport(map[string]any{"type": "map_report", "from_num": 1}); err == nil || !strings.Contains(err.Error(), "from") {
+ t.Fatalf("missing from error = %v, want from error", err)
+ }
+ if err := st.UpsertMapReport(map[string]any{"type": "map_report", "from": "!00000001"}); err == nil || !strings.Contains(err.Error(), "from_num") {
+ t.Fatalf("missing from_num error = %v, want from_num error", err)
}
}
-func TestNodeInfoMapNullablePublicKey(t *testing.T) {
+func TestNodeInfoFromRecordRejectsWrongType(t *testing.T) {
+ _, err := nodeInfoFromRecord(map[string]any{"type": "map_report"})
+ if err == nil {
+ t.Fatalf("nodeInfoFromRecord() error = nil, want error")
+ }
+}
+
+func TestMapReportFromRecordRejectsWrongType(t *testing.T) {
+ _, err := mapReportFromRecord(map[string]any{"type": "nodeinfo"})
+ if err == nil {
+ t.Fatalf("mapReportFromRecord() error = nil, want error")
+ }
+}
+
+func TestNodeInfoNullablePublicKey(t *testing.T) {
st := openTestStore(t)
defer st.Close()
record := map[string]any{"type": "nodeinfo", "from": "!00000001", "from_num": 1, "public_key": nil}
- if err := st.UpsertNodeInfoMap(record); err != nil {
- t.Fatalf("UpsertNodeInfoMap() error = %v", err)
+ if err := st.UpsertNodeInfo(record); err != nil {
+ t.Fatalf("UpsertNodeInfo() error = %v", err)
}
var publicKey sql.NullString
- if err := rawTestDB(t, st).QueryRow("SELECT public_key FROM nodeinfo_map WHERE node_id = ?", "!00000001").Scan(&publicKey); err != nil {
+ if err := rawTestDB(t, st).QueryRow("SELECT public_key FROM nodeinfo WHERE node_id = ?", "!00000001").Scan(&publicKey); err != nil {
t.Fatal(err)
}
if publicKey.Valid {
@@ -437,7 +441,7 @@ func rawTestDB(t *testing.T, st *store) *sql.DB {
return db
}
-func nodeInfoRecord(longName string) map[string]any {
+func nodeInfoTestRecord(longName string) map[string]any {
return map[string]any{
"type": "nodeinfo",
"from": "!12345678",
@@ -452,7 +456,7 @@ func nodeInfoRecord(longName string) map[string]any {
}
}
-func mapReportRecord(longName string) map[string]any {
+func mapReportTestRecord(longName string) map[string]any {
return map[string]any{
"type": "map_report",
"from": "!12345678",
diff --git a/main.go b/main.go
index b3d5a3a..eaafd5f 100644
--- a/main.go
+++ b/main.go
@@ -56,9 +56,15 @@ func (h *meshtasticFilterHook) OnPublish(cl *mqtt.Client, pk packets.Packet) (pa
}
switch record["type"] {
- case "nodeinfo", "map_report":
+ case "nodeinfo":
if h.store != nil {
- if err := h.store.UpsertNodeInfoMap(record); err != nil {
+ if err := h.store.UpsertNodeInfo(record); err != nil {
+ printJSON(map[string]any{"event": "db_error", "type": record["type"], "from": record["from"], "error": err.Error()})
+ }
+ }
+ case "map_report":
+ if h.store != nil {
+ if err := h.store.UpsertMapReport(record); err != nil {
printJSON(map[string]any{"event": "db_error", "type": record["type"], "from": record["from"], "error": err.Error()})
}
}
diff --git a/meshmap_frontend/src/App.vue b/meshmap_frontend/src/App.vue
index 9ad6f7c..0fa7f2f 100644
--- a/meshmap_frontend/src/App.vue
+++ b/meshmap_frontend/src/App.vue
@@ -1,60 +1,118 @@
-