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

traintastic / traintastic / 23669027368

27 Mar 2026 09:50PM UTC coverage: 26.198% (+0.02%) from 26.176%
23669027368

push

github

reinder
Merge remote-tracking branch 'origin/master' into cbus

11 of 144 new or added lines in 34 files covered. (7.64%)

1 existing line in 1 file now uncovered.

8256 of 31514 relevant lines covered (26.2%)

182.55 hits per line

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

0.0
/server/src/hardware/interface/cbusinterface.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 "cbusinterface.hpp"
23
#include "cbus/cbusnodelist.hpp"
24
#include "cbus/cbussettings.hpp"
25
#include "../decoder/decoderchangeflags.hpp"
26
#include "../decoder/list/decoderlist.hpp"
27
#include "../decoder/list/decoderlisttablemodel.hpp"
28
#include "../input/list/inputlist.hpp"
29
#include "../output/list/outputlist.hpp"
30
#include "../protocol/cbus/cbusconst.hpp"
31
#include "../protocol/cbus/cbuskernel.hpp"
32
#include "../protocol/cbus/iohandler/cbuscanusbiohandler.hpp"
33
#include "../protocol/cbus/iohandler/cbuscanetheriohandler.hpp"
34
#include "../protocol/cbus/iohandler/cbussimulationiohandler.hpp"
35
#include "../protocol/cbus/simulator/cbussimulator.hpp"
36
#include "../protocol/cbus/simulator/module/cbuscancmd.hpp"
37
#include "../../core/attributes.hpp"
38
#include "../../core/eventloop.hpp"
39
#include "../../core/method.tpp"
40
#include "../../core/objectproperty.tpp"
41
#include "../../log/log.hpp"
42
#include "../../log/logmessageexception.hpp"
43
#include "../../utils/displayname.hpp"
44
#include "../../utils/inrange.hpp"
45
#include "../../world/world.hpp"
46

47
constexpr auto decoderListColumns = DecoderListColumn::Id | DecoderListColumn::Name | DecoderListColumn::Address;
48
constexpr auto inputListColumns = InputListColumn::Channel | InputListColumn::Node | InputListColumn::Address;
49
constexpr auto outputListColumns = OutputListColumn::Channel | OutputListColumn::Node | OutputListColumn::Address;
50

51
constexpr auto nodeNumberRange = std::make_pair(std::numeric_limits<uint16_t>::min(), std::numeric_limits<uint16_t>::max());
52
constexpr auto eventNumberRange = std::make_pair(std::numeric_limits<uint16_t>::min(), std::numeric_limits<uint16_t>::max());
53

54
CREATE_IMPL(CBUSInterface)
×
55

56
CBUSInterface::CBUSInterface(World& world, std::string_view _id)
×
57
  : Interface(world, _id)
58
  , DecoderController(*this, decoderListColumns)
59
  , InputController(static_cast<IdObject&>(*this))
60
  , OutputController(static_cast<IdObject&>(*this))
61
  , type{this, "type", CBUSInterfaceType::CANUSB, PropertyFlags::ReadWrite | PropertyFlags::Store,
×
62
      [this](CBUSInterfaceType /*value*/)
×
63
      {
64
        updateVisible();
×
65
      }}
×
66
  , device{this, "device", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
×
67
  , hostname{this, "hostname", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
×
68
  , port{this, "port", 0, PropertyFlags::ReadWrite | PropertyFlags::Store}
×
69
  , cbus{this, "cbus", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject}
×
70
  , cbusNodeList{this, "cbus_node_list", nullptr, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::SubObject}
×
71
{
72
  name = "CBUS/VLCB";
×
73
  cbus.setValueInternal(std::make_shared<CBUSSettings>(*this, cbus.name()));
×
74
  cbusNodeList.setValueInternal(std::make_shared<CBUSNodeList>(*this, cbusNodeList.name()));
×
75

76
  Attributes::addDisplayName(type, DisplayName::Interface::type);
×
77
  Attributes::addEnabled(type, !online);
×
78
  Attributes::addValues(type, CBUSInterfaceTypeValues);
×
79
  m_interfaceItems.insertBefore(type, notes);
×
80

81
  Attributes::addEnabled(device, !online);
×
82
  Attributes::addVisible(device, false);
×
83
  m_interfaceItems.insertBefore(device, notes);
×
84

85
  Attributes::addDisplayName(hostname, DisplayName::IP::hostname);
×
86
  Attributes::addEnabled(hostname, !online);
×
87
  Attributes::addVisible(hostname, false);
×
88
  m_interfaceItems.insertBefore(hostname, notes);
×
89

90
  Attributes::addDisplayName(port, DisplayName::IP::port);
×
91
  Attributes::addEnabled(port, !online);
×
92
  Attributes::addVisible(port, false);
×
93
  m_interfaceItems.insertBefore(port, notes);
×
94

95
  m_interfaceItems.insertBefore(cbus, notes);
×
96

97
  m_interfaceItems.insertBefore(cbusNodeList, notes);
×
98

99
  m_interfaceItems.insertBefore(decoders, notes);
×
100

101
  m_interfaceItems.insertBefore(inputs, notes);
×
102

103
  m_interfaceItems.insertBefore(outputs, notes);
×
104

105
  m_cbusPropertyChanged = cbus->propertyChanged.connect(
×
106
    [this](BaseProperty& /*property*/)
×
107
    {
108
      if(m_kernel)
×
109
      {
110
        m_kernel->setConfig(cbus->config());
×
111
      }
112
    });
×
113

114
  updateVisible();
×
115
}
×
116

117
CBUSInterface::~CBUSInterface() = default;
×
118

119
bool CBUSInterface::send(std::vector<uint8_t> message)
×
120
{
121
  if(m_kernel)
×
122
  {
123
    return m_kernel->send(std::move(message));
×
124
  }
125
  return false;
×
126
}
127

128
bool CBUSInterface::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
129
{
130
  if(m_kernel)
×
131
  {
132
    return m_kernel->sendDCC(std::move(dccPacket), repeat);
×
133
  }
134
  return false;
×
135
}
136

137
std::span<const DecoderProtocol> CBUSInterface::decoderProtocols() const
×
138
{
139
  static constexpr std::array<DecoderProtocol, 2> protocols{DecoderProtocol::DCCShort, DecoderProtocol::DCCLong};
140
  return std::span<const DecoderProtocol>{protocols.data(), protocols.size()};
×
141
}
142

143
void CBUSInterface::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber)
×
144
{
145
  if(m_kernel)
×
146
  {
147
    const bool longAddress = (decoder.protocol == DecoderProtocol::DCCLong);
×
148

149
    if(has(changes, DecoderChangeFlags::FunctionValue) && functionNumber <= CBUS::engineFunctionMax)
×
150
    {
151
      m_kernel->setEngineFunction(
×
152
        decoder.address,
153
        longAddress,
154
        static_cast<uint8_t>(functionNumber),
155
        decoder.getFunctionValue(functionNumber));
×
156
    }
157
    else
158
    {
159
      m_kernel->setEngineSpeedDirection(
×
160
        decoder.address,
161
        longAddress,
162
        Decoder::throttleToSpeedStep<uint8_t>(decoder.throttle, decoder.speedSteps),
×
163
        decoder.speedSteps,
164
        decoder.emergencyStop,
165
        decoder.direction == Direction::Forward);
×
166
    }
167
  }
168
}
×
169

170
std::span<const InputChannel> CBUSInterface::inputChannels() const
×
171
{
172
  static const std::array<InputChannel, 2> channels{
173
    InputChannel::ShortEvent,
174
    InputChannel::LongEvent,
175
  };
176
  return channels;
×
177
}
178

179
bool CBUSInterface::isInputLocation(InputChannel channel, const InputLocation& location) const
×
180
{
181
  if(hasNodeAddressLocation(channel))
×
182
  {
183
    return
184
      inRange<uint32_t>(std::get<InputNodeAddress>(location).node, nodeNumberRange) &&
×
185
      inRange<uint32_t>(std::get<InputNodeAddress>(location).address, inputAddressMinMax(channel));
×
186
  }
187
  return InputController::isInputLocation(channel, location);
×
188
}
189

190
std::pair<uint32_t, uint32_t> CBUSInterface::inputAddressMinMax(InputChannel /*channel*/) const
×
191
{
192
  return eventNumberRange;
×
193
}
194

195
void CBUSInterface::inputSimulateChange(InputChannel channel, const InputLocation& location, SimulateInputAction action)
×
196
{
197
  if(m_simulator)
×
198
  {
NEW
199
    boost::asio::post(m_kernel->ioContext(),
×
200
      [this, channel, location, action]
×
201
      {
202
        switch(channel)
×
203
        {
204
          case InputChannel::ShortEvent:
×
205
            m_simulator->shortEvent(std::get<InputAddress>(location).address, action);
×
206
            break;
×
207

208
          case InputChannel::LongEvent:
×
209
            m_simulator->longEvent(std::get<InputNodeAddress>(location).node, std::get<InputNodeAddress>(location).address, action);
×
210
            break;
×
211

212
          default: [[unlikely]]
×
213
            assert(false);
×
214
            break;
215
        }
216
      });
×
217
  }
218
}
×
219

220
std::span<const OutputChannel> CBUSInterface::outputChannels() const
×
221
{
222
  static const std::array<OutputChannel, 4> channels{
223
    OutputChannel::ShortEvent,
224
    OutputChannel::LongEvent,
225
    OutputChannel::AccessoryDCC,
226
    OutputChannel::DCCext,
227
  };
228
  return channels;
×
229
}
230

231
std::pair<uint32_t, uint32_t> CBUSInterface::outputNodeMinMax(OutputChannel /*channel*/) const
×
232
{
233
  return eventNumberRange;
×
234
}
235

236
std::pair<uint32_t, uint32_t> CBUSInterface::outputAddressMinMax(OutputChannel channel) const
×
237
{
238
  switch(channel)
×
239
  {
240
    case OutputChannel::LongEvent:
×
241
    case OutputChannel::ShortEvent:
242
      return {std::numeric_limits<uint16_t>::min(), std::numeric_limits<uint16_t>::max()};
×
243

244
    default:
×
245
      return OutputController::outputAddressMinMax(channel);
×
246
  }
247
}
248

249
bool CBUSInterface::setOutputValue(OutputChannel channel, const OutputLocation& location, OutputValue value)
×
250
{
251
  if(m_kernel)
×
252
  {
253
    switch(channel)
×
254
    {
255
      case OutputChannel::ShortEvent:
×
256
        assert(std::holds_alternative<TriState>(value));
×
257
        if(auto v = std::get<TriState>(value); v != TriState::Undefined)
×
258
        {
259
          assert(std::holds_alternative<OutputAddress>(location));
×
260
          const auto address = static_cast<uint16_t>(std::get<OutputAddress>(location).address);
×
261
          m_kernel->setAccessoryShort(address, v == TriState::True);
×
262
          return true;
×
263
        }
264
        break;
×
265

266
      case OutputChannel::LongEvent:
×
267
        assert(std::holds_alternative<TriState>(value));
×
268
        if(auto v = std::get<TriState>(value); v != TriState::Undefined)
×
269
        {
270
          assert(std::holds_alternative<OutputNodeAddress>(location));
×
271
          const auto node = static_cast<uint16_t>(std::get<OutputNodeAddress>(location).node);
×
272
          const auto address = static_cast<uint16_t>(std::get<OutputNodeAddress>(location).address);
×
273
          m_kernel->setAccessory(node, address, v == TriState::True);
×
274
          return true;
×
275
        }
276
        break;
×
277

278
      case OutputChannel::AccessoryDCC:
×
279
        assert(std::holds_alternative<OutputPairValue>(value));
×
280
        if(const auto v = std::get<OutputPairValue>(value); v == OutputPairValue::First || v == OutputPairValue::Second)
×
281
        {
282
          assert(std::holds_alternative<OutputAddress>(location));
×
283
          const auto address = static_cast<uint16_t>(std::get<OutputAddress>(location).address);
×
284
          m_kernel->setDccAccessory(address, v == OutputPairValue::Second);
×
285
          return true;
×
286
        }
287
        break;
×
288

289
      case OutputChannel::DCCext:
×
290
        assert(std::holds_alternative<int16_t>(value));
×
291
        if(const auto v = std::get<int16_t>(value); inRange<int16_t>(v, std::numeric_limits<uint8_t>::min(), std::numeric_limits<uint8_t>::max()))
×
292
        {
293
          assert(std::holds_alternative<OutputAddress>(location));
×
294
          const auto address = static_cast<uint16_t>(std::get<OutputAddress>(location).address);
×
295
          m_kernel->setDccAdvancedAccessoryValue(address, static_cast<uint8_t>(v));
×
296
          return true;
×
297
        }
298
        break;
×
299

300
      default: [[unlikely]]
×
301
        assert(false);
×
302
        break;
303
    }
304
  }
305
  return false;
×
306
}
307

308
void CBUSInterface::addToWorld()
×
309
{
310
  Interface::addToWorld();
×
311
  DecoderController::addToWorld();
×
312
  InputController::addToWorld(inputListColumns);
×
313
  OutputController::addToWorld(outputListColumns);
×
314
}
×
315

316
void CBUSInterface::loaded()
×
317
{
318
  Interface::loaded();
×
319

320
  updateVisible();
×
321
}
×
322

323
void CBUSInterface::destroying()
×
324
{
325
  m_cbusPropertyChanged.disconnect();
×
326
  OutputController::destroying();
×
327
  InputController::destroying();
×
328
  DecoderController::destroying();
×
329
  Interface::destroying();
×
330
}
×
331

332
void CBUSInterface::worldEvent(WorldState state, WorldEvent event)
×
333
{
334
  Interface::worldEvent(state, event);
×
335

336
  switch(event)
×
337
  {
338
    case WorldEvent::PowerOff:
×
339
      if(m_kernel)
×
340
      {
341
        m_kernel->trackOff();
×
342
      }
343
      break;
×
344

345
    case WorldEvent::PowerOn:
×
346
      if(m_kernel)
×
347
      {
348
        m_kernel->trackOn();
×
349
      }
350
      break;
×
351

352
    case WorldEvent::Stop:
×
353
      if(m_kernel)
×
354
      {
355
        m_kernel->requestEmergencyStop();
×
356
      }
357
      break;
×
358

359
    case WorldEvent::Run:
×
360
      if(m_kernel)
×
361
      {
362
        // TODO: send all known speed values
363
      }
364
      break;
×
365

366
    default:
×
367
      break;
×
368
  }
369
}
×
370

371
bool CBUSInterface::setOnline(bool& value, bool simulation)
×
372
{
373
  if(!m_kernel && value)
×
374
  {
375
    try
376
    {
377
      if(simulation)
×
378
      {
379
        m_simulator = std::make_unique<CBUS::Simulator>();
×
380
        m_simulator->addModule(std::make_unique<CBUS::Module::CANCMD>(*m_simulator));
×
381
        m_kernel = CBUS::Kernel::create<CBUS::SimulationIOHandler>(id.value(), cbus->config(), std::ref(*m_simulator));
×
382
      }
383
      else
384
      {
385
        switch(type)
×
386
        {
387
          case CBUSInterfaceType::CANUSB:
×
388
            m_kernel = CBUS::Kernel::create<CBUS::CANUSBIOHandler>(id.value(), cbus->config(), device.value());
×
389
            break;
×
390

391
          case CBUSInterfaceType::CANEther:
×
392
            m_kernel = CBUS::Kernel::create<CBUS::CANEtherIOHandler>(id.value(), cbus->config(), hostname.value(), port.value());
×
393
            break;
×
394
        }
395
      }
396

397
      setState(InterfaceState::Initializing);
×
398

399
      m_kernel->setOnStarted(
×
400
        [this]()
×
401
        {
402
          setState(InterfaceState::Online);
×
403
        });
×
404
      m_kernel->setOnError(
×
405
        [this]()
×
406
        {
407
          setState(InterfaceState::Error);
×
408
          online = false; // communication no longer possible
×
409
        });
×
410
      m_kernel->onPresenceOfNode =
×
411
        [this](uint8_t canId, uint16_t nodeNumber, uint8_t manufacturerId, uint8_t moduleId)
×
412
        {
413
          cbusNodeList->add(CBUSNodeList::Node{
×
414
            .nodeNumber = nodeNumber,
415
            .canId = canId,
416
            .manufacturerId = manufacturerId,
417
            .moduleId = moduleId,
418
            .parameters = {},
419
          });
420
        };
×
421
      m_kernel->onNodeParameterResponse =
×
422
        [this](uint8_t canId, uint16_t nodeNumber, CBUS::NodeParameter parameter, uint8_t parameterValue)
×
423
        {
424
          auto& nodes = cbusNodeList->m_nodes;
×
425
          if(auto it = std::find_if(nodes.begin(), nodes.end(),
×
426
              [canId, nodeNumber](const auto& item)
×
427
              {
428
                return item.canId == canId && item.nodeNumber == nodeNumber;
×
429
              }); it != nodes.end())
×
430
          {
431
            switch(parameter)
×
432
            {
433
              using enum CBUS::NodeParameter;
434

435
              case VersionMajor:
×
436
                it->parameters.versionMajor = parameterValue;
×
437
                break;
×
438

439
              case VersionMinor:
×
440
                it->parameters.versionMinor = parameterValue;
×
441
                break;
×
442

443
              case BetaReleaseCode:
×
444
                it->parameters.betaReleaseCode = parameterValue;
×
445
                break;
×
446

447
              default:
×
448
                return;
×
449
            }
450
            cbusNodeList->rowChanged(static_cast<uint32_t>(std::distance(nodes.begin(), it)));
×
451
          }
452
        };
×
453
      m_kernel->onTrackOff =
×
454
        [this]()
×
455
        {
456
          if(contains(m_world.state, WorldState::PowerOn))
×
457
          {
458
            m_world.powerOff();
×
459
          }
460
        };
×
461
      m_kernel->onTrackOn =
×
462
        [this]()
×
463
        {
464
          if(!contains(m_world.state, WorldState::PowerOn))
×
465
          {
466
            m_world.powerOn();
×
467
          }
468
        };
×
469
      m_kernel->onEmergencyStop =
×
470
        [this]()
×
471
        {
472
          if(contains(m_world.state, WorldState::Run))
×
473
          {
474
            m_world.stop();
×
475
          }
476
        };
×
477
      m_kernel->onShortEvent =
×
478
        [this](uint16_t eventNumber, bool on)
×
479
        {
480
          updateInputValue(InputChannel::ShortEvent, InputAddress(eventNumber), toTriState(on));
×
481
          updateOutputValue(OutputChannel::ShortEvent, OutputAddress(eventNumber), toTriState(on));
×
482
        };
×
483
      m_kernel->onLongEvent =
×
484
        [this](uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
485
        {
486
          updateInputValue(InputChannel::LongEvent, InputNodeAddress(nodeNumber, eventNumber), toTriState(on));
×
487
          updateOutputValue(OutputChannel::LongEvent, OutputNodeAddress(nodeNumber, eventNumber), toTriState(on));
×
488
        };
×
489

490
      m_kernel->start();
×
491
    }
492
    catch(const LogMessageException& e)
×
493
    {
494
      setState(InterfaceState::Offline);
×
495
      Log::log(*this, e.message(), e.args());
×
496
      return false;
×
497
    }
×
498
  }
499
  else if(m_kernel && !value)
×
500
  {
501
    m_kernel->stop();
×
502
    EventLoop::deleteLater(m_kernel.release());
×
503
    EventLoop::deleteLater(m_simulator.release());
×
504

505
    cbusNodeList->clear();
×
506

507
    if(status->state != InterfaceState::Error)
×
508
    {
509
      setState(InterfaceState::Offline);
×
510
    }
511
  }
512
  return true;
×
513
}
514

515
void CBUSInterface::updateVisible()
×
516
{
517
  const bool isSerial = (type == CBUSInterfaceType::CANUSB);
×
518
  Attributes::setVisible(device, isSerial);
×
519

520
  const bool isNetwork = (type == CBUSInterfaceType::CANEther);
×
521
  Attributes::setVisible(hostname, isNetwork);
×
522
  Attributes::setVisible(port, isNetwork);
×
523
}
×
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