OpenCPN Partial API docs
Loading...
Searching...
No Matches
comm_ais.cpp
1/***************************************************************************
2 *
3 * Project: OpenCPN
4 *
5 ***************************************************************************
6 * Copyright (C) 2022 by David S. Register *
7 * *
8 * This program is free software; you can redistribute it and/or modify *
9 * it under the terms of the GNU General Public License as published by *
10 * the Free Software Foundation; either version 2 of the License, or *
11 * (at your option) any later version. *
12 * *
13 * This program is distributed in the hope that it will be useful, *
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16 * GNU General Public License for more details. *
17 * *
18 * You should have received a copy of the GNU General Public License *
19 * along with this program; if not, write to the *
20 * Free Software Foundation, Inc., *
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
22 ***************************************************************************
23 */
24#include <cmath>
25#include <memory>
26
27#include <wx/tokenzr.h>
28#include <wx/string.h>
29#include <wx/datetime.h>
30
31#include "model/comm_ais.h"
32
33#if !defined(NAN)
34static const long long lNaN = 0xfff8000000000000;
35#define NAN (*(double *)&lNaN)
36#endif
37
38//----------------------------------------------------------------------------------
39// Decode a single AIVDO sentence to a Generic Position Report
40//----------------------------------------------------------------------------------
41AisError DecodeSingleVDO(const wxString &str, GenericPosDatEx *pos) {
42 // Make some simple tests for validity
43 if (str.Len() > 128) return AIS_NMEAVDX_TOO_LONG;
44
45 if (!NMEA_AISCheckSumOK(str)) return AIS_NMEAVDX_CHECKSUM_BAD;
46
47 if (!pos) return AIS_GENERIC_ERROR;
48
49 // if (!ctx.accumulator) return AIS_GENERIC_ERROR;
50
51 // We only process AIVDO messages
52 if (!str.Mid(1, 5).IsSameAs(_T("AIVDO"))) return AIS_GENERIC_ERROR;
53
54 // Use a tokenizer to pull out the first 4 fields
55 wxStringTokenizer tkz(str, _T(","));
56
57 wxString token;
58 token = tkz.GetNextToken(); // !xxVDx
59
60 token = tkz.GetNextToken();
61 int nsentences = atoi(token.mb_str());
62
63 token = tkz.GetNextToken();
64 int isentence = atoi(token.mb_str());
65
66 token = tkz.GetNextToken(); // skip 2 fields
67 token = tkz.GetNextToken();
68
69 wxString string_to_parse;
70 string_to_parse.Clear();
71
72 // Fill the output structure with all NANs
73 pos->kLat = NAN;
74 pos->kLon = NAN;
75 pos->kCog = NAN;
76 pos->kSog = NAN;
77 pos->kHdt = NAN;
78 pos->kVar = NAN;
79 pos->kHdm = NAN;
80
81 // Simple case only
82 // First and only part of a one-part sentence
83 if ((1 == nsentences) && (1 == isentence)) {
84 string_to_parse = tkz.GetNextToken(); // the encapsulated data
85 } else {
86 wxASSERT_MSG(false, wxT("Multipart AIVDO detected"));
87 return AIS_INCOMPLETE_MULTIPART; // and non-zero return
88 }
89
90 // Create the bit accessible string
91 AisBitstring strbit(string_to_parse.mb_str());
92
93 auto TargetData = std::make_unique<AisTargetData>(
94 *AisTargetDataMaker::GetInstance().GetTargetData());
95
96 bool bdecode_result = Parse_VDXBitstring(&strbit, TargetData.get());
97
98 if (bdecode_result) {
99 switch (TargetData->MID) {
100 case 1:
101 case 2:
102 case 3:
103 case 18: {
104 if (!TargetData->b_positionDoubtful) {
105 pos->kLat = TargetData->Lat;
106 pos->kLon = TargetData->Lon;
107 } else {
108 pos->kLat = NAN;
109 pos->kLon = NAN;
110 }
111
112 if (TargetData->COG == 360.0)
113 pos->kCog = NAN;
114 else
115 pos->kCog = TargetData->COG;
116
117 if (TargetData->SOG > 102.2)
118 pos->kSog = NAN;
119 else
120 pos->kSog = TargetData->SOG;
121
122 if ((int)TargetData->HDG == 511)
123 pos->kHdt = NAN;
124 else
125 pos->kHdt = TargetData->HDG;
126
127 // VDO messages do not contain variation or magnetic heading
128 pos->kVar = NAN;
129 pos->kHdm = NAN;
130 break;
131 }
132 default:
133 return AIS_GENERIC_ERROR; // unrecognised sentence
134 }
135
136 return AIS_NoError;
137 } else
138 return AIS_GENERIC_ERROR;
139}
140
141//----------------------------------------------------------------------------
142// Parse a NMEA VDM/VDO Bitstring
143//----------------------------------------------------------------------------
144bool Parse_VDXBitstring(AisBitstring *bstr, AisTargetData *ptd) {
145 bool parse_result = false;
146 bool b_posn_report = false;
147
148 wxDateTime now = wxDateTime::Now();
149 now.MakeGMT();
150 int message_ID = bstr->GetInt(1, 6); // Parse on message ID
151 ptd->MID = message_ID;
152
153 // MMSI is always in the same spot in the bitstream
154 ptd->MMSI = bstr->GetInt(9, 30);
155
156 switch (message_ID) {
157 case 1: // Position Report
158 case 2:
159 case 3: {
160 ptd->NavStatus = bstr->GetInt(39, 4);
161 ptd->SOG = 0.1 * (bstr->GetInt(51, 10));
162
163 int lon = bstr->GetInt(62, 28);
164 if (lon & 0x08000000) // negative?
165 lon |= 0xf0000000;
166 double lon_tentative = lon / 600000.;
167
168 int lat = bstr->GetInt(90, 27);
169 if (lat & 0x04000000) // negative?
170 lat |= 0xf8000000;
171 double lat_tentative = lat / 600000.;
172
173 if ((lon_tentative <= 180.) && (lat_tentative <= 90.))
174 // Ship does not report Lat or Lon "unavailable"
175 {
176 ptd->Lon = lon_tentative;
177 ptd->Lat = lat_tentative;
178 ptd->b_positionDoubtful = false;
179 ptd->b_positionOnceValid = true; // Got the position at least once
180 ptd->PositionReportTicks = now.GetTicks();
181 } else
182 ptd->b_positionDoubtful = true;
183
184 // decode balance of message....
185 ptd->COG = 0.1 * (bstr->GetInt(117, 12));
186 ptd->HDG = 1.0 * (bstr->GetInt(129, 9));
187
188 ptd->ROTAIS = bstr->GetInt(43, 8);
189 double rot_dir = 1.0;
190
191 if (ptd->ROTAIS == 128)
192 ptd->ROTAIS = -128; // not available codes as -128
193 else if ((ptd->ROTAIS & 0x80) == 0x80) {
194 ptd->ROTAIS = ptd->ROTAIS - 256; // convert to twos complement
195 rot_dir = -1.0;
196 }
197
198 // Convert to indicated ROT
199 ptd->ROTIND = round(rot_dir * pow((((double)ptd->ROTAIS) / 4.733), 2));
200
201 ptd->m_utc_sec = bstr->GetInt(138, 6);
202
203 if ((1 == message_ID) || (2 == message_ID))
204 // decode SOTDMA per 7.6.7.2.2
205 {
206 ptd->SyncState = bstr->GetInt(151, 2);
207 ptd->SlotTO = bstr->GetInt(153, 2);
208 if ((ptd->SlotTO == 1) && (ptd->SyncState == 0)) // UTCDirect follows
209 {
210 ptd->m_utc_hour = bstr->GetInt(155, 5);
211
212 ptd->m_utc_min = bstr->GetInt(160, 7);
213
214 if ((ptd->m_utc_hour < 24) && (ptd->m_utc_min < 60) &&
215 (ptd->m_utc_sec < 60)) {
216 wxDateTime rx_time(ptd->m_utc_hour, ptd->m_utc_min, ptd->m_utc_sec);
217#ifdef AIS_DEBUG
218 rx_ticks = rx_time.GetTicks();
219 if (!b_firstrx) {
220 first_rx_ticks = rx_ticks;
221 b_firstrx = true;
222 }
223#endif
224 }
225 }
226 }
227
228 // Capture Euro Inland special passing arrangement signal ("stbd-stbd")
229 ptd->blue_paddle = bstr->GetInt(144, 2);
230 ptd->b_blue_paddle = (ptd->blue_paddle == 2); // paddle is set
231
232 if (!ptd->b_isDSCtarget) ptd->Class = AIS_CLASS_A;
233
234 // Check for SART and friends by looking at first two digits of MMSI
235 int mmsi_start = ptd->MMSI / 10000000;
236
237 if (mmsi_start == 97) {
238 ptd->Class = AIS_SART;
239 ptd->StaticReportTicks =
240 now.GetTicks(); // won't get a static report, so fake it here
241
242 // On receipt of Msg 3, force any existing SART target out of
243 // acknowledge mode by adjusting its ack_time to yesterday This will
244 // cause any previously "Acknowledged" SART to re-alert.
245
246 // On reflection, re-alerting seems a little excessive in real life
247 // use. After all, the target is on-screen, and in the AIS target
248 // list. So lets just honor the programmed ACK timout value for SART
249 // targets as well
250 // ptd->m_ack_time = wxDateTime::Now() - wxTimeSpan::Day();
251 }
252
253 parse_result = true; // so far so good
254 b_posn_report = true;
255
256 break;
257 }
258
259 case 18: {
260 // Class B targets have no status. Enforce this...
261 ptd->NavStatus = UNDEFINED;
262
263 ptd->SOG = 0.1 * (bstr->GetInt(47, 10));
264
265 int lon = bstr->GetInt(58, 28);
266 if (lon & 0x08000000) // negative?
267 lon |= 0xf0000000;
268 double lon_tentative = lon / 600000.;
269
270 int lat = bstr->GetInt(86, 27);
271 if (lat & 0x04000000) // negative?
272 lat |= 0xf8000000;
273 double lat_tentative = lat / 600000.;
274
275 if ((lon_tentative <= 180.) && (lat_tentative <= 90.))
276 // Ship does not report Lat or Lon "unavailable"
277 {
278 ptd->Lon = lon_tentative;
279 ptd->Lat = lat_tentative;
280 ptd->b_positionDoubtful = false;
281 ptd->b_positionOnceValid = true; // Got the position at least once
282 ptd->PositionReportTicks = now.GetTicks();
283 } else
284 ptd->b_positionDoubtful = true;
285
286 ptd->COG = 0.1 * (bstr->GetInt(113, 12));
287 ptd->HDG = 1.0 * (bstr->GetInt(125, 9));
288
289 ptd->m_utc_sec = bstr->GetInt(134, 6);
290
291 if (!ptd->b_isDSCtarget) ptd->Class = AIS_CLASS_B;
292
293 parse_result = true; // so far so good
294 b_posn_report = true;
295
296 break;
297 }
298
299 default: {
300 break;
301 }
302 }
303
304 if (b_posn_report) ptd->b_lost = false;
305
306 if (true == parse_result) {
307 // Revalidate the target under some conditions
308 if (!ptd->b_active && !ptd->b_positionDoubtful && b_posn_report)
309 ptd->b_active = true;
310 }
311
312 return parse_result;
313}
314
315bool NMEA_AISCheckSumOK(const wxString &str_in) {
316 unsigned char checksum_value = 0;
317 int sentence_hex_sum;
318
319 wxCharBuffer buf = str_in.ToUTF8();
320 if (!buf.data()) return false; // cannot decode string
321
322 char str_ascii[AIS_MAX_MESSAGE_LEN + 1];
323 strncpy(str_ascii, buf.data(), AIS_MAX_MESSAGE_LEN);
324 str_ascii[AIS_MAX_MESSAGE_LEN] = '\0';
325
326 int string_length = strlen(str_ascii);
327
328 int payload_length = 0;
329 while ((payload_length < string_length) &&
330 (str_ascii[payload_length] != '*')) // look for '*'
331 payload_length++;
332
333 if (payload_length == string_length)
334 return false; // '*' not found at all, no checksum
335
336 int index = 1; // Skip over the $ at the begining of the sentence
337
338 while (index < payload_length) {
339 checksum_value ^= str_ascii[index];
340 index++;
341 }
342
343 if (string_length > 4) {
344 char scanstr[3];
345 scanstr[0] = str_ascii[payload_length + 1];
346 scanstr[1] = str_ascii[payload_length + 2];
347 scanstr[2] = 0;
348 sscanf(scanstr, "%2x", &sentence_hex_sum);
349
350 if (sentence_hex_sum == checksum_value) return true;
351 }
352
353 return false;
354}
int GetInt(int sp, int len, bool signed_flag=false)
sp is starting bit, 1-based