issue-18 优化代码(只保证编译通过没检查)

This commit is contained in:
2025-09-26 22:50:08 +08:00
parent d9fe1683d2
commit 23b7f66d74
17 changed files with 767 additions and 251 deletions

View File

@@ -21,7 +21,10 @@ var (
type Service interface {
// Switch 用于切换指定设备的状态, 比如启动和停止
Switch(device models.Device, action DeviceAction) error
Switch(device *models.Device, action DeviceAction) error
// Collect 用于发起对指定区域主控下的多个设备的批量采集请求。
Collect(regionalControllerID uint, devicesToCollect []*models.Device) error
}
// 设备操作指令通用结构(最外层)

View File

@@ -1,31 +1,43 @@
package device
import (
"errors"
"fmt"
"strconv"
"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"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport"
"github.com/google/uuid"
gproto "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
type GeneralDeviceService struct {
deviceRepo repository.DeviceRepository
logger *logs.Logger
comm transport.Communicator
deviceRepo repository.DeviceRepository
deviceCommandLogRepo repository.DeviceCommandLogRepository
pendingCollectionRepo repository.PendingCollectionRepository
logger *logs.Logger
comm transport.Communicator
}
// NewGeneralDeviceService 创建一个通用设备服务
func NewGeneralDeviceService(deviceRepo repository.DeviceRepository, logger *logs.Logger, comm transport.Communicator) *GeneralDeviceService {
func NewGeneralDeviceService(
deviceRepo repository.DeviceRepository,
deviceCommandLogRepo repository.DeviceCommandLogRepository,
pendingCollectionRepo repository.PendingCollectionRepository,
logger *logs.Logger,
comm transport.Communicator,
) Service {
return &GeneralDeviceService{
deviceRepo: deviceRepo,
logger: logger,
comm: comm,
deviceRepo: deviceRepo,
deviceCommandLogRepo: deviceCommandLogRepo,
pendingCollectionRepo: pendingCollectionRepo,
logger: logger,
comm: comm,
}
}
@@ -45,6 +57,7 @@ func (g *GeneralDeviceService) Switch(device *models.Device, action DeviceAction
return fmt.Errorf("解析设备 %v(id=%v) 配置失败: %v", device.Name, device.ID, err)
}
// TODO 这种校验放自检里
busNumber, err := strconv.Atoi(fmt.Sprintf("%v", deviceInfo[models.BusNumber]))
if err != nil {
return fmt.Errorf("无效的总线号: %v", err)
@@ -87,11 +100,141 @@ func (g *GeneralDeviceService) Switch(device *models.Device, action DeviceAction
}
loraAddress := fmt.Sprintf("%v", thisDeviceinfo[models.LoRaAddress])
// 生成消息并发送
// 生成消息
message, err := gproto.Marshal(instruction)
if err != nil {
return fmt.Errorf("序列化指令失败: %v", err)
}
return g.comm.Send(loraAddress, message)
// 发送指令并获取 SendResult
sendResult, err := g.comm.Send(loraAddress, message)
if err != nil {
// 发送失败,直接返回错误
return fmt.Errorf("发送指令到设备 %s 失败: %w", loraAddress, err)
}
// 创建并保存命令日志
logRecord := &models.DeviceCommandLog{
MessageID: sendResult.MessageID,
DeviceID: thisDevice.ID, // thisDevice 是我们查出来的区域主控
SentAt: time.Now(),
}
if err := g.deviceCommandLogRepo.Create(logRecord); err != nil {
// 记录日志失败是一个需要关注的问题,但可能不应该中断主流程。
// 我们记录一个错误日志,然后成功返回。
g.logger.Errorf("创建指令日志失败 (MessageID: %s): %v", sendResult.MessageID, err)
}
g.logger.Infof("成功发送指令到设备 %s 并创建日志 (MessageID: %s)", loraAddress, sendResult.MessageID)
return nil
}
// Collect 实现了 Service 接口,用于发起对指定区域主控下的多个设备的批量采集请求。
// 它负责查找区域主控、生成关联ID、创建待处理记录、构建指令并最终发送。
func (g *GeneralDeviceService) Collect(regionalControllerID uint, devicesToCollect []*models.Device) error {
if regionalControllerID == 0 {
return errors.New("区域主控ID不能为空")
}
if len(devicesToCollect) == 0 {
// 如果没有要采集的设备,这不是一个错误,只是一个空操作。
g.logger.Info("待采集设备列表为空,无需执行采集任务。")
return nil
}
// 1. 查找并自检区域主控设备
regionalController, err := g.deviceRepo.FindByID(regionalControllerID)
if err != nil {
return fmt.Errorf("查找区域主控 (ID: %d) 失败: %w", regionalControllerID, err)
}
if !regionalController.SelfCheck() {
return fmt.Errorf("区域主控 (ID: %d) 未通过自检,缺少必要属性", regionalControllerID)
}
// 2. 准备采集任务列表和数据库存根,并验证设备
var childDeviceIDs []uint
var collectTasks []*proto.CollectTask
for _, dev := range devicesToCollect {
// 验证设备是否属于指定的区域主控
if dev.ParentID == nil || *dev.ParentID != regionalControllerID {
return fmt.Errorf("设备 '%s' (ID: %d) 不属于指定的区域主控 (ID: %d)", dev.Name, dev.ID, regionalControllerID)
}
// 对每个待采集的设备执行自检
if !dev.SelfCheck() {
g.logger.Warnf("跳过设备 %d因其未通过自检", dev.ID)
continue
}
// 自检已通过,我们可以安全地解析属性
var props map[string]interface{}
// 此时 ParseProperties 不应失败
_ = dev.ParseProperties(&props)
busNumber := props[models.BusNumber].(float64)
busAddress := props[models.BusAddress].(float64)
collectTasks = append(collectTasks, &proto.CollectTask{
DeviceAction: dev.Command,
BusNumber: int32(busNumber),
BusAddress: int32(busAddress),
})
childDeviceIDs = append(childDeviceIDs, dev.ID)
}
if len(childDeviceIDs) == 0 {
return errors.New("经过滤后,没有可通过自检的有效设备")
}
// 3. 从区域主控的属性中解析出 DevEui (loraAddress)
var rcProps map[string]interface{}
// SelfCheck 已保证属性可解析
_ = regionalController.ParseProperties(&rcProps)
loraAddress := rcProps[models.LoRaAddress].(string)
// 4. 创建待处理请求记录
correlationID := uuid.New().String()
pendingReq := &models.PendingCollection{
CorrelationID: correlationID,
DeviceID: regionalController.ID,
CommandMetadata: childDeviceIDs,
Status: models.PendingStatusPending,
CreatedAt: time.Now(),
}
if err := g.pendingCollectionRepo.Create(pendingReq); err != nil {
g.logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err)
return err
}
g.logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, regionalController.ID)
// 5. 构建最终的空中载荷
batchCmd := &proto.BatchCollectCommand{
CorrelationId: correlationID,
Tasks: collectTasks,
}
anyData, err := anypb.New(batchCmd)
if err != nil {
g.logger.Errorf("创建 Any Protobuf 失败 (CorrelationID: %s): %v", correlationID, err)
return err
}
instruction := &proto.Instruction{
Method: proto.MethodType_COLLECT,
Data: anyData,
}
payload, err := gproto.Marshal(instruction)
if err != nil {
g.logger.Errorf("序列化采集指令失败 (CorrelationID: %s): %v", correlationID, err)
return err
}
// 6. 发送指令
if _, err := g.comm.Send(loraAddress, payload); err != nil {
g.logger.DPanicf("待采集请求 (CorrelationID: %s) 已创建,但发送到设备失败: %v。数据可能不一致", correlationID, err)
return err
}
g.logger.Infof("成功将采集请求 (CorrelationID: %s) 发送到设备 %s", correlationID, loraAddress)
return nil
}

View File

@@ -69,7 +69,7 @@ func (MethodType) EnumDescriptor() ([]byte, []int) {
return file_device_proto_rawDescGZIP(), []int{0}
}
// 指令
// 指令 (所有空中数据都会被包装在这里面)
type Instruction struct {
state protoimpl.MessageState `protogen:"open.v1"`
Method MethodType `protobuf:"varint,1,opt,name=method,proto3,enum=device.MethodType" json:"method,omitempty"`
@@ -122,6 +122,7 @@ func (x *Instruction) GetData() *anypb.Any {
return nil
}
// Switch 指令的载荷
type Switch struct {
state protoimpl.MessageState `protogen:"open.v1"`
DeviceAction string `protobuf:"bytes,1,opt,name=device_action,json=deviceAction,proto3" json:"device_action,omitempty"` // 指令
@@ -190,29 +191,31 @@ func (x *Switch) GetRelayChannel() int32 {
return 0
}
type Collect struct {
// BatchCollectCommand
// 用于在平台内部构建一个完整的、包含所有元数据的批量采集任务。
// 这个消息本身不会被发送到设备。
type BatchCollectCommand struct {
state protoimpl.MessageState `protogen:"open.v1"`
BusNumber int32 `protobuf:"varint,1,opt,name=bus_number,json=busNumber,proto3" json:"bus_number,omitempty"` // 总线号
BusAddress int32 `protobuf:"varint,2,opt,name=bus_address,json=busAddress,proto3" json:"bus_address,omitempty"` // 总线地址
Value float32 `protobuf:"fixed32,3,opt,name=value,proto3" json:"value,omitempty"` // 采集值
CorrelationId string `protobuf:"bytes,1,opt,name=correlation_id,json=correlationId,proto3" json:"correlation_id,omitempty"` // 用于关联请求和响应的唯一ID
Tasks []*CollectTask `protobuf:"bytes,2,rep,name=tasks,proto3" json:"tasks,omitempty"` // 采集任务列表
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Collect) Reset() {
*x = Collect{}
func (x *BatchCollectCommand) Reset() {
*x = BatchCollectCommand{}
mi := &file_device_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Collect) String() string {
func (x *BatchCollectCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Collect) ProtoMessage() {}
func (*BatchCollectCommand) ProtoMessage() {}
func (x *Collect) ProtoReflect() protoreflect.Message {
func (x *BatchCollectCommand) ProtoReflect() protoreflect.Message {
mi := &file_device_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -224,55 +227,112 @@ func (x *Collect) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use Collect.ProtoReflect.Descriptor instead.
func (*Collect) Descriptor() ([]byte, []int) {
// Deprecated: Use BatchCollectCommand.ProtoReflect.Descriptor instead.
func (*BatchCollectCommand) Descriptor() ([]byte, []int) {
return file_device_proto_rawDescGZIP(), []int{2}
}
func (x *Collect) GetBusNumber() int32 {
func (x *BatchCollectCommand) GetCorrelationId() string {
if x != nil {
return x.CorrelationId
}
return ""
}
func (x *BatchCollectCommand) GetTasks() []*CollectTask {
if x != nil {
return x.Tasks
}
return nil
}
// CollectTask
// 定义了单个采集任务的“意图”。
type CollectTask struct {
state protoimpl.MessageState `protogen:"open.v1"`
DeviceAction string `protobuf:"bytes,1,opt,name=device_action,json=deviceAction,proto3" json:"device_action,omitempty"` // 指令
BusNumber int32 `protobuf:"varint,2,opt,name=bus_number,json=busNumber,proto3" json:"bus_number,omitempty"` // 总线号
BusAddress int32 `protobuf:"varint,3,opt,name=bus_address,json=busAddress,proto3" json:"bus_address,omitempty"` // 总线地址
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CollectTask) Reset() {
*x = CollectTask{}
mi := &file_device_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CollectTask) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CollectTask) ProtoMessage() {}
func (x *CollectTask) 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 CollectTask.ProtoReflect.Descriptor instead.
func (*CollectTask) Descriptor() ([]byte, []int) {
return file_device_proto_rawDescGZIP(), []int{3}
}
func (x *CollectTask) GetDeviceAction() string {
if x != nil {
return x.DeviceAction
}
return ""
}
func (x *CollectTask) GetBusNumber() int32 {
if x != nil {
return x.BusNumber
}
return 0
}
func (x *Collect) GetBusAddress() int32 {
func (x *CollectTask) GetBusAddress() int32 {
if x != nil {
return x.BusAddress
}
return 0
}
func (x *Collect) GetValue() float32 {
if x != nil {
return x.Value
}
return 0
}
// 用于批量上报的顶层消息
type UplinkPayload struct {
// CollectResult
// 这是设备响应的、极致精简的数据包。
type CollectResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
Readings []*Collect `protobuf:"bytes,1,rep,name=readings,proto3" json:"readings,omitempty"`
CorrelationId string `protobuf:"bytes,1,opt,name=correlation_id,json=correlationId,proto3" json:"correlation_id,omitempty"` // 从下行指令中原样返回的关联ID
Values []float32 `protobuf:"fixed32,2,rep,packed,name=values,proto3" json:"values,omitempty"` // 按预定顺序排列的采集值
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UplinkPayload) Reset() {
*x = UplinkPayload{}
mi := &file_device_proto_msgTypes[3]
func (x *CollectResult) Reset() {
*x = CollectResult{}
mi := &file_device_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UplinkPayload) String() string {
func (x *CollectResult) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UplinkPayload) ProtoMessage() {}
func (*CollectResult) ProtoMessage() {}
func (x *UplinkPayload) ProtoReflect() protoreflect.Message {
mi := &file_device_proto_msgTypes[3]
func (x *CollectResult) ProtoReflect() protoreflect.Message {
mi := &file_device_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -283,14 +343,21 @@ func (x *UplinkPayload) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use UplinkPayload.ProtoReflect.Descriptor instead.
func (*UplinkPayload) Descriptor() ([]byte, []int) {
return file_device_proto_rawDescGZIP(), []int{3}
// Deprecated: Use CollectResult.ProtoReflect.Descriptor instead.
func (*CollectResult) Descriptor() ([]byte, []int) {
return file_device_proto_rawDescGZIP(), []int{4}
}
func (x *UplinkPayload) GetReadings() []*Collect {
func (x *CollectResult) GetCorrelationId() string {
if x != nil {
return x.Readings
return x.CorrelationId
}
return ""
}
func (x *CollectResult) GetValues() []float32 {
if x != nil {
return x.Values
}
return nil
}
@@ -309,15 +376,19 @@ const file_device_proto_rawDesc = "" +
"bus_number\x18\x02 \x01(\x05R\tbusNumber\x12\x1f\n" +
"\vbus_address\x18\x03 \x01(\x05R\n" +
"busAddress\x12#\n" +
"\rrelay_channel\x18\x04 \x01(\x05R\frelayChannel\"_\n" +
"\aCollect\x12\x1d\n" +
"\rrelay_channel\x18\x04 \x01(\x05R\frelayChannel\"g\n" +
"\x13BatchCollectCommand\x12%\n" +
"\x0ecorrelation_id\x18\x01 \x01(\tR\rcorrelationId\x12)\n" +
"\x05tasks\x18\x02 \x03(\v2\x13.device.CollectTaskR\x05tasks\"r\n" +
"\vCollectTask\x12#\n" +
"\rdevice_action\x18\x01 \x01(\tR\fdeviceAction\x12\x1d\n" +
"\n" +
"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" +
"\rUplinkPayload\x12+\n" +
"\breadings\x18\x01 \x03(\v2\x0f.device.CollectR\breadings*%\n" +
"bus_number\x18\x02 \x01(\x05R\tbusNumber\x12\x1f\n" +
"\vbus_address\x18\x03 \x01(\x05R\n" +
"busAddress\"N\n" +
"\rCollectResult\x12%\n" +
"\x0ecorrelation_id\x18\x01 \x01(\tR\rcorrelationId\x12\x16\n" +
"\x06values\x18\x02 \x03(\x02R\x06values*%\n" +
"\n" +
"MethodType\x12\n" +
"\n" +
@@ -337,19 +408,20 @@ func file_device_proto_rawDescGZIP() []byte {
}
var file_device_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_device_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_device_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
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
(*UplinkPayload)(nil), // 4: device.UplinkPayload
(*anypb.Any)(nil), // 5: google.protobuf.Any
(MethodType)(0), // 0: device.MethodType
(*Instruction)(nil), // 1: device.Instruction
(*Switch)(nil), // 2: device.Switch
(*BatchCollectCommand)(nil), // 3: device.BatchCollectCommand
(*CollectTask)(nil), // 4: device.CollectTask
(*CollectResult)(nil), // 5: device.CollectResult
(*anypb.Any)(nil), // 6: google.protobuf.Any
}
var file_device_proto_depIdxs = []int32{
0, // 0: device.Instruction.method:type_name -> device.MethodType
5, // 1: device.Instruction.data:type_name -> google.protobuf.Any
3, // 2: device.UplinkPayload.readings:type_name -> device.Collect
6, // 1: device.Instruction.data:type_name -> google.protobuf.Any
4, // 2: device.BatchCollectCommand.tasks:type_name -> device.CollectTask
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
@@ -368,7 +440,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: 4,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},

View File

@@ -6,32 +6,50 @@ import "google/protobuf/any.proto";
option go_package = "internal/app/service/device/proto";
// --- 通用指令结构 ---
// 指令类型
enum MethodType{
SWITCH = 0; // 启停
enum MethodType {
SWITCH = 0; // 启停
COLLECT = 1; // 采集
}
// 指令
message Instruction{
// 指令 (所有空中数据都会被包装在这里面)
message Instruction {
MethodType method = 1;
google.protobuf.Any data = 2;
}
message Switch{
// Switch 指令的载荷
message Switch {
string device_action = 1; // 指令
int32 bus_number = 2; // 总线号
int32 bus_number = 2; // 总线号
int32 bus_address = 3; // 总线地址
int32 relay_channel = 4; // 继电器通道号
}
// --- 批量采集相关结构 ---
// BatchCollectCommand
// 用于在平台内部构建一个完整的、包含所有元数据的批量采集任务。
// 这个消息本身不会被发送到设备。
message BatchCollectCommand {
string correlation_id = 1; // 用于关联请求和响应的唯一ID
repeated CollectTask tasks = 2; // 采集任务列表
}
// CollectTask
// 定义了单个采集任务的“意图”。
message CollectTask {
string device_action = 1; // 指令
int32 bus_number = 2; // 总线号
int32 bus_address = 3; // 总线地址
int32 relay_channel = 4; // 继电器通道号
}
message Collect{
int32 bus_number = 1; // 总线号
int32 bus_address = 2; // 总线地址
float value = 3; // 采集值
}
// 用于批量上报的顶层消息
message UplinkPayload {
repeated Collect readings = 1;
// CollectResult
// 这是设备响应的、极致精简的数据包。
message CollectResult {
string correlation_id = 1; // 从下行指令中原样返回的关联ID
repeated float values = 2; // 按预定顺序排列的采集值
}

View File

@@ -21,28 +21,40 @@ type ReleaseFeedWeightTaskParams struct {
// ReleaseFeedWeightTask 是一个控制下料口释放指定重量的任务
type ReleaseFeedWeightTask struct {
deviceRepo repository.DeviceRepository
sensorDataRepo repository.SensorDataRepository
claimedLog *models.TaskExecutionLog
deviceRepo repository.DeviceRepository
sensorDataRepo repository.SensorDataRepository
deviceCommandLogRepo repository.DeviceCommandLogRepository
pendingCollectionRepo repository.PendingCollectionRepository
claimedLog *models.TaskExecutionLog
feedPortDevice *models.Device // 下料口基本信息
releaseWeight float64 // 需要释放的重量
mixingTankDeviceID uint // 搅拌罐称重传感器ID
comm transport.Communicator
feedPort *device.GeneralDeviceService // 下料口指令下发器
feedPort device.Service
logger *logs.Logger
}
// NewReleaseFeedWeightTask 创建一个新的 ReleaseFeedWeightTask 实例
func NewReleaseFeedWeightTask(claimedLog *models.TaskExecutionLog, deviceRepo repository.DeviceRepository, sensorDataRepo repository.SensorDataRepository, comm transport.Communicator, logger *logs.Logger) Task {
func NewReleaseFeedWeightTask(
claimedLog *models.TaskExecutionLog,
deviceRepo repository.DeviceRepository,
sensorDataRepo repository.SensorDataRepository,
deviceCommandLogRepo repository.DeviceCommandLogRepository,
pendingCollectionRepo repository.PendingCollectionRepository,
comm transport.Communicator,
logger *logs.Logger,
) Task {
return &ReleaseFeedWeightTask{
claimedLog: claimedLog,
deviceRepo: deviceRepo,
sensorDataRepo: sensorDataRepo,
comm: comm,
logger: logger,
claimedLog: claimedLog,
deviceRepo: deviceRepo,
sensorDataRepo: sensorDataRepo,
deviceCommandLogRepo: deviceCommandLogRepo,
pendingCollectionRepo: pendingCollectionRepo,
comm: comm,
logger: logger,
}
}
@@ -140,7 +152,7 @@ func (r *ReleaseFeedWeightTask) parseParameters() error {
r.releaseWeight = params.ReleaseWeight
r.mixingTankDeviceID = params.MixingTankDeviceID
r.feedPort = device.NewGeneralDeviceService(r.deviceRepo, r.logger, r.comm)
r.feedPort = device.NewGeneralDeviceService(r.deviceRepo, r.deviceCommandLogRepo, r.pendingCollectionRepo, r.logger, r.comm)
r.feedPortDevice, err = r.deviceRepo.FindByID(params.FeedPortDeviceID)
if err != nil {
r.logger.Errorf("任务 %v: 获取设备信息失败: %v", r.claimedLog.TaskID, err)

View File

@@ -83,6 +83,8 @@ type Scheduler struct {
executionLogRepo repository.ExecutionLogRepository
deviceRepo repository.DeviceRepository
sensorDataRepo repository.SensorDataRepository
deviceCommandLogRepo repository.DeviceCommandLogRepository
pendingCollectionRepo repository.PendingCollectionRepository
planRepo repository.PlanRepository
comm transport.Communicator
analysisPlanTaskManager *AnalysisPlanTaskManager
@@ -103,8 +105,11 @@ func NewScheduler(
comm transport.Communicator,
analysisPlanTaskManager *AnalysisPlanTaskManager,
logger *logs.Logger,
deviceCommandLogRepo repository.DeviceCommandLogRepository,
pendingCollectionRepo repository.PendingCollectionRepository,
interval time.Duration,
numWorkers int) *Scheduler {
numWorkers int,
) *Scheduler {
return &Scheduler{
pendingTaskRepo: pendingTaskRepo,
executionLogRepo: executionLogRepo,
@@ -114,6 +119,8 @@ func NewScheduler(
comm: comm,
analysisPlanTaskManager: analysisPlanTaskManager,
logger: logger,
deviceCommandLogRepo: deviceCommandLogRepo,
pendingCollectionRepo: pendingCollectionRepo,
pollingInterval: interval,
workers: numWorkers,
progressTracker: NewProgressTracker(),
@@ -289,7 +296,7 @@ func (s *Scheduler) taskFactory(claimedLog *models.TaskExecutionLog) Task {
case models.TaskTypeWaiting:
return NewDelayTask(s.logger, claimedLog)
case models.TaskTypeReleaseFeedWeight:
return NewReleaseFeedWeightTask(claimedLog, s.deviceRepo, s.sensorDataRepo, s.comm, s.logger)
return NewReleaseFeedWeightTask(claimedLog, s.deviceRepo, s.sensorDataRepo, s.deviceCommandLogRepo, s.pendingCollectionRepo, s.comm, s.logger)
default:
// TODO 这里直接panic合适吗? 不过这个场景确实不该出现任何异常的任务类型

View File

@@ -29,23 +29,27 @@ const (
// ChirpStackListener 是一个监听器, 用于监听ChirpStack反馈的设备上行事件
type ChirpStackListener struct {
logger *logs.Logger
sensorDataRepo repository.SensorDataRepository
deviceRepo repository.DeviceRepository
deviceCommandLogRepo repository.DeviceCommandLogRepository
logger *logs.Logger
sensorDataRepo repository.SensorDataRepository
deviceRepo repository.DeviceRepository
deviceCommandLogRepo repository.DeviceCommandLogRepository
pendingCollectionRepo repository.PendingCollectionRepository // 新增
}
// NewChirpStackListener 创建一个新的 ChirpStackListener 实例
func NewChirpStackListener(
logger *logs.Logger,
sensorDataRepo repository.SensorDataRepository,
deviceRepo repository.DeviceRepository,
deviceCommandLogRepo repository.DeviceCommandLogRepository,
) *ChirpStackListener {
pendingCollectionRepo repository.PendingCollectionRepository, // 新增
) ListenHandler { // 返回接口类型
return &ChirpStackListener{
logger: logger,
sensorDataRepo: sensorDataRepo,
deviceRepo: deviceRepo,
deviceCommandLogRepo: deviceCommandLogRepo,
logger: logger,
sensorDataRepo: sensorDataRepo,
deviceRepo: deviceRepo,
deviceCommandLogRepo: deviceCommandLogRepo,
pendingCollectionRepo: pendingCollectionRepo, // 新增
}
}
@@ -193,54 +197,92 @@ func (c *ChirpStackListener) handleUpEvent(event *UpEvent) {
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)
// 3.2 解析外层 "信封"
var instruction proto.Instruction
if err := gproto.Unmarshal(decodedData, &instruction); err != nil {
c.logger.Errorf("解析上行 Instruction Protobuf 失败: %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)
// 3.3 检查是否是采集响应
if instruction.Method != proto.MethodType_COLLECT {
c.logger.Infof("收到一个非采集响应的上行指令 (Method: %s),无需处理。", instruction.Method.String())
return
}
// 2.4 解包内层 CollectResult
var collectResp proto.CollectResult
if err := instruction.Data.UnmarshalTo(&collectResp); err != nil {
c.logger.Errorf("解包数据信息失败: %v", err)
return
}
correlationID := collectResp.CorrelationId
c.logger.Infof("成功解析采集响应 (CorrelationID: %s),包含 %d 个值。", correlationID, len(collectResp.Values))
// 3. 根据 CorrelationID 查找待处理请求
pendingReq, err := c.pendingCollectionRepo.FindByCorrelationID(correlationID)
if err != nil {
c.logger.Errorf("处理采集响应失败:无法找到待处理请求 (CorrelationID: %s): %v", correlationID, err)
return
}
// 检查状态,防止重复处理
if pendingReq.Status != models.PendingStatusPending && pendingReq.Status != models.PendingStatusTimedOut {
c.logger.Warnf("收到一个已处理过的采集响应 (CorrelationID: %s, Status: %s),将忽略。", correlationID, pendingReq.Status)
return
}
// 4. 匹配数据并存入数据库
deviceIDs := pendingReq.CommandMetadata
values := collectResp.Values
if len(deviceIDs) != len(values) {
c.logger.Errorf("数据不匹配:下行指令要求采集 %d 个设备,但上行响应包含 %d 个值 (CorrelationID: %s)", len(deviceIDs), len(values), correlationID)
// TODO 数量不匹配是否全改成失败
// 即使数量不匹配,也更新状态为完成,以防止请求永远 pending
err = c.pendingCollectionRepo.UpdateStatusToFulfilled(correlationID, event.Time)
if err != nil {
c.logger.Errorf("查找传感器设备失败: %v", err)
continue // 继续处理下一条读数
c.logger.Errorf("处理采集响应失败:无法更新待处理请求 (CorrelationID: %s) 的状态为完成: %v", correlationID, err)
}
return
}
// ✨ 核心修正: 直接从 models 包的公开 map 中查找转换关系 ✨
sensorDataType, ok := models.DeviceSubTypeToSensorDataTypeMap[sensorDevice.SubType]
if !ok {
// 如果一个设备子类型不在 map 中, 说明它不是一个需要记录数据的传感器, 这属于正常情况, 无需记录日志.
for i, deviceID := range deviceIDs {
value := values[i]
dev, err := c.deviceRepo.FindByID(deviceID)
if err != nil {
c.logger.Errorf("处理采集数据失败:无法找到设备 (ID: %d): %v", deviceID, err)
continue
}
sensorDataType, ok := models.DeviceSubTypeToSensorDataTypeMap[dev.SubType]
if !ok {
c.logger.Warnf("设备 %d 的子类型 '%s' 没有对应的传感器数据类型,跳过记录。", dev.ID, dev.SubType)
continue
}
// 3.3.2 根据转换后的 sensorDataType 构建具体的数据结构
var sensorData interface{}
switch sensorDataType {
case models.SensorDataTypeTemperature:
sensorData = models.TemperatureData{
TemperatureCelsius: float64(reading.Value),
}
sensorData = models.TemperatureData{TemperatureCelsius: float64(value)}
case models.SensorDataTypeHumidity:
sensorData = models.HumidityData{
HumidityPercent: float64(reading.Value),
}
sensorData = models.HumidityData{HumidityPercent: float64(value)}
case models.SensorDataTypeWeight:
sensorData = models.WeightData{
WeightKilograms: float64(reading.Value),
}
sensorData = models.WeightData{WeightKilograms: float64(value)}
default:
// 这个 case 理论上不会被触发
c.logger.Warnf("未处理的传感器数据类型 '%s' (设备ID: %d)", sensorDataType, sensorDevice.ID)
c.logger.Warnf("未处理的传感器数据类型 '%s' (设备ID: %d)", sensorDataType, dev.ID)
continue
}
// 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)
c.recordSensorData(pendingReq.DeviceID, dev.ID, event.Time, sensorDataType, sensorData)
c.logger.Infof("成功记录传感器数据: 设备ID=%d, 类型=%s, 值=%.2f", dev.ID, sensorDataType, value)
}
// 5. 更新请求状态为“已完成”
if err := c.pendingCollectionRepo.UpdateStatusToFulfilled(correlationID, event.Time); err != nil {
c.logger.Errorf("更新待采集请求状态为 'fulfilled' 失败 (CorrelationID: %s): %v", correlationID, err)
} else {
c.logger.Infof("成功完成并关闭采集请求 (CorrelationID: %s)", correlationID)
}
}