[APRS] Added Mic-E (#430)

pull/561/head
jgromes 2022-08-20 18:14:13 +02:00
rodzic a8c079f85e
commit 30cb7c8dd4
5 zmienionych plików z 351 dodań i 0 usunięć

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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
/*!

Wyświetl plik

@ -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];

Wyświetl plik

@ -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.