commit 18c00de38a4a818f2ebf2eba6c30e8d51f41f83e Author: fi Date: Tue Oct 21 02:37:36 2025 +0200 WIP diff --git a/.gitingnore b/.gitingnore new file mode 100644 index 0000000..f7275bb --- /dev/null +++ b/.gitingnore @@ -0,0 +1 @@ +venv/ diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..f6718e9 --- /dev/null +++ b/config.yaml @@ -0,0 +1,27 @@ +prometheus: + pushgateway_address: "localhost:9091" + job: "meshcore_repeater_telemetry" +serial_device_path: "/dev/ttyACM0" +radio_settings: + freq: 869.618 + bw: 62.5 + sf: 8 + cr: 8 +retries: 2 +timeout: 20 +repeaters: + # stay_hydrated_01 + - contact_data: "11004a629a62e51f0ec770afc47bb22010df0ac1c47475499b6b5e47b3487a4f71e680440669d4d9bdfacc88e82e441a86026082f74a53e962c7cf3981cdc2bcaa4a820ce77bf71ad2eddf196524e1b5397e9d105eacbfd866e211957a80c5eda788eb84fb01925916320377689600737461795f68796472617465645f3031" + password: "" + path: [] + # 25469 - hansemesh.de + - contact_data: "11014aa73518d39789c74af4ad460e31833cd824a5bc0f3df5482ecdf96eec2e30989cb53f0669defe3a07e35089ba86a739544dee340fc489d18379d2b6682eacf4b75b3e9a77f3fcae93bc7e26377977e0a74f0a84deb0cead20e98f62533e78f98a68de600b92484532035d0896003235343639202d2068616e73656d6573682e6465" + password: "" + path: + - "4a" + # CCCHH + - contact_data: "1102a74accc426cdf274e5b90dff50cc1081ab7195fb8166285a29b39b106fe31e80be6b19a0f768651168d5be7bce6ba4145a473045564e5ac83c014aa764dc94e31e22aee35460c86c39d8c4acd27647340264b0e7c5d82919e4eacc7ae796f92b071a6d09130592f43b310320bb97004343434848" + password: "" + path: + - "4a" + - "f4" diff --git a/main.py b/main.py new file mode 100755 index 0000000..9ce22b4 --- /dev/null +++ b/main.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import asyncio +import json +import yaml +import time +from meshcore import MeshCore, EventType, packets +from prometheus_client import CollectorRegistry, Gauge, push_to_gateway + +def parse_public_key_from_contact_data(contact_data): + public_key_length = 64 + packet_type_length = len(str(packets.PacketType.CONTACT_URI.value)) + packet_path_specifier_length = 2 + if contact_data and contact_data.startswith(str(packets.PacketType.CONTACT_URI.value)): + path_length = int(contact_data[packet_type_length:packet_type_length+packet_path_specifier_length]) + offset = packet_type_length + packet_path_specifier_length + path_length * 2 + return contact_data[offset:offset+public_key_length] + return False + +async def setup(meshcore, config): + await meshcore.commands.set_time(int(time.time())) + result = await meshcore.commands.set_radio(config["radio_settings"]["freq"], config["radio_settings"]["bw"], config["radio_settings"]["sf"], config["radio_settings"]["cr"]) + if result.type is EventType.ERROR: + print("Failed to setup radio") + exit(1) + await meshcore.commands.set_manual_add_contacts(False) + for repeater in config["repeaters"]: + public_key = parse_public_key_from_contact_data(repeater["contact_data"]) + if not public_key: + print("Failed to parse public key from contact data: %s" % repeater["contact_data"]) + continue + repeater["public_key"] = public_key + try: + result = await meshcore.commands.import_contact(bytes.fromhex(repeater["contact_data"])) + if result.type is EventType.ERROR: + raise Exception("Failed to import contact") + except: + print("Failed add contact from contact data: %s\nAborting." % repeater["contact_data"]) + exit(1) + await meshcore.commands.set_manual_add_contacts(True) + await meshcore.ensure_contacts() + +async def main(): + with open("config.yaml", "r") as file: + config = yaml.safe_load(file) + + meshcore = await MeshCore.create_serial(config["serial_device_path"]) + await setup(meshcore, config) + + for repeater in config["repeaters"]: + contact = meshcore.get_contact_by_key_prefix(repeater["public_key"]) + await meshcore.commands.change_contact_path(contact, "".join(repeater["path"])) + print(contact) + + tries = 0 + while(tries <= config["retries"]): + tries += 1 + + await meshcore.commands.send_login(contact, repeater["password"]) + result = await meshcore.wait_for_event(EventType.LOGIN_SUCCESS, timeout=config["timeout"]) + if result is None: + print("Timeout waiting on login for %s" % repeater["public_key"]) + else: + break + + if tries == config["retries"] + 1: + print("Maximum login retries exceeded for %s" % repeater["public_key"]) + break + + result = None + tries = 0 + while(tries <= config["retries"]): + tries += 1 + + result = await meshcore.commands.send_telemetry_req(repeater["public_key"]) + if result.type == EventType.ERROR: + print("Error sending telemetry request for %s" % repeater["public_key"]) + continue + + result = await meshcore.wait_for_event(EventType.TELEMETRY_RESPONSE, timeout=config["timeout"]) + if result is None: + print("Timeout waiting on telemetry for %s" % repeater["public_key"]) + continue + else: + break + + if tries == config["retries"] + 1: + print("Maximum telemetry request retries exceeded for %s" % repeater["public_key"]) + else: + telemetry_data_list = result.payload["lpp"] + + registry = CollectorRegistry() + for telemetry_data in telemetry_data_list: + if telemetry_data["type"] == "voltage": + gauge = Gauge("voltage", "Battery Voltage", registry=registry) + gauge.set(telemetry_data["value"]) + push_to_gateway(config["prometheus"]["pushgateway_address"], job=config["prometheus"]["job"], grouping_key={"instance": contact["adv_name"]}, registry=registry) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..02c28ec --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +bleak==1.1.1 +dbus-fast==2.44.5 +meshcore==2.1.12 +prometheus_client==0.23.1 +pycayennelpp==2.4.0 +pyserial==3.5 +pyserial-asyncio==0.6