pscan-pythoncan 0.1.0__cp38-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pscan_pythoncan/__init__.py +235 -0
- pscan_pythoncan/pscan_pythoncan_ext.pyd +0 -0
- pscan_pythoncan-0.1.0.dist-info/METADATA +7 -0
- pscan_pythoncan-0.1.0.dist-info/RECORD +7 -0
- pscan_pythoncan-0.1.0.dist-info/WHEEL +4 -0
- pscan_pythoncan-0.1.0.dist-info/entry_points.txt +2 -0
- pscan_pythoncan-0.1.0.dist-info/sboms/pscan-pythoncan.cyclonedx.json +1304 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import can
|
|
4
|
+
from typing import Optional, List, Tuple, Sequence
|
|
5
|
+
|
|
6
|
+
# Import the Rust extension
|
|
7
|
+
from . import pscan_pythoncan_ext
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger("can.pscan")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PscanBus(can.BusABC):
|
|
13
|
+
"""
|
|
14
|
+
PSCAN Hardware interface for python-can.
|
|
15
|
+
|
|
16
|
+
Provides native USB access to PSCAN CAN adapters via a Rust backend.
|
|
17
|
+
|
|
18
|
+
Usage::
|
|
19
|
+
|
|
20
|
+
# Auto-detect single device
|
|
21
|
+
bus = can.Bus(interface='pscan', bitrate=500000)
|
|
22
|
+
|
|
23
|
+
# Open by user-friendly name
|
|
24
|
+
bus = can.Bus(interface='pscan', name='MyBMS', bitrate=500000)
|
|
25
|
+
|
|
26
|
+
# Open by serial number
|
|
27
|
+
bus = can.Bus(interface='pscan', serial='P1P.IN:XFG:H001', bitrate=500000)
|
|
28
|
+
|
|
29
|
+
# With options
|
|
30
|
+
bus = can.Bus(interface='pscan', bitrate=250000, sample_point=800, listen_only=True)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
channel: str = 'PSCAN_USB1',
|
|
36
|
+
serial: str = None,
|
|
37
|
+
name: str = None,
|
|
38
|
+
bitrate: int = 500000,
|
|
39
|
+
sample_point: int = 875,
|
|
40
|
+
listen_only: bool = False,
|
|
41
|
+
loopback: bool = False,
|
|
42
|
+
can_filters=None,
|
|
43
|
+
**kwargs
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
:param channel: Channel name (used for python-can compatibility)
|
|
47
|
+
:param serial: Serial number of the PSCAN device
|
|
48
|
+
:param name: User-friendly device name (alternative to serial)
|
|
49
|
+
:param bitrate: Bitrate in bps (default: 500000)
|
|
50
|
+
:param sample_point: Sample point in permille (default: 875 = 87.5%)
|
|
51
|
+
:param listen_only: Enable listen-only mode (no TX, no ACK)
|
|
52
|
+
:param loopback: Enable loopback mode (TX frames are echoed to RX)
|
|
53
|
+
:param can_filters: Message filters, see :meth:`~can.BusABC.set_filters`
|
|
54
|
+
"""
|
|
55
|
+
# Priority: name > serial > auto-detect first device
|
|
56
|
+
if name:
|
|
57
|
+
self._dev = pscan_pythoncan_ext.NativePscanDevice.open_by_name(name)
|
|
58
|
+
self.channel_info = f"PSCAN: name={name}"
|
|
59
|
+
else:
|
|
60
|
+
self._dev = pscan_pythoncan_ext.NativePscanDevice(serial)
|
|
61
|
+
self.channel_info = f"PSCAN: {serial or 'Auto'}"
|
|
62
|
+
|
|
63
|
+
# Set CAN controller mode before starting bus
|
|
64
|
+
ctrlmode = 0
|
|
65
|
+
if listen_only:
|
|
66
|
+
ctrlmode |= 0x02 # CAN_CTRLMODE_LISTENONLY
|
|
67
|
+
if loopback:
|
|
68
|
+
ctrlmode |= 0x01 # CAN_CTRLMODE_LOOPBACK
|
|
69
|
+
if ctrlmode != 0:
|
|
70
|
+
self._dev.set_mode(ctrlmode)
|
|
71
|
+
|
|
72
|
+
self._dev.set_bitrate(bitrate, sample_point)
|
|
73
|
+
self._dev.set_bus_state(True)
|
|
74
|
+
self._listen_only = listen_only
|
|
75
|
+
|
|
76
|
+
# Call super().__init__ last — it sets _is_shutdown=False and applies filters
|
|
77
|
+
super().__init__(channel=channel, can_filters=can_filters, **kwargs)
|
|
78
|
+
|
|
79
|
+
def send(self, msg: can.Message, timeout: Optional[float] = None) -> None:
|
|
80
|
+
"""Transmit a CAN message.
|
|
81
|
+
|
|
82
|
+
:param msg: Message to send
|
|
83
|
+
:param timeout: Timeout in seconds (default: 0.05s = 50ms)
|
|
84
|
+
:raises can.CanOperationError: If send fails
|
|
85
|
+
:raises NotImplementedError: If CAN FD message is passed
|
|
86
|
+
"""
|
|
87
|
+
if self._listen_only:
|
|
88
|
+
raise can.CanOperationError("Cannot send in listen-only mode")
|
|
89
|
+
|
|
90
|
+
if msg.is_fd:
|
|
91
|
+
raise NotImplementedError("CAN FD not yet supported by PSCAN-USB")
|
|
92
|
+
|
|
93
|
+
# Convert timeout: None -> 50ms default, else seconds -> ms
|
|
94
|
+
if timeout is None:
|
|
95
|
+
timeout_ms = 50
|
|
96
|
+
else:
|
|
97
|
+
timeout_ms = max(1, int(timeout * 1000))
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
self._dev.send_frame(
|
|
101
|
+
msg.arbitration_id,
|
|
102
|
+
msg.is_extended_id,
|
|
103
|
+
msg.is_remote_frame,
|
|
104
|
+
msg.is_error_frame,
|
|
105
|
+
list(msg.data),
|
|
106
|
+
timeout_ms
|
|
107
|
+
)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
raise can.CanOperationError(f"Send failed: {e}") from e
|
|
110
|
+
|
|
111
|
+
def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[can.Message], bool]:
|
|
112
|
+
"""
|
|
113
|
+
Read a message from the hardware.
|
|
114
|
+
|
|
115
|
+
Returns (message, already_filtered) tuple.
|
|
116
|
+
Handles multi-frame USB reads via Rust-side buffering.
|
|
117
|
+
"""
|
|
118
|
+
# Calculate timeout in ms
|
|
119
|
+
timeout_ms = 0
|
|
120
|
+
if timeout is not None:
|
|
121
|
+
if timeout < 0:
|
|
122
|
+
timeout = 0
|
|
123
|
+
timeout_ms = int(timeout * 1000)
|
|
124
|
+
if timeout_ms == 0 and timeout > 0:
|
|
125
|
+
timeout_ms = 1 # Minimum 1ms
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
res = self._dev.receive_frame(timeout_ms)
|
|
129
|
+
if res is None:
|
|
130
|
+
return None, False
|
|
131
|
+
|
|
132
|
+
ts_us, arb_id, is_ext, is_rtr, is_err, dlc, data = res
|
|
133
|
+
|
|
134
|
+
msg = can.Message(
|
|
135
|
+
timestamp=ts_us / 1_000_000.0,
|
|
136
|
+
arbitration_id=arb_id,
|
|
137
|
+
is_extended_id=is_ext,
|
|
138
|
+
is_remote_frame=is_rtr,
|
|
139
|
+
is_error_frame=is_err,
|
|
140
|
+
dlc=dlc,
|
|
141
|
+
data=data,
|
|
142
|
+
channel=self.channel_info,
|
|
143
|
+
)
|
|
144
|
+
return msg, True # True = filtering already done in Rust
|
|
145
|
+
except Exception as e:
|
|
146
|
+
log.error("Error receiving CAN frame: %s", e)
|
|
147
|
+
raise can.CanOperationError(f"Receive failed: {e}") from e
|
|
148
|
+
|
|
149
|
+
def _apply_filters(self, filters) -> None:
|
|
150
|
+
"""Push filters to the Rust backend for high-performance filtering.
|
|
151
|
+
|
|
152
|
+
Filters are applied in Rust before frames reach Python,
|
|
153
|
+
using pscan-core's SoftFilter/SoftFilterSet implementation.
|
|
154
|
+
|
|
155
|
+
:param filters: List of filter dicts with 'can_id', 'can_mask', and optional 'extended' keys.
|
|
156
|
+
"""
|
|
157
|
+
if not filters:
|
|
158
|
+
self._dev.clear_filters()
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
rust_filters = []
|
|
162
|
+
for f in filters:
|
|
163
|
+
can_id = f.get('can_id', 0)
|
|
164
|
+
can_mask = f.get('can_mask', 0)
|
|
165
|
+
extended = f.get('extended', None) # None = match both std and ext
|
|
166
|
+
rust_filters.append((can_id, can_mask, extended))
|
|
167
|
+
|
|
168
|
+
self._dev.set_filters(rust_filters)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def state(self) -> can.BusState:
|
|
172
|
+
"""Return the current state of the CAN bus hardware.
|
|
173
|
+
|
|
174
|
+
Mapping: ErrorActive -> ACTIVE, ErrorWarning/Passive -> PASSIVE, BusOff -> ERROR
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
state_val, _, _ = self._dev.get_bus_state()
|
|
178
|
+
if state_val == 0: # ErrorActive
|
|
179
|
+
return can.BusState.ACTIVE
|
|
180
|
+
elif state_val in (1, 2): # ErrorWarning, ErrorPassive
|
|
181
|
+
return can.BusState.PASSIVE
|
|
182
|
+
else: # BusOff(3), Stopped(4), Sleeping(5)
|
|
183
|
+
return can.BusState.ERROR
|
|
184
|
+
except Exception:
|
|
185
|
+
return can.BusState.ERROR
|
|
186
|
+
|
|
187
|
+
@state.setter
|
|
188
|
+
def state(self, new_state: can.BusState) -> None:
|
|
189
|
+
"""Set the bus state.
|
|
190
|
+
|
|
191
|
+
:param new_state: ACTIVE to go bus-on, ERROR to go bus-off
|
|
192
|
+
"""
|
|
193
|
+
if new_state == can.BusState.ACTIVE:
|
|
194
|
+
self._dev.set_bus_state(True)
|
|
195
|
+
elif new_state == can.BusState.ERROR:
|
|
196
|
+
self._dev.set_bus_state(False)
|
|
197
|
+
else:
|
|
198
|
+
# PASSIVE — set listen-only mode
|
|
199
|
+
self._dev.set_mode(0x02)
|
|
200
|
+
|
|
201
|
+
def flush_tx_buffer(self) -> None:
|
|
202
|
+
"""Discard all pending messages in the TX queue."""
|
|
203
|
+
self._dev.flush_tx_queue()
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _detect_available_configs() -> List[dict]:
|
|
207
|
+
"""Detect all connected PSCAN devices.
|
|
208
|
+
|
|
209
|
+
:return: List of configs suitable for PscanBus constructor.
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
serials = pscan_pythoncan_ext.list_devices()
|
|
213
|
+
return [
|
|
214
|
+
{"interface": "pscan", "channel": "PSCAN_USB1", "serial": sn}
|
|
215
|
+
for sn in serials
|
|
216
|
+
]
|
|
217
|
+
except Exception:
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
def shutdown(self) -> None:
|
|
221
|
+
"""Shut down the bus: stop bus, release device, then clean up base class."""
|
|
222
|
+
if self._is_shutdown:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# Device cleanup FIRST, before super().shutdown() sets the guard flag
|
|
226
|
+
if self._dev is not None:
|
|
227
|
+
try:
|
|
228
|
+
time.sleep(0.05)
|
|
229
|
+
self._dev.set_bus_state(False)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
log.warning("Error during bus shutdown: %s", e)
|
|
232
|
+
finally:
|
|
233
|
+
self._dev = None
|
|
234
|
+
|
|
235
|
+
super().shutdown()
|
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pscan_pythoncan/__init__.py,sha256=9xAm3Uxgdm8q_Nf5Meo8wCZfQYr7QwH3keM7drmRKdw,8365
|
|
2
|
+
pscan_pythoncan/pscan_pythoncan_ext.pyd,sha256=uqublQoFUKoQ6jVuyPACX8IwPwocUN_lXe-OyT0z34I,540672
|
|
3
|
+
pscan_pythoncan-0.1.0.dist-info/METADATA,sha256=SAX94aMfCNbFxRM8KaB6hhsJLs7D6sgkMjuMwv3c_OQ,218
|
|
4
|
+
pscan_pythoncan-0.1.0.dist-info/WHEEL,sha256=pR3QVM8fbZ6NHAmOlF_iyjo3X1LezoW7fOuSwC4jKRw,95
|
|
5
|
+
pscan_pythoncan-0.1.0.dist-info/entry_points.txt,sha256=QBXFh4buwsUQEy-p6XVEgujEqrKFnaZSPZ3CtRiOnhg,47
|
|
6
|
+
pscan_pythoncan-0.1.0.dist-info/sboms/pscan-pythoncan.cyclonedx.json,sha256=EZ6xLzzudZ7G1DtxpvFN8kx_osmUnXz2mxF81-6amuI,40459
|
|
7
|
+
pscan_pythoncan-0.1.0.dist-info/RECORD,,
|