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

traintastic / traintastic / 26121453997

19 May 2026 07:52PM UTC coverage: 25.063% (-0.6%) from 25.624%
26121453997

Pull #221

github

web-flow
Merge 598936246 into 15e38bcf7
Pull Request #221: Xpressnet new messages

49 of 1129 new or added lines in 16 files covered. (4.34%)

782 existing lines in 21 files now uncovered.

8483 of 33847 relevant lines covered (25.06%)

172.88 hits per line

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

23.44
/server/src/hardware/input/feedback/feedbackmap.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 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 "feedbackmap.hpp"
23
#include "feedbackmapitem.hpp"
24
#include "feedbackmapinputcondition.hpp"
25
#include "../input.hpp"
26
#include "../inputcontroller.hpp"
27
#include "../../../core/attributes.hpp"
28
#include "../../../core/method.tpp"
29
#include "../../../core/objectproperty.tpp"
30
#include "../../../core/objectvectorproperty.tpp"
31
#include "../../../log/log.hpp"
32
#include "../../../utils/displayname.hpp"
33
#include "../../../utils/inrange.hpp"
34
#include "../../../world/getworld.hpp"
35

36
FeedbackMap::FeedbackMap(Object& _parent, std::string_view parentPropertyName)
36✔
37
  : SubObject(_parent, parentPropertyName)
38
  , parentObject{this, "parent", nullptr, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::NoScript}
36✔
39
  , interface{this, "interface", nullptr, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
72✔
40
      [this](const std::shared_ptr<InputController>& /*newValue*/)
36✔
41
      {
42
        interfaceChanged();
×
43
      },
×
44
      [this](const std::shared_ptr<InputController>& newValue)
72✔
45
      {
46
        m_interfaceDestroying.disconnect();
×
47

48
        if(newValue)
×
49
        {
50
          if(auto* object = dynamic_cast<Object*>(newValue.get())) /*[[likely]]*/
×
51
          {
52
            m_interfaceDestroying = object->onDestroying.connect(
×
53
              [this](Object& /*object*/)
×
54
              {
55
                interface = nullptr;
×
56
              });
×
57
          }
58

59
          if(!interface)
×
60
          {
61
            // No interface was assigned.
62
            assert(!newValue->inputChannels().empty());
×
63
            channel.setValueInternal(newValue->inputChannels().front());
×
64
          }
65
          else if(!newValue->isInputChannel(channel))
×
66
          {
67
            // New interface doesn't support channel, use the first one.
68
            channel.setValueInternal(newValue->inputChannels().front());
×
69
          }
70

71
          Attributes::setMinMax(addresses, newValue->inputAddressMinMax(channel));
×
72

73
          if(!interface) // No interface was assigned.
×
74
          {
75
            assert(addresses.empty());
×
76
            assert(m_inputs.empty());
×
77

78
            if(addresses.empty())
×
79
            {
80
              uint32_t address;
81
              if(auto unusedAddress = newValue->getUnusedInputAddress(channel))
×
82
              {
83
                address = *unusedAddress;
×
84
              }
85
              else
86
              {
87
                address = interface->inputAddressMinMax(channel).first;
×
88
              }
89
              addresses.appendInternal(address);
×
90
              addInput(channel, inputLocation(channel, node, address), *newValue);
×
91
              updateInputConditions();
×
92
            }
93
          }
94
        }
95
        else // no interface
96
        {
97
          for(auto& it : m_inputs)
×
98
          {
99
            if(it.first) /*[[likely]]*/
×
100
            {
101
              releaseInput(it);
×
102
            }
103
          }
104

105
          m_inputs.clear();
×
106
          addresses.clearInternal();
×
107
          addressesSizeChanged();
×
108
        }
109
        return true;
×
110
      }}
111
  , channel{this, "channel", InputChannel::Input, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
72✔
112
      [this](const InputChannel newValue)
36✔
113
      {
114
        // Release inputs for previous channel:
115
        releaseInputs(m_inputs);
×
116

117
        // Get inputs for new channel:
118
        for(uint32_t address : addresses)
×
119
        {
120
          addInput(newValue, inputLocation(newValue, node, address));
×
121
        }
122

123
        channelChanged();
×
124
      }}
×
125
  , node{this, "node", 0, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
72✔
126
      nullptr,
127
      [this](uint32_t& value)
36✔
128
      {
129
        if(!interface) [[unlikely]]
×
130
        {
131
          return false;
×
132
        }
133

134
        Inputs newInputs;
×
135
        for(auto address : addresses)
×
136
        {
137
          auto inputConnPair = getInput(channel, InputNodeAddress(value, address), *interface);
×
138
          if(inputConnPair.first) [[likely]]
×
139
          {
140
            newInputs.emplace_back(inputConnPair);
×
141
          }
142
        }
×
143

144
        if(newInputs.size() != m_inputs.size())
×
145
        {
146
          releaseInputs(newInputs);
×
147
          assert(newInputs.empty());
×
148
          return false;
×
149
        }
150

151
        releaseInputs(m_inputs);
×
152
        assert(m_inputs.empty());
×
153
        m_inputs = std::move(newInputs);
×
154
        return true;
×
155
      }}
×
156
  , addresses{*this, "addresses", {}, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript,
72✔
157
      nullptr,
158
      [this](uint32_t index, uint32_t& value)
36✔
159
      {
160
        (void)index;
161

162
        if(!interface) [[unlikely]]
×
163
        {
164
          return false;
×
165
        }
166

167
        if(std::find(addresses.begin(), addresses.end(), value) != addresses.end())
×
168
        {
169
          return false; // Duplicate addresses aren't allowed.
×
170
        }
171

172
        auto inputConnPair = getInput(channel, inputLocation(channel, node, value), *interface);
×
173
        if(!inputConnPair.first) [[unlikely]]
×
174
        {
175
          return false; // Input doesn't exist.
×
176
        }
177

178
        if(index < m_inputs.size() && m_inputs[index].first)
×
179
        {
180
          releaseInput(m_inputs[index]);
×
181
        }
182

183
        m_inputs[index] = inputConnPair;
×
184

185
        return true;
×
186
      }}
×
187
  , items{*this, "items", {}, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject}
36✔
188
  , addAddress{*this, "add_address", MethodFlags::NoScript,
72✔
189
      [this]()
36✔
190
      {
191
        if(interface && addresses.size() < addressesSizeMax) [[likely]]
×
192
        {
193
          assert(!addresses.empty());
×
194
          const auto addressRange = interface->inputAddressMinMax(channel);
×
195
          if(addresses.size() >= (addressRange.second - addressRange.first + 1)) [[unlikely]]
×
196
          {
197
            return; // All addresses used.
×
198
          }
199
          const uint32_t address = getUnusedAddress();
×
200
          addresses.appendInternal(address);
×
201
          addInput(channel, inputLocation(channel, node, address));
×
202
          addressesSizeChanged();
×
203
        }
204
      }}
205
  , removeAddress{*this, "remove_address", MethodFlags::NoScript,
72✔
206
      [this]()
36✔
207
      {
208
        if(interface && addresses.size() > addressesSizeMin) [[likely]]
×
209
        {
210
          addresses.eraseInternal(addresses.size() - 1);
×
211
          if(m_inputs.back().first)
×
212
          {
213
            releaseInput(m_inputs.back());
×
214
          }
215
          m_inputs.pop_back();
×
216
          addressesSizeChanged();
×
217
        }
218
      }}
72✔
219
{
220
  auto& world = getWorld(&_parent);
36✔
221
  const bool editable = contains(world.state.value(), WorldState::Edit);
36✔
222

223
  m_interfaceItems.add(parentObject);
36✔
224

225
  Attributes::addDisplayName(interface, DisplayName::Hardware::interface);
36✔
226
  Attributes::addEnabled(interface, editable);
36✔
227
  Attributes::addObjectList(interface, world.inputControllers);
36✔
228
  m_interfaceItems.add(interface);
36✔
229

230
  Attributes::addDisplayName(node, DisplayName::Hardware::node);
36✔
231
  Attributes::addEnabled(node, editable);
36✔
232
  Attributes::addVisible(node, false);
36✔
233
  Attributes::addMinMax<uint32_t>(node, 0, 0);
36✔
234
  m_interfaceItems.add(node);
36✔
235

236
  Attributes::addDisplayName(channel, DisplayName::Hardware::channel);
36✔
237
  Attributes::addEnabled(channel, editable);
36✔
238
  Attributes::addValues(channel, std::span<const InputChannel>());
36✔
239
  Attributes::addVisible(channel, false);
36✔
240
  m_interfaceItems.add(channel);
36✔
241

242
  Attributes::addDisplayName(addresses, DisplayName::Hardware::address);
36✔
243
  Attributes::addEnabled(addresses, editable);
36✔
244
  Attributes::addVisible(addresses, false);
36✔
245
  Attributes::addMinMax<uint32_t>(addresses, 0, 0);
36✔
246
  m_interfaceItems.add(addresses);
36✔
247

248
  m_interfaceItems.add(items);
36✔
249

250
  Attributes::addDisplayName(addAddress, DisplayName::OutputMap::addAddress);
36✔
251
  Attributes::addEnabled(addAddress, false);
36✔
252
  Attributes::addVisible(addAddress, false);
36✔
253
  m_interfaceItems.add(addAddress);
36✔
254

255
  Attributes::addDisplayName(removeAddress, DisplayName::OutputMap::removeAddress);
36✔
256
  Attributes::addEnabled(removeAddress, false);
36✔
257
  Attributes::addVisible(removeAddress, false);
36✔
258
  m_interfaceItems.add(removeAddress);
36✔
259

260
  updateEnabled();
36✔
261
}
36✔
262

263
FeedbackMap::~FeedbackMap() = default;
36✔
264

265
void FeedbackMap::load(WorldLoader& loader, const nlohmann::json& data)
×
266
{
267
  SubObject::load(loader, data);
×
268

269
  if(interface)
×
270
  {
271
    for(uint32_t address : addresses)
×
272
    {
273
      addInput(channel, inputLocation(channel, node, address));
×
274
    }
275
    updateInputConditions();
×
276
  }
277
}
×
278

279
void FeedbackMap::loaded()
×
280
{
281
  if(interface)
×
282
  {
283
    interfaceChanged();
×
284
  }
285
  SubObject::loaded();
×
286
}
×
287

288
void FeedbackMap::worldEvent(WorldState state, WorldEvent event)
16✔
289
{
290
  SubObject::worldEvent(state, event);
16✔
291
  updateEnabled();
16✔
292
}
16✔
293

294
void FeedbackMap::interfaceChanged()
×
295
{
296
  const auto inputChannels = interface ? interface->inputChannels() : std::span<const InputChannel>{};
×
297
  Attributes::setValues(channel, inputChannels);
×
298
  Attributes::setVisible(channel, interface);
×
299
  channelChanged();
×
300
}
×
301

302
void FeedbackMap::channelChanged()
×
303
{
304
  if(interface)
×
305
  {
306
    Attributes::setVisible({addresses, addAddress, removeAddress}, true);
×
307
    Attributes::setVisible(node, hasNodeAddressLocation(channel));
×
308

309
    if(hasNodeAddressLocation(channel))
×
310
    {
311
      Attributes::setMinMax<uint32_t>(node, 0, 65535);// FIXME: interface->inputNodeMinMax(channel));
×
312
    }
313

314
    const auto addressRange = interface->inputAddressMinMax(channel);
×
315
    const uint32_t addressCount = (addressRange.second - addressRange.first + 1);
×
316
    Attributes::setMinMax(addresses, addressRange);
×
317

318
    while(addressCount < addresses.size()) // Reduce number of addresses if larger than address space.
×
319
    {
320
      addresses.eraseInternal(addresses.size() - 1);
×
321
    }
322

323
    // Make sure all addresses are in range:
324
    for(size_t i = 0; i < addresses.size(); i++)
×
325
    {
326
      if(!inRange(addresses[i], addressRange))
×
327
      {
328
        addresses.setValueInternal(i, getUnusedAddress());
×
329
      }
330
    }
331

332
    addressesSizeChanged();
×
333
  }
334
  else
335
  {
336
    Attributes::setVisible({node, addresses, addAddress, removeAddress}, false);
×
337
    Attributes::setMinMax(node, std::numeric_limits<uint32_t>::min(), std::numeric_limits<uint32_t>::max());
×
338
    Attributes::setMinMax(addresses, std::numeric_limits<uint32_t>::min(), std::numeric_limits<uint32_t>::max());
×
339
  }
340
}
×
341

342
void FeedbackMap::addressesSizeChanged()
×
343
{
344
  assert(addresses.size() == m_inputs.size());
×
345
  updateAddressDisplayName();
×
346
  updateInputConditions();
×
347
  updateEnabled();
×
348
}
×
349

350
void FeedbackMap::updateInputConditions()
×
351
{
352
  for(const auto& item : items)
×
353
  {
354
    while(m_inputs.size() > item->inputConditions.size())
×
355
    {
356
      item->inputConditions.appendInternal(std::make_shared<FeedbackMapInputCondition>(*this, item->inputConditions.size()));
×
357
    }
358

359
    while(m_inputs.size() < item->inputConditions.size())
×
360
    {
361
      item->inputConditions.back()->destroy();
×
362
      item->inputConditions.removeInternal(item->inputConditions.back());
×
363
    }
364

365
    assert(m_inputs.size() == item->inputConditions.size());
×
366
  }
367
}
×
368

369
void FeedbackMap::updateEnabled()
52✔
370
{
371
  const bool editable = contains(getWorld(parent()).state.value(), WorldState::Edit);
52✔
372

373
  Attributes::setEnabled(interface, editable);
52✔
374
  Attributes::setEnabled(channel, editable);
52✔
375
  Attributes::setEnabled(node, editable);
52✔
376
  Attributes::setEnabled(addresses, editable);
52✔
377
  Attributes::setEnabled(addAddress, editable && addresses.size() < addressesSizeMax);
52✔
378
  Attributes::setEnabled(removeAddress, editable && addresses.size() > addressesSizeMin);
52✔
379
}
52✔
380

381
uint32_t FeedbackMap::getUnusedAddress() const
×
382
{
383
  assert(interface);
×
384
  const auto addressRange = interface->inputAddressMinMax(channel);
×
385
  assert((addressRange.second - addressRange.first + 1) > addresses.size());
×
386
  uint32_t address = addresses.empty() ? addressRange.first : addresses.back();
×
387
  do
388
  {
389
    address++;
×
390
    if(!inRange(address, addressRange))
×
391
    {
392
      address = addressRange.first;
×
393
    }
394
  }
395
  while(std::find(addresses.begin(), addresses.end(), address) != addresses.end());
×
396

397
  return address;
×
398
}
399

400
void FeedbackMap::addInput(InputChannel ch, const InputLocation& location)
×
401
{
402
  addInput(ch, location, *interface);
×
403
}
×
404

405
void FeedbackMap::addInput(InputChannel ch, const InputLocation& location, InputController& inputController)
×
406
{
407
  m_inputs.emplace_back(getInput(ch, location, inputController));
×
408
  assert(m_inputs.back().first);
×
409
}
×
410

411
FeedbackMap::InputConnectionPair FeedbackMap::getInput(InputChannel ch, const InputLocation& location, InputController& inputController)
×
412
{
413
  auto input = inputController.getInput(ch, location, parent());
×
414
  if(!input)
×
415
    return {};
×
416

417
  boost::signals2::connection conn = input->onValueChanged.connect(std::bind_front(&FeedbackMap::inputValueChanged, this));
×
418

UNCOV
419
  return {input, conn};
×
420
}
×
421

422
void FeedbackMap::releaseInput(InputConnectionPair& inputConnPair)
×
423
{
UNCOV
424
  inputConnPair.second.disconnect();
×
425
  interface->releaseInput(*inputConnPair.first, parent());
×
UNCOV
426
}
×
427

428
void FeedbackMap::releaseInputs(Inputs& inputs)
×
429
{
UNCOV
430
  while(!inputs.empty())
×
431
  {
UNCOV
432
    if(inputs.back().first) [[likely]]
×
433
    {
UNCOV
434
      releaseInput(inputs.back());
×
435
    }
UNCOV
436
    inputs.pop_back();
×
437
  }
UNCOV
438
}
×
439

UNCOV
440
void FeedbackMap::inputValueChanged(bool value, const std::shared_ptr<Input>& input)
×
441
{
UNCOV
442
  const size_t invalidIndex = std::numeric_limits<size_t>::max();
×
443
  size_t inputIndex = invalidIndex;
×
UNCOV
444
  for(size_t i = 0; i < m_inputs.size(); ++i)
×
445
  {
446
    if(m_inputs[i].first == input)
×
447
    {
UNCOV
448
      inputIndex = i;
×
449
      break;
×
450
    }
451
  }
452
  if(inputIndex == invalidIndex) [[unlikely]]
×
453
  {
UNCOV
454
    assert(false);
×
455
    return;
456
  }
457

458
  const auto condition = value ? InputCondition::On : InputCondition::Off;
×
459
  size_t matchCount = 0;
×
UNCOV
460
  size_t matchIndex = 0;
×
461
  for(size_t i = 0; i < items.size(); ++i)
×
462
  {
463
    if(items[i]->inputConditions[inputIndex]->condition.value() != condition)
×
464
    {
465
      continue; // eliminate when condition does not match
×
466
    }
467

UNCOV
468
    if(items[i]->matches())
×
469
    {
470
      matchCount++;
×
471
      matchIndex = i;
×
472
    }
473
  }
474

475
  if(matchCount == 0 && m_lastMatchResult != MatchResult::None)
×
476
  {
477
    m_lastMatchResult = MatchResult::None;
×
UNCOV
478
    matchResultChanged(m_lastMatchResult, invalidMatchIndex);
×
479
  }
UNCOV
480
  else if(matchCount == 1 && (m_lastMatchResult != MatchResult::Match || m_lastMatchIndex != matchIndex))
×
481
  {
UNCOV
482
    m_lastMatchResult = MatchResult::Match;
×
UNCOV
483
    m_lastMatchIndex = matchIndex;
×
UNCOV
484
    matchResultChanged(m_lastMatchResult, m_lastMatchIndex);
×
485
  }
UNCOV
486
  else if(matchCount > 1 && m_lastMatchResult != MatchResult::Conflict) // more than one match -> conflict
×
487
  {
UNCOV
488
    Log::log(parent(), LogMessage::E3011_FEEDBACK_CONFLICT_MULTIPLE_OPTIONS_ARE_VALID);
×
489
    m_lastMatchResult = MatchResult::Conflict;
×
490
    matchResultChanged(m_lastMatchResult, invalidMatchIndex);
×
491
  }
492
}
493

494
void FeedbackMap::updateAddressDisplayName()
×
495
{
496
  switch(channel)
×
497
  {
498
    using enum InputChannel;
499

UNCOV
500
    case Input:
×
501
    case LocoNet:
502
    case RBus:
503
    case S88:
504
    case S88_Left:
505
    case S88_Middle:
506
    case S88_Right:
507
    case ECoSDetector:
UNCOV
508
      Attributes::setDisplayName(addresses, addresses.size() == 1 ? DisplayName::Hardware::address : DisplayName::Hardware::addresses);
×
UNCOV
509
      Attributes::setDisplayName(addAddress, DisplayName::OutputMap::addAddress);
×
UNCOV
510
      Attributes::setDisplayName(removeAddress, DisplayName::OutputMap::removeAddress);
×
UNCOV
511
      break;
×
512

UNCOV
513
    case LongEvent:
×
514
    case ShortEvent:
UNCOV
515
      Attributes::setDisplayName(addresses, addresses.size() == 1 ? DisplayName::Hardware::event : DisplayName::Hardware::events);
×
UNCOV
516
      Attributes::setDisplayName(addAddress, DisplayName::OutputMap::addEvent);
×
UNCOV
517
      Attributes::setDisplayName(removeAddress, DisplayName::OutputMap::removeEvent);
×
UNCOV
518
      break;
×
519
  }
UNCOV
520
}
×
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