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

traintastic / traintastic / 22823086659

08 Mar 2026 02:30PM UTC coverage: 26.831% (+0.06%) from 26.774%
22823086659

push

github

reinder
[cbus] renamed CBUSAccessory(Short) to Long/Short event and added node setting for Long events.

0 of 15 new or added lines in 2 files covered. (0.0%)

1909 existing lines in 38 files now uncovered.

8230 of 30674 relevant lines covered (26.83%)

186.8 hits per line

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

17.55
/server/src/hardware/interface/ecosinterface.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 "ecosinterface.hpp"
23
#include "../decoder/list/decoderlist.hpp"
24
#include "../decoder/list/decoderlisttablemodel.hpp"
25
#include "../input/list/inputlist.hpp"
26
#include "../output/list/outputlist.hpp"
27
#include "../protocol/ecos/kernel.hpp"
28
#include "../protocol/ecos/settings.hpp"
29
#include "../protocol/ecos/messages.hpp"
30
#include "../protocol/ecos/iohandler/tcpiohandler.hpp"
31
#include "../protocol/ecos/iohandler/simulationiohandler.hpp"
32
#include "../protocol/ecos/object/switch.hpp"
33
#include "../../core/attributes.hpp"
34
#include "../../core/eventloop.hpp"
35
#include "../../core/method.tpp"
36
#include "../../core/objectproperty.tpp"
37
#include "../../log/log.hpp"
38
#include "../../log/logmessageexception.hpp"
39
#include "../../utils/displayname.hpp"
40
#include "../../utils/inrange.hpp"
41
#include "../../utils/makearray.hpp"
42
#include "../../world/world.hpp"
43
#include "../../world/worldloader.hpp"
44
#include "../../world/worldsaver.hpp"
45

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

50
CREATE_IMPL(ECoSInterface)
11✔
51

52
ECoSInterface::ECoSInterface(World& world, std::string_view _id)
11✔
53
  : Interface(world, _id)
54
  , DecoderController(*this, decoderListColumns)
55
  , InputController(static_cast<IdObject&>(*this))
56
  , OutputController(static_cast<IdObject&>(*this))
57
  , hostname{this, "hostname", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
22✔
58
  , ecos{this, "ecos", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject}
22✔
59
{
60
  name = "ECoS";
11✔
61
  ecos.setValueInternal(std::make_shared<ECoS::Settings>(*this, ecos.name()));
11✔
62

63
  Attributes::addDisplayName(hostname, DisplayName::IP::hostname);
11✔
64
  Attributes::addEnabled(hostname, !online);
11✔
65
  m_interfaceItems.insertBefore(hostname, notes);
11✔
66

67
  m_interfaceItems.insertBefore(ecos, notes);
11✔
68

69
  m_interfaceItems.insertBefore(decoders, notes);
11✔
70

71
  m_interfaceItems.insertBefore(inputs, notes);
11✔
72

73
  m_interfaceItems.insertBefore(outputs, notes);
11✔
74
}
11✔
75

76
ECoSInterface::~ECoSInterface() = default;
11✔
77

78
std::span<const DecoderProtocol> ECoSInterface::decoderProtocols() const
10✔
79
{
80
  static constexpr std::array<DecoderProtocol, 4> protocols{DecoderProtocol::DCCShort, DecoderProtocol::DCCLong, DecoderProtocol::Motorola, DecoderProtocol::Selectrix};
81
  return std::span<const DecoderProtocol>{protocols.data(), protocols.size()};
20✔
82
}
83

84
void ECoSInterface::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber)
4✔
85
{
86
  if(m_kernel)
4✔
UNCOV
87
    m_kernel->decoderChanged(decoder, changes, functionNumber);
×
88
}
4✔
89

90
std::span<const InputChannel> ECoSInterface::inputChannels() const
11✔
91
{
92
  static const auto values = makeArray(InputChannel::S88, InputChannel::ECoSDetector);
93
  return values;
11✔
94
}
95

UNCOV
96
std::pair<uint32_t, uint32_t> ECoSInterface::inputAddressMinMax(InputChannel channel) const
×
97
{
98
  using namespace ECoS;
99

UNCOV
100
  switch(channel)
×
101
  {
UNCOV
102
    case InputChannel::S88:
×
103
      return {Kernel::s88AddressMin, Kernel::s88AddressMax};
×
104

UNCOV
105
    case InputChannel::ECoSDetector:
×
106
      return {Kernel::ecosDetectorAddressMin, Kernel::ecosDetectorAddressMax};
×
107

UNCOV
108
    default: [[unlikely]]
×
109
      assert(false);
×
110
      return {0, 0};
111
  }
112
}
113

UNCOV
114
void ECoSInterface::inputSimulateChange(InputChannel channel, uint32_t address, SimulateInputAction action)
×
115
{
UNCOV
116
  if(m_kernel && inRange(address, inputAddressMinMax(channel)))
×
117
    m_kernel->simulateInputChange(channel, address, action);
×
118
}
×
119

120
std::span<const OutputChannel> ECoSInterface::outputChannels() const
62✔
121
{
122
  static const auto values = makeArray(OutputChannel::AccessoryDCC, OutputChannel::AccessoryMotorola, OutputChannel::ECoSObject);
123
  return values;
62✔
124
}
125

126
std::pair<std::span<const uint16_t>, std::span<const std::string>> ECoSInterface::getOutputECoSObjects(OutputChannel channel) const
1✔
127
{
128
  if(channel == OutputChannel::ECoSObject) /*[[likely]]*/
1✔
129
  {
130
    return {m_outputECoSObjectIds, m_outputECoSObjectNames};
1✔
131
  }
UNCOV
132
  return OutputController::getOutputECoSObjects(channel);
×
133
}
134

135
bool ECoSInterface::isOutputLocation(OutputChannel channel, const OutputLocation& location) const
8✔
136
{
137
  if(channel == OutputChannel::ECoSObject)
8✔
138
  {
139
    return inRange<uint16_t>(std::get<OutputECoSObject>(location).object, ECoS::ObjectId::switchMin, ECoS::ObjectId::switchMax);
3✔
140
  }
141
  return OutputController::isOutputLocation(channel, location);
5✔
142
}
143

UNCOV
144
bool ECoSInterface::setOutputValue(OutputChannel channel, const OutputLocation& location, OutputValue value)
×
145
{
146
  return
UNCOV
147
    m_kernel &&
×
148
    isOutputLocation(channel, location) &&
×
149
    m_kernel->setOutput(channel, location, value);
×
150
}
151

UNCOV
152
bool ECoSInterface::setOnline(bool& value, bool simulation)
×
153
{
UNCOV
154
  if(!m_kernel && value)
×
155
  {
156
    try
157
    {
UNCOV
158
      if(simulation)
×
159
        m_kernel = ECoS::Kernel::create<ECoS::SimulationIOHandler>(id.value(), ecos->config(), m_simulation);
×
160
      else
UNCOV
161
        m_kernel = ECoS::Kernel::create<ECoS::TCPIOHandler>(id.value(), ecos->config(), hostname.value());
×
162

UNCOV
163
      setState(InterfaceState::Initializing);
×
164

UNCOV
165
      m_kernel->setOnStarted(
×
166
        [this]()
×
167
        {
UNCOV
168
          setState(InterfaceState::Online);
×
169

UNCOV
170
          if(contains(m_world.state.value(), WorldState::Run))
×
171
          {
UNCOV
172
            m_kernel->go();
×
173
          }
174
          else
175
          {
UNCOV
176
            m_kernel->emergencyStop();
×
177
          }
UNCOV
178
        });
×
179
      m_kernel->setOnError(
×
180
        [this]()
×
181
        {
UNCOV
182
          setState(InterfaceState::Error);
×
183
          online = false; // communication no longer possible
×
184
        });
×
185
      m_kernel->setOnEmergencyStop(
×
186
        [this]()
×
187
        {
UNCOV
188
          if(contains(m_world.state.value(), WorldState::PowerOn | WorldState::Run))
×
189
            m_world.powerOff();
×
190
        });
×
191
      m_kernel->setOnGo(
×
192
        [this]()
×
193
        {
UNCOV
194
          if(!contains(m_world.state.value(), WorldState::Run))
×
195
            m_world.run();
×
196
        });
×
197
      m_kernel->setOnObjectChanged(
×
198
        [this](std::size_t typeHash, uint16_t objectId, const std::string& objectName)
×
199
        {
UNCOV
200
          if(typeHash == typeid(ECoS::Switch).hash_code())
×
201
          {
UNCOV
202
            if(auto it = std::find(m_outputECoSObjectIds.begin(), m_outputECoSObjectIds.end(), objectId); it != m_outputECoSObjectIds.end())
×
203
            {
UNCOV
204
              const std::size_t index = std::distance(m_outputECoSObjectIds.begin(), it);
×
205
              m_outputECoSObjectNames[index] = objectName;
×
206
            }
207
            else
208
            {
UNCOV
209
              m_outputECoSObjectIds.emplace_back(objectId);
×
210
              m_outputECoSObjectNames.emplace_back(objectName);
×
211
            }
UNCOV
212
            assert(m_outputECoSObjectIds.size() == m_outputECoSObjectNames.size());
×
213
            outputECoSObjectsChanged();
×
214
          }
UNCOV
215
        });
×
216
      m_kernel->setOnObjectRemoved(
×
217
        [this](uint16_t objectId)
×
218
        {
UNCOV
219
          assert(objectId == 0);
×
220
          if(auto it = std::find(m_outputECoSObjectIds.begin(), m_outputECoSObjectIds.end(), objectId); it != m_outputECoSObjectIds.end())
×
221
          {
UNCOV
222
            const std::size_t index = std::distance(m_outputECoSObjectIds.begin(), it);
×
223
            m_outputECoSObjectIds.erase(it);
×
224
            m_outputECoSObjectNames.erase(std::next(m_outputECoSObjectNames.begin(), index));
×
225
            assert(m_outputECoSObjectIds.size() == m_outputECoSObjectNames.size());
×
226
            outputECoSObjectsChanged();
×
227
          }
UNCOV
228
        });
×
229
      m_kernel->setDecoderController(this);
×
230
      m_kernel->setInputController(this);
×
231
      m_kernel->setOutputController(this);
×
232
      m_kernel->start();
×
233

UNCOV
234
      m_ecosPropertyChanged = ecos->propertyChanged.connect(
×
235
        [this](BaseProperty& /*property*/)
×
236
        {
UNCOV
237
          m_kernel->setConfig(ecos->config());
×
238
        });
×
239

240
      // Reset output object list:
UNCOV
241
      m_outputECoSObjectIds.assign({0});
×
242
      m_outputECoSObjectNames.assign({{}});
×
243

UNCOV
244
      Attributes::setEnabled(hostname, false);
×
245
    }
UNCOV
246
    catch(const LogMessageException& e)
×
247
    {
UNCOV
248
      setState(InterfaceState::Offline);
×
249
      Log::log(*this, e.message(), e.args());
×
250
      return false;
×
251
    }
×
252
  }
UNCOV
253
  else if(m_kernel && !value)
×
254
  {
UNCOV
255
    Attributes::setEnabled(hostname, true);
×
256

UNCOV
257
    m_ecosPropertyChanged.disconnect();
×
258

UNCOV
259
    m_kernel->stop(simulation ? nullptr : &m_simulation);
×
260
    EventLoop::deleteLater(m_kernel.release());
×
261

UNCOV
262
    setState(InterfaceState::Offline);
×
263
  }
UNCOV
264
  return true;
×
265
}
×
266

267
void ECoSInterface::addToWorld()
11✔
268
{
269
  Interface::addToWorld();
11✔
270
  DecoderController::addToWorld();
11✔
271
  InputController::addToWorld(inputListColumns);
11✔
272
  OutputController::addToWorld(outputListColumns);
11✔
273
}
11✔
274

275
void ECoSInterface::destroying()
11✔
276
{
277
  OutputController::destroying();
11✔
278
  InputController::destroying();
11✔
279
  DecoderController::destroying();
11✔
280
  Interface::destroying();
11✔
281
}
11✔
282

UNCOV
283
void ECoSInterface::load(WorldLoader& loader, const nlohmann::json& data)
×
284
{
UNCOV
285
  Interface::load(loader, data);
×
286

287
  // load simulation data:
288
  {
289
    using namespace nlohmann;
290

UNCOV
291
    json simulation;
×
292
    if(loader.readFile(simulationDataFilename(), simulation))
×
293
    {
294
      using namespace ECoS;
295

296
      // ECoS:
UNCOV
297
      if(json object = simulation.value("ecos", json::object()); !object.empty())
×
298
      {
UNCOV
299
        m_simulation.ecos.commandStationType = object.value("command_station_type", "");
×
300
        m_simulation.ecos.applicationVersion = object.value("application_version", "");
×
301
        m_simulation.ecos.applicationVersionSuffix = object.value("application_version_suffix", "");
×
302
        m_simulation.ecos.hardwareVersion = object.value("hardware_version", "");
×
303
        m_simulation.ecos.protocolVersion = object.value("protocol_version", "");
×
304
        m_simulation.ecos.railcom = object.value("railcom", false);
×
305
        m_simulation.ecos.railcomPlus = object.value("railcom_plus", false);
×
306
      }
×
307

UNCOV
308
      if(json locomotives = simulation.value("locomotives", json::array()); !locomotives.empty())
×
309
      {
UNCOV
310
        for(const json& object : locomotives)
×
311
        {
UNCOV
312
          const uint16_t objectId = object.value("id", 0U);
×
313
          LocomotiveProtocol protocol;
UNCOV
314
          const uint16_t address = object.value("address", 0U);
×
315
          if(objectId != 0 && fromString(object.value("protocol", ""), protocol) && address != 0)
×
316
            m_simulation.locomotives.emplace_back(Simulation::Locomotive{{objectId}, protocol, address});
×
317
        }
UNCOV
318
      }
×
319

UNCOV
320
      if(json switches = simulation.value("switches", json::array()); !switches.empty())
×
321
      {
UNCOV
322
        for(const json& object : switches)
×
323
        {
UNCOV
324
          const uint16_t objectId = object.value("id", 0U);
×
325
          const uint16_t address = object.value("address", 0U);
×
326
          if(objectId != 0 && address != 0)
×
327
          {
UNCOV
328
            m_simulation.switches.emplace_back(
×
329
              Simulation::Switch{
×
330
                {objectId},
UNCOV
331
                object.value("name1", ""),
×
332
                object.value("name2", ""),
×
333
                object.value("name3", ""),
×
334
                address,
UNCOV
335
                object.value("addrext", ""),
×
336
                object.value("type", ""),
×
337
                object.value("symbol", -1),
×
338
                object.value("protocol", ""),
×
339
                object.value<uint8_t>("state", 0U),
×
340
                object.value("mode", ""),
×
341
                object.value<uint16_t>("duration", 0U),
×
342
                object.value<uint8_t>("variant", 0U)
×
343
                });
344
          }
345
        }
UNCOV
346
      }
×
347

UNCOV
348
      if(json s88 = simulation.value("s88", json::array()); !s88.empty())
×
349
      {
UNCOV
350
        for(const json& object : s88)
×
351
        {
UNCOV
352
          const uint16_t objectId = object.value("id", 0U);
×
353
          const uint8_t ports = object.value("ports", 0U);
×
354
          if(objectId != 0 && (ports == 8 || ports == 16))
×
355
            m_simulation.s88.emplace_back(Simulation::S88{{objectId}, ports});
×
356
          else
357
            break;
358
        }
UNCOV
359
      }
×
360
    }
UNCOV
361
  }
×
362
}
×
363

UNCOV
364
void ECoSInterface::save(WorldSaver& saver, nlohmann::json& data, nlohmann::json& state) const
×
365
{
UNCOV
366
  Interface::save(saver, data, state);
×
367

368
  using nlohmann::json;
369

370
  // save data for simulation:
UNCOV
371
  json simulation = json::object();
×
372

373
  // ECoS:
374
  {
UNCOV
375
    simulation["ecos"] = {
×
376
      {"command_station_type", m_simulation.ecos.commandStationType},
×
377
      {"application_version", m_simulation.ecos.applicationVersion},
×
378
      {"application_version_suffix", m_simulation.ecos.applicationVersionSuffix},
×
379
      {"hardware_version", m_simulation.ecos.hardwareVersion},
×
380
      {"protocol_version", m_simulation.ecos.protocolVersion},
×
381
      {"railcom", m_simulation.ecos.railcom},
×
382
      {"railcom_plus", m_simulation.ecos.railcomPlus},
×
383
      };
×
384
  }
385

UNCOV
386
  if(!m_simulation.locomotives.empty())
×
387
  {
UNCOV
388
    json objects = json::array();
×
389
    for(const auto& locomotive : m_simulation.locomotives)
×
390
      objects.emplace_back(json::object({{"id", locomotive.id}, {"protocol", toString(locomotive.protocol)}, {"address", locomotive.address}}));
×
391
    simulation["locomotives"] = objects;
×
392
  }
×
393

UNCOV
394
  if(!m_simulation.switches.empty())
×
395
  {
UNCOV
396
    json objects = json::array();
×
397
    for(const auto& sw : m_simulation.switches)
×
398
    {
UNCOV
399
      objects.emplace_back(
×
400
        json::object({
×
401
          {"id", sw.id},
×
402
          {"name1", sw.name1},
×
403
          {"name2", sw.name2},
×
404
          {"name3", sw.name3},
×
405
          {"address", sw.address},
×
406
          {"addrext", sw.addrext},
×
407
          {"type", sw.type},
×
408
          {"symbol", sw.symbol},
×
409
          {"protocol", sw.protocol},
×
410
          {"state", sw.state},
×
411
          {"mode", sw.mode},
×
412
          {"duration", sw.duration},
×
413
          {"variant", sw.variant}
×
414
          }));
415
    }
UNCOV
416
    simulation["switches"] = objects;
×
417
  }
×
418

UNCOV
419
  if(!m_simulation.s88.empty())
×
420
  {
UNCOV
421
    json objects = json::array();
×
422
    for(const auto& s88 : m_simulation.s88)
×
423
      objects.emplace_back(json::object({{"id", s88.id}, {"ports", s88.ports}}));
×
424
    simulation["s88"] = objects;
×
425
  }
×
426

UNCOV
427
  if(!simulation.empty())
×
428
  {
UNCOV
429
    saver.writeFile(simulationDataFilename(), simulation.dump(2));
×
430
  }
UNCOV
431
}
×
432

UNCOV
433
void ECoSInterface::worldEvent(WorldState state, WorldEvent event)
×
434
{
UNCOV
435
  Interface::worldEvent(state, event);
×
436

UNCOV
437
  if(m_kernel)
×
438
  {
UNCOV
439
    switch(event)
×
440
    {
UNCOV
441
      case WorldEvent::PowerOff:
×
442
      case WorldEvent::Stop:
UNCOV
443
        m_kernel->emergencyStop();
×
444
        break;
×
445

UNCOV
446
      case WorldEvent::PowerOn:
×
447
      case WorldEvent::Run:
UNCOV
448
        if(contains(state, WorldState::PowerOn | WorldState::Run))
×
449
          m_kernel->go();
×
450
        break;
×
451

UNCOV
452
      default:
×
453
        break;
×
454
    }
455
  }
UNCOV
456
}
×
457

UNCOV
458
std::filesystem::path ECoSInterface::simulationDataFilename() const
×
459
{
UNCOV
460
  return (std::filesystem::path("simulation") / id.value()) += ".json";
×
461
}
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