pyxcp 0.23.8__cp313-cp313-macosx_11_0_arm64.whl → 0.25.7__cp313-cp313-macosx_11_0_arm64.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.
Files changed (89) hide show
  1. pyxcp/__init__.py +1 -1
  2. pyxcp/cmdline.py +14 -29
  3. pyxcp/config/__init__.py +1257 -1258
  4. pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
  5. pyxcp/cpp_ext/bin.hpp +7 -6
  6. pyxcp/cpp_ext/cpp_ext.cpython-310-darwin.so +0 -0
  7. pyxcp/cpp_ext/cpp_ext.cpython-311-darwin.so +0 -0
  8. pyxcp/cpp_ext/cpp_ext.cpython-312-darwin.so +0 -0
  9. pyxcp/cpp_ext/cpp_ext.cpython-313-darwin.so +0 -0
  10. pyxcp/cpp_ext/daqlist.hpp +241 -73
  11. pyxcp/cpp_ext/extension_wrapper.cpp +123 -15
  12. pyxcp/cpp_ext/framing.hpp +360 -0
  13. pyxcp/cpp_ext/helper.hpp +280 -280
  14. pyxcp/cpp_ext/mcobject.hpp +248 -246
  15. pyxcp/cpp_ext/sxi_framing.hpp +332 -0
  16. pyxcp/daq_stim/__init__.py +145 -67
  17. pyxcp/daq_stim/optimize/binpacking.py +2 -2
  18. pyxcp/daq_stim/scheduler.cpp +8 -8
  19. pyxcp/errormatrix.py +2 -2
  20. pyxcp/examples/run_daq.py +5 -4
  21. pyxcp/examples/xcp_policy.py +6 -6
  22. pyxcp/examples/xcp_read_benchmark.py +2 -2
  23. pyxcp/examples/xcp_skel.py +1 -2
  24. pyxcp/examples/xcp_unlock.py +10 -12
  25. pyxcp/examples/xcp_user_supplied_driver.py +1 -2
  26. pyxcp/examples/xcphello.py +2 -15
  27. pyxcp/examples/xcphello_recorder.py +2 -2
  28. pyxcp/master/__init__.py +1 -0
  29. pyxcp/master/errorhandler.py +134 -4
  30. pyxcp/master/master.py +823 -252
  31. pyxcp/recorder/.idea/.gitignore +8 -0
  32. pyxcp/recorder/.idea/misc.xml +4 -0
  33. pyxcp/recorder/.idea/modules.xml +8 -0
  34. pyxcp/recorder/.idea/recorder.iml +6 -0
  35. pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
  36. pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  37. pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  38. pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
  39. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
  40. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  41. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  42. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
  43. pyxcp/recorder/.idea/vcs.xml +10 -0
  44. pyxcp/recorder/__init__.py +96 -98
  45. pyxcp/recorder/converter/__init__.py +4 -10
  46. pyxcp/recorder/reader.hpp +138 -139
  47. pyxcp/recorder/reco.py +1 -0
  48. pyxcp/recorder/rekorder.cpython-310-darwin.so +0 -0
  49. pyxcp/recorder/rekorder.cpython-311-darwin.so +0 -0
  50. pyxcp/recorder/rekorder.cpython-312-darwin.so +0 -0
  51. pyxcp/recorder/rekorder.cpython-313-darwin.so +0 -0
  52. pyxcp/recorder/rekorder.hpp +274 -274
  53. pyxcp/recorder/unfolder.hpp +1354 -1319
  54. pyxcp/recorder/wrap.cpp +184 -183
  55. pyxcp/recorder/writer.hpp +302 -302
  56. pyxcp/scripts/xcp_daq_recorder.py +54 -0
  57. pyxcp/scripts/xcp_fetch_a2l.py +2 -2
  58. pyxcp/scripts/xcp_id_scanner.py +1 -2
  59. pyxcp/scripts/xcp_info.py +66 -51
  60. pyxcp/scripts/xcp_profile.py +1 -2
  61. pyxcp/tests/test_daq.py +1 -1
  62. pyxcp/tests/test_framing.py +262 -0
  63. pyxcp/tests/test_master.py +210 -100
  64. pyxcp/tests/test_transport.py +138 -42
  65. pyxcp/timing.py +1 -1
  66. pyxcp/transport/__init__.py +8 -5
  67. pyxcp/transport/base.py +70 -180
  68. pyxcp/transport/can.py +58 -7
  69. pyxcp/transport/eth.py +32 -15
  70. pyxcp/transport/hdf5_policy.py +167 -0
  71. pyxcp/transport/sxi.py +126 -52
  72. pyxcp/transport/transport_ext.cpython-310-darwin.so +0 -0
  73. pyxcp/transport/transport_ext.cpython-311-darwin.so +0 -0
  74. pyxcp/transport/transport_ext.cpython-312-darwin.so +0 -0
  75. pyxcp/transport/transport_ext.cpython-313-darwin.so +0 -0
  76. pyxcp/transport/transport_ext.hpp +214 -0
  77. pyxcp/transport/transport_wrapper.cpp +249 -0
  78. pyxcp/transport/usb_transport.py +47 -31
  79. pyxcp/types.py +0 -13
  80. pyxcp/{utils.py → utils/__init__.py} +1 -2
  81. pyxcp/utils/cli.py +78 -0
  82. {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/METADATA +4 -2
  83. pyxcp-0.25.7.dist-info/RECORD +158 -0
  84. {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/WHEEL +1 -1
  85. pyxcp/examples/conf_sxi.json +0 -9
  86. pyxcp/examples/conf_sxi.toml +0 -7
  87. pyxcp-0.23.8.dist-info/RECORD +0 -135
  88. {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/entry_points.txt +0 -0
  89. {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info/licenses}/LICENSE +0 -0
pyxcp/config/__init__.py CHANGED
@@ -1,1258 +1,1257 @@
1
- #!/usr/bin/env python
2
- import io
3
- import json
4
- import logging
5
- import sys
6
- import typing
7
- from pathlib import Path
8
-
9
- import can
10
- import toml
11
- from rich.logging import RichHandler
12
- from rich.prompt import Confirm
13
- from traitlets import (
14
- Any,
15
- Bool,
16
- Callable,
17
- Dict,
18
- Enum,
19
- Float,
20
- HasTraits,
21
- Integer,
22
- List,
23
- TraitError,
24
- Unicode,
25
- Union,
26
- )
27
- from traitlets.config import Application, Configurable, Instance, default
28
- from traitlets.config.loader import Config
29
-
30
- from pyxcp.config import legacy
31
-
32
-
33
- class CanBase:
34
- has_fd = False
35
- has_bitrate = True
36
- has_data_bitrate = False
37
- has_poll_interval = False
38
- has_receive_own_messages = False
39
- has_timing = False
40
-
41
- OPTIONAL_BASE_PARAMS = (
42
- "has_fd",
43
- "has_bitrate",
44
- "has_data_bitrate",
45
- "has_poll_interval",
46
- "has_receive_own_messages",
47
- "has_timing",
48
- )
49
-
50
- CAN_PARAM_MAP = {
51
- "sjw_abr": None,
52
- "tseg1_abr": None,
53
- "tseg2_abr": None,
54
- "sjw_dbr": None,
55
- "tseg1_dbr": None,
56
- "tseg2_dbr": None,
57
- }
58
-
59
-
60
- class CanAlystii(Configurable, CanBase):
61
- """CANalyst-II is a USB to CAN Analyzer device produced by Chuangxin Technology."""
62
-
63
- interface_name = "canalystii"
64
-
65
- has_timing = True
66
-
67
- device = Integer(default_value=None, allow_none=True, help="""Optional USB device number.""").tag(config=True)
68
- rx_queue_size = Integer(
69
- default_value=None,
70
- allow_none=True,
71
- help="""If set, software received message queue can only grow to this many
72
- messages (for all channels) before older messages are dropped """,
73
- ).tag(config=True)
74
-
75
-
76
- class CanTact(Configurable, CanBase):
77
- """Interface for CANtact devices from Linklayer Labs"""
78
-
79
- interface_name = "cantact"
80
-
81
- has_poll_interval = True
82
- has_timing = True
83
-
84
- monitor = Bool(default_value=False, allow_none=True, help="""If true, operate in listen-only monitoring mode""").tag(
85
- config=True
86
- )
87
-
88
-
89
- class Etas(Configurable, CanBase):
90
- """ETAS"""
91
-
92
- interface_name = "etas"
93
-
94
- has_fd = True
95
- has_data_bitrate = True
96
- has_receive_own_messages = True
97
-
98
-
99
- class Gs_Usb(Configurable, CanBase):
100
- """Geschwister Schneider USB/CAN devices and candleLight USB CAN interfaces."""
101
-
102
- interface_name = "gs_usb"
103
-
104
- index = Integer(
105
- default_value=None,
106
- allow_none=True,
107
- help="""device number if using automatic scan, starting from 0.
108
- If specified, bus/address shall not be provided.""",
109
- ).tag(config=True)
110
- bus = Integer(default_value=None, allow_none=True, help="""number of the bus that the device is connected to""").tag(
111
- config=True
112
- )
113
- address = Integer(default_value=None, allow_none=True, help="""address of the device on the bus it is connected to""").tag(
114
- config=True
115
- )
116
-
117
-
118
- class Neovi(Configurable, CanBase):
119
- """Intrepid Control Systems (ICS) neoVI interfaces."""
120
-
121
- interface_name = "neovi"
122
-
123
- has_fd = True
124
- has_data_bitrate = True
125
- has_receive_own_messages = True
126
-
127
- use_system_timestamp = Bool(
128
- default_value=None, allow_none=True, help="Use system timestamp for can messages instead of the hardware timestamp"
129
- ).tag(config=True)
130
- serial = Unicode(
131
- default_value=None, allow_none=True, help="Serial to connect (optional, will use the first found if not supplied)"
132
- ).tag(config=True)
133
- override_library_name = Unicode(
134
- default_value=None, allow_none=True, help="Absolute path or relative path to the library including filename."
135
- ).tag(config=True)
136
-
137
-
138
- class IsCan(Configurable, CanBase):
139
- """Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH."""
140
-
141
- interface_name = "iscan"
142
-
143
- has_poll_interval = True
144
-
145
-
146
- class Ixxat(Configurable, CanBase):
147
- """IXXAT Virtual Communication Interface"""
148
-
149
- interface_name = "ixxat"
150
-
151
- has_fd = True
152
- has_data_bitrate = True
153
- has_receive_own_messages = True
154
-
155
- unique_hardware_id = Integer(
156
- default_value=None,
157
- allow_none=True,
158
- help="""UniqueHardwareId to connect (optional, will use the first found if not supplied)""",
159
- ).tag(config=True)
160
- extended = Bool(default_value=None, allow_none=True, help="""Enables the capability to use extended IDs.""").tag(config=True)
161
- rx_fifo_size = Integer(default_value=None, allow_none=True, help="""Receive fifo size""").tag(config=True)
162
- tx_fifo_size = Integer(default_value=None, allow_none=True, help="""Transmit fifo size""").tag(config=True)
163
- ssp_dbr = Integer(
164
- default_value=None,
165
- allow_none=True,
166
- help="Secondary sample point (data). Only takes effect with fd and bitrate switch enabled.",
167
- ).tag(config=True)
168
-
169
- CAN_PARAM_MAP = {
170
- "sjw_abr": "sjw_abr",
171
- "tseg1_abr": "tseg1_abr",
172
- "tseg2_abr": "tseg2_abr",
173
- "sjw_dbr": "sjw_dbr",
174
- "tseg1_dbr": "tseg1_dbr",
175
- "tseg2_dbr": "tseg2_dbr",
176
- }
177
-
178
-
179
- class Kvaser(Configurable, CanBase):
180
- """Kvaser's CANLib"""
181
-
182
- interface_name = "kvaser"
183
-
184
- has_fd = True
185
- has_data_bitrate = True
186
- has_receive_own_messages = True
187
-
188
- CAN_PARAM_MAP = {
189
- "sjw_abr": "sjw",
190
- "tseg1_abr": "tseg1",
191
- "tseg2_abr": "tseg2",
192
- }
193
-
194
- accept_virtual = Bool(default_value=None, allow_none=True, help="If virtual channels should be accepted.").tag(config=True)
195
- no_samp = Enum(
196
- values=[1, 3],
197
- default_value=None,
198
- allow_none=True,
199
- help="""Either 1 or 3. Some CAN controllers can also sample each bit three times.
200
- In this case, the bit will be sampled three quanta in a row,
201
- with the last sample being taken in the edge between TSEG1 and TSEG2.
202
- Three samples should only be used for relatively slow baudrates""",
203
- ).tag(config=True)
204
- driver_mode = Bool(default_value=None, allow_none=True, help="Silent or normal.").tag(config=True)
205
- single_handle = Bool(
206
- default_value=None,
207
- allow_none=True,
208
- help="""Use one Kvaser CANLIB bus handle for both reading and writing.
209
- This can be set if reading and/or writing is done from one thread. """,
210
- ).tag(config=True)
211
-
212
-
213
- class NeouSys(Configurable, CanBase):
214
- """Neousys CAN Interface"""
215
-
216
- interface_name = "neousys"
217
-
218
- device = Integer(default_value=None, allow_none=True, help="Device number").tag(config=True)
219
-
220
-
221
- class NiCan(Configurable, CanBase):
222
- """National Instruments NI-CAN"""
223
-
224
- interface_name = "nican"
225
-
226
- log_errors = Bool(
227
- default_value=None,
228
- allow_none=True,
229
- help="""If True, communication errors will appear as CAN messages with
230
- ``is_error_frame`` set to True and ``arbitration_id`` will identify
231
- the error. """,
232
- ).tag(config=True)
233
-
234
-
235
- class NixNet(Configurable, CanBase):
236
- """National Instruments NI-XNET"""
237
-
238
- interface_name = "nixnet"
239
-
240
- has_poll_interval = True
241
- has_receive_own_messages = True
242
- has_timing = True
243
- has_fd = True
244
-
245
- CAN_PARAM_MAP = {
246
- "data_bitrate": "fd_bitrate",
247
- }
248
-
249
- can_termination = Bool(default_value=None, allow_none=True, help="Enable bus termination.")
250
-
251
-
252
- class PCan(Configurable, CanBase):
253
- """PCAN Basic API"""
254
-
255
- interface_name = "pcan"
256
-
257
- has_fd = True
258
- has_timing = True
259
-
260
- CAN_PARAM_MAP = {
261
- "sjw_abr": "nom_sjw",
262
- "tseg1_abr": "nom_tseg1",
263
- "tseg2_abr": "nom_tseg2",
264
- "sjw_dbr": "data_sjw",
265
- "tseg1_dbr": "data_tseg1",
266
- "tseg2_dbr": "data_tseg2",
267
- }
268
-
269
- device_id = Integer(
270
- default_value=None,
271
- allow_none=True,
272
- help="""Select the PCAN interface based on its ID. The device ID is a 8/32bit
273
- value that can be configured for each PCAN device. If you set the
274
- device_id parameter, it takes precedence over the channel parameter.
275
- The constructor searches all connected interfaces and initializes the
276
- first one that matches the parameter value. If no device is found,
277
- an exception is raised.""",
278
- ).tag(config=True)
279
- state = Instance(klass=can.BusState, default_value=None, allow_none=True, help="BusState of the channel.").tag(config=True)
280
-
281
- f_clock = Enum(
282
- values=[20000000, 24000000, 30000000, 40000000, 60000000, 80000000],
283
- default_value=None,
284
- allow_none=True,
285
- help="""Ignored if not using CAN-FD.
286
- Pass either f_clock or f_clock_mhz.""",
287
- ).tag(config=True)
288
- f_clock_mhz = Enum(
289
- values=[20, 24, 30, 40, 60, 80],
290
- default_value=None,
291
- allow_none=True,
292
- help="""Ignored if not using CAN-FD.
293
- Pass either f_clock or f_clock_mhz. """,
294
- ).tag(config=True)
295
-
296
- nom_brp = Integer(
297
- min=1,
298
- max=1024,
299
- default_value=None,
300
- allow_none=True,
301
- help="""Clock prescaler for nominal time quantum.
302
- Ignored if not using CAN-FD.""",
303
- ).tag(config=True)
304
- data_brp = Integer(
305
- min=1,
306
- max=1024,
307
- default_value=None,
308
- allow_none=True,
309
- help="""Clock prescaler for fast data time quantum.
310
- Ignored if not using CAN-FD.""",
311
- ).tag(config=True)
312
-
313
- auto_reset = Bool(
314
- default_value=None,
315
- allow_none=True,
316
- help="""Enable automatic recovery in bus off scenario.
317
- Resetting the driver takes ~500ms during which
318
- it will not be responsive.""",
319
- ).tag(config=True)
320
-
321
-
322
- class Robotell(Configurable, CanBase):
323
- """Interface for Chinese Robotell compatible interfaces"""
324
-
325
- interface_name = "robotell"
326
-
327
- ttyBaudrate = Integer(
328
- default_value=None,
329
- allow_none=True,
330
- help="""baudrate of underlying serial or usb device
331
- (Ignored if set via the `channel` parameter, e.g. COM7@11500).""",
332
- ).tag(config=True)
333
- rtscts = Bool(default_value=None, allow_none=True, help="turn hardware handshake (RTS/CTS) on and off.").tag(config=True)
334
-
335
-
336
- class SeeedStudio(Configurable, CanBase):
337
- """Seeed USB-Can analyzer interface."""
338
-
339
- interface_name = "seeedstudio"
340
-
341
- timeout = Float(default_value=None, allow_none=True, help="Timeout for the serial device in seconds.").tag(config=True)
342
- baudrate = Integer(default_value=None, allow_none=True, help="Baud rate of the serial device in bit/s.").tag(config=True)
343
- frame_type = Enum(
344
- values=["STD", "EXT"], default_value=None, allow_none=True, help="To select standard or extended messages."
345
- ).tag(config=True)
346
- operation_mode = Enum(
347
- values=["normal", "loopback", "silent", "loopback_and_silent"], default_value=None, allow_none=True, help=""" """
348
- ).tag(config=True)
349
-
350
-
351
- class Serial(Configurable, CanBase):
352
- """A text based interface."""
353
-
354
- interface_name = "serial"
355
-
356
- has_bitrate = False
357
-
358
- rtscts = Bool(default_value=None, allow_none=True, help="turn hardware handshake (RTS/CTS) on and off.").tag(config=True)
359
- timeout = Float(default_value=None, allow_none=True, help="Timeout for the serial device in seconds.").tag(config=True)
360
- baudrate = Integer(default_value=None, allow_none=True, help="Baud rate of the serial device in bit/s.").tag(config=True)
361
-
362
-
363
- class SlCan(Configurable, CanBase):
364
- """CAN over Serial / SLCAN."""
365
-
366
- interface_name = "slcan"
367
-
368
- has_poll_interval = True
369
-
370
- ttyBaudrate = Integer(default_value=None, allow_none=True, help="Baud rate of the serial device in bit/s.").tag(config=True)
371
- rtscts = Bool(default_value=None, allow_none=True, help="turn hardware handshake (RTS/CTS) on and off.").tag(config=True)
372
- timeout = Float(default_value=None, allow_none=True, help="Timeout for the serial device in seconds.").tag(config=True)
373
- btr = Integer(default_value=None, allow_none=True, help="BTR register value to set custom can speed.").tag(config=True)
374
- sleep_after_open = Float(
375
- default_value=None, allow_none=True, help="Time to wait in seconds after opening serial connection."
376
- ).tag(config=True)
377
-
378
-
379
- class SocketCan(Configurable, CanBase):
380
- """Linux SocketCAN."""
381
-
382
- interface_name = "socketcan"
383
-
384
- has_fd = True
385
- has_bitrate = False
386
- has_receive_own_messages = True
387
-
388
- local_loopback = Bool(
389
- default_value=None,
390
- allow_none=True,
391
- help="""If local loopback should be enabled on this bus.
392
- Please note that local loopback does not mean that messages sent
393
- on a socket will be readable on the same socket, they will only
394
- be readable on other open sockets on the same machine. More info
395
- can be read on the socketcan documentation:
396
- See https://www.kernel.org/doc/html/latest/networking/can.html#socketcan-local-loopback1""",
397
- ).tag(config=True)
398
-
399
-
400
- class SocketCanD(Configurable, CanBase):
401
- """Network-to-CAN bridge as a Linux damon."""
402
-
403
- interface_name = "socketcand"
404
-
405
- has_bitrate = False
406
-
407
- host = Unicode(default_value=None, allow_none=True, help=""" """).tag(config=True)
408
- port = Integer(default_value=None, allow_none=True, help=""" """).tag(config=True)
409
-
410
-
411
- class Systec(Configurable, CanBase):
412
- """SYSTEC interface"""
413
-
414
- interface_name = "systec"
415
-
416
- has_receive_own_messages = True
417
-
418
- state = Instance(klass=can.BusState, default_value=None, allow_none=True, help="BusState of the channel.").tag(config=True)
419
- device_number = Integer(min=0, max=254, default_value=None, allow_none=True, help="The device number of the USB-CAN.").tag(
420
- config=True
421
- )
422
- rx_buffer_entries = Integer(
423
- default_value=None, allow_none=True, help="The maximum number of entries in the receive buffer."
424
- ).tag(config=True)
425
- tx_buffer_entries = Integer(
426
- default_value=None, allow_none=True, help="The maximum number of entries in the transmit buffer."
427
- ).tag(config=True)
428
-
429
-
430
- class Udp_Multicast(Configurable, CanBase):
431
- """A virtual interface for CAN communications between multiple processes using UDP over Multicast IP."""
432
-
433
- interface_name = "udp_multicast"
434
-
435
- has_fd = True
436
- has_bitrate = False
437
- has_receive_own_messages = True
438
-
439
- port = Integer(default_value=None, allow_none=True, help="The IP port to read from and write to.").tag(config=True)
440
- hop_limit = Integer(default_value=None, allow_none=True, help="The hop limit in IPv6 or in IPv4 the time to live (TTL).").tag(
441
- config=True
442
- )
443
-
444
-
445
- class Usb2Can(Configurable, CanBase):
446
- """Interface to a USB2CAN Bus."""
447
-
448
- interface_name = "usb2can"
449
-
450
- flags = Integer(
451
- default_value=None, allow_none=True, help="Flags to directly pass to open function of the usb2can abstraction layer."
452
- ).tag(config=True)
453
- dll = Unicode(default_value=None, allow_none=True, help="Path to the DLL with the CANAL API to load.").tag(config=True)
454
- serial = Unicode(default_value=None, allow_none=True, help="Alias for `channel` that is provided for legacy reasons.").tag(
455
- config=True
456
- )
457
-
458
-
459
- class Vector(Configurable, CanBase):
460
- """Vector Informatik CAN interfaces."""
461
-
462
- interface_name = "vector"
463
-
464
- has_fd = True
465
- has_data_bitrate = True
466
- has_poll_interval = True
467
- has_receive_own_messages = True
468
- has_timing = True
469
-
470
- CAN_PARAM_MAP = {
471
- "sjw_abr": "sjw_abr",
472
- "tseg1_abr": "tseg1_abr",
473
- "tseg2_abr": "tseg2_abr",
474
- "sjw_dbr": "sjw_dbr",
475
- "tseg1_dbr": "tseg1_dbr",
476
- "tseg2_dbr": "tseg2_dbr",
477
- }
478
-
479
- serial = Integer(
480
- default_value=None,
481
- allow_none=True,
482
- help="""Serial number of the hardware to be used.
483
- If set, the channel parameter refers to the channels ONLY on the specified hardware.
484
- If set, the `app_name` does not have to be previously defined in
485
- *Vector Hardware Config*.""",
486
- ).tag(config=True)
487
- rx_queue_size = Integer(
488
- min=16, max=32768, default_value=None, allow_none=True, help="Number of messages in receive queue (power of 2)."
489
- ).tag(config=True)
490
- app_name = Unicode(default_value=None, allow_none=True, help="Name of application in *Vector Hardware Config*.").tag(
491
- config=True
492
- )
493
-
494
-
495
- class CanCustom(Configurable, CanBase):
496
- """Generic custom CAN interface.
497
-
498
- Enable basic CanBase options so user-provided python-can backends can
499
- consume common parameters like bitrate, fd, data_bitrate, poll_interval,
500
- receive_own_messages, and optional timing.
501
- """
502
-
503
- interface_name = "custom"
504
-
505
- # Allow usage of the basic options from CanBase for custom backends
506
- has_fd = True
507
- has_data_bitrate = True
508
- has_poll_interval = True
509
- has_receive_own_messages = True
510
- has_timing = True
511
-
512
-
513
- class Virtual(Configurable, CanBase):
514
- """ """
515
-
516
- interface_name = "virtual"
517
-
518
- has_bitrate = False
519
- has_receive_own_messages = True
520
-
521
- rx_queue_size = Integer(
522
- default_value=None,
523
- allow_none=True,
524
- help="""The size of the reception queue. The reception
525
- queue stores messages until they are read. If the queue reaches
526
- its capacity, it will start dropping the oldest messages to make
527
- room for new ones. If set to 0, the queue has an infinite capacity.
528
- Be aware that this can cause memory leaks if messages are read
529
- with a lower frequency than they arrive on the bus. """,
530
- ).tag(config=True)
531
- preserve_timestamps = Bool(
532
- default_value=None,
533
- allow_none=True,
534
- help="""If set to True, messages transmitted via
535
- will keep the timestamp set in the
536
- :class:`~can.Message` instance. Otherwise, the timestamp value
537
- will be replaced with the current system time.""",
538
- ).tag(config=True)
539
-
540
-
541
- CAN_INTERFACE_MAP = {
542
- "canalystii": CanAlystii,
543
- "cantact": CanTact,
544
- "etas": Etas,
545
- "gs_usb": Gs_Usb,
546
- "iscan": IsCan,
547
- "ixxat": Ixxat,
548
- "kvaser": Kvaser,
549
- "neousys": NeouSys,
550
- "neovi": Neovi,
551
- "nican": NiCan,
552
- "nixnet": NixNet,
553
- "pcan": PCan,
554
- "robotell": Robotell,
555
- "seeedstudio": SeeedStudio,
556
- "serial": Serial,
557
- "slcan": SlCan,
558
- "socketcan": SocketCan,
559
- "socketcand": SocketCanD,
560
- "systec": Systec,
561
- "udp_multicast": Udp_Multicast,
562
- "usb2can": Usb2Can,
563
- "vector": Vector,
564
- "virtual": Virtual,
565
- "custom": CanCustom,
566
- }
567
-
568
-
569
- class Can(Configurable):
570
- VALID_INTERFACES = set(can.interfaces.VALID_INTERFACES)
571
- VALID_INTERFACES.add("custom")
572
-
573
- interface = Enum(
574
- values=VALID_INTERFACES, default_value=None, allow_none=True, help="CAN interface supported by python-can"
575
- ).tag(config=True)
576
- channel = Any(
577
- default_value=None, allow_none=True, help="Channel identification. Expected type and value is backend dependent."
578
- ).tag(config=True)
579
- max_dlc_required = Bool(False, help="Master to slave frames always to have DLC = MAX_DLC = 8").tag(config=True)
580
- # max_can_fd_dlc = Integer(64, help="").tag(config=True)
581
- padding_value = Integer(0, help="Fill value, if max_dlc_required == True and DLC < MAX_DLC").tag(config=True)
582
- use_default_listener = Bool(True, help="").tag(config=True)
583
- can_id_master = Integer(allow_none=False, help="CAN-ID master -> slave (Bit31= 1: extended identifier)").tag(
584
- config=True
585
- ) # CMD and STIM packets
586
- can_id_slave = Integer(allow_none=True, help="CAN-ID slave -> master (Bit31= 1: extended identifier)").tag(
587
- config=True
588
- ) # RES, ERR, EV, SERV and DAQ packets.
589
- can_id_broadcast = Integer(
590
- default_value=None, allow_none=True, help="Auto detection CAN-ID (Bit31= 1: extended identifier)"
591
- ).tag(config=True)
592
- daq_identifier = List(trait=Integer(), default_value=[], allow_none=True, help="One CAN identifier per DAQ-list.").tag(
593
- config=True
594
- )
595
- bitrate = Integer(250000, help="CAN bitrate in bits/s (arbitration phase, if CAN FD).").tag(config=True)
596
- receive_own_messages = Bool(False, help="Enable self-reception of sent messages.").tag(config=True)
597
- poll_interval = Float(default_value=None, allow_none=True, help="Poll interval in seconds when reading messages.").tag(
598
- config=True
599
- )
600
- fd = Bool(False, help="If CAN-FD frames should be supported.").tag(config=True)
601
- data_bitrate = Integer(default_value=None, allow_none=True, help="Which bitrate to use for data phase in CAN FD.").tag(
602
- config=True
603
- )
604
- sjw_abr = Integer(
605
- default_value=None, allow_none=True, help="Bus timing value sample jump width (arbitration, SJW if CAN classic)."
606
- ).tag(config=True)
607
- tseg1_abr = Integer(
608
- default_value=None, allow_none=True, help="Bus timing value tseg1 (arbitration, TSEG1 if CAN classic)."
609
- ).tag(config=True)
610
- tseg2_abr = Integer(
611
- default_value=None, allow_none=True, help="Bus timing value tseg2 (arbitration, TSEG2, if CAN classic)"
612
- ).tag(config=True)
613
- sjw_dbr = Integer(default_value=None, allow_none=True, help="Bus timing value sample jump width (data).").tag(config=True)
614
- tseg1_dbr = Integer(default_value=None, allow_none=True, help="Bus timing value tseg1 (data).").tag(config=True)
615
- tseg2_dbr = Integer(default_value=None, allow_none=True, help="Bus timing value tseg2 (data).").tag(config=True)
616
- timing = Union(
617
- trait_types=[Instance(klass=can.BitTiming), Instance(klass=can.BitTimingFd)],
618
- default_value=None,
619
- allow_none=True,
620
- help="""Custom bit timing settings.
621
- (.s https://github.com/hardbyte/python-can/blob/develop/can/bit_timing.py)
622
- If this parameter is provided, it takes precedence over all other
623
- timing-related parameters.
624
- """,
625
- ).tag(config=True)
626
-
627
- classes = List(
628
- [
629
- CanAlystii,
630
- CanCustom,
631
- CanTact,
632
- Etas,
633
- Gs_Usb,
634
- Neovi,
635
- IsCan,
636
- Ixxat,
637
- Kvaser,
638
- NeouSys,
639
- NiCan,
640
- NixNet,
641
- PCan,
642
- Robotell,
643
- SeeedStudio,
644
- Serial,
645
- SlCan,
646
- SocketCan,
647
- SocketCanD,
648
- Systec,
649
- Udp_Multicast,
650
- Usb2Can,
651
- Vector,
652
- Virtual,
653
- ]
654
- )
655
-
656
- def __init__(self, **kws):
657
- super().__init__(**kws)
658
-
659
- if self.parent.layer == "CAN":
660
- if self.interface is None or self.interface not in self.VALID_INTERFACES:
661
- raise TraitError(
662
- f"CAN interface must be one of {sorted(list(self.VALID_INTERFACES))} not the"
663
- " {type(self.interface).__name__} {self.interface}."
664
- )
665
- self.canalystii = CanAlystii(config=self.config, parent=self)
666
- self.cancustom = CanCustom(config=self.config, parent=self)
667
- self.cantact = CanTact(config=self.config, parent=self)
668
- self.etas = Etas(config=self.config, parent=self)
669
- self.gs_usb = Gs_Usb(config=self.config, parent=self)
670
- self.neovi = Neovi(config=self.config, parent=self)
671
- self.iscan = IsCan(config=self.config, parent=self)
672
- self.ixxat = Ixxat(config=self.config, parent=self)
673
- self.kvaser = Kvaser(config=self.config, parent=self)
674
- self.neousys = NeouSys(config=self.config, parent=self)
675
- self.nican = NiCan(config=self.config, parent=self)
676
- self.nixnet = NixNet(config=self.config, parent=self)
677
- self.pcan = PCan(config=self.config, parent=self)
678
- self.robotell = Robotell(config=self.config, parent=self)
679
- self.seeedstudio = SeeedStudio(config=self.config, parent=self)
680
- self.serial = Serial(config=self.config, parent=self)
681
- self.slcan = SlCan(config=self.config, parent=self)
682
- self.socketcan = SocketCan(config=self.config, parent=self)
683
- self.socketcand = SocketCanD(config=self.config, parent=self)
684
- self.systec = Systec(config=self.config, parent=self)
685
- self.udp_multicast = Udp_Multicast(config=self.config, parent=self)
686
- self.usb2can = Usb2Can(config=self.config, parent=self)
687
- self.vector = Vector(config=self.config, parent=self)
688
- self.virtual = Virtual(config=self.config, parent=self)
689
-
690
-
691
- class Eth(Configurable):
692
- """Ethernet."""
693
-
694
- host = Unicode("localhost", help="Hostname or IP address of XCP slave.").tag(config=True)
695
- port = Integer(5555, help="TCP/UDP port to connect.").tag(config=True)
696
- protocol = Enum(values=["TCP", "UDP"], default_value="UDP", help="").tag(config=True)
697
- ipv6 = Bool(False, help="Use IPv6 if `True` else IPv4.").tag(config=True)
698
- tcp_nodelay = Bool(False, help="*** Expert option *** -- Disable Nagle's algorithm if `True`.").tag(config=True)
699
- bind_to_address = Unicode(default_value=None, allow_none=True, help="Bind to specific local address.").tag(config=True)
700
- bind_to_port = Integer(default_value=None, allow_none=True, help="Bind to specific local port.").tag(config=True)
701
-
702
-
703
- class SxI(Configurable):
704
- """SCI and SPI connections."""
705
-
706
- port = Unicode("COM1", help="Name of communication interface.").tag(config=True)
707
- bitrate = Integer(38400, help="Connection bitrate").tag(config=True)
708
- bytesize = Enum(values=[5, 6, 7, 8], default_value=8, help="Size of byte.").tag(config=True)
709
- parity = Enum(values=["N", "E", "O", "M", "S"], default_value="N", help="Paritybit calculation.").tag(config=True)
710
- stopbits = Enum(values=[1, 1.5, 2], default_value=1, help="Number of stopbits.").tag(config=True)
711
- mode = Enum(
712
- values=[
713
- "ASYNCH_FULL_DUPLEX_MODE",
714
- "SYNCH_FULL_DUPLEX_MODE_BYTE",
715
- "SYNCH_FULL_DUPLEX_MODE_WORD",
716
- "SYNCH_FULL_DUPLEX_MODE_DWORD",
717
- "SYNCH_MASTER_SLAVE_MODE_BYTE",
718
- "SYNCH_MASTER_SLAVE_MODE_WORD",
719
- "SYNCH_MASTER_SLAVE_MODE_DWORD",
720
- ],
721
- default_value="ASYNCH_FULL_DUPLEX_MODE",
722
- help="Asynchronous (SCI) or synchronous (SPI) communication mode.",
723
- ).tag(config=True)
724
- header_format = Enum(
725
- values=[
726
- "HEADER_LEN_BYTE",
727
- "HEADER_LEN_CTR_BYTE",
728
- "HEADER_LEN_FILL_BYTE",
729
- "HEADER_LEN_WORD",
730
- "HEADER_LEN_CTR_WORD",
731
- "HEADER_LEN_FILL_WORD",
732
- ],
733
- default_value="HEADER_LEN_CTR_WORD",
734
- help="""XCPonSxI header format.
735
- Number of bytes:
736
-
737
- LEN CTR FILL
738
- ______________________________________________________________
739
- HEADER_LEN_BYTE | 1 X X
740
- HEADER_LEN_CTR_BYTE | 1 1 X
741
- HEADER_LEN_FILL_BYTE | 1 X 1
742
- HEADER_LEN_WORD | 2 X X
743
- HEADER_LEN_CTR_WORD | 2 2 X
744
- HEADER_LEN_FILL_WORD | 2 X 2
745
- """,
746
- ).tag(config=True)
747
- tail_format = Enum(
748
- values=["NO_CHECKSUM", "CHECKSUM_BYTE", "CHECKSUM_WORD"], default_value="NO_CHECKSUM", help="XCPonSxI tail format."
749
- ).tag(config=True)
750
- framing = Bool(False, help="Enable SCI framing mechanism (ESC chars).").tag(config=True)
751
- esc_sync = Integer(0x01, min=0, max=255, help="SCI framing protocol character SYNC.").tag(config=True)
752
- esc_esc = Integer(0x00, min=0, max=255, help="SCI framing protocol character ESC.").tag(config=True)
753
-
754
-
755
- class Usb(Configurable):
756
- """Universal Serial Bus connections."""
757
-
758
- serial_number = Unicode("", help="Device serial number.").tag(config=True)
759
- configuration_number = Integer(1, help="USB configuration number.").tag(config=True)
760
- interface_number = Integer(2, help="USB interface number.").tag(config=True)
761
- vendor_id = Integer(0, help="USB vendor ID.").tag(config=True)
762
- product_id = Integer(0, help="USB product ID.").tag(config=True)
763
- library = Unicode("", help="Absolute path to USB shared library.").tag(config=True)
764
- header_format = Enum(
765
- values=[
766
- "HEADER_LEN_BYTE",
767
- "HEADER_LEN_CTR_BYTE",
768
- "HEADER_LEN_FILL_BYTE",
769
- "HEADER_LEN_WORD",
770
- "HEADER_LEN_CTR_WORD",
771
- "HEADER_LEN_FILL_WORD",
772
- ],
773
- default_value="HEADER_LEN_CTR_WORD",
774
- help="",
775
- ).tag(config=True)
776
- in_ep_number = Integer(1, help="Ingoing USB reply endpoint number (IN-EP for RES/ERR, DAQ, and EV/SERV).").tag(config=True)
777
- in_ep_transfer_type = Enum(
778
- values=["BULK_TRANSFER", "INTERRUPT_TRANSFER"], default_value="BULK_TRANSFER", help="Ingoing: Supported USB transfer types."
779
- ).tag(config=True)
780
- in_ep_max_packet_size = Integer(512, help="Ingoing: Maximum packet size of endpoint in bytes.").tag(config=True)
781
- in_ep_polling_interval = Integer(0, help="Ingoing: Polling interval of endpoint.").tag(config=True)
782
- in_ep_message_packing = Enum(
783
- values=["MESSAGE_PACKING_SINGLE", "MESSAGE_PACKING_MULTIPLE", "MESSAGE_PACKING_STREAMING"],
784
- default_value="MESSAGE_PACKING_SINGLE",
785
- help="Ingoing: Packing of XCP Messages.",
786
- ).tag(config=True)
787
- in_ep_alignment = Enum(
788
- values=["ALIGNMENT_8_BIT", "ALIGNMENT_16_BIT", "ALIGNMENT_32_BIT", "ALIGNMENT_64_BIT"],
789
- default_value="ALIGNMENT_8_BIT",
790
- help="Ingoing: Alignment border.",
791
- ).tag(config=True)
792
- in_ep_recommended_host_bufsize = Integer(0, help="Ingoing: Recommended host buffer size.").tag(config=True)
793
- out_ep_number = Integer(0, help="Outgoing USB command endpoint number (OUT-EP for CMD and STIM).").tag(config=True)
794
- out_ep_transfer_type = Enum(
795
- values=["BULK_TRANSFER", "INTERRUPT_TRANSFER"],
796
- default_value="BULK_TRANSFER",
797
- help="Outgoing: Supported USB transfer types.",
798
- ).tag(config=True)
799
- out_ep_max_packet_size = Integer(512, help="Outgoing: Maximum packet size of endpoint in bytes.").tag(config=True)
800
- out_ep_polling_interval = Integer(0, help="Outgoing: Polling interval of endpoint.").tag(config=True)
801
- out_ep_message_packing = Enum(
802
- values=["MESSAGE_PACKING_SINGLE", "MESSAGE_PACKING_MULTIPLE", "MESSAGE_PACKING_STREAMING"],
803
- default_value="MESSAGE_PACKING_SINGLE",
804
- help="Outgoing: Packing of XCP Messages.",
805
- ).tag(config=True)
806
- out_ep_alignment = Enum(
807
- values=["ALIGNMENT_8_BIT", "ALIGNMENT_16_BIT", "ALIGNMENT_32_BIT", "ALIGNMENT_64_BIT"],
808
- default_value="ALIGNMENT_8_BIT",
809
- help="Outgoing: Alignment border.",
810
- ).tag(config=True)
811
- out_ep_recommended_host_bufsize = Integer(0, help="Outgoing: Recommended host buffer size.").tag(config=True)
812
-
813
-
814
- class Transport(Configurable):
815
- """ """
816
-
817
- classes = List([Can, Eth, SxI, Usb])
818
-
819
- layer = Enum(
820
- values=["CAN", "ETH", "SXI", "USB"],
821
- default_value=None,
822
- allow_none=True,
823
- help="Choose one of the supported XCP transport layers.",
824
- ).tag(config=True)
825
- create_daq_timestamps = Bool(True, help="Record time of frame reception or set timestamp to 0.").tag(config=True)
826
- timeout = Float(
827
- 2.0,
828
- help="""raise `XcpTimeoutError` after `timeout` seconds
829
- if there is no response to a command.""",
830
- ).tag(config=True)
831
- alignment = Enum(values=[1, 2, 4, 8], default_value=1).tag(config=True)
832
-
833
- can = Instance(Can).tag(config=True)
834
- eth = Instance(Eth).tag(config=True)
835
- sxi = Instance(SxI).tag(config=True)
836
- usb = Instance(Usb).tag(config=True)
837
-
838
- def __init__(self, **kws):
839
- super().__init__(**kws)
840
- self.can = Can(config=self.config, parent=self)
841
- self.eth = Eth(config=self.config, parent=self)
842
- self.sxi = SxI(config=self.config, parent=self)
843
- self.usb = Usb(config=self.config, parent=self)
844
-
845
-
846
- class CustomArgs(Configurable):
847
- """Class to handle custom command-line arguments."""
848
-
849
- def __init__(self, **kwargs):
850
- super().__init__(**kwargs)
851
- self._custom_args = {}
852
-
853
- def add_argument(self, short_opt, long_opt="", dest="", help="", type=None, default=None, action=None):
854
- """Add a custom argument dynamically.
855
-
856
- This mimics the argparse.ArgumentParser.add_argument method.
857
- """
858
- if not dest and long_opt:
859
- dest = long_opt.lstrip("-").replace("-", "_")
860
-
861
- # Store the argument definition
862
- self._custom_args[dest] = {
863
- "short_opt": short_opt,
864
- "long_opt": long_opt,
865
- "help": help,
866
- "type": type,
867
- "default": default,
868
- "action": action,
869
- "value": default,
870
- }
871
-
872
- # Dynamically add a trait for this argument
873
- trait_type = Any()
874
- if type == bool or action == "store_true" or action == "store_false":
875
- trait_type = Bool(default)
876
- elif type == int:
877
- trait_type = Integer(default)
878
- elif type == float:
879
- trait_type = Float(default)
880
- elif type == str:
881
- trait_type = Unicode(default)
882
-
883
- # Add the trait to this instance
884
- self.add_trait(dest, trait_type)
885
- setattr(self, dest, default)
886
-
887
- def update_from_options(self, options):
888
- """Update trait values from parsed options."""
889
- for option in options:
890
- if option.dest and option.dest in self._custom_args:
891
- if option.default is not None:
892
- setattr(self, option.dest, option.default)
893
- self._custom_args[option.dest]["value"] = option.default
894
-
895
- def get_args(self):
896
- """Return an object with all custom arguments as attributes."""
897
-
898
- class Args:
899
- pass
900
-
901
- args = Args()
902
- for name, arg_def in self._custom_args.items():
903
- setattr(args, name, arg_def["value"])
904
-
905
- return args
906
-
907
-
908
- class General(Configurable):
909
- """ """
910
-
911
- disable_error_handling = Bool(False, help="Disable XCP error-handler for performance reasons.").tag(config=True)
912
- disconnect_response_optional = Bool(False, help="Ignore missing response on DISCONNECT request.").tag(config=True)
913
- connect_retries = Integer(help="Number of CONNECT retries (None for infinite retries).", allow_none=True, default_value=3).tag(
914
- config=True
915
- )
916
- # Structured diagnostics dump options
917
- diagnostics_on_failure = Bool(True, help="Append a structured diagnostics dump to timeout errors.").tag(config=True)
918
- diagnostics_last_pdus = Integer(20, help="How many recent PDUs to include in diagnostics dump.").tag(config=True)
919
-
920
- seed_n_key_dll = Unicode("", allow_none=False, help="Dynamic library used for slave resource unlocking.").tag(config=True)
921
- seed_n_key_dll_same_bit_width = Bool(False, help="").tag(config=True)
922
- custom_dll_loader = Unicode(allow_none=True, default_value=None, help="Use an custom seed and key DLL loader.").tag(config=True)
923
- seed_n_key_function = Callable(
924
- default_value=None,
925
- allow_none=True,
926
- help="""Python function used for slave resource unlocking.
927
- Could be used if seed-and-key algorithm is known instead of `seed_n_key_dll`.""",
928
- ).tag(config=True)
929
- stim_support = Bool(False, help="").tag(config=True)
930
-
931
-
932
- class ProfileCreate(Application):
933
- description = "\nCreate a new profile"
934
-
935
- dest_file = Unicode(default_value=None, allow_none=True, help="destination file name").tag(config=True)
936
- aliases = Dict( # type:ignore[assignment]
937
- dict(
938
- d="ProfileCreate.dest_file",
939
- o="ProfileCreate.dest_file",
940
- )
941
- )
942
-
943
- def start(self):
944
- pyxcp = self.parent.parent
945
- if self.dest_file:
946
- dest = Path(self.dest_file)
947
- if dest.exists():
948
- if not Confirm.ask(f"Destination file [green]{dest.name!r}[/green] already exists. Do you want to overwrite it?"):
949
- print("Aborting...")
950
- self.exit(1)
951
- with dest.open("w", encoding="latin1") as out_file:
952
- pyxcp.generate_config_file(out_file)
953
- else:
954
- pyxcp.generate_config_file(sys.stdout)
955
-
956
-
957
- class ProfileConvert(Application):
958
- description = "\nConvert legacy configuration file (.json/.toml) to new Python based format."
959
-
960
- config_file = Unicode(help="Name of legacy config file (.json/.toml).", default_value=None, allow_none=False).tag(
961
- config=True
962
- ) # default_value="pyxcp_conf.py",
963
-
964
- dest_file = Unicode(default_value=None, allow_none=True, help="destination file name").tag(config=True)
965
-
966
- aliases = Dict( # type:ignore[assignment]
967
- dict(
968
- c="ProfileConvert.config_file",
969
- d="ProfileConvert.dest_file",
970
- o="ProfileConvert.dest_file",
971
- )
972
- )
973
-
974
- def start(self):
975
- pyxcp = self.parent.parent
976
- pyxcp._read_configuration(self.config_file, emit_warning=False)
977
- if self.dest_file:
978
- dest = Path(self.dest_file)
979
- if dest.exists():
980
- if not Confirm.ask(f"Destination file [green]{dest.name!r}[/green] already exists. Do you want to overwrite it?"):
981
- print("Aborting...")
982
- self.exit(1)
983
- with dest.open("w", encoding="latin1") as out_file:
984
- pyxcp.generate_config_file(out_file)
985
- else:
986
- pyxcp.generate_config_file(sys.stdout)
987
-
988
-
989
- class ProfileApp(Application):
990
- subcommands = Dict(
991
- dict(
992
- create=(ProfileCreate, ProfileCreate.description.splitlines()[0]),
993
- convert=(ProfileConvert, ProfileConvert.description.splitlines()[0]),
994
- )
995
- )
996
-
997
- def start(self):
998
- if self.subapp is None:
999
- print(f"No subcommand specified. Must specify one of: {self.subcommands.keys()}")
1000
- print()
1001
- self.print_description()
1002
- self.print_subcommands()
1003
- self.exit(1)
1004
- else:
1005
- self.subapp.start()
1006
-
1007
-
1008
- class PyXCP(Application):
1009
- description = "pyXCP application"
1010
- config_file = Unicode(default_value="pyxcp_conf.py", help="base name of config file").tag(config=True)
1011
-
1012
- # Add callout function support
1013
- callout = Callable(default_value=None, allow_none=True, help="Callback function to be called with master and args").tag(
1014
- config=True
1015
- )
1016
-
1017
- # Logging options
1018
- structured_logging = Bool(False, help="Emit one-line JSON logs instead of rich text.").tag(config=True)
1019
- # Use log_output_format to avoid clashing with traitlets.Application.log_format (a %-style template)
1020
- log_output_format = Enum(values=["rich", "json"], default_value="rich", help="Select logging output format.").tag(config=True)
1021
-
1022
- classes = List([General, Transport, CustomArgs])
1023
-
1024
- subcommands = dict(
1025
- profile=(
1026
- ProfileApp,
1027
- """
1028
- Profile stuff
1029
- """.strip(),
1030
- )
1031
- )
1032
-
1033
- def start(self):
1034
- if self.subapp:
1035
- self.subapp.start()
1036
- exit(2)
1037
- else:
1038
- # Always read configuration and then set up our logger explicitly to avoid
1039
- # traitlets.Application default logging using an incompatible 'log_format'.
1040
- self._read_configuration(self.config_file)
1041
- try:
1042
- # Ensure base Application.log_format is a valid %-style template
1043
- # (Users might set c.PyXCP.log_format = "json" which clashes with traitlets behavior.)
1044
- self.log_format = "%(message)s" # type: ignore[assignment]
1045
- except Exception:
1046
- pass
1047
- self._setup_logger()
1048
- self.log.debug(f"pyxcp version: {self.version}")
1049
-
1050
- def _setup_logger(self):
1051
- from pyxcp.types import Command
1052
-
1053
- # Remove any handlers installed by `traitlets`.
1054
- for hdl in list(self.log.handlers):
1055
- self.log.removeHandler(hdl)
1056
-
1057
- # Decide formatter/handler based on config
1058
- use_json = False
1059
- try:
1060
- # Prefer explicit log_output_format; fallback to structured_logging for compatibility
1061
- use_json = getattr(self, "log_output_format", "rich") == "json" or getattr(self, "structured_logging", False)
1062
- # Backward-compat: if someone set PyXCP.log_format="json" in config, honor it here too
1063
- if not use_json:
1064
- lf = getattr(self, "log_format", None)
1065
- if isinstance(lf, str) and lf.lower() == "json":
1066
- use_json = True
1067
- except Exception:
1068
- use_json = False
1069
-
1070
- if use_json:
1071
-
1072
- class JSONFormatter(logging.Formatter):
1073
- def format(self, record: logging.LogRecord) -> str:
1074
- # Build a minimal structured payload
1075
- payload = {
1076
- "time": self.formatTime(record, self.datefmt),
1077
- "level": record.levelname,
1078
- "logger": record.name,
1079
- "message": record.getMessage(),
1080
- }
1081
- # Include extras if present
1082
- for key in ("transport", "host", "port", "protocol", "event", "command"):
1083
- if hasattr(record, key):
1084
- payload[key] = getattr(record, key)
1085
- # Exceptions
1086
- if record.exc_info:
1087
- payload["exc_type"] = record.exc_info[0].__name__ if record.exc_info[0] else None
1088
- payload["exc_text"] = self.formatException(record.exc_info)
1089
- try:
1090
- import json as _json
1091
-
1092
- return _json.dumps(payload, ensure_ascii=False)
1093
- except Exception:
1094
- return f"{payload}"
1095
-
1096
- handler = logging.StreamHandler()
1097
- formatter = JSONFormatter(datefmt=self.log_datefmt)
1098
- handler.setFormatter(formatter)
1099
- handler.setLevel(self.log_level)
1100
- self.log.addHandler(handler)
1101
- else:
1102
- keywords = list(Command.__members__.keys()) + ["ARGS", "KWS"] # Syntax highlight XCP commands and other stuff.
1103
- rich_handler = RichHandler(
1104
- rich_tracebacks=True,
1105
- tracebacks_show_locals=True,
1106
- log_time_format=self.log_datefmt,
1107
- level=self.log_level,
1108
- keywords=keywords,
1109
- )
1110
- self.log.addHandler(rich_handler)
1111
-
1112
- def initialize(self, argv=None):
1113
- from pyxcp import __version__ as pyxcp_version
1114
-
1115
- PyXCP.version = pyxcp_version
1116
- PyXCP.name = Path(sys.argv[0]).name
1117
- self.parse_command_line(argv[1:])
1118
-
1119
- def _read_configuration(self, file_name: str, emit_warning: bool = True) -> None:
1120
- self.read_configuration_file(file_name, emit_warning)
1121
- self.general = General(config=self.config, parent=self)
1122
- self.transport = Transport(parent=self)
1123
- self.custom_args = CustomArgs(config=self.config, parent=self)
1124
-
1125
- def read_configuration_file(self, file_name: str, emit_warning: bool = True):
1126
- self.legacy_config: bool = False
1127
-
1128
- pth = Path(file_name)
1129
- if not pth.exists():
1130
- raise FileNotFoundError(f"Configuration file {file_name!r} does not exist.")
1131
- suffix = pth.suffix.lower()
1132
- if suffix == ".py":
1133
- self.load_config_file(pth)
1134
- else:
1135
- self.legacy_config = True
1136
- if suffix == ".json":
1137
- reader = json
1138
- elif suffix == ".toml":
1139
- reader = toml
1140
- else:
1141
- raise ValueError(f"Unknown file type for config: {suffix}")
1142
- with pth.open("r") as f:
1143
- if emit_warning:
1144
- self.log.warning(f"Legacy configuration file format ({suffix}), please use Python based configuration.")
1145
- cfg = reader.loads(f.read())
1146
- if cfg:
1147
- cfg = legacy.convert_config(cfg, self.log)
1148
- self.config = cfg
1149
- return cfg
1150
-
1151
- flags = Dict( # type:ignore[assignment]
1152
- dict(
1153
- debug=({"PyXCP": {"log_level": 10}}, "Set loglevel to DEBUG"),
1154
- )
1155
- )
1156
-
1157
- @default("log_level")
1158
- def _default_value(self):
1159
- return logging.INFO # traitlets default is logging.WARN
1160
-
1161
- aliases = Dict( # type:ignore[assignment]
1162
- dict(
1163
- c="PyXCP.config_file", # Application
1164
- log_level="PyXCP.log_level",
1165
- l="PyXCP.log_level",
1166
- )
1167
- )
1168
-
1169
- def _iterate_config_class(self, klass, class_names: typing.List[str], config, out_file: io.IOBase = sys.stdout) -> None:
1170
- sub_classes = []
1171
- class_path = ".".join(class_names)
1172
- print(
1173
- f"""\n# ------------------------------------------------------------------------------
1174
- # {class_path} configuration
1175
- # ------------------------------------------------------------------------------""",
1176
- end="\n\n",
1177
- file=out_file,
1178
- )
1179
- if hasattr(klass, "classes"):
1180
- kkk = klass.classes
1181
- if hasattr(kkk, "default"):
1182
- if class_names[-1] not in ("PyXCP"):
1183
- sub_classes.extend(kkk.default())
1184
- for name, tr in klass.class_own_traits().items():
1185
- md = tr.metadata
1186
- if md.get("config"):
1187
- help = md.get("help", "").lstrip()
1188
- commented_lines = "\n".join([f"# {line}" for line in help.split("\n")])
1189
- print(f"#{commented_lines}", file=out_file)
1190
- value = tr.default()
1191
- if isinstance(tr, Instance) and tr.__class__.__name__ not in ("Dict", "List"):
1192
- continue
1193
- if isinstance(tr, Enum):
1194
- print(f"# Choices: {tr.info()}", file=out_file)
1195
- else:
1196
- print(f"# Type: {tr.info()}", file=out_file)
1197
- print(f"# Default: {value!r}", file=out_file)
1198
- if name in config:
1199
- cfg_value = config[name]
1200
- print(f"c.{class_path!s}.{name!s} = {cfg_value!r}", end="\n\n", file=out_file)
1201
- else:
1202
- print(f"# c.{class_path!s}.{name!s} = {value!r}", end="\n\n", file=out_file)
1203
- if class_names is None:
1204
- class_names = []
1205
- for sub_klass in sub_classes:
1206
- self._iterate_config_class(
1207
- sub_klass, class_names + [sub_klass.__name__], config=config.get(sub_klass.__name__, {}), out_file=out_file
1208
- )
1209
-
1210
- def generate_config_file(self, file_like: io.IOBase, config=None) -> None:
1211
- print("#", file=file_like)
1212
- print("# Configuration file for pyXCP.", file=file_like)
1213
- print("#", file=file_like)
1214
- print("c = get_config() # noqa", end="\n\n", file=file_like)
1215
-
1216
- for klass in self._classes_with_config_traits():
1217
- self._iterate_config_class(
1218
- klass, [klass.__name__], config=self.config.get(klass.__name__, {}) if config is None else {}, out_file=file_like
1219
- )
1220
-
1221
-
1222
- application: typing.Optional[PyXCP] = None
1223
-
1224
-
1225
- def create_application(options: typing.Optional[typing.List[typing.Any]] = None, callout=None) -> PyXCP:
1226
- global application
1227
- if options is None:
1228
- options = []
1229
- if application is not None:
1230
- return application
1231
- application = PyXCP()
1232
- application.initialize(sys.argv)
1233
- application.start()
1234
-
1235
- # Set callout function if provided
1236
- if callout is not None:
1237
- application.callout = callout
1238
-
1239
- # Process custom arguments if provided
1240
- if options and hasattr(application, "custom_args"):
1241
- application.custom_args.update_from_options(options)
1242
-
1243
- return application
1244
-
1245
-
1246
- def get_application(options: typing.Optional[typing.List[typing.Any]] = None, callout=None) -> PyXCP:
1247
- if options is None:
1248
- options = []
1249
- global application
1250
- if application is None:
1251
- application = create_application(options, callout)
1252
- return application
1253
-
1254
-
1255
- def reset_application() -> None:
1256
- global application
1257
- del application
1258
- application = None
1
+ #!/usr/bin/env python
2
+ import io
3
+ import json
4
+ import logging
5
+ import sys
6
+ import typing
7
+ from pathlib import Path
8
+
9
+ import can
10
+ import toml
11
+ from rich.logging import RichHandler
12
+ from rich.prompt import Confirm
13
+ from traitlets import (
14
+ Any,
15
+ Bool,
16
+ Callable,
17
+ Dict,
18
+ Enum,
19
+ Float,
20
+ HasTraits,
21
+ Integer,
22
+ List,
23
+ TraitError,
24
+ Unicode,
25
+ Union,
26
+ )
27
+ from traitlets.config import Application, Configurable, Instance, default
28
+ from traitlets.config.loader import Config
29
+
30
+ from pyxcp.config import legacy
31
+
32
+
33
+ class CanBase:
34
+ has_fd = False
35
+ has_bitrate = True
36
+ has_data_bitrate = False
37
+ has_poll_interval = False
38
+ has_receive_own_messages = False
39
+ has_timing = False
40
+
41
+ OPTIONAL_BASE_PARAMS = (
42
+ "has_fd",
43
+ "has_bitrate",
44
+ "has_data_bitrate",
45
+ "has_poll_interval",
46
+ "has_receive_own_messages",
47
+ "has_timing",
48
+ )
49
+
50
+ CAN_PARAM_MAP = {
51
+ "sjw_abr": None,
52
+ "tseg1_abr": None,
53
+ "tseg2_abr": None,
54
+ "sjw_dbr": None,
55
+ "tseg1_dbr": None,
56
+ "tseg2_dbr": None,
57
+ }
58
+
59
+
60
+ class CanAlystii(Configurable, CanBase):
61
+ """CANalyst-II is a USB to CAN Analyzer device produced by Chuangxin Technology."""
62
+
63
+ interface_name = "canalystii"
64
+
65
+ has_timing = True
66
+
67
+ device = Integer(default_value=None, allow_none=True, help="""Optional USB device number.""").tag(config=True)
68
+ rx_queue_size = Integer(
69
+ default_value=None,
70
+ allow_none=True,
71
+ help="""If set, software received message queue can only grow to this many
72
+ messages (for all channels) before older messages are dropped """,
73
+ ).tag(config=True)
74
+
75
+
76
+ class CanTact(Configurable, CanBase):
77
+ """Interface for CANtact devices from Linklayer Labs"""
78
+
79
+ interface_name = "cantact"
80
+
81
+ has_poll_interval = True
82
+ has_timing = True
83
+
84
+ monitor = Bool(default_value=False, allow_none=True, help="""If true, operate in listen-only monitoring mode""").tag(
85
+ config=True
86
+ )
87
+
88
+
89
+ class Etas(Configurable, CanBase):
90
+ """ETAS"""
91
+
92
+ interface_name = "etas"
93
+
94
+ has_fd = True
95
+ has_data_bitrate = True
96
+ has_receive_own_messages = True
97
+
98
+
99
+ class Gs_Usb(Configurable, CanBase):
100
+ """Geschwister Schneider USB/CAN devices and candleLight USB CAN interfaces."""
101
+
102
+ interface_name = "gs_usb"
103
+
104
+ index = Integer(
105
+ default_value=None,
106
+ allow_none=True,
107
+ help="""device number if using automatic scan, starting from 0.
108
+ If specified, bus/address shall not be provided.""",
109
+ ).tag(config=True)
110
+ bus = Integer(default_value=None, allow_none=True, help="""number of the bus that the device is connected to""").tag(
111
+ config=True
112
+ )
113
+ address = Integer(default_value=None, allow_none=True, help="""address of the device on the bus it is connected to""").tag(
114
+ config=True
115
+ )
116
+
117
+
118
+ class Neovi(Configurable, CanBase):
119
+ """Intrepid Control Systems (ICS) neoVI interfaces."""
120
+
121
+ interface_name = "neovi"
122
+
123
+ has_fd = True
124
+ has_data_bitrate = True
125
+ has_receive_own_messages = True
126
+
127
+ use_system_timestamp = Bool(
128
+ default_value=None, allow_none=True, help="Use system timestamp for can messages instead of the hardware timestamp"
129
+ ).tag(config=True)
130
+ serial = Unicode(
131
+ default_value=None, allow_none=True, help="Serial to connect (optional, will use the first found if not supplied)"
132
+ ).tag(config=True)
133
+ override_library_name = Unicode(
134
+ default_value=None, allow_none=True, help="Absolute path or relative path to the library including filename."
135
+ ).tag(config=True)
136
+
137
+
138
+ class IsCan(Configurable, CanBase):
139
+ """Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH."""
140
+
141
+ interface_name = "iscan"
142
+
143
+ has_poll_interval = True
144
+
145
+
146
+ class Ixxat(Configurable, CanBase):
147
+ """IXXAT Virtual Communication Interface"""
148
+
149
+ interface_name = "ixxat"
150
+
151
+ has_fd = True
152
+ has_data_bitrate = True
153
+ has_receive_own_messages = True
154
+
155
+ unique_hardware_id = Integer(
156
+ default_value=None,
157
+ allow_none=True,
158
+ help="""UniqueHardwareId to connect (optional, will use the first found if not supplied)""",
159
+ ).tag(config=True)
160
+ extended = Bool(default_value=None, allow_none=True, help="""Enables the capability to use extended IDs.""").tag(config=True)
161
+ rx_fifo_size = Integer(default_value=None, allow_none=True, help="""Receive fifo size""").tag(config=True)
162
+ tx_fifo_size = Integer(default_value=None, allow_none=True, help="""Transmit fifo size""").tag(config=True)
163
+ ssp_dbr = Integer(
164
+ default_value=None,
165
+ allow_none=True,
166
+ help="Secondary sample point (data). Only takes effect with fd and bitrate switch enabled.",
167
+ ).tag(config=True)
168
+
169
+ CAN_PARAM_MAP = {
170
+ "sjw_abr": "sjw_abr",
171
+ "tseg1_abr": "tseg1_abr",
172
+ "tseg2_abr": "tseg2_abr",
173
+ "sjw_dbr": "sjw_dbr",
174
+ "tseg1_dbr": "tseg1_dbr",
175
+ "tseg2_dbr": "tseg2_dbr",
176
+ }
177
+
178
+
179
+ class Kvaser(Configurable, CanBase):
180
+ """Kvaser's CANLib"""
181
+
182
+ interface_name = "kvaser"
183
+
184
+ has_fd = True
185
+ has_data_bitrate = True
186
+ has_receive_own_messages = True
187
+
188
+ CAN_PARAM_MAP = {
189
+ "sjw_abr": "sjw",
190
+ "tseg1_abr": "tseg1",
191
+ "tseg2_abr": "tseg2",
192
+ }
193
+
194
+ accept_virtual = Bool(default_value=None, allow_none=True, help="If virtual channels should be accepted.").tag(config=True)
195
+ no_samp = Enum(
196
+ values=[1, 3],
197
+ default_value=None,
198
+ allow_none=True,
199
+ help="""Either 1 or 3. Some CAN controllers can also sample each bit three times.
200
+ In this case, the bit will be sampled three quanta in a row,
201
+ with the last sample being taken in the edge between TSEG1 and TSEG2.
202
+ Three samples should only be used for relatively slow baudrates""",
203
+ ).tag(config=True)
204
+ driver_mode = Bool(default_value=None, allow_none=True, help="Silent or normal.").tag(config=True)
205
+ single_handle = Bool(
206
+ default_value=None,
207
+ allow_none=True,
208
+ help="""Use one Kvaser CANLIB bus handle for both reading and writing.
209
+ This can be set if reading and/or writing is done from one thread. """,
210
+ ).tag(config=True)
211
+
212
+
213
+ class NeouSys(Configurable, CanBase):
214
+ """Neousys CAN Interface"""
215
+
216
+ interface_name = "neousys"
217
+
218
+ device = Integer(default_value=None, allow_none=True, help="Device number").tag(config=True)
219
+
220
+
221
+ class NiCan(Configurable, CanBase):
222
+ """National Instruments NI-CAN"""
223
+
224
+ interface_name = "nican"
225
+
226
+ log_errors = Bool(
227
+ default_value=None,
228
+ allow_none=True,
229
+ help="""If True, communication errors will appear as CAN messages with
230
+ ``is_error_frame`` set to True and ``arbitration_id`` will identify
231
+ the error. """,
232
+ ).tag(config=True)
233
+
234
+
235
+ class NixNet(Configurable, CanBase):
236
+ """National Instruments NI-XNET"""
237
+
238
+ interface_name = "nixnet"
239
+
240
+ has_poll_interval = True
241
+ has_receive_own_messages = True
242
+ has_timing = True
243
+ has_fd = True
244
+
245
+ CAN_PARAM_MAP = {
246
+ "data_bitrate": "fd_bitrate",
247
+ }
248
+
249
+ can_termination = Bool(default_value=None, allow_none=True, help="Enable bus termination.")
250
+
251
+
252
+ class PCan(Configurable, CanBase):
253
+ """PCAN Basic API"""
254
+
255
+ interface_name = "pcan"
256
+
257
+ has_fd = True
258
+ has_timing = True
259
+
260
+ CAN_PARAM_MAP = {
261
+ "sjw_abr": "nom_sjw",
262
+ "tseg1_abr": "nom_tseg1",
263
+ "tseg2_abr": "nom_tseg2",
264
+ "sjw_dbr": "data_sjw",
265
+ "tseg1_dbr": "data_tseg1",
266
+ "tseg2_dbr": "data_tseg2",
267
+ }
268
+
269
+ device_id = Integer(
270
+ default_value=None,
271
+ allow_none=True,
272
+ help="""Select the PCAN interface based on its ID. The device ID is a 8/32bit
273
+ value that can be configured for each PCAN device. If you set the
274
+ device_id parameter, it takes precedence over the channel parameter.
275
+ The constructor searches all connected interfaces and initializes the
276
+ first one that matches the parameter value. If no device is found,
277
+ an exception is raised.""",
278
+ ).tag(config=True)
279
+ state = Instance(klass=can.BusState, default_value=None, allow_none=True, help="BusState of the channel.").tag(config=True)
280
+
281
+ f_clock = Enum(
282
+ values=[20000000, 24000000, 30000000, 40000000, 60000000, 80000000],
283
+ default_value=None,
284
+ allow_none=True,
285
+ help="""Ignored if not using CAN-FD.
286
+ Pass either f_clock or f_clock_mhz.""",
287
+ ).tag(config=True)
288
+ f_clock_mhz = Enum(
289
+ values=[20, 24, 30, 40, 60, 80],
290
+ default_value=None,
291
+ allow_none=True,
292
+ help="""Ignored if not using CAN-FD.
293
+ Pass either f_clock or f_clock_mhz. """,
294
+ ).tag(config=True)
295
+
296
+ nom_brp = Integer(
297
+ min=1,
298
+ max=1024,
299
+ default_value=None,
300
+ allow_none=True,
301
+ help="""Clock prescaler for nominal time quantum.
302
+ Ignored if not using CAN-FD.""",
303
+ ).tag(config=True)
304
+ data_brp = Integer(
305
+ min=1,
306
+ max=1024,
307
+ default_value=None,
308
+ allow_none=True,
309
+ help="""Clock prescaler for fast data time quantum.
310
+ Ignored if not using CAN-FD.""",
311
+ ).tag(config=True)
312
+
313
+ auto_reset = Bool(
314
+ default_value=None,
315
+ allow_none=True,
316
+ help="""Enable automatic recovery in bus off scenario.
317
+ Resetting the driver takes ~500ms during which
318
+ it will not be responsive.""",
319
+ ).tag(config=True)
320
+
321
+
322
+ class Robotell(Configurable, CanBase):
323
+ """Interface for Chinese Robotell compatible interfaces"""
324
+
325
+ interface_name = "robotell"
326
+
327
+ ttyBaudrate = Integer(
328
+ default_value=None,
329
+ allow_none=True,
330
+ help="""baudrate of underlying serial or usb device
331
+ (Ignored if set via the `channel` parameter, e.g. COM7@11500).""",
332
+ ).tag(config=True)
333
+ rtscts = Bool(default_value=None, allow_none=True, help="turn hardware handshake (RTS/CTS) on and off.").tag(config=True)
334
+
335
+
336
+ class SeeedStudio(Configurable, CanBase):
337
+ """Seeed USB-Can analyzer interface."""
338
+
339
+ interface_name = "seeedstudio"
340
+
341
+ timeout = Float(default_value=None, allow_none=True, help="Timeout for the serial device in seconds.").tag(config=True)
342
+ baudrate = Integer(default_value=None, allow_none=True, help="Baud rate of the serial device in bit/s.").tag(config=True)
343
+ frame_type = Enum(
344
+ values=["STD", "EXT"], default_value=None, allow_none=True, help="To select standard or extended messages."
345
+ ).tag(config=True)
346
+ operation_mode = Enum(
347
+ values=["normal", "loopback", "silent", "loopback_and_silent"], default_value=None, allow_none=True, help=""" """
348
+ ).tag(config=True)
349
+
350
+
351
+ class Serial(Configurable, CanBase):
352
+ """A text based interface."""
353
+
354
+ interface_name = "serial"
355
+
356
+ has_bitrate = False
357
+
358
+ rtscts = Bool(default_value=None, allow_none=True, help="turn hardware handshake (RTS/CTS) on and off.").tag(config=True)
359
+ timeout = Float(default_value=None, allow_none=True, help="Timeout for the serial device in seconds.").tag(config=True)
360
+ baudrate = Integer(default_value=None, allow_none=True, help="Baud rate of the serial device in bit/s.").tag(config=True)
361
+
362
+
363
+ class SlCan(Configurable, CanBase):
364
+ """CAN over Serial / SLCAN."""
365
+
366
+ interface_name = "slcan"
367
+
368
+ has_poll_interval = True
369
+
370
+ ttyBaudrate = Integer(default_value=None, allow_none=True, help="Baud rate of the serial device in bit/s.").tag(config=True)
371
+ rtscts = Bool(default_value=None, allow_none=True, help="turn hardware handshake (RTS/CTS) on and off.").tag(config=True)
372
+ timeout = Float(default_value=None, allow_none=True, help="Timeout for the serial device in seconds.").tag(config=True)
373
+ btr = Integer(default_value=None, allow_none=True, help="BTR register value to set custom can speed.").tag(config=True)
374
+ sleep_after_open = Float(
375
+ default_value=None, allow_none=True, help="Time to wait in seconds after opening serial connection."
376
+ ).tag(config=True)
377
+
378
+
379
+ class SocketCan(Configurable, CanBase):
380
+ """Linux SocketCAN."""
381
+
382
+ interface_name = "socketcan"
383
+
384
+ has_fd = True
385
+ has_bitrate = False
386
+ has_receive_own_messages = True
387
+
388
+ local_loopback = Bool(
389
+ default_value=None,
390
+ allow_none=True,
391
+ help="""If local loopback should be enabled on this bus.
392
+ Please note that local loopback does not mean that messages sent
393
+ on a socket will be readable on the same socket, they will only
394
+ be readable on other open sockets on the same machine. More info
395
+ can be read on the socketcan documentation:
396
+ See https://www.kernel.org/doc/html/latest/networking/can.html#socketcan-local-loopback1""",
397
+ ).tag(config=True)
398
+
399
+
400
+ class SocketCanD(Configurable, CanBase):
401
+ """Network-to-CAN bridge as a Linux damon."""
402
+
403
+ interface_name = "socketcand"
404
+
405
+ has_bitrate = False
406
+
407
+ host = Unicode(default_value=None, allow_none=True, help=""" """).tag(config=True)
408
+ port = Integer(default_value=None, allow_none=True, help=""" """).tag(config=True)
409
+
410
+
411
+ class Systec(Configurable, CanBase):
412
+ """SYSTEC interface"""
413
+
414
+ interface_name = "systec"
415
+
416
+ has_receive_own_messages = True
417
+
418
+ state = Instance(klass=can.BusState, default_value=None, allow_none=True, help="BusState of the channel.").tag(config=True)
419
+ device_number = Integer(min=0, max=254, default_value=None, allow_none=True, help="The device number of the USB-CAN.").tag(
420
+ config=True
421
+ )
422
+ rx_buffer_entries = Integer(
423
+ default_value=None, allow_none=True, help="The maximum number of entries in the receive buffer."
424
+ ).tag(config=True)
425
+ tx_buffer_entries = Integer(
426
+ default_value=None, allow_none=True, help="The maximum number of entries in the transmit buffer."
427
+ ).tag(config=True)
428
+
429
+
430
+ class Udp_Multicast(Configurable, CanBase):
431
+ """A virtual interface for CAN communications between multiple processes using UDP over Multicast IP."""
432
+
433
+ interface_name = "udp_multicast"
434
+
435
+ has_fd = True
436
+ has_bitrate = False
437
+ has_receive_own_messages = True
438
+
439
+ port = Integer(default_value=None, allow_none=True, help="The IP port to read from and write to.").tag(config=True)
440
+ hop_limit = Integer(default_value=None, allow_none=True, help="The hop limit in IPv6 or in IPv4 the time to live (TTL).").tag(
441
+ config=True
442
+ )
443
+
444
+
445
+ class Usb2Can(Configurable, CanBase):
446
+ """Interface to a USB2CAN Bus."""
447
+
448
+ interface_name = "usb2can"
449
+
450
+ flags = Integer(
451
+ default_value=None, allow_none=True, help="Flags to directly pass to open function of the usb2can abstraction layer."
452
+ ).tag(config=True)
453
+ dll = Unicode(default_value=None, allow_none=True, help="Path to the DLL with the CANAL API to load.").tag(config=True)
454
+ serial = Unicode(default_value=None, allow_none=True, help="Alias for `channel` that is provided for legacy reasons.").tag(
455
+ config=True
456
+ )
457
+
458
+
459
+ class Vector(Configurable, CanBase):
460
+ """Vector Informatik CAN interfaces."""
461
+
462
+ interface_name = "vector"
463
+
464
+ has_fd = True
465
+ has_data_bitrate = True
466
+ has_poll_interval = True
467
+ has_receive_own_messages = True
468
+ has_timing = True
469
+
470
+ CAN_PARAM_MAP = {
471
+ "sjw_abr": "sjw_abr",
472
+ "tseg1_abr": "tseg1_abr",
473
+ "tseg2_abr": "tseg2_abr",
474
+ "sjw_dbr": "sjw_dbr",
475
+ "tseg1_dbr": "tseg1_dbr",
476
+ "tseg2_dbr": "tseg2_dbr",
477
+ }
478
+
479
+ serial = Integer(
480
+ default_value=None,
481
+ allow_none=True,
482
+ help="""Serial number of the hardware to be used.
483
+ If set, the channel parameter refers to the channels ONLY on the specified hardware.
484
+ If set, the `app_name` does not have to be previously defined in
485
+ *Vector Hardware Config*.""",
486
+ ).tag(config=True)
487
+ rx_queue_size = Integer(
488
+ min=16, max=32768, default_value=None, allow_none=True, help="Number of messages in receive queue (power of 2)."
489
+ ).tag(config=True)
490
+ app_name = Unicode(default_value=None, allow_none=True, help="Name of application in *Vector Hardware Config*.").tag(
491
+ config=True
492
+ )
493
+
494
+
495
+ class CanCustom(Configurable, CanBase):
496
+ """Generic custom CAN interface.
497
+
498
+ Enable basic CanBase options so user-provided python-can backends can
499
+ consume common parameters like bitrate, fd, data_bitrate, poll_interval,
500
+ receive_own_messages, and optional timing.
501
+ """
502
+
503
+ interface_name = "custom"
504
+
505
+ # Allow usage of the basic options from CanBase for custom backends
506
+ has_fd = True
507
+ has_data_bitrate = True
508
+ has_poll_interval = True
509
+ has_receive_own_messages = True
510
+ has_timing = True
511
+
512
+
513
+ class Virtual(Configurable, CanBase):
514
+ """ """
515
+
516
+ interface_name = "virtual"
517
+
518
+ has_bitrate = False
519
+ has_receive_own_messages = True
520
+
521
+ rx_queue_size = Integer(
522
+ default_value=None,
523
+ allow_none=True,
524
+ help="""The size of the reception queue. The reception
525
+ queue stores messages until they are read. If the queue reaches
526
+ its capacity, it will start dropping the oldest messages to make
527
+ room for new ones. If set to 0, the queue has an infinite capacity.
528
+ Be aware that this can cause memory leaks if messages are read
529
+ with a lower frequency than they arrive on the bus. """,
530
+ ).tag(config=True)
531
+ preserve_timestamps = Bool(
532
+ default_value=None,
533
+ allow_none=True,
534
+ help="""If set to True, messages transmitted via
535
+ will keep the timestamp set in the
536
+ :class:`~can.Message` instance. Otherwise, the timestamp value
537
+ will be replaced with the current system time.""",
538
+ ).tag(config=True)
539
+
540
+
541
+ CAN_INTERFACE_MAP = {
542
+ "canalystii": CanAlystii,
543
+ "cantact": CanTact,
544
+ "etas": Etas,
545
+ "gs_usb": Gs_Usb,
546
+ "iscan": IsCan,
547
+ "ixxat": Ixxat,
548
+ "kvaser": Kvaser,
549
+ "neousys": NeouSys,
550
+ "neovi": Neovi,
551
+ "nican": NiCan,
552
+ "nixnet": NixNet,
553
+ "pcan": PCan,
554
+ "robotell": Robotell,
555
+ "seeedstudio": SeeedStudio,
556
+ "serial": Serial,
557
+ "slcan": SlCan,
558
+ "socketcan": SocketCan,
559
+ "socketcand": SocketCanD,
560
+ "systec": Systec,
561
+ "udp_multicast": Udp_Multicast,
562
+ "usb2can": Usb2Can,
563
+ "vector": Vector,
564
+ "virtual": Virtual,
565
+ "custom": CanCustom,
566
+ }
567
+
568
+
569
+ class Can(Configurable):
570
+ VALID_INTERFACES = set(can.interfaces.VALID_INTERFACES)
571
+ VALID_INTERFACES.add("custom")
572
+
573
+ interface = Enum(
574
+ values=VALID_INTERFACES, default_value=None, allow_none=True, help="CAN interface supported by python-can"
575
+ ).tag(config=True)
576
+ channel = Any(
577
+ default_value=None, allow_none=True, help="Channel identification. Expected type and value is backend dependent."
578
+ ).tag(config=True)
579
+ max_dlc_required = Bool(False, help="Master to slave frames always to have DLC = MAX_DLC = 8").tag(config=True)
580
+ # max_can_fd_dlc = Integer(64, help="").tag(config=True)
581
+ padding_value = Integer(0, help="Fill value, if max_dlc_required == True and DLC < MAX_DLC").tag(config=True)
582
+ use_default_listener = Bool(True, help="").tag(config=True)
583
+ can_id_master = Integer(allow_none=False, help="CAN-ID master -> slave (Bit31= 1: extended identifier)").tag(
584
+ config=True
585
+ ) # CMD and STIM packets
586
+ can_id_slave = Integer(allow_none=True, help="CAN-ID slave -> master (Bit31= 1: extended identifier)").tag(
587
+ config=True
588
+ ) # RES, ERR, EV, SERV and DAQ packets.
589
+ can_id_broadcast = Integer(
590
+ default_value=None, allow_none=True, help="Auto detection CAN-ID (Bit31= 1: extended identifier)"
591
+ ).tag(config=True)
592
+ daq_identifier = List(trait=Integer(), default_value=[], allow_none=True, help="One CAN identifier per DAQ-list.").tag(
593
+ config=True
594
+ )
595
+ bitrate = Integer(250000, help="CAN bitrate in bits/s (arbitration phase, if CAN FD).").tag(config=True)
596
+ receive_own_messages = Bool(False, help="Enable self-reception of sent messages.").tag(config=True)
597
+ poll_interval = Float(default_value=None, allow_none=True, help="Poll interval in seconds when reading messages.").tag(
598
+ config=True
599
+ )
600
+ fd = Bool(False, help="If CAN-FD frames should be supported.").tag(config=True)
601
+ data_bitrate = Integer(default_value=None, allow_none=True, help="Which bitrate to use for data phase in CAN FD.").tag(
602
+ config=True
603
+ )
604
+ sjw_abr = Integer(
605
+ default_value=None, allow_none=True, help="Bus timing value sample jump width (arbitration, SJW if CAN classic)."
606
+ ).tag(config=True)
607
+ tseg1_abr = Integer(
608
+ default_value=None, allow_none=True, help="Bus timing value tseg1 (arbitration, TSEG1 if CAN classic)."
609
+ ).tag(config=True)
610
+ tseg2_abr = Integer(
611
+ default_value=None, allow_none=True, help="Bus timing value tseg2 (arbitration, TSEG2, if CAN classic)"
612
+ ).tag(config=True)
613
+ sjw_dbr = Integer(default_value=None, allow_none=True, help="Bus timing value sample jump width (data).").tag(config=True)
614
+ tseg1_dbr = Integer(default_value=None, allow_none=True, help="Bus timing value tseg1 (data).").tag(config=True)
615
+ tseg2_dbr = Integer(default_value=None, allow_none=True, help="Bus timing value tseg2 (data).").tag(config=True)
616
+ timing = Union(
617
+ trait_types=[Instance(klass=can.BitTiming), Instance(klass=can.BitTimingFd)],
618
+ default_value=None,
619
+ allow_none=True,
620
+ help="""Custom bit timing settings.
621
+ (.s https://github.com/hardbyte/python-can/blob/develop/can/bit_timing.py)
622
+ If this parameter is provided, it takes precedence over all other
623
+ timing-related parameters.
624
+ """,
625
+ ).tag(config=True)
626
+
627
+ classes = List(
628
+ [
629
+ CanAlystii,
630
+ CanCustom,
631
+ CanTact,
632
+ Etas,
633
+ Gs_Usb,
634
+ Neovi,
635
+ IsCan,
636
+ Ixxat,
637
+ Kvaser,
638
+ NeouSys,
639
+ NiCan,
640
+ NixNet,
641
+ PCan,
642
+ Robotell,
643
+ SeeedStudio,
644
+ Serial,
645
+ SlCan,
646
+ SocketCan,
647
+ SocketCanD,
648
+ Systec,
649
+ Udp_Multicast,
650
+ Usb2Can,
651
+ Vector,
652
+ Virtual,
653
+ ]
654
+ )
655
+
656
+ def __init__(self, **kws):
657
+ super().__init__(**kws)
658
+
659
+ if self.parent.layer == "CAN":
660
+ if self.interface is None or self.interface not in self.VALID_INTERFACES:
661
+ raise TraitError(
662
+ f"CAN interface must be one of {sorted(list(self.VALID_INTERFACES))} not the"
663
+ " {type(self.interface).__name__} {self.interface}."
664
+ )
665
+ self.canalystii = CanAlystii(config=self.config, parent=self)
666
+ self.cancustom = CanCustom(config=self.config, parent=self)
667
+ self.cantact = CanTact(config=self.config, parent=self)
668
+ self.etas = Etas(config=self.config, parent=self)
669
+ self.gs_usb = Gs_Usb(config=self.config, parent=self)
670
+ self.neovi = Neovi(config=self.config, parent=self)
671
+ self.iscan = IsCan(config=self.config, parent=self)
672
+ self.ixxat = Ixxat(config=self.config, parent=self)
673
+ self.kvaser = Kvaser(config=self.config, parent=self)
674
+ self.neousys = NeouSys(config=self.config, parent=self)
675
+ self.nican = NiCan(config=self.config, parent=self)
676
+ self.nixnet = NixNet(config=self.config, parent=self)
677
+ self.pcan = PCan(config=self.config, parent=self)
678
+ self.robotell = Robotell(config=self.config, parent=self)
679
+ self.seeedstudio = SeeedStudio(config=self.config, parent=self)
680
+ self.serial = Serial(config=self.config, parent=self)
681
+ self.slcan = SlCan(config=self.config, parent=self)
682
+ self.socketcan = SocketCan(config=self.config, parent=self)
683
+ self.socketcand = SocketCanD(config=self.config, parent=self)
684
+ self.systec = Systec(config=self.config, parent=self)
685
+ self.udp_multicast = Udp_Multicast(config=self.config, parent=self)
686
+ self.usb2can = Usb2Can(config=self.config, parent=self)
687
+ self.vector = Vector(config=self.config, parent=self)
688
+ self.virtual = Virtual(config=self.config, parent=self)
689
+
690
+
691
+ class Eth(Configurable):
692
+ """Ethernet."""
693
+
694
+ host = Unicode("localhost", help="Hostname or IP address of XCP slave.").tag(config=True)
695
+ port = Integer(5555, help="TCP/UDP port to connect.").tag(config=True)
696
+ protocol = Enum(values=["TCP", "UDP"], default_value="UDP", help="").tag(config=True)
697
+ ipv6 = Bool(False, help="Use IPv6 if `True` else IPv4.").tag(config=True)
698
+ tcp_nodelay = Bool(False, help="*** Expert option *** -- Disable Nagle's algorithm if `True`.").tag(config=True)
699
+ bind_to_address = Unicode(default_value=None, allow_none=True, help="Bind to specific local address.").tag(config=True)
700
+ bind_to_port = Integer(default_value=None, allow_none=True, help="Bind to specific local port.").tag(config=True)
701
+
702
+
703
+ class SxI(Configurable):
704
+ """SCI and SPI connections."""
705
+
706
+ port = Unicode("COM1", help="Name of communication interface.").tag(config=True)
707
+ bitrate = Integer(38400, help="Connection bitrate").tag(config=True)
708
+ bytesize = Enum(values=[5, 6, 7, 8], default_value=8, help="Size of byte.").tag(config=True)
709
+ parity = Enum(values=["N", "E", "O", "M", "S"], default_value="N", help="Paritybit calculation.").tag(config=True)
710
+ stopbits = Enum(values=[1, 1.5, 2], default_value=1, help="Number of stopbits.").tag(config=True)
711
+ mode = Enum(
712
+ values=[
713
+ "ASYNCH_FULL_DUPLEX_MODE",
714
+ "SYNCH_FULL_DUPLEX_MODE_BYTE",
715
+ "SYNCH_FULL_DUPLEX_MODE_WORD",
716
+ "SYNCH_FULL_DUPLEX_MODE_DWORD",
717
+ "SYNCH_MASTER_SLAVE_MODE_BYTE",
718
+ "SYNCH_MASTER_SLAVE_MODE_WORD",
719
+ "SYNCH_MASTER_SLAVE_MODE_DWORD",
720
+ ],
721
+ default_value="ASYNCH_FULL_DUPLEX_MODE",
722
+ help="Asynchronous (SCI) or synchronous (SPI) communication mode.",
723
+ ).tag(config=True)
724
+ header_format = Enum(
725
+ values=[
726
+ "HEADER_LEN_BYTE",
727
+ "HEADER_LEN_CTR_BYTE",
728
+ "HEADER_LEN_FILL_BYTE",
729
+ "HEADER_LEN_WORD",
730
+ "HEADER_LEN_CTR_WORD",
731
+ "HEADER_LEN_FILL_WORD",
732
+ ],
733
+ default_value="HEADER_LEN_CTR_WORD",
734
+ help="""XCPonSxI header format.
735
+ Number of bytes:
736
+
737
+ LEN CTR FILL
738
+ ______________________________________________________________
739
+ HEADER_LEN_BYTE | 1 X X
740
+ HEADER_LEN_CTR_BYTE | 1 1 X
741
+ HEADER_LEN_FILL_BYTE | 1 X 1
742
+ HEADER_LEN_WORD | 2 X X
743
+ HEADER_LEN_CTR_WORD | 2 2 X
744
+ HEADER_LEN_FILL_WORD | 2 X 2
745
+ """,
746
+ ).tag(config=True)
747
+ tail_format = Enum(
748
+ values=["NO_CHECKSUM", "CHECKSUM_BYTE", "CHECKSUM_WORD"], default_value="NO_CHECKSUM", help="XCPonSxI tail format."
749
+ ).tag(config=True)
750
+ framing = Bool(False, help="Enable SCI framing mechanism (ESC chars).").tag(config=True)
751
+ esc_sync = Integer(0x01, min=0, max=255, help="SCI framing protocol character SYNC.").tag(config=True)
752
+ esc_esc = Integer(0x00, min=0, max=255, help="SCI framing protocol character ESC.").tag(config=True)
753
+
754
+
755
+ class Usb(Configurable):
756
+ """Universal Serial Bus connections."""
757
+
758
+ serial_number = Unicode("", help="Device serial number.").tag(config=True)
759
+ configuration_number = Integer(1, help="USB configuration number.").tag(config=True)
760
+ interface_number = Integer(2, help="USB interface number.").tag(config=True)
761
+ vendor_id = Integer(0, help="USB vendor ID.").tag(config=True)
762
+ product_id = Integer(0, help="USB product ID.").tag(config=True)
763
+ library = Unicode("", help="Absolute path to USB shared library.").tag(config=True)
764
+ header_format = Enum(
765
+ values=[
766
+ "HEADER_LEN_BYTE",
767
+ "HEADER_LEN_CTR_BYTE",
768
+ "HEADER_LEN_FILL_BYTE",
769
+ "HEADER_LEN_WORD",
770
+ "HEADER_LEN_CTR_WORD",
771
+ "HEADER_LEN_FILL_WORD",
772
+ ],
773
+ default_value="HEADER_LEN_CTR_WORD",
774
+ help="",
775
+ ).tag(config=True)
776
+ in_ep_number = Integer(1, help="Ingoing USB reply endpoint number (IN-EP for RES/ERR, DAQ, and EV/SERV).").tag(config=True)
777
+ in_ep_transfer_type = Enum(
778
+ values=["BULK_TRANSFER", "INTERRUPT_TRANSFER"], default_value="BULK_TRANSFER", help="Ingoing: Supported USB transfer types."
779
+ ).tag(config=True)
780
+ in_ep_max_packet_size = Integer(512, help="Ingoing: Maximum packet size of endpoint in bytes.").tag(config=True)
781
+ in_ep_polling_interval = Integer(0, help="Ingoing: Polling interval of endpoint.").tag(config=True)
782
+ in_ep_message_packing = Enum(
783
+ values=["MESSAGE_PACKING_SINGLE", "MESSAGE_PACKING_MULTIPLE", "MESSAGE_PACKING_STREAMING"],
784
+ default_value="MESSAGE_PACKING_SINGLE",
785
+ help="Ingoing: Packing of XCP Messages.",
786
+ ).tag(config=True)
787
+ in_ep_alignment = Enum(
788
+ values=["ALIGNMENT_8_BIT", "ALIGNMENT_16_BIT", "ALIGNMENT_32_BIT", "ALIGNMENT_64_BIT"],
789
+ default_value="ALIGNMENT_8_BIT",
790
+ help="Ingoing: Alignment border.",
791
+ ).tag(config=True)
792
+ in_ep_recommended_host_bufsize = Integer(0, help="Ingoing: Recommended host buffer size.").tag(config=True)
793
+ out_ep_number = Integer(0, help="Outgoing USB command endpoint number (OUT-EP for CMD and STIM).").tag(config=True)
794
+ out_ep_transfer_type = Enum(
795
+ values=["BULK_TRANSFER", "INTERRUPT_TRANSFER"],
796
+ default_value="BULK_TRANSFER",
797
+ help="Outgoing: Supported USB transfer types.",
798
+ ).tag(config=True)
799
+ out_ep_max_packet_size = Integer(512, help="Outgoing: Maximum packet size of endpoint in bytes.").tag(config=True)
800
+ out_ep_polling_interval = Integer(0, help="Outgoing: Polling interval of endpoint.").tag(config=True)
801
+ out_ep_message_packing = Enum(
802
+ values=["MESSAGE_PACKING_SINGLE", "MESSAGE_PACKING_MULTIPLE", "MESSAGE_PACKING_STREAMING"],
803
+ default_value="MESSAGE_PACKING_SINGLE",
804
+ help="Outgoing: Packing of XCP Messages.",
805
+ ).tag(config=True)
806
+ out_ep_alignment = Enum(
807
+ values=["ALIGNMENT_8_BIT", "ALIGNMENT_16_BIT", "ALIGNMENT_32_BIT", "ALIGNMENT_64_BIT"],
808
+ default_value="ALIGNMENT_8_BIT",
809
+ help="Outgoing: Alignment border.",
810
+ ).tag(config=True)
811
+ out_ep_recommended_host_bufsize = Integer(0, help="Outgoing: Recommended host buffer size.").tag(config=True)
812
+
813
+
814
+ class Transport(Configurable):
815
+ """ """
816
+
817
+ classes = List([Can, Eth, SxI, Usb])
818
+
819
+ layer = Enum(
820
+ values=["CAN", "ETH", "SXI", "USB"],
821
+ default_value=None,
822
+ allow_none=True,
823
+ help="Choose one of the supported XCP transport layers.",
824
+ ).tag(config=True)
825
+ create_daq_timestamps = Bool(True, help="Record time of frame reception or set timestamp to 0.").tag(config=True)
826
+ timeout = Float(
827
+ 2.0,
828
+ help="""raise `XcpTimeoutError` after `timeout` seconds
829
+ if there is no response to a command.""",
830
+ ).tag(config=True)
831
+ alignment = Enum(values=[1, 2, 4, 8], default_value=1).tag(config=True)
832
+
833
+ can = Instance(Can).tag(config=True)
834
+ eth = Instance(Eth).tag(config=True)
835
+ sxi = Instance(SxI).tag(config=True)
836
+ usb = Instance(Usb).tag(config=True)
837
+
838
+ def __init__(self, **kws):
839
+ super().__init__(**kws)
840
+ self.can = Can(config=self.config, parent=self)
841
+ self.eth = Eth(config=self.config, parent=self)
842
+ self.sxi = SxI(config=self.config, parent=self)
843
+ self.usb = Usb(config=self.config, parent=self)
844
+
845
+
846
+ class CustomArgs(Configurable):
847
+ """Class to handle custom command-line arguments."""
848
+
849
+ def __init__(self, **kwargs):
850
+ super().__init__(**kwargs)
851
+ self._custom_args = {}
852
+
853
+ def add_argument(self, short_opt, long_opt="", dest="", help="", type=None, default=None, action=None):
854
+ """Add a custom argument dynamically.
855
+
856
+ This mimics the argparse.ArgumentParser.add_argument method.
857
+ """
858
+ if not dest and long_opt:
859
+ dest = long_opt.lstrip("-").replace("-", "_")
860
+
861
+ # Store the argument definition
862
+ self._custom_args[dest] = {
863
+ "short_opt": short_opt,
864
+ "long_opt": long_opt,
865
+ "help": help,
866
+ "type": type,
867
+ "default": default,
868
+ "action": action,
869
+ "value": default,
870
+ }
871
+
872
+ # Dynamically add a trait for this argument
873
+ trait_type = Any()
874
+ if type == bool or action == "store_true" or action == "store_false":
875
+ trait_type = Bool(default)
876
+ elif type == int:
877
+ trait_type = Integer(default)
878
+ elif type == float:
879
+ trait_type = Float(default)
880
+ elif type == str:
881
+ trait_type = Unicode(default)
882
+
883
+ # Add the trait to this instance
884
+ self.add_trait(dest, trait_type)
885
+ setattr(self, dest, default)
886
+
887
+ def update_from_options(self, options):
888
+ """Update trait values from parsed options."""
889
+ for option in options:
890
+ if option.dest and option.dest in self._custom_args:
891
+ if option.default is not None:
892
+ setattr(self, option.dest, option.default)
893
+ self._custom_args[option.dest]["value"] = option.default
894
+
895
+ def get_args(self):
896
+ """Return an object with all custom arguments as attributes."""
897
+
898
+ class Args:
899
+ pass
900
+
901
+ args = Args()
902
+ for name, arg_def in self._custom_args.items():
903
+ setattr(args, name, arg_def["value"])
904
+
905
+ return args
906
+
907
+
908
+ class General(Configurable):
909
+ """ """
910
+
911
+ disable_error_handling = Bool(False, help="Disable XCP error-handler for performance reasons.").tag(config=True)
912
+ disconnect_response_optional = Bool(False, help="Ignore missing response on DISCONNECT request.").tag(config=True)
913
+ connect_retries = Integer(help="Number of CONNECT retries (None for infinite retries).", allow_none=True, default_value=3).tag(
914
+ config=True
915
+ )
916
+ # Structured diagnostics dump options
917
+ diagnostics_on_failure = Bool(True, help="Append a structured diagnostics dump to timeout errors.").tag(config=True)
918
+ diagnostics_last_pdus = Integer(20, help="How many recent PDUs to include in diagnostics dump.").tag(config=True)
919
+ seed_n_key_dll = Unicode("", allow_none=False, help="Dynamic library used for slave resource unlocking.").tag(config=True)
920
+ seed_n_key_dll_same_bit_width = Bool(False, help="").tag(config=True)
921
+ custom_dll_loader = Unicode(allow_none=True, default_value=None, help="Use an custom seed and key DLL loader.").tag(config=True)
922
+ seed_n_key_function = Callable(
923
+ default_value=None,
924
+ allow_none=True,
925
+ help="""Python function used for slave resource unlocking.
926
+ Could be used if seed-and-key algorithm is known instead of `seed_n_key_dll`.""",
927
+ ).tag(config=True)
928
+ stim_support = Bool(False, help="").tag(config=True)
929
+
930
+
931
+ class ProfileCreate(Application):
932
+ description = "\nCreate a new profile"
933
+
934
+ dest_file = Unicode(default_value=None, allow_none=True, help="destination file name").tag(config=True)
935
+ aliases = Dict( # type:ignore[assignment]
936
+ dict(
937
+ d="ProfileCreate.dest_file",
938
+ o="ProfileCreate.dest_file",
939
+ )
940
+ )
941
+
942
+ def start(self):
943
+ pyxcp = self.parent.parent
944
+ if self.dest_file:
945
+ dest = Path(self.dest_file)
946
+ if dest.exists():
947
+ if not Confirm.ask(f"Destination file [green]{dest.name!r}[/green] already exists. Do you want to overwrite it?"):
948
+ print("Aborting...")
949
+ self.exit(1)
950
+ with dest.open("w", encoding="latin1") as out_file:
951
+ pyxcp.generate_config_file(out_file)
952
+ else:
953
+ pyxcp.generate_config_file(sys.stdout)
954
+
955
+
956
+ class ProfileConvert(Application):
957
+ description = "\nConvert legacy configuration file (.json/.toml) to new Python based format."
958
+
959
+ config_file = Unicode(help="Name of legacy config file (.json/.toml).", default_value=None, allow_none=False).tag(
960
+ config=True
961
+ ) # default_value="pyxcp_conf.py",
962
+
963
+ dest_file = Unicode(default_value=None, allow_none=True, help="destination file name").tag(config=True)
964
+
965
+ aliases = Dict( # type:ignore[assignment]
966
+ dict(
967
+ c="ProfileConvert.config_file",
968
+ d="ProfileConvert.dest_file",
969
+ o="ProfileConvert.dest_file",
970
+ )
971
+ )
972
+
973
+ def start(self):
974
+ pyxcp = self.parent.parent
975
+ pyxcp._read_configuration(self.config_file, emit_warning=False)
976
+ if self.dest_file:
977
+ dest = Path(self.dest_file)
978
+ if dest.exists():
979
+ if not Confirm.ask(f"Destination file [green]{dest.name!r}[/green] already exists. Do you want to overwrite it?"):
980
+ print("Aborting...")
981
+ self.exit(1)
982
+ with dest.open("w", encoding="latin1") as out_file:
983
+ pyxcp.generate_config_file(out_file)
984
+ else:
985
+ pyxcp.generate_config_file(sys.stdout)
986
+
987
+
988
+ class ProfileApp(Application):
989
+ subcommands = Dict(
990
+ dict(
991
+ create=(ProfileCreate, ProfileCreate.description.splitlines()[0]),
992
+ convert=(ProfileConvert, ProfileConvert.description.splitlines()[0]),
993
+ )
994
+ )
995
+
996
+ def start(self):
997
+ if self.subapp is None:
998
+ print(f"No subcommand specified. Must specify one of: {self.subcommands.keys()}")
999
+ print()
1000
+ self.print_description()
1001
+ self.print_subcommands()
1002
+ self.exit(1)
1003
+ else:
1004
+ self.subapp.start()
1005
+
1006
+
1007
+ class PyXCP(Application):
1008
+ description = "pyXCP application"
1009
+ config_file = Unicode(default_value="pyxcp_conf.py", help="base name of config file").tag(config=True)
1010
+
1011
+ # Add callout function support
1012
+ callout = Callable(default_value=None, allow_none=True, help="Callback function to be called with master and args").tag(
1013
+ config=True
1014
+ )
1015
+
1016
+ # Logging options
1017
+ structured_logging = Bool(False, help="Emit one-line JSON logs instead of rich text.").tag(config=True)
1018
+ # Use log_output_format to avoid clashing with traitlets.Application.log_format (a %-style template)
1019
+ log_output_format = Enum(values=["rich", "json"], default_value="rich", help="Select logging output format.").tag(config=True)
1020
+
1021
+ classes = List([General, Transport, CustomArgs])
1022
+
1023
+ subcommands = dict(
1024
+ profile=(
1025
+ ProfileApp,
1026
+ """
1027
+ Profile stuff
1028
+ """.strip(),
1029
+ )
1030
+ )
1031
+
1032
+ def start(self):
1033
+ if self.subapp:
1034
+ self.subapp.start()
1035
+ exit(2)
1036
+ else:
1037
+ # Always read configuration and then set up our logger explicitly to avoid
1038
+ # traitlets.Application default logging using an incompatible 'log_format'.
1039
+ self._read_configuration(self.config_file)
1040
+ try:
1041
+ # Ensure base Application.log_format is a valid %-style template
1042
+ # (Users might set c.PyXCP.log_format = "json" which clashes with traitlets behavior.)
1043
+ self.log_format = "%(message)s" # type: ignore[assignment]
1044
+ except Exception:
1045
+ pass
1046
+ self._setup_logger()
1047
+ self.log.debug(f"pyxcp version: {self.version}")
1048
+
1049
+ def _setup_logger(self):
1050
+ from pyxcp.types import Command
1051
+
1052
+ # Remove any handlers installed by `traitlets`.
1053
+ for hdl in list(self.log.handlers):
1054
+ self.log.removeHandler(hdl)
1055
+
1056
+ # Decide formatter/handler based on config
1057
+ use_json = False
1058
+ try:
1059
+ # Prefer explicit log_output_format; fallback to structured_logging for compatibility
1060
+ use_json = getattr(self, "log_output_format", "rich") == "json" or getattr(self, "structured_logging", False)
1061
+ # Backward-compat: if someone set PyXCP.log_format="json" in config, honor it here too
1062
+ if not use_json:
1063
+ lf = getattr(self, "log_format", None)
1064
+ if isinstance(lf, str) and lf.lower() == "json":
1065
+ use_json = True
1066
+ except Exception:
1067
+ use_json = False
1068
+
1069
+ if use_json:
1070
+
1071
+ class JSONFormatter(logging.Formatter):
1072
+ def format(self, record: logging.LogRecord) -> str:
1073
+ # Build a minimal structured payload
1074
+ payload = {
1075
+ "time": self.formatTime(record, self.datefmt),
1076
+ "level": record.levelname,
1077
+ "logger": record.name,
1078
+ "message": record.getMessage(),
1079
+ }
1080
+ # Include extras if present
1081
+ for key in ("transport", "host", "port", "protocol", "event", "command"):
1082
+ if hasattr(record, key):
1083
+ payload[key] = getattr(record, key)
1084
+ # Exceptions
1085
+ if record.exc_info:
1086
+ payload["exc_type"] = record.exc_info[0].__name__ if record.exc_info[0] else None
1087
+ payload["exc_text"] = self.formatException(record.exc_info)
1088
+ try:
1089
+ import json as _json
1090
+
1091
+ return _json.dumps(payload, ensure_ascii=False)
1092
+ except Exception:
1093
+ return f"{payload}"
1094
+
1095
+ handler = logging.StreamHandler()
1096
+ formatter = JSONFormatter(datefmt=self.log_datefmt)
1097
+ handler.setFormatter(formatter)
1098
+ handler.setLevel(self.log_level)
1099
+ self.log.addHandler(handler)
1100
+ else:
1101
+ keywords = list(Command.__members__.keys()) + ["ARGS", "KWS"] # Syntax highlight XCP commands and other stuff.
1102
+ rich_handler = RichHandler(
1103
+ rich_tracebacks=True,
1104
+ tracebacks_show_locals=True,
1105
+ log_time_format=self.log_datefmt,
1106
+ level=self.log_level,
1107
+ keywords=keywords,
1108
+ )
1109
+ self.log.addHandler(rich_handler)
1110
+
1111
+ def initialize(self, argv=None):
1112
+ from pyxcp import __version__ as pyxcp_version
1113
+
1114
+ PyXCP.version = pyxcp_version
1115
+ PyXCP.name = Path(sys.argv[0]).name
1116
+ self.parse_command_line(argv[1:])
1117
+
1118
+ def _read_configuration(self, file_name: str, emit_warning: bool = True) -> None:
1119
+ self.read_configuration_file(file_name, emit_warning)
1120
+ self.general = General(config=self.config, parent=self)
1121
+ self.transport = Transport(parent=self)
1122
+ self.custom_args = CustomArgs(config=self.config, parent=self)
1123
+
1124
+ def read_configuration_file(self, file_name: str, emit_warning: bool = True):
1125
+ self.legacy_config: bool = False
1126
+
1127
+ pth = Path(file_name)
1128
+ if not pth.exists():
1129
+ raise FileNotFoundError(f"Configuration file {file_name!r} does not exist.")
1130
+ suffix = pth.suffix.lower()
1131
+ if suffix == ".py":
1132
+ self.load_config_file(pth)
1133
+ else:
1134
+ self.legacy_config = True
1135
+ if suffix == ".json":
1136
+ reader = json
1137
+ elif suffix == ".toml":
1138
+ reader = toml
1139
+ else:
1140
+ raise ValueError(f"Unknown file type for config: {suffix}")
1141
+ with pth.open("r") as f:
1142
+ if emit_warning:
1143
+ self.log.warning(f"Legacy configuration file format ({suffix}), please use Python based configuration.")
1144
+ cfg = reader.loads(f.read())
1145
+ if cfg:
1146
+ cfg = legacy.convert_config(cfg, self.log)
1147
+ self.config = cfg
1148
+ return cfg
1149
+
1150
+ flags = Dict( # type:ignore[assignment]
1151
+ dict(
1152
+ debug=({"PyXCP": {"log_level": 10}}, "Set loglevel to DEBUG"),
1153
+ )
1154
+ )
1155
+
1156
+ @default("log_level")
1157
+ def _default_value(self):
1158
+ return logging.INFO # traitlets default is logging.WARN
1159
+
1160
+ aliases = Dict( # type:ignore[assignment]
1161
+ dict(
1162
+ c="PyXCP.config_file", # Application
1163
+ log_level="PyXCP.log_level",
1164
+ l="PyXCP.log_level",
1165
+ )
1166
+ )
1167
+
1168
+ def _iterate_config_class(self, klass, class_names: typing.List[str], config, out_file: io.IOBase = sys.stdout) -> None:
1169
+ sub_classes = []
1170
+ class_path = ".".join(class_names)
1171
+ print(
1172
+ f"""\n# ------------------------------------------------------------------------------
1173
+ # {class_path} configuration
1174
+ # ------------------------------------------------------------------------------""",
1175
+ end="\n\n",
1176
+ file=out_file,
1177
+ )
1178
+ if hasattr(klass, "classes"):
1179
+ kkk = klass.classes
1180
+ if hasattr(kkk, "default"):
1181
+ if class_names[-1] not in ("PyXCP"):
1182
+ sub_classes.extend(kkk.default())
1183
+ for name, tr in klass.class_own_traits().items():
1184
+ md = tr.metadata
1185
+ if md.get("config"):
1186
+ help = md.get("help", "").lstrip()
1187
+ commented_lines = "\n".join([f"# {line}" for line in help.split("\n")])
1188
+ print(f"#{commented_lines}", file=out_file)
1189
+ value = tr.default()
1190
+ if isinstance(tr, Instance) and tr.__class__.__name__ not in ("Dict", "List"):
1191
+ continue
1192
+ if isinstance(tr, Enum):
1193
+ print(f"# Choices: {tr.info()}", file=out_file)
1194
+ else:
1195
+ print(f"# Type: {tr.info()}", file=out_file)
1196
+ print(f"# Default: {value!r}", file=out_file)
1197
+ if name in config:
1198
+ cfg_value = config[name]
1199
+ print(f"c.{class_path!s}.{name!s} = {cfg_value!r}", end="\n\n", file=out_file)
1200
+ else:
1201
+ print(f"# c.{class_path!s}.{name!s} = {value!r}", end="\n\n", file=out_file)
1202
+ if class_names is None:
1203
+ class_names = []
1204
+ for sub_klass in sub_classes:
1205
+ self._iterate_config_class(
1206
+ sub_klass, class_names + [sub_klass.__name__], config=config.get(sub_klass.__name__, {}), out_file=out_file
1207
+ )
1208
+
1209
+ def generate_config_file(self, file_like: io.IOBase, config=None) -> None:
1210
+ print("#", file=file_like)
1211
+ print("# Configuration file for pyXCP.", file=file_like)
1212
+ print("#", file=file_like)
1213
+ print("c = get_config() # noqa", end="\n\n", file=file_like)
1214
+
1215
+ for klass in self._classes_with_config_traits():
1216
+ self._iterate_config_class(
1217
+ klass, [klass.__name__], config=self.config.get(klass.__name__, {}) if config is None else {}, out_file=file_like
1218
+ )
1219
+
1220
+
1221
+ application: typing.Optional[PyXCP] = None
1222
+
1223
+
1224
+ def create_application(options: typing.Optional[typing.List[typing.Any]] = None, callout=None) -> PyXCP:
1225
+ global application
1226
+ if options is None:
1227
+ options = []
1228
+ if application is not None:
1229
+ return application
1230
+ application = PyXCP()
1231
+ application.initialize(sys.argv)
1232
+ application.start()
1233
+
1234
+ # Set callout function if provided
1235
+ if callout is not None:
1236
+ application.callout = callout
1237
+
1238
+ # Process custom arguments if provided
1239
+ if options and hasattr(application, "custom_args"):
1240
+ application.custom_args.update_from_options(options)
1241
+
1242
+ return application
1243
+
1244
+
1245
+ def get_application(options: typing.Optional[typing.List[typing.Any]] = None, callout=None) -> PyXCP:
1246
+ if options is None:
1247
+ options = []
1248
+ global application
1249
+ if application is None:
1250
+ application = create_application(options, callout)
1251
+ return application
1252
+
1253
+
1254
+ def reset_application() -> None:
1255
+ global application
1256
+ del application
1257
+ application = None