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.
- pyxcp/__init__.py +1 -1
- pyxcp/cmdline.py +14 -29
- pyxcp/config/__init__.py +1257 -1258
- pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
- pyxcp/cpp_ext/bin.hpp +7 -6
- pyxcp/cpp_ext/cpp_ext.cpython-310-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-311-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-312-darwin.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-313-darwin.so +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/helper.hpp +280 -280
- pyxcp/cpp_ext/mcobject.hpp +248 -246
- pyxcp/cpp_ext/sxi_framing.hpp +332 -0
- pyxcp/daq_stim/__init__.py +145 -67
- pyxcp/daq_stim/optimize/binpacking.py +2 -2
- pyxcp/daq_stim/scheduler.cpp +8 -8
- pyxcp/errormatrix.py +2 -2
- pyxcp/examples/run_daq.py +5 -4
- 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 +134 -4
- pyxcp/master/master.py +823 -252
- 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 +96 -98
- pyxcp/recorder/converter/__init__.py +4 -10
- pyxcp/recorder/reader.hpp +138 -139
- pyxcp/recorder/reco.py +1 -0
- pyxcp/recorder/rekorder.cpython-310-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-311-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-312-darwin.so +0 -0
- pyxcp/recorder/rekorder.cpython-313-darwin.so +0 -0
- pyxcp/recorder/rekorder.hpp +274 -274
- pyxcp/recorder/unfolder.hpp +1354 -1319
- pyxcp/recorder/wrap.cpp +184 -183
- pyxcp/recorder/writer.hpp +302 -302
- pyxcp/scripts/xcp_daq_recorder.py +54 -0
- 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 +70 -180
- pyxcp/transport/can.py +58 -7
- pyxcp/transport/eth.py +32 -15
- pyxcp/transport/hdf5_policy.py +167 -0
- pyxcp/transport/sxi.py +126 -52
- pyxcp/transport/transport_ext.cpython-310-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-311-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-312-darwin.so +0 -0
- pyxcp/transport/transport_ext.cpython-313-darwin.so +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} +1 -2
- pyxcp/utils/cli.py +78 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/METADATA +4 -2
- pyxcp-0.25.7.dist-info/RECORD +158 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/WHEEL +1 -1
- pyxcp/examples/conf_sxi.json +0 -9
- pyxcp/examples/conf_sxi.toml +0 -7
- pyxcp-0.23.8.dist-info/RECORD +0 -135
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info}/entry_points.txt +0 -0
- {pyxcp-0.23.8.dist-info → pyxcp-0.25.7.dist-info/licenses}/LICENSE +0 -0
pyxcp/tests/test_transport.py
CHANGED
|
@@ -1,55 +1,151 @@
|
|
|
1
1
|
from unittest import mock
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
+
import serial
|
|
5
|
+
from can.bus import BusABC
|
|
4
6
|
|
|
5
7
|
import pyxcp.transport.base as tr
|
|
6
8
|
|
|
7
9
|
|
|
10
|
+
def create_mock_serial():
|
|
11
|
+
"""Create a mock serial port for testing."""
|
|
12
|
+
mock_serial = mock.MagicMock(spec=serial.Serial)
|
|
13
|
+
mock_serial.portstr = "MOCK_PORT"
|
|
14
|
+
mock_serial.in_waiting = 0
|
|
15
|
+
mock_serial.read.return_value = b""
|
|
16
|
+
mock_serial.is_open = True
|
|
17
|
+
return mock_serial
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_mock_can_interface():
|
|
21
|
+
"""Create a mock CAN interface for testing."""
|
|
22
|
+
mock_can = mock.MagicMock(spec=BusABC)
|
|
23
|
+
mock_can.filters = []
|
|
24
|
+
mock_can.state = "ACTIVE"
|
|
25
|
+
mock_can.recv.return_value = None
|
|
26
|
+
return mock_can
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Mock CAN interface configuration class
|
|
30
|
+
class MockCanInterfaceConfig:
|
|
31
|
+
OPTIONAL_BASE_PARAMS = []
|
|
32
|
+
CAN_PARAM_MAP = {}
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def class_own_traits(cls):
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
|
|
8
39
|
def create_config():
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
# Create a class to simulate the config structure
|
|
41
|
+
class EthConfig:
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.host = "localhost"
|
|
44
|
+
self.port = 5555
|
|
45
|
+
self.bind_to_address = ""
|
|
46
|
+
self.bind_to_port = 0
|
|
47
|
+
self.protocol = "UDP"
|
|
48
|
+
self.ipv6 = False
|
|
49
|
+
self.tcp_nodelay = False
|
|
50
|
+
|
|
51
|
+
class SxiConfig:
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self.port = "MOCK_PORT" # This won't be used with the mock
|
|
54
|
+
self.bitrate = 115200
|
|
55
|
+
self.bytesize = 8
|
|
56
|
+
self.parity = "N"
|
|
57
|
+
self.stopbits = 1
|
|
58
|
+
self.mode = "NORMAL"
|
|
59
|
+
self.header_format = "HEADER_LEN_BYTE"
|
|
60
|
+
self.tail_format = ""
|
|
61
|
+
self.framing = 0
|
|
62
|
+
self.esc_sync = 0
|
|
63
|
+
self.esc_esc = 0
|
|
64
|
+
|
|
65
|
+
class CanConfig:
|
|
66
|
+
def __init__(self):
|
|
67
|
+
self.can_id_master = 1
|
|
68
|
+
self.can_id_slave = 2
|
|
69
|
+
self.interface = "MockCanInterface"
|
|
70
|
+
self.channel = "vcan0"
|
|
71
|
+
self.use_default_listener = False # Don't start the listener
|
|
72
|
+
self.fd = False
|
|
73
|
+
self.max_dlc_required = False
|
|
74
|
+
self.padding_value = 0
|
|
75
|
+
self.timeout = 1.0
|
|
76
|
+
self.daq_identifier = [] # Empty list for DAQ identifiers
|
|
77
|
+
|
|
78
|
+
# Add the MockCanInterface attribute
|
|
79
|
+
self.MockCanInterface = MockCanInterfaceConfig()
|
|
80
|
+
|
|
81
|
+
# Special flag for testing
|
|
82
|
+
self.testing = True
|
|
83
|
+
|
|
84
|
+
class Config:
|
|
85
|
+
def __init__(self):
|
|
86
|
+
# Set attributes directly on the class for BaseTransport.load_config
|
|
87
|
+
self.eth = EthConfig()
|
|
88
|
+
self.sxi = SxiConfig()
|
|
89
|
+
self.can = CanConfig()
|
|
90
|
+
|
|
91
|
+
# Set attributes for BaseTransport.__init__
|
|
92
|
+
self.create_daq_timestamps = False
|
|
93
|
+
self.alignment = 1
|
|
94
|
+
self.timeout = 1.0
|
|
95
|
+
|
|
96
|
+
return Config()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@mock.patch("pyxcp.transport.can.detect_available_configs")
|
|
100
|
+
@mock.patch("pyxcp.transport.can.CAN_INTERFACE_MAP")
|
|
101
|
+
def test_factory_works(mock_can_interface_map, mock_detect_configs):
|
|
102
|
+
# Mock the detect_available_configs function to return an empty list
|
|
103
|
+
mock_detect_configs.return_value = []
|
|
104
|
+
|
|
105
|
+
# Mock the CAN_INTERFACE_MAP to return an instance of our MockCanInterfaceConfig for any key
|
|
106
|
+
mock_can_interface_map.__getitem__.return_value = MockCanInterfaceConfig()
|
|
107
|
+
|
|
23
108
|
config = create_config()
|
|
109
|
+
mock_serial_port = create_mock_serial()
|
|
110
|
+
mock_can_interface = create_mock_can_interface()
|
|
111
|
+
|
|
112
|
+
# Test ETH transport
|
|
24
113
|
assert isinstance(tr.create_transport("eth", config=config), tr.BaseTransport)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
114
|
+
|
|
115
|
+
# Test SXI transport with mock serial port
|
|
116
|
+
assert isinstance(tr.create_transport("sxi", config=config, transport_layer_interface=mock_serial_port), tr.BaseTransport)
|
|
117
|
+
|
|
118
|
+
# Test CAN transport with mock CAN interface
|
|
119
|
+
# assert isinstance(
|
|
120
|
+
# tr.create_transport("can", config=config, transport_layer_interface=mock_can_interface),
|
|
121
|
+
# tr.BaseTransport,
|
|
122
|
+
# )
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@mock.patch("pyxcp.transport.can.detect_available_configs")
|
|
126
|
+
@mock.patch("pyxcp.transport.can.CAN_INTERFACE_MAP")
|
|
127
|
+
def test_factory_works_case_insensitive(mock_can_interface_map, mock_detect_configs):
|
|
128
|
+
# Mock the detect_available_configs function to return an empty list
|
|
129
|
+
mock_detect_configs.return_value = []
|
|
130
|
+
|
|
131
|
+
# Mock the CAN_INTERFACE_MAP to return an instance of our MockCanInterfaceConfig for any key
|
|
132
|
+
mock_can_interface_map.__getitem__.return_value = MockCanInterfaceConfig()
|
|
133
|
+
|
|
134
|
+
config = create_config()
|
|
135
|
+
mock_serial_port = create_mock_serial()
|
|
136
|
+
mock_can_interface = create_mock_can_interface()
|
|
137
|
+
|
|
138
|
+
# Test ETH transport with uppercase name
|
|
139
|
+
assert isinstance(tr.create_transport("ETH", config=config), tr.BaseTransport)
|
|
140
|
+
|
|
141
|
+
# Test SXI transport with uppercase name and mock serial port
|
|
142
|
+
assert isinstance(tr.create_transport("SXI", config=config, transport_layer_interface=mock_serial_port), tr.BaseTransport)
|
|
143
|
+
|
|
144
|
+
# Test CAN transport with uppercase name and mock CAN interface
|
|
145
|
+
# assert isinstance(
|
|
146
|
+
# tr.create_transport("CAN", config=config, transport_layer_interface=mock_can_interface),
|
|
147
|
+
# tr.BaseTransport,
|
|
148
|
+
# )
|
|
53
149
|
|
|
54
150
|
|
|
55
151
|
def test_factory_invalid_transport_name_raises():
|
pyxcp/timing.py
CHANGED
|
@@ -12,7 +12,7 @@ class Timing:
|
|
|
12
12
|
T_MS: "mS",
|
|
13
13
|
T_S: "S",
|
|
14
14
|
}
|
|
15
|
-
FMT = "min: {0:2.3f} {4}\nmax: {1:2.3f} {4}\
|
|
15
|
+
FMT = "min: {0:2.3f} {4}\nmax: {1:2.3f} {4}\navg: {2:2.3f} {4}\nlast: {3:2.3f} {4}"
|
|
16
16
|
|
|
17
17
|
def __init__(self, unit=T_MS, record=False):
|
|
18
18
|
self.min = None
|
pyxcp/transport/__init__.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
-
from .
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
# from .transport_ext import (
|
|
3
|
+
# FrameAcquisitionPolicy, # noqa: F401
|
|
4
|
+
# FrameRecorderPolicy, # noqa: F401
|
|
5
|
+
# LegacyFrameAcquisitionPolicy, # noqa: F401
|
|
6
|
+
# NoOpPolicy, # noqa: F401
|
|
7
|
+
# StdoutPolicy, # noqa: F401
|
|
8
|
+
# )
|
|
9
|
+
|
|
7
10
|
from .can import Can # noqa: F401
|
|
8
11
|
from .eth import Eth # noqa: F401
|
|
9
12
|
from .sxi import SxI # noqa: F401
|
pyxcp/transport/base.py
CHANGED
|
@@ -3,127 +3,50 @@ import abc
|
|
|
3
3
|
import logging
|
|
4
4
|
import threading
|
|
5
5
|
from collections import deque
|
|
6
|
-
from typing import Any, Dict, Optional,
|
|
7
|
-
|
|
6
|
+
from typing import Any, Dict, Optional, Type
|
|
7
|
+
from pyxcp.timing import Timing
|
|
8
8
|
import pyxcp.types as types
|
|
9
|
+
|
|
9
10
|
from pyxcp.cpp_ext.cpp_ext import Timestamp, TimestampType
|
|
10
|
-
from pyxcp.
|
|
11
|
-
|
|
11
|
+
from pyxcp.transport.transport_ext import (
|
|
12
|
+
FrameCategory,
|
|
13
|
+
FrameAcquisitionPolicy,
|
|
14
|
+
LegacyFrameAcquisitionPolicy,
|
|
15
|
+
XcpFraming,
|
|
16
|
+
XcpFramingConfig,
|
|
17
|
+
XcpTransportLayerType, # noqa: F401
|
|
18
|
+
ChecksumType, # noqa: F401
|
|
19
|
+
)
|
|
12
20
|
from pyxcp.utils import (
|
|
13
21
|
CurrentDatetime,
|
|
14
|
-
flatten,
|
|
15
22
|
hexDump,
|
|
16
23
|
seconds_to_nanoseconds,
|
|
17
24
|
short_sleep,
|
|
18
25
|
)
|
|
19
26
|
|
|
20
27
|
|
|
21
|
-
class FrameAcquisitionPolicy:
|
|
22
|
-
"""
|
|
23
|
-
Base class for all frame acquisition policies.
|
|
24
|
-
|
|
25
|
-
Parameters
|
|
26
|
-
---------
|
|
27
|
-
filter_out: set or None
|
|
28
|
-
A set of frame types to filter out.
|
|
29
|
-
If None, all frame types are accepted for further processing.
|
|
30
|
-
|
|
31
|
-
Example: (FrameType.REQUEST, FrameType.RESPONSE, FrameType.EVENT, FrameType.SERV)
|
|
32
|
-
==> care only about DAQ frames.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
def __init__(self, filter_out: Optional[Set[types.FrameCategory]] = None):
|
|
36
|
-
self._frame_types_to_filter_out = filter_out or set()
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def filtered_out(self) -> Set[types.FrameCategory]:
|
|
40
|
-
return self._frame_types_to_filter_out
|
|
41
|
-
|
|
42
|
-
def feed(self, frame_type: types.FrameCategory, counter: int, timestamp: int, payload: bytes) -> None: ... # noqa: E704
|
|
43
|
-
|
|
44
|
-
def finalize(self) -> None:
|
|
45
|
-
"""
|
|
46
|
-
Finalize the frame acquisition policy (if required).
|
|
47
|
-
"""
|
|
48
|
-
...
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class NoOpPolicy(FrameAcquisitionPolicy):
|
|
52
|
-
"""
|
|
53
|
-
No operation / do nothing policy.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class LegacyFrameAcquisitionPolicy(FrameAcquisitionPolicy):
|
|
58
|
-
"""Dequeue based frame acquisition policy.
|
|
59
|
-
|
|
60
|
-
Deprecated: Use only for compatibility reasons.
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
def __init__(self, filter_out: Optional[Set[types.FrameCategory]] = None) -> None:
|
|
64
|
-
super().__init__(filter_out)
|
|
65
|
-
self.reqQueue = deque()
|
|
66
|
-
self.resQueue = deque()
|
|
67
|
-
self.daqQueue = deque()
|
|
68
|
-
self.evQueue = deque()
|
|
69
|
-
self.servQueue = deque()
|
|
70
|
-
self.metaQueue = deque()
|
|
71
|
-
self.errorQueue = deque()
|
|
72
|
-
self.stimQueue = deque()
|
|
73
|
-
self.QUEUE_MAP = {
|
|
74
|
-
types.FrameCategory.CMD: self.reqQueue,
|
|
75
|
-
types.FrameCategory.RESPONSE: self.resQueue,
|
|
76
|
-
types.FrameCategory.EVENT: self.evQueue,
|
|
77
|
-
types.FrameCategory.SERV: self.servQueue,
|
|
78
|
-
types.FrameCategory.DAQ: self.daqQueue,
|
|
79
|
-
types.FrameCategory.METADATA: self.metaQueue,
|
|
80
|
-
types.FrameCategory.ERROR: self.errorQueue,
|
|
81
|
-
types.FrameCategory.STIM: self.stimQueue,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
def feed(self, frame_type: types.FrameCategory, counter: int, timestamp: int, payload: bytes) -> None:
|
|
85
|
-
if frame_type not in self.filtered_out:
|
|
86
|
-
queue = self.QUEUE_MAP.get(frame_type)
|
|
87
|
-
if queue is not None:
|
|
88
|
-
queue.append((counter, timestamp, payload))
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class FrameRecorderPolicy(FrameAcquisitionPolicy):
|
|
92
|
-
"""Frame acquisition policy that records frames."""
|
|
93
|
-
|
|
94
|
-
def __init__(
|
|
95
|
-
self,
|
|
96
|
-
file_name: str,
|
|
97
|
-
filter_out: Optional[Set[types.FrameCategory]] = None,
|
|
98
|
-
prealloc: int = 10,
|
|
99
|
-
chunk_size: int = 1,
|
|
100
|
-
) -> None:
|
|
101
|
-
super().__init__(filter_out)
|
|
102
|
-
self.recorder = XcpLogFileWriter(file_name, prealloc=prealloc, chunk_size=chunk_size)
|
|
103
|
-
|
|
104
|
-
def feed(self, frame_type: types.FrameCategory, counter: int, timestamp: int, payload: bytes) -> None:
|
|
105
|
-
if frame_type not in self.filtered_out:
|
|
106
|
-
self.recorder.add_frame(frame_type, counter, timestamp, payload)
|
|
107
|
-
|
|
108
|
-
def finalize(self) -> None:
|
|
109
|
-
self.recorder.finalize()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class StdoutPolicy(FrameAcquisitionPolicy):
|
|
113
|
-
"""Frame acquisition policy that prints frames to stdout."""
|
|
114
|
-
|
|
115
|
-
def __init__(self, filter_out: Optional[Set[types.FrameCategory]] = None) -> None:
|
|
116
|
-
super().__init__(filter_out)
|
|
117
|
-
|
|
118
|
-
def feed(self, frame_type: types.FrameCategory, counter: int, timestamp: int, payload: bytes) -> None:
|
|
119
|
-
if frame_type not in self.filtered_out:
|
|
120
|
-
print(f"{frame_type.name:8} {counter:6} {timestamp:8d} {hexDump(payload)}")
|
|
121
|
-
|
|
122
|
-
|
|
123
28
|
class EmptyFrameError(Exception):
|
|
124
29
|
"""Raised when an empty frame is received."""
|
|
125
30
|
|
|
126
31
|
|
|
32
|
+
def parse_header_format(header_format: str) -> tuple:
|
|
33
|
+
"""SxI and USB framing is configurable."""
|
|
34
|
+
if header_format == "HEADER_LEN_BYTE":
|
|
35
|
+
return 1, 0, 0
|
|
36
|
+
elif header_format == "HEADER_LEN_CTR_BYTE":
|
|
37
|
+
return 1, 1, 0
|
|
38
|
+
elif header_format == "HEADER_LEN_FILL_BYTE":
|
|
39
|
+
return 1, 0, 1
|
|
40
|
+
elif header_format == "HEADER_LEN_WORD":
|
|
41
|
+
return 2, 0, 0
|
|
42
|
+
elif header_format == "HEADER_LEN_CTR_WORD":
|
|
43
|
+
return 2, 2, 0
|
|
44
|
+
elif header_format == "HEADER_LEN_FILL_WORD":
|
|
45
|
+
return 2, 0, 2
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"Invalid header format: {header_format}")
|
|
48
|
+
|
|
49
|
+
|
|
127
50
|
class BaseTransport(metaclass=abc.ABCMeta):
|
|
128
51
|
"""Base class for transport-layers (Can, Eth, Sxi).
|
|
129
52
|
|
|
@@ -136,10 +59,17 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
136
59
|
|
|
137
60
|
"""
|
|
138
61
|
|
|
139
|
-
def __init__(
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
config,
|
|
65
|
+
framing_config: XcpFramingConfig,
|
|
66
|
+
policy: Optional[FrameAcquisitionPolicy] = None,
|
|
67
|
+
transport_layer_interface: Optional[Any] = None,
|
|
68
|
+
):
|
|
140
69
|
self.has_user_supplied_interface: bool = transport_layer_interface is not None
|
|
141
70
|
self.transport_layer_interface: Optional[Any] = transport_layer_interface
|
|
142
71
|
self.parent = None
|
|
72
|
+
self.framing = XcpFraming(framing_config)
|
|
143
73
|
self.policy: FrameAcquisitionPolicy = policy or LegacyFrameAcquisitionPolicy()
|
|
144
74
|
self.closeEvent: threading.Event = threading.Event()
|
|
145
75
|
|
|
@@ -150,7 +80,6 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
150
80
|
self._debug: bool = self.logger.level == 10
|
|
151
81
|
if transport_layer_interface:
|
|
152
82
|
self.logger.info(f"Transport - User Supplied Transport-Layer Interface: '{transport_layer_interface!s}'")
|
|
153
|
-
self.counter_send: int = 0
|
|
154
83
|
self.counter_received: int = -1
|
|
155
84
|
self.create_daq_timestamps: bool = config.create_daq_timestamps
|
|
156
85
|
self.timestamp = Timestamp(TimestampType.ABSOLUTE_TS)
|
|
@@ -164,6 +93,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
164
93
|
target=self.listen,
|
|
165
94
|
args=(),
|
|
166
95
|
kwargs={},
|
|
96
|
+
daemon=True,
|
|
167
97
|
)
|
|
168
98
|
|
|
169
99
|
self.first_daq_timestamp: Optional[int] = None
|
|
@@ -192,8 +122,12 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
192
122
|
def close(self) -> None:
|
|
193
123
|
"""Close the transport-layer connection and event-loop."""
|
|
194
124
|
self.finish_listener()
|
|
195
|
-
|
|
196
|
-
|
|
125
|
+
# Avoid indefinite blocking on buggy threads
|
|
126
|
+
try:
|
|
127
|
+
if self.listener.is_alive():
|
|
128
|
+
self.listener.join(timeout=2.0)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
197
131
|
self.close_connection()
|
|
198
132
|
|
|
199
133
|
@abc.abstractmethod
|
|
@@ -211,7 +145,6 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
211
145
|
raise EmptyFrameError
|
|
212
146
|
short_sleep()
|
|
213
147
|
item = self.resQueue.popleft()
|
|
214
|
-
# print("Q", item)
|
|
215
148
|
return item
|
|
216
149
|
|
|
217
150
|
@property
|
|
@@ -227,13 +160,14 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
227
160
|
def start_listener(self):
|
|
228
161
|
if self.listener.is_alive():
|
|
229
162
|
self.finish_listener()
|
|
230
|
-
|
|
163
|
+
# Avoid indefinite blocking on buggy threads
|
|
164
|
+
self.listener.join(timeout=2.0)
|
|
231
165
|
|
|
232
166
|
# Ensure the close event is cleared before starting a new listener thread.
|
|
233
167
|
if hasattr(self, "closeEvent"):
|
|
234
168
|
self.closeEvent.clear()
|
|
235
169
|
|
|
236
|
-
self.listener = threading.Thread(target=self.listen)
|
|
170
|
+
self.listener = threading.Thread(target=self.listen, daemon=True)
|
|
237
171
|
self.listener.start()
|
|
238
172
|
|
|
239
173
|
def finish_listener(self):
|
|
@@ -245,9 +179,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
245
179
|
frame = self._prepare_request(cmd, *data)
|
|
246
180
|
self.timing.start()
|
|
247
181
|
with self.policy_lock:
|
|
248
|
-
self.policy.feed(
|
|
249
|
-
# Record outgoing CMD for diagnostics
|
|
250
|
-
self._record_pdu("out", types.FrameCategory.CMD, self.counter_send, self.timestamp.value, frame)
|
|
182
|
+
self.policy.feed(FrameCategory.CMD, self.framing.counter_send, self.timestamp.value, frame)
|
|
251
183
|
self.send(frame)
|
|
252
184
|
try:
|
|
253
185
|
xcpPDU = self.get()
|
|
@@ -255,11 +187,11 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
255
187
|
if not ignore_timeout:
|
|
256
188
|
MSG = f"Response timed out (timeout={self.timeout / 1_000_000_000}s)"
|
|
257
189
|
with self.policy_lock:
|
|
258
|
-
self.policy.feed(
|
|
259
|
-
|
|
260
|
-
|
|
190
|
+
self.policy.feed(
|
|
191
|
+
FrameCategory.METADATA, self.framing.counter_send, self.timestamp.value, bytes(MSG, "ascii")
|
|
192
|
+
) if self._diagnostics_enabled() else ""
|
|
261
193
|
self.logger.debug("XCP request timeout", extra={"event": "timeout", "command": cmd.name})
|
|
262
|
-
raise types.XcpTimeoutError(MSG
|
|
194
|
+
raise types.XcpTimeoutError(MSG) from None
|
|
263
195
|
else:
|
|
264
196
|
self.timing.stop()
|
|
265
197
|
return
|
|
@@ -267,7 +199,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
267
199
|
pid = types.Response.parse(xcpPDU).type
|
|
268
200
|
if pid == "ERR" and cmd.name != "SYNCH":
|
|
269
201
|
with self.policy_lock:
|
|
270
|
-
self.policy.feed(
|
|
202
|
+
self.policy.feed(FrameCategory.ERROR, self.counter_received, self.timestamp.value, xcpPDU[1:])
|
|
271
203
|
err = types.XcpError.parse(xcpPDU[1:])
|
|
272
204
|
raise types.XcpResponseError(err)
|
|
273
205
|
return xcpPDU[1:]
|
|
@@ -299,8 +231,8 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
299
231
|
frame = self._prepare_request(cmd, *data)
|
|
300
232
|
with self.policy_lock:
|
|
301
233
|
self.policy.feed(
|
|
302
|
-
|
|
303
|
-
self.counter_send,
|
|
234
|
+
FrameCategory.CMD if int(cmd) >= 0xC0 else FrameCategory.STIM,
|
|
235
|
+
self.framing.counter_send,
|
|
304
236
|
self.timestamp.value,
|
|
305
237
|
frame,
|
|
306
238
|
)
|
|
@@ -313,33 +245,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
313
245
|
if self._debug:
|
|
314
246
|
self.logger.debug(cmd.name)
|
|
315
247
|
self.parent._setService(cmd)
|
|
316
|
-
|
|
317
|
-
cmd_len = cmd.bit_length() // 8 # calculate bytes needed for cmd
|
|
318
|
-
cmd_bytes = cmd.to_bytes(cmd_len, "big")
|
|
319
|
-
try:
|
|
320
|
-
from pyxcp.cpp_ext import accel as _accel
|
|
321
|
-
except Exception:
|
|
322
|
-
_accel = None # type: ignore
|
|
323
|
-
|
|
324
|
-
# Build payload (command + data) using optional accelerator
|
|
325
|
-
if _accel is not None and hasattr(_accel, "build_packet"):
|
|
326
|
-
packet = _accel.build_packet(cmd_bytes, data)
|
|
327
|
-
else:
|
|
328
|
-
packet = bytes(flatten(cmd_bytes, data))
|
|
329
|
-
|
|
330
|
-
header = self.HEADER.pack(len(packet), self.counter_send)
|
|
331
|
-
self.counter_send = (self.counter_send + 1) & 0xFFFF
|
|
332
|
-
|
|
333
|
-
frame = header + packet
|
|
334
|
-
|
|
335
|
-
# Align using optional accelerator
|
|
336
|
-
if _accel is not None and hasattr(_accel, "add_alignment"):
|
|
337
|
-
frame = _accel.add_alignment(frame, self.alignment)
|
|
338
|
-
else:
|
|
339
|
-
remainder = len(frame) % self.alignment
|
|
340
|
-
if remainder:
|
|
341
|
-
frame += b"\0" * (self.alignment - remainder)
|
|
342
|
-
|
|
248
|
+
frame = self.framing.prepare_request(cmd, *data)
|
|
343
249
|
if self._debug:
|
|
344
250
|
self.logger.debug(f"-> {hexDump(frame)}")
|
|
345
251
|
return frame
|
|
@@ -376,9 +282,9 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
376
282
|
if waited is not None:
|
|
377
283
|
msg += f" after {waited:.3f}s"
|
|
378
284
|
# Attach diagnostics
|
|
379
|
-
diag = self._build_diagnostics_dump() if self._diagnostics_enabled() else ""
|
|
285
|
+
# diag = self._build_diagnostics_dump() if self._diagnostics_enabled() else ""
|
|
380
286
|
self.logger.debug("XCP block_receive timeout", extra={"event": "timeout"})
|
|
381
|
-
raise types.XcpTimeoutError(msg
|
|
287
|
+
raise types.XcpTimeoutError(msg) from None
|
|
382
288
|
short_sleep()
|
|
383
289
|
return block_response
|
|
384
290
|
|
|
@@ -418,11 +324,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
418
324
|
# Record incoming non-DAQ frames for diagnostics
|
|
419
325
|
self._record_pdu(
|
|
420
326
|
"in",
|
|
421
|
-
(
|
|
422
|
-
types.FrameCategory.RESPONSE
|
|
423
|
-
if pid >= 0xFE
|
|
424
|
-
else types.FrameCategory.SERV if pid == 0xFC else types.FrameCategory.EVENT
|
|
425
|
-
),
|
|
327
|
+
(FrameCategory.RESPONSE if pid >= 0xFE else FrameCategory.SERV if pid == 0xFC else FrameCategory.EVENT),
|
|
426
328
|
counter,
|
|
427
329
|
recv_timestamp,
|
|
428
330
|
response,
|
|
@@ -431,15 +333,15 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
431
333
|
if pid >= 0xFE:
|
|
432
334
|
self.resQueue.append(response)
|
|
433
335
|
with self.policy_lock:
|
|
434
|
-
self.policy.feed(
|
|
336
|
+
self.policy.feed(FrameCategory.RESPONSE, self.counter_received, self.timestamp.value, response)
|
|
435
337
|
self.recv_timestamp = recv_timestamp
|
|
436
338
|
elif pid == 0xFD:
|
|
437
339
|
self.process_event_packet(response)
|
|
438
340
|
with self.policy_lock:
|
|
439
|
-
self.policy.feed(
|
|
341
|
+
self.policy.feed(FrameCategory.EVENT, self.counter_received, self.timestamp.value, response)
|
|
440
342
|
elif pid == 0xFC:
|
|
441
343
|
with self.policy_lock:
|
|
442
|
-
self.policy.feed(
|
|
344
|
+
self.policy.feed(FrameCategory.SERV, self.counter_received, self.timestamp.value, response)
|
|
443
345
|
else:
|
|
444
346
|
# DAQ traffic: Some transports reuse or do not advance the counter for DAQ frames.
|
|
445
347
|
# Do not drop DAQ frames on duplicate counters to avoid losing measurements.
|
|
@@ -459,17 +361,17 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
459
361
|
else:
|
|
460
362
|
timestamp = 0
|
|
461
363
|
# Record DAQ frame (only keep small prefix in payload string later)
|
|
462
|
-
self._record_pdu("in",
|
|
364
|
+
self._record_pdu("in", FrameCategory.DAQ, counter, timestamp, response, length)
|
|
463
365
|
# DAQ activity indicates the slave is alive/busy; keep extending the wait window for any
|
|
464
366
|
# outstanding request, similar to EV_CMD_PENDING behavior on stacks that don't emit it.
|
|
465
367
|
self.timer_restart_event.set()
|
|
466
368
|
with self.policy_lock:
|
|
467
|
-
self.policy.feed(
|
|
369
|
+
self.policy.feed(FrameCategory.DAQ, self.counter_received, timestamp, response)
|
|
468
370
|
|
|
469
371
|
def _record_pdu(
|
|
470
372
|
self,
|
|
471
373
|
direction: str,
|
|
472
|
-
category:
|
|
374
|
+
category: FrameCategory,
|
|
473
375
|
counter: int,
|
|
474
376
|
timestamp: int,
|
|
475
377
|
payload: bytes,
|
|
@@ -482,11 +384,11 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
482
384
|
"ctr": int(counter),
|
|
483
385
|
"ts": int(timestamp),
|
|
484
386
|
"len": int(length if length is not None else len(payload)),
|
|
485
|
-
"data": hexDump(payload if category !=
|
|
387
|
+
"data": hexDump(payload if category != FrameCategory.DAQ else payload[:8])[:512],
|
|
486
388
|
}
|
|
487
389
|
self._last_pdus.append(entry)
|
|
488
390
|
except Exception:
|
|
489
|
-
pass
|
|
391
|
+
pass # nosec
|
|
490
392
|
|
|
491
393
|
def _build_diagnostics_dump(self) -> str:
|
|
492
394
|
import json as _json
|
|
@@ -512,27 +414,15 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
512
414
|
try:
|
|
513
415
|
tp[key] = getattr(cfg, key)
|
|
514
416
|
except Exception:
|
|
515
|
-
pass
|
|
516
|
-
# negotiated properties
|
|
517
|
-
negotiated = None
|
|
518
|
-
try:
|
|
519
|
-
master = getattr(self, "parent", None)
|
|
520
|
-
if master is not None and hasattr(master, "slaveProperties"):
|
|
521
|
-
sp = getattr(master, "slaveProperties")
|
|
522
|
-
negotiated = getattr(sp, "__dict__", None) or str(sp)
|
|
523
|
-
except Exception:
|
|
524
|
-
negotiated = None
|
|
525
|
-
# last PDUs
|
|
526
|
-
general = None
|
|
417
|
+
pass # nosec
|
|
527
418
|
last_n = 20
|
|
528
419
|
try:
|
|
529
420
|
app = getattr(self.config, "parent", None)
|
|
530
421
|
app = getattr(app, "parent", None)
|
|
531
422
|
if app is not None and hasattr(app, "general") and hasattr(app.general, "diagnostics_last_pdus"):
|
|
532
423
|
last_n = int(app.general.diagnostics_last_pdus or last_n)
|
|
533
|
-
general = app.general
|
|
534
424
|
except Exception:
|
|
535
|
-
pass
|
|
425
|
+
pass # nosec
|
|
536
426
|
pdus = list(self._last_pdus)[-last_n:]
|
|
537
427
|
payload = {
|
|
538
428
|
"transport_params": tp,
|