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,195 @@
|
|
|
1
|
+
"""FastAPI application for the northbound interface."""
|
|
2
|
+
# pylint: disable=import-error
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from datetime import timezone
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from typing import Callable
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from dotenv import load_dotenv as load_env
|
|
15
|
+
from fastapi import FastAPI
|
|
16
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
17
|
+
|
|
18
|
+
from reticulum_telemetry_hub.api.models import ChatMessage
|
|
19
|
+
from reticulum_telemetry_hub.api.service import ReticulumTelemetryHubAPI
|
|
20
|
+
from reticulum_telemetry_hub.config.manager import HubConfigurationManager
|
|
21
|
+
from reticulum_telemetry_hub.lxmf_telemetry.telemetry_controller import (
|
|
22
|
+
TelemetryController,
|
|
23
|
+
)
|
|
24
|
+
from reticulum_telemetry_hub.reticulum_server.event_log import EventLog
|
|
25
|
+
from reticulum_telemetry_hub.reticulum_server.event_log import resolve_event_log_path
|
|
26
|
+
|
|
27
|
+
from .auth import ApiAuth
|
|
28
|
+
from .auth import build_protected_dependency
|
|
29
|
+
from .routes_files import register_file_routes
|
|
30
|
+
from .routes_chat import register_chat_routes
|
|
31
|
+
from .routes_rest import register_core_routes
|
|
32
|
+
from .routes_subscribers import register_subscriber_routes
|
|
33
|
+
from .routes_topics import register_topic_routes
|
|
34
|
+
from .routes_ws import register_ws_routes
|
|
35
|
+
from .internal_adapter import build_internal_adapter
|
|
36
|
+
from .internal_adapter import InternalAdapter
|
|
37
|
+
from .internal_adapter import register_internal_adapter
|
|
38
|
+
from .services import NorthboundServices
|
|
39
|
+
from .websocket import EventBroadcaster
|
|
40
|
+
from .websocket import MessageBroadcaster
|
|
41
|
+
from .websocket import TelemetryBroadcaster
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_openapi_spec() -> Optional[Path]:
|
|
45
|
+
"""Return the OpenAPI YAML path when available.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Optional[Path]: Path to the OpenAPI YAML file when present.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
52
|
+
spec_path = repo_root / "API" / "ReticulumCommunityHub-OAS.yaml"
|
|
53
|
+
if spec_path.exists():
|
|
54
|
+
return spec_path
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_storage_path() -> Path:
|
|
59
|
+
"""Return the storage path from environment defaults."""
|
|
60
|
+
|
|
61
|
+
storage_dir = os.environ.get("RTH_STORAGE_DIR")
|
|
62
|
+
if storage_dir:
|
|
63
|
+
return Path(storage_dir).expanduser().resolve()
|
|
64
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
65
|
+
repo_storage = repo_root / "RTH_Store"
|
|
66
|
+
if repo_storage.exists():
|
|
67
|
+
return repo_storage
|
|
68
|
+
return HubConfigurationManager().storage_path.resolve()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_app(
|
|
72
|
+
*,
|
|
73
|
+
api: Optional[ReticulumTelemetryHubAPI] = None,
|
|
74
|
+
telemetry_controller: Optional[TelemetryController] = None,
|
|
75
|
+
event_log: Optional[EventLog] = None,
|
|
76
|
+
command_manager: Optional[Any] = None,
|
|
77
|
+
routing_provider: Optional[Callable[[], list[str]]] = None,
|
|
78
|
+
started_at: Optional[datetime] = None,
|
|
79
|
+
auth: Optional[ApiAuth] = None,
|
|
80
|
+
message_dispatcher: Optional[
|
|
81
|
+
Callable[[str, Optional[str], Optional[str], Optional[dict]], ChatMessage | None]
|
|
82
|
+
] = None,
|
|
83
|
+
message_listener: Optional[
|
|
84
|
+
Callable[[Callable[[dict[str, object]], None]], Callable[[], None]]
|
|
85
|
+
] = None,
|
|
86
|
+
internal_adapter: Optional[InternalAdapter] = None,
|
|
87
|
+
) -> FastAPI:
|
|
88
|
+
"""Create the northbound FastAPI application.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
api (Optional[ReticulumTelemetryHubAPI]): API service instance.
|
|
92
|
+
telemetry_controller (Optional[TelemetryController]): Telemetry controller instance.
|
|
93
|
+
event_log (Optional[EventLog]): Event log instance.
|
|
94
|
+
command_manager (Optional[Any]): Command manager for help/examples text.
|
|
95
|
+
routing_provider (Optional[Callable[[], list[str]]]): Provider for routing destinations.
|
|
96
|
+
started_at (Optional[datetime]): Start time for uptime calculations.
|
|
97
|
+
auth (Optional[ApiAuth]): Auth validator.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
FastAPI: Configured FastAPI application.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
load_env()
|
|
104
|
+
config_manager = None
|
|
105
|
+
storage_path = None
|
|
106
|
+
if api is None:
|
|
107
|
+
storage_path = _resolve_storage_path()
|
|
108
|
+
config_manager = HubConfigurationManager(storage_path=storage_path)
|
|
109
|
+
api = ReticulumTelemetryHubAPI(config_manager=config_manager)
|
|
110
|
+
else:
|
|
111
|
+
config_manager = getattr(api, "_config_manager", None)
|
|
112
|
+
storage_path = getattr(config_manager, "storage_path", None)
|
|
113
|
+
|
|
114
|
+
if storage_path is None:
|
|
115
|
+
storage_path = _resolve_storage_path()
|
|
116
|
+
|
|
117
|
+
if event_log is None:
|
|
118
|
+
event_log_path = resolve_event_log_path(storage_path)
|
|
119
|
+
event_log = EventLog(event_path=event_log_path, tail=True)
|
|
120
|
+
|
|
121
|
+
if telemetry_controller is None:
|
|
122
|
+
telemetry_db_path = storage_path / "telemetry.db"
|
|
123
|
+
telemetry_controller = TelemetryController(
|
|
124
|
+
api=api,
|
|
125
|
+
event_log=event_log,
|
|
126
|
+
db_path=telemetry_db_path,
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
telemetry_controller.set_event_log(event_log)
|
|
130
|
+
services = NorthboundServices(
|
|
131
|
+
api=api,
|
|
132
|
+
telemetry=telemetry_controller,
|
|
133
|
+
event_log=event_log,
|
|
134
|
+
started_at=started_at or datetime.now(timezone.utc),
|
|
135
|
+
command_manager=command_manager,
|
|
136
|
+
routing_provider=routing_provider,
|
|
137
|
+
message_dispatcher=message_dispatcher,
|
|
138
|
+
)
|
|
139
|
+
auth = auth or ApiAuth()
|
|
140
|
+
require_protected = build_protected_dependency(auth)
|
|
141
|
+
|
|
142
|
+
app = FastAPI(title="ReticulumCommunityHub", version="northbound")
|
|
143
|
+
app.add_middleware(
|
|
144
|
+
CORSMiddleware,
|
|
145
|
+
allow_origins=["*"],
|
|
146
|
+
allow_credentials=False,
|
|
147
|
+
allow_methods=["*"],
|
|
148
|
+
allow_headers=["*"],
|
|
149
|
+
max_age=86400,
|
|
150
|
+
)
|
|
151
|
+
event_broadcaster = EventBroadcaster(event_log)
|
|
152
|
+
telemetry_broadcaster = TelemetryBroadcaster(telemetry_controller, api)
|
|
153
|
+
message_broadcaster = MessageBroadcaster(message_listener)
|
|
154
|
+
|
|
155
|
+
register_core_routes(
|
|
156
|
+
app,
|
|
157
|
+
services=services,
|
|
158
|
+
api=api,
|
|
159
|
+
telemetry_controller=telemetry_controller,
|
|
160
|
+
require_protected=require_protected,
|
|
161
|
+
resolve_openapi_spec=_resolve_openapi_spec,
|
|
162
|
+
)
|
|
163
|
+
register_file_routes(app, services=services, api=api)
|
|
164
|
+
register_chat_routes(
|
|
165
|
+
app,
|
|
166
|
+
services=services,
|
|
167
|
+
require_protected=require_protected,
|
|
168
|
+
)
|
|
169
|
+
register_topic_routes(
|
|
170
|
+
app,
|
|
171
|
+
services=services,
|
|
172
|
+
api=api,
|
|
173
|
+
require_protected=require_protected,
|
|
174
|
+
)
|
|
175
|
+
register_subscriber_routes(
|
|
176
|
+
app,
|
|
177
|
+
services=services,
|
|
178
|
+
api=api,
|
|
179
|
+
require_protected=require_protected,
|
|
180
|
+
)
|
|
181
|
+
register_ws_routes(
|
|
182
|
+
app,
|
|
183
|
+
services=services,
|
|
184
|
+
auth=auth,
|
|
185
|
+
event_broadcaster=event_broadcaster,
|
|
186
|
+
telemetry_broadcaster=telemetry_broadcaster,
|
|
187
|
+
message_broadcaster=message_broadcaster,
|
|
188
|
+
)
|
|
189
|
+
adapter = internal_adapter or build_internal_adapter()
|
|
190
|
+
register_internal_adapter(app, adapter=adapter)
|
|
191
|
+
|
|
192
|
+
return app
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
app = create_app()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Authentication helpers for the northbound API."""
|
|
2
|
+
# pylint: disable=import-error
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from dotenv import load_dotenv as load_env
|
|
10
|
+
from fastapi import Header
|
|
11
|
+
from fastapi import HTTPException
|
|
12
|
+
from fastapi import status
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApiAuth:
|
|
16
|
+
"""Validate API key or bearer token credentials."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, api_key: Optional[str] = None) -> None:
|
|
19
|
+
"""Initialize the auth validator.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
api_key (Optional[str]): API key override. When omitted, the
|
|
23
|
+
validator reads ``RTH_API_KEY`` from the environment.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
load_env()
|
|
27
|
+
self._api_key = api_key
|
|
28
|
+
|
|
29
|
+
def is_enabled(self) -> bool:
|
|
30
|
+
"""Return ``True`` when an API key is configured.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
bool: ``True`` when protected endpoints require credentials.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
return bool(self._api_key or self._env_api_key())
|
|
37
|
+
|
|
38
|
+
def validate_credentials(
|
|
39
|
+
self, api_key: Optional[str], token: Optional[str]
|
|
40
|
+
) -> bool:
|
|
41
|
+
"""Validate provided credentials.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
api_key (Optional[str]): API key header value.
|
|
45
|
+
token (Optional[str]): Bearer token value.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
bool: ``True`` when credentials are valid or auth is disabled.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
expected = self._api_key or self._env_api_key()
|
|
52
|
+
if not expected:
|
|
53
|
+
return True
|
|
54
|
+
return api_key == expected or token == expected
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _env_api_key() -> Optional[str]:
|
|
58
|
+
"""Return the configured API key from the environment.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Optional[str]: API key string if defined.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
return os.environ.get("RTH_API_KEY")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_bearer_token(authorization: Optional[str]) -> Optional[str]:
|
|
68
|
+
"""Extract a bearer token from an authorization header.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
authorization (Optional[str]): Authorization header value.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Optional[str]: Parsed bearer token, if present.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
if not authorization:
|
|
78
|
+
return None
|
|
79
|
+
parts = authorization.split()
|
|
80
|
+
if len(parts) != 2:
|
|
81
|
+
return None
|
|
82
|
+
if parts[0].lower() != "bearer":
|
|
83
|
+
return None
|
|
84
|
+
return parts[1]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_protected_dependency(auth: ApiAuth):
|
|
88
|
+
"""Return a dependency that enforces protected access.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
auth (ApiAuth): Auth validator instance.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Callable: Dependency function for FastAPI routes.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
async def _require_protected(
|
|
98
|
+
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
|
|
99
|
+
authorization: Optional[str] = Header(default=None, alias="Authorization"),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Validate protected endpoint credentials.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
x_api_key (Optional[str]): API key header value.
|
|
105
|
+
authorization (Optional[str]): Authorization header value.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
None: This dependency raises on invalid credentials.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
token = _parse_bearer_token(authorization)
|
|
112
|
+
if auth.validate_credentials(x_api_key, token):
|
|
113
|
+
return
|
|
114
|
+
raise HTTPException(
|
|
115
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
116
|
+
detail="Unauthorized",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return _require_protected
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Run the Reticulum hub and northbound API in a single process."""
|
|
2
|
+
# pylint: disable=import-error
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import threading
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from datetime import timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from typing import Protocol
|
|
15
|
+
|
|
16
|
+
import RNS
|
|
17
|
+
import uvicorn
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
|
|
20
|
+
from reticulum_telemetry_hub.config.constants import DEFAULT_ANNOUNCE_INTERVAL
|
|
21
|
+
from reticulum_telemetry_hub.config.constants import DEFAULT_HUB_TELEMETRY_INTERVAL
|
|
22
|
+
from reticulum_telemetry_hub.config.constants import DEFAULT_LOG_LEVEL_NAME
|
|
23
|
+
from reticulum_telemetry_hub.config.constants import DEFAULT_SERVICE_TELEMETRY_INTERVAL
|
|
24
|
+
from reticulum_telemetry_hub.config.constants import DEFAULT_STORAGE_PATH
|
|
25
|
+
from reticulum_telemetry_hub.config.manager import HubConfigurationManager
|
|
26
|
+
from reticulum_telemetry_hub.config.manager import _expand_user_path
|
|
27
|
+
from reticulum_telemetry_hub.northbound.app import create_app
|
|
28
|
+
from reticulum_telemetry_hub.northbound.auth import ApiAuth
|
|
29
|
+
from reticulum_telemetry_hub.reticulum_server.__main__ import ReticulumTelemetryHub
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GatewayHub(Protocol):
|
|
33
|
+
"""Protocol for hub dependencies consumed by the gateway app."""
|
|
34
|
+
|
|
35
|
+
api: object
|
|
36
|
+
tel_controller: object
|
|
37
|
+
event_log: object
|
|
38
|
+
command_manager: Optional[object]
|
|
39
|
+
|
|
40
|
+
def dispatch_northbound_message(
|
|
41
|
+
self,
|
|
42
|
+
message: str,
|
|
43
|
+
topic_id: Optional[str] = None,
|
|
44
|
+
destination: Optional[str] = None,
|
|
45
|
+
fields: Optional[dict] = None,
|
|
46
|
+
) -> object:
|
|
47
|
+
"""Send a northbound message through the hub."""
|
|
48
|
+
|
|
49
|
+
def register_message_listener(
|
|
50
|
+
self, listener: Callable[[dict[str, object]], None]
|
|
51
|
+
) -> Callable[[], None]:
|
|
52
|
+
"""Register an inbound message listener."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class GatewayConfig:
|
|
57
|
+
"""Configuration bundle for the gateway runner."""
|
|
58
|
+
|
|
59
|
+
storage_path: Path
|
|
60
|
+
identity_path: Path
|
|
61
|
+
config_path: Path
|
|
62
|
+
display_name: str
|
|
63
|
+
announce_interval: int
|
|
64
|
+
hub_telemetry_interval: int
|
|
65
|
+
service_telemetry_interval: int
|
|
66
|
+
loglevel: int
|
|
67
|
+
embedded: bool
|
|
68
|
+
daemon_mode: bool
|
|
69
|
+
services: list[str]
|
|
70
|
+
api_host: str
|
|
71
|
+
api_port: int
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _resolve_interval(value: int | None, fallback: int) -> int:
|
|
75
|
+
"""Return the positive interval derived from CLI/config values."""
|
|
76
|
+
|
|
77
|
+
if value is not None:
|
|
78
|
+
return max(0, int(value))
|
|
79
|
+
return max(0, int(fallback))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _build_log_levels() -> dict[str, int]:
|
|
83
|
+
"""Return the supported log level mapping for RNS."""
|
|
84
|
+
|
|
85
|
+
default_level = getattr(RNS, "LOG_DEBUG", getattr(RNS, "LOG_INFO", 3))
|
|
86
|
+
return {
|
|
87
|
+
"error": getattr(RNS, "LOG_ERROR", 1),
|
|
88
|
+
"warning": getattr(RNS, "LOG_WARNING", 2),
|
|
89
|
+
"info": getattr(RNS, "LOG_INFO", 3),
|
|
90
|
+
"debug": getattr(RNS, "LOG_DEBUG", default_level),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_args() -> argparse.Namespace:
|
|
95
|
+
"""Parse CLI arguments for the gateway runner."""
|
|
96
|
+
|
|
97
|
+
parser = argparse.ArgumentParser()
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"-c",
|
|
100
|
+
"--config",
|
|
101
|
+
dest="config_path",
|
|
102
|
+
help="Path to a unified config.ini file",
|
|
103
|
+
default=None,
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument("-s", "--storage_dir", help="Storage directory path", default=None)
|
|
106
|
+
parser.add_argument("--display_name", help="Display name for the server", default=None)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--announce-interval",
|
|
109
|
+
type=int,
|
|
110
|
+
default=None,
|
|
111
|
+
help="Seconds between announcement broadcasts",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--hub-telemetry-interval",
|
|
115
|
+
type=int,
|
|
116
|
+
default=None,
|
|
117
|
+
help="Seconds between local telemetry snapshots.",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--service-telemetry-interval",
|
|
121
|
+
type=int,
|
|
122
|
+
default=None,
|
|
123
|
+
help="Seconds between remote telemetry collector polls.",
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--log-level",
|
|
127
|
+
choices=list(_build_log_levels().keys()),
|
|
128
|
+
default=None,
|
|
129
|
+
help="Log level to emit RNS traffic to stdout",
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--embedded",
|
|
133
|
+
"--embedded-lxmd",
|
|
134
|
+
dest="embedded",
|
|
135
|
+
action=argparse.BooleanOptionalAction,
|
|
136
|
+
default=None,
|
|
137
|
+
help="Run the LXMF router/propagation threads in-process.",
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--daemon",
|
|
141
|
+
dest="daemon",
|
|
142
|
+
action="store_true",
|
|
143
|
+
help="Start local telemetry collectors and optional services.",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--service",
|
|
147
|
+
dest="services",
|
|
148
|
+
action="append",
|
|
149
|
+
default=[],
|
|
150
|
+
metavar="NAME",
|
|
151
|
+
help=(
|
|
152
|
+
"Enable an optional daemon service (e.g., gpsd). Repeat the flag for"
|
|
153
|
+
" multiple services."
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--api-host",
|
|
158
|
+
dest="api_host",
|
|
159
|
+
default="127.0.0.1",
|
|
160
|
+
help="Host address for the northbound API.",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--api-port",
|
|
164
|
+
dest="api_port",
|
|
165
|
+
type=int,
|
|
166
|
+
default=8000,
|
|
167
|
+
help="Port for the northbound API.",
|
|
168
|
+
)
|
|
169
|
+
return parser.parse_args()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_gateway_config(args: argparse.Namespace) -> GatewayConfig:
|
|
173
|
+
"""Build runtime configuration for the gateway runner."""
|
|
174
|
+
|
|
175
|
+
storage_path = _expand_user_path(args.storage_dir or DEFAULT_STORAGE_PATH)
|
|
176
|
+
identity_path = storage_path / "identity"
|
|
177
|
+
config_path = (
|
|
178
|
+
_expand_user_path(args.config_path)
|
|
179
|
+
if args.config_path
|
|
180
|
+
else storage_path / "config.ini"
|
|
181
|
+
)
|
|
182
|
+
config_manager = HubConfigurationManager(
|
|
183
|
+
storage_path=storage_path,
|
|
184
|
+
config_path=config_path,
|
|
185
|
+
)
|
|
186
|
+
runtime_config = config_manager.config.runtime
|
|
187
|
+
display_name = args.display_name or runtime_config.display_name
|
|
188
|
+
announce_interval = args.announce_interval or runtime_config.announce_interval
|
|
189
|
+
hub_interval = _resolve_interval(
|
|
190
|
+
args.hub_telemetry_interval,
|
|
191
|
+
runtime_config.hub_telemetry_interval or DEFAULT_HUB_TELEMETRY_INTERVAL,
|
|
192
|
+
)
|
|
193
|
+
service_interval = _resolve_interval(
|
|
194
|
+
args.service_telemetry_interval,
|
|
195
|
+
runtime_config.service_telemetry_interval or DEFAULT_SERVICE_TELEMETRY_INTERVAL,
|
|
196
|
+
)
|
|
197
|
+
log_level_name = (
|
|
198
|
+
args.log_level or runtime_config.log_level or DEFAULT_LOG_LEVEL_NAME
|
|
199
|
+
).lower()
|
|
200
|
+
log_levels = _build_log_levels()
|
|
201
|
+
loglevel = log_levels.get(log_level_name, log_levels["info"])
|
|
202
|
+
embedded = runtime_config.embedded_lxmd if args.embedded is None else args.embedded
|
|
203
|
+
requested_services = list(runtime_config.default_services)
|
|
204
|
+
requested_services.extend(args.services or [])
|
|
205
|
+
services = list(dict.fromkeys(requested_services))
|
|
206
|
+
return GatewayConfig(
|
|
207
|
+
storage_path=storage_path,
|
|
208
|
+
identity_path=identity_path,
|
|
209
|
+
config_path=config_path,
|
|
210
|
+
display_name=display_name,
|
|
211
|
+
announce_interval=announce_interval,
|
|
212
|
+
hub_telemetry_interval=hub_interval,
|
|
213
|
+
service_telemetry_interval=service_interval,
|
|
214
|
+
loglevel=loglevel,
|
|
215
|
+
embedded=embedded,
|
|
216
|
+
daemon_mode=bool(args.daemon),
|
|
217
|
+
services=services,
|
|
218
|
+
api_host=str(args.api_host),
|
|
219
|
+
api_port=int(args.api_port),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def build_gateway_app(
|
|
224
|
+
hub: GatewayHub,
|
|
225
|
+
*,
|
|
226
|
+
auth: Optional[ApiAuth] = None,
|
|
227
|
+
started_at: Optional[datetime] = None,
|
|
228
|
+
) -> FastAPI:
|
|
229
|
+
"""Create a northbound API app wired to the hub instance.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
hub (GatewayHub): Active hub instance used for dispatching messages.
|
|
233
|
+
auth (Optional[ApiAuth]): Auth override.
|
|
234
|
+
started_at (Optional[datetime]): Optional start time override.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
FastAPI: Configured FastAPI application.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
app = create_app(
|
|
241
|
+
api=hub.api,
|
|
242
|
+
telemetry_controller=hub.tel_controller,
|
|
243
|
+
event_log=hub.event_log,
|
|
244
|
+
command_manager=hub.command_manager,
|
|
245
|
+
message_dispatcher=hub.dispatch_northbound_message,
|
|
246
|
+
message_listener=hub.register_message_listener,
|
|
247
|
+
started_at=started_at or datetime.now(timezone.utc),
|
|
248
|
+
auth=auth,
|
|
249
|
+
)
|
|
250
|
+
app.state.hub = hub
|
|
251
|
+
return app
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _start_hub_thread(
|
|
255
|
+
hub: ReticulumTelemetryHub,
|
|
256
|
+
*,
|
|
257
|
+
daemon_mode: bool,
|
|
258
|
+
services: list[str],
|
|
259
|
+
) -> threading.Thread:
|
|
260
|
+
"""Start the hub run loop in a background thread."""
|
|
261
|
+
|
|
262
|
+
thread = threading.Thread(
|
|
263
|
+
target=hub.run,
|
|
264
|
+
kwargs={"daemon_mode": daemon_mode, "services": services},
|
|
265
|
+
daemon=True,
|
|
266
|
+
)
|
|
267
|
+
thread.start()
|
|
268
|
+
return thread
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main() -> None:
|
|
272
|
+
"""Start the hub + northbound API gateway."""
|
|
273
|
+
|
|
274
|
+
args = _parse_args()
|
|
275
|
+
config = _build_gateway_config(args)
|
|
276
|
+
config_manager = HubConfigurationManager(
|
|
277
|
+
storage_path=config.storage_path,
|
|
278
|
+
config_path=config.config_path,
|
|
279
|
+
)
|
|
280
|
+
hub = ReticulumTelemetryHub(
|
|
281
|
+
config.display_name,
|
|
282
|
+
config.storage_path,
|
|
283
|
+
config.identity_path,
|
|
284
|
+
embedded=config.embedded,
|
|
285
|
+
announce_interval=config.announce_interval,
|
|
286
|
+
loglevel=config.loglevel,
|
|
287
|
+
hub_telemetry_interval=config.hub_telemetry_interval,
|
|
288
|
+
service_telemetry_interval=config.service_telemetry_interval,
|
|
289
|
+
config_manager=config_manager,
|
|
290
|
+
)
|
|
291
|
+
hub_thread = _start_hub_thread(
|
|
292
|
+
hub,
|
|
293
|
+
daemon_mode=config.daemon_mode,
|
|
294
|
+
services=config.services,
|
|
295
|
+
)
|
|
296
|
+
app = build_gateway_app(hub)
|
|
297
|
+
try:
|
|
298
|
+
uvicorn.run(
|
|
299
|
+
app,
|
|
300
|
+
host=config.api_host,
|
|
301
|
+
port=config.api_port,
|
|
302
|
+
log_level="info",
|
|
303
|
+
)
|
|
304
|
+
finally:
|
|
305
|
+
hub.shutdown()
|
|
306
|
+
hub_thread.join(timeout=5)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
main()
|