This document aims at learning user to use the Toradex OakLib programming library to communicate with their Toradex Oak sensors under Windows.
To complete this tutorial, you will need:
Before we start, Make sure that you have installed Microsoft Visual C++ Express 2008. We will use this version because it is a free version of the Microsoft Visual Studio Development environment, which is the most widespread development environment for Windows. We will spend most of our time in Visual C++ Express, so if you have never used it, it is probably a good idea to start by learning how to use this environment.
The free Express version of Visual C++ has some limitation, but they are not problematic for this tutorial.
In the first part of the tutorial, we will create a new Visual C++ project, set up the OakLib library, and write the simplest Oak useful Oak application possible: a command-line program that reads the state of the device at a regular interval and display the read values.
In the second part, we will have a look at the other features of the Oak sensors, like setting the device and channel names, changing the sampling rate, modifying the led blink mode, etc...
Launch Visual C++ Express, then in the application menu, select File → New → Project... Fill the dialog that appears as in the screenshot below:
Note: you can select a different location that suits you better. In this tutorial, we will assume that you selected C:\Code.
Once you're done, click the OK button. A new dialog will appear, click Next, then fill the next dialog according to the following screenshot:
Once done, click Finish. You now have created a new empty project. It is time to add a source file to this project. In the Solution Explorer, right click on the OakTutorial project, then select Add → New Item.
Fill the dialog that appears as indicated in the screenshot below.
Once done, click the Add button.
To test that the setup is correct, paste the following code into main.cpp
#include <cstdio> using namespace std; int main() { printf("OakTutorial\n"); return 0; }
Compile the code ( Build → Build Solution ) and execute it ( Debug → Start without Debugging ) . If everything is OK, you should see the following:
Extract the Oak library archive to your computer. From the decompressed archive,
Before we can use the library, we still need to do two things:
The first function that we need to use is the one that 'finds' a sensor according to a series of criteria. The function declaration is
bool Oak_FindSensor(WORD PID, WORD REV, LPTSTR SN, LPTSTR DeviceName, LPTSTR ChannelName, LPTSTR UserDeviceName, LPTSTR UserChannelName, PtOakSensor SensorFound);
The 7 first parameters are the search criteria: USB PID (each sensor type has a fixed USB product ID), revision ID, serial number, Device Name, User Device Name, Channel Name, user channel name (we will discuss channel and device names later in this tutorial). If you do not want to use a search criterion, just set its value to 0. The last parameter, SensorFound, is a pointer to a structure tOakSensor that is defined in the oak.h header file. If a sensor matching the search criteria is found, the function will return true, and SensorFound will be filled with information about the sensor. If no matching sensor was found, the function will return false, and the content of SensorFound will be undetermined.
In our case, we want to find an Oak Pressure sensor, whose PID is 0x2. If you are following this tutorial with a different sensor, use the PID of your sensor. Let's modify main.cpp and finally start writing something useful:
#include "Oak.h" #include <stdexcept> #include <cstdio> #include <cassert> using namespace std; //********************************************************************************************************************** /// \brief Program entry point //********************************************************************************************************************** int main() { try { // Try to open a Oak P sensor (USB PID 0x2) tOakSensor sensor; if (!Oak_FindSensor(0x02, 00, 00, 00, 00, 00, 00, &sensor)) throw std::runtime_error("No Oak P sensor found"); // Display some informations about the sensor printf("Found a Oak P sensor\nUser Device Name: %s\nSerial Number: %s\n", sensor.UserDeviceName, sensor.SN) ; } catch (std::runtime_error const& e) { fprintf(stderr, "%s\n", e.what()); return 1; } return 0; }
This simple program use Oak_FindSensor() to locate a Oak P sensor, and if it finds one, it display some of the information available in the sensor structure.
Each Oak sensor has a set of parameters that can be read / written in Flash (persistent memory) or RAM (volatile memory). Some of these parameters are specific to a type of Oak sensors, while others are available on all types of Oak devices, like the LED mode, sample and report rates, etc...
To set / get the values of parameters, we will use USB Feature reports, and the Oak_Feature() function of the OakLib library. The Oak sensors datasheets contains a list of Feature Reports available for each sensor. As an example, let's write a function that cycles the LED mode (the way the red LED included on the device blinks). This implies two operations. We will first read the current LED mode, then change its value. Here is a copy of the description of the "LED Mode" Feature report in the Oak P datasheet:
The first byte (byte 0 - GNS) of the feature report indicates whether we want to set or get the value of the LED Mode. The second byte (Tgt, for target) indicates whether you want to set / get the parameter in the RAM or in FLASH memory. If you set the parameters in RAM, it will only be used until your device is powered down (by unplugging it or shutting down your computer). When you plug you device again, the LED mode will be reverted to the value that is stored in Flash memory. So if you want the setting to persist, set the target value to Flash. The 6th byte allows you to specify the LED Mode you want, and of course, it is not used when you set the value of GNS to Get.
The OakLib function for sending feature reports is
bool Oak_Feature(LPCTSTR DevicePath, BYTE RptBuf[33], bool ExpectResult);
One important note regarding the RptBuffer: the first byte of the report must be set to zero, which is the report number. The actual feature report should start at RptBuffer[1].
If you are expecting a reply, the content of the reply will be in the RptBuffer parameter upon function exit (provided the function completed successfully and returned 'true'). The actual data is starts at RptBuffer[2], as RptBuffer[0] is the report number and RptBuffer[1] is an operation code that is set of 0xff if the operation was successful. If you are expecting a multi-byte reply, the format is little-endian, meaning the least significant byte have the lowest index in the buffer.
With all this information, we can now write two functions for reading and writing the LED mode
//********************************************************************************************************************** /// \brief Set the LED mode for the given sensor /// /// \param[in] devicePath The path to the device /// \param[in] ledMode The new value for the LED mode. Refer to the device data sheet for the list of LED modes /// \param[in] persistent If set to true, the LED mode will be stored in FLASH, otherwise it will be stored in RAM /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool setLedMode(char* devicePath, unsigned char ledMode, bool persistent) { assert(ledMode < 5); unsigned char buffer[33] = { 0x00 /* report number */, 0x00 /* Set */, persistent ? 0x01 : 0x00, 0x01, 0x01, 0x00, ledMode}; return Oak_Feature(devicePath, buffer, false); } //********************************************************************************************************************** /// \brief Set the LED mode for the given sensor /// /// \param[in] devicePath The path to the device /// \param[out] outLedMode Upon function exit, if the function returns true this variable hold the retrieved value of the /// LED mode /// \param[in] persistent If set to true, the LED mode will be retrieved FLASH, otherwise it will be retrieved from RAM /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool getLedMode(char* devicePath, unsigned char& outLedMode, bool persistent) { unsigned char buffer[33] = { 0x00 /* report number */, 0x01 /* Get */, persistent ? 0x01 : 0x00, 0x01, 0x01, 0x00 }; if (!Oak_Feature(devicePath, buffer, true)) return false; outLedMode = buffer[2]; return true; }
Let's also modify our main function so that at every launch of the application, the LED mode is changed to the next one available:
//********************************************************************************************************************** /// \brief Program entry point //********************************************************************************************************************** int main() { try { // Try to open a Oak P sensor (USB PID 0x2) tOakSensor sensor; if (!Oak_FindSensor(0x02, 00, 00, 00, 00, 00, 00, &sensor)) throw std::runtime_error("No Oak P sensor found"); // Display some informations about the sensor printf("Found a Oak P sensor\nUser Device Name: %s\nSerial Number: %s\n", sensor.UserDeviceName, sensor.SN) ; unsigned char ledMode; if (!getLedMode(sensor.DevicePath, ledMode, true)) throw std::runtime_error("Error getting LED mode"); // Play a bit with the LED mode printf("Current LED mode: %u\n", ledMode); ledMode = (ledMode + 1) % 5; // advance to the next LED mode (acceptable values are in the range [0..4]) printf("New LED mode: %u\n", ledMode); // we set the LED mode to the next one if (!setLedMode(sensor.DevicePath, (ledMode + 1) % 5, true)) throw std::runtime_error("Error setting LED mode"); } catch (std::runtime_error const& e) { fprintf(stderr, "%s\n", e.what()); return 1; } return 0; }
The Oak USB architecture allows for report rate (the rate at which the device sends a report containing its current status, known in USB lingo as an "Interrupt In" in report. ranging from 1ms to 65535 ms. The lower limit of 1ms is imposed by the USB protocol. The sensor itself has a sampling rate, which can be different from the reporting rate. The limits of the sampling rate depend on the type of sensor and can be found in each device's datasheet.
These rates can be adjusted using three feature reports:
If you are unsure, use 'After Sampling'.
Each of these feature report are documented in the sensor's datasheet, and all sensors types use the same report, so it is easy to write functions for setting/getting the sample rate, the report rate and the report mode.
//********************************************************************************************************************** /// \brief Get the report rate of the given sensor /// /// \param[in] devicePath The path to the device /// \param[out] outReportRate Upon function exit, if the function returns true this variable hold the retrieved value /// of the report rate /// \param[in] persistent If set to true, the report rate will be retrieved FLASH, otherwise it will be retrieved from /// RAM /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool getReportRate(char* devicePath, unsigned short& outReportRate, bool persistent) { unsigned char buffer[33] = { 0x00 /* report number */, 0x01 /* Get */, persistent ? 0x01 : 0x00, 0x02, 0x00, 0x00 }; if (!Oak_Feature(devicePath, buffer, true)) return false; outReportRate = (unsigned short)(buffer[2]) | ((unsigned short)(buffer[3]) << 8); return true; } //********************************************************************************************************************** /// \brief Set the report rate for the given sensor /// /// \param[in] devicePath The path to the device /// \param[in] reportRate The new value for the report mode, in milliseconds /// \param[in] persistent If set to true, the report rate will be stored in FLASH, otherwise it will be stored in RAM /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool setReportRate(char* devicePath, unsigned short reportRate, bool persistent) { unsigned char buffer[33] = { 0x00 /* report number */, 0x00 /* Set */, persistent ? 0x01 : 0x00, 0x02, 0x00, 0x00, (unsigned char)(reportRate), (unsigned char)(reportRate >> 8)}; return Oak_Feature(devicePath, buffer, false); } //********************************************************************************************************************** /// \brief Get the sample rate of the given sensor /// /// \param[in] devicePath The path to the device /// \param[out] outSampleRate Upon function exit, if the function returns true this variable hold the retrieved value /// of the sample rate /// \param[in] persistent If set to true, the sample rate will be retrieved FLASH, otherwise it will be retrieved from /// RAM /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool getSampleRate(char* devicePath, unsigned short& outSampleRate, bool persistent) { unsigned char buffer[33] = { 0x00 /* report number */, 0x01 /* Get */, persistent ? 0x01 : 0x00, 0x02, 0x01, 0x00 }; if (!Oak_Feature(devicePath, buffer, true)) return false; outSampleRate = (unsigned short)(buffer[2]) | ((unsigned short)(buffer[3]) << 8); return true; } //********************************************************************************************************************** /// \brief Set the sample rate for the given sensor /// /// \param[in] devicePath The path to the device /// \param[in] sampleRate The new value for the report mode, in milliseconds /// \param[in] persistent If set to true, the sample rate will be stored in FLASH, otherwise it will be stored in RAM /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool setSampleRate(char* devicePath, unsigned short sampleRate, bool persistent) { unsigned char buffer[33] = { 0x00 /* report number */, 0x00 /* Set */, persistent ? 0x01 : 0x00, 0x02, 0x01, 0x00, (unsigned char)(sampleRate), (unsigned char)(sampleRate >> 8)}; return Oak_Feature(devicePath, buffer, false); } //********************************************************************************************************************** /// \brief Get the report mode of the given sensor /// /// \param[in] devicePath The path to the device /// \param[out] outReportMode Upon function exit, if the function returns true this variable hold the retrieved value /// of the report mode /// \param[in] persistent If set to true, the report mode will be retrieved FLASH, otherwise it will be retrieved from /// RAM /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool getReportMode(char* devicePath, unsigned char& outReportMode, bool persistent) { unsigned char buffer[33] = { 0x00 /* report number */, 0x01 /* Get */, persistent ? 0x01 : 0x00, 0x01, 0x00, 0x00 }; if (!Oak_Feature(devicePath, buffer, true)) return false; outReportMode = buffer[2]; return true; } //********************************************************************************************************************** /// \brief Set the report mode for the given sensor /// /// \param[in] devicePath The path to the device /// \param[in] reportMode The new value for the report mode, in milliseconds /// \param[in] persistent If set to true, the report mode will be stored in FLASH, otherwise it will be stored in RAM /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool setReportMode(char* devicePath, unsigned char reportMode, bool persistent) { assert(reportMode <= 2); unsigned char buffer[33] = { 0x00 /* report number */, 0x00 /* Set */, persistent ? 0x01 : 0x00, 0x01, 0x00, 0x00, reportMode }; return Oak_Feature(devicePath, buffer, false); }
Let's now add some code to our main function to set the report mode to 'After Sampling' and the sample rate to 100ms:
//********************************************************************************************************************** /// \brief Program entry point //********************************************************************************************************************** int main() { try { // Try to open a Oak P sensor (USB PID 0x2) tOakSensor sensor; if (!Oak_FindSensor(0x02, 00, 00, 00, 00, 00, 00, &sensor)) throw std::runtime_error("No Oak P sensor found"); // Display some informations about the sensor printf("Found a Oak P sensor\nUser Device Name: %s\nSerial Number: %s\n", sensor.UserDeviceName, sensor.SN) ; unsigned char ledMode; if (!getLedMode(sensor.DevicePath, ledMode, true)) throw std::runtime_error("Error getting LED mode"); // Play a bit with the LED mode printf("Current LED mode: %u\n", ledMode); ledMode = (ledMode + 1) % 5; // advance to the next LED mode (acceptable values are in the range [0..4]) printf("New LED mode: %u\n", ledMode); // we set the LED mode to the next one if (!setLedMode(sensor.DevicePath, (ledMode + 1) % 5, true)) throw std::runtime_error("Error setting LED mode"); // Set the report mode to 'After Sampling' if (!setReportMode(sensor.DevicePath, 0, true)) throw std::runtime_error("Error setting the report mode"); // Set the sample rate to 100ms if (!setSampleRate(sensor.DevicePath, 100, true)) throw std::runtime_error("Error setting sample rate"); } catch (std::runtime_error const& e) { fprintf(stderr, "%s\n", e.what()); return 1; } return 0; }
There are many other feature reports available, some are common to all Oak sensors, and some are specific to a certain type of sensors. Using the example we have just studied and your device datasheet, you should have no problems using these feature reports.
It's now time to read the actual sensor state! Sensor state is send at the rate specified by the setting with have just modified (every 100ms), using a report called Interrupt In report. There are two options for reading the interrupt report: Oak_GetInReport() or Oak_GetCurrentInReport().
This is the classic method. The function's prototype is:
bool Oak_GetInReport(LPCTSTR DevicePath, BYTE RptBuf[33], BYTE InReportLength, WORD Timeout_ms);
This function is blocking and will not return until the next Interrupt In report is received (or until the specified timeout has elpased).
As you see the execution time of this function is dependent on the sample and report rate selected, and may not be appropriate for GUI application unless you have several threads running.
This variant is only available on Windows XP and more recent flavors of Windows. The function's prototype is:
bool Oak_GetCurrentInReport(LPCTSTR DevicePath, BYTE RptBuf[33], BYTE InReportLength);
The 3 parameters have the exact same meaning as for the Oak_GetInReport(), and the only difference is that this function is non blocking (hence the absence of the timeout parameter): it returns immediately the last report that was received.
Picking one variant or the other really depends on the needs of your application.
The content of the Interrupt In report depends on the sensor type. Each sensor datasheet list the content of the Interrupt In buffer. For instance, for the Oak P sensor the description is the following:
With this we can easily write a function that read the Oak P Interrupt In report. Note that as usual, the report data starts at index 1, as the byte at index 0 is the report number.
//********************************************************************************************************************** /// \brief Read the state of the Oak distance sensor. This function waits until the next Interrupt In report arrive /// (delay depends on the sample rate, report mode and report rate), then parse the report, according the the /// format specified for the report in the device's datasheet, chapter 3.1. /// /// \param[in] devicePath The path to the device /// \param[out] outFrameNumber Upon function exit, if the function returns true this variable hold the frame number at /// which the sampling was performed (the frame number is time stamp in milliseconds, internally coded on 11-bits, so /// it wraps around every 2048 milliseconds. /// \param[out] outPressure Upon function exit, if the function returns true this variable hold the pressure read by the /// sensor /// \param[out] outTemperature Upon function exit, if the function returns true this variable hold the temperature read /// by the sensor /// \return true if and only if the operation completed successfully //********************************************************************************************************************** bool readPSensorState(char* devicePath, unsigned short& outFrameNumber, unsigned short& outPressure, unsigned short& outTemperature) { unsigned char buffer[33]; if (!Oak_GetInReport(devicePath, buffer, 33, 65535)) return false; // buffer[0] is the report number (0), actual data starts at buffer[1] outFrameNumber = (unsigned short)buffer[1] | (((unsigned short)(buffer[2])) << 8); outPressure = (unsigned short)buffer[3] | (((unsigned short)(buffer[4])) << 8); outTemperature = (unsigned short)buffer[5] | (((unsigned short)(buffer[6])) << 8); return true; }
Finally, we add some printout of the read value in our main() function:
//********************************************************************************************************************** /// \brief Program entry point //********************************************************************************************************************** int main() { try { // Try to open a Oak P sensor (USB PID 0x2) tOakSensor sensor; if (!Oak_FindSensor(0x02, 00, 00, 00, 00, 00, 00, &sensor)) throw std::runtime_error("No Oak P sensor found"); // Display some informations about the sensor printf("Found a Oak P sensor\nUser Device Name: %s\nSerial Number: %s\n", sensor.UserDeviceName, sensor.SN) ; unsigned char ledMode; if (!getLedMode(sensor.DevicePath, ledMode, true)) throw std::runtime_error("Error getting LED mode"); // Play a bit with the LED mode printf("Current LED mode: %u\n", ledMode); ledMode = (ledMode + 1) % 5; // advance to the next LED mode (acceptable values are in the range [0..4]) printf("New LED mode: %u\n", ledMode); // we set the LED mode to the next one if (!setLedMode(sensor.DevicePath, (ledMode + 1) % 5, true)) throw std::runtime_error("Error setting LED mode"); // Set the report mode to 'After Sampling' if (!setReportMode(sensor.DevicePath, 0, true)) throw std::runtime_error("Error setting the report mode"); // Set the sample rate to 100ms if (!setSampleRate(sensor.DevicePath, 100, true)) throw std::runtime_error("Error setting sample rate"); // now enter a loop where we read the device's state using the Interrupt reports arrive at the rate specified above while (GetAsyncKeyState(VK_ESCAPE) >= 0) // press the Escape key to end the program { unsigned short frameNumber(0), pressure(0), temperature(0); if (!readPSensorState(sensor.DevicePath, frameNumber, pressure, temperature)) throw std::runtime_error("Error reading sensor state"); // print the values, carefully taking care of the units specified in the datasheet. printf("Frame number : %5.3fs - Pressure: %6uPa - Temperature: %5.1fK\r", float(frameNumber) * 0.001f, pressure * 10, float(temperature)/ 10.0f ); } } catch (std::runtime_error const& e) { fprintf(stderr, "%s\n", e.what()); return 1; } return 0; }
This conclude this tutorial. You can download the complete solution folder for Visual Studio 2008.