fix(beacon): convert symlink to standalone executable with WAN bridge and phone detection

This commit is contained in:
Kryštof Pešek
2026-11-05 17:14:57 +01:00
parent 5206f09406
commit d22ccfe141
-1
View File
@@ -1 +0,0 @@
/home/kof/src/observator/otop/fleet-beacon.py
Executable
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""
fleet-beacon.py — Multi-node Realtime Telemetry Beacon
Part of Observatoř Top (otop)
Broadcasts lightweight UDP telemetry packets (port 9850) every 1.5s.
Zero external dependencies, minimal CPU impact (< 0.05%).
"""
import os
import sys
import time
import json
import socket
import shutil
import glob
import urllib.request
PORT = 9850
TARGETS = ["<broadcast>", "192.168.1.255", "192.168.1.100", "192.168.1.102", "10.0.0.255", "10.0.0.18", "10.0.0.14", "91.139.118.43", "192.168.43.255", "192.168.43.1", "127.0.0.1"]
def get_hostname():
if os.path.exists("/data/data/com.termux") or "TERMUX_VERSION" in os.environ:
return "phone"
h = socket.gethostname().split('.')[0]
if h in ("localhost", "android"):
return "phone"
return h
def read_cpu_percent(prev_stat):
try:
with open("/proc/stat", "r") as f:
fields = [float(x) for x in f.readline().strip().split()[1:8]]
# fields: user, nice, system, idle, iowait, irq, softirq
idle = fields[3] + fields[4]
total = sum(fields)
if prev_stat is not None:
prev_idle, prev_total = prev_stat
d_idle = idle - prev_idle
d_total = total - prev_total
pct = 100.0 * (1.0 - (d_idle / max(1.0, d_total)))
return round(max(0.0, min(100.0, pct)), 1), (idle, total)
return 0.0, (idle, total)
except Exception:
return 0.0, prev_stat
def read_memory():
total_gb, used_gb = 0.0, 0.0
try:
with open("/proc/meminfo", "r") as f:
mem = {}
for line in f:
parts = line.split(":")
if len(parts) == 2:
mem[parts[0].strip()] = float(parts[1].strip().split()[0])
total_kb = mem.get("MemTotal", 0.0)
avail_kb = mem.get("MemAvailable", mem.get("MemFree", 0.0))
total_gb = round(total_kb / (1024.0 * 1024.0), 1)
used_gb = round((total_kb - avail_kb) / (1024.0 * 1024.0), 1)
except Exception:
pass
return used_gb, total_gb
def read_disk():
hostname = get_hostname()
check_paths = ["/"]
if hostname == "boxC":
check_paths = ["/mnt/kof17", "/"]
elif hostname == "bazina":
check_paths = ["/mnt/ssd", "/mnt/seagate", "/"]
for p in check_paths:
if os.path.exists(p):
try:
du = shutil.disk_usage(p)
free_gb = round(du.free / (1024.0 ** 3), 1)
total_gb = round(du.total / (1024.0 ** 3), 1)
return p, free_gb, total_gb
except Exception:
continue
return "/", 0.0, 0.0
def read_gpu():
# AMD GPU busy percent (sysfs)
for path in glob.glob("/sys/class/drm/card*/device/gpu_busy_percent"):
try:
with open(path, "r") as f:
val = f.read().strip()
return f"Navi10 {val}%"
except Exception:
pass
return ""
def read_temp():
for path in ["/sys/class/thermal/thermal_zone0/temp", "/sys/class/hwmon/hwmon0/temp1_input"]:
if os.path.exists(path):
try:
with open(path, "r") as f:
return int(int(f.read().strip()) / 1000)
except Exception:
pass
return 0
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
hostname = get_hostname()
prev_stat = None
print(f"[fleet-beacon] Starting telemetry beacon on {hostname} (port {PORT})...")
while True:
cpu_pct, prev_stat = read_cpu_percent(prev_stat)
mem_used, mem_total = read_memory()
mount, disk_free, disk_total = read_disk()
gpu_str = read_gpu()
temp_c = read_temp()
alert = ""
if hostname == "boxC" and mount == "/mnt/kof17" and disk_free < 3.0:
alert = f"kof17 CRITICAL: {disk_free}G free"
elif hostname == "penguin" and disk_free < 1.0:
alert = f"penguin LOW DISK: {disk_free}G free"
payload = {
"name": hostname,
"hostname": hostname,
"timestamp": time.time(),
"cpu_percent": cpu_pct,
"mem_used_gb": mem_used,
"mem_total_gb": mem_total,
"disk_mount": mount,
"disk_free_gb": disk_free,
"disk_total_gb": disk_total,
"gpu_str": gpu_str,
"temp_c": temp_c,
"alert": alert
}
msg = json.dumps(payload).encode("utf-8")
for target in TARGETS:
try:
sock.sendto(msg, (target, PORT))
except Exception:
pass
# Update local SHM cache
try:
shm_fleet = "/dev/shm/fleet_telemetry.json"
data = {}
if os.path.exists(shm_fleet):
try:
with open(shm_fleet, "r") as f:
data = json.load(f)
except Exception:
data = {}
data[hostname] = payload
tmp_path = f"{shm_fleet}.tmp.{os.getpid()}"
with open(tmp_path, "w") as f:
json.dump(data, f)
os.replace(tmp_path, shm_fleet)
except Exception:
pass
# WAN HTTP bridge fallback for mobile/remote nodes (e.g. Staroměstská)
if hostname != "boxC":
try:
req = urllib.request.Request(
"http://91.139.118.43/telemetry.php",
data=msg,
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=1.5) as resp:
if resp.status == 200:
remote_data = json.loads(resp.read().decode("utf-8"))
if isinstance(remote_data, dict) and "boxC" in remote_data:
shm_fleet = "/dev/shm/fleet_telemetry.json"
tmp_path = f"{shm_fleet}.tmp.{os.getpid()}"
with open(tmp_path, "w") as f:
json.dump(remote_data, f)
os.replace(tmp_path, shm_fleet)
except Exception:
pass
time.sleep(1.5)
if __name__ == "__main__":
main()