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
DeviceRunnerand its hooks, withoutrunDevice.h, - end the run through a property that the controller reads.
For the general procedure, see Control a device from code.
Topology
Section titled “Topology”The example has no channels. MyDevice in MyDevice.h is an alias of fair::mq::Device. The device goes through its states without data.
Requirements
Section titled “Requirements”The example needs only FairMQ. It has no start script and no install rule.
Build and run
Section titled “Build and run”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
cd build/examples/custom-controller./fairmq-ex-custom-controllerThe 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.
The code
Section titled “The code”The program
Section titled “The program”main.cxx creates a DeviceRunner and adds two hooks:
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();hooks::LoadPluginsruns after the runner loads the built-in pluginconfigand before it loads the built-in plugincontrol. The hook loadss:mycontroller. The prefixs:loads a static plugin, a plugin that is part of the executable. The name is the second argument ofREGISTER_FAIRMQ_PLUGIN.hooks::InstantiateDevicesets the propertycatch-signalsto 0. Thus the built-in control plugin installs no signal handler. The hook also creates the device and subscribes to its state changes withSubscribeToStateChange().- In
RUNNING, the callback waits 3 s withWaitFor(). Then it sets the propertyplease-shut-me-downtotrue. 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.
The controller plugin
Section titled “The controller plugin”MyController is a fair::mq::Plugin. The constructor takes device control and subscribes to the state changes of the device:
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(); }};- The plugin manager creates the plugins in their load order:
config,mycontroller,control. ThusTakeDeviceControl()ofmycontrollersucceeds first. The built-in control plugin cannot take control. It logs the reason on the severitydebugand starts no controller. - For each state, the callback requests the next transition with
ChangeDeviceState(). InBINDING,CONNECTING,INITIALIZING TASK,RESETTING TASKandRESETTING DEVICE, the device changes state by itself, so the callback requests nothing. - The callback reads
please-shut-me-downwithGetProperty(). The default isfalse. - In
EXITING, the plugin callsReleaseDeviceControl().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:
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)The order of the callbacks
Section titled “The order of the callbacks”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:
- The callback of
main.cxxwaits 3 s and setsplease-shut-me-downtotrue. - The callback of the plugin requests
Stop. - The state handler of the device logs
fair::mq::Device running.... The device has no ownRun()code, and a transition is pending. Thus the device goes toREADYat once. - In
READY,DEVICE READYandIDLE, the plugin readstrue. It requestsResetTask,ResetDeviceandEnd.
Output
Section titled “Output”The log shows each state change (here without the prefix of the logger):
Starting FairMQ state machine --> IDLEIDLE ---> INITIALIZING DEVICEINITIALIZING DEVICE ---> INITIALIZEDINITIALIZED ---> BINDINGBINDING ---> BOUNDBOUND ---> CONNECTINGCONNECTING ---> DEVICE READYDEVICE READY ---> INITIALIZING TASKINITIALIZING TASK ---> READYREADY ---> RUNNINGfair::mq::Device running...RUNNING ---> READYREADY ---> RESETTING TASKRESETTING TASK ---> DEVICE READYDEVICE READY ---> RESETTING DEVICERESETTING DEVICE ---> IDLEIDLE ---> EXITINGThe 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., becausecatch-signalsis 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:
ctest -R Example.custom-controllerThings to try
Section titled “Things to try”- Change the time in
WaitFor(3s). - Set
please-shut-me-downtotruein the hookInstantiateDevice. Then the plugin requestsEndin the firstIDLE, and the device never runs. - Give the plugin its own options. Remove the
//in front of the functionMyControllerProgramOptions()inMyController.h. Then useexample::MyControllerProgramOptionsas the last argument ofREGISTER_FAIRMQ_PLUGIN, in place offair::mq::Plugin::NoProgramOptions. The options--custom-dummy-optionand--custom-dummy-option2then show in the output of--help.