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.
- pyxcp/__init__.py +1 -1
- pyxcp/asamkeydll.exe +0 -0
- pyxcp/cmdline.py +15 -30
- pyxcp/config/__init__.py +73 -20
- pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
- pyxcp/cpp_ext/bin.hpp +7 -6
- pyxcp/cpp_ext/cpp_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp312-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/daqlist.hpp +241 -73
- pyxcp/cpp_ext/extension_wrapper.cpp +123 -15
- pyxcp/cpp_ext/framing.hpp +360 -0
- pyxcp/cpp_ext/mcobject.hpp +5 -3
- pyxcp/cpp_ext/sxi_framing.hpp +332 -0
- pyxcp/daq_stim/__init__.py +182 -45
- pyxcp/daq_stim/optimize/binpacking.py +2 -2
- pyxcp/daq_stim/scheduler.cpp +8 -8
- pyxcp/daq_stim/stim.cp310-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp311-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp312-win_arm64.pyd +0 -0
- pyxcp/errormatrix.py +2 -2
- pyxcp/examples/run_daq.py +5 -3
- pyxcp/examples/xcp_policy.py +6 -6
- pyxcp/examples/xcp_read_benchmark.py +2 -2
- pyxcp/examples/xcp_skel.py +1 -2
- pyxcp/examples/xcp_unlock.py +10 -12
- pyxcp/examples/xcp_user_supplied_driver.py +1 -2
- pyxcp/examples/xcphello.py +2 -15
- pyxcp/examples/xcphello_recorder.py +2 -2
- pyxcp/master/__init__.py +1 -0
- pyxcp/master/errorhandler.py +248 -13
- pyxcp/master/master.py +838 -250
- pyxcp/recorder/.idea/.gitignore +8 -0
- pyxcp/recorder/.idea/misc.xml +4 -0
- pyxcp/recorder/.idea/modules.xml +8 -0
- pyxcp/recorder/.idea/recorder.iml +6 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
- pyxcp/recorder/.idea/vcs.xml +10 -0
- pyxcp/recorder/__init__.py +5 -10
- pyxcp/recorder/converter/__init__.py +4 -10
- pyxcp/recorder/reader.hpp +0 -1
- pyxcp/recorder/reco.py +1 -0
- pyxcp/recorder/rekorder.cp310-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp311-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp312-win_arm64.pyd +0 -0
- pyxcp/recorder/unfolder.hpp +129 -107
- pyxcp/recorder/wrap.cpp +3 -8
- pyxcp/scripts/xcp_fetch_a2l.py +2 -2
- pyxcp/scripts/xcp_id_scanner.py +1 -2
- pyxcp/scripts/xcp_info.py +66 -51
- pyxcp/scripts/xcp_profile.py +1 -2
- pyxcp/tests/test_daq.py +1 -1
- pyxcp/tests/test_framing.py +262 -0
- pyxcp/tests/test_master.py +210 -100
- pyxcp/tests/test_transport.py +138 -42
- pyxcp/timing.py +1 -1
- pyxcp/transport/__init__.py +8 -5
- pyxcp/transport/base.py +187 -143
- pyxcp/transport/can.py +117 -13
- pyxcp/transport/eth.py +55 -20
- pyxcp/transport/hdf5_policy.py +167 -0
- pyxcp/transport/sxi.py +126 -52
- pyxcp/transport/transport_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.cp312-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.hpp +214 -0
- pyxcp/transport/transport_wrapper.cpp +249 -0
- pyxcp/transport/usb_transport.py +47 -31
- pyxcp/types.py +0 -13
- pyxcp/{utils.py → utils/__init__.py} +3 -4
- pyxcp/utils/cli.py +78 -0
- pyxcp-0.25.6.dist-info/METADATA +341 -0
- pyxcp-0.25.6.dist-info/RECORD +153 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/WHEEL +1 -1
- pyxcp/examples/conf_sxi.json +0 -9
- pyxcp/examples/conf_sxi.toml +0 -7
- pyxcp-0.23.3.dist-info/METADATA +0 -219
- pyxcp-0.23.3.dist-info/RECORD +0 -131
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/entry_points.txt +0 -0
- {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
|
|
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(
|
|
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
|
-
|
|
118
|
-
self.listener.
|
|
119
|
-
|
|
120
|
-
|
|
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 >=
|
|
188
|
-
length, counter =
|
|
189
|
-
current_position +=
|
|
190
|
-
current_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
|
-
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|