OpenCPN Partial API docs
Loading...
Searching...
No Matches
autopilot_output.cpp
1/***************************************************************************
2 *
3 * Project: OpenCPN
4 * Purpose: Autopilot output support
5 * Author: David Register
6 *
7 ***************************************************************************
8 * Copyright (C) 2025 by David S. Register *
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 * This program is distributed in the hope that it will be useful, *
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
18 * GNU General Public License for more details. *
19 * *
20 * You should have received a copy of the GNU General Public License *
21 * along with this program; if not, write to the *
22 * Free Software Foundation, Inc., *
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
24 **************************************************************************/
25#include <cmath>
26#include <memory>
27#include <vector>
28
29#include <wx/wxprec.h>
30
31#include "model/autopilot_output.h"
32#include "model/comm_drv_n2k_serial.h"
34#include "model/comm_n0183_output.h"
35#include "model/comm_vars.h"
36#include "model/config_vars.h"
37#include "model/georef.h"
38#include "model/gui.h"
39#include "model/nmea_ctx_factory.h"
40#include "model/nmea_log.h"
41#include "model/own_ship.h"
42#include "model/routeman.h"
43#include "N2kMsg.h"
44#include "N2kMessages.h"
45
46#include "observable_globvar.h"
47
48#ifdef __ANDROID__
49#include "androidUTIL.h"
50#endif
51
52static NmeaLog *GetNmeaLog() {
53 auto w = wxWindow::FindWindowByName(kDataMonitorWindowName);
54 auto log = dynamic_cast<NmeaLog *>(w);
55 assert(log);
56 return log;
57}
58
59static void SendRmc(NMEA0183 &nmea0183, Routeman &routeman) {
60 SENTENCE snt;
61 nmea0183.Rmc.IsDataValid = bGPSValid ? NTrue : NFalse;
62
63 nmea0183.Rmc.Position.Latitude.Set(std::fabs(gLat), gLat < 0 ? "S" : "N");
64 nmea0183.Rmc.Position.Longitude.Set(std::fabs(gLon), gLon < 0 ? "W" : "E");
65
66 nmea0183.Rmc.SpeedOverGroundKnots = std::isnan(gSog) ? 0.0 : gSog;
67 nmea0183.Rmc.TrackMadeGoodDegreesTrue = std::isnan(gCog) ? 0.0 : gCog;
68
69 if (!std::isnan(gVar)) {
70 nmea0183.Rmc.MagneticVariation = std::fabs(gVar);
71 nmea0183.Rmc.MagneticVariationDirection = gVar < 0 ? West : East;
72 } else {
73 // A signal to NMEA converter, gVAR is unknown
74 nmea0183.Rmc.MagneticVariation = 361.;
75 }
76
77 // Send GPS time to autopilot if available else send local system time
78 if (!gRmcTime.IsEmpty() && !gRmcDate.IsEmpty()) {
79 nmea0183.Rmc.UTCTime = gRmcTime;
80 nmea0183.Rmc.Date = gRmcDate;
81 } else {
82 wxDateTime now = wxDateTime::Now();
83 wxDateTime utc = now.ToUTC();
84 wxString time = utc.Format(_T("%H%M%S"));
85 nmea0183.Rmc.UTCTime = time;
86 wxString date = utc.Format(_T("%d%m%y"));
87 nmea0183.Rmc.Date = date;
88 }
89
90 nmea0183.Rmc.FAAModeIndicator = "A";
91 if (!bGPSValid) nmea0183.Rmc.FAAModeIndicator = "N";
92
93 nmea0183.Rmc.Write(snt);
94
95 BroadcastNMEA0183Message(snt.Sentence, GetNmeaLog(),
96 routeman.GetMessageSentEventVar());
97}
98
99static void SendDummyRmb(NMEA0183 &nmea0183, Routeman &routeman) {
100 nmea0183.Rmb.IsDataValid = NTrue;
101 nmea0183.Rmb.CrossTrackError = 0;
102 nmea0183.Rmb.DirectionToSteer = Left;
103 nmea0183.Rmb.RangeToDestinationNauticalMiles = 0;
104 nmea0183.Rmb.BearingToDestinationDegreesTrue = 0;
105 nmea0183.Rmb.DestinationPosition.Latitude.Set(0, "N");
106 nmea0183.Rmb.DestinationPosition.Longitude.Set(0, "E");
107 nmea0183.Rmb.DestinationClosingVelocityKnots = 0;
108 nmea0183.Rmb.IsArrivalCircleEntered = NFalse;
109 nmea0183.Rmb.FAAModeIndicator = "A";
110 nmea0183.Rmb.To = "";
111 nmea0183.Rmb.From = "";
112
113 SENTENCE snt;
114 nmea0183.Rmb.Write(snt);
115 BroadcastNMEA0183Message(snt.Sentence, GetNmeaLog(),
116 routeman.GetMessageSentEventVar());
117}
118
119static void SendRmb(NMEA0183 &nmea0183, Routeman &routeman) {
120 SENTENCE snt;
121 RoutePoint *pActivePoint = routeman.GetpActivePoint();
122 const int maxName = 6;
123
124 nmea0183.Rmb.IsDataValid = bGPSValid ? NTrue : NFalse;
125 nmea0183.Rmb.CrossTrackError = routeman.GetCurrentXTEToActivePoint();
126 nmea0183.Rmb.DirectionToSteer = routeman.GetXTEDir() < 0 ? Left : Right;
127 nmea0183.Rmb.RangeToDestinationNauticalMiles =
128 routeman.GetCurrentRngToActivePoint();
129 nmea0183.Rmb.BearingToDestinationDegreesTrue =
130 routeman.GetCurrentBrgToActivePoint();
131
132 using std::fabs;
133 const char *ns = pActivePoint->m_lat < 0 ? "S" : "N";
134 nmea0183.Rmb.DestinationPosition.Latitude.Set(fabs(pActivePoint->m_lat), ns);
135 const char *ew = pActivePoint->m_lon < 0 ? "W" : "E";
136 nmea0183.Rmb.DestinationPosition.Longitude.Set(fabs(pActivePoint->m_lon), ew);
137
138 double sog = std::isnan(gSog) ? 0.0 : gSog;
139 double cog = std::isnan(gCog) ? 0.0 : gCog;
140 nmea0183.Rmb.DestinationClosingVelocityKnots =
141 sog * cos((cog - routeman.GetCurrentBrgToActivePoint()) * PI / 180.0);
142 nmea0183.Rmb.IsArrivalCircleEntered = routeman.GetArrival() ? NTrue : NFalse;
143 nmea0183.Rmb.FAAModeIndicator = bGPSValid ? "A" : "N";
144 // RMB is close to NMEA0183 length limit
145 // Restrict WP names further if necessary
146 int wp_len = maxName;
147 do {
148 nmea0183.Rmb.To = pActivePoint->GetName().Truncate(wp_len);
149 nmea0183.Rmb.From =
150 routeman.GetpActiveRouteSegmentBeginPoint()->GetName().Truncate(wp_len);
151 nmea0183.Rmb.Write(snt);
152 wp_len -= 1;
153 } while (snt.Sentence.size() > 82 && wp_len > 0);
154
155 BroadcastNMEA0183Message(snt.Sentence, GetNmeaLog(),
156 routeman.GetMessageSentEventVar());
157}
158
159bool SendNoRouteRmbRmc(Routeman &routeman) {
160 if (routeman.GetpActivePoint()) return false;
161 NMEA0183 nmea0183 = routeman.GetNMEA0183();
162 SendRmc(nmea0183, routeman);
163 SendDummyRmb(nmea0183, routeman);
164 return true;
165}
166
167bool UpdateAutopilotN0183(Routeman &routeman) {
168 NMEA0183 nmea0183 = routeman.GetNMEA0183();
169 RoutePoint *pActivePoint = routeman.GetpActivePoint();
170
171 // Set max WP name length
172 int maxName = 6;
173 if ((g_maxWPNameLength >= 3) && (g_maxWPNameLength <= 32))
174 maxName = g_maxWPNameLength;
175
176 SendRmb(nmea0183, routeman);
177 SendRmc(nmea0183, routeman);
178
179 // APB
180 {
181 SENTENCE snt;
182
183 nmea0183.Apb.IsLoranBlinkOK =
184 NTrue; // considered as "generic invalid fix" flag
185 if (!bGPSValid) nmea0183.Apb.IsLoranBlinkOK = NFalse;
186
187 nmea0183.Apb.IsLoranCCycleLockOK = NTrue;
188 if (!bGPSValid) nmea0183.Apb.IsLoranCCycleLockOK = NFalse;
189
190 nmea0183.Apb.CrossTrackErrorMagnitude =
191 routeman.GetCurrentXTEToActivePoint();
192
193 if (routeman.GetXTEDir() < 0)
194 nmea0183.Apb.DirectionToSteer = Left;
195 else
196 nmea0183.Apb.DirectionToSteer = Right;
197
198 nmea0183.Apb.CrossTrackUnits = _T("N");
199
200 if (routeman.GetArrival())
201 nmea0183.Apb.IsArrivalCircleEntered = NTrue;
202 else
203 nmea0183.Apb.IsArrivalCircleEntered = NFalse;
204
205 // We never pass the perpendicular, since we declare arrival before
206 // reaching this point
207 nmea0183.Apb.IsPerpendicular = NFalse;
208
209 nmea0183.Apb.To = pActivePoint->GetName().Truncate(maxName);
210
211 double brg1, dist1;
212 DistanceBearingMercator(pActivePoint->m_lat, pActivePoint->m_lon,
213 routeman.GetpActiveRouteSegmentBeginPoint()->m_lat,
214 routeman.GetpActiveRouteSegmentBeginPoint()->m_lon,
215 &brg1, &dist1);
216
217 if (g_bMagneticAPB && !std::isnan(gVar)) {
218 double brg1m =
219 ((brg1 - gVar) >= 0.) ? (brg1 - gVar) : (brg1 - gVar + 360.);
220 double bapm = ((routeman.GetCurrentBrgToActivePoint() - gVar) >= 0.)
221 ? (routeman.GetCurrentBrgToActivePoint() - gVar)
222 : (routeman.GetCurrentBrgToActivePoint() - gVar + 360.);
223
224 nmea0183.Apb.BearingOriginToDestination = brg1m;
225 nmea0183.Apb.BearingOriginToDestinationUnits = _T("M");
226
227 nmea0183.Apb.BearingPresentPositionToDestination = bapm;
228 nmea0183.Apb.BearingPresentPositionToDestinationUnits = _T("M");
229
230 nmea0183.Apb.HeadingToSteer = bapm;
231 nmea0183.Apb.HeadingToSteerUnits = _T("M");
232 } else {
233 nmea0183.Apb.BearingOriginToDestination = brg1;
234 nmea0183.Apb.BearingOriginToDestinationUnits = _T("T");
235
236 nmea0183.Apb.BearingPresentPositionToDestination =
237 routeman.GetCurrentBrgToActivePoint();
238 nmea0183.Apb.BearingPresentPositionToDestinationUnits = _T("T");
239
240 nmea0183.Apb.HeadingToSteer = routeman.GetCurrentBrgToActivePoint();
241 nmea0183.Apb.HeadingToSteerUnits = _T("T");
242 }
243
244 nmea0183.Apb.Write(snt);
245 BroadcastNMEA0183Message(snt.Sentence, GetNmeaLog(),
246 routeman.GetMessageSentEventVar());
247 }
248
249 // XTE
250 {
251 SENTENCE snt;
252
253 nmea0183.Xte.IsLoranBlinkOK =
254 NTrue; // considered as "generic invalid fix" flag
255 if (!bGPSValid) nmea0183.Xte.IsLoranBlinkOK = NFalse;
256
257 nmea0183.Xte.IsLoranCCycleLockOK = NTrue;
258 if (!bGPSValid) nmea0183.Xte.IsLoranCCycleLockOK = NFalse;
259
260 nmea0183.Xte.CrossTrackErrorDistance =
261 routeman.GetCurrentXTEToActivePoint();
262
263 if (routeman.GetXTEDir() < 0)
264 nmea0183.Xte.DirectionToSteer = Left;
265 else
266 nmea0183.Xte.DirectionToSteer = Right;
267
268 nmea0183.Xte.CrossTrackUnits = _T("N");
269
270 nmea0183.Xte.Write(snt);
271 BroadcastNMEA0183Message(snt.Sentence, GetNmeaLog(),
272 routeman.GetMessageSentEventVar());
273 }
274
275 return true;
276}
277
278bool UpdateAutopilotN2K(Routeman &routeman) {
279 bool fail_any = false;
280
281 // Get a suitable N2K Driver
282 auto &registry = CommDriverRegistry::GetInstance();
283 const std::vector<DriverPtr> &drivers = registry.GetDrivers();
284
285 AbstractCommDriver *found = nullptr;
286 for (auto key : routeman.GetOutpuDriverArray()) {
287 for (auto &d : drivers) {
288 if (d->Key() == key) {
289 std::unordered_map<std::string, std::string> attributes =
290 GetAttributes(key);
291 auto protocol_it = attributes.find("protocol");
292 if (protocol_it != attributes.end()) {
293 std::string protocol = protocol_it->second;
294
295 if (protocol == "nmea2000") {
296 found = d.get();
297 }
298 }
299 }
300 }
301 }
302 if (!found) return false;
303
304 // N2K serial drivers require maintenance of an enabled PGN TX list
305 auto drv_serial = dynamic_cast<CommDriverN2KSerial *>(found);
306 if (drv_serial) {
307 drv_serial->AddTxPGN(129283);
308 drv_serial->AddTxPGN(129284);
309 drv_serial->AddTxPGN(129285);
310 }
311 if (routeman.IsAnyRouteActive()) {
312 fail_any |= !SendPGN129285(routeman, found);
313 fail_any |= !SendPGN129284(routeman, found);
314 fail_any |= !SendPGN129283(routeman, found);
315 }
316
317 return (fail_any == 0);
318}
319
320bool SendPGN129285(Routeman &routeman, AbstractCommDriver *driver) {
321 bool fail_any = false;
322
323 // Set max WP name length
324 int maxName = 6;
325 if ((g_maxWPNameLength >= 3) && (g_maxWPNameLength <= 32))
326 maxName = g_maxWPNameLength;
327
328 // Create and transmit PGN 129285
329 // The basic PGN129285 head
330 tN2kMsg msg129285;
331 char route_name[] = "Route";
332 SetN2kPGN129285(msg129285, 0, 0, 0, N2kdir_forward, 0, route_name);
333
334 // Append start point of current leg
335 RoutePoint *pLegBeginPoint = routeman.GetpActiveRouteSegmentBeginPoint();
336 wxString start_point_name = pLegBeginPoint->GetName().Truncate(maxName);
337 std::string sname = start_point_name.ToStdString();
338 char *s = (char *)sname.c_str();
339
340 fail_any |= !AppendN2kPGN129285(msg129285, 0, s, pLegBeginPoint->m_lat,
341 pLegBeginPoint->m_lon);
342 // Append destination point of current leg
343 RoutePoint *pActivePoint = routeman.GetpActivePoint();
344 wxString destination_name = pActivePoint->GetName().Truncate(maxName);
345 std::string dname = destination_name.ToStdString();
346 char *d = (char *)dname.c_str();
347 fail_any |= !AppendN2kPGN129285(msg129285, 1, d, pActivePoint->m_lat,
348 pActivePoint->m_lon);
349
350 if (fail_any) return false;
351
352 auto dest_addr = std::make_shared<const NavAddr2000>(driver->iface, 255);
353 std::vector<uint8_t> payload;
354 for (int i = 0; i < msg129285.DataLen; i++)
355 payload.push_back(msg129285.Data[i]);
356 auto PGN129285 =
357 std::make_shared<const Nmea2000Msg>(129285, payload, dest_addr, 6);
358 fail_any |= !driver->SendMessage(PGN129285, dest_addr);
359
360 return (fail_any == 0);
361}
362
363bool SendPGN129284(Routeman &routeman, AbstractCommDriver *driver) {
364 bool fail_any = false;
365 tN2kMsg msg129284;
366 RoutePoint *pActivePoint = routeman.GetpActivePoint();
367
368 // Calculate closing velocity and ETA
369 double vmg = 0.;
370 if (!std::isnan(gCog) && !std::isnan(gSog)) {
371 double brg = routeman.GetCurrentBrgToActivePoint();
372 vmg = gSog * cos((brg - gCog) * PI / 180.);
373 }
374 wxTimeSpan tttg_span;
375 wxDateTime arrival_time = wxDateTime::Now();
376 if (vmg > 0.) {
377 double tttg_sec = (routeman.GetCurrentRngToActivePoint() / gSog) * 3600;
378 tttg_span = wxTimeSpan::Seconds((long)tttg_sec);
379 arrival_time += tttg_span;
380 }
381 double time_days_1979 = arrival_time.GetTicks() / (3600. * 24.);
382
383 // ETA time_seconds, expressed as seconds since midnight
384 // ETA date, expressed as whole days since 1 January 1970
385 double eta_time_days;
386 double eta_time_seconds = modf(time_days_1979, &eta_time_days);
387 int16_t eta_time_days_16 = static_cast<uint16_t>(eta_time_days);
388
389 SetN2kPGN129284(
390 msg129284,
391 0xFF, // SID
392 routeman.GetCurrentRngToActivePoint() * 1852., // DistanceToWaypoint
393 N2khr_true, // tN2kHeadingReference BearingReference
394 false, // PerpendicularCrossed
395 false, // ArrivalCircleEntered
396 N2kdct_RhumbLine, // tN2kDistanceCalculationType CalculationType
397 eta_time_seconds, // double ETATime,
398 eta_time_days_16, // int16_t ETADate,
399 routeman.GetCurrentSegmentCourse() * PI /
400 180., // BearingOriginToDestinationWaypoint,
401 routeman.GetCurrentBrgToActivePoint() * PI /
402 180., // BearingPositionToDestinationWaypoint,
403 0, // uint8_t OriginWaypointNumber,
404 1, // uint8_t DestinationWaypointNumber,
405 pActivePoint->m_lat, // double DestinationLatitude,
406 pActivePoint->m_lon, // double DestinationLongitude,
407 vmg); // double WaypointClosingVelocity);
408
409 auto dest_addr = std::make_shared<const NavAddr2000>(driver->iface, 255);
410 std::vector<uint8_t> payload;
411 for (int i = 0; i < msg129284.DataLen; i++)
412 payload.push_back(msg129284.Data[i]);
413 auto PGN129284 =
414 std::make_shared<const Nmea2000Msg>(129284, payload, dest_addr, 6);
415 fail_any |= !driver->SendMessage(PGN129284, dest_addr);
416
417 return (fail_any == 0);
418}
419
420bool SendPGN129283(Routeman &routeman, AbstractCommDriver *driver) {
421 bool fail_any = false;
422 tN2kMsg msg129283;
423 RoutePoint *pActivePoint = routeman.GetpActivePoint();
424 // N2K, distance units are metres, therefore convert from Nm
425 double xte = routeman.GetCurrentXTEToActivePoint() * 1852.;
426 // N2K, -ve xte, means to the left of the course, steer right, Reverse of
427 // current Routeman logic
428 if (routeman.GetXTEDir() > 0) {
429 xte = -xte;
430 }
431 SetN2kPGN129283(msg129283, 0,
432 N2kxtem_Autonomous, // tN2kXTEMode XTEMode,
433 false, // bool NavigationTerminated,
434 xte // double XTE
435 );
436
437 auto dest_addr = std::make_shared<const NavAddr2000>(driver->iface, 255);
438 std::vector<uint8_t> payload;
439 for (int i = 0; i < msg129283.DataLen; i++)
440 payload.push_back(msg129283.Data[i]);
441 auto PGN129283 =
442 std::make_shared<const Nmea2000Msg>(129283, payload, dest_addr, 6);
443 fail_any |= !driver->SendMessage(PGN129283, dest_addr);
444
445 return (fail_any == 0);
446}
Common interface for all drivers.
Definition comm_driver.h:67
const std::string iface
Physical device for 0183, else a unique string.
Definition comm_driver.h:97
NMEA Log Interface.
Definition nmea_log.h:72
Represents a waypoint or mark within the navigation system.
Definition route_point.h:70
Driver registration container, a singleton.
Hooks into gui available in model.
Basic DataMonitor logging interface: LogLine (reflects a line in the log) and NmeaLog,...
Global variables Listen()/Notify() wrapper.
const std::unordered_map< std::string, std::string > GetAttributes(DriverHandle handle)
Query a specific driver for attributes.