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 @@