• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

traintastic / traintastic / 24288605255

11 Apr 2026 06:17PM UTC coverage: 25.599% (-2.4%) from 27.99%
24288605255

push

github

web-flow
Merge pull request #222 from traintastic/cbus

Added CBUS/VLCB hardware support

169 of 3369 new or added lines in 99 files covered. (5.02%)

5 existing lines in 4 files now uncovered.

8300 of 32423 relevant lines covered (25.6%)

178.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.78
/server/src/hardware/output/map/outputmap.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2021-2026 Reinder Feenstra
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
 */
21

22
#include "outputmap.hpp"
23
#include <cassert>
24
#include "outputmapitem.hpp"
25
#include "outputmapsingleoutputaction.hpp"
26
#include "outputmappairoutputaction.hpp"
27
#include "outputmapaspectoutputaction.hpp"
28
#include "outputmapecosstateoutputaction.hpp"
29
#include "../outputcontroller.hpp"
30
#include "../../interface/interface.hpp"
31
#include "../../../core/attributes.hpp"
32
#include "../../../core/method.tpp"
33
#include "../../../core/objectproperty.tpp"
34
#include "../../../core/objectvectorproperty.tpp"
35
#include "../../../world/getworld.hpp"
36
#include "../../../world/worldloader.hpp"
37
#include "../../../world/worldsaver.hpp"
38
#include "../../../utils/displayname.hpp"
39
#include "../../../utils/inrange.hpp"
40

41
namespace
42
{
43

44
template<typename T>
45
void swap(Property<T>& a, Property<T>& b)
×
46
{
47
  T tmp = a;
×
48
  a = b.value();
×
49
  b = tmp;
×
50
}
×
51

52
constexpr OutputLocation outputLocation(OutputChannel channel, uint32_t node, uint32_t address) noexcept
8✔
53
{
54
  if(hasNode(channel))
8✔
55
  {
NEW
56
    return OutputNodeAddress(node, address);
×
57
  }
58
  return OutputAddress(address);
8✔
59
}
60

61
}
62

63
OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName)
41✔
64
  : SubObject(_parent, parentPropertyName)
65
  , parentObject{this, "parent", nullptr, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::NoScript}
41✔
66
  , interface{this, "interface", nullptr, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
82✔
67
      [this](const std::shared_ptr<OutputController>& /*newValue*/)
41✔
68
      {
69
        interfaceChanged();
8✔
70
      },
8✔
71
      [this](const std::shared_ptr<OutputController>& newValue)
82✔
72
      {
73
        m_interfaceDestroying.disconnect();
8✔
74

75
        if(newValue)
8✔
76
        {
77
          if(auto* object = dynamic_cast<Object*>(newValue.get())) /*[[likely]]*/
5✔
78
          {
79
            m_interfaceDestroying = object->onDestroying.connect(
10✔
80
              [this](Object& /*object*/)
10✔
81
              {
82
                interface = nullptr;
3✔
83
              });
8✔
84
          }
85

86
          if(!interface)
5✔
87
          {
88
            // No interface was assigned.
89
            assert(!newValue->outputChannels().empty());
3✔
90
            channel.setValueInternal(newValue->outputChannels().front());
3✔
91
          }
92
          else if(!newValue->isOutputChannel(channel) || newValue->outputType(channel) != interface->outputType(channel))
2✔
93
          {
94
            // New interface doesn't support channel or channel has different output type.
95
            const auto channels = newValue->outputChannels();
1✔
96
            const auto it = std::find_if(channels.begin(), channels.end(),
1✔
97
              [&controller=*newValue, outputType=interface->outputType(channel)](OutputChannel outputChannel)
1✔
98
              {
99
                return controller.outputType(outputChannel) == outputType;
1✔
100
              });
101

102
            if(it != channels.end())
1✔
103
            {
104
              // Found channel with same output type.
105
              channel.setValueInternal(*it);
1✔
106
            }
107
            else
108
            {
109
              // No channel found with same output type.
110
              channel.setValueInternal(newValue->outputChannels().front());
×
111
              // reset mapping
112
            }
113
          }
114

115
          Attributes::setMinMax(addresses, newValue->outputAddressMinMax(channel));
5✔
116

117
          if(!interface) // No interface was assigned.
5✔
118
          {
119
            assert(addresses.empty());
3✔
120
            assert(m_outputs.empty());
3✔
121

122
            switch(channel)
3✔
123
            {
124
              case OutputChannel::Output:
3✔
125
              case OutputChannel::Accessory:
126
              case OutputChannel::AccessoryDCC:
127
              case OutputChannel::AccessoryMotorola:
128
              case OutputChannel::DCCext:
129
              case OutputChannel::Turnout:
130
              case OutputChannel::LongEvent:
131
              case OutputChannel::ShortEvent:
132
              {
133
                const uint32_t address = newValue->getUnusedOutputAddress(channel);
3✔
134
                addresses.appendInternal(address);
3✔
135
                addOutput(channel, outputLocation(channel, node, address), *newValue);
3✔
136
                break;
3✔
137
              }
138
              case OutputChannel::ECoSObject:
×
NEW
139
                if(newValue->isOutputLocation(channel, OutputECoSObject(ecosObject)))
×
140
                {
NEW
141
                  addOutput(channel, OutputECoSObject(ecosObject), *newValue);
×
142
                }
143
                break;
×
144
            }
145
            updateOutputActions(newValue->outputType(channel));
3✔
146
          }
147
        }
148
        else // no interface
149
        {
150
          for(auto& it : m_outputs)
7✔
151
          {
152
            if(it.first) /*[[likely]]*/
4✔
153
            {
154
              releaseOutput(it);
4✔
155
            }
156
          }
157

158
          m_outputs.clear();
3✔
159
          addresses.clearInternal();
3✔
160
          addressesSizeChanged();
3✔
161
        }
162
        return true;
8✔
163
      }}
164
  , channel{this, "channel", OutputChannel::Accessory, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
82✔
165
      [this](const OutputChannel newValue)
41✔
166
      {
167
        // Release outputs for previous channel:
168
        releaseOutputs(m_outputs);
3✔
169

170
        // Get outputs for current channel:
171
        switch(newValue)
3✔
172
        {
173
          case OutputChannel::Output:
2✔
174
          case OutputChannel::Accessory:
175
          case OutputChannel::AccessoryDCC:
176
          case OutputChannel::AccessoryMotorola:
177
          case OutputChannel::DCCext:
178
          case OutputChannel::Turnout:
179
          case OutputChannel::LongEvent:
180
          case OutputChannel::ShortEvent:
181
            ecosObject.setValueInternal(0);
2✔
182
            for(uint32_t address : addresses)
3✔
183
            {
184
              addOutput(newValue, outputLocation(newValue, node, address));
1✔
185
            }
186
            break;
2✔
187

188
          case OutputChannel::ECoSObject:
1✔
189
            addresses.clearInternal();
1✔
190
            if(interface->isOutputLocation(newValue, OutputECoSObject(ecosObject)))
1✔
191
            {
NEW
192
              addOutput(newValue, OutputECoSObject(ecosObject));
×
193
            }
194
            break;
1✔
195
        }
196

197
        channelChanged();
3✔
198
      },
3✔
199
      [this](const OutputChannel newValue)
82✔
200
      {
201
        if(!interface) /*[[unlikely]]*/
3✔
202
        {
203
          return false;
×
204
        }
205

206
        // If output type is different reset all actions:
207
        if(interface->outputType(channel) != interface->outputType(newValue))
3✔
208
        {
209
          for(const auto& item : items)
8✔
210
          {
211
            item->outputActions.clearInternal();
6✔
212
          }
213
        }
214
        return true;
3✔
215
      }}
216
  , node{this, "node", 0, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
82✔
217
      nullptr,
218
      [this](uint32_t& value)
41✔
219
      {
NEW
220
        if(!interface) [[unlikely]]
×
221
        {
NEW
222
          return false;
×
223
        }
224

NEW
225
        Outputs newOutputs;
×
NEW
226
        for(auto address : addresses)
×
227
        {
NEW
228
          auto outputConnPair = getOutput(channel, OutputNodeAddress(value, address), *interface);
×
NEW
229
          if(outputConnPair.first) [[likely]]
×
230
          {
NEW
231
            newOutputs.emplace_back(outputConnPair);
×
232
          }
NEW
233
        }
×
234

NEW
235
        if(newOutputs.size() != m_outputs.size())
×
236
        {
NEW
237
          releaseOutputs(newOutputs);
×
NEW
238
          assert(newOutputs.empty());
×
NEW
239
          return false;
×
240
        }
241

NEW
242
        releaseOutputs(m_outputs);
×
NEW
243
        assert(m_outputs.empty());
×
NEW
244
        m_outputs = std::move(newOutputs);
×
NEW
245
        return true;
×
NEW
246
      }}
×
247
  , addresses{*this, "addresses", {}, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
82✔
248
      nullptr,
249
      [this](uint32_t index, uint32_t& value)
41✔
250
      {
251
        (void)index;
252

253
        if(!interface) [[unlikely]]
1✔
254
        {
255
          return false;
×
256
        }
257

258
        if(std::find(addresses.begin(), addresses.end(), value) != addresses.end())
1✔
259
        {
260
          return false; // Duplicate addresses aren't allowed.
×
261
        }
262

263
        auto outputConnPair = getOutput(channel, outputLocation(channel, node, value), *interface);
1✔
264
        if(!outputConnPair.first) [[unlikely]]
1✔
265
        {
266
          return false; // Output doesn't exist.
×
267
        }
268

269
        if(index < m_outputs.size() && m_outputs[index].first)
1✔
270
        {
271
          releaseOutput(m_outputs[index]);
1✔
272
        }
273

274
        m_outputs[index] = outputConnPair;
1✔
275

276
        return true;
1✔
277
      }}
1✔
278
  , ecosObject{this, "ecos_object", 0, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
82✔
279
      nullptr,
280
      [this](uint16_t& value)
41✔
281
      {
282
        if(!interface) /*[[unlikely]]*/
1✔
283
        {
284
          return false;
×
285
        }
286

287
        if(value == 0 || interface->isOutputLocation(channel, OutputECoSObject(value))) // 0 = no object
1✔
288
        {
289
          if(!m_outputs.empty())
1✔
290
          {
291
            releaseOutput(m_outputs.front());
×
292
            m_outputs.clear();
×
293
          }
294
          if(value != 0)
1✔
295
          {
296
            addOutput(channel, OutputECoSObject(value));
1✔
297
          }
298
          updateOutputActions();
1✔
299
          return true;
1✔
300
        }
301

302
        return false;
×
303
      }}
304
  , items{*this, "items", {}, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject}
41✔
305
  , addAddress{*this, "add_address", MethodFlags::NoScript,
82✔
306
      [this]()
41✔
307
      {
308
        if(interface && addresses.size() < addressesSizeMax) /*[[likely]]*/
2✔
309
        {
310
          assert(!addresses.empty());
2✔
311
          const auto addressRange = interface->outputAddressMinMax(channel);
2✔
312
          if(addresses.size() >= (addressRange.second - addressRange.first + 1)) /*[[unlikely]]*/
2✔
313
          {
314
            return; // All addresses used.
×
315
          }
316
          const uint32_t address = getUnusedAddress();
2✔
317
          addresses.appendInternal(address);
2✔
318
          addOutput(channel, outputLocation(channel, node, address));
2✔
319
          addressesSizeChanged();
2✔
320
        }
321
      }}
322
  , removeAddress{*this, "remove_address", MethodFlags::NoScript,
82✔
323
      [this]()
41✔
324
      {
325
        if(interface && addresses.size() > addressesSizeMin) /*[[likely]]*/
×
326
        {
327
          addresses.eraseInternal(addresses.size() - 1);
×
328
          if(m_outputs.back().first)
×
329
          {
330
            releaseOutput(m_outputs.back());
×
331
          }
332
          m_outputs.pop_back();
×
333
          addressesSizeChanged();
×
334
        }
335
      }}
×
336
  , swapOutputs{*this, "swap_outputs", MethodFlags::NoScript,
82✔
337
      [this]()
41✔
338
      {
339
        if(!interface)
×
340
        {
341
          return;
×
342
        }
343

344
        switch(interface->outputType(channel))
×
345
        {
346
          case OutputType::Pair:
×
347
            if(m_outputs.size() == 1 && items.size() == 2)
×
348
            {
349
              swap(static_cast<OutputMapPairOutputAction&>(*items[0]->outputActions[0]).action, static_cast<OutputMapPairOutputAction&>(*items[1]->outputActions[0]).action);
×
350
            }
351
            break;
×
352

353
          default:
×
354
            break;
×
355
        }
356
      }}
41✔
357
{
358
  auto& world = getWorld(&_parent);
41✔
359
  const bool editable = contains(world.state.value(), WorldState::Edit);
41✔
360

361
  m_interfaceItems.add(parentObject);
41✔
362

363
  Attributes::addDisplayName(interface, DisplayName::Hardware::interface);
41✔
364
  Attributes::addEnabled(interface, editable);
41✔
365
  Attributes::addObjectList(interface, world.outputControllers);
41✔
366
  m_interfaceItems.add(interface);
41✔
367

368
  Attributes::addDisplayName(node, DisplayName::Hardware::node);
41✔
369
  Attributes::addEnabled(node, editable);
41✔
370
  Attributes::addVisible(node, false);
41✔
371
  Attributes::addMinMax<uint32_t>(node, 0, 0);
41✔
372
  m_interfaceItems.add(node);
41✔
373

374
  Attributes::addDisplayName(channel, DisplayName::Hardware::channel);
41✔
375
  Attributes::addEnabled(channel, editable);
41✔
376
  Attributes::addValues(channel, std::span<const OutputChannel>());
41✔
377
  Attributes::addVisible(channel, false);
41✔
378
  m_interfaceItems.add(channel);
41✔
379

380
  Attributes::addDisplayName(addresses, DisplayName::Hardware::address);
41✔
381
  Attributes::addEnabled(addresses, editable);
41✔
382
  Attributes::addVisible(addresses, false);
41✔
383
  Attributes::addMinMax<uint32_t>(addresses, 0, 0);
41✔
384
  m_interfaceItems.add(addresses);
41✔
385

386
  Attributes::addAliases(ecosObject, std::span<const uint16_t>{}, std::span<const std::string>{});
41✔
387
  Attributes::addDisplayName(ecosObject, "output_map:ecos_object");
41✔
388
  Attributes::addEnabled(ecosObject, editable);
41✔
389
  Attributes::addValues(ecosObject, std::span<const uint16_t>{});
41✔
390
  Attributes::addVisible(ecosObject, false);
41✔
391
  m_interfaceItems.add(ecosObject);
41✔
392

393
  m_interfaceItems.add(items);
41✔
394

395
  Attributes::addDisplayName(addAddress, DisplayName::OutputMap::addAddress);
41✔
396
  Attributes::addEnabled(addAddress, false);
41✔
397
  Attributes::addVisible(addAddress, false);
41✔
398
  m_interfaceItems.add(addAddress);
41✔
399

400
  Attributes::addDisplayName(removeAddress, DisplayName::OutputMap::removeAddress);
41✔
401
  Attributes::addEnabled(removeAddress, false);
41✔
402
  Attributes::addVisible(removeAddress, false);
41✔
403
  m_interfaceItems.add(removeAddress);
41✔
404

405
  Attributes::addDisplayName(swapOutputs, "output_map:swap_outputs");
41✔
406
  Attributes::addEnabled(swapOutputs, false);
41✔
407
  Attributes::addVisible(swapOutputs, false);
41✔
408
  m_interfaceItems.add(swapOutputs);
41✔
409

410
  updateEnabled();
41✔
411
}
41✔
412

413
OutputMap::~OutputMap() = default;
41✔
414

415
void OutputMap::load(WorldLoader& loader, const nlohmann::json& data)
×
416
{
417
  SubObject::load(loader, data);
×
418

419
  if(interface)
×
420
  {
421
    switch(channel)
×
422
    {
423
      case OutputChannel::Output:
×
424
      case OutputChannel::Accessory:
425
      case OutputChannel::AccessoryDCC:
426
      case OutputChannel::AccessoryMotorola:
427
      case OutputChannel::DCCext:
428
      case OutputChannel::Turnout:
429
      case OutputChannel::LongEvent:
430
      case OutputChannel::ShortEvent:
UNCOV
431
        for(uint32_t address : addresses)
×
432
        {
NEW
433
          addOutput(channel, outputLocation(channel, node, address));
×
434
        }
435
        break;
×
436

437
      case OutputChannel::ECoSObject:
×
NEW
438
        if(interface->isOutputLocation(channel, OutputECoSObject(ecosObject)))
×
439
        {
NEW
440
          addOutput(channel, OutputECoSObject(ecosObject));
×
441
        }
442
        break;
×
443
    }
444

445
    updateOutputActions();
×
446
  }
447
}
×
448

449
void OutputMap::loaded()
×
450
{
451
  if(interface)
×
452
  {
453
    interfaceChanged();
×
454
  }
455
  SubObject::loaded();
×
456
}
×
457

458
void OutputMap::worldEvent(WorldState state, WorldEvent event)
16✔
459
{
460
  SubObject::worldEvent(state, event);
16✔
461

462
  updateEnabled();
16✔
463
}
16✔
464

465
void OutputMap::interfaceChanged()
8✔
466
{
467
  const auto outputChannels = interface ? interface->outputChannels() : std::span<const OutputChannel>{};
8✔
468
  Attributes::setValues(channel, outputChannels);
8✔
469
  Attributes::setVisible(channel, interface);
8✔
470

471
  m_outputECoSObjectsChanged.disconnect();
8✔
472

473
  if(std::find(outputChannels.begin(), outputChannels.end(), OutputChannel::ECoSObject) != outputChannels.end())
8✔
474
  {
475
    m_outputECoSObjectsChanged = interface->outputECoSObjectsChanged.connect(
9✔
476
      [this]()
6✔
477
      {
478
        const auto aliases = interface->getOutputECoSObjects(OutputChannel::ECoSObject);
×
479
        Attributes::setAliases(ecosObject, aliases.first, aliases.second);
×
480
        Attributes::setValues(ecosObject, aliases.first);
×
481
      });
3✔
482
  }
483

484
  channelChanged();
8✔
485
}
8✔
486

487
void OutputMap::channelChanged()
11✔
488
{
489
  if(interface)
11✔
490
  {
491
    switch(channel.value())
8✔
492
    {
493
      case OutputChannel::Output:
7✔
494
      case OutputChannel::Accessory:
495
      case OutputChannel::AccessoryDCC:
496
      case OutputChannel::AccessoryMotorola:
497
      case OutputChannel::DCCext:
498
      case OutputChannel::Turnout:
499
      case OutputChannel::LongEvent:
500
      case OutputChannel::ShortEvent:
501
      {
502
        Attributes::setVisible(node, hasNode(channel));
7✔
503
        Attributes::setVisible({addresses, addAddress, removeAddress}, true);
7✔
504
        Attributes::setVisible(ecosObject, false);
7✔
505
        Attributes::setAliases(ecosObject, std::span<const uint16_t>{}, std::span<const std::string>{});
7✔
506
        Attributes::setValues(ecosObject, std::span<const uint16_t>{});
7✔
507

508
        if(addresses.empty())
7✔
509
        {
510
          const auto address =
511
            hasNode(channel)
1✔
512
              ? interface->outputAddressMinMax(channel).first
1✔
513
              : interface->getUnusedOutputAddress(channel);
1✔
514
          addresses.appendInternal(address);
1✔
515
          addOutput(channel, outputLocation(channel, node, address));
1✔
516
          updateOutputActions();
1✔
517
        }
518

519
        if(hasNode(channel))
7✔
520
        {
NEW
521
          Attributes::setMinMax(node, interface->outputNodeMinMax(channel));
×
522
        }
523

524
        const auto addressRange = interface->outputAddressMinMax(channel);
7✔
525
        const uint32_t addressCount = (addressRange.second - addressRange.first + 1);
7✔
526
        Attributes::setMinMax(addresses, addressRange);
7✔
527

528
        while(addressCount < addresses.size()) // Reduce number of addresses if larger than address space.
7✔
529
        {
530
          addresses.eraseInternal(addresses.size() - 1);
×
531
        }
532

533
        // Make sure all addresses are in range:
534
        for(size_t i = 0; i < addresses.size(); i++)
16✔
535
        {
536
          if(!inRange(addresses[i], addressRange))
9✔
537
          {
538
            addresses.setValueInternal(i, getUnusedAddress());
×
539
          }
540
        }
541

542
        addressesSizeChanged();
7✔
543
        break;
7✔
544
      }
545
      case OutputChannel::ECoSObject:
1✔
546
      {
547
        Attributes::setVisible({node, addresses, addAddress, removeAddress}, false);
1✔
548
        Attributes::setVisible(ecosObject, true);
1✔
549
        const auto aliases = interface->getOutputECoSObjects(channel);
1✔
550
        Attributes::setAliases(ecosObject, aliases.first, aliases.second);
1✔
551
        Attributes::setValues(ecosObject, aliases.first);
1✔
552

553
        updateOutputActions();
1✔
554
        break;
1✔
555
      }
556
    }
557
  }
558
  else
559
  {
560
    Attributes::setVisible({node, addresses, addAddress, removeAddress, ecosObject}, false);
3✔
561
    Attributes::setMinMax(node, std::numeric_limits<uint32_t>::min(), std::numeric_limits<uint32_t>::max());
3✔
562
    Attributes::setMinMax(addresses, std::numeric_limits<uint32_t>::min(), std::numeric_limits<uint32_t>::max());
3✔
563
    Attributes::setAliases(ecosObject, std::span<const uint16_t>{}, std::span<const std::string>{});
3✔
564
    Attributes::setValues(ecosObject, std::span<const uint16_t>{});
3✔
565
  }
566
}
11✔
567

568
void OutputMap::addressesSizeChanged()
12✔
569
{
570
  assert(addresses.size() == m_outputs.size());
12✔
571
  updateAddressDisplayName();
12✔
572
  updateOutputActions();
12✔
573
  updateEnabled();
12✔
574
}
12✔
575

576
void OutputMap::updateOutputActions()
15✔
577
{
578
  assert(interface);
15✔
579
  updateOutputActions(interface->outputType(channel));
15✔
580
}
15✔
581

582
void OutputMap::updateOutputActions(OutputType outputType)
18✔
583
{
584
  for(const auto& item : items)
68✔
585
  {
586
    while(m_outputs.size() > item->outputActions.size())
70✔
587
    {
588
      std::shared_ptr<OutputMapOutputAction> outputAction = createOutputAction(outputType, item->outputActions.size(), getDefaultOutputActionValue(*item, outputType, item->outputActions.size()));
20✔
589
      assert(outputAction);
20✔
590
      item->outputActions.appendInternal(outputAction);
20✔
591
    }
20✔
592

593
    while(m_outputs.size() < item->outputActions.size())
61✔
594
    {
595
      item->outputActions.back()->destroy();
11✔
596
      item->outputActions.removeInternal(item->outputActions.back());
11✔
597
    }
598

599
    assert(m_outputs.size() == item->outputActions.size());
50✔
600
  }
601

602
  switch(outputType)
18✔
603
  {
604
    case OutputType::Pair:
16✔
605
      Attributes::setVisible(swapOutputs, m_outputs.size() == 1 && items.size() == 2);
16✔
606
      break;
16✔
607

608
    default:
2✔
609
      Attributes::setVisible(swapOutputs, false);
2✔
610
      break;
2✔
611
  }
612
}
18✔
613

614
void OutputMap::updateEnabled()
69✔
615
{
616
  const bool editable = contains(getWorld(parent()).state.value(), WorldState::Edit);
69✔
617

618
  Attributes::setEnabled(interface, editable);
69✔
619
  Attributes::setEnabled(channel, editable);
69✔
620
  Attributes::setEnabled(node, editable);
69✔
621
  Attributes::setEnabled(addresses, editable);
69✔
622
  Attributes::setEnabled(addAddress, editable && addresses.size() < addressesSizeMax);
69✔
623
  Attributes::setEnabled(removeAddress, editable && addresses.size() > addressesSizeMin);
69✔
624
  Attributes::setEnabled(ecosObject, editable);
69✔
625
  Attributes::setEnabled(swapOutputs, editable);
69✔
626
}
69✔
627

628
uint32_t OutputMap::getUnusedAddress() const
2✔
629
{
630
  assert(interface);
2✔
631
  const auto addressRange = interface->outputAddressMinMax(channel);
2✔
632
  assert((addressRange.second - addressRange.first + 1) > addresses.size());
2✔
633
  uint32_t address = addresses.empty() ? addressRange.first : addresses.back();
2✔
634
  do
635
  {
636
    address++;
2✔
637
    if(!inRange(address, addressRange))
2✔
638
    {
639
      address = addressRange.first;
×
640
    }
641
  }
642
  while(std::find(addresses.begin(), addresses.end(), address) != addresses.end());
2✔
643

644
  return address;
2✔
645
}
646

647
std::shared_ptr<OutputMapOutputAction> OutputMap::createOutputAction(OutputType outputType, size_t index, std::optional<OutputActionValue> actionValue)
20✔
648
{
649
  switch(outputType)
20✔
650
  {
651
    case OutputType::Single:
×
652
    {
653
      auto singleOutputAction = std::make_shared<OutputMapSingleOutputAction>(*this, index);
×
654
      if(actionValue)
×
655
      {
656
        singleOutputAction->action.setValueInternal(std::get<SingleOutputAction>(*actionValue));
×
657
      }
658
      return singleOutputAction;
×
659
    }
×
660
    case OutputType::Pair:
17✔
661
    {
662
      auto pairOutputAction = std::make_shared<OutputMapPairOutputAction>(*this, index);
17✔
663
      if(actionValue)
17✔
664
      {
665
        pairOutputAction->action.setValueInternal(std::get<PairOutputAction>(*actionValue));
7✔
666
      }
667
      return pairOutputAction;
17✔
668
    }
17✔
669
    case OutputType::Aspect:
×
670
    {
671
      auto aspectOutputAction = std::make_shared<OutputMapAspectOutputAction>(*this, index);
×
672
      if(actionValue)
×
673
      {
674
        aspectOutputAction->aspect.setValueInternal(std::get<int16_t>(*actionValue));
×
675
      }
676
      return aspectOutputAction;
×
677
    }
×
678
    case OutputType::ECoSState:
3✔
679
    {
680
      auto ecosStateOutputAction = std::make_shared<OutputMapECoSStateOutputAction>(*this, index);
3✔
681
      if(actionValue)
3✔
682
      {
683
        ecosStateOutputAction->state.setValueInternal(std::get<int16_t>(*actionValue));
×
684
      }
685
      return ecosStateOutputAction;
3✔
686
    }
3✔
687
  }
688
  assert(false);
×
689
  return {};
690
}
691

692
int OutputMap::getMatchingActionOnCurrentState()
×
693
{
694
  int i = 0;
×
695
  int wildcardIdx = -1;
×
696

697
  for(const auto& item : items)
×
698
  {
699
    OutputMapItem::MatchResult value = item->matchesCurrentOutputState();
×
700
    if(value == OutputMapItem::MatchResult::FullMatch)
×
701
    {
702
      return i; // We got a full match
×
703
    }
704
    if(value == OutputMapItem::MatchResult::WildcardMatch)
×
705
    {
706
      // We give wildcard matches a lower priority.
707
      // Save it for later, in the meantime we check for a better full match
708
      if(wildcardIdx == -1)
×
709
        wildcardIdx = i;
×
710
    }
711

712
    i++;
×
713
  }
714

715
  // No full match, do we have a wildcard match?
716
  if(wildcardIdx != -1)
×
717
    return wildcardIdx;
×
718

719
  return -1; // No match found
×
720
}
721

722
void OutputMap::updateStateFromOutput()
×
723
{
724
  // Default implementation is no-op
725
}
×
726

727
void OutputMap::addOutput(OutputChannel ch, const OutputLocation& location)
5✔
728
{
729
  addOutput(ch, location, *interface);
5✔
730
}
5✔
731

732
void OutputMap::addOutput(OutputChannel ch, const OutputLocation& location, OutputController& outputController)
8✔
733
{
734
  m_outputs.emplace_back(getOutput(ch, location, outputController));
8✔
735
  assert(m_outputs.back().first);
8✔
736
}
8✔
737

738
OutputMap::OutputConnectionPair OutputMap::getOutput(OutputChannel ch, const OutputLocation& location, OutputController& outputController)
9✔
739
{
740
  auto output = outputController.getOutput(ch, location, parent());
9✔
741
  if(!output)
9✔
742
    return {};
×
743

744
  boost::signals2::connection conn = output->onValueChangedGeneric.connect([this](const std::shared_ptr<Output>&){ updateStateFromOutput(); });
9✔
745
  return {output, conn};
9✔
746
}
9✔
747

748
void OutputMap::releaseOutput(OutputConnectionPair& outputConnPair)
9✔
749
{
750
  outputConnPair.second.disconnect();
9✔
751
  interface->releaseOutput(*outputConnPair.first, parent());
9✔
752
}
9✔
753

754
void OutputMap::releaseOutputs(Outputs& outputs)
3✔
755
{
756
  while(!outputs.empty())
7✔
757
  {
758
    if(outputs.back().first) [[likely]]
4✔
759
    {
760
      releaseOutput(outputs.back());
4✔
761
    }
762
    outputs.pop_back();
4✔
763
  }
764
}
3✔
765

766
void OutputMap::updateAddressDisplayName()
12✔
767
{
768
  switch(channel)
12✔
769
  {
770
    using enum OutputChannel;
771

772
    case Output:
12✔
773
    case Accessory:
774
    case AccessoryDCC:
775
    case AccessoryMotorola:
776
    case DCCext:
777
    case Turnout:
778
    case ECoSObject:
779
      Attributes::setDisplayName(addresses, addresses.size() == 1 ? DisplayName::Hardware::address : DisplayName::Hardware::addresses);
12✔
780
      Attributes::setDisplayName(addAddress, DisplayName::OutputMap::addAddress);
12✔
781
      Attributes::setDisplayName(removeAddress, DisplayName::OutputMap::removeAddress);
12✔
782
      break;
12✔
783

NEW
784
    case LongEvent:
×
785
    case ShortEvent:
NEW
786
      Attributes::setDisplayName(addresses, addresses.size() == 1 ? DisplayName::Hardware::event : DisplayName::Hardware::events);
×
NEW
787
      Attributes::setDisplayName(addAddress, DisplayName::OutputMap::addEvent);
×
NEW
788
      Attributes::setDisplayName(removeAddress, DisplayName::OutputMap::removeEvent);
×
NEW
789
      break;
×
790
  }
791
}
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc