OpenCPN Partial API docs
Loading...
Searching...
No Matches
comm_drv_n2k_canable.cpp
Go to the documentation of this file.
1/***************************************************************************
2 * Copyright (C) 2026 by twocanplugin@hotmail.com *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, see <https://www.gnu.org/licenses/>. *
16 **************************************************************************/
17
24
25#include "config.h"
26
27#include <algorithm>
28#include <cstdio>
29#include <cstring>
30#include <sstream>
31
32// Used by the fast message map
33static const int kNotFound = -1;
34
35// Pads fixed length string. Used for PGN 126996 Product Information fields
36// Mirrors comm_drv_n2k_socketcan.cpp's file-local AddStr()
37static void AddStr(std::vector<uint8_t>* data, const std::string& text,
38 size_t length) {
39 size_t i = 0;
40 for (; i < text.size() && i < length; i++) data->push_back(text[i]);
41 for (; i < length; i++) data->push_back(0);
42}
43
44// Canable Worker Thread
45CanableSerialThread::CanableSerialThread(CommDriverN2KCanable* parent,
46 const wxString& port_name)
47 : wxThread(wxTHREAD_JOINABLE),
48 m_parent(parent),
49 m_portName(port_name),
50 m_stopRequested(false) {
51 // Events are created in the ctor as they must be safe to call at any point
52 // in this object's life, independent of whether the port has been opened.
53
54 // Stop event is set upon closing and consumed by the overlapped read. It is a
55 // manual-reset event
56 m_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
57
58 // Write event is set upon Write and also consumed by overlapped read. It is
59 // an auto-reset event
60 m_pendingWriteEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
61}
62
63// Close our handles correctly
64CanableSerialThread::~CanableSerialThread() {
65 if (m_stopEvent) {
66 CloseHandle(m_stopEvent);
67 }
68 if (m_pendingWriteEvent) {
69 CloseHandle(m_pendingWriteEvent);
70 }
71}
72
73// Handle a stop request when OpenCPN is shutting down or our driver is
74// uninstalled
75void CanableSerialThread::RequestStop() {
76 m_stopRequested = true;
77 if (m_stopEvent) {
78 SetEvent(m_stopEvent);
79 }
80}
81
82// As it says on the tin...
83bool CanableSerialThread::OpenPort() {
84 wxString serialPortName = m_portName;
85 // Prefix the port name correctly (eg. COM10)
86 if (!serialPortName.StartsWith("\\\\.\\")) {
87 serialPortName = "\\\\.\\" + serialPortName;
88 }
89
90 // Open the serial port using overlapped I/O
91 m_serialPortHandle =
92 CreateFileW(serialPortName.wc_str(), GENERIC_READ | GENERIC_WRITE, 0,
93 nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
94
95 if (m_serialPortHandle == INVALID_HANDLE_VALUE) {
96 return false;
97 }
98
99 // Configure the comm settings & timeouts
100 DCB dcb = {0};
101 dcb.DCBlength = sizeof(dcb);
102
103 if (!GetCommState(m_serialPortHandle, &dcb)) {
104 CloseHandle(m_serialPortHandle);
105 m_serialPortHandle = INVALID_HANDLE_VALUE;
106 return false;
107 }
108 // For most SLCAN adapters (including CANable) the actual UART parameters are
109 // irrelevant to the USB link, but nonetheless use the same defaults
110 // as used by the TwoCan cantact driver as they seem to work OK
111 dcb.BaudRate = CBR_115200;
112 dcb.ByteSize = 8;
113 dcb.Parity = NOPARITY;
114 dcb.StopBits = ONESTOPBIT;
115 dcb.fBinary = TRUE;
116 dcb.fDtrControl = DTR_CONTROL_ENABLE;
117 dcb.fRtsControl = RTS_CONTROL_ENABLE; // Some USB controllers gate on RTS
118 SetCommState(m_serialPortHandle, &dcb);
119
120 // Even though using overlapped I/O these are still useful
121 COMMTIMEOUTS timeouts = {0};
122 timeouts.ReadIntervalTimeout = 10;
123 timeouts.ReadTotalTimeoutConstant = 100;
124 timeouts.ReadTotalTimeoutMultiplier = 0;
125 timeouts.WriteTotalTimeoutConstant = 50;
126 timeouts.WriteTotalTimeoutMultiplier = 0;
127 SetCommTimeouts(m_serialPortHandle, &timeouts);
128
129 // Configure the Overlapped I/O
130 ZeroMemory(&m_overlappedRead, sizeof(m_overlappedRead));
131 m_overlappedRead.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
132
133 ZeroMemory(&m_overlappedWrite, sizeof(m_overlappedWrite));
134 m_overlappedWrite.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
135
136 // The flag used to prevent issuing multiple read requests
137 m_isReadPending = false;
138
139 return true;
140}
141
142// Close down nicely...
143void CanableSerialThread::ClosePort() {
144 if (m_overlappedRead.hEvent) {
145 CloseHandle(m_overlappedRead.hEvent);
146 m_overlappedRead.hEvent = nullptr;
147 }
148 if (m_overlappedWrite.hEvent) {
149 CloseHandle(m_overlappedWrite.hEvent);
150 m_overlappedWrite.hEvent = nullptr;
151 }
152 if (m_serialPortHandle != INVALID_HANDLE_VALUE) {
153 CloseHandle(m_serialPortHandle);
154 m_serialPortHandle = INVALID_HANDLE_VALUE;
155 }
156}
157
158// Write the NMEA 2000 message
159bool CanableSerialThread::WriteFrame(const std::string& payload) {
160 ResetEvent(m_overlappedWrite.hEvent);
161 DWORD bytesWritten = 0;
162 BOOL result = WriteFile(m_serialPortHandle, payload.data(),
163 static_cast<DWORD>(payload.size()), &bytesWritten,
164 &m_overlappedWrite);
165
166 if (result) {
167 // The write completed synchronously.
168 return bytesWritten == payload.size();
169 }
170
171 if (GetLastError() != ERROR_IO_PENDING) {
172 return false;
173 }
174
175 // A write this small should complete almost immediately;
176 // If it doesn't, cancel it rather than risk blocking indefinitely.
177 DWORD wait_result = WaitForSingleObject(m_overlappedWrite.hEvent, 200);
178 if (wait_result != WAIT_OBJECT_0) {
179 CancelIoEx(m_serialPortHandle, &m_overlappedWrite);
180 return false;
181 }
182 // Overlapped write has completed
183 DWORD overlappedBytesWritten = 0;
184 if (!GetOverlappedResult(m_serialPortHandle, &m_overlappedWrite,
185 &overlappedBytesWritten, FALSE)) {
186 return false;
187 }
188 // Completed successfully
189 return overlappedBytesWritten == payload.size();
190}
191
192// Parses one complete SLCAN line into a can_frame.
193// Eg. "T1D11220381122334455667788\r"
194// Note the 'T' denotes an extended frame, ie. 29bit Can Id
195bool CanableSerialThread::DecodeSLCANFrame(const std::string& line,
196 can_frame* frame) {
197 if (line.size() < 10) { // Minimum potential size "T1D1122030" - no data!
198 return false;
199 }
200
201 // Parse the Can Id
202 frame->can_id =
203 static_cast<uint32_t>(strtoul(line.substr(1, 8).c_str(), nullptr, 16)) &
204 0x1FFFFFFFu;
205 // The data length
206 int dlc = line[9] - '0';
207 // Guard against an invalid length. Should either be 3 (PGN 59904) or 8 (any
208 // other PGN)
209 if (dlc < 0 || dlc > 8) {
210 return false;
211 }
212
213 frame->can_dlc = static_cast<uint8_t>(dlc);
214
215 std::memset(frame->data, 0, sizeof(frame->data));
216 // Index into the payload
217 size_t index = 10;
218 for (int i = 0; i < dlc; i++) {
219 if (index + 2 > line.size()) {
220 return false;
221 }
222 // Parse each pair of hex characters
223 frame->data[i] = static_cast<uint8_t>(
224 strtoul(line.substr(index, 2).c_str(), nullptr, 16));
225 index += 2;
226 }
227 return true;
228}
229
230void CanableSerialThread::ParseReceivedData(
231 const char* buf, size_t len, std::vector<can_frame>* out_frames) {
232 // Mirrors cantact.c's ReadThread() assembly loop: scan a raw chunk of
233 // serial bytes for 'T'-delimited, CR-terminated lines, tolerating
234 // partial lines that span two reads and resynchronizing if a CR arrives
235 // without a preceding 'T'.
236 for (size_t i = 0; i < len; i++) {
237 char c = buf[i];
238 // We have the start delimiter
239 if (c == 'T') {
240 m_hasStartDelimiter = true;
241 m_partialLine.clear();
242 m_partialLine += c;
243 continue;
244 }
245 // We have the terminating CR
246 if (c == '\r' || c == '\n') {
247 if (c == '\r' && m_hasStartDelimiter && !m_partialLine.empty()) {
248 can_frame frame;
249 // Process the received frame
250 if (DecodeSLCANFrame(m_partialLine, &frame)) {
251 out_frames->push_back(frame);
252 }
253 }
254 // Reset for next frame
255 m_hasStartDelimiter = false;
256 m_partialLine.clear();
257 continue;
258 }
259 // We've had the start delimiter, now append a normal character
260 if (m_hasStartDelimiter) {
261 m_partialLine += c;
262 // Guard against a malformed stream
263 if (m_partialLine.size() > 64) {
264 m_hasStartDelimiter = false;
265 m_partialLine.clear();
266 }
267 }
268 }
269}
270
271// Enqueue a complete SLCAN frame for transmission
272void CanableSerialThread::QueueWrite(const std::string& slcan_string) {
273 {
274 std::lock_guard<std::mutex> lock(m_transmitMutex);
275 m_transmitQueue.push_back(slcan_string);
276 }
277 // Wake up the thread in case it is blocked waiting on a read
278 if (m_pendingWriteEvent) {
279 SetEvent(m_pendingWriteEvent);
280 }
281}
282
283// Dequeue any write requests
284void CanableSerialThread::DequeueWrite() {
285 std::vector<std::string> slcan_strings;
286 {
287 std::lock_guard<std::mutex> lock(m_transmitMutex);
288 if (m_transmitQueue.empty()) {
289 return;
290 }
291 slcan_strings.swap(m_transmitQueue);
292 }
293 for (const auto& slcan_string : slcan_strings) {
294 WriteFrame(slcan_string);
295 // Wait a little to ensure we don't overwrite the device's buffer
296 wxMilliSleep(5);
297 }
298}
299
300// The wxThread entry point
301void* CanableSerialThread::Entry() {
302 if (!OpenPort()) {
303 wxLogMessage("CANable: failed to open serial port %s", m_portName);
304 return nullptr;
305 }
306
307 // Once we've opened the port, announce that stats may be available
308 m_parent->m_driverStatistics.available = true;
309
310 // Initialize the Canable adapter
311 // Close the port
312 WriteFrame("C\r");
313 wxMilliSleep(20);
314 // Set the bus speed to 250k
315 WriteFrame("S5\r");
316 wxMilliSleep(20);
317 // Open the port
318 WriteFrame("O\r");
319 wxMilliSleep(50);
320
321 // Send our address claim
322 m_parent->SendAddressClaim(CommDriverN2KCanable::kDefaultSourceAddress);
323
324 // Begin reading from the serial port
325 char serialBuffer[4096];
326 std::vector<can_frame> frames;
327
328 for (;;) {
329 if (!m_isReadPending) {
330 ResetEvent(m_overlappedRead.hEvent);
331 DWORD bytesRead = 0;
332 BOOL result =
333 ReadFile(m_serialPortHandle, serialBuffer, sizeof(serialBuffer),
334 &bytesRead, &m_overlappedRead);
335 if (result) {
336 // Completed synchronously
337 if (bytesRead > 0) {
338 frames.clear();
339 ParseReceivedData(serialBuffer, bytesRead, &frames);
340 for (const auto& frame : frames) {
341 // Send the frame to both OpenCPN and for local parsing of Address
342 // Claims etc.
343 m_parent->EncodeActisenseMessage(frame);
344 }
345 }
346 // Anything to write ?
347 DequeueWrite();
348 if (m_stopRequested) {
349 break;
350 }
351 continue;
352 }
353 if (GetLastError() != ERROR_IO_PENDING) {
354 // Perhaps a real error (e.g. the device was unplugged). Back off
355 // briefly rather than spin-looping on a persistent failure, but keep
356 // trying, RequestStop() is the only intended way to exit.
357 wxMilliSleep(50);
358 if (m_stopRequested) {
359 break;
360 }
361 continue;
362 }
363 m_isReadPending = true;
364 }
365
366 // Wait on whichever happens first: the pending read completes, a
367 // stop is requested, or a write is queued.
368 HANDLE handles[3] = {m_overlappedRead.hEvent, m_stopEvent,
369 m_pendingWriteEvent};
370 DWORD wait_result = WaitForMultipleObjects(3, handles, FALSE, 1000);
371
372 if (wait_result == WAIT_OBJECT_0) {
373 // Read completed
374 // GetOverlappedResult() provides the number of bytes read.
375 DWORD overlappedBytesRead = 0;
376 if (GetOverlappedResult(m_serialPortHandle, &m_overlappedRead,
377 &overlappedBytesRead, FALSE) &&
378 overlappedBytesRead > 0) {
379 frames.clear();
380 ParseReceivedData(serialBuffer, overlappedBytesRead, &frames);
381 for (const auto& frame : frames) {
382 m_parent->EncodeActisenseMessage(frame);
383 }
384 }
385 m_isReadPending = false;
386 } else if (wait_result == WAIT_OBJECT_0 + 1) {
387 // Stop requested: cancel the in-flight read so it doesn't leak as
388 // an orphaned pending I/O when the handle is closed below, then
389 // wait for the cancellation itself to be acknowledged before proceeding
390 CancelIoEx(m_serialPortHandle, &m_overlappedRead);
391 DWORD dummy = 0;
392 GetOverlappedResult(m_serialPortHandle, &m_overlappedRead, &dummy, TRUE);
393 m_isReadPending = false;
394 break;
395 } else if (wait_result == WAIT_OBJECT_0 + 2) {
396 // Write queued: the pending read is untouched and still in flight,
397 // nothing to do here except fall through to DrainWriteQueue() below.
398 } else if (wait_result == WAIT_TIMEOUT) {
399 // No read completion, no stop, no write queued within 1s
400 // Purely a defensive fallback the read stays pending, loop back and wait.
401 } else {
402 // WAIT_FAILED or an abandoned handle: bail out defensively
403 CancelIoEx(m_serialPortHandle, &m_overlappedRead);
404 DWORD dummy = 0;
405 GetOverlappedResult(m_serialPortHandle, &m_overlappedRead, &dummy, TRUE);
406 m_isReadPending = false;
407 break;
408 }
409
410 DequeueWrite();
411 if (m_stopRequested) {
412 if (m_isReadPending) {
413 CancelIoEx(m_serialPortHandle, &m_overlappedRead);
414 DWORD dummy = 0;
415 GetOverlappedResult(m_serialPortHandle, &m_overlappedRead, &dummy,
416 TRUE);
417 m_isReadPending = false;
418 }
419 break;
420 }
421 }
422
423 // Close the CAN channel before closing the port.
424 WriteFrame("C\r");
425 ClosePort();
426 return nullptr;
427}
428
429void CanableSerialThread::OnExit() {}
430
431// CANable Cantact driver
432CommDriverN2KCanable::CommDriverN2KCanable(const ConnectionParams* params,
433 DriverListener& listener)
434 : CommDriverN2K(params->GetStrippedDSPort()),
435 m_connectionParameters(*params),
436 m_driverListener(listener),
437 m_portName(params->GetStrippedDSPort()),
438 m_statsTimer(*this, std::chrono::milliseconds(2000)) {
439 // Stats driver mirrors that of CommDriverN2KSocketCanImpl
440 m_driverStatistics.driver_bus = NavAddr::Bus::N2000;
441 m_driverStatistics.driver_iface = params->GetStrippedDSPort();
442
443 this->attributes["canAdapter"] = std::string("CANable Cantact (slcan)");
444 this->attributes["serialPort"] = m_portName.ToStdString();
445 this->attributes["bitrate"] = std::string("250000");
446 this->attributes["canAddress"] = std::to_string(kDefaultSourceAddress);
447 this->attributes["ioDirection"] = PortDirectionToString(params->direction);
448
449 // Generate our NMEA 2000 NAME
450 SetN2kName();
451
452 // Fast Message Map. Used to reassemble fast messages
453 m_fastMessageMap = std::make_unique<FastMessageMap>();
454
455 // Initialize time stamping mechanism
456 m_startTime = std::chrono::steady_clock::now();
457
458 // Open the Port
459 Open();
460}
461
462CommDriverN2KCanable::~CommDriverN2KCanable() {
463 // Close the port
464 Close();
465}
466
467void CommDriverN2KCanable::SetN2kName() {
468 // Mirrors CommDriverN2KSocketCanImpl::SetN2K_Name():
469 m_deviceInformation.value.Name = 0;
470
471 // 16-bit hash of the hostname, used as a pseudo-unique serial number so
472 // two OpenCPN instances on different machines don't collide with
473 // identical NAMEs on the same bus.
474 wxString hostname = wxGetHostName();
475 std::string str(hostname.mb_str());
476 int hash = 0;
477 for (char ch : str) hash = hash + (hash << 5) + ch + (ch << 7);
478 m_uniqueNumber = ((hash) ^ (hash >> 16)) & 0xffff;
479
480 // Used for PGN 126996 Product Information
481 // Matches OpenCPN's "manufacturer code"
482 m_deviceInformation.SetManufacturerCode(2046);
483 m_deviceInformation.SetUniqueNumber(m_uniqueNumber);
484 m_deviceInformation.SetDeviceFunction(130);
485 m_deviceInformation.SetDeviceClass(120);
486 m_deviceInformation.SetIndustryGroup(4);
487 m_deviceInformation.SetSystemInstance(0);
488}
489
490// Persist our CAN address
491void CommDriverN2KCanable::UpdateAttrCanAddress() {
492 this->attributes["canAddress"] = std::to_string(m_sourceAddress.load());
493}
494
495// Populate the tick box in the connection manager
497 return m_driverStatistics;
498}
499
500// Start the worker thread (in turn opens the serial port)
501void CommDriverN2KCanable::Open() {
502 m_serialThread = std::make_unique<CanableSerialThread>(this, m_portName);
503 m_serialThread->Run();
504 m_isPortOpen = true;
505}
506
507// Stop the worker thread
508void CommDriverN2KCanable::Close() {
509 m_isPortOpen = false;
510 if (m_serialThread) {
511 m_serialThread->RequestStop();
512 m_serialThread->Wait();
513 m_serialThread.reset();
514 }
515}
516
517// Queue a SLCAN formatted frame for sending
518bool CommDriverN2KCanable::WriteFrame(const std::string& payload) {
519 // Guard against the thread not having been started
520 if (!m_serialThread) {
521 return false;
522 }
523 m_serialThread->QueueWrite(payload);
524 return true;
525}
526
527// Generate PGN 60928 Address Claim
528bool CommDriverN2KCanable::SendAddressClaim(int proposed_source_address) {
529 // Update our address
530 m_sourceAddress.store(proposed_source_address);
531 UpdateAttrCanAddress();
532
533 std::vector<uint8_t> data;
534 uint32_t unique_and_mfg = m_deviceInformation.value.UnicNumberAndManCode;
535
536 data.push_back(unique_and_mfg & 0xFF);
537 data.push_back((unique_and_mfg >> 8) & 0xFF);
538 data.push_back((unique_and_mfg >> 16) & 0xFF);
539 data.push_back((unique_and_mfg >> 24) & 0xFF);
540 data.push_back(m_deviceInformation.value.DeviceInstance);
541 data.push_back(m_deviceInformation.value.DeviceFunction);
542 data.push_back(m_deviceInformation.value.DeviceClass);
543 data.push_back(m_deviceInformation.value.IndustryGroupAndSystemInstance);
544
545 std::vector<std::string> slcan_strings = FormatCanMessage(
546 6, static_cast<uint8_t>(proposed_source_address), 0xFF, 60928, data);
547
548 bool result = true;
549 for (const auto& slcan_string : slcan_strings) {
550 result = WriteFrame(slcan_string) && result;
551 }
552 return result;
553}
554
555// Generates PGN 126996 Product Information
556bool CommDriverN2KCanable::SendProductInfo() {
557 std::vector<uint8_t> payload;
558 payload.push_back(2100 & 0xFF); // N2K version
559 payload.push_back((2100 >> 8) & 0xFF);
560 payload.push_back(0xEC); // Product code, arbitrary (1772, same as
561 payload.push_back(0x06); // comm_drv_n2k_socketcan.cpp placeholder)
562
563 AddStr(&payload, "OpenCPN", 32); // Model ID
564 AddStr(&payload, PACKAGE_VERSION, 32); // Software version
565 AddStr(&payload, PACKAGE_VERSION, 32); // Model version
566 AddStr(&payload, std::to_string(m_uniqueNumber), 32); // Serial number
567
568 payload.push_back(0); // Certification level
569 payload.push_back(0); // Load equivalency
570
571 int source = m_sourceAddress.load();
572 // Guard against an invalid source address
573 if (source < 0 || source > 253) {
574 return false;
575 }
576
577 std::vector<std::string> slcan_strings =
578 FormatCanMessage(6, static_cast<uint8_t>(source), 0xFF, 126996, payload);
579 bool result = true;
580 for (const auto& slcan_string : slcan_strings) {
581 result = WriteFrame(slcan_string) && result;
582 }
583 return result;
584}
585
586// Process Messages (which are in the stupid f...g Actisense format) and handle
587// the housekeeping PGN's Address Claims and ISO Requests
588// BUG BUG Contemplate support for PGN 126993 Heartbeat
589void CommDriverN2KCanable::ProcessMessages(
590 const std::shared_ptr<const Nmea2000Msg>& msg) {
591 // Mirrors Worker::ProcessRxMessages() in comm_drv_n2k_socketcan.cpp
592 uint8_t sourceAddress = m_sourceAddress.load();
593
594 // Handle PGGN 59904 ISO request if addressed directly to us or a broadcast
595 if (msg->PGN.pgn == 59904 && msg->payload.size() > 15 &&
596 (msg->payload.at(6) == static_cast<uint8_t>(sourceAddress) ||
597 msg->payload.at(6) == 0xFF)) {
598 unsigned long requested_pgn = 0;
599 requested_pgn = static_cast<unsigned long>(msg->payload.at(15)) << 16;
600 requested_pgn += static_cast<unsigned long>(msg->payload.at(14)) << 8;
601 requested_pgn += msg->payload.at(13);
602
603 switch (requested_pgn) {
604 case 60928:
605 SendAddressClaim(sourceAddress);
606 break;
607 case 126996:
608 SendProductInfo();
609 break;
610 default:
611 break;
612 }
613
614 // Handle PGN 60928 Address Claim
615 } else if (msg->PGN.pgn == 60928 && msg->payload.size() >= 21) {
616 // Perform the address claim dance if it is our source address
617 if (msg->payload.at(7) == static_cast<uint8_t>(sourceAddress)) {
618 uint64_t myName = m_deviceInformation.GetName();
619 uint64_t otherName = 0;
620 auto* p = reinterpret_cast<uint8_t*>(&otherName);
621 for (unsigned int i = 0; i < 8; i++) {
622 *p++ = msg->payload.at(13 + i);
623 }
624 // Compare the two NAME's
625 if (otherName < myName) {
626 // We lose; try the next address up.
627 sourceAddress++;
628 // Out of valid addresses!
629 if (sourceAddress > 253) {
630 sourceAddress = 254;
631 wxLogMessage("Canable: Could not claim a valid address");
632 }
633 }
634 // Resend our address claim, either the same address if we won,
635 // or the new address after yielding.
636 SendAddressClaim(sourceAddress);
637 }
638 }
639}
640
641// Encapsulate the received NMEA 2000 message into the Actisense format
642// Build the legacy Actisense-style envelope expected by Nmea2000Msg's
643// payload for received messages.
644// I will never, ever understand this mind boggling stupid design decision
645// Mirrors CommDriverN2KSocketCAN's PushCompleteMsg() / PushFastMsgFragment()
646// produce.
647void CommDriverN2KCanable::EncodeActisenseMessage(const can_frame& frame) {
648 CanHeader header(frame);
649 int position = kNotFound;
650 bool isFinalMessage = true;
651
652 if (header.IsFastMessage()) {
653 position = m_fastMessageMap->FindMatchingEntry(header, frame.data[0]);
654 if (position == kNotFound) {
655 position = m_fastMessageMap->AddNewEntry();
656 isFinalMessage =
657 m_fastMessageMap->InsertEntry(header, frame.data, position);
658 } else {
659 isFinalMessage =
660 m_fastMessageMap->AppendEntry(header, frame.data, position);
661 }
662 }
663 if (!isFinalMessage) {
664 // Still waiting on more fast packet frames.
665 return;
666 }
667 // Have the complete fast message
668 std::vector<uint8_t> envelope;
669 auto currentTime = std::chrono::steady_clock::now();
670 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
671 currentTime - m_startTime);
672
673 envelope.push_back(N2K_RX_CMD); // Actisense Command Id
674 if (header.IsFastMessage()) {
675 const FastMessageMap::Entry& e = (*m_fastMessageMap)[position];
676 envelope.push_back(static_cast<uint8_t>(e.expected_length + 11));
677 envelope.push_back(header.priority);
678 envelope.push_back(header.pgn & 0xFF);
679 envelope.push_back((header.pgn >> 8) & 0xFF);
680 envelope.push_back((header.pgn >> 16) & 0xFF);
681 envelope.push_back(header.destination);
682 envelope.push_back(header.source);
683 // Insert a proper time stamp
684 envelope.push_back(elapsed.count() & 0xFF);
685 envelope.push_back((elapsed.count() >> 8) & 0xFF);
686 envelope.push_back((elapsed.count() >> 16) & 0xFF);
687 envelope.push_back((elapsed.count() >> 24) & 0xFF);
688 envelope.push_back(e.expected_length);
689 for (size_t n = 0; n < e.expected_length; n++) {
690 envelope.push_back(e.data[n]);
691 }
692
693 // Remove this complete message from the map
694 m_fastMessageMap->Remove(position);
695
696 }
697 // Single frame message
698 else {
699 envelope.push_back(0x13); // 11 bytes of header and 8 bytes of data
700 envelope.push_back(header.priority);
701 envelope.push_back(header.pgn & 0xFF);
702 envelope.push_back((header.pgn >> 8) & 0xFF);
703 envelope.push_back((header.pgn >> 16) & 0xFF);
704 envelope.push_back(header.destination);
705 envelope.push_back(header.source);
706 // time stamp
707 envelope.push_back(elapsed.count() & 0xFF);
708 envelope.push_back((elapsed.count() >> 8) & 0xFF);
709 envelope.push_back((elapsed.count() >> 16) & 0xFF);
710 envelope.push_back((elapsed.count() >> 24) & 0xFF);
711 envelope.push_back(CAN_MAX_DLEN);
712 for (size_t n = 0; n < CAN_MAX_DLEN; n++) {
713 envelope.push_back(frame.data[n]);
714 }
715 }
716 // Calculate and append the checksum
717 int checksum = 0;
718 for (auto it : envelope) {
719 checksum += it;
720 }
721 // Checksum ensures modulo 256 = 0
722 checksum %= 256;
723
724 if (checksum != 0) {
725 checksum = 256 - checksum;
726 }
727 envelope.push_back(checksum);
728
729 // Send the formatted messages to the listener as well as locally for NMEA
730 // 2000 housekeeping
731 auto address = GetAddress(m_deviceInformation);
732 auto message =
733 std::make_shared<const Nmea2000Msg>(header.pgn, envelope, address);
734
735 // Perform local housekeeping
736 // BUG BUG, Really should perform local housekeeping on "un-enveloped" data
737 ProcessMessages(message);
738
739 // Send to OpenCPN
740 m_driverListener.Notify(message);
741
742 // Should really be the NMEA 2000 data size rather then the whole Actisense
743 // encapsulation
744 m_driverStatistics.rx_count += envelope.size();
745}
746
747// Builds one SLCAN "T<8-hex-id><1-hex-len><hex-data>\r" message from a can
748// frame.
749std::string CommDriverN2KCanable::EncodeSLCANFrame(
750 uint32_t can_id, const std::vector<uint8_t>& data) {
751 std::ostringstream os;
752 os << "T" << std::hex << std::uppercase;
753 os.width(8);
754 os.fill('0');
755 os << (can_id & 0x1FFFFFFFu);
756 os << std::dec << data.size();
757 os << std::hex << std::uppercase;
758 for (uint8_t value : data) {
759 os.width(2);
760 os.fill('0');
761 os << static_cast<int>(value);
762 }
763 os << "\r";
764 return os.str();
765}
766
767// Formats a CAN Message into Can Id and Payload and then Encodes it in SLCAN
768// format Returns a vector of SLCAN formatted strings
769std::vector<std::string> CommDriverN2KCanable::FormatCanMessage(
770 uint8_t priority, uint8_t source, uint8_t destination, uint64_t pgn,
771 const std::vector<uint8_t>& data) {
772 std::vector<std::string> slcan_strings;
773
774 uint32_t can_id =
775 static_cast<uint32_t>(BuildCanID(priority, source, destination, pgn));
776
777 if (!IsFastMessagePGN(pgn)) {
778 // Single frame message
779 slcan_strings.push_back(EncodeSLCANFrame(can_id, data));
780 return slcan_strings;
781 }
782
783 // Fast Packet fragmentation
784 int sequence;
785 {
786 std::lock_guard<std::mutex> lock(m_sequenceNumberMutex);
787 m_sequenceNumber = (m_sequenceNumber + 0x20) & 0xE0;
788 sequence = m_sequenceNumber;
789 }
790
791 // First Frame [sequence][total length][6 data bytes]
792 size_t offset = std::min<size_t>(6, data.size());
793 {
794 std::vector<uint8_t> frame;
795 frame.push_back(static_cast<uint8_t>(sequence));
796 frame.push_back(static_cast<uint8_t>(data.size()));
797 frame.insert(frame.end(), data.begin(), data.begin() + offset);
798 frame.resize(8, 0xFF); // Pad unused bytes with 0xFF.
799 slcan_strings.push_back(EncodeSLCANFrame(can_id, frame));
800 sequence++;
801 }
802
803 // Subsequent Frames [sequence][7 data bytes].
804 while (offset < data.size()) {
805 std::vector<uint8_t> frame;
806 frame.push_back(static_cast<uint8_t>(sequence));
807 size_t n = std::min<size_t>(7, data.size() - offset);
808 frame.insert(frame.end(), data.begin() + offset, data.begin() + offset + n);
809 frame.resize(8, 0xFF); // Pad unused bytes with 0xFF.
810 slcan_strings.push_back(EncodeSLCANFrame(can_id, frame));
811 offset += n;
812 sequence++;
813 }
814
815 return slcan_strings;
816}
817
818// Invoked by OpenCPN to send a NMEA 2000 message to the outside world
819bool CommDriverN2KCanable::SendMessage(std::shared_ptr<const NavMsg> msg,
820 std::shared_ptr<const NavAddr> addr) {
821 // Port never opened, or already closed.
822 if (!m_isPortOpen) {
823 return false;
824 }
825 if (!msg) {
826 return false;
827 }
828
829 int source = m_sourceAddress.load();
830 // Guard against an invalid address
831 if (source < 0 || source > 253) {
832 return false;
833 }
834
835 auto n2kMessage = std::dynamic_pointer_cast<const Nmea2000Msg>(msg);
836 if (!n2kMessage) {
837 return false;
838 }
839 auto destinationAddress = std::static_pointer_cast<const NavAddr2000>(addr);
840 // Either a specific address or a broadcast address
841 uint8_t destination = destinationAddress ? destinationAddress->address : 0xFF;
842
843 // We receive an unencapsulated NMEA 2000 message
844 // Fortunately no Actisense envelope
845 std::vector<std::string> slcan_strings =
846 FormatCanMessage(n2kMessage->priority, static_cast<uint8_t>(source),
847 destination, n2kMessage->PGN.pgn, n2kMessage->payload);
848 // Kind of bogus. The data is queued but we have no idea if it was actually
849 // sent on the wire. Port could be present and open, but disconnected from the
850 // network etc.
851 bool result = true;
852 for (const auto& slcan_string : slcan_strings) {
853 result = WriteFrame(slcan_string) && result;
854 }
855
856 // Update our stats
857 m_driverStatistics.tx_count += n2kMessage->payload.size();
858
859 // Notify Upper Layers for logging in Data Monitor
860 // This is not the "proper" way that logging is to be implemented, so
861 // commented out for now
862 /*
863 if (result) {
864 auto currentTime = std::chrono::steady_clock::now();
865 auto elapsed =
866 std::chrono::duration_cast<std::chrono::milliseconds>(currentTime -
867 m_startTime); std::vector<unsigned char> data; data.push_back(YD_TX_CMD);
868 data.push_back(n2kMessage->payload.size() + 6); // 11, header excluding
869 command, length & crc data.push_back(n2kMessage->priority);
870 data.push_back(n2kMessage->PGN.pgn & 0xFF);
871 data.push_back((n2kMessage->PGN.pgn >> 8) & 0xFF);
872 data.push_back((n2kMessage->PGN.pgn >> 16) & 0xFF);
873 data.push_back(destination);
874 data.push_back(source);
875 // Logging uses a bastardized format!
876 //data.push_back(elapsed.count() & 0xFF);
877 //data.push_back((elapsed.count() >> 8) & 0xFF);
878 //data.push_back((elapsed.count() >> 16) & 0xFF);
879 //data.push_back((elapsed.count() >> 24) & 0xFF);
880 //data.push_back(n2kMessage->payload.size());
881 for (size_t n = 0; n < n2kMessage->payload.size(); n++)
882 data.push_back(n2kMessage->payload[n]);
883 // Calculate and append the checksum
884 int checksum = 0;
885 for (auto it : data) {
886 checksum += it;
887 }
888 // Checksum ensures modulo 256 = 0
889 checksum %= 256;
890
891 if (checksum != 0) {
892 checksum = 256 - checksum;
893 }
894 data.push_back(checksum);
895 auto src = std::static_pointer_cast<const NavAddr2000>(addr);
896 m_driverListener.Notify(std::make_shared<const
897 Nmea2000Msg>(n2kMessage->PGN.pgn, data, src));
898 }
899 */
900 return result;
901}
CAN v2.0 29 bit header as used by NMEA 2000.
DriverStats GetDriverStats() const override
Get the Driver Statistics.
Connection data container close to a POD struct.
Definition conn_params.h:76
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.
unsigned int expected_length
total data length from first frame
std::vector< unsigned char > data
Received data.
comm_drv_n2k_canable.h NMEA2000 driver for CANable "Contact" (and other SLCAN/LAWICEL-firmware) USB-C...
std::string PortDirectionToString(PortDirection pd)
Return textual representation for use in driver ioDirection attribute.
Driver statistics report.
unsigned tx_count
Number of bytes sent since program start.
unsigned rx_count
Number of bytes received since program start.