1035 lines
27 KiB
Go
1035 lines
27 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
||
//
|
||
// Copyright (C) 2018 Canonical Ltd
|
||
// Copyright (C) 2018-2019 IOTech Ltd
|
||
// Copyright (C) 2021 Jiangxing Intelligence Ltd
|
||
// Copyright (C) 2022 HCL Technologies Ltd
|
||
//
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
// Package driver this package provides an UART implementation of
|
||
// ProtocolDriver interface.
|
||
//
|
||
// CONTRIBUTORS COMPANY
|
||
//===============================================================
|
||
// 1. Sathya Durai HCL Technologies
|
||
// 2. Sudhamani Bijivemula HCL Technologies
|
||
// 3. Vediyappan Villali HCL Technologies
|
||
// 4. Vijay Annamalaisamy HCL Technologies
|
||
//
|
||
//
|
||
|
||
package driver
|
||
|
||
import (
|
||
errorDefault "errors"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/edgexfoundry/device-sdk-go/v4/pkg/interfaces"
|
||
dsModels "github.com/edgexfoundry/device-sdk-go/v4/pkg/models"
|
||
"github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger"
|
||
"github.com/edgexfoundry/go-mod-core-contracts/v4/models"
|
||
"github.com/spf13/cast"
|
||
)
|
||
|
||
type Driver struct {
|
||
sdk interfaces.DeviceServiceSDK
|
||
lc logger.LoggingClient
|
||
asyncCh chan<- *dsModels.AsyncValues
|
||
deviceCh chan<- []dsModels.DiscoveredDevice
|
||
gpsDevice *LCX6XZ // GPS设备实例
|
||
}
|
||
|
||
// Initialize performs protocol-specific initialization for the device
|
||
// service.
|
||
func (s *Driver) Initialize(sdk interfaces.DeviceServiceSDK) error {
|
||
s.sdk = sdk
|
||
s.lc = sdk.LoggingClient()
|
||
s.asyncCh = sdk.AsyncValuesChannel()
|
||
s.deviceCh = sdk.DiscoveredDeviceChannel()
|
||
return nil
|
||
}
|
||
|
||
// Start runs device service startup tasks after the SDK has been completely
|
||
// initialized. This allows device service to safely use DeviceServiceSDK
|
||
// interface features in this function call
|
||
func (s *Driver) Start() error {
|
||
|
||
// 获取 UART 配置信息
|
||
// 通过结构体字段访问 Protocols
|
||
var deviceLocation string
|
||
var baudRate int
|
||
var ReadTimeout int
|
||
uartConfig, err := s.sdk.GetDeviceByName("GPS-Device-01")
|
||
if err != nil {
|
||
s.lc.Errorf("加载服务配置失败!")
|
||
}
|
||
for i, protocol := range uartConfig.Protocols {
|
||
deviceLocation = fmt.Sprintf("%v", protocol["deviceLocation"])
|
||
baudRate, _ = cast.ToIntE(protocol["baudRate"])
|
||
ReadTimeout, _ = cast.ToIntE(protocol["ReadTimeout"])
|
||
s.lc.Debugf("Driver.HandleReadCommands(): protocol = %v, device location = %v, baud rate = %v readTimeout=%v dataBits %v ",
|
||
i, deviceLocation, baudRate, ReadTimeout)
|
||
}
|
||
|
||
s.lc.Info("🚀 初始化GPS设备服务")
|
||
|
||
// 初始化GPS设备
|
||
gpsDevice, err := InitLCX6XZ(deviceLocation, baudRate, ReadTimeout)
|
||
if err != nil {
|
||
s.lc.Errorf("❌ GPS设备初始化失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
s.gpsDevice = gpsDevice
|
||
s.lc.Info("✅ GPS设备初始化成功")
|
||
|
||
return nil
|
||
}
|
||
|
||
// HandleReadCommands triggers a protocol Read operation for the specified device.
|
||
func (s *Driver) HandleReadCommands(deviceName string, protocols map[string]models.ProtocolProperties, reqs []dsModels.CommandRequest) (res []*dsModels.CommandValue, err error) {
|
||
s.lc.Debugf("📖 处理设备 %s 的读取命令", deviceName)
|
||
|
||
if s.gpsDevice == nil {
|
||
return nil, fmt.Errorf("GPS设备未初始化")
|
||
}
|
||
|
||
res = make([]*dsModels.CommandValue, 0, len(reqs))
|
||
|
||
for _, req := range reqs {
|
||
s.lc.Debugf("处理资源: %s", req.DeviceResourceName)
|
||
|
||
var cv *dsModels.CommandValue
|
||
|
||
switch req.DeviceResourceName {
|
||
case "latitude":
|
||
cv = s.getLatitude(req)
|
||
s.lc.Debugf("latitude: %v", cv)
|
||
case "longitude":
|
||
cv = s.getLongitude(req)
|
||
case "altitude":
|
||
cv = s.getAltitude(req)
|
||
case "speed":
|
||
cv = s.getSpeed(req)
|
||
case "course":
|
||
cv = s.getCourse(req)
|
||
case "utc_time":
|
||
cv = s.getUTCTime(req)
|
||
case "fix_quality":
|
||
cv = s.getFixQuality(req)
|
||
case "satellites_used":
|
||
cv = s.getSatellitesUsed(req)
|
||
case "hdop":
|
||
cv = s.getHDOP(req)
|
||
case "gps_status":
|
||
cv = s.getGPSStatus(req)
|
||
case "get_output_rates":
|
||
cv = s.getOutputRates(req)
|
||
default:
|
||
s.lc.Warnf("未知的资源名称: %s", req.DeviceResourceName)
|
||
continue
|
||
}
|
||
|
||
if cv != nil {
|
||
res = append(res, cv)
|
||
}
|
||
}
|
||
|
||
return res, nil
|
||
}
|
||
|
||
// HandleWriteCommands passes a slice of CommandRequest struct each representing
|
||
// a ResourceOperation for a specific device resource.
|
||
// Since the commands are actuation commands, params provide parameters for the individual
|
||
// command.
|
||
func (s *Driver) HandleWriteCommands(deviceName string, protocols map[string]models.ProtocolProperties, reqs []dsModels.CommandRequest,
|
||
params []*dsModels.CommandValue) error {
|
||
s.lc.Debugf("✍️ 处理设备 %s 的写入命令", deviceName)
|
||
|
||
if s.gpsDevice == nil {
|
||
return fmt.Errorf("GPS设备未初始化")
|
||
}
|
||
|
||
for i, req := range reqs {
|
||
s.lc.Debugf("处理写入资源: %s", req.DeviceResourceName)
|
||
|
||
switch req.DeviceResourceName {
|
||
case "set_output_rate":
|
||
err := s.setOutputRate(req, params[i])
|
||
if err != nil {
|
||
s.lc.Errorf("设置输出速率失败: %v", err)
|
||
return err
|
||
}
|
||
case "set_all_rates":
|
||
err := s.setAllOutputRates(req, params[i])
|
||
if err != nil {
|
||
s.lc.Errorf("批量设置输出速率失败: %v", err)
|
||
return err
|
||
}
|
||
default:
|
||
s.lc.Warnf("未知的写入资源名称: %s", req.DeviceResourceName)
|
||
return fmt.Errorf("不支持的写入操作: %s", req.DeviceResourceName)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Discover triggers protocol specific device discovery, asynchronously writes
|
||
// the results to the channel which is passed to the implementation via
|
||
// ProtocolDriver.Initialize()
|
||
func (s *Driver) Discover() error {
|
||
return fmt.Errorf("Discover function is yet to be implemented!")
|
||
|
||
}
|
||
|
||
// ValidateDevice triggers device's protocol properties validation, returns error
|
||
// if validation failed and the incoming device will not be added into EdgeX
|
||
func (s *Driver) ValidateDevice(device models.Device) error {
|
||
|
||
protocol, ok := device.Protocols["UART"]
|
||
if !ok {
|
||
return errorDefault.New("Missing 'UART' protocols")
|
||
}
|
||
|
||
deviceLocation, ok := protocol["deviceLocation"]
|
||
if !ok {
|
||
return errorDefault.New("Missing 'deviceLocation' information")
|
||
} else if deviceLocation == "" {
|
||
return errorDefault.New("deviceLocation must not empty")
|
||
}
|
||
|
||
baudRate, ok := protocol["baudRate"]
|
||
if !ok {
|
||
return errorDefault.New("Missing 'baudRate' information")
|
||
} else if baudRate == "" {
|
||
return errorDefault.New("baudRate must not empty")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Stop the protocol-specific DS code to shutdown gracefully, or
|
||
// if the force parameter is 'true', immediately. The driver is responsible
|
||
// for closing any in-use channels, including the channel used to send async
|
||
// readings (if supported).
|
||
func (s *Driver) Stop(force bool) error {
|
||
// Then Logging Client might not be initialized
|
||
if s.lc != nil {
|
||
s.lc.Debugf(fmt.Sprintf("Driver.Stop called: force=%v", force))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// AddDevice is a callback function that is invoked
|
||
// when a new Device associated with this Device Service is added
|
||
func (s *Driver) AddDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error {
|
||
s.lc.Debugf(fmt.Sprintf("a new Device is added: %s", deviceName))
|
||
return nil
|
||
}
|
||
|
||
// UpdateDevice is a callback function that is invoked
|
||
// when a Device associated with this Device Service is updated
|
||
func (s *Driver) UpdateDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error {
|
||
s.lc.Debugf(fmt.Sprintf("Device %s is updated", deviceName))
|
||
return nil
|
||
}
|
||
|
||
// RemoveDevice is a callback function that is invoked
|
||
// when a Device associated with this Device Service is removed
|
||
func (s *Driver) RemoveDevice(deviceName string, protocols map[string]models.ProtocolProperties) error {
|
||
s.lc.Debugf(fmt.Sprintf("Device %s is removed", deviceName))
|
||
return nil
|
||
}
|
||
|
||
// GPS数据读取辅助方法
|
||
|
||
// getLatitude 获取纬度
|
||
func (s *Driver) getLatitude(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil || s.gpsDevice.NMEA_RMC == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
lat := s.cleanString(string(s.gpsDevice.NMEA_RMC.Lat[:]))
|
||
ns := s.cleanString(string(s.gpsDevice.NMEA_RMC.N_S[:]))
|
||
|
||
if lat == "" || ns == "" {
|
||
return nil
|
||
}
|
||
|
||
// 转换为十进制度数格式
|
||
latValue, isValid := s.convertDMSToDecimalWithValidation(lat, ns)
|
||
if !isValid {
|
||
return nil
|
||
}
|
||
|
||
// 格式化为易读格式
|
||
formattedLat := s.formatCoordinate(latValue, true, ns)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedLat)
|
||
return cv
|
||
}
|
||
|
||
// getLongitude 获取经度
|
||
func (s *Driver) getLongitude(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil || s.gpsDevice.NMEA_RMC == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
lon := s.cleanString(string(s.gpsDevice.NMEA_RMC.Lon[:]))
|
||
ew := s.cleanString(string(s.gpsDevice.NMEA_RMC.E_W[:]))
|
||
|
||
if lon == "" || ew == "" {
|
||
return nil
|
||
}
|
||
|
||
// 转换为十进制度数格式
|
||
lonValue, isValid := s.convertDMSToDecimalWithValidation(lon, ew)
|
||
if !isValid {
|
||
return nil
|
||
}
|
||
|
||
// 格式化为易读格式
|
||
formattedLon := s.formatCoordinate(lonValue, false, ew)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedLon)
|
||
return cv
|
||
}
|
||
|
||
// getAltitude 获取海拔高度
|
||
func (s *Driver) getAltitude(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil || s.gpsDevice.NMEA_GGA == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
altStr := s.cleanString(string(s.gpsDevice.NMEA_GGA.Alt[:]))
|
||
if altStr == "" {
|
||
return nil
|
||
}
|
||
|
||
// 解析海拔高度
|
||
altitude := s.parseFloat(altStr)
|
||
|
||
// 格式化为易读格式
|
||
formattedAlt := s.formatAltitude(altitude)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedAlt)
|
||
return cv
|
||
}
|
||
|
||
// getSpeed 获取速度
|
||
func (s *Driver) getSpeed(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
var speedKmh float64
|
||
|
||
var hasValidData bool
|
||
|
||
// 优先从VTG获取km/h速度
|
||
if s.gpsDevice.NMEA_VTG != nil {
|
||
sogkStr := s.cleanString(string(s.gpsDevice.NMEA_VTG.SOGK[:]))
|
||
if sogkStr != "" {
|
||
speedKmh = s.parseFloat(sogkStr)
|
||
hasValidData = true
|
||
}
|
||
}
|
||
|
||
// 如果VTG中没有,从RMC获取节速度并转换
|
||
if !hasValidData && s.gpsDevice.NMEA_RMC != nil {
|
||
sogStr := s.cleanString(string(s.gpsDevice.NMEA_RMC.SOG[:]))
|
||
if sogStr != "" {
|
||
sog := s.parseFloat(sogStr)
|
||
speedKmh = sog * 1.852 // 1节 = 1.852 km/h
|
||
hasValidData = true
|
||
}
|
||
}
|
||
|
||
// 只有在没有任何有效数据时才返回nil
|
||
if !hasValidData {
|
||
return nil
|
||
}
|
||
|
||
// 格式化为易读格式
|
||
formattedSpeed := s.formatSpeed(speedKmh)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedSpeed)
|
||
return cv
|
||
}
|
||
|
||
// getCourse 获取航向
|
||
func (s *Driver) getCourse(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
var course float64
|
||
var hasValidData bool
|
||
|
||
// 优先从VTG获取航向
|
||
if s.gpsDevice.NMEA_VTG != nil {
|
||
cogtStr := s.cleanString(string(s.gpsDevice.NMEA_VTG.COGT[:]))
|
||
if cogtStr != "" {
|
||
course = s.parseFloat(cogtStr)
|
||
hasValidData = true
|
||
}
|
||
}
|
||
|
||
// 如果VTG中没有,从RMC获取
|
||
if !hasValidData && s.gpsDevice.NMEA_RMC != nil {
|
||
cogStr := s.cleanString(string(s.gpsDevice.NMEA_RMC.COG[:]))
|
||
if cogStr != "" {
|
||
course = s.parseFloat(cogStr)
|
||
hasValidData = true
|
||
}
|
||
}
|
||
|
||
// 只有在没有任何有效数据时才返回nil
|
||
if !hasValidData {
|
||
return nil
|
||
}
|
||
|
||
// 格式化为易读格式
|
||
formattedCourse := s.formatCourse(course)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedCourse)
|
||
return cv
|
||
}
|
||
|
||
// getUTCTime 获取UTC时间
|
||
func (s *Driver) getUTCTime(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil || s.gpsDevice.NMEA_RMC == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
utcStr := s.cleanString(string(s.gpsDevice.NMEA_RMC.UTC[:]))
|
||
if utcStr == "" {
|
||
return nil
|
||
}
|
||
|
||
// 将UTC时间格式化为易读格式
|
||
formattedTime := s.formatUTCTime(utcStr)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedTime)
|
||
return cv
|
||
}
|
||
|
||
// getFixQuality 获取定位质量
|
||
func (s *Driver) getFixQuality(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
var quality int32
|
||
|
||
// 优先从GGA获取详细的定位质量
|
||
if s.gpsDevice.NMEA_GGA != nil {
|
||
qualityStr := s.cleanString(string(s.gpsDevice.NMEA_GGA.Quality[:]))
|
||
if qualityStr != "" {
|
||
quality = int32(s.parseFloat(qualityStr))
|
||
}
|
||
}
|
||
|
||
// 如果GGA中没有,从RMC状态推断
|
||
if quality == 0 && s.gpsDevice.NMEA_RMC != nil {
|
||
status := s.cleanString(string(s.gpsDevice.NMEA_RMC.Status[:]))
|
||
if status == "A" {
|
||
quality = 1 // 有效定位
|
||
}
|
||
}
|
||
|
||
// 格式化为易读格式
|
||
formattedQuality := s.formatFixQuality(quality)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedQuality)
|
||
return cv
|
||
}
|
||
|
||
// getSatellitesUsed 获取使用的卫星数
|
||
func (s *Driver) getSatellitesUsed(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil || s.gpsDevice.NMEA_GGA == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
satStr := s.cleanString(string(s.gpsDevice.NMEA_GGA.NumSatUsed[:]))
|
||
if satStr == "" {
|
||
return nil
|
||
}
|
||
|
||
// 解析卫星数
|
||
satCount := int32(s.parseFloat(satStr))
|
||
|
||
// 格式化为易读格式
|
||
formattedSatCount := s.formatSatelliteCount(satCount)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedSatCount)
|
||
return cv
|
||
}
|
||
|
||
// getHDOP 获取水平精度因子
|
||
func (s *Driver) getHDOP(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil {
|
||
return nil
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
var hdopStr string
|
||
|
||
// 优先从GGA获取HDOP
|
||
if s.gpsDevice.NMEA_GGA != nil {
|
||
hdopStr = s.cleanString(string(s.gpsDevice.NMEA_GGA.HDOP[:]))
|
||
}
|
||
|
||
// 如果GGA中没有,尝试从GSA获取
|
||
if hdopStr == "" && s.gpsDevice.NMEA_GSA != nil {
|
||
hdopStr = s.cleanString(string(s.gpsDevice.NMEA_GSA.HDOP[:]))
|
||
}
|
||
|
||
if hdopStr == "" {
|
||
return nil
|
||
}
|
||
|
||
// 解析HDOP值
|
||
hdop := s.parseFloat(hdopStr)
|
||
|
||
// 格式化为易读格式
|
||
formattedHDOP := s.formatHDOP(hdop)
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", formattedHDOP)
|
||
return cv
|
||
}
|
||
|
||
// getGPSStatus 获取GPS状态
|
||
func (s *Driver) getGPSStatus(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil || s.gpsDevice.NMEA_RMC == nil {
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", "DISCONNECTED")
|
||
return cv
|
||
}
|
||
|
||
s.gpsDevice.mutex.Lock()
|
||
defer s.gpsDevice.mutex.Unlock()
|
||
|
||
status := s.cleanString(string(s.gpsDevice.NMEA_RMC.Status[:]))
|
||
var gpsStatus string
|
||
if status == "A" {
|
||
gpsStatus = "ACTIVE"
|
||
} else {
|
||
gpsStatus = "WARNING"
|
||
}
|
||
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", gpsStatus)
|
||
return cv
|
||
}
|
||
|
||
// getOutputRates 获取所有NMEA消息输出速率
|
||
func (s *Driver) getOutputRates(req dsModels.CommandRequest) *dsModels.CommandValue {
|
||
if s.gpsDevice == nil {
|
||
return nil
|
||
}
|
||
|
||
s.lc.Info("开始查询所有NMEA消息输出速率")
|
||
|
||
// 查询支持的NMEA类型及其输出速率
|
||
nmeaTypes := []struct {
|
||
sid NMEA_SUB_ID
|
||
name string
|
||
}{
|
||
{NMEA_GGA_SID, "GGA"},
|
||
{NMEA_GLL_SID, "GLL"},
|
||
{NMEA_GSA_SID, "GSA"},
|
||
{NMEA_GRS_SID, "GRS"},
|
||
{NMEA_GSV_SID, "GSV"},
|
||
{NMEA_RMC_SID, "RMC"},
|
||
{NMEA_VTG_SID, "VTG"},
|
||
{NMEA_ZDA_SID, "ZDA"},
|
||
{NMEA_GST_SID, "GST"},
|
||
}
|
||
|
||
var rateInfos []string
|
||
|
||
for _, nmea := range nmeaTypes {
|
||
// 发送查询命令
|
||
err := GetNMEAOutputRate(s.gpsDevice, nmea.sid)
|
||
if err != nil {
|
||
s.lc.Errorf("查询%s输出速率失败: %v", nmea.name, err)
|
||
rateInfos = append(rateInfos, fmt.Sprintf("%s: 查询失败", nmea.name))
|
||
continue
|
||
}
|
||
|
||
// 等待响应
|
||
time.Sleep(200 * time.Millisecond)
|
||
|
||
// 从设备存储的查询结果中获取实际输出速率
|
||
s.gpsDevice.mutex.Lock()
|
||
if rate, exists := s.gpsDevice.OutputRates[nmea.sid]; exists {
|
||
var rateDesc string
|
||
switch rate {
|
||
case 0:
|
||
rateDesc = "禁用"
|
||
case 1:
|
||
rateDesc = "1Hz"
|
||
case 5:
|
||
rateDesc = "5Hz"
|
||
case 10:
|
||
rateDesc = "10Hz"
|
||
default:
|
||
rateDesc = fmt.Sprintf("%dHz", rate)
|
||
}
|
||
rateInfos = append(rateInfos, fmt.Sprintf("%s: %s", nmea.name, rateDesc))
|
||
} else {
|
||
rateInfos = append(rateInfos, fmt.Sprintf("%s: 未知", nmea.name))
|
||
}
|
||
s.gpsDevice.mutex.Unlock()
|
||
|
||
s.lc.Debugf("已查询%s输出速率", nmea.name)
|
||
}
|
||
|
||
rateInfo := strings.Join(rateInfos, ", ")
|
||
s.lc.Infof("查询完成,输出速率: %s", rateInfo)
|
||
|
||
cv, _ := dsModels.NewCommandValue(req.DeviceResourceName, "String", rateInfo)
|
||
return cv
|
||
}
|
||
|
||
// setOutputRate 设置单个NMEA消息的输出速率
|
||
func (s *Driver) setOutputRate(req dsModels.CommandRequest, param *dsModels.CommandValue) error {
|
||
|
||
// 参数格式: "GGA:1" 或 "RMC:5"
|
||
configStr, ok := param.Value.(string)
|
||
if !ok {
|
||
return fmt.Errorf("参数值必须是字符串格式")
|
||
}
|
||
|
||
// 解析配置字符串
|
||
parts := strings.Split(configStr, ":")
|
||
if len(parts) != 2 {
|
||
return fmt.Errorf("无效的配置格式,应为 NMEA_TYPE:RATE")
|
||
}
|
||
|
||
nmeaType := strings.TrimSpace(strings.ToUpper(parts[0]))
|
||
rateStr := strings.TrimSpace(parts[1])
|
||
|
||
rateVal, err := strconv.ParseUint(rateStr, 10, 8)
|
||
if err != nil {
|
||
return fmt.Errorf("无效的速率值: %s", rateStr)
|
||
}
|
||
rate := uint8(rateVal)
|
||
|
||
// 转换NMEA类型为子ID
|
||
subID, err := s.getNMEASubID(nmeaType)
|
||
if err != nil {
|
||
return fmt.Errorf("不支持的NMEA类型: %s", nmeaType)
|
||
}
|
||
|
||
s.lc.Infof("设置%s消息输出速率为%d", nmeaType, rate)
|
||
return SetNMEAOutputRate(s.gpsDevice, subID, rate)
|
||
}
|
||
|
||
// setAllOutputRates 批量设置所有NMEA消息的输出速率
|
||
func (s *Driver) setAllOutputRates(req dsModels.CommandRequest, param *dsModels.CommandValue) error {
|
||
if param == nil {
|
||
return fmt.Errorf("参数值为空")
|
||
}
|
||
|
||
// 参数格式: "GGA:1,RMC:1,GSV:5,VTG:1,GSA:1"
|
||
configStr, ok := param.Value.(string)
|
||
if !ok {
|
||
return fmt.Errorf("参数值必须是字符串格式")
|
||
}
|
||
|
||
s.lc.Infof("开始批量设置输出速率: %s", configStr)
|
||
|
||
// 解析配置字符串
|
||
configs, err := s.parseMultipleRateConfig(configStr)
|
||
if err != nil {
|
||
return fmt.Errorf("解析配置字符串失败: %v", err)
|
||
}
|
||
|
||
// 逐个设置输出速率
|
||
var errors []string
|
||
for nmeaType, rate := range configs {
|
||
// 转换NMEA类型为子ID
|
||
subID, err := s.getNMEASubID(nmeaType)
|
||
if err != nil {
|
||
errors = append(errors, fmt.Sprintf("%s: %v", nmeaType, err))
|
||
continue
|
||
}
|
||
|
||
// 发送设置命令
|
||
err = SetNMEAOutputRate(s.gpsDevice, subID, rate)
|
||
if err != nil {
|
||
errors = append(errors, fmt.Sprintf("%s: %v", nmeaType, err))
|
||
continue
|
||
}
|
||
|
||
s.lc.Infof("成功设置%s输出速率为%d", nmeaType, rate)
|
||
|
||
// 在设置之间添加延迟,避免设备处理不过来
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
|
||
if len(errors) > 0 {
|
||
return fmt.Errorf("部分设置失败: %s", strings.Join(errors, "; "))
|
||
}
|
||
|
||
s.lc.Infof("批量设置输出速率完成")
|
||
return nil
|
||
}
|
||
|
||
// parseMultipleRateConfig 解析多个输出速率配置字符串
|
||
func (s *Driver) parseMultipleRateConfig(configStr string) (map[string]uint8, error) {
|
||
if configStr == "" {
|
||
return nil, fmt.Errorf("配置字符串为空")
|
||
}
|
||
|
||
configs := make(map[string]uint8)
|
||
|
||
// 支持逗号分隔的配置项
|
||
items := strings.Split(configStr, ",")
|
||
|
||
for _, item := range items {
|
||
item = strings.TrimSpace(item)
|
||
if item == "" {
|
||
continue
|
||
}
|
||
|
||
// 支持冒号或等号分隔
|
||
var parts []string
|
||
if strings.Contains(item, ":") {
|
||
parts = strings.Split(item, ":")
|
||
} else if strings.Contains(item, "=") {
|
||
parts = strings.Split(item, "=")
|
||
} else {
|
||
return nil, fmt.Errorf("无效的配置项格式: %s", item)
|
||
}
|
||
|
||
if len(parts) != 2 {
|
||
return nil, fmt.Errorf("无效的配置项格式: %s", item)
|
||
}
|
||
|
||
nmeaType := strings.TrimSpace(strings.ToUpper(parts[0]))
|
||
rateStr := strings.TrimSpace(parts[1])
|
||
|
||
rate, err := strconv.ParseUint(rateStr, 10, 8)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("无效的速率值: %s", rateStr)
|
||
}
|
||
|
||
configs[nmeaType] = uint8(rate)
|
||
}
|
||
|
||
if len(configs) == 0 {
|
||
return nil, fmt.Errorf("未找到有效的配置项")
|
||
}
|
||
|
||
return configs, nil
|
||
}
|
||
|
||
// getNMEASubID 将NMEA类型字符串转换为子ID
|
||
func (s *Driver) getNMEASubID(nmeaType string) (NMEA_SUB_ID, error) {
|
||
switch strings.ToUpper(nmeaType) {
|
||
case "GGA":
|
||
return NMEA_GGA_SID, nil
|
||
case "GLL":
|
||
return NMEA_GLL_SID, nil
|
||
case "GSA":
|
||
return NMEA_GSA_SID, nil
|
||
case "GRS":
|
||
return NMEA_GRS_SID, nil
|
||
case "GSV":
|
||
return NMEA_GSV_SID, nil
|
||
case "RMC":
|
||
return NMEA_RMC_SID, nil
|
||
case "VTG":
|
||
return NMEA_VTG_SID, nil
|
||
case "ZDA":
|
||
return NMEA_ZDA_SID, nil
|
||
case "GST":
|
||
return NMEA_GST_SID, nil
|
||
default:
|
||
return 0, fmt.Errorf("未知的NMEA类型: %s", nmeaType)
|
||
}
|
||
}
|
||
|
||
// 工具函数
|
||
|
||
// convertDMSToDecimalWithValidation 将度分秒格式转换为十进制度数,并返回是否有效
|
||
func (s *Driver) convertDMSToDecimalWithValidation(dmsStr, direction string) (float64, bool) {
|
||
if dmsStr == "" || direction == "" {
|
||
return 0.0, false
|
||
}
|
||
|
||
// 清理字符串
|
||
dmsStr = s.cleanString(dmsStr)
|
||
if len(dmsStr) < 4 {
|
||
return 0.0, false
|
||
}
|
||
|
||
// 解析度分格式 (ddmm.mmmm 或 dddmm.mmmm)
|
||
var degrees, minutes float64
|
||
var err error
|
||
|
||
if strings.Contains(dmsStr, ".") {
|
||
// 查找小数点位置
|
||
dotIndex := strings.Index(dmsStr, ".")
|
||
if dotIndex >= 4 {
|
||
// 经度格式 dddmm.mmmm
|
||
degrees, err = strconv.ParseFloat(dmsStr[:dotIndex-2], 64)
|
||
if err != nil {
|
||
return 0.0, false
|
||
}
|
||
minutes, err = strconv.ParseFloat(dmsStr[dotIndex-2:], 64)
|
||
if err != nil {
|
||
return 0.0, false
|
||
}
|
||
} else if dotIndex >= 3 {
|
||
// 纬度格式 ddmm.mmmm
|
||
degrees, err = strconv.ParseFloat(dmsStr[:dotIndex-2], 64)
|
||
if err != nil {
|
||
return 0.0, false
|
||
}
|
||
minutes, err = strconv.ParseFloat(dmsStr[dotIndex-2:], 64)
|
||
if err != nil {
|
||
return 0.0, false
|
||
}
|
||
} else {
|
||
return 0.0, false
|
||
}
|
||
} else {
|
||
return 0.0, false
|
||
}
|
||
|
||
decimal := degrees + minutes/60.0
|
||
|
||
// 根据方向调整符号
|
||
if direction == "S" || direction == "W" {
|
||
decimal = -decimal
|
||
}
|
||
|
||
return decimal, true
|
||
}
|
||
|
||
// convertDMSToDecimal 将度分秒格式转换为十进制度数(保持向后兼容)
|
||
func (s *Driver) convertDMSToDecimal(dmsStr, direction string) float64 {
|
||
result, _ := s.convertDMSToDecimalWithValidation(dmsStr, direction)
|
||
return result
|
||
}
|
||
|
||
// parseFloat 解析浮点数字符串
|
||
func (s *Driver) parseFloat(str string) float64 {
|
||
str = s.cleanString(str)
|
||
if str == "" {
|
||
return 0.0
|
||
}
|
||
|
||
val, err := strconv.ParseFloat(str, 64)
|
||
if err != nil {
|
||
return 0.0
|
||
}
|
||
|
||
return val
|
||
}
|
||
|
||
// cleanString 清理字符串,移除空字节和多余空格
|
||
func (s *Driver) cleanString(str string) string {
|
||
// 移除空字节
|
||
cleaned := strings.ReplaceAll(str, "\x00", "")
|
||
// 移除前后空格
|
||
cleaned = strings.TrimSpace(cleaned)
|
||
return cleaned
|
||
}
|
||
|
||
// formatUTCTime 将UTC时间字符串格式化为易读格式
|
||
// 输入格式: HHMMSS.sss (例如: 123456.00)
|
||
// 输出格式: HH:MM:SS.sss (例如: 12:34:56.00)
|
||
func (s *Driver) formatUTCTime(utcStr string) string {
|
||
if len(utcStr) < 6 {
|
||
return utcStr // 如果格式不正确,返回原始字符串
|
||
}
|
||
|
||
// 解析时分秒
|
||
hour := utcStr[0:2]
|
||
minute := utcStr[2:4]
|
||
second := utcStr[4:]
|
||
|
||
// 格式化为 HH:MM:SS.sss
|
||
return fmt.Sprintf("%s:%s:%s", hour, minute, second)
|
||
}
|
||
|
||
// formatCoordinate 格式化坐标为易读格式
|
||
// 输入: 十进制度数 (例如: 39.969056)
|
||
// 输出: 度分秒格式 (例如: 39°58'08.6"N)
|
||
func (s *Driver) formatCoordinate(decimal float64, isLatitude bool, direction string) string {
|
||
if decimal == 0.0 {
|
||
return "0°00'00.0\""
|
||
}
|
||
|
||
// 取绝对值进行计算
|
||
absDecimal := decimal
|
||
if absDecimal < 0 {
|
||
absDecimal = -absDecimal
|
||
}
|
||
|
||
// 计算度分秒
|
||
degrees := int(absDecimal)
|
||
minutes := (absDecimal - float64(degrees)) * 60
|
||
minutesInt := int(minutes)
|
||
seconds := (minutes - float64(minutesInt)) * 60
|
||
|
||
// 格式化输出
|
||
return fmt.Sprintf("%d°%02d'%04.1f\"%s", degrees, minutesInt, seconds, direction)
|
||
}
|
||
|
||
// formatSpeed 格式化速度为易读格式
|
||
func (s *Driver) formatSpeed(speedKmh float64) string {
|
||
return fmt.Sprintf("%.2f km/h", speedKmh)
|
||
}
|
||
|
||
// formatCourse 格式化航向为易读格式
|
||
func (s *Driver) formatCourse(course float64) string {
|
||
// 添加方向描述
|
||
var direction string
|
||
switch {
|
||
case course >= 0 && course < 22.5:
|
||
direction = "北"
|
||
case course >= 22.5 && course < 67.5:
|
||
direction = "东北"
|
||
case course >= 67.5 && course < 112.5:
|
||
direction = "东"
|
||
case course >= 112.5 && course < 157.5:
|
||
direction = "东南"
|
||
case course >= 157.5 && course < 202.5:
|
||
direction = "南"
|
||
case course >= 202.5 && course < 247.5:
|
||
direction = "西南"
|
||
case course >= 247.5 && course < 292.5:
|
||
direction = "西"
|
||
case course >= 292.5 && course < 337.5:
|
||
direction = "西北"
|
||
default:
|
||
direction = "北"
|
||
}
|
||
|
||
return fmt.Sprintf("%.1f° (%s)", course, direction)
|
||
}
|
||
|
||
// formatAltitude 格式化海拔高度为易读格式
|
||
func (s *Driver) formatAltitude(altitude float64) string {
|
||
return fmt.Sprintf("%.1f 米", altitude)
|
||
}
|
||
|
||
// formatFixQuality 格式化定位质量为易读格式
|
||
func (s *Driver) formatFixQuality(quality int32) string {
|
||
switch quality {
|
||
case 0:
|
||
return "无定位"
|
||
case 1:
|
||
return "GPS定位"
|
||
case 2:
|
||
return "差分GPS定位"
|
||
case 3:
|
||
return "PPS定位"
|
||
case 4:
|
||
return "RTK定位"
|
||
case 5:
|
||
return "浮点RTK"
|
||
case 6:
|
||
return "推算定位"
|
||
case 7:
|
||
return "手动输入"
|
||
case 8:
|
||
return "模拟定位"
|
||
default:
|
||
return fmt.Sprintf("未知质量(%d)", quality)
|
||
}
|
||
}
|
||
|
||
// formatSatelliteCount 格式化卫星数量为易读格式
|
||
func (s *Driver) formatSatelliteCount(count int32) string {
|
||
return fmt.Sprintf("%d 颗卫星", count)
|
||
}
|
||
|
||
// formatHDOP 格式化水平精度因子为易读格式
|
||
func (s *Driver) formatHDOP(hdop float64) string {
|
||
var quality string
|
||
switch {
|
||
case hdop <= 1:
|
||
quality = "优秀"
|
||
case hdop <= 2:
|
||
quality = "良好"
|
||
case hdop <= 5:
|
||
quality = "中等"
|
||
case hdop <= 10:
|
||
quality = "一般"
|
||
case hdop <= 20:
|
||
quality = "较差"
|
||
default:
|
||
quality = "很差"
|
||
}
|
||
return fmt.Sprintf("%.2f (%s)", hdop, quality)
|
||
}
|
||
|
||
// 公共格式化方法,供外部调用
|
||
|
||
// FormatUTCTime 公共方法:格式化UTC时间
|
||
func (s *Driver) FormatUTCTime(utcStr string) string {
|
||
return s.formatUTCTime(utcStr)
|
||
}
|
||
|
||
// FormatCoordinate 公共方法:格式化坐标
|
||
func (s *Driver) FormatCoordinate(decimal float64, isLatitude bool, direction string) string {
|
||
return s.formatCoordinate(decimal, isLatitude, direction)
|
||
}
|
||
|
||
// FormatSpeed 公共方法:格式化速度
|
||
func (s *Driver) FormatSpeed(speedKmh float64) string {
|
||
return s.formatSpeed(speedKmh)
|
||
}
|
||
|
||
// FormatCourse 公共方法:格式化航向
|
||
func (s *Driver) FormatCourse(course float64) string {
|
||
return s.formatCourse(course)
|
||
}
|
||
|
||
// FormatAltitude 公共方法:格式化海拔
|
||
func (s *Driver) FormatAltitude(altitude float64) string {
|
||
return s.formatAltitude(altitude)
|
||
}
|
||
|
||
// FormatFixQuality 公共方法:格式化定位质量
|
||
func (s *Driver) FormatFixQuality(quality int32) string {
|
||
return s.formatFixQuality(quality)
|
||
}
|
||
|
||
// FormatSatelliteCount 公共方法:格式化卫星数量
|
||
func (s *Driver) FormatSatelliteCount(count int32) string {
|
||
return s.formatSatelliteCount(count)
|
||
}
|
||
|
||
// FormatHDOP 公共方法:格式化HDOP
|
||
func (s *Driver) FormatHDOP(hdop float64) string {
|
||
return s.formatHDOP(hdop)
|
||
}
|