Skip to main content

python-can-pscan

A native python-can hardware interface for PSCAN USB CAN adapters.

python-can-pscan extends python-can by registering a new pscan interface backend. Once installed, you can use the standard can.Bus() API to send and receive CAN frames through your PSCAN hardware — no separate PSCAN DLL required (Windows requires the WinUSB driver). The heavy lifting (USB I/O, frame parsing, multi-frame buffering, and message filtering) is handled by a compiled native extension for maximum performance.

Key Features

  • Drop-in python-can backend — works with can.Bus(), can.Notifier, can.Logger, and all standard python-can tools.
  • Serial number based device selection — connect to a specific adapter by its unique serial number instead of an ambiguous channel or port name.
  • User-friendly name support — open devices by a short name stored in the adapter.
  • Auto-detection — connects automatically when exactly one PSCAN adapter is attached.
  • High-performance core — frame serialization, deserialization, multi-frame USB bulk reads, and software filtering all run natively, not in pure Python.
  • Listen-only and loopback modes — built-in CAN controller mode configuration.
  • Bus state monitoring — read error counters and CAN controller state (Error Active / Warning / Passive / Bus-Off).
  • Bus state change notifications — register callbacks, or receive error frames through can.Notifier, whenever the controller changes error state.
  • Optional transmission confirmation — modern firmware reports success, failure, arbitration loss or cancellation; older firmware falls back to success-only echoes.
  • Firmware diagnostics — capability flags, save status, configured/running timing and decoded historical controller errors are available through Python.

Installation

pip install python-can-pscan

python-can >= 4.0.0 is installed automatically as a dependency.

The project follows python-can's plugin naming convention: distribution python-can-pscan, import package can_pscan, interface name pscan. The earlier import name pscan_pythoncan still works and exposes the same objects, so classes, exceptions and isinstance checks agree across both names:

import can_pscan                      # preferred
import pscan_pythoncan                # legacy, same objects
assert can_pscan.PscanBus is pscan_pythoncan.PscanBus

Releases up to 0.1.9 were published as pscan-pythoncan. Uninstall that distribution before installing python-can-pscan, since both contain the pscan_pythoncan package.

Wheels (one abi3 wheel per platform, CPython 3.8 and newer):

OS Architectures
macOS Intel x86_64 (10.12+), Apple Silicon arm64 (11.0+)
Windows x86 (win32), x64 (win_amd64), arm64 (win_arm64)
Linux x86_64, i686, aarch64 (manylinux2014, glibc 2.17+)

Windows wheels link the MSVC C runtime statically, so no Visual C++ Redistributable is needed; they import only Windows system DLLs and require Windows 10 or newer plus the WinUSB driver for the adapter.

No source distribution is published, because building needs access to the private core library.


Quick Start

import can

# Open the only connected PSCAN device at 500 kbit/s
bus = can.Bus(interface='pscan', bitrate=500000)

# Send a CAN frame
msg = can.Message(arbitration_id=0x123, data=[0x11, 0x22, 0x33, 0x44], is_extended_id=False)
bus.send(msg)

# Receive a CAN frame (1 second timeout)
recv_msg = bus.recv(timeout=1.0)
if recv_msg is not None:
    print(recv_msg)

# Always shut down when done
bus.shutdown()

Opening a Device

PSCAN adapters are identified by their serial number (printed on the device label) or by an optional user-friendly name stored on the device. This avoids the ambiguity of generic channel numbers or COM ports — you always connect to exactly the hardware you intend.

bus = can.Bus(interface='pscan', serial='P1P.IN:XFG:H001', bitrate=500000)

By user-friendly name

If your adapter has been programmed with a short name (e.g. via PSCANStudio), you can open it by that name:

bus = can.Bus(interface='pscan', name='MyBMS', bitrate=500000)

Auto-detect (single device)

When exactly one PSCAN adapter is connected you can omit serial and name. With several adapters attached, auto-detect fails as ambiguous; pass serial or name instead:

bus = can.Bus(interface='pscan', bitrate=500000)

Constructor Parameters

All parameters are passed through the standard can.Bus() constructor:

bus = can.Bus(
    interface='pscan',
    channel='PSCAN_USB1',       # Channel label (for python-can compatibility)
    serial='P1P.IN:XFG:H001',  # Device serial number
    name='MyBMS',               # Device user-friendly name (alternative to serial)
    bitrate=500000,             # Bitrate in bits per second
    sample_point=875,           # Sample point in permille (875 = 87.5%)
    listen_only=False,          # Enable listen-only mode (no TX, no ACK)
    loopback=False,             # Enable loopback mode (TX echoed to RX)
    tx_confirm=False,           # True: wait for device transmission confirmation
    can_filters=None,           # Message filters (see Filtering section)
    state_notifications=False,  # Emit bus-state error frames to can.Notifier
    state_poll_interval=0.5,    # Seconds between bus-state polls (monitor)
)
Parameter Type Default Description
serial str None Serial number of the PSCAN adapter. Used to select a specific device when multiple adapters are connected.
name str None User-friendly name stored in the adapter (takes priority over serial).
bitrate int 500000 CAN bus bitrate in bits per second. Common values: 125000, 250000, 500000, 1000000.
sample_point int 875 CAN sample point in permille. 875 means 87.5%. Typical range: 750–875.
listen_only bool False When True, the adapter will not transmit any frames and will not send ACK bits on the bus. Useful for passive monitoring.
loopback bool False When True, transmitted frames are echoed back to the receive path. Useful for testing without a second node.
tx_confirm bool False Enable managed per-frame delivery confirmation. Firmware capabilities select modern results or legacy success-only echoes.
led_mode int 1 Hardware LED behavior: 0=Off, 1=On (default), 2=ActiveOnly, 3=TxOff, 4=RxOff.
channel str 'PSCAN_USB1' Channel identifier string for python-can compatibility. Not used for device selection.
can_filters list None List of filter dictionaries. See Message Filtering below.
state_notifications bool False When True, start the bus-state monitor at construction and deliver each controller state transition as a synthetic error frame on the receive path (so a can.Notifier sees it). Off by default so it never perturbs error-frame counters unless requested. See Bus State & Error Monitoring.
state_poll_interval float 0.5 Seconds between hardware bus-state polls for the notification monitor. Clamped to a minimum of 0.05s.

Device selection priority: name > serial > auto-detect (single device only).

Unsupported python-can options fail before the device opens instead of being ignored: fd=True, data_bitrate, bitrate_switch=True, timing= (including timing built by python-can from f_clock/brp/tseg1/tseg2/sjw config keys) and receive_own_messages=True raise NotImplementedError. Use bitrate and sample_point for timing, and loopback=True for controller loopback.


Sending Messages

Use the standard bus.send() method:

# Standard CAN frame (11-bit ID)
msg = can.Message(arbitration_id=0x123, data=[0x01, 0x02, 0x03], is_extended_id=False)
bus.send(msg)

# Extended CAN frame (29-bit ID)
msg = can.Message(arbitration_id=0x1ABCDEF0, data=[0xAA, 0xBB], is_extended_id=True)
bus.send(msg)

# Remote Transmit Request (RTR)
msg = can.Message(arbitration_id=0x200, is_remote_frame=True, dlc=8, is_extended_id=False)
bus.send(msg)

# With a custom timeout (seconds)
bus.send(msg, timeout=0.1)  # 100 ms timeout

Notes:

  • Without confirmation, the default USB submission timeout is 50 ms. With tx_confirm=True, send(msg, timeout=...) waits for success within one submission-and-confirmation deadline. timeout=None has no confirmation deadline; use an explicit timeout when bounded delivery time is required.
  • An explicit timeout=0 gives the USB transport no time budget: it submits nothing and raises can.CanOperationError, matching the pinned library.
  • Sending in listen-only mode raises can.CanOperationError.
  • A USB submission timeout without confirmation raises can_pscan.SendTimeoutError, a subclass of both can.CanOperationError and can.CanTimeoutError. The frame may or may not have been submitted.
  • CAN FD frames are not supported and will raise NotImplementedError.

Delivery results and cancellation

Ordinary send() without confirmation means the frame was submitted over USB; it does not prove a CAN peer acknowledged it. To inspect an individual result:

from can_pscan import PscanBus, TxTimeoutError

with PscanBus(serial="YOUR_SERIAL", bitrate=500000, tx_confirm=True) as bus:
    print(bus.confirmation_info)
    ticket = bus.submit(msg, timeout=0.050)   # one submission, never retried
    result = ticket.wait(timeout=1.0)        # None means still pending
    if result is None and bus.confirmation_info["cancellation_available"]:
        reply = ticket.cancel(timeout=0.2)  # request only, not proof of cancellation
        result = ticket.wait(timeout=1.0)
    print(result)  # success / failed / arbitration_lost / cancelled / uncertain

The library owns confirmation IDs and drains both RX and completion endpoints. A timeout, dropped ticket or uncertain USB write never permits blind retransmission or ID reuse. ticket.usb_error preserves an uncertain USB error; its eventual result may still be success. Cancellation may race with successful delivery.

send_with_confirmation(msg, timeout=1.0) returns the result, including failed results. If still pending, it raises TxTimeoutError; exception.ticket can be waited on or cancelled. Standard send() returns None on success and raises a python-can error for a failed or uncertain outcome. Neither method automatically retries a frame or treats an abort request as a cancelled result.

Legacy firmware supplies success-only echoes and cannot provide tracked targeted cancellation. Unsupported optional queries return None where documented; malformed responses and transport failures remain errors. No firmware version string is used to guess support.

Firmware settings and diagnostics

from can_pscan import get_library_info

print(get_library_info())             # package, product/API and dependency revision; no USB
print(bus.serial_number)
print(bus.firmware_features)          # known flags plus the complete raw mask
print(bus.compatibility_report())     # qualification, optional features and legacy paths
print(bus.get_bittiming())            # configured Classic CAN timing
print(bus.get_bittiming_snapshot())   # None if unsupported; no controller changes
print(bus.get_diagnostics())          # state/counters and a historical error
print(bus.get_settings_status())     # None on firmware without status support

# Explicitly stop before operations requiring a stopped controller.
bus.stop_all_periodic_tasks()
bus.set_bus_state(False)
bus.set_device_name("bench", wait_for_save=2.0)
bus.set_bitrate(1000000)
bus.wait_settings_saved(timeout=2.0)
bus.reset_timestamps()
bus.set_bus_state(True)

wait_settings_saved() only polls automatic persistence: it does not request a save, reboot or stop the bus. Only saved/unchanged prove durability. The atomic name-and-wait operation checks that CAN is stopped before writing. Channel IDs (get_channel_id/set_channel_id), volatile set_interface_name and supported termination control (get_termination/set_termination) are also exposed.

Controller diagnostics are separate observations, not an atomic snapshot and not proof of an individual frame's failure cause. An arbitration-lost TX result is per-frame evidence; a last recorded ACK error is historical context. Timing comparison is meaningful only while the peripheral is running.

Frame timestamps remain device-relative microseconds converted to seconds, not Unix wall-clock time. Opening the bus resets the device clock where the firmware supports it, so timestamps normally start near zero. Resetting timestamps deliberately changes the time origin. CAN FD/data-phase timing are still rejected until the main transport supports them.

Capture files

Use python-can's existing writers with can.Notifier; no extra PSCAN logging thread or format is required. Choose can.BLFWriter, can.ASCWriter, can.CanutilsLogWriter (candump) or can.CSVWriter:

writer = can.BLFWriter("capture.blf")
notifier = can.Notifier(bus, [writer])
try:
    # Application work; notifier receives in the background.
    run_application()
finally:
    notifier.stop()  # stops the writer and finalizes its file

See python-can file I/O for format limits and timestamp conventions. Experimental speed detection, bus load TUI and SocketCAN daemons remain standalone CLI tools. In python-can 4.6.1, the candump writer omits the requested DLC of RTR frames; use BLF, ASC or CSV when that field must survive a capture round trip.

PSCAN controls beyond BusABC

can.Bus(interface="pscan", ...) returns a PscanBus, so the same object also provides device controls that the portable BusABC interface does not define. The normal send, recv, filters, notifier and logger APIs remain available.

print(bus.get_identity())          # model, serial, name and all firmware/API versions
print(bus.get_bittiming_limits())  # clock, prescaler, segments and SJW limits
print(bus.get_supported_modes())  # capability flags; raw preserves unknown bits
print(bus.get_modes())
print(bus.get_statistics())       # host counters since open, across CAN restarts

# Explicitly change only the supplied settings, if supported by this device.
bus.stop_all_periodic_tasks()
bus.set_modes(one_shot=True, error_reporting=True)
bus.set_listen_only(True)
bus.set_listen_only(False)

if bus.firmware_features["identify"]:
    bus.identify(duration_ms=1000)
temperatures = bus.get_temperatures()  # None if the capability is absent

set_modes() accepts listen_only, loopback, one_shot, triple_sampling and error_reporting as optional booleans. Omitted flags are preserved, including confirmation flags. An actual change restarts a running CAN controller and stops native periodic workers; a no-op does neither. Stop Python callback periodic tasks before reconfiguring. Pending TX confirmation tickets must finish before a change; a cancellation request alone does not qualify. set_listen_only() is clearer than the compatibility state=BusState.PASSIVE setter, since the reported error-passive state and listen-only mode differ.

get_statistics() contains 64-bit tx_frames, rx_frames, tx_data, rx_data, overruns, bus_warnings and bus_off. TX counts completed USB submissions, not CAN acknowledgements; RX counts native queue dequeues before adapter filtering. Data counters sum DLC, including RTR requests. overruns counts host queue evictions. These are individually sampled counters, not an atomic wire-loss measurement. They remain cumulative across CAN restarts until close. bus_warnings and bus_off count controller error frames (bus_event_semantics is "controller_error_frames"), before adapter filtering. They only advance while the firmware delivers error frames, so zero alone does not prove the bus was quiet. Read get_diagnostics() for device-reported state, error counters and bus-off count.

Temperature responses preserve temp_mask, soc_temp, device_temp, aux1_temp and aux2_temp as unsigned firmware words; only present sensors are valid. Firmware documents SOC/device values in Celsius. The adapter does not guess auxiliary scaling or signed encoding. Advertised command failures remain errors, including a stalled identify request.

reset_can() uses the managed reset path, stops native periodic workers and preserves whether CAN was running. It rejects pending confirmed tickets and does not reboot the USB device. Full feature coverage and remaining limits are listed in Main-library integration.

Periodic messages

bus.send_periodic(msg, period) uses a native worker. Supplying modifier_callback uses a Python worker that invokes the callback immediately before each transmission, allowing rolling counters and checksums. Keep these callbacks short; stopping a task waits for an active callback to return. Inspect task.is_running and task.last_error to detect expiry or a send failure. Confirmed periodic sends have a 50 ms per-frame deadline and stop on failure or unknown delivery; they never silently retry. Pending tickets retain their IDs until a result arrives or the bus is shut down. A native periodic timeout does not expose an individual ticket; stop the task and shut down/reopen the bus if its result never arrives. Python callback timeout errors retain .ticket.

Direct sends and periodic start/modification use the same Classic CAN validation: valid arbitration ID, DLC 0–8, matching data length, and no FD or error-frame transmission. RTR frames have no data and may request DLC 0–8. Periods must be finite and at least one microsecond; this is input resolution, not a timing guarantee. Missed ticks are skipped to avoid catch-up bursts. Actual timing depends on the OS, USB and bus load.

shutdown() stops periodic workers and closes the native device even if task handles are retained or store_task=False was used. Tasks cannot restart after their bus shuts down. Pending and subsequent recv() calls raise can.CanOperationError on shutdown; stop a can.Notifier before shutting down its bus when possible.


Receiving Messages

Use the standard bus.recv() method:

# Blocking receive with timeout
msg = bus.recv(timeout=1.0)  # Wait up to 1 second

# Non-blocking receive
msg = bus.recv(timeout=0)

# Blocking forever (use with caution)
msg = bus.recv()  # Blocks until a frame arrives

The returned can.Message object contains:

Field Description
timestamp Hardware timestamp in seconds (microsecond resolution from the adapter)
arbitration_id CAN ID (11-bit or 29-bit)
is_extended_id True if 29-bit extended frame
is_remote_frame True if RTR frame
is_error_frame True if error frame
dlc Data Length Code (0–8)
data Frame payload as bytearray
channel Channel info string (e.g. "PSCAN: P1P.IN:XFG:H001")

Event-Driven Receive with Notifier

For background message processing, use can.Notifier:

class MyListener(can.Listener):
    def on_message_received(self, msg):
        print(f"RX: 0x{msg.arbitration_id:03X}  [{msg.dlc}]  {msg.data.hex()}")

listener = MyListener()
notifier = can.Notifier(bus, [listener])

# Main thread is free to do other work...
import time
time.sleep(10)

notifier.stop()
bus.shutdown()

Message Filtering

Filters are applied in the compiled backend for high performance — frames that don't match are discarded before they ever reach Python.

# Accept only CAN ID 0x123 (standard frames)
bus.set_filters([{"can_id": 0x123, "can_mask": 0x7FF, "extended": False}])

# Accept IDs 0x200–0x2FF (standard frames)
bus.set_filters([{"can_id": 0x200, "can_mask": 0x700, "extended": False}])

# Accept extended frames with ID 0x1ABCDE00–0x1ABCDEFF
bus.set_filters([{"can_id": 0x1ABCDE00, "can_mask": 0x1FFFFF00, "extended": True}])

# Multiple filters (frame passes if it matches ANY filter)
bus.set_filters([
    {"can_id": 0x100, "can_mask": 0x7FF, "extended": False},
    {"can_id": 0x200, "can_mask": 0x7FF, "extended": False},
])

# Clear all filters (accept everything)
bus.set_filters(None)
Filter Key Type Description
can_id int CAN ID to match
can_mask int Bitmask. A frame passes if (frame_id & can_mask) == (can_id & can_mask).
extended bool or omitted True = match only extended (29-bit) frames. False = match only standard (11-bit) frames. Omit to match both.

You can also set filters when constructing the bus:

bus = can.Bus(
    interface='pscan',
    bitrate=500000,
    can_filters=[{"can_id": 0x123, "can_mask": 0x7FF}]
)

Bus State & Error Monitoring

Reading the bus state

state = bus.state  # Returns can.BusState enum

if state == can.BusState.ACTIVE:
    print("Bus is Error Active (normal operation)")
elif state == can.BusState.PASSIVE:
    print("Bus is in Error Warning or Error Passive state")
elif state == can.BusState.ERROR:
    print("Bus-Off or Stopped")

Setting the bus state

bus.state = can.BusState.ACTIVE   # Go bus-on
bus.state = can.BusState.ERROR    # Go bus-off
bus.state = can.BusState.PASSIVE  # Switch to listen-only mode

Reading the full (raw) bus state

bus.state collapses the controller's six-way hardware state into python-can's three-way BusState. To read the raw state and error counters instead, use the PSCAN-specific get_raw_bus_state():

state, rx_err, tx_err = bus.get_raw_bus_state()
# state: 0=ErrorActive 1=ErrorWarning 2=ErrorPassive 3=BusOff 4=Stopped 5=Sleeping
print(f"raw state={state}  rx_err={rx_err}  tx_err={tx_err}")

Bus state change notifications

python-can exposes bus.state as a polled property — it has no built-in event for state transitions. python-can-pscan adds a background monitor that watches the controller's error state and notifies you when it changes, in two complementary ways.

1. Callback listeners (PSCAN extension)

Register a callback that fires on every transition. A daemon thread polls the hardware every state_poll_interval seconds (default 0.5 s) and calls your callback as callback(new_state, raw), where new_state is a can.BusState and raw is the full (state, rx_error_count, tx_error_count) tuple:

def on_state_change(new_state, raw):
    state_val, rx_err, tx_err = raw
    print(f"Bus state -> {new_state.name}  (rx_err={rx_err}, tx_err={tx_err})")

bus = can.Bus(interface='pscan', bitrate=500000)
bus.add_state_change_listener(on_state_change)

# ... run your application ...

bus.remove_state_change_listener(on_state_change)  # optional
bus.shutdown()  # also stops the monitor thread

The monitor starts automatically on the first add_state_change_listener() call and is stopped by shutdown(). Callbacks run on the monitor thread, so keep them short and non-blocking; an exception raised by one callback is logged and never affects the other listeners or the monitor.

2. Error frames through can.Notifier (opt-in, python-can idiomatic)

Pass state_notifications=True to also have each transition delivered as a synthetic error frame (is_error_frame=True) on the normal receive path. This lets an existing can.Notifier observe state changes with no extra wiring — the frame's first three data bytes carry (state, rx_error_count, tx_error_count). Its timestamp uses the device clock of hardware frames, extrapolated from the latest received frame or timestamp reset, so logs keep a single time base:

class StateWatcher(can.Listener):
    def on_message_received(self, msg):
        if msg.is_error_frame:
            state, rx_err, tx_err = msg.data[0], msg.data[1], msg.data[2]
            print(f"Controller state changed: state={state} rx_err={rx_err} tx_err={tx_err}")

bus = can.Bus(interface='pscan', bitrate=500000, state_notifications=True)
notifier = can.Notifier(bus, [StateWatcher()])
# ...
notifier.stop()
bus.shutdown()

Error-frame injection is off by default so it never perturbs your error-frame counters unless you ask for it. When state_notifications=True the monitor starts at construction (no listener registration required).

Flushing the transmit buffer

Discard frames still queued in the adapter for transmission:

bus.flush_tx_buffer()

The firmware only flushes safely while CAN is stopped, so on a running bus this briefly stops CAN, flushes and restarts it. Periodic tasks are stopped and frames already queued for receive are discarded. With tx_confirm=True, pending tickets must finish or be cancelled first.


Shutdown

Always shut down the bus when you are done to release the USB device:

bus.shutdown()

The bus can also be used as a context manager:

with can.Bus(interface='pscan', bitrate=500000) as bus:
    bus.send(can.Message(arbitration_id=0x123, data=[1, 2, 3]))
    msg = bus.recv(timeout=1.0)
# Bus is automatically shut down here

PSCAN-Specific Functions

The following methods are PSCAN-specific extensions that go beyond the standard python-can API.

Listing connected devices

Enumerate all connected PSCAN adapters and their serial numbers before opening a bus:

from can_pscan import PscanBus

serials = PscanBus.list_pscan_devices()
print(f"Connected PSCAN devices: {serials}")
# Example output: ['P1P.IN:XFG:H001', 'P2P.IN:ABC:D002']

This is also available through the standard python-can device discovery:

configs = can.detect_available_configs(interfaces=['pscan'])
print(configs)
# [{'interface': 'pscan', 'channel': 'PSCAN_USB1', 'serial': 'P1P.IN:XFG:H001'}, ...]

Querying device information

Read the hardware version, firmware version, and number of CAN channels from an open adapter:

bus = can.Bus(interface='pscan', bitrate=500000)

hw_version, fw_version, channel_count = bus.get_device_info()
print(f"Hardware : {hw_version}")
print(f"Firmware : {fw_version}")
print(f"Channels : {channel_count}")

Accessing Native Properties Directly

bus.name and bus.led_mode expose device settings. Writing a name does not itself prove persistence; use set_device_name(..., wait_for_save=...) while stopped when durability matters.

# Read or Overwrite the internal hardware short-name via USB
print(f"Current Name: {bus.name}")
bus.name = "MyBMS"

# Dynamically change the native physical indicator LED mode
bus.led_mode = 2  # Set to ActiveOnly (blinks on traffic)
bus.led_mode = 1  # Standard On

# Instantly pull real physical adapter bounds
caps = bus.capabilities
print(f"Clock Frequency: {caps['clock_freq']} Hz")
print(f"TX buffer bytes: {caps['tx_data_buffer_size']}")

Logging & Debugging

The library uses Python's standard logging module under the logger name can.pscan:

import logging
logging.basicConfig(level=logging.DEBUG)

Complete Example

import can
import time
from can_pscan import PscanBus

# --- Device Discovery ---
devices = PscanBus.list_pscan_devices()
print(f"Found {len(devices)} PSCAN adapter(s): {devices}")

if not devices:
    print("No PSCAN devices found.")
    exit(1)

# --- Open bus by serial number ---
with can.Bus(interface='pscan', serial=devices[0], bitrate=500000) as bus:

    # Print adapter info
    hw, fw, ch = bus.get_device_info()
    print(f"Adapter: HW={hw}  FW={fw}  Channels={ch}")

    # Set up a filter to accept only IDs 0x100–0x1FF
    bus.set_filters([{"can_id": 0x100, "can_mask": 0x700, "extended": False}])

    # Send a frame
    tx_msg = can.Message(
        arbitration_id=0x150,
        data=[0xDE, 0xAD, 0xBE, 0xEF],
        is_extended_id=False
    )
    bus.send(tx_msg)
    print(f"TX: {tx_msg}")

    # Receive frames for 5 seconds
    end_time = time.time() + 5
    count = 0
    while time.time() < end_time:
        msg = bus.recv(timeout=0.5)
        if msg is not None:
            count += 1
            print(f"RX: 0x{msg.arbitration_id:03X}  [{msg.dlc}]  {msg.data.hex()}")

    print(f"Received {count} messages in 5 seconds.")

# Bus is automatically shut down by the context manager

API Reference

PscanBus (extends can.BusABC)

Constructor

PscanBus(channel="PSCAN_USB1", serial=None, name=None, bitrate=500000,
         sample_point=875, listen_only=False, loopback=False, led_mode=1,
         can_filters=None, state_notifications=False, state_poll_interval=0.5,
         tx_confirm=False)

Standard python-can methods

Method Description
send(msg, timeout=None) Submit in 50 ms by default without confirmation; confirmed mode waits for a result, with no deadline when timeout is None.
recv(timeout=None) Receive a can.Message. Returns None on timeout.
set_filters(filters) Set message acceptance filters (applied in native code).
shutdown() Go bus-off, release the USB device, and clean up.
flush_tx_buffer() Discard queued TX frames by briefly restarting CAN, which stops periodic tasks and drops queued RX frames. Pending confirmed tickets must finish or be cancelled first.
state (property) Read or set the bus state (can.BusState.ACTIVE / PASSIVE / ERROR).

PSCAN-specific methods

Method Description
PscanBus.list_pscan_devices() Static. Returns List[str] of serial numbers for all connected PSCAN adapters.
get_device_info() Returns (hw_version: str, fw_version: str, channel_count: int) for the open adapter.
name (property) Read or set the saved device name (up to 15 ASCII letters, digits, underscores or hyphens).
led_mode (property) Read or set the physical LED mode (0=Off, 1=On, 2=ActiveOnly, 3=TxOff, 4=RxOff).
capabilities (property) Read a Python Dictionary containing structural hardware lengths and frequency mapping natively parsed by the backend extension.
get_raw_bus_state() Returns (state, rx_error_count, tx_error_count) without python-can's 3-way BusState collapse. state: 0=ErrorActive 1=ErrorWarning 2=ErrorPassive 3=BusOff 4=Stopped 5=Sleeping.
add_state_change_listener(cb) Register cb(new_state, raw) fired on every bus-state transition (starts the monitor thread).
remove_state_change_listener(cb) Unregister a previously added state-change callback.

Feature Compatibility

Supported python-can features

Feature Status Notes
can.Bus() instantiation ✅ Supported Via interface='pscan'
bus.send() ✅ Supported Standard (11-bit) and Extended (29-bit) CAN frames
bus.recv() ✅ Supported With hardware timestamps (microsecond resolution)
Remote Transmit Request (RTR) ✅ Supported Send and receive RTR frames
Error Frame detection (RX) ✅ Supported Error frames are flagged in received messages
bus.set_filters() ✅ Supported High-performance filtering in native code, not pure Python
bus.state property ✅ Supported Read and write; maps to Error Active / Warning / Passive / Bus-Off
Bus state change notifications ✅ Supported PSCAN extension: add_state_change_listener() callbacks, plus opt-in error frames via can.Notifier (state_notifications=True). python-can has no built-in state event.
bus.protocol property ✅ Supported Reports CanProtocol.CAN_20 (Classical CAN) on python-can ≥ 4.3.
bus.flush_tx_buffer() ✅ Supported Clears the adapter TX queue by briefly restarting CAN; see Flushing the transmit buffer
bus.shutdown() ✅ Supported Goes bus-off and releases the USB device
Context manager (with) ✅ Supported with can.Bus(...) as bus: auto-shuts down on exit
Iterator protocol ✅ Supported for msg in bus: works (inherited from BusABC)
can.Notifier ✅ Supported Background receive thread with listener callbacks
can.Logger / can.Printer ✅ Supported All standard python-can listeners work
can.detect_available_configs() ✅ Supported Returns list of connected PSCAN adapters with serial numbers
Listen-only mode ✅ Supported listen_only=True — no TX, no ACK on the bus
Loopback mode ✅ Supported loopback=True — TX frames echoed to RX
Periodic send (bus.send_periodic()) ✅ Supported Native workers for ordinary tasks; Python workers for per-transmission modifier callbacks. Stop interrupts the period wait.
Multi-threading ✅ Supported Safe for use with can.Notifier, background workers, and concurrent send/recv across Python threads.

Currently unsupported python-can features

The following standard python-can features are not yet implemented in this version. Attempting to use them will either raise an exception or fall back to default BusABC behavior.

Feature Status Details
CAN FD ❌ Blocked This transport supports Classic CAN only. FD options/frames raise NotImplementedError; constructor bitrates outside 1–1,000,000 bit/s raise CanInitializationError before opening USB.
Multi-channel support ❌ Not supported The managed transport requires single-channel devices and rejects other channel counts. Use separate bus instances for separate physical adapters.
Data bitrate / bitrate switch (BRS) ❌ Blocked data_bitrate and bitrate_switch=True raise NotImplementedError before the device opens.
timing= (can.BitTiming) ❌ Not supported Raises NotImplementedError, including timing python-can builds from f_clock/brp/tseg config keys. Use bitrate and sample_point; get_bittiming_limits() and get_bittiming() read the hardware timing.
receive_own_messages=True ❌ Not supported Raises NotImplementedError. loopback=True enables controller loopback, but echoed frames are not marked is_rx=False.
Bus load statistics ⚠️ Partial get_statistics() returns host frame/byte counters and error-frame bus warning/bus-off counts; get_raw_bus_state() returns live error counters. Bus load and rates must be computed in your application.
Hardware-level message filtering ⚠️ Software only Filters set via bus.set_filters() are applied in the native extension (software filtering) before frames reach Python. They are not pushed down to the CAN controller hardware. All frames are still received via USB and filtered in software on the host.

Receive buffering uses the main library’s bounded 20,000-frame queue. Slow consumers can still overflow it; inspect bus.dropped_frame_count. Physical throughput and loss depend on firmware, USB, host scheduling and application processing, and require hardware measurement.


License

Proprietary software (License: Proprietary in the package metadata).

Standalone development

This repository owns the Python package and its PyO3 extension. Version 0.2.1 is independent of the core library version. Rust 1.99 or newer is required. pscan-core and pscan-usb 1.4.0 are pinned to commit eb2e45dd6ba47b92a60f8f509feba69520847497 on Linux-SockectCAN in the private core repository; configure your normal GitLab credentials before building. No sibling checkout is required.

python3 -m venv .venv
. .venv/bin/activate  # Windows: .venv\Scripts\Activate.ps1
python -m pip install maturin pytest
cargo test --locked --lib
maturin develop --locked --release
python -m pytest tests/unit
maturin build --locked --release --out dist

Default pytest collection only runs hardware-free tests. examples/ contains hardware demos (including the former python-test scripts). Scripts under benchmarks/python/ require a dedicated, explicitly reserved physical CAN rig; see its README_TESTING.md. They are never run by this repository's CI.

The Linux CI matrix tests python-can 4.0.0 and 4.6.1. Each job runs native tests with in-memory device/session doubles and strict Clippy, then builds and installs a wheel and runs unit tests outside the source directory. JUnit results remain available on failures. Coverage includes managed TX/RX lifecycle, confirmation, legacy feature handling, shutdown, periodic tasks, notifications, capture formats, the entry point, and mocked benchmark/example lifecycles. Private dependency checkout uses a job-local Git configuration and credential helper; the core project must allow this project's CI_JOB_TOKEN. Tokens are never embedded in dependency URLs or persisted in Git configuration.

Releasing

Each release publishes the same compiled wheels under two PyPI names: python-can-pscan, and the legacy pscan-pythoncan, whose description opens with a notice to switch (see packaging/legacy-notice.md). The legacy wheels are repackaged from the new ones by scripts/make_legacy_wheels.py; only the distribution metadata differs.

Automated (GitLab CI, self-hosted runners)

Release wheels build on two machines registered as GitLab project runners:

Runner tag Machine Jobs
pscan-macos this Mac, shell executor macOS x86_64 + arm64 wheels; tests the native one
pscan-docker same Mac, Docker Desktop Linux x86_64, i686, aarch64 wheels; runs each on its own architecture (amd64 and 386 emulated); publishes
pscan_windows Windows PC, probesync/host-tools group runner (pwsh) win32, win_amd64, win_arm64 wheels (native MSVC); tests the x64 one

Merge request tests keep running on GitLab's shared runners.

One-time setup:

  1. In GitLab, Settings → CI/CD → Runners → New project runner: create the two Mac runners, with Protected ticked and Run untagged jobs off. Each shows a glrt-… token once. Protected runners only take jobs from protected branches and tags, so other branches cannot run code on these machines.
  2. On the Mac (Docker Desktop running): MACOS_RUNNER_TOKEN=glrt-… DOCKER_RUNNER_TOKEN=glrt-… scripts/runners/setup-macos-runner.sh
  3. Windows builds use the existing probesync/host-tools group runner tagged pscan_windows. It needs rustup, Python, Git for Windows and Visual Studio with the x64/x86 and ARM64 MSVC build tools. To register a different Windows machine instead, use scripts/runners/setup-windows-runner.ps1.
  4. Add the PyPI token under Settings → CI/CD → Variables as PYPI_API_TOKEN, masked and protected, with environment scope pypi so only the publish-pypi job receives it. The first upload creates the python-can-pscan project, so that token must be account-scoped; replace it with a project-scoped token afterwards. PYPI_LEGACY_API_TOKEN (optional) is used for pscan-pythoncan and defaults to PYPI_API_TOKEN. Do not put the token in a runner's environment, where every job on that runner can read it.
  5. Protect v* tags under Settings → Repository → Protected tags, so protected variables reach tag pipelines.

To release, bump version in pyproject.toml and Cargo.toml, update tests/unit/test_wheel.py, run cargo update --offline -p pscan-pythoncan, commit, then push a matching tag: git tag v0.2.1 && git push origin v0.2.1.

The tag pipeline runs the tests, builds all eight wheels, runs them, and only then runs publish-pypi. That job checks that the tag matches the version, that every platform wheel is present and that twine check --strict passes, then uploads both names. Re-running it skips files already uploaded. All three runners must be online; jobs wait for an offline runner. A manually started pipeline (Build → Pipelines → Run pipeline) on a protected branch such as main runs the same builds plus release-check, a dry run that uploads nothing.

Manual

  • scripts/build_wheels.sh [all|macos|linux|windows|TARGET...]: on macOS it builds all eight wheels (Linux through zig, Windows through maturin's xwin); on Linux it builds Linux and Windows. It needs rustup, maturin>=1.5 and zig (pip install ziglang).
  • scripts/build_linux_docker.sh [--out DIR] [--no-test]: builds the three Linux wheels inside Docker on a Mac, then runs each wheel's unit tests in a container of its architecture. Dependencies are fetched on the host, so no token enters the container.
  • scripts/build_wheels.ps1: native MSVC build of the three Windows wheels on Windows (Visual Studio Build Tools with x64/x86 and ARM64 C++ components).
  • scripts/publish.sh [--repository testpypi] [--allow-partial] [--dry-run] [DIST_DIR]: validates the wheels in dist/ and uploads both names. It reads tokens only from PYPI_API_TOKEN and PYPI_LEGACY_API_TOKEN. Use --repository testpypi with TestPyPI tokens to rehearse a release.

PyPI never accepts the same file twice, even after deletion, so a broken release needs a new version number.

Source history was extracted from crates/pscan-pythoncan in probesync/host-tools/lib at d5a772e1b49b41f31f134fd87a4da78d60f8f218. Hardware scripts and demos were copied from that same source revision.

Metadata

Release files for python-can-pscan 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for python-can-pscan 0.2.1
File
python_can_pscan-0.2.1-cp38-abi3-win_arm64.whl CPython 3.8 abi3 Windows ARM64 Details
python_can_pscan-0.2.1-cp38-abi3-win_amd64.whl CPython 3.8 abi3 Windows x86-64 Details
python_can_pscan-0.2.1-cp38-abi3-win32.whl CPython 3.8 abi3 Windows x86-32 Details
python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.8 abi3 Linux glibc 2.17+ x86-64 Details
python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl CPython 3.8 abi3 Linux glibc 2.17+ x86-32 Details
python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.8 abi3 Linux glibc 2.17+ ARM64 Details
python_can_pscan-0.2.1-cp38-abi3-macosx_11_0_arm64.whl CPython 3.8 abi3 macOS 11.0+ ARM64 Details
python_can_pscan-0.2.1-cp38-abi3-macosx_10_12_x86_64.whl CPython 3.8 abi3 macOS 10.12+ x86-64 Details

Total release size: 3.6 MB

Release files / python_can_pscan-0.2.1-cp38-abi3-win_arm64.whl

Download URL python_can_pscan-0.2.1-cp38-abi3-win_arm64.whl
Size 404.9 kB
Tags CPython 3.8 Windows ARM64 abi3
SHA-256 checksum
How to use checksums
1841b3fdbacbca27c949e41cc3f8469116967f15f1da77117d23537ef8b144f6
BLAKE2b-256 checksum
How to use checksums
c8c48433692be4ed5a95fa0abbdf6512a034d9dbd0cb839a44dca91bf0220c29
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / python_can_pscan-0.2.1-cp38-abi3-win_amd64.whl

Download URL python_can_pscan-0.2.1-cp38-abi3-win_amd64.whl
Size 422.1 kB
Tags CPython 3.8 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
ff41a5bca0eec21706777b517e78d1256121a919655c504d27b876dab3b3917a
BLAKE2b-256 checksum
How to use checksums
47980ea53ea2182114eca2dae597ffa0c1eb86eb51ac2418287d80427b8d1412
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / python_can_pscan-0.2.1-cp38-abi3-win32.whl

Download URL python_can_pscan-0.2.1-cp38-abi3-win32.whl
Size 414.8 kB
Tags CPython 3.8 Windows x86-32 abi3
SHA-256 checksum
How to use checksums
5556767af5918ae2ca755e3329f09f60725a74f23408a7cccb383890c83c66ff
BLAKE2b-256 checksum
How to use checksums
5445f873a65aa7b7ea529898c6cafd8f208479a920c520bf3ec3335a1a185eae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 483.1 kB
Tags CPython 3.8 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
8ee3c24c31307ef0231342f383b8fcbac212d55a23f010878205b5ff575261cf
BLAKE2b-256 checksum
How to use checksums
9ddcb94d13f60daab1d12c70247cde8d15f740cffce94a9f00c291684588620a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl

Download URL python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl
Size 532.2 kB
Tags CPython 3.8 Linux glibc 2.17+ x86-32 abi3
SHA-256 checksum
How to use checksums
5e6dc8344d775c692f720ba0db22d2e2162975b4fd3d2bf691c8a2fff1b25ba7
BLAKE2b-256 checksum
How to use checksums
fd2cc79343a3b2764e0cad18299b6202f176014b56e366b392e92759810c51c3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL python_can_pscan-0.2.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 471.4 kB
Tags CPython 3.8 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
518b66ce6e3ee81df06fbd1b8f8d5aa4cd44d7183c3a8d25ac58ce678124a8e0
BLAKE2b-256 checksum
How to use checksums
3da5f1abbe82a7aff548c64207fd705981d4281f733d70cec9a1ab147baeaedd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / python_can_pscan-0.2.1-cp38-abi3-macosx_11_0_arm64.whl

Download URL python_can_pscan-0.2.1-cp38-abi3-macosx_11_0_arm64.whl
Size 427.4 kB
Tags CPython 3.8 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
4e246763387a2241b8cbe7da7f2d11281933ac69bfd8e7436c7348067721dd22
BLAKE2b-256 checksum
How to use checksums
2e61ea01d73efd27d80926627c5fcf85266ecc88e869fc72ec8e53b525dd8a36
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / python_can_pscan-0.2.1-cp38-abi3-macosx_10_12_x86_64.whl

Download URL python_can_pscan-0.2.1-cp38-abi3-macosx_10_12_x86_64.whl
Size 448.4 kB
Tags CPython 3.8 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
65f3d96ec973c40ff4bb2bf80c33f3c04200b1aac82bf385c102a15ada2c7c70
BLAKE2b-256 checksum
How to use checksums
d173cacf0f7400f38684c973e833a5cd1b52fbb3545dfb294c2a11900d30ee51
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release history Release notifications | RSS feed

This release

0.2.1 This release

8 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page