Skip to content

Send Message Internals

MessageSender is a plain, non-GUI QObject that owns the transmit side of Send Message: the cyclic scheduler and the actual write path. It picks between two fundamentally different mechanisms per vendor, SendMode::DirectBackend or SendMode::NamedPipe, decided once in openPort() based on which backend matches the selected channel.

Why two mechanisms at all

Vector's XL Driver Library has a real permission-mask concept: a port opened with a zero permission mask joins a channel without requesting configuration rights, as a genuine second, independent handle. So Vector's path is simple - MessageSender just opens a second listen-only port directly on the same channel and calls writeFrame() on it.

sequenceDiagram
    participant UI as Stimulation tab
    participant MS as MessageSender
    participant VB as VectorBackend (2nd port)
    participant HW as Vector hardware

    UI->>MS: Start Sending / Send Now
    MS->>VB: initialize(requestOwnership=false)
    VB->>HW: xlOpenPort(zero permission mask)
    MS->>VB: writeFrame(CanFrame)
    VB->>HW: xlCanTransmitEx

PCAN-Basic (PEAK's driver) has no equivalent. There is no second-handle path onto a channel another process already initialized, full stop - confirmed for real: a second CAN_Initialize() call on an already-active PEAK channel returns PCAN_ERROR_INITIALIZE. The only process holding a valid PEAK handle is can2pcap.exe itself (it already called CAN_Initialize() to start capturing). So transmit has to be routed through can2pcap.exe, not around it:

sequenceDiagram
    participant UI as Stimulation tab
    participant MS as MessageSender
    participant Pipe as named pipe\ncantrip_tx_<interfaceId>
    participant CP as can2pcap.exe
    participant HW as PEAK hardware

    Note over CP: capture loop already running,<br/>already holds the one valid handle
    CP->>Pipe: CreateNamedPipeA (PIPE_NOWAIT)
    UI->>MS: Start Sending / Send Now
    MS->>Pipe: CreateFileA (connect as client)
    MS->>Pipe: WriteFile(raw CanFrame bytes)
    CP->>Pipe: ReadFile (polled each loop iteration)
    CP->>HW: CAN_Write / CAN_WriteFD (on its own handle)

The pipe carries raw CanFrame bytes with no serialization scheme - both processes are built from the same header by the same compiler in the same CMake project, so there's no cross-ABI risk, and writing/reading sizeof(CanFrame) bytes directly is simplest. The server side (can2pcap.exe) uses PIPE_NOWAIT rather than overlapped I/O, keeping its existing simple synchronous polling capture loop intact instead of introducing threads for what's a low-frequency, low-stakes internal control channel.

Why the port opens lazily

MessageSender::openPort() is only called on the first actual Start Sending/Send Now click, not automatically when a capture starts. Opening it automatically raced against tshark/can2pcap.exe actually finishing their own startup (DLL load, channel resolve, CAN_Initialize, pipe creation) - capture_.isRunning() in the GUI only reflects tshark's own process state, not whether the further child process can2pcap.exe has gotten that far yet. This caused a real, confirmed bug: a near-simultaneous second CAN_Initialize() call reset/conflicted with the channel PEAK capture had just set up, killing a live capture outright. The PEAK pipe client path also retries connecting for a couple of seconds to absorb this same startup latency rather than failing on the very first attempt.

The DLC conversion applies before either path

Both SendModes carry a TransmitMessage (byte-length dlc) that must be converted to a CanFrame (DLC-code dlc) before transmission - see The AVlabs CAN Backend: the DLC code vs byte length trap. This conversion happens once, in MessageSender::transmitOne(), before the SendMode branch, since it's about the payload itself, not how it's delivered.