Skip to content

Dive Detector module

File: backend/divedetector.py

The core detection and orchestration module. Each poll cycle it combines sensor data, operator toggles, and an OBS health probe to drive OBS into a definite state and publish observed health to Redis.

Class: DiveDetector

DiveDetector(
    bucket_name: str,
    redis_host: str = "localhost",
    redis_port: int = 6379,
)

Holds a Redis connection and exposes accessor methods for the sensor, toggle, and state keys belonging to one ship.

Constructor parameters

Parameter Type Default Description
bucket_name str Redis key prefix, e.g. "rcsn"
redis_host str "localhost" Redis server hostname
redis_port int 6379 Redis server port

Method: check_in_water

check_in_water(
    in_water_key: str = "rov:ctd:inwater",
    depth_key: str = "rov:mbari:depth",
    depth_threshold: float = 5.0,
) -> bool

Reads the inwater flag and depth from Redis. Returns True when both hold: inwater == 1 and depth > depth_threshold. Returns False (and logs a warning) if either key is missing or unparseable.

Method: auto_stream_enabled

auto_stream_enabled() -> bool

Reads {bucket}:config:auto_stream. Returns False if the key is missing or unparseable — the detector stays safely passive until an operator explicitly enables it from the dashboard.

Method: force_stream_enabled

force_stream_enabled() -> bool

Reads {bucket}:config:force_stream. Same default behaviour.

Method: set_streaming_state

set_streaming_state(streaming: bool) -> None

Writes {bucket}:state:streaming ("1" / "0") after a successful control cycle, so the dashboard can render the detector's view of OBS streaming state.

Method: set_obs_health

set_obs_health(ok: bool, last_error: str | None = None) -> None

Publishes the result of the OBS comms probe for the current poll:

  • {bucket}:state:obs_ok"1" / "0"
  • {bucket}:state:obs_last_error — empty string when healthy, otherwise the most recent error (truncated to 240 chars so it stays tooltip-sized on the dashboard).

Watched Redis keys

Redis key pattern Direction Description
{ship}:rov:ctd:inwater read "1" when the CTD sensor detects submersion
{ship}:rov:mbari:depth read Current ROV depth as a float string, in metres
{ship}:config:auto_stream read Operator-controlled auto toggle
{ship}:config:force_stream read Operator-controlled manual override
{ship}:state:streaming write Last commanded streaming state
{ship}:state:obs_ok write Whether the OBS comms probe succeeded
{ship}:state:obs_last_error write Empty when healthy; otherwise the last OBS error

Main loop

The script's __main__ block runs an infinite polling loop. Each cycle:

  1. dive_status = detector.check_in_water(...)
  2. auto = detector.auto_stream_enabled(), force = detector.force_stream_enabled()
  3. Decide should_stream and mode via the control table.
  4. Open an OBSController.
  5. Call obs.get_stream_status() — confirms comms AND returns OBS's real streaming state. The result drives set_obs_health().
  6. Apply scene + start/stop, but only call StartStream/StopStream if OBS's actual state differs from the desired state.
  7. detector.set_streaming_state(should_stream).
  8. obs.disconnect(), sleep.

Control table

force_stream auto_stream dive_status should_stream OBS scene
ON any any True MainOnly
OFF ON yes True MainOnly
OFF ON no False FeedOffline
OFF OFF any False FeedOffline

When both toggles are OFF the detector still actively drives OBS to the stopped state every poll — it does not leave OBS untouched.

CLI reference

usage: divedetector.py [options]

  --ship SHIP              Redis bucket name / ship identifier (e.g. rcsn)
  --redis-host HOST        Redis hostname (default: localhost)
  --redis-port PORT        Redis port (default: 6379)
  --depth-threshold DEPTH  Minimum depth in metres to count as diving (default: 5.0)
  --obs-host HOST          OBS WebSocket server hostname
  --poll-interval SECS     Polling frequency in seconds (default: 10)
  --debug 0|1              Enable verbose logging (default: 0)

In addition, the script reads OBS_PORT (default 4455) and OBS_PASSWORD (default empty) from the environment.

Example

OBS_PASSWORD="" \
uv run backend/divedetector.py \
  --ship rcsn \
  --redis-host redis \
  --obs-host rc-teleops.rc.mbari.org \
  --depth-threshold 5.0 \
  --poll-interval 10