fix camera recovery blocking the dora event loop

Camera::open() retried forever inside a fixed sleep, so a camera that did not
come back left the node stuck there: it stopped reading dora events and no
longer honoured Stop or AllInputsClosed.

Recovery is now a state machine over ticks. open() makes one attempt and
returns; the loop decides when to try again from a 1/2/5/10 s backoff deadline,
and nothing sleeps, so events keep being read while the camera is down. The
first attempt closes the handle; past that the device is wedged and the node
escalates to sl::Camera::reboot(), guarded to the ZED 2 and newer since it does
nothing on the ZED and ZED Mini. Reopening pins the serial so recovery gets the
same physical camera back.

Set async_grab_camera_recovery so a link hiccup returns CAMERA_REBOOTING
immediately instead of blocking the grab for ~17 s, and enable_image_validity_
check so the SDK flags frames a stalled camera hands back looking valid. Those
transient errors no longer drop the handle, since the SDK is already
reconnecting it.

An all-black frame now counts as a failed grab rather than being published, and
a camera delivering nothing usable for 5 s (30 s while the SDK reports it is
reconnecting) triggers recovery. A new camera_state output publishes
opening/streaming/recovering so a downstream node can tell "no frame yet" from
"camera lost".

ZED_RETRY_INTERVAL_SECONDS is replaced by the backoff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7g2cBJ5e3r15ZbJ9WZn8c
This commit is contained in:
cesar
2026-09-08 09:26:17 -03:00
co-authored by Claude Opus 5
parent 42c2b791fd
commit d8956f269d
6 changed files with 307 additions and 31 deletions
+116 -17
View File
@@ -1,12 +1,11 @@
#include "zed/camera.h"
#include "util/env.h"
#include <chrono>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <string>
#include <thread>
#include <utility>
namespace {
@@ -56,6 +55,24 @@ sl::FLIP_MODE parse_flip(const std::string& mode) {
return sl::FLIP_MODE::ON;
}
// A blank frame is checked on a strided sample rather than the full image, which
// is just as conclusive and costs nothing at frame rate.
constexpr int BLANK_FRAME_STRIDE = 16;
// sl::Camera::reboot() reaches the ZED 2 and newer. On the first-generation ZED
// and the ZED Mini it returns without touching the device, so calling it there
// only costs time and fills the log.
bool model_supports_reboot(sl::MODEL model) {
return model != sl::MODEL::ZED && model != sl::MODEL::ZED_M;
}
// The SDK reconnects a camera whose link hiccuped by itself. Dropping the handle
// over one of these takes the recovery away from the SDK and hands it to us,
// which is slower and, on a device the kernel is about to suspend, often fatal.
bool is_transient_grab_error(sl::ERROR_CODE err) {
return err == sl::ERROR_CODE::CAMERA_REBOOTING || err == sl::ERROR_CODE::CORRUPTED_FRAME;
}
void list_available_cameras() {
auto devices = sl::Camera::getDeviceList();
std::cout << "Detected ZED cameras: " << devices.size() << std::endl;
@@ -84,8 +101,6 @@ Config Config::from_env() {
util::get_env_int("ZED_TEXTURE_CONFIDENCE_THRESHOLD", c.texture_confidence_threshold);
c.flip = util::get_env_str("ZED_FLIP", c.flip);
c.warmup_frames = util::get_env_int("ZED_WARMUP_FRAMES", c.warmup_frames);
int retry_seconds = util::get_env_int("ZED_RETRY_INTERVAL_SECONDS", 5);
c.retry_interval_ms = retry_seconds * 1000;
return c;
}
@@ -102,6 +117,13 @@ Camera::Camera(const Config& config) : config_(config) {
init_params_.camera_image_flip = parse_flip(config_.flip);
init_params_.depth_minimum_distance = config_.depth_min_mm;
init_params_.depth_maximum_distance = config_.depth_max_mm;
// Recover a hiccuping link inside the SDK rather than out here. Without this
// a grab blocks for the roughly 17 s the SDK spends reconnecting, freezing the
// dora event loop; with it the grab returns CAMERA_REBOOTING straight away and
// the SDK reconnects in the background, holding the device open meanwhile.
init_params_.async_grab_camera_recovery = true;
// Let the SDK flag the frames a stalled camera hands back looking valid.
init_params_.enable_image_validity_check = 1;
runtime_params_.enable_depth = true;
runtime_params_.enable_fill_mode = config_.depth_fill;
@@ -116,28 +138,64 @@ Camera::Camera(const Config& config) : config_(config) {
<< ", confidence=" << config_.confidence_threshold
<< ", texture_confidence=" << config_.texture_confidence_threshold
<< ", fill=" << (config_.depth_fill ? "on" : "off") << std::endl;
list_available_cameras();
}
Camera::~Camera() {
close();
}
void Camera::open() {
list_available_cameras();
while (true) {
sl::ERROR_CODE err = zed_.open(init_params_);
if (err == sl::ERROR_CODE::SUCCESS) {
return;
}
bool Camera::open() {
sl::ERROR_CODE err = zed_.open(init_params_);
if (err != sl::ERROR_CODE::SUCCESS) {
std::cerr << "Failed to open ZED camera: " << sl::toString(err) << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(config_.retry_interval_ms));
return false;
}
recovering_ = false;
sl::CameraInformation info = zed_.getCameraInformation();
serial_number_ = info.serial_number;
model_ = info.camera_model;
// After the first open, reopen the same physical camera. Recovery runs while
// the device may be mid-re-enumeration, and picking whichever ZED answers
// first is not the same thing as picking ours.
init_params_.input.setFromSerialNumber(serial_number_);
std::cout << "ZED camera opened (" << sl::toString(model_) << ", SN " << serial_number_ << ")"
<< std::endl;
return true;
}
void Camera::close() {
recovering_ = false;
zed_.close();
}
void Camera::reset() {
close();
if (serial_number_ == 0 || !model_supports_reboot(model_)) {
return;
}
// The ZED can stop delivering frames while staying enumerated: the video
// endpoint halts, grabs fail, and reopening returns CAMERA FAILED TO SETUP
// because the endpoint is still halted. A device reset clears that, and
// reboot() is the SDK's way to ask for one without unplugging the camera.
// The device re-enumerates afterwards, so the caller's backoff before the
// next open is what lets it settle.
sl::ERROR_CODE err = sl::Camera::reboot(static_cast<int>(serial_number_), true);
std::cout << "reset ZED camera (SN " << serial_number_ << "): " << sl::toString(err)
<< std::endl;
}
bool Camera::is_open() const {
return zed_.isOpened();
}
bool Camera::recovering() const {
return recovering_;
}
void Camera::warmup() {
for (int i = 0; i < config_.warmup_frames; i++) {
zed_.grab(runtime_params_);
@@ -145,9 +203,27 @@ void Camera::warmup() {
}
bool Camera::grab() {
if (zed_.grab(runtime_params_) != sl::ERROR_CODE::SUCCESS) {
sl::ERROR_CODE err = zed_.grab(runtime_params_);
if (err != sl::ERROR_CODE::SUCCESS) {
if (is_transient_grab_error(err)) {
// Every grab repeats this for as long as the reconnect takes, so only
// the first one is worth a line in the log.
if (!recovering_) {
recovering_ = true;
std::cerr << "ZED camera link dropped (" << sl::toString(err)
<< "); the SDK is reconnecting it" << std::endl;
}
} else {
recovering_ = false;
std::cerr << "Failed to grab frame: " << sl::toString(err) << std::endl;
}
return false;
}
if (recovering_) {
recovering_ = false;
std::cout << "ZED camera link restored" << std::endl;
}
zed_.retrieveImage(image_mat_, sl::VIEW::LEFT);
zed_.retrieveMeasure(depth_mat_, sl::MEASURE::DEPTH);
// At 1080p a full XYZRGBA cloud is ~14.7 MB per frame, and retrieving it
@@ -175,6 +251,33 @@ Calibration Camera::calibration() const {
return result;
}
bool Camera::image_is_blank() {
const int height = image_mat_.getHeight();
const int width = image_mat_.getWidth();
if (height == 0 || width == 0) {
return true;
}
const size_t step = image_mat_.getStep(sl::MEM::CPU);
const sl::uchar4* src = image_mat_.getPtr<sl::uchar4>();
unsigned char lowest[3] = {255, 255, 255};
unsigned char highest[3] = {0, 0, 0};
for (int y = 0; y < height; y += BLANK_FRAME_STRIDE) {
const sl::uchar4* row = src + static_cast<size_t>(y) * step;
for (int x = 0; x < width; x += BLANK_FRAME_STRIDE) {
const unsigned char channels[3] = {row[x].x, row[x].y, row[x].z};
for (int c = 0; c < 3; c++) {
lowest[c] = std::min(lowest[c], channels[c]);
highest[c] = std::max(highest[c], channels[c]);
}
}
}
// Sensor noise makes a single-colour frame impossible for a live camera, so
// this tells a dead device apart from any real scene.
return lowest[0] == highest[0] && lowest[1] == highest[1] && lowest[2] == highest[2];
}
int64_t Camera::device_timestamp_ns() {
return static_cast<int64_t>(
zed_.getTimestamp(sl::TIME_REFERENCE::IMAGE).getNanoseconds());
@@ -192,8 +295,4 @@ sl::Mat& Camera::point_cloud() {
return point_cloud_mat_;
}
sl::Camera& Camera::handle() {
return zed_;
}
} // namespace zed