More then 8 repeaters needed to monitor #157

Closed
opened 2026-10-05 02:00:40 +02:00 by adam · 1 comment
Owner

Hello, Here in Oregon, we have 28 repeaters that connect the Southern half of the state. Trying to figure out how to monitor these in the overnight hours when mesh activity is low? Do we run multiple versions of remote term? is there another way to do this or a special update that would allow this?

Hello, Here in Oregon, we have 28 repeaters that connect the Southern half of the state. Trying to figure out how to monitor these in the overnight hours when mesh activity is low? Do we run multiple versions of remote term? is there another way to do this or a special update that would allow this? <!-- github-sync-id:4309424651 -->
adam closed this issue 2026-10-05 02:00:40 +02:00
Author
Owner

Hey there! I think we've corresponded via email. Nope, no need to run multiple instances of RemoteTerm. The API, actuated by a script, will let you issue whatever commands you want to as many repeaters as you want, whenever you want. From the interactive/GUI side of things, though, the application is capped at eight in order to limit automated traffic being spewed onto the mesh for stability reasons. The API is open so that people can do whatever they want with the interface, but the super-easily-accessible parts via the GUI is more limited to not make it trivial to issue mesh-destabilizing amounts of traffic.

https://github.com/jkingsman/Remote-Terminal-for-MeshCore/issues/32#issuecomment-3985636666 (that was out of date)

The below script provides the bones of a basic script for fetching repeater telemetry; with some minor adjustments or LLM assistance that should be trivial to adapt to provide whatever pattern or volume of repeater tracking you need.

#!/usr/bin/env python3
"""Poll a RemoteTerm repeater for status + sensor telemetry and log to CSV.

pip install requests
python fetch_telemetry_on_interval.py
"""

import csv
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():
    r = requests.post(
        f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/login",
        json={"password": PASSWORD},
        timeout=30,
    )
    r.raise_for_status()


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


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


def collect_sample():
    status = fetch_status()

    row = {"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S")}

    # Status telemetry
    for field in [
        "battery_volts",
        "noise_floor_dbm",
        "last_rssi_dbm",
        "last_snr_db",
        "uptime_seconds",
        "packets_received",
        "packets_sent",
        "airtime_seconds",
        "sent_flood",
        "sent_direct",
        "recv_flood",
        "recv_direct",
    ]:
        row[field] = status.get(field)

    # LPP sensors (best-effort — not all repeaters have these)
    try:
        for sensor in fetch_lpp():
            if not isinstance(sensor.get("value"), dict):
                row[f"lpp_{sensor['type_name']}"] = sensor["value"]
    except Exception:
        pass

    return row


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

    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")

    # Write first sample, then loop
    row = first
    while True:
        parts = [f"{k}={v}" for k, v in row.items() if k != "timestamp"]
        print(f"[{row['timestamp']}] {'  '.join(parts)}")
        writer.writerow(row)
        csv_file.flush()

        time.sleep(POLL_INTERVAL)
        try:
            row = collect_sample()
        except Exception as e:
            print(f"[{time.strftime('%H:%M:%S')}] Error: {e}")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nStopped.")
Hey there! I think we've corresponded via email. Nope, no need to run multiple instances of RemoteTerm. The API, actuated by a script, will let you issue whatever commands you want to as many repeaters as you want, whenever you want. From the interactive/GUI side of things, though, the application is capped at eight in order to limit automated traffic being spewed onto the mesh for stability reasons. The API is open so that people can do whatever they want with the interface, but the super-easily-accessible parts via the GUI is more limited to not make it trivial to issue mesh-destabilizing amounts of traffic. ~~https://github.com/jkingsman/Remote-Terminal-for-MeshCore/issues/32#issuecomment-3985636666~~ (that was out of date) The below script provides the bones of a basic script for fetching repeater telemetry; with some minor adjustments or LLM assistance that should be trivial to adapt to provide whatever pattern or volume of repeater tracking you need. ```python #!/usr/bin/env python3 """Poll a RemoteTerm repeater for status + sensor telemetry and log to CSV. pip install requests python fetch_telemetry_on_interval.py """ import csv 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(): r = requests.post( f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/login", json={"password": PASSWORD}, timeout=30, ) r.raise_for_status() def fetch_status(): r = requests.post( f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/status", timeout=30, ) r.raise_for_status() return r.json() def fetch_lpp(): r = requests.post( f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/lpp-telemetry", timeout=30, ) r.raise_for_status() return r.json()["sensors"] def collect_sample(): status = fetch_status() row = {"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S")} # Status telemetry for field in [ "battery_volts", "noise_floor_dbm", "last_rssi_dbm", "last_snr_db", "uptime_seconds", "packets_received", "packets_sent", "airtime_seconds", "sent_flood", "sent_direct", "recv_flood", "recv_direct", ]: row[field] = status.get(field) # LPP sensors (best-effort — not all repeaters have these) try: for sensor in fetch_lpp(): if not isinstance(sensor.get("value"), dict): row[f"lpp_{sensor['type_name']}"] = sensor["value"] except Exception: pass return row def main(): print(f"Logging in to repeater {REPEATER_PUBKEY[:12]}...") login() print("OK\n") 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") # Write first sample, then loop row = first while True: parts = [f"{k}={v}" for k, v in row.items() if k != "timestamp"] print(f"[{row['timestamp']}] {' '.join(parts)}") writer.writerow(row) csv_file.flush() time.sleep(POLL_INTERVAL) try: row = collect_sample() except Exception as e: print(f"[{time.strftime('%H:%M:%S')}] Error: {e}") if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\nStopped.") ``` <!-- github-comment-id:4300684757 -->
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#157