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:

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:

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:

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:

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(). If 0 (the default) is specified it will use exactly the number of buffers provided to InstrumentDevice::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 the BufferInfo 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!