rf-agent 0.3.0__py3-none-any.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.
- agent/__init__.py +1 -0
- agent/__main__.py +3 -0
- agent/app/__init__.py +43 -0
- agent/app/factories.py +126 -0
- agent/app/runner.py +295 -0
- agent/cli.py +499 -0
- agent/config/__init__.py +102 -0
- agent/config/errors.py +10 -0
- agent/config/loader.py +384 -0
- agent/domain/__init__.py +200 -0
- agent/processing/__init__.py +38 -0
- agent/processing/fft_pipeline.py +94 -0
- agent/processing/parse_iq.py +134 -0
- agent/processing/processor.py +136 -0
- agent/protocol/__init__.py +428 -0
- agent/session/__init__.py +433 -0
- agent/session/bandwidth.py +115 -0
- agent/source/__init__.py +0 -0
- agent/source/base.py +38 -0
- agent/source/sigmf.py +170 -0
- agent/source/simulator.py +74 -0
- agent/source/wav.py +245 -0
- agent/telemetry/__init__.py +20 -0
- agent/telemetry/loop.py +146 -0
- agent/telemetry/metrics.py +136 -0
- agent/telemetry/stage_timing.py +95 -0
- agent/transport/__init__.py +63 -0
- agent/transport/transport.py +149 -0
- agent/utils/__init__.py +0 -0
- agent/utils/power.py +32 -0
- rf_agent-0.3.0.dist-info/METADATA +50 -0
- rf_agent-0.3.0.dist-info/RECORD +34 -0
- rf_agent-0.3.0.dist-info/WHEEL +4 -0
- rf_agent-0.3.0.dist-info/entry_points.txt +2 -0
agent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""RF spectrum streaming agent."""
|
agent/__main__.py
ADDED
agent/app/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Agent orchestrator — composition and lifecycle.
|
|
2
|
+
|
|
3
|
+
This is the top-level entry point. It wires components together,
|
|
4
|
+
starts concurrent tasks, and handles shutdown.
|
|
5
|
+
|
|
6
|
+
It does no real work itself — only composes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentRuntime(Protocol):
|
|
15
|
+
"""Top-level agent lifecycle."""
|
|
16
|
+
|
|
17
|
+
async def run(self) -> None:
|
|
18
|
+
"""Start the agent. Runs until shutdown signal.
|
|
19
|
+
|
|
20
|
+
Wiring:
|
|
21
|
+
source = create_source(config)
|
|
22
|
+
processor = create_processor(config)
|
|
23
|
+
transport = create_transport(config)
|
|
24
|
+
session = create_session(transport, codec, config)
|
|
25
|
+
telemetry = create_telemetry(session, config)
|
|
26
|
+
|
|
27
|
+
iq_queue = asyncio.Queue(maxsize=config.queues.iq_queue_size)
|
|
28
|
+
frame_queue = asyncio.Queue(maxsize=config.queues.frame_queue_size)
|
|
29
|
+
|
|
30
|
+
run concurrently:
|
|
31
|
+
source.run(output=iq_queue)
|
|
32
|
+
processor.run(input=iq_queue, output=frame_queue)
|
|
33
|
+
session.run(frame_queue=frame_queue)
|
|
34
|
+
telemetry.run()
|
|
35
|
+
|
|
36
|
+
Shutdown:
|
|
37
|
+
On SIGINT/SIGTERM → cancel all tasks → close transport → stop source.
|
|
38
|
+
"""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
async def shutdown(self) -> None:
|
|
42
|
+
"""Graceful shutdown. Cancel tasks, close connections, release hardware."""
|
|
43
|
+
...
|
agent/app/factories.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Standard production component factories.
|
|
2
|
+
|
|
3
|
+
Wires WebSocketTransport, JsonBase64Codec, IQProcessor, Session, and
|
|
4
|
+
TelemetryLoop into a RunnerFactories bundle ready for AgentRunner.
|
|
5
|
+
|
|
6
|
+
A new WebSocketTransport is created per reconnect attempt; codec is shared.
|
|
7
|
+
Processor, session, and telemetry are constructed fresh per-attempt by the runner.
|
|
8
|
+
|
|
9
|
+
PipelineTiming and MetricsCollector are shared across attempts so cumulative
|
|
10
|
+
stats survive reconnects. The runner's own MetricsCollector arg in
|
|
11
|
+
make_telemetry is intentionally ignored in favour of the shared one.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import datetime
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from agent.app.runner import RunnerFactories
|
|
22
|
+
from agent.config import AgentConfig
|
|
23
|
+
from agent.domain import AgentMetrics
|
|
24
|
+
from agent.processing import Processor
|
|
25
|
+
from agent.processing.processor import IQProcessor
|
|
26
|
+
from agent.protocol import JsonBase64Codec, ProtocolCodec
|
|
27
|
+
from agent.session import Session
|
|
28
|
+
from agent.source.base import IQSource
|
|
29
|
+
from agent.telemetry.loop import TelemetryLoop
|
|
30
|
+
from agent.telemetry.metrics import MetricsCollector
|
|
31
|
+
from agent.telemetry.stage_timing import PipelineTiming
|
|
32
|
+
from agent.transport import Transport
|
|
33
|
+
from agent.transport.transport import WebSocketTransport
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _TransportSender:
|
|
37
|
+
"""Adapts (WebSocketTransport, JsonBase64Codec)
|
|
38
|
+
to TelemetryLoop's TelemetrySender."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, transport: Transport, codec: ProtocolCodec) -> None:
|
|
41
|
+
self._transport = transport
|
|
42
|
+
self._codec = codec
|
|
43
|
+
|
|
44
|
+
async def send_heartbeat(
|
|
45
|
+
self, node_id: str, session_id: str, timestamp_utc: str
|
|
46
|
+
) -> None:
|
|
47
|
+
await self._transport.send(
|
|
48
|
+
self._codec.encode_heartbeat(node_id, session_id, timestamp_utc)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def send_agent_status(
|
|
52
|
+
self,
|
|
53
|
+
node_id: str,
|
|
54
|
+
session_id: str,
|
|
55
|
+
timestamp_utc: str,
|
|
56
|
+
metrics: AgentMetrics,
|
|
57
|
+
) -> None:
|
|
58
|
+
await self._transport.send(
|
|
59
|
+
self._codec.encode_agent_status(node_id, session_id, timestamp_utc, metrics)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def make_standard_factories(
|
|
64
|
+
source_factory: Callable[[AgentConfig], IQSource],
|
|
65
|
+
) -> RunnerFactories:
|
|
66
|
+
"""Return a RunnerFactories bundle wired to standard production components."""
|
|
67
|
+
pipeline_timing = PipelineTiming()
|
|
68
|
+
shared_metrics = MetricsCollector(timings=pipeline_timing)
|
|
69
|
+
|
|
70
|
+
codec = JsonBase64Codec()
|
|
71
|
+
|
|
72
|
+
def make_transport(cfg: AgentConfig) -> Transport:
|
|
73
|
+
return WebSocketTransport()
|
|
74
|
+
|
|
75
|
+
def make_codec(cfg: AgentConfig) -> ProtocolCodec:
|
|
76
|
+
return codec
|
|
77
|
+
|
|
78
|
+
def make_processor(cfg: AgentConfig) -> Processor:
|
|
79
|
+
return IQProcessor(
|
|
80
|
+
descriptor=cfg.iq,
|
|
81
|
+
rf_config=cfg.rf,
|
|
82
|
+
timings=pipeline_timing,
|
|
83
|
+
metrics=shared_metrics,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def make_session(
|
|
87
|
+
cfg: AgentConfig,
|
|
88
|
+
t: Transport,
|
|
89
|
+
c: ProtocolCodec,
|
|
90
|
+
on_connected: Callable[[], None] | None = None,
|
|
91
|
+
) -> Session:
|
|
92
|
+
return Session(
|
|
93
|
+
config=cfg,
|
|
94
|
+
transport=t,
|
|
95
|
+
codec=c,
|
|
96
|
+
timings=pipeline_timing,
|
|
97
|
+
metrics=shared_metrics,
|
|
98
|
+
on_connected=on_connected,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def make_telemetry(
|
|
102
|
+
cfg: AgentConfig,
|
|
103
|
+
session: Any,
|
|
104
|
+
_metrics: MetricsCollector,
|
|
105
|
+
t: Transport,
|
|
106
|
+
c: ProtocolCodec,
|
|
107
|
+
) -> TelemetryLoop:
|
|
108
|
+
return TelemetryLoop(
|
|
109
|
+
node_id=cfg.identity.node_id,
|
|
110
|
+
session=session,
|
|
111
|
+
sender=_TransportSender(t, c),
|
|
112
|
+
metrics=shared_metrics,
|
|
113
|
+
heartbeat_interval_sec=cfg.telemetry.heartbeat_interval_s,
|
|
114
|
+
status_interval_sec=cfg.telemetry.status_interval_s,
|
|
115
|
+
clock=lambda: datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
116
|
+
sleep=asyncio.sleep,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return RunnerFactories(
|
|
120
|
+
make_source=source_factory,
|
|
121
|
+
make_processor=make_processor,
|
|
122
|
+
make_transport=make_transport,
|
|
123
|
+
make_codec=make_codec,
|
|
124
|
+
make_session=make_session,
|
|
125
|
+
make_telemetry=make_telemetry,
|
|
126
|
+
)
|
agent/app/runner.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""AgentRunner — top-level orchestrator.
|
|
2
|
+
|
|
3
|
+
Wires components together, manages task lifecycle, and drives the
|
|
4
|
+
reconnect loop. Owns queues, task startup, sibling cancellation, and
|
|
5
|
+
cleanup. Delegates everything else to the component it composes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import contextlib
|
|
12
|
+
import random
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Awaitable, Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any, Protocol
|
|
18
|
+
|
|
19
|
+
from agent.config import AgentConfig
|
|
20
|
+
from agent.domain import SpectrumFrame
|
|
21
|
+
from agent.processing import Processor
|
|
22
|
+
from agent.protocol import ProtocolCodec
|
|
23
|
+
from agent.source.base import IQSource
|
|
24
|
+
from agent.telemetry import MetricsCollector
|
|
25
|
+
from agent.session import FatalSessionError
|
|
26
|
+
from agent.transport import AuthenticationError, Transport
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Result types
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RunStopReason(Enum):
|
|
35
|
+
NORMAL_EXIT = "normal_exit" # A component returned without error
|
|
36
|
+
COMPONENT_FAILURE = "component_failure" # A component raised
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class RunResult:
|
|
41
|
+
reason: RunStopReason
|
|
42
|
+
error: BaseException | None = None
|
|
43
|
+
connected: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BuildFailure(Exception):
|
|
47
|
+
"""Component construction failed. run_forever() does not retry this."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Telemetry runnable protocol
|
|
52
|
+
#
|
|
53
|
+
# The runner only needs run() from telemetry. Defining a minimal protocol here
|
|
54
|
+
# avoids coupling the runner to TelemetryLoop internals and lets tests inject
|
|
55
|
+
# simple fakes.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _TelemetryRunnable(Protocol):
|
|
60
|
+
async def run(self) -> None: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Factory types
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
SourceFactory = Callable[[AgentConfig], IQSource]
|
|
68
|
+
ProcessorFactory = Callable[[AgentConfig], Processor]
|
|
69
|
+
TransportFactory = Callable[[AgentConfig], Transport]
|
|
70
|
+
CodecFactory = Callable[[AgentConfig], ProtocolCodec]
|
|
71
|
+
SessionFactory = Callable[
|
|
72
|
+
[AgentConfig, Transport, ProtocolCodec, "Callable[[], None] | None"], Any
|
|
73
|
+
]
|
|
74
|
+
TelemetryFactory = Callable[
|
|
75
|
+
[AgentConfig, Any, MetricsCollector, Transport, ProtocolCodec],
|
|
76
|
+
_TelemetryRunnable,
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class RunnerFactories:
|
|
82
|
+
"""All component factories. Replace any factory with a fake for testing."""
|
|
83
|
+
|
|
84
|
+
make_source: SourceFactory
|
|
85
|
+
make_processor: ProcessorFactory
|
|
86
|
+
make_transport: TransportFactory
|
|
87
|
+
make_codec: CodecFactory
|
|
88
|
+
make_session: SessionFactory
|
|
89
|
+
make_telemetry: TelemetryFactory
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# AgentRunner
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AgentRunner:
|
|
98
|
+
"""Top-level agent orchestrator.
|
|
99
|
+
|
|
100
|
+
Public API::
|
|
101
|
+
|
|
102
|
+
result = await runner.run_once() # single attempt
|
|
103
|
+
await runner.run_forever() # retries with backoff
|
|
104
|
+
|
|
105
|
+
Ownership:
|
|
106
|
+
- Creates queues, components, and tasks on every attempt.
|
|
107
|
+
- Cancels siblings when any task exits.
|
|
108
|
+
- Calls source.stop() and transport.close() in every path.
|
|
109
|
+
- Applies exponential backoff between retries in run_forever().
|
|
110
|
+
|
|
111
|
+
Does NOT own:
|
|
112
|
+
- Protocol message logic
|
|
113
|
+
- Handshake details
|
|
114
|
+
- IQ parsing / FFT
|
|
115
|
+
- Telemetry message schema
|
|
116
|
+
- Transport wire details
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
config: AgentConfig,
|
|
122
|
+
factories: RunnerFactories,
|
|
123
|
+
sleep: Callable[[float], Awaitable[None]] | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
self._config = config
|
|
126
|
+
self._factories = factories
|
|
127
|
+
self._sleep: Callable[[float], Awaitable[None]] = (
|
|
128
|
+
sleep if sleep is not None else asyncio.sleep
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# run_once
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
async def run_once(self) -> RunResult:
|
|
136
|
+
"""Build and run one agent attempt.
|
|
137
|
+
|
|
138
|
+
Returns when any component exits or fails.
|
|
139
|
+
Cancels sibling tasks and cleans up before returning.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
BuildFailure: if component construction raises. Never retried.
|
|
143
|
+
asyncio.CancelledError: on external cancellation (re-raised after
|
|
144
|
+
cancelling all tasks and running cleanup).
|
|
145
|
+
"""
|
|
146
|
+
config = self._config
|
|
147
|
+
source: IQSource | None = None
|
|
148
|
+
transport: Transport | None = None
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# ---- Build phase -----------------------------------------------
|
|
152
|
+
# Any exception here (including source.start()) is a BuildFailure.
|
|
153
|
+
# We do not retry build failures.
|
|
154
|
+
connected_event = asyncio.Event()
|
|
155
|
+
|
|
156
|
+
def _on_connected() -> None:
|
|
157
|
+
print("[rf-agent] connected", file=sys.stderr)
|
|
158
|
+
connected_event.set()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
transport = self._factories.make_transport(config)
|
|
162
|
+
codec = self._factories.make_codec(config)
|
|
163
|
+
session = self._factories.make_session(
|
|
164
|
+
config, transport, codec, _on_connected
|
|
165
|
+
)
|
|
166
|
+
source = self._factories.make_source(config)
|
|
167
|
+
processor = self._factories.make_processor(config)
|
|
168
|
+
metrics = MetricsCollector()
|
|
169
|
+
telemetry = self._factories.make_telemetry(
|
|
170
|
+
config, session, metrics, transport, codec
|
|
171
|
+
)
|
|
172
|
+
await source.start()
|
|
173
|
+
except asyncio.CancelledError:
|
|
174
|
+
raise
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
raise BuildFailure("Failed to construct agent components") from exc
|
|
177
|
+
|
|
178
|
+
# ---- Queues (fresh for every attempt) --------------------------
|
|
179
|
+
iq_queue: asyncio.Queue[bytes] = asyncio.Queue(
|
|
180
|
+
maxsize=config.queues.iq_queue_size
|
|
181
|
+
)
|
|
182
|
+
frame_queue: asyncio.Queue[SpectrumFrame] = asyncio.Queue(
|
|
183
|
+
maxsize=config.queues.frame_queue_size
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# ---- Task launch -----------------------------------------------
|
|
187
|
+
tasks = [
|
|
188
|
+
asyncio.create_task(source.run(iq_queue), name="source"),
|
|
189
|
+
asyncio.create_task(
|
|
190
|
+
processor.run(iq_queue, frame_queue), name="processor"
|
|
191
|
+
),
|
|
192
|
+
asyncio.create_task(session.run(frame_queue), name="session"),
|
|
193
|
+
asyncio.create_task(telemetry.run(), name="telemetry"),
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
first_error: BaseException | None = None
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
done, pending = await asyncio.wait(
|
|
200
|
+
tasks, return_when=asyncio.FIRST_COMPLETED
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Capture first non-cancellation error from finished tasks
|
|
204
|
+
for t in done:
|
|
205
|
+
if not t.cancelled():
|
|
206
|
+
task_exc = t.exception()
|
|
207
|
+
if task_exc is not None and first_error is None:
|
|
208
|
+
first_error = task_exc
|
|
209
|
+
|
|
210
|
+
# Cancel siblings
|
|
211
|
+
for t in pending:
|
|
212
|
+
t.cancel()
|
|
213
|
+
if pending:
|
|
214
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
215
|
+
|
|
216
|
+
except asyncio.CancelledError:
|
|
217
|
+
# External cancellation: cancel everything, then re-raise
|
|
218
|
+
for t in tasks:
|
|
219
|
+
t.cancel()
|
|
220
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
221
|
+
raise
|
|
222
|
+
|
|
223
|
+
return RunResult(
|
|
224
|
+
reason=(
|
|
225
|
+
RunStopReason.COMPONENT_FAILURE
|
|
226
|
+
if first_error is not None
|
|
227
|
+
else RunStopReason.NORMAL_EXIT
|
|
228
|
+
),
|
|
229
|
+
error=first_error,
|
|
230
|
+
connected=connected_event.is_set(),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
finally:
|
|
234
|
+
# Cleanup runs on every exit path (success, failure, cancellation)
|
|
235
|
+
if source is not None:
|
|
236
|
+
with contextlib.suppress(Exception):
|
|
237
|
+
await source.stop()
|
|
238
|
+
if transport is not None:
|
|
239
|
+
with contextlib.suppress(Exception):
|
|
240
|
+
await transport.close()
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------
|
|
243
|
+
# run_forever
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
async def run_forever(self) -> None:
|
|
247
|
+
"""Run the agent, retrying after each failed attempt.
|
|
248
|
+
|
|
249
|
+
Backoff:
|
|
250
|
+
Starts at reconnect.initial_delay_s, multiplied by
|
|
251
|
+
reconnect.backoff_factor after each attempt, capped at
|
|
252
|
+
reconnect.max_delay_s. Jitter (±50 %) is applied when
|
|
253
|
+
reconnect.jitter is True.
|
|
254
|
+
|
|
255
|
+
Stops immediately on:
|
|
256
|
+
- asyncio.CancelledError (external cancellation)
|
|
257
|
+
- BuildFailure (component construction error — do not retry)
|
|
258
|
+
"""
|
|
259
|
+
reconnect = self._config.reconnect
|
|
260
|
+
delay = reconnect.initial_delay_s
|
|
261
|
+
|
|
262
|
+
while True:
|
|
263
|
+
try:
|
|
264
|
+
result = await self.run_once()
|
|
265
|
+
except BuildFailure:
|
|
266
|
+
raise
|
|
267
|
+
except asyncio.CancelledError:
|
|
268
|
+
raise
|
|
269
|
+
|
|
270
|
+
if result.error is not None:
|
|
271
|
+
if isinstance(result.error, (AuthenticationError, FatalSessionError)):
|
|
272
|
+
raise result.error
|
|
273
|
+
print(
|
|
274
|
+
f"[rf-agent] disconnected"
|
|
275
|
+
f" ({type(result.error).__name__}): {result.error}",
|
|
276
|
+
file=sys.stderr,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if result.connected:
|
|
280
|
+
delay = reconnect.initial_delay_s
|
|
281
|
+
|
|
282
|
+
sleep_duration = delay
|
|
283
|
+
if reconnect.jitter:
|
|
284
|
+
sleep_duration = delay * (0.5 + random.random() * 0.5)
|
|
285
|
+
|
|
286
|
+
print(
|
|
287
|
+
f"[rf-agent] reconnecting in {sleep_duration:.1f}s...",
|
|
288
|
+
file=sys.stderr,
|
|
289
|
+
)
|
|
290
|
+
await self._sleep(sleep_duration)
|
|
291
|
+
|
|
292
|
+
delay = min(
|
|
293
|
+
delay * reconnect.backoff_factor,
|
|
294
|
+
reconnect.max_delay_s,
|
|
295
|
+
)
|