• 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

30.99
/server/src/hardware/interface/z21interface.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 "z21interface.hpp"
23
#include "../decoder/list/decoderlist.hpp"
24
#include "../input/list/inputlist.hpp"
25
#include "../output/list/outputlist.hpp"
26
#include "../protocol/dcc/dcc.hpp"
27
#include "../protocol/z21/clientkernel.hpp"
28
#include "../protocol/z21/clientsettings.hpp"
29
#include "../protocol/z21/messages.hpp"
30
#include "../protocol/z21/iohandler/simulationiohandler.hpp"
31
#include "../protocol/z21/iohandler/udpclientiohandler.hpp"
32
#include "../../core/attributes.hpp"
33
#include "../../core/eventloop.hpp"
34
#include "../../core/method.tpp"
35
#include "../../core/objectproperty.tpp"
36
#include "../../log/log.hpp"
37
#include "../../log/logmessageexception.hpp"
38
#include "../../utils/category.hpp"
39
#include "../../utils/displayname.hpp"
40
#include "../../utils/inrange.hpp"
41
#include "../../utils/makearray.hpp"
42
#include "../../world/world.hpp"
43

44
constexpr auto decoderListColumns = DecoderListColumn::Id | DecoderListColumn::Name | DecoderListColumn::Protocol | DecoderListColumn::Address;
45
constexpr auto inputListColumns = InputListColumn::Channel | InputListColumn::Address;
46
constexpr auto outputListColumns = OutputListColumn::Channel | OutputListColumn::Address;
47

48
CREATE_IMPL(Z21Interface)
8✔
49

50
Z21Interface::Z21Interface(World& world, std::string_view _id)
8✔
51
  : Interface(world, _id)
52
  , DecoderController(*this, decoderListColumns)
53
  , InputController(static_cast<IdObject&>(*this))
54
  , OutputController(static_cast<IdObject&>(*this))
55
  , hostname{this, "hostname", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
16✔
56
  , port{this, "port", 21105, PropertyFlags::ReadWrite | PropertyFlags::Store}
8✔
57
  , z21{this, "z21", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject}
8✔
58
  , hardwareType{this, "hardware_type", "", PropertyFlags::ReadOnly | PropertyFlags::NoStore}
16✔
59
  , serialNumber{this, "serial_number", "", PropertyFlags::ReadOnly | PropertyFlags::NoStore}
16✔
60
  , firmwareVersion{this, "firmware_version", "", PropertyFlags::ReadOnly | PropertyFlags::NoStore}
32✔
61
{
62
  name = "Z21";
8✔
63
  z21.setValueInternal(std::make_shared<Z21::ClientSettings>(*this, z21.name()));
8✔
64

65
  Attributes::addDisplayName(hostname, DisplayName::IP::hostname);
8✔
66
  Attributes::addEnabled(hostname, !online);
8✔
67
  m_interfaceItems.insertBefore(hostname, notes);
8✔
68

69
  Attributes::addDisplayName(port, DisplayName::IP::port);
8✔
70
  Attributes::addEnabled(port, !online);
8✔
71
  m_interfaceItems.insertBefore(port, notes);
8✔
72

73
  Attributes::addDisplayName(z21, DisplayName::Hardware::z21);
8✔
74
  m_interfaceItems.insertBefore(z21, notes);
8✔
75

76
  m_interfaceItems.insertBefore(decoders, notes);
8✔
77

78
  m_interfaceItems.insertBefore(inputs, notes);
8✔
79

80
  m_interfaceItems.insertBefore(outputs, notes);
8✔
81

82
  Attributes::addCategory(hardwareType, Category::info);
8✔
83
  m_interfaceItems.insertBefore(hardwareType, notes);
8✔
84

85
  Attributes::addCategory(serialNumber, Category::info);
8✔
86
  m_interfaceItems.insertBefore(serialNumber, notes);
8✔
87

88
  Attributes::addCategory(firmwareVersion, Category::info);
8✔
89
  m_interfaceItems.insertBefore(firmwareVersion, notes);
8✔
90
}
8✔
91

92
Z21Interface::~Z21Interface() = default;
8✔
93

94
std::span<const DecoderProtocol> Z21Interface::decoderProtocols() const
10✔
95
{
96
  static constexpr std::array<DecoderProtocol, 3> protocols{DecoderProtocol::DCCShort, DecoderProtocol::DCCLong, DecoderProtocol::Motorola};
97
  return std::span<const DecoderProtocol>{protocols.data(), protocols.size()};
20✔
98
}
99

100
std::pair<uint16_t, uint16_t> Z21Interface::decoderAddressMinMax(DecoderProtocol protocol) const
5✔
101
{
102
  if(protocol == DecoderProtocol::DCCLong)
5✔
103
  {
104
    return {DCC::addressLongStart, DCC::addressLongMax};
×
105
  }
106
  return DecoderController::decoderAddressMinMax(protocol);
5✔
107
}
108

109
void Z21Interface::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber)
4✔
110
{
111
  if(m_kernel)
4✔
112
    m_kernel->decoderChanged(decoder, changes, functionNumber);
×
113
}
4✔
114

115
std::span<const InputChannel> Z21Interface::inputChannels() const
8✔
116
{
117
  static const auto values = makeArray(InputChannel::RBus, InputChannel::LocoNet);
118
  return values;
8✔
119
}
120

121
std::pair<uint32_t, uint32_t> Z21Interface::inputAddressMinMax(InputChannel channel) const
×
122
{
123
  using namespace Z21;
124

125
  switch(channel)
×
126
  {
127
    case InputChannel::RBus:
×
128
      return {ClientKernel::rbusAddressMin, ClientKernel::rbusAddressMax};
×
129

130
    case InputChannel::LocoNet:
×
131
      return {ClientKernel::loconetAddressMin, ClientKernel::loconetAddressMax};
×
132

133
    default: [[unlikely]]
×
134
      assert(false);
×
135
      return {0, 0};
136
  }
137
}
138

139
void Z21Interface::inputSimulateChange(InputChannel channel, const InputLocation& location, SimulateInputAction action)
×
140
{
141
  assert(std::holds_alternative<InputAddress>(location));
×
142
  const auto address = std::get<InputAddress>(location).address;
×
143
  if(m_kernel && inRange(address, inputAddressMinMax(channel)))
×
UNCOV
144
    m_kernel->simulateInputChange(channel, address, action);
×
UNCOV
145
}
×
146

147
std::span<const OutputChannel> Z21Interface::outputChannels() const
24✔
148
{
149
  static const auto values = makeArray(OutputChannel::Accessory, OutputChannel::DCCext);
150
  return values;
24✔
151
}
152

153
std::pair<uint32_t, uint32_t> Z21Interface::outputAddressMinMax(OutputChannel channel) const
×
154
{
155
  if(channel == OutputChannel::Accessory)
×
156
  {
157
    return {Z21::ClientKernel::outputAddressMin, Z21::ClientKernel::outputAddressMax};
×
158
  }
UNCOV
159
  return OutputController::outputAddressMinMax(channel);
×
160
}
161

162
bool Z21Interface::setOutputValue(OutputChannel channel, const OutputLocation& location, OutputValue value)
×
163
{
164
  const auto address = std::get<OutputAddress>(location).address;
×
165
  return
166
      m_kernel &&
×
UNCOV
167
      inRange(address, outputAddressMinMax(channel)) &&
×
UNCOV
168
      m_kernel->setOutput(channel, static_cast<uint16_t>(address), value);
×
169
}
170

171
bool Z21Interface::setOnline(bool& value, bool simulation)
×
172
{
UNCOV
173
  if(!m_kernel && value)
×
174
  {
175
    try
176
    {
UNCOV
177
      if(simulation)
×
178
        m_kernel = Z21::ClientKernel::create<Z21::SimulationIOHandler>(id.value(), z21->config());
×
179
      else
180
        m_kernel = Z21::ClientKernel::create<Z21::UDPClientIOHandler>(id.value(), z21->config(), hostname.value(), port.value());
×
181

182
      setState(InterfaceState::Initializing);
×
183

UNCOV
184
      m_kernel->setOnStarted(
×
185
        [this]()
×
186
        {
187
          setState(InterfaceState::Online);
×
188
        });
×
UNCOV
189
      m_kernel->setOnError(
×
190
        [this]()
×
191
        {
192
          setState(InterfaceState::Error);
×
193
          online = false; // communication no longer possible
×
194
        });
×
UNCOV
195
      m_kernel->setOnSerialNumberChanged(
×
196
        [this](uint32_t newValue)
×
197
        {
198
          serialNumber.setValueInternal(std::to_string(newValue));
×
199
        });
×
UNCOV
200
      m_kernel->setOnHardwareInfoChanged(
×
201
        [this](Z21::HardwareType type, uint8_t versionMajor, uint8_t versionMinor)
×
202
        {
UNCOV
203
          hardwareType.setValueInternal(std::string(Z21::toString(type)));
×
204
          Log::log(*this, LogMessage::I2002_HARDWARE_TYPE_X, hardwareType.value());
×
205

206
          if(versionMajor != 0 || versionMinor != 0)
×
207
          {
UNCOV
208
            firmwareVersion.setValueInternal(std::to_string(versionMajor).append(".").append(std::to_string(versionMinor)));
×
UNCOV
209
            Log::log(*this, LogMessage::I2003_FIRMWARE_VERSION_X, firmwareVersion.value());
×
210
          }
211
          else
212
            firmwareVersion.setValueInternal("");
×
213
        });
×
UNCOV
214
      m_kernel->setOnTrackPowerChanged(
×
215
        [this](bool powerOn, bool isStopped)
×
216
        {
UNCOV
217
          if(powerOn)
×
218
          {
219
            /* NOTE:
220
             * Setting stop and powerOn together is not an atomic operation,
221
             * so it would trigger 2 state changes with in the middle state.
222
             * Fortunately this does not happen because at least one of the state is already set.
223
             * Because if we are in Run state we go to PowerOn,
224
             * and if we are on PowerOff then we go to PowerOn.
225
             */
226

227
            // First of all, stop if we have to, otherwhise we might inappropiately run trains
228
            if(isStopped && contains(m_world.state.value(), WorldState::Run))
×
229
            {
230
              m_world.stop();
×
231
            }
232
            else if(!contains(m_world.state.value(), WorldState::Run) && !isStopped)
×
233
            {
UNCOV
234
              m_world.run(); // Run trains yay!
×
235
            }
236

237
            // EmergencyStop in Z21 also means power is still on
238
            if(!contains(m_world.state.value(), WorldState::PowerOn) && isStopped)
×
239
            {
UNCOV
240
              m_world.powerOn(); // Just power on but keep stopped
×
241
            }
242
          }
243
          else
244
          {
245
            // Power off regardless of stop state
UNCOV
246
            if(contains(m_world.state.value(), WorldState::PowerOn))
×
247
              m_world.powerOff();
×
248
          }
249
        });
×
250

251
      m_kernel->setDecoderController(this);
×
UNCOV
252
      m_kernel->setInputController(this);
×
253
      m_kernel->setOutputController(this);
×
254

255
      m_kernel->start();
×
256

UNCOV
257
      m_z21PropertyChanged = z21->propertyChanged.connect(
×
258
        [this](BaseProperty& /*property*/)
×
259
        {
UNCOV
260
          m_kernel->setConfig(z21->config());
×
UNCOV
261
        });
×
262

263
      // Avoid to set multiple power states in rapid succession
264
      if(contains(m_world.state.value(), WorldState::PowerOn))
×
265
      {
UNCOV
266
        if(contains(m_world.state.value(), WorldState::Run))
×
267
          m_kernel->trackPowerOn(); // Run trains
×
268
        else
UNCOV
269
          m_kernel->emergencyStop(); // Emergency stop with power on
×
270
      }
271
      else
272
      {
UNCOV
273
        m_kernel->trackPowerOff(); // Stop by powering off
×
274
      }
275

276
      Attributes::setEnabled({hostname, port}, false);
×
277
    }
278
    catch(const LogMessageException& e)
×
279
    {
280
      setState(InterfaceState::Offline);
×
281
      Log::log(*this, e.message(), e.args());
×
UNCOV
282
      return false;
×
283
    }
×
284
  }
285
  else if(m_kernel && !value)
×
286
  {
287
    Attributes::setEnabled({hostname, port}, true);
×
288

289
    m_z21PropertyChanged.disconnect();
×
290

UNCOV
291
    m_kernel->stop();
×
292
    EventLoop::deleteLater(m_kernel.release());
×
293

294
    setState(InterfaceState::Offline);
×
295
    hardwareType.setValueInternal("");
×
UNCOV
296
    serialNumber.setValueInternal("");
×
297
    firmwareVersion.setValueInternal("");
×
298
  }
UNCOV
299
  return true;
×
300
}
301

302
void Z21Interface::addToWorld()
8✔
303
{
304
  Interface::addToWorld();
8✔
305
  DecoderController::addToWorld();
8✔
306
  InputController::addToWorld(inputListColumns);
8✔
307
  OutputController::addToWorld(outputListColumns);
8✔
308
}
8✔
309

310
void Z21Interface::destroying()
8✔
311
{
312
  OutputController::destroying();
8✔
313
  InputController::destroying();
8✔
314
  DecoderController::destroying();
8✔
315
  Interface::destroying();
8✔
316
}
8✔
317

318
void Z21Interface::worldEvent(WorldState state, WorldEvent event)
×
319
{
320
  Interface::worldEvent(state, event);
×
321

UNCOV
322
  if(m_kernel)
×
323
  {
324
    // Avoid to set multiple power states in rapid succession
325
    switch(event)
×
326
    {
327
      case WorldEvent::PowerOff:
×
328
      {
UNCOV
329
        m_kernel->trackPowerOff();
×
330
        break;
×
331
      }
332
      case WorldEvent::PowerOn:
×
333
      {
UNCOV
334
        if(contains(state, WorldState::Run))
×
335
          m_kernel->trackPowerOn();
×
336
        else
UNCOV
337
          m_kernel->emergencyStop(); // In Z21 E-Stop means power on but not running
×
338
        break;
×
339
      }
340
      case WorldEvent::Stop:
×
341
      {
UNCOV
342
        if(contains(state, WorldState::PowerOn))
×
343
        {
344
          // In Z21 E-Stop means power is on but trains are not running
UNCOV
345
          m_kernel->emergencyStop();
×
346
        }
347
        else
348
        {
349
          // This Stops everything by removing power
350
          m_kernel->trackPowerOff();
×
351
        }
352
        break;
×
353
      }
354
      case WorldEvent::Run:
×
355
      {
356
        if(contains(state, WorldState::PowerOn))
×
UNCOV
357
          m_kernel->trackPowerOn();
×
358
        break;
×
359
      }
UNCOV
360
      default:
×
UNCOV
361
        break;
×
362
    }
363
  }
UNCOV
364
}
×
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