一,了解 GPS NMEA-0183 协议
需要基础物联网对接知识,需要对解析协议有一定认识。
如果不知道怎么连接硬件,请看我的另一篇博客:https://blog.csdn.net/Crazy_Cw/article/details/126613967
这篇文章只说明,如何解析协议。
NMEA 是 National Marine Electronics Association 的缩写,是美国国家海洋电子协会的简称,现在是 GPS 导航设备统一的 RTCM 标准协议。NMEA-0183 协议是目前 GPS 接收机上使用最广泛的协议,大多数常见的 GPS 接收机、GPS 数据处理软件、导航软件都遵守或者至少兼容这个协议。
如果你使用过 GPS 传感器,那么可能对从串口中冒出了大量以 GPGGA、GPGSA、GPRMC 等开头的数据有印象,它们就是 NMEA-0183 协议数据。
[14:31:01.842]收←◆$GPGGA,063100.00,3104.39321639,N,12125.30910133,E,5,41,0.5,22.8980,M,11.6136,M,10,9959*55
$GNGLL,3104.39321639,N,12125.30910133,E,063100.00,A,D*79
$GPZDA,063100.00,08,04,2024,,*6A
$GPRMC,063100.00,A,3104.39321639,N,12125.30910133,E,0.005,340.1,080424,6.3,W,D*25
$GPVTG,340.106,T,346.435,M,0.00461,N,0.00853,K,D*28
$GPHDT,337.0082,T*08
$GPGGA,063101.00,3104.39321864,N,12125.30910076,E,5,41,0.5,22.8981,M,11.6136,M,11,9959*52
$GNGLL,3104.39321864,N,12125.30910076,E,063101.00,A,D*7E
$GPZDA,063101.00,08,04,2024,,*6B
$GPRMC,063101.00,A,3104.39321864,N,12125.30910076,E,0.005,309.9,080424,6.3,W,D*27
$GPVTG,309.931,T,316.260,M,0.00521,N,0.00966,K,D*28
$GPHDT,337.0855,T*0A
在这些数据中,包含了位置、速度、时间等信息,通过解析这数据,就可以实时获取物体的位置信息,或者实现时间同步。
二、协议格式
NMEA 0183 通讯协议是以 ASCII 码为基础的,一般格式如下:
$aaaaa,df1,df2,…[CR][LF]
格式说明:
$ 为起始标志;
, 为域分隔符;
- 为校验和识别符,其后两位数为校验和,代表了 $和 * 之间所有字符的按位异或值(不包括这两个字符);
\r\n 为终止符(不可见),所有的语句必须以来结束,也就是 ASCII 字符的“回车”(十六进制的 0D)和“换行”(十六进制的 0A)。
NMEA-0183 协议定义的语句非常多,但是常用的或者说兼容性最广的语句只有 GPGGA、GPGSA、GPGSV、GPRMC、GPVTG、GPGLL 等。下面给出这些常用 NMEA 0183 语句的字段定义解释。
2.1 GPGSA
GPS DOP and Active Satellites(GSA)当前卫星信息
$GPGSA,<1>,<2>,<3>,<3>,<3>,<3>,<3>,<4>,<5>,<6>,<7>
各字段描述如下:
- 模式 :M = 手动, A = 自动。
- 定位型式 1 = 未定位, 2 = 二维定位, 3 = 三维定位。
- PRN 数字:01 至 32 表天空使用中的卫星编号,最多可接收 12 颗卫星信息。
- PDOP 位置精度因子(0.5~99.9)
- HDOP 水平精度因子(0.5~99.9)
- VDOP 垂直精度因子(0.5~99.9)
- Checksum(检查位)
2.2 GPGSV
GPS Satellites in View(GSV)可见卫星信息
$GPGSV, <1>,<2>,<3>,<4>,<5>,<6>,<7>,?<4>,<5>,<6>,<7>,<8>
各字段描述如下:
- GSV语句的总数
- 本句GSV的编号
- 可见卫星的总数,00 至 12。
- 卫星编号, 01 至 32。
- 卫星仰角, 00 至 90 度。
- 卫星方位角, 000 至 359 度。实际值。
- 讯号噪声比(C/No), 00 至 99 dB;无表未接收到讯号。
- Checksum(检查位)
注意:第 <4>,<5>,<6>,<7> 项个别卫星会重复出现,每行最多有四颗卫星。其余卫星信息会于次一行出现,若未使用,这些字段会空白。
2.3 GPGGA
Global Positioning System Fix Data(GGA)GPS定位信息
$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh
各字段描述如下:
- UTC 时间,hhmmss(时分秒)格式
- 纬度 ddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 纬度半球 N(北纬)或 S(南纬)
- 经度 dddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 经度半球 E(东经)或 W(西经)
- GPS 状态:0=未定位,1=非差分定位,2=差分定位,6=正在估算
- 正在使用解算位置的卫星数量(00~12)(前面的 0 也将被传输)
- HDOP 水平精度因子(0.5~99.9)
- 海拔高度(-9999.9~99999.9)
- 地球椭球面相对大地水准面的高度
- 差分时间(从最近一次接收到差分信号开始的秒数,如果不是差分定位将为空)
- 差分站 ID 号 0000~1023(前面的 0 也将被传输,如果不是差分定位将为空)
2.4 GPRMC
Recommended Minimum Specific GPS/TRANSIT Data(RMC)推荐定位信息
$GPRMC,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>*hh
各字段描述如下:
- UTC 时间,格式 hhmmss.ssss,代表时分秒.毫秒
- 定位状态,A=有效定位,V=无效定位
- 纬度 ddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 纬度半球 N(北纬)或 S(南纬)
- 经度 dddmm.mmmm(度分)格式(前面的 0 也将被传输)
- 经度半球 E(东经)或 W(西经)
- 地面速率(000.0~999.9 节,前面的 0 也将被传输)
- 地面航向(方位角),等效于二维罗盘(000.0~359.9 度,以真北为参考基准,前面的 0 也将被传输)
- UTC 日期,DDMMYY(日月年)格式
- 磁偏角(000.0~180.0 度,前面的 0 也将被传输)
- 磁偏角方向,E(东)或 W(西)
- 模式指示(仅 NMEA0183 3.0 版本输出,A=自主定位,D=差分,E=估算,N=数据无效)
最后两个字节是校验和
注意:
如果字段 4 的值等于 N,则字段 3 的值等于 ddmm.mmmmmm
如果字段 4 的值等于 S,则字段 3 的值等于 -ddmm.mmmmmm
如果字段 6 的值等于 E,则字段 5 的值等于 ddmm.mmmmmm
如果字段 6 的值等于 W,则字段 5 的值等于 -ddmm.mmmmmm
十进制北纬度数 = dd + mm.mmmmmm/60
十进制南纬度数 = -(dd + mm.mmmmmm/60)
十进制东经度数 = ddd + mm.mmmmmm/60
十进制西经度数 = -(ddd + mm.mmmmmm/60)
2.5 GPVTG
Track Made Good and Ground Speed(VTG)地面速度信息
$GPVTG,<1>,T,<2>,M,<3>,N,<4>,K,<5>*hh
各字段描述如下:
- 以真北为参考基准的地面航向(000~359 度,前面的 0 也将被传输)
- 以磁北为参考基准的地面航向(000~359 度,前面的 0 也将被传输)
- 地面速率(000.0~999.9 节,前面的 0 也将被传输)
- 地面速率(0000.0~1851.8 公里/小时,前面的0也将被传输)
- 模式指示(仅 NMEA 0183 3.0 版本输出,A=自主定位,D=差分,E=估算,N=数据无效)
三、实践解析DEMO
解析GPRMC,实践
3.1 创建一个接收对象
package com.joyaiot.vehiclemonitor.netty.handler.carprotocol.entiy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.sql.Time;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 功能描述: TODO 方法描述
*
* @Author keLe
* @Date 2024/4/8
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GPRMCData {
/**UTC时间*/
private Time utcTime;
/**
* GPS状态
* A=有效,V=无效
*/
private String status;
/**
* 纬度,DDDMM.MMMMM
*/
private double latitude;
/**
* 纬度,DDDMM.MMMMM
*/
private double longitude;
/**
* 速度,Knots
*/
private double speedKnots;
/**
* 地面速度,节(nautical miles per hour)
*/
private double courseDegrees;
/**日期*/
private Date date;
/**
* 磁偏角
*/
private double magneticVariation;
/**
* 磁变方向,W=西变,E=东变
*/
private String magneticVariationDir;
/**
* 模式,A=自主定位,D=差分,E=估算,N=数据无效
* */
private String modeIndicator;
}
3.2 解析协议工具类
package com.joyaiot.vehiclemonitor.netty.util;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.fast.api.base.util.StringUtil;
import com.joyaiot.vehiclemonitor.netty.handler.carprotocol.entiy.GPRMCData;
import com.joyaiot.vehiclemonitor.utils.DecimalUtils;
import lombok.extern.slf4j.Slf4j;
import java.sql.Time;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 功能描述: GPRMC协议解析
* 协议格式:$GPRMC,075704.00,A,3104.39326987,N,12125.30904175,E,0.007,286.4,070424,6.3,W,D*29
* @see <p>https://blog.csdn.net/Crazy_Cw/article/details/137515699</p>
*
* @Author keLe
* @Date 2024/4/8
*/
@Slf4j
public class GPRMCParser {
/**
* 正则表达式
* 验证协议 $GPRMC,075704.00,A,3104.39326987,N,12125.30904175,E,0.007,286.4,070424,6.3,W,D*29
*/
private static final Pattern GPRMC_PATTERN = Pattern.compile(
"\\$GPRMC,(\\d+\\.?\\d*),([AV]),(\\d+\\.\\d+),(N|S),(\\d+\\.\\d+),(E|W)," +
"(\\d+\\.\\d+),(\\d+\\.\\d+)?,(\\d+)?,(\\d+\\.\\d+)?,(E|W)?,(A|D|E|N)\\*\\w+");
/**
* 1节等于1.852公里/小时(km/h) (单位换算)
*/
private static final double KNOTS_TO_KILOMETERS_PER_HOUR = 1.852;
/**
* 1km = 1000m (单位换算)
*/
private static final double KILOMETERS_TO_METERS = 1000;
/**
* 1小时等于3600秒 (单位换算)
*/
private static final double HOURS_TO_SECONDS = 3600.0;
/**
* 功能描述: 协议解析
* @Author keLe
* @Date 2024/4/8
* @param gprmcString 协议字符串
* @return com.joyaiot.vehiclemonitor.netty.handler.carprotocol.entiy.GPRMCData 协议解析结果
*/
public static GPRMCData parse(String gprmcString) {
GPRMCData data = new GPRMCData();
Matcher matcher = isValidGPRMCFormat(gprmcString);
// 校验GPRMC格式
if (null == matcher) {
log.error("Invalid GPRMC format");
return null;
}
try {
// 解析UTC时间
String utcTime = parseUTCTime(matcher,gprmcString);
data.setUtcTime(parseUTCTimeToDate(utcTime));
// 解析状态
data.setStatus(parseStatus(matcher,gprmcString));
// 解析纬度
double latitude = parseLatitude(matcher,gprmcString);
data.setLatitude(latitude);
// 解析经度
double longitude = parseLongitude(matcher,gprmcString);
data.setLongitude(longitude);
// 解析速度
double speedKnots = parseSpeed(matcher,gprmcString);
data.setSpeedKnots(convertKnotsToMetersPerSecond(speedKnots));
// 解析航向
data.setCourseDegrees(parseCourse(matcher,gprmcString));
// 解析日期
data.setDate(parseDate(matcher,gprmcString, utcTime));
// 解析磁偏角
data.setMagneticVariation(parseMagneticVariation(matcher,gprmcString));
// 解析磁偏角方向
data.setMagneticVariationDir(parseMagneticVariationDir(matcher,gprmcString));
// 解析模式指示
data.setModeIndicator(parseModeIndicator(matcher,gprmcString));
}catch (Exception e){
log.error("GPRMC parse error:{}", e.getMessage());
return null;
}
return data;
}
/**
* 校验GPRMC格式是否有效
* @Author keLe
* @Date 2024/4/8
* @param gprmcString GPRMC字符串
* @return Matcher 匹配器
*/
private static Matcher isValidGPRMCFormat(String gprmcString) {
Matcher matcher = GPRMC_PATTERN.matcher(gprmcString);
return matcher.matches() ? matcher : null;
}
/**
* 解析UTC时间
* @Author keLe
* @Date 2024/4/8
* @param gprmcString GPRMC字符串
* @return String UTC时间
*/
private static String parseUTCTime(Matcher matcher,String gprmcString) {
String utcTimeStr = matcher.group(1);
int hours = Integer.parseInt(utcTimeStr.substring(0, 2));
int minutes = Integer.parseInt(utcTimeStr.substring(2, 4));
String seconds = utcTimeStr.substring(4);
String[] split = seconds.split("\\.");
return hours + ":" + minutes + ":" + split[0] + "." + split[1];
}
/**
* 将UTC时间字符串转换为Date对象
* @Author keLe
* @Date 2024/4/8
* @param utcTimeStr UTC时间字符串
* @return Date UTC时间对应的Date对象
*/
private static Time parseUTCTimeToDate(String utcTimeStr) {
try {
DateTime parse = DateUtil.parse(utcTimeStr, "HH:mm:ss.ssss");
return new Time(parse.getTime());
} catch (Exception e) {
log.error("Failed to parse UTC time:{}, 报错原因:{}", utcTimeStr, e.toString());
return null;
}
}
/**
* 解析状态
* @Author keLe
* @Date 2024/4/8
* @param gprmcString GPRMC字符串
* @return String 状态
*/
private static String parseStatus(Matcher matcher,String gprmcString) {
return matcher.group(4).equals("A") ? "有效" : "无效";
}
/**
* 解析纬度
* @Author keLe
* @Date 2024/4/8
* @param gprmcString GPRMC字符串
* @return double 纬度
*/
private static double parseLatitude(Matcher matcher,String gprmcString) {
return parseCoordinate(matcher.group(3), matcher.group(4), 2);
}
/**
* 解析经度
* @Author keLe
* @Date 2024/4/8
* @param gprmcString GPRMC字符串
* @return double 经度
*/
private static double parseLongitude(Matcher matcher,String gprmcString) {
return parseCoordinate(matcher.group(5), matcher.group(6), 3);
}
/**
* 功能描述: 解析速度
* @Author keLe
* @Date 2024/4/8
* @param matcher 匹配器
* @param gprmcString GPRMC字符串
* @return java.lang.String
*/
private static double parseSpeed(Matcher matcher,String gprmcString) {
return parseOptionalDouble(matcher.group(7));
}
/**
* 功能描述: 解析时间
* @Author keLe
* @Date 2024/4/8
* @param matcher 匹配器
* @param gprmcString GPRMC字符串
* @return java.lang.String
*/
private static double parseCourse(Matcher matcher,String gprmcString) {
return parseOptionalDouble(matcher.group(8));
}
/**
* 功能描述: 解析时间
* @Author keLe
* @Date 2024/4/8
* @param matcher 匹配器
* @param gprmcString GPRMC字符串
* @return java.lang.String
*/
private static Date parseDate(Matcher matcher,String gprmcString, String utcTime) {
String yyyy = 2000 + Integer.parseInt(matcher.group(9).substring(4)) + "";
String MM = matcher.group(9).substring(2, 4);
String dd = matcher.group(9).substring(0, 2);
try {
return DateUtil.parse(yyyy + "-" + MM + "-" + dd + " " + utcTime, DatePattern.NORM_DATETIME_PATTERN);
} catch (Exception e) {
log.error("Failed to parse YYMMDD :{}, 报错原因:{}", matcher.group(9), e.toString());
return null;
}
}
/**
* 功能描述: 解析磁偏角
* @Author keLe
* @Date 2024/4/8
* @param matcher 匹配器
* @param gprmcString GPRMC字符串
* @return java.lang.String
*/
private static double parseMagneticVariation(Matcher matcher,String gprmcString) {
return parseOptionalDouble(matcher.group(10));
}
/**
* 功能描述: 解析磁偏角方向
* @Author keLe
* @Date 2024/4/8
* @param matcher 匹配器
* @param gprmcString GPRMC字符串
* @return java.lang.String
*/
private static String parseMagneticVariationDir(Matcher matcher,String gprmcString) {
return parseDir(matcher.group(11));
}
/**
* 功能描述: parse Mode
* @Author keLe
* @Date 2024/4/8
* @param matcher 匹配器
* @param gprmcString GPRMC字符串
* @return java.lang.String
*/
private static String parseModeIndicator(Matcher matcher,String gprmcString) {
return parseModel(matcher.group(12));
}
/**
* 功能描述: parse Coordinate
* @Author keLe
* @Date 2024/4/8
* @param degrees 经纬度
* @param direction 方向
* @param index 截取下标位置
* @return java.lang.String
*/
private static double parseCoordinate(String degrees, String direction, int index) {
double coord = Double.parseDouble(degrees.substring(0, index)) + Double.parseDouble(degrees.substring(index)) / 60.0;
if (direction.startsWith("S") || direction.startsWith("W")) {
coord *= -1;
}
return coord;
}
/**
* 功能描述: parse Double
* @Author keLe
* @Date 2024/4/8
* @param value 字符串
*/
private static double parseOptionalDouble(String value) {
if (value == null || value.isEmpty()) {
return Double.NaN;
}
return Double.parseDouble(value);
}
/**
* Converts speed from knots to meters per second.
*
* @param speedInKnots The speed in knots.
* @return The speed in meters per second.
*/
public static double convertKnotsToMetersPerSecond(double speedInKnots) {
if (speedInKnots == 0) {
return speedInKnots;
}
double speedInKilometersPerHour = speedInKnots * KNOTS_TO_KILOMETERS_PER_HOUR * KILOMETERS_TO_METERS;
return DecimalUtils.preserveDecimal(speedInKilometersPerHour / HOURS_TO_SECONDS, 3);
}
/**
* 功能描述: Mode indication (NMEA0183 version 3.0 output only, A=autonomous positioning,
* D=differential, E=estimate, N=invalid data)
* @Author keLe
* @Date 2024/4/8
* @param charStr 模式指示
* @return java.lang.String
*/
public static String parseModel(String charStr) {
String str = "数据无效";
if (StringUtil.isBlank(charStr)) {
return str;
}
switch (charStr) {
case "A":
str = "自主定位";
break;
case "D":
str = "差分";
break;
case "E":
str = "估算";
break;
default:
str = "数据无效";
break;
}
return str;
}
/**
* 功能描述: Magnetic declination direction, E (east) or W (west)
* @Author keLe
* @Date 2024/4/8
* @param group 磁偏角方向
* @return java.lang.String
*/
private static String parseDir(String group) {
if (StringUtil.isBlank(group)) {
return "数据无效";
}
return group.equals("E") ? "东" : "西";
}
public static void main(String[] args) {
String gprmcString = "$GPRMC,075704.00,A,3104.39326987,N,12125.30904175,E,0.007,286.4,070424,6.3,W,D*29";
System.out.println("原始报文:" + gprmcString);
System.out.println("解析报文结果:" + parse(gprmcString));
}
}