ReticulumTelemetryHub 0.1.0__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.1
2
+ Name: ReticulumTelemetryHub
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: naman108
6
+ Requires-Python: >=3.12,<4.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Requires-Dist: lxmf (>=0.4.4,<0.5.0)
10
+ Requires-Dist: msgpack (>=1.0.8,<2.0.0)
11
+ Requires-Dist: pytest (>=8.3.2,<9.0.0)
12
+ Requires-Dist: sqlalchemy (>=2.0.32,<3.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ from lxmf_telemetry.model.persistance.telemeter import Telemeter
2
+
3
+
4
+ class FieldTelmetryStream():
5
+
6
+ telemeters: list[Telemeter]
7
+
@@ -0,0 +1,3 @@
1
+ from sqlalchemy.ext.declarative import declarative_base
2
+
3
+ Base = declarative_base()
@@ -0,0 +1,19 @@
1
+ from sqlalchemy import Column, Integer, String, ForeignKey, BLOB
2
+ from sqlalchemy.orm import relationship
3
+ from . import Base
4
+
5
+
6
+ class Appearance(Base):
7
+
8
+ __tablename__ = "Appearance"
9
+
10
+ id = Column(Integer, primary_key=True, autoincrement=True)
11
+ icon = Column(String, nullable=False)
12
+ foreground = Column(String, nullable=False)
13
+ background = Column(String, nullable=False)
14
+ peer_id = Column(String, ForeignKey("Peer.destination_hash"))
15
+ peer = relationship("Peer", back_populates="appearance")
16
+
17
+ def __init__(self, peer, icon = "Default"):
18
+ self.peer = peer
19
+ self.icon = icon
@@ -0,0 +1,13 @@
1
+ from . import Base
2
+ from typing import TYPE_CHECKING, Optional
3
+ from sqlalchemy import Column, Integer, String
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+ if TYPE_CHECKING:
6
+ from .telemeter import Telemeter
7
+
8
+ class Peer(Base):
9
+ __tablename__ = 'Peer'
10
+
11
+ destination_hash: Mapped[str] = mapped_column(String, nullable=False, primary_key=True)
12
+ telemeters = relationship("Telemeter")
13
+ #appearance = relationship("Appearance", back_populates='peer')
@@ -0,0 +1,69 @@
1
+ from sqlalchemy import Column
2
+ from lxmf_telemetry.model.persistance.sensors.sensor import Sensor
3
+ from .sensor_enum import SID_LOCATION
4
+ import struct
5
+ import RNS
6
+ from sqlalchemy import Integer, ForeignKey, Float, DateTime
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+ from datetime import datetime
9
+
10
+ class Location(Sensor):
11
+ __tablename__ = 'Location'
12
+
13
+ id: Mapped[int] = mapped_column(ForeignKey('Sensor.id'), primary_key=True)
14
+ latitude: Mapped[float] = mapped_column()
15
+ longitude: Mapped[float] = mapped_column()
16
+ altitude: Mapped[float] = mapped_column()
17
+ speed: Mapped[float] = mapped_column()
18
+ bearing: Mapped[float] = mapped_column()
19
+ accuracy: Mapped[float] = mapped_column()
20
+ last_update: Mapped[datetime] = mapped_column(DateTime)
21
+
22
+ def __init__(self):
23
+ super().__init__(stale_time=15)
24
+ self.latitude = None
25
+ self.longitude = None
26
+ self.altitude = None
27
+ self.speed = None
28
+ self.bearing = None
29
+ self.accuracy = None
30
+
31
+ def pack(self):
32
+ try:
33
+ return [
34
+ struct.pack("!i", int(round(self.latitude, 6) * 1e6)),
35
+ struct.pack("!i", int(round(self.longitude, 6) * 1e6)),
36
+ struct.pack("!I", int(round(self.altitude, 2) * 1e2)),
37
+ struct.pack("!I", int(round(self.speed, 2) * 1e2)),
38
+ struct.pack("!I", int(round(self.bearing, 2) * 1e2)),
39
+ struct.pack("!H", int(round(self.accuracy, 2) * 1e2)),
40
+ self.last_update.timestamp(),
41
+ ]
42
+ except (KeyError, ValueError, struct.error) as e:
43
+ RNS.log(
44
+ "An error occurred while packing location sensor data. "
45
+ "The contained exception was: " + str(e),
46
+ RNS.LOG_ERROR,
47
+ )
48
+ return None
49
+
50
+ def unpack(self, packed):
51
+ try:
52
+ if packed is None:
53
+ return None
54
+ else:
55
+ self.latitude = struct.unpack("!i", packed[0])[0] / 1e6
56
+ self.longitude = struct.unpack("!i", packed[1])[0] / 1e6
57
+ self.altitude = struct.unpack("!I", packed[2])[0] / 1e2
58
+ self.speed = struct.unpack("!I", packed[3])[0] / 1e2
59
+ self.bearing = struct.unpack("!I", packed[4])[0] / 1e2
60
+ self.accuracy = struct.unpack("!H", packed[5])[0] / 1e2
61
+ self.last_update = datetime.fromtimestamp(packed[6])
62
+ except (struct.error, IndexError):
63
+ return None
64
+
65
+
66
+ __mapper_args__ = {
67
+ 'polymorphic_identity': SID_LOCATION,
68
+ 'with_polymorphic': '*'
69
+ }
@@ -0,0 +1,36 @@
1
+ from sqlalchemy import Column
2
+ from lxmf_telemetry.model.persistance.sensors.sensor import Sensor
3
+ from .sensor_enum import SID_MAGNETIC_FIELD
4
+ import struct
5
+ import RNS
6
+ from sqlalchemy import Integer, ForeignKey, Float, DateTime
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+ from datetime import datetime
9
+
10
+ class MagneticField(Sensor):
11
+ __tablename__ = 'MagneticField'
12
+
13
+ id: Mapped[int] = mapped_column(ForeignKey('Sensor.id'), primary_key=True)
14
+ x: Mapped[float] = mapped_column()
15
+ y: Mapped[float] = mapped_column()
16
+ z: Mapped[float] = mapped_column()
17
+
18
+ def __init__(self):
19
+ super().__init__(stale_time=15)
20
+ self.x = None
21
+ self.y = None
22
+ self.z = None
23
+
24
+ def pack(self):
25
+ return [self.x, self.y, self.z]
26
+
27
+ def unpack(self, packed):
28
+ try:
29
+ return {"x": packed[0], "y": packed[1], "z": packed[2]}
30
+ except:
31
+ return None
32
+
33
+ __mapper_args__ = {
34
+ 'polymorphic_identity': SID_MAGNETIC_FIELD,
35
+ 'with_polymorphic': '*'
36
+ }
@@ -0,0 +1,44 @@
1
+ from sqlalchemy import Column, ForeignKey, Integer, Float, Boolean, String, create_engine, BLOB
2
+ from msgpack import packb, unpackb
3
+ from sqlalchemy.ext.declarative import declarative_base
4
+ from sqlalchemy.orm import sessionmaker, relationship, Mapped, mapped_column
5
+ import time
6
+ from typing import TYPE_CHECKING
7
+ from .. import Base
8
+
9
+ class Sensor(Base):
10
+ __tablename__ = 'Sensor'
11
+
12
+ id = Column(Integer, primary_key=True, autoincrement=True)
13
+ sid = Column(Integer, nullable=False, default=0x00)
14
+ stale_time = Column(Float, nullable=True)
15
+ data = Column(BLOB, nullable=True)
16
+ synthesized = Column(Boolean, default=False)
17
+ telemeter_id: Mapped[int] = mapped_column(ForeignKey('Telemeter.id'))
18
+ telemeter = relationship("Telemeter", back_populates='sensors')
19
+
20
+ def __init__(self, stale_time=None, data=None, active=False, synthesized=False, last_update=0, last_read=0):
21
+ self.stale_time = stale_time
22
+ self.data = data
23
+ self.active = active
24
+ self.synthesized = synthesized
25
+ self.last_update = last_update
26
+ self.last_read = last_read
27
+
28
+ def packb(self):
29
+ return packb(self.pack())
30
+
31
+ def unpackb(self, packed):
32
+ return unpackb(self.unpack(packed))
33
+
34
+ def pack(self):
35
+ return self.data
36
+
37
+ def unpack(self, packed):
38
+ return packed
39
+
40
+ __mapper_args__ = {
41
+ 'polymorphic_identity': 'Sensor',
42
+ 'with_polymorphic': '*',
43
+ "polymorphic_on": sid
44
+ }
@@ -0,0 +1,24 @@
1
+ SID_NONE = 0x00
2
+ SID_TIME = 0x01
3
+ SID_LOCATION = 0x02
4
+ SID_PRESSURE = 0x03
5
+ SID_BATTERY = 0x04
6
+ SID_PHYSICAL_LINK = 0x05
7
+ SID_ACCELERATION = 0x06
8
+ SID_TEMPERATURE = 0x07
9
+ SID_HUMIDITY = 0x08
10
+ SID_MAGNETIC_FIELD = 0x09
11
+ SID_AMBIENT_LIGHT = 0x0A
12
+ SID_GRAVITY = 0x0B
13
+ SID_ANGULAR_VELOCITY = 0x0C
14
+ SID_PROXIMITY = 0x0E
15
+ SID_INFORMATION = 0x0F
16
+ SID_RECEIVED = 0x10
17
+ SID_POWER_CONSUMPTION = 0x11
18
+ SID_POWER_PRODUCTION = 0x12
19
+ SID_PROCESSOR = 0x13
20
+ SID_RAM = 0x14
21
+ SID_NVM = 0x15
22
+ SID_TANK = 0x16
23
+ SID_FUEL = 0x17
24
+ SID_CUSTOM = 0xff
@@ -0,0 +1,9 @@
1
+ from .sensor_enum import *
2
+ from .location import Location
3
+ from .time import Time
4
+ from .magnetic_field import MagneticField
5
+ sid_mapping = {
6
+ SID_LOCATION: Location,
7
+ SID_TIME: Time,
8
+ SID_MAGNETIC_FIELD: MagneticField,
9
+ }
@@ -0,0 +1,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 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
+ }
@@ -0,0 +1,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
+ 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()
@@ -0,0 +1,124 @@
1
+ from typing import Optional
2
+ from datetime import datetime
3
+ import LXMF
4
+ import RNS
5
+ from msgpack import packb, unpackb
6
+ from lxmf_telemetry.model.persistance import Base
7
+ from lxmf_telemetry.model.persistance.sensors.sensor import Sensor
8
+ from lxmf_telemetry.model.persistance.telemeter import Telemeter
9
+
10
+ from lxmf_telemetry.model.persistance.sensors.sensor_mapping import sid_mapping
11
+ from sqlalchemy import create_engine
12
+ from sqlalchemy.orm import sessionmaker, Session, joinedload
13
+
14
+ _engine = create_engine("sqlite:///telemetry.db")
15
+ Base.metadata.create_all(_engine)
16
+ Session_cls = sessionmaker(bind=_engine)
17
+
18
+
19
+ class TelemetryController:
20
+ """This class is responsible for managing the telemetry data."""
21
+
22
+ TELEMETRY_REQUEST = 1
23
+
24
+ def __init__(self) -> None:
25
+ pass
26
+
27
+ def get_telemetry(
28
+ self, start_time: Optional[datetime] = None, end_time: Optional[datetime] = None
29
+ ) -> list[Telemeter]:
30
+ """Get the telemetry data."""
31
+ with Session_cls() as ses:
32
+ query = ses.query(Telemeter)
33
+ if start_time:
34
+ query = query.filter(Telemeter.time >= start_time)
35
+ if end_time:
36
+ query = query.filter(Telemeter.time <= end_time)
37
+ tels = query.options(joinedload(Telemeter.sensors)).all()
38
+ return tels
39
+
40
+ def save_telemetry(self, telemetry_data: dict, peer_dest) -> None:
41
+ """Save the telemetry data."""
42
+ tel = self._deserialize_telemeter(telemetry_data, peer_dest)
43
+ with Session_cls() as ses:
44
+ ses.add(tel)
45
+ ses.commit()
46
+
47
+ def handle_message(self, message: LXMF.LXMessage) -> bool:
48
+ """Handle the incoming message."""
49
+ handled = False
50
+ if LXMF.FIELD_TELEMETRY in message.fields:
51
+ tel_data: dict = unpackb(
52
+ message.fields[LXMF.FIELD_TELEMETRY], strict_map_key=False
53
+ )
54
+ RNS.log(f"Telemetry data: {tel_data}")
55
+ self.save_telemetry(tel_data, RNS.hexrep(message.source_hash, False))
56
+ handled = True
57
+ if LXMF.FIELD_TELEMETRY_STREAM in message.fields:
58
+ tels_data = unpackb(
59
+ message.fields[LXMF.FIELD_TELEMETRY_STREAM], strict_map_key=False
60
+ )
61
+ for tel_data in tels_data:
62
+ self.save_telemetry(tel_data, RNS.hexrep(tel_data.pop(0)))
63
+ handled = True
64
+
65
+ return handled
66
+
67
+ def handle_command(self, command: dict, message: LXMF.LXMessage, my_lxm_dest) -> Optional[LXMF.LXMessage]:
68
+ """Handle the incoming command."""
69
+ if TelemetryController.TELEMETRY_REQUEST in command:
70
+ timebase = command[TelemetryController.TELEMETRY_REQUEST]
71
+ tels = self.get_telemetry(start_time=datetime.fromtimestamp(timebase))
72
+ packed_tels = []
73
+ dest = RNS.Destination(
74
+ message.source.identity,
75
+ RNS.Destination.OUT,
76
+ RNS.Destination.SINGLE,
77
+ "lxmf",
78
+ "delivery",
79
+ )
80
+ message = LXMF.LXMessage(
81
+ dest,
82
+ my_lxm_dest,
83
+ "Telemetry data",
84
+ desired_method=LXMF.LXMessage.DIRECT,
85
+ )
86
+ for tel in tels:
87
+ tel_data = self._serialize_telemeter(tel)
88
+ packed_tels.append(
89
+ [
90
+ bytes.fromhex(tel.peer_dest),
91
+ round(tel.time.timestamp()),
92
+ packb(tel_data),
93
+ ['account', b'\x00\x00\x00', b'\xff\xff\xff'],
94
+ ]
95
+ )
96
+ message.fields[LXMF.FIELD_TELEMETRY_STREAM] = packed_tels
97
+ print("+--- Sending telemetry data---------------------------------")
98
+ print(f"| Telemetry data: {packed_tels}")
99
+ print(f"| Message: {message}")
100
+ print("+------------------------------------------------------------")
101
+ return message
102
+ else:
103
+ return None
104
+
105
+ def _serialize_telemeter(self, telemeter: Telemeter) -> dict:
106
+ """Serialize the telemeter data."""
107
+ telemeter_data = {}
108
+ for sensor in telemeter.sensors:
109
+ sensor_data = sensor.pack()
110
+ telemeter_data[sensor.sid] = sensor_data
111
+ return telemeter_data
112
+
113
+ def _deserialize_telemeter(self, tel_data: dict, peer_dest: str) -> Telemeter:
114
+ """Deserialize the telemeter data."""
115
+ tel = Telemeter(peer_dest)
116
+ for sid in tel_data:
117
+ if sid in sid_mapping:
118
+ if tel_data[sid] is None:
119
+ RNS.log(f"Sensor data for {sid} is None")
120
+ continue
121
+ sensor = sid_mapping[sid]()
122
+ sensor.unpack(tel_data[sid])
123
+ tel.sensors.append(sensor)
124
+ return tel
@@ -0,0 +1,21 @@
1
+ [tool.poetry]
2
+ name = "ReticulumTelemetryHub"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = ["naman108"]
6
+ readme = "README.md"
7
+ packages = [
8
+ { include = "lxmf_telemetry" },
9
+ { include = "reticulum_server" },
10
+ ]
11
+
12
+ [tool.poetry.dependencies]
13
+ python = "^3.12"
14
+ lxmf = "^0.4.4"
15
+ msgpack = "^1.0.8"
16
+ sqlalchemy = "^2.0.32"
17
+ pytest = "^8.3.2"
18
+
19
+ [build-system]
20
+ requires = ["poetry-core"]
21
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,182 @@
1
+ import os
2
+ import time
3
+ import LXMF
4
+ import RNS
5
+ from lxmf_telemetry.telemetry_controller import TelemetryController
6
+
7
+ # Constants
8
+ STORAGE_PATH = "./tmp1" # Path to store temporary files
9
+ IDENTITY_PATH = os.path.join(STORAGE_PATH, "identity") # Path to store identity file
10
+ APP_NAME = LXMF.APP_NAME + ".delivery" # Application name for LXMF
11
+
12
+ tel_controller = TelemetryController()
13
+
14
+ class AnnounceHandler:
15
+ def __init__(self, connections, my_lxmf_dest, lxm_router):
16
+ self.aspect_filter = APP_NAME # Filter for LXMF announcements
17
+ self.connections = connections # List to store connections
18
+ self.my_lxmf_dest = my_lxmf_dest # LXMF destination
19
+ self.lxm_router = lxm_router # LXMF router
20
+
21
+ def received_announce(self, destination_hash, announced_identity, app_data):
22
+ # Log the received announcement details
23
+ RNS.log("\t+--- LXMF Announcement -----------------------------------------")
24
+ RNS.log(f"\t| Source hash : {RNS.prettyhexrep(destination_hash)}")
25
+ RNS.log(f"\t| Announced identity : {announced_identity}")
26
+ RNS.log(f"\t| App data : {app_data}")
27
+ RNS.log("\t+---------------------------------------------------------------")
28
+
29
+ # Create a new destination from the announced identity
30
+ dest = RNS.Destination(
31
+ announced_identity,
32
+ RNS.Destination.OUT,
33
+ RNS.Destination.SINGLE,
34
+ "lxmf",
35
+ "delivery",
36
+ )
37
+ self.connections.append(dest) # Add the new destination to connections
38
+
39
+ # Create and send a message to the new destination
40
+ message = LXMF.LXMessage(
41
+ dest, self.my_lxmf_dest, "Hi there", desired_method=LXMF.LXMessage.DIRECT
42
+ )
43
+ self.lxm_router.handle_outbound(message) # Handle outbound message
44
+
45
+ def command_handler(commands: list, message: LXMF.LXMessage, lxm_router, my_lxmf_dest):
46
+ for command in commands:
47
+ print(f"Command: {command}")
48
+ msg = tel_controller.handle_command(command, message, my_lxmf_dest)
49
+ if msg:
50
+ lxm_router.handle_outbound(msg)
51
+
52
+ def delivery_callback(message: LXMF.LXMessage, connections, my_lxmf_dest, lxm_router):
53
+ # Format the timestamp of the message
54
+ try:
55
+ time_string = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp))
56
+ signature_string = "Signature is invalid, reason undetermined"
57
+
58
+ # Determine the signature validation status
59
+ if message.signature_validated:
60
+ signature_string = "Validated"
61
+ elif message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID:
62
+ signature_string = "Invalid signature"
63
+ elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN:
64
+ signature_string = "Cannot verify, source is unknown"
65
+
66
+ if message.signature_validated and LXMF.FIELD_COMMANDS in message.fields:
67
+ command_handler(message.fields[LXMF.FIELD_COMMANDS], message, lxm_router, my_lxmf_dest)
68
+ return
69
+
70
+ if tel_controller.handle_message(message):
71
+ RNS.log("Telemetry data saved")
72
+ return
73
+
74
+ # Log the delivery details
75
+ RNS.log("\t+--- LXMF Delivery ---------------------------------------------")
76
+ RNS.log(f"\t| Source hash : {RNS.prettyhexrep(message.source_hash)}")
77
+ RNS.log(f"\t| Source instance : {message.get_source()}")
78
+ RNS.log(
79
+ f"\t| Destination hash : {RNS.prettyhexrep(message.destination_hash)}"
80
+ )
81
+ #RNS.log(f"\t| Destination identity : {message.source_identity}")
82
+ RNS.log(f"\t| Destination instance : {message.get_destination()}")
83
+ RNS.log(f"\t| Transport Encryption : {message.transport_encryption}")
84
+ RNS.log(f"\t| Timestamp : {time_string}")
85
+ RNS.log(f"\t| Title : {message.title_as_string()}")
86
+ RNS.log(f"\t| Content : {message.content_as_string()}")
87
+ RNS.log(f"\t| Fields : {message.fields}")
88
+ RNS.log(f"\t| Message signature : {signature_string}")
89
+ RNS.log("\t+---------------------------------------------------------------")
90
+ for connection in connections:
91
+ if connection.hash != message.source_hash:
92
+ response = LXMF.LXMessage(
93
+ connection,
94
+ my_lxmf_dest,
95
+ message.content_as_string(),
96
+ desired_method=LXMF.LXMessage.DIRECT,
97
+ )
98
+ lxm_router.handle_outbound(response) # Handle outbound response
99
+ except Exception as e:
100
+ RNS.log(f"Error: {e}")
101
+
102
+ def load_or_generate_identity(identity_path):
103
+ # Load existing identity or generate a new one
104
+ if os.path.exists(identity_path):
105
+ try:
106
+ RNS.log("Loading existing identity")
107
+ return RNS.Identity.from_file(identity_path)
108
+ except:
109
+ RNS.log("Failed to load existing identity, generating new")
110
+ else:
111
+ RNS.log("Generating new identity")
112
+
113
+ identity = RNS.Identity() # Create a new identity
114
+ identity.to_file(identity_path) # Save the new identity to file
115
+ return identity
116
+
117
+
118
+
119
+ def main():
120
+ global my_lxmf_dest
121
+ connections = [] # List to store connections
122
+ r = RNS.Reticulum() # Initialize Reticulum
123
+ lxm_router = LXMF.LXMRouter(storagepath=STORAGE_PATH) # Initialize LXMF router
124
+ # lxm_router.enable_propagation()
125
+ identity = load_or_generate_identity(IDENTITY_PATH) # Load or generate identity
126
+ my_lxmf_dest = lxm_router.register_delivery_identity(
127
+ identity, "test_server"
128
+ ) # Register delivery identity
129
+ lxm_router.set_message_storage_limit(megabytes=5)
130
+ lxm_router.enable_propagation()
131
+
132
+ # Register delivery callback
133
+ lxm_router.register_delivery_callback(
134
+ lambda msg: delivery_callback(msg, connections, my_lxmf_dest, lxm_router)
135
+ )
136
+ # Register announce handler
137
+ RNS.Transport.register_announce_handler(
138
+ AnnounceHandler(connections, my_lxmf_dest, lxm_router)
139
+ )
140
+
141
+ # Announce LXMF identity
142
+ my_lxmf_dest.announce()
143
+ lxm_router.announce_propagation_node()
144
+ RNS.log("LXMF identity announced")
145
+ RNS.log("\t+--- LXMF Identity ---------------------------------------------")
146
+ RNS.log(f"\t| Hash : {RNS.prettyhexrep(my_lxmf_dest.hash)}")
147
+ RNS.log(
148
+ f"\t| Public key : {RNS.prettyhexrep(my_lxmf_dest.identity.pub.public_bytes())}"
149
+ )
150
+ RNS.log("\t+---------------------------------------------------------------")
151
+
152
+ # Periodically announce the LXMF identity
153
+ while True:
154
+ choice = input("Enter your choice (exit/announce/telemetry): ")
155
+
156
+ if choice == "exit":
157
+ break
158
+ elif choice == "announce":
159
+ my_lxmf_dest.announce()
160
+ elif choice == "announce_prop":
161
+ lxm_router.announce_propagation_node()
162
+ elif choice == "telemetry":
163
+ connection_hash = input("Enter the connection hash: ")
164
+ found = False
165
+ for connection in connections:
166
+ if connection.hexhash == connection_hash:
167
+ message = LXMF.LXMessage(
168
+ connection,
169
+ my_lxmf_dest,
170
+ "Requesting telemetry",
171
+ desired_method=LXMF.LXMessage.DIRECT,
172
+ fields={LXMF.FIELD_COMMANDS: [{TelemetryController.TELEMETRY_REQUEST: 1000000000}]}
173
+ )
174
+ lxm_router.handle_outbound(message)
175
+ found = True
176
+ break
177
+ if not found:
178
+ print("Connection not found")
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()