Native Python-CAN hardware backend for PSCAN USB devices
Project description
pscan-pythoncan
A native python-can hardware interface for PSCAN USB CAN adapters.
pscan-pythoncan 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 extra drivers or DLLs required. 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's EEPROM.
- Auto-detection — automatically finds and connects to the first available PSCAN adapter.
- 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).
Installation
pip install pscan-pythoncan
python-can >= 4.0.0 is installed automatically as a dependency.
Supported platforms: Windows (x86, x64, aarch64). Python 3.8 and above.
Quick Start
import can
# Open the first available 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 in EEPROM. This avoids the ambiguity of generic channel numbers or COM ports — you always connect to exactly the hardware you intend.
By serial number (recommended)
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 only one PSCAN adapter is connected you can omit serial and name:
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)
can_filters=None, # Message filters (see Filtering section)
)
| 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's EEPROM (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. |
led_mode |
int |
1 |
Controls the hardware LED visual behavior natively. 0=Off, 1=On (Default), 2=ActiveOnly. |
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. |
Device selection priority: name > serial > auto-detect first device.
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:
- Default send timeout is 50 ms when no
timeoutargument is provided. - Sending in listen-only mode raises
can.CanOperationError. - CAN FD frames are not supported and will raise
NotImplementedError.
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
Flushing the transmit buffer
Discard all pending outgoing messages:
bus.flush_tx_buffer()
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 pscan_pythoncan 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
pscan-pythoncan exposes advanced parameters natively out of the classic CAN hardware over Python properties. Note that bus.name and integers update the physical EEPROM arrays seamlessly!
# 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 Elements: {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 pscan_pythoncan 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, serial, name, bitrate, sample_point, listen_only, loopback, can_filters)
Standard python-can methods
| Method | Description |
|---|---|
send(msg, timeout=None) |
Transmit a can.Message. Default timeout: 50 ms. |
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 all pending messages in the transmit queue. |
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 15-character physical name tag stored on the device ROM. |
led_mode (property) |
Read or set the physical LED mode (0=Off, 1=On, 2=ActiveOnly). |
capabilities (property) |
Read a Python Dictionary containing structural hardware lengths and frequency mapping natively parsed by the backend extension. |
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.flush_tx_buffer() |
✅ Supported | Clears the hardware transmit queue |
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 | High-precision timing via dedicated OS threads. Bypasses Python's GIL for ~1μs jitter. |
| 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 | The PSCAN-USB hardware currently supports Classical CAN only. bus strictly aborts fd=True/bitrates > 1Mbps/msg.is_fd directly returning NotImplementedError via explicit Python exceptions saving devs from silent execution. |
| Multi-channel support | ❌ Not supported | All operations are hardcoded to CAN channel 0. If your PSCAN adapter has multiple CAN channels, only the first channel is accessible. To use a second channel, open a separate can.Bus() instance with a different adapter. |
| Data bitrate / bitrate switch (BRS) | ❌ Blocked | CAN FD bitrate switching triggers native safety protocols enforcing Classic architecture bounds. Explicitly throws NotImplementedError("CAN FD and Bitrate Switching (BRS) are not yet supported"). |
| Hardware-level bus statistics | ❌ Not available | There is no get_bus_statistics() method. You can read the bus state and error counters via bus.state and the raw bus._dev.get_bus_state() call (which returns state, rx_error_count, tx_error_count), but aggregate statistics like bus load, messages-per-second, or frame counts must be implemented in your application using a can.Listener. |
| 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. |
Note: Software filtering runs natively and easily handles 100% bus loads without dropping frames, completely transparent to your Python application.
License
See the project repository for license details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pscan_pythoncan-0.1.8-cp314-cp314t-win_arm64.whl.
File metadata
- Download URL: pscan_pythoncan-0.1.8-cp314-cp314t-win_arm64.whl
- Upload date:
- Size: 298.7 kB
- Tags: CPython 3.14t, Windows ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f0797c3fc133726a6e5ad46c480ad509671c39b1346a9a4002f2311d4b5b8b25
|
|
| MD5 |
7f0af38ce7e74190a1563a7acf7571e0
|
|
| BLAKE2b-256 |
a4b03833a6dc4fb9f9aa06e10950d8e75c515e9a157e8e06c4cb77ef5dcee06a
|
File details
Details for the file pscan_pythoncan-0.1.8-cp313-cp313t-win_arm64.whl.
File metadata
- Download URL: pscan_pythoncan-0.1.8-cp313-cp313t-win_arm64.whl
- Upload date:
- Size: 300.7 kB
- Tags: CPython 3.13t, Windows ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b6dd88f5faeef60416f6604f06fc68ed4a4c5c26951528f02fd7c7a47ab7b95
|
|
| MD5 |
1662fe3f5ce437b6ab3d7ffec034bf57
|
|
| BLAKE2b-256 |
419c5b496954c3c37a4dee1f8f9a7a4fa4fd973a0a24e3183e4ff4792c156294
|
File details
Details for the file pscan_pythoncan-0.1.8-cp38-abi3-win_arm64.whl.
File metadata
- Download URL: pscan_pythoncan-0.1.8-cp38-abi3-win_arm64.whl
- Upload date:
- Size: 303.8 kB
- Tags: CPython 3.8+, Windows ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a639d42745ad2de46702ddb151d27878c0ef43034293a01e732a5388674c2f5
|
|
| MD5 |
078797510caab76603da98cf31dc80e0
|
|
| BLAKE2b-256 |
c9c3eed04d71d4708fcec3c509f9ee42bd57be026d02193444a422b9e212fbbb
|
File details
Details for the file pscan_pythoncan-0.1.8-cp38-abi3-win_amd64.whl.
File metadata
- Download URL: pscan_pythoncan-0.1.8-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 323.9 kB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f2e6e5483005402a71e8993a374d355ca6c9282becf05c29ec3708b880ac1f0a
|
|
| MD5 |
2f4710e82ee70b161287e9e78017522a
|
|
| BLAKE2b-256 |
65d4b99dcc36b050bae7208f187cf7fa1fc2af307d6e75839a8d2ac85427d85e
|
File details
Details for the file pscan_pythoncan-0.1.8-cp38-abi3-win32.whl.
File metadata
- Download URL: pscan_pythoncan-0.1.8-cp38-abi3-win32.whl
- Upload date:
- Size: 300.3 kB
- Tags: CPython 3.8+, Windows x86
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ec7ea99a5f842aca23d6368db92b9fae4165068c367f419c3ae1ecf689bee75
|
|
| MD5 |
486a88fd4d9043a44b302b9799438eb0
|
|
| BLAKE2b-256 |
535e79d02042e00d25ec5d859c0bd7b072f065b3be3fc9fbe845d4ce46674b0d
|
File details
Details for the file pscan_pythoncan-0.1.8-cp38-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: pscan_pythoncan-0.1.8-cp38-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 281.1 kB
- Tags: CPython 3.8+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ba2535a3f85760cbb91bad0e21500e14c54de1229ed4660fe6c7fa5e429b5b5
|
|
| MD5 |
355cb6f42450264d2ea7d271fc25fe1b
|
|
| BLAKE2b-256 |
4e4269e4956ae9bce76c7b205d92bff1b29895e5a834dffd625597715a5d320e
|
File details
Details for the file pscan_pythoncan-0.1.8-cp38-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: pscan_pythoncan-0.1.8-cp38-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 300.5 kB
- Tags: CPython 3.8+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05e4f3dca83a8a4df48170a4ebeada725e1b6a474aaba2b7b4a637ee401861c2
|
|
| MD5 |
834b70fb2bac5bb79c16c7e0e2d5e58e
|
|
| BLAKE2b-256 |
1a7e07e4cee26b3adbc5e2db41512fc3700e8d77207db658ee0c58dced3aa652
|