Files
autopilot/ai_controller/aienginegimbalserveractions.cpp
T
Nffj84 deb607237e Set camera ready for lift and drop
Added functionality to set camera ready for bringing it down or up.
Camera will be made available for AI after bringCameraDown command is given via UDPSocket.
Camera will be made unavailable for AI after bringCameraUp command is given via UDPSocket.
2025-03-24 18:01:47 +02:00

379 lines
16 KiB
C++

#include <QUdpSocket>
#include <QVariant>
#include "aienginegimbalserveractions.h"
#include "aienginegimbalserverudp.h"
AiEngineGimbalServerActions::AiEngineGimbalServerActions(QObject *parent)
: QObject{parent}
{
}
void AiEngineGimbalServerActions::setup(AiEngineGimbalServerUDP *udpSocket, AiEngineGimbalServerUDPCommand *udpCommand, AiEngineGimbalServerUDPResponse *udpResponse, AiEngineGimbalStatus *gimbalStatus)
{
mUdpSocket = udpSocket;
mUdpCommand = udpCommand;
mUdpResponse = udpResponse;
mGimbalStatus = gimbalStatus;
mAllowCameraCommands = false;
// Set initial position and update status
QByteArray tempCommand;
QByteArray tempResponse;
QHash<QString, QVariant> responseValues;
// Get camera ID
tempCommand = mUdpCommand->getCommand(UDP_COMMAND_ID::ACQUIRE_HW_INFO);
mUdpSocket->sendCommand(tempCommand);
tempResponse = mUdpSocket->readResponse();
responseValues = mUdpResponse->getResponceValues(tempResponse);
mGimbalStatus->hardwareID = responseValues["hardware_id"].toInt();
switch (mGimbalStatus->hardwareID) {
case AI_ENGINE_CAMERA_ZR10_HID:
mGimbalStatus->aiEngineCameraFOVHMin = AI_ENGINE_CAMERA_ZR10_FIELD_OF_VIEW_HORIZONTAL_MIN;
mGimbalStatus->aiEngineCameraFOVHMax = AI_ENGINE_CAMERA_ZR10_FIELD_OF_VIEW_HORIZONTAL_MAX;
mGimbalStatus->aiEngineCameraFOVVMin = AI_ENGINE_CAMERA_ZR10_FIELD_OF_VIEW_VERTICAL_MIN;
mGimbalStatus->aiEngineCameraFOVVMax = AI_ENGINE_CAMERA_ZR10_FIELD_OF_VIEW_VERTICAL_MAX;
mGimbalStatus->aiEngineCameraFLMin = AI_ENGINE_CAMERA_ZR10_FOCAL_LENGTH_MIN;
mGimbalStatus->aiEngineCameraFLMax = AI_ENGINE_CAMERA_ZR10_FOCAL_LENGTH_MAX;
break;
case AI_ENGINE_CAMERA_A8_HID:
mGimbalStatus->aiEngineCameraFOVHMin = AI_ENGINE_CAMERA_A8_FIELD_OF_VIEW_HORIZONTAL_MIN;
mGimbalStatus->aiEngineCameraFOVHMax = AI_ENGINE_CAMERA_A8_FIELD_OF_VIEW_HORIZONTAL_MAX;
mGimbalStatus->aiEngineCameraFOVVMin = AI_ENGINE_CAMERA_A8_FIELD_OF_VIEW_VERTICAL_MIN;
mGimbalStatus->aiEngineCameraFOVVMax = AI_ENGINE_CAMERA_A8_FIELD_OF_VIEW_VERTICAL_MAX;
mGimbalStatus->aiEngineCameraFLMin = AI_ENGINE_CAMERA_A8_FOCAL_LENGTH_MIN;
mGimbalStatus->aiEngineCameraFLMax = AI_ENGINE_CAMERA_A8_FOCAL_LENGTH_MAX;
break;
default:
qDebug().noquote().nospace() << "ERROR: Unknown HardwareID " << mGimbalStatus->hardwareID;
break;
}
// Get resolution to reduce calls to camera
tempCommand = mUdpCommand->getCommand(UDP_COMMAND_ID::ACQUIRE_CAMERA_CODEC_SPECS);
mUdpSocket->sendCommand(tempCommand);
tempResponse = mUdpSocket->readResponse();
responseValues = mUdpResponse->getResponceValues(tempResponse);
mGimbalStatus->resolutionX = responseValues["width"].toInt();
mGimbalStatus->resolutionY = responseValues["height"].toInt();
// Get max zoom value to reduce calls to camera
tempCommand = mUdpCommand->getCommand(UDP_COMMAND_ID::ACQUIRE_MAX_ZOOM_VALUE);
mUdpSocket->sendCommand(tempCommand);
tempResponse = mUdpSocket->readResponse();
responseValues = mUdpResponse->getResponceValues(tempResponse);
float maxZoom = responseValues["zoom"].toFloat();
mGimbalStatus->maxZoom = maxZoom > 10 ? 10 : maxZoom;
// Go to initial orientation and zoom
goToInitialOrientation();
mGimbalStatus->currentPitch = AI_ENGINE_GIMBAL_INITIAL_PITCH;
mGimbalStatus->currentRoll = AI_ENGINE_GIMBAL_INITIAL_ROLL;
mGimbalStatus->currentYaw = AI_ENGINE_GIMBAL_INITIAL_YAW;
mGimbalStatus->currentZoom = AI_ENGINE_CAMERA_INITIAL_ZOOM;
qDebug().noquote().nospace() << "mGimbalStatus->hardwareID: " << mGimbalStatus->hardwareID;
qDebug().noquote().nospace() << "mGimbalStatus->maxZoom: " << mGimbalStatus->maxZoom;
qDebug().noquote().nospace() << "mGimbalStatus->currentZoom: " << mGimbalStatus->currentZoom;
qDebug().noquote().nospace() << "mGimbalStatus->currentPitch: " << mGimbalStatus->currentPitch;
qDebug().noquote().nospace() << "mGimbalStatus->currentYaw: " << mGimbalStatus->currentYaw;
qDebug().noquote().nospace() << "mGimbalStatus->currentRoll: " << mGimbalStatus->currentRoll;
qDebug().noquote().nospace() << "mGimbalStatus->resolutionX: " << mGimbalStatus->resolutionX;
qDebug().noquote().nospace() << "mGimbalStatus->resolutionY: " << mGimbalStatus->resolutionY;
}
AiEngineRectangleProperties AiEngineGimbalServerActions::calculateRectangleProperties(int top, int left, int bottom, int right) {
// AI resolution is 640 * 360, which is half of 1280 * 720
// Multiply by 2 to get appropriate position on video
top = top * 2;
left = left * 2;
bottom = bottom * 2;
right = right * 2;
// Sanity check
// top cannot be greater than bottom
// left cannot be greater than right
if (top > bottom) {
int temp = top;
top = bottom;
bottom = temp;
qWarning().noquote().nospace() << "calculateRectangleProperties(): top and bottom mixed?";
}
if (left > right) {
int temp = left;
left = right;
right = temp;
qWarning().noquote().nospace() << "calculateRectangleProperties(): left and right mixed?";
}
AiEngineRectangleProperties properties;
properties.width = right - left;
properties.height = bottom - top;
properties.middleX = static_cast<int>(left + properties.width / 2);
properties.middleY = static_cast<int>(top + properties.height / 2);
// Sanity check, none cannot be 0
// If that is the case, we will not turn or zoom
if (properties.height == 0 || properties.width == 0 || properties.middleX == 0 || properties.middleY == 0) {
properties.height = mGimbalStatus->resolutionY;
properties.width = mGimbalStatus->resolutionX;
properties.middleX = mGimbalStatus->resolutionX / 2;
properties.middleY = mGimbalStatus->resolutionY / 2;
qWarning().noquote().nospace() << "calculateRectangleProperties(): Something was zero -> No zoom, no turn!";
}
return properties;
}
void AiEngineGimbalServerActions::getAnglesToOnScreenTarget(int targetX, int targetY, float &resultYaw, float &resultPitch)
{
// Get current yaw and pitch
resultYaw = mGimbalStatus->currentYaw;
resultPitch = mGimbalStatus->currentPitch;
// Normalize target pixel location to [-0.5, 0.5] range
float normPixelX = (targetX - mGimbalStatus->resolutionX / 2.0f) / (mGimbalStatus->resolutionX / 2.0f);
float normPixelY = (targetY - mGimbalStatus->resolutionY / 2.0f) / (mGimbalStatus->resolutionY / 2.0f);
// Adjust horizontal field of view for zoom
float horizontalFov = mGimbalStatus->aiEngineCameraFOVHMin + (10 - mGimbalStatus->currentZoom) * (mGimbalStatus->aiEngineCameraFOVHMax - mGimbalStatus->aiEngineCameraFOVHMin) / 9;
float verticalFov = mGimbalStatus->aiEngineCameraFOVVMin + (10 - mGimbalStatus->currentZoom) * (mGimbalStatus->aiEngineCameraFOVVMax - mGimbalStatus->aiEngineCameraFOVVMin) / 9;
float focalLength = mGimbalStatus->aiEngineCameraFLMin + (mGimbalStatus->currentZoom - 1) * (mGimbalStatus->aiEngineCameraFLMax - mGimbalStatus->aiEngineCameraFLMin) / 9;
// Calculate image plane dimensions based on focal length and aspect ratio
float imagePlaneWidth = 2.0f * focalLength * tan(degreesToRadians(horizontalFov) / 2.0f);
float imagePlaneHeight = 2.0f * focalLength * tan(degreesToRadians(verticalFov) / 2.0f);
// Calculate angle offsets based on normalized pixel location and image plane dimensions
float turnX = atan2(normPixelX * imagePlaneWidth / 2.0f, mGimbalStatus->aiEngineCameraFLMin) * 180.0f / M_PI;
float turnY = atan2(normPixelY * imagePlaneHeight / 2.0f, mGimbalStatus->aiEngineCameraFLMin) * 180.0f / M_PI;
// Make alterations to current angles
resultYaw -= turnX;
resultPitch -= turnY;
}
void AiEngineGimbalServerActions::turnToTarget(AiEngineRectangleProperties rectangle)
{
if (mAllowCameraCommands == true) {
qDebug().noquote().nospace() << "Turning to target";
float resultYaw = 0.0f;
float resultPitch = 0.0f;
getAnglesToOnScreenTarget(rectangle.middleX, rectangle.middleY, resultYaw, resultPitch);
QByteArray serialCommandTurn = mUdpCommand->getCommand(UDP_COMMAND_ID::TURN_TO_PIXEL);
int16_t degreesVal = resultYaw * 10;
serialCommandTurn[8] = degreesVal & 0xFF;
serialCommandTurn[9] = degreesVal >> 8;
degreesVal = resultPitch * 10;
serialCommandTurn[10] = degreesVal & 0xFF;
serialCommandTurn[11] = degreesVal >> 8;
mUdpSocket->sendCommand(serialCommandTurn);
}
}
void AiEngineGimbalServerActions::zoomToTarget(AiEngineRectangleProperties rectangle)
{
if (mAllowCameraCommands == true) {
qDebug().noquote().nospace() << "Zooming to target";
float fillRatio = 0.5;
float targetPixelWidth = rectangle.width;
float targetPixelHeight = rectangle.height;
float adjustedObjectWidth = targetPixelWidth / fillRatio;
float adjustedObjectHeight = targetPixelHeight / fillRatio;
float zoomWidth = static_cast<double>(mGimbalStatus->resolutionX) / adjustedObjectWidth;
float zoomHeight = static_cast<double>(mGimbalStatus->resolutionY) / adjustedObjectHeight;
float zoom = std::min(zoomWidth, zoomHeight);
if (zoom < 1.0f) {
zoom = 1.0f;
}
QByteArray serialCommandNewZoom = mUdpCommand->getCommand(UDP_COMMAND_ID::ZOOM_TO_X);
uint8_t integerPart = static_cast<uint8_t>(zoom);
float fractionalPart = zoom - integerPart;
uint8_t scaledFractional = uint8_t(fractionalPart * 10);
serialCommandNewZoom[8] = integerPart;
serialCommandNewZoom[9] = scaledFractional;
mUdpSocket->sendCommand(serialCommandNewZoom);
}
}
void AiEngineGimbalServerActions::getLocation(AiEngineDronePosition dronePosition, int targetIndex)
{
// From the drone and camera
CameraData cameraData = getCameraData();
GPSData gpsData = {dronePosition.position.alt, dronePosition.position.lat, dronePosition.position.lon};
DroneData droneData = {gpsData, dronePosition.yaw, dronePosition.pitch, 0.0f /* Roll */}; // GPS (latitude, longitude, altitude) and heading
// Calculate the GPS location of the target
AiEngineGeoPosition targetPosition = calculateTargetLocation(droneData, cameraData);
// TODO: Send position
AiEngineTargetPosition targetReturn;
targetReturn.position = targetPosition;
targetReturn.targetIndex = targetIndex;
qDebug() << "targetReturn.targetIndex: " << targetReturn.targetIndex;
qDebug() << "targetReturn.position.alt: " << targetReturn.position.alt;
qDebug() << "targetReturn.position.lat: " << targetReturn.position.lat;
qDebug() << "targetReturn.position.lon: " << targetReturn.position.lon;
emit aiTargetZoomed(targetReturn);
}
void AiEngineGimbalServerActions::restoreOrientationAndZoom(AiEngineGimbalStatus gimbalStatus)
{
if (mAllowCameraCommands == true) {
QByteArray tempCommand;
// Go to initial orientation
tempCommand = mUdpCommand->getCommand(UDP_COMMAND_ID::TURN_TO_DEGREES);
int16_t degreesVal = gimbalStatus.currentYaw * 10;
tempCommand[8] = degreesVal & 0xFF;
tempCommand[9] = degreesVal >> 8;
degreesVal = gimbalStatus.currentPitch * 10;
tempCommand[10] = degreesVal & 0xFF;
tempCommand[11] = degreesVal >> 8;
mUdpSocket->sendCommand(tempCommand);
// Go to initial zoom
tempCommand = mUdpCommand->getCommand(UDP_COMMAND_ID::ZOOM_TO_X);
uint8_t integerPart = static_cast<uint8_t>(gimbalStatus.currentZoom);
float fractionalPart = gimbalStatus.currentZoom - integerPart;
uint8_t scaledFractional = uint8_t(fractionalPart * 10);
tempCommand[8] = integerPart;
tempCommand[9] = scaledFractional;
mUdpSocket->sendCommand(tempCommand);
}
// TODO: Maybe send signal that all done?
}
CameraData AiEngineGimbalServerActions::getCameraData()
{
uint16_t height = mGimbalStatus->resolutionY;
uint16_t width = mGimbalStatus->resolutionX;
float yaw = 0 - mGimbalStatus->currentYaw; // Reverse value for calculation purposes
float pitch = 0 - mGimbalStatus->currentPitch; // Reverse value for calculation purposes
return {height, width, pitch, yaw};
}
// Function to calculate the new GPS location
AiEngineGeoPosition AiEngineGimbalServerActions::calculateTargetLocation(DroneData drone, CameraData camera)
{
// Calculate altitude and distance to the target
//float targetDistance = calculateTargetDistanceFromTargetSize(targetTrueSize, targetPixelSize, cameraData.width, degreesToRadians(cameraData.fow));
float slantDistance = 0;
float horizontalDistance = 0;
calculateDistancesToTarget(drone.gps.altitude, camera.pitch, slantDistance, horizontalDistance);
// Calculate new altitude using the slant distance and angle
float pitchRad = degreesToRadians(camera.pitch);
float sinPitchRad = std::sin(pitchRad);
float altitudeDifference = std::round(slantDistance * sinPitchRad * 100.0) / 100.0; // Rounding to avoid weird targetAltitude
float targetAltitude = (std::round(drone.gps.altitude * 100.0) / 100.0) - altitudeDifference; // Rounding to avoid weird targetAltitude
// Calculate the bearing from the drone orientation and camera orientation
float targetBearing = std::fmod(drone.yaw + camera.yaw, 360.0f);
// Convert bearing and drone's latitude/longitude to radians
float bearingRad = degreesToRadians(targetBearing);
float latRad = degreesToRadians(drone.gps.latitude);
float lonRad = degreesToRadians(drone.gps.longitude);
// Calculate new latitude using Haversine formula
float newLatRad = std::asin(std::sin(latRad) * std::cos(horizontalDistance / EARTH_RADIUS) + std::cos(latRad) * std::sin(horizontalDistance / EARTH_RADIUS) * std::cos(bearingRad));
// Calculate new longitude using Haversine formula
float newLonRad = lonRad + std::atan2(std::sin(bearingRad) * std::sin(horizontalDistance / EARTH_RADIUS) * std::cos(latRad), std::cos(horizontalDistance / EARTH_RADIUS) - std::sin(latRad) * std::sin(newLatRad));
// Convert back to degrees for latitude and longitude
AiEngineGeoPosition newLocation;
newLocation.alt = targetAltitude;
newLocation.lat = newLatRad * 180.0f / static_cast<float>(M_PI);
newLocation.lon = newLonRad * 180.0f / static_cast<float>(M_PI);
return newLocation;
}
void AiEngineGimbalServerActions::calculateDistancesToTarget(float altitude, float cameraPitch, float &slantDistance, float &horizontalDistance)
{
// Convert pitch angle from degrees to radians
float cameraPitchRadians = degreesToRadians(cameraPitch);
// Value has to be between 0-90
cameraPitchRadians = std::clamp((double)cameraPitchRadians, (double)0.0f, M_PI_2);
// Calculate horizontal distance
horizontalDistance = altitude / tan(cameraPitchRadians);
// Adjust for Earth's curvature:
// We need to find the horizontal distance on the curved Earth surface that corresponds to this flat distance
double centralAngle = horizontalDistance / EARTH_RADIUS; // in radians
// Calculate the arc length on Earth's surface
horizontalDistance = EARTH_RADIUS * centralAngle;
// Calculate slant distance considering the Earth's curvature
slantDistance = sqrt(altitude * altitude + horizontalDistance * horizontalDistance);
}
// Function to convert degrees to radians
float AiEngineGimbalServerActions::degreesToRadians(float degrees)
{
return degrees * M_PI / 180.0f;
}
void AiEngineGimbalServerActions::goToInitialOrientation(void)
{
// These camera commands should always be allowed
// Go to initial orientation
tempCommand = mUdpCommand->getCommand(UDP_COMMAND_ID::TURN_TO_DEGREES);
int16_t degreesVal = AI_ENGINE_GIMBAL_INITIAL_YAW * 10;
tempCommand[8] = degreesVal & 0xFF;
tempCommand[9] = degreesVal >> 8;
degreesVal = AI_ENGINE_GIMBAL_INITIAL_PITCH * 10;
tempCommand[10] = degreesVal & 0xFF;
tempCommand[11] = degreesVal >> 8;
mUdpSocket->sendCommand(tempCommand);
// Go to initial zoom
tempCommand = mUdpCommand->getCommand(UDP_COMMAND_ID::ZOOM_TO_X);
uint8_t integerPart = static_cast<uint8_t>(AI_ENGINE_CAMERA_INITIAL_ZOOM);
float fractionalPart = AI_ENGINE_CAMERA_INITIAL_ZOOM - integerPart;
uint8_t scaledFractional = uint8_t(fractionalPart * 10);
tempCommand[8] = integerPart;
tempCommand[9] = scaledFractional;
mUdpSocket->sendCommand(tempCommand);
}
void AiEngineGimbalServerActions::setAllowCameraCommands(bool allow)
{
mAllowCameraCommands = allow;
}