From 88f340af1435ba79f122ed5662bccd54bae90b22 Mon Sep 17 00:00:00 2001 From: neoricalex Date: Sun, 30 Nov 2025 13:20:55 +0100 Subject: [PATCH] Auto-commit via make git (triggered by NFDOS) --- src/neurotron/cortex.py | 61 +++++---- src/neurotron/telemetry.py | 251 +++++++++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+), 27 deletions(-) create mode 100644 src/neurotron/telemetry.py diff --git a/src/neurotron/cortex.py b/src/neurotron/cortex.py index 608a5d1..8978a5b 100644 --- a/src/neurotron/cortex.py +++ b/src/neurotron/cortex.py @@ -15,6 +15,7 @@ from .hippocampus import Hippocampus from .perception import Perception from .motor import Motor from .autodiagnostic import AutoDiagnostic +from .telemetry import TelemetryV5 # <-- NOVO from .neurotron_config import ( NEUROTRON_MODE, NEUROTRON_TICK, @@ -22,7 +23,6 @@ from .neurotron_config import ( NEUROTRON_DIAG_EVERY_TICKS, NEUROTRON_DATASET_PATH, HEARTBEAT_ENABLED, - NEUROTRON_THRESHOLDS, TELEMETRY_MAXLEN, TELEMETRY_FLUSH_EVERY_TICKS, ) @@ -32,7 +32,6 @@ class Cortex: self.runtime_dir = Path(runtime_dir) self.log_dir = Path(log_dir) - # tick pode vir como string → convertemos sempre try: self.tick = float(tick_seconds) except: @@ -55,13 +54,16 @@ class Cortex: EchoAgent(self), ] + # ---- NOVO: Telemetria V5 ---- + self.tele = TelemetryV5(event_callback=self._telemetry_event) + self._booted = False self.telemetry_path = Path(NEUROTRON_DATASET_PATH) / "telemetry.json" self.telemetry_path.parent.mkdir(parents=True, exist_ok=True) # ---------------------------------------- - # Boot + # boot # ---------------------------------------- def boot(self): if self._booted: @@ -71,7 +73,7 @@ class Cortex: self._booted = True # ---------------------------------------- - # Shutdown + Fatal + # shutdown # ---------------------------------------- def shutdown(self, reason=""): logbus.warn(f"Shutdown pedido — {reason}") @@ -104,7 +106,6 @@ class Cortex: if not action: return - # echo (debug) if action.get("action") == "echo": res = self.motor.run("echo", [action.get("text", "")]) msg = res.get("stdout", "").strip() @@ -112,15 +113,26 @@ class Cortex: logbus.info(f"[echo] {msg}") def rest(self): + # ------- Telemetria V5 ------ + try: + tele = self.tele.step() + self.telemetry.append(tele) + except Exception as e: + logbus.error(f"telemetry_step: {e}") + + # ------- Heartbeat visual ------ if HEARTBEAT_ENABLED: self._heartbeat() + # ------- Tick ------ sleep(self._safe_float(self.tick, fallback=1.0)) self._tick_count += 1 + # ------- Diag ------- if self._tick_count % NEUROTRON_DIAG_EVERY_TICKS == 0: self._run_diag() + # ------- Persistência ------- if self._tick_count % TELEMETRY_FLUSH_EVERY_TICKS == 0: self._flush_telemetry() @@ -135,7 +147,7 @@ class Cortex: return fallback # ---------------------------------------- - # heartbeat + # heartbeat — apenas visual, não cognitivo # ---------------------------------------- def _heartbeat(self): snap = self.perception.snapshot() @@ -145,22 +157,24 @@ class Cortex: load = snap.get("loadavg") load1 = self._safe_float(load[0] if load else 0.0, 0.0) - self.telemetry.append({ - "ts": time.time(), - "cpu": cpu, - "mem": mem, - "load1": load1, - "tick": self.tick, - }) - - # log em modo seguro try: logbus.heart(f"cpu={cpu}% mem={mem}% tick={self.tick:.2f}s") except: logbus.heart(f"cpu={cpu}% mem={mem}% tick={self.tick}") # ---------------------------------------- - # diag / homeostase + # telemetria persistência + # ---------------------------------------- + def _flush_telemetry(self): + try: + data = list(self.telemetry) + self.telemetry_path.write_text(json.dumps(data)) + self.memory.remember("telemetry.flush", {}) + except Exception as e: + logbus.error(f"telemetry_flush: {e}") + + # ---------------------------------------- + # diag + homeostase # ---------------------------------------- def _run_diag(self): state, snap = self.diagnostic.run_exam() @@ -181,20 +195,13 @@ class Cortex: self.tick = max(NEUROTRON_TICK_MIN, old - NEUROTRON_TICK_STEP / 2) if self.tick != old: - try: - logbus.info(f"tick ajustado {old:.2f}s → {self.tick:.2f}s") - except: - logbus.info(f"tick ajustado {old} → {self.tick}") + logbus.info(f"tick ajustado {old:.2f}s → {self.tick:.2f}s") # ---------------------------------------- - # telemetria + # callback eventos telemétricos (Hippocampus) # ---------------------------------------- - def _flush_telemetry(self): - try: - self.telemetry_path.write_text(json.dumps(list(self.telemetry))) - self.memory.remember("telemetry.flush", {}) - except Exception as e: - logbus.error(f"telemetry error: {e}") + def _telemetry_event(self, event_name, payload): + self.memory.remember(f"telemetry.event.{event_name}", payload) # ---------------------------------------- # bus interno diff --git a/src/neurotron/telemetry.py b/src/neurotron/telemetry.py new file mode 100644 index 0000000..feee1e7 --- /dev/null +++ b/src/neurotron/telemetry.py @@ -0,0 +1,251 @@ +""" +telemetry.py — Telemetria V5 do Neurotron +----------------------------------------- +Responsável por: + • Medições básicas (CPU, MEM, LOAD, IO) + • Delta entre ciclos + • Aceleração do delta + • Temperatura virtual (fadiga cognitiva) + • Jitter cognitivo (latência entre ciclos) + • FS Health (blocos, erros, RO-mode) + • Eventos telemétricos (V5) + • Classificação de estados cognitivos + +Este módulo é completamente independente: +o Cortex só chama TelemetryV5.step() a cada ciclo. +""" + +import time +import os +import psutil + + +class TelemetryV5: + + # ---------------------------------------------------------- + # Inicialização + # ---------------------------------------------------------- + def __init__(self, event_callback=None): + """ + event_callback(event_name, payload_dict) + """ + self.event_callback = event_callback + + self.last_raw = None + self.last_delta = None + self.last_ts = None + + self.temperature = 0.0 + self.jitter_avg = 0.0 + + # manter último estado cognitivo + self.last_state = "stable" + + # ---------------------------------------------------------- + # Coleta RAW (CPU, MEM, LOAD, IO) + # ---------------------------------------------------------- + def collect_raw(self): + now = time.monotonic() + + cpu = psutil.cpu_percent(interval=None) + mem = psutil.virtual_memory().percent + load = os.getloadavg()[0] # 1-min loadavg + + # IO delta é manual, usamos psutil + io = psutil.disk_io_counters() + disk_total = io.read_bytes + io.write_bytes + + return { + "ts": now, + "cpu": cpu, + "mem": mem, + "load": load, + "disk": disk_total, + } + + # ---------------------------------------------------------- + # Delta = diferença entre ciclos + # ---------------------------------------------------------- + def compute_delta(self, raw): + if self.last_raw is None: + self.last_raw = raw + return {k: 0 for k in raw.keys() if k != "ts"} + + delta = { + "cpu": raw["cpu"] - self.last_raw["cpu"], + "mem": raw["mem"] - self.last_raw["mem"], + "load": raw["load"] - self.last_raw["load"], + "disk": raw["disk"] - self.last_raw["disk"], + } + + self.last_delta = delta + self.last_raw = raw + + return delta + + # ---------------------------------------------------------- + # Aceleração = variação do delta + # ---------------------------------------------------------- + def compute_accel(self, delta): + if self.last_delta is None: + return {k: 0 for k in delta.keys()} + + accel = {k: delta[k] - self.last_delta[k] for k in delta.keys()} + return accel + + # ---------------------------------------------------------- + # Jitter Cognitivo + # ---------------------------------------------------------- + def compute_jitter(self, now): + if self.last_ts is None: + self.last_ts = now + return 0.0 + + jitter = now - self.last_ts + self.last_ts = now + + # média móvel + self.jitter_avg = (self.jitter_avg * 0.9) + (jitter * 0.1) + return jitter + + # ---------------------------------------------------------- + # Temperatura Virtual (fadiga cognitiva) + # ---------------------------------------------------------- + def compute_temperature(self, raw, delta): + # modelo simplificado + score = ( + raw["cpu"] * 0.6 + + raw["load"] * 0.3 + + abs(delta["cpu"]) * 0.2 + + raw["mem"] * 0.1 + ) + + # decay + acumulação + self.temperature = max(0, self.temperature * 0.90 + score * 0.10) + return self.temperature + + # ---------------------------------------------------------- + # FS Health + # ---------------------------------------------------------- + def compute_fs_health(self): + status = { + "read_only": False, + "io_errors": 0, + "free_percent": None + } + + try: + st = psutil.disk_usage("/") + status["free_percent"] = st.free / st.total * 100 + except Exception: + status["free_percent"] = None + + # leitura de erros EXT4 se existir + ext4_err_path = "/sys/fs/ext4/sda1/errors_count" + if os.path.exists(ext4_err_path): + try: + with open(ext4_err_path) as f: + status["io_errors"] = int(f.read().strip()) + except: + pass + + # detetar RO + try: + with open("/tmp/fs_test_rw", "w") as f: + f.write("x") + except Exception: + status["read_only"] = True + + return status + + # ---------------------------------------------------------- + # Classificação de Estado Cognitivo + # ---------------------------------------------------------- + def classify_state(self, temp, jitter): + if temp < 25: + return "stable" + elif temp < 45: + return "warm" + elif temp < 65: + return "hot" + elif temp < 85: + return "critical" + else: + return "recovery" + + # ---------------------------------------------------------- + # Eventos TeleMétricos V5 + # ---------------------------------------------------------- + def detect_events(self, raw, delta, accel, temp, jitter, fs): + events = [] + + # picos + if delta["cpu"] > 15 or accel["cpu"] > 10: + events.append("temp_rising_fast") + + # zona de stress + if raw["cpu"] > 85: + events.append("enter_stress_zone") + + # suspeita de loop + if jitter > (self.jitter_avg * 3): + events.append("loop_suspect") + + # fs + if fs["read_only"] or fs["io_errors"] > 0: + events.append("fs_warning") + + # recuperação + if self.last_state == "critical" and temp < 50: + events.append("recovering") + + # exportar via callback + if self.event_callback: + for e in events: + self.event_callback(e, { + "raw": raw, + "delta": delta, + "accel": accel, + "temp": temp, + "jitter": jitter, + "fs": fs, + }) + + return events + + # ---------------------------------------------------------- + # STEP — chamada a cada ciclo cognitivo + # ---------------------------------------------------------- + def step(self): + """ + Faz um ciclo completo da Telemetria V5: + - raw → delta → accel + - jitter + - temp + - fs health + - estado + - eventos + """ + + raw = self.collect_raw() + delta = self.compute_delta(raw) + accel = self.compute_accel(delta) + jitter = self.compute_jitter(raw["ts"]) + temp = self.compute_temperature(raw, delta) + fs = self.compute_fs_health() + + state = self.classify_state(temp, jitter) + events = self.detect_events(raw, delta, accel, temp, jitter, fs) + + self.last_state = state + + return { + "raw": raw, + "delta": delta, + "accel": accel, + "jitter": jitter, + "temp": temp, + "fs": fs, + "state": state, + "events": events, + }