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/memory_access.py ADDED
@@ -0,0 +1,387 @@
1
+ from enum import Enum
2
+ import logging
3
+ import threading
4
+ import j1939
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class DMState(Enum):
9
+ IDLE = 1
10
+ REQUEST_STARTED = 2
11
+ WAIT_RESPONSE = 3
12
+ WAIT_QUERY = 4
13
+ SERVER_CLEANUP = 5
14
+
15
+
16
+ class MemoryAccess:
17
+ def __init__(self, ca: j1939.ControllerApplication) -> None:
18
+ """
19
+ Makes an overarching Memory access class
20
+
21
+ Spawns a background servicer thread tied to the lifetime of this
22
+ instance. Call :meth:`stop` (or use the instance as a context
23
+ manager) when done. The instance is also registered as a dependent
24
+ of the parent ECU, so ``ecu.stop()`` will cascade and tear this
25
+ instance down automatically.
26
+
27
+ :param ca: Controller Application
28
+ """
29
+ self._ca = ca
30
+ self.query = j1939.Dm14Query(ca)
31
+ self.server = j1939.DM14Server(ca)
32
+ self._proceed_event = threading.Event()
33
+ self._ca.subscribe(self._listen_for_dm14)
34
+ self.state = DMState.IDLE
35
+ self.seed_security = False
36
+ self._notify_query_received = None
37
+ self._proceed_function = None
38
+
39
+ self._stopped = False
40
+ self._stop_lock = threading.Lock()
41
+ self._job_thread_end = threading.Event()
42
+ self._job_thread = threading.Thread(target=self._servicer, name='j1939.memory_access servicer_thread')
43
+ # A thread can be flagged as a "daemon thread". The significance of
44
+ # this flag is that the entire Python program exits when only daemon
45
+ # threads are left.
46
+ self._job_thread.daemon = True
47
+ self._job_thread.start()
48
+
49
+ # Register with the parent ECU so ecu.stop() cascades to this instance.
50
+ # Done after the thread has started so a failed registration during
51
+ # shutdown is still recoverable by the user calling stop() directly.
52
+ try:
53
+ self._ca.register_dependent(self)
54
+ except Exception:
55
+ # If registration fails (e.g. ECU already stopping) we still want
56
+ # the user to be able to stop us manually; just log and continue.
57
+ logger.exception("Failed to register MemoryAccess with ECU")
58
+
59
+ def stop(self, timeout: float = 2.0) -> None:
60
+ """Stop the background servicer thread and release resources.
61
+
62
+ Idempotent: subsequent calls are no-ops. Safe to call from any
63
+ thread, including from inside ``ecu.stop()``'s cascade.
64
+
65
+ :param float timeout:
66
+ Maximum time in seconds to wait for the servicer thread to exit.
67
+ """
68
+ with self._stop_lock:
69
+ if self._stopped:
70
+ return
71
+ self._stopped = True
72
+
73
+ # Signal shutdown and wake the servicer immediately so it does not
74
+ # have to wait out its full poll interval.
75
+ self._job_thread_end.set()
76
+ self._proceed_event.set()
77
+
78
+ if self._job_thread.is_alive():
79
+ self._job_thread.join(timeout=timeout)
80
+
81
+ # Best-effort cleanup of the CA-level subscription. If the CA/ECU
82
+ # is already torn down this may raise; that is fine.
83
+ try:
84
+ self._ca.unsubscribe(self._listen_for_dm14)
85
+ except Exception:
86
+ pass
87
+
88
+ # Best-effort removal from the ECU's dependent registry. If we are
89
+ # being called from inside the cascade this is a no-op (the registry
90
+ # has already been cleared); if we are being called explicitly it
91
+ # prevents a stale reference.
92
+ try:
93
+ self._ca.unregister_dependent(self)
94
+ except Exception:
95
+ pass
96
+
97
+ def __enter__(self):
98
+ return self
99
+
100
+ def __exit__(self, exc_type, exc, tb):
101
+ self.stop()
102
+ return False
103
+
104
+ def __del__(self):
105
+ # Defensive backstop only. The primary cleanup paths are explicit
106
+ # stop() / context-manager exit / ecu.stop() cascade. Guard against
107
+ # partial __init__ (where _job_thread may not exist) and swallow all
108
+ # exceptions per the __del__ contract.
109
+ try:
110
+ if getattr(self, '_job_thread', None) is None:
111
+ return
112
+ self.stop()
113
+ except Exception:
114
+ pass
115
+
116
+ def _servicer(self):
117
+ """
118
+ Job thread to service memory access requests.
119
+
120
+ Blocks on a threading.Event instead of busy-polling
121
+ """
122
+ while not self._job_thread_end.is_set():
123
+ triggered = self._proceed_event.wait(timeout=1.0)
124
+ if self._job_thread_end.is_set():
125
+ return
126
+ if triggered and self.state == DMState.WAIT_RESPONSE:
127
+ self._proceed_event.clear()
128
+ if self._notify_query_received is not None:
129
+ self._notify_query_received() # notify incoming request
130
+
131
+
132
+ def _handle_error(self, priority: int, pgn: int, sa: int, timestamp: int, data: bytearray, error_code: int) -> None:
133
+ """
134
+ Handles errors by resetting the state and unsubscribing from DM14 messages
135
+
136
+ :param priority: Priority of the message
137
+ :param pgn: Parameter Group Number of the message
138
+ :param sa: Source Address of the message
139
+ :param timestamp: Timestamp of the message
140
+ :param data: Data of the PDU
141
+ :param error_code: Error code to be set
142
+ """
143
+ self.server.error = error_code
144
+ self.server.set_busy(True)
145
+ self.server.parse_dm14(
146
+ priority, pgn, sa, timestamp, data
147
+ )
148
+ self.server.set_busy(False)
149
+ self.reset()
150
+
151
+ def _listen_for_dm14(
152
+ self, priority: int, pgn: int, sa: int, timestamp: int, data: bytearray
153
+ ) -> None:
154
+ """
155
+ Listens for dm14 messages and passes them to the appropriate function
156
+
157
+ :param priority: Priority of the message
158
+ :param pgn: Parameter Group Number of the message
159
+ :param sa: Source Address of the message
160
+ :param timestamp: Timestamp of the message
161
+ :param data: Data of the PDU
162
+ """
163
+ if pgn == j1939.ParameterGroupNumber.PGN.DM14:
164
+ match self.state:
165
+ case DMState.IDLE:
166
+ if self.server.state.value == DMState.IDLE.value:
167
+ self.state = DMState.REQUEST_STARTED
168
+ self.server.parse_dm14(priority, pgn, sa, timestamp, data)
169
+ if not self.seed_security:
170
+ self.state = DMState.WAIT_RESPONSE
171
+ self._ca.unsubscribe(self._listen_for_dm14)
172
+ if self._proceed_function is not None:
173
+ proceed = self._proceed_function(
174
+ self.server.command,
175
+ int.from_bytes(
176
+ bytes=self.server.address,
177
+ byteorder="little",
178
+ signed=False,
179
+ ),
180
+ self.server.pointer_type,
181
+ self.server.length,
182
+ self.server.object_count,
183
+ 0xFFFF, # placeholder for key
184
+ self.server.sa,
185
+ self.server.access_level,
186
+ 0x0, # placeholder for seed
187
+ ) # call proceed function and pass in basic parameters
188
+ if not proceed:
189
+ self._handle_error(priority, pgn, sa, timestamp, data, 0x100)
190
+ else:
191
+ self._proceed_event.set()
192
+ else:
193
+ self._proceed_event.set() # no security, so always proceed
194
+
195
+ case DMState.REQUEST_STARTED:
196
+ self.server.parse_dm14(priority, pgn, sa, timestamp, data)
197
+ if self.server.state == j1939.ResponseState.SEND_PROCEED:
198
+ self.state = DMState.WAIT_RESPONSE
199
+ if self.seed_security:
200
+ if self.server.verify_key(
201
+ self.server.seed, self.server.key
202
+ ):
203
+ if self._proceed_function is not None:
204
+ proceed = self._proceed_function(
205
+ self.server.command,
206
+ int.from_bytes(
207
+ bytes=self.server.address,
208
+ byteorder="little",
209
+ signed=False,
210
+ ),
211
+ self.server.pointer_type,
212
+ self.server.length,
213
+ self.server.object_count,
214
+ self.server.key,
215
+ self.server.sa,
216
+ self.server.access_level,
217
+ self.server.seed,
218
+ ) # call proceed function and pass in basic parameters
219
+ if not proceed:
220
+ self._handle_error(priority, pgn, sa, timestamp, data, 0x100)
221
+ else:
222
+ self._proceed_event.set()
223
+ else:
224
+ self._proceed_event.set() # no proceed function, so always proceed
225
+ else:
226
+ self._handle_error(priority, pgn, sa, timestamp, data, 0x1003)
227
+
228
+ case DMState.WAIT_QUERY:
229
+ self.server.set_busy(True)
230
+ self.server.parse_dm14(priority, pgn, sa, timestamp, data)
231
+ self.server.set_busy(False)
232
+
233
+ case DMState.SERVER_CLEANUP:
234
+ self.state = DMState.IDLE
235
+ case _:
236
+ pass
237
+
238
+ def respond(
239
+ self,
240
+ proceed: bool,
241
+ data: list = None,
242
+ error: int = 0xFFFFFF,
243
+ edcp: int = 0xFF,
244
+ max_timeout: int = 3,
245
+ ) -> list:
246
+ """
247
+ Responds with requested data and error code, if applicable, to a read request
248
+
249
+ :param bool proceed: whether the operation is good to proceed
250
+ :param list data: data to be sent to device
251
+ :param int error: error code to be sent to device
252
+ :param int edcp: value for edcp extension
253
+ :param int max_timeout: max timeout for transaction
254
+ """
255
+ if data is None:
256
+ data = []
257
+
258
+ if self.state is not DMState.WAIT_RESPONSE:
259
+ return data
260
+
261
+ self._proceed_event.clear()
262
+ self._ca.unsubscribe(self._listen_for_dm14)
263
+ return_data = self.server.respond(proceed, data, error, edcp, max_timeout)
264
+ self.state = DMState.SERVER_CLEANUP if self.server.state.value != DMState.IDLE.value else DMState.IDLE
265
+ self._ca.subscribe(self._listen_for_dm14)
266
+ return return_data
267
+
268
+ def read(
269
+ self,
270
+ dest_address: int,
271
+ direct: int,
272
+ address: int,
273
+ object_count: int,
274
+ object_byte_size: int = 1,
275
+ signed: bool = False,
276
+ return_raw_bytes: bool = False,
277
+ max_timeout: int = 1,
278
+ ) -> list:
279
+ """
280
+ Make a dm14 read Query
281
+
282
+ :param int dest_address: destination address of the message
283
+ :param int direct: direct address of the message
284
+ :param int address: address of the message
285
+ :param int object_count: number of objects to be read
286
+ :param int object_byte_size: size of each object in bytes
287
+ :param bool signed: whether the data is signed
288
+ :param bool return_raw_bytes: whether to return raw bytes or values
289
+ :param int max_timeout: max timeout for transaction
290
+ """
291
+ if self.state == DMState.IDLE:
292
+ self.state = DMState.WAIT_QUERY
293
+ self.address = dest_address
294
+ data = self.query.read(
295
+ dest_address,
296
+ direct,
297
+ address,
298
+ object_count,
299
+ object_byte_size,
300
+ signed,
301
+ return_raw_bytes,
302
+ max_timeout,
303
+ )
304
+ self.reset()
305
+ return data
306
+ else:
307
+ self.reset()
308
+ raise RuntimeWarning("Process already Running")
309
+
310
+ def write(
311
+ self,
312
+ dest_address: int,
313
+ direct: int,
314
+ address: int,
315
+ values: list,
316
+ object_byte_size: int = 1,
317
+ max_timeout: int = 1,
318
+ ) -> None:
319
+ """
320
+ Send a write query to dest_address, requesting to write values at address
321
+
322
+ :param int dest_address: destination address of the message
323
+ :param int direct: direct address of the message
324
+ :param int address: address of the message
325
+ :param list values: values to be written
326
+ :param int object_byte_size: size of each object in bytes
327
+ :param int max_timeout: max timeout for transaction
328
+ """
329
+ if self.state == DMState.IDLE:
330
+ self.state = DMState.WAIT_QUERY
331
+ self.address = dest_address
332
+ self.query.write(
333
+ dest_address, direct, address, values, object_byte_size, max_timeout
334
+ )
335
+ self.reset()
336
+
337
+ def set_seed_generator(self, seed_generator: callable) -> None:
338
+ """
339
+ Sets seed generator function to use
340
+ :param seed_generator: seed generator function
341
+ """
342
+ self.server.set_seed_generator(seed_generator)
343
+
344
+ def set_seed_key_algorithm(self, algorithm: callable) -> None:
345
+ """
346
+ Sets seed-key algorithm to be used for key generation
347
+
348
+ :param callable algorithm: seed-key algorithm
349
+ """
350
+ self.seed_security = True
351
+ self.query.set_seed_key_algorithm(algorithm)
352
+ self.server.set_seed_key_algorithm(algorithm)
353
+
354
+ def set_verify_key(self, verify_key: callable) -> None:
355
+ """
356
+ Sets verify key function to be used for verifying the key
357
+
358
+ :param callable verify_key: verify key function
359
+ """
360
+ self.server.set_verify_key(verify_key)
361
+
362
+ def set_notify(self, notify: callable) -> None:
363
+ """
364
+ Sets notify function to be used for notifying the user of memory accesses
365
+
366
+ :param callable notify: notify function
367
+ """
368
+ self._notify_query_received = notify
369
+
370
+ def set_proceed(self, proceed: callable) -> None:
371
+ """
372
+ Sets proceed function to determine if a memory query is valid or not
373
+
374
+ :param callable proceed: proceed function
375
+ """
376
+ self._proceed_function = proceed
377
+
378
+ def reset(self) -> None:
379
+ """
380
+ Resets both server and query to remove transaction specific data
381
+ """
382
+ self.state = DMState.IDLE
383
+ self._ca.unsubscribe(self._listen_for_dm14)
384
+ self._ca.subscribe(self._listen_for_dm14)
385
+ self.server.reset_server()
386
+ self.query.reset_query()
387
+ self._proceed_event.clear()
j1939/message_id.py ADDED
@@ -0,0 +1,51 @@
1
+
2
+ class MessageId:
3
+ """The CAN MessageId of an PDU.
4
+
5
+ The MessageId consists of three parts:
6
+ * Priority
7
+ * Parameter Group Number
8
+ * Source Address
9
+ """
10
+
11
+ def __init__(self, **kwargs): #priority=0, parameter_group_number=0, source_address=0):
12
+ """
13
+ :param priority:
14
+ 3-bit Priority
15
+ :param parameter_group_number:
16
+ 18-bit Parameter Group Number
17
+ :param source_address:
18
+ 8-bit Source Address
19
+ There is a total of 253 addresses available and every address must
20
+ be unique within the network.
21
+
22
+ :param can_id:
23
+ A 29-bit CAN-Id the MessageId should be parsed from.
24
+ """
25
+
26
+ if 'can_id' in kwargs:
27
+ # let the property can_id parse the given value
28
+ self.can_id = kwargs.get('can_id')
29
+ else:
30
+ self.priority = kwargs.get('priority', 0) & 7
31
+ self.parameter_group_number = kwargs.get('parameter_group_number', 0) & 0x3FFFF
32
+ self.source_address = kwargs.get('source_address', 0) & 0xFF
33
+
34
+ @property
35
+ def can_id(self):
36
+ """Transforms the MessageId object to a 29 bit CAN-Id"""
37
+ return (self.priority << 26) | (self.parameter_group_number << 8) | (self.source_address)
38
+
39
+ @can_id.setter
40
+ def can_id(self, can_id):
41
+ """Fill the MessageId with the information given in the 29 bit CAN-Id"""
42
+ self.source_address = can_id & 0xFF
43
+ self.parameter_group_number = (can_id >> 8) & 0x3FFFF
44
+ self.priority = (can_id >> 26) & 0x7
45
+
46
+
47
+ class FrameFormat:
48
+ CBFF = 0 # classical base frame format
49
+ CEFF = 1 # classical extended frame format
50
+ FBFF = 2 # flexible data rate base frame format
51
+ FEFF = 3 # flexible data rate extended frame format