OpenCPN Partial API docs
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)
34 static 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 //----------------------------------------------------------------------------------
41 AisError DecodeSingleVDO(const wxString &str,
42  GenericPosDatEx *pos) {
43  // Make some simple tests for validity
44  if (str.Len() > 128) 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 //----------------------------------------------------------------------------
146 bool 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 
321 bool 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