Update 2026-07-14 09:34:44

This commit is contained in:
kassam 2026-07-14 09:34:44 +04:00
parent 420eb5dd58
commit 2ab8a46d44
4 changed files with 453 additions and 33 deletions

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -119,6 +119,7 @@ MAP_SELECT=all
MAP_UPLOAD_MODE=multipart
MAP_POLL_INTERVAL=30
LOGS_INTERVAL=60
SOFTWARE_ROS=foxy
EOF
}