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)
|
|||
|
}
|