pyccp 0.10.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.
pyccp/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """CAN Calibration Protocol tools; applications configure logging handlers."""
2
+
3
+ import logging
4
+
5
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
pyccp/ccp.py ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import dataclasses
5
+ import enum
6
+
7
+ MAX_CRO = 0x0008
8
+ MAX_DTO = 0x0008
9
+
10
+ CCP_VERSION = (2, 1)
11
+
12
+
13
+ @dataclasses.dataclass(frozen=True)
14
+ class Command:
15
+ """
16
+ Immutable command configuration.
17
+
18
+ Represents a command by its numeric code and the timeout value associated
19
+ with executing or handling that command. Instances are frozen by the data class
20
+ declaration, so the recorded values are intended to remain stable after
21
+ creation.
22
+
23
+ :ivar code: Numeric identifier for the command.
24
+ :ivar timeout: Timeout value associated with the command in milliseconds.
25
+ """
26
+
27
+ code: int
28
+ timeout: int
29
+
30
+
31
+ # Mandatory Commands
32
+ CONNECT = Command(0x01, 25)
33
+ GET_CCP_VERSION = Command(0x1B, 25)
34
+ EXCHANGE_ID = Command(0x17, 25)
35
+ SET_MTA = Command(0x02, 25)
36
+ DNLOAD = Command(0x03, 25)
37
+ UPLOAD = Command(0x04, 25)
38
+ GET_DAQ_SIZE = Command(0x14, 25)
39
+ SET_DAQ_PTR = Command(0x15, 25)
40
+ WRITE_DAQ = Command(0x16, 25)
41
+ START_STOP = Command(0x06, 25)
42
+ DISCONNECT = Command(0x07, 25)
43
+ # Optional Commands
44
+ GET_SEED = Command(0x12, 25)
45
+ UNLOCK = Command(0x13, 25)
46
+ DNLOAD_6 = Command(0x23, 25)
47
+ SHORT_UP = Command(0x0F, 25)
48
+ SELECT_CAL_PAGE = Command(0x11, 25)
49
+ SET_S_STATUS = Command(0x0C, 25)
50
+ GET_S_STATUS = Command(0x0D, 25)
51
+ BUILD_CHKSUM = Command(0x0E, 30000)
52
+ CLEAR_MEMORY = Command(0x10, 30000)
53
+ PROGRAM = Command(0x18, 100)
54
+ PROGRAM_6 = Command(0x22, 100)
55
+ MOVE = Command(0x19, 30000)
56
+ TEST = Command(0x05, 25)
57
+ GET_ACTIVE_CAL_PAGE = Command(0x09, 25)
58
+ START_STOP_ALL = Command(0x08, 25)
59
+ DIAG_SERVICE = Command(0x20, 500)
60
+ ACTION_SERVICE = Command(0x21, 5000)
61
+
62
+
63
+ class ReturnCodes(enum.IntEnum):
64
+ ACKNOWLEDGE = 0x00
65
+ DAQ_PROCESSOR_OVERLOAD = 0x01 # C0
66
+ COMMAND_PROCESSOR_BUSY = 0x10 # C1 NONE (wait until ACK or timeout)
67
+ DAQ_PROCESSOR_BUSY = 0x11 # C1 NONE (wait until ACK or timeout)
68
+ INTERNAL_TIMEOUT = 0x12 # C1 NONE (wait until ACK or timeout)
69
+ KEY_REQUEST = 0x18 # C1 NONE (embedded seed&key)
70
+ SESSION_STATUS_REQUEST = 0x19 # C1 NONE (embedded SET_S_STATUS)
71
+ COLD_START_REQUEST = 0x20 # C2 COLD START
72
+ CAL_DATA_INIT_REQUEST = 0x21 # C2 cal. data initialization
73
+ DAQ_LIST_INIT_REQUEST = 0x22 # C2 DAQ list initialization
74
+ CODE_UPDATE_REQUEST = 0x23 # C2 (COLD START)
75
+ UNKNOWN_COMMAND = 0x30 # C3 (FAULT)
76
+ COMMAND_SYNTAX = 0x31 # C3 FAULT
77
+ PARAMETER_OUT_OF_RANGE = 0x32 # C3 FAULT
78
+ ACCESS_DENIED = 0x33 # C3 FAULT
79
+ OVERLOAD = 0x34 # C3 FAULT
80
+ ACCESS_LOCKED = 0x35 # C3 FAULT
81
+ RESOURCE_FUNCTION_NOT_AVAILABLE = 0x36 # C3 FAULT
82
+
83
+
84
+ class DisconnectType(enum.IntEnum):
85
+ TEMPORARY = 0x00
86
+ PERMANENT = 0x01
87
+
88
+
89
+ class SecondaryResource(enum.IntEnum):
90
+ """
91
+ Enumeration for secondary resource types.
92
+
93
+ This enumeration defines constants representing different types of secondary
94
+ resources that can be used or required in various systems or operations.
95
+
96
+ :cvar CAL: Represents the calibration resource.
97
+ :cvar DAQ: Represents the data acquisition resource.
98
+ :cvar PGM: Represents the programming resource.
99
+ """
100
+
101
+ CAL = 1 << 0
102
+ DAQ = 1 << 1
103
+ PGM = 1 << 6
104
+
105
+
106
+ class DTO:
107
+ class Type(enum.IntEnum):
108
+ COMMAND_RETURN_MESSAGE = 255
109
+ EVENT_MESSAGE = 254
110
+
111
+
112
+ def verify_ctr(ctr: int, response: bytes) -> bool:
113
+ if len(response) < 3:
114
+ return False
115
+
116
+ return ctr == response[2]
pyccp/master.py ADDED
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import ctypes
5
+ import logging
6
+ import struct
7
+ import threading
8
+ import time
9
+ from collections.abc import Callable
10
+ from concurrent.futures import Future
11
+ from threading import Event, Thread
12
+ from types import TracebackType
13
+ from typing import Optional
14
+
15
+ import can
16
+ from can import Message
17
+
18
+ from . import ccp
19
+ from .ccp import ReturnCodes, SecondaryResource
20
+
21
+ MTA0 = 0
22
+ MTA1 = 1
23
+
24
+
25
+ def _unsigned(name: str, value: int, bits: int) -> int:
26
+ if not isinstance(value, int) or not 0 <= value < (1 << bits):
27
+ raise ValueError(
28
+ f"{name} must be an unsigned {bits}-bit integer, got {value!r}"
29
+ )
30
+
31
+ return value
32
+
33
+
34
+ def _payload(data: bytes, minimum: int, maximum: int):
35
+ """Normalize byte sequences without interpreting integers as zero-filled data."""
36
+ if isinstance(data, (int, str)):
37
+ raise TypeError("Data must be a bytes-like object or byte sequence")
38
+
39
+ payload = bytes(data)
40
+
41
+ if not minimum <= len(payload) <= maximum:
42
+ raise ValueError(
43
+ f"Data length must be between {minimum} and {maximum}, got {len(payload)}"
44
+ )
45
+
46
+ return payload
47
+
48
+
49
+ def _transfer_size(size: int):
50
+ if not isinstance(size, int) or not 1 <= size <= 5:
51
+ raise ValueError(f"Size must be between 1 and 5, got {size!r}")
52
+
53
+ return size
54
+
55
+
56
+ def _pack_address(address: int):
57
+ return struct.pack("<L", _unsigned("Address", address, 32))
58
+
59
+
60
+ class CommandResponse:
61
+ __slots__ = ("code", "data")
62
+
63
+ def __init__(self, raw_data: bytes):
64
+ if len(raw_data) < 3:
65
+ raise ValueError("Raw data must be at least 3 bytes long")
66
+
67
+ self.code = ReturnCodes(raw_data[1])
68
+ self.data = raw_data[3:]
69
+
70
+
71
+ class DAQMessage:
72
+ __slots__ = ("daq_id", "pid", "data")
73
+
74
+ def __init__(self, message: Message):
75
+ self.daq_id = message.arbitration_id
76
+ self.pid = message.data[0]
77
+ self.data = message.data[1:]
78
+
79
+
80
+ DAQCallback = Callable[[DAQMessage], None]
81
+ EventCallback = Callable[[CommandResponse], None]
82
+
83
+
84
+ class Master:
85
+ class _Receiver(Thread):
86
+ def __init__(self, master: "Master"):
87
+ super().__init__(daemon=True)
88
+ self._master = master
89
+ self._stop_event = Event()
90
+
91
+ def run(self):
92
+ while True:
93
+ message = self._master.get_raw_data(timeout=0.1)
94
+
95
+ if message is not None:
96
+ self._master._on_message(message)
97
+
98
+ if self._stop_event.is_set():
99
+ break
100
+
101
+ def stop(self) -> None:
102
+ self._stop_event.set()
103
+ self.join()
104
+
105
+ def __init__(self, bus: can.BusABC, cro: int, dto: int):
106
+ """CCP Master implementation for sending and receiving messages over
107
+ a CAN Calibration Protocol.
108
+
109
+ :param bus: CAN Bus object for sending and receiving messages.
110
+ :type bus: `can.BusABC`
111
+ :param cro: The 11-bit or 29-bit identifier for outgoing CAN message from
112
+ the host.
113
+ :type cro: int
114
+ :param dto: The 11-bit or 29-bit identifier for incoming CAN message from
115
+ a slave.
116
+ :type dto: int
117
+ """
118
+ self.transport = bus
119
+ self.ctr = ctypes.c_uint8(0)
120
+ self.logger = logging.getLogger(__name__)
121
+ self._cro = cro
122
+ self._dto = dto
123
+ self._lock = threading.Lock()
124
+ self._receiver: Optional[Master._Receiver] = None
125
+ self._on_event: Optional[EventCallback] = None
126
+ self._on_daq: dict[int, DAQCallback] = {}
127
+ self._pending_requests: dict[int, Future] = {}
128
+
129
+ def __enter__(self) -> "Master":
130
+ if self._receiver is None:
131
+ self._receiver = Master._Receiver(self)
132
+
133
+ self._receiver.start()
134
+
135
+ return self
136
+
137
+ def __exit__(
138
+ self,
139
+ exc_type: type[BaseException] | None,
140
+ exc_val: BaseException | None,
141
+ exc_tb: TracebackType | None,
142
+ ) -> None:
143
+ self._shutdown()
144
+
145
+ def _shutdown(self) -> None:
146
+ if self._receiver is not None:
147
+ self._receiver.stop()
148
+ self._receiver = None
149
+
150
+ self.transport.shutdown()
151
+
152
+ def on_event(self, callback: EventCallback) -> None:
153
+ with self._lock:
154
+ self._on_event = callback
155
+
156
+ def on_daq(self, daq_id: int, callback: DAQCallback) -> None:
157
+ with self._lock:
158
+ self._on_daq[daq_id] = callback
159
+
160
+ def send_cro(self, cmd: ccp.Command, ctr, *data: int) -> int:
161
+ """
162
+ Send a Command Receive Object (CRO) message via the transport layer.
163
+
164
+ This method constructs the CRO data frame using the provided
165
+ Command Specifier (cmd), counter value (ctr), and additional data bytes.
166
+ The data frame is then sent using the transport layer. It also ensures
167
+ that the control byte value increments properly after each successful
168
+ transmission.
169
+
170
+ :param cmd: Command Specifier byte used in the CCP protocol.
171
+ :type cmd: int
172
+ :param ctr: Control byte used for controlling the flow of messages.
173
+ :type ctr: int
174
+ :param data: Up to six additional data bytes for the message payload.
175
+ :return: The transmitted counter (the internal counter advances after sending).
176
+ :rtype: int
177
+ :raises ValueError: If more than six data bytes are provided.
178
+ """
179
+ if len(data) > 6:
180
+ raise ValueError(f"Maximum 6 data bytes allowed, got {len(data)}")
181
+
182
+ data = bytearray((cmd.code, ctr, *data))
183
+
184
+ if len(data) < ccp.MAX_CRO:
185
+ data.extend(bytearray(ccp.MAX_CRO - len(data)))
186
+
187
+ msg = Message(arbitration_id=self._cro, data=data, is_rx=False)
188
+ self.logger.debug(f"Command: [{cmd}]")
189
+ self.logger.debug(f"Sending message: {msg}")
190
+ self.transport.send(msg)
191
+ self.ctr = ctypes.c_uint8(self.ctr.value + 1)
192
+
193
+ return ctr
194
+
195
+ def _on_message(self, message: Message) -> None:
196
+ resource_id = message.data[0]
197
+
198
+ if resource_id == ccp.DTO.Type.COMMAND_RETURN_MESSAGE:
199
+ self._handle_response(message)
200
+ elif resource_id == ccp.DTO.Type.EVENT_MESSAGE:
201
+ self._handle_event(message)
202
+ elif resource_id in self._on_daq:
203
+ self._handle_daq(message)
204
+
205
+ def _handle_response(self, message: Message) -> None:
206
+ """Handle a response message by verifying the control byte and
207
+ processing the response.
208
+ """
209
+ ctr = message.data[2]
210
+
211
+ with self._lock:
212
+ future = self._pending_requests.pop(ctr, None)
213
+
214
+ if future is not None:
215
+ future.set_result(CommandResponse(message.data))
216
+ else:
217
+ self.logger.warning(f"Unexpected response received: {message}")
218
+
219
+ def _handle_daq(self, message: Message) -> None:
220
+ """Handle a DAQ message by invoking the registered callback for the DAQ ID."""
221
+ daq_id = message.arbitration_id
222
+
223
+ with self._lock:
224
+ callback = self._on_daq.get(daq_id)
225
+
226
+ if callback is not None:
227
+ self.logger.debug(f"Handling DAQ message with ID: {daq_id}")
228
+ callback(DAQMessage(message))
229
+
230
+ def _handle_event(self, message: Message) -> None:
231
+ """Handle an event message by invoking the registered event callback."""
232
+ with self._lock:
233
+ if self._on_event is not None:
234
+ self.logger.debug(f"Handling event message: {message}")
235
+ self._on_event(CommandResponse(message.data))
236
+
237
+ def _get_data(self, timeout: Optional[float] = None) -> Optional[Message]:
238
+ """Receive the first available message within `timeout` seconds."""
239
+ if timeout is None:
240
+ return self.transport.recv(timeout=None)
241
+
242
+ deadline = time.monotonic() + timeout
243
+
244
+ while True:
245
+ residual = deadline - time.monotonic()
246
+
247
+ if residual <= 0:
248
+ return None
249
+
250
+ message = self.transport.recv(timeout=residual)
251
+
252
+ if message is not None:
253
+ return message
254
+
255
+ def get_data(self, timeout: Optional[float] = None) -> Optional[Message]:
256
+ """Return the first message addressed to the DTO id within `timeout` seconds."""
257
+ if timeout is None:
258
+ deadline = None
259
+ else:
260
+ deadline = time.monotonic() + timeout
261
+
262
+ while True:
263
+ if deadline is None:
264
+ residual = None
265
+ else:
266
+ residual = deadline - time.monotonic()
267
+
268
+ if residual <= 0:
269
+ break
270
+
271
+ message = self._get_data(residual)
272
+
273
+ if message is None:
274
+ break
275
+
276
+ if message.arbitration_id == self._dto:
277
+ self.logger.debug(f"Received message: {message}")
278
+ return message
279
+
280
+ self.logger.warning(f"Nothing received after: {timeout} seconds")
281
+
282
+ return None
283
+
284
+ def get_raw_data(self, timeout: Optional[float] = None) -> Optional[Message]:
285
+ return self._get_data(timeout)
286
+
287
+ def _transaction(
288
+ self,
289
+ timeout: int,
290
+ command: ccp.Command,
291
+ ctr: int,
292
+ *data: int,
293
+ ) -> Optional[CommandResponse]:
294
+ future = Future()
295
+
296
+ with self._lock:
297
+ ctr = self.send_cro(command, ctr, *data)
298
+ self._pending_requests[ctr] = future
299
+
300
+ try:
301
+ return future.result(timeout=timeout / 1000.0)
302
+ except TimeoutError:
303
+ self.logger.warning(f"Transaction timed out after {timeout} ms")
304
+
305
+ with self._lock:
306
+ self._pending_requests.pop(ctr, None)
307
+
308
+ return None
309
+
310
+ def _execute(
311
+ self, command: ccp.Command, *payload: int
312
+ ) -> Optional[CommandResponse]:
313
+ """Resolve transaction metadata once; public commands only encode payloads."""
314
+ return self._transaction(command.timeout, command, self.ctr.value, *payload)
315
+
316
+ # Mandatory Commands.
317
+ def connect(self, address: int) -> Optional[CommandResponse]:
318
+ address = struct.pack("<H", _unsigned("Station address", address, 16))
319
+ return self._execute(ccp.CONNECT, *address)
320
+
321
+ def get_ccp_version(
322
+ self, major: int = ccp.CCP_VERSION[0], minor: int = ccp.CCP_VERSION[1]
323
+ ) -> Optional[CommandResponse]:
324
+ return self._execute(
325
+ ccp.GET_CCP_VERSION,
326
+ major,
327
+ minor,
328
+ )
329
+
330
+ def exchange_id(self, info: bytes) -> Optional[CommandResponse]:
331
+ if len(info) > 6:
332
+ raise ValueError(f"Maximum 6 bytes allowed, got {len(info)}")
333
+
334
+ return self._execute(ccp.EXCHANGE_ID, *info)
335
+
336
+ def set_mta(
337
+ self, address: int, address_extension: int = 0x00, mta: int = MTA0
338
+ ) -> Optional[CommandResponse]:
339
+ address = _pack_address(address)
340
+ _unsigned("Address extension", address_extension, 8)
341
+
342
+ if mta not in (MTA0, MTA1):
343
+ raise ValueError(f"MTA must be 0 or 1, got {mta!r}")
344
+
345
+ return self._execute(
346
+ ccp.SET_MTA,
347
+ mta,
348
+ address_extension,
349
+ *address,
350
+ )
351
+
352
+ def dnload(self, size: int, data: bytes) -> Optional[CommandResponse]:
353
+ _transfer_size(size)
354
+ data = _payload(data, size, size)
355
+
356
+ return self._execute(
357
+ ccp.DNLOAD,
358
+ size,
359
+ *data,
360
+ )
361
+
362
+ def upload(self, size: int) -> Optional[CommandResponse]:
363
+ _transfer_size(size)
364
+
365
+ return self._execute(
366
+ ccp.UPLOAD,
367
+ size,
368
+ )
369
+
370
+ def get_daq_size(
371
+ self, daq_list_number: int, address: int
372
+ ) -> Optional[CommandResponse]:
373
+ address = _pack_address(address)
374
+
375
+ return self._execute(
376
+ ccp.GET_DAQ_SIZE,
377
+ daq_list_number,
378
+ 0x00,
379
+ *address,
380
+ )
381
+
382
+ def set_daq_ptr(
383
+ self, daq_list_number: int, odt_number: int, element_number: int
384
+ ) -> Optional[CommandResponse]:
385
+ return self._execute(
386
+ ccp.SET_DAQ_PTR,
387
+ daq_list_number,
388
+ odt_number,
389
+ element_number,
390
+ )
391
+
392
+ def write_daq(
393
+ self, element_size: int, address_extension: int, address: int
394
+ ) -> Optional[CommandResponse]:
395
+ address = _pack_address(address)
396
+ _unsigned("Address extension", address_extension, 8)
397
+ return self._execute(
398
+ ccp.WRITE_DAQ,
399
+ element_size,
400
+ address_extension,
401
+ *address,
402
+ )
403
+
404
+ def start_stop(
405
+ self,
406
+ mode: int,
407
+ daq_list_number: int,
408
+ last_odt_number: int,
409
+ event_channel: int,
410
+ rate_prescaler: int,
411
+ ) -> Optional[CommandResponse]:
412
+ rate_prescaler = struct.pack(
413
+ "<H", _unsigned("Rate prescaler", rate_prescaler, 16)
414
+ )
415
+
416
+ return self._execute(
417
+ ccp.START_STOP,
418
+ mode,
419
+ daq_list_number,
420
+ last_odt_number,
421
+ event_channel,
422
+ *rate_prescaler,
423
+ )
424
+
425
+ def disconnect(
426
+ self, disconnect_type: ccp.DisconnectType, address: int
427
+ ) -> Optional[CommandResponse]:
428
+ address = struct.pack("<H", _unsigned("Station address", address, 16))
429
+
430
+ return self._execute(
431
+ ccp.DISCONNECT,
432
+ disconnect_type,
433
+ 0x00,
434
+ *address,
435
+ )
436
+
437
+ # Optional Commands.
438
+ def test(self, station_id: int) -> Optional[CommandResponse]:
439
+ to_send = struct.pack("<H", _unsigned("Station address", station_id, 16))
440
+
441
+ return self._execute(ccp.TEST, *to_send)
442
+
443
+ def dnload6(self, data: bytes) -> Optional[CommandResponse]:
444
+ data = _payload(data, 6, 6)
445
+ return self._execute(
446
+ ccp.DNLOAD_6,
447
+ *data,
448
+ )
449
+
450
+ def short_up(
451
+ self, size: int, address: int, address_extension: int = 0
452
+ ) -> Optional[CommandResponse]:
453
+ _transfer_size(size)
454
+ address = _pack_address(address)
455
+ _unsigned("Address extension", address_extension, 8)
456
+ return self._execute(
457
+ ccp.SHORT_UP,
458
+ size,
459
+ address_extension,
460
+ *address,
461
+ )
462
+
463
+ def start_stop_all(self, mode: int) -> Optional[CommandResponse]:
464
+ return self._execute(
465
+ ccp.START_STOP_ALL,
466
+ mode,
467
+ )
468
+
469
+ def set_s_status(self, status: int) -> Optional[CommandResponse]:
470
+ status = struct.pack("<B", _unsigned("Status", status, 8))
471
+
472
+ return self._execute(
473
+ ccp.SET_S_STATUS,
474
+ *status,
475
+ )
476
+
477
+ def get_s_status(self) -> Optional[CommandResponse]:
478
+ return self._execute(ccp.GET_S_STATUS)
479
+
480
+ def build_chksum(self, block_size: int) -> Optional[CommandResponse]:
481
+ block_size = struct.pack("<L", _unsigned("Block size", block_size, 32))
482
+
483
+ return self._execute(
484
+ ccp.BUILD_CHKSUM,
485
+ *block_size,
486
+ )
487
+
488
+ def clear_memory(self, block_size: int) -> Optional[CommandResponse]:
489
+ block_size = struct.pack("<L", _unsigned("Block size", block_size, 32))
490
+
491
+ return self._execute(
492
+ ccp.CLEAR_MEMORY,
493
+ *block_size,
494
+ )
495
+
496
+ def program(self, data: bytes) -> Optional[CommandResponse]:
497
+ """Program 1–5 bytes at MTA0; empty data ends the programming sequence."""
498
+ data = _payload(data, 0, 5)
499
+
500
+ return self._execute(
501
+ ccp.PROGRAM,
502
+ len(data),
503
+ *data,
504
+ )
505
+
506
+ def program6(self, data: bytes) -> Optional[CommandResponse]:
507
+ """Program exactly six bytes at MTA0, with no size byte or implicit retries."""
508
+ data = _payload(data, 6, 6)
509
+
510
+ return self._execute(
511
+ ccp.PROGRAM_6,
512
+ *data,
513
+ )
514
+
515
+ def diag_service(
516
+ self, service_number: int, parameters: bytes = b""
517
+ ) -> Optional[CommandResponse]:
518
+ """Run a diagnostic service with up to four-parameter bytes.
519
+
520
+ Return the raw CRM (or None on timeout). The slave sets MTA0 to the
521
+ return information; use upload() separately to read that information.
522
+ """
523
+ service_number = struct.pack(
524
+ "<H", _unsigned("Diagnostic service number", service_number, 16)
525
+ )
526
+ parameters = _payload(parameters, 0, 4)
527
+
528
+ return self._execute(
529
+ ccp.DIAG_SERVICE,
530
+ *service_number,
531
+ *parameters,
532
+ )
533
+
534
+ def move(self, size: int) -> Optional[CommandResponse]:
535
+ size = struct.pack("<L", _unsigned("Size", size, 32))
536
+
537
+ return self._execute(ccp.MOVE, *size)
538
+
539
+ def get_active_cal_page(self) -> Optional[CommandResponse]:
540
+ return self._execute(ccp.GET_ACTIVE_CAL_PAGE)
541
+
542
+ def select_cal_page(self) -> Optional[CommandResponse]:
543
+ return self._execute(ccp.SELECT_CAL_PAGE)
544
+
545
+ def unlock(self, key: bytes) -> Optional[CommandResponse]:
546
+ key = _payload(key, 0, 6)
547
+ return self._execute(
548
+ ccp.UNLOCK,
549
+ *key,
550
+ )
551
+
552
+ def get_seed(self, resource: SecondaryResource) -> Optional[CommandResponse]:
553
+ return self._execute(
554
+ ccp.GET_SEED,
555
+ resource,
556
+ )
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyccp
3
+ Version: 0.10.0
4
+ Summary: CAN Calibration Protocol for Python
5
+ Author-email: Vladimir Petrigo <vladimir.petrigo@gmail.com>
6
+ License-File: LICENSE.md
7
+ Keywords: asam,automotive,ccp,ecu
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Software Development
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: python-can<5,>=4
20
+ Description-Content-Type: text/markdown
21
+
22
+ [![CI](https://github.com/vpetrigo/pyccp/actions/workflows/ci.yml/badge.svg)](https://github.com/vpetrigo/pyccp/actions/workflows/ci.yml)
23
+ [![PyPI - Version](https://img.shields.io/pypi/v/pyccp)](https://pypi.org/project/pyccp)
24
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyccp)
25
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyccp?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads%2Fmonth)](https://pepy.tech/projects/pyccp)
26
+
27
+ # CAN Calibration Protocol for Python
28
+
29
+ Python implementation of the CAN Calibration Protocol (CCP) v2.1 Master, built
30
+ on [python-can](https://python-can.readthedocs.io/). Inspired by [pySART/pyccp](https://github.com/pySART/pyccp).
31
+
32
+ ## Install
33
+
34
+ ```toml
35
+ dependencies = [
36
+ "pyccp==0.10.0"
37
+ ]
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ import can
44
+ from pyccp import Master
45
+ from pyccp.ccp import ReturnCodes
46
+
47
+ bus = can.Bus(interface="socketcan", channel="can0")
48
+
49
+ with Master(bus, cro=0x7E1, dto=0x815) as master:
50
+ resp = master.connect(address=0x39)
51
+ assert resp is not None and resp.code == ReturnCodes.ACKNOWLEDGE
52
+
53
+ resp = master.set_mta(address=0x1000)
54
+ assert resp is not None and resp.code == ReturnCodes.ACKNOWLEDGE
55
+
56
+ resp = master.clear_memory(block_size=0x200)
57
+ chunks = [b"abcdef", b"ghi", b""]
58
+
59
+ for chunk in chunks:
60
+ resp = master.program6(chunk)
61
+ assert resp is not None and resp.code == ReturnCodes.ACKNOWLEDGE
62
+ ```
63
+
64
+ ## DAQ & Events
65
+
66
+ ```python
67
+ from pyccp import Master, DAQMessage, CommandResponse
68
+
69
+ with Master(bus, cro=0x7E1, dto=0x815) as master:
70
+ # Register a callback for DAQ messages
71
+ master.on_daq(0x816, lambda msg: print(f"DAQ: {msg.daq_id:#04X}, ODT: {msg.pid}, data: {msg.data.hex()}"))
72
+ # Connect and configure DAQ list
73
+ ```
74
+
75
+ ## Logging
76
+
77
+ ```python
78
+ import logging
79
+
80
+ logging.getLogger("pyccp").setLevel(logging.DEBUG)
81
+ ```
82
+
83
+ ## Tests
84
+
85
+ ```sh
86
+ hatch test -py 3.15
87
+ ```
@@ -0,0 +1,7 @@
1
+ pyccp/__init__.py,sha256=Ys1PlqDDrAVc3Gnhj3hFWd85AEqIyrvFPCDQkoI5ET4,158
2
+ pyccp/ccp.py,sha256=7qJUz9WwvZeJ6PE8a7AOtRCNghOjyCjlhJazrYvSxyY,3317
3
+ pyccp/master.py,sha256=tTfkXdI12WJF44e2nq8n55gElP-QloicylO_ehkp0Ts,17031
4
+ pyccp-0.10.0.dist-info/METADATA,sha256=S8gblKY9AfL3A7zyUEM16O9yY6hcJ8H1AIujHnS7YI8,2709
5
+ pyccp-0.10.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
6
+ pyccp-0.10.0.dist-info/licenses/LICENSE.md,sha256=I5-2rJHdNVVOaXNl4aYS3C1CPfA9hDnkyNQkB8ZWlUY,1073
7
+ pyccp-0.10.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vladimir Petrigo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.