Files
ardupilot/libraries/AP_Camera/AP_Camera_Backend.cpp
T
Andrew Tridgell 6434442671 AP_Camera: relay MAVLink cameras and resolve camera selectors
Cache bounded stream lists and resume replies independently on each
GCS link after transmit backpressure. Expire remote capture status
after three seconds, retain locally scheduled interval capture, and
back off unanswered status requests.

Preserve native camera identity in cached replies and isolated-link
broadcasts, and suppress synthetic FC-owned status for native cameras.
Restore configured mount associations in cached information only when
a same-system camera advertises none.

Add CAMn_COMPID (0 or 7-255) so camera-v2 discovery can use a
non-default component ID. The first slot owns a colliding ID; duplicate
or invalid IDs warn and disable discovery.

Resolve command selectors as component IDs or legacy slots 1-6 with the
same rules for live commands and missions. NaN in formerly reserved
selector fields means unset. Video commands with a nonzero camera
selector treat param1 as a stream ID and pass the status frequency
through; without one they keep the old camera-slot interpretation.

Fill camera_device_id in camera information, settings, capture status,
field of view and stream messages, and document the routing and
selector design.
2026-09-23 08:59:12 +10:00

480 lines
16 KiB
C++

#include "AP_Camera_config.h"
#if AP_CAMERA_ENABLED
#include "AP_Camera_Backend.h"
#include <GCS_MAVLink/GCS.h>
#include <AP_GPS/AP_GPS.h>
#include <AP_Mount/AP_Mount.h>
#include <AP_AHRS/AP_AHRS.h>
extern const AP_HAL::HAL& hal;
// Constructor
AP_Camera_Backend::AP_Camera_Backend(AP_Camera &frontend, AP_Camera_Params &params, uint8_t instance) :
_frontend(frontend),
_params(params),
_instance(instance)
{}
void AP_Camera_Backend::init()
{
#if AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
_camera_info.focal_length = NaNf;
_camera_info.sensor_size_h = NaNf;
_camera_info.sensor_size_v = NaNf;
_camera_info.flags = CAMERA_CAP_FLAGS_CAPTURE_IMAGE;
#endif // AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
}
// update - should be called at 50hz
void AP_Camera_Backend::update()
{
// Check camera options and start/stop recording based on arm/disarm
if (option_is_enabled(Option::RecordWhileArmed)) {
if (hal.util->get_soft_armed() != last_is_armed) {
last_is_armed = hal.util->get_soft_armed();
if (!record_video(last_is_armed)) {
GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Camera: failed to %s recording", last_is_armed ? "start" : "stop");
}
}
}
// try to take picture if pending
if (trigger_pending) {
take_picture();
}
// check feedback pin
check_feedback();
// time based triggering
// if time and distance triggering both are enabled then we only do time based triggering
if (time_interval_settings.num_remaining != 0) {
uint32_t delta_ms = AP_HAL::millis() - last_picture_time_ms;
if (delta_ms > time_interval_settings.time_interval_ms) {
if (take_picture()) {
// decrease num_remaining except when its -1 i.e. capture forever
if (time_interval_settings.num_remaining > 0) {
time_interval_settings.num_remaining--;
}
}
}
return;
}
// implement trigger distance
if (!is_positive(_params.trigg_dist)) {
last_location.lat = 0;
last_location.lng = 0;
return;
}
const AP_AHRS &ahrs = AP::ahrs();
Location current_loc;
if (!ahrs.get_location(current_loc)) {
return;
}
// check vehicle flight mode supports trigg dist
if (!_frontend.vehicle_mode_ok_for_trigg_dist()) {
return;
}
// check vehicle roll angle is less than configured maximum
if ((_frontend.get_roll_max() > 0) && (fabsf(ahrs.get_roll_deg()) > _frontend.get_roll_max())) {
return;
}
// initialise last location to current location
if (last_location.lat == 0 && last_location.lng == 0) {
last_location = current_loc;
return;
}
if (last_location.lat == current_loc.lat && last_location.lng == current_loc.lng) {
// we haven't moved - this can happen as update() may
// be called without a new GPS fix
return;
}
// check vehicle has moved at least trigg_dist meters
if (current_loc.get_distance(last_location) < _params.trigg_dist) {
return;
}
take_picture();
}
// get corresponding mount instance for the camera
uint8_t AP_Camera_Backend::get_mount_instance() const
{
// instance 0 means default
if (_params.mount_instance.get() == 0) {
return _instance;
}
return _params.mount_instance.get() - 1;
}
// get mavlink gimbal device id which is normally mount_instance+1
uint8_t AP_Camera_Backend::get_gimbal_device_id() const
{
#if HAL_MOUNT_ENABLED
const uint8_t mount_instance = get_mount_instance();
AP_Mount* mount = AP::mount();
if (mount != nullptr) {
if (mount->get_mount_type(mount_instance) != AP_Mount::Type::None) {
return mount->get_device_id(mount_instance);
}
}
#endif
return 0;
}
// take a picture. returns true on success
bool AP_Camera_Backend::take_picture()
{
// setup feedback pin interrupt or timer
setup_feedback_callback();
// check minimum time interval since last picture taken
uint32_t now_ms = AP_HAL::millis();
if (now_ms - last_picture_time_ms < (uint32_t)(_params.interval_min * 1000)) {
trigger_pending = true;
return false;
}
trigger_pending = false;
// trigger actually taking picture and update image count
if (trigger_pic()) {
image_index++;
last_picture_time_ms = now_ms;
IGNORE_RETURN(AP::ahrs().get_location(last_location));
#if HAL_LOGGING_ENABLED
log_picture();
#endif
return true;
}
return false;
}
// take multiple pictures, time_interval between two consecutive pictures is in miliseconds
// total_num is number of pictures to be taken, -1 means capture forever
void AP_Camera_Backend::take_multiple_pictures(uint32_t time_interval_ms, int16_t total_num)
{
time_interval_settings = {time_interval_ms, total_num};
}
// stop capturing multiple image sequence
void AP_Camera_Backend::stop_capture()
{
time_interval_settings = {0, 0};
}
// handle camera control
void AP_Camera_Backend::control(float session, float zoom_pos, float zoom_step, float focus_lock, int32_t shooting_cmd, int32_t cmd_id)
{
// take picture
if (shooting_cmd == 1) {
take_picture();
}
}
// send camera feedback message to GCS
void AP_Camera_Backend::send_camera_feedback(mavlink_channel_t chan)
{
int32_t altitude = 0;
if (camera_feedback.location.initialised() && !camera_feedback.location.get_alt_cm(Location::AltFrame::ABSOLUTE, altitude)) {
// completely ignore this failure! this is a shouldn't-happen
// as current_loc should never be in an altitude we can't
// convert.
}
int32_t altitude_rel = 0;
if (camera_feedback.location.initialised() && !camera_feedback.location.get_alt_cm(Location::AltFrame::ABOVE_HOME, altitude_rel)) {
// completely ignore this failure! this is a shouldn't-happen
// as current_loc should never be in an altitude we can't
// convert.
}
// send camera feedback message
mavlink_msg_camera_feedback_send(
chan,
camera_feedback.timestamp_us, // image timestamp
0, // target system id
_instance, // camera id
image_index, // image index
camera_feedback.location.lat, // latitude
camera_feedback.location.lng, // longitude
altitude*1e-2f, // alt MSL
altitude_rel*1e-2f, // alt relative to home
camera_feedback.roll_deg, // roll angle (deg)
camera_feedback.pitch_deg, // pitch angle (deg)
camera_feedback.yaw_deg, // yaw angle (deg)
0.0f, // focal length
CAMERA_FEEDBACK_PHOTO, // flags
camera_feedback.feedback_trigger_logged_count); // completed image captures
}
// send camera information message to GCS
void AP_Camera_Backend::send_camera_information(mavlink_channel_t chan) const
{
mavlink_camera_information_t camera_info;
#if AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
camera_info = _camera_info;
#else
memset(&camera_info, 0, sizeof(camera_info));
camera_info.focal_length = NaNf;
camera_info.sensor_size_h = NaNf;
camera_info.sensor_size_v = NaNf;
camera_info.flags = CAMERA_CAP_FLAGS_CAPTURE_IMAGE;
#endif // AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
// Set fixed fields
// lens_id is populated with the instance number, to disambiguate multiple cameras
camera_info.lens_id = _instance;
camera_info.camera_device_id = _instance + 1;
camera_info.gimbal_device_id = get_gimbal_device_id();
camera_info.time_boot_ms = AP_HAL::millis();
// Send CAMERA_INFORMATION message
mavlink_msg_camera_information_send_struct(chan, &camera_info);
}
#if AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
void AP_Camera_Backend::set_camera_information(mavlink_camera_information_t camera_info)
{
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Camera %u CAMERA_INFORMATION (%s %s) set from script", _instance, camera_info.vendor_name, camera_info.model_name);
_camera_info = camera_info;
};
#endif // AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
#if AP_MAVLINK_MSG_VIDEO_STREAM_INFORMATION_ENABLED
// send video stream information message to GCS
bool AP_Camera_Backend::send_video_stream_information(mavlink_channel_t chan, uint8_t &next_stream) const
{
#if AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
WITH_SEMAPHORE(comm_chan_lock(chan));
if (!HAVE_PAYLOAD_SPACE(chan, VIDEO_STREAM_INFORMATION)) {
return false;
}
// Send VIDEO_STREAM_INFORMATION message
mavlink_video_stream_information_t stream_info = _stream_info;
stream_info.camera_device_id = _instance + 1;
mavlink_msg_video_stream_information_send_struct(chan, &stream_info);
#endif // AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
return true;
}
#endif // AP_MAVLINK_MSG_VIDEO_STREAM_INFORMATION_ENABLED
#if AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
void AP_Camera_Backend::set_stream_information(mavlink_video_stream_information_t stream_info)
{
_stream_info = stream_info;
};
#endif // AP_CAMERA_INFO_FROM_SCRIPT_ENABLED
// send camera settings message to GCS
void AP_Camera_Backend::send_camera_settings(mavlink_channel_t chan) const
{
// send CAMERA_SETTINGS message
mavlink_msg_camera_settings_send(
chan,
AP_HAL::millis(), // time_boot_ms
CAMERA_MODE_IMAGE, // camera mode (0:image, 1:video, 2:image survey)
NaNf, // zoomLevel float, percentage from 0 to 100, NaN if unknown
NaNf, // focusLevel float, percentage from 0 to 100, NaN if unknown
_instance + 1); // camera_device_id
}
#if AP_CAMERA_SEND_FOV_STATUS_ENABLED
// send camera field of view status
void AP_Camera_Backend::send_camera_fov_status(mavlink_channel_t chan) const
{
// getting corresponding mount instance for camera
AP_Mount* mount = AP::mount();
if (mount == nullptr) {
return;
}
// get latest POI from mount
Quaternion quat;
Location camera_loc;
Location poi_loc;
const bool have_poi_loc = mount->get_poi(get_mount_instance(), quat, camera_loc, poi_loc);
// if failed to get POI, get camera location directly from AHRS
// and attitude directly from mount
bool have_camera_loc = have_poi_loc;
if (!have_camera_loc) {
have_camera_loc = AP::ahrs().get_location(camera_loc);
mount->get_attitude_quaternion(get_mount_instance(), quat);
}
// calculate attitude quaternion in earth frame using AHRS yaw
Quaternion quat_ef;
quat_ef.from_euler(0, 0, AP::ahrs().get_yaw_rad());
quat_ef *= quat;
// send camera fov status message only if the last calculated values aren't stale
const float quat_array[4] = {
quat_ef.q1,
quat_ef.q2,
quat_ef.q3,
quat_ef.q4
};
mavlink_msg_camera_fov_status_send(
chan,
AP_HAL::millis(),
have_camera_loc ? camera_loc.lat : INT32_MAX,
have_camera_loc ? camera_loc.lng : INT32_MAX,
have_camera_loc ? camera_loc.alt * 10 : INT32_MAX,
have_poi_loc ? poi_loc.lat : INT32_MAX,
have_poi_loc ? poi_loc.lng : INT32_MAX,
have_poi_loc ? poi_loc.alt * 10 : INT32_MAX,
quat_array,
horizontal_fov() > 0 ? horizontal_fov() : NaNf,
vertical_fov() > 0 ? vertical_fov() : NaNf,
_instance + 1 // camera_device_id
);
}
#endif
// send camera capture status message to GCS
void AP_Camera_Backend::send_camera_capture_status(mavlink_channel_t chan) const
{
// Current status of image capturing (0: idle, 1: capture in progress, 2: interval set but idle, 3: interval set and capture in progress)
// num_remaining is -1 when capturing until stopped, so test the same way update() does
const uint8_t image_status = (time_interval_settings.num_remaining != 0) ? 2 : 0;
// send CAMERA_CAPTURE_STATUS message
mavlink_msg_camera_capture_status_send(
chan,
AP_HAL::millis(),
image_status,
0, // current status of video capturing (0: idle, 1: capture in progress)
static_cast<float>(time_interval_settings.time_interval_ms) / 1000.0, // image capture interval (s)
0, // elapsed time since recording started (ms)
NaNf, // available storage capacity (ms)
image_index, // total number of images captured
_instance + 1); // camera_device_id
}
// setup a callback for a feedback pin. When on PX4 with the right FMU
// mode we can use the microsecond timer.
void AP_Camera_Backend::setup_feedback_callback()
{
if (_params.feedback_pin <= 0 || timer_installed || isr_installed) {
// invalid or already installed
return;
}
// ensure we are in input mode
hal.gpio->pinMode(_params.feedback_pin, HAL_GPIO_INPUT);
// enable pullup/pulldown
uint8_t trigger_polarity = _params.feedback_polarity == 0 ? 0 : 1;
hal.gpio->write(_params.feedback_pin, !trigger_polarity);
if (hal.gpio->attach_interrupt(_params.feedback_pin, FUNCTOR_BIND_MEMBER(&AP_Camera_Backend::feedback_pin_isr, void, uint8_t, bool, uint32_t),
trigger_polarity?AP_HAL::GPIO::INTERRUPT_RISING:AP_HAL::GPIO::INTERRUPT_FALLING)) {
isr_installed = true;
} else {
// install a 1kHz timer to check feedback pin
hal.scheduler->register_timer_process(FUNCTOR_BIND_MEMBER(&AP_Camera_Backend::feedback_pin_timer, void));
timer_installed = true;
}
}
// interrupt handler for interrupt based feedback trigger
void AP_Camera_Backend::feedback_pin_isr(uint8_t pin, bool high, uint32_t timestamp_us)
{
feedback_trigger_timestamp_us = timestamp_us;
feedback_trigger_count++;
}
// check if feedback pin is high for timer based feedback trigger, when
// attach_interrupt fails
void AP_Camera_Backend::feedback_pin_timer()
{
uint8_t pin_state = hal.gpio->read(_params.feedback_pin);
uint8_t trigger_polarity = _params.feedback_polarity == 0 ? 0 : 1;
if (pin_state == trigger_polarity &&
last_pin_state != trigger_polarity) {
feedback_trigger_timestamp_us = AP_HAL::micros();
feedback_trigger_count++;
}
last_pin_state = pin_state;
}
// check for feedback pin update and log if necessary
void AP_Camera_Backend::check_feedback()
{
if (feedback_trigger_logged_count != feedback_trigger_count) {
#if HAL_LOGGING_ENABLED
const uint32_t timestamp32 = feedback_trigger_timestamp_us;
#endif
feedback_trigger_logged_count = feedback_trigger_count;
// we should consider doing this inside the ISR and pin_timer
prep_mavlink_msg_camera_feedback(feedback_trigger_timestamp_us);
#if HAL_LOGGING_ENABLED
// log camera message
uint32_t tdiff = AP_HAL::micros() - timestamp32;
uint64_t timestamp = AP_HAL::micros64();
Write_Camera(timestamp - tdiff);
#endif
}
}
void AP_Camera_Backend::prep_mavlink_msg_camera_feedback(uint64_t timestamp_us)
{
const AP_AHRS &ahrs = AP::ahrs();
if (!ahrs.get_location(camera_feedback.location)) {
// completely ignore this failure! AHRS will provide its best guess.
}
camera_feedback.timestamp_us = timestamp_us;
camera_feedback.roll_deg = ahrs.get_roll_deg();
camera_feedback.pitch_deg = ahrs.get_pitch_deg();
camera_feedback.yaw_deg = ahrs.get_yaw_deg();
camera_feedback.feedback_trigger_logged_count = feedback_trigger_logged_count;
GCS_SEND_MESSAGE(MSG_CAMERA_FEEDBACK);
}
#if HAL_LOGGING_ENABLED
// log picture
void AP_Camera_Backend::log_picture()
{
const bool using_feedback_pin = _params.feedback_pin > 0;
if (!using_feedback_pin) {
// if we're using a feedback pin then when the event occurs we
// stash the feedback data. Since we're not using a feedback
// pin, we just use "now".
prep_mavlink_msg_camera_feedback(AP::gps().time_epoch_usec());
}
if (!using_feedback_pin) {
Write_Camera();
} else {
Write_Trigger();
}
}
#endif
#endif // AP_CAMERA_ENABLED