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
j1939/memory_access.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import j1939
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
class DMState(Enum):
|
|
9
|
+
IDLE = 1
|
|
10
|
+
REQUEST_STARTED = 2
|
|
11
|
+
WAIT_RESPONSE = 3
|
|
12
|
+
WAIT_QUERY = 4
|
|
13
|
+
SERVER_CLEANUP = 5
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MemoryAccess:
|
|
17
|
+
def __init__(self, ca: j1939.ControllerApplication) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Makes an overarching Memory access class
|
|
20
|
+
|
|
21
|
+
Spawns a background servicer thread tied to the lifetime of this
|
|
22
|
+
instance. Call :meth:`stop` (or use the instance as a context
|
|
23
|
+
manager) when done. The instance is also registered as a dependent
|
|
24
|
+
of the parent ECU, so ``ecu.stop()`` will cascade and tear this
|
|
25
|
+
instance down automatically.
|
|
26
|
+
|
|
27
|
+
:param ca: Controller Application
|
|
28
|
+
"""
|
|
29
|
+
self._ca = ca
|
|
30
|
+
self.query = j1939.Dm14Query(ca)
|
|
31
|
+
self.server = j1939.DM14Server(ca)
|
|
32
|
+
self._proceed_event = threading.Event()
|
|
33
|
+
self._ca.subscribe(self._listen_for_dm14)
|
|
34
|
+
self.state = DMState.IDLE
|
|
35
|
+
self.seed_security = False
|
|
36
|
+
self._notify_query_received = None
|
|
37
|
+
self._proceed_function = None
|
|
38
|
+
|
|
39
|
+
self._stopped = False
|
|
40
|
+
self._stop_lock = threading.Lock()
|
|
41
|
+
self._job_thread_end = threading.Event()
|
|
42
|
+
self._job_thread = threading.Thread(target=self._servicer, name='j1939.memory_access servicer_thread')
|
|
43
|
+
# A thread can be flagged as a "daemon thread". The significance of
|
|
44
|
+
# this flag is that the entire Python program exits when only daemon
|
|
45
|
+
# threads are left.
|
|
46
|
+
self._job_thread.daemon = True
|
|
47
|
+
self._job_thread.start()
|
|
48
|
+
|
|
49
|
+
# Register with the parent ECU so ecu.stop() cascades to this instance.
|
|
50
|
+
# Done after the thread has started so a failed registration during
|
|
51
|
+
# shutdown is still recoverable by the user calling stop() directly.
|
|
52
|
+
try:
|
|
53
|
+
self._ca.register_dependent(self)
|
|
54
|
+
except Exception:
|
|
55
|
+
# If registration fails (e.g. ECU already stopping) we still want
|
|
56
|
+
# the user to be able to stop us manually; just log and continue.
|
|
57
|
+
logger.exception("Failed to register MemoryAccess with ECU")
|
|
58
|
+
|
|
59
|
+
def stop(self, timeout: float = 2.0) -> None:
|
|
60
|
+
"""Stop the background servicer thread and release resources.
|
|
61
|
+
|
|
62
|
+
Idempotent: subsequent calls are no-ops. Safe to call from any
|
|
63
|
+
thread, including from inside ``ecu.stop()``'s cascade.
|
|
64
|
+
|
|
65
|
+
:param float timeout:
|
|
66
|
+
Maximum time in seconds to wait for the servicer thread to exit.
|
|
67
|
+
"""
|
|
68
|
+
with self._stop_lock:
|
|
69
|
+
if self._stopped:
|
|
70
|
+
return
|
|
71
|
+
self._stopped = True
|
|
72
|
+
|
|
73
|
+
# Signal shutdown and wake the servicer immediately so it does not
|
|
74
|
+
# have to wait out its full poll interval.
|
|
75
|
+
self._job_thread_end.set()
|
|
76
|
+
self._proceed_event.set()
|
|
77
|
+
|
|
78
|
+
if self._job_thread.is_alive():
|
|
79
|
+
self._job_thread.join(timeout=timeout)
|
|
80
|
+
|
|
81
|
+
# Best-effort cleanup of the CA-level subscription. If the CA/ECU
|
|
82
|
+
# is already torn down this may raise; that is fine.
|
|
83
|
+
try:
|
|
84
|
+
self._ca.unsubscribe(self._listen_for_dm14)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# Best-effort removal from the ECU's dependent registry. If we are
|
|
89
|
+
# being called from inside the cascade this is a no-op (the registry
|
|
90
|
+
# has already been cleared); if we are being called explicitly it
|
|
91
|
+
# prevents a stale reference.
|
|
92
|
+
try:
|
|
93
|
+
self._ca.unregister_dependent(self)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
def __enter__(self):
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def __exit__(self, exc_type, exc, tb):
|
|
101
|
+
self.stop()
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def __del__(self):
|
|
105
|
+
# Defensive backstop only. The primary cleanup paths are explicit
|
|
106
|
+
# stop() / context-manager exit / ecu.stop() cascade. Guard against
|
|
107
|
+
# partial __init__ (where _job_thread may not exist) and swallow all
|
|
108
|
+
# exceptions per the __del__ contract.
|
|
109
|
+
try:
|
|
110
|
+
if getattr(self, '_job_thread', None) is None:
|
|
111
|
+
return
|
|
112
|
+
self.stop()
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def _servicer(self):
|
|
117
|
+
"""
|
|
118
|
+
Job thread to service memory access requests.
|
|
119
|
+
|
|
120
|
+
Blocks on a threading.Event instead of busy-polling
|
|
121
|
+
"""
|
|
122
|
+
while not self._job_thread_end.is_set():
|
|
123
|
+
triggered = self._proceed_event.wait(timeout=1.0)
|
|
124
|
+
if self._job_thread_end.is_set():
|
|
125
|
+
return
|
|
126
|
+
if triggered and self.state == DMState.WAIT_RESPONSE:
|
|
127
|
+
self._proceed_event.clear()
|
|
128
|
+
if self._notify_query_received is not None:
|
|
129
|
+
self._notify_query_received() # notify incoming request
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _handle_error(self, priority: int, pgn: int, sa: int, timestamp: int, data: bytearray, error_code: int) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Handles errors by resetting the state and unsubscribing from DM14 messages
|
|
135
|
+
|
|
136
|
+
:param priority: Priority of the message
|
|
137
|
+
:param pgn: Parameter Group Number of the message
|
|
138
|
+
:param sa: Source Address of the message
|
|
139
|
+
:param timestamp: Timestamp of the message
|
|
140
|
+
:param data: Data of the PDU
|
|
141
|
+
:param error_code: Error code to be set
|
|
142
|
+
"""
|
|
143
|
+
self.server.error = error_code
|
|
144
|
+
self.server.set_busy(True)
|
|
145
|
+
self.server.parse_dm14(
|
|
146
|
+
priority, pgn, sa, timestamp, data
|
|
147
|
+
)
|
|
148
|
+
self.server.set_busy(False)
|
|
149
|
+
self.reset()
|
|
150
|
+
|
|
151
|
+
def _listen_for_dm14(
|
|
152
|
+
self, priority: int, pgn: int, sa: int, timestamp: int, data: bytearray
|
|
153
|
+
) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Listens for dm14 messages and passes them to the appropriate function
|
|
156
|
+
|
|
157
|
+
:param priority: Priority of the message
|
|
158
|
+
:param pgn: Parameter Group Number of the message
|
|
159
|
+
:param sa: Source Address of the message
|
|
160
|
+
:param timestamp: Timestamp of the message
|
|
161
|
+
:param data: Data of the PDU
|
|
162
|
+
"""
|
|
163
|
+
if pgn == j1939.ParameterGroupNumber.PGN.DM14:
|
|
164
|
+
match self.state:
|
|
165
|
+
case DMState.IDLE:
|
|
166
|
+
if self.server.state.value == DMState.IDLE.value:
|
|
167
|
+
self.state = DMState.REQUEST_STARTED
|
|
168
|
+
self.server.parse_dm14(priority, pgn, sa, timestamp, data)
|
|
169
|
+
if not self.seed_security:
|
|
170
|
+
self.state = DMState.WAIT_RESPONSE
|
|
171
|
+
self._ca.unsubscribe(self._listen_for_dm14)
|
|
172
|
+
if self._proceed_function is not None:
|
|
173
|
+
proceed = self._proceed_function(
|
|
174
|
+
self.server.command,
|
|
175
|
+
int.from_bytes(
|
|
176
|
+
bytes=self.server.address,
|
|
177
|
+
byteorder="little",
|
|
178
|
+
signed=False,
|
|
179
|
+
),
|
|
180
|
+
self.server.pointer_type,
|
|
181
|
+
self.server.length,
|
|
182
|
+
self.server.object_count,
|
|
183
|
+
0xFFFF, # placeholder for key
|
|
184
|
+
self.server.sa,
|
|
185
|
+
self.server.access_level,
|
|
186
|
+
0x0, # placeholder for seed
|
|
187
|
+
) # call proceed function and pass in basic parameters
|
|
188
|
+
if not proceed:
|
|
189
|
+
self._handle_error(priority, pgn, sa, timestamp, data, 0x100)
|
|
190
|
+
else:
|
|
191
|
+
self._proceed_event.set()
|
|
192
|
+
else:
|
|
193
|
+
self._proceed_event.set() # no security, so always proceed
|
|
194
|
+
|
|
195
|
+
case DMState.REQUEST_STARTED:
|
|
196
|
+
self.server.parse_dm14(priority, pgn, sa, timestamp, data)
|
|
197
|
+
if self.server.state == j1939.ResponseState.SEND_PROCEED:
|
|
198
|
+
self.state = DMState.WAIT_RESPONSE
|
|
199
|
+
if self.seed_security:
|
|
200
|
+
if self.server.verify_key(
|
|
201
|
+
self.server.seed, self.server.key
|
|
202
|
+
):
|
|
203
|
+
if self._proceed_function is not None:
|
|
204
|
+
proceed = self._proceed_function(
|
|
205
|
+
self.server.command,
|
|
206
|
+
int.from_bytes(
|
|
207
|
+
bytes=self.server.address,
|
|
208
|
+
byteorder="little",
|
|
209
|
+
signed=False,
|
|
210
|
+
),
|
|
211
|
+
self.server.pointer_type,
|
|
212
|
+
self.server.length,
|
|
213
|
+
self.server.object_count,
|
|
214
|
+
self.server.key,
|
|
215
|
+
self.server.sa,
|
|
216
|
+
self.server.access_level,
|
|
217
|
+
self.server.seed,
|
|
218
|
+
) # call proceed function and pass in basic parameters
|
|
219
|
+
if not proceed:
|
|
220
|
+
self._handle_error(priority, pgn, sa, timestamp, data, 0x100)
|
|
221
|
+
else:
|
|
222
|
+
self._proceed_event.set()
|
|
223
|
+
else:
|
|
224
|
+
self._proceed_event.set() # no proceed function, so always proceed
|
|
225
|
+
else:
|
|
226
|
+
self._handle_error(priority, pgn, sa, timestamp, data, 0x1003)
|
|
227
|
+
|
|
228
|
+
case DMState.WAIT_QUERY:
|
|
229
|
+
self.server.set_busy(True)
|
|
230
|
+
self.server.parse_dm14(priority, pgn, sa, timestamp, data)
|
|
231
|
+
self.server.set_busy(False)
|
|
232
|
+
|
|
233
|
+
case DMState.SERVER_CLEANUP:
|
|
234
|
+
self.state = DMState.IDLE
|
|
235
|
+
case _:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
def respond(
|
|
239
|
+
self,
|
|
240
|
+
proceed: bool,
|
|
241
|
+
data: list = None,
|
|
242
|
+
error: int = 0xFFFFFF,
|
|
243
|
+
edcp: int = 0xFF,
|
|
244
|
+
max_timeout: int = 3,
|
|
245
|
+
) -> list:
|
|
246
|
+
"""
|
|
247
|
+
Responds with requested data and error code, if applicable, to a read request
|
|
248
|
+
|
|
249
|
+
:param bool proceed: whether the operation is good to proceed
|
|
250
|
+
:param list data: data to be sent to device
|
|
251
|
+
:param int error: error code to be sent to device
|
|
252
|
+
:param int edcp: value for edcp extension
|
|
253
|
+
:param int max_timeout: max timeout for transaction
|
|
254
|
+
"""
|
|
255
|
+
if data is None:
|
|
256
|
+
data = []
|
|
257
|
+
|
|
258
|
+
if self.state is not DMState.WAIT_RESPONSE:
|
|
259
|
+
return data
|
|
260
|
+
|
|
261
|
+
self._proceed_event.clear()
|
|
262
|
+
self._ca.unsubscribe(self._listen_for_dm14)
|
|
263
|
+
return_data = self.server.respond(proceed, data, error, edcp, max_timeout)
|
|
264
|
+
self.state = DMState.SERVER_CLEANUP if self.server.state.value != DMState.IDLE.value else DMState.IDLE
|
|
265
|
+
self._ca.subscribe(self._listen_for_dm14)
|
|
266
|
+
return return_data
|
|
267
|
+
|
|
268
|
+
def read(
|
|
269
|
+
self,
|
|
270
|
+
dest_address: int,
|
|
271
|
+
direct: int,
|
|
272
|
+
address: int,
|
|
273
|
+
object_count: int,
|
|
274
|
+
object_byte_size: int = 1,
|
|
275
|
+
signed: bool = False,
|
|
276
|
+
return_raw_bytes: bool = False,
|
|
277
|
+
max_timeout: int = 1,
|
|
278
|
+
) -> list:
|
|
279
|
+
"""
|
|
280
|
+
Make a dm14 read Query
|
|
281
|
+
|
|
282
|
+
:param int dest_address: destination address of the message
|
|
283
|
+
:param int direct: direct address of the message
|
|
284
|
+
:param int address: address of the message
|
|
285
|
+
:param int object_count: number of objects to be read
|
|
286
|
+
:param int object_byte_size: size of each object in bytes
|
|
287
|
+
:param bool signed: whether the data is signed
|
|
288
|
+
:param bool return_raw_bytes: whether to return raw bytes or values
|
|
289
|
+
:param int max_timeout: max timeout for transaction
|
|
290
|
+
"""
|
|
291
|
+
if self.state == DMState.IDLE:
|
|
292
|
+
self.state = DMState.WAIT_QUERY
|
|
293
|
+
self.address = dest_address
|
|
294
|
+
data = self.query.read(
|
|
295
|
+
dest_address,
|
|
296
|
+
direct,
|
|
297
|
+
address,
|
|
298
|
+
object_count,
|
|
299
|
+
object_byte_size,
|
|
300
|
+
signed,
|
|
301
|
+
return_raw_bytes,
|
|
302
|
+
max_timeout,
|
|
303
|
+
)
|
|
304
|
+
self.reset()
|
|
305
|
+
return data
|
|
306
|
+
else:
|
|
307
|
+
self.reset()
|
|
308
|
+
raise RuntimeWarning("Process already Running")
|
|
309
|
+
|
|
310
|
+
def write(
|
|
311
|
+
self,
|
|
312
|
+
dest_address: int,
|
|
313
|
+
direct: int,
|
|
314
|
+
address: int,
|
|
315
|
+
values: list,
|
|
316
|
+
object_byte_size: int = 1,
|
|
317
|
+
max_timeout: int = 1,
|
|
318
|
+
) -> None:
|
|
319
|
+
"""
|
|
320
|
+
Send a write query to dest_address, requesting to write values at address
|
|
321
|
+
|
|
322
|
+
:param int dest_address: destination address of the message
|
|
323
|
+
:param int direct: direct address of the message
|
|
324
|
+
:param int address: address of the message
|
|
325
|
+
:param list values: values to be written
|
|
326
|
+
:param int object_byte_size: size of each object in bytes
|
|
327
|
+
:param int max_timeout: max timeout for transaction
|
|
328
|
+
"""
|
|
329
|
+
if self.state == DMState.IDLE:
|
|
330
|
+
self.state = DMState.WAIT_QUERY
|
|
331
|
+
self.address = dest_address
|
|
332
|
+
self.query.write(
|
|
333
|
+
dest_address, direct, address, values, object_byte_size, max_timeout
|
|
334
|
+
)
|
|
335
|
+
self.reset()
|
|
336
|
+
|
|
337
|
+
def set_seed_generator(self, seed_generator: callable) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Sets seed generator function to use
|
|
340
|
+
:param seed_generator: seed generator function
|
|
341
|
+
"""
|
|
342
|
+
self.server.set_seed_generator(seed_generator)
|
|
343
|
+
|
|
344
|
+
def set_seed_key_algorithm(self, algorithm: callable) -> None:
|
|
345
|
+
"""
|
|
346
|
+
Sets seed-key algorithm to be used for key generation
|
|
347
|
+
|
|
348
|
+
:param callable algorithm: seed-key algorithm
|
|
349
|
+
"""
|
|
350
|
+
self.seed_security = True
|
|
351
|
+
self.query.set_seed_key_algorithm(algorithm)
|
|
352
|
+
self.server.set_seed_key_algorithm(algorithm)
|
|
353
|
+
|
|
354
|
+
def set_verify_key(self, verify_key: callable) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Sets verify key function to be used for verifying the key
|
|
357
|
+
|
|
358
|
+
:param callable verify_key: verify key function
|
|
359
|
+
"""
|
|
360
|
+
self.server.set_verify_key(verify_key)
|
|
361
|
+
|
|
362
|
+
def set_notify(self, notify: callable) -> None:
|
|
363
|
+
"""
|
|
364
|
+
Sets notify function to be used for notifying the user of memory accesses
|
|
365
|
+
|
|
366
|
+
:param callable notify: notify function
|
|
367
|
+
"""
|
|
368
|
+
self._notify_query_received = notify
|
|
369
|
+
|
|
370
|
+
def set_proceed(self, proceed: callable) -> None:
|
|
371
|
+
"""
|
|
372
|
+
Sets proceed function to determine if a memory query is valid or not
|
|
373
|
+
|
|
374
|
+
:param callable proceed: proceed function
|
|
375
|
+
"""
|
|
376
|
+
self._proceed_function = proceed
|
|
377
|
+
|
|
378
|
+
def reset(self) -> None:
|
|
379
|
+
"""
|
|
380
|
+
Resets both server and query to remove transaction specific data
|
|
381
|
+
"""
|
|
382
|
+
self.state = DMState.IDLE
|
|
383
|
+
self._ca.unsubscribe(self._listen_for_dm14)
|
|
384
|
+
self._ca.subscribe(self._listen_for_dm14)
|
|
385
|
+
self.server.reset_server()
|
|
386
|
+
self.query.reset_query()
|
|
387
|
+
self._proceed_event.clear()
|
j1939/message_id.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
class MessageId:
|
|
3
|
+
"""The CAN MessageId of an PDU.
|
|
4
|
+
|
|
5
|
+
The MessageId consists of three parts:
|
|
6
|
+
* Priority
|
|
7
|
+
* Parameter Group Number
|
|
8
|
+
* Source Address
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, **kwargs): #priority=0, parameter_group_number=0, source_address=0):
|
|
12
|
+
"""
|
|
13
|
+
:param priority:
|
|
14
|
+
3-bit Priority
|
|
15
|
+
:param parameter_group_number:
|
|
16
|
+
18-bit Parameter Group Number
|
|
17
|
+
:param source_address:
|
|
18
|
+
8-bit Source Address
|
|
19
|
+
There is a total of 253 addresses available and every address must
|
|
20
|
+
be unique within the network.
|
|
21
|
+
|
|
22
|
+
:param can_id:
|
|
23
|
+
A 29-bit CAN-Id the MessageId should be parsed from.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if 'can_id' in kwargs:
|
|
27
|
+
# let the property can_id parse the given value
|
|
28
|
+
self.can_id = kwargs.get('can_id')
|
|
29
|
+
else:
|
|
30
|
+
self.priority = kwargs.get('priority', 0) & 7
|
|
31
|
+
self.parameter_group_number = kwargs.get('parameter_group_number', 0) & 0x3FFFF
|
|
32
|
+
self.source_address = kwargs.get('source_address', 0) & 0xFF
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def can_id(self):
|
|
36
|
+
"""Transforms the MessageId object to a 29 bit CAN-Id"""
|
|
37
|
+
return (self.priority << 26) | (self.parameter_group_number << 8) | (self.source_address)
|
|
38
|
+
|
|
39
|
+
@can_id.setter
|
|
40
|
+
def can_id(self, can_id):
|
|
41
|
+
"""Fill the MessageId with the information given in the 29 bit CAN-Id"""
|
|
42
|
+
self.source_address = can_id & 0xFF
|
|
43
|
+
self.parameter_group_number = (can_id >> 8) & 0x3FFFF
|
|
44
|
+
self.priority = (can_id >> 26) & 0x7
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FrameFormat:
|
|
48
|
+
CBFF = 0 # classical base frame format
|
|
49
|
+
CEFF = 1 # classical extended frame format
|
|
50
|
+
FBFF = 2 # flexible data rate base frame format
|
|
51
|
+
FEFF = 3 # flexible data rate extended frame format
|