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