issue-18初步实现

This commit is contained in:
2025-09-26 15:26:21 +08:00
parent 3b4de24e1d
commit d9fe1683d2
6 changed files with 165 additions and 73 deletions

View File

@@ -250,6 +250,51 @@ func (x *Collect) GetValue() float32 {
return 0 return 0
} }
// 用于批量上报的顶层消息
type UplinkPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
Readings []*Collect `protobuf:"bytes,1,rep,name=readings,proto3" json:"readings,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UplinkPayload) Reset() {
*x = UplinkPayload{}
mi := &file_device_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UplinkPayload) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UplinkPayload) ProtoMessage() {}
func (x *UplinkPayload) ProtoReflect() protoreflect.Message {
mi := &file_device_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UplinkPayload.ProtoReflect.Descriptor instead.
func (*UplinkPayload) Descriptor() ([]byte, []int) {
return file_device_proto_rawDescGZIP(), []int{3}
}
func (x *UplinkPayload) GetReadings() []*Collect {
if x != nil {
return x.Readings
}
return nil
}
var File_device_proto protoreflect.FileDescriptor var File_device_proto protoreflect.FileDescriptor
const file_device_proto_rawDesc = "" + const file_device_proto_rawDesc = "" +
@@ -270,7 +315,9 @@ const file_device_proto_rawDesc = "" +
"bus_number\x18\x01 \x01(\x05R\tbusNumber\x12\x1f\n" + "bus_number\x18\x01 \x01(\x05R\tbusNumber\x12\x1f\n" +
"\vbus_address\x18\x02 \x01(\x05R\n" + "\vbus_address\x18\x02 \x01(\x05R\n" +
"busAddress\x12\x14\n" + "busAddress\x12\x14\n" +
"\x05value\x18\x03 \x01(\x02R\x05value*%\n" + "\x05value\x18\x03 \x01(\x02R\x05value\"<\n" +
"\rUplinkPayload\x12+\n" +
"\breadings\x18\x01 \x03(\v2\x0f.device.CollectR\breadings*%\n" +
"\n" + "\n" +
"MethodType\x12\n" + "MethodType\x12\n" +
"\n" + "\n" +
@@ -290,22 +337,24 @@ func file_device_proto_rawDescGZIP() []byte {
} }
var file_device_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_device_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_device_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_device_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_device_proto_goTypes = []any{ var file_device_proto_goTypes = []any{
(MethodType)(0), // 0: device.MethodType (MethodType)(0), // 0: device.MethodType
(*Instruction)(nil), // 1: device.Instruction (*Instruction)(nil), // 1: device.Instruction
(*Switch)(nil), // 2: device.Switch (*Switch)(nil), // 2: device.Switch
(*Collect)(nil), // 3: device.Collect (*Collect)(nil), // 3: device.Collect
(*anypb.Any)(nil), // 4: google.protobuf.Any (*UplinkPayload)(nil), // 4: device.UplinkPayload
(*anypb.Any)(nil), // 5: google.protobuf.Any
} }
var file_device_proto_depIdxs = []int32{ var file_device_proto_depIdxs = []int32{
0, // 0: device.Instruction.method:type_name -> device.MethodType 0, // 0: device.Instruction.method:type_name -> device.MethodType
4, // 1: device.Instruction.data:type_name -> google.protobuf.Any 5, // 1: device.Instruction.data:type_name -> google.protobuf.Any
2, // [2:2] is the sub-list for method output_type 3, // 2: device.UplinkPayload.readings:type_name -> device.Collect
2, // [2:2] is the sub-list for method input_type 3, // [3:3] is the sub-list for method output_type
2, // [2:2] is the sub-list for extension type_name 3, // [3:3] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension extendee 3, // [3:3] is the sub-list for extension type_name
0, // [0:2] is the sub-list for field type_name 3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
} }
func init() { file_device_proto_init() } func init() { file_device_proto_init() }
@@ -319,7 +368,7 @@ func file_device_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_device_proto_rawDesc), len(file_device_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_device_proto_rawDesc), len(file_device_proto_rawDesc)),
NumEnums: 1, NumEnums: 1,
NumMessages: 3, NumMessages: 4,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@@ -30,3 +30,8 @@ message Collect{
int32 bus_address = 2; // 总线地址 int32 bus_address = 2; // 总线地址
float value = 3; // 采集值 float value = 3; // 采集值
} }
// 用于批量上报的顶层消息
message UplinkPayload {
repeated Collect readings = 1;
}

View File

@@ -7,9 +7,11 @@ import (
"net/http" "net/http"
"time" "time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/device/proto"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
gproto "google.golang.org/protobuf/proto"
"gorm.io/datatypes" "gorm.io/datatypes"
) )
@@ -151,12 +153,20 @@ type GenericSensorReading struct {
// handleUpEvent 处理上行数据事件 // handleUpEvent 处理上行数据事件
func (c *ChirpStackListener) handleUpEvent(event *UpEvent) { func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
c.logger.Infof("处理 'up' 事件: %+v", event) c.logger.Infof("开始处理 'up' 事件, DevEui: %s", event.DeviceInfo.DevEui)
// 记录信号强度 // 1. 查找区域主控设备
regionalController, err := c.deviceRepo.FindByDevEui(event.DeviceInfo.DevEui)
if err != nil {
c.logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
return
}
c.logger.Infof("找到区域主控: %s (ID: %d)", regionalController.Name, regionalController.ID)
// 2. 记录区域主控的信号强度 (如果存在)
if len(event.RxInfo) > 0 {
// 根据业务逻辑,一个猪场只有一个网关,所以 RxInfo 中通常只有一个元素,或者 gateway_id 都是相同的。 // 根据业务逻辑,一个猪场只有一个网关,所以 RxInfo 中通常只有一个元素,或者 gateway_id 都是相同的。
// 因此,我们只取第一个 RxInfo 中的信号数据即可。 // 因此,我们只取第一个 RxInfo 中的信号数据即可。
if len(event.RxInfo) > 0 {
rx := event.RxInfo[0] // 取第一个接收到的网关信息 rx := event.RxInfo[0] // 取第一个接收到的网关信息
// 构建 SignalMetrics 结构体 // 构建 SignalMetrics 结构体
@@ -164,73 +174,73 @@ func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
RssiDbm: rx.Rssi, RssiDbm: rx.Rssi,
SnrDb: rx.Snr, SnrDb: rx.Snr,
} }
// 这里的 event.DeviceInfo.DevEui 对应的是区域主控的 DevEui
regionalController, err := c.deviceRepo.FindByDevEui(event.DeviceInfo.DevEui)
if err != nil {
c.logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
return
}
// 记录区域主控的信号强度
c.recordSensorData(regionalController.ID, regionalController.ID, event.Time, models.SensorDataTypeSignalMetrics, signalMetrics) c.recordSensorData(regionalController.ID, regionalController.ID, event.Time, models.SensorDataTypeSignalMetrics, signalMetrics)
c.logger.Infof("已记录区域主控 (ID: %d) 的信号强度: RSSI=%d, SNR=%.2f", regionalController.ID, rx.Rssi, rx.Snr)
} else { } else {
c.logger.Warnf("处理 'up' 事件时未找到 RxInfo无法记录信号数据。DevEui: %s", event.DeviceInfo.DevEui) c.logger.Warnf("处理 'up' 事件时未找到 RxInfo无法记录信号数据。DevEui: %s", event.DeviceInfo.DevEui)
} }
// 解析并记录传感器数据 (温度、湿度、重量) // 3. 处理上报的传感器数据
// 假设 event.Data (frmPayload) 是 Base64 编码的 JSON 数组字符串 if event.Data == "" {
if event.Data != "" { c.logger.Warnf("处理 'up' 事件时 Data 字段为空无需记录上行数据。DevEui: %s", event.DeviceInfo.DevEui)
return
}
// 3.1 Base64 解码
decodedData, err := base64.StdEncoding.DecodeString(event.Data) decodedData, err := base64.StdEncoding.DecodeString(event.Data)
if err != nil { if err != nil {
c.logger.Errorf("Base64 解码 'up' 事件的 Data 失败: %v, Data: %s", err, event.Data) c.logger.Errorf("Base64 解码 'up' 事件的 Data 失败: %v, Data: %s", err, event.Data)
return return
} }
var readings []GenericSensorReading // 3.2 Protobuf 反序列化
if err := json.Unmarshal(decodedData, &readings); err != nil { var payload proto.UplinkPayload
c.logger.Errorf("解析 'up' 事件的解码后 Data (JSON 数组) 失败: %v, Decoded Data: %s", err, string(decodedData)) if err := gproto.Unmarshal(decodedData, &payload); err != nil {
c.logger.Errorf("Protobuf 反序列化 'up' 事件的解码后 Data 失败: %v, Decoded Data: %x", err, decodedData)
return return
} }
c.logger.Infof("成功解析 Protobuf 数据, 包含 %d 条读数", len(payload.Readings))
// 查找区域主控设备以便记录其ID // 3.3 遍历处理每一条读数
regionalController, err := c.deviceRepo.FindByDevEui(event.DeviceInfo.DevEui) for _, reading := range payload.Readings {
// 3.3.1 根据物理地址查找对应的传感器设备
sensorDevice, err := c.deviceRepo.FindByParentAndPhysicalAddress(regionalController.ID, reading.BusNumber, reading.BusAddress)
if err != nil { if err != nil {
c.logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err) c.logger.Errorf("查找传感器设备失败: %v", err)
return continue // 继续处理下一条读数
} }
for _, reading := range readings { // ✨ 核心修正: 直接从 models 包的公开 map 中查找转换关系 ✨
// 根据类型构建具体的传感器数据结构体 sensorDataType, ok := models.DeviceSubTypeToSensorDataTypeMap[sensorDevice.SubType]
if !ok {
// 如果一个设备子类型不在 map 中, 说明它不是一个需要记录数据的传感器, 这属于正常情况, 无需记录日志.
continue
}
// 3.3.2 根据转换后的 sensorDataType 构建具体的数据结构
var sensorData interface{} var sensorData interface{}
var sensorDataType models.SensorDataType switch sensorDataType {
case models.SensorDataTypeTemperature:
switch reading.Type {
case models.SensorDataTypeTemperature: // 使用枚举常量
sensorData = models.TemperatureData{ sensorData = models.TemperatureData{
TemperatureCelsius: reading.Value, TemperatureCelsius: float64(reading.Value),
} }
sensorDataType = models.SensorDataTypeTemperature case models.SensorDataTypeHumidity:
case models.SensorDataTypeHumidity: // 使用枚举常量
sensorData = models.HumidityData{ sensorData = models.HumidityData{
HumidityPercent: reading.Value, HumidityPercent: float64(reading.Value),
} }
sensorDataType = models.SensorDataTypeHumidity case models.SensorDataTypeWeight:
case models.SensorDataTypeWeight: // 使用枚举常量
sensorData = models.WeightData{ sensorData = models.WeightData{
WeightKilograms: reading.Value, WeightKilograms: float64(reading.Value),
} }
sensorDataType = models.SensorDataTypeWeight
default: default:
c.logger.Warnf("处理 'up' 事件时遇到未知传感器类型: %s, Value: %f. 区域主控DevEui: %s, 传感器设备ID: %d", // 这个 case 理论上不会被触发
reading.Type, reading.Value, event.DeviceInfo.DevEui, reading.DeviceID) c.logger.Warnf("未处理的传感器数据类型 '%s' (设备ID: %d)", sensorDataType, sensorDevice.ID)
continue // 跳过未知类型 continue
} }
// 记录普通设备的传感器数据 // 3.3.3 记录传感器数据
c.recordSensorData(regionalController.ID, reading.DeviceID, event.Time, sensorDataType, sensorData) c.recordSensorData(regionalController.ID, sensorDevice.ID, event.Time, sensorDataType, sensorData)
} c.logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 值=%.2f", sensorDevice.ID, sensorDataType, reading.Value)
} else {
c.logger.Warnf("处理 'up' 事件时 Data 字段为空无法记录传感器数据。DevEui: %s", event.DeviceInfo.DevEui)
} }
} }

View File

@@ -30,6 +30,8 @@ const (
SubTypeSensorHumidity DeviceSubType = "humidity" SubTypeSensorHumidity DeviceSubType = "humidity"
// SubTypeSensorAmmonia 氨气传感器 // SubTypeSensorAmmonia 氨气传感器
SubTypeSensorAmmonia DeviceSubType = "ammonia" SubTypeSensorAmmonia DeviceSubType = "ammonia"
// SubTypeSensorWeight 电子秤
SubTypeSensorWeight DeviceSubType = "weight"
// SubTypeValveFeed 下料阀门 // SubTypeValveFeed 下料阀门
SubTypeValveFeed DeviceSubType = "feed_valve" SubTypeValveFeed DeviceSubType = "feed_valve"

View File

@@ -17,6 +17,14 @@ const (
SensorDataTypeWeight SensorDataType = "weight" // 重量 SensorDataTypeWeight SensorDataType = "weight" // 重量
) )
// DeviceSubTypeToSensorDataTypeMap 定义了设备子类型到其产生的传感器数据类型的静态映射.
// 这个公开的 map 是连接设备定义和数据记录的桥梁, 供其他包直接查询.
var DeviceSubTypeToSensorDataTypeMap = map[DeviceSubType]SensorDataType{
SubTypeSensorTemp: SensorDataTypeTemperature,
SubTypeSensorHumidity: SensorDataTypeHumidity,
SubTypeSensorWeight: SensorDataTypeWeight,
}
// SignalMetrics 存储信号强度数据 // SignalMetrics 存储信号强度数据
type SignalMetrics struct { type SignalMetrics struct {
RssiDbm int `json:"rssi_dbm"` // 绝对信号强度dBm受距离、障碍物影响 RssiDbm int `json:"rssi_dbm"` // 绝对信号强度dBm受距离、障碍物影响

View File

@@ -35,6 +35,9 @@ type DeviceRepository interface {
// FindByDevEui 根据 DevEui (存储在 properties JSONB 中的 lora_address) 查找设备 (新增) // FindByDevEui 根据 DevEui (存储在 properties JSONB 中的 lora_address) 查找设备 (新增)
FindByDevEui(devEui string) (*models.Device, error) FindByDevEui(devEui string) (*models.Device, error)
// FindByParentAndPhysicalAddress 根据父设备ID和物理地址(总线号、总线地址)查找设备
FindByParentAndPhysicalAddress(parentID uint, busNumber int32, busAddress int32) (*models.Device, error)
} }
// gormDeviceRepository 是 DeviceRepository 的 GORM 实现 // gormDeviceRepository 是 DeviceRepository 的 GORM 实现
@@ -121,3 +124,18 @@ func (r *gormDeviceRepository) FindByDevEui(devEui string) (*models.Device, erro
} }
return &device, nil return &device, nil
} }
// FindByParentAndPhysicalAddress 根据父设备ID和物理地址(总线号、总线地址)查找设备
func (r *gormDeviceRepository) FindByParentAndPhysicalAddress(parentID uint, busNumber int32, busAddress int32) (*models.Device, error) {
var device models.Device
// PostgreSQL 使用 ->> 操作符来查询 JSONB 字段的文本值
err := r.db.Where("parent_id = ?", parentID).
Where("properties->>'bus_number' = ?", strconv.Itoa(int(busNumber))).
Where("properties->>'bus_address' = ?", strconv.Itoa(int(busAddress))).
First(&device).Error
if err != nil {
return nil, fmt.Errorf("根据父设备ID %d 和物理地址 (总线号: %d, 总线地址: %d) 查找设备失败: %w", parentID, busNumber, busAddress, err)
}
return &device, nil
}