Skip to content

fix: reset colorFrameThread_ on stream disable so re-enable can recreate it#95

Open
mk37972 wants to merge 1 commit into
orbbec:v2-mainfrom
mk37972:feat/mincheol/color-thread-lifecycle-fix
Open

fix: reset colorFrameThread_ on stream disable so re-enable can recreate it#95
mk37972 wants to merge 1 commit into
orbbec:v2-mainfrom
mk37972:feat/mincheol/color-thread-lifecycle-fix

Conversation

@mk37972
Copy link
Copy Markdown

@mk37972 mk37972 commented May 23, 2026

Summary

colorFrameThread_, leftColorFrameThread_, and rightColorFrameThread_ are std::shared_ptr<std::thread> members. They are created in startStreams() (and startStream()) under the guard:

if (!colorFrameThread_ && enable_stream_[COLOR]) {
  colorFrameThread_ = std::make_shared<std::thread>([this]() { onNewColorFrameCallback(); });
}

Their worker functions loop on enable_stream_[COLOR] / COLOR_LEFT / COLOR_RIGHT and exit when the flag goes false. However, the shared_ptr is never reset after the thread exits, so on the next startStreams() the !colorFrameThread_ guard is permanently false (pointer is non-null, just holds a finished thread) and the worker is never recreated. After one /{camera}/toggle_color false -> true cycle (or similarly for left/right color), color frame processing is permanently stopped for that camera until the node is restarted.

The same lifecycle exists in v2.7.6 through v2.8.7 unchanged. The structural bug is present in OrbbecSDK_ROS2's color frame thread as well.

Reproduction

# Color processing is healthy at startup:
rostopic hz /camera/color/image_raw   # 30 Hz (or configured rate)

# Disable color:
rosservice call /camera/toggle_color "data: false"
rostopic hz /camera/color/image_raw   # 0 Hz (expected, stream is off)

# Re-enable color:
rosservice call /camera/toggle_color "data: true"
rostopic hz /camera/color/image_raw   # 0 Hz, never recovers (BUG)

The pipeline reports as restarted and enable_stream_[COLOR] == true, but onNewFrameSetCallback() pushes color frames into colorFrameQueue_ that no worker is draining, because the original worker exited and a replacement was never created.

Fix

Two-part patch:

1. src/ob_camera_node.cpp: let the worker wake on stream disable

The existing wait predicate watches only the queue and is_running_:

colorFrameCV_.wait(lock,
                   [this]() { return !colorFrameQueue_.empty() || !(is_running_.load()); });

A notify_all() call alone cannot wake the worker into exiting during a toggle: the predicate evaluates false (queue empty, is_running_ still true) and the worker goes straight back to wait. Add the stream's enable_stream_[stream_index] flag to the predicate so the worker can observe the disable and break out of the loop. Same change applied to onNewLeftColorFrameCallback and onNewRightColorFrameCallback.

2. src/ros_service.cpp: toggleSensor joins + resets the shared_ptr on disable

After enable_stream_[stream_index] = enabled; and before startStreams();, when disabling a color stream: notify_all() the matching CV, join() the worker, then reset() the shared_ptr. The subsequent startStreams() guard then evaluates true and recreates the worker normally on re-enable.

Total: 2 files, +44 / -9 lines. No public API or behavioral change for callers that never disable streams.

Shutdown path (unchanged)

The clean() / destructor path uses is_running_.store(false) to terminate the workers, which the existing predicate disjunct already handles. The patch adds a new disjunct alongside it, not replacing it, so shutdown ordering remains identical.

Notes

  • A subtle correctness concern: the new predicate reads enable_stream_[stream_index] while holding colorFrameMtx_, but the write happens under device_lock_ in toggleSensor. A data race on a non-atomic bool, benign on x86 (byte read/write atomic). For full correctness, enable_stream_ could be changed to std::array<std::atomic_bool, ...>. That is a separate type change worth doing but not strictly necessary for this fix.
  • The same fix pattern would apply to OrbbecSDK_ROS2 if you'd like a follow-up PR there.

Happy to iterate on style or split into smaller commits if useful.

…ate it

colorFrameThread_, leftColorFrameThread_, and rightColorFrameThread_ are
std::shared_ptr<std::thread> members guarded at creation time by:

  if (!colorFrameThread_ && enable_stream_[COLOR]) {
    colorFrameThread_ = std::make_shared<std::thread>(
        [this]() { onNewColorFrameCallback(); });
  }

Their worker functions loop on enable_stream_[COLOR] / COLOR_LEFT /
COLOR_RIGHT and exit when the flag goes false. However, the shared_ptr
is never reset after the thread exits, so on the next startStreams()
the "!colorFrameThread_" guard is permanently false (pointer is
non-null, just holds a finished thread) and the thread is never
recreated. After one toggle_<color_stream> false -> true cycle, color
frame processing is permanently stopped.

This patch fixes the lifecycle in two parts:

1. Wait predicate in onNewColorFrameCallback (and Left / Right siblings)
   now wakes on enable_stream_[stream] going false, so a worker thread
   parked in colorFrameCV_.wait() can exit promptly when toggle disables
   its stream. Without this, notify_all() alone cannot free the thread
   because the existing predicate only watches the queue and
   is_running_; the worker would simply go back to wait.

2. toggleSensor(), after flipping enable_stream_[stream_index] to false,
   notify_all()s the matching CV, join()s the worker, and reset()s the
   shared_ptr. The next startStreams() guard then evaluates true and
   recreates the worker normally.

The shutdown path through clean() / dtor is unaffected: is_running_
goes false there, both the existing and new predicate disjuncts wake
the threads, and clean() joins them as before.

The same structural bug exists in OrbbecSDK_ROS2's color frame thread.
The same shape of fix would apply there.
@mk37972 mk37972 force-pushed the feat/mincheol/color-thread-lifecycle-fix branch from 2aa0c42 to e20cbf8 Compare May 23, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant