[APRS] Added Mic-E (#430)
This commit is contained in:
parent
a8c079f85e
commit
30cb7c8dd4
5 changed files with 351 additions and 0 deletions
111
examples/APRS/APRS_MicE/APRS_MicE.ino
Normal file
111
examples/APRS/APRS_MicE/APRS_MicE.ino
Normal file
|
@ -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 <RadioLib.h>
|
||||
|
||||
// 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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
/*!
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue