python-can-j1939 0.1.0__py3-none-any.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.
- j1939/Dm14Query.py +331 -0
- j1939/Dm14Server.py +399 -0
- j1939/__init__.py +11 -0
- j1939/controller_application.py +363 -0
- j1939/diagnostic_messages.py +448 -0
- j1939/electronic_control_unit.py +499 -0
- j1939/error_info.py +93 -0
- j1939/j1939_21.py +543 -0
- j1939/j1939_22.py +845 -0
- j1939/memory_access.py +387 -0
- j1939/message_id.py +51 -0
- j1939/name.py +268 -0
- j1939/parameter_group_number.py +136 -0
- j1939/version.py +1 -0
- python_can_j1939-0.1.0.dist-info/METADATA +325 -0
- python_can_j1939-0.1.0.dist-info/RECORD +19 -0
- python_can_j1939-0.1.0.dist-info/WHEEL +5 -0
- python_can_j1939-0.1.0.dist-info/licenses/LICENSE +22 -0
- python_can_j1939-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import heapq
|
|
4
|
+
import logging
|
|
5
|
+
import can
|
|
6
|
+
from can import Listener
|
|
7
|
+
import time
|
|
8
|
+
import threading
|
|
9
|
+
import queue
|
|
10
|
+
from .controller_application import ControllerApplication
|
|
11
|
+
from .parameter_group_number import ParameterGroupNumber
|
|
12
|
+
from .j1939_21 import J1939_21
|
|
13
|
+
from .j1939_22 import J1939_22
|
|
14
|
+
from .message_id import FrameFormat
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class ElectronicControlUnit:
|
|
19
|
+
"""ElectronicControlUnit (ECU) holding one or more ControllerApplications (CAs)."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def __init__(self, data_link_layer='j1939-21', max_cmdt_packets=1, minimum_tp_rts_cts_dt_interval=None, minimum_tp_bam_dt_interval=None, send_message=None):
|
|
23
|
+
"""
|
|
24
|
+
:param data_link_layer:
|
|
25
|
+
specify data-link-layer, 'j1939-21' or 'j1939-22'
|
|
26
|
+
"""
|
|
27
|
+
if send_message:
|
|
28
|
+
self.send_message = send_message
|
|
29
|
+
|
|
30
|
+
#: A python-can :class:`can.BusABC` instance
|
|
31
|
+
self._bus = None
|
|
32
|
+
# Locking object for send
|
|
33
|
+
self._send_lock = threading.Lock()
|
|
34
|
+
|
|
35
|
+
if max_cmdt_packets > 0xFF:
|
|
36
|
+
raise ValueError("max number of segments that can be sent is 0xFF")
|
|
37
|
+
|
|
38
|
+
# set data link layer
|
|
39
|
+
if data_link_layer == 'j1939-21':
|
|
40
|
+
self.j1939_dll = J1939_21(self.send_message, self._protocol_wakeup, self._notify_subscribers, max_cmdt_packets, minimum_tp_rts_cts_dt_interval, minimum_tp_bam_dt_interval, self._is_message_acceptable)
|
|
41
|
+
elif data_link_layer == 'j1939-22':
|
|
42
|
+
self.j1939_dll = J1939_22(self.send_message, self._protocol_wakeup, self._notify_subscribers, max_cmdt_packets, minimum_tp_rts_cts_dt_interval, minimum_tp_bam_dt_interval, self._is_message_acceptable)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError("either 'j1939-21' or 'j1939-22' must be provided for data link layer")
|
|
45
|
+
|
|
46
|
+
#: Includes at least MessageListener.
|
|
47
|
+
self._listeners = [MessageListener(self)]
|
|
48
|
+
self._notifier = None
|
|
49
|
+
|
|
50
|
+
self._subscribers = []
|
|
51
|
+
self._subscribers_lock = threading.RLock()
|
|
52
|
+
|
|
53
|
+
# Heap-based timer event list: (deadline, seq, callback, cookie, delta_time)
|
|
54
|
+
self._timer_events = []
|
|
55
|
+
self._timer_seq = 0
|
|
56
|
+
self._timer_events_lock = threading.RLock()
|
|
57
|
+
|
|
58
|
+
# Dependent lifecycle registry. Any object that needs to be stopped before the ECU's own threads should be registered here. See :meth:`register_dependent`.
|
|
59
|
+
self._dependents = []
|
|
60
|
+
self._dependents_lock = threading.RLock()
|
|
61
|
+
self._stopping = False
|
|
62
|
+
|
|
63
|
+
self._job_thread_end = threading.Event()
|
|
64
|
+
|
|
65
|
+
# Protocol thread: owns TP/BAM timeout management only — no user callbacks
|
|
66
|
+
logger.info("Starting ECU protocol thread")
|
|
67
|
+
self._protocol_wakeup_queue = queue.Queue()
|
|
68
|
+
self._protocol_thread = threading.Thread(
|
|
69
|
+
target=self._protocol_job_thread, name='j1939.ecu protocol_thread')
|
|
70
|
+
self._protocol_thread.daemon = True
|
|
71
|
+
|
|
72
|
+
# Timer thread: owns application cyclic callbacks only
|
|
73
|
+
logger.info("Starting ECU timer thread")
|
|
74
|
+
self._timer_wakeup_queue = queue.Queue()
|
|
75
|
+
self._timer_thread = threading.Thread(
|
|
76
|
+
target=self._timer_job_thread, name='j1939.ecu timer_thread')
|
|
77
|
+
self._timer_thread.daemon = True
|
|
78
|
+
|
|
79
|
+
self._protocol_thread.start()
|
|
80
|
+
self._timer_thread.start()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def stop(self):
|
|
84
|
+
"""Stops the ECU background handling
|
|
85
|
+
|
|
86
|
+
This Function explicitly stops the background handling of the ECU.
|
|
87
|
+
|
|
88
|
+
Before stopping the ECU's own protocol/timer threads, every registered
|
|
89
|
+
dependent (see :meth:`register_dependent`) has its ``stop()`` method
|
|
90
|
+
invoked in LIFO order. Exceptions raised by a dependent's ``stop()``
|
|
91
|
+
are logged and swallowed so a single misbehaving dependent cannot
|
|
92
|
+
prevent the rest of the shutdown from completing.
|
|
93
|
+
"""
|
|
94
|
+
# Snapshot dependents under lock, then mark the ECU as stopping so any
|
|
95
|
+
# late registrations are rejected.
|
|
96
|
+
with self._dependents_lock:
|
|
97
|
+
self._stopping = True
|
|
98
|
+
dependents = list(self._dependents)
|
|
99
|
+
self._dependents.clear()
|
|
100
|
+
|
|
101
|
+
# LIFO: most-recently registered first.
|
|
102
|
+
for dep in reversed(dependents):
|
|
103
|
+
try:
|
|
104
|
+
dep.stop()
|
|
105
|
+
except Exception:
|
|
106
|
+
logger.exception("Error stopping dependent %r", dep)
|
|
107
|
+
|
|
108
|
+
self._job_thread_end.set()
|
|
109
|
+
self._protocol_wakeup_queue.put(1)
|
|
110
|
+
self._timer_wakeup_queue.put(1)
|
|
111
|
+
self._protocol_thread.join()
|
|
112
|
+
self._timer_thread.join()
|
|
113
|
+
|
|
114
|
+
def register_dependent(self, dependent):
|
|
115
|
+
"""Register a helper whose ``stop()`` should be called by :meth:`stop`.
|
|
116
|
+
|
|
117
|
+
Any helper object that owns threads, timers, or other resources tied
|
|
118
|
+
to this ECU should call this during construction. ``ecu.stop()`` will
|
|
119
|
+
invoke ``dependent.stop()`` in LIFO order before tearing down its own
|
|
120
|
+
threads.
|
|
121
|
+
|
|
122
|
+
Duplicate registrations of the same object (by identity) are silently
|
|
123
|
+
ignored.
|
|
124
|
+
|
|
125
|
+
:param dependent:
|
|
126
|
+
Any object exposing a no-arg ``stop()`` method.
|
|
127
|
+
|
|
128
|
+
:raises RuntimeError:
|
|
129
|
+
If called while the ECU is shutting down.
|
|
130
|
+
:raises TypeError:
|
|
131
|
+
If ``dependent`` does not expose a callable ``stop`` attribute.
|
|
132
|
+
"""
|
|
133
|
+
if not callable(getattr(dependent, 'stop', None)):
|
|
134
|
+
raise TypeError(
|
|
135
|
+
"dependent must expose a callable stop() method")
|
|
136
|
+
with self._dependents_lock:
|
|
137
|
+
if self._stopping:
|
|
138
|
+
raise RuntimeError(
|
|
139
|
+
"Cannot register a dependent while the ECU is stopping")
|
|
140
|
+
for existing in self._dependents:
|
|
141
|
+
if existing is dependent:
|
|
142
|
+
return
|
|
143
|
+
self._dependents.append(dependent)
|
|
144
|
+
|
|
145
|
+
def unregister_dependent(self, dependent):
|
|
146
|
+
"""Remove a previously-registered dependent.
|
|
147
|
+
|
|
148
|
+
:param dependent:
|
|
149
|
+
The object previously passed to :meth:`register_dependent`.
|
|
150
|
+
"""
|
|
151
|
+
with self._dependents_lock:
|
|
152
|
+
self._dependents = [
|
|
153
|
+
d for d in self._dependents if d is not dependent]
|
|
154
|
+
|
|
155
|
+
def add_timer(self, delta_time, callback, cookie=None):
|
|
156
|
+
"""Adds a callback to the list of timer events
|
|
157
|
+
|
|
158
|
+
:param delta_time:
|
|
159
|
+
The time in seconds after which the event is to be triggered.
|
|
160
|
+
:param callback:
|
|
161
|
+
The callback function to call
|
|
162
|
+
"""
|
|
163
|
+
deadline = time.monotonic() + delta_time
|
|
164
|
+
with self._timer_events_lock:
|
|
165
|
+
heapq.heappush(self._timer_events,
|
|
166
|
+
(deadline, self._timer_seq, callback, cookie, delta_time))
|
|
167
|
+
self._timer_seq += 1
|
|
168
|
+
self._timer_wakeup_queue.put(1)
|
|
169
|
+
|
|
170
|
+
def remove_timer(self, callback):
|
|
171
|
+
"""Removes ALL entries from the timer event list for the given callback
|
|
172
|
+
|
|
173
|
+
:param callback:
|
|
174
|
+
The callback to be removed from the timer event list
|
|
175
|
+
"""
|
|
176
|
+
with self._timer_events_lock:
|
|
177
|
+
self._timer_events = [e for e in self._timer_events if e[2] != callback]
|
|
178
|
+
heapq.heapify(self._timer_events)
|
|
179
|
+
self._timer_wakeup_queue.put(1)
|
|
180
|
+
|
|
181
|
+
def connect(self, *args, **kwargs):
|
|
182
|
+
"""Connect to CAN bus using python-can.
|
|
183
|
+
|
|
184
|
+
Arguments are passed directly to :class:`can.BusABC`. Typically these
|
|
185
|
+
may include:
|
|
186
|
+
|
|
187
|
+
:param channel:
|
|
188
|
+
Backend specific channel for the CAN interface.
|
|
189
|
+
:param str interface:
|
|
190
|
+
Name of the interface (formerly ``bustype``, renamed in python-can v4.2). See
|
|
191
|
+
`python-can manual <https://python-can.readthedocs.io/en/latest/configuration.html#interface-names>`__
|
|
192
|
+
for full list of supported interfaces.
|
|
193
|
+
:param int bitrate:
|
|
194
|
+
Bitrate in bit/s.
|
|
195
|
+
|
|
196
|
+
:raises can.CanError:
|
|
197
|
+
When connection fails.
|
|
198
|
+
"""
|
|
199
|
+
self._bus = can.interface.Bus(*args, **kwargs)
|
|
200
|
+
logger.info("Connected to '%s'", self._bus.channel_info)
|
|
201
|
+
self._notifier = can.Notifier(self._bus, self._listeners, 1)
|
|
202
|
+
return self._bus
|
|
203
|
+
|
|
204
|
+
def disconnect(self):
|
|
205
|
+
"""Disconnect from the CAN bus.
|
|
206
|
+
|
|
207
|
+
Must be overridden in a subclass if a custom interface is used.
|
|
208
|
+
"""
|
|
209
|
+
self._notifier.stop()
|
|
210
|
+
self._bus.shutdown()
|
|
211
|
+
self._bus = None
|
|
212
|
+
|
|
213
|
+
def subscribe(self, callback, device_address=None):
|
|
214
|
+
"""Add the given callback to the message notification stream.
|
|
215
|
+
|
|
216
|
+
:param callback:
|
|
217
|
+
Function to call when message is received.
|
|
218
|
+
:param int device_address:
|
|
219
|
+
Device address of the application.
|
|
220
|
+
This is a simple way for peer-to-peer reception without adding a controller-application.
|
|
221
|
+
Only one device address can be entered. Multiple device addresses are only possible with controller applications.
|
|
222
|
+
Note: TP.CMDT will only be received if the destination address is bound to a controller application.
|
|
223
|
+
"""
|
|
224
|
+
with self._subscribers_lock:
|
|
225
|
+
self._subscribers.append({'cb': callback, 'dev_adr': device_address})
|
|
226
|
+
|
|
227
|
+
def unsubscribe(self, callback):
|
|
228
|
+
"""Stop listening for message.
|
|
229
|
+
|
|
230
|
+
:param callback:
|
|
231
|
+
Function to call when message is received.
|
|
232
|
+
"""
|
|
233
|
+
with self._subscribers_lock:
|
|
234
|
+
self._subscribers = [d for d in self._subscribers if d['cb'] != callback]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def add_ca(self, **kwargs):
|
|
238
|
+
"""Add a ControllerApplication to the ECU.
|
|
239
|
+
|
|
240
|
+
:param controller_application:
|
|
241
|
+
A :class:`j1939.ControllerApplication` object.
|
|
242
|
+
|
|
243
|
+
:param name:
|
|
244
|
+
A :class:`j1939.Name` object.
|
|
245
|
+
|
|
246
|
+
:param device_address:
|
|
247
|
+
An integer representing the device address to announce to the bus.
|
|
248
|
+
|
|
249
|
+
:return:
|
|
250
|
+
The CA object that was added.
|
|
251
|
+
|
|
252
|
+
:rtype: r3964.ControllerApplication
|
|
253
|
+
"""
|
|
254
|
+
if 'controller_application' in kwargs:
|
|
255
|
+
ca = kwargs['controller_application']
|
|
256
|
+
else:
|
|
257
|
+
if 'name' not in kwargs:
|
|
258
|
+
raise ValueError("either 'controller_application' or 'name' must be provided")
|
|
259
|
+
name = kwargs.get('name')
|
|
260
|
+
da = kwargs.get('device_address', None)
|
|
261
|
+
ca = ControllerApplication(name, da)
|
|
262
|
+
|
|
263
|
+
self.j1939_dll.add_ca(ca)
|
|
264
|
+
ca.associate_ecu(self)
|
|
265
|
+
return ca
|
|
266
|
+
|
|
267
|
+
def remove_ca(self, device_address):
|
|
268
|
+
"""Remove a ControllerApplication from the ECU.
|
|
269
|
+
|
|
270
|
+
:param int device_address:
|
|
271
|
+
A integer representing the device address
|
|
272
|
+
|
|
273
|
+
:return:
|
|
274
|
+
True if the ControllerApplication was successfully removed, otherwise False is returned.
|
|
275
|
+
"""
|
|
276
|
+
return self.j1939_dll.remove_ca(device_address)
|
|
277
|
+
|
|
278
|
+
def add_bus(self, bus):
|
|
279
|
+
"""Add a bus to the ECU.
|
|
280
|
+
|
|
281
|
+
:param bus:
|
|
282
|
+
A :class:`can.BusABC` object.
|
|
283
|
+
"""
|
|
284
|
+
self._bus = bus
|
|
285
|
+
|
|
286
|
+
def add_notifier(self, notifier):
|
|
287
|
+
"""Add a notifier to the ECU.
|
|
288
|
+
|
|
289
|
+
:param notifier:
|
|
290
|
+
A :class:`can.Notifier` object.
|
|
291
|
+
"""
|
|
292
|
+
self._notifier = notifier
|
|
293
|
+
for listener in self._listeners:
|
|
294
|
+
self._notifier.add_listener(listener)
|
|
295
|
+
|
|
296
|
+
def remove_bus(self):
|
|
297
|
+
"""Remove the bus from the ECU.
|
|
298
|
+
"""
|
|
299
|
+
self._bus = None
|
|
300
|
+
|
|
301
|
+
def remove_notifier(self):
|
|
302
|
+
"""Remove the notifier from the ECU.
|
|
303
|
+
"""
|
|
304
|
+
for listener in self._listeners:
|
|
305
|
+
self._notifier.remove_listener(listener)
|
|
306
|
+
self._notifier = None
|
|
307
|
+
|
|
308
|
+
def send_pgn(self, data_page, pdu_format, pdu_specific, priority, src_address, data, time_limit=0, frame_format=FrameFormat.FEFF):
|
|
309
|
+
"""send a pgn
|
|
310
|
+
:param int data_page: data page
|
|
311
|
+
:param int pdu_format: pdu format
|
|
312
|
+
:param int pdu_specific: pdu specific
|
|
313
|
+
:param int priority: message priority
|
|
314
|
+
:param int src_address: address of the transmitter
|
|
315
|
+
:param list data: payload, each list index represents one payload byte
|
|
316
|
+
:param time_limit: option j1939-22 multi-pg: specify a time limit in s (e.g. 0.1 == 100ms),
|
|
317
|
+
after this time, the multi-pg will be sent. several pgs can thus be combined in one multi-pg.
|
|
318
|
+
0 or no time-limit means immediate sending.
|
|
319
|
+
"""
|
|
320
|
+
return self.j1939_dll.send_pgn(data_page, pdu_format, pdu_specific, priority, src_address, data, time_limit, frame_format)
|
|
321
|
+
|
|
322
|
+
def send_message(self, can_id, extended_id, data, fd_format=False):
|
|
323
|
+
"""Send a raw CAN message to the bus.
|
|
324
|
+
|
|
325
|
+
This method may be overridden in a subclass if you need to integrate
|
|
326
|
+
this library with a custom backend.
|
|
327
|
+
It is safe to call this from multiple threads.
|
|
328
|
+
|
|
329
|
+
:param int can_id:
|
|
330
|
+
CAN-ID of the message (always 29-bit)
|
|
331
|
+
:param data:
|
|
332
|
+
Data to be transmitted (anything that can be converted to bytes)
|
|
333
|
+
:param fd_format:
|
|
334
|
+
fd format means bitrate switching and payload of max 64Bytes is active
|
|
335
|
+
|
|
336
|
+
:raises can.CanError:
|
|
337
|
+
When the message fails to be transmitted
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
if not self._bus:
|
|
341
|
+
raise RuntimeError("Not connected to CAN bus")
|
|
342
|
+
msg = can.Message(is_extended_id=extended_id,
|
|
343
|
+
arbitration_id=can_id,
|
|
344
|
+
data=data,
|
|
345
|
+
is_fd=fd_format,
|
|
346
|
+
bitrate_switch=fd_format
|
|
347
|
+
)
|
|
348
|
+
with self._send_lock:
|
|
349
|
+
self._bus.send(msg)
|
|
350
|
+
# TODO: check error receivement
|
|
351
|
+
|
|
352
|
+
def notify(self, can_id, data, timestamp):
|
|
353
|
+
"""Feed incoming CAN message into this ecu.
|
|
354
|
+
|
|
355
|
+
If a custom interface is used, this function must be called for each
|
|
356
|
+
29-bit standard message read from the CAN bus.
|
|
357
|
+
|
|
358
|
+
:param int can_id:
|
|
359
|
+
CAN-ID of the message (always 29-bit)
|
|
360
|
+
:param bytearray data:
|
|
361
|
+
Data part of the message (0 - 8 bytes)
|
|
362
|
+
:param float timestamp:
|
|
363
|
+
The timestamp field in a CAN message is a floating point number
|
|
364
|
+
representing when the message was received since the epoch in
|
|
365
|
+
seconds.
|
|
366
|
+
Where possible this will be timestamped in hardware.
|
|
367
|
+
"""
|
|
368
|
+
self.j1939_dll.notify(can_id, data, timestamp)
|
|
369
|
+
|
|
370
|
+
def add_bus_filters(self, filters: can.typechecking.CanFilters | None):
|
|
371
|
+
"""Add bus filters to the underlying CAN bus.
|
|
372
|
+
|
|
373
|
+
:param filters:
|
|
374
|
+
An iterable of dictionaries each containing a "can_id",
|
|
375
|
+
a "can_mask", and an optional "extended" key
|
|
376
|
+
"""
|
|
377
|
+
if self._bus is None:
|
|
378
|
+
raise RuntimeError("Not connected to CAN bus")
|
|
379
|
+
self._bus.set_filters(filters)
|
|
380
|
+
|
|
381
|
+
def _protocol_job_thread(self):
|
|
382
|
+
"""Protocol thread: handles TP/BAM timeout management only.
|
|
383
|
+
|
|
384
|
+
This thread is isolated from application timer callbacks so that slow
|
|
385
|
+
user callbacks cannot delay protocol-level timeouts (which would cause
|
|
386
|
+
spurious ABORT messages on the bus).
|
|
387
|
+
"""
|
|
388
|
+
while not self._job_thread_end.is_set():
|
|
389
|
+
now = time.monotonic()
|
|
390
|
+
next_wakeup = self.j1939_dll.async_job_thread(now)
|
|
391
|
+
time_to_sleep = next_wakeup - time.monotonic()
|
|
392
|
+
if time_to_sleep > 0:
|
|
393
|
+
try:
|
|
394
|
+
self._protocol_wakeup_queue.get(True, time_to_sleep)
|
|
395
|
+
except queue.Empty:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
def _timer_job_thread(self):
|
|
399
|
+
"""Timer thread: handles application cyclic callbacks only.
|
|
400
|
+
|
|
401
|
+
Uses a heapq (min-heap keyed by deadline) for O(log n) scheduling.
|
|
402
|
+
Woken early via _timer_wakeup_queue whenever a timer is added/removed.
|
|
403
|
+
Callbacks returning True are rescheduled; returning False are removed.
|
|
404
|
+
"""
|
|
405
|
+
while not self._job_thread_end.is_set():
|
|
406
|
+
now = time.monotonic()
|
|
407
|
+
next_wakeup = now + 5.0
|
|
408
|
+
|
|
409
|
+
with self._timer_events_lock:
|
|
410
|
+
while self._timer_events and self._timer_events[0][0] <= now:
|
|
411
|
+
deadline, seq, cb, cookie, delta = heapq.heappop(self._timer_events)
|
|
412
|
+
logger.debug("Deadline for timer event reached")
|
|
413
|
+
try:
|
|
414
|
+
reschedule = (cb(cookie) is True)
|
|
415
|
+
except Exception:
|
|
416
|
+
#TODO: is there a better way to handle exceptions in user callbacks?
|
|
417
|
+
# We don't want one bad callback to break the timer thread,
|
|
418
|
+
# but we also don't want to just swallow it silently.
|
|
419
|
+
logger.exception("Timer callback failed: %r", cb)
|
|
420
|
+
reschedule = False
|
|
421
|
+
if reschedule:
|
|
422
|
+
# reschedule: advance deadline past now to avoid burst catch-up
|
|
423
|
+
new_deadline = deadline + delta
|
|
424
|
+
while new_deadline < now:
|
|
425
|
+
new_deadline += delta
|
|
426
|
+
heapq.heappush(self._timer_events,
|
|
427
|
+
(new_deadline, self._timer_seq, cb, cookie, delta))
|
|
428
|
+
self._timer_seq += 1
|
|
429
|
+
# returning False (or None) means remove — already popped, nothing to do
|
|
430
|
+
|
|
431
|
+
if self._timer_events:
|
|
432
|
+
next_wakeup = self._timer_events[0][0]
|
|
433
|
+
|
|
434
|
+
time_to_sleep = next_wakeup - time.monotonic()
|
|
435
|
+
if time_to_sleep > 0:
|
|
436
|
+
try:
|
|
437
|
+
self._timer_wakeup_queue.get(True, time_to_sleep)
|
|
438
|
+
except queue.Empty:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
def _protocol_wakeup(self):
|
|
442
|
+
"""Wakeup the protocol job thread.
|
|
443
|
+
|
|
444
|
+
Called by the DLL (j1939_21/j1939_22) when TP state changes require
|
|
445
|
+
immediate re-evaluation of protocol deadlines.
|
|
446
|
+
"""
|
|
447
|
+
self._protocol_wakeup_queue.put(1)
|
|
448
|
+
|
|
449
|
+
def _notify_subscribers(self, priority, pgn, sa, dest, timestamp, data):
|
|
450
|
+
"""Feed incoming message to subscribers.
|
|
451
|
+
|
|
452
|
+
:param int priority:
|
|
453
|
+
Priority of the message
|
|
454
|
+
:param int pgn:
|
|
455
|
+
Parameter Group Number of the message
|
|
456
|
+
:param int sa:
|
|
457
|
+
Source Address of the message
|
|
458
|
+
:param int dest:
|
|
459
|
+
Destination Address of the message
|
|
460
|
+
:param int timestamp:
|
|
461
|
+
Timestamp of the CAN message
|
|
462
|
+
:param bytearray data:
|
|
463
|
+
Data of the PDU
|
|
464
|
+
"""
|
|
465
|
+
logger.debug("notify subscribers for PGN {}".format(pgn))
|
|
466
|
+
# Snapshot under lock so subscribe/unsubscribe from any thread is safe.
|
|
467
|
+
with self._subscribers_lock:
|
|
468
|
+
snapshot = list(self._subscribers)
|
|
469
|
+
for dic in snapshot:
|
|
470
|
+
if (dic['dev_adr'] is None) or (dest == ParameterGroupNumber.Address.GLOBAL) or (callable(dic['dev_adr']) and dic['dev_adr'](dest)) or (dest == dic['dev_adr']):
|
|
471
|
+
dic['cb'](priority, pgn, sa, timestamp, data)
|
|
472
|
+
|
|
473
|
+
def _is_message_acceptable(self, dest):
|
|
474
|
+
with self._subscribers_lock:
|
|
475
|
+
return any(d['dev_adr'] == dest for d in self._subscribers)
|
|
476
|
+
|
|
477
|
+
class MessageListener(Listener):
|
|
478
|
+
"""Listens for messages on CAN bus and feeds them to an ECU instance.
|
|
479
|
+
|
|
480
|
+
:param j1939.ElectronicControlUnit ecu:
|
|
481
|
+
The ECU to notify on new messages.
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
def __init__(self, ecu : ElectronicControlUnit):
|
|
485
|
+
self.ecu = ecu
|
|
486
|
+
self.stopped = False
|
|
487
|
+
|
|
488
|
+
def on_message_received(self, msg : can.Message):
|
|
489
|
+
if self.stopped or msg.is_error_frame or msg.is_remote_frame or (msg.is_extended_id == False):
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
self.ecu.notify(msg.arbitration_id, msg.data, msg.timestamp)
|
|
494
|
+
except Exception as e:
|
|
495
|
+
# Exceptions in any callbaks should not affect CAN processing
|
|
496
|
+
logger.error(str(e))
|
|
497
|
+
|
|
498
|
+
def stop(self):
|
|
499
|
+
self.stopped = True
|
j1939/error_info.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class J1939Error(Enum):
|
|
4
|
+
"""
|
|
5
|
+
Enum of general errors based off of SAE Mobilus guidelines
|
|
6
|
+
"""
|
|
7
|
+
NO_ERROR = 0x0
|
|
8
|
+
UNKNOWN_ERROR = 0x1
|
|
9
|
+
BUSY = 0x2
|
|
10
|
+
BUSY_ERASE_REQUEST = 0x10
|
|
11
|
+
BUSY_READ_REQUEST = 0x11
|
|
12
|
+
BUSY_WRITE_REQUEST = 0x12
|
|
13
|
+
BUSY_STATUS_REQUEST = 0x13
|
|
14
|
+
BUSY_BOOT_LOAD_REQUEST = 0x16
|
|
15
|
+
BUSY_EDCP_GENERATION_REQUEST = 0x17
|
|
16
|
+
BUSY_UNKNOW_REQUEST = 0x1F
|
|
17
|
+
EDC_PARAMETER_ERROR = 0x20
|
|
18
|
+
RAM_ERROR = 0x21
|
|
19
|
+
FLASH_ERROR = 0x22
|
|
20
|
+
PROM_ERROR = 0x23
|
|
21
|
+
INTERNAL_ERROR = 0x24
|
|
22
|
+
GENERAL_ADDRESSING_ERROR = 0x100
|
|
23
|
+
ADDRESS_NOT_ON_BOUNDARY = 0x101
|
|
24
|
+
ADDRESS_INVALID_LENGTH = 0x102
|
|
25
|
+
ADDRESS_MEMORY_OVERFLOW = 0x103
|
|
26
|
+
ADDRESS_DATA_ERASE_REQUIRED = 0x104
|
|
27
|
+
ADDRESS_PROGRAM_ERASE_REQUIRED = 0x105
|
|
28
|
+
ADDRESS_TX_ERASE_PROGRAM_REQUIRED = 0x106
|
|
29
|
+
ADDRESS_BOOT_LOAD_OUT_OF_RANGE = 0x107
|
|
30
|
+
ADDRESS_BOOT_LOAD_NOT_ON_BOUNDARY = 0x108
|
|
31
|
+
DATA_OUT_OF_RANGE = 0x109
|
|
32
|
+
DATA_NAME_UNEXPECTED = 0x10A
|
|
33
|
+
SECURITY_GENERAL = 0x1000
|
|
34
|
+
SECURITY_INVALID_PASSWORD = 0x1001
|
|
35
|
+
SECURITY_INVALID_LEVEL = 0x1002
|
|
36
|
+
SECURITY_INVALID_KEY = 0x1003
|
|
37
|
+
SECURITY_NOT_DIAGNOSTIC = 0x1004
|
|
38
|
+
SECURITY_INCORRECT_MODE = 0x1005
|
|
39
|
+
SECURITY_ENGINE_RUNNING = 0x1006
|
|
40
|
+
SECURITY_VEHICLE_MOVING = 0x1007
|
|
41
|
+
ABORT_EXTERNAL = 0x10000
|
|
42
|
+
MAX_RETRY = 0x10001
|
|
43
|
+
NO_RESPONSE = 0x10002
|
|
44
|
+
INITILIZATION_TIMEOUT = 0x10003
|
|
45
|
+
COMPLETION_TIMEOUT = 0x10004
|
|
46
|
+
NO_INDICATOR = 0xFFFFFF
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
Dictionary of error codes and their corresponding error message
|
|
51
|
+
"""
|
|
52
|
+
ErrorInfo = {
|
|
53
|
+
J1939Error.NO_ERROR.value: "No error",
|
|
54
|
+
J1939Error.UNKNOWN_ERROR.value: "Unknown error",
|
|
55
|
+
J1939Error.BUSY.value: "Busy",
|
|
56
|
+
J1939Error.BUSY_ERASE_REQUEST.value: "Busy: erase request",
|
|
57
|
+
J1939Error.BUSY_READ_REQUEST.value: "Busy: read request",
|
|
58
|
+
J1939Error.BUSY_WRITE_REQUEST.value: "Busy: write request",
|
|
59
|
+
J1939Error.BUSY_STATUS_REQUEST.value: "Busy status request",
|
|
60
|
+
J1939Error.BUSY_BOOT_LOAD_REQUEST.value: "Busy: boot load request",
|
|
61
|
+
J1939Error.BUSY_EDCP_GENERATION_REQUEST.value: "Busy: EDCP generation request",
|
|
62
|
+
J1939Error.BUSY_UNKNOW_REQUEST.value: "Busy: unknown request",
|
|
63
|
+
J1939Error.EDC_PARAMETER_ERROR.value: "EDC parameter error",
|
|
64
|
+
J1939Error.RAM_ERROR.value: "RAM error",
|
|
65
|
+
J1939Error.FLASH_ERROR.value: "Flash error",
|
|
66
|
+
J1939Error.PROM_ERROR.value: "PROM error",
|
|
67
|
+
J1939Error.INTERNAL_ERROR.value: "Internal error",
|
|
68
|
+
J1939Error.GENERAL_ADDRESSING_ERROR.value: "General addressing error",
|
|
69
|
+
J1939Error.ADDRESS_NOT_ON_BOUNDARY.value: "Address: not on boundary",
|
|
70
|
+
J1939Error.ADDRESS_INVALID_LENGTH.value: "Address: invalid length",
|
|
71
|
+
J1939Error.ADDRESS_MEMORY_OVERFLOW.value: "Address: memory overflow",
|
|
72
|
+
J1939Error.ADDRESS_DATA_ERASE_REQUIRED.value: "Address: data erase required",
|
|
73
|
+
J1939Error.ADDRESS_PROGRAM_ERASE_REQUIRED.value: "Address: program erase required",
|
|
74
|
+
J1939Error.ADDRESS_TX_ERASE_PROGRAM_REQUIRED.value: "Address: TX erase program required",
|
|
75
|
+
J1939Error.ADDRESS_BOOT_LOAD_OUT_OF_RANGE.value: "Address: boot load out of range",
|
|
76
|
+
J1939Error.ADDRESS_BOOT_LOAD_NOT_ON_BOUNDARY.value: "Address: boot load not on boundary",
|
|
77
|
+
J1939Error.DATA_OUT_OF_RANGE.value: "Data out of range",
|
|
78
|
+
J1939Error.DATA_NAME_UNEXPECTED.value: "Data name unexpected",
|
|
79
|
+
J1939Error.SECURITY_GENERAL.value: "Security: general",
|
|
80
|
+
J1939Error.SECURITY_INVALID_PASSWORD.value: "Security: invalid password",
|
|
81
|
+
J1939Error.SECURITY_INVALID_LEVEL.value: "Security: invalid level",
|
|
82
|
+
J1939Error.SECURITY_INVALID_KEY.value: "Security: invalid key",
|
|
83
|
+
J1939Error.SECURITY_NOT_DIAGNOSTIC.value: "Security: not diagnostic",
|
|
84
|
+
J1939Error.SECURITY_INCORRECT_MODE.value: "Security: incorrect mode",
|
|
85
|
+
J1939Error.SECURITY_ENGINE_RUNNING.value: "Security: engine running",
|
|
86
|
+
J1939Error.SECURITY_VEHICLE_MOVING.value: "Security: vehicle moving",
|
|
87
|
+
J1939Error.ABORT_EXTERNAL.value: "Abort external",
|
|
88
|
+
J1939Error.MAX_RETRY.value: "Max retry",
|
|
89
|
+
J1939Error.NO_RESPONSE.value: "No response",
|
|
90
|
+
J1939Error.INITILIZATION_TIMEOUT.value: "Initilization timeout",
|
|
91
|
+
J1939Error.COMPLETION_TIMEOUT.value: "Completion timeout",
|
|
92
|
+
J1939Error.NO_INDICATOR.value: "No indicator",
|
|
93
|
+
}
|