Device Connection
Library Setup
As described in Driver Isolation Framework and and Driver Directory Structure drivers are loaded in a separate process and are searched for in a directory the user must specify.
If the driver isolation executable is not found at its default path, its
actual path may be set via the
fluxEngine::Handle::setDriverIsolationExecutable()
method:
1 try {
2 // Windows: use Unicode (wide) filenames
3 handle.setDriverIsolationExecutable(L"C:\\Path\\To\\fluxDriverIsolation.exe");
4 // All other operating systems (8bit filenames)
5 handle.setDriverIsolationExecutable("/path/to/fluxDriverIsolation");
6 } catch (std::exception& e) {
7 std::cerr << "An error occurred: " << e.what() << std::endl;
8 exit(1);
9 }
In the C++ API the driver directory may be set via the
fluxEngine::Handle::setDriverBaseDirectory()
method:
1 try {
2 // Windows: use Unicode (wide) directory names
3 handle.setDriverBaseDirectory(L"C:\\Path\\To\\drivers");
4 // All other operating systems (8bit filenames)
5 handle.setDriverBaseDirectory("/path/to/drivers");
6 } catch (std::exception& e) {
7 std::cerr << "An error occurred: " << e.what() << std::endl;
8 exit(1);
9 }
Note that on Windows systems it is also possible to specify 8bit filenames, but they must be enocded in the local code page, which may not be able to represent all possible names. It is never possible to specify wide filenames on non-Windows operating systems as the concept doesn’t exist there, and Unicode characters will typically be encoded as UTF-8 there.
Enumerating Devices
Before device connection is possible the user must enumerate devices
and drivers. This can be done with the
enumerateDevices()
function:
1 try {
2 using fluxEngine::EnumerationResult;
3 using fluxEngine::enumerateDevices;
4 int driverType = -1; // all supported types
5 std::chrono::seconds timeout{1};
6 EnumerationResult enumeratedDevices = enumerateDevices(handle, type, timeout);
7 } catch (std::exception& e) {
8 std::cerr << "An error occurred: " << e.what() << std::endl;
9 exit(1);
10 }
The user may select the driver type to enumerate. There are currently three options:
Instrument devices: cameras, spectrometers, and such. Specify
fluxEngine::DriverType::Instrument
as the device type to enumerateLight control devices: specify
fluxEngine::DriverType::LightControl
as the device type to enumerateAll support devices types: in that case the user should specify
-1
to find devices of all driver types.
The user must also specify a timeout for the enumeration process. The enumeration will take exactly that long and then return all devices and drivers that could be found within that time.
Note
For cameras with a GigE Vision interface a timeout larger than three seconds (recommended: at least four seconds) is required, as the standard specifies that as the timeout for device discovery, and the enumeration process does need to load the driver, so using exactly three seconds is likely not enough.
Error Reporting during Enumeration
If an exception is thrown during the call to
enumerateDevices()
this indicates that the enumeration process failed entirely. If an
error occurs with a single driver (for example, because it couldn’t be
loaded because a dependent DLL was not found) the enumeration process
itself will still succeed, but the driver-specific errors will be
reported as part of the enumeration result.
Additionally, some drivers may provide warnings during the enumeration process. For example, if a driver detects that the system configuration is such that it would never find any device, it may issue such a warning.
Accessing these errors and warnings may be done via the
EnumerationResult
structure. The following code would print out all errors and warnings
that were encountered during the enumeration process.
1 for (fluxEngine::EnumerationWarning const& warning
2 : enumeratedDevices.warnings) {
3 if (warning.driver)
4 std::cerr << "Warning for driver " << warning.driver->name << ": "
5 << warning.message << std::endl;
6 else
7 std::cerr << "Warning without a driver: "
8 << warning.message << std::endl;
9 }
10
11 for (fluxEngine::EnumerationError const& error
12 : enumeratedDevices.errors) {
13 if (error.driver)
14 std::cerr << "Error for driver " << error.driver->name << ": "
15 << error.message << std::endl;
16 else
17 std::cerr << "Error without a driver: "
18 << error.message << std::endl;
19 }
Enumerated Drivers and Devices
The enumeration result will also contain all drivers that were enumerated (even if they could not be loaded) as well as the devices that were found.
Each driver that was found has a name, which is given by the name of
the directory the driver was found in. For example, the virtual
PushBroom driver will have the name "VirtualHyperCamera"
. That
information, stored in
EnumeratedDriver::name
,
will always be present for every enumerated driver. Additionally a
state is provided via the
EnumeratedDriver::state
member, that indicates whether the driver was loaded successfully, or
whether it crashed during enumeration, for example.
The lists of drivers and devices are vectors of unique pointers to
allow cross-referencing between them (and cross-referencing the drivers
from the warning and error lists). In the C++ API the user is
responsible for managing the memory in the
EnumerationResult
structure they received, as it contains no opaque pointers, just
information.
The following code will list all drivers that were found during that enumeration process:
1 for (fluxEngine::EnumeratedDriver const& driver
2 : enumeratedDevices.drivers) {
3 std::string state;
4 switch (driver->state) {
5 case fluxEngine::DriverState::Unknown: state = "Unknown"; break;
6 case fluxEngine::DriverState::OK: state = "OK"; break;
7 case fluxEngine::DriverState::LoadTimeout: state = "LoadTimeout"; break;
8 case fluxEngine::DriverState::LoadError: state = "LoadError"; break;
9 case fluxEngine::DriverState::EnumerationError: state = "EnumerationError"; break;
10 case fluxEngine::DriverState::Crashed: state = "Crashed"; break;
11 default: state = "Unknown"; break;
12 }
13 std::string type;
14 switch (driver->type) {
15 case fluxEngine::DriverType::Instrument: type = "Instrument"; break;
16 case fluxEngine::DriverType::LightControl: type = "LightControl"; break;
17 default: type = "Unknown"; break;
18 }
19 std::cout << "Driver " << driver->name << " of type " << type
20 << " has state " << state << " and "
21 << driver->devices.size() << " devices." << std::endl;
22 }
Finally, there are the devices that were found. These may be accessed
either via the
EnumerationResult::devices
member to obtain a list of all devices, or the
EnumeratedDriver::devices
member of a specific driver to obtain only the devices associated with
that driver.
Each enumerated device is stored in a
EnumeratedDevice
structure
that has the following quantities associated with it:
A driver-specific id for this device. This id must be provided in conjunction with the type and name of the driver when attempting to connect to a device.
This id is not stable across system reboots or even unplugging a device and plugging it back in again. It is only considered stable for a short amount of time after enumerating all devices in order to be useful when conecting to it. The contents may also change depending on the driver version used. Never store this id long-term.
Typically the user should always enumerate all devices, find the device they want to connect to in the list of devices, and then use that id for the connection process.
A display name that contains a human-readable device name that may be used when e.g. showing a list of available devices to the user, or when logging a connection attempt.
The manufacturer of the device.
The model name of the device.
Optionally: a serial number of the device. Some devices do not have serial numbers, in which case this will always be empty for such devices. But other devices may indeed have a serial number, but that number is not accessible during enumeration, perhaps because obtaining it requires connecting to the device. In that case this might also be empty, even if the device has a serial number.
It is guaranteed though that if this is present this will contain the name string as
Device::serialNumber()
will return.A
ParameterInfo
structure that describes the connection parameters.Some devices may be connected directly and no additional information is required. In that case this may be ignored.
Other devices may require additional information to allow for a successful connection. This may be calibration data that is required for the driver to work – many hyperspectral cameras do not store the list of wavelengths on-camera and fluxEngine drivers for these cameras require them to be supplied in the form of a calibration file.
Yet another possibility are “manually connected” devices. While many devices that are supported by fluxEngine can be enumerated (because the use an interface that supports this, such as USB), some devices can’t be. In those cases the user must explicitly specify a port, or an address, or something similar in order to connect to that device. Connection parameters may also be used for this.
The
ParameterInfo
structure provided here allows the user to introspect the expected connection parameters by the driver for that specific device. This structure is not required to be used when connecting to a device, if no connection parameters are required, or the user already knows the format for these parameters. Further details on this structure may be found in the section Parameter Introspection.
Note
It is possible that an enumeration process does not yield any
devices - for example, if no device is currently connected. This is
not considered to be an error by
enumerateDevices()
.
The following code shows to how find the virtual PushBroom device from the list of enumerated devices:
1fluxEngine::EnumeratedDevice* virtualCamera = nullptr;
2 for (fluxEngine::EnumeratedDevice const& device
3 : enumeratedDevices->devices) {
4 if (device->driver && device->driver->name == "VirtualHyperCamera") {
5 virtualCamera = device.get();
6 break;
7 }
8 }
9
10 if (!virtualCamera) {
11 std::cerr << "VirtualHyperCamera device not found" << std::endl;
12 return;
13 }
Connecting to a Device
After the user has selected a device (from the list of enumerated
devices) to connect to, they may use the method
connectDeviceGroup()
to perform the connection.
This introduces the concept of a device group: some devices may have subdevices that perform different functions, but are accessed through the same interface. For this reason fluxEngine provides the additional abstraction of the device group – the resulting object of a connection process is a device group that will contain at least one device. All operations are performed on the individual devices, except for registering event notifications, and disconnecting.
In the vast majority of cases the device group will consist of only a single device.
The
connectDeviceGroup()
function expects a data structure that contains the information
required to perform the connection,
ConnectionSettings
.
The fields
ConnectionSettings::driverName
and
ConnectionSettings::driverType
have to be filled according to the
name
and
type
fields of the
EnumeratedDriver
of the
device the user wants to connect to.
The field
ConnectionSettings::id
must be set to the value obtained in
id
for the enumerated
device.
The user must also specify a timeout, after which the driver process
will forcibly be killed and the connection attempt will be assumed to
be unsuccessful. This must be set in
ConnectionSettings::timeout
.
Note that for many devices a timeout of 2 minutes is recommended, and
less than 10 seconds will not be enough time in the vast majority of
cases.
If the user wants to specify connection parameters they may do so by
inserting them into
ConnectionSettings::connectionParameters
,
which is a std::unordered_map<std::string, std::string>
. Please
refer to the documentation of that member for the proper encoding of
the various different values. Of note is that file names on Windows
systems must be encoded as UTF-8 and not the local code-page.
The following example shows how to connect to a virtual PushBroom
camera that was found in the previous example after enumeration. The
virtual PushBroom camera requires at least one connection parameter,
"Cube"
indicating the ENVI cube to load, but supports two more
parameters for the white and dark reference cubes,
"WhiteReferenceCube
and "DarkReferenceCube
.
1 fluxEngine::DeviceGroup deviceGroup;
2 fluxEngine::Device* device{};
3 try {
4 fluxEngine::ConnectionSettings settings;
5 settings.driverType = virtualCamera->driver->type;
6 settings.driverName = virtualCamera->driver->name;
7 settings.id = virtualCamera->id;
8 settings.timeout = std::chrono::seconds{60};
9 settings.connectionParameters["Cube"] = "C:\\Cube.hdr";
10 settings.connectionParameters["WhiteReferenceCube"] = "C:\\Cube_White.hdr";
11 settings.connectionParameters["DarkReferenceCube"] = "C:\\Cube_Dark.hdr";
12
13 deviceGroup = fluxEngine::connectDeviceGroup(handle, settings);
14 device = deviceGroup->primaryDevice();
15 } catch (std::exception& e) {
16 std::cerr << "An error occurred: " << e.what() << std::endl;
17 exit(1);
18 }
connectDeviceGroup()
blocks until the connection attempt has completed or there was an
error. If connecting to the device does not succeed, or the connection
attempt times out, an exception will be thrown.
Disconnecting from a Device
There are two methods to disconnect from a device:
Let the
DeviceGroup
object fall out of scope. This will forcibly disconnect the device by killing the driver process.Call
DeviceGroup::disconnect()
that allows the user to specify a timeout to use while attempting a disconnect operation. This is the recommended method as it allows the driver to properly free its resources. However, this call will block the caller for up to the specified timeout.After the disconnect has occurred the user may then let the
DeviceGroup
object fall out of scope to free any remaining resources.Any device belonging to the device group will not be usable anymore after a call to
DeviceGroup::disconnect()
, though the pointers will remain valid until the device group has been destroyed.
Note
After a device group has been destroyed any devices that have been pointed to must not be accessed by the user anymore. The device group objects is the owner of the individual device pointers.
Accessing Device Parameters
Most devices can be controlled via parameters. Introspection for these
parameters is available via the
Device::parameterList()
method. There are three parameter lists for different purposes:
Device parameters (
fluxEngine::Device::ParameterListType::Parameter
) that control the deviceMeta information parameters (
fluxEngine::Device::ParameterListType::MetaInfo
) that provide additional information about a device (these are read-only), such as the firmware version of the deviceStatus information parameters (
fluxEngine::Device::ParameterListType::Status
) that provide status information about the device (these are read-only), such as temperature sensor values
See the section on Parameter Introspection for further details on how to perform instrospection into the available parameters.
Parameters may be read via the following methods:
fluxEngine::Device::getParameterInteger()
for integer and enumeration parameters.
fluxEngine::Device::getParameterBoolean()
for boolean parameters.
fluxEngine::Device::getParameterFloat()
for floating point parameters.
fluxEngine::Device::getParameterString()
for all parameters that may be read. String parameters will be read as expected, and parameters of all other types will be converted into a canonical string representation.
For example, most instrument devices will have the standard ROI
parameters, such as "Width"
and "OffsetX"
. These will be
integers and may be read in the following manner:
1 try {
2 int64_t offsetX = device->getParameterInteger("OffsetX");
3 int64_t width = device->getParameterInteger("Width");
4 } catch (std::exception& e) {
5 std::cerr << "An error occurred: " << e.what() << std::endl;
6 exit(1);
7 }
Parameters may be changed via the following methods:
fluxEngine::Device::setParameterInteger()
for integer and enumeration parameters.
fluxEngine::Device::setParameterBoolean()
for boolean parameters.
fluxEngine::Device::setParameterFloat()
for floating point parameters.
fluxEngine::Device::setParameterString()
for all parameters that may be written to. String parameters will be written to as expected, and parameters of all other types will be converted from a canonical string representation.
The following example shows how the exposure time of a camera may be altered, assuming that camera uses a floating point exposure time in milliseconds (this can be queried via introspection, if required):
1 try {
2 device->setParameterFloat("ExposureTime", 3.5);
3 } catch (std::exception& e) {
4 std::cerr << "An error occurred: " << e.what() << std::endl;
5 exit(1);
6 }
Note
Changing parameters for instrument devices when acquisition is
currently active will likely cause them to stop acquisition
automatically in order to be able to change these parameters. In
that case the status of the device will enter the
InstrumentDevice::Status::ForcedStop
state. The user must then acknowledge this by calling
InstrumentDevice::stopAcquisition()
and only then may restart acquistion via
InstrumentDevice::startAcquisition()
.
Some parameter changes may not require acquisition to be stopped though, in which case the acquisition will continue to be active after such a change. Which parameters will stop acquisition will depend on the specific device. There is also no means of querying this information before making such a change, as stopping acquisition may only occur when a specific value is set, or when the device is in a specific state.
The virtual PushBroom driver only has a single parameter that indicates in what interval (in milliseconds) the individual frames should be provided.
1 try {
2 // Virtaul PushBroom: send a frame every 5ms
3 device->setParameterInteger("Interval", 5);
4 } catch (std::exception& e) {
5 std::cerr << "An error occurred: " << e.what() << std::endl;
6 exit(1);
7 }
Accessing Instrument Devices
For instrument devices the resulting device will be of type
InstrumentDevice
. Using an
appropriate cast one may obtain the correct object:
1 try {
2 fluxEngine::Device* device = deviceGroup.primaryDevice();
3 using fluxEngine::InstrumentDevice;
4 InstrumentDevice* instrumentDevice;
5 instrumentDevice = dynamic_cast<InstrumentDevice*>(device);
6 if (!instrumentDevice)
7 throw std::runtime_error("The primary device is not an instrument (not expected).");
8 } catch (std::exception& e) {
9 std::cerr << "An error occurred: " << e.what() << std::endl;
10 exit(1);
11 }
Instrument Device Setup
Directly after having connected to the instrument device the user must set up the shared memory region. The shared memory logic is described in further detail in the Instrument Buffers and Shared Memory section.
To setup the shared memory area one may use the method
InstrumentDevice::setupInternalBuffers()
.
This method may only be called once for the instrument device and
the setting that has been chosen here will be used as long as the
device is connected.
As the number of buffers actually used for specific acquisition phases can be specified at a later point in time, but must at most be the number specified here, it is recommended to err on the higher side for this setting, and potentially reduce the number of buffers when starting acquisition.
The trade-off for the number of buffers actually used is the following: more buffers reduce the chance of a dropped frame, but use more RAM and may dramatically increase the latency, i.e. the time it takes between the instrument measuring data and that data being processed by fluxEngine.
For inline data processing a number of 5 (the mimimum) is recommended to avoid issues with high latencies; when recording data higher numbers may be more useful, such as 100. Using more than 100 buffers is not recommended in most cases. (Though only the amount of RAM and possibly quirks of the operating system’s shared memory logic will limit the maximum number specified here.)
1 try {
2 // Use 16 as a compromise value here for now,
3 // but for low-latency acquisition we may choose
4 // to only use 5 of these buffers.
5 instrumentDevice->setupInternalBuffers(16);
6 } catch (std::exception& e) {
7 std::cerr << "An error occurred: " << e.what() << std::endl;
8 exit(1);
9 }
Data Acquisition
To acquire data from an instrument one must first call the
InstrumentDevice::startAcquisition()
method. Afterwards the instrument will start providing buffers in the
internal queue that may be retrieved via
InstrumentDevice::retrieveBuffer()
.
After the data from the buffer has been used the user must return the
buffer via
InstrumentDevice::returnBuffer()
.
Finally, once acquisition is no longer required, the user may stop
acquisition via
InstrumentDevice::stopAcquisition()
.
The
InstrumentDevice::startAcquisition()
requires the user to specify additional parameters required for the
acquisition. These are:
The name of the reference to measure. Some instruments have the ability to perform special operations when the user wants to measure a reference.
For example, some cameras have a shutter that the driver will automatically close when measuring a dark reference. This has the advantage that the user doesn’t need to turn of the light for these types of cameras.
Also, the virtual PushBroom driver will return different data (from the various different cubes specified during connection) depending on whether a reference measurement was selected.
Use
"WhiteReference"
to specify that a white reference is to be measured, and use"DarkReference"
to specify that a dark reference is to be measured. Use""
(and empty string, the default) to specify that regular data is to be measured.The actual number of buffers to use for this acquisition process. This allows the user to reduce the number of buffers actually used during this specific acquisition process. The largest number that may be specified here is the number supplied to
InstrumentDevice::setupInternalBuffers()
. If0
(the default) is specified it will use exactly the number of buffers provided toInstrumentDevice::setupInternalBuffers()
.
The following example code shows a typical acquisition loop:
1 try {
2 fluxEngine::InstrumentDevice::AcquisitionParameters parameters;
3 // Keep the parameters at their default
4 instrumentDevice->startAcquisition(parameters);
5 // Acquire 10 frames
6 for (int i = 0; i < 10; ++i) {
7 fluxEngine::BufferInfo buffer = instrumentDevice->retrieveBuffer(std::chrono::seconds{1});
8 if (!buffer.ok)
9 continue;
10 // Do something with buffer here
11 instrumentDevice->returnBuffer(buffer.id);
12 }
13 instrumentDevice->stopAcquisition();
14 } catch (std::exception& e) {
15 std::cerr << "An error occurred: " << e.what() << std::endl;
16 exit(1);
17 }
Measuring References
HSI measurements typically require at least a white reference to be useful for most HSI algorithms. This is because most HSI data processing is done with reflectance values.
fluxEngine provides some facilities to make measuring a reference
easier. The primary class here is
BufferContainer
, which
allows the user to store multiple buffers it acquired from a device,
and then later reuse them when setting up data processing.
In principle a single buffer could be used as a reference measurement, but in practice it is better to measure multiple buffers, so that noise can be reduced. For many measurements using 10 buffers is a good number, but for very percise measurements 100 buffers or more may be required. (Using more buffers will obviously also take longer and use more RAM.)
The following example code shows how a white and dark reference may be measured with fluxEngine. They will be stored in a buffer container each, which then later may be passed to fluxEngine when setting up data processing.
1 fluxEngine::BufferContainer whiteReference, darkReference;
2 try {
3 // Average 10 frames for the white reference
4 whiteReference = fluxEngine::createBufferContainer(camera, 10);
5 fluxEngine::InstrumentDevice::AcquisitionParameters parameters;
6 parameters.referenceName = "WhiteReference";
7 // insert code here to prompt user to insert the white
8 // reference underneath the sensor
9 instrumentDevice->startAcquisition(parameters);
10 while (whiteReference.count() < 10) {
11 fluxEngine::BufferInfo buffer = instrumentDevice->retrieveBuffer(std::chrono::seconds{1});
12 if (!buffer.ok)
13 continue;
14 whiteReference.add(buffer);
15 instrumentDevice->returnBuffer(buffer.id);
16 }
17 instrumentDevice->stopAcquisition();
18
19 // Average 10 frames for the dark reference
20 darkReference = fluxEngine::createBufferContainer(camera, 10);
21 parameters.referenceName = "DarkReference";
22 // insert code here to prompt user to close the lid
23 // of the sensor optics
24 instrumentDevice->startAcquisition(parameters);
25 while (darkReference.count() < 10) {
26 fluxEngine::BufferInfo buffer = instrumentDevice->retrieveBuffer(std::chrono::seconds{1});
27 if (!buffer.ok)
28 continue;
29 darkReference.add(buffer);
30 instrumentDevice->returnBuffer(buffer.id);
31 }
32 instrumentDevice->stopAcquisition();
33
34 // insert code here to prompt the user to open
35 // the lid again after the dark reference has
36 // been measured
37 } catch (std::exception& e) {
38 std::cerr << "An error occurred: " << e.what() << std::endl;
39 exit(1);
40 }
Persistent Buffers
Buffers must be returned to fluxEngine quickly. For this reason there
is another data structure available,
PersistentBufferInfo
,
that allows the user to store data from an individual buffer with a
lifetime that solely depends on the object lifetime of the persistent
buffer that was allocated.
Persistent buffers may be created in the following manner:
A persistent buffer that doesn’t contain any data may be allocated via the
InstrumentDevice::allocatePersistentBuffer()
method. It is up to the user to fill that persistent buffer afterwards. (See below for how this may be done.)The persistent buffer allocated in this manner will have the same structure as a buffer that would have been returned from the device.
A persistent buffer that is a copy of an existing buffer may be created via the
BufferInfo::copy()
method of theBufferInfo
class.A persistent buffer may be duplicated via the
PersistentBufferInfo::copy()
method.A single stored buffer in a buffer container may be extracted as a newly created persistent buffer via the
BufferContainer::copyBuffer()
method.
A persistent buffer may be used in any place where a buffer from a device could be used instead. For example, it may be added to a buffer container, it may also be used as input for a processing context (see the Data Processing chapter).
Additionally there is functionality of replacing the contents of an existing persistent buffer with other data (so that constant allocations and deallocations can be avoided):
The data of a buffer can be copied into a persistent buffer via the
BufferInfo::copyIno()
method. The structure of both objects must match for this to succeed.The data of an existing persistent buffer can be copied into another persistent buffer via the
PersistentBufferInfo::copyIno()
method. The structure of both objects must match for this to succeed.A single stored buffer in a buffer container may be extracted into an existing persistent buffer via the
BufferContainer::copyIntoBuffer()
method.
Using data from an Instrument
Most hyperspectral cameras don’t produce data that can be used as-is, but at least some processing has to happen first. fluxEngine provides the means for this, but these details are described in the Data Processing chapter.
Handling Notifications from a Device
Devices may produce notifications that may be of interest to the user. For example, if the device is unplugged while it is connected, that would trigger a notification with most drivers.
Notifications from devices are kept in a queue in the
DeviceGroup
object. The user
may retrieve the first notification in the queue via
DeviceGroup::nextNotification()
.
That will return a structure of type
DeviceGroup::Notification
.
The user must then first check the
DeviceGroup::Notification::type
member to see if there actually was a notification – if there was
none, the type will be
DeviceGroup::NotificationType::None
.
Otherwise the result will contain the first notification in the
notification queue in the device group, that has now been removed from
the queue.
Please refer to the reference documentation of
DeviceGroup::Notification
.
and
DeviceGroup::NotificationType
for details on the type of notifications.
In order to avoid having to constantly poll for notifications, it is
also possible to obtain an operating system handle (HANDLE
type on
Windows, a file descriptor on all other operating systems) that may be
used to include in an event loop. This may be done via the
DeviceGroup::notificationEventHandle()
method.
On Windows systems, this could be included in an event loop as follows:
1 // Event loop, elsewhere in the code
2 // (simplified, as an illustration)
3 std::vector<HANDLES> eventLoopHandles;
4 std::vector<std::function<void()>> eventLoopHandlers;
5 while (true) {
6 DWORD result = WaitForMultipleObjects(DWORD(eventLoopHandles.size()),
7 eventLoopHandles.data(),
8 FALSE,
9 1000);
10 if (result >= WAIT_OBJECT_0
11 && result < (WAIT_OBJECT_0 + eventLoopHandles.size())) {
12 size_t index = result - WAIT_OBJECT_0;
13 eventLoopHandlers[index]();
14 }
15 }
16
17 // After having connected the device group, register
18 // with the event loop
19 eventLoopHandles.push_back(deviceGroup.notificationEventHandle());
20 eventLoopHandlers.push_back([&deviceGroup] () {
21 fluxEngine::DeviceGroup::Notification notification = deviceGroup.nextNotification();
22 if (notification.type == fluxEngine::DeviceGroup::NotificationType::None)
23 // No actual notification presnet
24 return;
25 // Do something with the notification
26 });
On Linux systems, if Glib is used for the event loop, this could be included as follows:
1 int fd = deviceGroup.notificationEventHandle();
2 source_id = g_unix_fd_add_full(G_PRIORITY_DEFAULT, fd, G_IO_IN,
3 [] (gint fd, GIOCondition condition, gpointer userData) -> gboolean {
4 (void) fd;
5 (void) condition;
6 fluxEngine::DeviceGroup& deviceGroup =
7 *static_cast<fluxEngine::DeviceGroup*>(userData);
8
9 fluxEngine::DeviceGroup::Notification notification = deviceGroup.nextNotification();
10 if (notification.type == fluxEngine::DeviceGroup::NotificationType::None)
11 // No actual notification presnet
12 return TRUE;
13
14 // Do something with the notification
15 return TRUE;
16 }, &deviceGroup, nullptr);
When integrating with Qt the following example code may be helpful:
1 // This assumes we are inside a method of a QObject-derived
2 // class
3 #if defined(_WIN32)
4 HANDLE handle = deviceGroup.notificationEventHandle();
5 auto notifier = new QWinEventNotifier(handle, this);
6 connect(notifier, &QWinEventNotifier::activated,
7 this, [this] (HANDLE*) { deviceGroupNotificationEvent(); });
8 #else
9 int fd = deviceGroup.notificationEventHandle();
10 auto notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
11 connect(notifier, &QSocketNotifier::activated,
12 this, [this] (QSocketDescriptor, QSocketNotifier::Type)
13 { deviceGroupNotificationEvent(); });
14 #endif
15
16 // Our slot to handle the event
17 void deviceGroupNotificationEvent()
18 {
19 fluxEngine::DeviceGroup::Notification notification = deviceGroup.nextNotification();
20 if (notification.type == fluxEngine::DeviceGroup::NotificationType::None)
21 // No actual notification presnet
22 return;
23 // Do something with the notification
24 }
Note
The device group remains the owner of the handle. Once the device group has disconnected the handle will no longer be valid. The user must ensure that the notification handle is unregistered with the corresponding event loop before disconnecting the device group!