intermediate: NMEA0183A AIS to N2K
This commit is contained in:
parent
ce954e41f8
commit
3b437c8476
|
@ -0,0 +1,141 @@
|
|||
imported from https://github.com/aduvenhage/ais-decoder
|
||||
and from https://github.com/AK-Homberger/NMEA2000-AIS-Gateway
|
||||
AIS NMEA Message Decoder (v2.0)
|
||||
This project was created to learn more about AIS and see how easy it would be to create a decoder for the NMEA strings. The NMEA string decoding is implemented according to: 'http://catb.org/gpsd/AIVDM.html'.
|
||||
|
||||
Also please, checkout https://github.com/aduvenhage/ais_beast for an attemp to make a faster (C++ only) decoder.
|
||||
|
||||
The decoder is designed to work off of raw data (processed in blocks) received from, for example, a file or a socket. The raw data sentences (or lines) may be seperated by '[LF]' or '[CR][LF]'.
|
||||
|
||||
The key component to implement was the 6bit nibble packing and unpacking of arbitrarily sized signed and unsigned integers as well as strings (see PayloadBuffer in ais_decoder.h). The decoder interface also delivers clean strings (i.e. all characters after and including '@' removed and all trailing whitespace removed).
|
||||
|
||||
The decoder consists of a base class that does the decoding, with pure virtual methods for each AIS messages type. A user of the decoder has to inherit from the decoder class and implement/override 'onTypeXX(...)' style methods, meta/payload extraction, as well as error handling methods (see the examples, for how this is done). Basic error checking, including CRC checks, are done and errors are also reported through decoder interface.
|
||||
|
||||
The current 'onTypeXX(...)' message callback are unique for each message type (types 1, 2, 3, 4, 5, 9, 11, 18, 19, 24, 27 currently supported). No assumtions are made on default or blank values and all values are returned as integers -- the user has to scale and convert the values like position and speed to floats and the desired units.
|
||||
|
||||
The method 'enableMsgTypes(...)' can be used to enable/disable the decoding of specific messages. For example 'enableMsgTypes({1, 5})' will cause only type 1 and type 5 to be decoded internally, which could increase decoding performance, since the decoder will just skip over other message types. The method takes a list or set of integers, for example '{1, 2, 3}' or '{5}'.
|
||||
|
||||
The individual data sentences (per line) may also include meta data before or after the NMEA sentences. The decoder makes use of a sentence parser class that should be extended by the user to extract the NMEA data from each sentence (see example applications and default_sensor_parser.h). The meta data is provided as a header and a footer string to the user via one of the pure virtual methods on the decoder interface. For multi-line messages only the header and footer of the first sentence is reported (reported via 'onMessage(...)').
|
||||
|
||||
The decoder also provides access to the META and raw sentence data as messages are being decoded. The following methods can be called from inside the 'onMessage()', 'onTypeXX()' or 'onDecodeError()' methods:
|
||||
- 'header()' returns the extracted META data header
|
||||
- 'footer()' returns the extracted META data footer
|
||||
- 'payload()' returns the full NMEA payload
|
||||
- 'sentences()' returns list of original sentences that contributed
|
||||
|
||||
Some time was also spent on improving the speed of the NMEA string processing to see how quickly NMEA logs could be processed. Currently the multi-threaded file reading examples (running a thread per file) achieve more than 3M NMEA messages per second, per thread. When running on multiple logs concurrently (8 threads is a good number on modern hardware) 12M+ NMEA messages per second is possible. During testing it was also found that most of the time was spent on the 6bit nibble packing and unpacking, not the file IO.
|
||||
|
||||
SWIG is used to provide Python bindings. And the decoder can also be built and installed through python.
|
||||
|
||||
## Checklist
|
||||
- [x] Basic payload 6bit nibble stuffing and unpacking
|
||||
- [x] ASCII de-armouring
|
||||
- [x] CRC checking
|
||||
- [x] Multi-Sentence message handling
|
||||
- [x] Decoder base class
|
||||
- [x] Support types 1, 2, 3, 4, 5, 9, 11, 18, 19, 24, 27
|
||||
- [x] Test with large data-sets (files)
|
||||
- [x] Validate payload sizes (reject messages, where type and size does not match)
|
||||
- [x] Build-up message stats (bytes processed, messages processed, etc.)
|
||||
- [x] Profile and improve speed
|
||||
- [x] Validate fragment count and fragment number values
|
||||
- [x] Investigate faster ascii de-armouring and bit packing techniques (thanks to Frans van den Bergh)
|
||||
- [x] Add python interface
|
||||
- [x] Support NMEA files/data with non-standard meta data, timestamp data, etc.
|
||||
- [x] Improve multi-line performance (currently copying data, which is slow)
|
||||
- [x] Add support for custom sentence headers and footers (meta data)
|
||||
- [x] Allow for NMEA-to-NMEA filtering by storing and providing access to source sentences with each decoded message
|
||||
|
||||
- [ ] Validate talker IDs
|
||||
- [ ] Look at multiple threads/decoders working on the same file, for very large files
|
||||
- [ ] Add minimal networking to work with RTL-AIS (https://github.com/dgiardini/rtl-ais.git) and also to forward raw data
|
||||
|
||||
## Build
|
||||
This project uses CMAKE to build. To build through command line on linux, do the following:
|
||||
|
||||
- git clone https://github.com/aduvenhage/ais-decoder.git
|
||||
- mkdir ais-decoder-build
|
||||
- cd ais-decoder-build
|
||||
- cmake ../ais-decoder -DCMAKE_BUILD_TYPE=RELEASE
|
||||
- make
|
||||
- sudo make install
|
||||
|
||||
|
||||
## Examples
|
||||
The project includes some examples of how to use the AIS decoder lib.
|
||||
|
||||
|
||||
## Create a python module
|
||||
The module is built around the 'ais_quick' interface. See 'examples/quick'. This project uses [SWIG](http://www.swig.org/) to compile a python module. The SWIG interface file is located at 'python/ais_decoder.i'.
|
||||
|
||||
### To build and install using 'setuptools'
|
||||
The setup script does try to build and install the C++ library automatically and depends on CMAKE. If this fails see the [build](#build) instructions.
|
||||
|
||||
```
|
||||
cd python
|
||||
sudo python setup.py build
|
||||
sudo python setyp.py install
|
||||
```
|
||||
|
||||
### To build manually (tested with MacOS)
|
||||
Follow the decoder lib [build](#build) instructions first -- if the library is installed on the system, the SWIG steps are easier.
|
||||
|
||||
```
|
||||
cd python
|
||||
swig -Wall -c++ -python ais_decoder.i
|
||||
c++ -c -fPIC ais_decoder_wrap.cxx -I /System/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7/
|
||||
c++ -shared ais_decoder_wrap.o -lpython -lais_decoder -o _ais_decoder.so
|
||||
```
|
||||
|
||||
Make sure you use the correct python lib for the version you will be working with.
|
||||
|
||||
### Build Notes
|
||||
On Linux you will have to install the following:
|
||||
- 'cmake'
|
||||
- 'g++'
|
||||
- 'swig'
|
||||
- 'python-dev' or 'python3-dev' *for Python3*
|
||||
- 'python3-distutils' *for Python3*
|
||||
|
||||
## Import and use python module
|
||||
In python, do the following to test:
|
||||
|
||||
```
|
||||
import ais_decoder
|
||||
|
||||
str = "!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n"
|
||||
ais_decoder.pushAisSentence(str, len(str))
|
||||
|
||||
n = ais_decoder.numAisMessages()
|
||||
msg = ais_decoder.popAisMessage().asdict()
|
||||
```
|
||||
|
||||
|
||||
'pushAisSentence(...)' scans for one sentence only and sentences should always end with a newline. 'popAisMessage().asdict()' returns a Python dictionary with the message fields. Message fragments, for multi-fragment messages, are managed and stored internally. 'msg' will be empty if no output is ready yet.
|
||||
|
||||
The interface also has 'pushChunk(data, len)' that accepts any number of messages. Incomplete sentences at the end of the supplied chunk will be buffered until the next call.
|
||||
|
||||
```
|
||||
import ais_decoder
|
||||
|
||||
try:
|
||||
str = "!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n"
|
||||
ais_decoder.pushAisChunk(str, len(str))
|
||||
|
||||
n = ais_decoder.numAisMessages()
|
||||
print("num messages = ", n)
|
||||
|
||||
while True:
|
||||
if ais_decoder.numAisMessages() == 0:
|
||||
break
|
||||
|
||||
msg = ais_decoder.popAisMessage().asdict()
|
||||
print(msg)
|
||||
|
||||
except RuntimeError as err:
|
||||
print("Runtime error. ", err)
|
||||
except:
|
||||
print("Error.")
|
||||
```
|
||||
|
||||
Interface functions can throw Python 'RuntimeError' exceptions on critical errors, but message decoding errors are reported back as a AIS message with 'msg=0' and appropriate error information.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,397 @@
|
|||
|
||||
#ifndef TOOLKIT_AIS_DECODER_H
|
||||
#define TOOLKIT_AIS_DECODER_H
|
||||
|
||||
|
||||
#include "strutils.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <set>
|
||||
|
||||
|
||||
|
||||
namespace AIS
|
||||
{
|
||||
// constants
|
||||
const char ASCII_CHARS[] = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?";
|
||||
const size_t MAX_FRAGMENTS = 5;
|
||||
const size_t MAX_CHARS_PER_FRAGMENT = 82;
|
||||
|
||||
|
||||
/**
|
||||
Buffer used to push back 6 bit nibbles and extract as signed, unsigned, boolean and string values
|
||||
*/
|
||||
class PayloadBuffer
|
||||
{
|
||||
private:
|
||||
const static int MAX_PAYLOAD_SIZE = MAX_FRAGMENTS * MAX_CHARS_PER_FRAGMENT * 6 / 8 + 1;
|
||||
|
||||
public:
|
||||
PayloadBuffer();
|
||||
|
||||
/// set bit index back to zero
|
||||
void resetBitIndex();
|
||||
|
||||
/// unpack next _iBits (most significant bit is packed first)
|
||||
unsigned int getUnsignedValue(int _iBits);
|
||||
|
||||
/// unpack next _iBits (most significant bit is packed first; with sign check/conversion)
|
||||
int getSignedValue(int _iBits);
|
||||
|
||||
/// unback next boolean (1 bit)
|
||||
bool getBoolValue()
|
||||
{
|
||||
return getUnsignedValue(1) != 0;
|
||||
}
|
||||
|
||||
/// unback string (6 bit characters) -- already cleans string (removes trailing '@' and trailing spaces)
|
||||
std::string getString(int _iNumBits);
|
||||
|
||||
unsigned char* getData(void) {
|
||||
return &m_data[0];
|
||||
}
|
||||
|
||||
private:
|
||||
alignas(16) std::array<unsigned char, MAX_PAYLOAD_SIZE> m_data;
|
||||
int32_t m_iBitIndex;
|
||||
};
|
||||
|
||||
|
||||
/// Convert payload to decimal (de-armour) and concatenate 6bit decimal values into payload buffer. Returns the number of bits used.
|
||||
int decodeAscii(PayloadBuffer &_buffer, const StringRef &_strPayload, int _iFillBits);
|
||||
|
||||
/// calc CRC
|
||||
uint8_t crc(const StringRef &_strLine);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Multi-sentence messages migth span across different source buffers and input (string views) has to be stored internally.
|
||||
This class is a store of buffers for use by multi-sentence containers (see MultiSentence).
|
||||
|
||||
*/
|
||||
class MultiSentenceBufferStore
|
||||
{
|
||||
public:
|
||||
/// get a buffer to use for multi-line sentence decoding
|
||||
std::unique_ptr<Buffer> getBuffer();
|
||||
|
||||
/// return buffer to pool
|
||||
void returnBuffer(std::unique_ptr<Buffer> &_buffer);
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<Buffer>> m_buffers; ///< available buffers
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
Multi-sentence message container.
|
||||
|
||||
Multi-sentence messages migth span across different source buffers and the input has to be stored internally.
|
||||
A buffer pool (see MultiSentenceBufferStore) is used to avoid memory allocations as far as possible when storing the
|
||||
message data.
|
||||
|
||||
*/
|
||||
class MultiSentence
|
||||
{
|
||||
public:
|
||||
/// constructor -- add first fragment
|
||||
MultiSentence(int _iFragmentCount, const StringRef &_strFragment,
|
||||
const StringRef &_strLine,
|
||||
const StringRef &_strHeader, const StringRef &_strFooter,
|
||||
MultiSentenceBufferStore &_bufferStore);
|
||||
|
||||
~MultiSentence();
|
||||
|
||||
/// add more fragments (returns false if there is an fragment indexing error)
|
||||
bool addFragment(int _iFragmentNum, const StringRef &_strFragment, const StringRef &_strLine);
|
||||
|
||||
/// returns true if all fragments have been received
|
||||
bool isComplete() const;
|
||||
|
||||
/// returns full payload
|
||||
const StringRef &payload() const {return m_strPayload;}
|
||||
|
||||
/// returns full payload (ref stays valid only while this object exists and addFragment is not called)
|
||||
const StringRef &header() const {return m_strHeader;}
|
||||
|
||||
/// returns full payload (ref stays valid only while this object exists and addFragment is not called)
|
||||
const StringRef &footer() const {return m_strFooter;}
|
||||
|
||||
/// returns original sentences referenced my this multi-line sentence
|
||||
const std::vector<StringRef> &sentences() const {return m_vecLines;}
|
||||
|
||||
private:
|
||||
/// copies string view into internal buffer (adds to buffer)
|
||||
StringRef bufferString(const std::unique_ptr<Buffer> &_pBuffer, const StringRef &_str);
|
||||
|
||||
/// copies string view into internal buffer (creates new buffer)
|
||||
StringRef bufferString(const StringRef &_str);
|
||||
|
||||
protected:
|
||||
int m_iFragmentCount;
|
||||
int m_iFragmentNum;
|
||||
StringRef m_strHeader;
|
||||
StringRef m_strFooter;
|
||||
StringRef m_strPayload;
|
||||
std::vector<StringRef> m_vecLines;
|
||||
MultiSentenceBufferStore &m_bufferStore;
|
||||
std::vector<std::unique_ptr<Buffer>> m_metaBuffers;
|
||||
std::unique_ptr<Buffer> m_pPayloadBuffer;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Sentence Parser base class.
|
||||
This class can be extended to pull out NMEA string and META info from custom sentence formats.
|
||||
|
||||
The simplest implementation for this class would just return any source sentence as the full NMEA sentence:
|
||||
- 'onScanForNmea' just returns the input parameter
|
||||
- 'getHeader' returns an empty string
|
||||
- 'getFooter' returns an empty string
|
||||
- 'getTimestamp' returns 0
|
||||
|
||||
*/
|
||||
class SentenceParser
|
||||
{
|
||||
public:
|
||||
/// called to find NMEA start (scan past any headers, META data, etc.; returns NMEA payload; may be overloaded for app specific meta data)
|
||||
virtual StringRef onScanForNmea(const StringRef &_strSentence) const = 0;
|
||||
|
||||
/// calc header string from original line and extracted NMEA payload
|
||||
virtual StringRef getHeader(const StringRef &_strLine, const StringRef &_strNmea) const = 0;
|
||||
|
||||
/// calc footer string from original line and extracted NMEA payload
|
||||
virtual StringRef getFooter(const StringRef &_strLine, const StringRef &_strNmea) const = 0;
|
||||
|
||||
/// extracts the timestamp from the meta info (should return 0 if no timestamp found)
|
||||
virtual uint64_t getTimestamp(const AIS::StringRef &_strHeader, const AIS::StringRef &_strFooter) const = 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
AIS decoder base class.
|
||||
Implemented according to 'http://catb.org/gpsd/AIVDM.html'.
|
||||
|
||||
To use the decoder, call 'decodeMsg(...)' with some data until it returns 0. 'decodeMsg(...)' returns the number of bytes processed and offset should be set on each call to point to the new
|
||||
location in the buffer.
|
||||
\note: 'decodeMsg(...)' has to be called until it returns 0, to ensure that any buffered multi-line strings are backed up properly.
|
||||
|
||||
A user of the decoder has to inherit from the decoder class and implement/override 'onTypeXX(...)' style methods as well as error handling methods.
|
||||
Some user onTypeXX(...) methods are attached to multiple message types, for example: 123 (types 1, 2 & 3) and 411 (types 4 & 11), in which case the message type is the first parameter.
|
||||
|
||||
Callback sequence:
|
||||
- onSentence(..) provides raw message fragments as they are received
|
||||
- onMessage(...) provides message payload and meta info of the message being decoded
|
||||
- onTypeXX(...) provides message specific callbacks
|
||||
|
||||
Basic error checking, including CRC checks, are done and also reported.
|
||||
No assumtions are made on default or blank values -- all values are returned as integers and the user has to scale and convert the values like position and speed to floats and the desired units.
|
||||
|
||||
The method 'enableMsgTypes(...)' can be used to enable/disable the decoding of specific messages. For example 'enableMsgTypes({1, 5})' will cause only type 1 and type 5 to be decoded internally, which could
|
||||
increase decoding performance, since the decoder will just skip over other message types. The method takes a list or set of integers, for example '{1, 2, 3}' or '{5}'.
|
||||
|
||||
A SentenceParser object, supplied as a parameter to 'decodeMsg(...)', allows the decoder to support custom META data around the NMEA sentences.
|
||||
For multiline messages only the header and footer of the first sentence is reported when decoding messages (reported via 'onMessage(...)').
|
||||
|
||||
The decoder also provides access to the META and raw sentence data as messages are being decoded.
|
||||
The following methods can be called from inside the 'onMessage()', 'onTypeXX()' or 'onDecodeError()' methods:
|
||||
|
||||
- 'header()' returns the extracted META data header
|
||||
- 'footer()' returns the extracted META data footer
|
||||
- 'payload()' returns the full NMEA payload
|
||||
- 'sentences()' returns list of original sentences that contributed
|
||||
|
||||
*/
|
||||
class AisDecoder
|
||||
{
|
||||
private:
|
||||
const static int MAX_MSG_SEQUENCE_IDS = 10; ///< max multi-sentience message sequences
|
||||
const static int MAX_MSG_TYPES = 64; ///< max message type count (unique messsage IDs)
|
||||
const static int MAX_MSG_PAYLOAD_LENGTH = 82; ///< max payload length (NMEA limit)
|
||||
const static int MAX_MSG_FRAGMENTS = 5; ///< maximum number of fragments/sentences a message can have
|
||||
const static int MAX_MSG_WORDS = 10; ///< maximum number of words per sentence
|
||||
|
||||
using pfnMsgCallback = void (AisDecoder::*)(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
public:
|
||||
AisDecoder(int _iIndex = 0);
|
||||
|
||||
/// returns the user defined index
|
||||
int index() const {return m_iIndex;}
|
||||
|
||||
/**
|
||||
Enables which messages types to decode.
|
||||
An empty set will enable all message types.
|
||||
*/
|
||||
void enableMsgTypes(const std::set<int> &_types);
|
||||
|
||||
/**
|
||||
Decode next sentence (starts reading from input buffer with the specified offset; returns the number of bytes processed, or 0 when no more messages can be decoded).
|
||||
Has to be called until it returns 0, to ensure that any buffered multi-line strings are backed up properly.
|
||||
*/
|
||||
size_t decodeMsg(const char *_pNmeaBuffer, size_t _uBufferSize, size_t _uOffset, const SentenceParser &_parser);
|
||||
|
||||
/// returns the total number of messages processed
|
||||
uint64_t getTotalMessageCount() const {return m_uTotalMessages;}
|
||||
|
||||
/// returns the number of messages processed per type
|
||||
uint64_t getMessageCount(int _iMsgType) {return m_msgCounts[_iMsgType];}
|
||||
|
||||
/// returns the total number of bytes processed
|
||||
uint64_t getTotalBytes() const {return m_uTotalBytes;}
|
||||
|
||||
/// returns the number of CRC check errors
|
||||
uint64_t getCrcErrorCount() const {return m_uCrcErrors;}
|
||||
|
||||
/// returns the total number of decoding errors
|
||||
uint64_t getDecodingErrorCount() const {return m_uDecodingErrors;}
|
||||
|
||||
|
||||
// access to message info
|
||||
protected:
|
||||
/// returns the META header of the current message (valid during calls to 'onMessage' and 'onTypeXX' callbacks)
|
||||
const StringRef &header() const {return m_strHeader;}
|
||||
|
||||
/// returns the META footer of the current message (valid during calls to 'onMessage' and 'onTypeXX' callbacks)
|
||||
const StringRef &footer() const {return m_strFooter;}
|
||||
|
||||
/// returns the full NMEA string of the current message (valid during calls to 'onMessage' and 'onTypeXX' callbacks)
|
||||
const StringRef &payload() const {return m_strPayload;}
|
||||
|
||||
/// returns all the sentences that contributed to the current message (valid during calls to 'onMessage' and 'onTypeXX' callbacks)
|
||||
const std::vector<StringRef> &sentences() const {return m_vecSentences;}
|
||||
|
||||
/// returns current message timestamp, decoded from message header or footer (returns 0 if none found)
|
||||
uint64_t timestamp() const;
|
||||
|
||||
|
||||
// user defined callbacks
|
||||
protected:
|
||||
virtual void onType123(unsigned int _uMsgType, unsigned int _uMmsi, unsigned int _uNavstatus, int _iRot, unsigned int _uSog, bool _bPosAccuracy, long _iPosLon, long _iPosLat, int _iCog, int _iHeading, int _Repeat, bool _Raim, unsigned int _timestamp, unsigned int _maneuver_i) = 0;
|
||||
virtual void onType411(unsigned int _uMsgType, unsigned int _uMmsi, unsigned int _uYear, unsigned int _uMonth, unsigned int _uDay, unsigned int _uHour, unsigned int _uMinute, unsigned int _uSecond,
|
||||
bool _bPosAccuracy, int _iPosLon, int _iPosLat) = 0;
|
||||
virtual void onType5(unsigned int _uMsgType, unsigned int _uMmsi, unsigned int _uImo, const std::string &_strCallsign, const std::string &_strName,
|
||||
unsigned int _uType, unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard, unsigned int _uFixType,
|
||||
unsigned int _uEtaMonth, unsigned int _uEtaDay, unsigned int _uEtaHour, unsigned int _uEtaMinute, unsigned int _uDraught,
|
||||
const std::string &_strDestination, unsigned int _ais_version, unsigned int _repeat, bool _dte) = 0;
|
||||
|
||||
virtual void onType9(unsigned int _uMmsi, unsigned int _uSog, bool _bPosAccuracy, int _iPosLon, int _iPosLat, int _iCog, unsigned int _iAltitude) = 0;
|
||||
|
||||
virtual void onType14(unsigned int _repeat, unsigned int _uMmsi, const std::string &_strText, int _iPayloadSizeBits) = 0;
|
||||
|
||||
virtual void onType18(unsigned int _uMsgType, unsigned int _uMmsi, unsigned int _uSog, bool _bPosAccuracy,
|
||||
long _iPosLon, long _iPosLat, int _iCog, int _iHeading, bool _raim, unsigned int _repeat,
|
||||
bool _unit, bool _diplay, bool _dsc, bool _band, bool _msg22, bool _assigned,
|
||||
unsigned int _timestamp, bool _state ) = 0;
|
||||
|
||||
virtual void onType19(unsigned int _uMmsi, unsigned int _uSog, bool _bPosAccuracy, int _iPosLon, int _iPosLat, int _iCog, int _iHeading,
|
||||
const std::string &_strName, unsigned int _uType,
|
||||
unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort,
|
||||
unsigned int _uToStarboard, unsigned int timestamp, unsigned int fixtype, bool dte,
|
||||
bool assigned, unsigned int repeat, bool raim) = 0;
|
||||
|
||||
virtual void onType21(unsigned int _uMmsi, unsigned int _uAidType, const std::string &_strName, bool _bPosAccuracy, int _iPosLon, int _iPosLat,
|
||||
unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard) = 0;
|
||||
|
||||
virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, const std::string &_strName) = 0;
|
||||
|
||||
virtual void onType24B(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, const std::string &_strCallsign, unsigned int _uType, unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard, const std::string &_strVendor) = 0;
|
||||
|
||||
virtual void onType27(unsigned int _uMmsi, unsigned int _uNavstatus, unsigned int _uSog, bool _bPosAccuracy, int _iPosLon, int _iPosLat, int _iCog) = 0;
|
||||
|
||||
/// called on every sentence (raw data) received (includes all characters, including NL, CR, etc.; called before any validation or CRCs checks are performed)
|
||||
virtual void onSentence(const StringRef &_strSentence) = 0;
|
||||
|
||||
/// called on every full message, before 'onTypeXX(...)'
|
||||
virtual void onMessage(const StringRef &_strPayload, const StringRef &_strHeader, const StringRef &_strFooter) = 0;
|
||||
|
||||
/// called when message type is not supported (i.e. 'onTypeXX(...)' not implemented)
|
||||
virtual void onNotDecoded(const StringRef &_strPayload, int _iMsgType) = 0;
|
||||
|
||||
/// called when any decoding error ocurred
|
||||
virtual void onDecodeError(const StringRef &_strPayload, const std::string &_strError) = 0;
|
||||
|
||||
/// called when any parsing error ocurred
|
||||
virtual void onParseError(const StringRef &_strLine, const std::string &_strError) = 0;
|
||||
|
||||
private:
|
||||
/// enable/disable msg callback
|
||||
void setMsgCallback(int _iType, pfnMsgCallback _pfnCb, bool _bEnabled);
|
||||
|
||||
/// enable/disable msg callback
|
||||
void setMsgCallback(int _iType, pfnMsgCallback _pfnCb, const std::set<int> &_enabledTypes);
|
||||
|
||||
/// check sentence CRC
|
||||
bool checkCrc(const StringRef &_strPayload);
|
||||
|
||||
/// check talker id
|
||||
bool checkTalkerId(const StringRef &_strTalkerId);
|
||||
|
||||
/// decode Mobile AIS station message
|
||||
void decodeMobileAisMsg(const StringRef &_strPayload, int _iFillBits);
|
||||
|
||||
/// decode Position Report (class A; type nibble already pulled from buffer)
|
||||
void decodeType123(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Base Station Report (type nibble already pulled from buffer; or, response to inquiry)
|
||||
void decodeType411(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Voyage Report and Static Data (type nibble already pulled from buffer)
|
||||
void decodeType5(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Standard SAR Aircraft Position Report
|
||||
void decodeType9(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Standard SAR Aircraft Position Report
|
||||
void decodeType11(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Safety related Broadcast Message
|
||||
void decodeType14(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Position Report (class B; type nibble already pulled from buffer)
|
||||
void decodeType18(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Position Report (class B; type nibble already pulled from buffer)
|
||||
void decodeType19(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Aid-to-Navigation Report
|
||||
void decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Voyage Report and Static Data (type nibble already pulled from buffer)
|
||||
void decodeType24(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
/// decode Long Range AIS Broadcast message (type nibble already pulled from buffer)
|
||||
void decodeType27(PayloadBuffer &_buffer, unsigned int _uMsgType, int _iPayloadSizeBits);
|
||||
|
||||
private:
|
||||
int m_iIndex; ///< arbitrary id/index set by user for this decoder
|
||||
|
||||
PayloadBuffer m_binaryBuffer; ///< used internally to decode NMEA payloads
|
||||
std::array<std::unique_ptr<MultiSentence>, MAX_MSG_SEQUENCE_IDS> m_multiSentences; ///< used internally to buffer multi-line message sentences
|
||||
std::array<StringRef, MAX_MSG_WORDS> m_words; ///< used internally to buffer NMEA words
|
||||
MultiSentenceBufferStore m_multiSentenceBuffers;
|
||||
|
||||
std::vector<StringRef> m_vecSentences; ///< all NMEA/raw sentences for message - stored for each message just before user callbacks
|
||||
StringRef m_strHeader; ///< extracted META header
|
||||
StringRef m_strFooter; ///< extracted META header
|
||||
StringRef m_strPayload; ///< extracted full payload (concatenated fragments; ascii)
|
||||
|
||||
std::array<uint64_t, MAX_MSG_TYPES> m_msgCounts; ///< message counts per message type
|
||||
uint64_t m_uTotalMessages;
|
||||
uint64_t m_uTotalBytes;
|
||||
uint64_t m_uCrcErrors; ///< CRC check error count
|
||||
uint64_t m_uDecodingErrors; ///< decoding error count (includes CRC errors)
|
||||
|
||||
std::array<pfnMsgCallback, 100> m_vecMsgCallbacks; ///< message decoding functions mapped to message IDs
|
||||
};
|
||||
|
||||
|
||||
}; // namespace AIS
|
||||
|
||||
|
||||
#endif // #ifndef TOOLKIT_AIS_DECODER_H
|
|
@ -0,0 +1,152 @@
|
|||
|
||||
#include "default_sentence_parser.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
|
||||
using namespace AIS;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Called to find NMEA start (scan past any headers, META data, etc.; returns NMEA payload)
|
||||
This implementation will scan past META data that start and end with a '\'. It will also stop at NMEA CRC.
|
||||
|
||||
*/
|
||||
StringRef DefaultSentenceParser::onScanForNmea(const StringRef &_strSentence) const
|
||||
{
|
||||
const char *pPayloadStart = _strSentence.data();
|
||||
size_t uPayloadSize = _strSentence.size();
|
||||
|
||||
// check for common META data block headers
|
||||
const char *pCh = pPayloadStart;
|
||||
if (*pCh == '\\')
|
||||
{
|
||||
// find META data block end
|
||||
pCh = (const char*)memchr(pCh + 1, '\\', uPayloadSize - 1);
|
||||
if (pCh != nullptr)
|
||||
{
|
||||
pPayloadStart = pCh + 1;
|
||||
uPayloadSize = _strSentence.size() - (pPayloadStart - _strSentence.data());
|
||||
}
|
||||
else
|
||||
{
|
||||
uPayloadSize = 0;
|
||||
}
|
||||
}
|
||||
else if (std::strncmp(pCh, "$P", 2) == 0)
|
||||
{
|
||||
uPayloadSize = 0;
|
||||
}
|
||||
|
||||
// find payload size (using crc '*' plus 2 chars for crc value)
|
||||
if (uPayloadSize > 0)
|
||||
{
|
||||
pCh = (const char*)memchr(pPayloadStart, '*', uPayloadSize);
|
||||
if (pCh != nullptr)
|
||||
{
|
||||
uPayloadSize = pCh + 3 - pPayloadStart;
|
||||
}
|
||||
else
|
||||
{
|
||||
uPayloadSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return StringRef(pPayloadStart, uPayloadSize);
|
||||
}
|
||||
|
||||
/* calc header string from original line and extracted NMEA payload */
|
||||
StringRef DefaultSentenceParser::getHeader(const StringRef &_strLine, const StringRef &_strNmea) const
|
||||
{
|
||||
StringRef strHeader(_strLine.data(), 0);
|
||||
|
||||
if (_strNmea.data() > _strLine.data())
|
||||
{
|
||||
strHeader = _strLine.substr(0, _strNmea.data() - _strLine.data());
|
||||
|
||||
// remove last '\'
|
||||
if ( (strHeader.empty() == false) &&
|
||||
(strHeader[strHeader.size() - 1] == '\\') )
|
||||
{
|
||||
strHeader.remove_suffix(1);
|
||||
}
|
||||
|
||||
// remove first '\\'
|
||||
if ( (strHeader.empty() == false) &&
|
||||
(strHeader[0] == '\\') )
|
||||
{
|
||||
strHeader.remove_prefix(1);
|
||||
}
|
||||
}
|
||||
|
||||
return strHeader;
|
||||
}
|
||||
|
||||
/* calc footer string from original line and extracted NMEA payload */
|
||||
StringRef DefaultSentenceParser::getFooter(const StringRef &_strLine, const StringRef &_strNmea) const
|
||||
{
|
||||
StringRef strFooter(_strLine.data(), 0);
|
||||
|
||||
const char *pLineEnd = _strLine.data() + _strLine.size();
|
||||
const char *pNmeaEnd = _strNmea.data() + _strNmea.size();
|
||||
|
||||
if (pLineEnd > pNmeaEnd)
|
||||
{
|
||||
// NOTE: '_strLine' will end with <CR><LF> or <LF>
|
||||
strFooter = StringRef(pNmeaEnd, pLineEnd - pNmeaEnd - 1);
|
||||
|
||||
// remove last '<CR>'
|
||||
if ( (strFooter.empty() == false) &&
|
||||
(strFooter[strFooter.size() - 1] == '\r') )
|
||||
{
|
||||
strFooter.remove_suffix(1);
|
||||
}
|
||||
|
||||
// remove first ','
|
||||
if ( (strFooter.empty() == false) &&
|
||||
(strFooter[0] == ',') )
|
||||
{
|
||||
strFooter.remove_prefix(1);
|
||||
}
|
||||
}
|
||||
|
||||
return strFooter;
|
||||
}
|
||||
|
||||
/* extracts the timestamp from the meta info */
|
||||
uint64_t DefaultSentenceParser::getTimestamp(const AIS::StringRef &_strHeader, const AIS::StringRef &_strFooter) const
|
||||
{
|
||||
uint64_t uTimestamp = 0;
|
||||
|
||||
// try to get timestamp from header
|
||||
// NOTE: assumes header has comma seperated fields with 'c:' identifying unix timestamp
|
||||
if (_strHeader.size() > 0)
|
||||
{
|
||||
// seperate header into words
|
||||
std::array<AIS::StringRef, 8> words;
|
||||
size_t n = AIS::seperate<','>(words, _strHeader);
|
||||
|
||||
// find timestamp
|
||||
for (size_t i = 0; i < n; i++)
|
||||
{
|
||||
const auto &word = words[i];
|
||||
if ( (word.empty() == false) &&
|
||||
(word[0] == 'c') )
|
||||
{
|
||||
uTimestamp = (uint64_t)std::strtoul(word.data()+2, nullptr, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to get timestamp from footer
|
||||
// NOTE: assumes footer first word as timestamp
|
||||
if ( (_strFooter.empty() == false) &&
|
||||
(uTimestamp == 0) )
|
||||
{
|
||||
uTimestamp = (uint64_t)std::strtoul(_strFooter.data()+1, nullptr, 10);
|
||||
}
|
||||
|
||||
return uTimestamp;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
#ifndef TOOLKIT_AIS_DEFAULT_PARSER_H
|
||||
#define TOOLKIT_AIS_DEFAULT_PARSER_H
|
||||
|
||||
|
||||
#include "ais_decoder.h"
|
||||
|
||||
|
||||
|
||||
namespace AIS
|
||||
{
|
||||
/**
|
||||
Default Sentence Parser
|
||||
|
||||
This implementation will scan past META data that start and end with a '\'. It will also stop at NMEA CRC.
|
||||
The META data footer and header are calculated based on the start and the end of the NMEA string in each sentence.
|
||||
|
||||
*/
|
||||
class DefaultSentenceParser : public SentenceParser
|
||||
{
|
||||
public:
|
||||
/// called to find NMEA start (scan past any headers, META data, etc.; returns NMEA payload)
|
||||
virtual StringRef onScanForNmea(const StringRef &_strSentence) const override;
|
||||
|
||||
/// calc header string from original line and extracted NMEA payload
|
||||
virtual StringRef getHeader(const StringRef &_strLine, const StringRef &_strNmea) const override;
|
||||
|
||||
/// calc footer string from original line and extracted NMEA payload
|
||||
virtual StringRef getFooter(const StringRef &_strLine, const StringRef &_strNmea) const override;
|
||||
|
||||
/// extracts the timestamp from the meta info
|
||||
virtual uint64_t getTimestamp(const AIS::StringRef &_strHeader, const AIS::StringRef &_strFooter) const override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
}; // namespace AIS
|
||||
|
||||
|
||||
#endif // #ifndef TOOLKIT_AIS_DEFAULT_PARSER_H
|
|
@ -0,0 +1,363 @@
|
|||
#ifndef AIS_STR_UTIL_H
|
||||
#define AIS_STR_UTIL_H
|
||||
|
||||
|
||||
#include <cstring>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
namespace
|
||||
{
|
||||
//#if !defined(_GNU_SOURCE)
|
||||
// from 'https://github.com/freebsd/freebsd/blob/master/lib/libc/string/memrchr.c'
|
||||
inline void *memrchr(const void *s, int c, size_t n)
|
||||
{
|
||||
const unsigned char *cp;
|
||||
|
||||
if (n != 0) {
|
||||
cp = (unsigned char *)s + n;
|
||||
do {
|
||||
if (*(--cp) == (unsigned char)c) {
|
||||
return((void *)cp);
|
||||
}
|
||||
} while (--n != 0);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
//#endif
|
||||
};
|
||||
|
||||
*/
|
||||
|
||||
|
||||
namespace AIS
|
||||
{
|
||||
inline char ascii_toupper(char _ch)
|
||||
{
|
||||
if (_ch <= 'z' && _ch >= 'a') return _ch - 32;
|
||||
else return _ch;
|
||||
}
|
||||
|
||||
inline int ascii_stricmp(const std::string &_a, const std::string &_b)
|
||||
{
|
||||
const char *pChA = _a.c_str();
|
||||
const char *pChB = _b.c_str();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
char chA = *pChA++;
|
||||
char chB = *pChB++;
|
||||
|
||||
if ((chA == '\0') && (chB == '\0')) return 0;
|
||||
else if (chA == '\0') return -1;
|
||||
else if (chB == '\0') return 1;
|
||||
|
||||
chA = ascii_toupper(chA);
|
||||
chB = ascii_toupper(chB);
|
||||
|
||||
if (chA < chB) return -1;
|
||||
else if (chA > chB) return 1;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool ascii_isspace(char _ch)
|
||||
{
|
||||
return (_ch == ' ') || (_ch == '\t') || (_ch == '\n') || (_ch == '\r');
|
||||
}
|
||||
|
||||
/** strip trailing chars after and including '_chStrip' */
|
||||
inline std::string &stripTrailingAll(std::string &_str, char _chStrip)
|
||||
{
|
||||
const char *pNext = (const char*)memchr(_str.data(), _chStrip, _str.size());
|
||||
if (pNext != nullptr)
|
||||
{
|
||||
_str.resize(pNext - _str.data());
|
||||
}
|
||||
|
||||
return _str;
|
||||
}
|
||||
|
||||
/** strip trailing chars after and including '_chStrip' */
|
||||
inline std::string stripTrailingAll(const std::string &_str, char _chStrip)
|
||||
{
|
||||
std::string ret(_str);
|
||||
stripTrailingAll((std::string&)ret, _chStrip);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** strip trailing whitespace */
|
||||
inline std::string &stripTrailingWhitespace(std::string &_str)
|
||||
{
|
||||
const char *pStrStart = (const char *)_str.data();
|
||||
const char *pStrEnd = (const char *)_str.data() + _str.size();
|
||||
|
||||
while (pStrEnd > pStrStart)
|
||||
{
|
||||
if (ascii_isspace(*(--pStrEnd)) == false)
|
||||
{
|
||||
size_t n = pStrEnd - pStrStart + 1;
|
||||
if (n != _str.size())
|
||||
{
|
||||
_str.resize(n);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _str;
|
||||
}
|
||||
|
||||
/** strip trailing chars after and including '_chStrip' */
|
||||
inline std::string stripTrailingWhitespace(const std::string &_str)
|
||||
{
|
||||
std::string ret;
|
||||
stripTrailingWhitespace((std::string&)_str);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
String view that just references data from another buffer.
|
||||
Minimum string view implementation to work with pre-c++17 STL.
|
||||
*/
|
||||
class StringRef
|
||||
{
|
||||
public:
|
||||
static const size_t npos = size_t(-1);
|
||||
|
||||
public:
|
||||
StringRef()
|
||||
:m_pData(nullptr),
|
||||
m_uSize(0)
|
||||
{}
|
||||
|
||||
StringRef(const char *_pData, size_t _uSize)
|
||||
:m_pData(_pData),
|
||||
m_uSize(_uSize)
|
||||
{}
|
||||
|
||||
const char *data() const {return m_pData;}
|
||||
size_t size() const {return m_uSize;}
|
||||
bool empty() const {return m_uSize == 0;}
|
||||
|
||||
StringRef substr(size_t _pos = 0, size_t _count = npos) const {
|
||||
StringRef ret;
|
||||
|
||||
if ( (_count == npos) &&
|
||||
(_pos < m_uSize) ) {
|
||||
ret.m_pData = m_pData + _pos;
|
||||
ret.m_uSize = m_uSize - _pos;
|
||||
}
|
||||
else if (_pos + _count < m_uSize) {
|
||||
ret.m_pData = m_pData + _pos;
|
||||
ret.m_uSize = _count;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const char operator [] (size_t _i) const {
|
||||
return m_pData[_i];
|
||||
}
|
||||
|
||||
void remove_prefix(size_t _n) {
|
||||
if (_n < m_uSize) {
|
||||
m_pData += _n;
|
||||
m_uSize -= _n;
|
||||
}
|
||||
else {
|
||||
m_pData += m_uSize;
|
||||
m_uSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void remove_suffix(size_t _n) {
|
||||
if (_n < m_uSize) {
|
||||
m_uSize -= _n;
|
||||
}
|
||||
else {
|
||||
m_uSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
operator std::string () const {
|
||||
return std::string(m_pData, m_uSize);
|
||||
}
|
||||
|
||||
private:
|
||||
const char *m_pData;
|
||||
size_t m_uSize;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Lightweight buffer for processing data chunks.
|
||||
Internal buffer is allowed to grow, but not shrink.
|
||||
This avoids allocation and resize init overheads, if the buffer is reused for multiple chunks.
|
||||
*/
|
||||
struct Buffer
|
||||
{
|
||||
Buffer()
|
||||
:m_uSize(0)
|
||||
{}
|
||||
|
||||
Buffer(size_t _uReservedSize)
|
||||
:m_data(_uReservedSize, 0),
|
||||
m_uSize(0)
|
||||
{}
|
||||
|
||||
const char *data() const {return m_data.data();}
|
||||
char *data() {return (char*)m_data.data();}
|
||||
|
||||
size_t size() const {return m_uSize;}
|
||||
|
||||
void resize(size_t _uSize) {
|
||||
m_uSize = _uSize;
|
||||
if (m_uSize > m_data.size()) {
|
||||
m_data.resize(m_uSize);
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
m_uSize = 0;
|
||||
}
|
||||
|
||||
void append(const char *_pData, size_t _uSize) {
|
||||
if ( (_uSize > 0) &&
|
||||
(_pData != nullptr) )
|
||||
{
|
||||
size_t uOffset = size();
|
||||
resize(uOffset + _uSize);
|
||||
memcpy(data() + uOffset, _pData, _uSize);
|
||||
}
|
||||
}
|
||||
|
||||
void pop_front(size_t _uSize) {
|
||||
if (_uSize < m_uSize) {
|
||||
std::memmove((char*)m_data.data(), (char*)m_data.data() + _uSize, m_uSize - _uSize);
|
||||
m_uSize -= _uSize;
|
||||
}
|
||||
else {
|
||||
m_uSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<char> m_data;
|
||||
size_t m_uSize;
|
||||
};
|
||||
|
||||
|
||||
/// find the last of '_ch' in _str
|
||||
inline size_t findLastOf(const StringRef &_str, char _ch)
|
||||
{
|
||||
if (_str.size() > 0)
|
||||
{
|
||||
const char *pNext = (const char *)memrchr(_str.data(), _ch, _str.size());
|
||||
if (pNext != nullptr)
|
||||
{
|
||||
return pNext - _str.data();
|
||||
}
|
||||
}
|
||||
|
||||
return (size_t)-1; // npos
|
||||
}
|
||||
|
||||
/// Converts string to an integer. Returns 0 if conversion failed.
|
||||
inline int strtoi(const StringRef &_str)
|
||||
{
|
||||
// \note: this is a bit of a hack (might scan past end of _str, since not zero terminated, but will be terminated by a comma or end-of-line)
|
||||
return (int)std::strtol(_str.data(), nullptr, 10);
|
||||
}
|
||||
|
||||
/// Converts a single-digit string to an integer. Quick and dirty with no error checking, but guaranteed to at
|
||||
/// least clamp the result to the range [0,9]
|
||||
inline int single_digit_strtoi(const StringRef &_str)
|
||||
{
|
||||
return ((_str.data()[0] - '0') & 0x0f) % 10;
|
||||
}
|
||||
|
||||
/**
|
||||
Appends first line of text from input, starting at _uOffset (works with <LF> "\n" and <CR><LF> "\r\n").
|
||||
Returns the number of bytes processed (output includes CR & LF chars).
|
||||
*/
|
||||
inline size_t getLine(StringRef &_strOutput, const char *_pInput, size_t _uInputSize, size_t _uOffset)
|
||||
{
|
||||
const size_t n = _uInputSize - _uOffset;
|
||||
const char *pData = _pInput + _uOffset;
|
||||
const char *pSentinel = pData + n;
|
||||
|
||||
// find NL/LF
|
||||
const char *next = (const char*)memchr(pData, '\n', n);
|
||||
|
||||
if (next == nullptr || next >= pSentinel) {
|
||||
return 0;
|
||||
} else {
|
||||
// \note getLine() output includes <CR> and <LF> chars
|
||||
int nb = (int)(next - pData + 1);
|
||||
|
||||
// scan past broken line seperator pairs
|
||||
while (*pData == '\r') {
|
||||
pData++;
|
||||
}
|
||||
|
||||
// create output
|
||||
_strOutput = StringRef(pData, nb);
|
||||
|
||||
return nb;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Separate input string into words using the delimiter.
|
||||
The output vector is not resized and this function will not return more words than the size of the output.
|
||||
Returns the number of words added to output, starting at index 0.
|
||||
*/
|
||||
template <char CH, typename output_t>
|
||||
size_t seperate(output_t &_output, const StringRef &_strInput)
|
||||
{
|
||||
const char *pCh = _strInput.data();
|
||||
const char *pChEnd = pCh + _strInput.size();
|
||||
size_t uWordCount = 0;
|
||||
|
||||
while ( (pCh < pChEnd) &&
|
||||
(uWordCount < _output.size()) ) {
|
||||
|
||||
const char* next = (const char*)memchr(pCh, CH, pChEnd - pCh);
|
||||
if (next == nullptr || next > pChEnd) {
|
||||
// no comma found, assume we are in the last word
|
||||
next = pChEnd;
|
||||
}
|
||||
|
||||
_output[uWordCount] = StringRef(pCh, next - pCh);
|
||||
uWordCount++;
|
||||
|
||||
pCh = next + 1; // continue after comma
|
||||
}
|
||||
|
||||
return uWordCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}; // namespace AIS
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#endif // #ifndef AIS_STR_UTIL_H
|
|
@ -73,6 +73,16 @@ public:
|
|||
ConverterEntry e(1,lpgn, converter);
|
||||
converters[sentence] = e;
|
||||
}
|
||||
void registerConverter(int num,unsigned long *lpgn, String sentence, Converter converter)
|
||||
{
|
||||
ConverterEntry e(num,lpgn, converter);
|
||||
converters[sentence] = e;
|
||||
}
|
||||
void registerConverter(int num,unsigned long *lpgn, String sentence, LConverter converter)
|
||||
{
|
||||
ConverterEntry e(num,lpgn, converter);
|
||||
converters[sentence] = e;
|
||||
}
|
||||
void registerConverter(unsigned long pgn, unsigned long pgn2, String sentence, Converter converter)
|
||||
{
|
||||
unsigned long *lpgn=new unsigned long[2]{pgn,pgn2};
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
/*
|
||||
This code is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
This code is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
//imported from https://github.com/AK-Homberger/NMEA2000-AIS-Gateway/tree/main/MyAISToN2k
|
||||
#include <Arduino.h>
|
||||
#include "ais_decoder.h"
|
||||
#include "default_sentence_parser.h"
|
||||
#include "NMEA0183DataToN2K.h"
|
||||
#include "NMEA0183.h"
|
||||
|
||||
const double pi = 3.1415926535897932384626433832795;
|
||||
const double knToms = 1852.0 / 3600.0;
|
||||
const double degToRad = pi / 180.0;
|
||||
const double nmTom = 1.852 * 1000;
|
||||
|
||||
uint16_t DaysSince1970 = 0;
|
||||
|
||||
class MyAisDecoder : public AIS::AisDecoder
|
||||
{
|
||||
private:
|
||||
NMEA0183DataToN2K::N2kSender sender;
|
||||
void send(const tN2kMsg &msg){
|
||||
(*sender)(msg);
|
||||
}
|
||||
AIS::DefaultSentenceParser parser;
|
||||
public:
|
||||
MyAisDecoder(NMEA0183DataToN2K::N2kSender sender)
|
||||
{
|
||||
this->sender=sender;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void onType123(unsigned int _uMsgType, unsigned int _uMmsi, unsigned int _uNavstatus,
|
||||
int _iRot, unsigned int _uSog, bool _bPosAccuracy,
|
||||
long _iPosLon, long _iPosLat, int _iCog, int _iHeading, int _Repeat, bool _Raim,
|
||||
unsigned int _timestamp, unsigned int _maneuver_i) override {
|
||||
|
||||
// Serial.println("123");
|
||||
|
||||
tN2kMsg N2kMsg;
|
||||
|
||||
// PGN129038
|
||||
SetN2kAISClassAPosition(N2kMsg, _uMsgType, (tN2kAISRepeat)_Repeat, _uMmsi,
|
||||
_iPosLat / 600000.0, _iPosLon / 600000.0,
|
||||
_bPosAccuracy, _Raim, _timestamp,
|
||||
_iCog * degToRad, _uSog * knToms / 10.0,
|
||||
_iHeading * degToRad, _iRot, (tN2kAISNavStatus)_uNavstatus);
|
||||
|
||||
send(N2kMsg);
|
||||
}
|
||||
|
||||
virtual void onType411(unsigned int , unsigned int , unsigned int , unsigned int , unsigned int , unsigned int , unsigned int , unsigned int , bool , int , int ) override {
|
||||
//Serial.println("411");
|
||||
}
|
||||
|
||||
virtual void onType5(unsigned int _uMsgType, unsigned int _uMmsi, unsigned int _uImo, const std::string &_strCallsign,
|
||||
const std::string &_strName,
|
||||
unsigned int _uType, unsigned int _uToBow, unsigned int _uToStern,
|
||||
unsigned int _uToPort, unsigned int _uToStarboard, unsigned int _uFixType,
|
||||
unsigned int _uEtaMonth, unsigned int _uEtaDay, unsigned int _uEtaHour,
|
||||
unsigned int _uEtaMinute, unsigned int _uDraught,
|
||||
const std::string &_strDestination, unsigned int _ais_version,
|
||||
unsigned int _repeat, bool _dte) override {
|
||||
|
||||
// Serial.println("5");
|
||||
|
||||
// Necessary due to conflict with TimeLib.h (redefinition of tmElements_t)
|
||||
|
||||
time_t t = DaysSince1970 * (24UL * 3600UL);
|
||||
tmElements_t tm;
|
||||
|
||||
tNMEA0183Msg::breakTime(t, tm);
|
||||
|
||||
//tNMEA0183Msg::SetYear(tm, 2020);
|
||||
tNMEA0183Msg::SetMonth(tm, _uEtaMonth);
|
||||
tNMEA0183Msg::SetDay(tm, _uEtaDay);
|
||||
tNMEA0183Msg::SetHour(tm, 0);
|
||||
tNMEA0183Msg::SetMin(tm, 0);
|
||||
tNMEA0183Msg::SetSec(tm, 0);
|
||||
|
||||
uint16_t eta_days = tNMEA0183Msg::makeTime(tm) / (24UL * 3600UL);
|
||||
|
||||
tN2kMsg N2kMsg;
|
||||
char CS[30];
|
||||
char Name[30];
|
||||
char Dest[30];
|
||||
|
||||
strncpy(CS, _strCallsign.c_str(), sizeof(CS));
|
||||
strncpy(Name, _strName.c_str(), sizeof(Name));
|
||||
strncpy(Dest, _strDestination.c_str(), sizeof(Dest));
|
||||
|
||||
// PGN129794
|
||||
SetN2kAISClassAStatic(N2kMsg, _uMsgType, (tN2kAISRepeat) _repeat, _uMmsi,
|
||||
_uImo, CS, Name, _uType, _uToBow + _uToStern,
|
||||
_uToPort + _uToStarboard, _uToStarboard, _uToBow, eta_days,
|
||||
(_uEtaHour * 3600) + (_uEtaMinute * 60), _uDraught / 10.0, Dest,
|
||||
(tN2kAISVersion) _ais_version, (tN2kGNSStype) _uFixType,
|
||||
(tN2kAISDTE) _dte, (tN2kAISTranceiverInfo) _ais_version);
|
||||
|
||||
send(N2kMsg);
|
||||
}
|
||||
|
||||
virtual void onType9(unsigned int , unsigned int , bool , int , int , int , unsigned int ) override {
|
||||
//Serial.println("9");
|
||||
}
|
||||
|
||||
virtual void onType14(unsigned int _repeat, unsigned int _uMmsi,
|
||||
const std::string &_strText, int _iPayloadSizeBits) override {
|
||||
|
||||
tN2kMsg N2kMsg;
|
||||
char Text[162];
|
||||
strncpy(Text, _strText.c_str(), sizeof(Text));
|
||||
|
||||
N2kMsg.SetPGN(129802UL);
|
||||
N2kMsg.Priority = 4;
|
||||
N2kMsg.Destination = 255; // Redundant, PGN129802 is broadcast by default.
|
||||
N2kMsg.AddByte((_repeat & 0x03) << 6 | (14 & 0x3f));
|
||||
N2kMsg.Add4ByteUInt(_uMmsi);
|
||||
N2kMsg.AddByte(0);
|
||||
|
||||
if (strlen(Text) == 0) {
|
||||
N2kMsg.AddByte(0x03); N2kMsg.AddByte(0x01); N2kMsg.AddByte(0x00);
|
||||
} else {
|
||||
N2kMsg.AddByte(strlen(Text) + 2); N2kMsg.AddByte(0x01);
|
||||
for (int i = 0; i < strlen(Text); i++)
|
||||
N2kMsg.AddByte(Text[i]);
|
||||
}
|
||||
|
||||
send(N2kMsg);
|
||||
}
|
||||
|
||||
virtual void onType18(unsigned int _uMsgType, unsigned int _uMmsi, unsigned int _uSog, bool _bPosAccuracy,
|
||||
long _iPosLon, long _iPosLat, int _iCog, int _iHeading, bool _raim, unsigned int _repeat,
|
||||
bool _unit, bool _diplay, bool _dsc, bool _band, bool _msg22, bool _assigned,
|
||||
unsigned int _timestamp, bool _state ) override {
|
||||
//Serial.println("18");
|
||||
|
||||
tN2kMsg N2kMsg;
|
||||
|
||||
// PGN129039
|
||||
SetN2kAISClassBPosition(N2kMsg, _uMsgType, (tN2kAISRepeat) _repeat, _uMmsi,
|
||||
_iPosLat / 600000.0, _iPosLon / 600000.0, _bPosAccuracy, _raim,
|
||||
_timestamp, _iCog * degToRad, _uSog * knToms / 10.0,
|
||||
_iHeading * degToRad, (tN2kAISUnit) _unit,
|
||||
_diplay, _dsc, _band, _msg22, (tN2kAISMode) _assigned, _state);
|
||||
|
||||
send(N2kMsg);
|
||||
}
|
||||
|
||||
virtual void onType19(unsigned int _uMmsi, unsigned int _uSog, bool _bPosAccuracy, int _iPosLon, int _iPosLat,
|
||||
int _iCog, int _iHeading, const std::string &_strName, unsigned int _uType,
|
||||
unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort,
|
||||
unsigned int _uToStarboard, unsigned int _timestamp, unsigned int _fixtype,
|
||||
bool _dte, bool _assigned, unsigned int _repeat, bool _raim) override {
|
||||
|
||||
//Serial.println("19");
|
||||
tN2kMsg N2kMsg;
|
||||
|
||||
// PGN129040
|
||||
|
||||
char Name[21] = "";
|
||||
strncpy(Name, _strName.c_str(), sizeof(Name));
|
||||
|
||||
N2kMsg.SetPGN(129040UL);
|
||||
N2kMsg.Priority = 4;
|
||||
N2kMsg.AddByte((_repeat & 0x03) << 6 | (19 & 0x3f));
|
||||
N2kMsg.Add4ByteUInt(_uMmsi);
|
||||
N2kMsg.Add4ByteDouble(_iPosLon / 600000.0, 1e-07);
|
||||
N2kMsg.Add4ByteDouble(_iPosLat / 600000.0, 1e-07);
|
||||
N2kMsg.AddByte((_timestamp & 0x3f) << 2 | (_raim & 0x01) << 1 | (_bPosAccuracy & 0x01));
|
||||
N2kMsg.Add2ByteUDouble(_iCog * degToRad, 1e-04);
|
||||
N2kMsg.Add2ByteUDouble(_uSog * knToms / 10.0, 0.01);
|
||||
N2kMsg.AddByte(0xff); // Regional Application
|
||||
N2kMsg.AddByte(0xff); // Regional Application
|
||||
N2kMsg.AddByte(_uType );
|
||||
N2kMsg.Add2ByteUDouble(_iHeading * degToRad, 1e-04);
|
||||
N2kMsg.AddByte(_fixtype << 4);
|
||||
N2kMsg.Add2ByteDouble(_uToBow + _uToStern, 0.1);
|
||||
N2kMsg.Add2ByteDouble(_uToPort + _uToStarboard, 0.1);
|
||||
N2kMsg.Add2ByteDouble(_uToStarboard, 0.1);
|
||||
N2kMsg.Add2ByteDouble(_uToBow, 0.1);
|
||||
N2kMsg.AddStr(Name, 20);
|
||||
N2kMsg.AddByte((_dte & 0x01) | (_assigned & 0x01) << 1) ;
|
||||
N2kMsg.AddByte(0);
|
||||
|
||||
send(N2kMsg);
|
||||
}
|
||||
|
||||
|
||||
virtual void onType21(unsigned int , unsigned int , const std::string &, bool , int , int , unsigned int , unsigned int , unsigned int , unsigned int ) override {
|
||||
//Serial.println("21");
|
||||
}
|
||||
|
||||
virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi,
|
||||
const std::string &_strName) override {
|
||||
//Serial.println("24A");
|
||||
|
||||
tN2kMsg N2kMsg;
|
||||
char Name[30];
|
||||
strncpy(Name, _strName.c_str(), sizeof(Name));
|
||||
|
||||
// PGN129809
|
||||
SetN2kAISClassBStaticPartA(N2kMsg, _uMsgType, (tN2kAISRepeat) _repeat, _uMmsi, Name);
|
||||
|
||||
send(N2kMsg);
|
||||
}
|
||||
|
||||
virtual void onType24B(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi,
|
||||
const std::string &_strCallsign, unsigned int _uType,
|
||||
unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort,
|
||||
unsigned int _uToStarboard, const std::string &_strVendor) override {
|
||||
|
||||
// Serial.println("24B");
|
||||
|
||||
|
||||
tN2kMsg N2kMsg;
|
||||
char CS[30];
|
||||
char Vendor[30];
|
||||
|
||||
strncpy(CS, _strCallsign.c_str(), sizeof(CS));
|
||||
strncpy(Vendor, _strVendor.c_str(), sizeof(Vendor));
|
||||
|
||||
// PGN129810
|
||||
SetN2kAISClassBStaticPartB(N2kMsg, _uMsgType, (tN2kAISRepeat)_repeat, _uMmsi,
|
||||
_uType, Vendor, CS, _uToBow + _uToStern, _uToPort + _uToStarboard,
|
||||
_uToStarboard, _uToBow, _uMmsi);
|
||||
|
||||
send(N2kMsg);
|
||||
}
|
||||
|
||||
virtual void onType27(unsigned int , unsigned int , unsigned int , bool , int , int , int ) override {
|
||||
//Serial.println("27");
|
||||
}
|
||||
|
||||
virtual void onSentence(const AIS::StringRef &_Stc) override {
|
||||
//Serial.printf("Sentence: %s\n", _Stc);
|
||||
}
|
||||
|
||||
virtual void onMessage(const AIS::StringRef &, const AIS::StringRef &, const AIS::StringRef &) override {}
|
||||
|
||||
virtual void onNotDecoded(const AIS::StringRef &, int ) override {}
|
||||
|
||||
virtual void onDecodeError(const AIS::StringRef &_strMessage, const std::string &_strError) override {
|
||||
std::string msg(_strMessage.data(), _strMessage.size());
|
||||
AIS::stripTrailingWhitespace(msg);
|
||||
|
||||
Serial.printf("%s [%s]\n", _strError.c_str(), msg.c_str());
|
||||
}
|
||||
|
||||
virtual void onParseError(const AIS::StringRef &_strMessage, const std::string &_strError) override {
|
||||
std::string msg(_strMessage.data(), _strMessage.size());
|
||||
AIS::stripTrailingWhitespace(msg);
|
||||
|
||||
Serial.printf("%s [%s]\n", _strError.c_str(), msg.c_str());
|
||||
}
|
||||
public:
|
||||
void handleMessage(const char * msg){
|
||||
int len=strlen(msg);
|
||||
char buffer[len+1];
|
||||
memcpy(buffer,msg,len);
|
||||
strcat(buffer,"\n");
|
||||
size_t i=0;
|
||||
do {
|
||||
i=decodeMsg(buffer,len+1,i,parser);
|
||||
} while (i != 0) ;
|
||||
}
|
||||
};
|
|
@ -4,6 +4,7 @@
|
|||
#include "ConverterList.h"
|
||||
#include <map>
|
||||
#include <strings.h>
|
||||
#include "NMEA0183AIStoNMEA2000.h"
|
||||
NMEA0183DataToN2K::NMEA0183DataToN2K(GwLog *logger, GwBoatData *boatData,N2kSender callback)
|
||||
{
|
||||
this->sender = callback;
|
||||
|
@ -44,7 +45,7 @@ class SNMEA0183Msg : public tNMEA0183Msg{
|
|||
class NMEA0183DataToN2KFunctions : public NMEA0183DataToN2K
|
||||
{
|
||||
private:
|
||||
double dummy = 0;
|
||||
MyAisDecoder *aisDecoder=NULL;
|
||||
ConverterList<NMEA0183DataToN2KFunctions, SNMEA0183Msg> converters;
|
||||
std::map<unsigned long,unsigned long> lastSends;
|
||||
class WaypointNumber{
|
||||
|
@ -220,7 +221,9 @@ private:
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
void convertAIVDX(const SNMEA0183Msg &msg){
|
||||
aisDecoder->handleMessage(msg.line);
|
||||
}
|
||||
//shortcut for lambda converters
|
||||
#define CVL [](const SNMEA0183Msg &msg, NMEA0183DataToN2KFunctions *p) -> void
|
||||
void registerConverters()
|
||||
|
@ -230,6 +233,11 @@ private:
|
|||
converters.registerConverter(
|
||||
126992UL,129025UL,129026UL,127258UL,
|
||||
String(F("RMC")), &NMEA0183DataToN2KFunctions::convertRMC);
|
||||
unsigned long aispgns[7]{129810UL,129809UL,129040UL,129039UL,129802UL,129794UL,129038UL};
|
||||
converters.registerConverter(7,&aispgns[0],
|
||||
String(F("AIVDM")),&NMEA0183DataToN2KFunctions::convertAIVDX);
|
||||
converters.registerConverter(7,&aispgns[0],
|
||||
String(F("AIVDO")),&NMEA0183DataToN2KFunctions::convertAIVDX);
|
||||
}
|
||||
|
||||
public:
|
||||
|
@ -268,6 +276,7 @@ public:
|
|||
NMEA0183DataToN2KFunctions(GwLog *logger, GwBoatData *boatData, N2kSender callback)
|
||||
: NMEA0183DataToN2K(logger, boatData, callback)
|
||||
{
|
||||
aisDecoder= new MyAisDecoder(this->sender);
|
||||
registerConverters();
|
||||
LOG_DEBUG(GwLog::LOG, "NMEA0183DataToN2KFunctions: registered %d converters", converters.numConverters());
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue