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 ADDED
@@ -0,0 +1,331 @@
1
+ from enum import Enum
2
+ import queue
3
+ import j1939
4
+
5
+
6
+ class QueryState(Enum):
7
+ IDLE = 1
8
+ WAIT_FOR_SEED = 2
9
+ WAIT_FOR_DM16 = 3
10
+ WAIT_FOR_OPER_COMPLETE = 4
11
+
12
+
13
+ class Command(Enum):
14
+ ERASE = 0
15
+ READ = 1
16
+ WRITE = 2
17
+ STATUS_REQUEST = 3
18
+ OPERATION_COMPLETED = 4
19
+ OPERATION_FAILED = 5
20
+ BOOT_LOAD = 6
21
+ EDCP_GENERATION = 7
22
+
23
+
24
+ class Dm15Status(Enum):
25
+ PROCEED = 0
26
+ BUSY = 1
27
+ OPERATION_COMPLETE = 4
28
+ OPERATION_FAILED = 5
29
+
30
+
31
+ class Dm14Query:
32
+ def __init__(self, ca: j1939.ControllerApplication, user_level=7) -> None:
33
+ """
34
+ performs memory access queries using DM14-DM18 messaging. Presently only read and write queries are supported
35
+
36
+ :param obj ca: j1939 controller application
37
+ :param int user_level: the user level for the request
38
+ """
39
+
40
+ self._ca = ca
41
+ self.state = QueryState.IDLE
42
+ self._seed_from_key = None
43
+ self.data_queue = queue.Queue()
44
+ self.mem_data = None
45
+ self.exception_queue = queue.Queue()
46
+ self.user_level = user_level
47
+
48
+ def unsubscribe_all(self) -> None:
49
+ """
50
+ Unsubscribes all message handlers
51
+ """
52
+ self._ca.unsubscribe(self._parse_dm15)
53
+ self._ca.unsubscribe(self._parse_dm16)
54
+
55
+ def reset_query(self) -> None:
56
+ """
57
+ Resets query to remove transaction specific data
58
+ """
59
+ self.state = QueryState.IDLE
60
+ self._dest_address = None
61
+ self.address = None
62
+ self.object_count = 0
63
+ self.object_byte_size = 1
64
+ self.signed = False
65
+ self.return_raw_bytes = False
66
+ self.direct = 0
67
+ self.command = None
68
+ self.bytes = bytearray()
69
+ self.mem_data = None
70
+ self.data_queue = queue.Queue()
71
+ self.exception_queue = queue.Queue()
72
+ self.unsubscribe_all()
73
+
74
+
75
+ def _wait_for_data(self) -> None:
76
+ """
77
+ Determines whether to send data or wait to receive data based on the command type. If the command is a write command, then the data is sent.
78
+ If the command is a read command, then the device waits to receive data.
79
+ """
80
+ assert self.state is QueryState.WAIT_FOR_SEED
81
+ if self.command is Command.WRITE:
82
+ self._send_dm16()
83
+ self.state = QueryState.WAIT_FOR_OPER_COMPLETE
84
+ else:
85
+ self.state = QueryState.WAIT_FOR_DM16
86
+ self._ca.unsubscribe(self._parse_dm15)
87
+ self._ca.subscribe(self._parse_dm16)
88
+
89
+ def _send_operation_complete(self) -> None:
90
+ """
91
+ Send DM14 message to confirm the operation is complete
92
+ """
93
+ self.object_count = 1
94
+ self.command = Command.OPERATION_COMPLETED
95
+ self._send_dm14(0xFFFF)
96
+
97
+ def _send_dm14(self, key_or_user_level: int) -> None:
98
+ """
99
+ Send DM14 message to device, used to initialize a memory access operation,
100
+ respond with a key when needed, and to confirm the operation is complete
101
+
102
+ :param int key_or_user_level: key or user level
103
+ """
104
+ self._pgn = j1939.ParameterGroupNumber.PGN.DM14
105
+ pointer = self.address.to_bytes(length=4, byteorder="little")
106
+ data = []
107
+ data.append(self.object_count)
108
+ data.append(
109
+ (self.direct << 4) + (self.command.value << 1) + 1
110
+ ) # (SAE reserved = 1)
111
+ for octet in pointer:
112
+ data.append(octet)
113
+ data.append(key_or_user_level & 0xFF)
114
+ data.append(key_or_user_level >> 8)
115
+ self._ca.send_pgn(
116
+ 0, (self._pgn >> 8) & 0xFF, self._dest_address & 0xFF, 6, data
117
+ )
118
+
119
+ def _send_dm16(self) -> None:
120
+ """
121
+ Send DM16 message to device, used to send data to the device
122
+ """
123
+ self._pgn = j1939.ParameterGroupNumber.PGN.DM16
124
+ data = []
125
+ byte_count = len(self.bytes)
126
+ data.append(0xFF if byte_count > 7 else byte_count)
127
+ for i in range(byte_count):
128
+ data.append(self.bytes[i])
129
+ self._ca.send_pgn(
130
+ 0, (self._pgn >> 8) & 0xFF, self._dest_address & 0xFF, 6, data
131
+ )
132
+
133
+ def _parse_dm15(
134
+ self, priority: int, pgn: int, sa: int, timestamp: int, data: bytearray
135
+ ) -> None:
136
+ """
137
+ Parse DM15 message from device, used to determine whether device is ready, or if operation has completed and to receive seed from device
138
+ :param int priority: priority of the message
139
+ :param int pgn: parameter group number of the message
140
+ :param int sa: source address of the message
141
+ :param int timestamp: timestamp of the message
142
+ :param bytearray data: data of the PDU
143
+ """
144
+ if pgn != j1939.ParameterGroupNumber.PGN.DM15 or sa != self._dest_address:
145
+ return
146
+ seed = (data[7] << 8) + data[6]
147
+ status = (data[1] >> 1) & 7
148
+ if (
149
+ status is Dm15Status.BUSY.value
150
+ or status is Dm15Status.OPERATION_FAILED.value
151
+ ):
152
+ error = int.from_bytes(data[2:5], byteorder="little", signed=False)
153
+ edcp = data[5]
154
+ self.data_queue.put(None)
155
+ if edcp == 0x06 or edcp == 0x07:
156
+ if error in j1939.ErrorInfo:
157
+ self.exception_queue.put(
158
+ RuntimeError(
159
+ f"Device {hex(sa)} error: {hex(error)} {j1939.ErrorInfo[error]} edcp: {hex(edcp)}"
160
+ )
161
+ )
162
+ else:
163
+ self.exception_queue.put(
164
+ RuntimeError(
165
+ f"Device {hex(sa)} error: {hex(error)} edcp: {hex(edcp)}"
166
+ )
167
+ )
168
+ else:
169
+ length = data[0]
170
+ if seed == 0xFFFF and length == self.object_count:
171
+ self._wait_for_data()
172
+ else:
173
+ if self.state is QueryState.WAIT_FOR_OPER_COMPLETE:
174
+ assert status is Command.OPERATION_COMPLETED.value
175
+ self._send_operation_complete()
176
+ self.state = QueryState.IDLE
177
+ self.data_queue.put(self.mem_data)
178
+ else:
179
+ assert self.state is QueryState.WAIT_FOR_SEED
180
+ if self._seed_from_key is not None:
181
+ self._send_dm14(self._seed_from_key(seed))
182
+ else:
183
+ self.data_queue.put(None)
184
+ self.exception_queue.put(
185
+ RuntimeError(
186
+ "Key requested from host but no seed-key algorithm has been provided"
187
+ )
188
+ )
189
+
190
+ def _parse_dm16(
191
+ self, priority: int, pgn: int, sa: int, timestamp: int, data: bytearray
192
+ ) -> None:
193
+ """
194
+ parse DM16 message received from device, used to parse data received from device on a read command
195
+ :param int priority: priority of the message
196
+ :param int pgn: parameter group number of the message
197
+ :param int sa: source address of the message
198
+ :param int timestamp: timestamp of the message
199
+ :param bytearray data: data of the PDU
200
+ """
201
+ if pgn != j1939.ParameterGroupNumber.PGN.DM16 or sa != self._dest_address:
202
+ return
203
+ length = min(data[0], len(data) - 1)
204
+ # assert object_count == self.object_count
205
+ self.mem_data = data[1 : length + 1]
206
+ self._ca.unsubscribe(self._parse_dm16)
207
+ self._ca.subscribe(self._parse_dm15)
208
+ self.state = QueryState.WAIT_FOR_OPER_COMPLETE
209
+
210
+ def _values_to_bytes(self, values: list) -> bytearray:
211
+ """
212
+ convert values to bytes for sending to device
213
+ :param list values: values to be converted to bytes
214
+ """
215
+ bytes = []
216
+ for val in values:
217
+ bytes.extend(val.to_bytes(self.object_byte_size, byteorder="little"))
218
+ return bytes
219
+
220
+ def _bytes_to_values(self, raw_bytes: bytearray) -> list:
221
+ """
222
+ convert bytes received from device to values
223
+ :param bytearray raw_bytes: bytes received from device
224
+ """
225
+ values = []
226
+ for i in range(len(raw_bytes) // self.object_byte_size):
227
+ values.append(
228
+ int.from_bytes(
229
+ raw_bytes[i : self.object_byte_size],
230
+ byteorder="little",
231
+ signed=self.signed,
232
+ )
233
+ )
234
+ return values
235
+
236
+ def read(
237
+ self,
238
+ dest_address: int,
239
+ direct: int,
240
+ address: int,
241
+ object_count: int,
242
+ object_byte_size: int = 1,
243
+ signed: bool = False,
244
+ return_raw_bytes: bool = False,
245
+ max_timeout: int = 1,
246
+ ) -> list:
247
+ """
248
+ Send a read query to dest_address, requesting data at address
249
+ :param int dest_address: destination address of the message
250
+ :param int direct: direct address of the message
251
+ :param int address: address of the message
252
+ :param int object_count: number of objects to be read
253
+ :param int object_byte_size: size of each object in bytes
254
+ :param bool signed: whether the data is signed
255
+ :param bool return_raw_bytes: whether to return raw bytes or values
256
+ :param int max_timeout: max timeout for transaction
257
+ """
258
+ assert object_count > 0
259
+ self._dest_address = dest_address
260
+ self.direct = direct
261
+ self.address = address
262
+ self.object_count = object_count
263
+ self.object_byte_size = object_byte_size
264
+ self.signed = signed
265
+ self.return_raw_bytes = return_raw_bytes
266
+ self.command = Command.READ
267
+ self._ca.subscribe(self._parse_dm15)
268
+ self._send_dm14(self.user_level)
269
+ self.state = QueryState.WAIT_FOR_SEED
270
+ # wait for operation completed DM15 message
271
+ raw_bytes = None
272
+ try:
273
+ raw_bytes = self.data_queue.get(block=True, timeout=max_timeout)
274
+ except queue.Empty:
275
+ if self.state is QueryState.WAIT_FOR_SEED:
276
+ raise RuntimeError("No response from server")
277
+ pass
278
+ for _ in range(self.exception_queue.qsize()):
279
+ raise self.exception_queue.get(block=False, timeout=max_timeout)
280
+ if raw_bytes:
281
+ if self.return_raw_bytes:
282
+ return raw_bytes
283
+ else:
284
+ return self._bytes_to_values(raw_bytes)
285
+ else:
286
+ return []
287
+
288
+ def write(
289
+ self,
290
+ dest_address: int,
291
+ direct: int,
292
+ address: int,
293
+ values: list,
294
+ object_byte_size: int = 1,
295
+ max_timeout: int = 1,
296
+ ) -> None:
297
+ """
298
+ Send a write query to dest_address, requesting to write values at address
299
+ :param int dest_address: destination address of the message
300
+ :param int direct: direct address of the message
301
+ :param int address: address of the message
302
+ :param list values: values to be written
303
+ :param int object_byte_size: size of each object in bytes
304
+ :param int max_timeout: max timeout for transaction
305
+ """
306
+ self._dest_address = dest_address
307
+ self.direct = direct
308
+ self.address = address
309
+ self.object_byte_size = object_byte_size
310
+ self.command = Command.WRITE
311
+ self.bytes = self._values_to_bytes(values)
312
+ self.object_count = len(values)
313
+ self._ca.subscribe(self._parse_dm15)
314
+ self._send_dm14(self.user_level)
315
+ self.state = QueryState.WAIT_FOR_SEED
316
+ # wait for operation completed DM15 message
317
+ try:
318
+ self.data_queue.get(block=True, timeout=max_timeout)
319
+ for _ in range(self.exception_queue.qsize()):
320
+ raise self.exception_queue.get(block=False, timeout=max_timeout)
321
+ except queue.Empty:
322
+ if self.state is QueryState.WAIT_FOR_SEED:
323
+ raise RuntimeError("No response from server")
324
+ pass # expect empty queue for write
325
+
326
+ def set_seed_key_algorithm(self, algorithm: callable) -> None:
327
+ """
328
+ set seed-key algorithm to be used for key generation
329
+ :param callable algorithm: seed-key algorithm
330
+ """
331
+ self._seed_from_key = algorithm