diff --git a/scripts/dev/README.md b/scripts/dev/README.md index 1bdbd7d..97cb49e 100644 --- a/scripts/dev/README.md +++ b/scripts/dev/README.md @@ -24,10 +24,11 @@ If your `robot-command-center` is elsewhere, set `ROBOT_COMMAND_CENTER_ROOT` in - `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 +- `apply-camera-controls.sh`: applies the camera preset before `b_side_omnid` starts - `start-backend.sh`: starts Django ASGI with `uvicorn` - `start-frontend.sh`: starts the Vite dev server - `start-ros-receiver.sh`: starts the ROS2 `udp_teleop_bridge` receiver -- `start-b-side-omnid.sh`: starts `./bin/b_side_omnid` and uses `sudo -E` by default +- `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 @@ -69,6 +70,10 @@ Edit `scripts/dev/robot-remote.env` for shared changes such as: - `ROBOT_SIDE_OMNISOCKET_RELAY_VIA` - `VITE_API_BASE_URL` - `OMNI_CAMERA_DEVICE` +- `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` @@ -77,10 +82,13 @@ Edit `scripts/dev/robot-remote.env` for shared changes such as: - `OMNI_CONTROL_SERVER_IDLE_RECONNECT_MS` - `OMNI_VIDEO_MAX_FRAME_AGE_MS` +Camera presets use `v4l2-ctl` from `v4l-utils` on the robot side. + 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-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: @@ -96,3 +104,26 @@ ROBOT_COMMAND_CENTER_ROOT="$HOME/Documents/robot-command-center" OMNI_CAMERA_DEVICE="/dev/video30" B_SIDE_OMNID_USE_SUDO="0" ``` + +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" +``` diff --git a/scripts/dev/apply-camera-controls.sh b/scripts/dev/apply-camera-controls.sh new file mode 100644 index 0000000..6b32c81 --- /dev/null +++ b/scripts/dev/apply-camera-controls.sh @@ -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 diff --git a/scripts/dev/load-env.sh b/scripts/dev/load-env.sh index 9401236..097dca7 100644 --- a/scripts/dev/load-env.sh +++ b/scripts/dev/load-env.sh @@ -27,6 +27,33 @@ require_robot_command_center_root() { 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=( "${SCRIPT_DIR}/robot-remote.env" "${SCRIPT_DIR}/robot-remote.env.local" @@ -41,6 +68,22 @@ for env_file in "${ENV_FILES[@]}"; do fi done +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}" @@ -72,6 +115,11 @@ export ROBOT_RECEIVER_OUTPUT_TOPIC="${ROBOT_RECEIVER_OUTPUT_TOPIC:-/hric/robot/c 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_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_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:-}}" diff --git a/scripts/dev/robot-remote.env b/scripts/dev/robot-remote.env index b311ca6..bb8a295 100644 --- a/scripts/dev/robot-remote.env +++ b/scripts/dev/robot-remote.env @@ -38,6 +38,10 @@ ROBOT_RECEIVER_PUBLISH_RATE_HZ="100.0" OMNI_VIDEO_PEER_ID="peer-b-video" OMNI_VIDEO_TARGET_PEER="peer-a-video" OMNI_CAMERA_DEVICE="/dev/video26" +OMNI_CAMERA_PROFILE="night" +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="64" diff --git a/scripts/dev/start-b-side-omnid.sh b/scripts/dev/start-b-side-omnid.sh index e671a02..3c8001f 100755 --- a/scripts/dev/start-b-side-omnid.sh +++ b/scripts/dev/start-b-side-omnid.sh @@ -22,8 +22,13 @@ if [[ ! -x "./bin/b_side_omnid" ]]; then make b_side_omnid fi +launch_b_side_omnid() { + bash "${SCRIPT_DIR}/apply-camera-controls.sh" + exec ./bin/b_side_omnid +} + if [[ "${B_SIDE_OMNID_USE_SUDO}" == "1" && "${EUID}" -ne 0 ]]; then - exec sudo -E ./bin/b_side_omnid + exec sudo -E bash -lc 'cd "$1" && bash "$2" && exec "$3"' _ "${OMNISOCKETGO_ROOT}" "${SCRIPT_DIR}/apply-camera-controls.sh" "./bin/b_side_omnid" fi -exec ./bin/b_side_omnid +launch_b_side_omnid