diff --git a/README.md b/README.md index 57fabc21..5e67821d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ SX127x, RFM9x, SX126x, RF69, SX1231, CC1101, nRF24L01, RFM2x, Si443x and SX128x SX127x, RFM9x, SX126x, RF69, SX1231, CC1101, RFM2x and Si443x * [__Hellschreiber__](https://www.sigidwiki.com/wiki/Hellschreiber) using 2-FSK or AFSK for modules: SX127x, RFM9x, SX126x, RF69, SX1231, CC1101, nRF24L01, RFM2x, Si443x and SX128x +* [__APRS__](https://www.sigidwiki.com/wiki/APRS) using AFSK for modules: +SX127x, RFM9x, SX126x, RF69, SX1231, CC1101, nRF24L01, RFM2x, Si443x and SX128x ### Supported Arduino platforms: * __Arduino__ @@ -76,7 +78,6 @@ The list above is by no means exhaustive - RadioLib code is independent of the u ### In development: * __AX5243__ FSK module * __LoRaWAN__ protocol for SX127x, RFM9x and SX126x modules -* __APRS__ protocol for all the modules that can transmit AX.25 * ___and more!___ ## Frequently Asked Questions diff --git a/examples/APRS/APRS_Position/APRS_Position.ino b/examples/APRS/APRS_Position/APRS_Position.ino new file mode 100644 index 00000000..1fa8ca9e --- /dev/null +++ b/examples/APRS/APRS_Position/APRS_Position.ino @@ -0,0 +1,123 @@ +/* + RadioLib APRS Position Example + + This example sends APRS position reports + 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(434.0); + + // 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 position ... ")); + + // send a location without message or timestamp + int state = aprs.sendPosition("N0CALL", 0, "4911.67N", "01635.96E"); + delay(500); + + // send a location with message and without timestamp + state |= aprs.sendPosition("N0CALL", 0, "4911.67N", "01635.96E", "I'm here!"); + delay(500); + + // send a location with message and timestamp + state |= aprs.sendPosition("N0CALL", 0, "4911.67N", "01635.96E", "I'm here!", "093045z"); + delay(500); + + 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 7b86be5d..4dd0c926 100644 --- a/keywords.txt +++ b/keywords.txt @@ -48,6 +48,7 @@ SSTVClient KEYWORD1 HellClient KEYWORD1 AFSKClient KEYWORD1 FSK4Client KEYWORD1 +APRSClient KEYWORD1 # SSTV modes Scottie1 KEYWORD1 @@ -217,6 +218,9 @@ printGlyph KEYWORD2 tone KEYWORD2 noTone KEYWORD2 +# APRS +sendPosition KEYWORD2 + ####################################### # Constants (LITERAL1) ####################################### diff --git a/src/RadioLib.h b/src/RadioLib.h index 92b659dd..0d202699 100644 --- a/src/RadioLib.h +++ b/src/RadioLib.h @@ -21,6 +21,7 @@ - SSTV (SSTVClient) - Hellschreiber (HellClient) - 4-FSK (FSK4Client) + - APRS (APRSClient) \par Quick Links Documentation for most common methods can be found in its reference page (see the list above).\n @@ -89,6 +90,7 @@ #include "protocols/RTTY/RTTY.h" #include "protocols/SSTV/SSTV.h" #include "protocols/FSK4/FSK4.h" +#include "protocols/APRS/APRS.h" // only create Radio class when using RadioShield #if defined(RADIOLIB_RADIOSHIELD) diff --git a/src/TypeDef.h b/src/TypeDef.h index 6a0b0a60..a9687c13 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -245,6 +245,13 @@ */ #define RADIOLIB_ERR_INVALID_OOK_RSSI_PEAK_TYPE (-108) +// APRS status codes + +/*! + \brief Supplied APRS symbol is invalid. +*/ +#define RADIOLIB_ERR_INVALID_SYMBOL (-201) + // RTTY status codes /*! diff --git a/src/protocols/APRS/APRS.cpp b/src/protocols/APRS/APRS.cpp new file mode 100644 index 00000000..045d09a0 --- /dev/null +++ b/src/protocols/APRS/APRS.cpp @@ -0,0 +1,67 @@ +#include "APRS.h" + +APRSClient::APRSClient(AX25Client* ax) { + _ax = ax; +} + +int16_t APRSClient::begin(char symbol, bool alt) { + RADIOLIB_CHECK_RANGE(symbol, ' ', '}', RADIOLIB_ERR_INVALID_SYMBOL); + _symbol = symbol; + + if(alt) { + _table = '\\'; + } else { + _table = '/'; + } + + return(RADIOLIB_ERR_NONE); +} + +int16_t APRSClient::sendPosition(char* destCallsign, uint8_t destSSID, char* lat, char* lon, char* msg, char* time) { + #if !defined(RADIOLIB_STATIC_ONLY) + size_t len = 1 + strlen(lat) + 1 + strlen(lon); + if(msg != NULL) { + len += 1 + strlen(msg); + } + if(time != NULL) { + len += strlen(time); + } + char* info = new char[len]; + #else + char info[RADIOLIB_STATIC_ARRAY_SIZE]; + #endif + + // build the info field + if((msg == NULL) && (time == NULL)) { + // no message, no timestamp + sprintf(info, RADIOLIB_APRS_DATA_TYPE_POSITION_NO_TIME_NO_MSG "%s%c%s%c", lat, _table, lon, _symbol); + } else if((msg != NULL) && (time == NULL)) { + // message, no timestamp + sprintf(info, RADIOLIB_APRS_DATA_TYPE_POSITION_NO_TIME_MSG "%s%c%s%c%s", lat, _table, lon, _symbol, msg); + } else if((msg == NULL) && (time != NULL)) { + // timestamp, no message + sprintf(info, RADIOLIB_APRS_DATA_TYPE_POSITION_TIME_NO_MSG "%s%s%c%s%c", time, lat, _table, lon, _symbol); + } else { + // timestamp and message + sprintf(info, RADIOLIB_APRS_DATA_TYPE_POSITION_TIME_MSG "%s%s%c%s%c%s", time, lat, _table, lon, _symbol, msg); + } + + // send the frame + int16_t state = sendFrame(destCallsign, destSSID, 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]; + _ax->getCallsign(srcCallsign); + + AX25Frame frameUI(destCallsign, destSSID, srcCallsign, _ax->getSSID(), RADIOLIB_AX25_CONTROL_U_UNNUMBERED_INFORMATION | + RADIOLIB_AX25_CONTROL_POLL_FINAL_DISABLED | RADIOLIB_AX25_CONTROL_UNNUMBERED_FRAME, + RADIOLIB_AX25_PID_NO_LAYER_3, (const char*)info); + + return(_ax->sendFrame(&frameUI)); +} diff --git a/src/protocols/APRS/APRS.h b/src/protocols/APRS/APRS.h new file mode 100644 index 00000000..6e272aa7 --- /dev/null +++ b/src/protocols/APRS/APRS.h @@ -0,0 +1,101 @@ +#if !defined(_RADIOLIB_RADIOLIB_APRS_H) +#define _RADIOLIB_RADIOLIB_APRS_H + +#include "../../TypeDef.h" + +#if !defined(RADIOLIB_EXCLUDE_APRS) + +#include "../PhysicalLayer/PhysicalLayer.h" +#include "../AX25/AX25.h" + +// APRS data type identifiers +#define RADIOLIB_APRS_DATA_TYPE_POSITION_NO_TIME_NO_MSG "!" +#define RADIOLIB_APRS_DATA_TYPE_GPS_RAW "$" +#define RADIOLIB_APRS_DATA_TYPE_ITEM ")" +#define RADIOLIB_APRS_DATA_TYPE_TEST "," +#define RADIOLIB_APRS_DATA_TYPE_POSITION_TIME_NO_MSG "/" +#define RADIOLIB_APRS_DATA_TYPE_MSG ":" +#define RADIOLIB_APRS_DATA_TYPE_OBJECT ";" +#define RADIOLIB_APRS_DATA_TYPE_STATION_CAPABILITES "<" +#define RADIOLIB_APRS_DATA_TYPE_POSITION_NO_TIME_MSG "=" +#define RADIOLIB_APRS_DATA_TYPE_STATUS ">" +#define RADIOLIB_APRS_DATA_TYPE_QUERY "?" +#define RADIOLIB_APRS_DATA_TYPE_POSITION_TIME_MSG "@" +#define RADIOLIB_APRS_DATA_TYPE_TELEMETRY "T" +#define RADIOLIB_APRS_DATA_TYPE_MAIDENHEAD_BEACON "[" +#define RADIOLIB_APRS_DATA_TYPE_WEATHER_REPORT "_" +#define RADIOLIB_APRS_DATA_TYPE_USER_DEFINED "{" +#define RADIOLIB_APRS_DATA_TYPE_THIRD_PARTY "}" + +/*! + \class APRSClient + + \brief Client for APRS communication. +*/ +class APRSClient { + public: + /*! + \brief Default constructor. + + \param ax Pointer to the instance of AX25Client to be used for APRS. + */ + explicit APRSClient(AX25Client* ax); + + // basic methods + + /*! + \brief Initialization method. + + \param symbol APRS symbol to be displayed. + + \param alt Whether to use the primary (false) or alternate (true) symbol table. Defaults to primary table. + + \returns \ref status_codes + */ + int16_t begin(char symbol, bool alt = false); + + /*! + \brief Transmit position. + + \param destCallsign Destination station callsign. + + \param destSSID Destination station SSID. + + \param lat Latitude as a null-terminated string. + + \param long Longitude as a null-terminated string. + + \param msg Message to be transmitted. Defaults to NULL (no message). + + \param msg Position timestamp. Defaults to NULL (no timestamp). + + \returns \ref status_codes + */ + int16_t sendPosition(char* destCallsign, uint8_t destSSID, char* lat, char* lon, char* msg = NULL, char* time = NULL); + + /*! + \brief Transmit generic APRS frame. + + \param destCallsign Destination station callsign. + + \param destSSID Destination station SSID. + + \param info AX.25 info field contents. + + \returns \ref status_codes + */ + int16_t sendFrame(char* destCallsign, uint8_t destSSID, char* info); + +#if !defined(RADIOLIB_GODMODE) + private: +#endif + AX25Client* _ax; + + // default APRS symbol (car) + char _symbol = '>'; + char _table = '/'; +}; + +#endif + +#endif