Skip to content

Examples

custom-controller: control from code

This example replaces the built-in controller of a device with an own plugin. The plugin is compiled into the program. It takes device control and requests each transition from a state change callback.

The code is in examples/custom-controller. The example shows how to:

  • write a controller plugin that takes device control,
  • compile a plugin into the executable and load it with the prefix s:,
  • start a device with DeviceRunner and its hooks, without runDevice.h,
  • end the run through a property that the controller reads.

For the general procedure, see Control a device from code.

The example has no channels. MyDevice in MyDevice.h is an alias of fair::mq::Device. The device goes through its states without data.

The example needs only FairMQ. It has no start script and no install rule.

The examples build when the CMake option BUILD_EXAMPLES is ON (the default). The executable is in the build directory:

  • Directorybuild/examples/custom-controller/
    • fairmq-ex-custom-controller
Terminal window
cd build/examples/custom-controller
./fairmq-ex-custom-controller

The program stops by itself about 3 s after the device enters RUNNING. Its exit code is 0.

CMakeLists.txt sets the target property ENABLE_EXPORTS ON. The executable then exports its symbols, so the plugin manager can find the plugin factory in the program itself.

main.cxx creates a DeviceRunner and adds two hooks:

examples/custom-controller/main.cxx
DeviceRunner runner(argc, argv);
runner.AddHook<hooks::LoadPlugins>([](DeviceRunner& r) {
r.fPluginManager.LoadPlugin("s:mycontroller");
// 's:' stands for static because the plugin is compiled into the executable
// 'mycontroller' is the plugin name passed as second arg to REGISTER_FAIRMQ_PLUGIN
});
fair::Logger::SetConsoleSeverity(fair::Severity::debug);
runner.AddHook<hooks::InstantiateDevice>([](DeviceRunner& r) {
r.fConfig.SetProperty<int>("catch-signals", 0);
r.fConfig.SetProperty<bool>("please-shut-me-down", false);
r.fDevice = std::make_unique<example::MyDevice>(r.fConfig);
r.fDevice->SubscribeToStateChange("example", [&r](auto state) {
if (state == State::Running) {
r.fDevice->WaitFor(3s);
r.fConfig.SetProperty<bool>("please-shut-me-down", true);
}
});
});
return runner.RunWithExceptionHandlers();
examples/custom-controller/main.cxx:22–45
  • hooks::LoadPlugins runs after the runner loads the built-in plugin config and before it loads the built-in plugin control. The hook loads s:mycontroller. The prefix s: loads a static plugin, a plugin that is part of the executable. The name is the second argument of REGISTER_FAIRMQ_PLUGIN.
  • hooks::InstantiateDevice sets the property catch-signals to 0. Thus the built-in control plugin installs no signal handler. The hook also creates the device and subscribes to its state changes with SubscribeToStateChange().
  • In RUNNING, the callback waits 3 s with WaitFor(). Then it sets the property please-shut-me-down to true.
  • RunWithExceptionHandlers() runs the device. It returns 1 if an exception reaches it.

The hooks are declared in DeviceRunner.h. The order of the steps is in DeviceRunner.cxx.

MyController is a fair::mq::Plugin. The constructor takes device control and subscribes to the state changes of the device:

examples/custom-controller/MyController.h
struct MyController : fair::mq::Plugin // NOLINT
{
template<typename... Args>
MyController(Args&&... args)
: Plugin(std::forward<Args>(args)...)
{
TakeDeviceControl();
SubscribeToDeviceStateChange([this](auto state) {
auto shutdown = GetProperty<bool>("please-shut-me-down", false);
try {
switch (state) {
case DeviceState::Idle: {
ChangeDeviceState(shutdown ? DeviceStateTransition::End
: DeviceStateTransition::InitDevice);
break;
}
case DeviceState::InitializingDevice: {
ChangeDeviceState(DeviceStateTransition::CompleteInit);
break;
}
case DeviceState::Initialized: {
ChangeDeviceState(DeviceStateTransition::Bind);
break;
}
case DeviceState::Bound: {
ChangeDeviceState(DeviceStateTransition::Connect);
break;
}
case DeviceState::DeviceReady: {
ChangeDeviceState(shutdown ? DeviceStateTransition::ResetDevice
: DeviceStateTransition::InitTask);
break;
}
case DeviceState::Ready: {
ChangeDeviceState(shutdown ? DeviceStateTransition::ResetTask
: DeviceStateTransition::Run);
break;
}
case DeviceState::Running: {
ChangeDeviceState(DeviceStateTransition::Stop);
break;
}
case DeviceState::Exiting: {
ReleaseDeviceControl();
break;
}
default:
break;
}
} catch (fair::mq::PluginServices::DeviceControlError const&) {
// this means we do not have device control
}
});
}
~MyController() override { ReleaseDeviceControl(); }
};
examples/custom-controller/MyController.h:17–74
  • The plugin manager creates the plugins in their load order: config, mycontroller, control. Thus TakeDeviceControl() of mycontroller succeeds first. The built-in control plugin cannot take control. It logs the reason on the severity debug and starts no controller.
  • For each state, the callback requests the next transition with ChangeDeviceState(). In BINDING, CONNECTING, INITIALIZING TASK, RESETTING TASK and RESETTING DEVICE, the device changes state by itself, so the callback requests nothing.
  • The callback reads please-shut-me-down with GetProperty(). The default is false.
  • In EXITING, the plugin calls ReleaseDeviceControl(). DeviceRunner::Run() waits until all plugins release device control, and then returns.

The plugin requests these transitions:

State please-shut-me-down is false please-shut-me-down is true
IDLE InitDevice End
INITIALIZING DEVICE CompleteInit CompleteInit
INITIALIZED Bind Bind
BOUND Connect Connect
DEVICE READY InitTask ResetDevice
READY Run ResetTask
RUNNING Stop Stop

The macro REGISTER_FAIRMQ_PLUGIN registers the class under the name mycontroller:

examples/custom-controller/MyController.h
REGISTER_FAIRMQ_PLUGIN(example::MyController, // Class name
mycontroller, // Plugin name (string, lower case chars only)
(fair::mq::Plugin::Version{0, 42, 0}), // Version
"Mr. Dummy <dummy@test.net>", // Maintainer
"https://git.test.net/mycontroller.git", // Homepage
// example::MyControllerProgramOptions // Free
// function which declares custom
// program options for the plugin
fair::mq::Plugin::NoProgramOptions)
examples/custom-controller/MyController.h:87–95

RunStateMachine() first calls the state change callbacks with IDLE, so the plugin requests InitDevice. In each later state, the state machine calls the state change callbacks before the state handler of the device. See StateMachine::ProcessWork() and the loop of the state machine.

The state machine calls the callbacks in the order of the number at the start of their key. A key without a number, for example example or mycontroller, has the number 0. Callbacks with the same number run in the order of subscription. The hook in main.cxx subscribes before the plugin manager creates the plugin. Thus, in RUNNING, the callback of main.cxx runs first:

  1. The callback of main.cxx waits 3 s and sets please-shut-me-down to true.
  2. The callback of the plugin requests Stop.
  3. The state handler of the device logs fair::mq::Device running.... The device has no own Run() code, and a transition is pending. Thus the device goes to READY at once.
  4. In READY, DEVICE READY and IDLE, the plugin reads true. It requests ResetTask, ResetDevice and End.

The log shows each state change (here without the prefix of the logger):

Starting FairMQ state machine --> IDLE
IDLE ---> INITIALIZING DEVICE
INITIALIZING DEVICE ---> INITIALIZED
INITIALIZED ---> BINDING
BINDING ---> BOUND
BOUND ---> CONNECTING
CONNECTING ---> DEVICE READY
DEVICE READY ---> INITIALIZING TASK
INITIALIZING TASK ---> READY
READY ---> RUNNING
fair::mq::Device running...
RUNNING ---> READY
READY ---> RESETTING TASK
RESETTING TASK ---> DEVICE READY
DEVICE READY ---> RESETTING DEVICE
RESETTING DEVICE ---> IDLE
IDLE ---> EXITING

The 3 s pause is between READY ---> RUNNING and fair::mq::Device running.... main.cxx sets the console severity to debug, so the log also shows debug lines. The log also has these warnings:

  • Signal handling (e.g. Ctrl-C) has been deactivated., because catch-signals is 0.
  • No device ID or config-key provided for the configuration parser., because the program gives no --id.
  • No channels created after finishing initialization, because the device has no channels.

examples/custom-controller/CMakeLists.txt adds the test Example.custom-controller. It runs the executable and passes when the exit code is 0. The timeout is 30 s.

Configure with -DBUILD_TESTING=ON to register the test. Then run it in the build directory:

Terminal window
ctest -R Example.custom-controller
  • Change the time in WaitFor(3s).
  • Set please-shut-me-down to true in the hook InstantiateDevice. Then the plugin requests End in the first IDLE, and the device never runs.
  • Give the plugin its own options. Remove the // in front of the function MyControllerProgramOptions() in MyController.h. Then use example::MyControllerProgramOptions as the last argument of REGISTER_FAIRMQ_PLUGIN, in place of fair::mq::Plugin::NoProgramOptions. The options --custom-dummy-option and --custom-dummy-option2 then show in the output of --help.