diff --git a/applications/main/subghz/helpers/minmea.c b/applications/main/subghz/helpers/minmea.c new file mode 100644 index 000000000..a8be53de1 --- /dev/null +++ b/applications/main/subghz/helpers/minmea.c @@ -0,0 +1,640 @@ +/* + * Copyright © 2014 Kosma Moczek + * This program is free software. It comes without any warranty, to the extent + * permitted by applicable law. You can redistribute it and/or modify it under + * the terms of the Do What The Fuck You Want To Public License, Version 2, as + * published by Sam Hocevar. See the COPYING file for more details. + */ + +#include "minmea.h" + +#include +#include +#include + +#define boolstr(s) ((s) ? "true" : "false") + +static int hex2int(char c) { + if(c >= '0' && c <= '9') return c - '0'; + if(c >= 'A' && c <= 'F') return c - 'A' + 10; + if(c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +uint8_t minmea_checksum(const char* sentence) { + // Support senteces with or without the starting dollar sign. + if(*sentence == '$') sentence++; + + uint8_t checksum = 0x00; + + // The optional checksum is an XOR of all bytes between "$" and "*". + while(*sentence && *sentence != '*') checksum ^= *sentence++; + + return checksum; +} + +bool minmea_check(const char* sentence, bool strict) { + uint8_t checksum = 0x00; + + // A valid sentence starts with "$". + if(*sentence++ != '$') return false; + + // The optional checksum is an XOR of all bytes between "$" and "*". + while(*sentence && *sentence != '*' && isprint((unsigned char)*sentence)) + checksum ^= *sentence++; + + // If checksum is present... + if(*sentence == '*') { + // Extract checksum. + sentence++; + int upper = hex2int(*sentence++); + if(upper == -1) return false; + int lower = hex2int(*sentence++); + if(lower == -1) return false; + int expected = upper << 4 | lower; + + // Check for checksum mismatch. + if(checksum != expected) return false; + } else if(strict) { + // Discard non-checksummed frames in strict mode. + return false; + } + + // The only stuff allowed at this point is a newline. + while(*sentence == '\r' || *sentence == '\n') { + sentence++; + } + + if(*sentence) { + return false; + } + + return true; +} + +bool minmea_scan(const char* sentence, const char* format, ...) { + bool result = false; + bool optional = false; + + if(sentence == NULL) return false; + + va_list ap; + va_start(ap, format); + + const char* field = sentence; +#define next_field() \ + do { \ + /* Progress to the next field. */ \ + while(minmea_isfield(*sentence)) sentence++; \ + /* Make sure there is a field there. */ \ + if(*sentence == ',') { \ + sentence++; \ + field = sentence; \ + } else { \ + field = NULL; \ + } \ + } while(0) + + while(*format) { + char type = *format++; + + if(type == ';') { + // All further fields are optional. + optional = true; + continue; + } + + if(!field && !optional) { + // Field requested but we ran out if input. Bail out. + goto parse_error; + } + + switch(type) { + case 'c': { // Single character field (char). + char value = '\0'; + + if(field && minmea_isfield(*field)) value = *field; + + *va_arg(ap, char*) = value; + } break; + + case 'd': { // Single character direction field (int). + int value = 0; + + if(field && minmea_isfield(*field)) { + switch(*field) { + case 'N': + case 'E': + value = 1; + break; + case 'S': + case 'W': + value = -1; + break; + default: + goto parse_error; + } + } + + *va_arg(ap, int*) = value; + } break; + + case 'f': { // Fractional value with scale (struct minmea_float). + int sign = 0; + int_least32_t value = -1; + int_least32_t scale = 0; + + if(field) { + while(minmea_isfield(*field)) { + if(*field == '+' && !sign && value == -1) { + sign = 1; + } else if(*field == '-' && !sign && value == -1) { + sign = -1; + } else if(isdigit((unsigned char)*field)) { + int digit = *field - '0'; + if(value == -1) value = 0; + if(value > (INT_LEAST32_MAX - digit) / 10) { + /* we ran out of bits, what do we do? */ + if(scale) { + /* truncate extra precision */ + break; + } else { + /* integer overflow. bail out. */ + goto parse_error; + } + } + value = (10 * value) + digit; + if(scale) scale *= 10; + } else if(*field == '.' && scale == 0) { + scale = 1; + } else if(*field == ' ') { + /* Allow spaces at the start of the field. Not NMEA + * conformant, but some modules do this. PP */ + if(sign != 0 || value != -1 || scale != 0) goto parse_error; + } else { + goto parse_error; + } + field++; + } + } + + if((sign || scale) && value == -1) goto parse_error; + + if(value == -1) { + /* No digits were scanned. */ + value = 0; + scale = 0; + } else if(scale == 0) { + /* No decimal point. */ + scale = 1; + } + if(sign) value *= sign; + + *va_arg(ap, struct minmea_float*) = (struct minmea_float){value, scale}; + } break; + + case 'i': { // Integer value, default 0 (int). + int value = 0; + + if(field) { + char* endptr; + value = strtol(field, &endptr, 10); + if(minmea_isfield(*endptr)) goto parse_error; + } + + *va_arg(ap, int*) = value; + } break; + + case 's': { // String value (char *). + char* buf = va_arg(ap, char*); + + if(field) { + while(minmea_isfield(*field)) *buf++ = *field++; + } + + *buf = '\0'; + } break; + + case 't': { // NMEA talker+sentence identifier (char *). + // This field is always mandatory voss. + if(!field) goto parse_error; + + if(field[0] != '$') goto parse_error; + for(int f = 0; f < 5; f++) + if(!minmea_isfield(field[1 + f])) goto parse_error; + + char* buf = va_arg(ap, char*); + memcpy(buf, field + 1, 5); + buf[5] = '\0'; + } break; + + case 'D': { // Date (int, int, int), -1 if empty. + struct minmea_date* date = va_arg(ap, struct minmea_date*); + + int d = -1, m = -1, y = -1; + + if(field && minmea_isfield(*field)) { + // Always six digits. + for(int f = 0; f < 6; f++) + if(!isdigit((unsigned char)field[f])) goto parse_error; + + char dArr[] = {field[0], field[1], '\0'}; + char mArr[] = {field[2], field[3], '\0'}; + char yArr[] = {field[4], field[5], '\0'}; + d = strtol(dArr, NULL, 10); + m = strtol(mArr, NULL, 10); + y = strtol(yArr, NULL, 10); + } + + date->day = d; + date->month = m; + date->year = y; + } break; + + case 'T': { // Time (int, int, int, int), -1 if empty. + struct minmea_time* time_ = va_arg(ap, struct minmea_time*); + + int h = -1, i = -1, s = -1, u = -1; + + if(field && minmea_isfield(*field)) { + // Minimum required: integer time. + for(int f = 0; f < 6; f++) + if(!isdigit((unsigned char)field[f])) goto parse_error; + + char hArr[] = {field[0], field[1], '\0'}; + char iArr[] = {field[2], field[3], '\0'}; + char sArr[] = {field[4], field[5], '\0'}; + h = strtol(hArr, NULL, 10); + i = strtol(iArr, NULL, 10); + s = strtol(sArr, NULL, 10); + field += 6; + + // Extra: fractional time. Saved as microseconds. + if(*field++ == '.') { + uint32_t value = 0; + uint32_t scale = 1000000LU; + while(isdigit((unsigned char)*field) && scale > 1) { + value = (value * 10) + (*field++ - '0'); + scale /= 10; + } + u = value * scale; + } else { + u = 0; + } + } + + time_->hours = h; + time_->minutes = i; + time_->seconds = s; + time_->microseconds = u; + } break; + + case '_': { // Ignore the field. + } break; + + default: { // Unknown. + goto parse_error; + } + } + + next_field(); + } + + result = true; + +parse_error: + va_end(ap); + return result; +} + +bool minmea_talker_id(char talker[3], const char* sentence) { + char type[6]; + if(!minmea_scan(sentence, "t", type)) return false; + + talker[0] = type[0]; + talker[1] = type[1]; + talker[2] = '\0'; + + return true; +} + +enum minmea_sentence_id minmea_sentence_id(const char* sentence, bool strict) { + if(!minmea_check(sentence, strict)) return MINMEA_INVALID; + + char type[6]; + if(!minmea_scan(sentence, "t", type)) return MINMEA_INVALID; + + if(!strcmp(type + 2, "GBS")) return MINMEA_SENTENCE_GBS; + if(!strcmp(type + 2, "GGA")) return MINMEA_SENTENCE_GGA; + if(!strcmp(type + 2, "GLL")) return MINMEA_SENTENCE_GLL; + if(!strcmp(type + 2, "GSA")) return MINMEA_SENTENCE_GSA; + if(!strcmp(type + 2, "GST")) return MINMEA_SENTENCE_GST; + if(!strcmp(type + 2, "GSV")) return MINMEA_SENTENCE_GSV; + if(!strcmp(type + 2, "RMC")) return MINMEA_SENTENCE_RMC; + if(!strcmp(type + 2, "VTG")) return MINMEA_SENTENCE_VTG; + if(!strcmp(type + 2, "ZDA")) return MINMEA_SENTENCE_ZDA; + + return MINMEA_UNKNOWN; +} + +bool minmea_parse_gbs(struct minmea_sentence_gbs* frame, const char* sentence) { + // $GNGBS,170556.00,3.0,2.9,8.3,,,,*5C + char type[6]; + if(!minmea_scan( + sentence, + "tTfffifff", + type, + &frame->time, + &frame->err_latitude, + &frame->err_longitude, + &frame->err_altitude, + &frame->svid, + &frame->prob, + &frame->bias, + &frame->stddev)) + return false; + if(strcmp(type + 2, "GBS")) return false; + + return true; +} + +bool minmea_parse_rmc(struct minmea_sentence_rmc* frame, const char* sentence) { + // $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62 + char type[6]; + char validity; + int latitude_direction; + int longitude_direction; + int variation_direction; + if(!minmea_scan( + sentence, + "tTcfdfdffDfd", + type, + &frame->time, + &validity, + &frame->latitude, + &latitude_direction, + &frame->longitude, + &longitude_direction, + &frame->speed, + &frame->course, + &frame->date, + &frame->variation, + &variation_direction)) + return false; + if(strcmp(type + 2, "RMC")) return false; + + frame->valid = (validity == 'A'); + frame->latitude.value *= latitude_direction; + frame->longitude.value *= longitude_direction; + frame->variation.value *= variation_direction; + + return true; +} + +bool minmea_parse_gga(struct minmea_sentence_gga* frame, const char* sentence) { + // $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 + char type[6]; + int latitude_direction; + int longitude_direction; + + if(!minmea_scan( + sentence, + "tTfdfdiiffcfcf_", + type, + &frame->time, + &frame->latitude, + &latitude_direction, + &frame->longitude, + &longitude_direction, + &frame->fix_quality, + &frame->satellites_tracked, + &frame->hdop, + &frame->altitude, + &frame->altitude_units, + &frame->height, + &frame->height_units, + &frame->dgps_age)) + return false; + if(strcmp(type + 2, "GGA")) return false; + + frame->latitude.value *= latitude_direction; + frame->longitude.value *= longitude_direction; + + return true; +} + +bool minmea_parse_gsa(struct minmea_sentence_gsa* frame, const char* sentence) { + // $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39 + char type[6]; + + if(!minmea_scan( + sentence, + "tciiiiiiiiiiiiifff", + type, + &frame->mode, + &frame->fix_type, + &frame->sats[0], + &frame->sats[1], + &frame->sats[2], + &frame->sats[3], + &frame->sats[4], + &frame->sats[5], + &frame->sats[6], + &frame->sats[7], + &frame->sats[8], + &frame->sats[9], + &frame->sats[10], + &frame->sats[11], + &frame->pdop, + &frame->hdop, + &frame->vdop)) + return false; + if(strcmp(type + 2, "GSA")) return false; + + return true; +} + +bool minmea_parse_gll(struct minmea_sentence_gll* frame, const char* sentence) { + // $GPGLL,3723.2475,N,12158.3416,W,161229.487,A,A*41$; + char type[6]; + int latitude_direction; + int longitude_direction; + + if(!minmea_scan( + sentence, + "tfdfdTc;c", + type, + &frame->latitude, + &latitude_direction, + &frame->longitude, + &longitude_direction, + &frame->time, + &frame->status, + &frame->mode)) + return false; + if(strcmp(type + 2, "GLL")) return false; + + frame->latitude.value *= latitude_direction; + frame->longitude.value *= longitude_direction; + + return true; +} + +bool minmea_parse_gst(struct minmea_sentence_gst* frame, const char* sentence) { + // $GPGST,024603.00,3.2,6.6,4.7,47.3,5.8,5.6,22.0*58 + char type[6]; + + if(!minmea_scan( + sentence, + "tTfffffff", + type, + &frame->time, + &frame->rms_deviation, + &frame->semi_major_deviation, + &frame->semi_minor_deviation, + &frame->semi_major_orientation, + &frame->latitude_error_deviation, + &frame->longitude_error_deviation, + &frame->altitude_error_deviation)) + return false; + if(strcmp(type + 2, "GST")) return false; + + return true; +} + +bool minmea_parse_gsv(struct minmea_sentence_gsv* frame, const char* sentence) { + // $GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74 + // $GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D + // $GPGSV,4,2,11,08,51,203,30,09,45,215,28*75 + // $GPGSV,4,4,13,39,31,170,27*40 + // $GPGSV,4,4,13*7B + char type[6]; + + if(!minmea_scan( + sentence, + "tiii;iiiiiiiiiiiiiiii", + type, + &frame->total_msgs, + &frame->msg_nr, + &frame->total_sats, + &frame->sats[0].nr, + &frame->sats[0].elevation, + &frame->sats[0].azimuth, + &frame->sats[0].snr, + &frame->sats[1].nr, + &frame->sats[1].elevation, + &frame->sats[1].azimuth, + &frame->sats[1].snr, + &frame->sats[2].nr, + &frame->sats[2].elevation, + &frame->sats[2].azimuth, + &frame->sats[2].snr, + &frame->sats[3].nr, + &frame->sats[3].elevation, + &frame->sats[3].azimuth, + &frame->sats[3].snr)) { + return false; + } + if(strcmp(type + 2, "GSV")) return false; + + return true; +} + +bool minmea_parse_vtg(struct minmea_sentence_vtg* frame, const char* sentence) { + // $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48,S + // $GPVTG,156.1,T,140.9,M,0.0,N,0.0,K*41,I + // $GPVTG,096.5,T,083.5,M,0.0,N,0.0,K,D*22,L + // $GPVTG,188.36,T,,M,0.820,N,1.519,K,A*3F,333033 + char type[6]; + char c_true, c_magnetic, c_knots, c_kph, c_faa_mode; + + if(!minmea_scan( + sentence, + "t;fcfcfcfcc", + type, + &frame->true_track_degrees, + &c_true, + &frame->magnetic_track_degrees, + &c_magnetic, + &frame->speed_knots, + &c_knots, + &frame->speed_kph, + &c_kph, + &c_faa_mode)) + return false; + if(strcmp(type + 2, "VTG")) return false; + // values are only valid with the accompanying characters + if(c_true != 'T') frame->true_track_degrees.scale = 0; + if(c_magnetic != 'M') frame->magnetic_track_degrees.scale = 0; + if(c_knots != 'N') frame->speed_knots.scale = 0; + if(c_kph != 'K') frame->speed_kph.scale = 0; + frame->faa_mode = (enum minmea_faa_mode)c_faa_mode; + + return true; +} + +bool minmea_parse_zda(struct minmea_sentence_zda* frame, const char* sentence) { + // $GPZDA,201530.00,04,07,2002,00,00*60 + char type[6]; + + if(!minmea_scan( + sentence, + "tTiiiii", + type, + &frame->time, + &frame->date.day, + &frame->date.month, + &frame->date.year, + &frame->hour_offset, + &frame->minute_offset)) + return false; + if(strcmp(type + 2, "ZDA")) return false; + + // check offsets + if(abs(frame->hour_offset) > 13 || frame->minute_offset > 59 || frame->minute_offset < 0) + return false; + + return true; +} + +int minmea_getdatetime( + struct tm* tm, + const struct minmea_date* date, + const struct minmea_time* time_) { + if(date->year == -1 || time_->hours == -1) return -1; + + memset(tm, 0, sizeof(*tm)); + if(date->year < 80) { + tm->tm_year = 2000 + date->year - 1900; // 2000-2079 + } else if(date->year >= 1900) { + tm->tm_year = date->year - 1900; // 4 digit year, use directly + } else { + tm->tm_year = date->year; // 1980-1999 + } + tm->tm_mon = date->month - 1; + tm->tm_mday = date->day; + tm->tm_hour = time_->hours; + tm->tm_min = time_->minutes; + tm->tm_sec = time_->seconds; + + return 0; +} + +int minmea_gettime( + struct timespec* ts, + const struct minmea_date* date, + const struct minmea_time* time_) { + struct tm tm; + if(minmea_getdatetime(&tm, date, time_)) return -1; + + time_t timestamp = mktime(&tm); /* See README.md if your system lacks timegm(). */ + if(timestamp != (time_t)-1) { + ts->tv_sec = timestamp; + ts->tv_nsec = time_->microseconds * 1000; + return 0; + } else { + return -1; + } +} + +/* vim: set ts=4 sw=4 et: */ diff --git a/applications/main/subghz/helpers/minmea.h b/applications/main/subghz/helpers/minmea.h new file mode 100644 index 000000000..88eec4ae9 --- /dev/null +++ b/applications/main/subghz/helpers/minmea.h @@ -0,0 +1,295 @@ +/* + * Copyright © 2014 Kosma Moczek + * This program is free software. It comes without any warranty, to the extent + * permitted by applicable law. You can redistribute it and/or modify it under + * the terms of the Do What The Fuck You Want To Public License, Version 2, as + * published by Sam Hocevar. See the COPYING file for more details. + */ + +#ifndef MINMEA_H +#define MINMEA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#ifdef MINMEA_INCLUDE_COMPAT +#include +#endif + +#ifndef MINMEA_MAX_SENTENCE_LENGTH +#define MINMEA_MAX_SENTENCE_LENGTH 80 +#endif + +enum minmea_sentence_id { + MINMEA_INVALID = -1, + MINMEA_UNKNOWN = 0, + MINMEA_SENTENCE_GBS, + MINMEA_SENTENCE_GGA, + MINMEA_SENTENCE_GLL, + MINMEA_SENTENCE_GSA, + MINMEA_SENTENCE_GST, + MINMEA_SENTENCE_GSV, + MINMEA_SENTENCE_RMC, + MINMEA_SENTENCE_VTG, + MINMEA_SENTENCE_ZDA, +}; + +struct minmea_float { + int_least32_t value; + int_least32_t scale; +}; + +struct minmea_date { + int day; + int month; + int year; +}; + +struct minmea_time { + int hours; + int minutes; + int seconds; + int microseconds; +}; + +struct minmea_sentence_gbs { + struct minmea_time time; + struct minmea_float err_latitude; + struct minmea_float err_longitude; + struct minmea_float err_altitude; + int svid; + struct minmea_float prob; + struct minmea_float bias; + struct minmea_float stddev; +}; + +struct minmea_sentence_rmc { + struct minmea_time time; + bool valid; + struct minmea_float latitude; + struct minmea_float longitude; + struct minmea_float speed; + struct minmea_float course; + struct minmea_date date; + struct minmea_float variation; +}; + +struct minmea_sentence_gga { + struct minmea_time time; + struct minmea_float latitude; + struct minmea_float longitude; + int fix_quality; + int satellites_tracked; + struct minmea_float hdop; + struct minmea_float altitude; + char altitude_units; + struct minmea_float height; + char height_units; + struct minmea_float dgps_age; +}; + +enum minmea_gll_status { + MINMEA_GLL_STATUS_DATA_VALID = 'A', + MINMEA_GLL_STATUS_DATA_NOT_VALID = 'V', +}; + +// FAA mode added to some fields in NMEA 2.3. +enum minmea_faa_mode { + MINMEA_FAA_MODE_AUTONOMOUS = 'A', + MINMEA_FAA_MODE_DIFFERENTIAL = 'D', + MINMEA_FAA_MODE_ESTIMATED = 'E', + MINMEA_FAA_MODE_MANUAL = 'M', + MINMEA_FAA_MODE_SIMULATED = 'S', + MINMEA_FAA_MODE_NOT_VALID = 'N', + MINMEA_FAA_MODE_PRECISE = 'P', +}; + +struct minmea_sentence_gll { + struct minmea_float latitude; + struct minmea_float longitude; + struct minmea_time time; + char status; + char mode; +}; + +struct minmea_sentence_gst { + struct minmea_time time; + struct minmea_float rms_deviation; + struct minmea_float semi_major_deviation; + struct minmea_float semi_minor_deviation; + struct minmea_float semi_major_orientation; + struct minmea_float latitude_error_deviation; + struct minmea_float longitude_error_deviation; + struct minmea_float altitude_error_deviation; +}; + +enum minmea_gsa_mode { + MINMEA_GPGSA_MODE_AUTO = 'A', + MINMEA_GPGSA_MODE_FORCED = 'M', +}; + +enum minmea_gsa_fix_type { + MINMEA_GPGSA_FIX_NONE = 1, + MINMEA_GPGSA_FIX_2D = 2, + MINMEA_GPGSA_FIX_3D = 3, +}; + +struct minmea_sentence_gsa { + char mode; + int fix_type; + int sats[12]; + struct minmea_float pdop; + struct minmea_float hdop; + struct minmea_float vdop; +}; + +struct minmea_sat_info { + int nr; + int elevation; + int azimuth; + int snr; +}; + +struct minmea_sentence_gsv { + int total_msgs; + int msg_nr; + int total_sats; + struct minmea_sat_info sats[4]; +}; + +struct minmea_sentence_vtg { + struct minmea_float true_track_degrees; + struct minmea_float magnetic_track_degrees; + struct minmea_float speed_knots; + struct minmea_float speed_kph; + enum minmea_faa_mode faa_mode; +}; + +struct minmea_sentence_zda { + struct minmea_time time; + struct minmea_date date; + int hour_offset; + int minute_offset; +}; + +/** + * Calculate raw sentence checksum. Does not check sentence integrity. + */ +uint8_t minmea_checksum(const char* sentence); + +/** + * Check sentence validity and checksum. Returns true for valid sentences. + */ +bool minmea_check(const char* sentence, bool strict); + +/** + * Determine talker identifier. + */ +bool minmea_talker_id(char talker[3], const char* sentence); + +/** + * Determine sentence identifier. + */ +enum minmea_sentence_id minmea_sentence_id(const char* sentence, bool strict); + +/** + * Scanf-like processor for NMEA sentences. Supports the following formats: + * c - single character (char *) + * d - direction, returned as 1/-1, default 0 (int *) + * f - fractional, returned as value + scale (struct minmea_float *) + * i - decimal, default zero (int *) + * s - string (char *) + * t - talker identifier and type (char *) + * D - date (struct minmea_date *) + * T - time stamp (struct minmea_time *) + * _ - ignore this field + * ; - following fields are optional + * Returns true on success. See library source code for details. + */ +bool minmea_scan(const char* sentence, const char* format, ...); + +/* + * Parse a specific type of sentence. Return true on success. + */ +bool minmea_parse_gbs(struct minmea_sentence_gbs* frame, const char* sentence); +bool minmea_parse_rmc(struct minmea_sentence_rmc* frame, const char* sentence); +bool minmea_parse_gga(struct minmea_sentence_gga* frame, const char* sentence); +bool minmea_parse_gsa(struct minmea_sentence_gsa* frame, const char* sentence); +bool minmea_parse_gll(struct minmea_sentence_gll* frame, const char* sentence); +bool minmea_parse_gst(struct minmea_sentence_gst* frame, const char* sentence); +bool minmea_parse_gsv(struct minmea_sentence_gsv* frame, const char* sentence); +bool minmea_parse_vtg(struct minmea_sentence_vtg* frame, const char* sentence); +bool minmea_parse_zda(struct minmea_sentence_zda* frame, const char* sentence); + +/** + * Convert GPS UTC date/time representation to a UNIX calendar time. + */ +int minmea_getdatetime( + struct tm* tm, + const struct minmea_date* date, + const struct minmea_time* time_); + +/** + * Convert GPS UTC date/time representation to a UNIX timestamp. + */ +int minmea_gettime( + struct timespec* ts, + const struct minmea_date* date, + const struct minmea_time* time_); + +/** + * Rescale a fixed-point value to a different scale. Rounds towards zero. + */ +static inline int_least32_t minmea_rescale(const struct minmea_float* f, int_least32_t new_scale) { + if(f->scale == 0) return 0; + if(f->scale == new_scale) return f->value; + if(f->scale > new_scale) + return (f->value + ((f->value > 0) - (f->value < 0)) * f->scale / new_scale / 2) / + (f->scale / new_scale); + else + return f->value * (new_scale / f->scale); +} + +/** + * Convert a fixed-point value to a floating-point value. + * Returns NaN for "unknown" values. + */ +static inline float minmea_tofloat(const struct minmea_float* f) { + if(f->scale == 0) return NAN; + return (float)f->value / (float)f->scale; +} + +/** + * Convert a raw coordinate to a floating point DD.DDD... value. + * Returns NaN for "unknown" values. + */ +static inline float minmea_tocoord(const struct minmea_float* f) { + if(f->scale == 0) return NAN; + if(f->scale > (INT_LEAST32_MAX / 100)) return NAN; + if(f->scale < (INT_LEAST32_MIN / 100)) return NAN; + int_least32_t degrees = f->value / (f->scale * 100); + int_least32_t minutes = f->value % (f->scale * 100); + return (float)degrees + (float)minutes / (60 * f->scale); +} + +/** + * Check whether a character belongs to the set of characters allowed in a + * sentence data field. + */ +static inline bool minmea_isfield(char c) { + return isprint((unsigned char)c) && c != ',' && c != '*'; +} + +#ifdef __cplusplus +} +#endif + +#endif /* MINMEA_H */ + +/* vim: set ts=4 sw=4 et: */ diff --git a/applications/main/subghz/helpers/subghz_custom_event.h b/applications/main/subghz/helpers/subghz_custom_event.h index 5f1a02673..1e1925834 100644 --- a/applications/main/subghz/helpers/subghz_custom_event.h +++ b/applications/main/subghz/helpers/subghz_custom_event.h @@ -68,6 +68,8 @@ typedef enum { SubGhzCustomEventSceneReceiverInfoTxStart, SubGhzCustomEventSceneReceiverInfoTxStop, SubGhzCustomEventSceneReceiverInfoSave, + SubGhzCustomEventSceneReceiverInfoSats, + SubGhzCustomEventSceneReceiverInfoBack, SubGhzCustomEventSceneSaveName, SubGhzCustomEventSceneSaveSuccess, SubGhzCustomEventSceneShowErrorBack, diff --git a/applications/main/subghz/helpers/subghz_gps.c b/applications/main/subghz/helpers/subghz_gps.c new file mode 100644 index 000000000..77b0db5a5 --- /dev/null +++ b/applications/main/subghz/helpers/subghz_gps.c @@ -0,0 +1,171 @@ +#include "subghz_gps.h" + +static void subghz_gps_uart_parse_nmea(SubGhzGPS* subghz_gps, char* line) { + switch(minmea_sentence_id(line, false)) { + case MINMEA_SENTENCE_RMC: { + struct minmea_sentence_rmc frame; + if(minmea_parse_rmc(&frame, line)) { + subghz_gps->latitude = minmea_tocoord(&frame.latitude); + subghz_gps->longitude = minmea_tocoord(&frame.longitude); + subghz_gps->fix_second = frame.time.seconds; + subghz_gps->fix_minute = frame.time.minutes; + subghz_gps->fix_hour = frame.time.hours; + } + } break; + + case MINMEA_SENTENCE_GGA: { + struct minmea_sentence_gga frame; + if(minmea_parse_gga(&frame, line)) { + subghz_gps->latitude = minmea_tocoord(&frame.latitude); + subghz_gps->longitude = minmea_tocoord(&frame.longitude); + subghz_gps->satellites = frame.satellites_tracked; + subghz_gps->fix_second = frame.time.seconds; + subghz_gps->fix_minute = frame.time.minutes; + subghz_gps->fix_hour = frame.time.hours; + } + } break; + + case MINMEA_SENTENCE_GLL: { + struct minmea_sentence_gll frame; + if(minmea_parse_gll(&frame, line)) { + subghz_gps->latitude = minmea_tocoord(&frame.latitude); + subghz_gps->longitude = minmea_tocoord(&frame.longitude); + subghz_gps->fix_second = frame.time.seconds; + subghz_gps->fix_minute = frame.time.minutes; + subghz_gps->fix_hour = frame.time.hours; + } + } break; + + default: + break; + } +} + +static void subghz_gps_uart_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) { + SubGhzGPS* subghz_gps = (SubGhzGPS*)context; + + if(ev == UartIrqEventRXNE) { + furi_stream_buffer_send(subghz_gps->rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(subghz_gps->thread), WorkerEvtRxDone); + } +} + +static int32_t subghz_gps_uart_worker(void* context) { + SubGhzGPS* subghz_gps = (SubGhzGPS*)context; + + size_t rx_offset = 0; + + while(1) { + uint32_t events = + furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever); + furi_check((events & FuriFlagError) == 0); + + if(events & WorkerEvtStop) { + break; + } + + if(events & WorkerEvtRxDone) { + size_t len = 0; + do { + len = furi_stream_buffer_receive( + subghz_gps->rx_stream, + subghz_gps->rx_buf + rx_offset, + RX_BUF_SIZE - rx_offset, + 0); + + if(len > 0) { + rx_offset += len; + subghz_gps->rx_buf[rx_offset] = '\0'; + + char* current_line = (char*)subghz_gps->rx_buf; + while(true) { + while(*current_line == '\0' && + current_line < (char*)subghz_gps->rx_buf + rx_offset) { + current_line++; + } + + char* next_line = strchr(current_line, '\n'); + if(next_line) { + *next_line = '\0'; + subghz_gps_uart_parse_nmea(subghz_gps, current_line); + current_line = next_line + 1; + } else { + if(current_line > (char*)subghz_gps->rx_buf) { + rx_offset = 0; + while(*current_line) { + subghz_gps->rx_buf[rx_offset++] = *(current_line++); + } + } + break; + } + } + } + } while(len > 0); + } + } + + return 0; +} + +SubGhzGPS* subghz_gps_init() { + SubGhzGPS* subghz_gps = malloc(sizeof(SubGhzGPS)); + + subghz_gps->latitude = NAN; + subghz_gps->longitude = NAN; + subghz_gps->satellites = 0; + + subghz_gps->rx_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); + + subghz_gps->thread = furi_thread_alloc(); + furi_thread_set_name(subghz_gps->thread, "SubGhzGPSWorker"); + furi_thread_set_stack_size(subghz_gps->thread, 1024); + furi_thread_set_context(subghz_gps->thread, subghz_gps); + furi_thread_set_callback(subghz_gps->thread, subghz_gps_uart_worker); + // furi_thread_start(subghz_gps->thread); + + if(UART_CH == FuriHalUartIdUSART1) { + furi_hal_console_disable(); + } else if(UART_CH == FuriHalUartIdLPUART1) { + furi_hal_uart_init(UART_CH, 9600); + } + + furi_hal_uart_set_irq_cb(UART_CH, subghz_gps_uart_on_irq_cb, subghz_gps); + furi_hal_uart_set_br(UART_CH, 9600); + + return subghz_gps; +} + +void subghz_gps_deinit(SubGhzGPS* subghz_gps) { + furi_assert(subghz_gps); + // furi_thread_flags_set(furi_thread_get_id(subghz_gps->thread), WorkerEvtStop); + // furi_thread_join(subghz_gps->thread); + furi_thread_free(subghz_gps->thread); + + free(subghz_gps); + + furi_hal_uart_set_irq_cb(UART_CH, NULL, NULL); + if(UART_CH == FuriHalUartIdLPUART1) { + furi_hal_uart_deinit(UART_CH); + } else { + furi_hal_console_enable(); + } +} + +double subghz_gps_deg2rad(double deg) { + return (deg * (double)M_PI / 180); +} + +double subghz_gps_calc_distance(double lat1d, double lon1d, double lat2d, double lon2d) { + double lat1r, lon1r, lat2r, lon2r, u, v; + lat1r = subghz_gps_deg2rad(lat1d); + lon1r = subghz_gps_deg2rad(lon1d); + lat2r = subghz_gps_deg2rad(lat2d); + lon2r = subghz_gps_deg2rad(lon2d); + u = sin((lat2r - lat1r) / 2); + v = sin((lon2r - lon1r) / 2); + return 2 * 6371 * asin(sqrt(u * u + cos((double)lat1r) * cos((double)lat2r) * v * v)); +} + +double subghz_gps_calc_angle(double lat1, double lon1, double lat2, double lon2) { + return atan2(lat1 - lat2, lon1 - lon2) * 180 / (double)M_PI; +} \ No newline at end of file diff --git a/applications/main/subghz/helpers/subghz_gps.h b/applications/main/subghz/helpers/subghz_gps.h new file mode 100644 index 000000000..b9cb5689b --- /dev/null +++ b/applications/main/subghz/helpers/subghz_gps.h @@ -0,0 +1,65 @@ +#include +#include +#include "minmea.h" + +#define UART_CH \ + (XTREME_SETTINGS()->uart_nmea_channel == UARTDefault ? FuriHalUartIdUSART1 : \ + FuriHalUartIdLPUART1) + +#define RX_BUF_SIZE 1024 + +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone) + +typedef struct { + FuriThread* thread; + FuriStreamBuffer* rx_stream; + uint8_t rx_buf[RX_BUF_SIZE]; + + FuriTimer* timer; + + float latitude; + float longitude; + int satellites; + uint8_t fix_second; + uint8_t fix_minute; + uint8_t fix_hour; +} SubGhzGPS; + +SubGhzGPS* subghz_gps_init(); + +void subghz_gps_deinit(SubGhzGPS* subghz_gps); + +/** + * Convert degree to radian + * + * @param deg Degree + * @return double Radian + */ +double subghz_gps_deg2rad(double deg); + +/** + * Calculate distance between two coordinates + * + * @param lat1d Latitude 1 + * @param lon1d Longitude 1 + * @param lat2d Latitude 2 + * @param lon2d Longitude 2 + * @return double Distance in km +*/ +double subghz_gps_calc_distance(double lat1d, double lon1d, double lat2d, double lon2d); + +/** + * Calculate angle between two coordinates + * + * @param lat1 Latitude 1 + * @param lon1 Longitude 1 + * @param lat2 Latitude 2 + * @param lon2 Longitude 2 + * @return double Angle in degree +*/ +double subghz_gps_calc_angle(double lat1, double lon1, double lat2, double lon2); \ No newline at end of file diff --git a/applications/main/subghz/helpers/subghz_txrx.c b/applications/main/subghz/helpers/subghz_txrx.c index e173eab3c..583138e51 100644 --- a/applications/main/subghz/helpers/subghz_txrx.c +++ b/applications/main/subghz/helpers/subghz_txrx.c @@ -97,12 +97,16 @@ void subghz_txrx_set_preset( SubGhzTxRx* instance, const char* preset_name, uint32_t frequency, + float latitude, + float longitude, uint8_t* preset_data, size_t preset_data_size) { furi_assert(instance); furi_string_set(instance->preset->name, preset_name); SubGhzRadioPreset* preset = instance->preset; preset->frequency = frequency; + preset->latitude = latitude; + preset->longitude = longitude; preset->data = preset_data; preset->data_size = preset_data_size; } @@ -154,6 +158,20 @@ void subghz_txrx_get_frequency_and_modulation( } } +void subghz_txrx_get_latitude_and_longitude( + SubGhzTxRx* instance, + FuriString* latitude, + FuriString* longitude) { + furi_assert(instance); + SubGhzRadioPreset* preset = instance->preset; + if(latitude != NULL) { + furi_string_printf(latitude, "%f", (double)preset->latitude); + } + if(longitude != NULL) { + furi_string_printf(longitude, "%f", (double)preset->longitude); + } +} + static void subghz_txrx_begin(SubGhzTxRx* instance, uint8_t* preset_data) { furi_assert(instance); subghz_devices_reset(instance->radio_device); @@ -680,7 +698,7 @@ void subghz_txrx_set_default_preset(SubGhzTxRx* instance, uint32_t frequency) { if(frequency == 0) { frequency = subghz_setting_get_default_frequency(subghz_txrx_get_setting(instance)); } - subghz_txrx_set_preset(instance, default_modulation, frequency, NULL, 0); + subghz_txrx_set_preset(instance, default_modulation, frequency, 0, 0, NULL, 0); } const char* @@ -695,6 +713,8 @@ const char* instance, preset_name, frequency, + 0, + 0, subghz_setting_get_preset_data(setting, index), subghz_setting_get_preset_data_size(setting, index)); diff --git a/applications/main/subghz/helpers/subghz_txrx.h b/applications/main/subghz/helpers/subghz_txrx.h index a0f9f3055..fda684378 100644 --- a/applications/main/subghz/helpers/subghz_txrx.h +++ b/applications/main/subghz/helpers/subghz_txrx.h @@ -47,6 +47,8 @@ bool subghz_txrx_is_database_loaded(SubGhzTxRx* instance); * @param instance Pointer to a SubGhzTxRx * @param preset_name Name of preset * @param frequency Frequency in Hz + * @param latitude Latitude in float + * @param longitude Longitude in float * @param preset_data Data of preset * @param preset_data_size Size of preset data */ @@ -54,6 +56,8 @@ void subghz_txrx_set_preset( SubGhzTxRx* instance, const char* preset_name, uint32_t frequency, + float latitude, + float longitude, uint8_t* preset_data, size_t preset_data_size); @@ -87,6 +91,18 @@ void subghz_txrx_get_frequency_and_modulation( FuriString* modulation, bool long_name); +/** + * Get string latitude and longitude + * + * @param instance Pointer to a SubGhzTxRx + * @param latitude Pointer to a string latitude + * @param longitude Pointer to a string longitude +*/ +void subghz_txrx_get_latitude_and_longitude( + SubGhzTxRx* instance, + FuriString* latitude, + FuriString* longitude); + /** * Start TX CC1101 * diff --git a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c index 040aff46c..680935fbb 100644 --- a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c +++ b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c @@ -25,7 +25,7 @@ bool subghz_txrx_gen_data_protocol( bool res = false; - subghz_txrx_set_preset(instance, preset_name, frequency, NULL, 0); + subghz_txrx_set_preset(instance, preset_name, frequency, 0, 0, NULL, 0); instance->decoder_result = subghz_receiver_search_decoder_base_by_name(instance->receiver, protocol_name); @@ -98,7 +98,7 @@ bool subghz_txrx_gen_keeloq_protocol( //TODO lead to a general appearance instance->transmitter = subghz_transmitter_alloc_init(instance->environment, SUBGHZ_PROTOCOL_KEELOQ_NAME); - subghz_txrx_set_preset(instance, preset_name, frequency, NULL, 0); + subghz_txrx_set_preset(instance, preset_name, frequency, 0, 0, NULL, 0); if(instance->transmitter && subghz_protocol_keeloq_create_data( @@ -131,7 +131,7 @@ bool subghz_txrx_gen_keeloq_bft_protocol( txrx->transmitter = subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_KEELOQ_NAME); - subghz_txrx_set_preset(txrx, preset_name, frequency, NULL, 0); + subghz_txrx_set_preset(txrx, preset_name, frequency, 0, 0, NULL, 0); if(txrx->transmitter && subghz_protocol_keeloq_bft_create_data( subghz_transmitter_get_protocol_instance(txrx->transmitter), @@ -175,7 +175,7 @@ bool subghz_txrx_gen_nice_flor_s_protocol( txrx->transmitter = subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_NICE_FLOR_S_NAME); - subghz_txrx_set_preset(txrx, preset_name, frequency, NULL, 0); + subghz_txrx_set_preset(txrx, preset_name, frequency, 0, 0, NULL, 0); if(txrx->transmitter && subghz_protocol_nice_flor_s_create_data( subghz_transmitter_get_protocol_instance(txrx->transmitter), @@ -208,7 +208,7 @@ bool subghz_txrx_gen_faac_slh_protocol( txrx->transmitter = subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_FAAC_SLH_NAME); - subghz_txrx_set_preset(txrx, preset_name, frequency, NULL, 0); + subghz_txrx_set_preset(txrx, preset_name, frequency, 0, 0, NULL, 0); if(txrx->transmitter && subghz_protocol_faac_slh_create_data( subghz_transmitter_get_protocol_instance(txrx->transmitter), @@ -251,7 +251,7 @@ bool subghz_txrx_gen_alutech_at_4n_protocol( txrx->transmitter = subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_ALUTECH_AT_4N_NAME); - subghz_txrx_set_preset(txrx, preset_name, frequency, NULL, 0); + subghz_txrx_set_preset(txrx, preset_name, frequency, 0, 0, NULL, 0); if(txrx->transmitter && subghz_protocol_alutech_at_4n_create_data( subghz_transmitter_get_protocol_instance(txrx->transmitter), @@ -280,7 +280,7 @@ bool subghz_txrx_gen_came_atomo_protocol( txrx->transmitter = subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_CAME_ATOMO_NAME); - subghz_txrx_set_preset(txrx, preset_name, frequency, NULL, 0); + subghz_txrx_set_preset(txrx, preset_name, frequency, 0, 0, NULL, 0); if(txrx->transmitter && subghz_protocol_came_atomo_create_data( subghz_transmitter_get_protocol_instance(txrx->transmitter), @@ -309,7 +309,7 @@ bool subghz_txrx_gen_somfy_telis_protocol( txrx->transmitter = subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_SOMFY_TELIS_NAME); - subghz_txrx_set_preset(txrx, preset_name, frequency, NULL, 0); + subghz_txrx_set_preset(txrx, preset_name, frequency, 0, 0, NULL, 0); if(txrx->transmitter && subghz_protocol_somfy_telis_create_data( subghz_transmitter_get_protocol_instance(txrx->transmitter), @@ -338,7 +338,7 @@ bool subghz_txrx_gen_secplus_v2_protocol( bool ret = false; instance->transmitter = subghz_transmitter_alloc_init(instance->environment, SUBGHZ_PROTOCOL_SECPLUS_V2_NAME); - subghz_txrx_set_preset(instance, name_preset, frequency, NULL, 0); + subghz_txrx_set_preset(instance, name_preset, frequency, 0, 0, NULL, 0); if(instance->transmitter) { subghz_protocol_secplus_v2_create_data( subghz_transmitter_get_protocol_instance(instance->transmitter), diff --git a/applications/main/subghz/scenes/subghz_scene_config.h b/applications/main/subghz/scenes/subghz_scene_config.h index b12a66f98..e475c87f1 100644 --- a/applications/main/subghz/scenes/subghz_scene_config.h +++ b/applications/main/subghz/scenes/subghz_scene_config.h @@ -23,4 +23,6 @@ ADD_SCENE(subghz, more_raw, MoreRAW) ADD_SCENE(subghz, decode_raw, DecodeRAW) ADD_SCENE(subghz, delete_raw, DeleteRAW) ADD_SCENE(subghz, need_saving, NeedSaving) -ADD_SCENE(subghz, rpc, Rpc) \ No newline at end of file +ADD_SCENE(subghz, rpc, Rpc) +ADD_SCENE(subghz, show_gps, ShowGps) +ADD_SCENE(subghz, saved_show_gps, SavedShowGps) \ No newline at end of file diff --git a/applications/main/subghz/scenes/subghz_scene_decode_raw.c b/applications/main/subghz/scenes/subghz_scene_decode_raw.c index 85a524117..8df85de48 100644 --- a/applications/main/subghz/scenes/subghz_scene_decode_raw.c +++ b/applications/main/subghz/scenes/subghz_scene_decode_raw.c @@ -6,7 +6,8 @@ static void subghz_scene_receiver_update_statusbar(void* context) { SubGhz* subghz = context; FuriString* history_stat_str = furi_string_alloc(); - if(!subghz_history_get_text_space_left(subghz->history, history_stat_str)) { + if(!subghz_history_get_text_space_left( + subghz->history, history_stat_str, subghz->gps->satellites)) { FuriString* frequency_str = furi_string_alloc(); FuriString* modulation_str = furi_string_alloc(); @@ -51,6 +52,8 @@ static void subghz_scene_add_to_history_callback( FuriString* item_time = furi_string_alloc(); uint16_t idx = subghz_history_get_item(subghz->history); SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx); + preset.latitude = subghz->gps->latitude; + preset.longitude = subghz->gps->longitude; if(subghz_history_add_to_history(subghz->history, decoder_base, &preset)) { furi_string_reset(item_name); diff --git a/applications/main/subghz/scenes/subghz_scene_need_saving.c b/applications/main/subghz/scenes/subghz_scene_need_saving.c index 8259b6e82..9a2d047ba 100644 --- a/applications/main/subghz/scenes/subghz_scene_need_saving.c +++ b/applications/main/subghz/scenes/subghz_scene_need_saving.c @@ -51,7 +51,7 @@ bool subghz_scene_need_saving_on_event(void* context, SceneManagerEvent event) { if(state == SubGhzRxKeyStateExit) { subghz_txrx_set_preset( - subghz->txrx, "AM650", subghz->last_settings->frequency, NULL, 0); + subghz->txrx, "AM650", subghz->last_settings->frequency, 0, 0, NULL, 0); scene_manager_search_and_switch_to_previous_scene( subghz->scene_manager, SubGhzSceneStart); } else { diff --git a/applications/main/subghz/scenes/subghz_scene_radio_settings.c b/applications/main/subghz/scenes/subghz_scene_radio_settings.c index 969c7c1a9..95514a1c8 100644 --- a/applications/main/subghz/scenes/subghz_scene_radio_settings.c +++ b/applications/main/subghz/scenes/subghz_scene_radio_settings.c @@ -32,6 +32,12 @@ const char* const debug_pin_text[DEBUG_P_COUNT] = { "17(1W)", }; +#define GPS_COUNT 2 +const char* const gps_text[GPS_COUNT] = { + "OFF", + "ON", +}; + #define DEBUG_COUNTER_COUNT 13 const char* const debug_counter_text[DEBUG_COUNTER_COUNT] = { "+1", @@ -115,6 +121,17 @@ static void subghz_scene_reciever_config_set_ext_mod_power_amp_text(VariableItem } } +static void subghz_scene_receiver_config_set_gps(VariableItem* item) { + SubGhz* subghz = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, gps_text[index]); + + subghz->last_settings->gps_enabled = index == 1; + subghz_last_settings_save( + subghz->last_settings); //TODO, make it to choose baudrate. now it is 9600 +} + static void subghz_scene_receiver_config_set_timestamp_file_names(VariableItem* item) { SubGhz* subghz = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -157,6 +174,12 @@ void subghz_scene_radio_settings_on_enter(void* context) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, ext_mod_power_amp_text[value_index]); + item = variable_item_list_add( + variable_item_list, "GPS", GPS_COUNT, subghz_scene_receiver_config_set_gps, subghz); + value_index = subghz->last_settings->gps_enabled ? 1 : 0; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, gps_text[value_index]); + item = variable_item_list_add( variable_item_list, "Time In Names", diff --git a/applications/main/subghz/scenes/subghz_scene_receiver.c b/applications/main/subghz/scenes/subghz_scene_receiver.c index 9dc48965d..f978d9869 100644 --- a/applications/main/subghz/scenes/subghz_scene_receiver.c +++ b/applications/main/subghz/scenes/subghz_scene_receiver.c @@ -39,7 +39,8 @@ const NotificationSequence subghz_sequence_rx_locked = { static void subghz_scene_receiver_update_statusbar(void* context) { SubGhz* subghz = context; FuriString* history_stat_str = furi_string_alloc(); - if(!subghz_history_get_text_space_left(subghz->history, history_stat_str)) { + if(!subghz_history_get_text_space_left( + subghz->history, history_stat_str, subghz->gps->satellites)) { FuriString* frequency_str = furi_string_alloc(); FuriString* modulation_str = furi_string_alloc(); @@ -112,6 +113,9 @@ static void subghz_scene_add_to_history_callback( uint16_t idx = subghz_history_get_item(history); SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx); + preset.latitude = subghz->gps->latitude; + preset.longitude = subghz->gps->longitude; + if(subghz_history_add_to_history(history, decoder_base, &preset)) { furi_string_reset(item_name); furi_string_reset(item_time); @@ -127,7 +131,7 @@ static void subghz_scene_add_to_history_callback( subghz_history_get_type_protocol(history, idx)); subghz_scene_receiver_update_statusbar(subghz); - if(subghz_history_get_text_space_left(subghz->history, NULL)) { + if(subghz_history_get_text_space_left(subghz->history, NULL, 0)) { notification_message(subghz->notifications, &sequence_error); } } @@ -144,6 +148,10 @@ void subghz_scene_receiver_on_enter(void* context) { SubGhz* subghz = context; SubGhzHistory* history = subghz->history; + if(subghz->last_settings->gps_enabled) { + furi_thread_start(subghz->gps->thread); + } + FuriString* item_name = furi_string_alloc(); FuriString* item_time = furi_string_alloc(); @@ -188,7 +196,7 @@ void subghz_scene_receiver_on_enter(void* context) { subghz->subghz_receiver, subghz_scene_receiver_callback, subghz); subghz_txrx_set_rx_callback(subghz->txrx, subghz_scene_add_to_history_callback, subghz); - if(!subghz_history_get_text_space_left(subghz->history, NULL)) { + if(!subghz_history_get_text_space_left(subghz->history, NULL, 0)) { subghz->state_notifications = SubGhzNotificationStateRx; } @@ -286,13 +294,35 @@ bool subghz_scene_receiver_on_event(void* context, SceneManagerEvent event) { SubGhzThresholdRssiData ret_rssi = subghz_threshold_get_rssi_data( subghz->threshold_rssi, subghz_txrx_radio_device_get_rssi(subghz->txrx)); + if(subghz->last_settings->gps_enabled) { + FuriHalRtcDateTime datetime; + furi_hal_rtc_get_datetime(&datetime); + if((datetime.second - subghz->gps->fix_second) > 15) { + subghz->gps->latitude = NAN; + subghz->gps->longitude = NAN; + subghz->gps->satellites = 0; + subghz->gps->fix_hour = 0; + subghz->gps->fix_minute = 0; + subghz->gps->fix_second = 0; + } + subghz_scene_receiver_update_statusbar(subghz); + } + subghz_receiver_rssi(subghz->subghz_receiver, ret_rssi.rssi); subghz_protocol_decoder_bin_raw_data_input_rssi( (SubGhzProtocolDecoderBinRAW*)subghz_txrx_get_decoder(subghz->txrx), ret_rssi.rssi); switch(subghz->state_notifications) { case SubGhzNotificationStateRx: - notification_message(subghz->notifications, &sequence_blink_cyan_10); + if(subghz->last_settings->gps_enabled) { + if(subghz->gps->satellites > 0) { + notification_message(subghz->notifications, &sequence_blink_green_10); + } else { + notification_message(subghz->notifications, &sequence_blink_red_10); + } + } else { + notification_message(subghz->notifications, &sequence_blink_cyan_10); + } break; case SubGhzNotificationStateRxDone: if(!subghz_is_locked(subghz)) { @@ -310,5 +340,9 @@ bool subghz_scene_receiver_on_event(void* context, SceneManagerEvent event) { } void subghz_scene_receiver_on_exit(void* context) { - UNUSED(context); + SubGhz* subghz = context; + if(subghz->last_settings->gps_enabled) { + furi_thread_flags_set(furi_thread_get_id(subghz->gps->thread), WorkerEvtStop); + furi_thread_join(subghz->gps->thread); + } } diff --git a/applications/main/subghz/scenes/subghz_scene_receiver_config.c b/applications/main/subghz/scenes/subghz_scene_receiver_config.c index d09b63c4e..a6d8edab1 100644 --- a/applications/main/subghz/scenes/subghz_scene_receiver_config.c +++ b/applications/main/subghz/scenes/subghz_scene_receiver_config.c @@ -155,6 +155,8 @@ static void subghz_scene_receiver_config_set_frequency(VariableItem* item) { subghz->txrx, furi_string_get_cstr(preset.name), frequency, + 0, + 0, preset.data, preset.data_size); @@ -181,6 +183,8 @@ static void subghz_scene_receiver_config_set_preset(VariableItem* item) { subghz->txrx, preset_name, preset.frequency, + 0, + 0, subghz_setting_get_preset_data(setting, index), subghz_setting_get_preset_data_size(setting, index)); subghz->last_settings->preset_index = index; @@ -213,6 +217,8 @@ static void subghz_scene_receiver_config_set_hopping_running(VariableItem* item) subghz->txrx, furi_string_get_cstr(preset.name), frequency, + 0, + 0, preset.data, preset.data_size); variable_item_set_current_value_index( diff --git a/applications/main/subghz/scenes/subghz_scene_receiver_info.c b/applications/main/subghz/scenes/subghz_scene_receiver_info.c index cd4f0f836..739005861 100644 --- a/applications/main/subghz/scenes/subghz_scene_receiver_info.c +++ b/applications/main/subghz/scenes/subghz_scene_receiver_info.c @@ -17,6 +17,11 @@ void subghz_scene_receiver_info_callback(GuiButtonType result, InputType type, v } else if((result == GuiButtonTypeRight) && (type == InputTypeShort)) { view_dispatcher_send_custom_event( subghz->view_dispatcher, SubGhzCustomEventSceneReceiverInfoSave); + } else if( + (result == GuiButtonTypeLeft) && (type == InputTypeShort) && + subghz->last_settings->gps_enabled) { + view_dispatcher_send_custom_event( + subghz->view_dispatcher, SubGhzCustomEventSceneReceiverInfoSats); } } @@ -37,6 +42,8 @@ static bool subghz_scene_receiver_info_update_parser(void* context) { subghz->txrx, furi_string_get_cstr(preset->name), preset->frequency, + 0, + 0, preset->data, preset->data_size); @@ -74,6 +81,15 @@ void subghz_scene_receiver_info_draw_widget(SubGhz* subghz) { widget_add_string_multiline_element( subghz->widget, 0, 0, AlignLeft, AlignTop, FontSecondary, furi_string_get_cstr(text)); + if(subghz->last_settings->gps_enabled) { + widget_add_button_element( + subghz->widget, + GuiButtonTypeLeft, + "Geo", + subghz_scene_receiver_info_callback, + subghz); + } + furi_string_free(frequency_str); furi_string_free(modulation_str); furi_string_free(text); @@ -111,7 +127,7 @@ void subghz_scene_receiver_info_on_enter(void* context) { subghz_scene_receiver_info_draw_widget(subghz); - if(!subghz_history_get_text_space_left(subghz->history, NULL)) { + if(!subghz_history_get_text_space_left(subghz->history, NULL, 0)) { subghz->state_notifications = SubGhzNotificationStateRx; } } @@ -147,7 +163,7 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event) subghz_txrx_rx_start(subghz->txrx); subghz_txrx_hopper_unpause(subghz->txrx); - if(!subghz_history_get_text_space_left(subghz->history, NULL)) { + if(!subghz_history_get_text_space_left(subghz->history, NULL, 0)) { subghz->state_notifications = SubGhzNotificationStateRx; } } @@ -168,6 +184,13 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event) scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName); } return true; + } else if(event.event == SubGhzCustomEventSceneReceiverInfoSats) { + if(subghz->last_settings->gps_enabled) { + scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowGps); + return true; + } else { + return false; + } } } else if(event.type == SceneManagerEventTypeTick) { if(subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF) { @@ -178,7 +201,15 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event) notification_message(subghz->notifications, &sequence_blink_magenta_10); break; case SubGhzNotificationStateRx: - notification_message(subghz->notifications, &sequence_blink_cyan_10); + if(subghz->last_settings->gps_enabled) { + if(subghz->gps->satellites > 0) { + notification_message(subghz->notifications, &sequence_blink_green_10); + } else { + notification_message(subghz->notifications, &sequence_blink_red_10); + } + } else { + notification_message(subghz->notifications, &sequence_blink_cyan_10); + } break; case SubGhzNotificationStateRxDone: notification_message(subghz->notifications, &sequence_blink_green_100); diff --git a/applications/main/subghz/scenes/subghz_scene_saved_menu.c b/applications/main/subghz/scenes/subghz_scene_saved_menu.c index a65830f4b..5dcd1c61d 100644 --- a/applications/main/subghz/scenes/subghz_scene_saved_menu.c +++ b/applications/main/subghz/scenes/subghz_scene_saved_menu.c @@ -4,6 +4,7 @@ enum SubmenuIndex { SubmenuIndexEmulate, SubmenuIndexEdit, SubmenuIndexDelete, + SubmenuIndexGeo, }; void subghz_scene_saved_menu_submenu_callback(void* context, uint32_t index) { @@ -34,6 +35,24 @@ void subghz_scene_saved_menu_on_enter(void* context) { subghz_scene_saved_menu_submenu_callback, subghz); + FuriString* lat_str = furi_string_alloc(); + FuriString* lon_str = furi_string_alloc(); + + subghz_txrx_get_latitude_and_longitude(subghz->txrx, lat_str, lon_str); + + if(strcmp(furi_string_get_cstr(lat_str), "nan") != 0 && + strcmp(furi_string_get_cstr(lon_str), "nan") != 0) { + submenu_add_item( + subghz->submenu, + "Geographic info", + SubmenuIndexGeo, + subghz_scene_saved_menu_submenu_callback, + subghz); + } + + furi_string_free(lon_str); + furi_string_free(lat_str); + submenu_set_selected_item( subghz->submenu, scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneSavedMenu)); @@ -60,6 +79,11 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) { subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEdit); scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName); return true; + } else if(event.event == SubmenuIndexGeo) { + scene_manager_set_scene_state( + subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexGeo); + scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSavedShowGps); + return true; } } return false; diff --git a/applications/main/subghz/scenes/subghz_scene_saved_show_gps.c b/applications/main/subghz/scenes/subghz_scene_saved_show_gps.c new file mode 100644 index 000000000..ddef62293 --- /dev/null +++ b/applications/main/subghz/scenes/subghz_scene_saved_show_gps.c @@ -0,0 +1,110 @@ +#include "../subghz_i.h" +#include "../helpers/subghz_custom_event.h" + +void subghz_scene_saved_show_gps_draw_satellites(void* context) { + SubGhz* subghz = context; + + FuriString* text_str = furi_string_alloc(); + FuriString* lat_str = furi_string_alloc(); + FuriString* lon_str = furi_string_alloc(); + + subghz_txrx_get_latitude_and_longitude(subghz->txrx, lat_str, lon_str); + + FuriHalRtcDateTime datetime; + furi_hal_rtc_get_datetime(&datetime); + if((datetime.second - subghz->gps->fix_second) > 15) { + subghz->gps->latitude = NAN; + subghz->gps->longitude = NAN; + subghz->gps->satellites = 0; + subghz->gps->fix_hour = 0; + subghz->gps->fix_minute = 0; + subghz->gps->fix_second = 0; + } + + double latitude = atof(furi_string_get_cstr(lat_str)); + double longitude = atof(furi_string_get_cstr(lon_str)); + + double distance = subghz_gps_calc_distance( + latitude, longitude, (double)subghz->gps->latitude, (double)subghz->gps->longitude); + + float angle = subghz_gps_calc_angle( + latitude, longitude, (double)subghz->gps->latitude, (double)subghz->gps->longitude); + + char* angle_str = "N"; + if(angle > 22.5 && angle <= 67.5) { + angle_str = "NE"; + } else if(angle > 67.5 && angle <= 112.5) { + angle_str = "E"; + } else if(angle > 112.5 && angle <= 157.5) { + angle_str = "SE"; + } else if(angle > 157.5 && angle <= 202.5) { + angle_str = "S"; + } else if(angle > 202.5 && angle <= 247.5) { + angle_str = "SW"; + } else if(angle > 247.5 && angle <= 292.5) { + angle_str = "W"; + } else if(angle > 292.5 && angle <= 337.5) { + angle_str = "NW"; + } + + furi_string_printf( + text_str, + "Captured at: %f,\r\n%f\r\n\r\nRealtime: Sats: %d\r\nDistance: %.2f%s Dir: %s\r\nGPS time: %d:%d:%d UTC", + latitude, + longitude, + subghz->gps->satellites, + subghz->gps->satellites > 0 ? distance > 1 ? distance : distance * 1000 : 0, + distance > 1 ? "km" : "m", + angle_str, + subghz->gps->fix_hour, + subghz->gps->fix_minute, + subghz->gps->fix_second); + + widget_add_text_scroll_element(subghz->widget, 0, 0, 128, 64, furi_string_get_cstr(text_str)); + + furi_string_free(text_str); + furi_string_free(lat_str); + furi_string_free(lon_str); +} + +void subghz_scene_saved_show_gps_refresh_screen(void* context) { + SubGhz* subghz = context; + widget_reset(subghz->widget); + subghz_scene_saved_show_gps_draw_satellites(subghz); +} + +void subghz_scene_saved_show_gps_on_enter(void* context) { + SubGhz* subghz = context; + + if(subghz->last_settings->gps_enabled) { + furi_thread_start(subghz->gps->thread); + } + + subghz_scene_saved_show_gps_draw_satellites(subghz); + + subghz->gps->timer = furi_timer_alloc( + subghz_scene_saved_show_gps_refresh_screen, FuriTimerTypePeriodic, subghz); + furi_timer_start(subghz->gps->timer, 1000); + + view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdWidget); +} + +bool subghz_scene_saved_show_gps_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void subghz_scene_saved_show_gps_on_exit(void* context) { + SubGhz* subghz = context; + + if(subghz->last_settings->gps_enabled) { + furi_thread_flags_set(furi_thread_get_id(subghz->gps->thread), WorkerEvtStop); + furi_thread_join(subghz->gps->thread); + } + + furi_timer_stop(subghz->gps->timer); + furi_timer_free(subghz->gps->timer); + + widget_reset(subghz->widget); +} \ No newline at end of file diff --git a/applications/main/subghz/scenes/subghz_scene_show_gps.c b/applications/main/subghz/scenes/subghz_scene_show_gps.c new file mode 100644 index 000000000..03d4fe191 --- /dev/null +++ b/applications/main/subghz/scenes/subghz_scene_show_gps.c @@ -0,0 +1,143 @@ +#include "../subghz_i.h" +#include "../helpers/subghz_custom_event.h" + +void subghz_scene_show_gps_draw_satellites(void* context) { + SubGhz* subghz = context; + + FuriString* text = furi_string_alloc(); + FuriString* time_text = furi_string_alloc(); + + FuriHalRtcDateTime datetime; + furi_hal_rtc_get_datetime(&datetime); + if((datetime.second - subghz->gps->fix_second) > 15) { + subghz->gps->latitude = NAN; + subghz->gps->longitude = NAN; + subghz->gps->satellites = 0; + subghz->gps->fix_hour = 0; + subghz->gps->fix_minute = 0; + subghz->gps->fix_second = 0; + } + + subghz_history_get_time_item_menu(subghz->history, time_text, subghz->idx_menu_chosen); + + double distance = subghz_gps_calc_distance( + (double)subghz_history_get_latitude(subghz->history, subghz->idx_menu_chosen), + (double)subghz_history_get_longitude(subghz->history, subghz->idx_menu_chosen), + (double)subghz->gps->latitude, + (double)subghz->gps->longitude); + + float angle = subghz_gps_calc_angle( + (double)subghz_history_get_latitude(subghz->history, subghz->idx_menu_chosen), + (double)subghz_history_get_longitude(subghz->history, subghz->idx_menu_chosen), + (double)subghz->gps->latitude, + (double)subghz->gps->longitude); + + char* angle_str = "N"; + if(angle > 22.5 && angle <= 67.5) { + angle_str = "NE"; + } else if(angle > 67.5 && angle <= 112.5) { + angle_str = "E"; + } else if(angle > 112.5 && angle <= 157.5) { + angle_str = "SE"; + } else if(angle > 157.5 && angle <= 202.5) { + angle_str = "S"; + } else if(angle > 202.5 && angle <= 247.5) { + angle_str = "SW"; + } else if(angle > 247.5 && angle <= 292.5) { + angle_str = "W"; + } else if(angle > 292.5 && angle <= 337.5) { + angle_str = "NW"; + } + + FuriString* lat_str = furi_string_alloc(); + FuriString* lon_str = furi_string_alloc(); + + if(isnan(subghz->gps->latitude)) { + furi_string_printf(lat_str, "N/A"); + } else { + furi_string_printf(lat_str, "%f", (double)subghz->gps->latitude); + } + + if(isnan(subghz->gps->longitude)) { + furi_string_printf(lon_str, "N/A"); + } else { + furi_string_printf(lon_str, "%f", (double)subghz->gps->longitude); + } + + furi_string_printf( + text, + // "Captured at: %f,\r\n%f Time: %s\r\n\r\nRealtime: Sats: %d\r\nDistance: %.2f%s Dir: %s\r\nGPS time: %d:%d:%d UTC", + // (double)subghz_history_get_latitude(subghz->history, subghz->idx_menu_chosen), + // (double)subghz_history_get_longitude(subghz->history, subghz->idx_menu_chosen), + "Captured at: %s,\r\n%s Time: %s\r\n\r\nRealtime: Sats: %d\r\nDistance: %.2f%s Dir: %s\r\nGPS time: %d:%d:%d UTC", + furi_string_get_cstr(lat_str), + furi_string_get_cstr(lon_str), + furi_string_get_cstr(time_text), + subghz->gps->satellites, + subghz->gps->satellites > 0 ? distance > 1 ? distance : distance * 1000 : 0, + distance > 1 ? "km" : "m", + angle_str, + subghz->gps->fix_hour, + subghz->gps->fix_minute, + subghz->gps->fix_second); + + widget_add_text_scroll_element(subghz->widget, 0, 0, 128, 64, furi_string_get_cstr(text)); + + furi_string_free(lat_str); + furi_string_free(lon_str); + + furi_string_free(text); + furi_string_free(time_text); +} + +void subghz_scene_show_gps_refresh_screen(void* context) { + SubGhz* subghz = context; + widget_reset(subghz->widget); + subghz_scene_show_gps_draw_satellites(subghz); +} + +void subghz_scene_show_gps_on_enter(void* context) { + SubGhz* subghz = context; + + if(subghz->last_settings->gps_enabled) { + furi_thread_start(subghz->gps->thread); + } + + subghz_scene_show_gps_draw_satellites(subghz); + + subghz->gps->timer = + furi_timer_alloc(subghz_scene_show_gps_refresh_screen, FuriTimerTypePeriodic, subghz); + furi_timer_start(subghz->gps->timer, 1000); +} + +bool subghz_scene_show_gps_on_event(void* context, SceneManagerEvent event) { + SubGhz* subghz = context; + if(event.type == SceneManagerEventTypeTick) { + if(subghz->state_notifications == SubGhzNotificationStateRx) { + if(subghz->last_settings->gps_enabled) { + if(subghz->gps->satellites > 0) { + notification_message(subghz->notifications, &sequence_blink_green_10); + } else { + notification_message(subghz->notifications, &sequence_blink_red_10); + } + } else { + notification_message(subghz->notifications, &sequence_blink_cyan_10); + } + } + } + return false; +} + +void subghz_scene_show_gps_on_exit(void* context) { + SubGhz* subghz = context; + + if(subghz->last_settings->gps_enabled) { + furi_thread_flags_set(furi_thread_get_id(subghz->gps->thread), WorkerEvtStop); + furi_thread_join(subghz->gps->thread); + } + + furi_timer_stop(subghz->gps->timer); + furi_timer_free(subghz->gps->timer); + + widget_reset(subghz->widget); +} \ No newline at end of file diff --git a/applications/main/subghz/subghz.c b/applications/main/subghz/subghz.c index 3cbb648d3..401dc4a65 100644 --- a/applications/main/subghz/subghz.c +++ b/applications/main/subghz/subghz.c @@ -244,6 +244,8 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) { //Init Error_str subghz->error_str = furi_string_alloc(); + subghz->gps = subghz_gps_init(); + return subghz; } @@ -340,6 +342,8 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) { furi_string_free(subghz->file_path); furi_string_free(subghz->file_path_tmp); + subghz_gps_deinit(subghz->gps); + // The rest free(subghz); } diff --git a/applications/main/subghz/subghz_history.c b/applications/main/subghz/subghz_history.c index 396e28421..f50e14500 100644 --- a/applications/main/subghz/subghz_history.c +++ b/applications/main/subghz/subghz_history.c @@ -13,6 +13,8 @@ typedef struct { uint8_t type; SubGhzRadioPreset* preset; FuriHalRtcDateTime datetime; + float latitude; + float longitude; } SubGhzHistoryItem; ARRAY_DEF(SubGhzHistoryItemArray, SubGhzHistoryItem, M_POD_OPLIST) @@ -73,6 +75,18 @@ const char* subghz_history_get_preset(SubGhzHistory* instance, uint16_t idx) { return furi_string_get_cstr(item->preset->name); } +float subghz_history_get_latitude(SubGhzHistory* instance, uint16_t idx) { + furi_assert(instance); + SubGhzHistoryItem* item = SubGhzHistoryItemArray_get(instance->history->data, idx); + return item->latitude; +} + +float subghz_history_get_longitude(SubGhzHistory* instance, uint16_t idx) { + furi_assert(instance); + SubGhzHistoryItem* item = SubGhzHistoryItemArray_get(instance->history->data, idx); + return item->longitude; +} + void subghz_history_reset(SubGhzHistory* instance) { furi_assert(instance); furi_string_reset(instance->tmp_string); @@ -147,7 +161,7 @@ FlipperFormat* subghz_history_get_raw_data(SubGhzHistory* instance, uint16_t idx return NULL; } } -bool subghz_history_get_text_space_left(SubGhzHistory* instance, FuriString* output) { +bool subghz_history_get_text_space_left(SubGhzHistory* instance, FuriString* output, uint8_t sats) { furi_assert(instance); if(memmgr_get_free_heap() < SUBGHZ_HISTORY_FREE_HEAP) { if(output != NULL) furi_string_printf(output, " Free heap LOW"); @@ -157,8 +171,23 @@ bool subghz_history_get_text_space_left(SubGhzHistory* instance, FuriString* out if(output != NULL) furi_string_printf(output, " Memory is FULL"); return true; } - if(output != NULL) - furi_string_printf(output, "%02u/%02u", instance->last_index_write, SUBGHZ_HISTORY_MAX); + if(output != NULL) { + if(sats == 0) { + furi_string_printf( + output, "%02u/%02u", instance->last_index_write, SUBGHZ_HISTORY_MAX); + return false; + } else { + FuriHalRtcDateTime datetime; + furi_hal_rtc_get_datetime(&datetime); + + if(furi_hal_rtc_datetime_to_timestamp(&datetime) % 2) { + furi_string_printf( + output, "%02u/%02u", instance->last_index_write, SUBGHZ_HISTORY_MAX); + } else { + furi_string_printf(output, "%d sats", sats); + } + } + } return false; } @@ -207,6 +236,8 @@ bool subghz_history_add_to_history( item->preset->data = preset->data; item->preset->data_size = preset->data_size; furi_hal_rtc_get_datetime(&item->datetime); + item->latitude = preset->latitude; + item->longitude = preset->longitude; item->item_str = furi_string_alloc(); item->flipper_string = flipper_format_string_alloc(); diff --git a/applications/main/subghz/subghz_history.h b/applications/main/subghz/subghz_history.h index 1b27d52ad..34d4cbd45 100644 --- a/applications/main/subghz/subghz_history.h +++ b/applications/main/subghz/subghz_history.h @@ -90,9 +90,10 @@ void subghz_history_get_time_item_menu(SubGhzHistory* instance, FuriString* outp * * @param instance - SubGhzHistory instance * @param output - FuriString* output + * @param sats - Number of satellites * @return bool - is FULL */ -bool subghz_history_get_text_space_left(SubGhzHistory* instance, FuriString* output); +bool subghz_history_get_text_space_left(SubGhzHistory* instance, FuriString* output, uint8_t sats); /** Return last index * @@ -120,3 +121,19 @@ bool subghz_history_add_to_history( * @return SubGhzProtocolCommonLoad* */ FlipperFormat* subghz_history_get_raw_data(SubGhzHistory* instance, uint16_t idx); + +/** Get latitude to history[idx] + * + * @param instance - SubGhzHistory instance + * @param idx - Record index + * @return latitude - Float +*/ +float subghz_history_get_latitude(SubGhzHistory* instance, uint16_t idx); + +/** Get longitude to history[idx] + * + * @param instance - SubGhzHistory instance + * @param idx - Record index + * @return longitude - Float +*/ +float subghz_history_get_longitude(SubGhzHistory* instance, uint16_t idx); diff --git a/applications/main/subghz/subghz_i.c b/applications/main/subghz/subghz_i.c index 3af85750a..7baae5ab1 100644 --- a/applications/main/subghz/subghz_i.c +++ b/applications/main/subghz/subghz_i.c @@ -71,6 +71,8 @@ bool subghz_key_load(SubGhz* subghz, const char* file_path, bool show_dialog) { SubGhzLoadKeyState load_key_state = SubGhzLoadKeyStateParseErr; FuriString* temp_str = furi_string_alloc(); uint32_t temp_data32; + float temp_lat = NAN; // NAN or 0.0?? because 0.0 is valid value + float temp_lon = NAN; do { stream_clean(fff_data_stream); @@ -136,12 +138,26 @@ bool subghz_key_load(SubGhz* subghz, const char* file_path, bool show_dialog) { break; } } + + //Load latitute and longitude if present + if(!flipper_format_read_float(fff_data_file, "Latitute", (float*)&temp_lat, 1)) { + FURI_LOG_E(TAG, "Missing Latitude (optional)"); + } + flipper_format_rewind(fff_data_file); + + if(!flipper_format_read_float(fff_data_file, "Longitude", (float*)&temp_lon, 1)) { + FURI_LOG_E(TAG, "Missing Longitude (optional)"); + } + flipper_format_rewind(fff_data_file); + size_t preset_index = subghz_setting_get_inx_preset_by_name(setting, furi_string_get_cstr(temp_str)); subghz_txrx_set_preset( subghz->txrx, furi_string_get_cstr(temp_str), temp_data32, + temp_lat, + temp_lon, subghz_setting_get_preset_data(setting, preset_index), subghz_setting_get_preset_data_size(setting, preset_index)); diff --git a/applications/main/subghz/subghz_i.h b/applications/main/subghz/subghz_i.h index 9a08b48b7..e50d3169e 100644 --- a/applications/main/subghz/subghz_i.h +++ b/applications/main/subghz/subghz_i.h @@ -39,6 +39,7 @@ #include "helpers/subghz_threshold_rssi.h" #include "helpers/subghz_txrx.h" +#include "helpers/subghz_gps.h" #define SUBGHZ_MAX_LEN_NAME 64 #define SUBGHZ_EXT_PRESET_NAME true @@ -93,6 +94,7 @@ struct SubGhz { SubGhzThresholdRssi* threshold_rssi; SubGhzRxKeyState rx_key_state; SubGhzHistory* history; + SubGhzGPS* gps; uint16_t idx_menu_chosen; SubGhzLoadTypeFile load_type_file; diff --git a/applications/main/subghz/subghz_last_settings.c b/applications/main/subghz/subghz_last_settings.c index 2109024ad..74922aa67 100644 --- a/applications/main/subghz/subghz_last_settings.c +++ b/applications/main/subghz/subghz_last_settings.c @@ -15,6 +15,7 @@ #define SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER "ExtPower" #define SUBGHZ_LAST_SETTING_FIELD_TIMESTAMP_FILE_NAMES "TimestampNames" #define SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER_AMP "ExtPowerAmp" +#define SUBGHZ_LAST_SETTING_FIELD_GPS "Gps" #define SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE "Hopping" #define SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER "IgnoreFilter" #define SUBGHZ_LAST_SETTING_FIELD_FILTER "Filter" @@ -55,6 +56,7 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count bool ignore_filter_was_read = false; bool frequency_analyzer_feedback_level_was_read = false; bool frequency_analyzer_trigger_was_read = false; + bool temp_gps_enabled = false; if(FSE_OK == storage_sd_status(storage) && SUBGHZ_LAST_SETTINGS_PATH && flipper_format_file_open_existing(fff_data_file, SUBGHZ_LAST_SETTINGS_PATH)) { @@ -92,6 +94,8 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER_AMP, (bool*)&temp_external_module_power_amp, 1); + flipper_format_read_bool( + fff_data_file, SUBGHZ_LAST_SETTING_FIELD_GPS, (bool*)&temp_gps_enabled, 1); flipper_format_read_bool( fff_data_file, SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE, @@ -121,6 +125,7 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count instance->external_module_enabled = false; instance->timestamp_file_names = false; instance->external_module_power_amp = false; + instance->gps_enabled = false; instance->enable_hopping = false; instance->ignore_filter = 0x00; // See bin_raw_value in applications/main/subghz/scenes/subghz_scene_receiver_config.c @@ -175,6 +180,8 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count #endif // Set globally in furi hal furi_hal_subghz_set_ext_power_amp(instance->external_module_power_amp); + + instance->gps_enabled = temp_gps_enabled; } flipper_format_file_close(fff_data_file); @@ -254,6 +261,10 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) { 1)) { break; } + if(!flipper_format_insert_or_update_bool( + file, SUBGHZ_LAST_SETTING_FIELD_GPS, &instance->gps_enabled, 1)) { + break; + } if(!flipper_format_insert_or_update_bool( file, SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE, &instance->enable_hopping, 1)) { break; diff --git a/applications/main/subghz/subghz_last_settings.h b/applications/main/subghz/subghz_last_settings.h index b3742d4bf..b292f0b06 100644 --- a/applications/main/subghz/subghz_last_settings.h +++ b/applications/main/subghz/subghz_last_settings.h @@ -26,6 +26,7 @@ typedef struct { bool external_module_power_amp; // saved so as not to change the version bool timestamp_file_names; + bool gps_enabled; bool enable_hopping; uint32_t ignore_filter; uint32_t filter; diff --git a/lib/subghz/blocks/generic.c b/lib/subghz/blocks/generic.c index 1827388af..f13e4aa9b 100644 --- a/lib/subghz/blocks/generic.c +++ b/lib/subghz/blocks/generic.c @@ -64,6 +64,16 @@ SubGhzProtocolStatus subghz_block_generic_serialize( break; } } + if(!flipper_format_write_float(flipper_format, "Latitute", &preset->latitude, 1)) { + FURI_LOG_E(TAG, "Unable to add Latitute"); + res = SubGhzProtocolStatusErrorParserLatitude; + break; + } + if(!flipper_format_write_float(flipper_format, "Longitude", &preset->longitude, 1)) { + FURI_LOG_E(TAG, "Unable to add Longitude"); + res = SubGhzProtocolStatusErrorParserLongitude; + break; + } if(!flipper_format_write_string_cstr(flipper_format, "Protocol", instance->protocol_name)) { FURI_LOG_E(TAG, "Unable to add Protocol"); res = SubGhzProtocolStatusErrorParserProtocolName; diff --git a/lib/subghz/blocks/generic.h b/lib/subghz/blocks/generic.h index 10e7b63fa..7d4358917 100644 --- a/lib/subghz/blocks/generic.h +++ b/lib/subghz/blocks/generic.h @@ -17,6 +17,8 @@ typedef struct SubGhzBlockGeneric SubGhzBlockGeneric; struct SubGhzBlockGeneric { const char* protocol_name; + float latitude; + float longitude; uint64_t data; uint64_t data_2; uint32_t serial; diff --git a/lib/subghz/types.h b/lib/subghz/types.h index 8999e1d54..a2e25c8ad 100644 --- a/lib/subghz/types.h +++ b/lib/subghz/types.h @@ -37,6 +37,8 @@ typedef struct { uint32_t frequency; uint8_t* data; size_t data_size; + float latitude; + float longitude; } SubGhzRadioPreset; typedef enum { @@ -59,6 +61,8 @@ typedef enum { SubGhzProtocolStatusErrorEncoderGetUpload = (-12), ///< Payload encoder failure // Special Values SubGhzProtocolStatusErrorProtocolNotFound = (-13), ///< Protocol not found + SubGhzProtocolStatusErrorParserLatitude = (-14), ///< Missing `Latitude` + SubGhzProtocolStatusErrorParserLongitude = (-15), ///< Missing `Longitude` SubGhzProtocolStatusReserved = 0x7FFFFFFF, ///< Prevents enum down-size compiler optimization. } SubGhzProtocolStatus;