OpenCPN Partial API docs
Loading...
Searching...
No Matches
comm_drv_n2k_socketcan.cpp
1/***************************************************************************
2 *
3 * Project: OpenCPN
4 * Purpose: Implement comm_drv_socketcan.h -- socketcan driver.
5 * Author: David Register, Alec Leamas
6 *
7 ***************************************************************************
8 * Copyright (C) 2022 by David Register, Alec Leamas *
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
26#if !defined(__linux__) || defined(__ANDROID__)
27#error "This file can only be compiled on Linux"
28#endif
29
30#include <algorithm>
31#include <atomic>
32#include <chrono>
33#include <mutex>
34#include <thread>
35#include <vector>
36#include <future>
37
38#include "config.h"
39
40#include <net/if.h>
41#include <serial/serial.h>
42#include <sys/ioctl.h>
43#include <sys/socket.h>
44#include <sys/time.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"
52#include "model/comm_drv_n2k_socketcan.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->socketCAN_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.socketCAN_port.c_str());
213 m_stats_timer.Stop();
214 m_worker.StopThread();
215
216 // We cannot use shared_from_this() since we might be in the destructor.
217 auto& registry = CommDriverRegistry::GetInstance();
218 auto& me = FindDriver(registry.GetDrivers(), iface, bus);
219 registry.Deactivate(me);
220}
221
222bool CommDriverN2KSocketCanImpl::SendAddressClaim(int proposed_source_address) {
223 wxMutexLocker lock(m_TX_mutex);
224
225 int socket = GetWorker().GetSocket();
226
227 if (socket < 0) return false;
228
229 CanFrame frame;
230 memset(&frame, 0, sizeof(frame));
231
232 uint64_t _pgn = 60928;
233 unsigned long canId = BuildCanID(6, proposed_source_address, 255, _pgn);
234 frame.can_id = canId | CAN_EFF_FLAG;
235
236 // Load the data
237 uint32_t b32_0 = node_name.value.UnicNumberAndManCode;
238 memcpy(&frame.data, &b32_0, 4);
239
240 unsigned char b81 = node_name.value.DeviceInstance;
241 memcpy(&frame.data[4], &b81, 1);
242
243 b81 = node_name.value.DeviceFunction;
244 memcpy(&frame.data[5], &b81, 1);
245
246 b81 = (node_name.value.DeviceClass);
247 memcpy(&frame.data[6], &b81, 1);
248
249 b81 = node_name.value.IndustryGroupAndSystemInstance;
250 memcpy(&frame.data[7], &b81, 1);
251
252 frame.can_dlc = 8; // data length
253
254 int sentbytes = write(socket, &frame, sizeof(frame));
255
256 return (sentbytes == 16);
257}
258
259void AddStr(std::vector<uint8_t>& vec, std::string str, size_t max_len) {
260 size_t i;
261 for (i = 0; i < str.size(); i++) {
262 vec.push_back(str[i]);
263 ;
264 }
265 for (; i < max_len; i++) {
266 vec.push_back(0);
267 }
268}
269
270bool CommDriverN2KSocketCanImpl::SendProductInfo() {
271 // Create the payload
272 std::vector<uint8_t> payload;
273
274 payload.push_back(2100 & 0xFF); // N2KVersion
275 payload.push_back(2100 >> 8);
276 payload.push_back(0xEC); // Product Code, 1772
277 payload.push_back(0x06);
278
279 std::string ModelID("OpenCPN"); // Model ID
280 AddStr(payload, ModelID, 32);
281
282 std::string ModelSWCode(PACKAGE_VERSION); // SwCode
283 AddStr(payload, ModelSWCode, 32);
284
285 std::string ModelVersion(PACKAGE_VERSION); // Model Version
286 AddStr(payload, ModelVersion, 32);
287
288 std::string ModelSerialCode(
289 std::to_string(m_unique_number)); // Model Serial Code
290 AddStr(payload, ModelSerialCode, 32);
291
292 payload.push_back(0); // CertificationLevel
293 payload.push_back(0); // LoadEquivalency
294
295 auto dest_addr = std::make_shared<const NavAddr2000>(iface, 255);
296 uint64_t _PGN;
297 _PGN = 126996;
298
299 auto msg = std::make_shared<const Nmea2000Msg>(_PGN, payload, dest_addr);
300 SendMessage(msg, dest_addr);
301
302 return true;
303}
304
305bool CommDriverN2KSocketCanImpl::SendMessage(
306 std::shared_ptr<const NavMsg> msg, std::shared_ptr<const NavAddr> addr) {
307 wxMutexLocker lock(m_TX_mutex);
308
309 // Verify claimed address is useable
310 if (m_source_address < 0) return false;
311
312 if (m_source_address > 253) // Could not claim...
313 return false;
314
315 int socket = GetWorker().GetSocket();
316
317 if (socket < 0) return false;
318
319 CanFrame frame;
320 memset(&frame, 0, sizeof(frame));
321
322 auto msg_n2k = std::dynamic_pointer_cast<const Nmea2000Msg>(msg);
323 std::vector<uint8_t> load = msg_n2k->payload;
324
325 uint64_t _pgn = msg_n2k->PGN.pgn;
326 auto destination_address = std::static_pointer_cast<const NavAddr2000>(addr);
327
328 unsigned long canId = BuildCanID(msg_n2k->priority, m_source_address,
329 destination_address->address, _pgn);
330
331 frame.can_id = canId | CAN_EFF_FLAG;
332
333 int sentbytes = 0;
334
335 if (load.size() <= 8) {
336 frame.can_dlc = load.size();
337 if (load.size() > 0) memcpy(&frame.data, load.data(), load.size());
338
339 sentbytes += write(socket, &frame, sizeof(frame));
340 } else { // Fast Packet
341 int sequence = (m_last_TX_sequence + 0x20) & 0xE0;
342 m_last_TX_sequence = sequence;
343 unsigned char* data_ptr = load.data();
344 int n_remaining = load.size();
345
346 // First packet
347 frame.can_dlc = 8;
348 frame.data[0] = sequence;
349 frame.data[1] = load.size();
350 int data_len_0 = wxMin(load.size(), 6);
351 memcpy(&frame.data[2], load.data(), data_len_0);
352
353 sentbytes += write(socket, &frame, sizeof(frame));
354
355 data_ptr += data_len_0;
356 n_remaining -= data_len_0;
357 sequence++;
358
359 // The rest of the bytes
360 while (n_remaining > 0) {
361 wxMilliSleep(10);
362 frame.data[0] = sequence;
363 int data_len_n = wxMin(n_remaining, 7);
364 memcpy(&frame.data[1], data_ptr, data_len_n);
365
366 sentbytes += write(socket, &frame, sizeof(frame));
367
368 data_ptr += data_len_n;
369 n_remaining -= data_len_n;
370 sequence++;
371 }
372 }
373
374 DriverStats stats = GetDriverStats();
375 stats.tx_count += sentbytes;
376 SetDriverStats(stats);
377
378 return true;
379}
380
381// CommDriverN2KSocketCAN implementation
382
383CommDriverN2KSocketCAN::CommDriverN2KSocketCAN(const ConnectionParams* params,
384 DriverListener& listener)
385 : CommDriverN2K(params->GetStrippedDSPort()),
386 m_params(*params),
387 m_listener(listener),
388 m_stats_timer(*this, 2s),
389 m_ok(false),
390 m_portstring(params->GetDSPort()),
391 m_baudrate(wxString::Format("%i", params->Baudrate)) {
392 this->attributes["canPort"] = params->socketCAN_port.ToStdString();
393 this->attributes["canAddress"] = std::to_string(DEFAULT_N2K_SOURCE_ADDRESS);
394 this->attributes["userComment"] = params->UserComment.ToStdString();
395 this->attributes["ioDirection"] = std::string("IN/OUT");
396
397 m_driver_stats.driver_bus = NavAddr::Bus::N2000;
398 m_driver_stats.driver_iface = params->GetStrippedDSPort();
399}
400
401CommDriverN2KSocketCAN::~CommDriverN2KSocketCAN() {}
402
403// Worker implementation
404
405Worker::Worker(CommDriverN2KSocketCAN* parent, const wxString& port_name)
406 : m_parent_driver(dynamic_cast<CommDriverN2KSocketCanImpl*>(parent)),
407 m_port_name(port_name.Clone()),
408 m_run_flag(-1),
409 m_socket(-1) {
410 assert(m_parent_driver != 0);
411}
412
413std::vector<unsigned char> Worker::PushCompleteMsg(const CanHeader header,
414 int position,
415 const CanFrame frame) {
416 std::vector<unsigned char> data;
417 data.push_back(0x93);
418 data.push_back(0x13);
419 data.push_back(header.priority);
420 data.push_back(header.pgn & 0xFF);
421 data.push_back((header.pgn >> 8) & 0xFF);
422 data.push_back((header.pgn >> 16) & 0xFF);
423 data.push_back(header.destination);
424 data.push_back(header.source);
425 data.push_back(0xFF); // FIXME (dave) generate the time fields
426 data.push_back(0xFF);
427 data.push_back(0xFF);
428 data.push_back(0xFF);
429 data.push_back(CAN_MAX_DLEN); // nominally 8
430 for (size_t n = 0; n < CAN_MAX_DLEN; n++) data.push_back(frame.data[n]);
431 data.push_back(0x55); // CRC dummy, not checked
432 return data;
433}
434
435std::vector<unsigned char> Worker::PushFastMsgFragment(const CanHeader& header,
436 int position) {
437 std::vector<unsigned char> data;
438 data.push_back(0x93);
439 data.push_back(fast_messages[position].expected_length + 11);
440 data.push_back(header.priority);
441 data.push_back(header.pgn & 0xFF);
442 data.push_back((header.pgn >> 8) & 0xFF);
443 data.push_back((header.pgn >> 16) & 0xFF);
444 data.push_back(header.destination);
445 data.push_back(header.source);
446 data.push_back(0xFF); // FIXME (dave) Could generate the time fields
447 data.push_back(0xFF);
448 data.push_back(0xFF);
449 data.push_back(0xFF);
450 data.push_back(fast_messages[position].expected_length);
451 for (size_t n = 0; n < fast_messages[position].expected_length; n++)
452 data.push_back(fast_messages[position].data[n]);
453 data.push_back(0x55); // CRC dummy
454 fast_messages.Remove(position);
455 return data;
456}
457
458void Worker::ThreadMessage(const std::string& msg, wxLogLevel level) {
459 wxLogGeneric(level, wxString(msg.c_str()));
460 auto s = std::string("CommDriverN2KSocketCAN: ") + msg;
461 CommDriverRegistry::GetInstance().evt_driver_msg.Notify(level, s);
462}
463
464void Worker::SocketMessage(const std::string& msg, const std::string& device) {
465 std::stringstream ss;
466 ss << msg << device << ": " << strerror(errno);
467 ThreadMessage(ss.str());
468}
469
476int Worker::InitSocket(const std::string port_name) {
477 int sock = socket(PF_CAN, SOCK_RAW, CAN_RAW);
478 if (sock < 0) {
479 SocketMessage("SocketCAN socket create failed: ", port_name);
480 return -1;
481 }
482
483 // Get the interface index
484 struct ifreq if_request;
485 strcpy(if_request.ifr_name, port_name.c_str());
486 if (ioctl(sock, SIOCGIFINDEX, &if_request) < 0) {
487 SocketMessage("SocketCAN ioctl (SIOCGIFINDEX) failed: ", port_name);
488 return -1;
489 }
490
491 // Check if interface is UP
492 struct sockaddr_can can_address;
493 can_address.can_family = AF_CAN;
494 can_address.can_ifindex = if_request.ifr_ifindex;
495 if (ioctl(sock, SIOCGIFFLAGS, &if_request) < 0) {
496 SocketMessage("SocketCAN socket IOCTL (SIOCGIFFLAGS) failed: ", port_name);
497 return -1;
498 }
499 if (if_request.ifr_flags & IFF_UP) {
500 ThreadMessage("socketCan interface is UP");
501 } else {
502 ThreadMessage("socketCan interface is NOT UP");
503 return -1;
504 }
505
506 // Set timeout and bind
507 struct timeval tv;
508 tv.tv_sec = kSocketTimeoutSeconds;
509 tv.tv_usec = 0;
510 int r =
511 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
512 if (r < 0) {
513 SocketMessage("SocketCAN setsockopt SO_RCVTIMEO failed on device: ",
514 port_name);
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 return -1;
521 }
522 DriverStats stats = m_parent_driver->GetDriverStats();
523 stats.available = true;
524 m_parent_driver->SetDriverStats(stats);
525
526 return sock;
527}
528
535void Worker::HandleInput(CanFrame frame) {
536 int position = -1;
537 bool ready = true;
538
539 CanHeader header(frame);
540 if (header.IsFastMessage()) {
541 position = fast_messages.FindMatchingEntry(header, frame.data[0]);
542 if (position == kNotFound) {
543 // Not an existing fast message: create new entry and insert first frame
544 position = fast_messages.AddNewEntry();
545 ready = fast_messages.InsertEntry(header, frame.data, position);
546 } else {
547 // An existing fast message entry is present, append the frame
548 ready = fast_messages.AppendEntry(header, frame.data, position);
549 }
550 }
551 if (ready) {
552 std::vector<unsigned char> vec;
553 if (position >= 0) {
554 // Re-assembled fast message
555 vec = PushFastMsgFragment(header, position);
556 } else {
557 // Single frame message
558 vec = PushCompleteMsg(header, position, frame);
559 }
560 // auto name = N2kName(static_cast<uint64_t>(header.pgn));
561 auto src_addr = m_parent_driver->GetAddress(m_parent_driver->node_name);
562 auto msg = std::make_shared<const Nmea2000Msg>(header.pgn, vec, src_addr);
563 auto msg_all = std::make_shared<const Nmea2000Msg>(1, vec, src_addr);
564
565 ProcessRxMessages(msg);
566 m_parent_driver->m_listener.Notify(std::move(msg));
567 m_parent_driver->m_listener.Notify(std::move(msg_all));
568
569 DriverStats stats = m_parent_driver->GetDriverStats();
570 stats.rx_count += vec.size();
571 m_parent_driver->SetDriverStats(stats);
572 }
573}
574
576void Worker::ProcessRxMessages(std::shared_ptr<const Nmea2000Msg> n2k_msg) {
577 if (n2k_msg->PGN.pgn == 59904) {
578 unsigned long RequestedPGN = 0;
579 RequestedPGN = n2k_msg->payload.at(15) << 16;
580 RequestedPGN += n2k_msg->payload.at(14) << 8;
581 RequestedPGN += n2k_msg->payload.at(13);
582
583 switch (RequestedPGN) {
584 case 60928:
585 m_parent_driver->SendAddressClaim(m_parent_driver->m_source_address);
586 break;
587 case 126996:
588 m_parent_driver->SendProductInfo();
589 break;
590 default:
591 break;
592 }
593 }
594
595 else if (n2k_msg->PGN.pgn == 60928) {
596 // Watch for conflicting source address
597 if (n2k_msg->payload.at(7) == m_parent_driver->m_source_address) {
598 // My name
599 uint64_t my_name = m_parent_driver->node_name.GetName();
600
601 // His name
602 uint64_t his_name = 0;
603 unsigned char* p = (unsigned char*)&his_name;
604 for (unsigned int i = 0; i < 8; i++) *p++ = n2k_msg->payload.at(13 + i);
605
606 // Compare literally the NAME values
607 if (his_name < my_name) {
608 // I lose, so select a new address
609 m_parent_driver->m_source_address++;
610 if (m_parent_driver->m_source_address > 253)
611 // Could not claim an address
612 m_parent_driver->m_source_address = 254;
613 m_parent_driver->UpdateAttrCanAddress();
614 }
615
616 // Claim the existing or modified address
617 m_parent_driver->SendAddressClaim(m_parent_driver->m_source_address);
618 }
619 }
620}
621
623void Worker::Entry() {
624 int recvbytes;
625 int socket;
626 CanFrame frame;
627
628 socket = InitSocket(m_port_name.ToStdString());
629 if (socket < 0) {
630 std::string msg("SocketCAN socket create failed: ");
631 ThreadMessage(msg + m_port_name.ToStdString());
632 m_run_flag = -1;
633 return;
634 }
635 m_socket = socket;
636
637 // Claim our default address
638 if (m_parent_driver->SendAddressClaim(DEFAULT_N2K_SOURCE_ADDRESS)) {
639 m_parent_driver->m_source_address = DEFAULT_N2K_SOURCE_ADDRESS;
640 m_parent_driver->UpdateAttrCanAddress();
641 }
642
643 // The main loop
644 while (m_run_flag > 0) {
645 recvbytes = read(socket, &frame, sizeof(frame));
646 if (recvbytes == -1) {
647 if (errno == EAGAIN || errno == EWOULDBLOCK) continue; // timeout
648
649 wxLogWarning("can socket %s: fatal error %s", m_port_name.c_str(),
650 strerror(errno));
651 break;
652 }
653 if (recvbytes != 16) {
654 wxLogWarning("can socket %s: bad frame size: %d (ignored)",
655 m_port_name.c_str(), recvbytes);
656 sleep(1);
657 continue;
658 }
659 HandleInput(frame);
660 }
661 m_run_flag = -1;
662 return;
663}
664
665bool Worker::StartThread() {
666 m_run_flag = 1;
667 std::thread t(&Worker::Entry, this);
668 t.detach();
669 return true;
670}
671
672void Worker::StopThread() {
673 if (m_run_flag < 0) {
674 wxLogMessage("Attempt to stop already dead thread (ignored).");
675 return;
676 }
677 wxLogMessage("Stopping Worker Thread");
678
679 m_run_flag = 0;
680 int tsec = 10;
681 while ((m_run_flag >= 0) && (tsec--)) wxSleep(1);
682
683 if (m_run_flag < 0)
684 wxLogMessage("StopThread: Stopped in %d sec.", 10 - tsec);
685 else
686 wxLogWarning("StopThread: Not Stopped after 10 sec.");
687}
const std::string iface
Physical device for 0183, else a unique string.
Definition comm_driver.h:88
CAN v2.0 29 bit header as used by NMEA 2000.
bool IsFastMessage() const
Return true if header reflects a multipart fast message.
Local driver implementation, not visible outside this file.
EventVar evt_driver_msg
Notified for messages from drivers.
Interface implemented by transport layer and possible other parties like test code which should handl...
Definition comm_driver.h:48
virtual void Notify(std::shared_ptr< const NavMsg > message)=0
Handle a received message.
const void Notify()
Notify all listeners, no data supplied.
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.
Keeps listening over it's lifespan, removes itself on destruction.
Definition observable.h:131
Custom event class for OpenCPN's notification system.
Manages reading the N2K data stream provided by some N2K gateways from the declared serial port.
DriverPtr & FindDriver(const std::vector< DriverPtr > &drivers, const std::string &iface, const NavAddr::Bus _bus)
Search list of drivers for a driver with given interface string.
Driver registration container, a singleton.
Raw messages layer, supports sending and recieving navmsg messages.
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