mirror of https://github.com/hmatuschek/libsdr
Implemented trivial APRS decoder.
parent
acee3dc318
commit
36ac8d3cc1
@ -1,41 +0,0 @@
|
|||||||
#include "wavfile.hh"
|
|
||||||
#include "autocast.hh"
|
|
||||||
#include "interpolate.hh"
|
|
||||||
#include "fsk.hh"
|
|
||||||
#include "ax25.hh"
|
|
||||||
#include "utils.hh"
|
|
||||||
|
|
||||||
|
|
||||||
using namespace sdr;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
if (2 > argc) { std::cout << "USAGE: sdr_afsk1200 FILENAME" << std::endl; return -1; }
|
|
||||||
|
|
||||||
sdr::Logger::get().addHandler(
|
|
||||||
new sdr::StreamLogHandler(std::cerr, sdr::LOG_DEBUG));
|
|
||||||
|
|
||||||
Queue &queue = Queue::get();
|
|
||||||
|
|
||||||
WavSource src(argv[1], 1024);
|
|
||||||
if (! src.isOpen()) { std::cout << "Can not open file " << argv[1] << std::endl; return -1; }
|
|
||||||
|
|
||||||
AutoCast< int16_t > cast;
|
|
||||||
FSKDetector demod(1200, 1200, 2200);
|
|
||||||
BitStream bits(1200, BitStream::TRANSITION);
|
|
||||||
AX25 decode;
|
|
||||||
|
|
||||||
src.connect(&cast);
|
|
||||||
cast.connect(&demod);
|
|
||||||
demod.connect(&bits);
|
|
||||||
bits.connect(&decode);
|
|
||||||
|
|
||||||
Queue::get().addIdle(&src, &WavSource::next);
|
|
||||||
src.addEOS(&queue, &Queue::stop);
|
|
||||||
|
|
||||||
Queue::get().start();
|
|
||||||
Queue::get().wait();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,339 @@
|
|||||||
|
#include "aprs.hh"
|
||||||
|
#include "logger.hh"
|
||||||
|
|
||||||
|
using namespace sdr;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ******************************************************************************************** *
|
||||||
|
* Implementation of APRS
|
||||||
|
* ******************************************************************************************** */
|
||||||
|
APRS::APRS()
|
||||||
|
: AX25()
|
||||||
|
{
|
||||||
|
// pass...
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
APRS::handleAX25Message(const AX25::Message &message) {
|
||||||
|
// Skip non-UI frames
|
||||||
|
if (0x03 != uint8_t(message.payload()[0])) {
|
||||||
|
LogMessage msg(LOG_DEBUG);
|
||||||
|
msg << "APRS: Skip non-UI frame (type="
|
||||||
|
<< std::hex << int(uint8_t(message.payload()[0]))
|
||||||
|
<< std::dec << "): " << message;
|
||||||
|
Logger::get().log(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip frames woth level-3 protocol
|
||||||
|
if (0xf0 != uint8_t(message.payload()[1])) {
|
||||||
|
LogMessage msg(LOG_DEBUG);
|
||||||
|
msg << "APRS: Skip invalid UI (pid="
|
||||||
|
<< std::hex << int(uint8_t(message.payload()[1]))
|
||||||
|
<< std::dec << "): " << message;
|
||||||
|
Logger::get().log(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Construct APRS message
|
||||||
|
Message msg(message);
|
||||||
|
// Handle message
|
||||||
|
this->handleAPRSMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
APRS::handleAPRSMessage(const Message &message) {
|
||||||
|
std::cerr << message << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ******************************************************************************************** *
|
||||||
|
* Implementation of APRS::Message
|
||||||
|
* ******************************************************************************************** */
|
||||||
|
bool __is_number(char c) {
|
||||||
|
return (('0'<=c) && ('9'>=c));
|
||||||
|
}
|
||||||
|
|
||||||
|
APRS::Message::Symbol
|
||||||
|
__toSymbol(char table, char sym) {
|
||||||
|
if ('/' == table) {
|
||||||
|
switch (sym) {
|
||||||
|
case 'P':
|
||||||
|
case '!': return APRS::Message::POLICE;
|
||||||
|
case '%':
|
||||||
|
case '&':
|
||||||
|
case '(':
|
||||||
|
case 'B':
|
||||||
|
case 'n':
|
||||||
|
case '#': return APRS::Message::DIGI;
|
||||||
|
case '[':
|
||||||
|
case 'e':
|
||||||
|
case '$': return APRS::Message::JOGGER;
|
||||||
|
case 'X':
|
||||||
|
case '^':
|
||||||
|
case 'g':
|
||||||
|
case '\'': return APRS::Message::AIRCRAFT;
|
||||||
|
case '-': return APRS::Message::HOUSE;
|
||||||
|
case 'b':
|
||||||
|
case '<': return APRS::Message::MOTORCYCLE;
|
||||||
|
case '=':
|
||||||
|
case '*':
|
||||||
|
case 'U':
|
||||||
|
case 'j':
|
||||||
|
case 'k':
|
||||||
|
case 'u':
|
||||||
|
case 'v':
|
||||||
|
case '>': return APRS::Message::CAR;
|
||||||
|
case 'Y':
|
||||||
|
case 's':
|
||||||
|
case 'C': return APRS::Message::BOAT;
|
||||||
|
case 'O': return APRS::Message::BALLOON;
|
||||||
|
case '_': return APRS::Message::WX;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
} else if ('\\' == table) {
|
||||||
|
switch (sym) {
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return APRS::Message::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
APRS::Message::Message()
|
||||||
|
: AX25::Message(), _hasLocation(false), _latitude(0), _longitude(0), _symbol(NONE),
|
||||||
|
_hasTime(false), _time(::time(0))
|
||||||
|
{
|
||||||
|
// pass...
|
||||||
|
}
|
||||||
|
|
||||||
|
APRS::Message::Message(const AX25::Message &msg)
|
||||||
|
: AX25::Message(msg),
|
||||||
|
_hasLocation(false), _latitude(0), _longitude(0), _symbol(NONE),
|
||||||
|
_hasTime(false), _time(::time(0))
|
||||||
|
{
|
||||||
|
size_t offset = 2;
|
||||||
|
// Dispatch by message type
|
||||||
|
switch (_payload[offset]) {
|
||||||
|
case '=':
|
||||||
|
case '!':
|
||||||
|
_hasLocation = true;
|
||||||
|
offset++;
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
case '@':
|
||||||
|
_hasTime = true;
|
||||||
|
_hasLocation = true;
|
||||||
|
offset++;
|
||||||
|
break;
|
||||||
|
case ';':
|
||||||
|
_hasTime = true;
|
||||||
|
_hasLocation = true;
|
||||||
|
// Skip ';', 9 char of object identifier and delimiter ('*' or '_').
|
||||||
|
offset += 11;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// On unknown message type -> store complete message as comment
|
||||||
|
if (offset < _payload.size()) {
|
||||||
|
_comment = _payload.substr(offset);
|
||||||
|
offset += _comment.size();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hasTime) {
|
||||||
|
if (! _readTime(offset)) { return; }
|
||||||
|
}
|
||||||
|
if (_hasLocation) {
|
||||||
|
if (! _readLocation(offset)) { return; }
|
||||||
|
}
|
||||||
|
// Remaining text is comment
|
||||||
|
if (offset < _payload.size()) {
|
||||||
|
_comment = _payload.substr(offset);
|
||||||
|
offset += _comment.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
APRS::Message::_readLocation(size_t &offset) {
|
||||||
|
// Read latitude
|
||||||
|
if(! _readLatitude(offset)) { return false; }
|
||||||
|
|
||||||
|
// Read symbol table
|
||||||
|
char symbolTable = _payload[offset]; offset++;
|
||||||
|
|
||||||
|
// Read longitude
|
||||||
|
if (! _readLongitude(offset)) { return false; }
|
||||||
|
|
||||||
|
// Read symbol
|
||||||
|
_symbol = __toSymbol(symbolTable, _payload[offset]); offset++;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
APRS::Message::_readLatitude(size_t &offset)
|
||||||
|
{
|
||||||
|
// read degree
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
_latitude = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
_latitude = _latitude*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
|
||||||
|
// read mintues
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
double min = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
min = min*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
|
||||||
|
// check for '.'
|
||||||
|
if ('.' != _payload[offset]) { return false; } offset++;
|
||||||
|
|
||||||
|
// read minutes decimal
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
double mindec = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
mindec = mindec*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
min += mindec/100;
|
||||||
|
|
||||||
|
// Update degree
|
||||||
|
_latitude += min/60;
|
||||||
|
|
||||||
|
// Process north/south indicator
|
||||||
|
switch (_payload[offset]) {
|
||||||
|
case 'N': offset++; break;
|
||||||
|
case 'S': _latitude = -_latitude; offset++; break;
|
||||||
|
default: return false; // <- on invalid indicator.
|
||||||
|
}
|
||||||
|
|
||||||
|
// done.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
APRS::Message::_readLongitude(size_t &offset) {
|
||||||
|
// read degree
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
_longitude = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
_longitude = _longitude*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
_longitude = _longitude*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
|
||||||
|
// read minutes
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
double min = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
min = min*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
|
||||||
|
// skip '.'
|
||||||
|
if ('.' != _payload[offset]) { return false; } offset++;
|
||||||
|
|
||||||
|
// Read minutes decimals
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
double mindec = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
mindec = mindec*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
min += mindec/100;
|
||||||
|
|
||||||
|
// Update longitude
|
||||||
|
_longitude += min/60;
|
||||||
|
|
||||||
|
// read east/west indicator
|
||||||
|
switch (_payload[offset]) {
|
||||||
|
case 'E': offset++; break;
|
||||||
|
case 'W': offset++; _longitude = -_longitude; break;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
APRS::Message::_readTime(size_t &offset) {
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
int a = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
a = a*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
int b = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
b = b*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
int c = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
c = c*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
|
||||||
|
// Determine type of date-time
|
||||||
|
if ('z' == _payload[offset]) {
|
||||||
|
// Alter current time in terms of UTC
|
||||||
|
struct tm timeStruct = *gmtime(&_time);
|
||||||
|
timeStruct.tm_mday = a;
|
||||||
|
timeStruct.tm_hour = b;
|
||||||
|
timeStruct.tm_min = c;
|
||||||
|
// Update time-stamp
|
||||||
|
_time = mktime(&timeStruct);
|
||||||
|
offset++;
|
||||||
|
} else if ('/' == _payload[offset]) {
|
||||||
|
// Alter current time in terms of local time
|
||||||
|
struct tm timeStruct = *localtime(&_time);
|
||||||
|
timeStruct.tm_mday = a;
|
||||||
|
timeStruct.tm_hour = b;
|
||||||
|
timeStruct.tm_min = c;
|
||||||
|
// Update time-stamp
|
||||||
|
_time = mktime(&timeStruct);
|
||||||
|
offset++;
|
||||||
|
} else if ('h' == _payload[offset]) {
|
||||||
|
// Alter current time in terms of local time
|
||||||
|
struct tm timeStruct = *localtime(&_time);
|
||||||
|
timeStruct.tm_hour = a;
|
||||||
|
timeStruct.tm_min = b;
|
||||||
|
timeStruct.tm_sec = c;
|
||||||
|
// Update time-stamp
|
||||||
|
_time = mktime(&timeStruct);
|
||||||
|
offset++;
|
||||||
|
} else if (('0' <= _payload[offset]) && (('9' >= _payload[offset]))) {
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
int d = int(_payload[offset]-0x30); offset++;
|
||||||
|
if (! __is_number(_payload[offset])) { return false; }
|
||||||
|
d = d*10 + int(_payload[offset]-0x30); offset++;
|
||||||
|
// Alter current time in terms of local time
|
||||||
|
struct tm timeStruct = *localtime(&_time);
|
||||||
|
timeStruct.tm_mon = a;
|
||||||
|
timeStruct.tm_mday = b;
|
||||||
|
timeStruct.tm_hour = c;
|
||||||
|
timeStruct.tm_min = d;
|
||||||
|
// Update time-stamp
|
||||||
|
_time = mktime(&timeStruct);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// done...
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::ostream &
|
||||||
|
sdr::operator <<(std::ostream &stream, const APRS::Message &message) {
|
||||||
|
std::cerr << "APRS: " << message.from() << " > " << message.to();
|
||||||
|
if (message.via().size()) {
|
||||||
|
std::cerr << " via " << message.via()[0];
|
||||||
|
for (size_t i=1; i<message.via().size(); i++) {
|
||||||
|
std::cerr << ", " << message.via()[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::cerr << std::endl << " time: " << asctime(localtime(&message.time()));
|
||||||
|
if (message.hasLocation()) {
|
||||||
|
std::cerr << " location: " << message.latitude() << ", " << message.longitude()
|
||||||
|
<< std::endl << " symbol: " << message.symbol() << std::endl;
|
||||||
|
}
|
||||||
|
if (message.hasComment()) {
|
||||||
|
std::cerr << " comment: " << message.comment() << std::endl;
|
||||||
|
}
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
#ifndef __SDR_APRS_HH__
|
||||||
|
#define __SDR_APRS_HH__
|
||||||
|
|
||||||
|
#include "ax25.hh"
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
|
||||||
|
namespace sdr {
|
||||||
|
|
||||||
|
class APRS: public AX25
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
class Message: public AX25::Message
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/** A small selection of possible symbols to display the station. */
|
||||||
|
typedef enum {
|
||||||
|
NONE, POLICE, DIGI, PHONE, AIRCRAFT, HOUSE, MOTORCYCLE, CAR, BBS, BALLOON, BUS,
|
||||||
|
BOAT, JOGGER, WX
|
||||||
|
} Symbol;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Message();
|
||||||
|
Message(const AX25::Message &msg);
|
||||||
|
Message(const Message &msg);
|
||||||
|
|
||||||
|
Message &operator=(const Message &other);
|
||||||
|
|
||||||
|
inline bool hasLocation() const { return _hasLocation; }
|
||||||
|
inline double latitude() const { return _latitude; }
|
||||||
|
inline double longitude() const { return _longitude; }
|
||||||
|
inline Symbol symbol() const { return _symbol; }
|
||||||
|
|
||||||
|
inline const time_t &time() const { return _time; }
|
||||||
|
|
||||||
|
inline bool hasComment() const { return (0 != _comment.size()); }
|
||||||
|
inline const std::string &comment() const { return _comment; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool _readLocation(size_t &offset);
|
||||||
|
bool _readLatitude(size_t &offset);
|
||||||
|
bool _readLongitude(size_t &offset);
|
||||||
|
bool _readTime(size_t &offset);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool _hasLocation;
|
||||||
|
double _latitude;
|
||||||
|
double _longitude;
|
||||||
|
Symbol _symbol;
|
||||||
|
bool _hasTime;
|
||||||
|
time_t _time;
|
||||||
|
std::string _comment;
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
APRS();
|
||||||
|
|
||||||
|
void handleAX25Message(const AX25::Message &message);
|
||||||
|
virtual void handleAPRSMessage(const Message &message);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream &stream, const APRS::Message &msg);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __SDR_APRS_HH__
|
||||||
Loading…
Reference in New Issue