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/Dm14Server.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import queue
|
|
3
|
+
import secrets
|
|
4
|
+
import j1939
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResponseState(Enum):
|
|
8
|
+
IDLE = 1
|
|
9
|
+
WAIT_FOR_DM14 = 2
|
|
10
|
+
WAIT_FOR_KEY = 3
|
|
11
|
+
SEND_PROCEED = 4
|
|
12
|
+
SEND_OPERATION_COMPLETE = 5
|
|
13
|
+
WAIT_OPERATION_COMPLETE = 6
|
|
14
|
+
SEND_ERROR = 7
|
|
15
|
+
WAIT_FOR_DM16 = 8
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DM14Server:
|
|
19
|
+
def __init__(self, ca: j1939.ControllerApplication) -> None:
|
|
20
|
+
"""
|
|
21
|
+
performs memory access responses using DM14-DM18 messaging.
|
|
22
|
+
:param obj ca: j1939 controller application
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
self._ca = ca
|
|
26
|
+
self._busy = False
|
|
27
|
+
self.sa = None
|
|
28
|
+
self.state = ResponseState.IDLE
|
|
29
|
+
self._key_from_seed = None
|
|
30
|
+
self.data_queue = queue.Queue()
|
|
31
|
+
self._seed_generator = self.generate_seed
|
|
32
|
+
self._verify_key = None
|
|
33
|
+
self.address = None
|
|
34
|
+
self.length = 8
|
|
35
|
+
self.proceed = False
|
|
36
|
+
self.data = []
|
|
37
|
+
self.error = 0x00
|
|
38
|
+
self.edcp = 0x07
|
|
39
|
+
self.status = j1939.Dm15Status.PROCEED.value
|
|
40
|
+
self.direct = 0
|
|
41
|
+
|
|
42
|
+
def _wait_for_data(self) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Determines whether to send data or wait to receive data based on the command type.
|
|
45
|
+
If the command is a read command, then the data requested is sent.
|
|
46
|
+
"""
|
|
47
|
+
self._ca.subscribe(self._parse_dm16)
|
|
48
|
+
self._send_dm15(
|
|
49
|
+
self.length,
|
|
50
|
+
self.direct,
|
|
51
|
+
self.status,
|
|
52
|
+
self.state,
|
|
53
|
+
self.object_count,
|
|
54
|
+
self.sa,
|
|
55
|
+
j1939.ParameterGroupNumber.PGN.DM15,
|
|
56
|
+
self.error,
|
|
57
|
+
self.edcp,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
self.command is j1939.Command.READ.value
|
|
62
|
+
and self.state == ResponseState.SEND_PROCEED
|
|
63
|
+
):
|
|
64
|
+
self._ca.unsubscribe(self._parse_dm16)
|
|
65
|
+
self._send_dm16()
|
|
66
|
+
if (len(self.data)) <= 8:
|
|
67
|
+
self.proceed = True
|
|
68
|
+
self.state = ResponseState.SEND_OPERATION_COMPLETE
|
|
69
|
+
self._ca.subscribe(self.parse_dm14)
|
|
70
|
+
self._send_dm15(
|
|
71
|
+
self.length,
|
|
72
|
+
self.direct,
|
|
73
|
+
self.status,
|
|
74
|
+
self.state,
|
|
75
|
+
self.object_count,
|
|
76
|
+
self.sa,
|
|
77
|
+
j1939.ParameterGroupNumber.PGN.DM15,
|
|
78
|
+
self.error,
|
|
79
|
+
self.edcp,
|
|
80
|
+
)
|
|
81
|
+
elif (
|
|
82
|
+
self.command is j1939.Command.WRITE.value
|
|
83
|
+
and self.state == ResponseState.SEND_PROCEED
|
|
84
|
+
):
|
|
85
|
+
self.state = ResponseState.WAIT_FOR_DM16
|
|
86
|
+
else:
|
|
87
|
+
self._ca.unsubscribe(self._parse_dm16)
|
|
88
|
+
self.state = ResponseState.IDLE
|
|
89
|
+
self.sa = None
|
|
90
|
+
|
|
91
|
+
def parse_dm14(
|
|
92
|
+
self, priority: int, pgn: int, sa: int, timestamp: int, data: bytearray
|
|
93
|
+
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
parse DM14 message received
|
|
96
|
+
:param int priority: priority of the message
|
|
97
|
+
:param int pgn: parameter group number of the message
|
|
98
|
+
:param int sa: source address of the message
|
|
99
|
+
:param int timestamp: timestamp of the message
|
|
100
|
+
:param bytearray data: data of the PDU
|
|
101
|
+
"""
|
|
102
|
+
if pgn != j1939.ParameterGroupNumber.PGN.DM14:
|
|
103
|
+
return
|
|
104
|
+
if (
|
|
105
|
+
(self.sa is not None and sa != self.sa)
|
|
106
|
+
or (
|
|
107
|
+
self.address is not None and self.address != data[2 : (self.length - 2)]
|
|
108
|
+
)
|
|
109
|
+
or self._busy
|
|
110
|
+
):
|
|
111
|
+
self._send_dm15(
|
|
112
|
+
self.length,
|
|
113
|
+
data[1] >> 4,
|
|
114
|
+
j1939.Dm15Status.OPERATION_FAILED.value,
|
|
115
|
+
j1939.ResponseState.SEND_ERROR,
|
|
116
|
+
data[0],
|
|
117
|
+
sa,
|
|
118
|
+
j1939.ParameterGroupNumber.PGN.DM15,
|
|
119
|
+
self.error if self.error != 0x00 else 0x2,
|
|
120
|
+
0x7,
|
|
121
|
+
)
|
|
122
|
+
self.set_busy(False)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
self.length = len(data)
|
|
126
|
+
self.direct = data[1] >> 4
|
|
127
|
+
|
|
128
|
+
match self.state:
|
|
129
|
+
case ResponseState.IDLE:
|
|
130
|
+
self.pgn = pgn
|
|
131
|
+
self.sa = sa
|
|
132
|
+
self.status = j1939.Dm15Status.PROCEED.value
|
|
133
|
+
self.address = data[2 : (self.length - 2)]
|
|
134
|
+
self.direct = data[1] >> 4
|
|
135
|
+
self.command = ((data[1] - 1) & 0x0F) >> 1
|
|
136
|
+
self.pointer_type = (data[1] >> 4) & 0x1
|
|
137
|
+
self.object_count = data[0]
|
|
138
|
+
self.access_level = (data[self.length - 1] << 8) + data[self.length - 2]
|
|
139
|
+
self.data = data
|
|
140
|
+
if self._key_from_seed is not None:
|
|
141
|
+
self.state = ResponseState.WAIT_FOR_KEY
|
|
142
|
+
self._pgn = j1939.ParameterGroupNumber.PGN.DM15
|
|
143
|
+
self._send_dm15(
|
|
144
|
+
self.length,
|
|
145
|
+
self.direct,
|
|
146
|
+
self.status,
|
|
147
|
+
self.state,
|
|
148
|
+
self.object_count,
|
|
149
|
+
self.sa,
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
self.state = ResponseState.SEND_PROCEED
|
|
153
|
+
|
|
154
|
+
case ResponseState.WAIT_FOR_KEY:
|
|
155
|
+
self.length = len(data)
|
|
156
|
+
self.address = data[2 : (self.length - 2)]
|
|
157
|
+
self.command = ((data[1] - 1) & 0x0F) >> 1
|
|
158
|
+
self.object_count = data[0]
|
|
159
|
+
self.key = (data[self.length - 1] << 8) + data[self.length - 2]
|
|
160
|
+
self.data = data
|
|
161
|
+
self.state = ResponseState.SEND_PROCEED
|
|
162
|
+
|
|
163
|
+
case ResponseState.WAIT_OPERATION_COMPLETE:
|
|
164
|
+
self.reset_server()
|
|
165
|
+
|
|
166
|
+
case _:
|
|
167
|
+
self.reset_server()
|
|
168
|
+
raise ValueError("Invalid state")
|
|
169
|
+
|
|
170
|
+
def _send_dm15(
|
|
171
|
+
self,
|
|
172
|
+
length: int,
|
|
173
|
+
direct: int,
|
|
174
|
+
status: int,
|
|
175
|
+
state: ResponseState,
|
|
176
|
+
object_count: int,
|
|
177
|
+
sa: int,
|
|
178
|
+
pgn: int = j1939.ParameterGroupNumber.PGN.DM15,
|
|
179
|
+
error: int = None,
|
|
180
|
+
edcp: int = None,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Send DM15 message to device, used to send the proceed message,
|
|
184
|
+
the generated seed, or the operation complete message
|
|
185
|
+
:param int length: length of data
|
|
186
|
+
:param int direct: value of direct transaction or not
|
|
187
|
+
:param int status: status of operation
|
|
188
|
+
:param ResponseState state: state of operation
|
|
189
|
+
:param int object_count: amount of objects to read
|
|
190
|
+
:param int sa: source address
|
|
191
|
+
:param int pgn: pgn value
|
|
192
|
+
:param int error: error code if necessary
|
|
193
|
+
:param int edcp: value of edcp for transaction
|
|
194
|
+
"""
|
|
195
|
+
self._pgn = j1939.ParameterGroupNumber.PGN.DM15
|
|
196
|
+
data = [0xFF] * length
|
|
197
|
+
data[1] = (direct << 4) + (status << 1) + 1
|
|
198
|
+
match state:
|
|
199
|
+
case ResponseState.WAIT_FOR_KEY:
|
|
200
|
+
self.seed = self._seed_generator()
|
|
201
|
+
data[0] = 0x00
|
|
202
|
+
data[length - 2] = self.seed & 0xFF
|
|
203
|
+
data[length - 1] = self.seed >> 8
|
|
204
|
+
|
|
205
|
+
case ResponseState.SEND_PROCEED:
|
|
206
|
+
data[0] = object_count
|
|
207
|
+
|
|
208
|
+
case ResponseState.SEND_OPERATION_COMPLETE:
|
|
209
|
+
self.command = j1939.Command.OPERATION_COMPLETED.value
|
|
210
|
+
data[0] = 0x00
|
|
211
|
+
data[1] = (direct << 4) + (self.command << 1) + 1
|
|
212
|
+
self.state = ResponseState.WAIT_OPERATION_COMPLETE
|
|
213
|
+
|
|
214
|
+
case ResponseState.SEND_ERROR:
|
|
215
|
+
status = j1939.Dm15Status.OPERATION_FAILED.value
|
|
216
|
+
data[0] = 0x00
|
|
217
|
+
data[1] = (direct << 4) + (status << 1) + 1
|
|
218
|
+
data[length - 6] = error & 0xFF
|
|
219
|
+
data[length - 5] = (error >> 8) & 0xFF
|
|
220
|
+
data[length - 4] = error >> 16
|
|
221
|
+
data[length - 3] = edcp
|
|
222
|
+
|
|
223
|
+
case _:
|
|
224
|
+
self.reset_server()
|
|
225
|
+
raise ValueError("Invalid state")
|
|
226
|
+
self._ca.send_pgn(0, (pgn >> 8) & 0xFF, sa & 0xFF, 6, data)
|
|
227
|
+
|
|
228
|
+
def _send_dm16(self) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Send DM16 message to device, used to send requested data
|
|
231
|
+
"""
|
|
232
|
+
self._pgn = j1939.ParameterGroupNumber.PGN.DM16
|
|
233
|
+
data = []
|
|
234
|
+
byte_count = len(self.data)
|
|
235
|
+
data.append(0xFF if byte_count > 7 else byte_count)
|
|
236
|
+
for i in range((byte_count)):
|
|
237
|
+
data.append(self.data[i])
|
|
238
|
+
|
|
239
|
+
data.extend([0xFF] * (self.length - byte_count - 1))
|
|
240
|
+
if byte_count > 8:
|
|
241
|
+
self._ca.subscribe(self._parse_dm16)
|
|
242
|
+
self._ca.send_pgn(0, (self._pgn >> 8) & 0xFF, self.sa & 0xFF, 7, data)
|
|
243
|
+
|
|
244
|
+
def _parse_dm16(
|
|
245
|
+
self, priority: int, pgn: int, sa: int, timestamp: int, data: bytearray
|
|
246
|
+
) -> None:
|
|
247
|
+
"""
|
|
248
|
+
parse DM16 message received, used to parse data received write command
|
|
249
|
+
:param int priority: priority of the message
|
|
250
|
+
:param int pgn: parameter group number of the message
|
|
251
|
+
:param int sa: source address of the message
|
|
252
|
+
:param int timestamp: timestamp of the message
|
|
253
|
+
:param bytearray data: data of the PDU
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
if pgn != j1939.ParameterGroupNumber.PGN.DM16 or sa != self.sa:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
length = min(data[0], len(data) - 1)
|
|
260
|
+
self.data_queue.put(data[1 : length + 1])
|
|
261
|
+
self._ca.unsubscribe(self._parse_dm16)
|
|
262
|
+
self._ca.subscribe(self.parse_dm14)
|
|
263
|
+
self.state = ResponseState.SEND_OPERATION_COMPLETE
|
|
264
|
+
|
|
265
|
+
self._send_dm15(
|
|
266
|
+
self.length,
|
|
267
|
+
self.direct,
|
|
268
|
+
self.status,
|
|
269
|
+
self.state,
|
|
270
|
+
self.object_count,
|
|
271
|
+
self.sa,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def bytes_to_int(self, data: bytearray) -> int:
|
|
275
|
+
"""
|
|
276
|
+
Convert bytearray to integer
|
|
277
|
+
:param bytearray data: bytearray to be converted to integer
|
|
278
|
+
"""
|
|
279
|
+
return int.from_bytes(data, byteorder="little", signed=False)
|
|
280
|
+
|
|
281
|
+
def set_busy(self, busy: bool) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Sets busy variable to indicate if busy or not
|
|
284
|
+
:param busy: busy value
|
|
285
|
+
"""
|
|
286
|
+
self._busy = busy
|
|
287
|
+
|
|
288
|
+
def generate_seed(self) -> int:
|
|
289
|
+
"""
|
|
290
|
+
Generte a random seed value for key generation
|
|
291
|
+
"""
|
|
292
|
+
seed = secrets.randbits(16)
|
|
293
|
+
if (seed == 0xFFFF) or (seed == 0x0000):
|
|
294
|
+
seed = 0xBEEF
|
|
295
|
+
return seed
|
|
296
|
+
|
|
297
|
+
def set_seed_key_algorithm(self, algorithm: callable) -> None:
|
|
298
|
+
"""
|
|
299
|
+
Set seed key algorithm to be used for key generation
|
|
300
|
+
:param callable algorithm: seed-key algorithm
|
|
301
|
+
"""
|
|
302
|
+
self._key_from_seed = algorithm
|
|
303
|
+
|
|
304
|
+
def set_seed_generator(self, algorithm: callable) -> None:
|
|
305
|
+
"""
|
|
306
|
+
Sets seed generation algorithm to be used for generating a seed value
|
|
307
|
+
:param callable algorithm: seed generation algorithm
|
|
308
|
+
"""
|
|
309
|
+
self._seed_generator = algorithm
|
|
310
|
+
|
|
311
|
+
def set_verify_key(self, algorithm: callable) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Set key verification algorithm to be used for key verification
|
|
314
|
+
:param callable algorithm: key verification algorithm
|
|
315
|
+
"""
|
|
316
|
+
self._verify_key = algorithm
|
|
317
|
+
|
|
318
|
+
def verify_key(self, seed: int, key: int) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
Checks to see if key is valid
|
|
321
|
+
:param int seed: seed
|
|
322
|
+
:param int key: key
|
|
323
|
+
"""
|
|
324
|
+
if self._verify_key is not None:
|
|
325
|
+
# TODO: add ability to dynamically pass arguments to verification function if needed,
|
|
326
|
+
# if this is breaking can just add **kwargs to function defintion used to set the verification function
|
|
327
|
+
# this will allow for the reception of additional arguments if needed
|
|
328
|
+
return self._verify_key(
|
|
329
|
+
seed=seed, key=key, address=self.bytes_to_int(self.address), sa=self.sa
|
|
330
|
+
)
|
|
331
|
+
return self._key_from_seed(seed) == key
|
|
332
|
+
|
|
333
|
+
def unsubscribe_all(self) -> None:
|
|
334
|
+
"""
|
|
335
|
+
Unsubscribes all message handlers
|
|
336
|
+
"""
|
|
337
|
+
self._ca.unsubscribe(self.parse_dm14)
|
|
338
|
+
self._ca.unsubscribe(self._parse_dm16)
|
|
339
|
+
|
|
340
|
+
def reset_server(self) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Resets server to remove transaction specific data
|
|
343
|
+
"""
|
|
344
|
+
self.state = ResponseState.IDLE
|
|
345
|
+
self.data_queue = queue.Queue()
|
|
346
|
+
self.sa = None
|
|
347
|
+
self.seed = None
|
|
348
|
+
self.key = None
|
|
349
|
+
self._busy = False
|
|
350
|
+
self.address = None
|
|
351
|
+
self.length = 8
|
|
352
|
+
self.proceed = False
|
|
353
|
+
self.data = []
|
|
354
|
+
self.error = 0x00
|
|
355
|
+
self.edcp = 0x07
|
|
356
|
+
self.status = j1939.Dm15Status.PROCEED.value
|
|
357
|
+
self.direct = 0
|
|
358
|
+
self.unsubscribe_all()
|
|
359
|
+
|
|
360
|
+
def respond(
|
|
361
|
+
self,
|
|
362
|
+
proceed: bool,
|
|
363
|
+
data=None,
|
|
364
|
+
error: int = 0xFFFFFF,
|
|
365
|
+
edcp: int = 0xFF,
|
|
366
|
+
max_timeout: int = 3,
|
|
367
|
+
) -> list:
|
|
368
|
+
"""
|
|
369
|
+
Respond to DM14 query with the requested data or confimation of operation is good to proceed
|
|
370
|
+
:param bool proceed: whether the operation is good to proceed
|
|
371
|
+
:param list data: data to be sent to device
|
|
372
|
+
:param int error: error code to be sent to device
|
|
373
|
+
:param int edcp: value for edcp extension
|
|
374
|
+
:param int max_timeout: max time for transaction
|
|
375
|
+
"""
|
|
376
|
+
if data is None:
|
|
377
|
+
data = []
|
|
378
|
+
self.proceed = proceed
|
|
379
|
+
self.data = data
|
|
380
|
+
self.error = error
|
|
381
|
+
self.edcp = edcp
|
|
382
|
+
self.status = (
|
|
383
|
+
j1939.Dm15Status.PROCEED.value
|
|
384
|
+
if proceed
|
|
385
|
+
else j1939.Dm15Status.OPERATION_FAILED.value
|
|
386
|
+
)
|
|
387
|
+
if self.status == j1939.Dm15Status.PROCEED.value:
|
|
388
|
+
self.state = ResponseState.SEND_PROCEED
|
|
389
|
+
else:
|
|
390
|
+
self.state = ResponseState.SEND_ERROR
|
|
391
|
+
self._wait_for_data()
|
|
392
|
+
mem_data = None
|
|
393
|
+
if self.state == ResponseState.WAIT_FOR_DM16:
|
|
394
|
+
try:
|
|
395
|
+
mem_data = self.data_queue.get(block=True, timeout=max_timeout)
|
|
396
|
+
except queue.Empty:
|
|
397
|
+
self.reset_server()
|
|
398
|
+
raise RuntimeError("No data received from DM16 within timeout period")
|
|
399
|
+
return mem_data
|
j1939/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .version import __version__
|
|
2
|
+
from .electronic_control_unit import ElectronicControlUnit
|
|
3
|
+
from .controller_application import ControllerApplication
|
|
4
|
+
from .name import Name
|
|
5
|
+
from .message_id import MessageId
|
|
6
|
+
from .parameter_group_number import ParameterGroupNumber
|
|
7
|
+
from .diagnostic_messages import *
|
|
8
|
+
from .memory_access import *
|
|
9
|
+
from .error_info import *
|
|
10
|
+
from .Dm14Query import *
|
|
11
|
+
from .Dm14Server import *
|