/* * Copyright (c) 2023 Trackunit Corporation * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include "gnss_nmea0183.h" #include "gnss_parse.h" #define GNSS_NMEA0183_PICO_DEGREES_IN_DEGREE (1000000000000ULL) #define GNSS_NMEA0183_PICO_DEGREES_IN_MINUTE (GNSS_NMEA0183_PICO_DEGREES_IN_DEGREE / 60ULL) #define GNSS_NMEA0183_PICO_DEGREES_IN_NANO_DEGREE (1000ULL) #define GNSS_NMEA0183_NANO_KNOTS_IN_MMS (1943861LL) #define GNSS_NMEA0183_MESSAGE_SIZE_MIN (6) #define GNSS_NMEA0183_MESSAGE_CHECKSUM_SIZE (3) #define GNSS_NMEA0183_GSV_HDR_ARG_CNT (4) #define GNSS_NMEA0183_GSV_SV_ARG_CNT (4) #define GNSS_NMEA0183_GSV_PRN_GPS_RANGE (32) #define GNSS_NMEA0183_GSV_PRN_SBAS_OFFSET (87) #define GNSS_NMEA0183_GSV_PRN_GLONASS_OFFSET (64) #define GNSS_NMEA0183_GSV_PRN_BEIDOU_OFFSET (100) struct gsv_header_args { const char *message_id; const char *number_of_messages; const char *message_number; const char *numver_of_svs; }; struct gsv_sv_args { const char *prn; const char *elevation; const char *azimuth; const char *snr; }; static int gnss_system_from_gsv_header_args(const struct gsv_header_args *args, enum gnss_system *sv_system) { switch (args->message_id[2]) { case 'A': *sv_system = GNSS_SYSTEM_GALILEO; break; case 'B': *sv_system = GNSS_SYSTEM_BEIDOU; break; case 'P': *sv_system = GNSS_SYSTEM_GPS; break; case 'L': *sv_system = GNSS_SYSTEM_GLONASS; break; case 'Q': *sv_system = GNSS_SYSTEM_QZSS; break; default: return -EINVAL; } return 0; } static void align_satellite_with_gnss_system(enum gnss_system sv_system, struct gnss_satellite *satellite) { switch (sv_system) { case GNSS_SYSTEM_GPS: if (satellite->prn > GNSS_NMEA0183_GSV_PRN_GPS_RANGE) { satellite->system = GNSS_SYSTEM_SBAS; satellite->prn += GNSS_NMEA0183_GSV_PRN_SBAS_OFFSET; break; } satellite->system = GNSS_SYSTEM_GPS; break; case GNSS_SYSTEM_GLONASS: satellite->system = GNSS_SYSTEM_GLONASS; satellite->prn -= GNSS_NMEA0183_GSV_PRN_GLONASS_OFFSET; break; case GNSS_SYSTEM_GALILEO: satellite->system = GNSS_SYSTEM_GALILEO; break; case GNSS_SYSTEM_BEIDOU: satellite->system = GNSS_SYSTEM_BEIDOU; satellite->prn -= GNSS_NMEA0183_GSV_PRN_BEIDOU_OFFSET; break; case GNSS_SYSTEM_QZSS: satellite->system = GNSS_SYSTEM_QZSS; break; case GNSS_SYSTEM_IRNSS: case GNSS_SYSTEM_IMES: case GNSS_SYSTEM_SBAS: break; } } uint8_t gnss_nmea0183_checksum(const char *str) { uint8_t checksum = 0; size_t end; __ASSERT(str != NULL, "str argument must be provided"); end = strlen(str); for (size_t i = 0; i < end; i++) { checksum = checksum ^ str[i]; } return checksum; } int gnss_nmea0183_snprintk(char *str, size_t size, const char *fmt, ...) { va_list ap; uint8_t checksum; int pos; int len; __ASSERT(str != NULL, "str argument must be provided"); __ASSERT(fmt != NULL, "fmt argument must be provided"); if (size < GNSS_NMEA0183_MESSAGE_SIZE_MIN) { return -ENOMEM; } str[0] = '$'; va_start(ap, fmt); pos = vsnprintk(&str[1], size - 1, fmt, ap) + 1; va_end(ap); if (pos < 0) { return -EINVAL; } len = pos + GNSS_NMEA0183_MESSAGE_CHECKSUM_SIZE; if ((size - 1) < len) { return -ENOMEM; } checksum = gnss_nmea0183_checksum(&str[1]); pos = snprintk(&str[pos], size - pos, "*%02X", checksum); if (pos != 3) { return -EINVAL; } str[len] = '\0'; return len; } int gnss_nmea0183_ddmm_mmmm_to_ndeg(const char *ddmm_mmmm, int64_t *ndeg) { uint64_t pico_degrees = 0; int8_t decimal = -1; int8_t pos = 0; uint64_t increment; __ASSERT(ddmm_mmmm != NULL, "ddmm_mmmm argument must be provided"); __ASSERT(ndeg != NULL, "ndeg argument must be provided"); /* Find decimal */ while (ddmm_mmmm[pos] != '\0') { /* Verify if char is decimal */ if (ddmm_mmmm[pos] == '.') { decimal = pos; break; } /* Advance position */ pos++; } /* Verify decimal was found and placed correctly */ if (decimal < 1) { return -EINVAL; } /* Validate potential degree fraction is within bounds */ if (decimal > 1 && ddmm_mmmm[decimal - 2] > '5') { return -EINVAL; } /* Convert minute fraction to pico degrees and add it to pico_degrees */ pos = decimal + 1; increment = (GNSS_NMEA0183_PICO_DEGREES_IN_MINUTE / 10); while (ddmm_mmmm[pos] != '\0') { /* Verify char is decimal */ if (ddmm_mmmm[pos] < '0' || ddmm_mmmm[pos] > '9') { return -EINVAL; } /* Add increment to pico_degrees */ pico_degrees += (ddmm_mmmm[pos] - '0') * increment; /* Update unit */ increment /= 10; /* Increment position */ pos++; } /* Convert minutes and degrees to pico_degrees */ pos = decimal - 1; increment = GNSS_NMEA0183_PICO_DEGREES_IN_MINUTE; while (pos >= 0) { /* Check if digit switched from minutes to degrees */ if ((decimal - pos) == 3) { /* Reset increment to degrees */ increment = GNSS_NMEA0183_PICO_DEGREES_IN_DEGREE; } /* Verify char is decimal */ if (ddmm_mmmm[pos] < '0' || ddmm_mmmm[pos] > '9') { return -EINVAL; } /* Add increment to pico_degrees */ pico_degrees += (ddmm_mmmm[pos] - '0') * increment; /* Update unit */ increment *= 10; /* Decrement position */ pos--; } /* Convert to nano degrees */ *ndeg = (int64_t)(pico_degrees / GNSS_NMEA0183_PICO_DEGREES_IN_NANO_DEGREE); return 0; } bool gnss_nmea0183_validate_message(char **argv, uint16_t argc) { int32_t tmp = 0; uint8_t checksum = 0; size_t len; __ASSERT(argv != NULL, "argv argument must be provided"); /* Message must contain message id and checksum */ if (argc < 2) { return false; } /* First argument should start with '$' which is not covered by checksum */ if ((argc < 1) || (argv[0][0] != '$')) { return false; } len = strlen(argv[0]); for (uint16_t u = 1; u < len; u++) { checksum ^= argv[0][u]; } checksum ^= ','; /* Cover all except last argument which contains the checksum*/ for (uint16_t i = 1; i < (argc - 1); i++) { len = strlen(argv[i]); for (uint16_t u = 0; u < len; u++) { checksum ^= argv[i][u]; } checksum ^= ','; } if ((gnss_parse_atoi(argv[argc - 1], 16, &tmp) < 0) || (tmp > UINT8_MAX) || (tmp < 0)) { return false; } return checksum == (uint8_t)tmp; } int gnss_nmea0183_knots_to_mms(const char *str, int64_t *mms) { int ret; __ASSERT(str != NULL, "str argument must be provided"); __ASSERT(mms != NULL, "mms argument must be provided"); ret = gnss_parse_dec_to_nano(str, mms); if (ret < 0) { return ret; } *mms = (*mms) / GNSS_NMEA0183_NANO_KNOTS_IN_MMS; return 0; } int gnss_nmea0183_parse_hhmmss(const char *hhmmss, struct gnss_time *utc) { int64_t i64; int32_t i32; char part[3] = {0}; __ASSERT(hhmmss != NULL, "hhmmss argument must be provided"); __ASSERT(utc != NULL, "utc argument must be provided"); if (strlen(hhmmss) < 6) { return -EINVAL; } memcpy(part, hhmmss, 2); if ((gnss_parse_atoi(part, 10, &i32) < 0) || (i32 < 0) || (i32 > 23)) { return -EINVAL; } utc->hour = (uint8_t)i32; memcpy(part, &hhmmss[2], 2); if ((gnss_parse_atoi(part, 10, &i32) < 0) || (i32 < 0) || (i32 > 59)) { return -EINVAL; } utc->minute = (uint8_t)i32; if ((gnss_parse_dec_to_milli(&hhmmss[4], &i64) < 0) || (i64 < 0) || (i64 > 59999)) { return -EINVAL; } utc->millisecond = (uint16_t)i64; return 0; } int gnss_nmea0183_parse_ddmmyy(const char *ddmmyy, struct gnss_time *utc) { int32_t i32; char part[3] = {0}; __ASSERT(ddmmyy != NULL, "ddmmyy argument must be provided"); __ASSERT(utc != NULL, "utc argument must be provided"); if (strlen(ddmmyy) != 6) { return -EINVAL; } memcpy(part, ddmmyy, 2); if ((gnss_parse_atoi(part, 10, &i32) < 0) || (i32 < 1) || (i32 > 31)) { return -EINVAL; } utc->month_day = (uint8_t)i32; memcpy(part, &ddmmyy[2], 2); if ((gnss_parse_atoi(part, 10, &i32) < 0) || (i32 < 1) || (i32 > 12)) { return -EINVAL; } utc->month = (uint8_t)i32; memcpy(part, &ddmmyy[4], 2); if ((gnss_parse_atoi(part, 10, &i32) < 0) || (i32 < 0) || (i32 > 99)) { return -EINVAL; } utc->century_year = (uint8_t)i32; return 0; } int gnss_nmea0183_parse_rmc(const char **argv, uint16_t argc, struct gnss_data *data) { int64_t tmp; __ASSERT(argv != NULL, "argv argument must be provided"); __ASSERT(data != NULL, "data argument must be provided"); if (argc < 10) { return -EINVAL; } /* Validate GNSS has fix */ if (argv[2][0] == 'V') { return 0; } if (argv[2][0] != 'A') { return -EINVAL; } /* Parse UTC time */ if ((gnss_nmea0183_parse_hhmmss(argv[1], &data->utc) < 0)) { return -EINVAL; } /* Validate cardinal directions */ if (((argv[4][0] != 'N') && (argv[4][0] != 'S')) || ((argv[6][0] != 'E') && (argv[6][0] != 'W'))) { return -EINVAL; } /* Parse coordinates */ if ((gnss_nmea0183_ddmm_mmmm_to_ndeg(argv[3], &data->nav_data.latitude) < 0) || (gnss_nmea0183_ddmm_mmmm_to_ndeg(argv[5], &data->nav_data.longitude) < 0)) { return -EINVAL; } /* Align sign of coordinates with cardinal directions */ data->nav_data.latitude = argv[4][0] == 'N' ? data->nav_data.latitude : -data->nav_data.latitude; data->nav_data.longitude = argv[6][0] == 'E' ? data->nav_data.longitude : -data->nav_data.longitude; /* Parse speed */ if ((gnss_nmea0183_knots_to_mms(argv[7], &tmp) < 0) || (tmp > UINT32_MAX)) { return -EINVAL; } data->nav_data.speed = (uint32_t)tmp; /* Parse bearing */ if ((gnss_parse_dec_to_milli(argv[8], &tmp) < 0) || (tmp > 359999) || (tmp < 0)) { return -EINVAL; } data->nav_data.bearing = (uint32_t)tmp; /* Parse UTC date */ if ((gnss_nmea0183_parse_ddmmyy(argv[9], &data->utc) < 0)) { return -EINVAL; } return 0; } static int parse_gga_fix_quality(const char *str, enum gnss_fix_quality *fix_quality) { __ASSERT(str != NULL, "str argument must be provided"); __ASSERT(fix_quality != NULL, "fix_quality argument must be provided"); if ((str[1] != ((char)'\0')) || (str[0] < ((char)'0')) || (((char)'6') < str[0])) { return -EINVAL; } (*fix_quality) = (enum gnss_fix_quality)(str[0] - ((char)'0')); return 0; } static enum gnss_fix_status fix_status_from_fix_quality(enum gnss_fix_quality fix_quality) { enum gnss_fix_status fix_status = GNSS_FIX_STATUS_NO_FIX; switch (fix_quality) { case GNSS_FIX_QUALITY_GNSS_SPS: case GNSS_FIX_QUALITY_GNSS_PPS: fix_status = GNSS_FIX_STATUS_GNSS_FIX; break; case GNSS_FIX_QUALITY_DGNSS: case GNSS_FIX_QUALITY_RTK: case GNSS_FIX_QUALITY_FLOAT_RTK: fix_status = GNSS_FIX_STATUS_DGNSS_FIX; break; case GNSS_FIX_QUALITY_ESTIMATED: fix_status = GNSS_FIX_STATUS_ESTIMATED_FIX; break; default: break; } return fix_status; } int gnss_nmea0183_parse_gga(const char **argv, uint16_t argc, struct gnss_data *data) { int32_t tmp32; int64_t tmp64; __ASSERT(argv != NULL, "argv argument must be provided"); __ASSERT(data != NULL, "data argument must be provided"); if (argc < 12) { return -EINVAL; } /* Parse fix quality and status */ if (parse_gga_fix_quality(argv[6], &data->info.fix_quality) < 0) { return -EINVAL; } data->info.fix_status = fix_status_from_fix_quality(data->info.fix_quality); /* Validate GNSS has fix */ if (data->info.fix_status == GNSS_FIX_STATUS_NO_FIX) { return 0; } /* Parse number of satellites */ if ((gnss_parse_atoi(argv[7], 10, &tmp32) < 0) || (tmp32 > UINT16_MAX) || (tmp32 < 0)) { return -EINVAL; } data->info.satellites_cnt = (uint16_t)tmp32; /* Parse HDOP */ if ((gnss_parse_dec_to_milli(argv[8], &tmp64) < 0) || (tmp64 > UINT16_MAX) || (tmp64 < 0)) { return -EINVAL; } data->info.hdop = (uint16_t)tmp64; /* Parse altitude */ if ((gnss_parse_dec_to_milli(argv[11], &tmp64) < 0) || (tmp64 > INT32_MAX) || (tmp64 < INT32_MIN)) { return -EINVAL; } data->nav_data.altitude = (int32_t)tmp64; return 0; } static int parse_gsv_svs(struct gnss_satellite *satellites, const struct gsv_sv_args *svs, uint16_t svs_size) { int32_t i32; for (uint16_t i = 0; i < svs_size; i++) { /* Parse PRN */ if ((gnss_parse_atoi(svs[i].prn, 10, &i32) < 0) || (i32 < 0) || (i32 > UINT16_MAX)) { return -EINVAL; } satellites[i].prn = (uint16_t)i32; /* Parse elevation */ if ((gnss_parse_atoi(svs[i].elevation, 10, &i32) < 0) || (i32 < 0) || (i32 > 90)) { return -EINVAL; } satellites[i].elevation = (uint8_t)i32; /* Parse azimuth */ if ((gnss_parse_atoi(svs[i].azimuth, 10, &i32) < 0) || (i32 < 0) || (i32 > 359)) { return -EINVAL; } satellites[i].azimuth = (uint16_t)i32; /* Parse SNR */ if (strlen(svs[i].snr) == 0) { satellites[i].snr = 0; satellites[i].is_tracked = false; continue; } if ((gnss_parse_atoi(svs[i].snr, 10, &i32) < 0) || (i32 < 0) || (i32 > 99)) { return -EINVAL; } satellites[i].snr = (uint16_t)i32; satellites[i].is_tracked = true; } return 0; } int gnss_nmea0183_parse_gsv_header(const char **argv, uint16_t argc, struct gnss_nmea0183_gsv_header *header) { const struct gsv_header_args *args = (const struct gsv_header_args *)argv; int i32; __ASSERT(argv != NULL, "argv argument must be provided"); __ASSERT(header != NULL, "header argument must be provided"); if (argc < 4) { return -EINVAL; } /* Parse GNSS sv_system */ if (gnss_system_from_gsv_header_args(args, &header->system) < 0) { return -EINVAL; } /* Parse number of messages */ if ((gnss_parse_atoi(args->number_of_messages, 10, &i32) < 0) || (i32 < 0) || (i32 > UINT16_MAX)) { return -EINVAL; } header->number_of_messages = (uint16_t)i32; /* Parse message number */ if ((gnss_parse_atoi(args->message_number, 10, &i32) < 0) || (i32 < 0) || (i32 > UINT16_MAX)) { return -EINVAL; } header->message_number = (uint16_t)i32; /* Parse message number */ if ((gnss_parse_atoi(args->numver_of_svs, 10, &i32) < 0) || (i32 < 0) || (i32 > UINT16_MAX)) { return -EINVAL; } header->number_of_svs = (uint16_t)i32; return 0; } int gnss_nmea0183_parse_gsv_svs(const char **argv, uint16_t argc, struct gnss_satellite *satellites, uint16_t size) { const struct gsv_header_args *header_args = (const struct gsv_header_args *)argv; const struct gsv_sv_args *sv_args = (const struct gsv_sv_args *)(argv + 4); uint16_t sv_args_size; enum gnss_system sv_system; __ASSERT(argv != NULL, "argv argument must be provided"); __ASSERT(satellites != NULL, "satellites argument must be provided"); if (argc < 9) { return 0; } sv_args_size = (argc - GNSS_NMEA0183_GSV_HDR_ARG_CNT) / GNSS_NMEA0183_GSV_SV_ARG_CNT; if (size < sv_args_size) { return -ENOMEM; } if (parse_gsv_svs(satellites, sv_args, sv_args_size) < 0) { return -EINVAL; } if (gnss_system_from_gsv_header_args(header_args, &sv_system) < 0) { return -EINVAL; } for (uint16_t i = 0; i < sv_args_size; i++) { align_satellite_with_gnss_system(sv_system, &satellites[i]); } return (int)sv_args_size; }