#!/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 = ["", "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 read_battery(): # Probe ACPI / sysfs battery capacity for p in glob.glob("/sys/class/power_supply/BAT*/capacity") + glob.glob("/sys/class/power_supply/*battery*/capacity") + glob.glob("/sys/class/power_supply/sbs-*/capacity"): try: with open(p, "r") as f: return int(f.read().strip()) except Exception: pass return -1 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() bat_pct = read_battery() 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" elif bat_pct >= 0 and bat_pct <= 15: alert = f"{hostname} BATTERY CRITICAL: {bat_pct}%" 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, "battery_percent": bat_pct, "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 # Merge any WAN telemetry files (< 30s old) try: import glob for wpath in glob.glob("/dev/shm/fleet_wan_*.json"): if time.time() - os.path.getmtime(wpath) < 30: with open(wpath, "r") as wf: wpayload = json.load(wf) if isinstance(wpayload, dict) and "name" in wpayload: data[wpayload["name"]] = wpayload except Exception: pass tmp_path = f"{shm_fleet}.tmp.{os.getpid()}" with open(tmp_path, "w") as f: json.dump(data, f) try: os.chmod(tmp_path, 0o666) except Exception: pass 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()