Skip to content

slcan doesn't receive corresponding response after sending can message. #2060

Description

@earthPerson-001

Describe the bug

While calling slcanBus.send, the corresponding response isn't read. This makes it report false success even on failed messages.

To Reproduce

Setup two can devices, both configured to send ack on successful can message receive. Disconnect the can wires after some time.

Expected behavior

Expected to receive ack when wire is connected (slcan should report successful transmission)
Expected to receive nack(bel) when wire is disconnected (slcan should report failed transmission)

Additional context

OS and version: Ubuntu 24.04.3 LTS
Python version: Python 3.11.14
python-can version: 4.6.1
python-can interface/s: slcan with custom slcan device (esp32s3)

Details The following implementation solves that problem (introduces other ones)
import can
from can.interfaces.slcan import slcanBus
import threading


class SLCANBusWithErrorCheck(slcanBus):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._tx_lock = threading.Lock()

    def send(self, msg, timeout=None):
        if timeout != self.serialPortOrig.write_timeout:
            self.serialPortOrig.write_timeout = timeout

        if msg.is_remote_frame:
            if msg.is_extended_id:
                sendStr = f"R{msg.arbitration_id:08X}{msg.dlc:d}"
            else:
                sendStr = f"r{msg.arbitration_id:03X}{msg.dlc:d}"
        else:
            if msg.is_extended_id:
                sendStr = f"T{msg.arbitration_id:08X}{msg.dlc:d}"
            else:
                sendStr = f"t{msg.arbitration_id:03X}{msg.dlc:d}"
            sendStr += msg.data.hex().upper()

        with self._tx_lock:
            # Drain any leftover bytes from a previous error before sending
            self.serialPortOrig.reset_input_buffer()
            self._buffer.clear()

            self._write(sendStr)
            response = self._read(timeout=max(timeout or 0, 2.0))

        if response is None:
            raise can.CanError("TX timeout - no response from adapter")
        if self._ERROR.decode() in response:
            raise can.CanError("TX failed - BEL received (no ACK on bus)")

    def recv(self, timeout=None):
        if self._tx_lock.locked():
            return None
        return super().recv(timeout=timeout)

I know following are the errors in the provided solution:

  • If we are mid receive, then any call to send can create race condition (clear the serial buffers mid receive)
  • the timeout logic is also incorrect (additional 2.0 second for timeout < 2.0 second, patch for some other problem)
  • Receive fails immediately if transmission is going on

Any solution i came up for these problems seemed patchy as best. So, expecting it to solve inside of python-can itself or provide a better solution than locking.

Activity

  1. Nakakiyo092 commented on Jun 13, 2026

    @Nakakiyo092

    Hello,

    I think there's a small misunderstanding here about how SLCAN ACK/NACK is supposed to work.

    The protocol originated with LAWICEL's CAN232/CANUSB, and on those devices the ACK response to a send command does not mean "the frame was transmitted on the bus". It just means "your command parsed and was queued into the Tx buffer". The minimal implementation literally only checks command syntax. As far as I can tell, CANUSB works this way, and every custom SLCAN firmware I've come across follows the same convention — I've never seen one where ACK/NACK implies bus-level transmission.

    Regards.

  2. earthPerson-001 commented on Jun 14, 2026

    @earthPerson-001
    Author

    Based upon the implementation of CANUSB (socketcan specifically), I have used, they will report failure to transmit after the Tx buffer is full, eventually making it known to the higher level that there is some problem with the CAN bus. There is the same kind of queue/buffers in almost all implementations, i.e. we won't know when exactly the bus failed and will only report after the queue/buffer is full.

    In the case of SLCAN, if the response is totally rejected, then all frames will be reported as success even if hundreds of transmissions fail, and the higher level will never know if the underlying CAN bus has some errors. There probably will be workarounds for this (rejecting serial messages until we can fully transmit through the bus), but if there is already a functionality that achieves the same, shouldn't we use that?

    Based on the LAWICEL's documentation, there is a provision for ACK/NACK after transmission. Will there be problems if we expect a response after each transmission (won't the existing implementation support this)?

    I am not arguing about the meaning of ACK/NACK; that will be dependent upon the CAN device itself, but totally ignoring the response will neither inform about full buffers/queues nor about the failed bus-level transmission.

  3. Nakakiyo092 commented on Jun 14, 2026

    @Nakakiyo092

    Hello,

    I agree that host-side failure detection would be helpful.

    That said, in the LAWICEL protocol, device state — including bus errors and Tx buffer-full — is reported through the F command, which the host is expected to poll. So for a LAWICEL-compliant device, the correct model is ACK/NACK checking plus F polling — they cover different failure modes. But neither is implemented in python-can.

    What python-can should actually do here is a harder question. The LAWICEL protocol is one thing; the SLCAN ecosystem in the wild is another. Outside of LAWICEL's own devices, fully spec-compliant implementations are rare — many firmwares leave ACK/NACK and/or F unimplemented or stub them with fake replies. So strict spec enforcement at the library layer would help spec-compliant devices but not the bulk of devices in the field.

    Regards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions