ReticulumTelemetryHub 0.1.0__py3-none-any.whl → 0.143.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.
- reticulum_telemetry_hub/api/__init__.py +23 -0
- reticulum_telemetry_hub/api/models.py +323 -0
- reticulum_telemetry_hub/api/service.py +836 -0
- reticulum_telemetry_hub/api/storage.py +528 -0
- reticulum_telemetry_hub/api/storage_base.py +156 -0
- reticulum_telemetry_hub/api/storage_models.py +118 -0
- reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
- reticulum_telemetry_hub/atak_cot/base.py +277 -0
- reticulum_telemetry_hub/atak_cot/chat.py +506 -0
- reticulum_telemetry_hub/atak_cot/detail.py +235 -0
- reticulum_telemetry_hub/atak_cot/event.py +181 -0
- reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
- reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
- reticulum_telemetry_hub/config/__init__.py +25 -0
- reticulum_telemetry_hub/config/constants.py +7 -0
- reticulum_telemetry_hub/config/manager.py +515 -0
- reticulum_telemetry_hub/config/models.py +215 -0
- reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
- reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
- reticulum_telemetry_hub/internal_api/__init__.py +21 -0
- reticulum_telemetry_hub/internal_api/bus.py +344 -0
- reticulum_telemetry_hub/internal_api/core.py +690 -0
- reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
- reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
- reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
- reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
- reticulum_telemetry_hub/internal_api/versioning.py +63 -0
- reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
- reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
- reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
- reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
- reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
- reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
- reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
- reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
- reticulum_telemetry_hub/northbound/__init__.py +5 -0
- reticulum_telemetry_hub/northbound/app.py +195 -0
- reticulum_telemetry_hub/northbound/auth.py +119 -0
- reticulum_telemetry_hub/northbound/gateway.py +310 -0
- reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
- reticulum_telemetry_hub/northbound/models.py +213 -0
- reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
- reticulum_telemetry_hub/northbound/routes_files.py +119 -0
- reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
- reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
- reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
- reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
- reticulum_telemetry_hub/northbound/serializers.py +72 -0
- reticulum_telemetry_hub/northbound/services.py +373 -0
- reticulum_telemetry_hub/northbound/websocket.py +855 -0
- reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
- reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
- reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
- reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
- reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
- reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
- reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
- reticulum_telemetry_hub/reticulum_server/services.py +422 -0
- reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
- reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
- {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
- reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
- lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
- lxmf_telemetry/model/persistance/__init__.py +0 -3
- lxmf_telemetry/model/persistance/sensors/location.py +0 -69
- lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
- lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
- lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
- lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
- lxmf_telemetry/telemetry_controller.py +0 -124
- reticulum_server/main.py +0 -182
- reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
- reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
- {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
- {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
- {reticulum_server → reticulum_telemetry_hub/reticulum_server}/__init__.py +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Runtime helpers for ReticulumTelemetryHub daemon services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
import threading
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterator, Mapping
|
|
11
|
+
|
|
12
|
+
import RNS
|
|
13
|
+
|
|
14
|
+
try: # pragma: no cover - optional dependency
|
|
15
|
+
from gpsdclient import GPSDClient # type: ignore
|
|
16
|
+
except ImportError: # pragma: no cover - gpsdclient is optional
|
|
17
|
+
GPSDClient = None # type: ignore
|
|
18
|
+
|
|
19
|
+
from reticulum_telemetry_hub.atak_cot.tak_connector import TakConnector
|
|
20
|
+
from reticulum_telemetry_hub.config.manager import HubConfigurationManager
|
|
21
|
+
from reticulum_telemetry_hub.lxmf_telemetry.telemeter_manager import (
|
|
22
|
+
TelemeterManager,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from reticulum_telemetry_hub.reticulum_server.__main__ import (
|
|
27
|
+
ReticulumTelemetryHub,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _utcnow() -> datetime:
|
|
32
|
+
"""
|
|
33
|
+
Return a timezone-aware UTC timestamp with the tzinfo stripped.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
datetime: The current UTC timestamp without timezone information.
|
|
37
|
+
"""
|
|
38
|
+
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class HubService:
|
|
43
|
+
"""Base class for long running Reticulum telemetry services."""
|
|
44
|
+
|
|
45
|
+
name: str
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Initialize thread synchronization primitives for the service.
|
|
50
|
+
|
|
51
|
+
Sets up the stop event and placeholder thread handle used by the
|
|
52
|
+
lifecycle helpers.
|
|
53
|
+
"""
|
|
54
|
+
self._stop_event = threading.Event()
|
|
55
|
+
self._thread: threading.Thread | None = None
|
|
56
|
+
|
|
57
|
+
def start(self) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Start the service in a background thread if supported.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
bool: ``True`` if the service was started; ``False`` if it was
|
|
63
|
+
already running or unsupported on this host.
|
|
64
|
+
"""
|
|
65
|
+
if self._thread is not None:
|
|
66
|
+
return False
|
|
67
|
+
if not self.is_supported():
|
|
68
|
+
RNS.log(
|
|
69
|
+
(
|
|
70
|
+
"Skipping daemon service "
|
|
71
|
+
f"'{self.name}' because the host does not provide "
|
|
72
|
+
"the required hardware/software"
|
|
73
|
+
),
|
|
74
|
+
RNS.LOG_INFO,
|
|
75
|
+
)
|
|
76
|
+
return False
|
|
77
|
+
self._thread = threading.Thread(target=self._run_wrapper, daemon=True)
|
|
78
|
+
self._thread.start()
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def stop(self) -> None:
|
|
82
|
+
"""Stop the background thread and reset lifecycle flags."""
|
|
83
|
+
if self._thread is None:
|
|
84
|
+
return
|
|
85
|
+
self._stop_event.set()
|
|
86
|
+
self._thread.join()
|
|
87
|
+
self._thread = None
|
|
88
|
+
self._stop_event.clear()
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
# overridable hooks
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
def is_supported(self) -> bool: # pragma: no cover - trivial default
|
|
94
|
+
"""
|
|
95
|
+
Determine whether the service can run on the current host.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
bool: ``True`` when the dependencies are available, otherwise
|
|
99
|
+
``False``.
|
|
100
|
+
"""
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
def poll_interval(self) -> float: # pragma: no cover - trivial default
|
|
104
|
+
"""
|
|
105
|
+
Return the preferred polling interval in seconds.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
float: Number of seconds to wait between iterations.
|
|
109
|
+
"""
|
|
110
|
+
return 1.0
|
|
111
|
+
|
|
112
|
+
def _run(self) -> None: # pragma: no cover - interface method
|
|
113
|
+
"""
|
|
114
|
+
Execute the service logic.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
NotImplementedError: Must be implemented by subclasses.
|
|
118
|
+
"""
|
|
119
|
+
raise NotImplementedError
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
# internal helpers
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
def _run_wrapper(self) -> None:
|
|
125
|
+
"""Wrap ``_run`` with crash logging and cleanup handling."""
|
|
126
|
+
try:
|
|
127
|
+
self._run()
|
|
128
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
129
|
+
RNS.log(
|
|
130
|
+
f"Daemon service '{self.name}' crashed: {exc}",
|
|
131
|
+
RNS.LOG_ERROR,
|
|
132
|
+
)
|
|
133
|
+
finally:
|
|
134
|
+
self._thread = None
|
|
135
|
+
self._stop_event.clear()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class GpsTelemetryService(HubService):
|
|
139
|
+
"""GPS backed telemetry mutator that enriches location sensors."""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
telemeter_manager: TelemeterManager,
|
|
145
|
+
client_factory: Callable[..., GPSDClient] | None = None,
|
|
146
|
+
host: str | None = None,
|
|
147
|
+
port: int | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Initialize the gpsd-backed telemetry service.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
telemeter_manager (TelemeterManager): Manager providing the location
|
|
154
|
+
sensor to update.
|
|
155
|
+
client_factory (Callable[..., GPSDClient] | None): Factory used to
|
|
156
|
+
create GPSD clients. Defaults to ``GPSDClient`` when available.
|
|
157
|
+
host (str | None): gpsd host, defaults to ``127.0.0.1``.
|
|
158
|
+
port (int | None): gpsd TCP port, defaults to ``2947``.
|
|
159
|
+
"""
|
|
160
|
+
super().__init__(name="gpsd")
|
|
161
|
+
self._telemeter_manager = telemeter_manager
|
|
162
|
+
self._client_factory = client_factory or (
|
|
163
|
+
lambda **kwargs: GPSDClient(**kwargs)
|
|
164
|
+
) # type: ignore[arg-type]
|
|
165
|
+
self._host = host or "127.0.0.1"
|
|
166
|
+
self._port = port or 2947
|
|
167
|
+
|
|
168
|
+
def is_supported(self) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Check whether gpsd dependencies are present.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
bool: ``True`` when gpsdclient is available and a telemeter manager
|
|
174
|
+
has been configured.
|
|
175
|
+
"""
|
|
176
|
+
return GPSDClient is not None and self._telemeter_manager is not None
|
|
177
|
+
|
|
178
|
+
def _run(self) -> None:
|
|
179
|
+
"""Continuously poll gpsd and apply updates to the location sensor."""
|
|
180
|
+
manager = self._telemeter_manager
|
|
181
|
+
if manager is None:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Ensure the location sensor exists before polling GPS data.
|
|
185
|
+
manager.enable_sensor("location")
|
|
186
|
+
sensor = manager.get_sensor("location")
|
|
187
|
+
if sensor is None:
|
|
188
|
+
RNS.log(
|
|
189
|
+
("GPS daemon service could not obtain a location sensor; " "aborting"),
|
|
190
|
+
RNS.LOG_WARNING,
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
client = self._client_factory(host=self._host, port=self._port)
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
RNS.log(
|
|
198
|
+
("Unable to connect to gpsd on " f"{self._host}:{self._port}: {exc}"),
|
|
199
|
+
RNS.LOG_ERROR,
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
stream = self._iter_gps_stream(client)
|
|
204
|
+
for payload in stream:
|
|
205
|
+
if self._stop_event.is_set():
|
|
206
|
+
break
|
|
207
|
+
self._apply_gps_payload(sensor, payload)
|
|
208
|
+
|
|
209
|
+
def _iter_gps_stream(
|
|
210
|
+
self, client: GPSDClient
|
|
211
|
+
) -> Iterator[dict]: # pragma: no cover - passthrough
|
|
212
|
+
"""
|
|
213
|
+
Yield GPS samples from the gpsdclient stream.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
client (GPSDClient): Connected gpsd client.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Iterator[dict]: Iterable of GPS payload dictionaries.
|
|
220
|
+
"""
|
|
221
|
+
return client.dict_stream(convert_datetime=False)
|
|
222
|
+
|
|
223
|
+
def _apply_gps_payload(self, sensor, payload: Mapping[str, Any]) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Map gpsd payload fields onto the hub's location sensor.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
sensor: Location sensor instance to mutate.
|
|
229
|
+
payload (Mapping[str, Any]): Raw gpsd payload dictionary.
|
|
230
|
+
"""
|
|
231
|
+
lat = payload.get("lat")
|
|
232
|
+
lon = payload.get("lon")
|
|
233
|
+
if lat is None or lon is None:
|
|
234
|
+
return
|
|
235
|
+
sensor.latitude = float(lat)
|
|
236
|
+
sensor.longitude = float(lon)
|
|
237
|
+
sensor.altitude = self._coerce_float(payload.get("alt"), sensor.altitude)
|
|
238
|
+
sensor.speed = self._coerce_float(payload.get("speed"), sensor.speed)
|
|
239
|
+
sensor.bearing = self._coerce_float(payload.get("track"), sensor.bearing)
|
|
240
|
+
sensor.accuracy = self._coerce_float(payload.get("eps"), sensor.accuracy)
|
|
241
|
+
sensor.last_update = _utcnow()
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _coerce_float(
|
|
245
|
+
value: Any, current: float | None, *, default: float = 0.0
|
|
246
|
+
) -> float:
|
|
247
|
+
"""
|
|
248
|
+
Convert a value to float, falling back to the current or default value.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
value (Any): Candidate value to convert.
|
|
252
|
+
current (float | None): Current sensor value to preserve on failure.
|
|
253
|
+
default (float): Default when neither the value nor current is set.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
float: Coerced float value.
|
|
257
|
+
"""
|
|
258
|
+
if value is None:
|
|
259
|
+
return current if current is not None else default
|
|
260
|
+
try:
|
|
261
|
+
return float(value)
|
|
262
|
+
except (TypeError, ValueError):
|
|
263
|
+
return current if current is not None else default
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class CotTelemetryService(HubService):
|
|
267
|
+
"""Scheduler that pushes location updates to a TAK endpoint."""
|
|
268
|
+
|
|
269
|
+
def __init__(
|
|
270
|
+
self,
|
|
271
|
+
*,
|
|
272
|
+
connector: TakConnector,
|
|
273
|
+
interval: float | None,
|
|
274
|
+
keepalive_interval: float | None = None,
|
|
275
|
+
ping_interval: float | None = None,
|
|
276
|
+
) -> None:
|
|
277
|
+
"""
|
|
278
|
+
Initialize the TAK connector scheduler.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
connector (TakConnector): Connected TAK connector instance.
|
|
282
|
+
interval (float | None): Desired CoT send interval in seconds.
|
|
283
|
+
keepalive_interval (float | None): Override for keepalive cadence.
|
|
284
|
+
ping_interval (float | None): Override for ping cadence.
|
|
285
|
+
"""
|
|
286
|
+
super().__init__(name="tak_cot")
|
|
287
|
+
self._connector = connector
|
|
288
|
+
connector_interval = connector.config.poll_interval_seconds
|
|
289
|
+
default_keepalive = connector.config.keepalive_interval_seconds
|
|
290
|
+
self._interval = interval if interval and interval > 0 else connector_interval
|
|
291
|
+
if self._interval <= 0:
|
|
292
|
+
self._interval = 1.0
|
|
293
|
+
resolved_keepalive = (
|
|
294
|
+
keepalive_interval
|
|
295
|
+
if keepalive_interval is not None and keepalive_interval > 0
|
|
296
|
+
else default_keepalive
|
|
297
|
+
)
|
|
298
|
+
self._keepalive_interval = (
|
|
299
|
+
resolved_keepalive if resolved_keepalive > 0 else 60.0
|
|
300
|
+
)
|
|
301
|
+
self._ping_interval = (
|
|
302
|
+
ping_interval
|
|
303
|
+
if ping_interval is not None and ping_interval > 0
|
|
304
|
+
else self._keepalive_interval
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def is_supported(self) -> bool:
|
|
308
|
+
"""
|
|
309
|
+
Confirm the TAK connector is configured.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
bool: ``True`` when the connector exists and the interval is valid.
|
|
313
|
+
"""
|
|
314
|
+
return self._connector is not None and self._interval > 0
|
|
315
|
+
|
|
316
|
+
def poll_interval(self) -> float:
|
|
317
|
+
"""
|
|
318
|
+
Return the configured CoT polling interval.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
float: Seconds between location pushes.
|
|
322
|
+
"""
|
|
323
|
+
return self._interval
|
|
324
|
+
|
|
325
|
+
def _run(self) -> None:
|
|
326
|
+
"""Send periodic location updates, keepalives, and pings."""
|
|
327
|
+
last_keepalive = time.monotonic() - self._keepalive_interval
|
|
328
|
+
last_location = time.monotonic() - self._interval
|
|
329
|
+
last_ping = time.monotonic() - self._ping_interval
|
|
330
|
+
while not self._stop_event.is_set():
|
|
331
|
+
now = time.monotonic()
|
|
332
|
+
if now - last_ping >= self._ping_interval:
|
|
333
|
+
try:
|
|
334
|
+
asyncio.run(self._connector.send_ping())
|
|
335
|
+
last_ping = time.monotonic()
|
|
336
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
337
|
+
RNS.log(
|
|
338
|
+
f"TAK connector failed to send hello keepalive: {exc}",
|
|
339
|
+
RNS.LOG_ERROR,
|
|
340
|
+
)
|
|
341
|
+
if now - last_keepalive >= self._keepalive_interval:
|
|
342
|
+
try:
|
|
343
|
+
asyncio.run(self._connector.send_keepalive())
|
|
344
|
+
last_keepalive = time.monotonic()
|
|
345
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
346
|
+
RNS.log(
|
|
347
|
+
f"TAK connector failed to send keepalive: {exc}",
|
|
348
|
+
RNS.LOG_ERROR,
|
|
349
|
+
)
|
|
350
|
+
if now - last_location >= self._interval:
|
|
351
|
+
try:
|
|
352
|
+
asyncio.run(self._connector.send_latest_location())
|
|
353
|
+
last_location = time.monotonic()
|
|
354
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
355
|
+
RNS.log(
|
|
356
|
+
f"TAK connector failed to send CoT update: {exc}",
|
|
357
|
+
RNS.LOG_ERROR,
|
|
358
|
+
)
|
|
359
|
+
remaining_keepalive = self._keepalive_interval - (
|
|
360
|
+
time.monotonic() - last_keepalive
|
|
361
|
+
)
|
|
362
|
+
remaining_location = self._interval - (time.monotonic() - last_location)
|
|
363
|
+
remaining_ping = self._ping_interval - (time.monotonic() - last_ping)
|
|
364
|
+
wait_time = max(
|
|
365
|
+
min(remaining_keepalive, remaining_location, remaining_ping), 0.01
|
|
366
|
+
)
|
|
367
|
+
self._stop_event.wait(wait_time)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _gps_factory(hub: "ReticulumTelemetryHub") -> HubService:
|
|
371
|
+
"""
|
|
372
|
+
Build the GPS daemon service from hub configuration.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
hub (ReticulumTelemetryHub): Active hub instance.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
HubService: Configured GPS telemetry service.
|
|
379
|
+
"""
|
|
380
|
+
config_manager = hub.config_manager or HubConfigurationManager(
|
|
381
|
+
storage_path=hub.storage_path
|
|
382
|
+
)
|
|
383
|
+
runtime_config = config_manager.runtime_config
|
|
384
|
+
return GpsTelemetryService(
|
|
385
|
+
telemeter_manager=hub.telemeter_manager,
|
|
386
|
+
host=runtime_config.gpsd_host,
|
|
387
|
+
port=runtime_config.gpsd_port,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _cot_factory(hub: "ReticulumTelemetryHub") -> HubService:
|
|
392
|
+
"""
|
|
393
|
+
Build the TAK CoT scheduler from hub configuration.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
hub (ReticulumTelemetryHub): Active hub instance.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
HubService: Configured CoT telemetry service.
|
|
400
|
+
"""
|
|
401
|
+
config_manager = hub.config_manager or HubConfigurationManager(
|
|
402
|
+
storage_path=hub.storage_path
|
|
403
|
+
)
|
|
404
|
+
connector = hub.tak_connector
|
|
405
|
+
if connector is None:
|
|
406
|
+
connector = TakConnector(
|
|
407
|
+
config=config_manager.tak_config,
|
|
408
|
+
telemeter_manager=hub.telemeter_manager,
|
|
409
|
+
telemetry_controller=hub.tel_controller,
|
|
410
|
+
identity_lookup=hub._lookup_identity_label,
|
|
411
|
+
)
|
|
412
|
+
interval = connector.config.poll_interval_seconds
|
|
413
|
+
keepalive = connector.config.keepalive_interval_seconds
|
|
414
|
+
return CotTelemetryService(
|
|
415
|
+
connector=connector, interval=interval, keepalive_interval=keepalive
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
SERVICE_FACTORIES: dict[str, Callable[["ReticulumTelemetryHub"], HubService]] = {
|
|
420
|
+
"gpsd": _gps_factory,
|
|
421
|
+
"tak_cot": _cot_factory,
|
|
422
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ReticulumTelemetryHub
|
|
3
|
+
Version: 0.143.0
|
|
4
|
+
Summary: Reticulum-Telemetry-Hub (RTH) manages a complete TCP node across a Reticulum-based network, enabling communication and data sharing between clients like Sideband or Meshchat.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: naman108, corvo
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: aiohttp (>=3.10.10,<4.0.0)
|
|
15
|
+
Requires-Dist: cryptography (>=46.0.2,<47.0.0)
|
|
16
|
+
Requires-Dist: fastapi (>=0.115.0,<0.116.0)
|
|
17
|
+
Requires-Dist: gpsdclient (>=1.3.2,<2.0.0)
|
|
18
|
+
Requires-Dist: httpx (>=0.27.0,<0.28.0)
|
|
19
|
+
Requires-Dist: lxmf (>=0.9.4,<0.10.0)
|
|
20
|
+
Requires-Dist: msgpack (>=1.0.8,<2.0.0)
|
|
21
|
+
Requires-Dist: pydantic (>=2.8.2,<3.0.0)
|
|
22
|
+
Requires-Dist: pynacl (>=1.5.0,<2.0.0)
|
|
23
|
+
Requires-Dist: pytak (>=7.0.2,<8.0.0)
|
|
24
|
+
Requires-Dist: pytest (>=8.3.2,<9.0.0)
|
|
25
|
+
Requires-Dist: pytest-cov (>=6.1,<7.0)
|
|
26
|
+
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
27
|
+
Requires-Dist: python-multipart (>=0.0.20,<0.0.21)
|
|
28
|
+
Requires-Dist: qrcode (>=7.4.2,<8.0.0)
|
|
29
|
+
Requires-Dist: rns (>=1.1.1,<2.0.0)
|
|
30
|
+
Requires-Dist: sqlalchemy (>=2.0.32,<3.0.0)
|
|
31
|
+
Requires-Dist: uvicorn (>=0.30.0,<0.31.0)
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Reticulum Community Hub (RCH)
|
|
35
|
+
|
|
36
|
+
<img src="RCH.png" alt="RCH logo" width="100" height="100" style="float:left; margin-right:12px;">
|
|
37
|
+
|
|
38
|
+
Reticulum Community Hub (RCH) is a shared coordination point for mesh networks. It allows people and groups to exchange messages, share situational updates, and distribute files in a structured and reliable way, even across intermittent or low-connectivity environments, while remaining independent from centralized internet services.
|
|
39
|
+
<div style="clear: both;"></div>
|
|
40
|
+
|
|
41
|
+
## What it does
|
|
42
|
+
|
|
43
|
+
- One-to-many and topic-scoped message fan-out over LXMF.
|
|
44
|
+
- Telemetry collection and on-demand telemetry responses.
|
|
45
|
+
- File and image attachment storage with retrieval by ID.
|
|
46
|
+
- Northbound REST + WebSocket API for operators and the admin UI.
|
|
47
|
+
- Optional TAK/CoT bridge for chat and location updates.
|
|
48
|
+
|
|
49
|
+
## What it looks like
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
## Quickstart (from source)
|
|
55
|
+
|
|
56
|
+
1. Clone and enter the repo.
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/FreeTAKTeam/Reticulum-Community-Hub.git
|
|
59
|
+
cd Reticulum-Community-Hub
|
|
60
|
+
```
|
|
61
|
+
2. Create and activate a virtual environment.
|
|
62
|
+
```bash
|
|
63
|
+
python -m venv .venv
|
|
64
|
+
# Linux/macOS
|
|
65
|
+
source .venv/bin/activate
|
|
66
|
+
# Windows (PowerShell)
|
|
67
|
+
.venv\Scripts\Activate.ps1
|
|
68
|
+
```
|
|
69
|
+
3. Install dependencies.
|
|
70
|
+
```bash
|
|
71
|
+
python -m pip install --upgrade pip
|
|
72
|
+
python -m pip install -e .
|
|
73
|
+
```
|
|
74
|
+
4. Prepare a storage directory and config.
|
|
75
|
+
- Copy `RCH_Store/config.ini` into your storage directory.
|
|
76
|
+
- Adjust paths in the `[hub]`, `[files]`, and `[images]` sections.
|
|
77
|
+
5. Start the hub.
|
|
78
|
+
```bash
|
|
79
|
+
python -m reticulum_telemetry_hub.reticulum_server \
|
|
80
|
+
--storage_dir ./RCH_Store \
|
|
81
|
+
--display_name "RCH"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For configuration, services, and client usage details, see `docs/userManual.md`.
|
|
85
|
+
|
|
86
|
+
## Install from PyPI
|
|
87
|
+
|
|
88
|
+
1. Create and activate a virtual environment.
|
|
89
|
+
```bash
|
|
90
|
+
python -m venv .venv
|
|
91
|
+
# Linux/macOS
|
|
92
|
+
source .venv/bin/activate
|
|
93
|
+
# Windows (PowerShell)
|
|
94
|
+
.venv\Scripts\Activate.ps1
|
|
95
|
+
```
|
|
96
|
+
2. Install the package.
|
|
97
|
+
```bash
|
|
98
|
+
python -m pip install --upgrade pip
|
|
99
|
+
python -m pip install ReticulumCommunityHub
|
|
100
|
+
```
|
|
101
|
+
3. Start the hub (point at your storage directory).
|
|
102
|
+
```bash
|
|
103
|
+
python -m reticulum_telemetry_hub.reticulum_server --storage_dir /path/to/RCH_Store
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Northbound API and admin UI
|
|
107
|
+
|
|
108
|
+
The northbound FastAPI service exposes REST + WebSocket endpoints used by the admin UI.
|
|
109
|
+
|
|
110
|
+
- Run the hub + API together (recommended for chat/message sending):
|
|
111
|
+
```bash
|
|
112
|
+
python -m reticulum_telemetry_hub.northbound.gateway \
|
|
113
|
+
--storage_dir ./RCH_Store \
|
|
114
|
+
--api-host 0.0.0.0 \
|
|
115
|
+
--api-port 8000
|
|
116
|
+
```
|
|
117
|
+
- Run only the API server (read-only unless you provide a message dispatcher):
|
|
118
|
+
```bash
|
|
119
|
+
uvicorn reticulum_telemetry_hub.northbound.app:app --host 0.0.0.0 --port 8000
|
|
120
|
+
```
|
|
121
|
+
- Protect admin endpoints by setting `RCH_API_KEY` (accepts `X-API-Key` or Bearer token).
|
|
122
|
+
- The UI lives in `ui/`:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cd ui
|
|
126
|
+
npm install
|
|
127
|
+
npm run dev
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Set `VITE_RCH_BASE_URL` when the UI should target a different hub.
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
- `docs/README.md` (documentation map)
|
|
135
|
+
- `docs/userManual.md` (user and operator guide)
|
|
136
|
+
- `architecture.md` (system overview and references)
|
|
137
|
+
- `API/ReticulumCommunityHub-OAS.yaml` (REST/OpenAPI reference)
|
|
138
|
+
|
|
139
|
+
## Contributing
|
|
140
|
+
|
|
141
|
+
We welcome and encourage contributions. Please include appropriate tests and follow the
|
|
142
|
+
project coding standards.
|
|
143
|
+
|
|
144
|
+
### Linting
|
|
145
|
+
|
|
146
|
+
RCH uses Ruff for linting with a 120-character line length and ignores `E203` to align
|
|
147
|
+
with Black-style slicing.
|
|
148
|
+
|
|
149
|
+
- With Poetry (installs dev dependencies, including Ruff):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
poetry install --with dev
|
|
153
|
+
poetry run ruff check .
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- With a plain virtual environment:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
python -m pip install ruff
|
|
160
|
+
ruff check .
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
This project is licensed under the Eclipse Public License (EPL). For more details, refer to the
|
|
166
|
+
`LICENSE` file in the repository.
|
|
167
|
+
|
|
168
|
+
## Support
|
|
169
|
+
|
|
170
|
+
For issues or support, open a GitHub issue
|
|
171
|
+
|
|
172
|
+
## Support Reticulum
|
|
173
|
+
|
|
174
|
+
You can help support the continued development of open, free and private communications systems
|
|
175
|
+
by donating via one of the following channels to Mark, the original Reticulum author:
|
|
176
|
+
|
|
177
|
+
- Monero: 84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
|
178
|
+
- Ethereum: 0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73
|
|
179
|
+
- Bitcoin: 35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH
|
|
180
|
+
- Ko-Fi: https://ko-fi.com/markqvist
|
|
181
|
+
|