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/transport/can.py CHANGED
@@ -20,9 +20,14 @@ from can.interface import _get_class_for_interface
20
20
  from rich.console import Console
21
21
 
22
22
  from pyxcp.config import CAN_INTERFACE_MAP
23
- from pyxcp.transport.base import BaseTransport
23
+ from pyxcp.transport.base import (
24
+ BaseTransport,
25
+ ChecksumType,
26
+ XcpFramingConfig,
27
+ XcpTransportLayerType,
28
+ )
24
29
 
25
- from ..utils import seconds_to_nanoseconds
30
+ from ..utils import seconds_to_nanoseconds, short_sleep
26
31
 
27
32
 
28
33
  console = Console()
@@ -397,7 +402,15 @@ class Can(BaseTransport):
397
402
  HEADER_SIZE = 0
398
403
 
399
404
  def __init__(self, config, policy=None, transport_layer_interface: Optional[BusABC] = None):
400
- super().__init__(config, policy, transport_layer_interface)
405
+ framing_config = XcpFramingConfig(
406
+ transport_layer_type=XcpTransportLayerType.CAN,
407
+ header_len=0,
408
+ header_ctr=0,
409
+ header_fill=0,
410
+ tail_fill=False,
411
+ tail_cs=ChecksumType.NO_CHECKSUM,
412
+ )
413
+ super().__init__(config, framing_config, policy, transport_layer_interface)
401
414
  self.load_config(config)
402
415
  self.useDefaultListener = self.config.use_default_listener
403
416
  self.can_id_master = Identifier(self.config.can_id_master)
@@ -485,12 +498,50 @@ class Can(BaseTransport):
485
498
  )
486
499
 
487
500
  def listen(self):
501
+ """Process CAN frames received from the interface.
502
+
503
+ This method runs in a separate thread and continuously polls the CAN interface
504
+ for new frames. When a frame is received, it extracts the data and timestamp
505
+ and passes them to the data_received method for further processing.
506
+
507
+ The method includes periodic sleep to prevent CPU hogging and error handling
508
+ to ensure the listener thread doesn't crash on exceptions.
509
+ """
510
+ # Cache frequently used methods and attributes for better performance
511
+ close_event_set = self.closeEvent.is_set
512
+ can_interface_read = self.can_interface.read
513
+ data_received = self.data_received
514
+
515
+ # State variables for processing
516
+ last_sleep = self.timestamp.value
517
+ FIVE_MS = 5_000_000 # Five milliseconds in nanoseconds
518
+
488
519
  while True:
489
- if self.closeEvent.is_set():
520
+ # Check if we should exit the loop
521
+ if close_event_set():
490
522
  return
491
- frame = self.can_interface.read()
492
- if frame:
493
- self.data_received(frame.data, frame.timestamp)
523
+
524
+ # Periodically sleep to prevent CPU hogging
525
+ if self.timestamp.value - last_sleep >= FIVE_MS:
526
+ short_sleep()
527
+ last_sleep = self.timestamp.value
528
+
529
+ try:
530
+ # Try to read a frame from the CAN interface
531
+ frame = can_interface_read()
532
+ if frame:
533
+ # Process the frame if one was received
534
+ data_received(frame.data, frame.timestamp)
535
+ else:
536
+ # No frame available, sleep briefly to avoid busy waiting
537
+ short_sleep()
538
+ last_sleep = self.timestamp.value
539
+ except Exception as e:
540
+ # Log any exceptions but continue processing
541
+ self.logger.error(f"Error in CAN listen thread: {e}")
542
+ # Sleep briefly to avoid tight error loops
543
+ short_sleep()
544
+ last_sleep = self.timestamp.value
494
545
 
495
546
  def connect(self):
496
547
  # Start listener lazily after a successful interface connection to avoid a dangling
pyxcp/transport/eth.py CHANGED
@@ -6,7 +6,12 @@ import threading
6
6
  from collections import deque
7
7
  from typing import Optional
8
8
 
9
- from pyxcp.transport.base import BaseTransport
9
+ from pyxcp.transport.base import (
10
+ BaseTransport,
11
+ ChecksumType,
12
+ XcpFramingConfig,
13
+ XcpTransportLayerType,
14
+ )
10
15
  from pyxcp.utils import short_sleep
11
16
 
12
17
 
@@ -36,11 +41,18 @@ class Eth(BaseTransport):
36
41
 
37
42
  MAX_DATAGRAM_SIZE = 512
38
43
  HEADER = struct.Struct("<HH")
39
- HEADER_SIZE = HEADER.size
40
44
 
41
45
  def __init__(self, config=None, policy=None, transport_layer_interface: Optional[socket.socket] = None) -> None:
42
- super().__init__(config, policy, transport_layer_interface)
43
46
  self.load_config(config)
47
+ framing_config = XcpFramingConfig(
48
+ transport_layer_type=XcpTransportLayerType.ETH,
49
+ header_len=2,
50
+ header_ctr=2,
51
+ header_fill=0,
52
+ tail_fill=False,
53
+ tail_cs=ChecksumType.NO_CHECKSUM,
54
+ )
55
+ super().__init__(config, framing_config, policy, transport_layer_interface)
44
56
  self.host: str = self.config.host
45
57
  self.port: int = self.config.port
46
58
  self.protocol: int = self.config.protocol
@@ -96,6 +108,7 @@ class Eth(BaseTransport):
96
108
  target=self._packet_listen,
97
109
  args=(),
98
110
  kwargs={},
111
+ daemon=True,
99
112
  )
100
113
  self._packets = deque()
101
114
 
@@ -109,17 +122,23 @@ class Eth(BaseTransport):
109
122
  def start_listener(self) -> None:
110
123
  super().start_listener()
111
124
  if self._packet_listener.is_alive():
112
- self._packet_listener.join()
113
- self._packet_listener = threading.Thread(target=self._packet_listen)
125
+ self._packet_listener.join(timeout=2.0)
126
+ self._packet_listener = threading.Thread(target=self._packet_listen, daemon=True)
114
127
  self._packet_listener.start()
115
128
 
116
129
  def close(self) -> None:
117
130
  """Close the transport-layer connection and event-loop."""
118
131
  self.finish_listener()
119
- if self.listener.is_alive():
120
- self.listener.join()
121
- if self._packet_listener.is_alive():
122
- self._packet_listener.join()
132
+ try:
133
+ if self.listener.is_alive():
134
+ self.listener.join(timeout=2.0)
135
+ except Exception:
136
+ pass
137
+ try:
138
+ if self._packet_listener.is_alive():
139
+ self._packet_listener.join(timeout=2.0)
140
+ except Exception:
141
+ pass
123
142
  self.close_connection()
124
143
 
125
144
  def _packet_listen(self) -> None:
@@ -162,8 +181,6 @@ class Eth(BaseTransport):
162
181
  break
163
182
 
164
183
  def listen(self) -> None:
165
- HEADER_UNPACK_FROM = self.HEADER.unpack_from
166
- HEADER_SIZE = self.HEADER_SIZE
167
184
  process_response = self.process_response
168
185
  popleft = self._packets.popleft
169
186
  close_event_set = self.closeEvent.is_set
@@ -186,10 +203,10 @@ class Eth(BaseTransport):
186
203
  current_position: int = 0
187
204
  while True:
188
205
  if length is None:
189
- if current_size >= HEADER_SIZE:
190
- length, counter = HEADER_UNPACK_FROM(data, current_position)
191
- current_position += HEADER_SIZE
192
- current_size -= HEADER_SIZE
206
+ if current_size >= self.framing.header_size:
207
+ length, counter = self.framing.unpack_header(bytes(data), initial_offset=current_position)
208
+ current_position += self.framing.header_size
209
+ current_size -= self.framing.header_size
193
210
  else:
194
211
  data = data[current_position:]
195
212
  break
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python
2
+
3
+ import datetime
4
+ from pathlib import Path
5
+ from typing import List, Dict
6
+
7
+ import h5py
8
+ import numpy as np
9
+ from pyxcp.daq_stim import DaqOnlinePolicy, DaqList
10
+ from pyxcp import __version__ as pyxcp_version
11
+
12
+ BATCH_SIZE = 4096
13
+
14
+ MAP_TO_NP = {
15
+ "U8": np.uint8,
16
+ "I8": np.int8,
17
+ "U16": np.uint16,
18
+ "I16": np.int16,
19
+ "U32": np.uint32,
20
+ "I32": np.int32,
21
+ "U64": np.uint64,
22
+ "I64": np.int64,
23
+ "F32": np.float32,
24
+ "F64": np.float64,
25
+ "F16": np.float16,
26
+ "BF16": np.float16,
27
+ }
28
+
29
+ MAP_TO_ASAM_HO = {
30
+ "U8": "A_UINT8",
31
+ "I8": "A_INT8",
32
+ "U16": "A_UINT16",
33
+ "I16": "A_INT16",
34
+ "U32": "A_UINT32",
35
+ "I32": "A_INT32",
36
+ "U64": "A_UINT64",
37
+ "I64": "A_INT64",
38
+ "F32": "A_FLOAT32",
39
+ "F64": "A_FLOAT64",
40
+ "F16": "A_FLOAT16",
41
+ "BF16": "A_FLOAT16",
42
+ }
43
+
44
+
45
+ class BufferedDataset:
46
+ def __init__(self, dataset: h5py.Dataset):
47
+ self.dataset = dataset
48
+ self.buffer: List[int | float] = []
49
+
50
+ def add_sample(self, sample: int | float):
51
+ self.buffer.append(sample)
52
+ if len(self.buffer) >= BATCH_SIZE:
53
+ self.flush()
54
+
55
+ def flush(self):
56
+ batch = np.array(self.buffer)
57
+ self.dataset.resize((self.dataset.shape[0] + len(batch),))
58
+ self.dataset[-len(batch) :] = batch
59
+ self.buffer.clear()
60
+ self.dataset.flush()
61
+
62
+ def __len__(self):
63
+ return len(self.buffer)
64
+
65
+
66
+ class DatasetGroup:
67
+ def __init__(
68
+ self,
69
+ ts0_ds: BufferedDataset,
70
+ ts1_ds: BufferedDataset,
71
+ datasets: List[BufferedDataset],
72
+ ):
73
+ self.ts0_ds = ts0_ds
74
+ self.ts1_ds = ts1_ds
75
+ self.datasets = datasets
76
+
77
+ def feed(self, ts0: int, ts1: int, *datasets):
78
+ self.ts0_ds.add_sample(ts0)
79
+ self.ts1_ds.add_sample(ts1)
80
+ for dataset, value in zip(self.datasets, datasets):
81
+ dataset.add_sample(value)
82
+
83
+ def finalize(self):
84
+ for dataset in self.datasets:
85
+ dataset.flush()
86
+ self.ts0_ds.flush()
87
+ self.ts1_ds.flush()
88
+
89
+
90
+ def create_timestamp_column(hdf_file: h5py.File, group_name: str, num: int) -> h5py.Dataset:
91
+ result = hdf_file.create_dataset(
92
+ f"/{group_name}/timestamp{num}",
93
+ shape=(0,),
94
+ maxshape=(None,),
95
+ dtype=np.uint64,
96
+ chunks=True,
97
+ )
98
+ result.attrs["asam_data_type"] = "A_UINT64"
99
+ result.attrs["resolution"] = ("1 nanosecond",)
100
+ return result
101
+
102
+
103
+ class Hdf5OnlinePolicy(DaqOnlinePolicy):
104
+ def __init__(self, file_name: str | Path, daq_lists: List[DaqList], **metadata):
105
+ super().__init__(daq_lists=daq_lists)
106
+ path = Path(file_name)
107
+ if path.suffix != ".h5":
108
+ path = path.with_suffix(".h5")
109
+ self.hdf = h5py.File(path, "w", libver="latest")
110
+ self.metadata = self.set_metadata(**metadata)
111
+
112
+ def set_metadata(self, **metadata):
113
+ basic = {
114
+ "tool_name": "pyXCP",
115
+ "tool_version": f"{pyxcp_version}",
116
+ "created": f"{datetime.datetime.now().astimezone().isoformat()}",
117
+ }
118
+ for k, v in (basic | metadata).items():
119
+ self.hdf.attrs[k] = v
120
+
121
+ def initialize(self):
122
+ self.log.debug("Hdf5OnlinePolicy::Initialize()")
123
+ self.datasets: Dict[int, DatasetGroup] = {}
124
+ for num, daq_list in enumerate(self.daq_lists):
125
+ if daq_list.stim:
126
+ continue
127
+ grp = self.hdf.create_group(daq_list.name)
128
+ grp.attrs["event_num"] = daq_list.event_num
129
+ grp.attrs["enable_timestamps"] = daq_list.enable_timestamps
130
+ grp.attrs["prescaler"] = daq_list.prescaler
131
+ grp.attrs["priority"] = daq_list.priority
132
+ grp.attrs["direction"] = "STIM" if daq_list.stim else "DAQ"
133
+ ts0 = BufferedDataset(create_timestamp_column(self.hdf, daq_list.name, 0))
134
+ ts1 = BufferedDataset(create_timestamp_column(self.hdf, daq_list.name, 1))
135
+ meas_map = {m.name: m for m in self.daq_lists[num].measurements}
136
+ dsets = []
137
+ for name, _ in daq_list.headers:
138
+ meas = meas_map[name]
139
+ dataset = self.hdf.create_dataset(
140
+ f"/{daq_list.name}/{meas.name}/raw",
141
+ shape=(0,),
142
+ maxshape=(None,),
143
+ dtype=MAP_TO_NP[meas.data_type],
144
+ chunks=(1024,),
145
+ )
146
+ sub_group = dataset.parent
147
+ sub_group.attrs["asam_data_type"] = MAP_TO_ASAM_HO.get(meas.data_type, "n/a")
148
+ dataset.attrs["ecu_address"] = meas.address
149
+ dataset.attrs["ecu_address_extension"] = meas.ext
150
+ dsets.append(BufferedDataset(dataset))
151
+ self.datasets[num] = DatasetGroup(ts0_ds=ts0, ts1_ds=ts1, datasets=dsets)
152
+ self.hdf.flush()
153
+
154
+ def finalize(self):
155
+ self.log.debug("Hdf5OnlinePolicy::finalize()")
156
+ if hasattr(self, "datasets"):
157
+ for group in self.datasets.values():
158
+ group.finalize()
159
+ if hasattr(self, "hdf"):
160
+ self.hdf.close()
161
+
162
+ def on_daq_list(self, daq_list: int, timestamp0: int, timestamp1: int, payload: list):
163
+ group = self.datasets.get(daq_list)
164
+ if group is None:
165
+ self.log.warning(f"Received data for unknown DAQ list {daq_list}")
166
+ return
167
+ group.feed(timestamp0, timestamp1, *payload)
pyxcp/transport/sxi.py CHANGED
@@ -1,12 +1,38 @@
1
- import struct
1
+ import threading
2
2
  from collections import deque
3
3
  from dataclasses import dataclass
4
- from typing import Optional
4
+ from typing import Any, Optional
5
5
 
6
6
  import serial
7
7
 
8
- import pyxcp.types as types
9
- from pyxcp.transport.base import BaseTransport
8
+ from pyxcp.transport.transport_ext import (
9
+ SxiFrLBCN,
10
+ SxiFrLBC8,
11
+ SxiFrLBC16,
12
+ SxiFrLCBCN,
13
+ SxiFrLCBC8,
14
+ SxiFrLCBC16,
15
+ SxiFrLFBCN,
16
+ SxiFrLFBC8,
17
+ SxiFrLFBC16,
18
+ SxiFrLWCN,
19
+ SxiFrLWC8,
20
+ SxiFrLWC16,
21
+ SxiFrLCWCN,
22
+ SxiFrLCWC8,
23
+ SxiFrLCWC16,
24
+ SxiFrLFWCN,
25
+ SxiFrLFWC8,
26
+ SxiFrLFWC16,
27
+ )
28
+
29
+ from pyxcp.transport.base import (
30
+ BaseTransport,
31
+ ChecksumType,
32
+ XcpFramingConfig,
33
+ XcpTransportLayerType,
34
+ parse_header_format,
35
+ )
10
36
 
11
37
 
12
38
  @dataclass
@@ -19,11 +45,23 @@ class HeaderValues:
19
45
  RECV_SIZE = 16384
20
46
 
21
47
 
48
+ def get_receiver_class(header_format: str, checksum_format: str) -> Any:
49
+ COLUMN = {"NO_CHECKSUM": 0, "CHECKSUM_BYTE": 1, "CHECKSUM_WORD": 2}
50
+ FORMATS = {
51
+ "HEADER_LEN_BYTE": (SxiFrLBCN, SxiFrLBC8, SxiFrLBC16),
52
+ "HEADER_LEN_CTR_BYTE": (SxiFrLCBCN, SxiFrLCBC8, SxiFrLCBC16),
53
+ "HEADER_LEN_FILL_BYTE": (SxiFrLFBCN, SxiFrLFBC8, SxiFrLFBC16),
54
+ "HEADER_LEN_WORD": (SxiFrLWCN, SxiFrLWC8, SxiFrLWC16),
55
+ "HEADER_LEN_CTR_WORD": (SxiFrLCWCN, SxiFrLCWC8, SxiFrLCWC16),
56
+ "HEADER_LEN_FILL_WORD": (SxiFrLFWCN, SxiFrLFWC8, SxiFrLFWC16),
57
+ }
58
+ return FORMATS[header_format][COLUMN[checksum_format]]
59
+
60
+
22
61
  class SxI(BaseTransport):
23
62
  """"""
24
63
 
25
64
  def __init__(self, config=None, policy=None, transport_layer_interface: Optional[serial.Serial] = None) -> None:
26
- super().__init__(config, policy, transport_layer_interface)
27
65
  self.load_config(config)
28
66
  self.port_name = self.config.port
29
67
  self.baudrate = self.config.bitrate
@@ -31,12 +69,28 @@ class SxI(BaseTransport):
31
69
  self.parity = self.config.parity
32
70
  self.stopbits = self.config.stopbits
33
71
  self.mode = self.config.mode
34
- self.header_format = self.config.header_format
72
+ header_len, header_ctr, header_fill = parse_header_format(self.config.header_format)
73
+ tail_cs_map = {
74
+ "NO_CHECKSUM": ChecksumType.NO_CHECKSUM,
75
+ "CHECKSUM_BYTE": ChecksumType.BYTE_CHECKSUM,
76
+ "CHECKSUM_WORD": ChecksumType.WORD_CHECKSUM,
77
+ }
78
+ # self._listener_running = threading.Event()
79
+ tail_cs = tail_cs_map[self.config.tail_format]
80
+ ReceiverKlass = get_receiver_class(self.config.header_format, self.config.tail_format)
81
+ self.receiver = ReceiverKlass(self.frame_dispatcher)
82
+ framing_config = XcpFramingConfig(
83
+ transport_layer_type=XcpTransportLayerType.SXI,
84
+ header_len=header_len,
85
+ header_ctr=header_ctr,
86
+ header_fill=header_fill,
87
+ tail_fill=False,
88
+ tail_cs=tail_cs,
89
+ )
90
+ super().__init__(config, framing_config, policy, transport_layer_interface)
35
91
  self.tail_format = self.config.tail_format
36
- self.framing = self.config.framing
37
92
  self.esc_sync = self.config.esc_sync
38
93
  self.esc_esc = self.config.esc_esc
39
- self.make_header()
40
94
  self.comm_port: serial.Serial
41
95
 
42
96
  if self.has_user_supplied_interface and transport_layer_interface:
@@ -50,47 +104,30 @@ class SxI(BaseTransport):
50
104
  bytesize=self.bytesize,
51
105
  parity=self.parity,
52
106
  stopbits=self.stopbits,
53
- timeout=self.timeout,
107
+ timeout=0.1, # self.timeout,
54
108
  write_timeout=self.timeout,
55
109
  )
56
110
  except serial.SerialException as e:
57
111
  self.logger.critical(f"XCPonSxI - {e}")
58
112
  raise
59
- self._packets = deque()
113
+ self._condition = threading.Condition()
114
+ self._frames = deque()
115
+ # self._frame_listener = threading.Thread(
116
+ # target=self._frame_listen,
117
+ # args=(),
118
+ # kwargs={},
119
+ # )
60
120
 
61
121
  def __del__(self) -> None:
62
122
  self.close_connection()
63
123
 
64
- def make_header(self) -> None:
65
- def unpack_len(args):
66
- (length,) = args
67
- return HeaderValues(length=length)
68
-
69
- def unpack_len_counter(args):
70
- length, counter = args
71
- return HeaderValues(length=length, counter=counter)
72
-
73
- def unpack_len_filler(args):
74
- length, filler = args
75
- return HeaderValues(length=length, filler=filler)
76
-
77
- HEADER_FORMATS = {
78
- "HEADER_LEN_BYTE": ("B", unpack_len),
79
- "HEADER_LEN_CTR_BYTE": ("BB", unpack_len_counter),
80
- "HEADER_LEN_FILL_BYTE": ("BB", unpack_len_filler),
81
- "HEADER_LEN_WORD": ("H", unpack_len),
82
- "HEADER_LEN_CTR_WORD": ("HH", unpack_len_counter),
83
- "HEADER_LEN_FILL_WORD": ("HH", unpack_len_filler),
84
- }
85
- fmt, unpacker = HEADER_FORMATS[self.header_format]
86
- self.HEADER = struct.Struct(f"<{fmt}")
87
- self.HEADER_SIZE = self.HEADER.size
88
- self.unpacker = unpacker
89
-
90
124
  def connect(self) -> None:
91
125
  self.logger.info(
92
- f"XCPonSxI - serial comm_port {self.comm_port.portstr!r} openend [{self.baudrate}/{self.bytesize}-{self.parity}-{self.stopbits}]"
126
+ f"XCPonSxI - serial comm_port {self.comm_port.portstr!r} openend "
127
+ f"[{self.baudrate}/{self.bytesize}-{self.parity}-{self.stopbits}] "
128
+ f"mode: {self.config.mode}"
93
129
  )
130
+ self.logger.info(f"Framing: {self.config.header_format} {self.config.tail_format}")
94
131
  self.start_listener()
95
132
 
96
133
  def output(self, enable) -> None:
@@ -106,26 +143,63 @@ class SxI(BaseTransport):
106
143
 
107
144
  def start_listener(self) -> None:
108
145
  super().start_listener()
146
+ if hasattr(self, "_frame_listener") and self._frame_listener.is_alive():
147
+ self._frame_listener.join(timeout=2.0)
148
+ self._frame_listener = threading.Thread(target=self._frame_listen, daemon=True)
149
+ self._frame_listener.start()
150
+ # self._listener_running.wait(2.0)
151
+
152
+ def close(self) -> None:
153
+ """Close the transport-layer connection and event-loop."""
154
+ self.finish_listener()
155
+ self.closeEvent.set()
156
+ try:
157
+ if self.listener.is_alive():
158
+ self.listener.join(timeout=2.0)
159
+ except Exception:
160
+ pass
161
+ try:
162
+ if hasattr(self, "_frame_listener") and self._frame_listener.is_alive():
163
+ self._frame_listener.join(timeout=2.0)
164
+ except Exception:
165
+ pass
166
+ self.close_connection()
109
167
 
110
168
  def listen(self) -> None:
111
169
  while True:
112
170
  if self.closeEvent.is_set():
113
171
  return
114
- if not self.comm_port.in_waiting:
115
- continue
116
-
117
- recv_timestamp = self.timestamp.value
118
- header_values = self.unpacker(self.HEADER.unpack(self.comm_port.read(self.HEADER_SIZE)))
119
- length, counter, _ = header_values.length, header_values.counter, header_values.filler
120
-
121
- response = self.comm_port.read(length)
122
- self.timing.stop()
123
-
124
- if len(response) != length:
125
- raise types.FrameSizeError("Size mismatch.")
126
- self.process_response(response, length, counter, recv_timestamp)
127
-
128
- def send(self, frame) -> None:
172
+ frame_to_process = None
173
+ with self._condition:
174
+ while not self._frames:
175
+ res = self._condition.wait(1.0)
176
+ if not res:
177
+ break
178
+ if self._frames:
179
+ frame_to_process = self._frames.popleft()
180
+
181
+ if frame_to_process:
182
+ frame, length, counter, timestamp = frame_to_process
183
+ self.process_response(frame, length, counter, timestamp)
184
+
185
+ def _frame_listen(self) -> None:
186
+ # self._listener_running.set()
187
+ while True:
188
+ if self.closeEvent.is_set():
189
+ return
190
+ data = self.comm_port.read(1)
191
+ if data:
192
+ self.receiver.feed_bytes(data)
193
+ data = self.comm_port.read(self.comm_port.in_waiting)
194
+ if data:
195
+ self.receiver.feed_bytes(data)
196
+
197
+ def frame_dispatcher(self, data: bytes, length: int, counter: int) -> None:
198
+ with self._condition:
199
+ self._frames.append((bytes(data), length, counter, self.timestamp.value))
200
+ self._condition.notify()
201
+
202
+ def send(self, frame: bytes) -> None:
129
203
  self.pre_send_timestamp = self.timestamp.value
130
204
  self.comm_port.write(frame)
131
205
  self.post_send_timestamp = self.timestamp.value