• 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

17.41
/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✔
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

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

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

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

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

114
void ECoSInterface::inputSimulateChange(InputChannel channel, const InputLocation& location, SimulateInputAction action)
×
115
{
116
  assert(std::holds_alternative<InputAddress>(location));
×
117
  const auto address = std::get<InputAddress>(location).address;
×
118
  if(m_kernel && inRange(address, inputAddressMinMax(channel)))
×
UNCOV
119
    m_kernel->simulateInputChange(channel, address, action);
×
UNCOV
120
}
×
121

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

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

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

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

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

165
      setState(InterfaceState::Initializing);
×
166

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

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

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

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

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

259
    m_ecosPropertyChanged.disconnect();
×
260

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

264
    setState(InterfaceState::Offline);
×
265
  }
UNCOV
266
  return true;
×
UNCOV
267
}
×
268

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

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

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

289
  // load simulation data:
290
  {
291
    using namespace nlohmann;
292

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

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

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

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

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

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

370
  using nlohmann::json;
371

372
  // save data for simulation:
UNCOV
373
  json simulation = json::object();
×
374

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

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

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

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

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

435
void ECoSInterface::worldEvent(WorldState state, WorldEvent event)
×
436
{
437
  Interface::worldEvent(state, event);
×
438

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

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

UNCOV
454
      default:
×
UNCOV
455
        break;
×
456
    }
457
  }
458
}
×
459

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