From 30cb7c8dd4447448e763f94d237ca39665ddbcfd Mon Sep 17 00:00:00 2001 From: jgromes Date: Sat, 20 Aug 2022 18:14:13 +0200 Subject: [PATCH] [APRS] Added Mic-E (#430) --- examples/APRS/APRS_MicE/APRS_MicE.ino | 111 ++++++++++++++++++ keywords.txt | 9 ++ src/TypeDef.h | 15 +++ src/protocols/APRS/APRS.cpp | 160 ++++++++++++++++++++++++++ src/protocols/APRS/APRS.h | 56 +++++++++ 5 files changed, 351 insertions(+) create mode 100644 examples/APRS/APRS_MicE/APRS_MicE.ino diff --git a/examples/APRS/APRS_MicE/APRS_MicE.ino b/examples/APRS/APRS_MicE/APRS_MicE.ino new file mode 100644 index 000000000..9ddd22873 --- /dev/null +++ b/examples/APRS/APRS_MicE/APRS_MicE.ino @@ -0,0 +1,111 @@ +/* + RadioLib APRS Mic-E Example + + This example sends APRS position reports + encoded in the Mic-E format using SX1278's + FSK modem. The data is modulated as AFSK + at 1200 baud using Bell 202 tones. + + DO NOT transmit in APRS bands unless + you have a ham radio license! + + Other modules that can be used for APRS: + - SX127x/RFM9x + - RF69 + - SX1231 + - CC1101 + - nRF24 + - Si443x/RFM2x + + For default module settings, see the wiki page + https://github.com/jgromes/RadioLib/wiki/Default-configuration + + For full API reference, see the GitHub Pages + https://jgromes.github.io/RadioLib/ +*/ + +// include the library +#include + +// SX1278 has the following connections: +// NSS pin: 10 +// DIO0 pin: 2 +// RESET pin: 9 +// DIO1 pin: 3 +SX1278 radio = new Module(10, 2, 9, 3); + +// or using RadioShield +// https://github.com/jgromes/RadioShield +//SX1278 radio = RadioShield.ModuleA; + +// create AFSK client instance using the FSK module +// pin 5 is connected to SX1278 DIO2 +AFSKClient audio(&radio, 5); + +// create AX.25 client instance using the AFSK instance +AX25Client ax25(&audio); + +// create APRS client isntance using the AX.25 client +APRSClient aprs(&ax25); + +void setup() { + Serial.begin(9600); + + // initialize SX1278 + // NOTE: moved to ISM band on purpose + // DO NOT transmit in APRS bands without ham radio license! + Serial.print(F("[SX1278] Initializing ... ")); + int state = radio.beginFSK(); + + // when using one of the non-LoRa modules for AX.25 + // (RF69, CC1101, Si4432 etc.), use the basic begin() method + // int state = radio.begin(); + + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } + + // initialize AX.25 client + Serial.print(F("[AX.25] Initializing ... ")); + // source station callsign: "N7LEM" + // source station SSID: 0 + // preamble length: 8 bytes + state = ax25.begin("N7LEM"); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } + + // initialize APRS client + Serial.print(F("[APRS] Initializing ... ")); + // symbol: '>' (car) + state = aprs.begin('>'); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } +} + +void loop() { + Serial.print(F("[APRS] Sending Mic-E position ... ")); + int state = aprs.sendMicE(49.1945, 16.6000, 120, 10, RADIOLIB_APRS_MIC_E_TYPE_EN_ROUTE); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + } + + // wait one minute before transmitting again + delay(60000); +} diff --git a/keywords.txt b/keywords.txt index 8c3a0a339..ce8ca0ae1 100644 --- a/keywords.txt +++ b/keywords.txt @@ -150,6 +150,8 @@ randomByte KEYWORD2 getPacketLength KEYWORD2 setFifoEmptyAction KEYWORD2 clearFifoEmptyAction KEYWORD2 +setFifoFullAction KEYWORD2 +clearFifoFullAction KEYWORD2 fifoAdd KEYWORD2 fifoGet KEYWORD2 @@ -234,6 +236,7 @@ noTone KEYWORD2 # APRS sendPosition KEYWORD2 +sendMicE KEYWORD2 ####################################### # Constants (LITERAL1) @@ -291,6 +294,12 @@ RADIOLIB_ERR_INVALID_RX_BANDWIDTH LITERAL1 RADIOLIB_ERR_INVALID_SYNC_WORD LITERAL1 RADIOLIB_ERR_INVALID_DATA_SHAPING LITERAL1 RADIOLIB_ERR_INVALID_MODULATION LITERAL1 +RADIOLIB_ERR_INVALID_OOK_RSSI_PEAK_TYPE LITERAL1 + +RADIOLIB_ERR_INVALID_SYMBOL LITERAL1 +RADIOLIB_ERR_INVALID_MIC_E_TELEMETRY LITERAL1 +RADIOLIB_ERR_INVALID_MIC_E_TELEMETRY_LENGTH LITERAL1 +RADIOLIB_ERR_MIC_E_TELEMETRY_STATUS LITERAL1 RADIOLIB_ASCII LITERAL1 RADIOLIB_ASCII_EXTENDED LITERAL1 diff --git a/src/TypeDef.h b/src/TypeDef.h index 71b738c26..0ac5d2ef0 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -262,6 +262,21 @@ */ #define RADIOLIB_ERR_INVALID_SYMBOL (-201) +/*! + \brief Mic-E Telemetry is invalid. +*/ +#define RADIOLIB_ERR_INVALID_MIC_E_TELEMETRY (-202) + +/*! + \brief Mic-E Telemetry length is invalid (only 0, 2 or 5 is allowed). +*/ +#define RADIOLIB_ERR_INVALID_MIC_E_TELEMETRY_LENGTH (-203) + +/*! + \brief Mic-E message cannot contaion both telemetry and status text. +*/ +#define RADIOLIB_ERR_MIC_E_TELEMETRY_STATUS (-204) + // RTTY status codes /*! diff --git a/src/protocols/APRS/APRS.cpp b/src/protocols/APRS/APRS.cpp index 045d09a0a..7f4976b08 100644 --- a/src/protocols/APRS/APRS.cpp +++ b/src/protocols/APRS/APRS.cpp @@ -54,6 +54,166 @@ int16_t APRSClient::sendPosition(char* destCallsign, uint8_t destSSID, char* lat return(state); } +int16_t APRSClient::sendMicE(float lat, float lon, uint16_t heading, uint16_t speed, uint8_t type, uint8_t* telem, size_t telemLen, char* grid, char* status, int32_t alt) { + // sanity checks first + if(((telemLen == 0) && (telem != NULL)) || ((telemLen != 0) && (telem == NULL))) { + return(RADIOLIB_ERR_INVALID_MIC_E_TELEMETRY); + } + + if((telemLen != 0) && (telemLen != 2) && (telemLen != 5)) { + return(RADIOLIB_ERR_INVALID_MIC_E_TELEMETRY_LENGTH); + } + + if((telemLen > 0) && ((grid != NULL) || (status != NULL) || (alt != RADIOLIB_APRS_MIC_E_ALTITUDE_UNUSED))) { + // can't have both telemetry and status + return(RADIOLIB_ERR_MIC_E_TELEMETRY_STATUS); + } + + // prepare buffers + char destCallsign[7]; + #if !defined(RADIOLIB_STATIC_ONLY) + size_t infoLen = 10; + if(telemLen > 0) { + infoLen += 1 + telemLen; + } else { + if(grid != NULL) { + infoLen += strlen(grid) + 2; + } + if(status != NULL) { + infoLen += strlen(status); + } + if(alt > RADIOLIB_APRS_MIC_E_ALTITUDE_UNUSED) { + infoLen += 4; + } + } + char* info = new char[infoLen]; + #else + char info[RADIOLIB_STATIC_ARRAY_SIZE]; + #endif + size_t infoPos = 0; + + // the following is based on APRS Mic-E implementation by https://github.com/omegat + // as discussed in https://github.com/jgromes/RadioLib/issues/430 + + // latitude first, because that is in the destination field + float lat_abs = abs(lat); + int lat_deg = (int)lat_abs; + int lat_min = (lat_abs - (float)lat_deg) * 60.0f; + int lat_hun = (((lat_abs - (float)lat_deg) * 60.0f) - lat_min) * 100.0f; + destCallsign[0] = lat_deg/10; + destCallsign[1] = lat_deg%10; + destCallsign[2] = lat_min/10; + destCallsign[3] = lat_min%10; + destCallsign[4] = lat_hun/10; + destCallsign[5] = lat_hun%10; + + // next, add the extra bits + if(type & 0x04) { destCallsign[0] += RADIOLIB_APRS_MIC_E_DEST_BIT_OFFSET; } + if(type & 0x02) { destCallsign[1] += RADIOLIB_APRS_MIC_E_DEST_BIT_OFFSET; } + if(type & 0x01) { destCallsign[2] += RADIOLIB_APRS_MIC_E_DEST_BIT_OFFSET; } + if(lat >= 0) { destCallsign[3] += RADIOLIB_APRS_MIC_E_DEST_BIT_OFFSET; } + if(lon >= 100 || lon <= -100) { destCallsign[4] += RADIOLIB_APRS_MIC_E_DEST_BIT_OFFSET; } + if(lon < 0) { destCallsign[5] += RADIOLIB_APRS_MIC_E_DEST_BIT_OFFSET; } + destCallsign[6] = '\0'; + + // now convert to Mic-E characters to get the "callsign" + for(uint8_t i = 0; i < 6; i++) { + if(destCallsign[i] <= 9) { + destCallsign[i] += '0'; + } else { + destCallsign[i] += ('A' - 10); + } + } + + // setup the information field + info[infoPos++] = RADIOLIB_APRS_MIC_E_GPS_DATA_CURRENT; + + // encode the longtitude + float lon_abs = abs(lon); + int32_t lon_deg = (int32_t)lon_abs; + int32_t lon_min = (lon_abs - (float)lon_deg) * 60.0f; + int32_t lon_hun = (((lon_abs - (float)lon_deg) * 60.0f) - lon_min) * 100.0f; + + if(lon_deg <= 9) { + info[infoPos++] = lon_deg + 118; + } else if(lon_deg <= 99) { + info[infoPos++] = lon_deg + 28; + } else if(lon_deg <= 109) { + info[infoPos++] = lon_deg + 8; + } else { + info[infoPos++] = lon_deg - 72; + } + + if(lon_min <= 9){ + info[infoPos++] = lon_min + 88; + } else { + info[infoPos++] = lon_min + 28; + } + + info[infoPos++] = lon_hun + 28; + + // now the speed and heading - this gets really weird + int32_t speed_hun_ten = speed/10; + int32_t speed_uni = speed%10; + int32_t head_hun = heading/100; + int32_t head_ten_uni = heading%100; + + if(speed <= 199) { + info[infoPos++] = speed_hun_ten + 'l'; + } else { + info[infoPos++] = speed_hun_ten + '0'; + } + + info[infoPos++] = speed_uni*10 + head_hun + 32; + info[infoPos++] = head_ten_uni + 28; + info[infoPos++] = _symbol; + info[infoPos++] = _table; + + // onto the optional stuff - check telemetry first + if(telemLen > 0) { + if(telemLen == 2) { + info[infoPos++] = RADIOLIB_APRS_MIC_E_TELEMETRY_LEN_2; + } else { + info[infoPos++] = RADIOLIB_APRS_MIC_E_TELEMETRY_LEN_5; + } + for(uint8_t i = 0; i < telemLen; i++) { + sprintf(&(info[infoPos]), "%02X", telem[i]); + infoPos += 2; + } + + } else { + if(grid != NULL) { + memcpy(&(info[infoPos]), grid, strlen(grid)); + infoPos += strlen(grid); + info[infoPos++] = '/'; + info[infoPos++] = 'G'; + } + if(status != NULL) { + info[infoPos++] = ' '; + memcpy(&(info[infoPos]), status, strlen(status)); + infoPos += strlen(status); + } + if(alt > RADIOLIB_APRS_MIC_E_ALTITUDE_UNUSED) { + // altitude is offset by -10 km + int32_t alt_val = alt + 10000; + + // ... and encoded in base 91 for some reason + info[infoPos++] = (alt_val / 8281) + 33; + info[infoPos++] = ((alt_val % 8281) / 91) + 33; + info[infoPos++] = ((alt_val % 8281) % 91) + 33; + info[infoPos++] = '}'; + } + } + info[infoPos++] = '\0'; + + // send the frame + int16_t state = sendFrame(destCallsign, 0, info); + #if !defined(RADIOLIB_STATIC_ONLY) + delete[] info; + #endif + return(state); +} + int16_t APRSClient::sendFrame(char* destCallsign, uint8_t destSSID, char* info) { // get AX.25 callsign char srcCallsign[RADIOLIB_AX25_MAX_CALLSIGN_LEN + 1]; diff --git a/src/protocols/APRS/APRS.h b/src/protocols/APRS/APRS.h index 6e272aa70..39f89c64d 100644 --- a/src/protocols/APRS/APRS.h +++ b/src/protocols/APRS/APRS.h @@ -27,6 +27,37 @@ #define RADIOLIB_APRS_DATA_TYPE_USER_DEFINED "{" #define RADIOLIB_APRS_DATA_TYPE_THIRD_PARTY "}" +/*! + \defgroup mic_e_message_types Mic-E message types. + + \{ +*/ +#define RADIOLIB_APRS_MIC_E_TYPE_OFF_DUTY 0b00000111 +#define RADIOLIB_APRS_MIC_E_TYPE_EN_ROUTE 0b00000110 +#define RADIOLIB_APRS_MIC_E_TYPE_IN_SERVICE 0b00000101 +#define RADIOLIB_APRS_MIC_E_TYPE_RETURNING 0b00000100 +#define RADIOLIB_APRS_MIC_E_TYPE_COMMITTED 0b00000011 +#define RADIOLIB_APRS_MIC_E_TYPE_SPECIAL 0b00000010 +#define RADIOLIB_APRS_MIC_E_TYPE_PRIORITY 0b00000001 +#define RADIOLIB_APRS_MIC_E_TYPE_EMERGENCY 0b00000000 +/*! + \} +*/ + +// magic offset applied to encode extra bits in the Mic-E destination field +#define RADIOLIB_APRS_MIC_E_DEST_BIT_OFFSET 25 + +// Mic-E data types +#define RADIOLIB_APRS_MIC_E_GPS_DATA_CURRENT '`' +#define RADIOLIB_APRS_MIC_E_GPS_DATA_OLD '\'' + +// Mic-E telemetry flags +#define RADIOLIB_APRS_MIC_E_TELEMETRY_LEN_2 '`' +#define RADIOLIB_APRS_MIC_E_TELEMETRY_LEN_5 '\'' + +// alias for unused altitude in Mic-E +#define RADIOLIB_APRS_MIC_E_ALTITUDE_UNUSED -1000000 + /*! \class APRSClient @@ -73,6 +104,31 @@ class APRSClient { */ int16_t sendPosition(char* destCallsign, uint8_t destSSID, char* lat, char* lon, char* msg = NULL, char* time = NULL); + /* + \brief Transmit position using Mic-E encoding. + + \param lat Geographical latitude, positive for north, negative for south. + + \param lon Geographical longitude, positive for east, negative for west. + + \param heading Heading in degrees. + + \param speed Speed in knots. + + \param type Mic-E message type - see \ref mic_e_message_types. + + \param telem Pointer to telemetry array (either 2 or 5 bytes long). NULL when telemetry is not used. + + \param telemLen Telemetry length, 2 or 5. 0 when telemetry is not used. + + \param grid Maidenhead grid locator. NULL when not used. + + \param status Status message to send. NULL when not used. + + \param alt Altitude to send. RADIOLIB_APRS_MIC_E_ALTITUDE_UNUSED when not used. + */ + int16_t sendMicE(float lat, float lon, uint16_t heading, uint16_t speed, uint8_t type, uint8_t* telem = NULL, size_t telemLen = 0, char* grid = NULL, char* status = NULL, int32_t alt = RADIOLIB_APRS_MIC_E_ALTITUDE_UNUSED); + /*! \brief Transmit generic APRS frame.