Bare bones are ready
This commit is contained in:
commit
07ddcdf5d1
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
29
.idea/inspectionProfiles/Project_Default.xml
Normal file
29
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<Languages>
|
||||||
|
<language minSize="141" name="Python" />
|
||||||
|
</Languages>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="3">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="torch" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="scipy" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="numpy" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N806" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
10
.idea/meshtastic-metrics-exporter.iml
Normal file
10
.idea/meshtastic-metrics-exporter.iml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
7
.idea/misc.xml
Normal file
7
.idea/misc.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.12 (meshtastic-metrics-exporter)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (meshtastic-metrics-exporter)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/meshtastic-metrics-exporter.iml" filepath="$PROJECT_DIR$/.idea/meshtastic-metrics-exporter.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
1
exporter/__init__.py
Normal file
1
exporter/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .processors import MessageProcessor
|
16
exporter/processors.py
Normal file
16
exporter/processors.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from meshtastic.mesh_pb2 import MeshPacket
|
||||||
|
from prometheus_client import CollectorRegistry
|
||||||
|
|
||||||
|
from exporter.registry import ProcessorRegistry
|
||||||
|
|
||||||
|
|
||||||
|
class MessageProcessor:
|
||||||
|
def __init__(self, registry: CollectorRegistry):
|
||||||
|
self.registry = registry
|
||||||
|
|
||||||
|
def process(self, mesh_packet: MeshPacket):
|
||||||
|
port_num = mesh_packet.decoded.portnum
|
||||||
|
payload = mesh_packet.decoded.payload
|
||||||
|
processor = ProcessorRegistry.get_processor(port_num)(self.registry)
|
||||||
|
|
||||||
|
processor.process(payload)
|
252
exporter/registry.py
Normal file
252
exporter/registry.py
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from venv import logger
|
||||||
|
|
||||||
|
import unishox2
|
||||||
|
from meshtastic.admin_pb2 import AdminMessage
|
||||||
|
from meshtastic.mesh_pb2 import Position, User, Routing, Waypoint, RouteDiscovery, NeighborInfo
|
||||||
|
from meshtastic.mqtt_pb2 import MapReport
|
||||||
|
from meshtastic.paxcount_pb2 import Paxcount
|
||||||
|
from meshtastic.portnums_pb2 import PortNum
|
||||||
|
from meshtastic.remote_hardware_pb2 import HardwareMessage
|
||||||
|
from meshtastic.storeforward_pb2 import StoreAndForward
|
||||||
|
from meshtastic.telemetry_pb2 import Telemetry
|
||||||
|
from prometheus_client import CollectorRegistry
|
||||||
|
|
||||||
|
|
||||||
|
class Processor(ABC):
|
||||||
|
def __init__(self, registry: CollectorRegistry):
|
||||||
|
self.registry = registry
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def process(self, payload):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessorRegistry:
|
||||||
|
_registry = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_processor(cls, portnum):
|
||||||
|
def inner_wrapper(wrapped_class):
|
||||||
|
cls._registry[portnum] = wrapped_class()
|
||||||
|
return wrapped_class
|
||||||
|
|
||||||
|
return inner_wrapper
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_processor(cls, portnum):
|
||||||
|
return cls._registry.get(portnum, UnknownAppProcessor())
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.UNKNOWN_APP)
|
||||||
|
class UnknownAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received UNKNOWN_APP packet")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.TEXT_MESSAGE_APP)
|
||||||
|
class TextMessageAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received TEXT_MESSAGE_APP packet")
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.REMOTE_HARDWARE_APP)
|
||||||
|
class RemoteHardwareAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received REMOTE_HARDWARE_APP packet")
|
||||||
|
hardware_message = HardwareMessage()
|
||||||
|
hardware_message.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.POSITION_APP)
|
||||||
|
class PositionAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received POSITION_APP packet")
|
||||||
|
position = Position()
|
||||||
|
position.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.NODEINFO_APP)
|
||||||
|
class NodeInfoAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received NODEINFO_APP packet")
|
||||||
|
user = User()
|
||||||
|
user.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.ROUTING_APP)
|
||||||
|
class RoutingAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received ROUTING_APP packet")
|
||||||
|
routing = Routing()
|
||||||
|
routing.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.ADMIN_APP)
|
||||||
|
class AdminAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received ADMIN_APP packet")
|
||||||
|
admin_message = AdminMessage()
|
||||||
|
admin_message.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.TEXT_MESSAGE_COMPRESSED_APP)
|
||||||
|
class TextMessageCompressedAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received TEXT_MESSAGE_COMPRESSED_APP packet")
|
||||||
|
decompressed_payload = unishox2.decompress(payload, len(payload))
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.WAYPOINT_APP)
|
||||||
|
class WaypointAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received WAYPOINT_APP packet")
|
||||||
|
waypoint = Waypoint()
|
||||||
|
waypoint.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.AUDIO_APP)
|
||||||
|
class AudioAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received AUDIO_APP packet")
|
||||||
|
pass # NOTE: Audio packet. should probably be processed
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.DETECTION_SENSOR_APP)
|
||||||
|
class DetectionSensorAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received DETECTION_SENSOR_APP packet")
|
||||||
|
pass # NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.REPLY_APP)
|
||||||
|
class ReplyAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received REPLY_APP packet")
|
||||||
|
pass # NOTE: Provides a 'ping' service that replies to any packet it receives. This is useful for testing.
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.IP_TUNNEL_APP)
|
||||||
|
class IpTunnelAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received IP_TUNNEL_APP packet")
|
||||||
|
pass # NOTE: IP Packet. Handled by the python API, firmware ignores this one and passes it on.
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.PAXCOUNTER_APP)
|
||||||
|
class PaxCounterAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received PAXCOUNTER_APP packet")
|
||||||
|
paxcounter = Paxcount()
|
||||||
|
paxcounter.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.SERIAL_APP)
|
||||||
|
class SerialAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received SERIAL_APP packet")
|
||||||
|
pass # NOTE: Provides a hardware serial interface to send and receive from the Meshtastic network.
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.STORE_FORWARD_APP)
|
||||||
|
class StoreForwardAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received STORE_FORWARD_APP packet")
|
||||||
|
store_and_forward = StoreAndForward()
|
||||||
|
store_and_forward.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.RANGE_TEST_APP)
|
||||||
|
class RangeTestAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received RANGE_TEST_APP packet")
|
||||||
|
pass # NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.TELEMETRY_APP)
|
||||||
|
class TelemetryAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received TELEMETRY_APP packet")
|
||||||
|
telemetry = Telemetry()
|
||||||
|
telemetry.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.ZPS_APP)
|
||||||
|
class ZpsAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received ZPS_APP packet")
|
||||||
|
pass # NOTE: Experimental tools for estimating node position without a GPS
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.SIMULATOR_APP)
|
||||||
|
class SimulatorAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received SIMULATOR_APP packet")
|
||||||
|
pass # NOTE: Used to let multiple instances of Linux native applications communicate as if they did using their LoRa chip.
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.TRACEROUTE_APP)
|
||||||
|
class TraceRouteAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received TRACEROUTE_APP packet")
|
||||||
|
traceroute = RouteDiscovery()
|
||||||
|
traceroute.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.NEIGHBORINFO_APP)
|
||||||
|
class NeighborInfoAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received NEIGHBORINFO_APP packet")
|
||||||
|
neighbor_info = NeighborInfo()
|
||||||
|
neighbor_info.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.ATAK_PLUGIN)
|
||||||
|
class AtakPluginProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received ATAK_PLUGIN packet")
|
||||||
|
pass # NOTE: ATAK Plugin
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.MAP_REPORT_APP)
|
||||||
|
class MapReportAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received MAP_REPORT_APP packet")
|
||||||
|
map_report = MapReport()
|
||||||
|
map_report.ParseFromString(payload)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.PRIVATE_APP)
|
||||||
|
class PrivateAppProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received PRIVATE_APP packet")
|
||||||
|
pass # NOTE: Private application portnum
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.ATAK_FORWARDER)
|
||||||
|
class AtakForwarderProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received ATAK_FORWARDER packet")
|
||||||
|
pass # NOTE: ATAK Forwarder
|
||||||
|
|
||||||
|
|
||||||
|
@ProcessorRegistry.register_processor(PortNum.MAX)
|
||||||
|
class MaxProcessor(Processor):
|
||||||
|
def process(self, payload):
|
||||||
|
logger.debug("Received MAX packet")
|
||||||
|
pass # NOTE: Maximum portnum value
|
72
main.py
Normal file
72
main.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import redis
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from meshtastic.mesh_pb2 import MeshPacket
|
||||||
|
from meshtastic.mqtt_pb2 import ServiceEnvelope
|
||||||
|
from prometheus_client import push_to_gateway, CollectorRegistry
|
||||||
|
|
||||||
|
from exporter.processors import MessageProcessor
|
||||||
|
|
||||||
|
|
||||||
|
def handle_connect(client, userdata, flags, reason_code, properties):
|
||||||
|
print(f"Connected with result code {reason_code}")
|
||||||
|
client.subscribe(os.getenv('mqtt_topic', 'msh/israel/#'))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_message(client, userdata, message):
|
||||||
|
print(f"Received message '{message.payload.decode()}' on topic '{message.topic}'")
|
||||||
|
envelope = ServiceEnvelope()
|
||||||
|
envelope.ParseFromString(message.payload)
|
||||||
|
|
||||||
|
packet: MeshPacket = envelope.packet
|
||||||
|
if redis_client.set(str(packet.id), 1, nx=True, ex=os.getenv('redis_expiration', 60), get=True) is not None:
|
||||||
|
logging.debug(f"Packet {packet.id} already processed")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process the packet
|
||||||
|
processor.process(packet)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
load_dotenv()
|
||||||
|
# Create Redis client
|
||||||
|
redis_client = redis.Redis(
|
||||||
|
host=os.getenv('redis_host'),
|
||||||
|
port=int(os.getenv('redis_port')),
|
||||||
|
db=int(os.getenv('redis_db', 0)),
|
||||||
|
password=os.getenv('redis_password', None),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure Prometheus exporter
|
||||||
|
registry = CollectorRegistry()
|
||||||
|
push_to_gateway(
|
||||||
|
os.getenv('prometheus_pushgateway'),
|
||||||
|
job=os.getenv('prometheus_job'),
|
||||||
|
registry=registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an MQTT client
|
||||||
|
mqtt_client = mqtt.Client()
|
||||||
|
|
||||||
|
mqtt_client.on_connect = handle_connect
|
||||||
|
mqtt_client.on_message = handle_message
|
||||||
|
|
||||||
|
if bool(os.getenv('mqtt_is_tls', False)):
|
||||||
|
tls_context = mqtt.ssl.create_default_context()
|
||||||
|
mqtt_client.tls_set_context(tls_context)
|
||||||
|
|
||||||
|
if os.getenv('mqtt_username', None) and os.getenv('mqtt_password', None):
|
||||||
|
mqtt_client.username_pw_set(os.getenv('mqtt_username'), os.getenv('mqtt_password'))
|
||||||
|
|
||||||
|
mqtt_client.connect(
|
||||||
|
os.getenv('mqtt_host'),
|
||||||
|
int(os.getenv('mqtt_port')),
|
||||||
|
keepalive=int(os.getenv('mqtt_keepalive', 60)),
|
||||||
|
)
|
||||||
|
# Configure the Processor and the Exporter
|
||||||
|
processor = MessageProcessor(registry)
|
||||||
|
|
||||||
|
mqtt_client.loop_forever()
|
Loading…
Reference in a new issue