OpenCPN Partial API docs
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"
53 #include "model/comm_drv_registry.h"
54 #include "model/comm_navmsg_bus.h"
55 #include "model/config_vars.h"
56 
57 #define DEFAULT_N2K_SOURCE_ADDRESS 72
58 
59 wxDEFINE_EVENT(EVT_N2K_59904, ObservedEvt);
60 
61 static const int kNotFound = -1;
62 
64 static const int kSocketTimeoutSeconds = 2;
65 
66 typedef struct can_frame CanFrame;
67 
68 class CommDriverN2KSocketCanImpl; // fwd
69 
94 class Worker {
95 public:
96  Worker(CommDriverN2KSocketCAN* parent, const wxString& PortName);
97 
98  bool StartThread();
99  void StopThread();
100  int GetSocket(){return m_socket;}
101 
102 private:
103  void Entry();
104 
105  void ThreadMessage(const std::string& msg, wxLogLevel l = wxLOG_Message);
106 
107  int InitSocket(const std::string port_name);
108  void SocketMessage(const std::string& msg, const std::string& device);
109  void HandleInput(CanFrame frame);
110  void ProcessRxMessages(std::shared_ptr<const Nmea2000Msg> n2k_msg);
111 
112  std::vector<unsigned char> PushCompleteMsg(const CanHeader header,
113  int position,
114  const CanFrame frame);
115  std::vector<unsigned char> PushFastMsgFragment(const CanHeader& header,
116  int position);
117 
118  CommDriverN2KSocketCanImpl* const m_parent_driver;
119  const wxString m_port_name;
120  std::atomic<int> m_run_flag;
121  FastMessageMap fast_messages;
122  int m_socket;
123 };
124 
127  friend class Worker;
128 
129 public:
131  : CommDriverN2KSocketCAN(p, l), m_worker(this, p->socketCAN_port),
132  m_source_address(-1), m_last_TX_sequence(0){
133  SetN2K_Name();
134  Open();
135  }
136 
137  ~CommDriverN2KSocketCanImpl() { Close(); }
138 
139  bool Open();
140  void Close();
141  void SetN2K_Name();
142 
143  bool SendMessage(std::shared_ptr<const NavMsg> msg,
144  std::shared_ptr<const NavAddr> addr);
145 
146  int DoAddressClaim();
147  bool SendAddressClaim(int proposed_source_address);
148  bool SendProductInfo();
149 
150  Worker& GetWorker(){ return m_worker; }
151  void UpdateAttrCanAddress();
152 
153 private:
154  N2kName node_name;
155  Worker m_worker;
156  int m_source_address;
157  int m_last_TX_sequence;
158  std::future<int> m_AddressClaimFuture;
159  wxMutex m_TX_mutex;
160  int m_unique_number;
161 
162  ObservableListener listener_N2K_59904;
163  bool HandleN2K_59904( std::shared_ptr<const Nmea2000Msg> n2k_msg );
164 
165 };
166 
167 
168 // Static CommDriverN2KSocketCAN factory implementation.
169 
170 std::shared_ptr<CommDriverN2KSocketCAN> CommDriverN2KSocketCAN::Create(
171  const ConnectionParams* params, DriverListener& listener) {
172  return std::shared_ptr<CommDriverN2KSocketCAN>(
173  new CommDriverN2KSocketCanImpl(params, listener));
174 }
175 
176 
177 
178 // CommDriverN2KSocketCanImpl implementation
179 
180 void CommDriverN2KSocketCanImpl::SetN2K_Name() {
181  // We choose some "benign" values for OCPN socketCan interface
182  node_name.value.Name = 0;
183 
184  m_unique_number = 1;
185  // Build a simple 16 bit hash of g_hostname, to use as unique "serial number"
186  int hash = 0;
187  std::string str(g_hostname.mb_str());
188  int len = str.size();
189  const char* ch = str.data();
190  for (int i = 0; i < len; i++)
191  hash = hash + ((hash) << 5) + *(ch + i) + ((*(ch + i)) << 7);
192  m_unique_number = ((hash) ^ (hash >> 16)) & 0xffff;
193 
194  node_name.SetManufacturerCode(2046);
195  node_name.SetUniqueNumber(m_unique_number);
196  node_name.SetDeviceFunction(130); // Display
197  node_name.SetDeviceClass(120); // Display
198  node_name.SetIndustryGroup(4); // Marine
199  node_name.SetSystemInstance(0);
200 }
201 
202 void CommDriverN2KSocketCanImpl::UpdateAttrCanAddress() {
203  this->attributes["canAddress"] = std::to_string(m_source_address);
204 }
205 
206 
207 bool CommDriverN2KSocketCanImpl::Open() {
208 
209  // Start the RX worker thread
210  bool bws = m_worker.StartThread();
211  return bws;
212 
213 }
214 
215 void CommDriverN2KSocketCanImpl::Close() {
216  wxLogMessage("Closing N2K socketCAN: %s", m_params.socketCAN_port.c_str());
217  m_worker.StopThread();
218 
219  // We cannot use shared_from_this() since we might be in the destructor.
220  auto& registry = CommDriverRegistry::GetInstance();
221  auto me = FindDriver(registry.GetDrivers(), iface, bus);
222  registry.Deactivate(me);
223 }
224 
225 
226 bool CommDriverN2KSocketCanImpl::SendAddressClaim(int proposed_source_address) {
227 
228  wxMutexLocker lock(m_TX_mutex);
229 
230  int socket = GetWorker().GetSocket();
231 
232  if (socket < 0)
233  return false;
234 
235  CanFrame frame;
236  memset(&frame, 0, sizeof(frame));
237 
238  uint64_t _pgn = 60928;
239  unsigned long canId = BuildCanID(6, proposed_source_address, 255, _pgn);
240  frame.can_id = canId | CAN_EFF_FLAG;
241 
242  // Load the data
243  uint32_t b32_0 = node_name.value.UnicNumberAndManCode;
244  memcpy(&frame.data, &b32_0, 4);
245 
246  unsigned char b81 = node_name.value.DeviceInstance;
247  memcpy(&frame.data[4], &b81, 1);
248 
249  b81 = node_name.value.DeviceFunction;
250  memcpy(&frame.data[5], &b81, 1);
251 
252  b81 = (node_name.value.DeviceClass);
253  memcpy(&frame.data[6], &b81, 1);
254 
255  b81 = node_name.value.IndustryGroupAndSystemInstance;
256  memcpy(&frame.data[7], &b81, 1);
257 
258  frame.can_dlc = 8; // data length
259 
260  int sentbytes = write(socket, &frame, sizeof(frame));
261 
262  return (sentbytes == 16);
263 }
264 
265 void AddStr( std::vector<uint8_t> &vec, std::string str, size_t max_len) {
266  size_t i;
267  for (i = 0; i<str.size(); i++) {
268  vec.push_back(str[i]);;
269  }
270  for (; i<max_len; i++) {
271  vec.push_back(0);
272  }
273 }
274 
275 bool CommDriverN2KSocketCanImpl::SendProductInfo() {
276 
277  // Create the payload
278  std::vector<uint8_t> payload;
279 
280  payload.push_back(2100 & 0xFF); //N2KVersion
281  payload.push_back(2100 >> 8);
282  payload.push_back(0xEC); //Product Code, 1772
283  payload.push_back(0x06);
284 
285  std::string ModelID("OpenCPN"); // Model ID
286  AddStr(payload, ModelID, 32);
287 
288  std::string ModelSWCode(PACKAGE_VERSION); // SwCode
289  AddStr(payload, ModelSWCode, 32);
290 
291  std::string ModelVersion(PACKAGE_VERSION); // Model Version
292  AddStr(payload, ModelVersion, 32);
293 
294  std::string ModelSerialCode(std::to_string(m_unique_number)); // Model Serial Code
295  AddStr(payload, ModelSerialCode, 32);
296 
297  payload.push_back(0); // CertificationLevel
298  payload.push_back(0); // LoadEquivalency
299 
300  auto dest_addr = std::make_shared<const NavAddr2000>(iface, 255);
301  uint64_t _PGN;
302  _PGN = 126996;
303 
304  auto msg = std::make_shared<const Nmea2000Msg>(_PGN, payload, dest_addr);
305  SendMessage(msg, dest_addr);
306 
307  return true;
308 }
309 
310 bool CommDriverN2KSocketCanImpl::SendMessage(std::shared_ptr<const NavMsg> msg,
311  std::shared_ptr<const NavAddr> addr) {
312 
313  wxMutexLocker lock(m_TX_mutex);
314 
315  // Verify claimed address is useable
316  if ( m_source_address < 0)
317  return false;
318 
319  if ( m_source_address > 253) // Could not claim...
320  return false;
321 
322  int socket = GetWorker().GetSocket();
323 
324  if (socket < 0)
325  return false;
326 
327  CanFrame frame;
328  memset(&frame, 0, sizeof(frame));
329 
330  auto msg_n2k = std::dynamic_pointer_cast<const Nmea2000Msg>(msg);
331  std::vector<uint8_t> load = msg_n2k->payload;
332 
333  uint64_t _pgn = msg_n2k->PGN.pgn;
334  auto destination_address = std::static_pointer_cast<const NavAddr2000>(addr);
335 
336 
337  unsigned long canId = BuildCanID(msg_n2k->priority, m_source_address,
338  destination_address->address, _pgn);
339 
340  frame.can_id = canId | CAN_EFF_FLAG;
341 
342  if (load.size() <= 8){
343  frame.can_dlc = load.size();
344  if (load.size() > 0)
345  memcpy(&frame.data, load.data(), load.size());
346 
347  int sentbytes = write(socket, &frame, sizeof(frame));
348  }
349  else { // Fast Packet
350  int sequence = (m_last_TX_sequence + 0x20) & 0xE0;
351  m_last_TX_sequence = sequence;
352  unsigned char *data_ptr = load.data();
353  int n_remaining = load.size();
354 
355  // First packet
356  frame.can_dlc = 8;
357  frame.data[0] = sequence;
358  frame.data[1] = load.size();
359  int data_len_0 = wxMin(load.size(), 6);
360  memcpy(&frame.data[2], load.data(), data_len_0);
361 
362  int sentbytes0 = write(socket, &frame, sizeof(frame));
363 
364  data_ptr += data_len_0;
365  n_remaining -= data_len_0;
366  sequence++;
367 
368  // The rest of the bytes
369  while (n_remaining > 0){
370  wxMilliSleep(10);
371  frame.data[0] = sequence;
372  int data_len_n = wxMin(n_remaining, 7);
373  memcpy(&frame.data[1], data_ptr, data_len_n);
374 
375  int sentbytesn = write(socket, &frame, sizeof(frame));
376 
377  data_ptr += data_len_n;
378  n_remaining -= data_len_n;
379  sequence++;
380  }
381  }
382 
383 
384  return true;
385 }
386 
387 
388 
389 // CommDriverN2KSocketCAN implementation
390 
391 CommDriverN2KSocketCAN::CommDriverN2KSocketCAN(const ConnectionParams* params,
392  DriverListener& listener)
393  : CommDriverN2K(((ConnectionParams*)params)->GetStrippedDSPort()),
394  m_params(*params),
395  m_listener(listener),
396  m_ok(false),
397  m_portstring(params->GetDSPort()),
398  m_baudrate(wxString::Format("%i", params->Baudrate))
399  {
400  this->attributes["canPort"] = params->socketCAN_port.ToStdString();
401  this->attributes["canAddress"] = std::to_string(DEFAULT_N2K_SOURCE_ADDRESS);
402  this->attributes["userComment"] = params->UserComment.ToStdString();
403  this->attributes["ioDirection"] = std::string("IN/OUT");
404 
405  }
406 
407 CommDriverN2KSocketCAN::~CommDriverN2KSocketCAN() {}
408 
410  CommDriverRegistry::GetInstance().Activate(shared_from_this());
411 }
412 
413 
414 // Worker implementation
415 
416 Worker::Worker(CommDriverN2KSocketCAN* parent, const wxString& port_name)
417  : m_parent_driver(dynamic_cast<CommDriverN2KSocketCanImpl*>(parent)),
418  m_port_name(port_name.Clone()),
419  m_run_flag(-1),
420  m_socket(-1) {
421  assert(m_parent_driver != 0);
422 }
423 
424 std::vector<unsigned char> Worker::PushCompleteMsg(const CanHeader header,
425  int position,
426  const CanFrame frame) {
427  std::vector<unsigned char> data;
428  data.push_back(0x93);
429  data.push_back(0x13);
430  data.push_back(header.priority);
431  data.push_back(header.pgn & 0xFF);
432  data.push_back((header.pgn >> 8) & 0xFF);
433  data.push_back((header.pgn >> 16) & 0xFF);
434  data.push_back(header.destination);
435  data.push_back(header.source);
436  data.push_back(0xFF); // FIXME (dave) generate the time fields
437  data.push_back(0xFF);
438  data.push_back(0xFF);
439  data.push_back(0xFF);
440  data.push_back(CAN_MAX_DLEN); // nominally 8
441  for (size_t n = 0; n < CAN_MAX_DLEN; n++) data.push_back(frame.data[n]);
442  data.push_back(0x55); // CRC dummy, not checked
443  return data;
444 }
445 
446 std::vector<unsigned char> Worker::PushFastMsgFragment(const CanHeader& header,
447  int position) {
448  std::vector<unsigned char> data;
449  data.push_back(0x93);
450  data.push_back(fast_messages[position].expected_length + 11);
451  data.push_back(header.priority);
452  data.push_back(header.pgn & 0xFF);
453  data.push_back((header.pgn >> 8) & 0xFF);
454  data.push_back((header.pgn >> 16) & 0xFF);
455  data.push_back(header.destination);
456  data.push_back(header.source);
457  data.push_back(0xFF); // FIXME (dave) Could generate the time fields
458  data.push_back(0xFF);
459  data.push_back(0xFF);
460  data.push_back(0xFF);
461  data.push_back(fast_messages[position].expected_length);
462  for (size_t n = 0; n < fast_messages[position].expected_length; n++)
463  data.push_back(fast_messages[position].data[n]);
464  data.push_back(0x55); // CRC dummy
465  fast_messages.Remove(position);
466  return data;
467 }
468 
469 void Worker::ThreadMessage(const std::string& msg, wxLogLevel level) {
470  wxLogGeneric(level, wxString(msg.c_str()));
471  auto s = std::string("CommDriverN2KSocketCAN: ") + msg;
472  CommDriverRegistry::GetInstance().evt_driver_msg.Notify(level, s);
473 }
474 
475 void Worker::SocketMessage(const std::string& msg, const std::string& device) {
476  std::stringstream ss;
477  ss << msg << device << ": " << strerror(errno);
478  ThreadMessage(ss.str());
479 }
480 
487 int Worker::InitSocket(const std::string port_name) {
488  int sock = socket(PF_CAN, SOCK_RAW, CAN_RAW);
489  if (sock < 0) {
490  SocketMessage("SocketCAN socket create failed: ", port_name);
491  return -1;
492  }
493 
494  // Get the interface index
495  struct ifreq if_request;
496  strcpy(if_request.ifr_name, port_name.c_str());
497  if (ioctl(sock, SIOCGIFINDEX, &if_request) < 0) {
498  SocketMessage("SocketCAN ioctl (SIOCGIFINDEX) failed: ", port_name);
499  return -1;
500  }
501 
502  // Check if interface is UP
503  struct sockaddr_can can_address;
504  can_address.can_family = AF_CAN;
505  can_address.can_ifindex = if_request.ifr_ifindex;
506  if (ioctl(sock, SIOCGIFFLAGS, &if_request) < 0) {
507  SocketMessage("SocketCAN socket IOCTL (SIOCGIFFLAGS) failed: ", port_name);
508  return -1;
509  }
510  if (if_request.ifr_flags & IFF_UP) {
511  ThreadMessage("socketCan interface is UP");
512  } else {
513  ThreadMessage("socketCan interface is NOT UP");
514  return -1;
515  }
516 
517  // Set timeout and bind
518  struct timeval tv;
519  tv.tv_sec = kSocketTimeoutSeconds;
520  tv.tv_usec = 0;
521  int r =
522  setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
523  if (r < 0) {
524  SocketMessage("SocketCAN setsockopt SO_RCVTIMEO failed on device: ",
525  port_name);
526  return -1;
527  }
528  r = bind(sock, (struct sockaddr*)&can_address, sizeof(can_address));
529  if (r < 0) {
530  SocketMessage("SocketCAN socket bind() failed: ", port_name);
531  return -1;
532  }
533  return sock;
534 }
535 
542 void Worker::HandleInput(CanFrame frame) {
543  int position = -1;
544  bool ready = true;
545 
546  CanHeader header(frame);
547  if (header.IsFastMessage()) {
548  position = fast_messages.FindMatchingEntry(header, frame.data[0]);
549  if (position == kNotFound) {
550  // Not an existing fast message: create new entry and insert first frame
551  position = fast_messages.AddNewEntry();
552  ready = fast_messages.InsertEntry(header, frame.data, position);
553  } else {
554  // An existing fast message entry is present, append the frame
555  ready = fast_messages.AppendEntry(header, frame.data, position);
556  }
557  }
558  if (ready) {
559  std::vector<unsigned char> vec;
560  if (position >= 0) {
561  // Re-assembled fast message
562  vec = PushFastMsgFragment(header, position);
563  } else {
564  // Single frame message
565  vec = PushCompleteMsg(header, position, frame);
566  }
567  //auto name = N2kName(static_cast<uint64_t>(header.pgn));
568  auto src_addr = m_parent_driver->GetAddress(m_parent_driver->node_name);
569  auto msg = std::make_shared<const Nmea2000Msg>(header.pgn, vec, src_addr);
570  auto msg_all = std::make_shared<const Nmea2000Msg>(1, vec, src_addr);
571 
572  ProcessRxMessages(msg);
573  m_parent_driver->m_listener.Notify(std::move(msg));
574  m_parent_driver->m_listener.Notify(std::move(msg_all));
575 
576  }
577 }
578 
580 void Worker::ProcessRxMessages(std::shared_ptr<const Nmea2000Msg> n2k_msg){
581 
582  if(n2k_msg->PGN.pgn == 59904 ){
583  unsigned long RequestedPGN = 0;
584  RequestedPGN = n2k_msg->payload.at(15) << 16;
585  RequestedPGN += n2k_msg->payload.at(14) << 8;
586  RequestedPGN += n2k_msg->payload.at(13);
587 
588  switch (RequestedPGN){
589  case 60928:
590  m_parent_driver->SendAddressClaim(m_parent_driver->m_source_address);
591  break;
592  case 126996:
593  m_parent_driver->SendProductInfo();
594  break;
595  default:
596  break;
597  }
598  }
599 
600  else if(n2k_msg->PGN.pgn == 60928 ){
601  // Watch for conflicting source address
602  if (n2k_msg->payload.at(7) == m_parent_driver->m_source_address){
603  // My name
604  uint64_t my_name = m_parent_driver->node_name.GetName();
605 
606  // His name
607  uint64_t his_name = 0;
608  unsigned char *p = (unsigned char *)&his_name;
609  for (unsigned int i=0 ; i < 8 ; i++)
610  *p++ = n2k_msg->payload.at(13 + i);
611 
612  // Compare literally the NAME values
613  if (his_name < my_name){
614  // I lose, so select a new address
615  m_parent_driver->m_source_address++;
616  if ( m_parent_driver->m_source_address > 253)
617  // Could not claim an address
618  m_parent_driver->m_source_address = 254;
619  m_parent_driver->UpdateAttrCanAddress();
620  }
621 
622  // Claim the existing or modified address
623  m_parent_driver->SendAddressClaim(m_parent_driver->m_source_address);
624  }
625  }
626 }
627 
628 
630 void Worker::Entry() {
631  int recvbytes;
632  int socket;
633  CanFrame frame;
634 
635  socket = InitSocket(m_port_name.ToStdString());
636  if (socket < 0) {
637  std::string msg("SocketCAN socket create failed: ");
638  ThreadMessage(msg + m_port_name.ToStdString());
639  m_run_flag = -1;
640  return;
641  }
642  m_socket = socket;
643 
644  // Claim our default address
645  if (m_parent_driver->SendAddressClaim(DEFAULT_N2K_SOURCE_ADDRESS)){
646  m_parent_driver->m_source_address = DEFAULT_N2K_SOURCE_ADDRESS;
647  m_parent_driver->UpdateAttrCanAddress();
648  }
649 
650 
651  // The main loop
652  while (m_run_flag > 0) {
653  recvbytes = read(socket, &frame, sizeof(frame));
654  if (recvbytes == -1) {
655  if (errno == EAGAIN || errno == EWOULDBLOCK) continue; // timeout
656 
657  wxLogWarning("can socket %s: fatal error %s", m_port_name.c_str(),
658  strerror(errno));
659  break;
660  }
661  if (recvbytes != 16) {
662  wxLogWarning("can socket %s: bad frame size: %d (ignored)",
663  m_port_name.c_str(), recvbytes);
664  sleep(1);
665  continue;
666  }
667  HandleInput(frame);
668  }
669  m_run_flag = -1;
670  return;
671 }
672 
673 bool Worker::StartThread() {
674  m_run_flag = 1;
675  std::thread t(&Worker::Entry, this);
676  t.detach();
677  return true;
678 }
679 
680 void 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 }
696 
const std::string iface
Physical device for 0183, else a unique string.
Definition: comm_driver.h:89
CAN v2.0 29 bit header as used by NMEA 2000.
Definition: comm_can_util.h:64
bool IsFastMessage() const
Return true if header reflects a multipart fast message.
void Activate() override
Register driver and possibly do other post-ctor steps.
Local driver implementation, not visible outside this file.
EventVar evt_driver_msg
Notified for messages from drivers.
void Activate(DriverPtr driver)
Add driver to list of active drivers.
Interface implemented by transport layer and possible other parties like test code which should handl...
Definition: comm_driver.h:47
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.
Definition: comm_can_util.h:80
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:133
Adds a std::shared<void> element to wxCommandEvent.
Definition: ocpn_plugin.h:1652
Manages reading the N2K data stream provided by some N2K gateways from the declared serial port.
wxDEFINE_EVENT(REST_IO_EVT, ObservedEvt)
Event from IO thread to main.
N2k uses CAN which defines the basic properties of messages.
Definition: comm_navmsg.h:59