From 2ab8a46d44fd9b27b070586c2b987e11cd462616 Mon Sep 17 00:00:00 2001 From: kassam Date: Tue, 14 Jul 2026 09:34:44 +0400 Subject: [PATCH] Update 2026-07-14 09:34:44 --- agents/g1/sanad_api_g1.py | 164 +++++++++++++++++++++++++++++++++--- agents/go2/sanad_api_go2.py | 157 +++++++++++++++++++++++++++++++--- agents/r1/sanad_api_r1.py | 164 +++++++++++++++++++++++++++++++++--- fleet_install.sh | 1 + 4 files changed, 453 insertions(+), 33 deletions(-) diff --git a/agents/g1/sanad_api_g1.py b/agents/g1/sanad_api_g1.py index bdd8593..b39db9e 100644 --- a/agents/g1/sanad_api_g1.py +++ b/agents/g1/sanad_api_g1.py @@ -145,6 +145,8 @@ class Config: project_log_path: str project_log_label: str project_log_backfill: int + project_log_exclude: str + ros_distro: str # transport verify_tls: bool http_timeout: float @@ -197,6 +199,11 @@ class Config: project_log_path=_env("PROJECT_LOG_PATH", ""), project_log_label=_env("PROJECT_LOG_LABEL", ""), project_log_backfill=int(_env("PROJECT_LOG_BACKFILL", "100")), + # drop uvicorn/access-log noise so shipped lines match the project's + # own LIVE LOGS panel exactly (app-logger lines + tracebacks only) + project_log_exclude=_env("PROJECT_LOG_EXCLUDE", + r"^(INFO|WARNING|ERROR|DEBUG|CRITICAL):\s"), + ros_distro=_env("SOFTWARE_ROS", ""), verify_tls=_env_bool("VERIFY_TLS", True), http_timeout=float(_env("HTTP_TIMEOUT", "30")), ) @@ -232,6 +239,93 @@ def read_mac(interface: str) -> str: return ":".join(f"{(n >> b) & 0xff:02x}" for b in range(40, -1, -8)) +_AGENT_VERSION = "2026.07.13" + +_software_cache: Optional[Dict[str, Any]] = None + + +def read_software(cfg: Config) -> Dict[str, Any]: + """Robot software/OS card: ROS distro, host OS (via /host), kernel, arch. + The container shares the HOST kernel; the host OS comes from + /host/etc/os-release (the read-only /:/host mount).""" + global _software_cache + if _software_cache is not None: + return _software_cache + sw: Dict[str, Any] = {} + # ROS distro: pinned via SOFTWARE_ROS, else detect host installs /opt/ros/* + ros = cfg.ros_distro + if not ros: + try: + for base in ("/host/opt/ros", "/opt/ros"): + p = Path(base) + if p.is_dir(): + distros = sorted(d.name for d in p.iterdir() if d.is_dir()) + if distros: + ros = ",".join(distros) + break + except Exception: + pass + sw["ros"] = ros or None + # host OS (firmware/OS card) + os_name = os_ver = None + for rel in ("/host/etc/os-release", "/etc/os-release"): + try: + kv = {} + for line in Path(rel).read_text().splitlines(): + if "=" in line: + k, _, v = line.partition("=") + kv[k] = v.strip().strip('"') + os_name = kv.get("PRETTY_NAME") or kv.get("NAME") + os_ver = kv.get("VERSION_ID") + break + except Exception: + continue + sw["os"] = os_name # e.g. "Ubuntu 20.04.6 LTS" + sw["os_version"] = os_ver # e.g. "20.04" + u = os.uname() + sw["kernel"] = u.release # host kernel (shared with the container) + sw["arch"] = u.machine # e.g. aarch64 + sw["python"] = ".".join(map(str, sys.version_info[:3])) + sw["agent"] = f"{log.name} {_AGENT_VERSION}" + _software_cache = sw + return sw + + +_firmware_cache: Optional[Dict[str, Any]] = None + + +def read_firmware_static() -> Dict[str, Any]: + """Board-level firmware card (static, via /host): compute board model + + Jetson L4T/BSP release + kernel. DDS adds live robot/bms fw versions.""" + global _firmware_cache + if _firmware_cache is not None: + return dict(_firmware_cache) + fw: Dict[str, Any] = {} + # board model, e.g. "NVIDIA Orin NX Developer Kit" + for p in ("/host/sys/firmware/devicetree/base/model", + "/host/proc/device-tree/model", + "/sys/firmware/devicetree/base/model", + "/proc/device-tree/model"): + try: + fw["board"] = Path(p).read_bytes().decode().strip("\x00 \n") + break + except Exception: + continue + # Jetson L4T/BSP: "# R35 (release), REVISION: 3.1, ..." -> "R35.3.1" + for p in ("/host/etc/nv_tegra_release", "/etc/nv_tegra_release"): + try: + head = Path(p).read_text().splitlines()[0] + import re + m = re.search(r"(R\d+).*?REVISION:\s*([\d.]+)", head) + fw["l4t"] = f"{m.group(1)}.{m.group(2)}" if m else head.lstrip("# ").strip() + break + except Exception: + continue + fw["kernel"] = os.uname().release + _firmware_cache = fw + return dict(fw) + + _data_size_cache: Dict[str, Any] = {"ts": 0.0, "kb": None} @@ -285,6 +379,7 @@ class DDSReader: self._temps: List[float] = [] self._max_dq = 0.0 self._xy: Optional[Dict[str, float]] = None + self._fw: Dict[str, Any] = {} # live fw versions (robot ctrl + bms) self._loco = None self.ok = False self._start() @@ -372,6 +467,13 @@ class DDSReader: temp_c = max(vals) except Exception: temp_c = None + # BMS firmware version (version_high.version_low) + try: + vh, vl = getattr(msg, "version_high", None), getattr(msg, "version_low", None) + if vh is not None: + self._fw["bms"] = f"{int(vh)}.{int(vl or 0)}" + except Exception: + pass with self._lock: self._bms = { "soc": max(0, min(100, soc)), @@ -387,6 +489,14 @@ class DDSReader: def _on_low(self, msg) -> None: try: + # robot controller firmware version (LowState.version array) + try: + v = getattr(msg, "version", None) + if v is not None and "robot" not in self._fw: + vals = [int(x) for x in v] if hasattr(v, "__iter__") else [int(v)] + self._fw["robot"] = ".".join(map(str, vals)) + except Exception: + pass temps: List[float] = [] max_dq = 0.0 for m in (getattr(msg, "motor_state", None) or []): @@ -429,6 +539,7 @@ class DDSReader: "temps": list(self._temps), "max_dq": self._max_dq, "xy": dict(self._xy) if self._xy else None, + "fw": dict(self._fw), } def fsm_id(self) -> Optional[int]: @@ -1018,6 +1129,13 @@ class ProjectLogTail: self._cur: Optional[Path] = None self._pos = 0 self._backfill = max(0, cfg.project_log_backfill) + self._exclude = None + if cfg.project_log_exclude: + try: + import re + self._exclude = re.compile(cfg.project_log_exclude) + except Exception: + self._exclude = None self.active = False try: self._resolve(cfg) @@ -1071,26 +1189,44 @@ class ProjectLogTail: def _start(self, p: Path, label: str) -> None: self._cur = p + self.label = label size = p.stat().st_size - self._pos = size # default: only NEW lines ship - # backfill: start N lines before EOF so recent history ships at startup + self._pos = size # new lines ship from here on + self._pending: List[str] = [] + # backfill: the last N RELEVANT lines (post-filter) from the last ~1 MB, + # so the noise (access-log spam) doesn't eat the history window if self._backfill and size: try: - take = min(size, 512 * 1024) + take = min(size, 8 * 1024 * 1024) with p.open("rb") as f: f.seek(size - take) tail = f.read(take) - parts = tail.splitlines(keepends=True)[-self._backfill:] - self._pos = size - sum(len(x) for x in parts) + raw = tail.decode("utf-8", "replace").splitlines() + if take < size and raw: + raw = raw[1:] # first line is a partial record — drop it + for ln in raw: + ln = ln.strip() + if ln.startswith("{"): + try: + ln = (json.loads(ln).get("log") or "").rstrip() + except Exception: + pass + if not ln: + continue + if self._exclude is not None and self._exclude.search(ln): + continue + self._pending.append(f"[{label}] {ln}") + self._pending = self._pending[-self._backfill:] except Exception: - self._pos = size - self.label = label + self._pending = [] self.active = True def poll(self) -> List[str]: - """New lines since last poll (docker json-log unwrapped), labeled.""" + """New lines since last poll (docker json-log unwrapped), labeled. + The startup backfill (last N relevant lines) is returned on first call.""" if not self.active or self._cur is None: return [] + pend, self._pending = self._pending, [] out: List[str] = [] try: st = self._cur.stat() @@ -1108,11 +1244,14 @@ class ProjectLogTail: ln = (json.loads(ln).get("log") or "").rstrip() except Exception: pass - if ln: - out.append(f"[{self.label}] {ln}") + if not ln: + continue + if self._exclude is not None and self._exclude.search(ln): + continue # access-log noise — not in the project's log panel + out.append(f"[{self.label}] {ln}") except Exception as e: log.debug("project-log poll failed: %s", e) - return out[-100:] # cap per cycle + return pend + out[-100:] # backfill first, then ≤100 new/cycle _PROJECT_TAIL: Optional[ProjectLogTail] = None @@ -1252,6 +1391,9 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], "brand": cfg.brand, "type": cfg.robot_type, # humanoid "model": cfg.model, # g1 + "software": read_software(cfg), # ros/os/kernel/arch/python/agent + "firmware": {**read_firmware_static(), **(snap.get("fw") or {})}, + # board/l4t/kernel + live robot/bms fw versions "battery": battery, # null = couldn't read (heartbeat) "charging": charging, "battery_detail": battery_detail, diff --git a/agents/go2/sanad_api_go2.py b/agents/go2/sanad_api_go2.py index f4d73f8..3d51d5d 100644 --- a/agents/go2/sanad_api_go2.py +++ b/agents/go2/sanad_api_go2.py @@ -128,6 +128,8 @@ class Config: project_log_path: str project_log_label: str project_log_backfill: int + project_log_exclude: str + ros_distro: str verify_tls: bool http_timeout: float @@ -176,6 +178,9 @@ class Config: project_log_path=_env("PROJECT_LOG_PATH", ""), project_log_label=_env("PROJECT_LOG_LABEL", ""), project_log_backfill=int(_env("PROJECT_LOG_BACKFILL", "100")), + project_log_exclude=_env("PROJECT_LOG_EXCLUDE", + r"^(INFO|WARNING|ERROR|DEBUG|CRITICAL):\s"), + ros_distro=_env("SOFTWARE_ROS", ""), verify_tls=_env_bool("VERIFY_TLS", True), http_timeout=float(_env("HTTP_TIMEOUT", "10")), ) @@ -208,6 +213,93 @@ def read_mac(interface: str) -> str: return ":".join(f"{(n >> b) & 0xff:02x}" for b in range(40, -1, -8)) +_AGENT_VERSION = "2026.07.13" + +_software_cache: Optional[Dict[str, Any]] = None + + +def read_software(cfg: Config) -> Dict[str, Any]: + """Robot software/OS card: ROS distro, host OS (via /host), kernel, arch. + The container shares the HOST kernel; the host OS comes from + /host/etc/os-release (the read-only /:/host mount).""" + global _software_cache + if _software_cache is not None: + return _software_cache + sw: Dict[str, Any] = {} + # ROS distro: pinned via SOFTWARE_ROS, else detect host installs /opt/ros/* + ros = cfg.ros_distro + if not ros: + try: + for base in ("/host/opt/ros", "/opt/ros"): + p = Path(base) + if p.is_dir(): + distros = sorted(d.name for d in p.iterdir() if d.is_dir()) + if distros: + ros = ",".join(distros) + break + except Exception: + pass + sw["ros"] = ros or None + # host OS (firmware/OS card) + os_name = os_ver = None + for rel in ("/host/etc/os-release", "/etc/os-release"): + try: + kv = {} + for line in Path(rel).read_text().splitlines(): + if "=" in line: + k, _, v = line.partition("=") + kv[k] = v.strip().strip('"') + os_name = kv.get("PRETTY_NAME") or kv.get("NAME") + os_ver = kv.get("VERSION_ID") + break + except Exception: + continue + sw["os"] = os_name # e.g. "Ubuntu 20.04.6 LTS" + sw["os_version"] = os_ver # e.g. "20.04" + u = os.uname() + sw["kernel"] = u.release # host kernel (shared with the container) + sw["arch"] = u.machine # e.g. aarch64 + sw["python"] = ".".join(map(str, sys.version_info[:3])) + sw["agent"] = f"{log.name} {_AGENT_VERSION}" + _software_cache = sw + return sw + + +_firmware_cache: Optional[Dict[str, Any]] = None + + +def read_firmware_static() -> Dict[str, Any]: + """Board-level firmware card (static, via /host): compute board model + + Jetson L4T/BSP release + kernel. DDS adds live robot/bms fw versions.""" + global _firmware_cache + if _firmware_cache is not None: + return dict(_firmware_cache) + fw: Dict[str, Any] = {} + # board model, e.g. "NVIDIA Orin NX Developer Kit" + for p in ("/host/sys/firmware/devicetree/base/model", + "/host/proc/device-tree/model", + "/sys/firmware/devicetree/base/model", + "/proc/device-tree/model"): + try: + fw["board"] = Path(p).read_bytes().decode().strip("\x00 \n") + break + except Exception: + continue + # Jetson L4T/BSP: "# R35 (release), REVISION: 3.1, ..." -> "R35.3.1" + for p in ("/host/etc/nv_tegra_release", "/etc/nv_tegra_release"): + try: + head = Path(p).read_text().splitlines()[0] + import re + m = re.search(r"(R\d+).*?REVISION:\s*([\d.]+)", head) + fw["l4t"] = f"{m.group(1)}.{m.group(2)}" if m else head.lstrip("# ").strip() + break + except Exception: + continue + fw["kernel"] = os.uname().release + _firmware_cache = fw + return dict(fw) + + _data_size_cache: Dict[str, Any] = {"ts": 0.0, "kb": None} @@ -258,6 +350,7 @@ class DDSReader: self._temps: List[float] = [] self._max_dq = 0.0 self._xy: Optional[Dict[str, float]] = None + self._fw: Dict[str, Any] = {} # live fw versions (robot ctrl + bms) self.ok = False self._start() @@ -290,10 +383,23 @@ class DDSReader: def _on_low(self, msg) -> None: try: + try: + v = getattr(msg, "version", None) + if v is not None and "robot" not in self._fw: + vals = [int(x) for x in v] if hasattr(v, "__iter__") else [int(v)] + self._fw["robot"] = ".".join(map(str, vals)) + except Exception: + pass bms = getattr(msg, "bms_state", None) or getattr(msg, "bms", None) if bms is not None: soc = int(getattr(bms, "soc", 0) or 0) cur = int(getattr(bms, "current", 0) or 0) # mA + try: + vh, vl = getattr(bms, "version_high", None), getattr(bms, "version_low", None) + if vh is not None: + self._fw["bms"] = f"{int(vh)}.{int(vl or 0)}" + except Exception: + pass # Go2: pack voltage is LowState_.power_v (V); pack temp from the # BMS NTC sensors (bq_ntc / mcu_ntc, °C). All defensive getattrs. volt_v = None @@ -367,6 +473,7 @@ class DDSReader: "temps": list(self._temps), "max_dq": self._max_dq, "xy": dict(self._xy) if self._xy else None, + "fw": dict(self._fw), } @@ -941,6 +1048,13 @@ class ProjectLogTail: self._cur: Optional[Path] = None self._pos = 0 self._backfill = max(0, cfg.project_log_backfill) + self._exclude = None + if cfg.project_log_exclude: + try: + import re + self._exclude = re.compile(cfg.project_log_exclude) + except Exception: + self._exclude = None self.active = False try: self._resolve(cfg) @@ -994,26 +1108,44 @@ class ProjectLogTail: def _start(self, p: Path, label: str) -> None: self._cur = p + self.label = label size = p.stat().st_size - self._pos = size # default: only NEW lines ship - # backfill: start N lines before EOF so recent history ships at startup + self._pos = size # new lines ship from here on + self._pending: List[str] = [] + # backfill: the last N RELEVANT lines (post-filter) from the last ~1 MB, + # so the noise (access-log spam) doesn't eat the history window if self._backfill and size: try: - take = min(size, 512 * 1024) + take = min(size, 8 * 1024 * 1024) with p.open("rb") as f: f.seek(size - take) tail = f.read(take) - parts = tail.splitlines(keepends=True)[-self._backfill:] - self._pos = size - sum(len(x) for x in parts) + raw = tail.decode("utf-8", "replace").splitlines() + if take < size and raw: + raw = raw[1:] # first line is a partial record — drop it + for ln in raw: + ln = ln.strip() + if ln.startswith("{"): + try: + ln = (json.loads(ln).get("log") or "").rstrip() + except Exception: + pass + if not ln: + continue + if self._exclude is not None and self._exclude.search(ln): + continue + self._pending.append(f"[{label}] {ln}") + self._pending = self._pending[-self._backfill:] except Exception: - self._pos = size - self.label = label + self._pending = [] self.active = True def poll(self) -> List[str]: - """New lines since last poll (docker json-log unwrapped), labeled.""" + """New lines since last poll (docker json-log unwrapped), labeled. + The startup backfill (last N relevant lines) is returned on first call.""" if not self.active or self._cur is None: return [] + pend, self._pending = self._pending, [] out: List[str] = [] try: st = self._cur.stat() @@ -1031,11 +1163,14 @@ class ProjectLogTail: ln = (json.loads(ln).get("log") or "").rstrip() except Exception: pass - if ln: - out.append(f"[{self.label}] {ln}") + if not ln: + continue + if self._exclude is not None and self._exclude.search(ln): + continue # access-log noise — not in the project's log panel + out.append(f"[{self.label}] {ln}") except Exception as e: log.debug("project-log poll failed: %s", e) - return out[-100:] # cap per cycle + return pend + out[-100:] # backfill first, then ≤100 new/cycle _PROJECT_TAIL: Optional[ProjectLogTail] = None diff --git a/agents/r1/sanad_api_r1.py b/agents/r1/sanad_api_r1.py index f3309d6..61f8df8 100644 --- a/agents/r1/sanad_api_r1.py +++ b/agents/r1/sanad_api_r1.py @@ -145,6 +145,8 @@ class Config: project_log_path: str project_log_label: str project_log_backfill: int + project_log_exclude: str + ros_distro: str # transport verify_tls: bool http_timeout: float @@ -197,6 +199,11 @@ class Config: project_log_path=_env("PROJECT_LOG_PATH", ""), project_log_label=_env("PROJECT_LOG_LABEL", ""), project_log_backfill=int(_env("PROJECT_LOG_BACKFILL", "100")), + # drop uvicorn/access-log noise so shipped lines match the project's + # own LIVE LOGS panel exactly (app-logger lines + tracebacks only) + project_log_exclude=_env("PROJECT_LOG_EXCLUDE", + r"^(INFO|WARNING|ERROR|DEBUG|CRITICAL):\s"), + ros_distro=_env("SOFTWARE_ROS", ""), verify_tls=_env_bool("VERIFY_TLS", True), http_timeout=float(_env("HTTP_TIMEOUT", "30")), ) @@ -232,6 +239,93 @@ def read_mac(interface: str) -> str: return ":".join(f"{(n >> b) & 0xff:02x}" for b in range(40, -1, -8)) +_AGENT_VERSION = "2026.07.13" + +_software_cache: Optional[Dict[str, Any]] = None + + +def read_software(cfg: Config) -> Dict[str, Any]: + """Robot software/OS card: ROS distro, host OS (via /host), kernel, arch. + The container shares the HOST kernel; the host OS comes from + /host/etc/os-release (the read-only /:/host mount).""" + global _software_cache + if _software_cache is not None: + return _software_cache + sw: Dict[str, Any] = {} + # ROS distro: pinned via SOFTWARE_ROS, else detect host installs /opt/ros/* + ros = cfg.ros_distro + if not ros: + try: + for base in ("/host/opt/ros", "/opt/ros"): + p = Path(base) + if p.is_dir(): + distros = sorted(d.name for d in p.iterdir() if d.is_dir()) + if distros: + ros = ",".join(distros) + break + except Exception: + pass + sw["ros"] = ros or None + # host OS (firmware/OS card) + os_name = os_ver = None + for rel in ("/host/etc/os-release", "/etc/os-release"): + try: + kv = {} + for line in Path(rel).read_text().splitlines(): + if "=" in line: + k, _, v = line.partition("=") + kv[k] = v.strip().strip('"') + os_name = kv.get("PRETTY_NAME") or kv.get("NAME") + os_ver = kv.get("VERSION_ID") + break + except Exception: + continue + sw["os"] = os_name # e.g. "Ubuntu 20.04.6 LTS" + sw["os_version"] = os_ver # e.g. "20.04" + u = os.uname() + sw["kernel"] = u.release # host kernel (shared with the container) + sw["arch"] = u.machine # e.g. aarch64 + sw["python"] = ".".join(map(str, sys.version_info[:3])) + sw["agent"] = f"{log.name} {_AGENT_VERSION}" + _software_cache = sw + return sw + + +_firmware_cache: Optional[Dict[str, Any]] = None + + +def read_firmware_static() -> Dict[str, Any]: + """Board-level firmware card (static, via /host): compute board model + + Jetson L4T/BSP release + kernel. DDS adds live robot/bms fw versions.""" + global _firmware_cache + if _firmware_cache is not None: + return dict(_firmware_cache) + fw: Dict[str, Any] = {} + # board model, e.g. "NVIDIA Orin NX Developer Kit" + for p in ("/host/sys/firmware/devicetree/base/model", + "/host/proc/device-tree/model", + "/sys/firmware/devicetree/base/model", + "/proc/device-tree/model"): + try: + fw["board"] = Path(p).read_bytes().decode().strip("\x00 \n") + break + except Exception: + continue + # Jetson L4T/BSP: "# R35 (release), REVISION: 3.1, ..." -> "R35.3.1" + for p in ("/host/etc/nv_tegra_release", "/etc/nv_tegra_release"): + try: + head = Path(p).read_text().splitlines()[0] + import re + m = re.search(r"(R\d+).*?REVISION:\s*([\d.]+)", head) + fw["l4t"] = f"{m.group(1)}.{m.group(2)}" if m else head.lstrip("# ").strip() + break + except Exception: + continue + fw["kernel"] = os.uname().release + _firmware_cache = fw + return dict(fw) + + _data_size_cache: Dict[str, Any] = {"ts": 0.0, "kb": None} @@ -285,6 +379,7 @@ class DDSReader: self._temps: List[float] = [] self._max_dq = 0.0 self._xy: Optional[Dict[str, float]] = None + self._fw: Dict[str, Any] = {} # live fw versions (robot ctrl + bms) self._loco = None self.ok = False self._start() @@ -372,6 +467,13 @@ class DDSReader: temp_c = max(vals) except Exception: temp_c = None + # BMS firmware version (version_high.version_low) + try: + vh, vl = getattr(msg, "version_high", None), getattr(msg, "version_low", None) + if vh is not None: + self._fw["bms"] = f"{int(vh)}.{int(vl or 0)}" + except Exception: + pass with self._lock: self._bms = { "soc": max(0, min(100, soc)), @@ -387,6 +489,14 @@ class DDSReader: def _on_low(self, msg) -> None: try: + # robot controller firmware version (LowState.version array) + try: + v = getattr(msg, "version", None) + if v is not None and "robot" not in self._fw: + vals = [int(x) for x in v] if hasattr(v, "__iter__") else [int(v)] + self._fw["robot"] = ".".join(map(str, vals)) + except Exception: + pass temps: List[float] = [] max_dq = 0.0 for m in (getattr(msg, "motor_state", None) or []): @@ -429,6 +539,7 @@ class DDSReader: "temps": list(self._temps), "max_dq": self._max_dq, "xy": dict(self._xy) if self._xy else None, + "fw": dict(self._fw), } def fsm_id(self) -> Optional[int]: @@ -1018,6 +1129,13 @@ class ProjectLogTail: self._cur: Optional[Path] = None self._pos = 0 self._backfill = max(0, cfg.project_log_backfill) + self._exclude = None + if cfg.project_log_exclude: + try: + import re + self._exclude = re.compile(cfg.project_log_exclude) + except Exception: + self._exclude = None self.active = False try: self._resolve(cfg) @@ -1071,26 +1189,44 @@ class ProjectLogTail: def _start(self, p: Path, label: str) -> None: self._cur = p + self.label = label size = p.stat().st_size - self._pos = size # default: only NEW lines ship - # backfill: start N lines before EOF so recent history ships at startup + self._pos = size # new lines ship from here on + self._pending: List[str] = [] + # backfill: the last N RELEVANT lines (post-filter) from the last ~1 MB, + # so the noise (access-log spam) doesn't eat the history window if self._backfill and size: try: - take = min(size, 512 * 1024) + take = min(size, 8 * 1024 * 1024) with p.open("rb") as f: f.seek(size - take) tail = f.read(take) - parts = tail.splitlines(keepends=True)[-self._backfill:] - self._pos = size - sum(len(x) for x in parts) + raw = tail.decode("utf-8", "replace").splitlines() + if take < size and raw: + raw = raw[1:] # first line is a partial record — drop it + for ln in raw: + ln = ln.strip() + if ln.startswith("{"): + try: + ln = (json.loads(ln).get("log") or "").rstrip() + except Exception: + pass + if not ln: + continue + if self._exclude is not None and self._exclude.search(ln): + continue + self._pending.append(f"[{label}] {ln}") + self._pending = self._pending[-self._backfill:] except Exception: - self._pos = size - self.label = label + self._pending = [] self.active = True def poll(self) -> List[str]: - """New lines since last poll (docker json-log unwrapped), labeled.""" + """New lines since last poll (docker json-log unwrapped), labeled. + The startup backfill (last N relevant lines) is returned on first call.""" if not self.active or self._cur is None: return [] + pend, self._pending = self._pending, [] out: List[str] = [] try: st = self._cur.stat() @@ -1108,11 +1244,14 @@ class ProjectLogTail: ln = (json.loads(ln).get("log") or "").rstrip() except Exception: pass - if ln: - out.append(f"[{self.label}] {ln}") + if not ln: + continue + if self._exclude is not None and self._exclude.search(ln): + continue # access-log noise — not in the project's log panel + out.append(f"[{self.label}] {ln}") except Exception as e: log.debug("project-log poll failed: %s", e) - return out[-100:] # cap per cycle + return pend + out[-100:] # backfill first, then ≤100 new/cycle _PROJECT_TAIL: Optional[ProjectLogTail] = None @@ -1252,6 +1391,9 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], "brand": cfg.brand, "type": cfg.robot_type, # humanoid "model": cfg.model, # g1 + "software": read_software(cfg), # ros/os/kernel/arch/python/agent + "firmware": {**read_firmware_static(), **(snap.get("fw") or {})}, + # board/l4t/kernel + live robot/bms fw versions "battery": battery, # null = couldn't read (heartbeat) "charging": charging, "battery_detail": battery_detail, diff --git a/fleet_install.sh b/fleet_install.sh index 86abff9..babaf44 100755 --- a/fleet_install.sh +++ b/fleet_install.sh @@ -119,6 +119,7 @@ MAP_SELECT=all MAP_UPLOAD_MODE=multipart MAP_POLL_INTERVAL=30 LOGS_INTERVAL=60 +SOFTWARE_ROS=foxy EOF }