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

traintastic / traintastic / 23088148108

14 Mar 2026 12:40PM UTC coverage: 26.661% (-0.2%) from 26.84%
23088148108

push

github

reinder
[cbus] added input support for short/long events

0 of 133 new or added lines in 4 files covered. (0.0%)

587 existing lines in 20 files now uncovered.

8246 of 30929 relevant lines covered (26.66%)

185.83 hits per line

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

42.62
/server/src/hardware/interface/xpressnetinterface.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2019-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 "xpressnetinterface.hpp"
23
#include "../decoder/list/decoderlist.hpp"
24
#include "../input/input.hpp"
25
#include "../input/list/inputlist.hpp"
26
#include "../output/list/outputlist.hpp"
27
#include "../protocol/xpressnet/kernel.hpp"
28
#include "../protocol/xpressnet/settings.hpp"
29
#include "../protocol/xpressnet/messages.hpp"
30
#include "../protocol/xpressnet/iohandler/serialiohandler.hpp"
31
#include "../protocol/xpressnet/iohandler/simulationiohandler.hpp"
32
#include "../protocol/xpressnet/iohandler/liusbiohandler.hpp"
33
#include "../protocol/xpressnet/iohandler/rosofts88xpressnetliiohandler.hpp"
34
#include "../protocol/xpressnet/iohandler/tcpiohandler.hpp"
35
#include "../../core/attributes.hpp"
36
#include "../../core/eventloop.hpp"
37
#include "../../core/method.tpp"
38
#include "../../core/objectproperty.tpp"
39
#include "../../log/log.hpp"
40
#include "../../log/logmessageexception.hpp"
41
#include "../../utils/displayname.hpp"
42
#include "../../utils/inrange.hpp"
43
#include "../../utils/makearray.hpp"
44
#include "../../world/world.hpp"
45

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

50
CREATE_IMPL(XpressNetInterface)
9✔
51

52
XpressNetInterface::XpressNetInterface(World& world, std::string_view _id)
9✔
53
  : Interface(world, _id)
54
  , DecoderController(*this, decoderListColumns)
55
  , InputController(static_cast<IdObject&>(*this))
56
  , OutputController(static_cast<IdObject&>(*this))
57
  , type{this, "type", XpressNetInterfaceType::Serial, PropertyFlags::ReadWrite | PropertyFlags::Store,
18✔
58
      [this](XpressNetInterfaceType /*value*/)
9✔
59
      {
60
        updateVisible();
×
61
      }}
×
62
  , serialInterfaceType{this, "interface", XpressNetSerialInterfaceType::LenzLI100, PropertyFlags::ReadWrite | PropertyFlags::Store,
18✔
63
      [this](XpressNetSerialInterfaceType value)
9✔
64
      {
65
        switch(value)
×
66
        {
67
          case XpressNetSerialInterfaceType::LenzLI100:
×
68
          case XpressNetSerialInterfaceType::RoSoftS88XPressNetLI:
69
            baudrate.setValueInternal(9600);
×
70
            flowControl.setValueInternal(SerialFlowControl::Hardware);
×
71
            break;
×
72

73
          case XpressNetSerialInterfaceType::LenzLI100F:
×
74
          case XpressNetSerialInterfaceType::LenzLI101F:
75
            baudrate.setValueInternal(19200);
×
76
            flowControl.setValueInternal(SerialFlowControl::Hardware);
×
77
            break;
×
78

79
          case XpressNetSerialInterfaceType::LenzLIUSB:
×
80
            baudrate.setValueInternal(57600);
×
81
            flowControl.setValueInternal(SerialFlowControl::None);
×
82
            break;
×
83

84
          case XpressNetSerialInterfaceType::DigikeijsDR5000:
×
85
            baudrate.setValueInternal(115200);
×
86
            flowControl.setValueInternal(SerialFlowControl::None);
×
87
            break;
×
88
        }
89
        updateVisible();
×
90
      }}
×
91
  , device{this, "device", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
18✔
92
  , baudrate{this, "baudrate", 19200, PropertyFlags::ReadWrite | PropertyFlags::Store}
9✔
93
  , flowControl{this, "flow_control", SerialFlowControl::None, PropertyFlags::ReadWrite | PropertyFlags::Store}
9✔
94
  , hostname{this, "hostname", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
18✔
95
  , port{this, "port", 5550, PropertyFlags::ReadWrite | PropertyFlags::Store}
9✔
96
  , s88StartAddress{this, "s88_start_address", XpressNet::RoSoftS88XpressNetLI::S88StartAddress::startAddressDefault, PropertyFlags::ReadWrite | PropertyFlags::Store}
9✔
97
  , s88ModuleCount{this, "s88_module_count", XpressNet::RoSoftS88XpressNetLI::S88ModuleCount::moduleCountDefault, PropertyFlags::ReadWrite | PropertyFlags::Store}
9✔
98
  , xpressnet{this, "xpressnet", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject}
18✔
99
{
100
  name = "XpressNet";
9✔
101
  xpressnet.setValueInternal(std::make_shared<XpressNet::Settings>(*this, xpressnet.name()));
9✔
102

103
  Attributes::addDisplayName(type, DisplayName::Interface::type);
9✔
104
  Attributes::addEnabled(type, !online);
9✔
105
  Attributes::addValues(type, xpressNetInterfaceTypeValues);
9✔
106
  m_interfaceItems.insertBefore(type, notes);
9✔
107

108
  Attributes::addValues(serialInterfaceType, XpressNetSerialInterfaceTypeValues);
9✔
109
  Attributes::addEnabled(serialInterfaceType, !online);
9✔
110
  Attributes::addVisible(serialInterfaceType, false);
9✔
111
  m_interfaceItems.insertBefore(serialInterfaceType, notes);
9✔
112

113
  Attributes::addEnabled(device, !online);
9✔
114
  Attributes::addVisible(device, false);
9✔
115
  m_interfaceItems.insertBefore(device, notes);
9✔
116

117
  Attributes::addDisplayName(baudrate, DisplayName::Serial::baudrate);
9✔
118
  Attributes::addEnabled(baudrate, !online);
9✔
119
  Attributes::addVisible(baudrate, false);
9✔
120
  m_interfaceItems.insertBefore(baudrate, notes);
9✔
121

122
  Attributes::addDisplayName(flowControl, DisplayName::Serial::flowControl);
9✔
123
  Attributes::addEnabled(flowControl, !online);
9✔
124
  Attributes::addValues(flowControl, SerialFlowControlValues);
9✔
125
  Attributes::addVisible(flowControl, false);
9✔
126
  m_interfaceItems.insertBefore(flowControl, notes);
9✔
127

128
  Attributes::addDisplayName(hostname, DisplayName::IP::hostname);
9✔
129
  Attributes::addEnabled(hostname, !online);
9✔
130
  Attributes::addVisible(hostname, false);
9✔
131
  m_interfaceItems.insertBefore(hostname, notes);
9✔
132

133
  Attributes::addDisplayName(port, DisplayName::IP::port);
9✔
134
  Attributes::addEnabled(port, !online);
9✔
135
  Attributes::addVisible(port, false);
9✔
136
  m_interfaceItems.insertBefore(port, notes);
9✔
137

138
  Attributes::addMinMax(s88StartAddress, XpressNet::RoSoftS88XpressNetLI::S88StartAddress::startAddressMin, XpressNet::RoSoftS88XpressNetLI::S88StartAddress::startAddressMax);
9✔
139
  Attributes::addEnabled(s88StartAddress, !online);
9✔
140
  Attributes::addVisible(s88StartAddress, false);
9✔
141
  m_interfaceItems.insertBefore(s88StartAddress, notes);
9✔
142

143
  Attributes::addMinMax(s88ModuleCount, XpressNet::RoSoftS88XpressNetLI::S88ModuleCount::moduleCountMin, XpressNet::RoSoftS88XpressNetLI::S88ModuleCount::moduleCountMax);
9✔
144
  Attributes::addEnabled(s88ModuleCount, !online);
9✔
145
  Attributes::addVisible(s88ModuleCount, false);
9✔
146
  m_interfaceItems.insertBefore(s88ModuleCount, notes);
9✔
147

148
  Attributes::addDisplayName(xpressnet, DisplayName::Hardware::xpressnet);
9✔
149
  m_interfaceItems.insertBefore(xpressnet, notes);
9✔
150

151
  m_interfaceItems.insertBefore(decoders, notes);
9✔
152

153
  m_interfaceItems.insertBefore(inputs, notes);
9✔
154

155
  m_interfaceItems.insertBefore(outputs, notes);
9✔
156

157
  updateVisible();
9✔
158
}
9✔
159

160
XpressNetInterface::~XpressNetInterface() = default;
9✔
161

162
std::span<const DecoderProtocol> XpressNetInterface::decoderProtocols() const
10✔
163
{
164
  static constexpr std::array<DecoderProtocol, 2> protocols{DecoderProtocol::DCCShort, DecoderProtocol::DCCLong};
165
  return std::span<const DecoderProtocol>{protocols.data(), protocols.size()};
20✔
166
}
167

168
std::pair<uint16_t, uint16_t> XpressNetInterface::decoderAddressMinMax(DecoderProtocol protocol) const
5✔
169
{
170
  switch(protocol)
5✔
171
  {
172
    case DecoderProtocol::DCCShort:
5✔
173
      return {XpressNet::shortAddressMin, XpressNet::shortAddressMax};
5✔
174

175
    case DecoderProtocol::DCCLong:
×
176
      return {XpressNet::longAddressMin, XpressNet::longAddressMax};
×
177

178
    default: /*[[unlikely]]*/
×
179
      return DecoderController::decoderAddressMinMax(protocol);
×
180
  }
181
}
182

183
std::span<const uint8_t> XpressNetInterface::decoderSpeedSteps(DecoderProtocol protocol) const
5✔
184
{
185
  static constexpr std::array<uint8_t, 4> dccSpeedSteps{{14, 27, 28, 128}}; // XpressNet also support 27 steps
186

187
  switch(protocol)
5✔
188
  {
189
    case DecoderProtocol::DCCShort:
5✔
190
    case DecoderProtocol::DCCLong:
191
      return dccSpeedSteps;
5✔
192

193
    default: /*[[unlikely]]*/
×
194
      return DecoderController::decoderSpeedSteps(protocol);
×
195
  }
196
}
197

198
void XpressNetInterface::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber)
4✔
199
{
200
  if(m_kernel)
4✔
201
    m_kernel->decoderChanged(decoder, changes, functionNumber);
×
202
}
4✔
203

204
std::span<const InputChannel> XpressNetInterface::inputChannels() const
9✔
205
{
206
  static const auto values = makeArray(InputChannel::Input);
207
  return values;
9✔
208
}
209

210
std::pair<uint32_t, uint32_t> XpressNetInterface::inputAddressMinMax(InputChannel /*channel*/) const
×
211
{
212
  return {XpressNet::Kernel::inputAddressMin, XpressNet::Kernel::inputAddressMax};
×
213
}
214

215
void XpressNetInterface::inputSimulateChange(InputChannel channel, const InputLocation& location, SimulateInputAction action)
×
216
{
217
  assert(std::holds_alternative<InputAddress>(location));
×
218
  const auto address = std::get<InputAddress>(location).address;
×
219
  if(m_kernel && inRange(address, inputAddressMinMax(channel)))
×
UNCOV
220
    m_kernel->simulateInputChange(address, action);
×
UNCOV
221
}
×
222

223
std::span<const OutputChannel> XpressNetInterface::outputChannels() const
20✔
224
{
225
  static const auto values = makeArray(OutputChannel::Accessory);
226
  return values;
20✔
227
}
228

229
std::pair<uint32_t, uint32_t> XpressNetInterface::outputAddressMinMax(OutputChannel /*channel*/) const
2✔
230
{
231
  return {XpressNet::Kernel::accessoryOutputAddressMin, XpressNet::Kernel::accessoryOutputAddressMax};
2✔
232
}
233

234
bool XpressNetInterface::setOutputValue(OutputChannel channel, const OutputLocation& location, OutputValue value)
×
235
{
UNCOV
236
  assert(isOutputChannel(channel));
×
237
  const auto address = std::get<OutputAddress>(location).address;
×
238
  return
239
      m_kernel &&
×
UNCOV
240
      inRange(address, outputAddressMinMax(channel)) &&
×
UNCOV
241
      m_kernel->setOutput(static_cast<uint16_t>(address), std::get<OutputPairValue>(value));
×
242
}
243

244
bool XpressNetInterface::setOnline(bool& value, bool simulation)
×
245
{
UNCOV
246
  if(!m_kernel && value)
×
247
  {
248
    try
249
    {
250
      if(simulation)
×
251
      {
UNCOV
252
        m_kernel = XpressNet::Kernel::create<XpressNet::SimulationIOHandler>(id.value(), xpressnet->config());
×
253
      }
254
      else
255
      {
256
        switch(type)
×
257
        {
UNCOV
258
          case XpressNetInterfaceType::Serial:
×
259
            switch(serialInterfaceType)
×
260
            {
UNCOV
261
              case XpressNetSerialInterfaceType::LenzLI100:
×
262
              case XpressNetSerialInterfaceType::LenzLI100F:
263
              case XpressNetSerialInterfaceType::LenzLI101F:
UNCOV
264
                m_kernel = XpressNet::Kernel::create<XpressNet::SerialIOHandler>(id.value(), xpressnet->config(), device.value(), baudrate.value(), flowControl.value());
×
265
                break;
×
266

267
              case XpressNetSerialInterfaceType::RoSoftS88XPressNetLI:
×
UNCOV
268
                m_kernel = XpressNet::Kernel::create<XpressNet::RoSoftS88XPressNetLIIOHandler>(id.value(), xpressnet->config(), device.value(), baudrate.value(), flowControl.value(), s88StartAddress.value(), s88ModuleCount.value());
×
269
                break;
×
270

271
              case XpressNetSerialInterfaceType::LenzLIUSB:
×
272
              case XpressNetSerialInterfaceType::DigikeijsDR5000:
UNCOV
273
                m_kernel = XpressNet::Kernel::create<XpressNet::LIUSBIOHandler>(id.value(), xpressnet->config(), device.value(), baudrate.value(), flowControl.value());
×
274
                break;
×
275
            }
276
            break;
×
277

278
          case XpressNetInterfaceType::Network:
×
UNCOV
279
            m_kernel = XpressNet::Kernel::create<XpressNet::TCPIOHandler>(id.value(), xpressnet->config(), hostname.value(), port.value());
×
UNCOV
280
            break;
×
281
        }
282
      }
283

284
      if(!m_kernel)
×
285
      {
UNCOV
286
        assert(false);
×
287
        return false;
288
      }
289

290
      setState(InterfaceState::Initializing);
×
291

UNCOV
292
      m_kernel->setOnStarted(
×
293
        [this]()
×
294
        {
295
          setState(InterfaceState::Online);
×
296
        });
×
UNCOV
297
      m_kernel->setOnError(
×
298
        [this]()
×
299
        {
300
          setState(InterfaceState::Error);
×
301
          online = false; // communication no longer possible
×
302
        });
×
UNCOV
303
      m_kernel->setOnTrackPowerChanged(
×
304
        [this](bool powerOn, bool isStopped)
×
305
        {
UNCOV
306
          if(powerOn)
×
307
          {
308
            /* NOTE:
309
             * Setting stop and powerOn together is not an atomic operation,
310
             * so it would trigger 2 state changes with in the middle state.
311
             * Fortunately this does not happen because at least one of the state is already set.
312
             * Because if we are in Run state we go to PowerOn,
313
             * and if we are on PowerOff then we go to PowerOn.
314
             */
315

316
            // First of all, stop if we have to, otherwhise we might inappropiately run trains
317
            if(isStopped && contains(m_world.state.value(), WorldState::Run))
×
318
            {
319
              m_world.stop();
×
320
            }
321
            else if(!contains(m_world.state.value(), WorldState::Run) && !isStopped)
×
322
            {
UNCOV
323
              m_world.run(); // Run trains yay!
×
324
            }
325

326
            // EmergencyStop in XpressNet also means power is still on
327
            if(!contains(m_world.state.value(), WorldState::PowerOn) && isStopped)
×
328
            {
UNCOV
329
              m_world.powerOn(); // Just power on but keep stopped
×
330
            }
331
          }
332
          else
333
          {
334
            // Power off regardless of stop state
UNCOV
335
            if(contains(m_world.state.value(), WorldState::PowerOn))
×
336
              m_world.powerOff();
×
337
          }
338
        });
×
339

340
      m_kernel->setDecoderController(this);
×
341
      m_kernel->setInputController(this);
×
UNCOV
342
      m_kernel->setOutputController(this);
×
343
      m_kernel->start();
×
344

UNCOV
345
      m_xpressnetPropertyChanged = xpressnet->propertyChanged.connect(
×
346
        [this](BaseProperty& /*property*/)
×
347
        {
UNCOV
348
          m_kernel->setConfig(xpressnet->config());
×
UNCOV
349
        });
×
350

351
      // Avoid to set multiple power states in rapid succession
352
      if(!contains(m_world.state.value(), WorldState::PowerOn))
×
353
        m_kernel->stopOperations(); // Stop by powering off
×
UNCOV
354
      else if(!contains(m_world.state.value(), WorldState::Run))
×
355
        m_kernel->stopAllLocomotives(); // Emergency stop with power on
×
356
      else
357
        m_kernel->resumeOperations(); // Run trains
×
358

359
      Attributes::setEnabled({type, serialInterfaceType, device, baudrate, flowControl, hostname, port, s88StartAddress, s88ModuleCount}, false);
×
360
    }
361
    catch(const LogMessageException& e)
×
362
    {
363
      setState(InterfaceState::Offline);
×
364
      Log::log(*this, e.message(), e.args());
×
UNCOV
365
      return false;
×
366
    }
×
367
  }
368
  else if(m_kernel && !value)
×
369
  {
370
    Attributes::setEnabled({type, serialInterfaceType, device, baudrate, flowControl, hostname, port, s88StartAddress, s88ModuleCount}, true);
×
371

372
    m_xpressnetPropertyChanged.disconnect();
×
373

UNCOV
374
    m_kernel->stop();
×
375
    EventLoop::deleteLater(m_kernel.release());
×
376

377
    setState(InterfaceState::Offline);
×
378
  }
UNCOV
379
  return true;
×
380
}
381

382
void XpressNetInterface::addToWorld()
9✔
383
{
384
  Interface::addToWorld();
9✔
385
  DecoderController::addToWorld();
9✔
386
  InputController::addToWorld(inputListColumns);
9✔
387
  OutputController::addToWorld(outputListColumns);
9✔
388
}
9✔
389

390
void XpressNetInterface::loaded()
×
391
{
392
  Interface::loaded();
×
393

UNCOV
394
  updateVisible();
×
UNCOV
395
}
×
396

397
void XpressNetInterface::destroying()
9✔
398
{
399
  OutputController::destroying();
9✔
400
  InputController::destroying();
9✔
401
  DecoderController::destroying();
9✔
402
  Interface::destroying();
9✔
403
}
9✔
404

405
void XpressNetInterface::worldEvent(WorldState state, WorldEvent event)
×
406
{
407
  Interface::worldEvent(state, event);
×
408

409
  if(m_kernel)
×
410
  {
411
    switch(event)
×
412
    {
413
      case WorldEvent::PowerOff:
×
414
      {
UNCOV
415
        m_kernel->stopOperations();
×
416
        break;
×
417
      }
418
      case WorldEvent::PowerOn:
×
419
      {
UNCOV
420
        if(contains(state, WorldState::Run))
×
421
          m_kernel->resumeOperations();
×
422
        else
UNCOV
423
          m_kernel->stopAllLocomotives(); // In XpressNet E-Stop means power on but not running
×
424
        break;
×
425
      }
426
      case WorldEvent::Stop:
×
427
      {
UNCOV
428
        if(contains(state, WorldState::PowerOn))
×
429
        {
430
          // In XpressNet E-Stop means power is on but trains are not running
UNCOV
431
          m_kernel->stopAllLocomotives();
×
432
        }
433
        else
434
        {
435
          // This Stops everything by removing power
436
          m_kernel->stopOperations();
×
437
        }
438
        break;
×
439
      }
440
      case WorldEvent::Run:
×
441
      {
442
        if(contains(state, WorldState::PowerOn))
×
UNCOV
443
          m_kernel->resumeOperations();
×
444
        break;
×
445
      }
UNCOV
446
      default:
×
UNCOV
447
        break;
×
448
    }
449
  }
UNCOV
450
}
×
451

452
void XpressNetInterface::updateVisible()
9✔
453
{
454
  const bool isSerial = (type == XpressNetInterfaceType::Serial);
9✔
455
  Attributes::setVisible(serialInterfaceType, isSerial);
9✔
456
  Attributes::setVisible(device, isSerial);
9✔
457
  Attributes::setVisible(baudrate, isSerial);
9✔
458
  Attributes::setVisible(flowControl, isSerial);
9✔
459

460
  const bool isNetwork = (type == XpressNetInterfaceType::Network);
9✔
461
  Attributes::setVisible(hostname, isNetwork);
9✔
462
  Attributes::setVisible(port, isNetwork);
9✔
463

464
  const bool isRoSoftS88XPressNetLI = isSerial && (serialInterfaceType == XpressNetSerialInterfaceType::RoSoftS88XPressNetLI);
9✔
465
  Attributes::setVisible(s88StartAddress, isRoSoftS88XPressNetLI);
9✔
466
  Attributes::setVisible(s88ModuleCount, isRoSoftS88XPressNetLI);
9✔
467
}
9✔
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