Zero2WFpv application
Note: Zero2WFpv application is a software prototype and research work only. The software is not intended for real use and is added to the RapidPixel SDK as bonus additional software without technical support or updates.
v1.0.1
Table of contents
- Overview
- Versions
- Source code files
- Config file
- On screen menu
- Auto control algorithm
- How to copy Zero2WFpv to RPI
- Run application
- Build application
- Raspberry PI configuration
- Hardware installation
- Source code
- Betaflight related information
- Licensing mechanism
- Overlay examples
Overview
Zero2WFpv application implements video processing pipeline: video capture > tracking > Analog or Rtp output streaming as well as providing auto control for Fpv drone. The application combines libraries: VSourceLibCamera (video capture from Libcamera API compatible devices), CvTracker, FormatConverterOpenCv, VOutputFb, CrsfParser, SerialPort, RtpPusher, VCodecV4L2 and License. The applications shows how to use combination of the libraries listed above. It is built for Raspberry PI5 and Raspberry PI Zero 2W, it is tested on Raspberry Pi OS (Bookworn x64).
After start application reads JSON config file which includes video capture parameters, video output parameters, communication parameters, auto control algorithm coefficients and auto control channel configurations. If there is no config file the application will create new one with default parameters. Additionally the application creates folder “Log” to write log information.
Versions
Table 1 - Application versions.
Version | Release date | What’s new |
---|---|---|
1.0.0 | 01.02.2025 | - First version. |
1.0.1 | 17.02.2025 | - Update submodules. |
Source code files
The library application by source code only. The user would be given a set of files in the form of a CMake project (repository). The repository structure is shown below:
CMakeLists.txt ------------- Main CMake file.
3rdparty ------------------- Folder with third-party libraries.
CMakeLists.txt --------- CMake file to include third-party libraries.
VSourceLibCamera ------- Source code of VSourceLibCamera library.
CvTracker -------------- Source code of CvTracker library.
FormatConverterOpenCv -- Source code of FormatConverterOpenCv library.
VOutputFb -------------- Source code of VOutputFb library.
CrsfParser ------------- Source code of CrsfParser library.
SerialPort ------------- Source code of SerialPort library.
License ---------------- Source code of License library.
RtpPusher -------------- Source code of RtpPusher library.
VCodecV4L2 ------------- Source code of VCodecV4L2 library.
src ------------------------ Folder with application source code.
CMakeLists.txt --------- CMake file.
Zero2WFpvVersion.h ----- Header file with application version.
Zero2WFpvVersion.h.in -- File for CMake to generate version header.
main.cpp --------------- Application source code file.
utils.h ---------------- Utility functions header file.
utils.cpp -------------- Utility functions source code file.
Pid.h ------------------ PID controller header file.
Pid.cpp ---------------- PID controller source code file.
Config file
Zero2WFpv application reads config file Zero2WFpv.json in the same folder with application executable file. Config file content:
{
"Params":
{
"communication":
{
"crsfUdpPort": 5000,
"isCrsfOverUdp": false,
"serialPortName": "/dev/ttyAMA0"
},
"pidControl":
{
"constantPitch": 1060,
"isConstantPitchEnabled": true,
"isPrintThrottle": false,
"isRollControlEnabled": true,
"isThrottleControlEnabled": true,
"isYawControlEnabled": true,
"rollCenter": 992,
"rollKd": 0.0,
"rollKi": 0.0,
"rollKp": 4.0,
"rollMax": 1600,
"rollMin": 300,
"throttleCenter": 536,
"throttleKd": 25.0,
"throttleKi": 0.0,
"throttleKp": 1.0,
"throttleMax": 1600,
"throttleMin": 500,
"yawCenter": 992,
"yawKd": 0.0,
"yawKi": 0.0,
"yawKp": 1.0,
"yawMax": 1600,
"yawMin": 300
},
"trackerControl":
{
"armChannel": 8,
"armChannelTriggerValue": 1000,
"captureChannel": 9,
"captureChannelTriggerValue": 1000,
"pitchChannel": 2,
"rollChannel": 1,
"throttleChannel": 0,
"yawChannel": 3
},
"videoOutput":
{
"bandwidthKbps": 1000000,
"encodingBitrateKpbs": 3000,
"fps": 30,
"gopSize": 30,
"ip": "127.0.0.1",
"isRtpOutput": false,
"port": 7032
},
"videoSource":
{
"cameraIndex": 0,
"fps": 30,
"height": 576,
"pixelFormat": "YUYV",
"width": 736
}
}
}
Table 2 - Config file parameters description.
Parameter | type | Description |
---|---|---|
Communication: | ||
crsfUdpPort | int | Crsf udp port number. |
isCrsfOverUdp | bool | Enable crsf input over udp. |
serialPortName | string | Serial port name. |
PID control parameters: | ||
constantPitch | int | Constant pitch value. Higher value means higher pitch so drone will move faster towards object. |
isConstantPitchEnabled | bool | Enable constant pitch for auto control. |
isPrintThrottle | bool | Print auto control calculated throttle and raw throttle values. |
isRollControlEnabled | bool | Enable roll control for auto control. |
isThrottleControlEnabled | bool | Enable throttle control for auto control. |
isYawControlEnabled | bool | Enable yaw control for auto control. |
rollCenter | int | Roll center value. |
rollKd | float | Roll Kd value. It is recommended to set this value to 0.0. |
rollKi | float | Roll Ki value. It is recommended to set this value to 0.0. |
rollKp | float | Roll Kp value. |
rollMax | int | Roll max value. |
rollMin | int | Roll min value. |
throttleCenter | int | Throttle center value. |
throttleKd | float | Throttle Kd value. |
throttleKi | float | Throttle Ki value. It is recommended to set this value to 0.0. |
throttleKp | float | Throttle Kp value. |
throttleMax | int | Throttle max value. |
throttleMin | int | Throttle min value. If diagonal trajectory is desired, this value should be close to throttleCenter. However, if horizontal distance to object is not enough long, drone may pass the object. |
yawCenter | int | Yaw center value. |
yawKd | float | Yaw Kd value. It is recommended to set this value to 0.0. |
yawKi | float | Yaw Ki value. It is recommended to set this value to 0.0. |
yawKp | float | Yaw Kp value. |
yawMax | int | Yaw max value. |
yawMin | int | Yaw min value. |
Tracker control parameters: | ||
armChannel | int | Arm channel number. |
armChannelTriggerValue | int | Arm channel trigger value. |
captureChannel | int | Capture channel number. |
captureChannelTriggerValue | int | Capture channel trigger value. |
pitchChannel | int | Pitch channel number. |
rollChannel | int | Roll channel number. |
throttleChannel | int | Throttle channel number. |
yawChannel | int | Yaw channel number. |
Video output parameters: | ||
bandwidthKbps | int | Communication channel bandwidth in Kbps. |
encodingBitrateKpbs | int | Video encoding bitrate in Kbps. |
fps | int | Video frames per second. |
gopSize | int | Group of pictures size. |
ip | string | Destination IP address for RTP output. |
isRtpOutput | bool | Enable RTP output. If RTP output is enabled, analog output will be disabled. |
port | int | Destination port for RTP output. |
Video source parameters: | ||
cameraIndex | int | Camera index. If only one camera connected to system, it is going to be 0. |
fps | int | Fps of video source. |
width | int | Video source width. |
height | int | Video source height. |
pixelFormat | string | Video source pixel format. (For example : “YUYV” , “NV12”, “BGR24”, “RGB24”) |
On screen menu
Zero2WFpv provides an on screen menu to configure some of application parameters without modifying config file. Access to menu and parameters configuration is done only by joystick channels.
- In order to open menu joystick position should be hold for 5 seconds as follows:
- Throttle channel should be at minimum value.
- Yaw channel should be at maximum left position.
- In order to close menu joystick position should be hold for 1 seconds as follows while in main menu:
- Throttle channel should be at minimum value.
- Yaw channel should be at maximum right position.
- In order to navigate in menu:
- Roll channel should be used to navigate submenu and back to main menu.
- Pitch channel should be used to iterate through parameters.
- Yaw channel should be used to change parameter value.
Table 3 - Main menu items.
Item name | Description |
---|---|
Throttle control | PID coefficients for throttle control as well as enable/disable throttle control. |
Roll control | PID coefficients for roll control as well as enable/disable roll control. |
Yaw control | PID coefficients for yaw control as well as enable/disable yaw control. |
Pitch control | Constant pitch value as well as enable/disable constant pitch. |
Config | Submenu to configure communication additional parameters. |
Table 4 - Controllers menu items.
Item name | Description |
---|---|
Center | Center value of controller. |
Kp | Proportional coefficient. |
Ki | Integral coefficient. |
Kd | Derivative coefficient. |
Min | Minimum value of controller. |
Max | Maximum value of controller. |
Enable | Enable/disable controller. |
Table 5 - Config menu items.
Item name | Description |
---|---|
Print throttle | Print auto control calculated throttle and raw throttle values. |
Capture channel | Capture channel number. |
Auto control channel | Auto control channel number. |
Throttle channel | Throttle channel number. |
Yaw channel | Yaw channel number. |
Roll channel | Roll channel number. |
Pitch channel | Pitch channel number. |
Auto control algorithm
Zero2WFpv application has 3 independent PID control algorithms to control FPV drone towards the object that is being tracked. These PID controllers are for roll, throttle and yaw control. When auto control mode is enabled, drone will move towards to object with a constant pitch value. These PID controllers can be indecently enabled or disabled as well as constant pitch value. Also all parameters related by controllers can be configured by config file and on screen menu.
If auto control is disabled Zero2WFpv application will bypass input CRSF data to Flight controller directly without modification on it. However, if auto control algorithm is enabled and tracker is in tracking mode, application will calculate new values for roll, throttle and yaw channels. These calculated CRSF values will be sent to Flight controller via serial port. All others CRSF input data will be bypassed to Flight controller without modification.
Also while auto control is on, operator can give commands to tracker by using joystick. Supported commands are:
- Moving tracking rectangle by using roll and pitch channels.
- Changing tracking rectangle size by using yaw channel.
How to copy Zero2WFpv executable to Raspberry PI
In order to copy ready application to raspberry pi follow steps. Run commands on your windows computer where you have Zero2WFpv file.
cd <directory where you have Zero2WFpv>
for example if you downloaded Zero2WFpv repository, there is compiled version inside. In directory “/Zero2WFpv/Compiled”
cd ./Compiled
then copy file to raspberry pi
scp ./Zero2WFpv pi@192.168.1.100:/home/pi
Enter password
It will be copied to raspberry pi’s home directory.
Note: The procedure till here, can be carried out without command line. It is much easier. Please check application WinSCP. It does not require any command line interaction.
Now it should become executable, it is just a raw file for raspberry yet. Run following commands on raspberry pi.
Go to home directory where we have Zero2WFpv.
cd
Make file executable.
chmod +x ./Zero2WFpv
Now application is ready to run.
Run application
Copy application (Zero2WFpv executable and Zero2WFpv.json) to any folder and run :
./Zero2WFpv
Table 6 - Application arguments.
Argument | Description |
---|---|
–install | Install application as systemd service that will be run automatically after boot. |
-v | Enable logger to print logs to console. |
-rc | Prints raw CRSF data to console. It is useful for determining channel numbers. |
-h, –help | Prints help message that includes application arguments. |
When the application is started for the first time, it creates a configuration file named Zero2WFpv.json if file does not exist (refer to the Config file section). If the application is run as a superuser using sudo, the file will be owned by the root user. Therefore, to modify the configuration file, superuser privileges will be necessary. For example:
sudo ./Zero2WFpv --install
Build application
Before build user should configure raspberry (Raspberry PI configuration). Typical build commands:
cd Zero2WFpv
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make
Raspberry PI configuration
OS install
-
Download Raspberry PI imager.
-
Choose Operating System: Raspberry Pi OS (other) -> Raspberry Pi OS Lite (64-bit) Debian Bookworm 12.
-
Choose storage (MicroSD) card.
-
Set additional options: do not set hostname “raspberrypi.local”, enable SSH (Use password authentication), set username “pi” and password “pi”, configure wireless LAN according your settings. You will need wi-fi for software installation, set appropriate time zone and wireless zone.
-
Save changes and push “Write” button. After push “Yes” to rewrite data on MicroSD. At the end remove MicroSD.
-
Insert MicroSD in Raspberry and power up Raspberry.
LAN configuration
-
Configure LAN IP address on your PC. For Windows 11 got to Settings -> Network & Internet -> Advanced Network Settings -> More network adapter options. Click right button on LAN connection used to connect to Raspberry PI and chose “Properties”. Double click on “Internet Protocol Version 4 (TCP/IPv4)”. Set static IP address 192.168.0.1 and mask 255.255.255.0.
-
Connect raspberry via LAN cable. After power up you don’t know IP address of Raspberry board but you can connect to raspberry via SSH using “raspberrypi” name. In Windows 11 open windows terminal or power shell terminal and type command ssh pi@raspberrypi. After connection type yes to establish authenticity. WARNING: If you work with Windows we recommend delete information about previous connections. Go to folder C:/Users/[your user name]/.ssh. Open file known_hosts is exists in text editor. Delete all lines “raspberrypi”.
-
You have to set static IP address in Raspberry PI but not NOW. After static IP set you will lose wi-fi connection which you need for software installation.
Install dependencies
-
Connect to raspberry via SSH: ssh pi@raspberrypi. WARNING: If you work with Windows we recommend delete information about previous connections. Go to folder C:/Users/[your user name]/.ssh. Open file known_hosts is exists in text editor. Delete all lines “raspberrypi”.
-
Install libraries:
sudo apt-get install cmake build-essential libopencv-dev libcamera-dev -y
-
Reboot the system.
sudo reboot
Configuration of analog video output and serial port
In order to use serial port and analog video output, RaspberryPi should be configured. Please follow the steps below:
-
Open the configuration file with the following command:
sudo nano /boot/firmware/config.txt
-
Add the following lines to the end of the file:
For RaspberryPi 5: bash sdtv_mode=2 sdtv_aspect=1 hdmi_force_hotplug=0 hdmi_ignore_hotplug=1 enable_tvout=1 dtparam=uart0=on
For RaspberryPi Zero 2W: bash sdtv_mode=2 sdtv_aspect=1 hdmi_force_hotplug=0 hdmi_ignore_hotplug=1 enable_tvout=1 dtoverlay=pi3-disable-bt enable_uart=1 init_uart_clock=16000000 dtparam=krnbt=off dtoverlay=uart0
also find the line dtoverlay=vc4-fkms-v3d and append “,composite” to the end of the line. It should be like this:
dtoverlay=vc4-fkms-v3d,composite
As alternative these configuration can be done by using raspi-config tool.
-
Save the file and exit.
-
Open the cmdline.txt file with the following command:
sudo nano /boot/firmware/cmdline.txt
-
Add the following line to beginning of the file:
video=Composite-1:720x576-24@25,tv_mode=PAL vc4.tv_norm=PAL
This kernel configuration will force composite video output. However, resolution of frame buffer can be different because of unstable firmware. So, please check frame buffer resolution by using following command:
```bash
fbset
```
Also ensure that there is nothing like “console=serial0,115200” in the file. If it exists, remove it. There can be console=ttyX but not serial0. This will ensure that serial port is not used for console.
File should be like this:
```bash
video=Composite-1:720x576-24@25,tv_mode=PAL vc4.tv_norm=PAL console=tty1 ........
```
-
Save the file and exit.
-
Reboot the RaspberryPi with the following command:
sudo reboot
How to set static IP
Static IP can be set by using /boot/firmware/cmdline.txt file. Open the file with following command:
sudo nano /boot/firmware/cmdline.txt
Add the following line to the end of the file:
ip=192.168.0.5::192.168.0.6:255.255.255.0:rpi:eth0:off
format of ip parameter is :
ip=<client-ip>::<gateway-ip>:<netmask>:<hostname>:<device>:<autoconf>
Also be aware that this file is located in /boot partition of SD card which means it can be accessed from any computer and can be modified. This configuration can be done while inserting SD card to any computer.
After setting static IP, reboot the RaspberryPi with the following command:
sudo reboot
Hardware installation
Following figure shows how to connect Raspberry PI to Fpv drone.
This figure shows for analog video output and CRSF is read from RC receiver. If CRSF over UDP and/or RTP output is enabled following connections should be done:
NOTE: Raspberry Pi should be powered from a stable source. If Raspberry Pi Zero 2W is used, it is possible to power it from FC since it will not exceed 1 Amp power. However for Raspberry Pi 5, it is recommended to use a separate DC-DC converter to power Raspberry Pi.
Source code
#include <iostream>
#include <string>
#include <fstream>
#include <cstdlib>
#include <unistd.h>
#include <limits.h>
#include <algorithm>
#include <cstring>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <opencv2/opencv.hpp>
#include "VSourceLibCamera.h"
#include "VOutputFb.h"
#include "FormatConverterOpenCv.h"
#include "CvTracker.h"
#include "Logger.h"
#include "CrsfParser.h"
#include "SerialPort.h"
#include "VCodecV4L2.h"
#include "RtpPusher.h"
#include "utils.h"
#include "Pid.h"
#include "Zero2WFpvVersion.h"
/// Log folder.
#define LOG_FOLDER "Log"
// Link namespaces.
using namespace cv;
using namespace std;
using namespace cr::clib;
using namespace cr::video;
using namespace cr::utils;
using namespace std::chrono;
using namespace cr::vtracker;
using namespace cr::rtp;
/// Application params.
Params g_params;
/// Logger.
Logger g_log;
/// Log flag.
PrintFlag g_logFlag{PrintFlag::DISABLE};
/// Size of frame buffer.
const int g_frameBufferSize{4};
/// Shared frame buffer.
Frame g_sharedFrame[g_frameBufferSize];
/// Shared frame index.
atomic<uint8_t> g_sharedFrameIndex{0};
/// Shared frame condition variable.
condition_variable g_sharedFrameCond;
/// Shared frame condition variable mutex.
mutex g_sharedFrameCondMutex;
/// Shared frame condition variable flag.
atomic<bool> g_sharedFrameCondFlag;
/// Tracking rectangle X coordinate.
atomic<int> g_trackerResultX;
/// Tracking rectangle Y coordinate.
atomic<int> g_trackerResultY;
/// Tracking rectangle width.
atomic<int> g_trackerResultWidth;
/// Tracking rectangle height.
atomic<int> g_trackerResultHeight;
/// Tracking rectangle mode.
/// 0 - free mode, 1 - tracking mode, 2 - inertial mode, 3 - static mode.
atomic<int> g_trackerResultMode;
/// Tracker processing time.
atomic<int> g_trackerProcessingTime;
/// Tracker.
CvTracker g_tracker;
/// Flag that indicates if auto control is enabled.
atomic<bool> g_isArmed;
/// Throttle value.
atomic<int> g_throttleValue{0};
/// Flag for printing RC channels.
bool g_printRcChannels{false};
/// Yaw PID controller.
Pid g_pidYaw;
/// Throttle PID controller.
Pid g_pidThrottle;
/// Roll PID controller.
Pid g_pidRoll;
/// Pitch PID controller. It is not actually a PID controller, but a constant value.
Pid g_pidPitch;
/// Executable name.
string g_executableName;
/**
* @brief Crsf communication thread function.
*/
void CrsfThreadFunc();
/**
* @brief Tracker thread function.
*/
void trackerThreadFunc();
/// Config mode flag.
std::atomic<bool> g_configMode{false};
/// Menu index. 0 - Main menu, 1 - Sub menu.
std::atomic<int> g_MenuIndex{0};
/// Cursor index for main menu. 0 - Throttle, 1 - Yaw, 2 - Roll, 3 - Constant Pitch, 4 - Config.
std::atomic<int> g_cursorIndex{0};
/// Cursor index for sub menu.
///Values : For cursor 0 - 3 :
/// 0 - Center, 1 - Kp, 2 - Kd, 3 - Ki, 4 - PID Max, 5 - PID Min, 6 - PID Enable.
/// For cursor 4 :
/// 0 - Capture Channel, 1 - Arm Channel, 2 - Throttle Channel, 3 - Roll Channel, 4 - Pitch Channel, 5 - Yaw Channel.
std::atomic<int> g_subCursorIndex{0};
/// Output resolution. If rtp output is enabled, set output resolution to directly to camera resolution else to frame buffer resolution.
int g_outputWidth = 704;
int g_outputHeight = 576;
int main(int argc, char **argv)
{
//checkLicense();
// Configure folder to save logs.
Logger::setSaveLogParams(LOG_FOLDER, "log", 20, 1);
// Check if the executable name starts with "./"
g_executableName = argv[0];
if (g_executableName.rfind("./", 0) == 0)
g_executableName = g_executableName.substr(2); // Remove the "./" prefix
// Welcome message.
g_log.print(PrintColor::YELLOW, PrintFlag::CONSOLE) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] " <<
g_executableName << " application v" << ZERO2W_FPV_VERSION << std::endl;
// Parse arguments.
if (argc > 1)
{
// Parse the arguments
for (int i = 1; i < argc; ++i)
{
std::string arg = argv[i];
if (arg == "-v" || arg == "-vv")
g_logFlag = PrintFlag::CONSOLE_AND_FILE;
else if (arg == "--install")
createSystemdService(g_executableName);
else if (arg == "-h" || arg == "--help")
printUsage(g_executableName);
else if (arg == "-rc")
g_printRcChannels = true;
else
{
std::cerr << "See -h or --help option for usage." << std::endl;
return -1;
}
}
}
// Load config file.
if (!loadConfig(g_executableName + ".json"))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't load config file." << std::endl;
return -1;
}
g_log.print(PrintColor::CYAN, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] " <<
"Config file loaded." << std::endl;
// Init pid controllers with config params.
g_pidThrottle = Pid(g_params.pidControl.throttleKp, g_params.pidControl.throttleKd, g_params.pidControl.throttleKi,
g_params.pidControl.throttleCenter, g_params.pidControl.throttleMin, g_params.pidControl.throttleMax, g_params.pidControl.isThrottleControlEnabled);
g_pidYaw = Pid(g_params.pidControl.yawKp, g_params.pidControl.yawKd, g_params.pidControl.yawKi,
g_params.pidControl.yawCenter, g_params.pidControl.yawMin, g_params.pidControl.yawMax, g_params.pidControl.isYawControlEnabled);
g_pidRoll = Pid(g_params.pidControl.rollKp, g_params.pidControl.rollKd, g_params.pidControl.rollKi,
g_params.pidControl.rollCenter, g_params.pidControl.rollMin, g_params.pidControl.rollMax, g_params.pidControl.isRollControlEnabled);
g_pidPitch = Pid(0, 0, 0, g_params.pidControl.constantPitch, 0, 0, g_params.pidControl.isConstantPitchEnabled);
// Rtp pusher.
RtpPusher rtpPusher;
// Frame buffer output.
VOutputFb frameBufferOutput;
// Init rtp pusher or frame buffer output based on video output config params.
if (g_params.videoOutput.isRtpOutput)
{
if (!rtpPusher.init(g_params.videoOutput.ip, g_params.videoOutput.port, g_params.videoOutput.bandwidthKbps))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't initialise rtp pusher." << std::endl;
return -1;
}
}
else
{
if (!frameBufferOutput.init())
{
g_log.print(cr::utils::PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't initialise video output." << std::endl;
return -1;
}
}
if(g_params.videoOutput.isRtpOutput)
{
// If rtp output is enabled, set output resolution to directly to camera resolution.
g_outputWidth = g_params.videoSource.width;
g_outputHeight = g_params.videoSource.height;
}
else
{
// If frame buffer output is enabled, set output resolution to frame buffer resolution.
frameBufferOutput.getResolution(g_outputWidth, g_outputHeight);
}
// Init video source.
VSource* videoSource = new VSourceLibCamera();
// Set video source parameters.
videoSource->setParam(VSourceParam::ROI_X, (g_params.videoSource.width - g_outputWidth) / 2);
videoSource->setParam(VSourceParam::ROI_Y, (g_params.videoSource.height - g_outputHeight) / 2);
videoSource->setParam(VSourceParam::ROI_WIDTH, g_outputWidth);
videoSource->setParam(VSourceParam::ROI_HEIGHT, g_outputHeight);
videoSource->setParam(VSourceParam::FPS, 60);
videoSource->setParam(VSourceParam::EXPOSURE_MODE, 1); // Auto exposure.
videoSource->setParam(VSourceParam::GAIN_MODE, 1); // Auto gain.
// Init string for video source.
string initString = to_string(g_params.videoSource.cameraIndex) + ";" + to_string(g_params.videoSource.width) + ";" +
to_string(g_params.videoSource.height) + ";" + to_string(g_params.videoSource.fps) + ";" + g_params.videoSource.pixelFormat;
// Init video source.
if (!videoSource->openVSource(initString))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't initialise video source." << std::endl;
return -1;
}
g_log.print(PrintColor::CYAN, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] INFO: " <<
"Video source init." << std::endl;
// Wait for camera to init.
this_thread::sleep_for(seconds(1));
// Start Crsf communication thread.
thread crsfThread(CrsfThreadFunc);
crsfThread.detach();
// Start tracker thread.
thread trackerThread(trackerThreadFunc);
trackerThread.detach();
/// Source frame.
Frame sourceFrame;
Frame sourceFrameYuv;
sourceFrameYuv.fourcc = Fourcc::YUV24;
/// Bgr frame for frame buffer output.
Frame bgrFrame;
bgrFrame.fourcc = Fourcc::BGR24;
/// NV12 frame for H264 encoding.
Frame nv12Frame;
nv12Frame.fourcc = Fourcc::NV12;
/// H264 frame for RTP output.
Frame h264Frame (g_outputWidth, g_outputHeight, Fourcc::H264);
// Error message when there is no frame from camera.
cr::video::Frame emptyFrame (g_outputWidth, g_outputHeight, cr::video::Fourcc::BGR24);
Mat errorMessage(emptyFrame.height, emptyFrame.width, CV_8UC3, emptyFrame.data);
string text = "No video from camera";
int fontFace = FONT_HERSHEY_SIMPLEX;
double fontScale = 1.5;
int thickness = 2;
// Calculate text size
int baseline = 0;
Size textSize = getTextSize(text, fontFace, fontScale, thickness, &baseline);
baseline += thickness;
// Center the text
Point textOrg((errorMessage.cols - textSize.width) / 2, (errorMessage.rows + textSize.height) / 2);
// Render the text on the frame
putText(errorMessage, text, textOrg, fontFace, fontScale, Scalar(0, 0, 255), thickness, 8);
// Init frame buffer scaled by 2.
for (int i = 0; i < g_frameBufferSize; i++)
g_sharedFrame[i] = Frame(g_outputWidth / 2, g_outputHeight / 2, Fourcc::YUV24);
// Init frame converter.
FormatConverterOpenCv converter;
// Converter for NV12
FormatConverterOpenCv converterNV12;
// Codec for H264 encoding.
VCodecV4L2 codec;
// Set codec parameters.
codec.setParam(VCodecParam::BITRATE_KBPS, g_params.videoOutput.encodingBitrateKpbs);
codec.setParam(VCodecParam::GOP, g_params.videoOutput.gopSize);
codec.setParam(VCodecParam::FPS, g_params.videoOutput.fps);
codec.setDevice("/dev/video11");
while(true)
{
// Get frame from video source. 100 msec timeout.
if (!videoSource->getFrame(sourceFrame, 100))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't get frame from video source." << std::endl;
// Put empty frame to output.
if (!frameBufferOutput.write(emptyFrame))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't write empty frame to frame buffer." << std::endl;
}
/// @todo Error frame to rtp pusher should be added.
continue;
}
// Convert source frame to YUV.
if (!converter.convert(sourceFrame, sourceFrameYuv))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't convert frame to YUV24." << std::endl;
continue;
}
// Copy frame to frame buffer.
uint8_t index = g_sharedFrameIndex.load();
Mat sourceFrameYuvMat(sourceFrame.height, sourceFrame.width, CV_8UC3, sourceFrameYuv.data);
Mat sourceFrameYuvMatScaled(g_sharedFrame[index].height, g_sharedFrame[index].width, CV_8UC3, g_sharedFrame[index].data);
resize(sourceFrameYuvMat, sourceFrameYuvMatScaled, Size(g_sharedFrame[index].width, g_sharedFrame[index].height), 0.0, 0.0, INTER_LINEAR);
// Change index.
index++;
index = index & 0x03;
g_sharedFrameIndex.store(index);
// Notify tracker thread about new frame.
unique_lock<mutex> lock(g_sharedFrameCondMutex);
g_sharedFrameCondFlag.store(true);
g_sharedFrameCond.notify_one();
lock.unlock();
// Draw tracker results.
Mat drawingMat(sourceFrameYuv.height, sourceFrameYuv.width, CV_8UC3, sourceFrameYuv.data);
cv::Scalar colorDark(0, 128, 128); // Black in YUV
cv::Scalar colorWhite(255, 128, 128); // White in YUV
// Draw config menu.
if (g_configMode.load())
{
drawConfigMenu(drawingMat, g_MenuIndex.load(), g_cursorIndex.load(), g_subCursorIndex.load(), g_outputWidth, g_outputHeight, g_pidThrottle, g_pidYaw, g_pidRoll, g_pidPitch);
}
else
{
// Draw tracking rectangle.
if (g_trackerResultMode.load() != 0)
{
drawRectangle(drawingMat, sourceFrameYuv.width, sourceFrameYuv.height, g_trackerResultWidth.load() / 2, g_trackerResultHeight.load() / 2, g_trackerResultX.load(), g_trackerResultY.load(), 32, colorDark);
drawRectangle(drawingMat, sourceFrameYuv.width, sourceFrameYuv.height, g_trackerResultWidth.load() / 2 - 4, g_trackerResultHeight.load() / 2 - 4, g_trackerResultX.load(), g_trackerResultY.load(), 28, colorWhite);
}
if (g_isArmed)
{
std::string isArmed = "Auto"; ;
// Put to center of the screen.
int x = g_outputWidth / 2 - 15; // Almost center.
int y = g_outputHeight - 100; // Bottom of the screen.
putText(drawingMat, isArmed, Point(x, y), FONT_HERSHEY_SIMPLEX, 0.9, colorDark, 1);
putText(drawingMat, isArmed, Point(x + 1, y), FONT_HERSHEY_SIMPLEX, 0.9, colorWhite, 1);
}
if (g_params.pidControl.isPrintThrottle)
{
std::string throttleValue = "Throttle (current) : " + to_string(g_throttleValue.load());
putText(drawingMat, throttleValue, Point(240, (g_outputHeight - 46)), FONT_HERSHEY_SIMPLEX, 0.9, Scalar(0, 0, 255), 2);
std::string throtteCenter = "Throttle center : " + to_string(g_pidThrottle.center);
putText(drawingMat, throtteCenter, Point(240, (g_outputHeight - 16)), FONT_HERSHEY_SIMPLEX, 0.9, Scalar(0, 0, 255), 2);
}
// Dark circle in the center of the screen.
circle(drawingMat, Point(g_outputWidth / 2, g_outputHeight / 2), 4, colorDark, -2);
circle(drawingMat, Point(g_outputWidth / 2, g_outputHeight / 2), 2, colorWhite, -2);
// Draw only if tracker is in free mode.
if (g_trackerResultMode.load() == 0)
{
drawRectangle(drawingMat, sourceFrameYuv.width, sourceFrameYuv.height, 64, 64, g_outputWidth / 2, g_outputHeight / 2, 8, colorDark);
drawRectangle(drawingMat, sourceFrameYuv.width, sourceFrameYuv.height, 60, 60, g_outputWidth / 2, g_outputHeight / 2, 4, colorWhite);
}
}
if (g_params.videoOutput.isRtpOutput)
{
// Convert YUV frame to NV12 frame
if (!converterNV12.convert(sourceFrameYuv, nv12Frame))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't convert frame to NV12." << std::endl;
continue;
}
// Encode NV12 frame to H264.
if (!codec.transcode(nv12Frame, h264Frame))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't encode frame to H264." << std::endl;
continue;
}
// Write H264 frame to V4L2 output.
if (!rtpPusher.sendFrame(h264Frame))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't write frame to V4L2 output." << std::endl;
}
}
else // Frame buffer output.
{
// Convert source frame to BGR24. Use frame from previous iteration.
if (!converter.convert(sourceFrameYuv, bgrFrame))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't convert frame to BGR24." << endl;
continue;
}
// Write frame to output.
if (!frameBufferOutput.write(bgrFrame))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't write frame to frame buffer." << std::endl;
}
}
}
return 0;
}
void trackerThreadFunc()
{
// Set default tracker params.
g_tracker.setParam(VTrackerParam::RECT_HEIGHT, 32);
g_tracker.setParam(VTrackerParam::RECT_WIDTH, 32);
g_tracker.setParam(VTrackerParam::SEARCH_WINDOW_HEIGHT, 128);
g_tracker.setParam(VTrackerParam::SEARCH_WINDOW_WIDTH, 128);
g_tracker.setParam(VTrackerParam::MULTIPLE_THREADS, 1);
g_tracker.setParam(VTrackerParam::NUM_CHANNELS, 3);
while(true)
{
// Wait frame from main thread.
if(!g_sharedFrameCondFlag.load())
{
std::unique_lock<std::mutex> lock(g_sharedFrameCondMutex);
while(!g_sharedFrameCondFlag.load())
{
g_sharedFrameCond.wait(lock);
}
lock.unlock();
}
// Get frame index.
uint8_t index = g_sharedFrameIndex.load();
index--;
index = index & 0x03;
// Process frame. Use frame from previous iteration.
if (!g_tracker.processFrame(g_sharedFrame[index]))
{
g_log.print(cr::utils::PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't process frame." << std::endl;
continue;
}
// Get tracker result.
VTrackerParams params;
g_tracker.getParams(params);
// Set tracker rectangle size in FREE mode.
if (params.mode == 0)
{
g_tracker.setParam(VTrackerParam::RECT_HEIGHT, 64);
g_tracker.setParam(VTrackerParam::RECT_WIDTH, 64);
}
g_trackerResultX.store(params.rectX * 2);
g_trackerResultY.store(params.rectY * 2);
g_trackerResultWidth.store(params.rectWidth * 2);
g_trackerResultHeight.store(params.rectHeight * 2);
g_trackerResultMode.store(params.mode);
g_trackerProcessingTime.store(params.processingTimeMks);
}
}
void CrsfThreadFunc()
{
cr::clib::SerialPort serialPort;
cr::clib::UdpSocket udpSocket;
if (g_params.communication.isCrsfOverUdp)
{
if (!udpSocket.open(g_params.communication.crsfUdpPort, true))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't open udp socket." << std::endl;
exit(-1);
}
}
// Serial port is always used. Even if UDP is enabled for reading CRSF, serial port is used to write CRSF data to FC.
if (!serialPort.open(g_params.communication.serialPortName, 420000, 1000))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't open serial port : " << g_params.communication.serialPortName << std::endl;
exit(-1);
}
/// CRSF parser.
cr::clib::CrsfParser parser;
/// Capture channel last value.
uint16_t captureChannelLastValue = 0;
/// Arm channel last value.
uint16_t armChannelLastValue = 0;
/// Capture counter to prevent multiple capture.
int captureCounter = 0;
/// Cursor movement counter to prevent multiple cursor movement.
int cursorIndexCounter = 0;
/// Menu enable counter to prevent menu enable easily.
int menuEnableCounter = 0;
/// Moving tracker result counter.
int trackingCommandCounter = 0;
/// Throttle config mode counter.
int throttleConfigModeCounter = 0;
while(true)
{
// Read incoming data from serial port.
const int bufferSize = 256;
uint8_t rxBuffer[bufferSize];
int bytesRead = 0;
if (g_params.communication.isCrsfOverUdp)
bytesRead = udpSocket.read(rxBuffer, 64);
else
bytesRead = serialPort.read(rxBuffer, 64);
if (bytesRead <= 0 && !g_isArmed)
continue;
// Decode incoming data.
uint32_t crsfChannels[16];
uint8_t packet[64];
int size = 0;
bool isDetected = false;
for (int i = 0; i < bytesRead; i++)
{
if (parser.detect(rxBuffer[i], packet, size) == cr::clib::CrsfPacket::RC_CHANNELS)
{
parser.getRcChannels(crsfChannels);
break;
}
}
if (g_printRcChannels)
{
std::cout << "\r"
<< "Ch0: " << crsfChannels[0] << " "
<< "Ch1: " << crsfChannels[1] << " "
<< "Ch2: " << crsfChannels[2] << " "
<< "Ch3: " << crsfChannels[3] << " "
<< "Ch4: " << crsfChannels[4] << " "
<< "Ch5: " << crsfChannels[5] << " "
<< "Ch6: " << crsfChannels[6] << " "
<< "Ch7: " << crsfChannels[7] << " "
<< "Ch8: " << crsfChannels[8] << " "
<< "Ch9: " << crsfChannels[9] << " "
<< "Ch10: " << crsfChannels[10] << " "
<< "Ch11: " << crsfChannels[11] << " "
<< "Ch12: " << crsfChannels[12] << " "
<< "Ch13: " << crsfChannels[13] << " "
<< "Ch14: " << crsfChannels[14] << " "
<< "Ch15: " << crsfChannels[15] << " "
<< std::flush;
}
// Close config mode and save params to config file.
if (crsfChannels[g_params.trackerControl.throttleChannel] < 300 && g_MenuIndex == 0 && g_configMode.load() && crsfChannels[g_params.trackerControl.yawChannel] > 1500 && cursorIndexCounter >= 50)
{
cursorIndexCounter = 0;
g_configMode.store(false);
// Save PID values to config file.
g_params.pidControl.throttleKp = g_pidThrottle.kP;
g_params.pidControl.throttleKd = g_pidThrottle.kD;
g_params.pidControl.throttleKi = g_pidThrottle.kI;
g_params.pidControl.throttleCenter = g_pidThrottle.center;
g_params.pidControl.throttleMin = g_pidThrottle.min;
g_params.pidControl.throttleMax = g_pidThrottle.max;
g_params.pidControl.isThrottleControlEnabled = g_pidThrottle.isEnabled;
g_params.pidControl.yawKp = g_pidYaw.kP;
g_params.pidControl.yawKd = g_pidYaw.kD;
g_params.pidControl.yawKi = g_pidYaw.kI;
g_params.pidControl.yawCenter = g_pidYaw.center;
g_params.pidControl.yawMin = g_pidYaw.min;
g_params.pidControl.yawMax = g_pidYaw.max;
g_params.pidControl.isYawControlEnabled = g_pidYaw.isEnabled;
g_params.pidControl.rollKp = g_pidRoll.kP;
g_params.pidControl.rollKd = g_pidRoll.kD;
g_params.pidControl.rollKi = g_pidRoll.kI;
g_params.pidControl.rollCenter = g_pidRoll.center;
g_params.pidControl.rollMin = g_pidRoll.min;
g_params.pidControl.rollMax = g_pidRoll.max;
g_params.pidControl.isRollControlEnabled = g_pidRoll.isEnabled;
g_params.pidControl.constantPitch = g_pidPitch.center;
g_params.pidControl.isConstantPitchEnabled = g_pidPitch.isEnabled;
// Config reader.
ConfigReader config;
if (!config.set(g_params, "Params"))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't set config reader." << std::endl;
}
// Save config file.
if (!config.writeToFile(g_executableName + ".json"))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't save config file." << std::endl;
}
}
else if (crsfChannels[g_params.trackerControl.throttleChannel] < 300 && !g_configMode.load() && crsfChannels[g_params.trackerControl.yawChannel] < 200 && menuEnableCounter >= 1000 && cursorIndexCounter >= 50)
{
// Enter config mode.
menuEnableCounter = 0;
cursorIndexCounter = 0;
g_configMode.store(true);
}
else if (crsfChannels[g_params.trackerControl.throttleChannel] < 300 && !g_configMode.load() && crsfChannels[g_params.trackerControl.yawChannel] < 200 && menuEnableCounter <= 1000)
{
menuEnableCounter++;
}
// Update menu cursor with pitch channel max and min values.
if (crsfChannels[g_params.trackerControl.pitchChannel] > 1500 && cursorIndexCounter >= 50 && g_MenuIndex == 0)
{
cursorIndexCounter = 0;
g_cursorIndex = (g_cursorIndex - 1 + 5) % 5;
}
else if (crsfChannels[g_params.trackerControl.pitchChannel] < 200 && cursorIndexCounter >= 50 && g_MenuIndex == 0)
{
cursorIndexCounter = 0;
g_cursorIndex = (g_cursorIndex + 1) % 5;
}
// Update sub menu cursor with pitch channel max and min values.
else if (crsfChannels[g_params.trackerControl.pitchChannel] > 1500 && cursorIndexCounter >= 50 && g_MenuIndex == 1)
{
cursorIndexCounter = 0;
g_subCursorIndex = (g_subCursorIndex - 1 + 7) % 7;
}
else if (crsfChannels[g_params.trackerControl.pitchChannel] < 200 && cursorIndexCounter >= 50 && g_MenuIndex == 1)
{
cursorIndexCounter = 0;
g_subCursorIndex = (g_subCursorIndex + 1) % 7;
}
// Check we enter sub menu by using roll channel.
if (crsfChannels[g_params.trackerControl.rollChannel] > 1500 && cursorIndexCounter >= 50)
{
cursorIndexCounter = 0;
g_MenuIndex = 1; // Enter sub menu.
g_subCursorIndex = 0;
}
else if (crsfChannels[g_params.trackerControl.rollChannel] < 200 && cursorIndexCounter >= 50)
{
cursorIndexCounter = 0;
g_MenuIndex = 0; // Exit sub menu.
g_subCursorIndex = 0;
g_cursorIndex = 0;
}
// PID coefficients adjustment with 0 throttle and sub menu is open.
if (crsfChannels[g_params.trackerControl.throttleChannel] < 300 && g_MenuIndex == 1 && g_configMode.load() && cursorIndexCounter >= 50)
{
// Adjust PID coefficients with in current menu item and cursor index.
// Check if we are in sub menu.
cursorIndexCounter = 0;
// Update PID controller values.
if (g_cursorIndex < 4)
{
switch (g_cursorIndex)
{
case 0: // Throttle
g_pidThrottle.update(g_subCursorIndex.load(), crsfChannels[g_params.trackerControl.yawChannel]);
break;
case 1: // Yaw
g_pidYaw.update(g_subCursorIndex.load(), crsfChannels[g_params.trackerControl.yawChannel]);
break;
case 2: // Roll
g_pidRoll.update(g_subCursorIndex.load(), crsfChannels[g_params.trackerControl.yawChannel]);
break;
case 3: // Pitch
g_pidPitch.update(g_subCursorIndex.load(), crsfChannels[g_params.trackerControl.yawChannel]);
break;
}
}
else if (g_cursorIndex == 4) // Config channels
{
switch (g_subCursorIndex)
{
case 0: // Capture channel
{
if (crsfChannels[g_params.trackerControl.yawChannel] > 1500)
g_params.trackerControl.captureChannel = (g_params.trackerControl.captureChannel + 1) % 16;
else if (crsfChannels[g_params.trackerControl.yawChannel] < 200)
g_params.trackerControl.captureChannel = (g_params.trackerControl.captureChannel - 1 + 16) % 16;
break;
}
case 1: // Arm channel
{
if (crsfChannels[g_params.trackerControl.yawChannel] > 1500)
g_params.trackerControl.armChannel = (g_params.trackerControl.armChannel + 1) % 16;
else if (crsfChannels[g_params.trackerControl.yawChannel] < 200)
g_params.trackerControl.armChannel = (g_params.trackerControl.armChannel - 1 + 16) % 16;
break;
}
case 2: // Throttle channel
{
if (crsfChannels[g_params.trackerControl.yawChannel] > 1500)
g_params.trackerControl.throttleChannel = (g_params.trackerControl.throttleChannel + 1) % 16;
else if (crsfChannels[g_params.trackerControl.yawChannel] < 200)
g_params.trackerControl.throttleChannel = (g_params.trackerControl.throttleChannel - 1 + 16) % 16;
break;
}
case 3: // Roll channel
{
if (crsfChannels[g_params.trackerControl.yawChannel] > 1500)
g_params.trackerControl.rollChannel = (g_params.trackerControl.rollChannel + 1) % 16;
else if (crsfChannels[g_params.trackerControl.yawChannel] < 200)
g_params.trackerControl.rollChannel = (g_params.trackerControl.rollChannel - 1 + 16) % 16;
break;
}
case 4: // Pitch channel
{
if (crsfChannels[g_params.trackerControl.yawChannel] > 1500)
g_params.trackerControl.pitchChannel = (g_params.trackerControl.pitchChannel + 1) % 16;
else if (crsfChannels[g_params.trackerControl.yawChannel] < 200)
g_params.trackerControl.pitchChannel = (g_params.trackerControl.pitchChannel - 1 + 16) % 16;
break;
}
case 5: // Yaw channel
{
if (crsfChannels[g_params.trackerControl.yawChannel] > 1500)
g_params.trackerControl.yawChannel = (g_params.trackerControl.yawChannel + 1) % 16;
else if (crsfChannels[g_params.trackerControl.yawChannel] < 200)
g_params.trackerControl.yawChannel = (g_params.trackerControl.yawChannel - 1 + 16) % 16;
break;
}
case 6: // is print throttle value
{
if (crsfChannels[g_params.trackerControl.yawChannel] > 1500)
g_params.pidControl.isPrintThrottle = !g_params.pidControl.isPrintThrottle;
else if (crsfChannels[g_params.trackerControl.yawChannel] < 200)
g_params.pidControl.isPrintThrottle = !g_params.pidControl.isPrintThrottle;
break;
}
}
}
}
cursorIndexCounter++;
// Check if tracker capture channel is changed. This is just an example.
if (crsfChannels[g_params.trackerControl.captureChannel] > g_params.trackerControl.captureChannelTriggerValue)
{
captureCounter++;
uint8_t commandBuffer[32];
int commandSize = 0;
if (g_trackerResultMode.load() == 1 && captureCounter >= 40) // Capture and reset commands can be send min 40 cycle period
{
captureCounter = 0;
if (!g_tracker.executeCommand(cr::vtracker::VTrackerCommand::RESET))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't send tracker command (RESET)." << std::endl;
}
g_isArmed = false;
}
else if (g_trackerResultMode.load() == 0 && captureCounter >= 40)
{
captureCounter = 0;
if (!g_tracker.executeCommand(cr::vtracker::VTrackerCommand::CAPTURE_PERCENTS, 50, 50))
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't send tracker command (CAPTURE)." << std::endl;
}
}
}
// Arming (Auto control). Only if throttle config mode is not enabled.
if (abs(crsfChannels[g_params.trackerControl.armChannel] - armChannelLastValue) > 500)
{
if (crsfChannels[g_params.trackerControl.armChannel] > g_params.trackerControl.armChannelTriggerValue && g_trackerResultMode.load() == 1)
{
g_isArmed = true;
trackingCommandCounter = 0;
}
else if (crsfChannels[g_params.trackerControl.armChannel] < g_params.trackerControl.armChannelTriggerValue && g_isArmed)
{
g_isArmed = false;
}
armChannelLastValue = crsfChannels[g_params.trackerControl.armChannel];
}
// If it is armed and tracker is in tracking mode then control the drone.
if (g_isArmed && g_trackerResultMode.load() == 1)
{
if (trackingCommandCounter >= 75)
{
// Control tracking rectangle with channels.
if (crsfChannels[g_params.trackerControl.rollChannel] < 500)
g_tracker.executeCommand(cr::vtracker::VTrackerCommand::MOVE_RECT, -1, 0);
else if (crsfChannels[g_params.trackerControl.rollChannel] > 1400)
g_tracker.executeCommand(cr::vtracker::VTrackerCommand::MOVE_RECT, 1, 0);
if (crsfChannels[g_params.trackerControl.pitchChannel] < 500)
g_tracker.executeCommand(cr::vtracker::VTrackerCommand::MOVE_RECT, 0, 1);
else if (crsfChannels[g_params.trackerControl.pitchChannel] > 1400)
g_tracker.executeCommand(cr::vtracker::VTrackerCommand::MOVE_RECT, 0, -1);
// Change tracking rectangle size with yaw channel.
if (crsfChannels[g_params.trackerControl.yawChannel] < 500)
g_tracker.executeCommand(cr::vtracker::VTrackerCommand::CHANGE_RECT_SIZE, -1, -1);
else if (crsfChannels[g_params.trackerControl.yawChannel] > 1500)
g_tracker.executeCommand(cr::vtracker::VTrackerCommand::CHANGE_RECT_SIZE, 1, 1);
}
else
{
trackingCommandCounter++;
}
// Yaw control.
if (g_pidYaw.isEnabled)
{
int yawSetPoint = (g_outputWidth / 2);
int yawInput = g_trackerResultX.load();
int yawControl = g_pidYaw.compute(yawInput, yawSetPoint);
yawControl = clamp(yawControl, -500, 500); // May be removed in future by testing.
crsfChannels[g_params.trackerControl.yawChannel] = g_pidYaw.center + yawControl;
crsfChannels[g_params.trackerControl.yawChannel] = clamp(crsfChannels[g_params.trackerControl.yawChannel], g_pidYaw.min, g_pidYaw.max);
}
// Throttle control.
if (g_pidThrottle.isEnabled)
{
int throttleSetPoint = (g_outputHeight / 2);
int throttleInput = g_trackerResultY.load();
int throttleControl = g_pidThrottle.compute(throttleInput, throttleSetPoint);
throttleControl = clamp(throttleControl, -400, 400); // May be removed in future by testing.
crsfChannels[g_params.trackerControl.throttleChannel] = g_pidThrottle.center - throttleControl;
crsfChannels[g_params.trackerControl.throttleChannel] = clamp(crsfChannels[g_params.trackerControl.throttleChannel], g_pidThrottle.min, g_pidThrottle.max);
}
// Roll control.
if (g_pidRoll.isEnabled)
{
int rollSetPoint = (g_outputWidth / 2);
int rollInput = g_trackerResultX.load();
int rollControl = g_pidRoll.compute(rollInput, rollSetPoint);
rollControl = clamp(rollControl, -500, 500); // May be removed in future by testing.
crsfChannels[g_params.trackerControl.rollChannel] = g_pidRoll.center + rollControl;
crsfChannels[g_params.trackerControl.rollChannel] = clamp(crsfChannels[g_params.trackerControl.rollChannel], g_pidRoll.min, g_pidRoll.max);
}
// Constant pitch control.
if (g_pidPitch.isEnabled)
crsfChannels[g_params.trackerControl.pitchChannel] = g_pidPitch.center;
}
g_throttleValue = crsfChannels[g_params.trackerControl.throttleChannel];
// Encoder RC channels.
parser.encodeRcChannels(crsfChannels, packet, size);
// Write data to serial port.
if (serialPort.write(packet, size) != size)
{
g_log.print(PrintColor::RED, g_logFlag) <<
"[" << __LOGFILENAME__ << "][" << __LINE__ << "] ERROR: " <<
"Can't write data to serial port." << std::endl;
}
}
}
Betaflight related information
Zero2WFpv application is tested with AT32 and STM32 flight controllers with Betaflight 4.3 and 4.4 versions. Zero2wFpv works only with ANGLE and HORIZON modes, ACRO mode is not supported.
NOTE: Betaflight antigravity feature should be configured properly for the drone. Throttle controller may activate antigravity feature in Betaflight. If drone configuration is not done properly, antigravity may make drone unstable under throttle controll. This is especially important for the drones with high power motors and heavy payloads.
Licensing mechanism
Zero2WFpv has an integrated licensing mechanism to protect it to be copied it to different hardware. Current licensing mechanism uses following hardware dependent parameters to validate the license:
- Unique ID of Raspberry Pi
- UUID of SD card
- Serial number of SD card
If there is no license.txt file in same directory of Zero2WFpv, it will create a token.txt file based on these hardware dependent parameters. And license.txt file can be obtained by using License library from the token that is in token.txt. License file should be placed in same directory of Zero2WFpv executable.
Overlay examples
Zero2WFpv can be in mainly 3 different states. These are:
-
Free mode: In this mode, tracker is waiting capture command from the center of the screen. Overlay rectangle corners are smaller than the rectangle in tracking mode. This mode is used to capture the object to be tracked.
-
Tracking mode: In this mode, tracker is tracking the object. Overlay rectangle corners are bigger than the rectangle in free mode. This state indicates that Zero2WFpv is ready to start auto control towards the object.
-
Auto control mode: In this mode, Zero2WFpv is controlling the drone towards the object. Overlay rectangle corners are same as the rectangle in tracking mode but circle in the center indicates the drones current heading.
Following image shows the overlay examples for these 3 different states with same order.