Scaffolding
Navigate to the root of the VCA Drivers repository. Create the skeleton of the new camera connector. Use the scaffolding script:
./scaffolding.sh NumbersWithFileLogger
After running the script, the root files CMakeLists.txt and CMakePresets.json should be updated with the new configuration. The folder ./drivers/numberswithfilelogger should be created with the following structure:
drivers/
numberswithfilelogger/ - Parent folder of the new camera connector
include/ - Copy camera vendor provided C++ header files here
Place 3rd party include files here.txt
lib/ - Copy camera vendor provided library files here
Place 3rd party libs here.txt
src/
NumbersWithFileLoggerApi.cpp
NumbersWithFileLoggerApi.h - Contains the exposed functions used by VCA
NumbersWithFileLoggerCamera.cpp - Contains the main logic for the camera
NumbersWithFileLoggerCamera.h
NumbersWithFileLoggerConnector.cpp - Contains the main logic for camera discovery and camera creation
NumbersWithFileLoggerConnector.h
NumbersWithFileLoggerPluginLogger.h - Contains this camera connectors specific logging macros and the logger instance
tests/
CMakeLists.txt - CMakeLists.txt file with the test configuration
testMain.cpp - Entrypoint for the gtest based tests
build.sh - Build script for building and installing the release version this camera connector
CMakeLists.txt - Main CMakeLists.txt file of the camera connector
Preparing the 3rd-party dependencies
In this step you copy all the header files provided by the camera vendor to the "include" folder and the library file to the "lib" folder. The header files are included automatically, but the camera connector file CMakeLists.txt must be manually modified to link the necessary 3rd-party libraries.
In our example, you don’t use these two folders. The CMakeLists.txt file must be updated so that the camera connector can use the OpenCV. Add the following lines:
```
find_package(OpenCV REQUIRED)
target_link_libraries(${LIBRARY_NAME} PRIVATE opencv_core opencv_videoio ${LIBS})
```
The modified CMakeLists.txt file looks like this in the affected areas:
```
...
include_directories(${SDK_INCLUDE_DIR})
link_directories(${SDK_LIBRARY_DIR})
include_directories(${PROJECT_SOURCE_DIR}/include)
link_directories(${PROJECT_SOURCE_DIR}/lib)
find_package(OpenCV REQUIRED)
if(TESTING STREQUAL "ON")
add_subdirectory(tests)
endif()
...
```
```
...
add_library(${LIBRARY_NAME} SHARED ${PROJECT_SOURCES})
target_link_libraries(${LIBRARY_NAME} PRIVATE opencv_core opencv_videoio ${LIBS})
target_compile_options(${LIBRARY_NAME} PRIVATE -fno-gnu-unique)
install(TARGETS ${LIBRARY_NAME} DESTINATION ${LIBRARY_NAME})
...
```
For more information on structuring the CMakeLists.txt files, see the examples.
Configure and build a new custom camera connector
After scaffolding, new configuration and build presets are available. To continue developing the camera connector, you must configure a new project. This can be done from the IDE or from the terminal by running one of these commands:
```
cmake --preset numberswithfilelogger-debug
```
or
```
cmake --preset numberswithfilelogger-release
```
depending on what build type is required.
Although the current NumbersWithFileLogger camera connector does not contain any camera-related implementation, you can still build the project thanks to the scaffolding. Again, you can do this from the IDE or from the terminal by running one of these commands:
```
cmake --build --preset numberswithfilelogger-debug-build --target numberswithfilelogger
```
or
```
cmake --build --preset numberswithfilelogger-release-build --target numberswithfilelogger
```
depending on the previously selected configuration. After that, the file libnumberswithfilelogger.so should be generated in the output directory.
Implementing NumbersWithFileLoggerConnector class
The CameraConnector class is responsible for discovering and creating cameras. The class derived from NumbersWithFileLoggerConnector must contain the camera-specific logic for discovering and creating.
In this example, you will modify the discover method so that it returns a list of colors as unique IDs for the possible cameras.
Modified code:
```
std::vector<std::string> NumbersWithFileLoggerConnector::discover() const
{
return {"red", "green", "blue", "black"};
}
```
After loading the finalized camera connector in VCA, this list will appear in the UI:
Implementing NumbersWithFileLoggerCamera class
Compared to the CameraConnector, the Camera class is more complex due to its overall functioning. Therefore, first define a member variable so that you can track the status of the camera.
Updated NumbersWithFileLoggerCamera.h:
```
private:
int m_width = 1920;
int m_height = 1080;
cv::Scalar m_backgroundColor;
double m_fontScale = 1.0;
int m_thickness = 2;
int m_baseline = 0;
int m_fps = 1;
```
With these variables, you define a default state for the cameras and can change the behavior of the cameras at runtime.
Update the file NumbersWithFileLoggerCamera.cpp method by method.
Update the constructor NumbersWithFileLoggerCamera.cpp to set the background color defined by the unique ID:
```
NumbersWithFileLoggerCamera::NumbersWithFileLoggerCamera(const std::string& uniqueId) : Camera(uniqueId)
{
if (uniqueId == "red")
{
m_backgroundColor = cv::Scalar(0, 0, 255);
}
else if (uniqueId == "green")
{
m_backgroundColor = cv::Scalar(0, 255, 0);
}
else if (uniqueId == "blue")
{
m_backgroundColor = cv::Scalar(255, 0, 0);
}
else
{
m_backgroundColor = cv::Scalar(0, 0, 0);
}
}
```
Now you have all the variable sets you need to generate and acquire images. The updated acquireImage method looks like this:
```
std::shared_ptr<VCA::SDK::v1::Image> NumbersWithFileLoggerCamera::acquireImage()
{
increaseImageSequenceCounter();
int fontFace = cv::FONT_HERSHEY_SIMPLEX;
cv::Mat rawImage(m_height, m_width, CV_8UC3, m_backgroundColor);
std::string number = std::to_string(imageSequenceCounter());
cv::Size textSize = cv::getTextSize(number, fontFace, m_fontScale, m_thickness, &m_baseline);
int x = (rawImage.cols - textSize.width) / 2;
int y = (rawImage.rows + textSize.height) / 2;
cv::putText(rawImage, number, cv::Point(x, y), fontFace, m_fontScale, cv::Scalar(255, 255, 255), m_thickness);
const auto totalElementCount = rawImage.total() * rawImage.elemSize();
auto image = std::make_shared<SDK::v1::Image>();
auto imageData =
std::make_unique<SDK::v1::ImageData> (reinterpret_cast<uint8_t*>(rawImage.data), totalElementCount);
const VCA::SDK::v1::ImageDetail imageDetail(
VCA::SDK::v1::CameraInformation(cameraUniqueId(), "1"),
VCA::SDK::v1::ImageInformation(imageSequenceCounter(), rawImage.cols, rawImage.rows, "BGR8"),
VCA::SDK::v1::PTPInformation("Disabled", "0"));
image->addImagePart(std::move(imageData), imageDetail);
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / m_fps));
return image;
}
```
With the exception of ```increaseImageSequenceCounter()```, which you use to update the displayed counter up until ```auto image = std::make_shared<SDK::v1::Image>();```, everything is OpenCV-specific code. This part of the code varies from one camera vendor API to another.
The important part is creating ```ImageData``` and ```ImageDetail``` objects and adding them to the ```Image``` as an image part.
The ```ImageData``` constructor uses two parameters, one is the actual image buffer and the second is the size of the buffer. In the ```ImageData``` constructor the buffer is copied, and the copied buffer is managed internally.
You also need an ```ImageDetail``` object to add the image part to the ```Image```. One ```ImageDetail``` object contains the crucial information about the given image frame. Such information includes ```CameraInformation```, ```ImageInformation``` and ```PTPInformation```.
The ```CameraInformation``` contains the unique camera ID and the stream ID.
The ```ImageInformation``` contains the frame count, the width and height of the frame, and the pixel format. Although not every camera or use case supports the Precision Time Protocol it is necessary to provide the ```PTPInformation``` for the ```ImageDetail``` object. In this example PTP is not used, so the "Disabled" state with the value 0 is used.
As you have both ```ImageData``` and ```ImageDetail``` objects, you can add them to the ```Image``` and return it when it has been successfully acquired.
With the current implementation, the NumbersWithFileLoggerCamera can now return images, but only with the same default configuration. You must modify the ```getConfig``` and ```setConfig``` methods if you want to change the camera and the acquired images.
The ```getConfig``` method needs to return the list of parameters supported by the camera, such as FPS, width, height etc. The parameter list varies from camera to camera, even for the same brand. With getConfig, the VCA UI can dynamically display the available configuration options of the camera.
The ```setConfig``` command needs to handle the configuration changes and return a list of the changed configuration entries, in our case variables and their change status.
The implementation of ```getConfig``` and ```setConfig``` is as follows:
```
SDK::v1::CameraParameters NumbersWithFileLoggerCamera::getConfig()
{
SDK::v1::CameraParameters cameraParameters;
SDK::v1::CameraParameter widthCameraParameter;
widthCameraParameter.setName("Width");
widthCameraParameter.setValue(std::to_string(m_width));
widthCameraParameter.setType("int");
widthCameraParameter.setDescription("Width");
widthCameraParameter.setReadOnly(false);
SDK::v1::CameraParameter heightCameraParameter;
heightCameraParameter.setName("Height");
heightCameraParameter.setValue(std::to_string(m_height));
heightCameraParameter.setType("int");
heightCameraParameter.setDescription("Height");
heightCameraParameter.setReadOnly(false);
SDK::v1::CameraParameter fontScaleCameraParameter;
fontScaleCameraParameter.setName("Font Scale");
fontScaleCameraParameter.setValue(std::to_string(m_fontScale));
fontScaleCameraParameter.setType("double");
fontScaleCameraParameter.setDescription("Font Scale");
fontScaleCameraParameter.setReadOnly(false);
SDK::v1::CameraParameter thicknessCameraParameter;
thicknessCameraParameter.setName("Thickness");
thicknessCameraParameter.setValue(std::to_string(m_thickness));
thicknessCameraParameter.setType("int");
thicknessCameraParameter.setDescription("Thickness");
thicknessCameraParameter.setReadOnly(false);
SDK::v1::CameraParameter fpsCameraParameter;
fpsCameraParameter.setName("FPS");
fpsCameraParameter.setValue(std::to_string(m_fps));
fpsCameraParameter.setType("int");
fpsCameraParameter.setDescription("FPS");
fpsCameraParameter.setMin("1");
fpsCameraParameter.setMax("1000");
fpsCameraParameter.setReadOnly(false);
cameraParameters.push_back(widthCameraParameter);
cameraParameters.push_back(heightCameraParameter);
cameraParameters.push_back(fontScaleCameraParameter);
cameraParameters.push_back(thicknessCameraParameter);
cameraParameters.push_back(fpsCameraParameter);
return cameraParameters;
}
```
```
SDK::v1::CameraParameterStatuses NumbersWithFileLoggerCamera::setConfig(const SDK::v1::CameraParameters& parametersToChange){ SDK::v1::CameraParameterStatuses parameterStatuses; for (const auto& parameterToChange : parametersToChange) { const auto parameterName = parameterToChange.name(); const auto parameterValue = parameterToChange.value(); if (parameterName == "Width") { m_width = std::stoi(parameterValue); } else if (parameterName == "Height") { m_height = std::stoi(parameterValue); } else if (parameterName == "Font Scale") { m_fontScale = std::stod(parameterValue); } else if (parameterName == "Thickness") { m_thickness = std::stoi(parameterValue); } else if (parameterName == "FPS") { m_fps = std::stoi(parameterValue); } parameterStatuses.push_back(SDK::v1::CameraParameterStatus (parameterName, parameterValue, SDK::v1::CameraParameterStatus:: Status::OK)); } return parameterStatuses;} ```
Now you can change the camera configuration from the VCA UI while using the camera.
Adding an internal logger
To add an internal file logger, you need to declare a new ```logToFile``` function in ```NumbersWithFileLoggerPluginLogger.h``` and define it in ```NumbersWithFileLoggerPluginLogger.cpp```.
Add the following to ```NumbersWithFileLoggerPluginLogger.h```
```
void logToFile(VCA::SDK::v1::PluginLogLevel level, const std::string& msg, const std::string& file, int line);
```
and to the ```NumbersWithFileLoggerPluginLogger.cpp```
```
std::mutex file_mutex;
void logToFile([[maybe_unused]] VCA::SDK::v1::PluginLogLevel level, const std::string& msg, [[maybe_unused]] const std::string& file, [[maybe_unused]] int line)
{
try
{
const std::lock_guard<std::mutex> lock(file_mutex);
const auto filePath = "/logs/numbers_with_file_logger.txt";
fs::create_directories(fs::path(filePath).parent_path());
std::ofstream file(filePath, std::ios::app);
if (!file)
{
throw std::ios_base::failure("Failed to open log file");
}
file << msg << std::endl;
}
catch (const std::exception& ex)
{
std::cout << "Error: " << ex.what() << std::endl;
}
}
```
Register this function with the logger. Use the constructor of the '```NumbersWithFileLoggerConnector``` class. As the connector is instantiated only once by VCA itself, it is safe to use this constructor to initialize resources or, in our case, to register the internal logger.
Updating the constructor NumbersWithFileLoggerConnector:
```
#include "NumbersWithFileLoggerPluginLogger.h"
...
NumbersWithFileLoggerConnector::NumbersWithFileLoggerConnector()
{
logger.registerCallback("NumbersWithFileLogger Logger", logToFile);
LOG_NUMBERSWITHFILELOGGER_PLUGIN_INFO("Camera connector initialized");
}
```