OpenCPN Partial API docs
Loading...
Searching...
No Matches
comm_drv_n2k_socketcan.cpp
Go to the documentation of this file.
1/***************************************************************************
2 * Copyright (C) 2022 by 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#if !defined(__linux__) || defined(__ANDROID__)
26#error "This file can only be compiled on Linux"
27#endif
28
29#include "config.h"
30
31#include <algorithm>
32#include <atomic>
33#include <chrono>
34#include <mutex>
35#include <thread>
36#include <vector>
37#include <future>
38
39#include <net/if.h>
40#include <serial/serial.h>
41#include <sys/ioctl.h>
42#include <sys/socket.h>
43#include <sys/time.h>
44#include <unistd.h>
45
46#include <wx/log.h>
47#include <wx/string.h>
48#include <wx/utils.h>
49#include <wx/thread.h>
50
51#include "model/comm_can_util.h"
55#include "model/config_vars.h"
56
57#define DEFAULT_N2K_SOURCE_ADDRESS 72
58
59wxDEFINE_EVENT(EVT_N2K_59904, ObservedEvt);
60
61static const int kNotFound = -1;
62
64static const int kSocketTimeoutSeconds = 2;
65
66typedef struct can_frame CanFrame;
67
69using namespace std::literals::chrono_literals;
70
95class Worker {
96public:
97 Worker(CommDriverN2KSocketCAN* parent, const wxString& PortName);
98
99 bool StartThread();
100 void StopThread();
101 int GetSocket() { return m_socket; }
102
103private:
104 void Entry();
105
106 void ThreadMessage(const std::string& msg, wxLogLevel l = wxLOG_Message);
107
108 int InitSocket(const std::string port_name);
109 void SocketMessage(const std::string& msg, const std::string& device);
110 void HandleInput(CanFrame frame);
111 void ProcessRxMessages(std::shared_ptr<const Nmea2000Msg> n2k_msg);
112
113 std::vector<unsigned char> PushCompleteMsg(const CanHeader header,
114 int position,
115 const CanFrame frame);
116 std::vector<unsigned char> PushFastMsgFragment(const CanHeader& header,
117 int position);
118
119 CommDriverN2KSocketCanImpl* const m_parent_driver;
120 const wxString m_port_name;
121 std::atomic<int> m_run_flag;
122 FastMessageMap fast_messages;
123 int m_socket;
124};
125
128 friend class Worker;
129
130public:
133 m_worker(this, p->socket_can_port),
134 m_source_address(-1),
135 m_last_TX_sequence(0) {
136 SetN2K_Name();
137 Open();
138 }
139
140 ~CommDriverN2KSocketCanImpl() { Close(); }
141
142 bool Open();
143 void Close();
144 void SetN2K_Name();
145
146 bool SendMessage(std::shared_ptr<const NavMsg> msg,
147 std::shared_ptr<const NavAddr> addr);
148
149 int DoAddressClaim();
150 bool SendAddressClaim(int proposed_source_address);
151 bool SendProductInfo();
152
153 Worker& GetWorker() { return m_worker; }
154 void UpdateAttrCanAddress();
155
156private:
157 N2kName node_name;
158 Worker m_worker;
159 int m_source_address;
160 int m_last_TX_sequence;
161 std::future<int> m_AddressClaimFuture;
162 wxMutex m_TX_mutex;
163 int m_unique_number;
164
165 ObservableListener listener_N2K_59904;
166 bool HandleN2K_59904(std::shared_ptr<const Nmea2000Msg> n2k_msg);
167};
168
169// Static CommDriverN2KSocketCAN factory implementation.
170
171std::unique_ptr<CommDriverN2KSocketCAN> CommDriverN2KSocketCAN::Create(
172 const ConnectionParams* params, DriverListener& listener) {
173 return std::unique_ptr<CommDriverN2KSocketCAN>(
174 new CommDriverN2KSocketCanImpl(params, listener));
175}
176
177// CommDriverN2KSocketCanImpl implementation
178
179void CommDriverN2KSocketCanImpl::SetN2K_Name() {
180 // We choose some "benign" values for OCPN socketCan interface
181 node_name.value.Name = 0;
182
183 m_unique_number = 1;
184 // Build a simple 16 bit hash of g_hostname, to use as unique "serial number"
185 int hash = 0;
186 std::string str(g_hostname.mb_str());
187 int len = str.size();
188 const char* ch = str.data();
189 for (int i = 0; i < len; i++)
190 hash = hash + ((hash) << 5) + *(ch + i) + ((*(ch + i)) << 7);
191 m_unique_number = ((hash) ^ (hash >> 16)) & 0xffff;
192
193 node_name.SetManufacturerCode(2046);
194 node_name.SetUniqueNumber(m_unique_number);
195 node_name.SetDeviceFunction(130); // Display
196 node_name.SetDeviceClass(120); // Display
197 node_name.SetIndustryGroup(4); // Marine
198 node_name.SetSystemInstance(0);
199}
200
201void CommDriverN2KSocketCanImpl::UpdateAttrCanAddress() {
202 this->attributes["canAddress"] = std::to_string(m_source_address);
203}
204
205bool CommDriverN2KSocketCanImpl::Open() {
206 // Start the RX worker thread
207 bool bws = m_worker.StartThread();
208 return bws;
209}
210
211void CommDriverN2KSocketCanImpl::Close() {
212 wxLogMessage("Closing N2K socketCAN: %s", m_params.socket_can_port.c_str());
213 m_stats_timer.Stop();
214 m_worker.StopThread();
215}
216
217bool CommDriverN2KSocketCanImpl::SendAddressClaim(int proposed_source_address) {
218 wxMutexLocker lock(m_TX_mutex);
219
220 int socket = GetWorker().GetSocket();
221
222 if (socket < 0) return false;
223
224 CanFrame frame;
225 memset(&frame, 0, sizeof(frame));
226
227 uint64_t _pgn = 60928;
228 unsigned long canId = BuildCanID(6, proposed_source_address, 255, _pgn);
229 frame.can_id = canId | CAN_EFF_FLAG;
230
231 // Load the data
232 uint32_t b32_0 = node_name.value.UnicNumberAndManCode;
233 memcpy(&frame.data, &b32_0, 4);
234
235 unsigned char b81 = node_name.value.DeviceInstance;
236 memcpy(&frame.data[4], &b81, 1);
237
238 b81 = node_name.value.DeviceFunction;
239 memcpy(&frame.data[5], &b81, 1);
240
241 b81 = (node_name.value.DeviceClass);
242 memcpy(&frame.data[6], &b81, 1);
243
244 b81 = node_name.value.IndustryGroupAndSystemInstance;
245 memcpy(&frame.data[7], &b81, 1);
246
247 frame.can_dlc = 8; // data length
248
249 int sentbytes = write(socket, &frame, sizeof(frame));
250
251 return (sentbytes == 16);
252}
253
254void AddStr(std::vector<uint8_t>& vec, std::string str, size_t max_len) {
255 size_t i;
256 for (i = 0; i < str.size(); i++) {
257 vec.push_back(str[i]);
258 ;
259 }
260 for (; i < max_len; i++) {
261 vec.push_back(0);
262 }
263}
264
265bool CommDriverN2KSocketCanImpl::SendProductInfo() {
266 // Create the payload
267 std::vector<uint8_t> payload;
268
269 payload.push_back(2100 & 0xFF); // N2KVersion
270 payload.push_back(2100 >> 8);
271 payload.push_back(0xEC); // Product Code, 1772
272 payload.push_back(0x06);
273
274 std::string ModelID("OpenCPN"); // Model ID
275 AddStr(payload, ModelID, 32);
276
277 std::string ModelSWCode(PACKAGE_VERSION); // SwCode
278 AddStr(payload, ModelSWCode, 32);
279
280 std::string ModelVersion(PACKAGE_VERSION); // Model Version
281 AddStr(payload, ModelVersion, 32);
282
283 std::string ModelSerialCode(
284 std::to_string(m_unique_number)); // Model Serial Code
285 AddStr(payload, ModelSerialCode, 32);
286
287 payload.push_back(0); // CertificationLevel
288 payload.push_back(0); // LoadEquivalency
289
290 auto dest_addr = std::make_shared<const NavAddr2000>(iface, 255);
291 uint64_t _PGN;
292 _PGN = 126996;
293
294 auto msg = std::make_shared<const Nmea2000Msg>(_PGN, payload, dest_addr);
295 SendMessage(msg, dest_addr);
296
297 return true;
298}
299
300bool CommDriverN2KSocketCanImpl::SendMessage(
301 std::shared_ptr<const NavMsg> msg, std::shared_ptr<const NavAddr> addr) {
302 if (!msg) return false;
303 wxMutexLocker lock(m_TX_mutex);
304
305 // Verify claimed address is useable
306 if (m_source_address < 0) return false;
307
308 if (m_source_address > 253) // Could not claim...
309 return false;
310
311 int socket = GetWorker().GetSocket();
312
313 if (socket < 0) return false;
314
315 CanFrame frame;
316 memset(&frame, 0, sizeof(frame));
317
318 auto msg_n2k = std::dynamic_pointer_cast<const Nmea2000Msg>(msg);
319 std::vector<uint8_t> load = msg_n2k->payload;
320
321 uint64_t _pgn = msg_n2k->PGN.pgn;
322 auto destination_address = std::static_pointer_cast<const NavAddr2000>(addr);
323
324 unsigned long canId = BuildCanID(msg_n2k->priority, m_source_address,
325 destination_address->address, _pgn);
326
327 frame.can_id = canId | CAN_EFF_FLAG;
328
329 int sentbytes = 0;
330
331 if (!IsFastMessagePGN(_pgn)) {
332 frame.can_dlc = load.size();
333 if (load.size() > 0) memcpy(&frame.data, load.data(), load.size());
334
335 sentbytes += write(socket, &frame, sizeof(frame));
336 } else { // Fast Packet
337 int sequence = (m_last_TX_sequence + 0x20) & 0xE0;
338 m_last_TX_sequence = sequence;
339 unsigned char* data_ptr = load.data();
340 int n_remaining = load.size();
341
342 // First packet
343 frame.can_dlc = 8;
344 frame.data[0] = sequence;
345 frame.data[1] = load.size();
346 int data_len_0 = wxMin(load.size(), 6);
347 memcpy(&frame.data[2], load.data(), data_len_0);
348
349 sentbytes += write(socket, &frame, sizeof(frame));
350
351 data_ptr += data_len_0;
352 n_remaining -= data_len_0;
353 sequence++;
354
355 // The rest of the bytes
356 while (n_remaining > 0) {
357 wxMilliSleep(10);
358 frame.data[0] = sequence;
359 int data_len_n = wxMin(n_remaining, 7);
360 memcpy(&frame.data[1], data_ptr, data_len_n);
361
362 sentbytes += write(socket, &frame, sizeof(frame));
363
364 data_ptr += data_len_n;
365 n_remaining -= data_len_n;
366 sequence++;
367 }
368 }
369
370 DriverStats stats = GetDriverStats();
371 stats.tx_count += sentbytes;
372 SetDriverStats(stats);
373
374 return true;
375}
376
377// CommDriverN2KSocketCAN implementation
378
379CommDriverN2KSocketCAN::CommDriverN2KSocketCAN(const ConnectionParams* params,
380 DriverListener& listener)
381 : CommDriverN2K(params->GetStrippedDSPort()),
382 m_params(*params),
383 m_listener(listener),
384 m_stats_timer(*this, 2s),
385 m_ok(false),
386 m_portstring(params->GetDSPort()),
387 m_baudrate(wxString::Format("%i", params->baudrate)) {
388 this->attributes["canPort"] = params->socket_can_port.ToStdString();
389 this->attributes["canAddress"] = std::to_string(DEFAULT_N2K_SOURCE_ADDRESS);
390 this->attributes["userComment"] = params->user_comment.ToStdString();
391 this->attributes["ioDirection"] = std::string("IN/OUT");
392
393 m_driver_stats.driver_bus = NavAddr::Bus::N2000;
394 m_driver_stats.driver_iface = params->GetStrippedDSPort();
395}
396
397CommDriverN2KSocketCAN::~CommDriverN2KSocketCAN() {}
398
399// Worker implementation
400
401Worker::Worker(CommDriverN2KSocketCAN* parent, const wxString& port_name)
402 : m_parent_driver(dynamic_cast<CommDriverN2KSocketCanImpl*>(parent)),
403 m_port_name(port_name.Clone()),
404 m_run_flag(-1),
405 m_socket(-1) {
406 assert(m_parent_driver != 0);
407}
408
409std::vector<unsigned char> Worker::PushCompleteMsg(const CanHeader header,
410 int position,
411 const CanFrame frame) {
412 std::vector<unsigned char> data;
413 data.push_back(0x93);
414 data.push_back(0x13);
415 data.push_back(header.priority);
416 data.push_back(header.pgn & 0xFF);
417 data.push_back((header.pgn >> 8) & 0xFF);
418 data.push_back((header.pgn >> 16) & 0xFF);
419 data.push_back(header.destination);
420 data.push_back(header.source);
421 data.push_back(0xFF); // FIXME (dave) generate the time fields
422 data.push_back(0xFF);
423 data.push_back(0xFF);
424 data.push_back(0xFF);
425 data.push_back(CAN_MAX_DLEN); // nominally 8
426 for (size_t n = 0; n < CAN_MAX_DLEN; n++) data.push_back(frame.data[n]);
427 data.push_back(0x55); // CRC dummy, not checked
428 return data;
429}
430
431std::vector<unsigned char> Worker::PushFastMsgFragment(const CanHeader& header,
432 int position) {
433 std::vector<unsigned char> data;
434 data.push_back(0x93);
435 data.push_back(fast_messages[position].expected_length + 11);
436 data.push_back(header.priority);
437 data.push_back(header.pgn & 0xFF);
438 data.push_back((header.pgn >> 8) & 0xFF);
439 data.push_back((header.pgn >> 16) & 0xFF);
440 data.push_back(header.destination);
441 data.push_back(header.source);
442 data.push_back(0xFF); // FIXME (dave) Could generate the time fields
443 data.push_back(0xFF);
444 data.push_back(0xFF);
445 data.push_back(0xFF);
446 data.push_back(fast_messages[position].expected_length);
447 for (size_t n = 0; n < fast_messages[position].expected_length; n++)
448 data.push_back(fast_messages[position].data[n]);
449 data.push_back(0x55); // CRC dummy
450 fast_messages.Remove(position);
451 return data;
452}
453
454void Worker::ThreadMessage(const std::string& msg, wxLogLevel level) {
455 wxLogGeneric(level, wxString(msg.c_str()));
456 auto s = std::string("CommDriverN2KSocketCAN: ") + msg;
457 CommDriverRegistry::GetInstance().evt_driver_msg.Notify(level, s);
458}
459
460void Worker::SocketMessage(const std::string& msg, const std::string& device) {
461 std::stringstream ss;
462 ss << msg << device << ": " << strerror(errno);
463 ThreadMessage(ss.str());
464}
465
472int Worker::InitSocket(const std::string port_name) {
473 int sock = socket(PF_CAN, SOCK_RAW, CAN_RAW);
474 if (sock < 0) {
475 SocketMessage("SocketCAN socket create failed: ", port_name);
476 return -1;
477 }
478
479 // Get the interface index
480 struct ifreq if_request;
481 strcpy(if_request.ifr_name, port_name.c_str());
482 if (ioctl(sock, SIOCGIFINDEX, &if_request) < 0) {
483 SocketMessage("SocketCAN ioctl (SIOCGIFINDEX) failed: ", port_name);
484 close(sock);
485 return -1;
486 }
487
488 // Check if interface is UP
489 struct sockaddr_can can_address;
490 can_address.can_family = AF_CAN;
491 can_address.can_ifindex = if_request.ifr_ifindex;
492 if (ioctl(sock, SIOCGIFFLAGS, &if_request) < 0) {
493 SocketMessage("SocketCAN socket IOCTL (SIOCGIFFLAGS) failed: ", port_name);
494 close(sock);
495 return -1;
496 }
497 if (if_request.ifr_flags & IFF_UP) {
498 ThreadMessage("socketCan interface is UP");
499 } else {
500 ThreadMessage("socketCan interface is NOT UP");
501 close(sock);
502 return -1;
503 }
504
505 // Set timeout and bind
506 struct timeval tv;
507 tv.tv_sec = kSocketTimeoutSeconds;
508 tv.tv_usec = 0;
509 int r =
510 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
511 if (r < 0) {
512 SocketMessage("SocketCAN setsockopt SO_RCVTIMEO failed on device: ",
513 port_name);
514 close(sock);
515 return -1;
516 }
517 r = bind(sock, (struct sockaddr*)&can_address, sizeof(can_address));
518 if (r < 0) {
519 SocketMessage("SocketCAN socket bind() failed: ", port_name);
520 close(sock);
521 return -1;
522 }
523 DriverStats stats = m_parent_driver->GetDriverStats();
524 stats.available = true;
525 m_parent_driver->SetDriverStats(stats);
526
527 return sock;
528}
529
536void Worker::HandleInput(CanFrame frame) {
537 int position = -1;
538 bool ready = true;
539
540 CanHeader header(frame);
541 if (header.IsFastMessage()) {
542 position = fast_messages.FindMatchingEntry(header, frame.data[0]);
543 if (position == kNotFound) {
544 // Not an existing fast message: create new entry and insert first frame
545 position = fast_messages.AddNewEntry();
546 ready = fast_messages.InsertEntry(header, frame.data, position);
547 } else {
548 // An existing fast message entry is present, append the frame
549 ready = fast_messages.AppendEntry(header, frame.data, position);
550 }
551 }
552 if (ready) {
553 std::vector<unsigned char> vec;
554 if (position >= 0) {
555 // Re-assembled fast message
556 vec = PushFastMsgFragment(header, position);
557 } else {
558 // Single frame message
559 vec = PushCompleteMsg(header, position, frame);
560 }
561 // auto name = N2kName(static_cast<uint64_t>(header.pgn));
562 auto src_addr = m_parent_driver->GetAddress(m_parent_driver->node_name);
563 auto msg = std::make_shared<const Nmea2000Msg>(header.pgn, vec, src_addr);
564
565 ProcessRxMessages(msg);
566 m_parent_driver->m_listener.Notify(std::move(msg));
567
568 DriverStats stats = m_parent_driver->GetDriverStats();
569 stats.rx_count += vec.size();
570 m_parent_driver->SetDriverStats(stats);
571 }
572}
573
575void Worker::ProcessRxMessages(std::shared_ptr<const Nmea2000Msg> n2k_msg) {
576 if (n2k_msg->PGN.pgn == 59904 &&
577 (n2k_msg->payload.at(6) == m_parent_driver->m_source_address ||
578 n2k_msg->payload.at(6) == 0xff)) {
579 unsigned long RequestedPGN = 0;
580 RequestedPGN = n2k_msg->payload.at(15) << 16;
581 RequestedPGN += n2k_msg->payload.at(14) << 8;
582 RequestedPGN += n2k_msg->payload.at(13);
583
584 switch (RequestedPGN) {
585 case 60928:
586 m_parent_driver->SendAddressClaim(m_parent_driver->m_source_address);
587 break;
588 case 126996:
589 m_parent_driver->SendProductInfo();
590 break;
591 default:
592 break;
593 }
594 }
595
596 else if (n2k_msg->PGN.pgn == 60928) {
597 // Watch for conflicting source address
598 if (n2k_msg->payload.at(7) == m_parent_driver->m_source_address) {
599 // My name
600 uint64_t my_name = m_parent_driver->node_name.GetName();
601
602 // His name
603 uint64_t his_name = 0;
604 unsigned char* p = (unsigned char*)&his_name;
605 for (unsigned int i = 0; i < 8; i++) *p++ = n2k_msg->payload.at(13 + i);
606
607 // Compare literally the NAME values
608 if (his_name < my_name) {
609 // I lose, so select a new address
610 m_parent_driver->m_source_address++;
611 if (m_parent_driver->m_source_address > 253)
612 // Could not claim an address
613 m_parent_driver->m_source_address = 254;
614 m_parent_driver->UpdateAttrCanAddress();
615 }
616
617 // Claim the existing or modified address
618 m_parent_driver->SendAddressClaim(m_parent_driver->m_source_address);
619 }
620 }
621}
622
624void Worker::Entry() {
625 int recvbytes;
626 int socket;
627 CanFrame frame;
628
629 socket = InitSocket(m_port_name.ToStdString());
630 if (socket < 0) {
631 std::string msg("SocketCAN socket create failed: ");
632 ThreadMessage(msg + m_port_name.ToStdString());
633 m_run_flag = -1;
634 return;
635 }
636 m_socket = socket;
637
638 // Claim our default address
639 if (m_parent_driver->SendAddressClaim(DEFAULT_N2K_SOURCE_ADDRESS)) {
640 m_parent_driver->m_source_address = DEFAULT_N2K_SOURCE_ADDRESS;
641 m_parent_driver->UpdateAttrCanAddress();
642 }
643
644 // The main loop
645 while (m_run_flag > 0) {
646 recvbytes = read(socket, &frame, sizeof(frame));
647 if (recvbytes == -1) {
648 if (errno == EAGAIN || errno == EWOULDBLOCK) continue; // timeout
649
650 wxLogWarning("can socket %s: fatal error %s", m_port_name.c_str(),
651 strerror(errno));
652 break;
653 }
654 if (recvbytes != 16) {
655 wxLogWarning("can socket %s: bad frame size: %d (ignored)",
656 m_port_name.c_str(), recvbytes);
657 sleep(1);
658 continue;
659 }
660 HandleInput(frame);
661 }
662
663 // Release the socket on both exit paths, normal stop and fatal read error.
664 // Without this each driver teardown strands an open CAN descriptor, and its
665 // kernel receive buffer, for the life of the process.
666 close(socket);
667 m_socket = -1;
668
669 m_run_flag = -1;
670 return;
671}
672
673bool Worker::StartThread() {
674 m_run_flag = 1;
675 std::thread t(&Worker::Entry, this);
676 t.detach();
677 return true;
678}
679
680void Worker::StopThread() {
681 if (m_run_flag < 0) {
682 wxLogMessage("Attempt to stop already dead thread (ignored).");
683 return;
684 }
685 wxLogMessage("Stopping Worker Thread");
686
687 m_run_flag = 0;
688 int tsec = 10;
689 while ((m_run_flag >= 0) && (tsec--)) wxSleep(1);
690
691 if (m_run_flag < 0)
692 wxLogMessage("StopThread: Stopped in %d sec.", 10 - tsec);
693 else
694 wxLogWarning("StopThread: Not Stopped after 10 sec.");
695}
const std::string iface
Physical device for 0183, else a unique string.
Definition comm_driver.h:95
CAN v2.0 29 bit header as used by NMEA 2000.
bool IsFastMessage() const
Return true if header reflects a multipart fast message.
DriverStats GetDriverStats() const override
Get the Driver Statistics.
Local driver implementation, not visible outside this file.
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.
Interface for handling incoming messages.
Definition comm_driver.h:50
virtual void Notify(std::shared_ptr< const NavMsg > message)=0
Handle a received message.
Track fast message fragments eventually forming complete messages.
int AddNewEntry(void)
Allocate a new, fresh entry and return index to it.
void Remove(int pos)
Remove entry at pos.
bool AppendEntry(const CanHeader hdr, const unsigned char *data, int index)
Append fragment to existing multipart message.
int FindMatchingEntry(const CanHeader header, const unsigned char sid)
Setter.
bool InsertEntry(const CanHeader header, const unsigned char *data, int index)
Insert a new entry, first part of a multipart message.
Manages listening to an Observable instance.
Definition observable.h:175
Custom event class for OpenCPN's notification system.
Manages reading the N2K data stream provided by some N2K gateways from the declared serial port.
void Notify() override
Notify all listeners, no data supplied.
Definition evtvar.h:83
Low-level socketcan utility functions.
Low-level driver for socketcan devices (linux only).
Driver registration container, a singleton.
Raw messages layer, supports sending and recieving navmsg messages.
Global variables stored in configuration file.
Driver statistics report.
unsigned tx_count
Number of bytes sent since program start.
unsigned rx_count
Number of bytes received since program start.
N2k uses CAN which defines the basic properties of messages.
Definition comm_navmsg.h:70