ChimeraTK-ApplicationCore 04.08.00
Loading...
Searching...
No Matches
ConnectionMaker.cc
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#include "ConnectionMaker.h"
5
6#include "Application.h"
7#include "ConsumingFanOut.h"
9#include "DeviceManager.h"
11#include "FanOut.h"
12#include "Flags.h"
14#include "TestableMode.h"
15#include "ThreadedFanOut.h"
16#include "TriggerFanOut.h"
17
18#include <ChimeraTK/NDRegisterAccessor.h>
19#include <ChimeraTK/SystemTags.h>
20
21#include <algorithm>
22
23namespace ChimeraTK {
24
25 /********************************************************************************************************************/
26
28 NetworkInformation net{&proxy};
29
30 debug("Checking network \"" + proxy.getName() + "\" consistency");
31 // Sanity check for the type and lengths of the nodes, extract the feeding node if any
32
33 VariableNetworkNode firstNodeWithType; // used for helpful error message only
34
35 net.useReverseRecovery = proxy.getTags().contains(ChimeraTK::SystemTags::reverseRecovery);
36
37 if(net.useReverseRecovery) {
38 debug(" Network has reverse recovery");
39 }
40 else {
41 debug(" Network does not have reverse recovery");
42 }
43
44 int bidirectionalDeviceNodeCount = 0;
45 std::vector<std::shared_ptr<VariableNetworkNode>> unidirectionalDeviceNodes;
46
47 for(const auto& node : proxy.getNodes()) {
48 if(node->getDirection().withReturn) {
49 net.numberOfBidirectionalNodes++;
50 }
51 if(node->getDirection().dir == VariableDirection::feeding) {
52 std::stringstream ss;
53 node->dump(ss);
54 auto nodeDump = ss.str();
55
56 // Remove trailing newline
57 nodeDump.erase(nodeDump.length() - 1);
58 debug(" Feeder: ", nodeDump);
59
60 if(net.feeder.getType() == NodeType::invalid) {
61 net.feeder = *node;
62 }
63 else {
64 std::stringstream ss1;
65 net.feeder.dump(ss1);
66 std::stringstream ss2;
67 node->dump(ss2);
68 throw ChimeraTK::logic_error("Variable network " + proxy.getFullyQualifiedPath() +
69 " has more than one feeder:\n" + ss1.str() + ss2.str());
70 }
71
72 // feeding a constant (created with ApplicationModule::constant()) is not allowed
73 if(boost::starts_with(node->getName(), ApplicationModule::namePrefixConstant)) {
74 throw ChimeraTK::logic_error("Feeding a constant is not allowed (" + node->getQualifiedName() + ")");
75 }
76 }
77 else if(node->getDirection().dir == VariableDirection::consuming) {
78 if(node->getDirection().withReturn) {
79 net.numberOfBidirectionalConsumers++;
80 }
81 std::stringstream ss;
82 node->dump(ss);
83 auto consumerDump = ss.str();
84 consumerDump.erase(consumerDump.length() - 1);
85 debug(" Consumer: ", consumerDump);
86 net.consumers.push_back(*node);
87 if(node->getMode() == UpdateMode::poll) {
88 net.numberOfPollingConsumers++;
89 }
90
91 if(node->getType() == NodeType::Device) {
92 if(node->getDirection().withReturn) {
93 bidirectionalDeviceNodeCount++;
94 }
95 else {
96 unidirectionalDeviceNodes.push_back(node);
97 }
98 }
99 }
100 else {
101 // There should not be an invalid direction variable in here. FIXME: is that true?
102 assert(false);
103 }
104
105 if(*net.valueType == typeid(AnyType)) {
106 net.valueType = &node->getValueType();
107 firstNodeWithType = *node;
108 }
109 else {
110 if(*net.valueType != node->getValueType() && node->getValueType() != typeid(AnyType)) {
111 std::stringstream ss1;
112 firstNodeWithType.dump(ss1);
113 std::stringstream ss2;
114 node->dump(ss2);
115 throw ChimeraTK::logic_error("Variable network " + proxy.getFullyQualifiedPath() +
116 " contains nodes with different types: " + boost::core::demangle(net.valueType->name()) +
117 " != " + boost::core::demangle(node->getValueType().name()) + "\n" + ss1.str() + ss2.str());
118 }
119 }
120
121 if(net.valueLength == 0) {
122 net.valueLength = node->getNumberOfElements();
123 }
124 else {
125 if(net.valueLength != node->getNumberOfElements() && node->getNumberOfElements() != 0) {
126 throw ChimeraTK::logic_error(
127 "Variable network " + proxy.getFullyQualifiedPath() + " contains nodes with different sizes");
128 }
129 }
130
131 // Get unit and description of network from nodes. First one wins
132 if(net.description.empty()) {
133 net.description = node->getDescription();
134 }
135
136 if(net.unit.empty()) {
137 net.unit = node->getUnit();
138 }
139 }
140
141 if(bidirectionalDeviceNodeCount == 0 && net.useReverseRecovery) {
142 debug(" Network has no bidirectional device nodes but uses reverse recovery, flagging device nodes as having "
143 "return channel");
144 if(unidirectionalDeviceNodes.size() > 1) {
145 throw ChimeraTK::logic_error(
146 "Invalid network " + proxy.getFullyQualifiedPath() + ", reverse recovery causes initial value conflict");
147 }
148 for(const auto& node : unidirectionalDeviceNodes) {
149 if(node->getDirection().dir == VariableDirection::consuming && node->isReadable()) {
150 node->setDirection({VariableDirection::consuming, true});
151 net.numberOfBidirectionalNodes++;
152 net.numberOfBidirectionalConsumers++;
153 }
154 }
155 }
156
157 if(net.feeder.getType() == NodeType::Application) {
158 auto* owner = dynamic_cast<Module*>(net.feeder.getOwningModule());
159 assert(owner != nullptr);
160 auto* feederApplicationModule = owner->findApplicationModule();
161 for(const auto& consumer : net.consumers) {
162 if(consumer.getType() != NodeType::Application) {
163 continue;
164 }
165
166 auto* module = dynamic_cast<Module*>(consumer.getOwningModule());
167 assert(module != nullptr);
168 if(feederApplicationModule == module->findApplicationModule()) {
169 throw ChimeraTK::logic_error(
170 std::string("Network for ") + consumer.getQualifiedName() + "feeds itself in the same module");
171 }
172 }
173 }
174
175 // If we are left with an undefined network at this point this should be trigger network and can be assumed
176 // to be void
177 if(*net.valueType == typeid(AnyType)) {
178 net.valueType = &typeid(ChimeraTK::Void);
179 }
180
181 // For void, a length of 0 is ok, otherwise this is not allowed
182 if(net.valueLength == 0 && *net.valueType != typeid(ChimeraTK::Void)) {
183 throw ChimeraTK::logic_error("Cannot determine length of network " + proxy.getFullyQualifiedPath());
184 }
185
186 if(net.feeder.getType() == NodeType::invalid && net.consumers.empty()) {
187 throw ChimeraTK::logic_error(
188 "Variable network '" + proxy.getFullyQualifiedPath() + "' is empty. Must not happen");
189 }
190
191 return net;
192 }
193
194 /********************************************************************************************************************/
195
197 // This will do two things:
198 // - Check the network consistency
199 // - Return feeder and consumers, if available
200 auto info = checkNetwork(proxy);
201 finaliseNetwork(info);
202 return info;
203 }
204
205 /********************************************************************************************************************/
206
208 debug("Finalising network \"" + net.proxy->getName() + "\"");
209 // check whether this is a constant created via ApplicationModule::constant()
210 bool isConstant{!net.consumers.empty() &&
211 boost::starts_with(net.consumers.front().getName(), ApplicationModule::namePrefixConstant)};
212 if(isConstant) {
213 assert(!net.feeder.isValid());
214
215 net.feeder =
216 VariableNetworkNode{&net.consumers.front().getValueType(), true, net.consumers.front().getNumberOfElements()};
217
218 // Extract value from constant name. The format of a constant path name is:
219 // /@CONST@/<type>/<uniqueId>/<value>
220 RegisterPath name(net.consumers.front().getName());
221 auto components = name.getComponents();
222 assert(components.size() == 4);
223 std::string stringValue = components[3];
224
225 callForType(net.consumers.front().getValueType(), [&](auto t) {
226 using UserType = decltype(t);
227 net.feeder.setConstantValue(userTypeToUserType<UserType>(Utilities::unescapeName(stringValue)));
228 });
229 }
230
231 bool neededFeeder{false};
232 if(not net.feeder.isValid()) {
233 debug(" No feeder in network, creating ControlSystem feeder ", net.proxy->getFullyQualifiedPath());
234 debug(" Bi-directional consumers: ", net.numberOfBidirectionalNodes);
235
236 // If we have a bi-directional consumer, mark this CS feeder as bidirectional as well
238 VariableDirection{VariableDirection::feeding, net.numberOfBidirectionalNodes > 0}, *net.valueType,
239 net.valueLength);
240
241 neededFeeder = true;
242 }
243 assert(net.feeder.isValid());
244
245 if(not neededFeeder and not isConstant) {
246 // Only add CS consumer if we did not previously add CS feeder, we will add one or the other, but never both
247 // Also we will not add CS consumers for constants.
248 //
249 // If this is a one-on-one network with reverse recovery or none of the other consumers is bi-directional, we
250 // have to make the CS feeder bi-directional
251 auto needReturn = net.useReverseRecovery && net.numberOfBidirectionalConsumers == 0;
252 debug(" Network has a non-CS feeder, can create additional ControlSystem consumer");
253 debug(" with" + std::string(needReturn ? "" : "out") + " return");
255 {VariableDirection::consuming, needReturn}, *net.valueType, net.valueLength));
256 }
257 assert(not net.consumers.empty());
258
259 // register PVs with the control system adapter
260 try {
261 callForType(*net.valueType, [&](auto t) {
262 using UserType = decltype(t);
263
264 for(auto& node : net.consumers) {
265 if(node.getType() != NodeType::ControlSystem) {
266 continue;
267 }
268 this->createProcessVariable<UserType>(
269 node, net.valueLength, net.unit, net.description, {AccessMode::wait_for_new_data});
270 }
271
273 AccessModeFlags flags = {AccessMode::wait_for_new_data};
274
275 if(net.consumers.size() == 1) {
276 auto consumer = net.consumers.front();
277 if(consumer.getType() == NodeType::Application && consumer.getMode() == UpdateMode::poll) {
278 flags = {};
279 }
280 }
281
282 this->createProcessVariable<UserType>(net.feeder, net.valueLength, net.unit, net.description, flags);
283 }
284 });
285 }
286 catch(std::bad_cast& e) {
287 std::cerr << "Illegal value type " + boost::core::demangle(net.valueType->name()) + " of variable network: "
288 << net.proxy->getFullyQualifiedPath() << std::endl;
289 throw;
290 }
291 debug();
292 }
293
294 /********************************************************************************************************************/
295
296 template<typename... Args>
297 void NetworkVisitor::debug(Args&&... args) {
298 // Fold expression printer from https://en.cppreference.com/w/cpp/language/fold
299 (logger(Logger::Severity::debug, "ConnectionMaker") << ... << args) << std::endl;
300 }
301
302 /********************************************************************************************************************/
303 /* ConnectionMaker implementations */
304 /********************************************************************************************************************/
305
306 void ConnectionMaker::connectNetwork(Model::ProcessVariableProxy& proxy) {
307 auto path = proxy.getFullyQualifiedPath();
308 debug("Network found: ", path);
309
310 auto triggerFinder = [&](auto p) {
311 auto deviceTrigger = p.getTrigger();
312
313 if(deviceTrigger.isValid()) {
314 debug(" Found Feeding device ", p.getAliasOrCdd(), " with trigger ", p.getTrigger().getFullyQualifiedPath());
315 }
316 else {
317 debug(" Feeding from device ", p.getAliasOrCdd(), " but without any trigger");
318 }
319
320 return std::make_pair(deviceTrigger, p);
321 };
322
323 Model::ProcessVariableProxy trigger{};
324 Model::DeviceModuleProxy device{};
325
326 // Use external trigger if feeder is poll-type and number of poll-type consumers != 1.
327 // If there is exactly one poll-type consumer, transfers will be triggered by that consumer.
328 if(_networks.at(path).feeder.getMode() == UpdateMode::poll && _networks.at(path).numberOfPollingConsumers != 1) {
329 _networks.at(path).useExternalTrigger = true;
330 std::tie(trigger, device) =
331 proxy.visit(triggerFinder, Model::adjacentInSearch, Model::keepPvAccess, Model::keepDeviceModules,
332 Model::returnFirstHit(std::make_pair(Model::ProcessVariableProxy{}, Model::DeviceModuleProxy{})));
333 if(!trigger.isValid()) {
334 throw ChimeraTK::logic_error(
335 "Poll-Type feeder " + _networks.at(path).feeder.getName() + " needs trigger, but none provided");
336 }
337 }
338
339 auto constantFeeder = _networks.at(path).feeder.getType() == NodeType::Constant;
340
341 if(_networks.at(path).feeder.hasImplementation() && !constantFeeder) {
342 debug(" Creating fixed implementation for feeder '", _networks.at(path).feeder.getName(), "'...");
343
344 if(_networks.at(path).consumers.size() == 1 && !_networks.at(path).useExternalTrigger) {
345 debug(" One consumer without external trigger, creating direct connection");
346 makeDirectConnectionForFeederWithImplementation(_networks.at(path));
347 }
348 else {
349 debug(std::format(" More than one consuming node ({}) or having external trigger ({}), setting up FanOut",
350 _networks.at(path).consumers.size(), _networks.at(path).useExternalTrigger));
351 makeFanOutConnectionForFeederWithImplementation(_networks.at(path), device, trigger);
352 }
353 }
354 else if(not constantFeeder) {
355 debug(" Feeder '", _networks.at(path).feeder.getName(), "' does not require a fixed implementation.");
356 assert(not trigger.isValid());
357 makeConnectionForFeederWithoutImplementation(_networks.at(path));
358 }
359 else { // constant feeder
360 debug(" Using constant feeder '", _networks.at(path).feeder.getName(), "'.");
361 makeConnectionForConstantFeeder(_networks.at(path));
362 }
363
364 // Mark circular networks
365 for(auto& node : _networks.at(path).consumers) {
366 // A variable network is a tree-like network of VariableNetworkNodes (one feeder and one or more multiple
367 // consumers) A circular network is a list of modules (EntityOwners) which have a circular dependency
368 auto circularNetwork = node.scanForCircularDepencency();
369 if(not circularNetwork.empty()) {
370 auto circularNetworkHash = boost::hash_range(circularNetwork.begin(), circularNetwork.end());
371 _app._circularDependencyNetworks[circularNetworkHash] = circularNetwork;
372 _app._circularNetworkInvalidityCounters[circularNetworkHash] = 0;
373
374 debug(" Circular network detected: " + proxy.getFullyQualifiedPath() + " is part of " +
375 std::to_string(circularNetworkHash));
376 }
377 }
378 debug();
379 }
380
381 /********************************************************************************************************************/
382
383 void ConnectionMaker::finalise() {
384 debug("Calling finalise()...");
385
386 debug("Preparing trigger networks");
387 debug("Collecting triggers");
388
389 // Collect all triggers, add a TriggerReceiver placeholder for every device associated with that trigger
390 std::list<Model::DeviceModuleProxy> dmProxyList;
391 auto triggerCollector = [&](auto proxy) { dmProxyList.push_back(proxy); };
392 _app.getModel().visit(triggerCollector, Model::depthFirstSearch, Model::keepDeviceModules);
393 for(auto& proxy : dmProxyList) {
394 auto trigger = proxy.getTrigger();
395 if(not trigger.isValid()) {
396 continue;
397 }
398 _triggers.insert(trigger);
399 VariableNetworkNode placeholder(proxy.getAliasOrCdd(), 0);
400 proxy.addVariable(trigger, placeholder);
401 }
402 debug(" Found " + std::to_string(_triggers.size()) + " trigger(s)");
403
404 debug("---------------------------");
405 debug("Finalising trigger networks");
406 debug("---------------------------");
407 for(auto trigger : _triggers) {
408 auto info = checkAndFinaliseNetwork(trigger);
409 _triggerNetworks.insert(trigger.getFullyQualifiedPath());
410 _networks.insert({trigger.getFullyQualifiedPath(), info});
411 debug(" trigger network: " + trigger.getFullyQualifiedPath());
412 }
413
414 debug("-------------------------");
415 debug("Finalising other networks");
416 debug("-------------------------");
417 auto connectingVisitor = [&](auto proxy) {
418 if(_triggerNetworks.count(proxy.getFullyQualifiedPath()) != 0) {
419 return;
420 }
421
422 _networks.insert({proxy.getFullyQualifiedPath(), checkAndFinaliseNetwork(proxy)});
423 };
424
425 // ChimeraTK::Model::keepParenthood - small optimisation for iterating the model only once
426 _app.getModel().visit(connectingVisitor, ChimeraTK::Model::depthFirstSearch, ChimeraTK::Model::keepProcessVariables,
427 ChimeraTK::Model::keepParenthood);
428 }
429
430 /********************************************************************************************************************/
431
432 void ConnectionMaker::connect() {
433 debug("Calling connect()...");
434
435 // Improve: Likely no need to distinguish trigger and normal networks here... Also just iterate _networks instead
436 // of the model!
437
438 debug("---------------------------");
439 debug("Connecting trigger networks");
440 debug("---------------------------");
441 for(auto trigger : _triggers) {
442 connectNetwork(trigger);
443 }
444
445 debug("-------------------------");
446 debug("Connecting other networks");
447 debug("-------------------------");
448 auto connectingVisitor = [&](auto proxy) {
449 if(_triggerNetworks.count(proxy.getFullyQualifiedPath()) != 0) {
450 return;
451 }
452
453 connectNetwork(proxy);
454 };
455
456 // ChimeraTK::Model::keepParenthood - small optimisation for iterating the model only once
457 _app.getModel().visit(connectingVisitor, ChimeraTK::Model::depthFirstSearch, ChimeraTK::Model::keepProcessVariables,
458 ChimeraTK::Model::keepParenthood);
459 }
460
461 /********************************************************************************************************************/
462
463 void ConnectionMaker::makeDirectConnectionForFeederWithImplementation(NetworkInformation& net) {
464 debug(" Making direct connection for feeder with implementation");
465
466 callForType(*net.valueType, [&](auto t) {
467 using UserType = decltype(t);
468
469 auto consumer = net.consumers.front();
470 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> feedingImpl;
471
472 if(net.feeder.getType() == NodeType::Device) {
473 feedingImpl = createDeviceVariable<UserType>(net.feeder);
474 }
475 else if(net.feeder.getType() == NodeType::ControlSystem) {
476 feedingImpl = getProcessVariable<UserType>(net.feeder);
477 }
478 else {
479 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
480 }
481
482 // We need a threaded fan-out most of the time, unless the consumer is an application node
483 // Then we have a thread in the application module already
484 auto needsFanOut{true};
485 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> consumingImpl;
486
487 switch(consumer.getType()) {
488 case NodeType::Application:
489 debug(" Node type is Application");
490 consumer.setAppAccessorImplementation(feedingImpl);
491 needsFanOut = false;
492 // If the Application consumer has the noInitialValueReadTag, mark the
493 // ExceptionHandlingDecorator within feedingImpl to skip waitForInitialValues().
494 // This is required because the module thread reads directly through the
495 // ExceptionHandlingDecorator (no FanOut), so blocking would affect
496 // the user's mainLoop().
497 if(consumer.getTags().contains(ChimeraTK::noInitialValueReadTag)) {
498 auto* ehd = dynamic_cast<ExceptionHandlingDecorator<UserType>*>(feedingImpl.get());
499 if(ehd) {
500 ehd->setSkipInitialValueWait(true);
501 }
502 }
503 break;
504 case NodeType::ControlSystem:
505 debug(" Node type is ControlSystem");
506 consumingImpl = getProcessVariable<UserType>(consumer);
507 break;
508 case NodeType::Device:
509 consumingImpl = createDeviceVariable<UserType>(consumer);
510 debug(" Node type is Device");
511 break;
512 case NodeType::TriggerReceiver: {
513 needsFanOut = false;
514 debug(" Node type is TriggerReceiver (Alias = " + consumer.getDeviceAlias() + ")");
515
516 // create the trigger fan out and store it in the map and the internalModuleList
517 auto triggerFanOut =
518 boost::make_shared<TriggerFanOut>(feedingImpl, *_app.getDeviceManager(consumer.getDeviceAlias()));
519 _app._internalModuleList.push_back(triggerFanOut);
520 net.triggerImpl[consumer.getDeviceAlias()] = triggerFanOut;
521 } break;
522 default:
523 throw ChimeraTK::logic_error("Unexpected node type!");
524 }
525
526 if(needsFanOut) {
527 debug(" needing an additional fan-out");
528 assert(consumingImpl != nullptr);
529
530 auto consumerImplPair = ConsumerImplementationPairs<UserType>{{consumingImpl, consumer}};
531 boost::shared_ptr<ThreadedFanOut<UserType>> threadedFanOut;
532 if(not net.feeder.getDirection().withReturn) {
533 debug(" No return channel");
534 threadedFanOut = boost::make_shared<ThreadedFanOut<UserType>>(feedingImpl, consumerImplPair);
535 }
536 else {
537 debug(" With return channel");
538 threadedFanOut = boost::make_shared<ThreadedFanOutWithReturn<UserType>>(feedingImpl, consumerImplPair);
539 }
540 _app._internalModuleList.push_back(threadedFanOut);
541 }
542 });
543 }
544
545 /********************************************************************************************************************/
546
547 void ConnectionMaker::makeFanOutConnectionForFeederWithImplementation(
548 NetworkInformation& net, const Model::DeviceModuleProxy& device, const Model::ProcessVariableProxy& trigger) {
549 // TODO needs sanity check?
550 auto feederTrigger = !net.useExternalTrigger && net.feeder.getMode() == UpdateMode::push;
551 assert(feederTrigger || net.useExternalTrigger || net.numberOfPollingConsumers == 1);
552
553 callForType(*net.valueType, [&](auto t) {
554 using UserType = decltype(t);
555
556 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> feedingImpl;
557 if(net.feeder.getType() == NodeType::Device) {
558 debug(" Device feeder, creating Device variable");
559 feedingImpl = createDeviceVariable<UserType>(net.feeder);
560 }
561 else if(net.feeder.getType() == NodeType::ControlSystem) {
562 debug(" CS feeder, creating CS variable");
563 feedingImpl = getProcessVariable<UserType>(net.feeder);
564 }
565 else {
566 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
567 }
568
569 boost::shared_ptr<FanOut<UserType>> fanOut;
570 boost::shared_ptr<ConsumingFanOut<UserType>> consumingFanOut;
571
572 // Fanouts need to know the consumers on construction, so we collect them first
573 auto consumerImplementationPairs = setConsumerImplementations<UserType>(net);
574
575 if(net.useExternalTrigger) {
576 assert(trigger.isValid());
577
578 debug(" Using external trigger (Alias = " + device.getAliasOrCdd() + ")");
579
580 auto& triggerNet = _networks.at(trigger.getFullyQualifiedPath());
581 auto jt = triggerNet.triggerImpl.find(device.getAliasOrCdd());
582 assert(jt != triggerNet.triggerImpl.end());
583
584 // if external trigger is enabled, use externally triggered threaded
585 // FanOut. Create one per external trigger impl.
586
587 jt->second->addNetwork(feedingImpl, consumerImplementationPairs);
588 }
589 else if(feederTrigger) {
590 debug(" Using feeder trigger.");
591 // if the trigger is provided by the pushing feeder, use the threaded
592 // version of the FanOut to distribute new values immediately to all
593 // consumers. Depending on whether we have a return channel or not, pick
594 // the right implementation of the FanOut
595 boost::shared_ptr<ThreadedFanOut<UserType>> threadedFanOut;
596 if(not net.feeder.getDirection().withReturn) {
597 debug(" No return channel");
598 threadedFanOut = boost::make_shared<ThreadedFanOut<UserType>>(feedingImpl, consumerImplementationPairs);
599 }
600 else {
601 debug(" With return channel");
602 threadedFanOut =
603 boost::make_shared<ThreadedFanOutWithReturn<UserType>>(feedingImpl, consumerImplementationPairs);
604 }
605 _app._internalModuleList.push_back(threadedFanOut);
606 fanOut = threadedFanOut;
607 }
608 else {
609 // Trigger by single poll-type consumer
610 debug(" No trigger, using consuming fanout.");
611 consumingFanOut = boost::make_shared<ConsumingFanOut<UserType>>(feedingImpl, consumerImplementationPairs);
612
613 // TODO Is this correct? we already added all consumer as slaves in the fanout constructor.
614 // Maybe assert that we only have a single poll-type node (is there a check in checkConnections?)
615 for(const auto& consumer : net.consumers) {
616 if(consumer.getMode() == UpdateMode::poll) {
617 consumer.setAppAccessorImplementation<UserType>(consumingFanOut);
618 // If the poll-type consumer has the noInitialValueReadTag, mark the
619 // ExceptionHandlingDecorator within feedingImpl to skip waitForInitialValues().
620 // This is the only path where the module thread reads directly through the
621 // ExceptionHandlingDecorator (via ConsumingFanOut), so blocking would affect
622 // the user's mainLoop().
623 if(consumer.getTags().contains(ChimeraTK::noInitialValueReadTag)) {
624 auto* ehd = dynamic_cast<ExceptionHandlingDecorator<UserType>*>(feedingImpl.get());
625 if(ehd) {
626 ehd->setSkipInitialValueWait(true);
627 }
628 }
629 break;
630 }
631 }
632 }
633 });
634 }
635
636 /********************************************************************************************************************/
637
638 template<typename UserType>
639 void NetworkVisitor::createProcessVariable(const VariableNetworkNode& node, size_t length, const std::string& unit,
640 const std::string& description, AccessModeFlags flags) {
641 // Implementation note: This function needs to create the PV in the control system PV manager, so the control system
642 // adapter already sees the PVs before calling run().
643 // It also has to decorate the implementation with the testable mode decorator (if in testable mode), because this
644 // must happen before the TestFacility hands out decorated PVs to the tests.
645
646 // If we are generating the XML file only, there will be no PV manager and we will not use the PVs later anyway,
647 // so simply do nothing in that case. Note that Application::initialise() checks for the presence of a PV manager,
648 // so if the real application starts we have the guarantee of the presence of a PV manager.
649 if(!_app.getPVManager()) {
650 return;
651 }
652
653 SynchronizationDirection dir;
654 if(node.getDirection().withReturn) {
655 dir = SynchronizationDirection::bidirectional;
656 }
657 else if(node.getDirection().dir == VariableDirection::feeding) {
658 dir = SynchronizationDirection::controlSystemToDevice;
659 }
660 else {
661 dir = SynchronizationDirection::deviceToControlSystem;
662 }
663
664 debug(" calling createProcessArray()");
665
666 auto pv = _app.getPVManager()->createProcessArray<UserType>(
667 dir, node.getPublicName(), length, unit, description, {}, 3, flags);
668
669 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> pvImpl = pv;
670
671 if(node.getDirection().dir == VariableDirection::feeding) {
672 // Wrap push-type CS->App PVs in testable mode decorator
673 if(flags.has(AccessMode::wait_for_new_data)) {
674 auto varId = detail::TestableMode::getNextVariableId();
675 _app._pvIdMap[pv->getUniqueId()] = varId;
676 pvImpl = _app.getTestableMode().decorate<UserType>(
677 pvImpl, detail::TestableMode::DecoratorType::READ, "ControlSystem:" + node.getPublicName(), varId);
678 }
679 // poll-type CS->App PVs are not wrapped
680 }
681 else if(dir == SynchronizationDirection::bidirectional) {
682 // App->CS PVs are only wrapped into testablemode decorator if they are bidirectional
683 auto varId = detail::TestableMode::getNextVariableId();
684 _app._pvIdMap[pv->getUniqueId()] = varId;
685 pvImpl = _app.getTestableMode().decorate<UserType>(
686 pvImpl, detail::TestableMode::DecoratorType::READ, "ControlSystem:" + node.getPublicName());
687 }
688
689 boost::fusion::at_key<UserType>(_decoratedPvImpls.table)[node.getPublicName()] = pvImpl;
690 }
691
692 /********************************************************************************************************************/
693
694 template<typename UserType>
695 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> ConnectionMaker::getProcessVariable(
696 const VariableNetworkNode& node) {
697 return boost::fusion::at_key<UserType>(_decoratedPvImpls.table).at(node.getPublicName());
698 }
699 /********************************************************************************************************************/
700
701 template<typename UserType>
702 boost::shared_ptr<NDRegisterAccessor<UserType>> ConnectionMaker::createDeviceVariable(
703 VariableNetworkNode const& node) {
704 const auto& deviceAlias = node.getDeviceAlias();
705 const auto& registerName = node.getRegisterName();
706 auto direction = node.getDirection();
707 auto mode = node.getMode();
708 auto nElements = node.getNumberOfElements();
709
710 auto dev = _app._deviceManagerMap.at(deviceAlias)->getDevice().getBackend();
711
712 // use wait_for_new_data mode if push update mode was requested
713 // Feeding to the network means reading from a device to feed it into the network.
714 AccessModeFlags flags{};
715 if(mode == UpdateMode::push && direction.dir == VariableDirection::feeding) {
716 flags = {AccessMode::wait_for_new_data};
717 }
718
719 // obtain the register accessor from the device
720 auto accessor = dev->getRegisterAccessor<UserType>(registerName, nElements, 0, flags);
721
722 // Receiving accessors should be faulty after construction,
723 // see data validity propagation spec 2.6.1
724 if(node.getDirection().dir == VariableDirection::feeding || node.getDirection().withReturn) {
725 accessor->setDataValidity(DataValidity::faulty);
726 }
727
728 // decorate push-type feeders with testable mode decorator, if needed
729 if(mode == UpdateMode::push && direction.dir == VariableDirection::feeding) {
730 accessor = _app.getTestableMode().decorate(accessor, detail::TestableMode::DecoratorType::READ);
731 }
732
733 auto recoveryHelper = boost::make_shared<RecoveryHelper>();
734
735 if(node.getDirection().dir == VariableDirection::consuming && node.getDirection().withReturn) {
736 accessor = boost::make_shared<ReverseRecoveryDecorator<UserType>>(accessor, recoveryHelper);
737 }
738
739 return boost::make_shared<ExceptionHandlingDecorator<UserType>>(accessor, node, recoveryHelper);
740 }
741
742 /********************************************************************************************************************/
743
744 template<typename UserType>
745 ConsumerImplementationPairs<UserType> ConnectionMaker::setConsumerImplementations(NetworkInformation& net) {
746 debug(" setConsumerImplementations");
748
749 for(const auto& consumer : net.consumers) {
751 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>(), consumer};
752
753 if(consumer.getType() == NodeType::Application) {
754 debug(" Node type is Application: " + consumer.getQualifiedName());
755 auto impls = createApplicationVariable<UserType>(consumer);
756 consumer.setAppAccessorImplementation<UserType>(impls.second);
757 pair = std::make_pair(impls.first, consumer);
758 }
759 else if(consumer.getType() == NodeType::ControlSystem) {
760 debug(" Node type is ControlSystem");
761 auto impl = getProcessVariable<UserType>(consumer);
762 pair = std::make_pair(impl, consumer);
763 }
764 else if(consumer.getType() == NodeType::Device) {
765 debug(" Node type is Device");
766 auto impl = createDeviceVariable<UserType>(consumer);
767 pair = std::make_pair(impl, consumer);
768 }
769 else if(consumer.getType() == NodeType::TriggerReceiver) {
770 debug(" Node type is TriggerReceiver");
771 auto triggerConnection = createApplicationVariable<UserType>(net.feeder);
772
773 auto triggerFanOut = boost::make_shared<TriggerFanOut>(
774 triggerConnection.second, *_app.getDeviceManager(consumer.getDeviceAlias()));
775 _app._internalModuleList.push_back(triggerFanOut);
776 net.triggerImpl[consumer.getDeviceAlias()] = triggerFanOut;
777
778 pair = std::make_pair(triggerConnection.first, consumer);
779 }
780 else {
781 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
782 }
783
784 consumerImplPairs.push_back(pair);
785 }
786
787 return consumerImplPairs;
788 }
789
790 /********************************************************************************************************************/
791
792 template<typename UserType>
793 std::pair<boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>,
794 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>>
795 ConnectionMaker::createApplicationVariable(VariableNetworkNode const& node, VariableNetworkNode const& consumer) {
796 // obtain the meta data
797 size_t nElements = node.getNumberOfElements();
798 std::string name = node.getName();
799 assert(not name.empty());
800 AccessModeFlags flags = {};
801 if(consumer.isValid()) {
802 if(consumer.getMode() == UpdateMode::push) {
803 flags = {AccessMode::wait_for_new_data};
804 }
805 }
806 else {
807 if(node.getMode() == UpdateMode::push) {
808 flags = {AccessMode::wait_for_new_data};
809 }
810 }
811
812 // create the ProcessArray for the proper UserType
813 std::pair<boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>,
814 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>>
815 pvarPair;
816 if(consumer.isValid()) {
817 assert(node.getDirection().withReturn == consumer.getDirection().withReturn);
818 }
819
820 if(!node.getDirection().withReturn) {
821 pvarPair = createSynchronizedProcessArray<UserType>(
822 nElements, name, node.getUnit(), node.getDescription(), {}, 3, flags);
823 }
824 else {
825 pvarPair = createBidirectionalSynchronizedProcessArray<UserType>(
826 nElements, name, node.getUnit(), node.getDescription(), {}, 3, flags);
827 }
828 assert(pvarPair.first->getName() != "");
829 assert(pvarPair.second->getName() != "");
830
831 if(flags.has(AccessMode::wait_for_new_data)) {
832 pvarPair = _app.getTestableMode().decorate(pvarPair, node, consumer);
833 }
834
835 // if debug mode was requested for either node, decorate both accessors
836 if(_app._debugMode_variableList.count(node.getUniqueId()) ||
837 (consumer.getType() != NodeType::invalid && _app._debugMode_variableList.count(consumer.getUniqueId()))) {
838 if(consumer.getType() != NodeType::invalid) {
839 assert(node.getDirection().dir == VariableDirection::feeding);
840 assert(consumer.getDirection().dir == VariableDirection::consuming);
841 pvarPair.first =
842 boost::make_shared<DebugPrintAccessorDecorator<UserType>>(pvarPair.first, node.getQualifiedName());
843 pvarPair.second =
844 boost::make_shared<DebugPrintAccessorDecorator<UserType>>(pvarPair.second, consumer.getQualifiedName());
845 }
846 else {
847 pvarPair.first =
848 boost::make_shared<DebugPrintAccessorDecorator<UserType>>(pvarPair.first, node.getQualifiedName());
849 pvarPair.second =
850 boost::make_shared<DebugPrintAccessorDecorator<UserType>>(pvarPair.second, node.getQualifiedName());
851 }
852 }
853
854 // return the pair
855 return pvarPair;
856 }
857
858 /********************************************************************************************************************/
859
860 void ChimeraTK::ConnectionMaker::makeConnectionForFeederWithoutImplementation(NetworkInformation& net) {
861 // we should be left with an application feeder node
862 if(net.feeder.getType() != NodeType::Application) {
863 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
864 }
865
866 if(net.consumers.size() == 1) {
867 debug(" Network of two nodes, connect directly");
868
869 const auto& consumer = net.consumers.front();
870
871 switch(consumer.getType()) {
873 debug(" Node type is Application");
874 callForType(*net.valueType, [&](auto t) {
875 using UserType = decltype(t);
876 auto impls = createApplicationVariable<UserType>(net.feeder, consumer);
877 net.feeder.setAppAccessorImplementation<UserType>(impls.first);
878 consumer.setAppAccessorImplementation<UserType>(impls.second);
879 });
880 break;
882 debug(" Node type is ControlSystem");
883 callForType(*net.valueType, [&](auto t) {
884 using UserType = decltype(t);
885 auto impl = getProcessVariable<UserType>(consumer);
886 net.feeder.setAppAccessorImplementation(impl);
887 });
888 break;
889 case NodeType::Device:
890 debug(" Node type is Device");
891 callForType(*net.valueType, [&](auto t) {
892 using UserType = decltype(t);
893 auto impl = createDeviceVariable<UserType>(consumer);
894 net.feeder.setAppAccessorImplementation(impl);
895 });
896 break;
898 debug(" Node type is TriggerReceiver");
899
900 // create a PV implementation to connect the Application with the TriggerFanOut.
901 {
902 boost::shared_ptr<TransferElement> consumingImpl;
903 callForType(*net.valueType, [&](auto t) {
904 using UserType = decltype(t);
905 auto impls = createApplicationVariable<UserType>(net.feeder, consumer);
906 net.feeder.setAppAccessorImplementation<UserType>(impls.first);
907 consumingImpl = impls.second;
908 });
909
910 // create the trigger fan out and store it in the map and the internalModuleList
911 auto triggerFanOut =
912 boost::make_shared<TriggerFanOut>(consumingImpl, *_app.getDeviceManager(consumer.getDeviceAlias()));
913 _app._internalModuleList.emplace_back(triggerFanOut);
914 net.triggerImpl[consumer.getDeviceAlias()] = triggerFanOut;
915 }
916
917 break;
919 debug(" Node type is Constant");
920 net.feeder.setAppAccessorConstImplementation(net.feeder);
921 break;
922 default:
923 throw ChimeraTK::logic_error("Unexpected node type!");
924 }
925 }
926 else if(net.consumers.size() > 1) {
927 debug(std::format(" More than one consumer, using fan-out as feeder impl (with return: {})",
928 net.feeder.getDirection().withReturn));
929 callForType(*net.valueType, [&](auto t) {
930 using UserType = decltype(t);
931 auto consumerImplementationPairs = setConsumerImplementations<UserType>(net);
932
933 // create FanOut and use it as the feeder implementation
934 auto fanOut = boost::make_shared<FeedingFanOut<UserType>>(net.feeder.getName(), net.unit, net.description,
935 net.valueLength, net.feeder.getDirection().withReturn, consumerImplementationPairs);
936 net.feeder.setAppAccessorImplementation<UserType>(fanOut);
937 });
938 }
939 else {
940 debug(" No consumer (presumably optimised out)");
941 net.feeder.setAppAccessorConstImplementation(VariableNetworkNode(net.valueType, true, net.valueLength));
942 }
943 }
944
945 /********************************************************************************************************************/
946
947 void ConnectionMaker::makeConnectionForConstantFeeder(NetworkInformation& net) {
948 assert(net.feeder.getType() == NodeType::Constant);
949 for(const auto& consumer : net.consumers) {
950 AccessModeFlags flags{};
951 if(consumer.getMode() == UpdateMode::push) {
952 flags = {AccessMode::wait_for_new_data};
953 }
954
955 callForType(*net.valueType, [&](auto t) {
956 using UserType = decltype(t);
957 // each consumer gets its own implementation
958 if(consumer.getType() == NodeType::Application) {
959 consumer.setAppAccessorConstImplementation(net.feeder);
960 }
961 else if(consumer.getType() == NodeType::ControlSystem) {
962 throw ChimeraTK::logic_error("Using constants as feeders for control system variables is not supported!");
963 }
964 else if(consumer.getType() == NodeType::Device) {
965 // We register the required accessor as a recovery accessor. This is just a bare RegisterAccessor without
966 // any decorations directly from the backend.
967 auto deviceManager = _app.getDeviceManager(consumer.getDeviceAlias());
968 auto dev = deviceManager->getDevice().getBackend();
969 auto impl =
970 dev->getRegisterAccessor<UserType>(consumer.getRegisterName(), consumer.getNumberOfElements(), 0, {});
971 auto catalog = deviceManager->getDevice().getRegisterCatalogue();
972 auto tags = catalog.getRegister(consumer.getRegisterName()).getTags();
973
974 // Set the value
975 impl->accessChannel(0) =
976 std::vector<UserType>(consumer.getNumberOfElements(), net.feeder.getConstantValue<UserType>());
977
978 // The accessor implementation already has its data in the user buffer. We now just have to add a valid
979 // version number and have a recovery accessors (RecoveryHelper to be exact) which we can register at the
980 // DeviceModule. As this is a constant we don't need to change it later and don't have to store it somewhere
981 // else.
982 // If this register is considered for reverse recovery (Device pushes to application), do not add an
983 // accessor at all, since pushing to a constant does not make any sense.
984 if(!tags.contains(ChimeraTK::SystemTags::reverseRecovery)) {
985 deviceManager->addRecoveryAccessor(
986 boost::make_shared<RecoveryHelper>(impl, VersionNumber(), deviceManager->writeOrder()));
987 }
988 }
989 else if(consumer.getType() == NodeType::TriggerReceiver) {
990 throw ChimeraTK::logic_error("Using constants as triggers is not supported!");
991 }
992 else {
993 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
994 }
995 });
996 }
997 }
998
999 /********************************************************************************************************************/
1000
1001 void ConnectionMaker::optimiseUnmappedVariables(const std::set<std::string>& names) {
1002 debug("-----------------------------");
1003 debug("Optimising unmapped variables");
1004 debug("-----------------------------");
1005
1006 for(const auto& name : names) {
1007 debug("Looking at network " + name);
1008 auto& network = _networks.at(name);
1009 // if the control system is the feeder, change it into a constant
1010 if(network.feeder.getType() == NodeType::ControlSystem) {
1011 if(network.useReverseRecovery) {
1012 // We need to promote the accessor with the reverse recovery tag to the network feeder
1013 // to prevent writing down the constant value into the device and propagating the
1014 // recovery value to the other consumers instead.
1015 auto reverseConsumer = std::ranges::find_if(network.consumers, [](auto& consumer) {
1016 return consumer.getType() == NodeType::Device &&
1017 consumer.getTags().contains(ChimeraTK::SystemTags::reverseRecovery);
1018 });
1019 if(reverseConsumer->isReadable()) {
1020 debug(std::format(" Promoting reverse consumer {} to feeder", reverseConsumer->getName()));
1021 network.feeder = *reverseConsumer;
1022 network.consumers.remove(*reverseConsumer);
1023 }
1024 else {
1025 debug(std::format(
1026 " Reverse consumer {} is not readable, adding constant feeder instead", reverseConsumer->getName()));
1027 network.feeder = VariableNetworkNode(network.valueType, true, network.valueLength);
1028 }
1029 }
1030 else {
1031 debug(" Adding constant feeder");
1032 network.feeder = VariableNetworkNode(network.valueType, true, network.valueLength);
1033 }
1034 }
1035 else {
1036 // control system is a consumer: remove it from the list of consumers
1037 debug(" Dropping CS consumer");
1038 network.consumers.remove_if([](auto& consumer) { return consumer.getType() == NodeType::ControlSystem; });
1039 }
1040 }
1041 }
1042
1043} // namespace ChimeraTK
Pseudo type to identify nodes which can have arbitrary types.
std::list< boost::shared_ptr< InternalModule > > _internalModuleList
List of InternalModules.
boost::shared_ptr< DeviceManager > getDeviceManager(const std::string &aliasOrCDD)
Return the DeviceManager for the given alias name or CDD.
void optimiseUnmappedVariables(const std::set< std::string > &names)
Execute the optimisation request from the control system adapter (remove unused variables)
static constexpr std::string_view namePrefixConstant
Prefix for constants created by constant().
Decorator of the NDRegisterAccessor which facilitates tests of the application.
const std::vector< std::shared_ptr< VariableNetworkNode > > & getNodes() const
Return all VariableNetworkNodes for this variable.
Definition Model.cc:381
const std::string & getName() const
Get the name of the ProcessVariable.
Definition Model.cc:375
const std::unordered_set< std::string > & getTags() const
Return all tags attached to this variable.
Definition Model.cc:387
auto visit(VISITOR visitor, Args... args) const
Traverse the model using the specified filter and call the visitor functor for each ModuleGroup,...
Definition Model.h:1451
std::string getFullyQualifiedPath() const
Return the fully qualified path.
Definition Model.cc:25
Base class for ApplicationModule and DeviceModule, to have a common interface for these module types.
Definition Module.h:21
void debug(Args &&...)
NetworkInformation checkAndFinaliseNetwork(Model::ProcessVariableProxy &proxy)
void finaliseNetwork(NetworkInformation &net)
std::map< std::string, NetworkInformation > _networks
NetworkInformation checkNetwork(Model::ProcessVariableProxy &proxy)
Class describing a node of a variable network.
void dump(std::ostream &stream=std::cout) const
Print node information to specified stream.
NodeType getType() const
Getter for the properties.
const std::string & getRegisterName() const
const std::string & getPublicName() const
VariableDirection getDirection() const
const std::string & getDeviceAlias() const
InvalidityTracer application module.
constexpr char noInitialValueReadTag[]
System tag to mark an accessor which should be excluded from the initial value read during applicatio...
std::list< std::pair< boost::shared_ptr< ChimeraTK::NDRegisterAccessor< UserType > >, VariableNetworkNode > > ConsumerImplementationPairs
Definition FanOut.h:18
Logger::StreamProxy logger(Logger::Severity severity, std::string context)
Convenience function to obtain the logger stream.
Definition Logger.h:156
const Model::ProcessVariableProxy * proxy
std::list< VariableNetworkNode > consumers
Struct to define the direction of variables.
Definition Flags.h:13
enum ChimeraTK::VariableDirection::@0 dir
Enum to define directions of variables.
bool withReturn
Presence of return channel.
Definition Flags.h:21