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
|
@@ -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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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)
|