Organize host and robot streaming releases

This commit is contained in:
Mike Mi
2026-08-09 15:56:20 +08:00
commit decff19daf
532 changed files with 127054 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
# 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/setup-control-side.sh # first run on the control computer
bash scripts/dev/start-local-hub.sh # direct-LAN local KCP hub
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"
```

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
Django>=5.2,<6.0
djangorestframework>=3.17,<4
django-cors-headers>=4,<5
channels>=4,<5
uvicorn>=0.52,<1
PyYAML>=6,<7
setuptools>=80

View 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

View 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())

View File

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

View File

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

View File

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

View 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="81.70.156.140:10909" # D
CONTROL_SIDE_OMNISOCKET_RELAY_VIA="106.55.173.235:10909" # C
ROBOT_SIDE_OMNISOCKET_SERVER_ADDR="81.70.156.140:10909" # D
ROBOT_SIDE_OMNISOCKET_RELAY_VIA="81.70.156.140:10909" # 直连 D
# 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"

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/load-env.sh"
if [[ ! -x "${PYTHON_VENV_PATH}/bin/python" ]]; then
"${PYTHON3_BIN}" -m venv "${PYTHON_VENV_PATH}"
fi
venv_python="${PYTHON_VENV_PATH}/bin/python"
"${venv_python}" -m pip install -r "${SCRIPT_DIR}/control-side-requirements.txt"
make -C "${OMNISOCKETGO_ROOT}" python-ext PYTHON="${venv_python}"
"${venv_python}" -m pip install --no-build-isolation -e "${OMNISOCKETGO_ROOT}/python"
echo "[setup-control-side] ready: ${PYTHON_VENV_PATH}" >&2

View File

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

View 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

View 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}"

View 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}"

View 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}"

View 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}"

View File

@@ -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[@]}"