issue-18初步实现
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,4 +29,9 @@ message Collect{
|
|||||||
int32 bus_number = 1; // 总线号
|
int32 bus_number = 1; // 总线号
|
||||||
int32 bus_address = 2; // 总线地址
|
int32 bus_address = 2; // 总线地址
|
||||||
float value = 3; // 采集值
|
float value = 3; // 采集值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于批量上报的顶层消息
|
||||||
|
message UplinkPayload {
|
||||||
|
repeated Collect readings = 1;
|
||||||
}
|
}
|
||||||
@@ -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. 查找区域主控设备
|
||||||
// 根据业务逻辑,一个猪场只有一个网关,所以 RxInfo 中通常只有一个元素,或者 gateway_id 都是相同的。
|
regionalController, err := c.deviceRepo.FindByDevEui(event.DeviceInfo.DevEui)
|
||||||
// 因此,我们只取第一个 RxInfo 中的信号数据即可。
|
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 {
|
if len(event.RxInfo) > 0 {
|
||||||
|
// 根据业务逻辑,一个猪场只有一个网关,所以 RxInfo 中通常只有一个元素,或者 gateway_id 都是相同的。
|
||||||
|
// 因此,我们只取第一个 RxInfo 中的信号数据即可。
|
||||||
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)
|
||||||
decodedData, err := base64.StdEncoding.DecodeString(event.Data)
|
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 {
|
if err != nil {
|
||||||
c.logger.Errorf("Base64 解码 'up' 事件的 Data 失败: %v, Data: %s", err, event.Data)
|
c.logger.Errorf("查找传感器设备失败: %v", err)
|
||||||
return
|
continue // 继续处理下一条读数
|
||||||
}
|
}
|
||||||
|
|
||||||
var readings []GenericSensorReading
|
// ✨ 核心修正: 直接从 models 包的公开 map 中查找转换关系 ✨
|
||||||
if err := json.Unmarshal(decodedData, &readings); err != nil {
|
sensorDataType, ok := models.DeviceSubTypeToSensorDataTypeMap[sensorDevice.SubType]
|
||||||
c.logger.Errorf("解析 'up' 事件的解码后 Data (JSON 数组) 失败: %v, Decoded Data: %s", err, string(decodedData))
|
if !ok {
|
||||||
return
|
// 如果一个设备子类型不在 map 中, 说明它不是一个需要记录数据的传感器, 这属于正常情况, 无需记录日志.
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找区域主控设备,以便记录其ID
|
// 3.3.2 根据转换后的 sensorDataType 构建具体的数据结构
|
||||||
regionalController, err := c.deviceRepo.FindByDevEui(event.DeviceInfo.DevEui)
|
var sensorData interface{}
|
||||||
if err != nil {
|
switch sensorDataType {
|
||||||
c.logger.Errorf("处理 'up' 事件失败:无法通过 DevEui '%s' 找到区域主控设备: %v", event.DeviceInfo.DevEui, err)
|
case models.SensorDataTypeTemperature:
|
||||||
return
|
sensorData = models.TemperatureData{
|
||||||
}
|
TemperatureCelsius: float64(reading.Value),
|
||||||
|
|
||||||
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 // 跳过未知类型
|
|
||||||
}
|
}
|
||||||
|
case models.SensorDataTypeHumidity:
|
||||||
// 记录普通设备的传感器数据
|
sensorData = models.HumidityData{
|
||||||
c.recordSensorData(regionalController.ID, reading.DeviceID, event.Time, sensorDataType, sensorData)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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),受距离、障碍物影响
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user