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.
@@ -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