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