Skip to content

Commit 646aade

Browse files
authored
Merge pull request #97 from FreeTAKTeam/reticulum_network
Reticulum network
2 parents 209a24c + 98dc748 commit 646aade

31 files changed

Lines changed: 654 additions & 2 deletions

Services.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ In your configuration ini file you will add the following section:
2020
```ini
2121
[application_name.service_id]
2222
__class = digitalpy.core.service_management.domain.model.service_configuration.ServiceConfiguration
23-
status = STOPPED
23+
status = [RUNNING|STOPPED]
2424
name = MyNewService
2525
port = 8443
2626
host = 0.0.0.0
27-
protocol = TCP
27+
protocol = [FlaskHTTPNetworkBlueprints|TCPNetwork|Reticulum]
2828
```
2929

3030
Next you will add or update the ServiceManagementConfiguration section as follows:

digitalpy/core/core_config.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ blueprint_import_base = digitalpy.blueprints
125125
; NETWORK OBJECTS
126126

127127
; the default tcp_network
128+
[Reticulum]
129+
__class = digitalpy.core.network.impl.network_reticulum.ReticulumNetwork
130+
client = DefaultClient
131+
128132
[TCPNetwork]
129133
__class = digitalpy.core.network.impl.network_tcp.TCPNetwork
130134
client = DefaultClient
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""This file contains the reticulum network implementation.
2+
This consits of two main classes
3+
1. reticulum manager which is responsible for running the reticulum network stack in a separate process and exposing to the network.
4+
2. reticulum network which is responsible for exposing the network interface to a service
5+
"""
6+
7+
import threading
8+
import RNS
9+
import LXMF
10+
import os
11+
import time
12+
import zmq
13+
from multiprocessing import Queue
14+
from typing import Callable
15+
from digitalpy.core.zmanager.response import Response
16+
from digitalpy.core.domain.object_id import ObjectId
17+
from digitalpy.core.network.domain.client_status import ClientStatus
18+
from digitalpy.core.domain.domain.network_client import NetworkClient
19+
from digitalpy.core.main.object_factory import ObjectFactory
20+
from digitalpy.core.network.network_sync_interface import NetworkSyncInterface
21+
from digitalpy.core.zmanager.request import Request
22+
23+
APP_NAME = LXMF.APP_NAME + ".delivery"
24+
25+
class AnnounceHandler:
26+
def __init__(self, identities):
27+
self.aspect_filter = APP_NAME # Filter for LXMF announcements
28+
self.identities = identities # Dictionary to store identities
29+
30+
def received_announce(self, destination_hash, announced_identity, app_data):
31+
if destination_hash not in self.identities:
32+
self.identities[destination_hash] = announced_identity
33+
34+
class ReticulumNetwork(NetworkSyncInterface):
35+
def __init__(self):
36+
self._storage_path = None
37+
self._identity_path = None
38+
self._announcer_thread = None
39+
self.message_queue = Queue()
40+
self._clients = {}
41+
self._ret = None
42+
self._lxm_router = None
43+
self._identity = None
44+
self._my_identity = None
45+
self._identities = {}
46+
47+
def initialize_network(self, _, _port, storage_path, identity_path, service_desc):
48+
self._storage_path = storage_path
49+
self._identity_path = identity_path
50+
self._ret = RNS.Reticulum()
51+
self._lxm_router = LXMF.LXMRouter(storagepath=self._storage_path)
52+
RNS.Transport.register_announce_handler(AnnounceHandler(self._identities))
53+
self._identity = self._load_or_generate_identity()
54+
self._my_identity = self._lxm_router.register_delivery_identity(self._identity)
55+
self._lxm_router.register_delivery_callback(self._ret_deliver)
56+
announcer_thread = threading.Thread(target=self._announcer)
57+
announcer_thread.start()
58+
self._service_desc = service_desc
59+
60+
def _load_or_generate_identity(self):
61+
if os.path.exists(self._identity_path):
62+
try:
63+
return RNS.Identity.from_file(self._identity_path)
64+
except RNS.InvalidIdentityFile:
65+
pass
66+
identity = RNS.Identity()
67+
identity.to_file(self._identity_path)
68+
return identity
69+
70+
def _get_client(self, identity: RNS.Identity) -> NetworkClient:
71+
if identity.hash in self._clients:
72+
return self._clients[identity.hash]
73+
else:
74+
client = self._register_new_client(identity.hash)
75+
self._clients[identity.hash] = client
76+
self._identities[identity.hash] = identity
77+
return client
78+
79+
def _ret_deliver(self, message: LXMF.LXMessage):
80+
try:
81+
# validate the message
82+
if message.signature_validated:
83+
validated = True
84+
elif message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID:
85+
validated = False
86+
elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN:
87+
validated = False
88+
else:
89+
validated = False
90+
91+
# deliver the message to the network
92+
if validated and message.content is not None and message.content != b"":
93+
req: Request = ObjectFactory.get_new_instance("Request")
94+
req.set_value("body", message.content.decode("utf-8"))
95+
req.set_action("reticulum_message")
96+
req.set_value("client", self._get_client(message.source.identity))
97+
self.message_queue.put(req, block=False, timeout=0)
98+
except Exception as e:
99+
print(e)
100+
101+
def _register_new_client(self, destination_hash: bytes):
102+
"""Register a new client to the network.
103+
Args:
104+
destination_hash (bytes): The hash of the client destination to register.
105+
"""
106+
oid = ObjectId("network_client", id=str(destination_hash))
107+
client: NetworkClient = ObjectFactory.get_new_instance(
108+
"DefaultClient", dynamic_configuration={"oid": oid}
109+
)
110+
client.id = destination_hash
111+
client.status = ClientStatus.CONNECTED
112+
client.service_id = self._service_desc.name
113+
client.protocol = self._service_desc.protocol
114+
return client
115+
116+
def _get_client_identity(self, message: LXMF.LXMessage) -> bytes:
117+
"""Get the identity of the client that sent the message. This is used for IAM and client tracking.
118+
Args:
119+
message (LXMF.LXMessage): The message to extract the identity from.
120+
121+
Returns:
122+
bytes: The identity of the client as bytes
123+
"""
124+
return message.source.identity.hash
125+
126+
def _announcer(self, interval: int = 60):
127+
"""Announce the reticulum network to the network."""
128+
while True:
129+
try:
130+
self._my_identity.announce()
131+
except Exception as e:
132+
pass
133+
time.sleep(interval)
134+
135+
def _send_message_to_all_clients(self, message: str):
136+
for identity in self._clients.values():
137+
dest = RNS.Destination(
138+
self._identities[identity.id],
139+
RNS.Destination.OUT,
140+
RNS.Destination.SINGLE,
141+
"lxmf",
142+
"delivery",
143+
)
144+
msg = LXMF.LXMessage(
145+
destination=dest,
146+
source=self._my_identity,
147+
content=message.encode("utf-8"),
148+
desired_method=LXMF.LXMessage.DIRECT,
149+
)
150+
self._lxm_router.handle_outbound(msg)
151+
152+
def _send_message_to_client(self, message: dict, client: NetworkClient):
153+
identity = self._identities.get(client.id)
154+
if identity is not None:
155+
dest = RNS.Destination(
156+
identity,
157+
RNS.Destination.OUT,
158+
RNS.Destination.SINGLE,
159+
"lxmf",
160+
"delivery",
161+
)
162+
msg = LXMF.LXMessage(
163+
destination=dest,
164+
source=self._my_identity,
165+
content=message.encode("utf-8"),
166+
desired_method=LXMF.LXMessage.DIRECT,
167+
)
168+
self._lxm_router.handle_outbound(msg)
169+
170+
def service_connections(self, max_requests=1000, blocking=False, timeout=0):
171+
start_time = time.time()
172+
messages = []
173+
if self.message_queue.empty():
174+
return []
175+
messages.append(self.message_queue.get(block=blocking, timeout=timeout))
176+
while time.time() - start_time < timeout and len(messages) < max_requests:
177+
try:
178+
message = self.message_queue.get(block=False)
179+
messages.append(message)
180+
except Exception as e:
181+
break
182+
return messages
183+
184+
def send_response(self, response):
185+
if response.get_value("client") is not None:
186+
self._send_message_to_client(response.get_value("message"), response.get_value("client"))
187+
else:
188+
self._send_message_to_all_clients(response.get_value("message"))
189+
190+
def receive_message(self, blocking = False):
191+
return self.message_queue.get(block=blocking)
192+
193+
def receive_message_from_client(self, client, blocking = False):
194+
raise NotImplementedError
195+
196+
def teardown_network(self):
197+
pass

examples/reticulum_app/blueprints/__init__.py

Whitespace-only changes.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from digitalpy.core.component_management.impl.default_facade import DefaultFacade
2+
from digitalpy.core.zmanager.impl.async_action_mapper import AsyncActionMapper
3+
from digitalpy.core.zmanager.impl.default_action_mapper import DefaultActionMapper
4+
from digitalpy.core.zmanager.request import Request
5+
from digitalpy.core.zmanager.response import Response
6+
from .controllers.chat_controller import ChatController
7+
from .configuration.chat_constants import (
8+
ACTION_MAPPING_PATH,
9+
LOGGING_CONFIGURATION_PATH,
10+
INTERNAL_ACTION_MAPPING_PATH,
11+
MANIFEST_PATH,
12+
CONFIGURATION_PATH_TEMPLATE,
13+
LOG_FILE_PATH,
14+
ACTION_FLOW_PATH,
15+
)
16+
17+
from . import base
18+
19+
20+
class Chat(DefaultFacade):
21+
"""
22+
"""
23+
24+
def __init__(self, sync_action_mapper: DefaultActionMapper, request: Request,
25+
response: Response, configuration,
26+
action_mapper: AsyncActionMapper = None, # type: ignore
27+
tracing_provider_instance=None): # type: ignore
28+
super().__init__(
29+
# the path to the external action mapping
30+
action_mapping_path=str(ACTION_MAPPING_PATH),
31+
# the path to the internal action mapping
32+
internal_action_mapping_path=str(INTERNAL_ACTION_MAPPING_PATH),
33+
# the path to the logger configuration
34+
logger_configuration=str(LOGGING_CONFIGURATION_PATH),
35+
# the package containing the base classes
36+
base=base, # type: ignore
37+
# the general action mapper (passed by constructor)
38+
action_mapper=sync_action_mapper,
39+
# the request object (passed by constructor)
40+
request=request,
41+
# the response object (passed by constructor)
42+
response=response,
43+
# the configuration object (passed by constructor)
44+
configuration=configuration,
45+
# log file path
46+
log_file_path=LOG_FILE_PATH,
47+
# the tracing provider used
48+
tracing_provider_instance=tracing_provider_instance,
49+
# the template for the absolute path to the model object definitions
50+
configuration_path_template=CONFIGURATION_PATH_TEMPLATE,
51+
# the path to the manifest file
52+
manifest_path=str(MANIFEST_PATH),
53+
# the general action mapper (passed by constructor)
54+
action_flow_path=str(ACTION_FLOW_PATH),
55+
)
56+
self.Chat_controller = ChatController(
57+
request, response, sync_action_mapper, configuration)
58+
59+
def initialize(self, request: Request, response: Response):
60+
self.Chat_controller.initialize(request, response)
61+
62+
return super().initialize(request, response)
63+
64+
def execute(self, method=None):
65+
try:
66+
if hasattr(self, method): # type: ignore
67+
print("executing method "+str(method)) # type: ignore
68+
getattr(self, method)(**self.request.get_values()) # type: ignore
69+
else:
70+
self.request.set_value("logger", self.logger)
71+
self.request.set_value("config_loader", self.config_loader)
72+
self.request.set_value("tracer", self.tracer)
73+
response = self.execute_sub_action(self.request.get_action())
74+
self.response.set_values(response.get_values())
75+
except Exception as e:
76+
self.logger.fatal(str(e))
77+
78+
@DefaultFacade.public
79+
def Chat(self, *args, **kwargs):
80+
"""Updates an existing Genre record.
81+
"""
82+
self.Chat_controller.Chat(*args, **kwargs)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# FilmologyManagement Digital Component
2+
3+
## Description
4+
"Filmology is a test API that describes all the possible variant of a DAF + API model and his implementation as a DigitalPy application"
5+
6+
## Configuration
7+
1. copy the file FilmologyManagement_blueprint.py into your application blueprints folder
8+
2. add the following to your configured core api flows in the configuration/object_configuration.ini file.
9+
```ini
10+
[
11+
; FilmologyManagement component flows
12+
,FilmologyManagement__POSTMovie
13+
,FilmologyManagement__DELETEMovie
14+
,FilmologyManagement__GETMovie
15+
,FilmologyManagement__PATCHMovie
16+
,FilmologyManagement__GETDirectorId
17+
,FilmologyManagement__POSTPoster
18+
,FilmologyManagement__DELETEPoster
19+
,FilmologyManagement__GETPoster
20+
,FilmologyManagement__PATCHPoster
21+
,FilmologyManagement__GETGenreId
22+
,FilmologyManagement__POSTDate
23+
,FilmologyManagement__DELETEDate
24+
,FilmologyManagement__GETDate
25+
,FilmologyManagement__PATCHDate
26+
,FilmologyManagement__GETLanguageId
27+
,FilmologyManagement__POSTDirector
28+
,FilmologyManagement__DELETEDirector
29+
,FilmologyManagement__GETDirector
30+
,FilmologyManagement__PATCHDirector
31+
,FilmologyManagement__GETDateId
32+
,FilmologyManagement__POSTActor
33+
,FilmologyManagement__DELETEActor
34+
,FilmologyManagement__GETActor
35+
,FilmologyManagement__PATCHActor
36+
,FilmologyManagement__GETMovieId
37+
,FilmologyManagement__POSTLanguage
38+
,FilmologyManagement__DELETELanguage
39+
,FilmologyManagement__GETLanguage
40+
,FilmologyManagement__PATCHLanguage
41+
,FilmologyManagement__GETPosterId
42+
,FilmologyManagement__GETActorId
43+
,FilmologyManagement__POSTGenre
44+
,FilmologyManagement__DELETEGenre
45+
,FilmologyManagement__GETGenre
46+
,FilmologyManagement__PATCHGenre
47+
]
48+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""This module contains all the supporting components without business logic
2+
it should also be noted that the component action mapper must be exposed as action mapper.
3+
"""
4+
from .chat_action_mapper import ChatActionMapper as ActionMapper
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from digitalpy.core.zmanager.impl.default_action_mapper import DefaultActionMapper
2+
3+
4+
class ChatActionMapper(DefaultActionMapper):
5+
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class ChatDomain():
2+
pass

examples/reticulum_app/components/Chat/configuration/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)