Organize host and robot streaming releases
This commit is contained in:
188
robot/v4l2/OmniSocketGo_robot/scripts/dev/README.md
Normal file
188
robot/v4l2/OmniSocketGo_robot/scripts/dev/README.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Dev Startup Scripts
|
||||
|
||||
This directory lives inside the `OmniSocketGo` repo and acts as the main launch entry for the whole local setup.
|
||||
|
||||
Default layout:
|
||||
|
||||
```text
|
||||
~/Documents/
|
||||
OmniSocketGo/
|
||||
scripts/dev/
|
||||
robot-command-center/
|
||||
```
|
||||
|
||||
The scripts assume:
|
||||
|
||||
- `OmniSocketGo` is the current repo
|
||||
- `robot-command-center` is a sibling directory next to it
|
||||
|
||||
If your `robot-command-center` is elsewhere, set `ROBOT_COMMAND_CENTER_ROOT` in `robot-remote.env.local`.
|
||||
`start-backend.sh` and `start-frontend.sh` need that repo; `start-ros-receiver.sh` and `start-b-side-omnid.sh` do not.
|
||||
|
||||
## Files
|
||||
|
||||
- `robot-remote.env`: shared defaults for backend, frontend, ROS, and `b_side_omnid`
|
||||
- `robot-remote.env.local`: optional local override file loaded after `robot-remote.env`
|
||||
- `load-env.sh`: loads the shared environment into the current shell
|
||||
- `prepare-camera-device.sh`: reports V4L2 owners and optionally stops one explicitly configured camera service
|
||||
- `resolve-camera-device.sh`: resolves an RGB capture node by USB serial plus MJPEG resolution support
|
||||
- `apply-camera-controls.sh`: applies the camera preset before `b_side_omnid` starts
|
||||
- `start-backend.sh`: starts Django ASGI with `uvicorn`
|
||||
- `log-network-summary.py`: polls the backend `network/latest` API and appends compact JSONL snapshots
|
||||
- `start-frontend.sh`: starts the Vite dev server
|
||||
- `start-ros-receiver.sh`: starts the ROS2 `udp_teleop_bridge` receiver
|
||||
- `start-b-side-omnid.sh`: applies camera controls, then starts `./bin/b_side_omnid` and uses `sudo -E` by default
|
||||
- `start-dev-tmux.sh`: optional one-command `tmux` launcher for all four processes
|
||||
|
||||
## Usage
|
||||
|
||||
Run these from the `OmniSocketGo` repo root:
|
||||
|
||||
```bash
|
||||
bash scripts/dev/start-backend.sh
|
||||
bash scripts/dev/start-frontend.sh
|
||||
bash scripts/dev/start-ros-receiver.sh
|
||||
bash scripts/dev/start-b-side-omnid.sh
|
||||
```
|
||||
|
||||
If you prefer one command and use `tmux`:
|
||||
|
||||
```bash
|
||||
bash scripts/dev/start-dev-tmux.sh
|
||||
```
|
||||
|
||||
If you only want the shared environment for manual commands:
|
||||
|
||||
```bash
|
||||
source scripts/dev/load-env.sh
|
||||
```
|
||||
|
||||
When you launch via `start-*.sh`, you do not need to manually `export` the variables from
|
||||
`robot-remote.env` or `robot-remote.env.local`. `load-env.sh` loads those files with `set -a`,
|
||||
so the variables are exported automatically for the child process. Manual `export` is only needed
|
||||
if you bypass these scripts and start binaries directly from a clean shell.
|
||||
|
||||
## Customizing
|
||||
|
||||
Edit `scripts/dev/robot-remote.env` for shared changes such as:
|
||||
|
||||
- `ROBOT_COMMAND_CENTER_ROOT`
|
||||
- `CONTROL_SIDE_OMNISOCKET_SERVER_ADDR`
|
||||
- `CONTROL_SIDE_OMNISOCKET_RELAY_VIA`
|
||||
- `ROBOT_SIDE_OMNISOCKET_SERVER_ADDR`
|
||||
- `ROBOT_SIDE_OMNISOCKET_RELAY_VIA`
|
||||
- `VITE_API_BASE_URL`
|
||||
- `OMNI_CAMERA_DEVICE`
|
||||
- `OMNI_CAMERA_AUTO_DISCOVER=1` resolves both camera nodes on every start instead of trusting unstable `/dev/video*` numbers
|
||||
- `OMNI_CAMERA_HEAD_SERIAL` and `OMNI_CAMERA_WAIST_SERIAL` permanently map the physical cameras to the head/waist roles
|
||||
- `OMNI_CAMERA_HEAD_DEVICE` and `OMNI_CAMERA_WAIST_DEVICE` are fallback nodes when automatic discovery is disabled
|
||||
- `OMNI_CAMERA_DISCOVERY_WIDTH`, `OMNI_CAMERA_DISCOVERY_HEIGHT`, and `OMNI_CAMERA_DISCOVERY_TIMEOUT_SEC` tune capability matching and retry time
|
||||
- `OMNI_CAMERA_ACTIVE=head|waist` selects the camera sent at startup
|
||||
|
||||
`b_side_omnid` keeps both configured cameras streaming and only decodes/encodes/sends the selected input. Send a text control message with body `camera:head` or `camera:waist` to `peer-b-ctrl` to switch without reopening either camera. After applying the selection, the robot replies to the sender with `{"type":"camera.selected","camera":"head|waist"}` so callers can confirm the actual state. The normal fixed-size binary robot control packets are unchanged.
|
||||
- `OMNI_CAMERA_OCCUPANCY_POLICY`
|
||||
- `OMNI_CAMERA_RELEASE_SERVICE`
|
||||
- `OMNI_CAMERA_PROFILE`
|
||||
- `OMNI_CAMERA_BRIGHTNESS`
|
||||
- `OMNI_CAMERA_CUSTOM_CTRL`
|
||||
- `OMNI_CAMERA_VERIFY`
|
||||
- `OMNI_VIDEO_PEER_ID`
|
||||
- `OMNI_CONTROL_PEER_ID`
|
||||
- `OMNI_VIDEO_SOFT_BACKPRESSURE_SEGMENTS`
|
||||
- `OMNI_VIDEO_HARD_BACKPRESSURE_SEGMENTS`
|
||||
- `OMNI_VIDEO_HARD_BACKPRESSURE_HOLD_MS`
|
||||
- `OMNI_CONTROL_SERVER_IDLE_RECONNECT_MS`
|
||||
- `OMNI_VIDEO_MAX_FRAME_AGE_MS`
|
||||
- `OMNISOCKET_TELEMETRY_PEER_ID`
|
||||
- `OMNISOCKET_TELEMETRY_INTERVAL_MS`
|
||||
- `OMNISOCKET_TELEMETRY_STALE_AFTER_MS`
|
||||
- `OMNI_NETWORK_SUMMARY_LOG_ENABLED`
|
||||
- `OMNI_NETWORK_SUMMARY_LOG_PATH`
|
||||
- `OMNI_NETWORK_SUMMARY_LOG_INTERVAL_MS`
|
||||
|
||||
Camera discovery uses `udevadm` and `v4l2-ctl` on the robot side. A candidate must have the configured serial number and advertise MJPEG at the configured capture resolution; depth, IR, Bayer, and metadata nodes are rejected. With `OMNI_CAMERA_OCCUPANCY_POLICY=release-known`, the start script stops the configured head/waist Orbbec services before discovery so libusb-owned interfaces can reattach to `uvcvideo`.
|
||||
|
||||
Role mapping:
|
||||
|
||||
- `start-backend.sh` uses the `CONTROL_SIDE_*` address pair
|
||||
- `start-b-side-omnid.sh` uses the `ROBOT_SIDE_*` address pair
|
||||
- `start-b-side-omnid.sh` also applies the `OMNI_CAMERA_*` preset before the daemon opens the camera
|
||||
- `start-b-side-omnid.sh` runs the camera occupancy preflight before applying camera controls
|
||||
- `start-ros-receiver.sh` defaults to the robot-side address pair, but with `transport=unix_dgram` it usually does not need the server address
|
||||
|
||||
New repair knobs:
|
||||
|
||||
- `OMNI_VIDEO_SOFT_BACKPRESSURE_SEGMENTS`, `OMNI_VIDEO_HARD_BACKPRESSURE_SEGMENTS`, and `OMNI_VIDEO_HARD_BACKPRESSURE_HOLD_MS` are used by `b_side_omnid`
|
||||
- `OMNI_CONTROL_SERVER_IDLE_RECONNECT_MS` is used by `b_side_omnid`
|
||||
- `OMNI_VIDEO_MAX_FRAME_AGE_MS` is used by `start-backend.sh` on the A-side backend, not by `b_side_omnid`
|
||||
- `OMNISOCKET_TELEMETRY_INTERVAL_MS` and `OMNISOCKET_TELEMETRY_STALE_AFTER_MS` tune the backend's D-side telemetry freshness window
|
||||
- `OMNI_NETWORK_SUMMARY_LOG_*` controls the A-side JSONL summary logger that polls `GET /api/network/latest/`
|
||||
|
||||
Default long-run network logging:
|
||||
|
||||
- A-side starts a compact JSONL logger by default at `${OMNISOCKETGO_ROOT}/logs/a-network-summary.jsonl`
|
||||
- The default A-side polling interval is `2000 ms`
|
||||
- For D-side long runs, prefer:
|
||||
|
||||
```bash
|
||||
./bin/kcpserver -listen 0.0.0.0:10909 \
|
||||
-telemetry-peer peer-a-telemetry \
|
||||
-telemetry-interval 1000ms \
|
||||
-kcp-session-stats-log logs/d-kcp-stats.jsonl \
|
||||
-kcp-session-stats-interval 1000ms
|
||||
```
|
||||
|
||||
- Keep `-latency-log` and `-kcp-ts-debug-log` off by default for multi-hour runs
|
||||
- Do not continuously redirect relay `C` stderr to a file unless you are reproducing a short issue window
|
||||
|
||||
Put machine-specific overrides into `scripts/dev/robot-remote.env.local`. Example:
|
||||
|
||||
```bash
|
||||
ROBOT_COMMAND_CENTER_ROOT="$HOME/Documents/robot-command-center"
|
||||
OMNI_CAMERA_DEVICE="/dev/video30"
|
||||
B_SIDE_OMNID_USE_SUDO="0"
|
||||
OMNI_NETWORK_SUMMARY_LOG_INTERVAL_MS="5000"
|
||||
```
|
||||
|
||||
Camera occupancy handling is deliberately narrow. `check` only reports owners and fails if the
|
||||
device is busy. `release-known` stops exactly `OMNI_CAMERA_RELEASE_SERVICE`, then checks the device
|
||||
again. It never kills an arbitrary PID and refuses to stop `proc_manager.service` automatically:
|
||||
|
||||
```bash
|
||||
OMNI_CAMERA_DEVICE="/dev/video18"
|
||||
OMNI_CAMERA_OCCUPANCY_POLICY="release-known"
|
||||
OMNI_CAMERA_RELEASE_SERVICE="orbbec_waist.service"
|
||||
```
|
||||
|
||||
Run the preflight without starting the daemon:
|
||||
|
||||
```bash
|
||||
bash scripts/dev/prepare-camera-device.sh
|
||||
```
|
||||
|
||||
If a remaining owner belongs to `proc_manager.service`, inspect it with `ros2 component list` and
|
||||
unload only the camera component. Stopping the complete process manager can interrupt unrelated
|
||||
robot functions.
|
||||
|
||||
Default camera behavior is the `night` preset:
|
||||
|
||||
```bash
|
||||
OMNI_CAMERA_PROFILE="night"
|
||||
# Optional per-machine tweak:
|
||||
OMNI_CAMERA_BRIGHTNESS="8"
|
||||
```
|
||||
|
||||
To switch to a daytime preset with brightness only:
|
||||
|
||||
```bash
|
||||
OMNI_CAMERA_PROFILE="day"
|
||||
OMNI_CAMERA_BRIGHTNESS="8"
|
||||
```
|
||||
|
||||
To send the raw `v4l2-ctl --set-ctrl=...` payload yourself:
|
||||
|
||||
```bash
|
||||
OMNI_CAMERA_PROFILE="custom"
|
||||
OMNI_CAMERA_CUSTOM_CTRL="brightness=8,auto_exposure=1,exposure_time_absolute=800,gain=64"
|
||||
OMNI_CAMERA_VERIFY="1"
|
||||
```
|
||||
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import html
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Aggregate run logs into control/video latency estimate outputs.")
|
||||
parser.add_argument("--run-dir", required=True, help="Run directory containing JSONL logs.")
|
||||
parser.add_argument("--output-dir", help="Output directory. Defaults to --run-dir.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def iter_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
records: list[dict[str, Any]] = []
|
||||
if not path.exists():
|
||||
return records
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
for raw_line in handle:
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(payload, dict):
|
||||
records.append(payload)
|
||||
return records
|
||||
|
||||
|
||||
def load_glob_jsonl(run_dir: Path, pattern: str) -> list[dict[str, Any]]:
|
||||
records: list[dict[str, Any]] = []
|
||||
for path in sorted(run_dir.glob(pattern)):
|
||||
records.extend(iter_jsonl(path))
|
||||
return records
|
||||
|
||||
|
||||
def write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
|
||||
with path.open("w", encoding="utf-8") as handle:
|
||||
for record in records:
|
||||
handle.write(json.dumps(record, ensure_ascii=False, separators=(",", ":")))
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def parse_unix_ms(value: Any) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return None
|
||||
if text.endswith("Z"):
|
||||
text = f"{text[:-1]}+00:00"
|
||||
try:
|
||||
return int(datetime.fromisoformat(text).astimezone(timezone.utc).timestamp() * 1000)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def flatten_net_epoch(samples: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
flattened: list[dict[str, Any]] = []
|
||||
for sample in samples:
|
||||
links = sample.get("links") or {}
|
||||
a_to_d = (links.get("a_to_d") or {}).get("sessions") or {}
|
||||
d_to_b = (links.get("d_to_b") or {}).get("sessions") or {}
|
||||
a_control = (a_to_d.get("control") or {}).get("kcp") or {}
|
||||
d_control = (d_to_b.get("control") or {}).get("kcp") or {}
|
||||
a_video = (a_to_d.get("video") or {}).get("kcp") or {}
|
||||
d_video = (d_to_b.get("video") or {}).get("kcp") or {}
|
||||
flattened.append(
|
||||
{
|
||||
"updated_at": sample.get("updated_at"),
|
||||
"a_to_d_control_srtt_ms": a_control.get("srtt_ms"),
|
||||
"a_to_d_control_min_srtt_ms": a_control.get("min_srtt_ms"),
|
||||
"d_to_b_control_srtt_ms": d_control.get("srtt_ms"),
|
||||
"d_to_b_control_min_srtt_ms": d_control.get("min_srtt_ms"),
|
||||
"a_to_d_video_srtt_ms": a_video.get("srtt_ms"),
|
||||
"a_to_d_video_min_srtt_ms": a_video.get("min_srtt_ms"),
|
||||
"d_to_b_video_srtt_ms": d_video.get("srtt_ms"),
|
||||
"d_to_b_video_min_srtt_ms": d_video.get("min_srtt_ms"),
|
||||
"a_to_d_control_feedback_age_ms": a_control.get("last_feedback_age_ms"),
|
||||
"d_to_b_control_feedback_age_ms": d_control.get("last_feedback_age_ms"),
|
||||
"a_to_d_video_feedback_age_ms": a_video.get("last_feedback_age_ms"),
|
||||
"d_to_b_video_feedback_age_ms": d_video.get("last_feedback_age_ms"),
|
||||
"a_to_d_control_retrans_delta": ((a_to_d.get("control") or {}).get("trend") or {}).get("retrans_delta"),
|
||||
"d_to_b_control_retrans_delta": ((d_to_b.get("control") or {}).get("trend") or {}).get("retrans_delta"),
|
||||
"a_to_d_video_retrans_delta": ((a_to_d.get("video") or {}).get("trend") or {}).get("retrans_delta"),
|
||||
"d_to_b_video_retrans_delta": ((d_to_b.get("video") or {}).get("trend") or {}).get("retrans_delta"),
|
||||
"a_to_d_video_window_pressure_pct": a_video.get("window_pressure_pct"),
|
||||
"d_to_b_video_window_pressure_pct": d_video.get("window_pressure_pct"),
|
||||
"robot_health": sample.get("robot_health"),
|
||||
}
|
||||
)
|
||||
return flattened
|
||||
|
||||
|
||||
def aggregate_control_estimates(
|
||||
network_samples: list[dict[str, Any]],
|
||||
control_events: list[dict[str, Any]],
|
||||
control_acks: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
if control_acks:
|
||||
return control_acks
|
||||
|
||||
fallback: list[dict[str, Any]] = []
|
||||
for sample in network_samples:
|
||||
estimate = sample.get("latency_estimate") or {}
|
||||
fallback.append(
|
||||
{
|
||||
"updated_at": sample.get("updated_at"),
|
||||
"estimate_method": "srtt_fallback",
|
||||
"control_loop_rtt_ms": estimate.get("control_loop_rtt_ms"),
|
||||
"control_to_persist_est_ms": estimate.get("control_to_persist_est_ms"),
|
||||
"control_oneway_srtt_est_ms": estimate.get("control_oneway_srtt_est_ms"),
|
||||
"control_oneway_bestcase_est_ms": estimate.get("control_oneway_bestcase_est_ms"),
|
||||
"source_event_count": len(control_events),
|
||||
}
|
||||
)
|
||||
return fallback
|
||||
|
||||
|
||||
def aggregate_video_estimates(
|
||||
network_samples: list[dict[str, Any]],
|
||||
frame_recv_records: list[dict[str, Any]],
|
||||
display_probe_records: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
network_timeline = sorted(
|
||||
(
|
||||
(updated_at_ms, sample.get("latency_estimate") or {})
|
||||
for sample in network_samples
|
||||
for updated_at_ms in [parse_unix_ms(sample.get("updated_at"))]
|
||||
if updated_at_ms is not None
|
||||
),
|
||||
key=lambda item: item[0],
|
||||
)
|
||||
probes_by_seq = {
|
||||
int(record["frame_seq"]): record
|
||||
for record in display_probe_records
|
||||
if record.get("frame_seq") is not None
|
||||
}
|
||||
estimates: list[dict[str, Any]] = []
|
||||
timeline_index = 0
|
||||
|
||||
for record in frame_recv_records:
|
||||
frame_seq = record.get("frame_seq")
|
||||
if frame_seq is None:
|
||||
continue
|
||||
probe = probes_by_seq.get(int(frame_seq))
|
||||
backend_received_unix_ns = record.get("backend_received_unix_ns")
|
||||
backend_received_unix_ms = None
|
||||
try:
|
||||
if backend_received_unix_ns is not None:
|
||||
backend_received_unix_ms = int(int(backend_received_unix_ns) / 1_000_000)
|
||||
except (TypeError, ValueError):
|
||||
backend_received_unix_ms = None
|
||||
|
||||
latency_estimate: dict[str, Any] = {}
|
||||
if backend_received_unix_ms is not None and network_timeline:
|
||||
while timeline_index + 1 < len(network_timeline) and network_timeline[timeline_index + 1][0] <= backend_received_unix_ms:
|
||||
timeline_index += 1
|
||||
if network_timeline[timeline_index][0] <= backend_received_unix_ms:
|
||||
latency_estimate = network_timeline[timeline_index][1]
|
||||
|
||||
network_oneway = latency_estimate.get("video_network_oneway_est_ms")
|
||||
capture_to_send = record.get("b_side_capture_to_send_ms")
|
||||
partial_est = None
|
||||
if capture_to_send is not None or network_oneway is not None:
|
||||
partial_est = round(float(capture_to_send or 0.0) + float(network_oneway or 0.0), 3)
|
||||
request_to_paint_ms = None
|
||||
if probe is not None and probe.get("request_to_paint_ms") is not None:
|
||||
request_to_paint_ms = round(float(probe["request_to_paint_ms"]), 3)
|
||||
elif probe is not None and probe.get("request_started_unix_ms") is not None and probe.get("paint_unix_ms") is not None:
|
||||
request_to_paint_ms = round(float(probe["paint_unix_ms"]) - float(probe["request_started_unix_ms"]), 3)
|
||||
video_e2e_est_ms = round(partial_est + request_to_paint_ms, 3) if partial_est is not None and request_to_paint_ms is not None else None
|
||||
estimates.append(
|
||||
{
|
||||
"frame_seq": frame_seq,
|
||||
"backend_received_unix_ns": record.get("backend_received_unix_ns"),
|
||||
"frame_hash": record.get("frame_hash"),
|
||||
"estimate_method": "capture_to_send+srtt/2+request_to_paint" if video_e2e_est_ms is not None else "capture_to_send+srtt/2",
|
||||
"video_network_oneway_est_ms": network_oneway,
|
||||
"b_side_capture_to_send_ms": capture_to_send,
|
||||
"request_to_paint_ms": request_to_paint_ms,
|
||||
"response_to_paint_ms": probe.get("response_to_paint_ms") if probe is not None else None,
|
||||
"backend_to_request_ms": probe.get("backend_to_request_ms") if probe is not None else None,
|
||||
"backend_to_request_ms_raw": probe.get("backend_to_request_ms_raw") if probe is not None else None,
|
||||
"backend_to_paint_ms": probe.get("backend_to_paint_ms") if probe is not None else None,
|
||||
"backend_to_paint_ms_raw": probe.get("backend_to_paint_ms_raw") if probe is not None else None,
|
||||
"browser_backend_clock_offset_ms": probe.get("browser_backend_clock_offset_ms") if probe is not None else None,
|
||||
"browser_backend_clock_rtt_ms": probe.get("browser_backend_clock_rtt_ms") if probe is not None else None,
|
||||
"video_partial_est_ms": partial_est,
|
||||
"video_e2e_est_ms": video_e2e_est_ms,
|
||||
"sequence_gap": record.get("sequence_gap"),
|
||||
"repeat_flag": record.get("repeat_flag"),
|
||||
"sender_clock_delta_ms_raw": record.get("sender_clock_delta_ms_raw"),
|
||||
}
|
||||
)
|
||||
return estimates
|
||||
|
||||
|
||||
def write_html_summary(
|
||||
path: Path,
|
||||
*,
|
||||
net_epochs: list[dict[str, Any]],
|
||||
control_estimates: list[dict[str, Any]],
|
||||
video_estimates: list[dict[str, Any]],
|
||||
) -> None:
|
||||
latest_control = control_estimates[-1] if control_estimates else {}
|
||||
latest_video = video_estimates[-1] if video_estimates else {}
|
||||
latest_net = net_epochs[-1] if net_epochs else {}
|
||||
html_text = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Latency Estimates</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 24px; background: #0b1020; color: #eef2ff; }}
|
||||
.grid {{ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }}
|
||||
.card {{ border: 1px solid #334155; border-radius: 8px; padding: 16px; background: #111827; }}
|
||||
h1, h2 {{ margin-top: 0; }}
|
||||
p {{ margin: 6px 0; line-height: 1.5; }}
|
||||
code {{ color: #93c5fd; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Latency Estimates</h1>
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Control</h2>
|
||||
<p><strong>loop RTT:</strong> {html.escape(str(latest_control.get("control_loop_rtt_ms")))}</p>
|
||||
<p><strong>to persist:</strong> {html.escape(str(latest_control.get("control_to_persist_est_ms")))}</p>
|
||||
<p><strong>method:</strong> {html.escape(str(latest_control.get("estimate_method")))}</p>
|
||||
<p><strong>samples:</strong> {len(control_estimates)}</p>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Video</h2>
|
||||
<p><strong>network one-way:</strong> {html.escape(str(latest_video.get("video_network_oneway_est_ms")))}</p>
|
||||
<p><strong>partial:</strong> {html.escape(str(latest_video.get("video_partial_est_ms")))}</p>
|
||||
<p><strong>end-to-end:</strong> {html.escape(str(latest_video.get("video_e2e_est_ms")))}</p>
|
||||
<p><strong>samples:</strong> {len(video_estimates)}</p>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Net Epoch</h2>
|
||||
<p><strong>a→d control srtt:</strong> {html.escape(str(latest_net.get("a_to_d_control_srtt_ms")))}</p>
|
||||
<p><strong>d→b control srtt:</strong> {html.escape(str(latest_net.get("d_to_b_control_srtt_ms")))}</p>
|
||||
<p><strong>a→d video srtt:</strong> {html.escape(str(latest_net.get("a_to_d_video_srtt_ms")))}</p>
|
||||
<p><strong>d→b video srtt:</strong> {html.escape(str(latest_net.get("d_to_b_video_srtt_ms")))}</p>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
path.write_text(html_text, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
run_dir = Path(args.run_dir).resolve()
|
||||
output_dir = Path(args.output_dir).resolve() if args.output_dir else run_dir
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
network_samples = load_glob_jsonl(run_dir, "a-network-summary.*.jsonl")
|
||||
control_events = load_glob_jsonl(run_dir, "a-control-events.*.jsonl")
|
||||
control_acks = load_glob_jsonl(run_dir, "a-control-acks.*.jsonl")
|
||||
frame_recv_records = load_glob_jsonl(run_dir, "a-video-frame-recv.*.jsonl")
|
||||
display_probe_records = load_glob_jsonl(run_dir, "a-video-display-probe.*.jsonl")
|
||||
|
||||
net_epochs = flatten_net_epoch(network_samples)
|
||||
control_estimates = aggregate_control_estimates(network_samples, control_events, control_acks)
|
||||
video_estimates = aggregate_video_estimates(network_samples, frame_recv_records, display_probe_records)
|
||||
|
||||
write_jsonl(output_dir / "net-epoch-summary.jsonl", net_epochs)
|
||||
write_jsonl(output_dir / "control-latency-estimates.jsonl", control_estimates)
|
||||
write_jsonl(output_dir / "video-latency-estimates.jsonl", video_estimates)
|
||||
write_html_summary(
|
||||
output_dir / "latency-estimates.html",
|
||||
net_epochs=net_epochs,
|
||||
control_estimates=control_estimates,
|
||||
video_estimates=video_estimates,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
|
||||
camera_device="${OMNI_CAMERA_DEVICE}"
|
||||
camera_profile="${OMNI_CAMERA_PROFILE}"
|
||||
camera_brightness="${OMNI_CAMERA_BRIGHTNESS}"
|
||||
camera_custom_ctrl="${OMNI_CAMERA_CUSTOM_CTRL}"
|
||||
camera_verify="${OMNI_CAMERA_VERIFY}"
|
||||
|
||||
is_truthy() {
|
||||
case "${1:-0}" in
|
||||
1|true|TRUE|yes|YES|on|ON)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_v4l2_ctl() {
|
||||
if command -v v4l2-ctl >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Missing required command: v4l2-ctl. Install v4l-utils on the robot side before starting b_side_omnid." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_v4l2_ctl() {
|
||||
v4l2-ctl -d "${camera_device}" "$@"
|
||||
}
|
||||
|
||||
set_ctrl() {
|
||||
local ctrl="$1"
|
||||
|
||||
echo "[camera-controls] set ${camera_device} ${ctrl}"
|
||||
run_v4l2_ctl "--set-ctrl=${ctrl}"
|
||||
}
|
||||
|
||||
verify_ctrl() {
|
||||
local ctrl="$1"
|
||||
|
||||
echo "[camera-controls] verify ${camera_device} ${ctrl}"
|
||||
run_v4l2_ctl "--get-ctrl=${ctrl}"
|
||||
}
|
||||
|
||||
needs_v4l2_ctl=0
|
||||
|
||||
case "${camera_profile}" in
|
||||
night)
|
||||
needs_v4l2_ctl=1
|
||||
;;
|
||||
day)
|
||||
if [[ -n "${camera_brightness}" ]]; then
|
||||
needs_v4l2_ctl=1
|
||||
fi
|
||||
;;
|
||||
custom)
|
||||
if [[ -z "${camera_custom_ctrl}" ]]; then
|
||||
echo "OMNI_CAMERA_CUSTOM_CTRL must be non-empty when OMNI_CAMERA_PROFILE=custom." >&2
|
||||
exit 1
|
||||
fi
|
||||
needs_v4l2_ctl=1
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported OMNI_CAMERA_PROFILE: ${camera_profile}. Expected one of: night, day, custom." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if is_truthy "${camera_verify}"; then
|
||||
needs_v4l2_ctl=1
|
||||
fi
|
||||
|
||||
if [[ "${needs_v4l2_ctl}" == "0" ]]; then
|
||||
echo "[camera-controls] profile=${camera_profile}; no camera controls requested"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_v4l2_ctl
|
||||
|
||||
case "${camera_profile}" in
|
||||
night)
|
||||
set_ctrl "auto_exposure=1"
|
||||
set_ctrl "exposure_time_absolute=800"
|
||||
set_ctrl "gain=64"
|
||||
if [[ -n "${camera_brightness}" ]]; then
|
||||
set_ctrl "brightness=${camera_brightness}"
|
||||
fi
|
||||
;;
|
||||
day)
|
||||
if [[ -n "${camera_brightness}" ]]; then
|
||||
set_ctrl "brightness=${camera_brightness}"
|
||||
fi
|
||||
;;
|
||||
custom)
|
||||
set_ctrl "${camera_custom_ctrl}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if is_truthy "${camera_verify}"; then
|
||||
verify_ctrl "auto_exposure"
|
||||
verify_ctrl "exposure_time_absolute"
|
||||
verify_ctrl "gain"
|
||||
verify_ctrl "brightness"
|
||||
fi
|
||||
335
robot/v4l2/OmniSocketGo_robot/scripts/dev/load-env.sh
Normal file
335
robot/v4l2/OmniSocketGo_robot/scripts/dev/load-env.sh
Normal file
@@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
LOAD_ENV_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEFAULT_OMNISOCKETGO_ROOT="$(cd "${LOAD_ENV_SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
die() {
|
||||
echo "$*" >&2
|
||||
return 1 2>/dev/null || exit 1
|
||||
}
|
||||
|
||||
normalize_loaded_env_vars() {
|
||||
local var_name
|
||||
local value
|
||||
|
||||
for var_name in $(compgen -A variable); do
|
||||
case "${var_name}" in
|
||||
BACKEND_*|BLITZ_*|B_SIDE_*|CONTROL_*|FRONTEND_*|OMNI_*|PYTHON3_BIN|PYTHON_VENV_PATH|ROBOT_*|ROS_DISTRO|VITE_*)
|
||||
value="${!var_name}"
|
||||
if [[ "${value}" == *$'\r' ]]; then
|
||||
printf -v "${var_name}" '%s' "${value%$'\r'}"
|
||||
export "${var_name}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
is_omnisocketgo_root() {
|
||||
local dir="$1"
|
||||
[[ -f "${dir}/Makefile" && -f "${dir}/cmd/b_side_omnid.c" && -d "${dir}/ros-control-py" ]]
|
||||
}
|
||||
|
||||
is_robot_command_center_root() {
|
||||
local dir="$1"
|
||||
[[ -f "${dir}/backend/config/asgi.py" && -f "${dir}/frontend/package.json" ]]
|
||||
}
|
||||
|
||||
require_robot_command_center_root() {
|
||||
if ! is_robot_command_center_root "${ROBOT_COMMAND_CENTER_ROOT}"; then
|
||||
die "ROBOT_COMMAND_CENTER_ROOT must point to the robot-command-center repo root. Current value: ${ROBOT_COMMAND_CENTER_ROOT}. Set it in ${LOAD_ENV_SCRIPT_DIR}/robot-remote.env.local if needed."
|
||||
fi
|
||||
}
|
||||
|
||||
export OMNISOCKETGO_ROOT="${OMNISOCKETGO_ROOT:-${DEFAULT_OMNISOCKETGO_ROOT}}"
|
||||
|
||||
omni_camera_device_was_set=0
|
||||
omni_camera_profile_was_set=0
|
||||
omni_camera_brightness_was_set=0
|
||||
omni_camera_custom_ctrl_was_set=0
|
||||
omni_camera_verify_was_set=0
|
||||
|
||||
if [[ "${OMNI_CAMERA_DEVICE+x}" == "x" ]]; then
|
||||
omni_camera_device_was_set=1
|
||||
preserved_omni_camera_device="${OMNI_CAMERA_DEVICE}"
|
||||
fi
|
||||
if [[ "${OMNI_CAMERA_PROFILE+x}" == "x" ]]; then
|
||||
omni_camera_profile_was_set=1
|
||||
preserved_omni_camera_profile="${OMNI_CAMERA_PROFILE}"
|
||||
fi
|
||||
if [[ "${OMNI_CAMERA_BRIGHTNESS+x}" == "x" ]]; then
|
||||
omni_camera_brightness_was_set=1
|
||||
preserved_omni_camera_brightness="${OMNI_CAMERA_BRIGHTNESS}"
|
||||
fi
|
||||
if [[ "${OMNI_CAMERA_CUSTOM_CTRL+x}" == "x" ]]; then
|
||||
omni_camera_custom_ctrl_was_set=1
|
||||
preserved_omni_camera_custom_ctrl="${OMNI_CAMERA_CUSTOM_CTRL}"
|
||||
fi
|
||||
if [[ "${OMNI_CAMERA_VERIFY+x}" == "x" ]]; then
|
||||
omni_camera_verify_was_set=1
|
||||
preserved_omni_camera_verify="${OMNI_CAMERA_VERIFY}"
|
||||
fi
|
||||
|
||||
ENV_FILES=(
|
||||
"${LOAD_ENV_SCRIPT_DIR}/robot-remote.env"
|
||||
"${LOAD_ENV_SCRIPT_DIR}/robot-remote.env.local"
|
||||
)
|
||||
|
||||
for env_file in "${ENV_FILES[@]}"; do
|
||||
if [[ -f "${env_file}" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "${env_file}"
|
||||
set +a
|
||||
fi
|
||||
done
|
||||
|
||||
normalize_loaded_env_vars
|
||||
|
||||
if [[ "${omni_camera_device_was_set}" == "1" ]]; then
|
||||
export OMNI_CAMERA_DEVICE="${preserved_omni_camera_device}"
|
||||
fi
|
||||
if [[ "${omni_camera_profile_was_set}" == "1" ]]; then
|
||||
export OMNI_CAMERA_PROFILE="${preserved_omni_camera_profile}"
|
||||
fi
|
||||
if [[ "${omni_camera_brightness_was_set}" == "1" ]]; then
|
||||
export OMNI_CAMERA_BRIGHTNESS="${preserved_omni_camera_brightness}"
|
||||
fi
|
||||
if [[ "${omni_camera_custom_ctrl_was_set}" == "1" ]]; then
|
||||
export OMNI_CAMERA_CUSTOM_CTRL="${preserved_omni_camera_custom_ctrl}"
|
||||
fi
|
||||
if [[ "${omni_camera_verify_was_set}" == "1" ]]; then
|
||||
export OMNI_CAMERA_VERIFY="${preserved_omni_camera_verify}"
|
||||
fi
|
||||
|
||||
export OMNISOCKETGO_ROOT="${OMNISOCKETGO_ROOT:-${DEFAULT_OMNISOCKETGO_ROOT}}"
|
||||
export ROBOT_COMMAND_CENTER_ROOT="${ROBOT_COMMAND_CENTER_ROOT:-$(dirname "${OMNISOCKETGO_ROOT}")/robot-command-center}"
|
||||
|
||||
if ! is_omnisocketgo_root "${OMNISOCKETGO_ROOT}"; then
|
||||
die "OMNISOCKETGO_ROOT must point to the OmniSocketGo repo root. Current value: ${OMNISOCKETGO_ROOT}"
|
||||
fi
|
||||
|
||||
export BACKEND_DIR="${BACKEND_DIR:-${ROBOT_COMMAND_CENTER_ROOT}/backend}"
|
||||
export FRONTEND_DIR="${FRONTEND_DIR:-${ROBOT_COMMAND_CENTER_ROOT}/frontend}"
|
||||
export ROS_CONTROL_PY_DIR="${ROS_CONTROL_PY_DIR:-${OMNISOCKETGO_ROOT}/ros-control-py}"
|
||||
export PYTHON3_BIN="${PYTHON3_BIN:-python3}"
|
||||
export PYTHON_VENV_PATH="${PYTHON_VENV_PATH:-${OMNISOCKETGO_ROOT}/.venv}"
|
||||
export BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}"
|
||||
export BACKEND_PORT="${BACKEND_PORT:-8001}"
|
||||
export FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}"
|
||||
export FRONTEND_PORT="${FRONTEND_PORT:-5173}"
|
||||
export OMNISOCKET_TELEMETRY_PEER_ID="${OMNISOCKET_TELEMETRY_PEER_ID:-peer-a-telemetry}"
|
||||
export OMNISOCKET_TELEMETRY_INTERVAL_MS="${OMNISOCKET_TELEMETRY_INTERVAL_MS:-1000}"
|
||||
export OMNISOCKET_TELEMETRY_STALE_AFTER_MS="${OMNISOCKET_TELEMETRY_STALE_AFTER_MS:-3000}"
|
||||
export OMNI_NETWORK_SUMMARY_LOG_ENABLED="${OMNI_NETWORK_SUMMARY_LOG_ENABLED:-1}"
|
||||
export OMNI_NETWORK_SUMMARY_LOG_PATH="${OMNI_NETWORK_SUMMARY_LOG_PATH:-${OMNISOCKETGO_ROOT}/logs/a-network-summary.jsonl}"
|
||||
export OMNI_NETWORK_SUMMARY_LOG_INTERVAL_MS="${OMNI_NETWORK_SUMMARY_LOG_INTERVAL_MS:-1000}"
|
||||
export OMNI_NETWORK_SUMMARY_LOG_REQUEST_TIMEOUT_SEC="${OMNI_NETWORK_SUMMARY_LOG_REQUEST_TIMEOUT_SEC:-3}"
|
||||
export CONTROL_SIDE_OMNISOCKET_SERVER_ADDR="${CONTROL_SIDE_OMNISOCKET_SERVER_ADDR:-}"
|
||||
export CONTROL_SIDE_OMNISOCKET_RELAY_VIA="${CONTROL_SIDE_OMNISOCKET_RELAY_VIA:-}"
|
||||
export ROBOT_SIDE_OMNISOCKET_SERVER_ADDR="${ROBOT_SIDE_OMNISOCKET_SERVER_ADDR:-}"
|
||||
export ROBOT_SIDE_OMNISOCKET_RELAY_VIA="${ROBOT_SIDE_OMNISOCKET_RELAY_VIA:-}"
|
||||
export ROS_DISTRO="${ROS_DISTRO:-jazzy}"
|
||||
export ROBOT_RECEIVER_TRANSPORT="${ROBOT_RECEIVER_TRANSPORT:-unix_dgram}"
|
||||
export ROBOT_RECEIVER_SERVER_ADDR="${ROBOT_RECEIVER_SERVER_ADDR:-${ROBOT_SIDE_OMNISOCKET_SERVER_ADDR:-}}"
|
||||
export ROBOT_RECEIVER_RELAY_VIA="${ROBOT_RECEIVER_RELAY_VIA:-${ROBOT_SIDE_OMNISOCKET_RELAY_VIA:-}}"
|
||||
export ROBOT_RECEIVER_PEER_ID="${ROBOT_RECEIVER_PEER_ID:-ros-bridge-ctrl}"
|
||||
export ROBOT_RECEIVER_EXPECTED_SENDER="${ROBOT_RECEIVER_EXPECTED_SENDER:-}"
|
||||
export ROBOT_RECEIVER_LOCAL_SOCKET_PATH="${ROBOT_RECEIVER_LOCAL_SOCKET_PATH:-/tmp/omnisocket-b-side-cmd.sock}"
|
||||
export ROBOT_RECEIVER_OUTPUT_TOPIC="${ROBOT_RECEIVER_OUTPUT_TOPIC:-/hric/robot/cmd_vel}"
|
||||
export ROBOT_RECEIVER_FRAME_ID="${ROBOT_RECEIVER_FRAME_ID:-pelvis}"
|
||||
export ROBOT_RECEIVER_WATCHDOG_TIMEOUT="${ROBOT_RECEIVER_WATCHDOG_TIMEOUT:-0.5}"
|
||||
export ROBOT_RECEIVER_PUBLISH_RATE_HZ="${ROBOT_RECEIVER_PUBLISH_RATE_HZ:-100.0}"
|
||||
export OMNI_CAMERA_DEVICE="${OMNI_CAMERA_DEVICE:-/dev/video0}"
|
||||
export OMNI_CAMERA_HEAD_DEVICE="${OMNI_CAMERA_HEAD_DEVICE:-/dev/video26}"
|
||||
export OMNI_CAMERA_WAIST_DEVICE="${OMNI_CAMERA_WAIST_DEVICE:-/dev/video18}"
|
||||
export OMNI_CAMERA_AUTO_DISCOVER="${OMNI_CAMERA_AUTO_DISCOVER:-0}"
|
||||
export OMNI_CAMERA_HEAD_SERIAL="${OMNI_CAMERA_HEAD_SERIAL:-}"
|
||||
export OMNI_CAMERA_WAIST_SERIAL="${OMNI_CAMERA_WAIST_SERIAL:-}"
|
||||
export OMNI_CAMERA_DISCOVERY_WIDTH="${OMNI_CAMERA_DISCOVERY_WIDTH:-1280}"
|
||||
export OMNI_CAMERA_DISCOVERY_HEIGHT="${OMNI_CAMERA_DISCOVERY_HEIGHT:-720}"
|
||||
export OMNI_CAMERA_DISCOVERY_TIMEOUT_SEC="${OMNI_CAMERA_DISCOVERY_TIMEOUT_SEC:-10}"
|
||||
export OMNI_CAMERA_HEAD_RELEASE_SERVICE="${OMNI_CAMERA_HEAD_RELEASE_SERVICE:-orbbec_head.service}"
|
||||
export OMNI_CAMERA_WAIST_RELEASE_SERVICE="${OMNI_CAMERA_WAIST_RELEASE_SERVICE:-orbbec_waist.service}"
|
||||
export OMNI_CAMERA_ACTIVE="${OMNI_CAMERA_ACTIVE:-head}"
|
||||
export OMNI_CAMERA_PROFILE="${OMNI_CAMERA_PROFILE:-night}"
|
||||
export OMNI_CAMERA_BRIGHTNESS="${OMNI_CAMERA_BRIGHTNESS:-}"
|
||||
export OMNI_CAMERA_CUSTOM_CTRL="${OMNI_CAMERA_CUSTOM_CTRL:-}"
|
||||
export OMNI_CAMERA_VERIFY="${OMNI_CAMERA_VERIFY:-0}"
|
||||
export OMNI_GPSD_HOST="${OMNI_GPSD_HOST:-127.0.0.1}"
|
||||
export OMNI_VIDEO_SERVER_ADDR="${OMNI_VIDEO_SERVER_ADDR:-${ROBOT_SIDE_OMNISOCKET_SERVER_ADDR:-}}"
|
||||
export OMNI_VIDEO_RELAY_VIA="${OMNI_VIDEO_RELAY_VIA:-${ROBOT_SIDE_OMNISOCKET_RELAY_VIA:-}}"
|
||||
export OMNI_CONTROL_SERVER_ADDR="${OMNI_CONTROL_SERVER_ADDR:-${ROBOT_SIDE_OMNISOCKET_SERVER_ADDR:-}}"
|
||||
export OMNI_CONTROL_RELAY_VIA="${OMNI_CONTROL_RELAY_VIA:-${ROBOT_SIDE_OMNISOCKET_RELAY_VIA:-}}"
|
||||
export OMNI_CONTROL_UNIX_SOCKET_PATH="${OMNI_CONTROL_UNIX_SOCKET_PATH:-${ROBOT_RECEIVER_LOCAL_SOCKET_PATH}}"
|
||||
export OMNI_CONTROL_ACK_PEER_ID="${OMNI_CONTROL_ACK_PEER_ID:-peer-b-ctrl-ack}"
|
||||
export OMNI_CONTROL_ACK_TARGET_PEER="${OMNI_CONTROL_ACK_TARGET_PEER:-peer-a-ctrl-ack}"
|
||||
export B_SIDE_OMNID_USE_SUDO="${B_SIDE_OMNID_USE_SUDO:-1}"
|
||||
export BLITZ_RUNTIME_DIR="${BLITZ_RUNTIME_DIR:-${OMNISOCKETGO_ROOT}/logs/runtime}"
|
||||
export BLITZ_RUN_ROOT="${BLITZ_RUN_ROOT:-${OMNISOCKETGO_ROOT}/logs}"
|
||||
export BLITZ_RUN_CONTEXT_FILE="${BLITZ_RUN_CONTEXT_FILE:-${BLITZ_RUNTIME_DIR}/run-context.env}"
|
||||
export BLITZ_RUN_ID_FILE="${BLITZ_RUN_ID_FILE:-${BLITZ_RUNTIME_DIR}/run-id}"
|
||||
export BLITZ_CURRENT_RUN_LINK="${BLITZ_CURRENT_RUN_LINK:-${BLITZ_RUN_ROOT}/current}"
|
||||
export BLITZ_5G_INTERFACE="${BLITZ_5G_INTERFACE:-}"
|
||||
export BLITZ_5G_MODEM_SUBNET="${BLITZ_5G_MODEM_SUBNET:-192.168.224.0/22}"
|
||||
export BLITZ_5G_GATEWAY="${BLITZ_5G_GATEWAY:-192.168.225.1}"
|
||||
export BLITZ_5G_ROUTE_TARGETS="${BLITZ_5G_ROUTE_TARGETS:-106.55.173.235}"
|
||||
export BLITZ_5G_INFO_JSON="${BLITZ_5G_INFO_JSON:-${OMNISOCKETGO_ROOT}/scripts/boot/modem_network_info.json}"
|
||||
export BLITZ_TIME_SERVER_IP="${BLITZ_TIME_SERVER_IP:-}"
|
||||
export BLITZ_KCP_STATS_INTERVAL_MS="${BLITZ_KCP_STATS_INTERVAL_MS:-1000}"
|
||||
export BLITZ_CONTROL_LATENCY_LOG_ENABLED="${BLITZ_CONTROL_LATENCY_LOG_ENABLED:-1}"
|
||||
export BLITZ_CONTROL_LATENCY_LOG_SAMPLE_MOD="${BLITZ_CONTROL_LATENCY_LOG_SAMPLE_MOD:-100}"
|
||||
export BLITZ_CONTROL_ACK_SAMPLE_MOD="${BLITZ_CONTROL_ACK_SAMPLE_MOD:-10}"
|
||||
export BLITZ_VIDEO_STAGE_LOG_ENABLED="${BLITZ_VIDEO_STAGE_LOG_ENABLED:-1}"
|
||||
export BLITZ_VIDEO_STAGE_LOG_SAMPLE_MOD="${BLITZ_VIDEO_STAGE_LOG_SAMPLE_MOD:-10}"
|
||||
export BLITZ_5G_LINK_LOG_INTERVAL_SEC="${BLITZ_5G_LINK_LOG_INTERVAL_SEC:-5}"
|
||||
export BLITZ_JSONL_FLUSH_INTERVAL_MS="${BLITZ_JSONL_FLUSH_INTERVAL_MS:-1000}"
|
||||
export BLITZ_JSONL_FLUSH_BYTES="${BLITZ_JSONL_FLUSH_BYTES:-262144}"
|
||||
export BLITZ_JSONL_ROTATE_BYTES="${BLITZ_JSONL_ROTATE_BYTES:-134217728}"
|
||||
export BLITZ_JSONL_ROTATE_FILES="${BLITZ_JSONL_ROTATE_FILES:-8}"
|
||||
|
||||
blitz_dev_utc_compact_timestamp() {
|
||||
date -u '+%Y%m%dT%H%M%SZ'
|
||||
}
|
||||
|
||||
blitz_dev_git_commit() {
|
||||
git -C "${OMNISOCKETGO_ROOT}" rev-parse HEAD 2>/dev/null || true
|
||||
}
|
||||
|
||||
blitz_dev_git_dirty_flag() {
|
||||
if git -C "${OMNISOCKETGO_ROOT}" diff --quiet --ignore-submodules=dirty >/dev/null 2>&1; then
|
||||
printf '0\n'
|
||||
return 0
|
||||
fi
|
||||
printf '1\n'
|
||||
}
|
||||
|
||||
blitz_dev_prepare_dirs() {
|
||||
mkdir -p "${BLITZ_RUNTIME_DIR}" "${BLITZ_RUN_ROOT}/runs" "${BLITZ_RUN_ROOT}/incidents"
|
||||
}
|
||||
|
||||
blitz_dev_write_run_info() {
|
||||
local run_dir="$1"
|
||||
local run_id="$2"
|
||||
local boot_id="$3"
|
||||
local tmp_info="${run_dir}/run-info.json.tmp.$$"
|
||||
local started_at
|
||||
local commit_hash
|
||||
local dirty_flag
|
||||
|
||||
started_at="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
commit_hash="$(blitz_dev_git_commit)"
|
||||
dirty_flag="$(blitz_dev_git_dirty_flag)"
|
||||
|
||||
python3 - "${tmp_info}" "${run_id}" "${run_dir}" "${boot_id}" "${started_at}" "${commit_hash}" "${dirty_flag}" "${HOSTNAME:-$(hostname)}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
path, run_id, run_dir, boot_id, started_at, commit_hash, dirty_flag, hostname = sys.argv[1:9]
|
||||
payload = {
|
||||
"run_id": run_id,
|
||||
"run_dir": run_dir,
|
||||
"boot_id": boot_id,
|
||||
"started_at": started_at,
|
||||
"hostname": hostname,
|
||||
"git_commit": commit_hash,
|
||||
"git_dirty": dirty_flag == "1",
|
||||
"env": {
|
||||
key: os.environ.get(key, "")
|
||||
for key in sorted(os.environ)
|
||||
if key.startswith(("BLITZ_", "OMNI_", "ROBOT_RECEIVER_"))
|
||||
},
|
||||
}
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
PY
|
||||
mv -f "${tmp_info}" "${run_dir}/run-info.json"
|
||||
}
|
||||
|
||||
blitz_dev_init_run_context() {
|
||||
local run_id="${1:-$(blitz_dev_utc_compact_timestamp)}"
|
||||
local boot_id="dev-$(blitz_dev_utc_compact_timestamp)"
|
||||
local run_dir="${BLITZ_RUN_ROOT}/runs/${run_id}"
|
||||
local tmp_context="${BLITZ_RUN_CONTEXT_FILE}.tmp.$$"
|
||||
|
||||
blitz_dev_prepare_dirs
|
||||
mkdir -p "${run_dir}"
|
||||
export BLITZ_RUN_ID="${run_id}"
|
||||
export BLITZ_RUN_DIR="${run_dir}"
|
||||
export BLITZ_BOOT_ID="${boot_id}"
|
||||
printf '%s\n' "${run_id}" > "${BLITZ_RUN_ID_FILE}"
|
||||
cat > "${tmp_context}" <<EOF
|
||||
BLITZ_RUN_ID=${run_id}
|
||||
BLITZ_RUN_DIR=${run_dir}
|
||||
BLITZ_BOOT_ID=${boot_id}
|
||||
BLITZ_RUN_ROOT=${BLITZ_RUN_ROOT}
|
||||
EOF
|
||||
mv -f "${tmp_context}" "${BLITZ_RUN_CONTEXT_FILE}"
|
||||
ln -sfn "${run_dir}" "${BLITZ_CURRENT_RUN_LINK}"
|
||||
blitz_dev_write_run_info "${run_dir}" "${run_id}" "${boot_id}"
|
||||
}
|
||||
|
||||
blitz_dev_load_run_context() {
|
||||
if [[ ! -f "${BLITZ_RUN_CONTEXT_FILE}" ]]; then
|
||||
return 1
|
||||
fi
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "${BLITZ_RUN_CONTEXT_FILE}"
|
||||
set +a
|
||||
}
|
||||
|
||||
blitz_dev_ensure_run_context() {
|
||||
if blitz_dev_load_run_context; then
|
||||
return 0
|
||||
fi
|
||||
blitz_dev_init_run_context
|
||||
}
|
||||
|
||||
blitz_dev_reset_run_context() {
|
||||
rm -f "${BLITZ_RUN_CONTEXT_FILE}" "${BLITZ_RUN_ID_FILE}"
|
||||
blitz_dev_init_run_context
|
||||
}
|
||||
|
||||
blitz_dev_init_instance_context() {
|
||||
if [[ -z "${BLITZ_INSTANCE_ID:-}" ]]; then
|
||||
export BLITZ_INSTANCE_ID="$(blitz_dev_utc_compact_timestamp)-$$"
|
||||
fi
|
||||
}
|
||||
|
||||
blitz_dev_component_log_path() {
|
||||
local stem="$1"
|
||||
printf '%s/%s.%s.jsonl\n' "${BLITZ_RUN_DIR}" "${stem}" "${BLITZ_INSTANCE_ID}"
|
||||
}
|
||||
|
||||
blitz_dev_prepare_backend_logging_env() {
|
||||
blitz_dev_init_instance_context
|
||||
if [[ "${OMNI_NETWORK_SUMMARY_LOG_PATH}" == "${OMNISOCKETGO_ROOT}/logs/a-network-summary.jsonl" ]]; then
|
||||
export OMNI_NETWORK_SUMMARY_LOG_PATH
|
||||
OMNI_NETWORK_SUMMARY_LOG_PATH="$(blitz_dev_component_log_path "a-network-summary")"
|
||||
fi
|
||||
export BLITZ_A_CONTROL_EVENTS_LOG_PATH="${BLITZ_A_CONTROL_EVENTS_LOG_PATH:-$(blitz_dev_component_log_path "a-control-events")}"
|
||||
export BLITZ_A_CONTROL_ACKS_LOG_PATH="${BLITZ_A_CONTROL_ACKS_LOG_PATH:-$(blitz_dev_component_log_path "a-control-acks")}"
|
||||
export BLITZ_A_VIDEO_FRAME_RECV_LOG_PATH="${BLITZ_A_VIDEO_FRAME_RECV_LOG_PATH:-$(blitz_dev_component_log_path "a-video-frame-recv")}"
|
||||
export BLITZ_A_VIDEO_DISPLAY_PROBE_LOG_PATH="${BLITZ_A_VIDEO_DISPLAY_PROBE_LOG_PATH:-$(blitz_dev_component_log_path "a-video-display-probe")}"
|
||||
}
|
||||
|
||||
blitz_dev_prepare_bside_logging_env() {
|
||||
blitz_dev_init_instance_context
|
||||
export BLITZ_KCP_STATS_LOG_PATH="${BLITZ_KCP_STATS_LOG_PATH:-$(blitz_dev_component_log_path "b-kcp-session-stats")}"
|
||||
export BLITZ_CONTROL_LATENCY_LOG_PATH="${BLITZ_CONTROL_LATENCY_LOG_PATH:-$(blitz_dev_component_log_path "b-control-latency")}"
|
||||
export BLITZ_VIDEO_STAGE_LOG_PATH="${BLITZ_VIDEO_STAGE_LOG_PATH:-$(blitz_dev_component_log_path "b-video-frame-stages")}"
|
||||
}
|
||||
|
||||
blitz_dev_prepare_5g_logging_env() {
|
||||
blitz_dev_init_instance_context
|
||||
export BLITZ_5G_LINK_LOG_PATH="${BLITZ_5G_LINK_LOG_PATH:-$(blitz_dev_component_log_path "b-5g-link-quality")}"
|
||||
}
|
||||
|
||||
if [[ "${BLITZ_SKIP_DEV_RUN_CONTEXT_INIT:-0}" != "1" && "${BLITZ_BOOT_LOADING_ENV:-0}" != "1" && "${OMNI_BOOT_MODE:-0}" != "1" ]]; then
|
||||
blitz_dev_ensure_run_context
|
||||
elif [[ -f "${BLITZ_RUN_CONTEXT_FILE}" ]]; then
|
||||
blitz_dev_load_run_context || true
|
||||
fi
|
||||
103
robot/v4l2/OmniSocketGo_robot/scripts/dev/log-network-summary.py
Normal file
103
robot/v4l2/OmniSocketGo_robot/scripts/dev/log-network-summary.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
STOP_REQUESTED = False
|
||||
LOCAL_HTTP_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
|
||||
|
||||
def handle_signal(signum: int, frame: object) -> None:
|
||||
del signum, frame
|
||||
global STOP_REQUESTED
|
||||
STOP_REQUESTED = True
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Poll /api/network/latest/ and append JSONL snapshots.")
|
||||
parser.add_argument("--url", required=True, help="HTTP endpoint that returns the network summary JSON.")
|
||||
parser.add_argument("--output", required=True, help="Output JSONL path.")
|
||||
parser.add_argument(
|
||||
"--interval-ms",
|
||||
type=int,
|
||||
default=2000,
|
||||
help="Polling interval in milliseconds. Default: 2000.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--request-timeout-sec",
|
||||
type=float,
|
||||
default=3.0,
|
||||
help="Single request timeout in seconds. Default: 3.0.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def sleep_with_stop(seconds: float) -> None:
|
||||
deadline = time.monotonic() + max(0.0, seconds)
|
||||
while not STOP_REQUESTED:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0.0:
|
||||
return
|
||||
time.sleep(min(remaining, 0.2))
|
||||
|
||||
|
||||
def fetch_json(url: str, timeout_sec: float) -> str:
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
method="GET",
|
||||
)
|
||||
# This logger always polls the local backend. Ignore HTTP_PROXY/HTTPS_PROXY
|
||||
# so a developer proxy cannot turn a 127.0.0.1 request into a 502.
|
||||
with LOCAL_HTTP_OPENER.open(request, timeout=timeout_sec) as response:
|
||||
charset = response.headers.get_content_charset("utf-8")
|
||||
payload = response.read().decode(charset)
|
||||
parsed = json.loads(payload)
|
||||
return json.dumps(parsed, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
interval_sec = max(args.interval_ms, 200) / 1000.0
|
||||
output_path = Path(args.output)
|
||||
last_error_log_monotonic = 0.0
|
||||
|
||||
signal.signal(signal.SIGINT, handle_signal)
|
||||
signal.signal(signal.SIGTERM, handle_signal)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with output_path.open("a", encoding="utf-8") as output_file:
|
||||
while not STOP_REQUESTED:
|
||||
started = time.monotonic()
|
||||
try:
|
||||
line = fetch_json(args.url, args.request_timeout_sec)
|
||||
except (TimeoutError, urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError) as error:
|
||||
now = time.monotonic()
|
||||
if now - last_error_log_monotonic >= 10.0:
|
||||
print(f"[network-summary] poll failed: {error}", file=sys.stderr)
|
||||
last_error_log_monotonic = now
|
||||
else:
|
||||
output_file.write(line)
|
||||
output_file.write("\n")
|
||||
output_file.flush()
|
||||
|
||||
elapsed = time.monotonic() - started
|
||||
sleep_with_stop(max(0.0, interval_sec - elapsed))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
|
||||
camera_device="${OMNI_CAMERA_DEVICE}"
|
||||
occupancy_policy="${OMNI_CAMERA_OCCUPANCY_POLICY:-check}"
|
||||
release_service="${OMNI_CAMERA_RELEASE_SERVICE:-}"
|
||||
|
||||
die() {
|
||||
echo "[camera-preflight] $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
camera_pids() {
|
||||
fuser "${camera_device}" 2>/dev/null \
|
||||
| tr ' ' '\n' \
|
||||
| grep -E '^[0-9]+$' \
|
||||
| sort -nu \
|
||||
|| true
|
||||
}
|
||||
|
||||
pid_service() {
|
||||
local pid="$1"
|
||||
local service
|
||||
|
||||
service="$(sed -nE 's#.*[/:]([^/:]+\.service)$#\1#p' "/proc/${pid}/cgroup" 2>/dev/null | head -1)"
|
||||
printf '%s' "${service:-unknown}"
|
||||
}
|
||||
|
||||
report_owners() {
|
||||
local pid
|
||||
local comm
|
||||
local service
|
||||
local found=0
|
||||
|
||||
while read -r pid; do
|
||||
[[ -n "${pid}" ]] || continue
|
||||
found=1
|
||||
comm="$(cat "/proc/${pid}/comm" 2>/dev/null || printf 'unknown')"
|
||||
service="$(pid_service "${pid}")"
|
||||
echo "[camera-preflight] owner pid=${pid} command=${comm} service=${service}" >&2
|
||||
done < <(camera_pids)
|
||||
|
||||
if [[ "${found}" == "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
has_proc_manager_owner() {
|
||||
local pid
|
||||
|
||||
while read -r pid; do
|
||||
[[ -n "${pid}" ]] || continue
|
||||
if [[ "$(pid_service "${pid}")" == "proc_manager.service" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done < <(camera_pids)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ ! -e "${camera_device}" ]]; then
|
||||
die "camera device does not exist: ${camera_device}"
|
||||
fi
|
||||
|
||||
if ! command -v fuser >/dev/null 2>&1; then
|
||||
die "missing required command: fuser (install the psmisc package)"
|
||||
fi
|
||||
|
||||
resolved_device="$(readlink -f "${camera_device}" 2>/dev/null || printf '%s' "${camera_device}")"
|
||||
echo "[camera-preflight] checking ${camera_device} (${resolved_device}) policy=${occupancy_policy}" >&2
|
||||
|
||||
if ! report_owners; then
|
||||
echo "[camera-preflight] ${camera_device} is free" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "${occupancy_policy}" in
|
||||
check)
|
||||
die "${camera_device} is busy; no process was stopped"
|
||||
;;
|
||||
release-known)
|
||||
if [[ -z "${release_service}" ]]; then
|
||||
die "OMNI_CAMERA_RELEASE_SERVICE is required when policy=release-known"
|
||||
fi
|
||||
|
||||
echo "[camera-preflight] stopping known camera service ${release_service}" >&2
|
||||
systemctl stop "${release_service}"
|
||||
|
||||
if ! report_owners; then
|
||||
echo "[camera-preflight] ${camera_device} was released by ${release_service}" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if has_proc_manager_owner; then
|
||||
die "${camera_device} is still owned by proc_manager.service; refusing to stop the whole process manager. Unload the owning ROS component explicitly."
|
||||
fi
|
||||
|
||||
die "${camera_device} remains busy after stopping ${release_service}"
|
||||
;;
|
||||
*)
|
||||
die "unsupported OMNI_CAMERA_OCCUPANCY_POLICY=${occupancy_policy}; expected check or release-known"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export BLITZ_SKIP_DEV_RUN_CONTEXT_INIT="1"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
|
||||
blitz_dev_reset_run_context
|
||||
printf 'run_id=%s\nrun_dir=%s\n' "${BLITZ_RUN_ID}" "${BLITZ_RUN_DIR}"
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
camera_serial="${1:-}"
|
||||
camera_label="${2:-camera}"
|
||||
capture_width="${OMNI_CAMERA_DISCOVERY_WIDTH:-1280}"
|
||||
capture_height="${OMNI_CAMERA_DISCOVERY_HEIGHT:-720}"
|
||||
timeout_sec="${OMNI_CAMERA_DISCOVERY_TIMEOUT_SEC:-10}"
|
||||
|
||||
die() {
|
||||
echo "[camera-discovery] ${camera_label}: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -n "${camera_serial}" ]] || die "camera serial is required"
|
||||
[[ "${timeout_sec}" =~ ^[0-9]+$ ]] || die "OMNI_CAMERA_DISCOVERY_TIMEOUT_SEC must be a non-negative integer"
|
||||
|
||||
command -v udevadm >/dev/null 2>&1 || die "missing required command: udevadm"
|
||||
command -v v4l2-ctl >/dev/null 2>&1 || die "missing required command: v4l2-ctl"
|
||||
|
||||
shopt -s nullglob
|
||||
deadline=$((SECONDS + timeout_sec))
|
||||
|
||||
while true; do
|
||||
matches=()
|
||||
serial_nodes=()
|
||||
|
||||
for device in /dev/video*; do
|
||||
[[ -c "${device}" ]] || continue
|
||||
device_serial="$(
|
||||
udevadm info --query=property --name="${device}" 2>/dev/null \
|
||||
| sed -n 's/^ID_SERIAL_SHORT=//p' \
|
||||
| head -1
|
||||
)"
|
||||
[[ "${device_serial}" == "${camera_serial}" ]] || continue
|
||||
serial_nodes+=("${device}")
|
||||
|
||||
formats="$(v4l2-ctl -d "${device}" --list-formats-ext 2>/dev/null || true)"
|
||||
grep -q "'MJPG'" <<<"${formats}" || continue
|
||||
grep -q "Size: Discrete ${capture_width}x${capture_height}" <<<"${formats}" || continue
|
||||
matches+=("${device}")
|
||||
done
|
||||
|
||||
if (( ${#matches[@]} == 1 )); then
|
||||
echo "[camera-discovery] ${camera_label}: serial=${camera_serial} -> ${matches[0]} (MJPG ${capture_width}x${capture_height})" >&2
|
||||
printf '%s\n' "${matches[0]}"
|
||||
exit 0
|
||||
fi
|
||||
if (( ${#matches[@]} > 1 )); then
|
||||
die "serial=${camera_serial} matched multiple MJPG nodes: ${matches[*]}"
|
||||
fi
|
||||
if (( SECONDS >= deadline )); then
|
||||
if (( ${#serial_nodes[@]} == 0 )); then
|
||||
die "serial=${camera_serial} was not found under /dev/video*"
|
||||
fi
|
||||
die "serial=${camera_serial} has no MJPG ${capture_width}x${capture_height} node; serial nodes: ${serial_nodes[*]}"
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
87
robot/v4l2/OmniSocketGo_robot/scripts/dev/robot-remote.env
Normal file
87
robot/v4l2/OmniSocketGo_robot/scripts/dev/robot-remote.env
Normal file
@@ -0,0 +1,87 @@
|
||||
# Optional absolute path override for the companion repo.
|
||||
# By default the scripts assume:
|
||||
# OmniSocketGo -> current repo
|
||||
# robot-command-center -> sibling directory next to OmniSocketGo
|
||||
# Example:
|
||||
# ROBOT_COMMAND_CENTER_ROOT="$HOME/Documents/robot-command-center"
|
||||
|
||||
CONTROL_SIDE_OMNISOCKET_SERVER_ADDR="192.168.41.144:10909" # Local LAN hub
|
||||
CONTROL_SIDE_OMNISOCKET_RELAY_VIA="" # No relay
|
||||
|
||||
ROBOT_SIDE_OMNISOCKET_SERVER_ADDR="192.168.41.144:10909" # Local LAN hub
|
||||
ROBOT_SIDE_OMNISOCKET_RELAY_VIA="" # Direct LAN
|
||||
# Log one normal relay packet out of every N packets. Drop events still log immediately.
|
||||
OMNI_RELAY_PACKET_LOG_SAMPLE_EVERY="200"
|
||||
|
||||
CONTROL_WS_ALLOWED_ORIGINS="http://127.0.0.1:5173,http://localhost:5173"
|
||||
VITE_API_BASE_URL="http://127.0.0.1:8001"
|
||||
|
||||
PYTHON3_BIN="python3"
|
||||
PYTHON_VENV_PATH="${OMNISOCKETGO_ROOT}/.venv"
|
||||
|
||||
BACKEND_HOST="0.0.0.0"
|
||||
BACKEND_PORT="8001"
|
||||
OMNISOCKET_TELEMETRY_PEER_ID="peer-a-telemetry"
|
||||
OMNISOCKET_TELEMETRY_INTERVAL_MS="1000"
|
||||
OMNISOCKET_TELEMETRY_STALE_AFTER_MS="3000"
|
||||
OMNI_NETWORK_SUMMARY_LOG_ENABLED="1"
|
||||
OMNI_NETWORK_SUMMARY_LOG_PATH="${OMNISOCKETGO_ROOT}/logs/a-network-summary.jsonl"
|
||||
OMNI_NETWORK_SUMMARY_LOG_INTERVAL_MS="1000"
|
||||
OMNI_NETWORK_SUMMARY_LOG_REQUEST_TIMEOUT_SEC="3"
|
||||
|
||||
FRONTEND_HOST="0.0.0.0"
|
||||
FRONTEND_PORT="5173"
|
||||
|
||||
ROS_DISTRO="jazzy"
|
||||
ROBOT_RECEIVER_TRANSPORT="unix_dgram"
|
||||
ROBOT_RECEIVER_SERVER_ADDR="${ROBOT_SIDE_OMNISOCKET_SERVER_ADDR}"
|
||||
ROBOT_RECEIVER_RELAY_VIA="${ROBOT_SIDE_OMNISOCKET_RELAY_VIA}"
|
||||
ROBOT_RECEIVER_PEER_ID="ros-bridge-ctrl"
|
||||
ROBOT_RECEIVER_EXPECTED_SENDER=""
|
||||
ROBOT_RECEIVER_LOCAL_SOCKET_PATH="/tmp/omnisocket-b-side-cmd.sock"
|
||||
ROBOT_RECEIVER_OUTPUT_TOPIC="/hric/robot/cmd_vel"
|
||||
ROBOT_RECEIVER_FRAME_ID="pelvis"
|
||||
ROBOT_RECEIVER_WATCHDOG_TIMEOUT="0.5"
|
||||
ROBOT_RECEIVER_PUBLISH_RATE_HZ="100.0"
|
||||
|
||||
OMNI_VIDEO_PEER_ID="peer-b-video"
|
||||
OMNI_VIDEO_TARGET_PEER="peer-a-video"
|
||||
OMNI_GPSD_HOST="127.0.0.1"
|
||||
OMNI_CAMERA_HEAD_DEVICE="/dev/video26"
|
||||
OMNI_CAMERA_WAIST_DEVICE="/dev/video18"
|
||||
OMNI_CAMERA_AUTO_DISCOVER="1"
|
||||
OMNI_CAMERA_HEAD_SERIAL="CP9E163000H3"
|
||||
OMNI_CAMERA_WAIST_SERIAL="CPCK8530005N"
|
||||
OMNI_CAMERA_DISCOVERY_WIDTH="1280"
|
||||
OMNI_CAMERA_DISCOVERY_HEIGHT="720"
|
||||
OMNI_CAMERA_DISCOVERY_TIMEOUT_SEC="10"
|
||||
OMNI_CAMERA_HEAD_RELEASE_SERVICE="orbbec_head.service"
|
||||
OMNI_CAMERA_WAIST_RELEASE_SERVICE="orbbec_waist.service"
|
||||
OMNI_CAMERA_ACTIVE="head"
|
||||
OMNI_CAMERA_OCCUPANCY_POLICY="release-known"
|
||||
OMNI_CAMERA_PROFILE="day"
|
||||
OMNI_CAMERA_BRIGHTNESS=""
|
||||
OMNI_CAMERA_CUSTOM_CTRL=""
|
||||
OMNI_CAMERA_VERIFY="0"
|
||||
OMNI_VIDEO_SERVER_ADDR="${ROBOT_SIDE_OMNISOCKET_SERVER_ADDR}"
|
||||
OMNI_VIDEO_RELAY_VIA="${ROBOT_SIDE_OMNISOCKET_RELAY_VIA}"
|
||||
OMNI_VIDEO_SOFT_BACKPRESSURE_SEGMENTS="256"
|
||||
OMNI_VIDEO_HARD_BACKPRESSURE_SEGMENTS="1024"
|
||||
OMNI_VIDEO_HARD_BACKPRESSURE_HOLD_MS="5000"
|
||||
OMNI_VIDEO_FRAME_STALL_RECONNECT_MS="30000"
|
||||
OMNI_CONTROL_PEER_ID="peer-b-ctrl"
|
||||
OMNI_CONTROL_EXPECTED_SENDER="peer-a-ctrl"
|
||||
OMNI_CONTROL_SERVER_ADDR="${ROBOT_SIDE_OMNISOCKET_SERVER_ADDR}"
|
||||
OMNI_CONTROL_RELAY_VIA="${ROBOT_SIDE_OMNISOCKET_RELAY_VIA}"
|
||||
OMNI_CONTROL_UNIX_SOCKET_PATH="${ROBOT_RECEIVER_LOCAL_SOCKET_PATH}"
|
||||
OMNI_CONTROL_ACK_PEER_ID="peer-b-ctrl-ack"
|
||||
OMNI_CONTROL_ACK_TARGET_PEER="peer-a-ctrl-ack"
|
||||
BLITZ_CONTROL_ACK_SAMPLE_MOD="10"
|
||||
BLITZ_VIDEO_STAGE_LOG_ENABLED="1"
|
||||
BLITZ_VIDEO_STAGE_LOG_SAMPLE_MOD="10"
|
||||
OMNI_CONTROL_SERVER_IDLE_RECONNECT_MS="30000"
|
||||
|
||||
# A-side backend video freshness guard. Used by scripts/dev/start-backend.sh.
|
||||
OMNI_VIDEO_MAX_FRAME_AGE_MS="1000"
|
||||
|
||||
B_SIDE_OMNID_USE_SUDO="1"
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
|
||||
blitz_dev_prepare_5g_logging_env
|
||||
exec bash "${OMNISOCKETGO_ROOT}/scripts/boot/blitz-5g-link-logger.sh"
|
||||
100
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-b-side-omnid.sh
Normal file
100
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-b-side-omnid.sh
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
blitz_dev_prepare_bside_logging_env
|
||||
|
||||
cd "${OMNISOCKETGO_ROOT}"
|
||||
|
||||
export OMNISOCKET_SERVER_ADDR="${ROBOT_SIDE_OMNISOCKET_SERVER_ADDR}"
|
||||
export OMNISOCKET_RELAY_VIA="${ROBOT_SIDE_OMNISOCKET_RELAY_VIA}"
|
||||
export OMNI_VIDEO_SERVER_ADDR="${OMNI_VIDEO_SERVER_ADDR}"
|
||||
export OMNI_VIDEO_RELAY_VIA="${OMNI_VIDEO_RELAY_VIA}"
|
||||
export OMNI_CONTROL_SERVER_ADDR="${OMNI_CONTROL_SERVER_ADDR}"
|
||||
export OMNI_CONTROL_RELAY_VIA="${OMNI_CONTROL_RELAY_VIA}"
|
||||
|
||||
logger_pid=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${logger_pid}" ]]; then
|
||||
kill "${logger_pid}" 2>/dev/null || true
|
||||
wait "${logger_pid}" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
start_5g_link_logger_if_needed() {
|
||||
if [[ "${OMNI_5G_LINK_LOG_ENABLED:-1}" != "1" ]]; then
|
||||
echo "[start-b-side-omnid] 5G link logger disabled" >&2
|
||||
return 0
|
||||
fi
|
||||
if [[ "${OMNI_BOOT_MODE:-0}" == "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
bash "${SCRIPT_DIR}/start-5g-link-logger.sh" &
|
||||
logger_pid=$!
|
||||
echo "[start-b-side-omnid] 5G link logger -> ${BLITZ_5G_LINK_LOG_PATH:-unset}" >&2
|
||||
}
|
||||
|
||||
release_known_camera_service() {
|
||||
local service="$1"
|
||||
|
||||
if [[ "${OMNI_CAMERA_OCCUPANCY_POLICY:-check}" != "release-known" || -z "${service}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if systemctl is-active --quiet "${service}"; then
|
||||
echo "[start-b-side-omnid] stopping known camera service ${service} before discovery" >&2
|
||||
systemctl stop "${service}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_camera_devices() {
|
||||
if [[ "${OMNI_CAMERA_AUTO_DISCOVER:-0}" != "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
OMNI_CAMERA_HEAD_DEVICE="$(
|
||||
bash "${SCRIPT_DIR}/resolve-camera-device.sh" "${OMNI_CAMERA_HEAD_SERIAL}" head
|
||||
)"
|
||||
OMNI_CAMERA_WAIST_DEVICE="$(
|
||||
bash "${SCRIPT_DIR}/resolve-camera-device.sh" "${OMNI_CAMERA_WAIST_SERIAL}" waist
|
||||
)"
|
||||
if [[ "${OMNI_CAMERA_HEAD_DEVICE}" == "${OMNI_CAMERA_WAIST_DEVICE}" ]]; then
|
||||
echo "[start-b-side-omnid] head and waist resolved to the same device: ${OMNI_CAMERA_HEAD_DEVICE}" >&2
|
||||
return 1
|
||||
fi
|
||||
export OMNI_CAMERA_HEAD_DEVICE OMNI_CAMERA_WAIST_DEVICE
|
||||
echo "[start-b-side-omnid] resolved head=${OMNI_CAMERA_HEAD_DEVICE} waist=${OMNI_CAMERA_WAIST_DEVICE}" >&2
|
||||
}
|
||||
|
||||
if [[ ! -x "./bin/b_side_omnid" ]]; then
|
||||
if [[ "${OMNI_BOOT_MODE:-0}" == "1" ]]; then
|
||||
echo "Missing ./bin/b_side_omnid in boot mode; build it before enabling the autostart service." >&2
|
||||
exit 1
|
||||
fi
|
||||
make b_side_omnid
|
||||
fi
|
||||
|
||||
launch_b_side_omnid() {
|
||||
trap cleanup EXIT INT TERM
|
||||
start_5g_link_logger_if_needed
|
||||
release_known_camera_service "${OMNI_CAMERA_HEAD_RELEASE_SERVICE:-orbbec_head.service}"
|
||||
release_known_camera_service "${OMNI_CAMERA_WAIST_RELEASE_SERVICE:-orbbec_waist.service}"
|
||||
resolve_camera_devices
|
||||
OMNI_CAMERA_DEVICE="${OMNI_CAMERA_HEAD_DEVICE}" \
|
||||
OMNI_CAMERA_RELEASE_SERVICE="${OMNI_CAMERA_HEAD_RELEASE_SERVICE:-orbbec_head.service}" \
|
||||
bash "${SCRIPT_DIR}/prepare-camera-device.sh"
|
||||
OMNI_CAMERA_DEVICE="${OMNI_CAMERA_WAIST_DEVICE}" \
|
||||
OMNI_CAMERA_RELEASE_SERVICE="${OMNI_CAMERA_WAIST_RELEASE_SERVICE:-orbbec_waist.service}" \
|
||||
bash "${SCRIPT_DIR}/prepare-camera-device.sh"
|
||||
OMNI_CAMERA_DEVICE="${OMNI_CAMERA_HEAD_DEVICE}" bash "${SCRIPT_DIR}/apply-camera-controls.sh"
|
||||
OMNI_CAMERA_DEVICE="${OMNI_CAMERA_WAIST_DEVICE}" bash "${SCRIPT_DIR}/apply-camera-controls.sh"
|
||||
./bin/b_side_omnid
|
||||
}
|
||||
|
||||
if [[ "${B_SIDE_OMNID_USE_SUDO}" == "1" && "${EUID}" -ne 0 ]]; then
|
||||
exec sudo -E bash -lc 'cd "$1" && export B_SIDE_OMNID_USE_SUDO=0 && exec bash "$2"' _ "${OMNISOCKETGO_ROOT}" "${SCRIPT_DIR}/start-b-side-omnid.sh"
|
||||
fi
|
||||
|
||||
launch_b_side_omnid
|
||||
60
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-backend.sh
Normal file
60
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-backend.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
require_robot_command_center_root
|
||||
blitz_dev_prepare_backend_logging_env
|
||||
|
||||
if [[ ! -x "${PYTHON_VENV_PATH}/bin/python" ]]; then
|
||||
echo "[start-backend] creating or repairing virtualenv at ${PYTHON_VENV_PATH}" >&2
|
||||
"${PYTHON3_BIN}" -m venv "${PYTHON_VENV_PATH}"
|
||||
fi
|
||||
|
||||
if [[ ! -x "${PYTHON_VENV_PATH}/bin/python" ]]; then
|
||||
echo "[start-backend] virtualenv is incomplete: missing ${PYTHON_VENV_PATH}/bin/python" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "${PYTHON_VENV_PATH}/bin/activate"
|
||||
|
||||
cd "${BACKEND_DIR}"
|
||||
export OMNISOCKET_SERVER_ADDR="${CONTROL_SIDE_OMNISOCKET_SERVER_ADDR}"
|
||||
export OMNISOCKET_RELAY_VIA="${CONTROL_SIDE_OMNISOCKET_RELAY_VIA}"
|
||||
|
||||
logger_pid=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${logger_pid}" ]]; then
|
||||
kill "${logger_pid}" 2>/dev/null || true
|
||||
wait "${logger_pid}" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
start_network_summary_logger() {
|
||||
local logger_url
|
||||
local logger_dir
|
||||
|
||||
if [[ "${OMNI_NETWORK_SUMMARY_LOG_ENABLED}" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
logger_url="http://127.0.0.1:${BACKEND_PORT}/api/network/latest/"
|
||||
logger_dir="$(dirname "${OMNI_NETWORK_SUMMARY_LOG_PATH}")"
|
||||
mkdir -p "${logger_dir}"
|
||||
|
||||
python "${SCRIPT_DIR}/log-network-summary.py" \
|
||||
--url "${logger_url}" \
|
||||
--output "${OMNI_NETWORK_SUMMARY_LOG_PATH}" \
|
||||
--interval-ms "${OMNI_NETWORK_SUMMARY_LOG_INTERVAL_MS}" \
|
||||
--request-timeout-sec "${OMNI_NETWORK_SUMMARY_LOG_REQUEST_TIMEOUT_SEC}" &
|
||||
logger_pid=$!
|
||||
echo "[start-backend] network summary logger -> ${OMNI_NETWORK_SUMMARY_LOG_PATH} (${OMNI_NETWORK_SUMMARY_LOG_INTERVAL_MS} ms)" >&2
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
start_network_summary_logger
|
||||
python -m uvicorn config.asgi:application --host "${BACKEND_HOST}" --port "${BACKEND_PORT}"
|
||||
21
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-dev-tmux.sh
Normal file
21
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-dev-tmux.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SESSION_NAME="${1:-robot-remote}"
|
||||
|
||||
if ! command -v tmux >/dev/null 2>&1; then
|
||||
echo "tmux is required for this launcher" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
|
||||
exec tmux attach -t "${SESSION_NAME}"
|
||||
fi
|
||||
|
||||
tmux new-session -d -s "${SESSION_NAME}" -n backend "bash -lc '${SCRIPT_DIR}/start-backend.sh'"
|
||||
tmux new-window -t "${SESSION_NAME}:" -n frontend "bash -lc '${SCRIPT_DIR}/start-frontend.sh'"
|
||||
tmux new-window -t "${SESSION_NAME}:" -n ros "bash -lc '${SCRIPT_DIR}/start-ros-receiver.sh'"
|
||||
tmux new-window -t "${SESSION_NAME}:" -n b-side "bash -lc '${SCRIPT_DIR}/start-b-side-omnid.sh'"
|
||||
|
||||
exec tmux attach -t "${SESSION_NAME}"
|
||||
10
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-frontend.sh
Normal file
10
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-frontend.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
require_robot_command_center_root
|
||||
|
||||
cd "${FRONTEND_DIR}"
|
||||
exec npm run dev -- --host "${FRONTEND_HOST}" --port "${FRONTEND_PORT}"
|
||||
22
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-local-hub.sh
Normal file
22
robot/v4l2/OmniSocketGo_robot/scripts/dev/start-local-hub.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
|
||||
hub_binary="${OMNISOCKETGO_ROOT}/bin/kcpserver"
|
||||
hub_listen_addr="${LOCAL_HUB_LISTEN_ADDR:-0.0.0.0:10909}"
|
||||
telemetry_peer_id="${LOCAL_HUB_TELEMETRY_PEER_ID:-peer-a-telemetry}"
|
||||
telemetry_interval="${LOCAL_HUB_TELEMETRY_INTERVAL:-1000ms}"
|
||||
|
||||
if [[ ! -x "${hub_binary}" ]]; then
|
||||
echo "[start-local-hub] missing executable ${hub_binary}; run: make bin/kcpserver" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[start-local-hub] listen=${hub_listen_addr} relay=disabled telemetry_peer=${telemetry_peer_id}" >&2
|
||||
exec "${hub_binary}" \
|
||||
-listen "${hub_listen_addr}" \
|
||||
-telemetry-peer "${telemetry_peer_id}" \
|
||||
-telemetry-interval "${telemetry_interval}"
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source_with_nounset_off() {
|
||||
set +u
|
||||
# shellcheck disable=SC1090
|
||||
source "$1"
|
||||
set -u
|
||||
}
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/load-env.sh"
|
||||
if [[ ! -f "/opt/ros/${ROS_DISTRO}/setup.bash" ]]; then
|
||||
echo "Missing ROS distro setup: /opt/ros/${ROS_DISTRO}/setup.bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
source_with_nounset_off "/opt/ros/${ROS_DISTRO}/setup.bash"
|
||||
|
||||
cd "${ROS_CONTROL_PY_DIR}"
|
||||
if [[ ! -f "install/setup.bash" ]]; then
|
||||
echo "Missing ROS workspace setup: ${ROS_CONTROL_PY_DIR}/install/setup.bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
source_with_nounset_off "install/setup.bash"
|
||||
|
||||
launch_args=(
|
||||
"transport:=${ROBOT_RECEIVER_TRANSPORT}"
|
||||
"peer_id:=${ROBOT_RECEIVER_PEER_ID}"
|
||||
"local_socket_path:=${ROBOT_RECEIVER_LOCAL_SOCKET_PATH}"
|
||||
"output_topic:=${ROBOT_RECEIVER_OUTPUT_TOPIC}"
|
||||
"frame_id:=${ROBOT_RECEIVER_FRAME_ID}"
|
||||
"watchdog_timeout:=${ROBOT_RECEIVER_WATCHDOG_TIMEOUT}"
|
||||
"publish_rate_hz:=${ROBOT_RECEIVER_PUBLISH_RATE_HZ}"
|
||||
)
|
||||
|
||||
if [[ -n "${ROBOT_RECEIVER_SERVER_ADDR}" ]]; then
|
||||
launch_args+=("server_addr:=${ROBOT_RECEIVER_SERVER_ADDR}")
|
||||
fi
|
||||
|
||||
if [[ -n "${ROBOT_RECEIVER_RELAY_VIA}" ]]; then
|
||||
launch_args+=("relay_via:=${ROBOT_RECEIVER_RELAY_VIA}")
|
||||
fi
|
||||
|
||||
if [[ -n "${ROBOT_RECEIVER_EXPECTED_SENDER}" ]]; then
|
||||
launch_args+=("expected_sender:=${ROBOT_RECEIVER_EXPECTED_SENDER}")
|
||||
fi
|
||||
|
||||
exec ros2 launch udp_teleop_bridge robot_udp_receiver.launch.py "${launch_args[@]}"
|
||||
Reference in New Issue
Block a user