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.
Files changed (108) hide show
  1. reticulum_telemetry_hub/api/__init__.py +23 -0
  2. reticulum_telemetry_hub/api/models.py +323 -0
  3. reticulum_telemetry_hub/api/service.py +836 -0
  4. reticulum_telemetry_hub/api/storage.py +528 -0
  5. reticulum_telemetry_hub/api/storage_base.py +156 -0
  6. reticulum_telemetry_hub/api/storage_models.py +118 -0
  7. reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
  8. reticulum_telemetry_hub/atak_cot/base.py +277 -0
  9. reticulum_telemetry_hub/atak_cot/chat.py +506 -0
  10. reticulum_telemetry_hub/atak_cot/detail.py +235 -0
  11. reticulum_telemetry_hub/atak_cot/event.py +181 -0
  12. reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
  13. reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
  14. reticulum_telemetry_hub/config/__init__.py +25 -0
  15. reticulum_telemetry_hub/config/constants.py +7 -0
  16. reticulum_telemetry_hub/config/manager.py +515 -0
  17. reticulum_telemetry_hub/config/models.py +215 -0
  18. reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
  19. reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
  20. reticulum_telemetry_hub/internal_api/__init__.py +21 -0
  21. reticulum_telemetry_hub/internal_api/bus.py +344 -0
  22. reticulum_telemetry_hub/internal_api/core.py +690 -0
  23. reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
  24. reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
  25. reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
  26. reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
  27. reticulum_telemetry_hub/internal_api/versioning.py +63 -0
  28. reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
  29. reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
  30. reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
  31. reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
  32. reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
  33. reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
  34. reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
  35. reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
  36. reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
  37. reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
  38. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
  39. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
  40. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
  41. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
  42. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
  43. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
  44. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
  45. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
  46. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
  47. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
  48. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
  49. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
  50. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
  51. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
  52. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
  53. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
  54. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
  55. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
  56. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
  57. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
  58. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
  59. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
  60. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
  61. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
  62. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
  63. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
  64. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
  65. reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
  66. reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
  67. reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
  68. reticulum_telemetry_hub/northbound/__init__.py +5 -0
  69. reticulum_telemetry_hub/northbound/app.py +195 -0
  70. reticulum_telemetry_hub/northbound/auth.py +119 -0
  71. reticulum_telemetry_hub/northbound/gateway.py +310 -0
  72. reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
  73. reticulum_telemetry_hub/northbound/models.py +213 -0
  74. reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
  75. reticulum_telemetry_hub/northbound/routes_files.py +119 -0
  76. reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
  77. reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
  78. reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
  79. reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
  80. reticulum_telemetry_hub/northbound/serializers.py +72 -0
  81. reticulum_telemetry_hub/northbound/services.py +373 -0
  82. reticulum_telemetry_hub/northbound/websocket.py +855 -0
  83. reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
  84. reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
  85. reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
  86. reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
  87. reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
  88. reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
  89. reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
  90. reticulum_telemetry_hub/reticulum_server/services.py +422 -0
  91. reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
  92. reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
  93. {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
  94. reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
  95. lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
  96. lxmf_telemetry/model/persistance/__init__.py +0 -3
  97. lxmf_telemetry/model/persistance/sensors/location.py +0 -69
  98. lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
  99. lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
  100. lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
  101. lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
  102. lxmf_telemetry/telemetry_controller.py +0 -124
  103. reticulum_server/main.py +0 -182
  104. reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
  105. reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
  106. {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
  107. {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
  108. {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
+ ![Dashboard](image.png)
52
+ ![Map](image-1.png)
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
+