EdgexAgent/device-gps-go/run/driver/gpsdriver.go
2025-07-10 20:30:06 +08:00

1035 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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