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/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 *