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
}
// 用于批量上报的顶层消息
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
const file_device_proto_rawDesc = "" +
@@ -270,7 +315,9 @@ const file_device_proto_rawDesc = "" +
"bus_number\x18\x01 \x01(\x05R\tbusNumber\x12\x1f\n" +
"\vbus_address\x18\x02 \x01(\x05R\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" +
"MethodType\x12\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_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_device_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_device_proto_goTypes = []any{
(MethodType)(0), // 0: device.MethodType
(*Instruction)(nil), // 1: device.Instruction
(*Switch)(nil), // 2: device.Switch
(*Collect)(nil), // 3: device.Collect
(*anypb.Any)(nil), // 4: google.protobuf.Any
(MethodType)(0), // 0: device.MethodType
(*Instruction)(nil), // 1: device.Instruction
(*Switch)(nil), // 2: device.Switch
(*Collect)(nil), // 3: device.Collect
(*UplinkPayload)(nil), // 4: device.UplinkPayload
(*anypb.Any)(nil), // 5: google.protobuf.Any
}
var file_device_proto_depIdxs = []int32{
0, // 0: device.Instruction.method:type_name -> device.MethodType
4, // 1: device.Instruction.data:type_name -> google.protobuf.Any
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
5, // 1: device.Instruction.data:type_name -> google.protobuf.Any
3, // 2: device.UplinkPayload.readings:type_name -> device.Collect
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension 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() }
@@ -319,7 +368,7 @@ func file_device_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_device_proto_rawDesc), len(file_device_proto_rawDesc)),
NumEnums: 1,
NumMessages: 3,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,9 @@ type DeviceRepository interface {
// FindByDevEui 根据 DevEui (存储在 properties JSONB 中的 lora_address) 查找设备 (新增)
FindByDevEui(devEui string) (*models.Device, error)
// FindByParentAndPhysicalAddress 根据父设备ID和物理地址(总线号、总线地址)查找设备
FindByParentAndPhysicalAddress(parentID uint, busNumber int32, busAddress int32) (*models.Device, error)
}
// gormDeviceRepository 是 DeviceRepository 的 GORM 实现
@@ -121,3 +124,18 @@ func (r *gormDeviceRepository) FindByDevEui(devEui string) (*models.Device, erro
}
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
}