实现定时采集

This commit is contained in:
2025-10-26 15:10:38 +08:00
parent 5c83c19bce
commit 6a8e8f1f7d
7 changed files with 176 additions and 14 deletions

View File

@@ -48,8 +48,10 @@ chirp_stack:
api_host: "http://localhost:8080" # ChirpStack API 主机地址
api_token: "your_chirpstack_api_token" # ChirpStack API Token
fport: 10 # ChirpStack FPort
api_timeout: 5 # API 请求超时时间 (秒)
collection_request_timeout: 10 # 采集请求超时时间 (秒)
api_timeout: 10 # ChirpStack API请求超时时间(秒)
# 等待设备上行响应的超时时间(秒)
# 对于LoRaWAN这种延迟较高的网络建议设置为5分钟 (300秒) 或更长。
collection_request_timeout: 300
# 任务调度配置
task:
@@ -62,12 +64,28 @@ lora:
# Lora Mesh 配置
lora_mesh:
uart_port: "/dev/ttyUSB0" # UART 串口路径
baud_rate: 115200 # 波特率
timeout: 5 # 超时时间 (秒)
lora_mesh_mode: "transparent" # Lora Mesh 模式: transparent, command
max_chunk_size: 200 # 最大数据块大小
reassembly_timeout: 10 # 重组超时时间 (秒)
# 主节点串口
uart_port: "COM7"
# LoRa模块的通信波特率
baud_rate: 9600
# 等待LoRa模块AT指令响应的超时时间(ms)
timeout: 50
# LoRa Mesh 模块发送模式(EC: 透传; ED: 完整数据包)
# e.g.
# EC: 接收端只会接收到消息, 不会接收到请求头
# e.g. 发送: EC 05 02 01 48 65 6c 6c 6f
# (EC + 05(消息长度) + 0201(地址) + "Hello"(消息本体))
# 接收: 48 65 6c 6c 6f ("Hello")
# ED: 接收端会接收完整数据包,包含自定义协议头和地址信息。
# e.g. 发送: ED 05 12 34 01 00 01 02 03
# (ED(帧头) + 05(Length, 即 1(总包数)+1(当前包序号)+3(数据块)) + 12 34(目标地址) + 01(总包数) + 00(当前包序号) + 01 02 03(数据块))
# 接收: ED 05 12 34 01 00 01 02 03 56 78(56 78 是发送方地址,会自动拼接到消息末尾)
lora_mesh_mode: "ED"
# 单包最大用户数据数据长度, 模块限制240, 去掉两位自定义包头, 还剩238
max_chunk_size: 238
#分片重组超时时间(秒)。如果在一个分片到达后,超过这个时间
# 还没收到完整的包,则认为接收失败。
reassembly_timeout: 30
# 通知服务配置
notify:
@@ -91,3 +109,7 @@ notify:
enabled: false # 是否启用飞书通知
appID: "cli_xxxxxxxxxx" # 应用 ID
appSecret: "your_lark_app_secret" # 应用密钥
# 定时采集配置
collection:
interval: 300 # 采集间隔 (秒)

View File

@@ -87,3 +87,7 @@ lora_mesh:
#分片重组超时时间(秒)。如果在一个分片到达后,超过这个时间
# 还没收到完整的包,则认为接收失败。
reassembly_timeout: 30
# 定时采集配置
collection:
interval: 300 # 采集间隔 (秒)

View File

@@ -11,6 +11,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/audit"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/collection"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
@@ -28,11 +29,12 @@ import (
// Application 是整个应用的核心,封装了所有组件和生命周期。
type Application struct {
Config *config.Config
Logger *logs.Logger
Storage database.Storage
Executor *task.Scheduler
API *api.API // 添加 API 对象
Config *config.Config
Logger *logs.Logger
Storage database.Storage
Executor *task.Scheduler
API *api.API
Collector collection.Collector
// 新增的仓库和管理器字段,以便在 initializePendingTasks 中访问
planRepo repository.PlanRepository
@@ -177,6 +179,14 @@ func NewApplication(configPath string) (*Application, error) {
cfg.Task.NumWorkers,
)
// --- 初始化定时采集器 ---
timedCollector := collection.NewTimedCollector(
deviceRepo,
generalDeviceService,
logger,
time.Duration(cfg.Collection.Interval)*time.Second,
)
// 初始化 API 服务器
apiServer := api.NewAPI(
cfg.Server,
@@ -204,6 +214,7 @@ func NewApplication(configPath string) (*Application, error) {
Storage: storage,
Executor: executor,
API: apiServer,
Collector: timedCollector,
planRepo: planRepo,
pendingTaskRepo: pendingTaskRepo,
executionLogRepo: executionLogRepo,
@@ -327,6 +338,9 @@ func (app *Application) Start() error {
// 启动任务执行器
app.Executor.Start()
// 启动定时采集器
app.Collector.Start()
// 启动 API 服务器
app.API.Start()
@@ -349,6 +363,9 @@ func (app *Application) Stop() error {
// 关闭任务执行器
app.Executor.Stop()
// 关闭定时采集器
app.Collector.Stop()
// 断开数据库连接
if err := app.Storage.Disconnect(); err != nil {
app.Logger.Errorw("数据库连接断开失败", "error", err)

View File

@@ -0,0 +1,6 @@
package collection
type Collector interface {
Start()
Stop()
}

View File

@@ -0,0 +1,89 @@
package collection
import (
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
"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"
)
// TimedCollector 实现了 Collector 接口,用于定时从数据库获取设备信息并下发采集指令
type TimedCollector struct {
deviceRepo repository.DeviceRepository
deviceService device.Service
logger *logs.Logger
interval time.Duration
ticker *time.Ticker
done chan bool
}
// NewTimedCollector 创建一个定时采集器实例
func NewTimedCollector(
deviceRepo repository.DeviceRepository,
deviceService device.Service,
logger *logs.Logger,
interval time.Duration,
) Collector {
return &TimedCollector{
deviceRepo: deviceRepo,
deviceService: deviceService,
logger: logger,
interval: interval,
done: make(chan bool),
}
}
// Start 开始定时采集
func (c *TimedCollector) Start() {
c.logger.Infof("定时采集器启动,采集间隔: %s", c.interval)
c.ticker = time.NewTicker(c.interval)
go func() {
for {
select {
case <-c.done:
return
case <-c.ticker.C:
c.collect()
}
}
}()
}
// Stop 停止定时采集
func (c *TimedCollector) Stop() {
c.logger.Info("定时采集器停止")
c.ticker.Stop()
c.done <- true
}
// collect 是核心的采集逻辑
func (c *TimedCollector) collect() {
c.logger.Info("开始新一轮的设备数据采集")
sensors, err := c.deviceRepo.ListAllSensors()
if err != nil {
c.logger.Errorf("采集周期: 从数据库获取所有传感器失败: %v", err)
return
}
if len(sensors) == 0 {
c.logger.Info("采集周期: 未发现任何传感器设备,跳过本次采集")
return
}
sensorsByController := make(map[uint][]*models.Device)
for _, sensor := range sensors {
sensorsByController[sensor.AreaControllerID] = append(sensorsByController[sensor.AreaControllerID], sensor)
}
for controllerID, controllerSensors := range sensorsByController {
c.logger.Infof("采集周期: 准备为区域主控 %d 下的 %d 个传感器下发采集指令", controllerID, len(controllerSensors))
if err := c.deviceService.Collect(controllerID, controllerSensors); err != nil {
c.logger.Errorf("采集周期: 为区域主控 %d 下发采集指令失败: %v", controllerID, err)
}
}
c.logger.Info("本轮设备数据采集完成")
}

View File

@@ -44,6 +44,9 @@ type Config struct {
// Notify 通知服务配置
Notify NotifyConfig `yaml:"notify"`
// Collection 定时采集配置
Collection CollectionConfig `yaml:"collection"`
}
// AppConfig 代表应用基础配置
@@ -195,6 +198,11 @@ type LarkConfig struct {
AppSecret string `yaml:"appSecret"`
}
// CollectionConfig 代表定时采集配置
type CollectionConfig struct {
Interval int `yaml:"interval"`
}
// NewConfig 创建并返回一个新的配置实例
func NewConfig() *Config {
// 默认值可以在这里设置,但我们优先使用配置文件中的值

View File

@@ -23,6 +23,9 @@ type DeviceRepository interface {
// ListAll 获取所有设备的列表
ListAll() ([]*models.Device, error)
// ListAllSensors 获取所有传感器类型的设备列表
ListAllSensors() ([]*models.Device, error)
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备。
ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error)
@@ -84,6 +87,19 @@ func (r *gormDeviceRepository) ListAll() ([]*models.Device, error) {
return devices, nil
}
// ListAllSensors 检索归类为传感器的所有设备
func (r *gormDeviceRepository) ListAllSensors() ([]*models.Device, error) {
var sensors []*models.Device
err := r.db.Preload("AreaController").Preload("DeviceTemplate").
Joins("JOIN device_templates ON device_templates.id = devices.device_template_id").
Where("device_templates.category = ?", models.CategorySensor).
Find(&sensors).Error
if err != nil {
return nil, fmt.Errorf("查询所有传感器失败: %w", err)
}
return sensors, nil
}
// ListByAreaControllerID 根据区域主控 ID 列出所有子设备
func (r *gormDeviceRepository) ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error) {
var devices []*models.Device