feat: mqtt receive.
This commit is contained in:
@@ -8,3 +8,13 @@ ai:
|
|||||||
base_url: https://api.lkeap.cloud.tencent.com/v1
|
base_url: https://api.lkeap.cloud.tencent.com/v1
|
||||||
api_key: ""
|
api_key: ""
|
||||||
model: deepseek-v3.2
|
model: deepseek-v3.2
|
||||||
|
mqtt:
|
||||||
|
enabled: true
|
||||||
|
host: mqtt.weihua-iot.cn
|
||||||
|
port: 10237
|
||||||
|
username: public_client
|
||||||
|
password: uXC3M4ObO9KpdU
|
||||||
|
client_id_prefix: hr-receiver
|
||||||
|
region: "+"
|
||||||
|
use_tls: true
|
||||||
|
qos: 0
|
||||||
|
|||||||
+15
-2
@@ -26,9 +26,22 @@ type AIConfig struct {
|
|||||||
Model string `mapstructure:"model" yaml:"model"`
|
Model string `mapstructure:"model" yaml:"model"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MQTTConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
||||||
|
Host string `mapstructure:"host" yaml:"host"`
|
||||||
|
Port int `mapstructure:"port" yaml:"port"`
|
||||||
|
Username string `mapstructure:"username" yaml:"username"`
|
||||||
|
Password string `mapstructure:"password" yaml:"password"`
|
||||||
|
ClientIDPrefix string `mapstructure:"client_id_prefix" yaml:"client_id_prefix"`
|
||||||
|
Region string `mapstructure:"region" yaml:"region"`
|
||||||
|
UseTLS bool `mapstructure:"use_tls" yaml:"use_tls"`
|
||||||
|
QoS int `mapstructure:"qos" yaml:"qos"`
|
||||||
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
DB DBConfig `mapstructure:"database" yaml:"database"`
|
DB DBConfig `mapstructure:"database" yaml:"database"`
|
||||||
AI AIConfig `mapstructure:"ai" yaml:"ai"`
|
AI AIConfig `mapstructure:"ai" yaml:"ai"`
|
||||||
|
MQTT MQTTConfig `mapstructure:"mqtt" yaml:"mqtt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitConfig() {
|
func InitConfig() {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ require (
|
|||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.5.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/fumiama/imgsz v0.0.2 // indirect
|
github.com/fumiama/imgsz v0.0.2 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
@@ -28,6 +29,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
@@ -40,6 +42,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
|
|||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"hr_receiver/config"
|
"hr_receiver/config"
|
||||||
"hr_receiver/models"
|
"hr_receiver/models"
|
||||||
|
"hr_receiver/mqtt"
|
||||||
"hr_receiver/routes"
|
"hr_receiver/routes"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -25,8 +27,15 @@ func main() {
|
|||||||
&models.StepStrideFreq{},
|
&models.StepStrideFreq{},
|
||||||
&models.RegressionResult{},
|
&models.RegressionResult{},
|
||||||
&models.User{},
|
&models.User{},
|
||||||
|
&models.MqttHeartRateRecord{},
|
||||||
|
&models.MqttStepCountRecord{},
|
||||||
|
&models.MqttGatewayStatusRecord{},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if err := mqtt.Start(config.DB, config.App.MQTT); err != nil {
|
||||||
|
log.Printf("mqtt listener start failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 启动服务
|
// 启动服务
|
||||||
r := routes.SetupRouter()
|
r := routes.SetupRouter()
|
||||||
r.Run(":8081")
|
r.Run(":8081")
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type MqttHeartRateRecord struct {
|
||||||
|
gorm.Model
|
||||||
|
Identifier string `gorm:"uniqueIndex;size:255" json:"identifier"`
|
||||||
|
Topic string `gorm:"size:255;index" json:"topic"`
|
||||||
|
RegionID uint32 `gorm:"index" json:"regionId"`
|
||||||
|
GatewayMAC string `gorm:"size:32;index" json:"gatewayMac"`
|
||||||
|
BandID uint32 `gorm:"index" json:"bandId"`
|
||||||
|
BeltAddr string `gorm:"size:64;index" json:"beltAddr"`
|
||||||
|
PacketNum uint32 `gorm:"index" json:"packetNum"`
|
||||||
|
HeartRate int `gorm:"type:int" json:"heartRate"`
|
||||||
|
HrConfidence int `gorm:"type:int" json:"hrConfidence"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
IsOnSkin bool `json:"isOnSkin"`
|
||||||
|
Battery uint32 `json:"battery"`
|
||||||
|
SignalRSSINeg float64 `gorm:"type:double precision" json:"signalRssiNeg"`
|
||||||
|
SNR float64 `gorm:"type:double precision" json:"snr"`
|
||||||
|
HubBusID uint32 `json:"hubBusId"`
|
||||||
|
HubSubDevID uint32 `json:"hubSubDevId"`
|
||||||
|
ReceivedAt int64 `gorm:"type:bigint;index" json:"receivedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MqttStepCountRecord struct {
|
||||||
|
gorm.Model
|
||||||
|
Identifier string `gorm:"uniqueIndex;size:255" json:"identifier"`
|
||||||
|
Topic string `gorm:"size:255;index" json:"topic"`
|
||||||
|
RegionID uint32 `gorm:"index" json:"regionId"`
|
||||||
|
GatewayMAC string `gorm:"size:32;index" json:"gatewayMac"`
|
||||||
|
BandID uint32 `gorm:"index" json:"bandId"`
|
||||||
|
BeltAddr string `gorm:"size:64;index" json:"beltAddr"`
|
||||||
|
PacketNum uint32 `gorm:"index" json:"packetNum"`
|
||||||
|
StepCount uint32 `json:"stepCount"`
|
||||||
|
SignalRSSINeg float64 `gorm:"type:double precision" json:"signalRssiNeg"`
|
||||||
|
SNR float64 `gorm:"type:double precision" json:"snr"`
|
||||||
|
HubBusID uint32 `json:"hubBusId"`
|
||||||
|
HubSubDevID uint32 `json:"hubSubDevId"`
|
||||||
|
ReceivedAt int64 `gorm:"type:bigint;index" json:"receivedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MqttGatewayStatusRecord struct {
|
||||||
|
gorm.Model
|
||||||
|
Identifier string `gorm:"uniqueIndex;size:255" json:"identifier"`
|
||||||
|
Topic string `gorm:"size:255;index" json:"topic"`
|
||||||
|
RegionID uint32 `gorm:"index" json:"regionId"`
|
||||||
|
GatewayMAC string `gorm:"size:32;index" json:"gatewayMac"`
|
||||||
|
BootCount uint32 `json:"bootCount"`
|
||||||
|
UptimeMs uint32 `json:"uptimeMs"`
|
||||||
|
DurationMsSinceLastPacket uint32 `json:"durationMsSinceLastPacket"`
|
||||||
|
RxCount uint32 `json:"rxCount"`
|
||||||
|
BatteryVoltageMV uint32 `json:"batteryVoltageMv"`
|
||||||
|
BatterySOCPercentage uint32 `json:"batterySocPercentage"`
|
||||||
|
ChargingRatePercentage int32 `json:"chargingRatePercentage"`
|
||||||
|
ReceivedAt int64 `gorm:"type:bigint;index" json:"receivedAt"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
package mqtt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"hr_receiver/config"
|
||||||
|
"hr_receiver/models"
|
||||||
|
whgw_hrpb "hr_receiver/proto"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultQueueSize = 2048
|
||||||
|
defaultWorkers = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type Listener struct {
|
||||||
|
db *gorm.DB
|
||||||
|
cfg config.MQTTConfig
|
||||||
|
client mqtt.Client
|
||||||
|
writeCh chan interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start(db *gorm.DB, cfg config.MQTTConfig) error {
|
||||||
|
if !cfg.Enabled {
|
||||||
|
log.Println("mqtt listener disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := validateConfig(cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
listener := &Listener{
|
||||||
|
db: db,
|
||||||
|
cfg: cfg,
|
||||||
|
writeCh: make(chan interface{}, defaultQueueSize),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < defaultWorkers; i++ {
|
||||||
|
go listener.writeWorker()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := listener.connect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(cfg config.MQTTConfig) error {
|
||||||
|
if cfg.Host == "" {
|
||||||
|
return fmt.Errorf("missing config: mqtt.host")
|
||||||
|
}
|
||||||
|
if cfg.Port == 0 {
|
||||||
|
return fmt.Errorf("missing config: mqtt.port")
|
||||||
|
}
|
||||||
|
if cfg.Region == "" {
|
||||||
|
return fmt.Errorf("missing config: mqtt.region")
|
||||||
|
}
|
||||||
|
if cfg.ClientIDPrefix == "" {
|
||||||
|
return fmt.Errorf("missing config: mqtt.client_id_prefix")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) connect() error {
|
||||||
|
opts := mqtt.NewClientOptions()
|
||||||
|
scheme := "tcp"
|
||||||
|
if l.cfg.UseTLS {
|
||||||
|
scheme = "ssl"
|
||||||
|
opts.SetTLSConfig(&tls.Config{MinVersion: tls.VersionTLS12})
|
||||||
|
}
|
||||||
|
broker := fmt.Sprintf("%s://%s:%d", scheme, l.cfg.Host, l.cfg.Port)
|
||||||
|
opts.AddBroker(broker)
|
||||||
|
opts.SetClientID(fmt.Sprintf("%s-%d", l.cfg.ClientIDPrefix, time.Now().UnixNano()))
|
||||||
|
opts.SetUsername(l.cfg.Username)
|
||||||
|
opts.SetPassword(l.cfg.Password)
|
||||||
|
opts.SetKeepAlive(60 * time.Second)
|
||||||
|
opts.SetAutoReconnect(true)
|
||||||
|
opts.SetConnectRetry(true)
|
||||||
|
opts.SetConnectRetryInterval(5 * time.Second)
|
||||||
|
opts.SetOnConnectHandler(func(client mqtt.Client) {
|
||||||
|
if err := l.subscribe(client); err != nil {
|
||||||
|
log.Printf("mqtt subscribe failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("mqtt connected to %s", broker)
|
||||||
|
})
|
||||||
|
opts.SetConnectionLostHandler(func(client mqtt.Client, err error) {
|
||||||
|
log.Printf("mqtt connection lost: %v", err)
|
||||||
|
})
|
||||||
|
opts.SetDefaultPublishHandler(l.handleMessage)
|
||||||
|
|
||||||
|
l.client = mqtt.NewClient(opts)
|
||||||
|
token := l.client.Connect()
|
||||||
|
if !token.WaitTimeout(15 * time.Second) {
|
||||||
|
return fmt.Errorf("mqtt connect timeout")
|
||||||
|
}
|
||||||
|
return token.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) subscribe(client mqtt.Client) error {
|
||||||
|
topics := []string{
|
||||||
|
fmt.Sprintf("/whgw/v2/region/%s/measurement/band/+/hr", l.cfg.Region),
|
||||||
|
fmt.Sprintf("/whgw/v2/region/%s/measurement/band/+/step", l.cfg.Region),
|
||||||
|
fmt.Sprintf("/whgw/v2/region/%s/gateway/+/status", l.cfg.Region),
|
||||||
|
}
|
||||||
|
for _, topic := range topics {
|
||||||
|
token := client.Subscribe(topic, byte(l.cfg.QoS), l.handleMessage)
|
||||||
|
if !token.WaitTimeout(10 * time.Second) {
|
||||||
|
return fmt.Errorf("mqtt subscribe timeout for topic %s", topic)
|
||||||
|
}
|
||||||
|
if err := token.Error(); err != nil {
|
||||||
|
return fmt.Errorf("mqtt subscribe topic %s: %w", topic, err)
|
||||||
|
}
|
||||||
|
log.Printf("mqtt subscribed: %s", topic)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) handleMessage(_ mqtt.Client, msg mqtt.Message) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("mqtt message handling panic, topic=%s err=%v", msg.Topic(), r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(msg.Payload()) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var packet whgw_hrpb.GatewaySlaveOutCloudMasterInMsg
|
||||||
|
if err := proto.Unmarshal(msg.Payload(), &packet); err != nil {
|
||||||
|
log.Printf("mqtt payload parse failed, topic=%s err=%v", msg.Topic(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
switch payload := packet.Choice.(type) {
|
||||||
|
case *whgw_hrpb.GatewaySlaveOutCloudMasterInMsg_NtfHrMeasurement:
|
||||||
|
record := buildHeartRateRecord(payload.NtfHrMeasurement, msg.Topic(), now)
|
||||||
|
l.enqueue(&record)
|
||||||
|
case *whgw_hrpb.GatewaySlaveOutCloudMasterInMsg_NtfStepCountMeasurement:
|
||||||
|
record := buildStepCountRecord(payload.NtfStepCountMeasurement, msg.Topic(), now)
|
||||||
|
l.enqueue(&record)
|
||||||
|
case *whgw_hrpb.GatewaySlaveOutCloudMasterInMsg_NtfGatewayStatus:
|
||||||
|
record := buildGatewayStatusRecord(payload.NtfGatewayStatus, msg.Topic(), now)
|
||||||
|
l.enqueue(&record)
|
||||||
|
default:
|
||||||
|
log.Printf("mqtt payload ignored, unsupported type on topic=%s", msg.Topic())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) enqueue(record interface{}) {
|
||||||
|
select {
|
||||||
|
case l.writeCh <- record:
|
||||||
|
default:
|
||||||
|
log.Printf("mqtt write queue full, dropping record of type %T", record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) writeWorker() {
|
||||||
|
for record := range l.writeCh {
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("mqtt record persist panic, type=%T err=%v", record, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := l.db.Clauses(clause.OnConflict{DoNothing: true}).Create(record).Error; err != nil {
|
||||||
|
log.Printf("mqtt record persist failed, type=%T err=%v", record, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildHeartRateRecord(measurement *whgw_hrpb.HrMeasurement, topic string, now int64) models.MqttHeartRateRecord {
|
||||||
|
regionID := measurement.GetGatewayInfo().GetRegionId()
|
||||||
|
if regionID == 0 {
|
||||||
|
regionID = parseRegionFromTopic(topic)
|
||||||
|
}
|
||||||
|
gatewayMAC := formatMAC(measurement.GetGatewayInfo().GetGatewayMac())
|
||||||
|
packet := measurement.GetHrPacket()
|
||||||
|
rssi, snr := parsePacketStatus(measurement.GetPacketStatus())
|
||||||
|
beltAddr := fmt.Sprintf("%d-%d", regionID, packet.GetId())
|
||||||
|
|
||||||
|
return models.MqttHeartRateRecord{
|
||||||
|
Identifier: fmt.Sprintf("hr:%d:%s:%d:%d", regionID, gatewayMAC, packet.GetId(), packet.GetPacketNum()),
|
||||||
|
Topic: topic,
|
||||||
|
RegionID: regionID,
|
||||||
|
GatewayMAC: gatewayMAC,
|
||||||
|
BandID: packet.GetId(),
|
||||||
|
BeltAddr: beltAddr,
|
||||||
|
PacketNum: packet.GetPacketNum(),
|
||||||
|
HeartRate: int(packet.GetHr()),
|
||||||
|
HrConfidence: int(packet.GetStatus().GetHrConfidence().Number()),
|
||||||
|
IsActive: packet.GetStatus().GetIsActive(),
|
||||||
|
IsOnSkin: packet.GetStatus().GetIsOnSkin(),
|
||||||
|
Battery: packet.GetStatus().GetBattery(),
|
||||||
|
SignalRSSINeg: rssi,
|
||||||
|
SNR: snr,
|
||||||
|
HubBusID: measurement.GetHubInfo().GetBusId(),
|
||||||
|
HubSubDevID: measurement.GetHubInfo().GetSubDevId(),
|
||||||
|
ReceivedAt: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildStepCountRecord(measurement *whgw_hrpb.StepCountMeasurement, topic string, now int64) models.MqttStepCountRecord {
|
||||||
|
regionID := measurement.GetGatewayInfo().GetRegionId()
|
||||||
|
if regionID == 0 {
|
||||||
|
regionID = parseRegionFromTopic(topic)
|
||||||
|
}
|
||||||
|
gatewayMAC := formatMAC(measurement.GetGatewayInfo().GetGatewayMac())
|
||||||
|
packet := measurement.GetStepCountPacket()
|
||||||
|
rssi, snr := parsePacketStatus(measurement.GetPacketStatus())
|
||||||
|
beltAddr := fmt.Sprintf("%d-%d", regionID, packet.GetId())
|
||||||
|
|
||||||
|
return models.MqttStepCountRecord{
|
||||||
|
Identifier: fmt.Sprintf("step:%d:%s:%d:%d", regionID, gatewayMAC, packet.GetId(), packet.GetPacketNum()),
|
||||||
|
Topic: topic,
|
||||||
|
RegionID: regionID,
|
||||||
|
GatewayMAC: gatewayMAC,
|
||||||
|
BandID: packet.GetId(),
|
||||||
|
BeltAddr: beltAddr,
|
||||||
|
PacketNum: packet.GetPacketNum(),
|
||||||
|
StepCount: packet.GetStepCount(),
|
||||||
|
SignalRSSINeg: rssi,
|
||||||
|
SNR: snr,
|
||||||
|
HubBusID: measurement.GetHubInfo().GetBusId(),
|
||||||
|
HubSubDevID: measurement.GetHubInfo().GetSubDevId(),
|
||||||
|
ReceivedAt: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGatewayStatusRecord(status *whgw_hrpb.GatewayStatus, topic string, now int64) models.MqttGatewayStatusRecord {
|
||||||
|
regionID := status.GetInfo().GetRegionId()
|
||||||
|
if regionID == 0 {
|
||||||
|
regionID = parseRegionFromTopic(topic)
|
||||||
|
}
|
||||||
|
gatewayMAC := formatMAC(status.GetInfo().GetGatewayMac())
|
||||||
|
|
||||||
|
return models.MqttGatewayStatusRecord{
|
||||||
|
Identifier: fmt.Sprintf("gateway:%d:%s:%d:%d:%d", regionID, gatewayMAC, status.GetStat().GetBootCount(), status.GetStat().GetUptimeMs(), status.GetStat().GetRxCount()),
|
||||||
|
Topic: topic,
|
||||||
|
RegionID: regionID,
|
||||||
|
GatewayMAC: gatewayMAC,
|
||||||
|
BootCount: status.GetStat().GetBootCount(),
|
||||||
|
UptimeMs: status.GetStat().GetUptimeMs(),
|
||||||
|
DurationMsSinceLastPacket: status.GetStat().GetDurationMsSinceLastPacket(),
|
||||||
|
RxCount: status.GetStat().GetRxCount(),
|
||||||
|
BatteryVoltageMV: status.GetStat().GetBatteryInfo().GetVoltageMv(),
|
||||||
|
BatterySOCPercentage: status.GetStat().GetBatteryInfo().GetSocPercentage(),
|
||||||
|
ChargingRatePercentage: status.GetStat().GetBatteryInfo().GetChargingRatePercentage(),
|
||||||
|
ReceivedAt: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePacketStatus(status *whgw_hrpb.IPacketStatus) (float64, float64) {
|
||||||
|
if status == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
if parsed := status.GetParsed(); parsed != nil {
|
||||||
|
return float64(parsed.GetSignalRssiNeg()), float64(parsed.GetSnrPkt())
|
||||||
|
}
|
||||||
|
if raw := status.GetRaw(); raw != nil {
|
||||||
|
return -float64(raw.GetSignalRssiX2Neg()) / 2, float64(raw.GetSnrPktX4()) / 4
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMAC(data []byte) string {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(data))
|
||||||
|
for _, b := range data {
|
||||||
|
parts = append(parts, fmt.Sprintf("%02x", b))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRegionFromTopic(topic string) uint32 {
|
||||||
|
parts := strings.Split(topic, "/")
|
||||||
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
|
if parts[i] == "region" {
|
||||||
|
var region uint32
|
||||||
|
if _, err := fmt.Sscanf(parts[i+1], "%d", ®ion); err == nil {
|
||||||
|
return region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package whgw_hr;
|
||||||
|
|
||||||
|
option go_package = "hr_receiver/proto;whgw_hrpb";
|
||||||
|
|
||||||
|
enum HrConfidence {
|
||||||
|
ZERO = 0;
|
||||||
|
LOW = 1;
|
||||||
|
MEDIUM = 2;
|
||||||
|
HIGH = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoRaBW {
|
||||||
|
BW_NONE = 0;
|
||||||
|
BW_10_4 = 0x08;
|
||||||
|
BW_15_6 = 0x01;
|
||||||
|
BW_20_8 = 0x09;
|
||||||
|
BW_31_25 = 0x02;
|
||||||
|
BW_41_7 = 0x0A;
|
||||||
|
BW_62_5 = 0x03;
|
||||||
|
BW_125_0 = 0x04;
|
||||||
|
BW_250_0 = 0x05;
|
||||||
|
BW_500_0 = 0x06;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoRaParameters {
|
||||||
|
LoRaBW bw = 1;
|
||||||
|
uint32 sf = 2;
|
||||||
|
float frequency_mhz = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StatusFlag {
|
||||||
|
HrConfidence hr_confidence = 1;
|
||||||
|
bool is_active = 2;
|
||||||
|
bool is_on_skin = 3;
|
||||||
|
uint32 battery = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HrPacket {
|
||||||
|
StatusFlag status = 1;
|
||||||
|
uint32 id = 2;
|
||||||
|
uint32 packet_num = 3;
|
||||||
|
uint32 hr = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StepCountPacket {
|
||||||
|
uint32 id = 1;
|
||||||
|
uint32 packet_num = 2;
|
||||||
|
uint32 step_count = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RawPacketStatus {
|
||||||
|
uint32 signal_rssi_x2_neg = 1;
|
||||||
|
int32 snr_pkt_x4 = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PacketStatus {
|
||||||
|
float signal_rssi_neg = 1;
|
||||||
|
float snr_pkt = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HubInfo {
|
||||||
|
uint32 bus_id = 1;
|
||||||
|
uint32 sub_dev_id = 2;
|
||||||
|
LoRaParameters radio_parameters = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RadioData {
|
||||||
|
HubInfo hub_info = 1;
|
||||||
|
RawPacketStatus raw_packet_status = 2;
|
||||||
|
bytes data = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PacketKind {
|
||||||
|
NTF = 0;
|
||||||
|
SIG = 1;
|
||||||
|
INF = 2;
|
||||||
|
CNF = 3;
|
||||||
|
REQ = 4;
|
||||||
|
RSP = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayStateReport {
|
||||||
|
bool network_has_connection = 1;
|
||||||
|
bool network_has_ip = 2;
|
||||||
|
bool network_has_mqtt_connection = 3;
|
||||||
|
bool bluetooth_has_connect = 4;
|
||||||
|
uint32 error_code = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HubSlaveOutGatewayMasterInMsg {
|
||||||
|
oneof choice {
|
||||||
|
RadioData ntf_radio_data = 1;
|
||||||
|
BatteryInfo ntf_battery_info = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayMasterOutHubSlaveInMsg {
|
||||||
|
oneof choice {
|
||||||
|
GatewayStateReport sig_gateway_state_report = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewaySlaveOutCloudMasterInMsg {
|
||||||
|
oneof choice {
|
||||||
|
HrMeasurement ntf_hr_measurement = 1;
|
||||||
|
GatewayStatus ntf_gateway_status = 2;
|
||||||
|
StepCountMeasurement ntf_step_count_measurement = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message BatteryInfo {
|
||||||
|
uint32 voltage_mv = 1;
|
||||||
|
uint32 soc_percentage = 2;
|
||||||
|
sint32 charging_rate_percentage = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayInfo {
|
||||||
|
uint32 region_id = 1;
|
||||||
|
bytes gateway_mac = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IPacketStatus {
|
||||||
|
oneof choice {
|
||||||
|
RawPacketStatus raw = 1;
|
||||||
|
PacketStatus parsed = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message HrMeasurement {
|
||||||
|
HrPacket hr_packet = 1;
|
||||||
|
IPacketStatus packet_status = 2;
|
||||||
|
GatewayInfo gateway_info = 3;
|
||||||
|
HubInfo hub_info = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StepCountMeasurement {
|
||||||
|
StepCountPacket step_count_packet = 1;
|
||||||
|
IPacketStatus packet_status = 2;
|
||||||
|
GatewayInfo gateway_info = 3;
|
||||||
|
HubInfo hub_info = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayStatistic {
|
||||||
|
uint32 boot_count = 1;
|
||||||
|
uint32 uptime_ms = 2;
|
||||||
|
uint32 duration_ms_since_last_packet = 3;
|
||||||
|
uint32 rx_count = 4;
|
||||||
|
BatteryInfo battery_info = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayStatus {
|
||||||
|
GatewayInfo info = 1;
|
||||||
|
GatewayStatistic stat = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Status {
|
||||||
|
int32 code = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GatewayConfigEntryKind {
|
||||||
|
REGION_ID = 0;
|
||||||
|
WIFI_SSID = 1;
|
||||||
|
WIFI_PASSWORD = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayConfigEntry {
|
||||||
|
oneof choice {
|
||||||
|
uint32 region_id = 1;
|
||||||
|
string wifi_ssid = 2;
|
||||||
|
string wifi_password = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayConfigMasterOutSlaveInMsg {
|
||||||
|
oneof choice {
|
||||||
|
GatewayConfigEntry cnf_get_entry = 1;
|
||||||
|
Status cnf_get_failure = 2;
|
||||||
|
Status cnf_set_entry = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayConfigMasterInSlaveOutMsg {
|
||||||
|
oneof choice {
|
||||||
|
GatewayConfigEntryKind inf_get_entry = 1;
|
||||||
|
GatewayConfigEntry inf_set_entry = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user