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 -->
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.
from__future__importannotationsimportcsvimportioimportsysimporttimefrompathlibimportPathimportrequests# ── Configure these ──────────────────────────────────────────────────────────BASE_URL="http://localhost:8000"REPEATER_PUBKEY="put_your_64char_hex_public_key_here"PASSWORD=""# empty string for guest loginPOLL_INTERVAL=60# seconds between samplesCSV_PATH=Path("repeater_log.csv")# ─────────────────────────────────────────────────────────────────────────────deflogin()->None:r=requests.post(f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/login",json={"password":PASSWORD},timeout=30,)r.raise_for_status()deffetch_status()->dict:r=requests.post(f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/status",timeout=30,)r.raise_for_status()returnr.json()deffetch_lpp()->list[dict]:r=requests.post(f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}/repeater/lpp-telemetry",timeout=30,)r.raise_for_status()returnr.json()["sensors"]deffetch_contact()->dict:r=requests.get(f"{BASE_URL}/api/contacts/{REPEATER_PUBKEY}",timeout=10,)r.raise_for_status()returnr.json()defcollect_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"forsensorinsensors:col=f"lpp_{sensor['type_name']}"row[col]=sensor["value"]returnrowdefprint_row(row:dict)->None:buf=io.StringIO()parts=[f"{k}={v}"fork,vinrow.items()ifk!="timestamp"]buf.write(f"[{row['timestamp']}] ")buf.write(" ".join(parts))print(buf.getvalue())defmain()->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")ifnotfile_exists:writer.writeheader()print(f"Writing to {CSV_PATH} (Ctrl-C to stop)\n")print_row(first)writer.writerow(first)csv_file.flush()whileTrue:time.sleep(POLL_INTERVAL)try:row=collect_sample()print_row(row)writer.writerow(row)csv_file.flush()exceptrequests.HTTPErrorase:print(f"[{time.strftime('%H:%M:%S')}] HTTP {e.response.status_code}: {e.response.text[:120]}")exceptrequests.ConnectionError:print(f"[{time.strftime('%H:%M:%S')}] Connection failed — server down?")if__name__=="__main__":try:main()exceptKeyboardInterrupt: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 -->
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 -->
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 -->
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 -->
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 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.
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
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!
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
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).
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.
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.