pyxcp 0.23.0__cp313-cp313-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.
Potentially problematic release.
This version of pyxcp might be problematic. Click here for more details.
- pyxcp/__init__.py +20 -0
- pyxcp/aml/EtasCANMonitoring.a2l +82 -0
- pyxcp/aml/EtasCANMonitoring.aml +67 -0
- pyxcp/aml/XCP_Common.aml +408 -0
- pyxcp/aml/XCPonCAN.aml +78 -0
- pyxcp/aml/XCPonEth.aml +33 -0
- pyxcp/aml/XCPonFlx.aml +113 -0
- pyxcp/aml/XCPonSxI.aml +66 -0
- pyxcp/aml/XCPonUSB.aml +106 -0
- pyxcp/aml/ifdata_CAN.a2l +20 -0
- pyxcp/aml/ifdata_Eth.a2l +11 -0
- pyxcp/aml/ifdata_Flx.a2l +94 -0
- pyxcp/aml/ifdata_SxI.a2l +13 -0
- pyxcp/aml/ifdata_USB.a2l +81 -0
- pyxcp/asam/__init__.py +0 -0
- pyxcp/asam/types.py +131 -0
- pyxcp/asamkeydll.c +116 -0
- pyxcp/asamkeydll.sh +2 -0
- pyxcp/checksum.py +732 -0
- pyxcp/cmdline.py +52 -0
- pyxcp/config/__init__.py +1113 -0
- pyxcp/config/legacy.py +120 -0
- pyxcp/constants.py +47 -0
- pyxcp/cpp_ext/__init__.py +0 -0
- pyxcp/cpp_ext/bin.hpp +104 -0
- pyxcp/cpp_ext/blockmem.hpp +58 -0
- 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/cpp_ext.cp313-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/daqlist.hpp +206 -0
- pyxcp/cpp_ext/event.hpp +67 -0
- pyxcp/cpp_ext/extension_wrapper.cpp +100 -0
- pyxcp/cpp_ext/helper.hpp +280 -0
- pyxcp/cpp_ext/mcobject.hpp +246 -0
- pyxcp/cpp_ext/tsqueue.hpp +46 -0
- pyxcp/daq_stim/__init__.py +232 -0
- pyxcp/daq_stim/optimize/__init__.py +67 -0
- pyxcp/daq_stim/optimize/binpacking.py +41 -0
- pyxcp/daq_stim/scheduler.cpp +62 -0
- pyxcp/daq_stim/scheduler.hpp +75 -0
- 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/daq_stim/stim.cp313-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cpp +13 -0
- pyxcp/daq_stim/stim.hpp +604 -0
- pyxcp/daq_stim/stim_wrapper.cpp +50 -0
- pyxcp/dllif.py +100 -0
- pyxcp/errormatrix.py +878 -0
- pyxcp/examples/conf_can.toml +19 -0
- pyxcp/examples/conf_can_user.toml +16 -0
- pyxcp/examples/conf_can_vector.json +11 -0
- pyxcp/examples/conf_can_vector.toml +11 -0
- pyxcp/examples/conf_eth.toml +9 -0
- pyxcp/examples/conf_nixnet.json +20 -0
- pyxcp/examples/conf_socket_can.toml +12 -0
- pyxcp/examples/conf_sxi.json +9 -0
- pyxcp/examples/conf_sxi.toml +7 -0
- pyxcp/examples/run_daq.py +163 -0
- pyxcp/examples/xcp_policy.py +60 -0
- pyxcp/examples/xcp_read_benchmark.py +38 -0
- pyxcp/examples/xcp_skel.py +49 -0
- pyxcp/examples/xcp_unlock.py +38 -0
- pyxcp/examples/xcp_user_supplied_driver.py +44 -0
- pyxcp/examples/xcphello.py +78 -0
- pyxcp/examples/xcphello_recorder.py +107 -0
- pyxcp/master/__init__.py +9 -0
- pyxcp/master/errorhandler.py +442 -0
- pyxcp/master/master.py +2047 -0
- pyxcp/py.typed +0 -0
- pyxcp/recorder/__init__.py +101 -0
- pyxcp/recorder/build_clang.cmd +1 -0
- pyxcp/recorder/build_clang.sh +2 -0
- pyxcp/recorder/build_gcc.cmd +1 -0
- pyxcp/recorder/build_gcc.sh +2 -0
- pyxcp/recorder/build_gcc_arm.sh +2 -0
- pyxcp/recorder/converter/__init__.py +450 -0
- pyxcp/recorder/lz4.c +2829 -0
- pyxcp/recorder/lz4.h +879 -0
- pyxcp/recorder/lz4hc.c +2041 -0
- pyxcp/recorder/lz4hc.h +413 -0
- pyxcp/recorder/mio.hpp +1714 -0
- pyxcp/recorder/reader.hpp +139 -0
- pyxcp/recorder/reco.py +277 -0
- pyxcp/recorder/recorder.rst +0 -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/rekorder.cp313-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cpp +59 -0
- pyxcp/recorder/rekorder.hpp +274 -0
- pyxcp/recorder/setup.py +41 -0
- pyxcp/recorder/test_reko.py +34 -0
- pyxcp/recorder/unfolder.hpp +1332 -0
- pyxcp/recorder/wrap.cpp +189 -0
- pyxcp/recorder/writer.hpp +302 -0
- pyxcp/scripts/__init__.py +0 -0
- pyxcp/scripts/pyxcp_probe_can_drivers.py +20 -0
- pyxcp/scripts/xcp_examples.py +64 -0
- pyxcp/scripts/xcp_fetch_a2l.py +40 -0
- pyxcp/scripts/xcp_id_scanner.py +19 -0
- pyxcp/scripts/xcp_info.py +144 -0
- pyxcp/scripts/xcp_profile.py +27 -0
- pyxcp/scripts/xmraw_converter.py +31 -0
- pyxcp/stim/__init__.py +0 -0
- pyxcp/tests/test_asam_types.py +24 -0
- pyxcp/tests/test_binpacking.py +186 -0
- pyxcp/tests/test_can.py +1324 -0
- pyxcp/tests/test_checksum.py +95 -0
- pyxcp/tests/test_daq.py +193 -0
- pyxcp/tests/test_daq_opt.py +426 -0
- pyxcp/tests/test_frame_padding.py +156 -0
- pyxcp/tests/test_master.py +2006 -0
- pyxcp/tests/test_transport.py +81 -0
- pyxcp/tests/test_utils.py +30 -0
- pyxcp/timing.py +60 -0
- pyxcp/transport/__init__.py +10 -0
- pyxcp/transport/base.py +440 -0
- pyxcp/transport/base_transport.hpp +0 -0
- pyxcp/transport/can.py +556 -0
- pyxcp/transport/eth.py +219 -0
- pyxcp/transport/sxi.py +135 -0
- pyxcp/transport/transport_wrapper.cpp +0 -0
- pyxcp/transport/usb_transport.py +213 -0
- pyxcp/types.py +1000 -0
- pyxcp/utils.py +128 -0
- pyxcp/vector/__init__.py +0 -0
- pyxcp/vector/map.py +82 -0
- pyxcp-0.23.0.dist-info/LICENSE +165 -0
- pyxcp-0.23.0.dist-info/METADATA +107 -0
- pyxcp-0.23.0.dist-info/RECORD +134 -0
- pyxcp-0.23.0.dist-info/WHEEL +4 -0
- pyxcp-0.23.0.dist-info/entry_points.txt +9 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import pyxcp.transport.base as tr
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_config():
|
|
9
|
+
# Exception: XCPonEth - Failed to resolve address <MagicMock name='mock.transport.eth.host' id='2414047113872'>:<MagicMock name='mock.transport.eth.port' id='2414047478992'>
|
|
10
|
+
config = mock.MagicMock()
|
|
11
|
+
config.general.return_value = mock.MagicMock()
|
|
12
|
+
config.transport.return_value = mock.MagicMock()
|
|
13
|
+
config.transport.eth.return_value = mock.MagicMock()
|
|
14
|
+
config.transport.eth.host = "localhost"
|
|
15
|
+
config.transport.eth.port = 5555
|
|
16
|
+
config.transport.eth.bind_to_address = ""
|
|
17
|
+
config.transport.eth.bind_to_port = 0
|
|
18
|
+
config.transport.create_daq_timestamps = False
|
|
19
|
+
return config
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_factory_works():
|
|
23
|
+
config = create_config()
|
|
24
|
+
assert isinstance(tr.create_transport("eth", config=config), tr.BaseTransport)
|
|
25
|
+
assert isinstance(tr.create_transport("sxi", config=config), tr.BaseTransport)
|
|
26
|
+
assert isinstance(
|
|
27
|
+
tr.create_transport(
|
|
28
|
+
"can",
|
|
29
|
+
config={
|
|
30
|
+
"CAN_ID_MASTER": 1,
|
|
31
|
+
"CAN_ID_SLAVE": 2,
|
|
32
|
+
"CAN_DRIVER": "MockCanInterface",
|
|
33
|
+
},
|
|
34
|
+
),
|
|
35
|
+
tr.BaseTransport,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_factory_works_case_insensitive():
|
|
40
|
+
assert isinstance(tr.create_transport("ETH"), tr.BaseTransport)
|
|
41
|
+
assert isinstance(tr.create_transport("SXI"), tr.BaseTransport)
|
|
42
|
+
assert isinstance(
|
|
43
|
+
tr.create_transport(
|
|
44
|
+
"CAN",
|
|
45
|
+
config={
|
|
46
|
+
"CAN_ID_MASTER": 1,
|
|
47
|
+
"CAN_ID_SLAVE": 2,
|
|
48
|
+
"CAN_DRIVER": "MockCanInterface",
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
tr.BaseTransport,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_factory_invalid_transport_name_raises():
|
|
56
|
+
with pytest.raises(ValueError):
|
|
57
|
+
tr.create_transport("xCp")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_transport_names():
|
|
61
|
+
transports = tr.available_transports()
|
|
62
|
+
|
|
63
|
+
assert "can" in transports
|
|
64
|
+
assert "eth" in transports
|
|
65
|
+
assert "sxi" in transports
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_transport_names_are_lower_case_only():
|
|
69
|
+
transports = tr.available_transports()
|
|
70
|
+
|
|
71
|
+
assert "CAN" not in transports
|
|
72
|
+
assert "ETH" not in transports
|
|
73
|
+
assert "SXI" not in transports
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_transport_classes():
|
|
77
|
+
transports = tr.available_transports()
|
|
78
|
+
|
|
79
|
+
assert issubclass(transports.get("can"), tr.BaseTransport)
|
|
80
|
+
assert issubclass(transports.get("eth"), tr.BaseTransport)
|
|
81
|
+
assert issubclass(transports.get("sxi"), tr.BaseTransport)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from sys import version_info
|
|
2
|
+
|
|
3
|
+
from pyxcp.utils import PYTHON_VERSION, flatten, getPythonVersion, hexDump, slicer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_hexdump(capsys):
|
|
7
|
+
print(hexDump(range(16)), end="")
|
|
8
|
+
captured = capsys.readouterr()
|
|
9
|
+
assert captured.out == "[00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f]"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_slicer1():
|
|
13
|
+
res = slicer([1, 2, 3, 4, 5, 6, 7, 8], 4)
|
|
14
|
+
assert res == [[1, 2, 3, 4], [5, 6, 7, 8]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_slicer2():
|
|
18
|
+
res = slicer(["10", "20", "30", "40", "50", "60", "70", "80"], 4, tuple)
|
|
19
|
+
assert res == [("10", "20", "30", "40"), ("50", "60", "70", "80")]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_flatten1():
|
|
23
|
+
res = flatten([[1, 2, 3, 4], [5, 6, 7, 8]])
|
|
24
|
+
assert res == [1, 2, 3, 4, 5, 6, 7, 8]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_version():
|
|
28
|
+
assert getPythonVersion() == version_info
|
|
29
|
+
assert PYTHON_VERSION == version_info
|
|
30
|
+
assert getPythonVersion() == PYTHON_VERSION
|
pyxcp/timing.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Timing:
|
|
6
|
+
T_US = 1000 * 1000
|
|
7
|
+
T_MS = 1000
|
|
8
|
+
T_S = 1
|
|
9
|
+
|
|
10
|
+
UNIT_MAP = {
|
|
11
|
+
T_US: "uS",
|
|
12
|
+
T_MS: "mS",
|
|
13
|
+
T_S: "S",
|
|
14
|
+
}
|
|
15
|
+
FMT = "min: {0:2.3f} {4}\nmax: {1:2.3f} {4}\n" "avg: {2:2.3f} {4}\nlast: {3:2.3f} {4}"
|
|
16
|
+
|
|
17
|
+
def __init__(self, unit=T_MS, record=False):
|
|
18
|
+
self.min = None
|
|
19
|
+
self.max = None
|
|
20
|
+
self.avg = None
|
|
21
|
+
self._previous = None
|
|
22
|
+
self.unit = unit
|
|
23
|
+
self._record = record
|
|
24
|
+
self._values = []
|
|
25
|
+
|
|
26
|
+
def start(self):
|
|
27
|
+
self._start = time.perf_counter()
|
|
28
|
+
|
|
29
|
+
def stop(self):
|
|
30
|
+
self._stop = time.perf_counter()
|
|
31
|
+
elapsed = self._stop - self._start
|
|
32
|
+
if self._record:
|
|
33
|
+
self._values.append(elapsed)
|
|
34
|
+
if self._previous:
|
|
35
|
+
self.min = min(self._previous, elapsed)
|
|
36
|
+
self.max = max(self._previous, elapsed)
|
|
37
|
+
self.avg = (self._previous + elapsed) / 2
|
|
38
|
+
else:
|
|
39
|
+
self.min = self.max = self.avg = elapsed
|
|
40
|
+
self._previous = elapsed
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
unitName = Timing.UNIT_MAP.get(self.unit, "??")
|
|
44
|
+
self.min = 0 if self.min is None else self.min
|
|
45
|
+
self.max = 0 if self.max is None else self.max
|
|
46
|
+
self.avg = 0 if self.avg is None else self.avg
|
|
47
|
+
self._previous = 0 if self._previous is None else self._previous
|
|
48
|
+
return Timing.FMT.format(
|
|
49
|
+
self.min * self.unit,
|
|
50
|
+
self.max * self.unit,
|
|
51
|
+
self.avg * self.unit,
|
|
52
|
+
self._previous * self.unit,
|
|
53
|
+
unitName,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__repr__ = __str__
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def values(self):
|
|
60
|
+
return self._values
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from .base import FrameAcquisitionPolicy # noqa: F401
|
|
3
|
+
from .base import FrameRecorderPolicy # noqa: F401
|
|
4
|
+
from .base import LegacyFrameAcquisitionPolicy # noqa: F401
|
|
5
|
+
from .base import NoOpPolicy # noqa: F401
|
|
6
|
+
from .base import StdoutPolicy # noqa: F401
|
|
7
|
+
from .can import Can # noqa: F401
|
|
8
|
+
from .eth import Eth # noqa: F401
|
|
9
|
+
from .sxi import SxI # noqa: F401
|
|
10
|
+
from .usb_transport import Usb # noqa: F401
|
pyxcp/transport/base.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import abc
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Any, Dict, Optional, Set, Type
|
|
7
|
+
|
|
8
|
+
import pyxcp.types as types
|
|
9
|
+
from pyxcp.cpp_ext.cpp_ext import Timestamp, TimestampType
|
|
10
|
+
from pyxcp.recorder import XcpLogFileWriter
|
|
11
|
+
from pyxcp.timing import Timing
|
|
12
|
+
from pyxcp.utils import (
|
|
13
|
+
CurrentDatetime,
|
|
14
|
+
flatten,
|
|
15
|
+
hexDump,
|
|
16
|
+
seconds_to_nanoseconds,
|
|
17
|
+
short_sleep,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
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
|
+
class EmptyFrameError(Exception):
|
|
124
|
+
"""Raised when an empty frame is received."""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class BaseTransport(metaclass=abc.ABCMeta):
|
|
128
|
+
"""Base class for transport-layers (Can, Eth, Sxi).
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
config: dict-like
|
|
133
|
+
Parameters like bitrate.
|
|
134
|
+
loglevel: ["INFO", "WARN", "DEBUG", "ERROR", "CRITICAL"]
|
|
135
|
+
Controls the verbosity of log messages.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, config, policy: Optional[FrameAcquisitionPolicy] = None, transport_layer_interface: Optional[Any] = None):
|
|
140
|
+
self.has_user_supplied_interface: bool = transport_layer_interface is not None
|
|
141
|
+
self.transport_layer_interface: Optional[Any] = transport_layer_interface
|
|
142
|
+
self.parent = None
|
|
143
|
+
self.policy: FrameAcquisitionPolicy = policy or LegacyFrameAcquisitionPolicy()
|
|
144
|
+
self.closeEvent: threading.Event = threading.Event()
|
|
145
|
+
|
|
146
|
+
self.command_lock: threading.Lock = threading.Lock()
|
|
147
|
+
self.policy_lock: threading.Lock = threading.Lock()
|
|
148
|
+
|
|
149
|
+
self.logger = logging.getLogger("PyXCP")
|
|
150
|
+
self._debug: bool = self.logger.level == 10
|
|
151
|
+
if transport_layer_interface:
|
|
152
|
+
self.logger.info(f"Transport - User Supplied Transport-Layer Interface: '{transport_layer_interface!s}'")
|
|
153
|
+
self.counter_send: int = 0
|
|
154
|
+
self.counter_received: int = -1
|
|
155
|
+
self.create_daq_timestamps: bool = config.create_daq_timestamps
|
|
156
|
+
self.timestamp = Timestamp(TimestampType.ABSOLUTE_TS)
|
|
157
|
+
self._start_datetime: CurrentDatetime = CurrentDatetime(self.timestamp.initial_value)
|
|
158
|
+
self.alignment: int = config.alignment
|
|
159
|
+
self.timeout: int = seconds_to_nanoseconds(config.timeout)
|
|
160
|
+
self.timer_restart_event: threading.Event = threading.Event()
|
|
161
|
+
self.timing: Timing = Timing()
|
|
162
|
+
self.resQueue: deque = deque()
|
|
163
|
+
self.listener: threading.Thread = threading.Thread(
|
|
164
|
+
target=self.listen,
|
|
165
|
+
args=(),
|
|
166
|
+
kwargs={},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self.first_daq_timestamp: Optional[int] = None
|
|
170
|
+
# self.timestamp_origin = self.timestamp.value
|
|
171
|
+
# self.datetime_origin = datetime.fromtimestamp(self.timestamp_origin)
|
|
172
|
+
self.pre_send_timestamp: int = self.timestamp.value
|
|
173
|
+
self.post_send_timestamp: int = self.timestamp.value
|
|
174
|
+
self.recv_timestamp: int = self.timestamp.value
|
|
175
|
+
|
|
176
|
+
def __del__(self) -> None:
|
|
177
|
+
self.finish_listener()
|
|
178
|
+
self.close_connection()
|
|
179
|
+
|
|
180
|
+
def load_config(self, config) -> None:
|
|
181
|
+
"""Load configuration data."""
|
|
182
|
+
class_name: str = self.__class__.__name__.lower()
|
|
183
|
+
self.config: Any = getattr(config, class_name)
|
|
184
|
+
|
|
185
|
+
def close(self) -> None:
|
|
186
|
+
"""Close the transport-layer connection and event-loop."""
|
|
187
|
+
self.finish_listener()
|
|
188
|
+
if self.listener.is_alive():
|
|
189
|
+
self.listener.join()
|
|
190
|
+
self.close_connection()
|
|
191
|
+
|
|
192
|
+
@abc.abstractmethod
|
|
193
|
+
def connect(self) -> None:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
def get(self):
|
|
197
|
+
"""Get an item from a deque considering a timeout condition."""
|
|
198
|
+
start: int = self.timestamp.value
|
|
199
|
+
while not self.resQueue:
|
|
200
|
+
if self.timer_restart_event.is_set():
|
|
201
|
+
start: int = self.timestamp.value
|
|
202
|
+
self.timer_restart_event.clear()
|
|
203
|
+
if self.timestamp.value - start > self.timeout:
|
|
204
|
+
raise EmptyFrameError
|
|
205
|
+
short_sleep()
|
|
206
|
+
item = self.resQueue.popleft()
|
|
207
|
+
# print("Q", item)
|
|
208
|
+
return item
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def start_datetime(self) -> int:
|
|
212
|
+
"""datetime of program start.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
int
|
|
217
|
+
"""
|
|
218
|
+
return self._start_datetime
|
|
219
|
+
|
|
220
|
+
def start_listener(self):
|
|
221
|
+
if self.listener.is_alive():
|
|
222
|
+
self.finish_listener()
|
|
223
|
+
self.listener.join()
|
|
224
|
+
|
|
225
|
+
self.listener = threading.Thread(target=self.listen)
|
|
226
|
+
self.listener.start()
|
|
227
|
+
|
|
228
|
+
def finish_listener(self):
|
|
229
|
+
if hasattr(self, "closeEvent"):
|
|
230
|
+
self.closeEvent.set()
|
|
231
|
+
|
|
232
|
+
def _request_internal(self, cmd, ignore_timeout=False, *data):
|
|
233
|
+
with self.command_lock:
|
|
234
|
+
frame = self._prepare_request(cmd, *data)
|
|
235
|
+
self.timing.start()
|
|
236
|
+
with self.policy_lock:
|
|
237
|
+
self.policy.feed(types.FrameCategory.CMD, self.counter_send, self.timestamp.value, frame)
|
|
238
|
+
self.send(frame)
|
|
239
|
+
try:
|
|
240
|
+
xcpPDU = self.get()
|
|
241
|
+
except EmptyFrameError:
|
|
242
|
+
if not ignore_timeout:
|
|
243
|
+
MSG = f"Response timed out (timeout={self.timeout / 1_000_000_000}s)"
|
|
244
|
+
with self.policy_lock:
|
|
245
|
+
self.policy.feed(types.FrameCategory.METADATA, self.counter_send, self.timestamp.value, bytes(MSG, "ascii"))
|
|
246
|
+
raise types.XcpTimeoutError(MSG) from None
|
|
247
|
+
else:
|
|
248
|
+
self.timing.stop()
|
|
249
|
+
return
|
|
250
|
+
self.timing.stop()
|
|
251
|
+
pid = types.Response.parse(xcpPDU).type
|
|
252
|
+
if pid == "ERR" and cmd.name != "SYNCH":
|
|
253
|
+
with self.policy_lock:
|
|
254
|
+
self.policy.feed(types.FrameCategory.ERROR, self.counter_received, self.timestamp.value, xcpPDU[1:])
|
|
255
|
+
err = types.XcpError.parse(xcpPDU[1:])
|
|
256
|
+
raise types.XcpResponseError(err)
|
|
257
|
+
return xcpPDU[1:]
|
|
258
|
+
|
|
259
|
+
def request(self, cmd, *data):
|
|
260
|
+
return self._request_internal(cmd, False, *data)
|
|
261
|
+
|
|
262
|
+
def request_optional_response(self, cmd, *data):
|
|
263
|
+
return self._request_internal(cmd, True, *data)
|
|
264
|
+
|
|
265
|
+
def block_request(self, cmd, *data):
|
|
266
|
+
"""
|
|
267
|
+
Implements packet transmission for block communication model (e.g. DOWNLOAD block mode)
|
|
268
|
+
All parameters are the same as in request(), but it does not receive response.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
# check response queue before each block request, so that if the slave device
|
|
272
|
+
# has responded with a negative response (e.g. ACCESS_DENIED or SEQUENCE_ERROR), we can
|
|
273
|
+
# process it.
|
|
274
|
+
if self.resQueue:
|
|
275
|
+
xcpPDU = self.resQueue.popleft()
|
|
276
|
+
pid = types.Response.parse(xcpPDU).type
|
|
277
|
+
if pid == "ERR" and cmd.name != "SYNCH":
|
|
278
|
+
err = types.XcpError.parse(xcpPDU[1:])
|
|
279
|
+
raise types.XcpResponseError(err)
|
|
280
|
+
with self.command_lock:
|
|
281
|
+
if isinstance(data, list):
|
|
282
|
+
data = data[0] # C++ interfacing.
|
|
283
|
+
frame = self._prepare_request(cmd, *data)
|
|
284
|
+
with self.policy_lock:
|
|
285
|
+
self.policy.feed(
|
|
286
|
+
types.FrameCategory.CMD if int(cmd) >= 0xC0 else types.FrameCategory.STIM,
|
|
287
|
+
self.counter_send,
|
|
288
|
+
self.timestamp.value,
|
|
289
|
+
frame,
|
|
290
|
+
)
|
|
291
|
+
self.send(frame)
|
|
292
|
+
|
|
293
|
+
def _prepare_request(self, cmd, *data):
|
|
294
|
+
"""
|
|
295
|
+
Prepares a request to be sent
|
|
296
|
+
"""
|
|
297
|
+
if self._debug:
|
|
298
|
+
self.logger.debug(cmd.name)
|
|
299
|
+
self.parent._setService(cmd)
|
|
300
|
+
|
|
301
|
+
cmd_len = cmd.bit_length() // 8 # calculate bytes needed for cmd
|
|
302
|
+
packet = bytes(flatten(cmd.to_bytes(cmd_len, "big"), data))
|
|
303
|
+
|
|
304
|
+
header = self.HEADER.pack(len(packet), self.counter_send)
|
|
305
|
+
self.counter_send = (self.counter_send + 1) & 0xFFFF
|
|
306
|
+
|
|
307
|
+
frame = header + packet
|
|
308
|
+
|
|
309
|
+
remainder = len(frame) % self.alignment
|
|
310
|
+
if remainder:
|
|
311
|
+
frame += b"\0" * (self.alignment - remainder)
|
|
312
|
+
|
|
313
|
+
if self._debug:
|
|
314
|
+
self.logger.debug(f"-> {hexDump(frame)}")
|
|
315
|
+
return frame
|
|
316
|
+
|
|
317
|
+
def block_receive(self, length_required: int) -> bytes:
|
|
318
|
+
"""
|
|
319
|
+
Implements packet reception for block communication model
|
|
320
|
+
(e.g. for XCP on CAN)
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
length_required: int
|
|
325
|
+
number of bytes to be expected in block response packets
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
bytes
|
|
330
|
+
all payload bytes received in block response packets
|
|
331
|
+
|
|
332
|
+
Raises
|
|
333
|
+
------
|
|
334
|
+
:class:`pyxcp.types.XcpTimeoutError`
|
|
335
|
+
"""
|
|
336
|
+
block_response = b""
|
|
337
|
+
start = self.timestamp.value
|
|
338
|
+
while len(block_response) < length_required:
|
|
339
|
+
if len(self.resQueue):
|
|
340
|
+
partial_response = self.resQueue.popleft()
|
|
341
|
+
block_response += partial_response[1:]
|
|
342
|
+
else:
|
|
343
|
+
if self.timestamp.value - start > self.timeout:
|
|
344
|
+
raise types.XcpTimeoutError("Response timed out [block_receive].") from None
|
|
345
|
+
short_sleep()
|
|
346
|
+
return block_response
|
|
347
|
+
|
|
348
|
+
@abc.abstractmethod
|
|
349
|
+
def send(self, frame):
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
@abc.abstractmethod
|
|
353
|
+
def close_connection(self):
|
|
354
|
+
"""Does the actual connection shutdown.
|
|
355
|
+
Needs to be implemented by any sub-class.
|
|
356
|
+
"""
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
@abc.abstractmethod
|
|
360
|
+
def listen(self):
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
def process_event_packet(self, packet):
|
|
364
|
+
packet = packet[1:]
|
|
365
|
+
ev_type = packet[0]
|
|
366
|
+
self.logger.debug(f"EVENT-PACKET: {hexDump(packet)}")
|
|
367
|
+
if ev_type == types.Event.EV_CMD_PENDING:
|
|
368
|
+
self.timer_restart_event.set()
|
|
369
|
+
|
|
370
|
+
def process_response(self, response: bytes, length: int, counter: int, recv_timestamp: int) -> None:
|
|
371
|
+
if counter == self.counter_received:
|
|
372
|
+
self.logger.warning(f"Duplicate message counter {counter} received from the XCP slave")
|
|
373
|
+
if self._debug:
|
|
374
|
+
self.logger.debug(f"<- L{length} C{counter} {hexDump(response[:512])}")
|
|
375
|
+
return
|
|
376
|
+
self.counter_received = counter
|
|
377
|
+
pid = response[0]
|
|
378
|
+
if pid >= 0xFC:
|
|
379
|
+
if self._debug:
|
|
380
|
+
self.logger.debug(f"<- L{length} C{counter} {hexDump(response)}")
|
|
381
|
+
if pid >= 0xFE:
|
|
382
|
+
self.resQueue.append(response)
|
|
383
|
+
with self.policy_lock:
|
|
384
|
+
self.policy.feed(types.FrameCategory.RESPONSE, self.counter_received, self.timestamp.value, response)
|
|
385
|
+
self.recv_timestamp = recv_timestamp
|
|
386
|
+
elif pid == 0xFD:
|
|
387
|
+
self.process_event_packet(response)
|
|
388
|
+
with self.policy_lock:
|
|
389
|
+
self.policy.feed(types.FrameCategory.EVENT, self.counter_received, self.timestamp.value, response)
|
|
390
|
+
elif pid == 0xFC:
|
|
391
|
+
with self.policy_lock:
|
|
392
|
+
self.policy.feed(types.FrameCategory.SERV, self.counter_received, self.timestamp.value, response)
|
|
393
|
+
else:
|
|
394
|
+
if self._debug:
|
|
395
|
+
self.logger.debug(f"<- L{length} C{counter} ODT_Data[0:8] {hexDump(response[:8])}")
|
|
396
|
+
if self.first_daq_timestamp is None:
|
|
397
|
+
self.first_daq_timestamp = recv_timestamp
|
|
398
|
+
if self.create_daq_timestamps:
|
|
399
|
+
timestamp = recv_timestamp
|
|
400
|
+
else:
|
|
401
|
+
timestamp = 0
|
|
402
|
+
with self.policy_lock:
|
|
403
|
+
self.policy.feed(types.FrameCategory.DAQ, self.counter_received, timestamp, response)
|
|
404
|
+
|
|
405
|
+
# @abc.abstractproperty
|
|
406
|
+
# @property
|
|
407
|
+
# def transport_layer_interface(self) -> Any:
|
|
408
|
+
# pass
|
|
409
|
+
|
|
410
|
+
# @transport_layer_interface.setter
|
|
411
|
+
# def transport_layer_interface(self, value: Any) -> None:
|
|
412
|
+
# self._transport_layer_interface = value
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def create_transport(name: str, *args, **kws) -> BaseTransport:
|
|
416
|
+
"""Factory function for transports.
|
|
417
|
+
|
|
418
|
+
Returns
|
|
419
|
+
-------
|
|
420
|
+
:class:`BaseTransport` derived instance.
|
|
421
|
+
"""
|
|
422
|
+
name = name.lower()
|
|
423
|
+
transports = available_transports()
|
|
424
|
+
if name in transports:
|
|
425
|
+
transport_class: Type[BaseTransport] = transports[name]
|
|
426
|
+
else:
|
|
427
|
+
raise ValueError(f"{name!r} is an invalid transport -- please choose one of [{' | '.join(transports.keys())}].")
|
|
428
|
+
return transport_class(*args, **kws)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def available_transports() -> Dict[str, Type[BaseTransport]]:
|
|
432
|
+
"""List all subclasses of :class:`BaseTransport`.
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
dict
|
|
437
|
+
name: class
|
|
438
|
+
"""
|
|
439
|
+
transports = BaseTransport.__subclasses__()
|
|
440
|
+
return {t.__name__.lower(): t for t in transports}
|
|
File without changes
|