ReticulumTelemetryHub 0.1.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.
- lxmf_telemetry/__init__.py +0 -0
- lxmf_telemetry/model/fields/field_telemetry_stream.py +7 -0
- lxmf_telemetry/model/persistance/__init__.py +3 -0
- lxmf_telemetry/model/persistance/appearance.py +19 -0
- lxmf_telemetry/model/persistance/peer.py +13 -0
- lxmf_telemetry/model/persistance/sensors/__init__.py +0 -0
- lxmf_telemetry/model/persistance/sensors/location.py +69 -0
- lxmf_telemetry/model/persistance/sensors/magnetic_field.py +36 -0
- lxmf_telemetry/model/persistance/sensors/sensor.py +44 -0
- lxmf_telemetry/model/persistance/sensors/sensor_enum.py +24 -0
- lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +9 -0
- lxmf_telemetry/model/persistance/sensors/time.py +32 -0
- lxmf_telemetry/model/persistance/telemeter.py +23 -0
- lxmf_telemetry/telemetry_controller.py +124 -0
- reticulum_server/__init__.py +0 -0
- reticulum_server/main.py +182 -0
- reticulumtelemetryhub-0.1.0.dist-info/METADATA +15 -0
- reticulumtelemetryhub-0.1.0.dist-info/RECORD +19 -0
- reticulumtelemetryhub-0.1.0.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -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')
|
|
File without changes
|
|
@@ -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,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
|
|
File without changes
|
reticulum_server/main.py
ADDED
|
@@ -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()
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
lxmf_telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
lxmf_telemetry/model/fields/field_telemetry_stream.py,sha256=d1cZ4MICGnJLh6n0kG0Rfh27h1rTiSlUNilgFHO9Q8o,139
|
|
3
|
+
lxmf_telemetry/model/persistance/__init__.py,sha256=hD85jXD4cCPILUALrU0NB3Cy-bpaxPjkeeUsMYWjkLA,84
|
|
4
|
+
lxmf_telemetry/model/persistance/appearance.py,sha256=D_IeRMW3OjdEzJB295h0lIHsRrG6JyeQmoC_Y2k085s,631
|
|
5
|
+
lxmf_telemetry/model/persistance/peer.py,sha256=tvTePw_krqGzcZWAc8QI8WwP4BXQ5mgrVsj3gB0TLWs,488
|
|
6
|
+
lxmf_telemetry/model/persistance/sensors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
lxmf_telemetry/model/persistance/sensors/location.py,sha256=rMqsfCksHvp-Ol3YkfKHzkDBP4Wv-uki44oCHSHiKzM,2686
|
|
8
|
+
lxmf_telemetry/model/persistance/sensors/magnetic_field.py,sha256=n5FBwUgYMTremcUUTp06ZiRE_ZiHtcjP9EUgk6rcd0U,1063
|
|
9
|
+
lxmf_telemetry/model/persistance/sensors/sensor.py,sha256=6wajbHL1ns5khwuje7-DSQmUYGsCMclkccsy8LCAe-o,1502
|
|
10
|
+
lxmf_telemetry/model/persistance/sensors/sensor_enum.py,sha256=JUjfjg4RKHGhyNjKURPOr1Ij9gge8tD1IBgWXSiRJX0,718
|
|
11
|
+
lxmf_telemetry/model/persistance/sensors/sensor_mapping.py,sha256=3FREi7bhgXxSpVH6iZcWznKxvRwEQVCXuyBFwwSeuRk,237
|
|
12
|
+
lxmf_telemetry/model/persistance/sensors/time.py,sha256=oor-uHLq05N-5TPvXzBIfTgXH0tAkQ7T61dXGCcyn1o,958
|
|
13
|
+
lxmf_telemetry/model/persistance/telemeter.py,sha256=skhoPaifmQsypv11Aon4b8zQg84qohuVmYGJtSmqaU0,973
|
|
14
|
+
lxmf_telemetry/telemetry_controller.py,sha256=wMmrNY-CRfbwfQvA4xWT1vPz1VnLER6cRrPictkkPC4,4961
|
|
15
|
+
reticulum_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
reticulum_server/main.py,sha256=BPLcln7ZQgaUREJvFgBZnkfLRtYZnEUx1M96fMq7PYY,7940
|
|
17
|
+
reticulumtelemetryhub-0.1.0.dist-info/METADATA,sha256=Qe-h5spqSFpHa-AgNrGGzJ5_cfIf0olBpDu5tn_lTl4,422
|
|
18
|
+
reticulumtelemetryhub-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
19
|
+
reticulumtelemetryhub-0.1.0.dist-info/RECORD,,
|