148 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			148 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package lora
 | 
						||
 | 
						||
import (
 | 
						||
	"time"
 | 
						||
 | 
						||
	"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/lora/chirp_stack_proto/client/device_service"
 | 
						||
	"github.com/go-openapi/runtime"
 | 
						||
	httptransport "github.com/go-openapi/runtime/client"
 | 
						||
	"github.com/go-openapi/strfmt"
 | 
						||
 | 
						||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/transport/lora/chirp_stack_proto/client"
 | 
						||
)
 | 
						||
 | 
						||
// ChirpStackConfig 保存连接到 ChirpStack API 所需的配置。
 | 
						||
type ChirpStackConfig struct {
 | 
						||
	// ServerAddress 是 ChirpStack API 服务器的地址,例如 "localhost:8080"。
 | 
						||
	ServerAddress string
 | 
						||
	// APIKey 是用于认证的 API 密钥。
 | 
						||
	APIKey string
 | 
						||
	// LoRaWAN 端口, 需要和设备一致
 | 
						||
	Fport int64
 | 
						||
}
 | 
						||
 | 
						||
// GenerateAPIKey 用于补齐API Key作为请求头时缺失的部分
 | 
						||
func (c ChirpStackConfig) GenerateAPIKey() string {
 | 
						||
	return "Bearer " + c.APIKey
 | 
						||
}
 | 
						||
 | 
						||
// ChirpStackTransport 是一个客户端,用于封装与 ChirpStack REST API 的交互。
 | 
						||
type ChirpStackTransport struct {
 | 
						||
	client   *client.ChirpStackRESTAPI
 | 
						||
	authInfo runtime.ClientAuthInfoWriter
 | 
						||
	config   ChirpStackConfig
 | 
						||
 | 
						||
	deviceCommandLogRepo repository.DeviceCommandLogRepository
 | 
						||
	deviceRepo           repository.DeviceRepository
 | 
						||
 | 
						||
	logger *logs.Logger
 | 
						||
}
 | 
						||
 | 
						||
// NewChirpStackTransport 创建一个新的通信实例,用于与 ChirpStack 通信。
 | 
						||
func NewChirpStackTransport(
 | 
						||
	config ChirpStackConfig,
 | 
						||
	logger *logs.Logger,
 | 
						||
	deviceCommandLogRepo repository.DeviceCommandLogRepository,
 | 
						||
	deviceRepo repository.DeviceRepository,
 | 
						||
) *ChirpStackTransport {
 | 
						||
	// 使用配置中的服务器地址创建一个 HTTP transport。
 | 
						||
	// 它会使用生成的客户端中定义的默认 base path 和 schemes。
 | 
						||
	transport := httptransport.New(config.ServerAddress, client.DefaultBasePath, client.DefaultSchemes)
 | 
						||
 | 
						||
	// 使用 transport 和默认的字符串格式化器,创建一个 API 主客户端。
 | 
						||
	apiClient := client.New(transport, strfmt.Default)
 | 
						||
 | 
						||
	// 使用 API Key 创建认证信息写入器。
 | 
						||
	authInfo := httptransport.APIKeyAuth("grpc-metadata-authorization", "header", config.GenerateAPIKey())
 | 
						||
 | 
						||
	return &ChirpStackTransport{
 | 
						||
		client:               apiClient,
 | 
						||
		authInfo:             authInfo,
 | 
						||
		config:               config,
 | 
						||
		logger:               logger,
 | 
						||
		deviceCommandLogRepo: deviceCommandLogRepo,
 | 
						||
		deviceRepo:           deviceRepo,
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
func (c *ChirpStackTransport) Send(address string, payload []byte) error {
 | 
						||
	// 1. 构建 API 请求体。
 | 
						||
	//    - Confirmed: true 表示确认消息, 设为false将不保证消息送达(但可以节约下行容量)。
 | 
						||
	//    - Data: 经过 Base64 编码的数据。
 | 
						||
	//    - FPort: LoRaWAN 端口。
 | 
						||
	body := device_service.DeviceServiceEnqueueBody{
 | 
						||
		QueueItem: &device_service.DeviceServiceEnqueueParamsBodyQueueItem{
 | 
						||
			Confirmed: true,
 | 
						||
			Data:      payload,
 | 
						||
			FPort:     c.config.Fport,
 | 
						||
		},
 | 
						||
	}
 | 
						||
 | 
						||
	// 2. 构建 API 请求参数。
 | 
						||
	//    - WithTimeout 设置一个合理的请求超时。
 | 
						||
	//    - WithQueueItemDevEui 指定目标设备的 EUI。
 | 
						||
	//    - WithBody 设置请求体。
 | 
						||
	params := device_service.NewDeviceServiceEnqueueParams().
 | 
						||
		WithTimeout(10 * time.Second).
 | 
						||
		WithQueueItemDevEui(address).
 | 
						||
		WithBody(body)
 | 
						||
 | 
						||
	// 3. 调用生成的客户端方法来发送请求。
 | 
						||
	//    c.authInfo 是您在 NewChirpStackTransport 中创建的认证信息。
 | 
						||
	resp, err := c.client.DeviceService.DeviceServiceEnqueue(params, c.authInfo)
 | 
						||
	if err != nil {
 | 
						||
		c.logger.Errorf("设备 %s 调用ChirpStack Enqueue失败: %v", address, err)
 | 
						||
		return err
 | 
						||
	}
 | 
						||
 | 
						||
	// 4. 成功发送后,尝试记录下行任务
 | 
						||
	messageID := ""
 | 
						||
	if resp != nil && resp.Payload != nil && resp.Payload.ID != "" { // 根据实际结构,使用 resp.Payload.ID
 | 
						||
		messageID = resp.Payload.ID
 | 
						||
	} else {
 | 
						||
		c.logger.Warnf("ChirpStack Enqueue 响应未包含 MessageID (ID),无法记录下行任务。设备: %s", address)
 | 
						||
		// 即使无法获取 MessageID,也认为发送成功,因为 ChirpStack Enqueue 成功了
 | 
						||
		return nil
 | 
						||
	}
 | 
						||
 | 
						||
	// 调用私有方法记录下行任务
 | 
						||
	if err := c.recordDownlinkTask(address, messageID); err != nil {
 | 
						||
		// 记录失败不影响下行命令的发送成功
 | 
						||
		c.logger.Errorf("记录下行任务失败 (MessageID: %s, DevEui: %s): %v", messageID, address, err)
 | 
						||
		return nil
 | 
						||
	}
 | 
						||
 | 
						||
	c.logger.Infof("设备 %s 调用ChirpStack Enqueue成功,并创建下行任务记录 (MessageID: %s)", address, messageID)
 | 
						||
 | 
						||
	return nil
 | 
						||
}
 | 
						||
 | 
						||
// recordDownlinkTask 记录下行任务到数据库
 | 
						||
func (c *ChirpStackTransport) recordDownlinkTask(devEui string, messageID string) error {
 | 
						||
	// 获取区域主控的内部 DeviceID
 | 
						||
	regionalController, err := c.deviceRepo.FindByDevEui(devEui)
 | 
						||
	if err != nil {
 | 
						||
		c.logger.Errorf("记录下行任务失败:无法通过 DevEui '%s' 找到区域主控设备: %v", devEui, err)
 | 
						||
		return err
 | 
						||
	}
 | 
						||
 | 
						||
	// 创建 DeviceCommandLog
 | 
						||
	record := &models.DeviceCommandLog{
 | 
						||
		MessageID:      messageID,
 | 
						||
		DeviceID:       regionalController.ID,
 | 
						||
		SentAt:         time.Now(),
 | 
						||
		AcknowledgedAt: nil, // 初始状态为未确认
 | 
						||
	}
 | 
						||
 | 
						||
	if err := c.deviceCommandLogRepo.Create(record); err != nil {
 | 
						||
		c.logger.Errorf("创建下行任务记录失败 (MessageID: %s, DeviceID: %d): %v", messageID, regionalController.ID, err)
 | 
						||
		return err
 | 
						||
	}
 | 
						||
 | 
						||
	c.logger.Infof("成功创建下行任务记录 (MessageID: %s, DeviceID: %d)", messageID, regionalController.ID)
 | 
						||
	return nil
 | 
						||
}
 |