Feature request: Node telemetry monitoring (SoC, GPS, etc) #10

Closed
opened 2026-10-05 01:52:17 +02:00 by adam · 5 comments
Owner

Thank you for this software, everything has been working great!

An awesome feature would be some sort of scheduled monitoring of a chosen subset of repeaters/nodes.

A concrete example is solar repeater nodes. Having a way to get an overview of the battery history could help with detecting and fixing issues before the node dies completely.

A fun (but not critical) feature would be monitoring/polling of GPS enabled companion nodes.

My understanding is that this would need some sort of scheduled polling system, increasing airtime. So sane intervals would be needed. I don't know if battery state is sent in adverts, if so then this could be used instead.

Thank you for this software, everything has been working great! An awesome feature would be some sort of scheduled monitoring of a chosen subset of repeaters/nodes. A concrete example is solar repeater nodes. Having a way to get an overview of the battery history could help with detecting and fixing issues before the node dies completely. A fun (but not critical) feature would be monitoring/polling of GPS enabled companion nodes. My understanding is that this would need some sort of scheduled polling system, increasing airtime. So sane intervals would be needed. I don't know if battery state is sent in adverts, if so then this could be used instead. <!-- github-sync-id:4010031707 -->
adam closed this issue 2026-10-05 01:52:17 +02:00
Author
Owner

So glad you're finding it useful!

I do see the appeal of the idea. I'm hesitant, as a matter of design choice, to add anything to emit radio traffic that's not in response to user impulse (advertise, outgoing message) or another message (bot responses). I think what you're looking to do could be pretty trivially accomplished with a python script to dump out the data, though. I had an LLM whack out a simple ingest script that can use RemoteTerm endpoints to track the data.

EDIT 2026-04-22: This script is outmoded and won't work as it used to. Use https://github.com/jkingsman/Remote-Terminal-for-MeshCore/issues/217#issuecomment-4300684757 as a reference now.

LLM generated script
from __future__ import annotations

import csv
import io
import sys
import time
from pathlib import Path

import requests

# ── Configure these ──────────────────────────────────────────────────────────
BASE_URL = "http://localhost:8000"
REPEATER_PUBKEY = "put_your_64char_hex_public_key_here"
PASSWORD = ""  # empty string for guest login
POLL_INTERVAL = 60  # seconds between samples
CSV_PATH = Path("repeater_log.csv")
# ─────────────────────────────────────────────────────────────────────────────


def login() -> None:
    r = requests.post(
        f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/login",
        json={"password": PASSWORD},
        timeout=30,
    )
    r.raise_for_status()


def fetch_status() -> dict:
    r = requests.post(
        f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/status",
        timeout=30,
    )
    r.raise_for_status()
    return r.json()


def fetch_lpp() -> list[dict]:
    r = requests.post(
        f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/lpp-telemetry",
        timeout=30,
    )
    r.raise_for_status()
    return r.json()["sensors"]


def fetch_contact() -> dict:
    r = requests.get(
        f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}",
        timeout=10,
    )
    r.raise_for_status()
    return r.json()


def collect_sample() -> dict:
    """Gather one combined sample from all three endpoints."""
    status = fetch_status()
    contact = fetch_contact()
    sensors = fetch_lpp()

    row = {
        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
        # Status telemetry
        "battery_volts": status["battery_volts"],
        "noise_floor_dbm": status["noise_floor_dbm"],
        "last_rssi_dbm": status["last_rssi_dbm"],
        "last_snr_db": status["last_snr_db"],
        "uptime_seconds": status["uptime_seconds"],
        "packets_received": status["packets_received"],
        "packets_sent": status["packets_sent"],
        # Contact record (GPS position)
        "lat": contact.get("lat"),
        "lon": contact.get("lon"),
    }

    # Flatten LPP sensors into columns like "lpp_temperature", "lpp_humidity"
    for sensor in sensors:
        col = f"lpp_{sensor['type_name']}"
        row[col] = sensor["value"]

    return row


def print_row(row: dict) -> None:
    buf = io.StringIO()
    parts = [f"{k}={v}" for k, v in row.items() if k != "timestamp"]
    buf.write(f"[{row['timestamp']}] ")
    buf.write("  ".join(parts))
    print(buf.getvalue())


def main() -> None:
    print(f"Logging in to repeater {REPEATER_PUBKEY[:12]}...")
    login()
    print("OK\n")

    # Collect first sample to discover all columns (LPP sensors vary by device)
    first = collect_sample()
    columns = list(first.keys())

    file_exists = CSV_PATH.exists()
    csv_file = open(CSV_PATH, "a", newline="")
    writer = csv.DictWriter(csv_file, fieldnames=columns, extrasaction="ignore")
    if not file_exists:
        writer.writeheader()

    print(f"Writing to {CSV_PATH}  (Ctrl-C to stop)\n")
    print_row(first)
    writer.writerow(first)
    csv_file.flush()

    while True:
        time.sleep(POLL_INTERVAL)
        try:
            row = collect_sample()
            print_row(row)
            writer.writerow(row)
            csv_file.flush()
        except requests.HTTPError as e:
            print(f"[{time.strftime('%H:%M:%S')}] HTTP {e.response.status_code}: {e.response.text[:120]}")
        except requests.ConnectionError:
            print(f"[{time.strftime('%H:%M:%S')}] Connection failed — server down?")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nStopped.")

I totally get that part of the appeal would probably be having this as a sort of "dashboard" display, but right now the idea of long-running background tasks that are generating autonomous radio traffic is something I'd actually prefer to still have a somewhat higher barrier of entry to, just because it's so easy to clog up the mesh. I'll think about it, though.

Thanks for the idea!

So glad you're finding it useful! I do see the appeal of the idea. I'm hesitant, as a matter of design choice, to add anything to emit radio traffic that's not in response to user impulse (advertise, outgoing message) or another message (bot responses). I think what you're looking to do could be pretty trivially accomplished with a python script to dump out the data, though. I had an LLM whack out a simple ingest script that can use RemoteTerm endpoints to track the data. **EDIT 2026-04-22: This script is outmoded and won't work as it used to. Use https://github.com/jkingsman/Remote-Terminal-for-MeshCore/issues/217#issuecomment-4300684757 as a reference now.** <details> <summary>LLM generated script</summary> ```python from __future__ import annotations import csv import io import sys import time from pathlib import Path import requests # ── Configure these ────────────────────────────────────────────────────────── BASE_URL = "http://localhost:8000" REPEATER_PUBKEY = "put_your_64char_hex_public_key_here" PASSWORD = "" # empty string for guest login POLL_INTERVAL = 60 # seconds between samples CSV_PATH = Path("repeater_log.csv") # ───────────────────────────────────────────────────────────────────────────── def login() -> None: r = requests.post( f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/login", json={"password": PASSWORD}, timeout=30, ) r.raise_for_status() def fetch_status() -> dict: r = requests.post( f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/status", timeout=30, ) r.raise_for_status() return r.json() def fetch_lpp() -> list[dict]: r = requests.post( f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/lpp-telemetry", timeout=30, ) r.raise_for_status() return r.json()["sensors"] def fetch_contact() -> dict: r = requests.get( f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}", timeout=10, ) r.raise_for_status() return r.json() def collect_sample() -> dict: """Gather one combined sample from all three endpoints.""" status = fetch_status() contact = fetch_contact() sensors = fetch_lpp() row = { "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), # Status telemetry "battery_volts": status["battery_volts"], "noise_floor_dbm": status["noise_floor_dbm"], "last_rssi_dbm": status["last_rssi_dbm"], "last_snr_db": status["last_snr_db"], "uptime_seconds": status["uptime_seconds"], "packets_received": status["packets_received"], "packets_sent": status["packets_sent"], # Contact record (GPS position) "lat": contact.get("lat"), "lon": contact.get("lon"), } # Flatten LPP sensors into columns like "lpp_temperature", "lpp_humidity" for sensor in sensors: col = f"lpp_{sensor['type_name']}" row[col] = sensor["value"] return row def print_row(row: dict) -> None: buf = io.StringIO() parts = [f"{k}={v}" for k, v in row.items() if k != "timestamp"] buf.write(f"[{row['timestamp']}] ") buf.write(" ".join(parts)) print(buf.getvalue()) def main() -> None: print(f"Logging in to repeater {REPEATER_PUBKEY[:12]}...") login() print("OK\n") # Collect first sample to discover all columns (LPP sensors vary by device) first = collect_sample() columns = list(first.keys()) file_exists = CSV_PATH.exists() csv_file = open(CSV_PATH, "a", newline="") writer = csv.DictWriter(csv_file, fieldnames=columns, extrasaction="ignore") if not file_exists: writer.writeheader() print(f"Writing to {CSV_PATH} (Ctrl-C to stop)\n") print_row(first) writer.writerow(first) csv_file.flush() while True: time.sleep(POLL_INTERVAL) try: row = collect_sample() print_row(row) writer.writerow(row) csv_file.flush() except requests.HTTPError as e: print(f"[{time.strftime('%H:%M:%S')}] HTTP {e.response.status_code}: {e.response.text[:120]}") except requests.ConnectionError: print(f"[{time.strftime('%H:%M:%S')}] Connection failed — server down?") if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\nStopped.") ``` </details> I totally get that part of the appeal would probably be having this as a sort of "dashboard" display, but right now the idea of long-running background tasks that are generating autonomous radio traffic is something I'd actually prefer to still have a somewhat higher barrier of entry to, just because it's so easy to clog up the mesh. I'll think about it, though. Thanks for the idea! <!-- github-comment-id:3985636666 -->
Author
Owner

That does sound reasonable.

I tested out the LLM script and it worked first try. Ill adapt it for my use case for posting to grafana/HA once a day.

Is the battery as a percentage value provided in the LPP packets? Or is that something done client side? I noticed that we never see the percentage as an attribute here, just voltage

That does sound reasonable. I tested out the LLM script and it worked first try. Ill adapt it for my use case for posting to grafana/HA once a day. Is the battery as a percentage value provided in the LPP packets? Or is that something done client side? I noticed that we never see the percentage as an attribute here, just voltage <!-- github-comment-id:3989630034 -->
Author
Owner

No, battery as raw millivoltage only (and it fluctuates a lot in my experience; I suspect it's due to a combination of inconsistent activity level/current draw and the board just not having a great voltage sensor).

No, battery as raw millivoltage only (and it fluctuates a lot in my experience; I suspect it's due to a combination of inconsistent activity level/current draw and the board just not having a great voltage sensor). <!-- github-comment-id:3992187142 -->
Author
Owner

Let me know if you had other thoughts on this -- as is, I'm inclined to close with this status, but I don't want to cut the conversation off if you have more conversation you'd like to have.

Let me know if you had other thoughts on this -- as is, I'm inclined to close with this status, but I don't want to cut the conversation off if you have more conversation you'd like to have. <!-- github-comment-id:4002584693 -->
Author
Owner

I got ground down 😜 Opt-in telemetry tracking for up to eight repeaters, polling once every eight hours, is now supported on the main branch. A docker build will come later tonight.

I got ground down 😜 Opt-in telemetry tracking for up to eight repeaters, polling once every eight hours, is now supported on the main branch. A docker build will come later tonight. <!-- github-comment-id:4185137411 -->
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/Remote-Terminal-for-MeshCore#10