pyxcp 0.23.3__cp312-cp312-win_arm64.whl → 0.25.6__cp312-cp312-win_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 (87) hide show
  1. pyxcp/__init__.py +1 -1
  2. pyxcp/asamkeydll.exe +0 -0
  3. pyxcp/cmdline.py +15 -30
  4. pyxcp/config/__init__.py +73 -20
  5. pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
  6. pyxcp/cpp_ext/bin.hpp +7 -6
  7. pyxcp/cpp_ext/cpp_ext.cp310-win_arm64.pyd +0 -0
  8. pyxcp/cpp_ext/cpp_ext.cp311-win_arm64.pyd +0 -0
  9. pyxcp/cpp_ext/cpp_ext.cp312-win_arm64.pyd +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/mcobject.hpp +5 -3
  14. pyxcp/cpp_ext/sxi_framing.hpp +332 -0
  15. pyxcp/daq_stim/__init__.py +182 -45
  16. pyxcp/daq_stim/optimize/binpacking.py +2 -2
  17. pyxcp/daq_stim/scheduler.cpp +8 -8
  18. pyxcp/daq_stim/stim.cp310-win_arm64.pyd +0 -0
  19. pyxcp/daq_stim/stim.cp311-win_arm64.pyd +0 -0
  20. pyxcp/daq_stim/stim.cp312-win_arm64.pyd +0 -0
  21. pyxcp/errormatrix.py +2 -2
  22. pyxcp/examples/run_daq.py +5 -3
  23. pyxcp/examples/xcp_policy.py +6 -6
  24. pyxcp/examples/xcp_read_benchmark.py +2 -2
  25. pyxcp/examples/xcp_skel.py +1 -2
  26. pyxcp/examples/xcp_unlock.py +10 -12
  27. pyxcp/examples/xcp_user_supplied_driver.py +1 -2
  28. pyxcp/examples/xcphello.py +2 -15
  29. pyxcp/examples/xcphello_recorder.py +2 -2
  30. pyxcp/master/__init__.py +1 -0
  31. pyxcp/master/errorhandler.py +248 -13
  32. pyxcp/master/master.py +838 -250
  33. pyxcp/recorder/.idea/.gitignore +8 -0
  34. pyxcp/recorder/.idea/misc.xml +4 -0
  35. pyxcp/recorder/.idea/modules.xml +8 -0
  36. pyxcp/recorder/.idea/recorder.iml +6 -0
  37. pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
  38. pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  39. pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  40. pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
  41. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
  42. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  43. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  44. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
  45. pyxcp/recorder/.idea/vcs.xml +10 -0
  46. pyxcp/recorder/__init__.py +5 -10
  47. pyxcp/recorder/converter/__init__.py +4 -10
  48. pyxcp/recorder/reader.hpp +0 -1
  49. pyxcp/recorder/reco.py +1 -0
  50. pyxcp/recorder/rekorder.cp310-win_arm64.pyd +0 -0
  51. pyxcp/recorder/rekorder.cp311-win_arm64.pyd +0 -0
  52. pyxcp/recorder/rekorder.cp312-win_arm64.pyd +0 -0
  53. pyxcp/recorder/unfolder.hpp +129 -107
  54. pyxcp/recorder/wrap.cpp +3 -8
  55. pyxcp/scripts/xcp_fetch_a2l.py +2 -2
  56. pyxcp/scripts/xcp_id_scanner.py +1 -2
  57. pyxcp/scripts/xcp_info.py +66 -51
  58. pyxcp/scripts/xcp_profile.py +1 -2
  59. pyxcp/tests/test_daq.py +1 -1
  60. pyxcp/tests/test_framing.py +262 -0
  61. pyxcp/tests/test_master.py +210 -100
  62. pyxcp/tests/test_transport.py +138 -42
  63. pyxcp/timing.py +1 -1
  64. pyxcp/transport/__init__.py +8 -5
  65. pyxcp/transport/base.py +187 -143
  66. pyxcp/transport/can.py +117 -13
  67. pyxcp/transport/eth.py +55 -20
  68. pyxcp/transport/hdf5_policy.py +167 -0
  69. pyxcp/transport/sxi.py +126 -52
  70. pyxcp/transport/transport_ext.cp310-win_arm64.pyd +0 -0
  71. pyxcp/transport/transport_ext.cp311-win_arm64.pyd +0 -0
  72. pyxcp/transport/transport_ext.cp312-win_arm64.pyd +0 -0
  73. pyxcp/transport/transport_ext.hpp +214 -0
  74. pyxcp/transport/transport_wrapper.cpp +249 -0
  75. pyxcp/transport/usb_transport.py +47 -31
  76. pyxcp/types.py +0 -13
  77. pyxcp/{utils.py → utils/__init__.py} +3 -4
  78. pyxcp/utils/cli.py +78 -0
  79. pyxcp-0.25.6.dist-info/METADATA +341 -0
  80. pyxcp-0.25.6.dist-info/RECORD +153 -0
  81. {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/WHEEL +1 -1
  82. pyxcp/examples/conf_sxi.json +0 -9
  83. pyxcp/examples/conf_sxi.toml +0 -7
  84. pyxcp-0.23.3.dist-info/METADATA +0 -219
  85. pyxcp-0.23.3.dist-info/RECORD +0 -131
  86. {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/entry_points.txt +0 -0
  87. {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info/licenses}/LICENSE +0 -0
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
@@ -69,8 +81,8 @@ class Eth(BaseTransport):
69
81
  self.sockaddr,
70
82
  ) = addrinfo[0]
71
83
  except BaseException as ex: # noqa: B036
72
- msg = f"XCPonEth - Failed to resolve address {self.host}:{self.port}"
73
- self.logger.critical(msg)
84
+ msg = f"XCPonEth - Failed to resolve address {self.host}:{self.port} ({self.protocol}, ipv6={self.ipv6}): {ex.__class__.__name__}: {ex}"
85
+ self.logger.critical(msg, extra={"transport": "eth", "host": self.host, "port": self.port, "protocol": self.protocol})
74
86
  raise Exception(msg) from ex
75
87
  self.status: int = 0
76
88
  self.sock = socket.socket(self.address_family, self.socktype, self.proto)
@@ -87,13 +99,16 @@ class Eth(BaseTransport):
87
99
  try:
88
100
  self.sock.bind(self._local_address)
89
101
  except BaseException as ex: # noqa: B036
90
- msg = f"XCPonEth - Failed to bind socket to given address {self._local_address}"
91
- self.logger.critical(msg)
102
+ msg = f"XCPonEth - Failed to bind socket to given address {self._local_address}: {ex.__class__.__name__}: {ex}"
103
+ self.logger.critical(
104
+ msg, extra={"transport": "eth", "host": self.host, "port": self.port, "protocol": self.protocol}
105
+ )
92
106
  raise Exception(msg) from ex
93
107
  self._packet_listener = threading.Thread(
94
108
  target=self._packet_listen,
95
109
  args=(),
96
110
  kwargs={},
111
+ daemon=True,
97
112
  )
98
113
  self._packets = deque()
99
114
 
@@ -107,17 +122,23 @@ class Eth(BaseTransport):
107
122
  def start_listener(self) -> None:
108
123
  super().start_listener()
109
124
  if self._packet_listener.is_alive():
110
- self._packet_listener.join()
111
- 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)
112
127
  self._packet_listener.start()
113
128
 
114
129
  def close(self) -> None:
115
130
  """Close the transport-layer connection and event-loop."""
116
131
  self.finish_listener()
117
- if self.listener.is_alive():
118
- self.listener.join()
119
- if self._packet_listener.is_alive():
120
- 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
121
142
  self.close_connection()
122
143
 
123
144
  def _packet_listen(self) -> None:
@@ -160,8 +181,6 @@ class Eth(BaseTransport):
160
181
  break
161
182
 
162
183
  def listen(self) -> None:
163
- HEADER_UNPACK_FROM = self.HEADER.unpack_from
164
- HEADER_SIZE = self.HEADER_SIZE
165
184
  process_response = self.process_response
166
185
  popleft = self._packets.popleft
167
186
  close_event_set = self.closeEvent.is_set
@@ -184,17 +203,33 @@ class Eth(BaseTransport):
184
203
  current_position: int = 0
185
204
  while True:
186
205
  if length is None:
187
- if current_size >= HEADER_SIZE:
188
- length, counter = HEADER_UNPACK_FROM(data, current_position)
189
- current_position += HEADER_SIZE
190
- 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
191
210
  else:
192
211
  data = data[current_position:]
193
212
  break
194
213
  else:
195
214
  if current_size >= length:
196
215
  response = data[current_position : current_position + length]
197
- process_response(response, length, counter, timestamp)
216
+ try:
217
+ process_response(response, length, counter, timestamp)
218
+ except BaseException as ex: # Guard listener against unhandled exceptions (e.g., disk full in policy)
219
+ try:
220
+ self.logger.critical(
221
+ f"Listener error in process_response: {ex.__class__.__name__}: {ex}. Stopping listener.",
222
+ extra={"event": "listener_error"},
223
+ )
224
+ except Exception:
225
+ pass
226
+ try:
227
+ # Signal all loops to stop
228
+ if hasattr(self, "closeEvent"):
229
+ self.closeEvent.set()
230
+ except Exception:
231
+ pass
232
+ return
198
233
  current_size -= length
199
234
  current_position += length
200
235
  length = None
@@ -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