ChimeraTK-ApplicationCore 04.06.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 if(not _debugConnections) {
299 return;
300 }
301
302 // Fold expression printer from https://en.cppreference.com/w/cpp/language/fold
303 (logger(Logger::Severity::debug, "ConnectionMaker") << ... << args) << std::endl;
304 }
305
306 /********************************************************************************************************************/
307 /* ConnectionMaker implementations */
308 /********************************************************************************************************************/
309
310 void ConnectionMaker::connectNetwork(Model::ProcessVariableProxy& proxy) {
311 auto path = proxy.getFullyQualifiedPath();
312 debug("Network found: ", path);
313
314 auto triggerFinder = [&](auto p) {
315 auto deviceTrigger = p.getTrigger();
316
317 if(deviceTrigger.isValid()) {
318 debug(" Found Feeding device ", p.getAliasOrCdd(), " with trigger ", p.getTrigger().getFullyQualifiedPath());
319 }
320 else {
321 debug(" Feeding from device ", p.getAliasOrCdd(), " but without any trigger");
322 }
323
324 return std::make_pair(deviceTrigger, p);
325 };
326
327 Model::ProcessVariableProxy trigger{};
328 Model::DeviceModuleProxy device{};
329
330 // Use external trigger if feeder is poll-type and number of poll-type consumers != 1.
331 // If there is exactly one poll-type consumer, transfers will be triggered by that consumer.
332 if(_networks.at(path).feeder.getMode() == UpdateMode::poll && _networks.at(path).numberOfPollingConsumers != 1) {
333 _networks.at(path).useExternalTrigger = true;
334 std::tie(trigger, device) =
335 proxy.visit(triggerFinder, Model::adjacentInSearch, Model::keepPvAccess, Model::keepDeviceModules,
336 Model::returnFirstHit(std::make_pair(Model::ProcessVariableProxy{}, Model::DeviceModuleProxy{})));
337 if(!trigger.isValid()) {
338 throw ChimeraTK::logic_error(
339 "Poll-Type feeder " + _networks.at(path).feeder.getName() + " needs trigger, but none provided");
340 }
341 }
342
343 auto constantFeeder = _networks.at(path).feeder.getType() == NodeType::Constant;
344
345 if(_networks.at(path).feeder.hasImplementation() && !constantFeeder) {
346 debug(" Creating fixed implementation for feeder '", _networks.at(path).feeder.getName(), "'...");
347
348 if(_networks.at(path).consumers.size() == 1 && !_networks.at(path).useExternalTrigger) {
349 debug(" One consumer without external trigger, creating direct connection");
350 makeDirectConnectionForFeederWithImplementation(_networks.at(path));
351 }
352 else {
353 debug(std::format(" More than one consuming node ({}) or having external trigger ({}), setting up FanOut",
354 _networks.at(path).consumers.size(), _networks.at(path).useExternalTrigger));
355 makeFanOutConnectionForFeederWithImplementation(_networks.at(path), device, trigger);
356 }
357 }
358 else if(not constantFeeder) {
359 debug(" Feeder '", _networks.at(path).feeder.getName(), "' does not require a fixed implementation.");
360 assert(not trigger.isValid());
361 makeConnectionForFeederWithoutImplementation(_networks.at(path));
362 }
363 else { // constant feeder
364 debug(" Using constant feeder '", _networks.at(path).feeder.getName(), "'.");
365 makeConnectionForConstantFeeder(_networks.at(path));
366 }
367
368 // Mark circular networks
369 for(auto& node : _networks.at(path).consumers) {
370 // A variable network is a tree-like network of VariableNetworkNodes (one feeder and one or more multiple
371 // consumers) A circular network is a list of modules (EntityOwners) which have a circular dependency
372 auto circularNetwork = node.scanForCircularDepencency();
373 if(not circularNetwork.empty()) {
374 auto circularNetworkHash = boost::hash_range(circularNetwork.begin(), circularNetwork.end());
375 _app._circularDependencyNetworks[circularNetworkHash] = circularNetwork;
376 _app._circularNetworkInvalidityCounters[circularNetworkHash] = 0;
377
378 debug(" Circular network detected: " + proxy.getFullyQualifiedPath() + " is part of " +
379 std::to_string(circularNetworkHash));
380 }
381 }
382 debug();
383 }
384
385 /********************************************************************************************************************/
386
387 void ConnectionMaker::finalise() {
388 debug("Calling finalise()...");
389
390 _app.getTestableMode()._debugDecorating = _debugConnections;
391
392 debug("Preparing trigger networks");
393 debug("Collecting triggers");
394
395 // Collect all triggers, add a TriggerReceiver placeholder for every device associated with that trigger
396 std::list<Model::DeviceModuleProxy> dmProxyList;
397 auto triggerCollector = [&](auto proxy) { dmProxyList.push_back(proxy); };
398 _app.getModel().visit(triggerCollector, Model::depthFirstSearch, Model::keepDeviceModules);
399 for(auto& proxy : dmProxyList) {
400 auto trigger = proxy.getTrigger();
401 if(not trigger.isValid()) {
402 continue;
403 }
404 _triggers.insert(trigger);
405 VariableNetworkNode placeholder(proxy.getAliasOrCdd(), 0);
406 proxy.addVariable(trigger, placeholder);
407 }
408 debug(" Found " + std::to_string(_triggers.size()) + " trigger(s)");
409
410 debug("---------------------------");
411 debug("Finalising trigger networks");
412 debug("---------------------------");
413 for(auto trigger : _triggers) {
414 auto info = checkAndFinaliseNetwork(trigger);
415 _triggerNetworks.insert(trigger.getFullyQualifiedPath());
416 _networks.insert({trigger.getFullyQualifiedPath(), info});
417 debug(" trigger network: " + trigger.getFullyQualifiedPath());
418 }
419
420 debug("-------------------------");
421 debug("Finalising other networks");
422 debug("-------------------------");
423 auto connectingVisitor = [&](auto proxy) {
424 if(_triggerNetworks.count(proxy.getFullyQualifiedPath()) != 0) {
425 return;
426 }
427
428 _networks.insert({proxy.getFullyQualifiedPath(), checkAndFinaliseNetwork(proxy)});
429 };
430
431 // ChimeraTK::Model::keepParenthood - small optimisation for iterating the model only once
432 _app.getModel().visit(connectingVisitor, ChimeraTK::Model::depthFirstSearch, ChimeraTK::Model::keepProcessVariables,
433 ChimeraTK::Model::keepParenthood);
434 }
435
436 /********************************************************************************************************************/
437
438 void ConnectionMaker::connect() {
439 debug("Calling connect()...");
440
441 _app.getTestableMode()._debugDecorating = _debugConnections;
442
443 // Improve: Likely no need to distinguish trigger and normal networks here... Also just iterate _networks instead
444 // of the model!
445
446 debug("---------------------------");
447 debug("Connecting trigger networks");
448 debug("---------------------------");
449 for(auto trigger : _triggers) {
450 connectNetwork(trigger);
451 }
452
453 debug("-------------------------");
454 debug("Connecting other networks");
455 debug("-------------------------");
456 auto connectingVisitor = [&](auto proxy) {
457 if(_triggerNetworks.count(proxy.getFullyQualifiedPath()) != 0) {
458 return;
459 }
460
461 connectNetwork(proxy);
462 };
463
464 // ChimeraTK::Model::keepParenthood - small optimisation for iterating the model only once
465 _app.getModel().visit(connectingVisitor, ChimeraTK::Model::depthFirstSearch, ChimeraTK::Model::keepProcessVariables,
466 ChimeraTK::Model::keepParenthood);
467 }
468
469 /********************************************************************************************************************/
470
471 void ConnectionMaker::makeDirectConnectionForFeederWithImplementation(NetworkInformation& net) {
472 debug(" Making direct connection for feeder with implementation");
473
474 callForType(*net.valueType, [&](auto t) {
475 using UserType = decltype(t);
476
477 auto consumer = net.consumers.front();
478 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> feedingImpl;
479
480 if(net.feeder.getType() == NodeType::Device) {
481 feedingImpl = createDeviceVariable<UserType>(net.feeder);
482 }
483 else if(net.feeder.getType() == NodeType::ControlSystem) {
484 feedingImpl = getProcessVariable<UserType>(net.feeder);
485 }
486 else {
487 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
488 }
489
490 // We need a threaded fan-out most of the time, unless the consumer is an application node
491 // Then we have a thread in the application module already
492 auto needsFanOut{true};
493 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> consumingImpl;
494
495 switch(consumer.getType()) {
496 case NodeType::Application:
497 debug(" Node type is Application");
498 consumer.setAppAccessorImplementation(feedingImpl);
499 needsFanOut = false;
500 break;
501 case NodeType::ControlSystem:
502 debug(" Node type is ControlSystem");
503 consumingImpl = getProcessVariable<UserType>(consumer);
504 break;
505 case NodeType::Device:
506 consumingImpl = createDeviceVariable<UserType>(consumer);
507 debug(" Node type is Device");
508 break;
509 case NodeType::TriggerReceiver: {
510 needsFanOut = false;
511 debug(" Node type is TriggerReceiver (Alias = " + consumer.getDeviceAlias() + ")");
512
513 // create the trigger fan out and store it in the map and the internalModuleList
514 auto triggerFanOut =
515 boost::make_shared<TriggerFanOut>(feedingImpl, *_app.getDeviceManager(consumer.getDeviceAlias()));
516 _app._internalModuleList.push_back(triggerFanOut);
517 net.triggerImpl[consumer.getDeviceAlias()] = triggerFanOut;
518 } break;
519 default:
520 throw ChimeraTK::logic_error("Unexpected node type!");
521 }
522
523 if(needsFanOut) {
524 debug(" needing an additional fan-out");
525 assert(consumingImpl != nullptr);
526
527 auto consumerImplPair = ConsumerImplementationPairs<UserType>{{consumingImpl, consumer}};
528 boost::shared_ptr<ThreadedFanOut<UserType>> threadedFanOut;
529 if(not net.feeder.getDirection().withReturn) {
530 debug(" No return channel");
531 threadedFanOut = boost::make_shared<ThreadedFanOut<UserType>>(feedingImpl, consumerImplPair);
532 }
533 else {
534 debug(" With return channel");
535 threadedFanOut = boost::make_shared<ThreadedFanOutWithReturn<UserType>>(feedingImpl, consumerImplPair);
536 }
537 _app._internalModuleList.push_back(threadedFanOut);
538 }
539 });
540 }
541
542 /********************************************************************************************************************/
543
544 void ConnectionMaker::makeFanOutConnectionForFeederWithImplementation(
545 NetworkInformation& net, const Model::DeviceModuleProxy& device, const Model::ProcessVariableProxy& trigger) {
546 // TODO needs sanity check?
547 auto feederTrigger = !net.useExternalTrigger && net.feeder.getMode() == UpdateMode::push;
548 assert(feederTrigger || net.useExternalTrigger || net.numberOfPollingConsumers == 1);
549
550 callForType(*net.valueType, [&](auto t) {
551 using UserType = decltype(t);
552
553 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> feedingImpl;
554 if(net.feeder.getType() == NodeType::Device) {
555 debug(" Device feeder, creating Device variable");
556 feedingImpl = createDeviceVariable<UserType>(net.feeder);
557 }
558 else if(net.feeder.getType() == NodeType::ControlSystem) {
559 debug(" CS feeder, creating CS variable");
560 feedingImpl = getProcessVariable<UserType>(net.feeder);
561 }
562 else {
563 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
564 }
565
566 boost::shared_ptr<FanOut<UserType>> fanOut;
567 boost::shared_ptr<ConsumingFanOut<UserType>> consumingFanOut;
568
569 // Fanouts need to know the consumers on construction, so we collect them first
570 auto consumerImplementationPairs = setConsumerImplementations<UserType>(net);
571
572 if(net.useExternalTrigger) {
573 assert(trigger.isValid());
574
575 debug(" Using external trigger (Alias = " + device.getAliasOrCdd() + ")");
576
577 auto& triggerNet = _networks.at(trigger.getFullyQualifiedPath());
578 auto jt = triggerNet.triggerImpl.find(device.getAliasOrCdd());
579 assert(jt != triggerNet.triggerImpl.end());
580
581 // if external trigger is enabled, use externally triggered threaded
582 // FanOut. Create one per external trigger impl.
583
584 jt->second->addNetwork(feedingImpl, consumerImplementationPairs);
585 }
586 else if(feederTrigger) {
587 debug(" Using feeder trigger.");
588 // if the trigger is provided by the pushing feeder, use the threaded
589 // version of the FanOut to distribute new values immediately to all
590 // consumers. Depending on whether we have a return channel or not, pick
591 // the right implementation of the FanOut
592 boost::shared_ptr<ThreadedFanOut<UserType>> threadedFanOut;
593 if(not net.feeder.getDirection().withReturn) {
594 debug(" No return channel");
595 threadedFanOut = boost::make_shared<ThreadedFanOut<UserType>>(feedingImpl, consumerImplementationPairs);
596 }
597 else {
598 debug(" With return channel");
599 threadedFanOut =
600 boost::make_shared<ThreadedFanOutWithReturn<UserType>>(feedingImpl, consumerImplementationPairs);
601 }
602 _app._internalModuleList.push_back(threadedFanOut);
603 fanOut = threadedFanOut;
604 }
605 else {
606 // Trigger by single poll-type consumer
607 debug(" No trigger, using consuming fanout.");
608 consumingFanOut = boost::make_shared<ConsumingFanOut<UserType>>(feedingImpl, consumerImplementationPairs);
609
610 // TODO Is this correct? we already added all consumer as slaves in the fanout constructor.
611 // Maybe assert that we only have a single poll-type node (is there a check in checkConnections?)
612 for(const auto& consumer : net.consumers) {
613 if(consumer.getMode() == UpdateMode::poll) {
614 consumer.setAppAccessorImplementation<UserType>(consumingFanOut);
615 break;
616 }
617 }
618 }
619 });
620 }
621
622 /********************************************************************************************************************/
623
624 template<typename UserType>
625 void NetworkVisitor::createProcessVariable(const VariableNetworkNode& node, size_t length, const std::string& unit,
626 const std::string& description, AccessModeFlags flags) {
627 // Implementation note: This function needs to create the PV in the control system PV manager, so the control system
628 // adapter already sees the PVs before calling run().
629 // It also has to decorate the implementation with the testable mode decorator (if in testable mode), because this
630 // must happen before the TestFacility hands out decorated PVs to the tests.
631
632 // If we are generating the XML file only, there will be no PV manager and we will not use the PVs later anyway,
633 // so simply do nothing in that case. Note that Application::initialise() checks for the presence of a PV manager,
634 // so if the real application starts we have the guarantee of the presence of a PV manager.
635 if(!_app.getPVManager()) {
636 return;
637 }
638
639 SynchronizationDirection dir;
640 if(node.getDirection().withReturn) {
641 dir = SynchronizationDirection::bidirectional;
642 }
643 else if(node.getDirection().dir == VariableDirection::feeding) {
644 dir = SynchronizationDirection::controlSystemToDevice;
645 }
646 else {
647 dir = SynchronizationDirection::deviceToControlSystem;
648 }
649
650 debug(" calling createProcessArray()");
651
652 auto pv = _app.getPVManager()->createProcessArray<UserType>(
653 dir, node.getPublicName(), length, unit, description, {}, 3, flags);
654
655 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> pvImpl = pv;
656
657 if(node.getDirection().dir == VariableDirection::feeding) {
658 // Wrap push-type CS->App PVs in testable mode decorator
659 if(flags.has(AccessMode::wait_for_new_data)) {
660 auto varId = detail::TestableMode::getNextVariableId();
661 _app._pvIdMap[pv->getUniqueId()] = varId;
662 pvImpl = _app.getTestableMode().decorate<UserType>(
663 pvImpl, detail::TestableMode::DecoratorType::READ, "ControlSystem:" + node.getPublicName(), varId);
664 }
665 // poll-type CS->App PVs are not wrapped
666 }
667 else if(dir == SynchronizationDirection::bidirectional) {
668 // App->CS PVs are only wrapped into testablemode decorator if they are bidirectional
669 auto varId = detail::TestableMode::getNextVariableId();
670 _app._pvIdMap[pv->getUniqueId()] = varId;
671 pvImpl = _app.getTestableMode().decorate<UserType>(
672 pvImpl, detail::TestableMode::DecoratorType::READ, "ControlSystem:" + node.getPublicName());
673 }
674
675 boost::fusion::at_key<UserType>(_decoratedPvImpls.table)[node.getPublicName()] = pvImpl;
676 }
677
678 /********************************************************************************************************************/
679
680 template<typename UserType>
681 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> ConnectionMaker::getProcessVariable(
682 const VariableNetworkNode& node) {
683 return boost::fusion::at_key<UserType>(_decoratedPvImpls.table).at(node.getPublicName());
684 }
685 /********************************************************************************************************************/
686
687 template<typename UserType>
688 boost::shared_ptr<NDRegisterAccessor<UserType>> ConnectionMaker::createDeviceVariable(
689 VariableNetworkNode const& node) {
690 const auto& deviceAlias = node.getDeviceAlias();
691 const auto& registerName = node.getRegisterName();
692 auto direction = node.getDirection();
693 auto mode = node.getMode();
694 auto nElements = node.getNumberOfElements();
695
696 auto dev = _app._deviceManagerMap.at(deviceAlias)->getDevice().getBackend();
697
698 // use wait_for_new_data mode if push update mode was requested
699 // Feeding to the network means reading from a device to feed it into the network.
700 AccessModeFlags flags{};
701 if(mode == UpdateMode::push && direction.dir == VariableDirection::feeding) {
702 flags = {AccessMode::wait_for_new_data};
703 }
704
705 // obtain the register accessor from the device
706 auto accessor = dev->getRegisterAccessor<UserType>(registerName, nElements, 0, flags);
707
708 // Receiving accessors should be faulty after construction,
709 // see data validity propagation spec 2.6.1
710 if(node.getDirection().dir == VariableDirection::feeding || node.getDirection().withReturn) {
711 accessor->setDataValidity(DataValidity::faulty);
712 }
713
714 // decorate push-type feeders with testable mode decorator, if needed
715 if(mode == UpdateMode::push && direction.dir == VariableDirection::feeding) {
716 accessor = _app.getTestableMode().decorate(accessor, detail::TestableMode::DecoratorType::READ);
717 }
718
719 auto recoveryHelper = boost::make_shared<RecoveryHelper>();
720
721 if(node.getDirection().dir == VariableDirection::consuming && node.getDirection().withReturn) {
722 accessor = boost::make_shared<ReverseRecoveryDecorator<UserType>>(accessor, recoveryHelper);
723 }
724
725 return boost::make_shared<ExceptionHandlingDecorator<UserType>>(accessor, node, recoveryHelper);
726 }
727
728 /********************************************************************************************************************/
729
730 template<typename UserType>
731 ConsumerImplementationPairs<UserType> ConnectionMaker::setConsumerImplementations(NetworkInformation& net) {
732 debug(" setConsumerImplementations");
734
735 for(const auto& consumer : net.consumers) {
737 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>(), consumer};
738
739 if(consumer.getType() == NodeType::Application) {
740 debug(" Node type is Application: " + consumer.getQualifiedName());
741 auto impls = createApplicationVariable<UserType>(consumer);
742 consumer.setAppAccessorImplementation<UserType>(impls.second);
743 pair = std::make_pair(impls.first, consumer);
744 }
745 else if(consumer.getType() == NodeType::ControlSystem) {
746 debug(" Node type is ControlSystem");
747 auto impl = getProcessVariable<UserType>(consumer);
748 pair = std::make_pair(impl, consumer);
749 }
750 else if(consumer.getType() == NodeType::Device) {
751 debug(" Node type is Device");
752 auto impl = createDeviceVariable<UserType>(consumer);
753 pair = std::make_pair(impl, consumer);
754 }
755 else if(consumer.getType() == NodeType::TriggerReceiver) {
756 debug(" Node type is TriggerReceiver");
757 auto triggerConnection = createApplicationVariable<UserType>(net.feeder);
758
759 auto triggerFanOut = boost::make_shared<TriggerFanOut>(
760 triggerConnection.second, *_app.getDeviceManager(consumer.getDeviceAlias()));
761 _app._internalModuleList.push_back(triggerFanOut);
762 net.triggerImpl[consumer.getDeviceAlias()] = triggerFanOut;
763
764 pair = std::make_pair(triggerConnection.first, consumer);
765 }
766 else {
767 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
768 }
769
770 consumerImplPairs.push_back(pair);
771 }
772
773 return consumerImplPairs;
774 }
775
776 /********************************************************************************************************************/
777
778 template<typename UserType>
779 std::pair<boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>,
780 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>>
781 ConnectionMaker::createApplicationVariable(VariableNetworkNode const& node, VariableNetworkNode const& consumer) {
782 // obtain the meta data
783 size_t nElements = node.getNumberOfElements();
784 std::string name = node.getName();
785 assert(not name.empty());
786 AccessModeFlags flags = {};
787 if(consumer.isValid()) {
788 if(consumer.getMode() == UpdateMode::push) {
789 flags = {AccessMode::wait_for_new_data};
790 }
791 }
792 else {
793 if(node.getMode() == UpdateMode::push) {
794 flags = {AccessMode::wait_for_new_data};
795 }
796 }
797
798 // create the ProcessArray for the proper UserType
799 std::pair<boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>,
800 boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>>
801 pvarPair;
802 if(consumer.isValid()) {
803 assert(node.getDirection().withReturn == consumer.getDirection().withReturn);
804 }
805
806 if(!node.getDirection().withReturn) {
807 pvarPair = createSynchronizedProcessArray<UserType>(
808 nElements, name, node.getUnit(), node.getDescription(), {}, 3, flags);
809 }
810 else {
811 pvarPair = createBidirectionalSynchronizedProcessArray<UserType>(
812 nElements, name, node.getUnit(), node.getDescription(), {}, 3, flags);
813 }
814 assert(pvarPair.first->getName() != "");
815 assert(pvarPair.second->getName() != "");
816
817 if(flags.has(AccessMode::wait_for_new_data)) {
818 pvarPair = _app.getTestableMode().decorate(pvarPair, node, consumer);
819 }
820
821 // if debug mode was requested for either node, decorate both accessors
822 if(_app._debugMode_variableList.count(node.getUniqueId()) ||
823 (consumer.getType() != NodeType::invalid && _app._debugMode_variableList.count(consumer.getUniqueId()))) {
824 if(consumer.getType() != NodeType::invalid) {
825 assert(node.getDirection().dir == VariableDirection::feeding);
826 assert(consumer.getDirection().dir == VariableDirection::consuming);
827 pvarPair.first =
828 boost::make_shared<DebugPrintAccessorDecorator<UserType>>(pvarPair.first, node.getQualifiedName());
829 pvarPair.second =
830 boost::make_shared<DebugPrintAccessorDecorator<UserType>>(pvarPair.second, consumer.getQualifiedName());
831 }
832 else {
833 pvarPair.first =
834 boost::make_shared<DebugPrintAccessorDecorator<UserType>>(pvarPair.first, node.getQualifiedName());
835 pvarPair.second =
836 boost::make_shared<DebugPrintAccessorDecorator<UserType>>(pvarPair.second, node.getQualifiedName());
837 }
838 }
839
840 // return the pair
841 return pvarPair;
842 }
843
844 /********************************************************************************************************************/
845
846 void ChimeraTK::ConnectionMaker::makeConnectionForFeederWithoutImplementation(NetworkInformation& net) {
847 // we should be left with an application feeder node
848 if(net.feeder.getType() != NodeType::Application) {
849 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
850 }
851
852 if(net.consumers.size() == 1) {
853 debug(" Network of two nodes, connect directly");
854
855 const auto& consumer = net.consumers.front();
856
857 switch(consumer.getType()) {
859 debug(" Node type is Application");
860 callForType(*net.valueType, [&](auto t) {
861 using UserType = decltype(t);
862 auto impls = createApplicationVariable<UserType>(net.feeder, consumer);
863 net.feeder.setAppAccessorImplementation<UserType>(impls.first);
864 consumer.setAppAccessorImplementation<UserType>(impls.second);
865 });
866 break;
868 debug(" Node type is ControlSystem");
869 callForType(*net.valueType, [&](auto t) {
870 using UserType = decltype(t);
871 auto impl = getProcessVariable<UserType>(consumer);
872 net.feeder.setAppAccessorImplementation(impl);
873 });
874 break;
875 case NodeType::Device:
876 debug(" Node type is Device");
877 callForType(*net.valueType, [&](auto t) {
878 using UserType = decltype(t);
879 auto impl = createDeviceVariable<UserType>(consumer);
880 net.feeder.setAppAccessorImplementation(impl);
881 });
882 break;
884 debug(" Node type is TriggerReceiver");
885
886 // create a PV implementation to connect the Application with the TriggerFanOut.
887 {
888 boost::shared_ptr<TransferElement> consumingImpl;
889 callForType(*net.valueType, [&](auto t) {
890 using UserType = decltype(t);
891 auto impls = createApplicationVariable<UserType>(net.feeder, consumer);
892 net.feeder.setAppAccessorImplementation<UserType>(impls.first);
893 consumingImpl = impls.second;
894 });
895
896 // create the trigger fan out and store it in the map and the internalModuleList
897 auto triggerFanOut =
898 boost::make_shared<TriggerFanOut>(consumingImpl, *_app.getDeviceManager(consumer.getDeviceAlias()));
899 _app._internalModuleList.emplace_back(triggerFanOut);
900 net.triggerImpl[consumer.getDeviceAlias()] = triggerFanOut;
901 }
902
903 break;
905 debug(" Node type is Constant");
906 net.feeder.setAppAccessorConstImplementation(net.feeder);
907 break;
908 default:
909 throw ChimeraTK::logic_error("Unexpected node type!");
910 }
911 }
912 else if(net.consumers.size() > 1) {
913 debug(std::format(" More than one consumer, using fan-out as feeder impl (with return: {})",
914 net.feeder.getDirection().withReturn));
915 callForType(*net.valueType, [&](auto t) {
916 using UserType = decltype(t);
917 auto consumerImplementationPairs = setConsumerImplementations<UserType>(net);
918
919 // create FanOut and use it as the feeder implementation
920 auto fanOut = boost::make_shared<FeedingFanOut<UserType>>(net.feeder.getName(), net.unit, net.description,
921 net.valueLength, net.feeder.getDirection().withReturn, consumerImplementationPairs);
922 net.feeder.setAppAccessorImplementation<UserType>(fanOut);
923 });
924 }
925 else {
926 debug(" No consumer (presumably optimised out)");
927 net.feeder.setAppAccessorConstImplementation(VariableNetworkNode(net.valueType, true, net.valueLength));
928 }
929 }
930
931 /********************************************************************************************************************/
932
933 void ConnectionMaker::makeConnectionForConstantFeeder(NetworkInformation& net) {
934 assert(net.feeder.getType() == NodeType::Constant);
935 for(const auto& consumer : net.consumers) {
936 AccessModeFlags flags{};
937 if(consumer.getMode() == UpdateMode::push) {
938 flags = {AccessMode::wait_for_new_data};
939 }
940
941 callForType(*net.valueType, [&](auto t) {
942 using UserType = decltype(t);
943 // each consumer gets its own implementation
944 if(consumer.getType() == NodeType::Application) {
945 consumer.setAppAccessorConstImplementation(net.feeder);
946 }
947 else if(consumer.getType() == NodeType::ControlSystem) {
948 throw ChimeraTK::logic_error("Using constants as feeders for control system variables is not supported!");
949 }
950 else if(consumer.getType() == NodeType::Device) {
951 // We register the required accessor as a recovery accessor. This is just a bare RegisterAccessor without
952 // any decorations directly from the backend.
953 auto deviceManager = _app.getDeviceManager(consumer.getDeviceAlias());
954 auto dev = deviceManager->getDevice().getBackend();
955 auto impl =
956 dev->getRegisterAccessor<UserType>(consumer.getRegisterName(), consumer.getNumberOfElements(), 0, {});
957 auto catalog = deviceManager->getDevice().getRegisterCatalogue();
958 auto tags = catalog.getRegister(consumer.getRegisterName()).getTags();
959
960 // Set the value
961 impl->accessChannel(0) =
962 std::vector<UserType>(consumer.getNumberOfElements(), net.feeder.getConstantValue<UserType>());
963
964 // The accessor implementation already has its data in the user buffer. We now just have to add a valid
965 // version number and have a recovery accessors (RecoveryHelper to be exact) which we can register at the
966 // DeviceModule. As this is a constant we don't need to change it later and don't have to store it somewhere
967 // else.
968 // If this register is considered for reverse recovery (Device pushes to application), do not add an
969 // accessor at all, since pushing to a constant does not make any sense.
970 if(!tags.contains(ChimeraTK::SystemTags::reverseRecovery)) {
971 deviceManager->addRecoveryAccessor(
972 boost::make_shared<RecoveryHelper>(impl, VersionNumber(), deviceManager->writeOrder()));
973 }
974 }
975 else if(consumer.getType() == NodeType::TriggerReceiver) {
976 throw ChimeraTK::logic_error("Using constants as triggers is not supported!");
977 }
978 else {
979 throw ChimeraTK::logic_error("Unexpected node type!"); // LCOV_EXCL_LINE (assert-like)
980 }
981 });
982 }
983 }
984
985 /********************************************************************************************************************/
986
987 void ConnectionMaker::optimiseUnmappedVariables(const std::set<std::string>& names) {
988 debug("-----------------------------");
989 debug("Optimising unmapped variables");
990 debug("-----------------------------");
991
992 for(const auto& name : names) {
993 debug("Looking at network " + name);
994 auto& network = _networks.at(name);
995 // if the control system is the feeder, change it into a constant
996 if(network.feeder.getType() == NodeType::ControlSystem) {
997 if(network.useReverseRecovery) {
998 // We need to promote the accessor with the reverse recovery tag to the network feeder
999 // to prevent writing down the constant value into the device and propagating the
1000 // recovery value to the other consumers instead.
1001 auto reverseConsumer = std::ranges::find_if(network.consumers, [](auto& consumer) {
1002 return consumer.getType() == NodeType::Device &&
1003 consumer.getTags().contains(ChimeraTK::SystemTags::reverseRecovery);
1004 });
1005 if(reverseConsumer->isReadable()) {
1006 debug(std::format(" Promoting reverse consumer {} to feeder", reverseConsumer->getName()));
1007 network.feeder = *reverseConsumer;
1008 network.consumers.remove(*reverseConsumer);
1009 }
1010 else {
1011 debug(std::format(
1012 " Reverse consumer {} is not readable, adding constant feeder instead", reverseConsumer->getName()));
1013 network.feeder = VariableNetworkNode(network.valueType, true, network.valueLength);
1014 }
1015 }
1016 else {
1017 debug(" Adding constant feeder");
1018 network.feeder = VariableNetworkNode(network.valueType, true, network.valueLength);
1019 }
1020 }
1021 else {
1022 // control system is a consumer: remove it from the list of consumers
1023 debug(" Dropping CS consumer");
1024 network.consumers.remove_if([](auto& consumer) { return consumer.getType() == NodeType::ControlSystem; });
1025 }
1026 }
1027 }
1028
1029} // 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().
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.
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:124
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