新增几个数据表
This commit is contained in:
@@ -103,6 +103,10 @@ meshtastic:
|
|||||||
|
|
||||||
- `nodeinfo_map`:融合 `type == "nodeinfo"` 和 `type == "map_report"` 的节点信息
|
- `nodeinfo_map`:融合 `type == "nodeinfo"` 和 `type == "map_report"` 的节点信息
|
||||||
- `text_message`:追加保存 `type == "text_message"` 的聊天消息
|
- `text_message`:追加保存 `type == "text_message"` 的聊天消息
|
||||||
|
- `position`:追加保存 `type == "position"` 的位置包
|
||||||
|
- `telemetry`:追加保存 `type == "telemetry"` 的遥测包
|
||||||
|
- `routing`:追加保存 `type == "routing"` 的路由控制包
|
||||||
|
- `traceroute`:追加保存 `type == "traceroute"` 的路径追踪包
|
||||||
|
|
||||||
`nodeinfo_map` 规则:
|
`nodeinfo_map` 规则:
|
||||||
|
|
||||||
@@ -120,6 +124,15 @@ meshtastic:
|
|||||||
- 保存 `from_id`、`from_num`、`text`、`payload_hex`、topic、packet 元数据和完整 `content_json`
|
- 保存 `from_id`、`from_num`、`text`、`payload_hex`、topic、packet 元数据和完整 `content_json`
|
||||||
- 保存 MQTT 客户端信息:`mqtt_client_id`、`mqtt_username`、`mqtt_listener`、`mqtt_remote_addr`、`mqtt_remote_host`、`mqtt_remote_port`
|
- 保存 MQTT 客户端信息:`mqtt_client_id`、`mqtt_username`、`mqtt_listener`、`mqtt_remote_addr`、`mqtt_remote_host`、`mqtt_remote_port`
|
||||||
|
|
||||||
|
`position` / `telemetry` / `routing` / `traceroute` 规则:
|
||||||
|
|
||||||
|
- 都使用自增 `id` 作为主键
|
||||||
|
- 每条有效记录都会新增一行,不做去重
|
||||||
|
- 保存通用 packet 元数据、MQTT 客户端信息和完整 `content_json`
|
||||||
|
- `position` 额外保存经纬度、海拔、时间、定位来源、精度、速度、卫星数等字段
|
||||||
|
- `telemetry` 额外保存 `telemetry_type`,并把动态 `metrics` 对象保存为 `metrics_json`
|
||||||
|
- `routing` 和 `traceroute` 当前保存通用元数据和完整 JSON;后续如果解析更多 payload 字段,可继续扩展列
|
||||||
|
|
||||||
查询最近聊天消息示例:
|
查询最近聊天消息示例:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -129,6 +142,24 @@ ORDER BY id DESC
|
|||||||
LIMIT 20;
|
LIMIT 20;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
查询位置包示例:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT id, created_at, from_id, latitude, longitude, altitude
|
||||||
|
FROM position
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
查询遥测包示例:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT id, created_at, from_id, telemetry_type, metrics_json
|
||||||
|
FROM telemetry
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
SQLite 默认路径:
|
SQLite 默认路径:
|
||||||
|
|
||||||
- Unix/Linux:`/srv/mesh_mqtt_go/mesh_mqtt_go.db`
|
- Unix/Linux:`/srv/mesh_mqtt_go/mesh_mqtt_go.db`
|
||||||
|
|||||||
@@ -32,6 +32,36 @@ type mqttClientInfo struct {
|
|||||||
RemotePort string
|
RemotePort string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppendPacketFields struct {
|
||||||
|
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
FromID string `gorm:"column:from_id;not null;index"`
|
||||||
|
FromNum int64 `gorm:"column:from_num;not null;index"`
|
||||||
|
Topic string `gorm:"column:topic;not null"`
|
||||||
|
ChannelID *string `gorm:"column:channel_id"`
|
||||||
|
GatewayID *string `gorm:"column:gateway_id"`
|
||||||
|
PacketID *int64 `gorm:"column:packet_id;index"`
|
||||||
|
PacketTo *string `gorm:"column:packet_to"`
|
||||||
|
PacketToNum *int64 `gorm:"column:packet_to_num"`
|
||||||
|
Portnum *string `gorm:"column:portnum"`
|
||||||
|
PayloadLen *int64 `gorm:"column:payload_len"`
|
||||||
|
PayloadVariant *string `gorm:"column:payload_variant"`
|
||||||
|
ViaMQTT *bool `gorm:"column:via_mqtt"`
|
||||||
|
PKIEncrypted *bool `gorm:"column:pki_encrypted"`
|
||||||
|
DecryptSuccess *bool `gorm:"column:decrypt_success"`
|
||||||
|
DecryptStatus *string `gorm:"column:decrypt_status"`
|
||||||
|
ContentJSON string `gorm:"column:content_json;not null"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MQTTClientRecordFields struct {
|
||||||
|
MQTTClientID *string `gorm:"column:mqtt_client_id"`
|
||||||
|
MQTTUsername *string `gorm:"column:mqtt_username"`
|
||||||
|
MQTTListener *string `gorm:"column:mqtt_listener"`
|
||||||
|
MQTTRemoteAddr *string `gorm:"column:mqtt_remote_addr"`
|
||||||
|
MQTTRemoteHost *string `gorm:"column:mqtt_remote_host"`
|
||||||
|
MQTTRemotePort *string `gorm:"column:mqtt_remote_port"`
|
||||||
|
}
|
||||||
|
|
||||||
type nodeInfoMapRecord struct {
|
type nodeInfoMapRecord struct {
|
||||||
NodeID string `gorm:"column:node_id;primaryKey;not null"`
|
NodeID string `gorm:"column:node_id;primaryKey;not null"`
|
||||||
NodeNum int64 `gorm:"column:node_num;not null"`
|
NodeNum int64 `gorm:"column:node_num;not null"`
|
||||||
@@ -94,6 +124,68 @@ func (textMessageRecord) TableName() string {
|
|||||||
return "text_message"
|
return "text_message"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type positionRecord struct {
|
||||||
|
AppendPacketFields `gorm:"embedded"`
|
||||||
|
MQTTClientRecordFields `gorm:"embedded"`
|
||||||
|
Latitude *float64 `gorm:"column:latitude"`
|
||||||
|
Longitude *float64 `gorm:"column:longitude"`
|
||||||
|
Altitude *int64 `gorm:"column:altitude"`
|
||||||
|
PositionTime *int64 `gorm:"column:position_time"`
|
||||||
|
LocationSource *string `gorm:"column:location_source"`
|
||||||
|
AltitudeSource *string `gorm:"column:altitude_source"`
|
||||||
|
Timestamp *int64 `gorm:"column:timestamp"`
|
||||||
|
TimestampMillisAdjust *int64 `gorm:"column:timestamp_millis_adjust"`
|
||||||
|
AltitudeHAE *int64 `gorm:"column:altitude_hae"`
|
||||||
|
AltitudeGeoidalSeparation *int64 `gorm:"column:altitude_geoidal_separation"`
|
||||||
|
PDOP *float64 `gorm:"column:pdop"`
|
||||||
|
HDOP *float64 `gorm:"column:hdop"`
|
||||||
|
VDOP *float64 `gorm:"column:vdop"`
|
||||||
|
GPSAccuracy *int64 `gorm:"column:gps_accuracy"`
|
||||||
|
GroundSpeed *int64 `gorm:"column:ground_speed"`
|
||||||
|
GroundTrack *float64 `gorm:"column:ground_track"`
|
||||||
|
FixQuality *int64 `gorm:"column:fix_quality"`
|
||||||
|
FixType *int64 `gorm:"column:fix_type"`
|
||||||
|
SatsInView *int64 `gorm:"column:sats_in_view"`
|
||||||
|
SensorID *int64 `gorm:"column:sensor_id"`
|
||||||
|
NextUpdate *int64 `gorm:"column:next_update"`
|
||||||
|
SeqNumber *int64 `gorm:"column:seq_number"`
|
||||||
|
PrecisionBits *int64 `gorm:"column:precision_bits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (positionRecord) TableName() string {
|
||||||
|
return "position"
|
||||||
|
}
|
||||||
|
|
||||||
|
type telemetryRecord struct {
|
||||||
|
AppendPacketFields `gorm:"embedded"`
|
||||||
|
MQTTClientRecordFields `gorm:"embedded"`
|
||||||
|
TelemetryTime *int64 `gorm:"column:telemetry_time"`
|
||||||
|
TelemetryType *string `gorm:"column:telemetry_type;index"`
|
||||||
|
MetricsJSON *string `gorm:"column:metrics_json"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (telemetryRecord) TableName() string {
|
||||||
|
return "telemetry"
|
||||||
|
}
|
||||||
|
|
||||||
|
type routingRecord struct {
|
||||||
|
AppendPacketFields `gorm:"embedded"`
|
||||||
|
MQTTClientRecordFields `gorm:"embedded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (routingRecord) TableName() string {
|
||||||
|
return "routing"
|
||||||
|
}
|
||||||
|
|
||||||
|
type tracerouteRecord struct {
|
||||||
|
AppendPacketFields `gorm:"embedded"`
|
||||||
|
MQTTClientRecordFields `gorm:"embedded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tracerouteRecord) TableName() string {
|
||||||
|
return "traceroute"
|
||||||
|
}
|
||||||
|
|
||||||
func openStore(cfg databaseConfig) (*store, error) {
|
func openStore(cfg databaseConfig) (*store, error) {
|
||||||
var dialector gorm.Dialector
|
var dialector gorm.Dialector
|
||||||
switch cfg.Driver {
|
switch cfg.Driver {
|
||||||
@@ -143,31 +235,49 @@ func (s *store) Close() error {
|
|||||||
func (s *store) migrate() error {
|
func (s *store) migrate() error {
|
||||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
migrator := tx.Migrator()
|
migrator := tx.Migrator()
|
||||||
if !migrator.HasTable(&nodeInfoMapRecord{}) {
|
for _, item := range []struct {
|
||||||
if err := migrator.CreateTable(&nodeInfoMapRecord{}); err != nil {
|
label string
|
||||||
return fmt.Errorf("migrate nodeinfo_map table: %w", err)
|
model any
|
||||||
}
|
}{
|
||||||
}
|
{label: "nodeinfo_map", model: &nodeInfoMapRecord{}},
|
||||||
if !migrator.HasTable(&textMessageRecord{}) {
|
{label: "text_message", model: &textMessageRecord{}},
|
||||||
if err := migrator.CreateTable(&textMessageRecord{}); err != nil {
|
{label: "position", model: &positionRecord{}},
|
||||||
return fmt.Errorf("migrate text_message table: %w", err)
|
{label: "telemetry", model: &telemetryRecord{}},
|
||||||
}
|
{label: "routing", model: &routingRecord{}},
|
||||||
}
|
{label: "traceroute", model: &tracerouteRecord{}},
|
||||||
for _, indexName := range []string{
|
|
||||||
"idx_text_message_from_num_created_at",
|
|
||||||
"idx_text_message_created_at",
|
|
||||||
"idx_text_message_packet_id",
|
|
||||||
} {
|
} {
|
||||||
if !migrator.HasIndex(&textMessageRecord{}, indexName) {
|
if !migrator.HasTable(item.model) {
|
||||||
if err := migrator.CreateIndex(&textMessageRecord{}, indexName); err != nil {
|
if err := migrator.CreateTable(item.model); err != nil {
|
||||||
return fmt.Errorf("migrate text_message index %s: %w", indexName, err)
|
return fmt.Errorf("migrate %s table: %w", item.label, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, item := range []struct {
|
||||||
|
label string
|
||||||
|
model any
|
||||||
|
indexes []string
|
||||||
|
}{
|
||||||
|
{label: "text_message", model: &textMessageRecord{}, indexes: []string{"idx_text_message_from_num_created_at", "idx_text_message_created_at", "idx_text_message_packet_id"}},
|
||||||
|
} {
|
||||||
|
if err := createMissingIndexes(migrator, item.model, item.label, item.indexes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createMissingIndexes(migrator gorm.Migrator, model any, label string, indexNames []string) error {
|
||||||
|
for _, indexName := range indexNames {
|
||||||
|
if !migrator.HasIndex(model, indexName) {
|
||||||
|
if err := migrator.CreateIndex(model, indexName); err != nil {
|
||||||
|
return fmt.Errorf("migrate %s index %s: %w", label, indexName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *store) UpsertNodeInfoMap(record map[string]any) error {
|
func (s *store) UpsertNodeInfoMap(record map[string]any) error {
|
||||||
node, err := nodeInfoMapFromRecord(record)
|
node, err := nodeInfoMapFromRecord(record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -238,6 +348,50 @@ func (s *store) InsertTextMessage(record map[string]any, clientInfo mqttClientIn
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *store) InsertPosition(record map[string]any, clientInfo mqttClientInfo) error {
|
||||||
|
position, err := positionFromRecord(record, clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.db.Create(position).Error; err != nil {
|
||||||
|
return fmt.Errorf("insert position from %s: %w", position.FromID, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) InsertTelemetry(record map[string]any, clientInfo mqttClientInfo) error {
|
||||||
|
telemetry, err := telemetryFromRecord(record, clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.db.Create(telemetry).Error; err != nil {
|
||||||
|
return fmt.Errorf("insert telemetry from %s: %w", telemetry.FromID, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) InsertRouting(record map[string]any, clientInfo mqttClientInfo) error {
|
||||||
|
routing, err := routingFromRecord(record, clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.db.Create(routing).Error; err != nil {
|
||||||
|
return fmt.Errorf("insert routing from %s: %w", routing.FromID, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) InsertTraceroute(record map[string]any, clientInfo mqttClientInfo) error {
|
||||||
|
traceroute, err := tracerouteFromRecord(record, clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.db.Create(traceroute).Error; err != nil {
|
||||||
|
return fmt.Errorf("insert traceroute from %s: %w", traceroute.FromID, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func nodeInfoMapFromRecord(record map[string]any) (*nodeInfoMapRecord, error) {
|
func nodeInfoMapFromRecord(record map[string]any) (*nodeInfoMapRecord, error) {
|
||||||
latestType, ok := record["type"].(string)
|
latestType, ok := record["type"].(string)
|
||||||
if !ok || (latestType != "nodeinfo" && latestType != "map_report") {
|
if !ok || (latestType != "nodeinfo" && latestType != "map_report") {
|
||||||
@@ -285,28 +439,131 @@ func textMessageFromRecord(record map[string]any, clientInfo mqttClientInfo) (*t
|
|||||||
if !ok || recordType != "text_message" {
|
if !ok || recordType != "text_message" {
|
||||||
return nil, fmt.Errorf("record type %v is not text_message", record["type"])
|
return nil, fmt.Errorf("record type %v is not text_message", record["type"])
|
||||||
}
|
}
|
||||||
|
common, clientFields, err := AppendPacketFieldsFromRecord(record, "text_message", clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &textMessageRecord{
|
||||||
|
FromID: common.FromID,
|
||||||
|
FromNum: common.FromNum,
|
||||||
|
Text: nullableString(record["text"]),
|
||||||
|
PayloadHex: nullableString(record["payload_hex"]),
|
||||||
|
Topic: common.Topic,
|
||||||
|
ChannelID: common.ChannelID,
|
||||||
|
GatewayID: common.GatewayID,
|
||||||
|
PacketID: common.PacketID,
|
||||||
|
PacketTo: common.PacketTo,
|
||||||
|
PacketToNum: common.PacketToNum,
|
||||||
|
Portnum: common.Portnum,
|
||||||
|
PayloadLen: common.PayloadLen,
|
||||||
|
PayloadVariant: common.PayloadVariant,
|
||||||
|
ViaMQTT: common.ViaMQTT,
|
||||||
|
PKIEncrypted: common.PKIEncrypted,
|
||||||
|
DecryptSuccess: common.DecryptSuccess,
|
||||||
|
DecryptStatus: common.DecryptStatus,
|
||||||
|
MQTTClientID: clientFields.MQTTClientID,
|
||||||
|
MQTTUsername: clientFields.MQTTUsername,
|
||||||
|
MQTTListener: clientFields.MQTTListener,
|
||||||
|
MQTTRemoteAddr: clientFields.MQTTRemoteAddr,
|
||||||
|
MQTTRemoteHost: clientFields.MQTTRemoteHost,
|
||||||
|
MQTTRemotePort: clientFields.MQTTRemotePort,
|
||||||
|
ContentJSON: common.ContentJSON,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func positionFromRecord(record map[string]any, clientInfo mqttClientInfo) (*positionRecord, error) {
|
||||||
|
common, clientFields, err := AppendPacketFieldsFromRecord(record, "position", clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &positionRecord{
|
||||||
|
AppendPacketFields: common,
|
||||||
|
MQTTClientRecordFields: clientFields,
|
||||||
|
Latitude: nullableFloat64(record["latitude"]),
|
||||||
|
Longitude: nullableFloat64(record["longitude"]),
|
||||||
|
Altitude: nullableInt64(record["altitude"]),
|
||||||
|
PositionTime: nullableInt64(record["time"]),
|
||||||
|
LocationSource: nullableStringValue(record["location_source"]),
|
||||||
|
AltitudeSource: nullableStringValue(record["altitude_source"]),
|
||||||
|
Timestamp: nullableInt64(record["timestamp"]),
|
||||||
|
TimestampMillisAdjust: nullableInt64(record["timestamp_millis_adjust"]),
|
||||||
|
AltitudeHAE: nullableInt64(record["altitude_hae"]),
|
||||||
|
AltitudeGeoidalSeparation: nullableInt64(record["altitude_geoidal_separation"]),
|
||||||
|
PDOP: nullableFloat64(record["pdop"]),
|
||||||
|
HDOP: nullableFloat64(record["hdop"]),
|
||||||
|
VDOP: nullableFloat64(record["vdop"]),
|
||||||
|
GPSAccuracy: nullableInt64(record["gps_accuracy"]),
|
||||||
|
GroundSpeed: nullableInt64(record["ground_speed"]),
|
||||||
|
GroundTrack: nullableFloat64(record["ground_track"]),
|
||||||
|
FixQuality: nullableInt64(record["fix_quality"]),
|
||||||
|
FixType: nullableInt64(record["fix_type"]),
|
||||||
|
SatsInView: nullableInt64(record["sats_in_view"]),
|
||||||
|
SensorID: nullableInt64(record["sensor_id"]),
|
||||||
|
NextUpdate: nullableInt64(record["next_update"]),
|
||||||
|
SeqNumber: nullableInt64(record["seq_number"]),
|
||||||
|
PrecisionBits: nullableInt64(record["precision_bits"]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func telemetryFromRecord(record map[string]any, clientInfo mqttClientInfo) (*telemetryRecord, error) {
|
||||||
|
common, clientFields, err := AppendPacketFieldsFromRecord(record, "telemetry", clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
metricsJSON, err := nullableJSON(record["metrics"])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode telemetry metrics_json: %w", err)
|
||||||
|
}
|
||||||
|
return &telemetryRecord{
|
||||||
|
AppendPacketFields: common,
|
||||||
|
MQTTClientRecordFields: clientFields,
|
||||||
|
TelemetryTime: nullableInt64(record["time"]),
|
||||||
|
TelemetryType: nullableString(record["telemetry_type"]),
|
||||||
|
MetricsJSON: metricsJSON,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func routingFromRecord(record map[string]any, clientInfo mqttClientInfo) (*routingRecord, error) {
|
||||||
|
common, clientFields, err := AppendPacketFieldsFromRecord(record, "routing", clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &routingRecord{AppendPacketFields: common, MQTTClientRecordFields: clientFields}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tracerouteFromRecord(record map[string]any, clientInfo mqttClientInfo) (*tracerouteRecord, error) {
|
||||||
|
common, clientFields, err := AppendPacketFieldsFromRecord(record, "traceroute", clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tracerouteRecord{AppendPacketFields: common, MQTTClientRecordFields: clientFields}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendPacketFieldsFromRecord(record map[string]any, wantType string, clientInfo mqttClientInfo) (AppendPacketFields, MQTTClientRecordFields, error) {
|
||||||
|
recordType, ok := record["type"].(string)
|
||||||
|
if !ok || recordType != wantType {
|
||||||
|
return AppendPacketFields{}, MQTTClientRecordFields{}, fmt.Errorf("record type %v is not %s", record["type"], wantType)
|
||||||
|
}
|
||||||
fromID, ok := record["from"].(string)
|
fromID, ok := record["from"].(string)
|
||||||
if !ok || fromID == "" {
|
if !ok || fromID == "" {
|
||||||
return nil, fmt.Errorf("text_message missing from")
|
return AppendPacketFields{}, MQTTClientRecordFields{}, fmt.Errorf("%s missing from", wantType)
|
||||||
}
|
}
|
||||||
fromNum, err := int64FromAny(record["from_num"])
|
fromNum, err := int64FromAny(record["from_num"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("text_message from_num: %w", err)
|
return AppendPacketFields{}, MQTTClientRecordFields{}, fmt.Errorf("%s from_num: %w", wantType, err)
|
||||||
}
|
}
|
||||||
topic, ok := record["topic"].(string)
|
topic, ok := record["topic"].(string)
|
||||||
if !ok || topic == "" {
|
if !ok || topic == "" {
|
||||||
return nil, fmt.Errorf("text_message missing topic")
|
return AppendPacketFields{}, MQTTClientRecordFields{}, fmt.Errorf("%s missing topic", wantType)
|
||||||
}
|
}
|
||||||
contentJSON, err := json.Marshal(record)
|
contentJSON, err := json.Marshal(record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("encode text_message content_json: %w", err)
|
return AppendPacketFields{}, MQTTClientRecordFields{}, fmt.Errorf("encode %s content_json: %w", wantType, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &textMessageRecord{
|
return AppendPacketFields{
|
||||||
FromID: fromID,
|
FromID: fromID,
|
||||||
FromNum: fromNum,
|
FromNum: fromNum,
|
||||||
Text: nullableString(record["text"]),
|
|
||||||
PayloadHex: nullableString(record["payload_hex"]),
|
|
||||||
Topic: topic,
|
Topic: topic,
|
||||||
ChannelID: nullableString(record["channel_id"]),
|
ChannelID: nullableString(record["channel_id"]),
|
||||||
GatewayID: nullableString(record["gateway_id"]),
|
GatewayID: nullableString(record["gateway_id"]),
|
||||||
@@ -320,13 +577,14 @@ func textMessageFromRecord(record map[string]any, clientInfo mqttClientInfo) (*t
|
|||||||
PKIEncrypted: nullableBool(record["pki_encrypted"]),
|
PKIEncrypted: nullableBool(record["pki_encrypted"]),
|
||||||
DecryptSuccess: nullableBool(record["decrypt_success"]),
|
DecryptSuccess: nullableBool(record["decrypt_success"]),
|
||||||
DecryptStatus: nullableString(record["decrypt_status"]),
|
DecryptStatus: nullableString(record["decrypt_status"]),
|
||||||
|
ContentJSON: string(contentJSON),
|
||||||
|
}, MQTTClientRecordFields{
|
||||||
MQTTClientID: nullableString(clientInfo.ClientID),
|
MQTTClientID: nullableString(clientInfo.ClientID),
|
||||||
MQTTUsername: nullableString(clientInfo.Username),
|
MQTTUsername: nullableString(clientInfo.Username),
|
||||||
MQTTListener: nullableString(clientInfo.Listener),
|
MQTTListener: nullableString(clientInfo.Listener),
|
||||||
MQTTRemoteAddr: nullableString(clientInfo.RemoteAddr),
|
MQTTRemoteAddr: nullableString(clientInfo.RemoteAddr),
|
||||||
MQTTRemoteHost: nullableString(clientInfo.RemoteHost),
|
MQTTRemoteHost: nullableString(clientInfo.RemoteHost),
|
||||||
MQTTRemotePort: nullableString(clientInfo.RemotePort),
|
MQTTRemotePort: nullableString(clientInfo.RemotePort),
|
||||||
ContentJSON: string(contentJSON),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,6 +628,23 @@ func nullableString(value any) *string {
|
|||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nullableStringValue(value any) *string {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s, ok := value.(string); ok {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
s := fmt.Sprint(value)
|
||||||
|
if s == "" || s == "<nil>" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
func nullableBool(value any) *bool {
|
func nullableBool(value any) *bool {
|
||||||
b, ok := value.(bool)
|
b, ok := value.(bool)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -422,6 +697,18 @@ func nullableFloat64(value any) *float64 {
|
|||||||
return &out
|
return &out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nullableJSON(value any) (*string, error) {
|
||||||
|
if value == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s := string(data)
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
func addStringUpdate(updates map[string]any, column string, value *string) {
|
func addStringUpdate(updates map[string]any, column string, value *string) {
|
||||||
if value != nil {
|
if value != nil {
|
||||||
updates[column] = *value
|
updates[column] = *value
|
||||||
|
|||||||
+194
-4
@@ -11,7 +11,7 @@ func TestOpenStoreCreatesTables(t *testing.T) {
|
|||||||
st := openTestStore(t)
|
st := openTestStore(t)
|
||||||
defer st.Close()
|
defer st.Close()
|
||||||
|
|
||||||
for _, table := range []string{"nodeinfo_map", "text_message"} {
|
for _, table := range []string{"nodeinfo_map", "text_message", "position", "telemetry", "routing", "traceroute"} {
|
||||||
var name string
|
var name string
|
||||||
if err := rawTestDB(t, st).QueryRow("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table).Scan(&name); err != nil {
|
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)
|
t.Fatalf("%s table missing: %v", table, err)
|
||||||
@@ -283,6 +283,139 @@ func TestInsertTextMessageRequiresFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInsertPositionAppendsRows(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
clientInfo := mqttClientInfo{ClientID: "client-1", RemoteAddr: "127.0.0.1:54321", RemoteHost: "127.0.0.1", RemotePort: "54321"}
|
||||||
|
if err := st.InsertPosition(positionTestRecord(), clientInfo); err != nil {
|
||||||
|
t.Fatalf("first InsertPosition() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := st.InsertPosition(positionTestRecord(), clientInfo); err != nil {
|
||||||
|
t.Fatalf("second InsertPosition() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int
|
||||||
|
if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM position WHERE from_id = ?", "!12345678").Scan(&count); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 2 {
|
||||||
|
t.Fatalf("position count = %d, want 2", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
var latitude, longitude float64
|
||||||
|
var altitude int64
|
||||||
|
var locationSource, remoteHost string
|
||||||
|
if err := rawTestDB(t, st).QueryRow("SELECT latitude, longitude, altitude, location_source, mqtt_remote_host FROM position ORDER BY id LIMIT 1").Scan(&latitude, &longitude, &altitude, &locationSource, &remoteHost); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if latitude != 42.5 || longitude != -83.1 || altitude != 200 || locationSource != "LOC_INTERNAL" || remoteHost != "127.0.0.1" {
|
||||||
|
t.Fatalf("position row = lat %v lon %v alt %v source %q remote %q", latitude, longitude, altitude, locationSource, remoteHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertTelemetryAppendsRowsAndStoresMetricsJSON(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
if err := st.InsertTelemetry(telemetryTestRecord(), mqttClientInfo{}); err != nil {
|
||||||
|
t.Fatalf("InsertTelemetry() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var telemetryType, metricsJSON, contentJSON string
|
||||||
|
if err := rawTestDB(t, st).QueryRow("SELECT telemetry_type, metrics_json, content_json FROM telemetry LIMIT 1").Scan(&telemetryType, &metricsJSON, &contentJSON); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if telemetryType != "device_metrics" {
|
||||||
|
t.Fatalf("telemetry_type = %q, want device_metrics", telemetryType)
|
||||||
|
}
|
||||||
|
if !strings.Contains(metricsJSON, "battery_level") || !strings.Contains(metricsJSON, "voltage") {
|
||||||
|
t.Fatalf("metrics_json = %q, want battery_level and voltage", metricsJSON)
|
||||||
|
}
|
||||||
|
if !strings.Contains(contentJSON, "telemetry") {
|
||||||
|
t.Fatalf("content_json = %q, want telemetry", contentJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertRoutingAndTracerouteAppendRows(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
if err := st.InsertRouting(routingTestRecord(), mqttClientInfo{}); err != nil {
|
||||||
|
t.Fatalf("first InsertRouting() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := st.InsertRouting(routingTestRecord(), mqttClientInfo{}); err != nil {
|
||||||
|
t.Fatalf("second InsertRouting() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := st.InsertTraceroute(tracerouteTestRecord(), mqttClientInfo{}); err != nil {
|
||||||
|
t.Fatalf("first InsertTraceroute() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := st.InsertTraceroute(tracerouteTestRecord(), mqttClientInfo{}); err != nil {
|
||||||
|
t.Fatalf("second InsertTraceroute() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range []string{"routing", "traceroute"} {
|
||||||
|
var count int
|
||||||
|
if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM "+table+" WHERE from_id = ?", "!12345678").Scan(&count); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 2 {
|
||||||
|
t.Fatalf("%s count = %d, want 2", table, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
var packetID int64
|
||||||
|
var contentJSON string
|
||||||
|
if err := rawTestDB(t, st).QueryRow("SELECT packet_id, content_json FROM "+table+" ORDER BY id LIMIT 1").Scan(&packetID, &contentJSON); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if packetID != 42 || !strings.Contains(contentJSON, table) {
|
||||||
|
t.Fatalf("%s row packet_id=%d content_json=%q", table, packetID, contentJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertPacketTablesRequireFields(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
insert func(map[string]any) error
|
||||||
|
record map[string]any
|
||||||
|
}{
|
||||||
|
{name: "position", insert: func(r map[string]any) error { return st.InsertPosition(r, mqttClientInfo{}) }, record: positionTestRecord()},
|
||||||
|
{name: "telemetry", insert: func(r map[string]any) error { return st.InsertTelemetry(r, mqttClientInfo{}) }, record: telemetryTestRecord()},
|
||||||
|
{name: "routing", insert: func(r map[string]any) error { return st.InsertRouting(r, mqttClientInfo{}) }, record: routingTestRecord()},
|
||||||
|
{name: "traceroute", insert: func(r map[string]any) error { return st.InsertTraceroute(r, mqttClientInfo{}) }, record: tracerouteTestRecord()},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
wrongType := cloneRecord(tt.record)
|
||||||
|
wrongType["type"] = "text_message"
|
||||||
|
if err := tt.insert(wrongType); err == nil || !strings.Contains(err.Error(), tt.name) {
|
||||||
|
t.Fatalf("%s wrong type error = %v, want %s", tt.name, err, tt.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
missingFrom := cloneRecord(tt.record)
|
||||||
|
delete(missingFrom, "from")
|
||||||
|
if err := tt.insert(missingFrom); err == nil || !strings.Contains(err.Error(), "from") {
|
||||||
|
t.Fatalf("%s missing from error = %v, want from error", tt.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
missingFromNum := cloneRecord(tt.record)
|
||||||
|
delete(missingFromNum, "from_num")
|
||||||
|
if err := tt.insert(missingFromNum); err == nil || !strings.Contains(err.Error(), "from_num") {
|
||||||
|
t.Fatalf("%s missing from_num error = %v, want from_num error", tt.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
missingTopic := cloneRecord(tt.record)
|
||||||
|
delete(missingTopic, "topic")
|
||||||
|
if err := tt.insert(missingTopic); err == nil || !strings.Contains(err.Error(), "topic") {
|
||||||
|
t.Fatalf("%s missing topic error = %v, want topic error", tt.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func openTestStore(t *testing.T) *store {
|
func openTestStore(t *testing.T) *store {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
st, err := openStore(databaseConfig{
|
st, err := openStore(databaseConfig{
|
||||||
@@ -341,21 +474,78 @@ func mapReportRecord(longName string) map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func textMessageTestRecord(text any) map[string]any {
|
func textMessageTestRecord(text any) map[string]any {
|
||||||
|
record := commonPacketTestRecord("text_message", "TEXT_MESSAGE_APP")
|
||||||
|
record["text"] = text
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
func positionTestRecord() map[string]any {
|
||||||
|
record := commonPacketTestRecord("position", "POSITION_APP")
|
||||||
|
record["latitude"] = 42.5
|
||||||
|
record["longitude"] = -83.1
|
||||||
|
record["altitude"] = int32(200)
|
||||||
|
record["time"] = uint32(123456)
|
||||||
|
record["location_source"] = "LOC_INTERNAL"
|
||||||
|
record["altitude_source"] = "ALT_INTERNAL"
|
||||||
|
record["timestamp"] = uint32(123456)
|
||||||
|
record["timestamp_millis_adjust"] = uint32(10)
|
||||||
|
record["altitude_hae"] = int32(210)
|
||||||
|
record["altitude_geoidal_separation"] = int32(20)
|
||||||
|
record["pdop"] = 1.1
|
||||||
|
record["hdop"] = 1.2
|
||||||
|
record["vdop"] = 1.3
|
||||||
|
record["gps_accuracy"] = uint32(1000)
|
||||||
|
record["ground_speed"] = uint32(2)
|
||||||
|
record["ground_track"] = 180.5
|
||||||
|
record["fix_quality"] = uint32(1)
|
||||||
|
record["fix_type"] = uint32(3)
|
||||||
|
record["sats_in_view"] = uint32(8)
|
||||||
|
record["sensor_id"] = uint32(1)
|
||||||
|
record["next_update"] = uint32(60)
|
||||||
|
record["seq_number"] = uint32(7)
|
||||||
|
record["precision_bits"] = uint32(16)
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
func telemetryTestRecord() map[string]any {
|
||||||
|
record := commonPacketTestRecord("telemetry", "TELEMETRY_APP")
|
||||||
|
record["time"] = uint32(123456)
|
||||||
|
record["telemetry_type"] = "device_metrics"
|
||||||
|
record["metrics"] = map[string]any{"battery_level": 85, "voltage": 4.1}
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
func routingTestRecord() map[string]any {
|
||||||
|
return commonPacketTestRecord("routing", "ROUTING_APP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func tracerouteTestRecord() map[string]any {
|
||||||
|
return commonPacketTestRecord("traceroute", "TRACEROUTE_APP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func commonPacketTestRecord(recordType, portnum string) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"type": "text_message",
|
"type": recordType,
|
||||||
"topic": "msh/US/test",
|
"topic": "msh/US/test",
|
||||||
"channel_id": "LongFast",
|
"channel_id": "LongFast",
|
||||||
"gateway_id": "!gateway",
|
"gateway_id": "!gateway",
|
||||||
"from": "!12345678",
|
"from": "!12345678",
|
||||||
"from_num": uint32(0x12345678),
|
"from_num": uint32(0x12345678),
|
||||||
"text": text,
|
|
||||||
"packet_id": uint32(42),
|
"packet_id": uint32(42),
|
||||||
"packet_to": "!ffffffff",
|
"packet_to": "!ffffffff",
|
||||||
"packet_to_num": uint32(0xffffffff),
|
"packet_to_num": uint32(0xffffffff),
|
||||||
"portnum": "TEXT_MESSAGE_APP",
|
"portnum": portnum,
|
||||||
"payload_len": 5,
|
"payload_len": 5,
|
||||||
"payload_variant": "decoded",
|
"payload_variant": "decoded",
|
||||||
"via_mqtt": true,
|
"via_mqtt": true,
|
||||||
"pki_encrypted": false,
|
"pki_encrypted": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneRecord(record map[string]any) map[string]any {
|
||||||
|
clone := make(map[string]any, len(record))
|
||||||
|
for key, value := range record {
|
||||||
|
clone[key] = value
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,30 @@ func (h *meshtasticFilterHook) OnPublish(cl *mqtt.Client, pk packets.Packet) (pa
|
|||||||
printJSON(map[string]any{"event": "db_error", "type": record["type"], "from": record["from"], "error": err.Error()})
|
printJSON(map[string]any{"event": "db_error", "type": record["type"], "from": record["from"], "error": err.Error()})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "position":
|
||||||
|
if h.store != nil {
|
||||||
|
if err := h.store.InsertPosition(record, mqttClientInfoFromClient(cl)); err != nil {
|
||||||
|
printJSON(map[string]any{"event": "db_error", "type": record["type"], "from": record["from"], "error": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "telemetry":
|
||||||
|
if h.store != nil {
|
||||||
|
if err := h.store.InsertTelemetry(record, mqttClientInfoFromClient(cl)); err != nil {
|
||||||
|
printJSON(map[string]any{"event": "db_error", "type": record["type"], "from": record["from"], "error": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "routing":
|
||||||
|
if h.store != nil {
|
||||||
|
if err := h.store.InsertRouting(record, mqttClientInfoFromClient(cl)); err != nil {
|
||||||
|
printJSON(map[string]any{"event": "db_error", "type": record["type"], "from": record["from"], "error": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "traceroute":
|
||||||
|
if h.store != nil {
|
||||||
|
if err := h.store.InsertTraceroute(record, mqttClientInfoFromClient(cl)); err != nil {
|
||||||
|
printJSON(map[string]any{"event": "db_error", "type": record["type"], "from": record["from"], "error": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if record["type"] != "empty_packet" {
|
if record["type"] != "empty_packet" {
|
||||||
printJSON(record)
|
printJSON(record)
|
||||||
|
|||||||
Reference in New Issue
Block a user