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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import j1939
|
|
3
|
+
from .message_id import FrameFormat
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class ControllerApplication:
|
|
8
|
+
"""ControllerApplication (CA) identified by a Name and an Address."""
|
|
9
|
+
|
|
10
|
+
class State:
|
|
11
|
+
NONE = 0
|
|
12
|
+
WAIT_VETO = 1
|
|
13
|
+
NORMAL = 2
|
|
14
|
+
CANNOT_CLAIM = 3
|
|
15
|
+
|
|
16
|
+
class ClaimTimeout:
|
|
17
|
+
VETO = 0.250
|
|
18
|
+
REQUEST_FOR_CLAIM = 1.250
|
|
19
|
+
|
|
20
|
+
class FieldValue:
|
|
21
|
+
# The following values are in "Little Endian First" Byteorder
|
|
22
|
+
|
|
23
|
+
# indicates, that the parameter is "not available"
|
|
24
|
+
NOT_AVAILABLE_8 = 0xFF
|
|
25
|
+
NOT_AVAILABLE_16 = 0xFF00
|
|
26
|
+
NOT_AVAILABLE_16_ARR = [0xFF, 0x00]
|
|
27
|
+
# indicates, that the parameter is "not valid" or "in error"
|
|
28
|
+
NOT_VALID_8 = 0xFE
|
|
29
|
+
NOT_VALID_16 = 0xFE00
|
|
30
|
+
NOT_VALID_16_ARR = [0xFE, 0x00]
|
|
31
|
+
# raw parameter values must not exceed the following values
|
|
32
|
+
MAX_8 = 0xFA
|
|
33
|
+
MAX_16 = 0xFAFF
|
|
34
|
+
MAX_16_ARR = [0xFA, 0xFF]
|
|
35
|
+
|
|
36
|
+
def __init__(self, name, device_address_preferred=None, bypass_address_claim=False):
|
|
37
|
+
"""
|
|
38
|
+
:param name:
|
|
39
|
+
A j1939 :class:`j1939.Name` instance
|
|
40
|
+
:param device_address_preferred:
|
|
41
|
+
The device_address this CA should claim on the bus.
|
|
42
|
+
:param bypass_address_claim:
|
|
43
|
+
Flag to bypass address claim procedure
|
|
44
|
+
"""
|
|
45
|
+
self._name = name
|
|
46
|
+
self._device_address_preferred = device_address_preferred
|
|
47
|
+
if bypass_address_claim and (device_address_preferred is not None):
|
|
48
|
+
self._device_address_announced = device_address_preferred
|
|
49
|
+
self._device_address = device_address_preferred
|
|
50
|
+
self._device_address_state = ControllerApplication.State.NORMAL
|
|
51
|
+
else:
|
|
52
|
+
self._device_address_announced = j1939.ParameterGroupNumber.Address.NULL
|
|
53
|
+
self._device_address = j1939.ParameterGroupNumber.Address.NULL
|
|
54
|
+
self._device_address_state = ControllerApplication.State.NONE
|
|
55
|
+
self._ecu = None
|
|
56
|
+
self._subscribers_request = []
|
|
57
|
+
self._subscribers_acknowledge = []
|
|
58
|
+
self._started = False
|
|
59
|
+
|
|
60
|
+
def associate_ecu(self, ecu):
|
|
61
|
+
"""Binds this CA to the ECU given
|
|
62
|
+
:param ecu:
|
|
63
|
+
The ECU this CA should be bound to.
|
|
64
|
+
A j1939 :class:`j1939.ElectronicControlUnit` instance
|
|
65
|
+
"""
|
|
66
|
+
self._ecu : j1939.ElectronicControlUnit
|
|
67
|
+
self._ecu = ecu
|
|
68
|
+
|
|
69
|
+
def remove_ecu(self):
|
|
70
|
+
|
|
71
|
+
self._ecu = None
|
|
72
|
+
|
|
73
|
+
def subscribe(self, callback):
|
|
74
|
+
"""Add the given callback to the message notification stream.
|
|
75
|
+
:param callback:
|
|
76
|
+
Function to call when message is received.
|
|
77
|
+
"""
|
|
78
|
+
self._ecu.subscribe(callback, self.message_acceptable)
|
|
79
|
+
|
|
80
|
+
def unsubscribe(self, callback):
|
|
81
|
+
"""Stop listening for message.
|
|
82
|
+
:param callback:
|
|
83
|
+
Function to call when message is received.
|
|
84
|
+
"""
|
|
85
|
+
self._ecu.unsubscribe(callback)
|
|
86
|
+
|
|
87
|
+
def subscribe_request(self, callback):
|
|
88
|
+
"""Add the given callback to the request notification stream.
|
|
89
|
+
:param callback: Function to call when a request is received.
|
|
90
|
+
"""
|
|
91
|
+
self._subscribers_request.append(callback)
|
|
92
|
+
|
|
93
|
+
def unsubscribe_request(self, callback):
|
|
94
|
+
"""Remove the given callback to the request notification stream.
|
|
95
|
+
:param callback: Function to call when a request is received.
|
|
96
|
+
"""
|
|
97
|
+
self._subscribers_request.remove(callback)
|
|
98
|
+
|
|
99
|
+
def subscribe_acknowledge(self, callback):
|
|
100
|
+
"""Add the given callback from the acknowledge notification stream
|
|
101
|
+
:param callback: Function to call when an acknowledge is received.
|
|
102
|
+
"""
|
|
103
|
+
self._subscribers_acknowledge.append(callback)
|
|
104
|
+
|
|
105
|
+
def unsubscribe_acknowledge(self, callback):
|
|
106
|
+
"""Remove the given callback from the request notification stream.
|
|
107
|
+
:param callback: Function to call when an acknowledge is received.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def add_timer(self, delta_time, callback, cookie=None):
|
|
111
|
+
"""Adds a callback to the list of timer events
|
|
112
|
+
:param delta_time:
|
|
113
|
+
The time in seconds after which the event is to be triggered.
|
|
114
|
+
:param callback:
|
|
115
|
+
The callback function to call
|
|
116
|
+
"""
|
|
117
|
+
self._ecu.add_timer(delta_time, callback, cookie)
|
|
118
|
+
|
|
119
|
+
def remove_timer(self, callback):
|
|
120
|
+
"""Removes ALL entries from the timer event list for the given callback
|
|
121
|
+
:param callback:
|
|
122
|
+
The callback to be removed from the timer event list
|
|
123
|
+
"""
|
|
124
|
+
self._ecu.remove_timer(callback)
|
|
125
|
+
|
|
126
|
+
def register_dependent(self, dependent):
|
|
127
|
+
"""Register a helper whose ``stop()`` should be called on ECU shutdown.
|
|
128
|
+
|
|
129
|
+
Convenience forwarder to :meth:`ElectronicControlUnit.register_dependent`
|
|
130
|
+
for helpers that only hold a reference to a CA.
|
|
131
|
+
|
|
132
|
+
:param dependent:
|
|
133
|
+
Any object exposing a no-arg ``stop()`` method.
|
|
134
|
+
"""
|
|
135
|
+
self._ecu.register_dependent(dependent)
|
|
136
|
+
|
|
137
|
+
def unregister_dependent(self, dependent):
|
|
138
|
+
"""Remove a previously-registered dependent.
|
|
139
|
+
|
|
140
|
+
Convenience forwarder to
|
|
141
|
+
:meth:`ElectronicControlUnit.unregister_dependent`.
|
|
142
|
+
|
|
143
|
+
:param dependent:
|
|
144
|
+
The object previously passed to :meth:`register_dependent`.
|
|
145
|
+
"""
|
|
146
|
+
self._ecu.unregister_dependent(dependent)
|
|
147
|
+
|
|
148
|
+
def start(self, claim_delay=0.5):
|
|
149
|
+
"""Starts the CA
|
|
150
|
+
:param claim_delay:
|
|
151
|
+
The time in seconds to wait before starting the address claim procedure.
|
|
152
|
+
"""
|
|
153
|
+
# TODO raise RuntimeError("Can't start CA. Seems to be already running.")? or just ignore?
|
|
154
|
+
# check if we are not already started and there is an ecu connected
|
|
155
|
+
if self._ecu and not self.started:
|
|
156
|
+
self._started = True
|
|
157
|
+
self._ecu.add_timer(claim_delay, self._process_claim_async)
|
|
158
|
+
|
|
159
|
+
def stop(self):
|
|
160
|
+
"""Stops the CA
|
|
161
|
+
"""
|
|
162
|
+
# check if we are already started and there is an ecu connected
|
|
163
|
+
if self._ecu and self.started:
|
|
164
|
+
self._started = False
|
|
165
|
+
self._ecu.remove_timer(self._process_claim_async)
|
|
166
|
+
|
|
167
|
+
def _process_claim_async(self, cookie):
|
|
168
|
+
time_to_sleep = 0.500
|
|
169
|
+
if self._device_address_state == ControllerApplication.State.NONE:
|
|
170
|
+
if self._device_address_preferred != None:
|
|
171
|
+
self._device_address_announced = self._device_address_preferred
|
|
172
|
+
self._send_address_claimed(self._device_address_announced)
|
|
173
|
+
if self._device_address_announced > 127 and self._device_address_announced < 248:
|
|
174
|
+
self._device_address_state = ControllerApplication.State.WAIT_VETO
|
|
175
|
+
time_to_sleep = ControllerApplication.ClaimTimeout.VETO
|
|
176
|
+
else:
|
|
177
|
+
# addresses from 0..127 and 248..253 should start immediately
|
|
178
|
+
self._device_address = self._device_address_announced
|
|
179
|
+
self._device_address_state = ControllerApplication.State.NORMAL
|
|
180
|
+
elif self._device_address_state == ControllerApplication.State.WAIT_VETO:
|
|
181
|
+
# if we reach this phase, there was no VETO to our address claimed message so far
|
|
182
|
+
self._device_address = self._device_address_announced
|
|
183
|
+
self._device_address_state = ControllerApplication.State.NORMAL
|
|
184
|
+
elif self._device_address_state == ControllerApplication.State.NORMAL:
|
|
185
|
+
# do nothing
|
|
186
|
+
pass
|
|
187
|
+
elif self._device_address_state == ControllerApplication.State.CANNOT_CLAIM:
|
|
188
|
+
# do nothing
|
|
189
|
+
pass
|
|
190
|
+
# add new event with (possibly) new timeout value
|
|
191
|
+
self._ecu.add_timer(time_to_sleep, self._process_claim_async)
|
|
192
|
+
# returning false deletes the event from the list
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
def _process_addressclaim(self, mid, data, timestamp):
|
|
196
|
+
"""Processes an address claim message
|
|
197
|
+
:param j1939.MessageId mid:
|
|
198
|
+
A MessageId object holding the information extracted from the can_id.
|
|
199
|
+
:param bytearray data:
|
|
200
|
+
The data contained in the can-message.
|
|
201
|
+
:param float timestamp:
|
|
202
|
+
The timestamp the message was received (mostly) in fractions of Epoch-Seconds.
|
|
203
|
+
"""
|
|
204
|
+
src_address = mid.source_address
|
|
205
|
+
logger.debug("Received ADDRESS CLAIMED message from source '%d'", src_address)
|
|
206
|
+
|
|
207
|
+
# are we awaiting this address claimed message?
|
|
208
|
+
if (0
|
|
209
|
+
or (self._device_address_state == ControllerApplication.State.NORMAL and src_address == self._device_address)
|
|
210
|
+
or (self._device_address_state == ControllerApplication.State.WAIT_VETO and src_address == self._device_address_announced)
|
|
211
|
+
):
|
|
212
|
+
|
|
213
|
+
logger.info("Received ADDRESS CLAIMED message with conflicting address '%d'", src_address)
|
|
214
|
+
|
|
215
|
+
contenders_name = j1939.Name(bytes = data)
|
|
216
|
+
|
|
217
|
+
if self._name.value == contenders_name.value:
|
|
218
|
+
# both have the same name - this could mean that we are the device or there is a duplicate
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
if self._name.value > contenders_name.value:
|
|
222
|
+
# we have to release our address and claim another one
|
|
223
|
+
logger.info("We have to release our address '%d' because the contenders name is less than ours", src_address)
|
|
224
|
+
# TODO: are there any state variables we have to care about?
|
|
225
|
+
self._device_address = j1939.ParameterGroupNumber.Address.NULL
|
|
226
|
+
# TODO: maybe we should call an overloadable function here
|
|
227
|
+
if self._name.arbitrary_address_capable == False:
|
|
228
|
+
# bad luck
|
|
229
|
+
logger.error("After releasing our address we are configured to stop operation (CANNOT CLAIM)")
|
|
230
|
+
self._device_address_state = ControllerApplication.State.CANNOT_CLAIM
|
|
231
|
+
self._device_address = None
|
|
232
|
+
self._send_address_claimed(j1939.ParameterGroupNumber.Address.NULL) # send CANNOT CLAIM
|
|
233
|
+
else:
|
|
234
|
+
# TODO: we should check the address range here
|
|
235
|
+
self._device_address_announced += 1
|
|
236
|
+
logger.info("Try the next address '%d'", self._device_address_announced)
|
|
237
|
+
self._send_address_claimed(self._device_address_announced)
|
|
238
|
+
# TODO: it's not possible to set the VETO-Timeout from here
|
|
239
|
+
self._device_address_state = ControllerApplication.State.WAIT_VETO
|
|
240
|
+
|
|
241
|
+
else:
|
|
242
|
+
# we have higher prio - repeat our claim message
|
|
243
|
+
logger.info("Contender lost the competition - we can keep our address")
|
|
244
|
+
if self._device_address_state == ControllerApplication.State.NORMAL:
|
|
245
|
+
# we own our address already
|
|
246
|
+
self._send_address_claimed(self._device_address)
|
|
247
|
+
else:
|
|
248
|
+
# we are in the middle of the claim-process
|
|
249
|
+
self._send_address_claimed(self._device_address_announced)
|
|
250
|
+
|
|
251
|
+
def _process_request(self, mid, dest_address, data, timestamp):
|
|
252
|
+
"""Processes a REQUEST message
|
|
253
|
+
:param j1939.MessageId mid:
|
|
254
|
+
A MessageId object holding the information extracted from the can_id.
|
|
255
|
+
:param int dest_address:
|
|
256
|
+
The destination address of the message
|
|
257
|
+
:param bytearray data:
|
|
258
|
+
The data contained in the can-message.
|
|
259
|
+
:param float timestamp:
|
|
260
|
+
The timestamp the message was received (mostly) in fractions of Epoch-Seconds.
|
|
261
|
+
"""
|
|
262
|
+
pgn = data[0] | (data[1] << 8) | (data[2] << 16)
|
|
263
|
+
src_address = mid.source_address
|
|
264
|
+
|
|
265
|
+
if (self.state != ControllerApplication.State.NORMAL) or ((self._device_address != dest_address) and (dest_address != j1939.ParameterGroupNumber.Address.GLOBAL)):
|
|
266
|
+
# only answer if
|
|
267
|
+
# - we have a valid address and
|
|
268
|
+
# - the destination_addr is ours OR the destination_addr is the GLOBAL one
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
# special case j1939.ParameterGroupNumber.PGN.ADDRESSCLAIM
|
|
272
|
+
if pgn==j1939.ParameterGroupNumber.PGN.ADDRESSCLAIM:
|
|
273
|
+
# answer the request with our name...
|
|
274
|
+
self._send_address_claimed(self._device_address)
|
|
275
|
+
else:
|
|
276
|
+
for subscriber in self._subscribers_request:
|
|
277
|
+
subscriber(src_address, dest_address, pgn)
|
|
278
|
+
|
|
279
|
+
def send_message(self, priority, parameter_group_number, data):
|
|
280
|
+
if self.state != ControllerApplication.State.NORMAL:
|
|
281
|
+
raise RuntimeError("Could not send message unless address claiming has finished")
|
|
282
|
+
|
|
283
|
+
mid = j1939.MessageId(priority=priority, parameter_group_number=parameter_group_number, source_address=self._device_address)
|
|
284
|
+
self._ecu.send_message(mid.can_id, True, data)
|
|
285
|
+
|
|
286
|
+
def send_pgn(self, data_page, pdu_format, pdu_specific, priority, data, time_limit=0, frame_format=FrameFormat.FEFF):
|
|
287
|
+
"""send a pgn
|
|
288
|
+
:param int data_page: data page
|
|
289
|
+
:param int pdu_format: pdu format
|
|
290
|
+
:param int pdu_specific: pdu specific
|
|
291
|
+
:param int priority: message priority
|
|
292
|
+
:param list data: payload, each list index represents one payload byte
|
|
293
|
+
:param time_limit: option j1939-22 multi-pg: specify a time limit in s (e.g. 0.1 == 100ms),
|
|
294
|
+
after this time, the multi-pg will be sent. several pgs can thus be combined in one multi-pg.
|
|
295
|
+
0 or no time-limit means immediate sending.
|
|
296
|
+
"""
|
|
297
|
+
if self.state != ControllerApplication.State.NORMAL:
|
|
298
|
+
raise RuntimeError("Could not send message unless address claiming has finished")
|
|
299
|
+
|
|
300
|
+
return self._ecu.send_pgn(data_page, pdu_format, pdu_specific, priority, self._device_address, data, time_limit, frame_format)
|
|
301
|
+
|
|
302
|
+
def send_request(self, data_page, pgn, destination):
|
|
303
|
+
"""send a request message
|
|
304
|
+
:param int data_page: data page
|
|
305
|
+
:param int pgn: pgn to be requested
|
|
306
|
+
:param list data: destination address
|
|
307
|
+
"""
|
|
308
|
+
if self.state != ControllerApplication.State.NORMAL:
|
|
309
|
+
if pgn != j1939.ParameterGroupNumber.PGN.ADDRESSCLAIM:
|
|
310
|
+
raise RuntimeError("Could not send request message unless address claiming has finished")
|
|
311
|
+
source_address = j1939.ParameterGroupNumber.Address.NULL
|
|
312
|
+
else:
|
|
313
|
+
source_address = self._device_address
|
|
314
|
+
|
|
315
|
+
data = [(pgn & 0xFF), ((pgn >> 8) & 0xFF), ((pgn >> 16) & 0xFF)]
|
|
316
|
+
self._ecu.send_pgn(data_page, (j1939.ParameterGroupNumber.PGN.REQUEST >> 8) & 0xFF, destination & 0xFF, 6, source_address, data)
|
|
317
|
+
|
|
318
|
+
def _send_address_claimed(self, address):
|
|
319
|
+
# TODO: Normally the (initial) address claimed message must not be an auto repeat message.
|
|
320
|
+
# We have to use a single-shot message instead!
|
|
321
|
+
# After a (send-)error occurs we have to wait 0..153 msec before repeating.
|
|
322
|
+
pgn = j1939.ParameterGroupNumber(0, 238, j1939.ParameterGroupNumber.Address.GLOBAL)
|
|
323
|
+
mid = j1939.MessageId(priority=6, parameter_group_number=pgn.value, source_address=address)
|
|
324
|
+
data = self._name.bytes
|
|
325
|
+
self._ecu.send_message(mid.can_id, True, data)
|
|
326
|
+
|
|
327
|
+
def on_request(self, src_address, dest_address, pgn):
|
|
328
|
+
"""Callback for PGN requests
|
|
329
|
+
:param int src_address:
|
|
330
|
+
The address the request comes from
|
|
331
|
+
:param int dest_address:
|
|
332
|
+
The address the request was sent to; normally ours, but can also be GLOBAL
|
|
333
|
+
:param int pgn:
|
|
334
|
+
Parameter Group Number requested
|
|
335
|
+
"""
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
def message_acceptable(self, dest_address):
|
|
339
|
+
"""Indicates if this CA would accept a message
|
|
340
|
+
This function indicates the acceptance of this CA for the given dest_address.
|
|
341
|
+
"""
|
|
342
|
+
if self.state != j1939.ControllerApplication.State.NORMAL:
|
|
343
|
+
return False
|
|
344
|
+
if dest_address == j1939.ParameterGroupNumber.Address.GLOBAL:
|
|
345
|
+
return True
|
|
346
|
+
return (self.device_address == dest_address)
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def state(self):
|
|
350
|
+
return self._device_address_state
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def device_address(self):
|
|
354
|
+
if self.state != j1939.ControllerApplication.State.NORMAL:
|
|
355
|
+
return j1939.ParameterGroupNumber.Address.NULL
|
|
356
|
+
return self._device_address
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def started(self) -> bool:
|
|
360
|
+
"""
|
|
361
|
+
Getter for the started property
|
|
362
|
+
"""
|
|
363
|
+
return self._started
|