OpenCPN Partial API docs
Loading...
Searching...
No Matches
comm_drv_n0183_net.cpp
Go to the documentation of this file.
1/**************************************************************************
2 * Copyright (C) 2022 David Register *
3 * Copyright (C) 2022 Alec Leamas *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, see <https://www.gnu.org/licenses/>. *
17 **************************************************************************/
18
25#include <ctime>
26#include <deque>
27
28#ifdef __MSVC__
29#include "winsock2.h"
30#include <wx/msw/winundef.h>
31#include <ws2tcpip.h>
32#endif
33
34#ifndef _WIN32
35#include <arpa/inet.h>
36#include <netinet/tcp.h>
37#endif
38
39#include <wx/wxprec.h>
40#ifndef WX_PRECOMP
41#include <wx/wx.h>
42#endif
43
44#include <wx/datetime.h>
45#include <wx/socket.h>
46#include <wx/log.h>
47#include <wx/memory.h>
48#include <wx/chartype.h>
49#include <wx/sckaddr.h>
50
52
56#include "model/config_vars.h"
58#include "model/idents.h"
59#include "model/logger.h"
60#include "model/sys_events.h"
61
62using namespace std::literals::chrono_literals;
63
64#define N_DOG_TIMEOUT 8
65
67static bool IsBroadcastAddr(unsigned addr, unsigned netmask_bits) {
68 assert(netmask_bits <= 32);
69#if defined(_MSC_VER) || __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
70 uint32_t netmask = 0xffffffff >> (32 - netmask_bits);
71#else
72 uint32_t netmask = 0xffffffff << (32 - netmask_bits);
73#endif
74 uint32_t host_mask = ~netmask;
75 return (addr & host_mask) == host_mask;
76}
77
79public:
80 struct ip_mreq m_mrq;
81 void SetMrqAddr(unsigned int addr) {
82 m_mrq.imr_multiaddr.s_addr = addr;
83 m_mrq.imr_interface.s_addr = INADDR_ANY;
84 }
85};
86
87static bool SetOutputSocketOptions(wxSocketBase* tsock) {
88 int ret;
89
90 // Disable nagle algorithm on outgoing connection
91 // Doing this here rather than after the accept() is
92 // pointless on platforms where TCP_NODELAY is
93 // not inherited. However, none of OpenCPN's currently
94 // supported platforms fall into that category.
95
96 int nagleDisable = 1;
97 ret = tsock->SetOption(IPPROTO_TCP, TCP_NODELAY, &nagleDisable,
98 sizeof(nagleDisable));
99
100 // Drastically reduce the size of the socket output buffer
101 // so that when client goes away without properly closing, the stream will
102 // quickly fill the output buffer, and thus fail the write() call
103 // within a few seconds.
104 unsigned long outbuf_size = 1024; // Smallest allowable value on Linux
105 return (tsock->SetOption(SOL_SOCKET, SO_SNDBUF, &outbuf_size,
106 sizeof(outbuf_size)) &&
107 ret);
108}
109
110//========================================================================
111/*
112 * CommDriverN0183Net implementation
113 */
114CommDriverN0183Net::CommDriverN0183Net(const ConnectionParams* params,
115 DriverListener& listener)
116 : CommDriverN0183(NavAddr::Bus::N0183, params->GetStrippedDSPort()),
117 m_params(*params),
118 m_listener(listener),
119 m_sock(nullptr),
120 m_tsock(nullptr),
121 m_socket_server(nullptr),
122 m_is_multicast(false),
123 m_stats_timer(*this, 2s),
124 m_txenter(0),
125 m_dog_value(0),
126 m_rx_connect_event(false),
127 m_socket_timer(*this),
128 m_socketread_watchdog_timer(*this),
129 m_ok(false),
130 m_is_conn_err_reported(false) {
131 m_addr.Hostname(params->network_address);
132 m_addr.Service(params->network_port);
133 this->attributes["netAddress"] = params->network_address.ToStdString();
134 this->attributes["netPort"] = std::to_string(params->network_port);
135 this->attributes["userComment"] = params->user_comment.ToStdString();
136 this->attributes["ioDirection"] = PortDirectionToString(params->direction);
137 m_driver_stats.driver_bus = NavAddr::Bus::N0183;
138 m_driver_stats.driver_iface = params->GetStrippedDSPort();
139
140 m_mrq_container = std::make_unique<MrqContainer>();
141
142 // Establish event listeners
143 resume_listener.Init(SystemEvents::GetInstance().evt_resume,
144 [&](ObservedEvt&) { HandleResume(); });
145 Bind(wxEVT_SOCKET, &CommDriverN0183Net::OnSocketEvent, this, DS_SOCKET_ID);
146 Bind(wxEVT_SOCKET, &CommDriverN0183Net::OnServerSocketEvent, this,
147 DS_SERVERSOCKET_ID);
148
149 Open();
150}
151
152CommDriverN0183Net::~CommDriverN0183Net() { Close(); }
153
154void CommDriverN0183Net::HandleN0183Msg(const std::string& sentence) {
155 // Sanity check
156 m_driver_stats.rx_count += sentence.size();
157 SendToListener(sentence, m_listener, m_params);
158}
159
160void CommDriverN0183Net::Open() {
161#ifdef __UNIX__
162 in_addr_t addr =
163 ((struct sockaddr_in*)m_addr.GetAddressData())->sin_addr.s_addr;
164#else
165 unsigned int addr = inet_addr(m_addr.IPAddress().mb_str());
166#endif
167 // Create the socket
168 switch (m_params.net_protocol) {
169 case GPSD: {
170 OpenNetworkGpsd();
171 break;
172 }
173 case TCP: {
174 OpenNetworkTcp(addr);
175 break;
176 }
177 case UDP: {
178 OpenNetworkUdp(addr);
179 break;
180 }
181 default:
182 break;
183 }
184 m_ok = true;
185}
186
187void CommDriverN0183Net::OpenNetworkUdp(unsigned int addr) {
188 if (m_params.direction != PortDirection::kOutput &&
189 m_params.direction != PortDirection::kUpload) {
190 // We need a local (bindable) address to create the Datagram receive socket
191 // Set up the reception socket
192 wxIPV4address conn_addr;
193 conn_addr.Service(std::to_string(m_params.network_port));
194 conn_addr.AnyAddress();
195 conn_addr.AnyAddress();
196 m_sock =
197 new wxDatagramSocket(conn_addr, wxSOCKET_NOWAIT | wxSOCKET_REUSEADDR);
198
199 // Test if address is IPv4 multicast
200 if ((ntohl(addr) & 0xf0000000) == 0xe0000000) {
201 m_is_multicast = true;
202 m_mrq_container->SetMrqAddr(addr);
203 m_sock->SetOption(IPPROTO_IP, IP_ADD_MEMBERSHIP, &m_mrq_container->m_mrq,
204 sizeof(m_mrq_container->m_mrq));
205 }
206
207 m_sock->SetEventHandler(*this, DS_SOCKET_ID);
208
209 m_sock->SetNotify(wxSOCKET_CONNECTION_FLAG | wxSOCKET_INPUT_FLAG |
210 wxSOCKET_LOST_FLAG);
211 m_sock->Notify(TRUE);
212 m_sock->SetTimeout(1); // Short timeout
213 m_driver_stats.available = true;
214 }
215
216 // Set up another socket for transmit
217 if (m_params.direction != PortDirection::kInput) {
218 wxIPV4address tconn_addr;
219 tconn_addr.Service(0); // use ephemeral out port
220 tconn_addr.AnyAddress();
221 m_tsock =
222 new wxDatagramSocket(tconn_addr, wxSOCKET_NOWAIT | wxSOCKET_REUSEADDR);
223 // Here would be the place to disable multicast loopback
224 // but for consistency with broadcast behaviour, we will
225 // instead rely on setting priority levels to ignore
226 // sentences read back that have just been transmitted
227 if (!m_is_multicast && IsBroadcastAddr(addr, g_netmask_bits)) {
228 int broadcastEnable = 1;
229 m_tsock->SetOption(SOL_SOCKET, SO_BROADCAST, &broadcastEnable,
230 sizeof(broadcastEnable));
231 m_driver_stats.available = true;
232 }
233 }
234
235 // In case the connection is lost before acquired....
236 m_connect_time = std::chrono::steady_clock::now();
237}
238
239void CommDriverN0183Net::OpenNetworkTcp(unsigned int addr) {
240 if (addr == INADDR_ANY) {
241 MESSAGE_LOG << "Listening for TCP connections on " << INADDR_ANY;
242 m_socket_server = new wxSocketServer(m_addr, wxSOCKET_REUSEADDR);
243 m_socket_server->SetEventHandler(*this, DS_SERVERSOCKET_ID);
244 m_socket_server->SetNotify(wxSOCKET_CONNECTION_FLAG);
245 m_socket_server->Notify(TRUE);
246 m_socket_server->SetTimeout(1); // Short timeout
247 m_driver_stats.available = m_socket_server->IsOk();
248 } else {
249 MESSAGE_LOG << "Opening TCP connection to " << m_params.network_address
250 << ":" << m_params.network_port;
251 m_sock = new wxSocketClient();
252 m_sock->SetEventHandler(*this, DS_SOCKET_ID);
253 int notify_flags = (wxSOCKET_CONNECTION_FLAG | wxSOCKET_LOST_FLAG);
254 if (m_params.direction != PortDirection::kInput)
255 notify_flags |= wxSOCKET_OUTPUT_FLAG;
256 if (m_params.direction != PortDirection::kOutput)
257 notify_flags |= wxSOCKET_INPUT_FLAG;
258 m_sock->SetNotify(notify_flags);
259 m_sock->Notify(true);
260 m_sock->SetTimeout(1); // Short timeout
261
262 m_rx_connect_event = false;
263 m_socket_timer.Start(100, wxTIMER_ONE_SHOT); // schedule a connection
264 m_driver_stats.available = m_sock->IsOk();
265 }
266 // In case the connection is lost before acquired....
267 m_connect_time = std::chrono::steady_clock::now();
268}
269
270void CommDriverN0183Net::OpenNetworkGpsd() {
271 m_sock = new wxSocketClient();
272 m_sock->SetEventHandler(*this, DS_SOCKET_ID);
273 m_sock->SetNotify(wxSOCKET_CONNECTION_FLAG | wxSOCKET_INPUT_FLAG |
274 wxSOCKET_LOST_FLAG);
275 m_sock->Notify(TRUE);
276 m_sock->SetTimeout(1); // Short timeout
277
278 auto* tcp_socket = dynamic_cast<wxSocketClient*>(m_sock);
279 tcp_socket->Connect(m_addr, false);
280 m_rx_connect_event = false;
281}
282
283void CommDriverN0183Net::OnSocketReadWatchdogTimer() {
284 m_dog_value--;
285
286 if (m_dog_value <= 0) { // No receive in n seconds
287 if (GetParams().no_data_reconnect) {
288 // Reconnect on NO DATA is true, so try to reconnect now.
289 if (m_params.net_protocol == TCP) {
290 auto* tcp_socket = dynamic_cast<wxSocketClient*>(m_sock);
291 if (tcp_socket) tcp_socket->Close();
292
293 int n_reconnect_delay = wxMax(N_DOG_TIMEOUT - 2, 2);
294 wxLogMessage("Reconnection scheduled in %d seconds.",
295 n_reconnect_delay);
296 m_socket_timer.Start(n_reconnect_delay * 1000, wxTIMER_ONE_SHOT);
297
298 // Stop DATA watchdog, will be restarted on successful connection.
299 m_socketread_watchdog_timer.Stop();
300 }
301 }
302 }
303}
304
305void CommDriverN0183Net::OnTimerSocket() {
306 // Attempt a connection
307 using namespace std::chrono;
308 auto* tcp_socket = dynamic_cast<wxSocketClient*>(m_sock);
309 if (tcp_socket) {
310 if (tcp_socket->IsDisconnected()) {
311 m_driver_stats.available = false;
312 wxLogDebug("Attempting reconnection...");
313 m_rx_connect_event = false;
314 // Stop DATA watchdog, may be restarted on successful connection.
315 m_socketread_watchdog_timer.Stop();
316 tcp_socket->Connect(m_addr, false);
317
318 // schedule another connection attempt, in case this one fails
319 int n_reconnect_delay = N_DOG_TIMEOUT;
320 m_socket_timer.Start(n_reconnect_delay * 1000, wxTIMER_ONE_SHOT);
321
322 // Possibly report connect error to GUI.
323 if (m_connect_time == time_point<steady_clock>()) return;
324 auto since_connect = steady_clock::now() - m_connect_time;
325 if (since_connect > 10s && !m_is_conn_err_reported) {
326 std::stringstream ss;
327 ss << _("Cannot connect to remote server ") << m_params.network_address
328 << ":" << m_params.network_port;
329 CommDriverRegistry::GetInstance().evt_driver_msg.Notify(ss.str());
330 m_is_conn_err_reported = true;
331 m_driver_stats.error_count++;
332 }
333 };
334 m_driver_stats.available = tcp_socket->IsOk();
335 }
336}
337
338void CommDriverN0183Net::HandleResume() {
339 // Attempt a stop and restart of connection
340 auto* tcp_socket = dynamic_cast<wxSocketClient*>(m_sock);
341 if (tcp_socket) {
342 m_socketread_watchdog_timer.Stop();
343
344 tcp_socket->Close();
345
346 // schedule reconnect attempt
347 int n_reconnect_delay = wxMax(N_DOG_TIMEOUT - 2, 2);
348 wxLogMessage("Reconnection scheduled in %d seconds.", n_reconnect_delay);
349
350 m_socket_timer.Start(n_reconnect_delay * 1000, wxTIMER_ONE_SHOT);
351 }
352}
353
354bool CommDriverN0183Net::SendMessage(std::shared_ptr<const NavMsg> msg,
355 std::shared_ptr<const NavAddr> addr) {
356 auto msg_0183 = std::dynamic_pointer_cast<const Nmea0183Msg>(msg);
357 std::string payload(msg_0183->payload);
358 if (!ocpn::endswith(payload, "\r\n")) payload += "\r\n";
359 m_driver_stats.tx_count += payload.size();
360 return SendSentenceNetwork(payload.c_str());
361}
362
363void CommDriverN0183Net::OnSocketEvent(wxSocketEvent& event) {
364#define RD_BUF_SIZE 4096
365 // Allows handling of high volume data streams, such as a National AIS
366 // stream with 100s of msgs a second.
367
368 switch (event.GetSocketEvent()) {
369 case wxSOCKET_INPUT: // from gpsd Daemon
370 {
371 // TODO determine if the following SetFlags needs to be done at every
372 // socket event or only once when socket is created, it it needs to be
373 // done at all!
374 // m_sock->SetFlags(wxSOCKET_WAITALL | wxSOCKET_BLOCK); // was
375 // (wxSOCKET_NOWAIT);
376
377 // We use wxSOCKET_BLOCK to avoid Yield() reentrancy problems
378 // if a long ProgressDialog is active, as in S57 SENC creation.
379
380 // Disable input event notifications to preclude re-entrancy on
381 // non-blocking socket
382 // m_sock->SetNotify(wxSOCKET_LOST_FLAG);
383 uint8_t buff[RD_BUF_SIZE + 1];
384 event.GetSocket()->Read(buff, RD_BUF_SIZE);
385 if (!event.GetSocket()->Error()) {
386 unsigned count = event.GetSocket()->LastCount();
387 for (unsigned i = 0; i < count; i += 1) n0183_buffer.Put(buff[i]);
388 while (n0183_buffer.HasSentence()) {
389 HandleN0183Msg(n0183_buffer.GetSentence() + "\r\n");
390 }
391 }
392 m_dog_value = N_DOG_TIMEOUT; // feed the dog
393 break;
394 }
395
396 case wxSOCKET_LOST: {
397 m_driver_stats.available = GetSock()->IsOk();
398 using namespace std::chrono;
399 if (m_params.net_protocol == TCP || m_params.net_protocol == GPSD) {
400 if (m_rx_connect_event) {
401 MESSAGE_LOG << "NetworkDataStream connection lost: "
402 << m_params.GetDSPort();
403 }
404 if (m_socket_server) {
405 m_sock->Destroy();
406 m_sock = nullptr;
407 break;
408 }
409 auto since_connect = 10s;
410 // ten secs assumed, if connect time is uninitialized
411 auto now = steady_clock::now();
412 if (m_connect_time != time_point<steady_clock>())
413 since_connect = duration_cast<seconds>(now - m_connect_time);
414
415 auto retry_time = 5s; // default
416 // If the socket has never connected, and it is a short interval since
417 // the connect request then stretch the time a bit. This happens on
418 // Windows if there is no default IP on any interface
419 if (!m_rx_connect_event && (since_connect < 5s)) retry_time = 10s;
420
421 m_socketread_watchdog_timer.Stop();
422
423 // Schedule a re-connect attempt
424 m_socket_timer.Start(duration_cast<milliseconds>(retry_time).count(),
425 wxTIMER_ONE_SHOT);
426 }
427 break;
428 }
429
430 case wxSOCKET_CONNECTION: {
431 if (m_params.net_protocol == GPSD) {
432 // Sign up for watcher mode, Cooked NMEA
433 // Note that SIRF devices will be converted by gpsd into
434 // pseudo-NMEA
435
436 char cmd[] = R"--(?WATCH={"class":"WATCH", "nmea":true})--";
437 m_sock->Write(cmd, strlen(cmd));
438 } else if (m_params.net_protocol == TCP) {
439 MESSAGE_LOG << "TCP NetworkDataStream connection established: "
440 << m_params.GetDSPort();
441
442 m_dog_value = N_DOG_TIMEOUT; // feed the dog
443 if (m_params.direction != PortDirection::kOutput) {
444 // start the DATA watchdog only if NODATA Reconnect is desired
445 if (GetParams().no_data_reconnect)
446 m_socketread_watchdog_timer.Start(1000);
447 }
448
449 if (m_params.direction != PortDirection::kInput && GetSock()->IsOk())
450 (void)SetOutputSocketOptions(m_sock);
451 m_socket_timer.Stop();
452 m_rx_connect_event = true;
453 }
454
455 m_driver_stats.available = true;
456 m_connect_time = std::chrono::steady_clock::now();
457 break;
458 }
459
460 default:
461 break;
462 }
463}
464
465void CommDriverN0183Net::OnServerSocketEvent(wxSocketEvent& event) {
466 switch (event.GetSocketEvent()) {
467 case wxSOCKET_CONNECTION: {
468 m_sock = m_socket_server->Accept(false);
469
470 if (GetSock()) {
471 m_sock->SetTimeout(2);
472 // GetSock()->SetFlags(wxSOCKET_BLOCK);
473 m_sock->SetEventHandler(*this, DS_SOCKET_ID);
474 int notify_flags = (wxSOCKET_CONNECTION_FLAG | wxSOCKET_LOST_FLAG);
475 if (m_params.direction != PortDirection::kInput) {
476 notify_flags |= wxSOCKET_OUTPUT_FLAG;
477 (void)SetOutputSocketOptions(m_sock);
478 }
479 if (m_params.direction != PortDirection::kOutput)
480 notify_flags |= wxSOCKET_INPUT_FLAG;
481 m_sock->SetNotify(notify_flags);
482 m_sock->Notify(true);
483 }
484 break;
485 }
486 default:
487 break;
488 }
489}
490
491bool CommDriverN0183Net::SendSentenceNetwork(const wxString& payload) {
492 if (m_txenter)
493 return false; // do not allow recursion, could happen with non-blocking
494 // sockets
495 m_txenter++;
496
497 bool ret = true;
498 wxDatagramSocket* udp_socket;
499 switch (m_params.net_protocol) {
500 case TCP:
501 if (GetSock() && GetSock()->IsOk()) {
502 m_sock->Write(payload.mb_str(), strlen(payload.mb_str()));
503 m_dog_value = N_DOG_TIMEOUT; // feed the dog
504 if (GetSock()->Error()) {
505 if (m_socket_server) {
506 m_sock->Destroy();
507 m_sock = nullptr;
508 } else {
509 auto* tcp_socket = dynamic_cast<wxSocketClient*>(m_sock);
510 if (tcp_socket) tcp_socket->Close();
511 if (!m_socket_timer.IsRunning())
512 m_socket_timer.Start(5000, wxTIMER_ONE_SHOT);
513 // schedule a reconnect
514 m_socketread_watchdog_timer.Stop();
515 }
516 ret = false;
517 }
518
519 } else {
520 ret = false;
521 }
522
523 break;
524 case UDP:
525 udp_socket = dynamic_cast<wxDatagramSocket*>(m_tsock);
526 if (udp_socket && udp_socket->IsOk()) {
527 udp_socket->SendTo(m_addr, payload.mb_str(), payload.size());
528 m_dog_value = N_DOG_TIMEOUT; // feed the dog
529 if (udp_socket->Error()) ret = false;
530 } else {
531 ret = false;
532 }
533 m_driver_stats.available = ret;
534 break;
535
536 case GPSD:
537 default:
538 ret = false;
539 break;
540 }
541 m_txenter--;
542 return ret;
543}
544
545void CommDriverN0183Net::Close() {
546 MESSAGE_LOG << "Closing NMEA NetworkDataStream " << m_params.network_port;
547 m_stats_timer.Stop();
548 // Kill off the TCP Socket if alive
549 if (m_sock) {
550 if (m_is_multicast)
551 m_sock->SetOption(IPPROTO_IP, IP_DROP_MEMBERSHIP, &m_mrq_container->m_mrq,
552 sizeof(m_mrq_container->m_mrq));
553 m_sock->Notify(FALSE);
554 m_sock->Destroy();
555 }
556
557 if (m_tsock) {
558 m_tsock->Notify(FALSE);
559 m_tsock->Destroy();
560 }
561
562 if (m_socket_server) {
563 m_socket_server->Notify(FALSE);
564 m_socket_server->Destroy();
565 }
566
567 m_socket_timer.Stop();
568 m_socketread_watchdog_timer.Stop();
569 m_driver_stats.available = false;
570}
NMEA0183 basic parsing common parts:
void SendToListener(const std::string &payload, DriverListener &listener, const ConnectionParams &params)
Wrap argument string in NavMsg pointer, forward to listener.
obs::EventVar evt_driver_msg
Notified for messages from drivers.
Connection data container close to a POD struct.
Definition conn_params.h:75
std::string GetStrippedDSPort() const
Return port string with possible windows extra data removed, in some cases empty.
wxString GetDSPort() const
Return port description including for example serial port or network address/port,...
Interface for handling incoming messages.
Definition comm_driver.h:50
bool HasSentence() const
Return true if a sentence is available to be returned by GetSentence()
std::string GetSentence()
Retrieve a sentence from buffer.
void Put(uint8_t ch)
Add a single character, possibly making a sentence available.
Where messages are sent to or received from.
Custom event class for OpenCPN's notification system.
void Notify() override
Notify all listeners, no data supplied.
Definition evtvar.h:83
NMEA0183 over IP driver.
Driver registration container, a singleton.
Raw messages layer, supports sending and recieving navmsg messages.
Global variables stored in configuration file.
std::string PortDirectionToString(PortDirection pd)
Return textual representation for use in driver ioDirection attribute.
NMEA Data Object.
GUI constant definitions.
Enhanced logging interface on top of wx/log.h.
bool endswith(const std::string &str, const std::string &suffix)
Return true if s ends with given suffix.
General observable pattern implementation built on top of wxWidgets event handling.
unsigned tx_count
Number of bytes sent since program start.
unsigned rx_count
Number of bytes received since program start.
unsigned error_count
Number of detected errors since program start.
Suspend/resume and new devices events exchange point.