Skip to content

Commit

Permalink
[APRS] Added Mic-E (#430)
Browse files Browse the repository at this point in the history
  • Loading branch information
jgromes committed Aug 20, 2022
1 parent a8c079f commit 30cb7c8
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 0 deletions.
111 changes: 111 additions & 0 deletions examples/APRS/APRS_MicE/APRS_MicE.ino
Original file line number Diff line number Diff line change
@@ -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);
}
9 changes: 9 additions & 0 deletions keywords.txt
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ randomByte KEYWORD2
getPacketLength KEYWORD2
setFifoEmptyAction KEYWORD2
clearFifoEmptyAction KEYWORD2
setFifoFullAction KEYWORD2
clearFifoFullAction KEYWORD2
fifoAdd KEYWORD2
fifoGet KEYWORD2

Expand Down Expand Up @@ -234,6 +236,7 @@ noTone KEYWORD2

# APRS
sendPosition KEYWORD2
sendMicE KEYWORD2

#######################################
# Constants (LITERAL1)
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/TypeDef.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

/*!
Expand Down
160 changes: 160 additions & 0 deletions src/protocols/APRS/APRS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Loading

0 comments on commit 30cb7c8

Please sign in to comment.