aioebus 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {aioebus-0.2.2 → aioebus-0.3.0}/PKG-INFO +1 -1
- {aioebus-0.2.2 → aioebus-0.3.0}/pyproject.toml +1 -1
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/__init__.py +3 -2
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/engine.py +25 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus.egg-info/PKG-INFO +1 -1
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_engine.py +34 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/LICENSE +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/README.md +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/setup.cfg +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/active.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/codecs.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/conditions.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/convert.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/frame.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/messages.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/passive.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/poll.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/py.typed +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/scan.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/translations.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/transport.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus.egg-info/SOURCES.txt +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus.egg-info/dependency_links.txt +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus.egg-info/top_level.txt +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_active.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_active_read.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_bitfields.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_codecs.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_condition_eval.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_conditions.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_datatypes.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_frame.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_messages.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_passive.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_poll.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_scan.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_translations.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_transport.py +0 -0
- {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_write.py +0 -0
|
@@ -20,7 +20,7 @@ from .active import (
|
|
|
20
20
|
)
|
|
21
21
|
from .codecs import Codec, decode, encode, make_codec
|
|
22
22
|
from .conditions import ConditionContext, context_from_scan, evaluate, evaluate_all
|
|
23
|
-
from .engine import EbusEngine, value_key
|
|
23
|
+
from .engine import EbusEngine, EngineStats, value_key
|
|
24
24
|
from .frame import Telegram, crc8, escape, parse_telegram, unescape
|
|
25
25
|
from .messages import FieldDef, MessageDef, parse_csv
|
|
26
26
|
from .passive import MessageRegistry, decode_raw
|
|
@@ -34,6 +34,7 @@ __all__ = [
|
|
|
34
34
|
"Codec",
|
|
35
35
|
"ConditionContext",
|
|
36
36
|
"EbusEngine",
|
|
37
|
+
"EngineStats",
|
|
37
38
|
"EnhDecoder",
|
|
38
39
|
"FieldDef",
|
|
39
40
|
"MessageDef",
|
|
@@ -64,4 +65,4 @@ __all__ = [
|
|
|
64
65
|
"unescape",
|
|
65
66
|
"value_key",
|
|
66
67
|
]
|
|
67
|
-
__version__ = "0.
|
|
68
|
+
__version__ = "0.3.0"
|
|
@@ -40,6 +40,19 @@ class _Active:
|
|
|
40
40
|
driver: _CommandDriver | None = None
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
@dataclass
|
|
44
|
+
class EngineStats:
|
|
45
|
+
"""Live counters for the adapter link, meant to be surfaced as the adapter's
|
|
46
|
+
own diagnostic entities (connection, bus activity, poll success)."""
|
|
47
|
+
|
|
48
|
+
connected: bool = False # the stream is up and bytes are arriving
|
|
49
|
+
telegrams: int = 0 # telegrams decoded off the bus passively (free)
|
|
50
|
+
polls_ok: int = 0 # scheduled polls that got an answer
|
|
51
|
+
polls_failed: int = 0 # scheduled polls that stayed unanswered (backed off)
|
|
52
|
+
writes: int = 0 # active writes performed
|
|
53
|
+
last_telegram: float | None = None # monotonic time of the last bus telegram
|
|
54
|
+
|
|
55
|
+
|
|
43
56
|
class EbusEngine:
|
|
44
57
|
"""Drive one eBUS adapter connection: passive decode + interleaved actives."""
|
|
45
58
|
|
|
@@ -68,6 +81,7 @@ class EbusEngine:
|
|
|
68
81
|
self._active: _Active | None = None
|
|
69
82
|
self._queue: deque[_Active] = deque()
|
|
70
83
|
self.cache: dict[tuple[str, str, str], object] = {}
|
|
84
|
+
self.stats = EngineStats()
|
|
71
85
|
self._stop = False
|
|
72
86
|
|
|
73
87
|
# -- public API ---------------------------------------------------------
|
|
@@ -81,11 +95,14 @@ class EbusEngine:
|
|
|
81
95
|
break
|
|
82
96
|
if not chunk:
|
|
83
97
|
break
|
|
98
|
+
self.stats.connected = True
|
|
84
99
|
for code, data in self._dec.feed(chunk):
|
|
85
100
|
await self._on_event(code, data)
|
|
101
|
+
self.stats.connected = False
|
|
86
102
|
|
|
87
103
|
def stop(self) -> None:
|
|
88
104
|
self._stop = True
|
|
105
|
+
self.stats.connected = False
|
|
89
106
|
|
|
90
107
|
def attach(self, send: Callable[[bytes], Awaitable[None]],
|
|
91
108
|
recv: Callable[[], Awaitable[bytes]]) -> None:
|
|
@@ -97,6 +114,7 @@ class EbusEngine:
|
|
|
97
114
|
self._buf.clear()
|
|
98
115
|
self._active = None
|
|
99
116
|
self._stop = False
|
|
117
|
+
self.stats.connected = False
|
|
100
118
|
|
|
101
119
|
async def read(self, msg: MessageDef, timeout: float = 4.0) -> dict:
|
|
102
120
|
"""Actively read a message as soon as the bus is idle; return its values."""
|
|
@@ -154,6 +172,8 @@ class EbusEngine:
|
|
|
154
172
|
return
|
|
155
173
|
if not tg.crc_ok:
|
|
156
174
|
return
|
|
175
|
+
self.stats.telegrams += 1
|
|
176
|
+
self.stats.last_telegram = time.monotonic()
|
|
157
177
|
decoded = self.registry.decode(tg)
|
|
158
178
|
if decoded is not None:
|
|
159
179
|
self._store(*decoded)
|
|
@@ -193,6 +213,10 @@ class EbusEngine:
|
|
|
193
213
|
if drv.error is None:
|
|
194
214
|
if isinstance(drv, ReadDriver) and act.msg is not None:
|
|
195
215
|
self._store(act.msg, act.msg.decode_labeled(slave=drv.result))
|
|
216
|
+
if act.future is None: # a scheduled poll, not an explicit read
|
|
217
|
+
self.stats.polls_ok += 1
|
|
218
|
+
elif isinstance(drv, WriteDriver):
|
|
219
|
+
self.stats.writes += 1
|
|
196
220
|
if act.future is not None and not act.future.done():
|
|
197
221
|
act.future.set_result(drv.result)
|
|
198
222
|
return
|
|
@@ -219,3 +243,4 @@ class EbusEngine:
|
|
|
219
243
|
# the same dead register, starving the rest; the backoff then fades a
|
|
220
244
|
# persistently unanswered register out of the rotation entirely.
|
|
221
245
|
self.scheduler.mark_failure(act.msg, time.monotonic())
|
|
246
|
+
self.stats.polls_failed += 1
|
|
@@ -112,6 +112,40 @@ class _ArbLossAdapter:
|
|
|
112
112
|
return bytes(out)
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
def test_stats_track_connection_and_bus_activity():
|
|
116
|
+
"""A decoded bus telegram counts as activity and marks the link connected;
|
|
117
|
+
an unanswered poll is counted as a failed poll."""
|
|
118
|
+
async def inner():
|
|
119
|
+
link = _Link()
|
|
120
|
+
eng = EbusEngine(link.send, link.recv, REG)
|
|
121
|
+
assert eng.stats.connected is False
|
|
122
|
+
task = asyncio.create_task(eng.run())
|
|
123
|
+
link.inject(encode_received_frame(bytes.fromhex("3138b509030d1800740003910100bb00")))
|
|
124
|
+
await asyncio.sleep(0.05)
|
|
125
|
+
assert eng.stats.connected is True
|
|
126
|
+
assert eng.stats.telegrams == 1
|
|
127
|
+
assert eng.stats.last_telegram is not None
|
|
128
|
+
eng.stop()
|
|
129
|
+
link.inject(b"")
|
|
130
|
+
await asyncio.wait_for(task, 1)
|
|
131
|
+
assert eng.stats.connected is False
|
|
132
|
+
|
|
133
|
+
msg = BY["Statenumber"]
|
|
134
|
+
sched = PollScheduler([msg], base_interval=10.0)
|
|
135
|
+
eng2 = EbusEngine(link.send, link.recv, REG, scheduler=sched)
|
|
136
|
+
|
|
137
|
+
class _FailedDriver:
|
|
138
|
+
error = "timeout waiting for slave"
|
|
139
|
+
|
|
140
|
+
act = _Active(lambda: _FailedDriver(), msg, None) # future None -> a poll
|
|
141
|
+
act.driver = _FailedDriver()
|
|
142
|
+
eng2._active = act
|
|
143
|
+
await eng2._finish_active()
|
|
144
|
+
assert eng2.stats.polls_failed == 1
|
|
145
|
+
|
|
146
|
+
asyncio.run(inner())
|
|
147
|
+
|
|
148
|
+
|
|
115
149
|
def test_arbitration_loss_captures_winner_passively():
|
|
116
150
|
"""When our poll loses arbitration, the telegram that won is decoded
|
|
117
151
|
passively (seeded with the winner's address) instead of being discarded."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|