diff --git a/PIPELINE.md b/PIPELINE.md index ef209f6..e472517 100644 --- a/PIPELINE.md +++ b/PIPELINE.md @@ -92,10 +92,18 @@ POST /api/v1/fleet/ingest/telemetry Authorization: Bearer Content-Type: application/json -{ "sn": "r1_82", +{ "sn": "E39N4000Q6D7E70F", // the robot's REAL Unitree serial "mac": "4c:bb:47:51:25:9a", - "battery": 80, + "brand": "unitree", + "type": "humanoid", // humanoid | dog + "model": "r1", // r1 | g1 | go2 + "battery": 62, "charging": false, + "battery_detail": { "voltage_v": 34.5, "current_a": -3.08, + "temp_c": 42, "soh": 99, "cycles": 10 }, + "motor_temp": { "max": 49.0, "avg": 37.8, "min": 32.0 }, // null = not receiving + "storage": { "total_gb": 98.2, "free_gb": 58.5, "used_percent": 36.2, + "data_kb": 770.0 }, // data_kb only when STORAGE_DATA_PATH set "status": "idle", "position": { "x": 12.4, "y": 3.1 }, // or null when no localization source "faults": [], diff --git a/agents/g1t/.env.example b/agents/g1t/.env.example index 546c5ce..9771d6a 100644 --- a/agents/g1t/.env.example +++ b/agents/g1t/.env.example @@ -33,3 +33,11 @@ MOTOR_TEMP_MAX=85 POLL_INTERVAL=2 VERIFY_TLS=1 HTTP_TIMEOUT=10 + +# ── robot identity card (shown on the dashboard) ───────────────────────────── +ROBOT_BRAND=unitree +ROBOT_TYPE=humanoid +ROBOT_MODEL=g1 +# Optional: Sanad data dir whose size is reported in storage (as /host/ +# when the installer's read-only /:/host mount is used). Empty = omit. +STORAGE_DATA_PATH= diff --git a/agents/g1t/sanad_api_g1t.py b/agents/g1t/sanad_api_g1t.py index 140be64..5f931d7 100644 --- a/agents/g1t/sanad_api_g1t.py +++ b/agents/g1t/sanad_api_g1t.py @@ -47,6 +47,7 @@ import argparse import json import logging import os +import shutil import sys import threading import time @@ -85,6 +86,12 @@ class Config: server_url: str device_token: str sn: str + name: str + brand: str + robot_type: str + model: str + storage_path: str + data_path: str dds_interface: str dds_domain: int mac_interface: str @@ -109,6 +116,12 @@ class Config: return cls( server_url=server, device_token=token, sn=_env("SN", "g1_0000"), + name=_env("ROBOT_NAME", "") or _env("SN", "g1_0000"), + brand=_env("ROBOT_BRAND", "unitree"), + robot_type=_env("ROBOT_TYPE", "humanoid"), + model=_env("ROBOT_MODEL", "g1"), + storage_path=_env("STORAGE_PATH", ""), + data_path=_env("STORAGE_DATA_PATH", ""), dds_interface=iface, dds_domain=int(_env("DDS_DOMAIN", "0")), mac_interface=_env("MAC_INTERFACE", iface), read_fsm=_env_bool("G1_READ_FSM", False), @@ -141,6 +154,43 @@ def read_mac(interface: str) -> str: return ":".join(f"{(n >> b) & 0xff:02x}" for b in range(40, -1, -8)) +_data_size_cache: Dict[str, Any] = {"ts": 0.0, "kb": None} + + +def read_storage(cfg: Config) -> Optional[Dict[str, Any]]: + """Disk usage of the robot's root fs + optional Sanad data-dir size. + + In docker, bind-mount the host root read-only at /host (the installer does) + so this reports the HOST disk, not the container overlay.""" + root = cfg.storage_path or ("/host" if os.path.isdir("/host") else "/") + try: + du = shutil.disk_usage(root) + out: Dict[str, Any] = { + "total_gb": round(du.total / 1e9, 2), + "free_gb": round(du.free / 1e9, 2), + "used_percent": round(du.used / du.total * 100, 1), + } + except Exception: + return None + if cfg.data_path and os.path.isdir(cfg.data_path): + now = time.monotonic() + if _data_size_cache["kb"] is None or now - _data_size_cache["ts"] > 60: + try: + total = 0 + for r, _, files in os.walk(cfg.data_path): + for f in files: + try: + total += os.path.getsize(os.path.join(r, f)) + except OSError: + pass + _data_size_cache.update(ts=now, kb=round(total / 1024, 1)) + except Exception: + pass + if _data_size_cache["kb"] is not None: + out["data_kb"] = _data_size_cache["kb"] + return out + + class DDSReader: """Subscribes rt/lf/bmsstate + rt/lowstate (+ rt/lf/odommodestate for position). Passive reads; the only RPC ever issued is GET_FSM_ID.""" @@ -216,8 +266,40 @@ class DDSReader: try: soc = int(getattr(msg, "soc", 0) or 0) cur_mA = int(getattr(msg, "current", 0) or 0) + # Pack voltage: prefer bmsvoltage[0] (mV); else sum of cell voltages. + volt_mv = 0 + bv = getattr(msg, "bmsvoltage", None) + try: + if bv is not None and len(bv) and int(bv[0]): + volt_mv = int(bv[0]) + except Exception: + volt_mv = 0 + if not volt_mv: + cv = getattr(msg, "cell_vol", None) + if cv is not None: + try: + volt_mv = int(sum(int(x) for x in cv if x)) + except Exception: + volt_mv = 0 + # Max plausible pack temperature (int16 °C). + temp_c = None + tt = getattr(msg, "temperature", None) + if tt is not None: + try: + vals = [int(x) for x in tt if -40 <= int(x) <= 150] + if vals: + temp_c = max(vals) + except Exception: + temp_c = None with self._lock: - self._bms = {"soc": max(0, min(100, soc)), "current_a": round(cur_mA / 1000.0, 2)} + self._bms = { + "soc": max(0, min(100, soc)), + "current_a": round(cur_mA / 1000.0, 2), + "voltage_v": round(volt_mv / 1000.0, 1) if volt_mv else None, + "temp_c": temp_c, + "soh": int(getattr(msg, "soh", 0) or 0), + "cycles": int(getattr(msg, "cycle", 0) or 0), + } self._bms_ts = time.monotonic() except Exception: pass @@ -231,7 +313,8 @@ class DDSReader: if t is not None: try: vals = [float(x) for x in t] if hasattr(t, "__iter__") else [float(t)] - temps.extend(v for v in vals if -40 <= v <= 200) + # 0 = slot not reporting (unpopulated motor), not a real temp + temps.extend(v for v in vals if 0 < v <= 200) except Exception: pass dq = getattr(m, "dq", None) @@ -352,7 +435,8 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], pos: Optional[RosbridgePosition], sim: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: if sim is not None: - snap = {"bms": {"soc": sim["battery"], "current_a": 0.5 if sim["charging"] else -0.3}, + snap = {"bms": {"soc": sim["battery"], "current_a": 0.5 if sim["charging"] else -0.3, + "voltage_v": 47.5, "temp_c": 36, "soh": 100, "cycles": 45}, "low_age": 0.1, "temps": [sim.get("temp", 45)], "max_dq": sim.get("max_dq", 0.0), "xy": sim.get("position")} fsm = sim.get("fsm") @@ -366,13 +450,34 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], status = derive_status(cfg, snap, fsm) faults = derive_faults(cfg, snap) + # Battery detail (voltage / current / pack temp / health / cycles). + battery_detail = None + if bms: + battery_detail = {"voltage_v": bms.get("voltage_v"), "current_a": bms.get("current_a"), + "temp_c": bms.get("temp_c"), "soh": bms.get("soh"), + "cycles": bms.get("cycles")} + + # Motor temperature stats; null = temps not receiving. + temps = snap.get("temps") or [] + motor_temp = ({"max": round(max(temps), 1), "avg": round(sum(temps) / len(temps), 1), + "min": round(min(temps), 1)} if temps else None) + position = snap.get("xy") if position is None and pos is not None: position = pos.get() return { - "sn": cfg.sn, "mac": mac, - "battery": battery, "charging": charging, "status": status, + "sn": cfg.sn, + "name": cfg.name, # friendly display name (e.g. g1_58) + "mac": mac, + "brand": cfg.brand, + "type": cfg.robot_type, # humanoid | dog + "model": cfg.model, # r1 | g1 | go2 + "battery": battery, "charging": charging, + "battery_detail": battery_detail, + "motor_temp": motor_temp, # null = not receiving + "storage": read_storage(cfg), + "status": status, "position": position, "faults": faults, "ts": int(time.time()), } diff --git a/agents/go2/.env.example b/agents/go2/.env.example index fe186a7..d958996 100644 --- a/agents/go2/.env.example +++ b/agents/go2/.env.example @@ -27,3 +27,11 @@ MOTOR_TEMP_MAX=85 POLL_INTERVAL=2 VERIFY_TLS=1 HTTP_TIMEOUT=10 + +# ── robot identity card (shown on the dashboard) ───────────────────────────── +ROBOT_BRAND=unitree +ROBOT_TYPE=dog +ROBOT_MODEL=go2 +# Optional: Sanad data dir whose size is reported in storage (as /host/ +# when the installer's read-only /:/host mount is used). Empty = omit. +STORAGE_DATA_PATH= diff --git a/agents/go2/sanad_api_go2.py b/agents/go2/sanad_api_go2.py index 8effa79..582979f 100644 --- a/agents/go2/sanad_api_go2.py +++ b/agents/go2/sanad_api_go2.py @@ -40,6 +40,7 @@ import argparse import json import logging import os +import shutil import sys import threading import time @@ -78,6 +79,12 @@ class Config: server_url: str device_token: str sn: str + name: str + brand: str + robot_type: str + model: str + storage_path: str + data_path: str dds_interface: str dds_domain: int mac_interface: str @@ -101,6 +108,12 @@ class Config: return cls( server_url=server, device_token=token, sn=_env("SN", "go2_0000"), + name=_env("ROBOT_NAME", "") or _env("SN", "go2_0000"), + brand=_env("ROBOT_BRAND", "unitree"), + robot_type=_env("ROBOT_TYPE", "dog"), + model=_env("ROBOT_MODEL", "go2"), + storage_path=_env("STORAGE_PATH", ""), + data_path=_env("STORAGE_DATA_PATH", ""), dds_interface=iface, dds_domain=int(_env("DDS_DOMAIN", "0")), mac_interface=_env("MAC_INTERFACE", iface), position_source=_env("GO2_POSITION_SOURCE", "none").lower(), @@ -132,6 +145,43 @@ def read_mac(interface: str) -> str: return ":".join(f"{(n >> b) & 0xff:02x}" for b in range(40, -1, -8)) +_data_size_cache: Dict[str, Any] = {"ts": 0.0, "kb": None} + + +def read_storage(cfg: Config) -> Optional[Dict[str, Any]]: + """Disk usage of the robot's root fs + optional Sanad data-dir size. + + In docker, bind-mount the host root read-only at /host (the installer does) + so this reports the HOST disk, not the container overlay.""" + root = cfg.storage_path or ("/host" if os.path.isdir("/host") else "/") + try: + du = shutil.disk_usage(root) + out: Dict[str, Any] = { + "total_gb": round(du.total / 1e9, 2), + "free_gb": round(du.free / 1e9, 2), + "used_percent": round(du.used / du.total * 100, 1), + } + except Exception: + return None + if cfg.data_path and os.path.isdir(cfg.data_path): + now = time.monotonic() + if _data_size_cache["kb"] is None or now - _data_size_cache["ts"] > 60: + try: + total = 0 + for r, _, files in os.walk(cfg.data_path): + for f in files: + try: + total += os.path.getsize(os.path.join(r, f)) + except OSError: + pass + _data_size_cache.update(ts=now, kb=round(total / 1024, 1)) + except Exception: + pass + if _data_size_cache["kb"] is not None: + out["data_kb"] = _data_size_cache["kb"] + return out + + class DDSReader: """Subscribes rt/lowstate (unitree_go LowState_) and (optionally) sportmodestate. Battery comes from the nested LowState_.bms_state. Passive reads only.""" @@ -181,8 +231,36 @@ class DDSReader: if bms is not None: soc = int(getattr(bms, "soc", 0) or 0) cur = int(getattr(bms, "current", 0) or 0) # mA + # 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 + try: + pv = getattr(msg, "power_v", None) + if pv: + volt_v = round(float(pv), 1) + except Exception: + volt_v = None + temp_c = None + try: + ntc_vals = [] + for attr in ("bq_ntc", "mcu_ntc"): + nt = getattr(bms, attr, None) + if nt is not None: + vals = [int(x) for x in nt] if hasattr(nt, "__iter__") else [int(nt)] + ntc_vals.extend(v for v in vals if -40 <= v <= 150) + if ntc_vals: + temp_c = max(ntc_vals) + except Exception: + temp_c = None with self._lock: - self._bms = {"soc": max(0, min(100, soc)), "current_a": round(cur / 1000.0, 2)} + self._bms = { + "soc": max(0, min(100, soc)), + "current_a": round(cur / 1000.0, 2), + "voltage_v": volt_v, + "temp_c": temp_c, + "soh": int(getattr(bms, "soh", 0) or 0), + "cycles": int(getattr(bms, "cycle", 0) or 0), + } temps: List[float] = [] max_dq = 0.0 for m in (getattr(msg, "motor_state", None) or []): @@ -190,7 +268,8 @@ class DDSReader: if t is not None: try: vals = [float(x) for x in t] if hasattr(t, "__iter__") else [float(t)] - temps.extend(v for v in vals if -40 <= v <= 200) + # 0 = slot not reporting (unpopulated motor), not a real temp + temps.extend(v for v in vals if 0 < v <= 200) except Exception: pass dq = getattr(m, "dq", None) @@ -296,7 +375,8 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], pos: Optional[RosbridgePosition], sim: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: if sim is not None: - snap = {"bms": {"soc": sim["battery"], "current_a": 0.5 if sim["charging"] else -0.3}, + snap = {"bms": {"soc": sim["battery"], "current_a": 0.5 if sim["charging"] else -0.3, + "voltage_v": 28.6, "temp_c": 36, "soh": 100, "cycles": 45}, "low_age": 0.1, "temps": [sim.get("temp", 45)], "max_dq": sim.get("max_dq", 0.0), "xy": sim.get("position")} else: @@ -308,13 +388,34 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], status = derive_status(cfg, snap) faults = derive_faults(cfg, snap) + # Battery detail (voltage / current / pack temp / health / cycles). + battery_detail = None + if bms: + battery_detail = {"voltage_v": bms.get("voltage_v"), "current_a": bms.get("current_a"), + "temp_c": bms.get("temp_c"), "soh": bms.get("soh"), + "cycles": bms.get("cycles")} + + # Motor temperature stats; null = temps not receiving. + temps = snap.get("temps") or [] + motor_temp = ({"max": round(max(temps), 1), "avg": round(sum(temps) / len(temps), 1), + "min": round(min(temps), 1)} if temps else None) + position = snap.get("xy") if position is None and pos is not None: position = pos.get() return { - "sn": cfg.sn, "mac": mac, - "battery": battery, "charging": charging, "status": status, + "sn": cfg.sn, + "name": cfg.name, # friendly display name (e.g. go2_XX) + "mac": mac, + "brand": cfg.brand, + "type": cfg.robot_type, # humanoid | dog + "model": cfg.model, # r1 | g1 | go2 + "battery": battery, "charging": charging, + "battery_detail": battery_detail, + "motor_temp": motor_temp, # null = not receiving + "storage": read_storage(cfg), + "status": status, "position": position, "faults": faults, "ts": int(time.time()), } diff --git a/agents/r1/.env.example b/agents/r1/.env.example index 00d7598..077d7fc 100644 --- a/agents/r1/.env.example +++ b/agents/r1/.env.example @@ -33,3 +33,11 @@ MOTOR_TEMP_MAX=85 POLL_INTERVAL=2 VERIFY_TLS=1 HTTP_TIMEOUT=10 + +# ── robot identity card (shown on the dashboard) ───────────────────────────── +ROBOT_BRAND=unitree +ROBOT_TYPE=humanoid +ROBOT_MODEL=r1 +# Optional: Sanad data dir whose size is reported in storage (as /host/ +# when the installer's read-only /:/host mount is used). Empty = omit. +STORAGE_DATA_PATH= diff --git a/agents/r1/sanad_api_r1.py b/agents/r1/sanad_api_r1.py index dcbcd2f..0733b77 100644 --- a/agents/r1/sanad_api_r1.py +++ b/agents/r1/sanad_api_r1.py @@ -59,6 +59,7 @@ import json import logging import math import os +import shutil import socket import sys import threading @@ -104,6 +105,12 @@ class Config: server_url: str device_token: str sn: str + name: str + brand: str + robot_type: str + model: str + storage_path: str + data_path: str dds_interface: str dds_domain: int mac_interface: str @@ -129,6 +136,12 @@ class Config: server_url=server, device_token=token, sn=_env("SN", "r1_0000"), + name=_env("ROBOT_NAME", "") or _env("SN", "r1_0000"), + brand=_env("ROBOT_BRAND", "unitree"), + robot_type=_env("ROBOT_TYPE", "humanoid"), + model=_env("ROBOT_MODEL", "r1"), + storage_path=_env("STORAGE_PATH", ""), + data_path=_env("STORAGE_DATA_PATH", ""), dds_interface=iface, dds_domain=int(_env("DDS_DOMAIN", "0")), mac_interface=_env("MAC_INTERFACE", iface), @@ -169,6 +182,47 @@ def read_mac(interface: str) -> str: return ":".join(f"{(n >> b) & 0xff:02x}" for b in range(40, -1, -8)) +# --------------------------------------------------------------------------- # +# storage (host disk usage; mount / at /host:ro in docker) +# --------------------------------------------------------------------------- # +_data_size_cache: Dict[str, Any] = {"ts": 0.0, "kb": None} + + +def read_storage(cfg: Config) -> Optional[Dict[str, Any]]: + """Disk usage of the robot's root fs + optional Sanad data-dir size. + + In docker, bind-mount the host root read-only at /host (the installer does) + so this reports the HOST disk, not the container overlay.""" + root = cfg.storage_path or ("/host" if os.path.isdir("/host") else "/") + try: + du = shutil.disk_usage(root) + out: Dict[str, Any] = { + "total_gb": round(du.total / 1e9, 2), + "free_gb": round(du.free / 1e9, 2), + "used_percent": round(du.used / du.total * 100, 1), + } + except Exception: + return None + # data-dir size is a directory walk — cache it (refresh every 60 s) + if cfg.data_path and os.path.isdir(cfg.data_path): + now = time.monotonic() + if _data_size_cache["kb"] is None or now - _data_size_cache["ts"] > 60: + try: + total = 0 + for r, _, files in os.walk(cfg.data_path): + for f in files: + try: + total += os.path.getsize(os.path.join(r, f)) + except OSError: + pass + _data_size_cache.update(ts=now, kb=round(total / 1024, 1)) + except Exception: + pass + if _data_size_cache["kb"] is not None: + out["data_kb"] = _data_size_cache["kb"] + return out + + # --------------------------------------------------------------------------- # # DDS reader (optional — degrades if unitree_sdk2py is absent) # --------------------------------------------------------------------------- # @@ -242,11 +296,38 @@ class DDSReader: try: soc = int(getattr(msg, "soc", 0) or 0) cur_mA = int(getattr(msg, "current", 0) or 0) + # Pack voltage: prefer bmsvoltage[0] (mV); else sum of cell voltages. + volt_mv = 0 + bv = getattr(msg, "bmsvoltage", None) + try: + if bv is not None and len(bv) and int(bv[0]): + volt_mv = int(bv[0]) + except Exception: + volt_mv = 0 + if not volt_mv: + cv = getattr(msg, "cell_vol", None) + if cv is not None: + try: + volt_mv = int(sum(int(x) for x in cv if x)) + except Exception: + volt_mv = 0 + # Max plausible pack temperature (int16 °C). + temp_c = None + tt = getattr(msg, "temperature", None) + if tt is not None: + try: + vals = [int(x) for x in tt if -40 <= int(x) <= 150] + if vals: + temp_c = max(vals) + except Exception: + temp_c = None batt = { "soc": max(0, min(100, soc)), "current_a": round(cur_mA / 1000.0, 2), + "voltage_v": round(volt_mv / 1000.0, 1) if volt_mv else None, + "temp_c": temp_c, "soh": int(getattr(msg, "soh", 0) or 0), - "cycle": int(getattr(msg, "cycle", 0) or 0), + "cycles": int(getattr(msg, "cycle", 0) or 0), } with self._lock: self._bms = batt @@ -265,7 +346,8 @@ class DDSReader: try: # temperature may be a scalar or a small array (surface/winding) vals = [float(x) for x in t] if hasattr(t, "__iter__") else [float(t)] - temps.extend(v for v in vals if -40 <= v <= 200) + # 0 = slot not reporting (unpopulated motor), not a real temp + temps.extend(v for v in vals if 0 < v <= 200) except Exception: pass dq = getattr(m, "dq", None) @@ -392,7 +474,8 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], pos: Optional[RosbridgePosition], sim: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: if sim is not None: - snap = {"bms": {"soc": sim["battery"], "current_a": 0.5 if sim["charging"] else -0.3}, + snap = {"bms": {"soc": sim["battery"], "current_a": 0.5 if sim["charging"] else -0.3, + "voltage_v": 47.5, "temp_c": 36, "soh": 100, "cycles": 45}, "bms_age": 0.1, "low_age": 0.1, "temps": [sim.get("temp", 45)], "max_dq": sim.get("max_dq", 0.0)} fsm = sim.get("fsm") else: @@ -405,6 +488,18 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], status = derive_status(cfg, snap, fsm) faults = derive_faults(cfg, snap) + # Battery detail (voltage / current / pack temp / health / cycles). + battery_detail = None + if bms: + battery_detail = {"voltage_v": bms.get("voltage_v"), "current_a": bms.get("current_a"), + "temp_c": bms.get("temp_c"), "soh": bms.get("soh"), + "cycles": bms.get("cycles")} + + # Motor temperature stats; null = temps not receiving. + temps = snap.get("temps") or [] + motor_temp = ({"max": round(max(temps), 1), "avg": round(sum(temps) / len(temps), 1), + "min": round(min(temps), 1)} if temps else None) + position = None if sim is not None: position = sim.get("position") @@ -413,9 +508,16 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader], payload: Dict[str, Any] = { "sn": cfg.sn, + "name": cfg.name, # friendly display name (e.g. r1_82) "mac": mac, + "brand": cfg.brand, + "type": cfg.robot_type, # humanoid | dog + "model": cfg.model, # r1 | g1 | go2 "battery": battery, # null = couldn't read (heartbeat) "charging": charging, + "battery_detail": battery_detail, + "motor_temp": motor_temp, # null = not receiving + "storage": read_storage(cfg), "status": status, "position": position, # null when no localization source "faults": faults, diff --git a/fleet_install.sh b/fleet_install.sh index 60de8a0..a2b263f 100755 --- a/fleet_install.sh +++ b/fleet_install.sh @@ -46,16 +46,24 @@ run_args(){ -v /home/$USER_/$(rdir_of "$1")/web_data:/data/web_data:ro \ -v /home/$USER_/$(rdir_of "$1")/state:/data/state" else - echo "$base" + # telemetry agents: host root read-only at /host → real disk-usage stats + echo "$base -v /:/host:ro" fi } +# Resolve the SERVER_URL + VERIFY_TLS for a deploy: a full --server-url (real +# HTTPS fleet server, TLS verified) wins; otherwise http://: +# (the local test server, TLS off). +server_url(){ [ -n "$SERVER_URL_OVERRIDE" ] && echo "$SERVER_URL_OVERRIDE" || echo "http://$1:$PORT"; } +verify_tls(){ [ -n "$VERIFY_TLS_OPT" ] && echo "$VERIFY_TLS_OPT" || { [ -n "$SERVER_URL_OVERRIDE" ] && echo 1 || echo 0; }; } + # ---- write the robot-side .env ---- push_env(){ - local t="$1" sip="$2" rdir; rdir="$(rdir_of "$t")" + local t="$1" sip="$2" rdir surl vtls; rdir="$(rdir_of "$t")" + surl="$(server_url "$sip")"; vtls="$(verify_tls)" if [ "$t" = g1 ]; then rmt_in "cat > ~/$rdir/.env" <_ (e.g. r1_82, g1_58) + local btype=humanoid model="$t" + [ "$t" = g1t ] && model=g1 + [ "$t" = go2 ] && btype=dog + local rname="${NAME:-${model}_${IP##*.}}" + # optional Sanad data dir (its size is shown on the dashboard) — probe + # the known per-robot locations; path is as seen through /host (ro mount) + local dpath="" + for c in "/home/$USER_/SanadR1/data" "/home/$USER_/sanad_deploy/Sanad_Package_4/data"; do + if rmt "test -d $c" 2>/dev/null; then dpath="/host$c"; break; fi + done rmt_in "cat > ~/$rdir/.env" < fleet server $(server_url "$sip") ==" echo ">> rsync agent -> $USER_@$IP:~/$rdir/" rsync -az --delete --exclude '.env' --exclude '__pycache__' --exclude '*.pyc' \ --exclude 'data/' --exclude 'state/' \ @@ -195,13 +222,17 @@ PY } # --------------------------- arg parsing --------------------------- # -CMD=""; ROBOT=""; IP=""; SERVER_IP=""; PORT=8799; TOKEN="test-token"; SN=""; USER_="unitree"; KEEP_SERVER=0 +CMD=""; ROBOT=""; IP=""; SERVER_IP=""; PORT=8799; TOKEN="test-token"; SN=""; NAME=""; USER_="unitree"; KEEP_SERVER=0 +SERVER_URL_OVERRIDE=""; VERIFY_TLS_OPT="" POSA=() while [ $# -gt 0 ]; do case "$1" in --server-ip) SERVER_IP="$2"; shift 2;; + --server-url) SERVER_URL_OVERRIDE="$2"; shift 2;; # full URL (real HTTPS fleet server) → VERIFY_TLS=1 + --verify-tls) VERIFY_TLS_OPT="$2"; shift 2;; --port) PORT="$2"; shift 2;; --token) TOKEN="$2"; shift 2;; --sn) SN="$2"; shift 2;; + --name) NAME="$2"; shift 2;; # friendly display name (default _) --user) USER_="$2"; shift 2;; --keep-server) KEEP_SERVER=1; shift;; -h|--help) grep -E '^#( |$)' "$0" | sed 's/^# \{0,1\}//'; exit 0;;