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.
@@ -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
+ Metadata-Version: 2.4
2
+ Name: pscan-pythoncan
3
+ Version: 0.1.0
4
+ Requires-Dist: python-can>=4.0.0
5
+ Summary: Native Python-CAN hardware backend for PSCAN USB devices
6
+ Author-email: PSCAN <info@pscan.com>
7
+ Requires-Python: >=3.8
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.12.6)
3
+ Root-Is-Purelib: false
4
+ Tag: cp38-abi3-win_amd64
@@ -0,0 +1,2 @@
1
+ [can.interface]
2
+ pscan=pscan_pythoncan:PscanBus