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
@@ -1,32 +1,36 @@
1
- from datetime import datetime
2
- from typing import Optional
3
-
4
- from sqlalchemy import ForeignKey, DateTime
5
- from sqlalchemy.orm import Mapped, mapped_column
6
-
7
- from lxmf_telemetry.model.persistance.sensors.sensor_enum import SID_TIME
8
- from lxmf_telemetry.model.persistance.sensors.sensor import Sensor
9
-
10
- class Time(Sensor):
11
- __tablename__ = 'Time'
12
-
13
- id: Mapped[int] = mapped_column(ForeignKey('Sensor.id'), primary_key=True)
14
- utc: Mapped[datetime] = mapped_column(DateTime)
15
-
16
- def __init__(self, utc: Optional[datetime] = None):
17
- super().__init__(stale_time=15)
18
- self.utc = utc or datetime.now()
19
-
20
- def pack(self):
21
- return self.utc.timestamp()
22
-
23
- def unpack(self, packed):
24
- if packed is None:
25
- return None
26
- else:
27
- self.utc = datetime.fromtimestamp(packed)
28
-
29
- __mapper_args__ = {
30
- 'polymorphic_identity': SID_TIME,
31
- 'with_polymorphic': '*'
32
- }
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from sqlalchemy import ForeignKey, DateTime
5
+ from sqlalchemy.orm import Mapped, mapped_column
6
+
7
+ from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.sensors.sensor_enum import (
8
+ SID_TIME,
9
+ )
10
+ from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.sensors.sensor import (
11
+ Sensor,
12
+ )
13
+
14
+
15
+ class Time(Sensor):
16
+ __tablename__ = "Time"
17
+
18
+ id: Mapped[int] = mapped_column(ForeignKey("Sensor.id"), primary_key=True)
19
+ utc: Mapped[datetime] = mapped_column(DateTime)
20
+
21
+ def __init__(self, utc: Optional[datetime] = None):
22
+ super().__init__(stale_time=15)
23
+ self.sid = SID_TIME
24
+ self.utc = utc or datetime.now()
25
+
26
+ def pack(self):
27
+ return self.utc.timestamp()
28
+
29
+ def unpack(self, packed):
30
+ if packed is None:
31
+ return None
32
+ else:
33
+ self.utc = datetime.fromtimestamp(packed)
34
+ return {"timestamp": float(packed), "iso": self.utc.isoformat()}
35
+
36
+ __mapper_args__ = {"polymorphic_identity": SID_TIME, "with_polymorphic": "*"}
@@ -1,23 +1,26 @@
1
- from typing import TYPE_CHECKING, Optional
2
- from . import Base
3
- from sqlalchemy import Column, Integer, DateTime, String, ForeignKey
4
- from sqlalchemy.orm import relationship, Mapped, mapped_column
5
- from datetime import datetime
6
- from msgpack import packb, unpackb
7
-
8
- if TYPE_CHECKING:
9
- from .sensors.sensor import Sensor
10
-
11
- class Telemeter(Base):
12
- __tablename__ = "Telemeter"
13
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
14
- time: Mapped[datetime] = mapped_column(DateTime, nullable=False)
15
-
16
- sensors: Mapped[list["Sensor"]] = relationship("Sensor", back_populates="telemeter")
17
-
18
- peer_dest: Mapped[str] = mapped_column(String, nullable=False) # mapped_column(ForeignKey("Peer.destination_hash"))
19
- #peer = relationship("Peer", back_populates="telemeters")
20
-
21
- def __init__(self, peer_dest: str, time: Optional[datetime] = None):
22
- self.peer_dest = peer_dest
23
- self.time = time or datetime.now()
1
+ from typing import TYPE_CHECKING, Optional
2
+ from . import Base
3
+ from sqlalchemy import Column, Integer, DateTime, String, ForeignKey
4
+ from sqlalchemy.orm import relationship, Mapped, mapped_column
5
+ from datetime import datetime
6
+ from msgpack import packb, unpackb
7
+
8
+ if TYPE_CHECKING:
9
+ from .sensors.sensor import Sensor
10
+
11
+
12
+ class Telemeter(Base):
13
+ __tablename__ = "Telemeter"
14
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
15
+ time: Mapped[datetime] = mapped_column(DateTime, nullable=False)
16
+
17
+ sensors: Mapped[list["Sensor"]] = relationship("Sensor", back_populates="telemeter")
18
+
19
+ peer_dest: Mapped[str] = mapped_column(
20
+ String, nullable=False
21
+ ) # mapped_column(ForeignKey("Peer.destination_hash"))
22
+ # peer = relationship("Peer", back_populates="telemeters")
23
+
24
+ def __init__(self, peer_dest: str, time: Optional[datetime] = None):
25
+ self.peer_dest = peer_dest
26
+ self.time = time or datetime.now()
@@ -0,0 +1,229 @@
1
+ """Telemetry sampling helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from typing import Callable, Protocol, Sequence
9
+
10
+ import LXMF
11
+ import RNS
12
+ from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.sensors.sensor_enum import (
13
+ SID_TIME,
14
+ )
15
+ from reticulum_telemetry_hub.lxmf_telemetry.telemeter_manager import TelemeterManager
16
+ from reticulum_telemetry_hub.lxmf_telemetry.telemetry_controller import (
17
+ TelemetryController,
18
+ )
19
+
20
+
21
+ class TelemetryCollector(Protocol):
22
+ """Protocol describing callables that return telemetry payloads."""
23
+
24
+ def __call__(self) -> "TelemetrySample | dict | None": ...
25
+
26
+
27
+ @dataclass
28
+ class TelemetrySample:
29
+ """Container describing telemetry payloads gathered by the sampler."""
30
+
31
+ payload: dict
32
+ peer_dest: str | None = None
33
+
34
+
35
+ @dataclass
36
+ class _SamplerJob:
37
+ name: str
38
+ interval: float
39
+ collectors: Sequence[TelemetryCollector | Callable[[], object]]
40
+ last_run: float = field(default_factory=time.monotonic)
41
+
42
+
43
+ class TelemetrySampler:
44
+ """Background worker that periodically emits telemetry snapshots."""
45
+
46
+ def __init__(
47
+ self,
48
+ controller: TelemetryController,
49
+ router: LXMF.LXMRouter,
50
+ source_destination: RNS.Destination,
51
+ *,
52
+ connections: dict[bytes, RNS.Destination] | None = None,
53
+ hub_interval: float | None = None,
54
+ service_interval: float | None = None,
55
+ hub_collectors: (
56
+ Sequence[TelemetryCollector | Callable[[], object]] | None
57
+ ) = None,
58
+ service_collectors: (
59
+ Sequence[TelemetryCollector | Callable[[], object]] | None
60
+ ) = None,
61
+ telemeter_manager: TelemeterManager | None = None,
62
+ broadcast_updates: bool = False,
63
+ ) -> None:
64
+ self._controller = controller
65
+ self._router = router
66
+ self._source_destination = source_destination
67
+ self._connections = connections if connections is not None else {}
68
+ self._broadcast_updates = broadcast_updates
69
+ self._stop_event = threading.Event()
70
+ self._thread: threading.Thread | None = None
71
+ self._jobs: list[_SamplerJob] = []
72
+ self._local_peer_dest = (
73
+ RNS.hexrep(source_destination.hash, False)
74
+ if hasattr(source_destination, "hash")
75
+ else ""
76
+ )
77
+
78
+ self._telemeter_manager = telemeter_manager
79
+
80
+ if hub_interval is not None and hub_interval > 0:
81
+ collectors = list(hub_collectors) if hub_collectors is not None else []
82
+ if not collectors:
83
+ collectors = [self._collect_telemeter_snapshot]
84
+ if collectors:
85
+ interval = float(hub_interval)
86
+ self._jobs.append(
87
+ _SamplerJob(
88
+ "hub",
89
+ interval,
90
+ collectors,
91
+ time.monotonic() - interval,
92
+ )
93
+ )
94
+
95
+ if service_interval is not None and service_interval > 0:
96
+ collectors = (
97
+ list(service_collectors) if service_collectors is not None else []
98
+ )
99
+ if collectors:
100
+ interval = float(service_interval)
101
+ self._jobs.append(
102
+ _SamplerJob(
103
+ "service",
104
+ interval,
105
+ collectors,
106
+ time.monotonic() - interval,
107
+ )
108
+ )
109
+
110
+ # ------------------------------------------------------------------
111
+ # lifecycle helpers
112
+ # ------------------------------------------------------------------
113
+ def start(self) -> None:
114
+ if not self._jobs or self._thread is not None:
115
+ return
116
+ self._thread = threading.Thread(target=self._run, daemon=True)
117
+ self._thread.start()
118
+
119
+ def stop(self) -> None:
120
+ if self._thread is None:
121
+ return
122
+ self._stop_event.set()
123
+ self._thread.join()
124
+ self._thread = None
125
+ self._stop_event.clear()
126
+
127
+ # ------------------------------------------------------------------
128
+ # sampler internals
129
+ # ------------------------------------------------------------------
130
+ def _run(self) -> None:
131
+ while not self._stop_event.is_set():
132
+ now = time.monotonic()
133
+ next_wake = None
134
+ for job in self._jobs:
135
+ remaining = job.interval - (now - job.last_run)
136
+ if remaining <= 0:
137
+ self._execute_job(job)
138
+ job.last_run = time.monotonic()
139
+ remaining = job.interval
140
+ next_wake = (
141
+ remaining if next_wake is None else min(next_wake, remaining)
142
+ )
143
+ if next_wake is None:
144
+ break
145
+ self._stop_event.wait(next_wake)
146
+
147
+ def _execute_job(self, job: _SamplerJob) -> None:
148
+ for collector in job.collectors:
149
+ sample = self._invoke_collector(collector)
150
+ if sample is None:
151
+ continue
152
+ self._process_sample(sample)
153
+
154
+ def _invoke_collector(
155
+ self, collector: TelemetryCollector | Callable[[], object]
156
+ ) -> TelemetrySample | None:
157
+ try:
158
+ result = collector()
159
+ except Exception as exc: # pragma: no cover - defensive logging
160
+ RNS.log(f"Telemetry collector {collector!r} failed: {exc}", RNS.LOG_ERROR)
161
+ return None
162
+
163
+ if result is None:
164
+ return None
165
+
166
+ if isinstance(result, TelemetrySample):
167
+ return result
168
+
169
+ if isinstance(result, dict):
170
+ return TelemetrySample(result)
171
+
172
+ raise TypeError(
173
+ "Telemetry collectors must return a dict or TelemetrySample; "
174
+ f"received {type(result)!r}"
175
+ )
176
+
177
+ def _process_sample(self, sample: TelemetrySample) -> None:
178
+ peer_dest = sample.peer_dest or self._local_peer_dest
179
+ encoded = self._controller.ingest_local_payload(
180
+ sample.payload, peer_dest=peer_dest
181
+ )
182
+ if not encoded:
183
+ return
184
+
185
+ if not self._broadcast_updates:
186
+ return
187
+
188
+ destinations: Sequence[RNS.Destination]
189
+ if hasattr(self._connections, "values"):
190
+ destinations = list(self._connections.values())
191
+ else:
192
+ destinations = list(self._connections)
193
+
194
+ if not destinations:
195
+ return
196
+
197
+ for destination in destinations:
198
+ try:
199
+ message = LXMF.LXMessage(
200
+ destination,
201
+ self._source_destination,
202
+ fields={LXMF.FIELD_TELEMETRY: encoded},
203
+ desired_method=LXMF.LXMessage.DIRECT,
204
+ )
205
+ if hasattr(destination, "identity") and hasattr(
206
+ destination.identity, "hash"
207
+ ):
208
+ message.destination_hash = destination.identity.hash
209
+ self._router.handle_outbound(message)
210
+ except Exception as exc: # pragma: no cover - defensive logging
211
+ RNS.log(
212
+ f"Failed to deliver telemetry sample to {destination}: {exc}",
213
+ RNS.LOG_ERROR,
214
+ )
215
+
216
+ # ------------------------------------------------------------------
217
+ # built-in collectors
218
+ # ------------------------------------------------------------------
219
+ def _collect_time_sensor(self) -> TelemetrySample:
220
+ payload = {SID_TIME: time.time()}
221
+ return TelemetrySample(payload, self._local_peer_dest)
222
+
223
+ def _collect_telemeter_snapshot(self) -> TelemetrySample:
224
+ if self._telemeter_manager is None:
225
+ return self._collect_time_sensor()
226
+ payload = self._telemeter_manager.snapshot()
227
+ if SID_TIME not in payload:
228
+ payload[SID_TIME] = time.time()
229
+ return TelemetrySample(payload, self._local_peer_dest)