• 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/protocol/ecos/kernel.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 "kernel.hpp"
23
#include <algorithm>
24
#include <typeinfo>
25
#include "messages.hpp"
26
#include "simulation.hpp"
27
#include "object/ecos.hpp"
28
#include "object/locomotivemanager.hpp"
29
#include "object/locomotive.hpp"
30
#include "object/switchmanager.hpp"
31
#include "object/switch.hpp"
32
#include "object/feedbackmanager.hpp"
33
#include "object/feedback.hpp"
34
#include "../../protocol/dcc/dcc.hpp"
35
#include "../../decoder/decoder.hpp"
36
#include "../../decoder/decoderchangeflags.hpp"
37
#include "../../input/inputcontroller.hpp"
38
#include "../../output/outputcontroller.hpp"
39
#include "../../../utils/setthreadname.hpp"
40
#include "../../../utils/startswith.hpp"
41
#include "../../../utils/ltrim.hpp"
42
#include "../../../utils/rtrim.hpp"
43
#include "../../../utils/tohex.hpp"
44
#include "../../../core/eventloop.hpp"
45
#include "../../../log/log.hpp"
46
#include "../../../log/logmessageexception.hpp"
47

48
#define ASSERT_IS_KERNEL_THREAD assert(isKernelThread())
49

50
namespace ECoS {
51

52
static constexpr DecoderProtocol toDecoderProtocol(LocomotiveProtocol locomotiveProtocol, uint16_t address)
×
53
{
54
  switch(locomotiveProtocol)
×
55
  {
56
    case LocomotiveProtocol::DCC14:
×
57
    case LocomotiveProtocol::DCC28:
58
    case LocomotiveProtocol::DCC128:
59
      return DCC::getProtocol(address);
×
60

61
    case LocomotiveProtocol::MM14:
×
62
    case LocomotiveProtocol::MM27:
63
    case LocomotiveProtocol::MM28:
64
      return DecoderProtocol::Motorola;
×
65

66
    case LocomotiveProtocol::SX32:
×
67
      return DecoderProtocol::Selectrix;
×
68

69
    case LocomotiveProtocol::Unknown:
×
70
    case LocomotiveProtocol::MMFKT:
71
      break;
×
72
  }
73
  return DecoderProtocol::None;
×
74
}
75

76
Kernel::Kernel(std::string logId_, const Config& config, bool simulation)
×
77
  : KernelBase(std::move(logId_))
×
78
  , m_simulation{simulation}
×
79
  , m_decoderController{nullptr}
×
80
  , m_inputController{nullptr}
×
81
  , m_outputController{nullptr}
×
82
  , m_config{config}
×
83
{
84
}
×
85

86
void Kernel::setConfig(const Config& config)
×
87
{
NEW
88
  boost::asio::post(m_ioContext,
×
89
    [this, newConfig=config]()
×
90
    {
91
      m_config = newConfig;
×
92
    });
×
93
}
×
94

95
void Kernel::setOnEmergencyStop(std::function<void()> callback)
×
96
{
97
  assert(!m_started);
×
98
  m_onEmergencyStop = std::move(callback);
×
99
}
×
100

101
void Kernel::setOnGo(std::function<void()> callback)
×
102
{
103
  assert(!m_started);
×
104
  m_onGo = std::move(callback);
×
105
}
×
106

107
void Kernel::setOnObjectChanged(OnObjectChanged callback)
×
108
{
109
  assert(!m_started);
×
110
  m_onObjectChanged = std::move(callback);
×
111
}
×
112

113
void Kernel::setOnObjectRemoved(OnObjectRemoved callback)
×
114
{
115
  assert(!m_started);
×
116
  m_onObjectRemoved = std::move(callback);
×
117
}
×
118
void Kernel::setDecoderController(DecoderController* decoderController)
×
119
{
120
  assert(!m_started);
×
121
  m_decoderController = decoderController;
×
122
}
×
123

124
void Kernel::setInputController(InputController* inputController)
×
125
{
126
  assert(!m_started);
×
127
  m_inputController = inputController;
×
128
}
×
129

130
void Kernel::setOutputController(OutputController* outputController)
×
131
{
132
  assert(!m_started);
×
133
  m_outputController = outputController;
×
134
}
×
135

136
void Kernel::start()
×
137
{
138
  assert(m_ioHandler);
×
139
  assert(!m_started);
×
140
  assert(m_objects.empty());
×
141

142
  m_thread = std::thread(
×
143
    [this]()
×
144
    {
145
      setThreadName("ecos");
×
NEW
146
      boost::asio::executor_work_guard<decltype(m_ioContext.get_executor())> work{m_ioContext.get_executor()};
×
147
      m_ioContext.run();
×
148
    });
×
149

NEW
150
  boost::asio::post(m_ioContext,
×
151
    [this]()
×
152
    {
153
      try
154
      {
155
        m_ioHandler->start();
×
156
      }
157
      catch(const LogMessageException& e)
×
158
      {
159
        EventLoop::call(
×
160
          [this, e]()
×
161
          {
162
            Log::log(logId, e.message(), e.args());
×
163
            error();
×
164
          });
×
165
        return;
×
166
      }
×
167
    });
168

169
#ifndef NDEBUG
170
  m_started = true;
×
171
#endif
172
}
×
173

174
void Kernel::stop(Simulation* simulation)
×
175
{
NEW
176
  boost::asio::post(m_ioContext,
×
177
    [this]()
×
178
    {
179
      m_ioHandler->stop();
×
180
    });
×
181

182
  m_ioContext.stop();
×
183

184
  m_thread.join();
×
185

186
  if(simulation && !m_objects.empty()) // get simulation data
×
187
  {
188
    simulation->clear();
×
189

190
    // ECoS:
191
    {
192
      simulation->ecos.commandStationType = toString(ecos().model());
×
193
      simulation->ecos.protocolVersion = ::toString(ecos().protocolVersion());
×
194
      simulation->ecos.hardwareVersion = ::toString(ecos().hardwareVersion());
×
195
      simulation->ecos.applicationVersion = ::toString(ecos().applicationVersion());
×
196
      simulation->ecos.applicationVersionSuffix = ecos().applicationVersionSuffix();
×
197
      simulation->ecos.railcom = ecos().railcom();
×
198
      simulation->ecos.railcomPlus = ecos().railcomPlus();
×
199
    }
200

201
    // Locomotives / switches:
202
    for(const auto& it : m_objects)
×
203
    {
204
      if(const auto* locomotive = dynamic_cast<const Locomotive*>(it.second.get()))
×
205
      {
206
        simulation->locomotives.emplace_back(
×
207
          Simulation::Locomotive{
×
208
            {locomotive->id()},
×
209
            locomotive->protocol(),
×
210
            locomotive->address()});
×
211
      }
212
      else if(const auto* sw = dynamic_cast<const Switch*>(it.second.get()))
×
213
      {
214
        simulation->switches.emplace_back(
×
215
          Simulation::Switch{
×
216
            {sw->id()},
×
217
            sw->name1(),
×
218
            sw->name2(),
×
219
            sw->name3(),
×
220
            sw->address(),
×
221
            toString(sw->addrext()),
222
            std::string{toString(sw->type())},
×
223
            static_cast<int>(sw->symbol()),
×
224
            std::string{toString(sw->protocol())},
×
225
            sw->state(),
×
226
            std::string{toString(sw->mode())},
×
227
            sw->duration(),
×
228
            sw->variant()
×
229
            });
230
      }
231
    }
232

233
    // S88:
234
    {
235
      uint16_t id = ObjectId::s88;
×
236
      auto it = m_objects.find(id);
×
237
      while(it != m_objects.end())
×
238
      {
239
        if(const auto* feedback = dynamic_cast<const Feedback*>(it->second.get()))
×
240
          simulation->s88.emplace_back(Simulation::S88{{feedback->id()}, feedback->ports()});
×
241
        else
242
          break;
×
243
        it = m_objects.find(++id);
×
244
      }
245
    }
246
  }
247

248
  m_objects.clear();
×
249

250
#ifndef NDEBUG
251
  m_started = false;
×
252
#endif
253
}
×
254

255
void Kernel::started()
×
256
{
257
  assert(isKernelThread());
×
258

259
  m_objects.add(std::make_unique<ECoS>(*this));
×
260
  m_objects.add(std::make_unique<LocomotiveManager>(*this));
×
261
  m_objects.add(std::make_unique<SwitchManager>(*this));
×
262
  m_objects.add(std::make_unique<FeedbackManager>(*this));
×
263

264
  KernelBase::started();
×
265
}
×
266

267
void Kernel::receive(std::string_view message)
×
268
{
269
  if(m_config.debugLogRXTX)
×
270
  {
271
    std::string msg{rtrim(message, {'\r', '\n'})};
×
272
    std::replace_if(msg.begin(), msg.end(), [](char c){ return c == '\r' || c == '\n'; }, ';');
×
273
    EventLoop::call([this, msg](){ Log::log(logId, LogMessage::D2002_RX_X, msg); });
×
274
  }
×
275

276
  if(Reply reply; parseReply(message, reply))
×
277
  {
278
    auto it = m_objects.find(reply.objectId);
×
279
    if(it != m_objects.end())
×
280
      it->second->receiveReply(reply);
×
281
  }
282
  else if(Event event; parseEvent(message, event))
×
283
  {
284
    auto it = m_objects.find(event.objectId);
×
285
    if(it != m_objects.end())
×
286
      it->second->receiveEvent(event);
×
287
  }
288
  else
289
  {}//  EventLoop::call([this]() { Log::log(logId, LogMessage::E2018_ParseError); });
×
290
}
×
291

292
ECoS& Kernel::ecos()
×
293
{
294
  assert(isKernelThread() || m_ioContext.stopped());
×
295

296
  return static_cast<ECoS&>(*m_objects[ObjectId::ecos]);
×
297
}
298

299
void Kernel::ecosGoChanged(TriState value)
×
300
{
301
  ASSERT_IS_KERNEL_THREAD;
×
302

303
  if(value == TriState::False && m_onEmergencyStop)
×
304
    EventLoop::call([this]() { m_onEmergencyStop(); });
×
305
  else if(value == TriState::True && m_onGo)
×
306
    EventLoop::call([this]() { m_onGo(); });
×
307
}
×
308

309
Locomotive* Kernel::getLocomotive(DecoderProtocol protocol, uint16_t address, uint8_t speedSteps)
×
310
{
311
  ASSERT_IS_KERNEL_THREAD;
×
312

313
  //! \todo optimize this
314

315
  auto it = std::find_if(m_objects.begin(), m_objects.end(),
×
316
    [protocol, address, speedSteps](const auto& item)
×
317
    {
318
      auto* l = dynamic_cast<Locomotive*>(item.second.get());
×
319
      return
320
        l &&
×
321
        protocol == toDecoderProtocol(l->protocol(), l->address()) &&
×
322
        address == l->address()  &&
×
323
        speedSteps == l->speedSteps();
×
324
    });
325

326
  if(it != m_objects.end())
×
327
    return static_cast<Locomotive*>(it->second.get());
×
328

329
  return nullptr;
×
330
}
331

332
SwitchManager& Kernel::switchManager()
×
333
{
334
  ASSERT_IS_KERNEL_THREAD;
×
335

336
  return static_cast<SwitchManager&>(*m_objects[ObjectId::switchManager]);
×
337
}
338

339
void Kernel::emergencyStop()
×
340
{
NEW
341
  boost::asio::post(m_ioContext, [this]() { ecos().stop(); });
×
342
}
×
343

344
void Kernel::go()
×
345
{
NEW
346
  boost::asio::post(m_ioContext, [this]() { ecos().go(); });
×
347
}
×
348

349
void Kernel::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber)
×
350
{
351
  if(has(changes, DecoderChangeFlags::Direction))
×
352
  {
NEW
353
    boost::asio::post(m_ioContext,
×
354
      [this,
×
355
        protocol=decoder.protocol.value(),
×
356
        address=decoder.address.value(),
×
357
        speedSteps=decoder.speedSteps.value(),
×
358
        direction=decoder.direction.value()]()
×
359
      {
360
        if(auto* locomotive = getLocomotive(protocol, address, speedSteps))
×
361
          locomotive->setDirection(direction);
×
362
      });
×
363
  }
364
  else if(has(changes, DecoderChangeFlags::EmergencyStop | DecoderChangeFlags::Throttle))
×
365
  {
NEW
366
    boost::asio::post(m_ioContext,
×
367
      [this,
×
368
        protocol=decoder.protocol.value(),
×
369
        address=decoder.address.value(),
×
370
        speedSteps=decoder.speedSteps.value(),
×
371
        throttle=decoder.throttle.value(),
×
372
        emergencyStop=decoder.emergencyStop.value()]()
×
373
      {
374
        if(auto* locomotive = getLocomotive(protocol, address, speedSteps))
×
375
        {
376
          if(emergencyStop)
×
377
            locomotive->stop();
×
378
          else
379
            locomotive->setSpeedStep(Decoder::throttleToSpeedStep(throttle, locomotive->speedSteps()));
×
380
        }
381
      });
×
382
  }
383
  else if(has(changes, DecoderChangeFlags::FunctionValue) && functionNumber <= std::numeric_limits<uint8_t>::max())
×
384
  {
NEW
385
    boost::asio::post(m_ioContext,
×
386
      [this,
×
387
        protocol=decoder.protocol.value(),
×
388
        address=decoder.address.value(),
×
389
        speedSteps=decoder.speedSteps.value(),
×
390
        index=static_cast<uint8_t>(functionNumber),
391
        value=decoder.getFunctionValue(functionNumber)]()
×
392
      {
393
        if(auto* locomotive = getLocomotive(protocol, address, speedSteps))
×
394
          locomotive->setFunctionValue(index, value);
×
395
      });
×
396
  }
397
}
×
398

399
bool Kernel::setOutput(OutputChannel channel, const OutputLocation& location, OutputValue value)
×
400
{
401
  assert(isEventLoopThread());
×
402

403
  switch(channel)
×
404
  {
405
    case OutputChannel::AccessoryDCC:
×
406
    case OutputChannel::AccessoryMotorola:
407
    {
408
      const auto switchProtocol = (channel == OutputChannel::AccessoryDCC) ? SwitchProtocol::DCC : SwitchProtocol::Motorola;
×
NEW
409
      boost::asio::post(m_ioContext,
×
410
        [this, switchProtocol, address=std::get<OutputAddress>(location).address, port=(std::get<OutputPairValue>(value) == OutputPairValue::Second)]()
×
411
        {
412
          switchManager().setSwitch(switchProtocol, address, port);
×
413
        });
×
414
      return true;
×
415
    }
416
    case OutputChannel::ECoSObject:
×
417
    {
NEW
418
      boost::asio::post(m_ioContext,
×
419
        [this, object=std::get<OutputECoSObject>(location).object, state=std::get<uint8_t>(value)]()
×
420
        {
421
          if(auto it = m_objects.find(object); it != m_objects.end())
×
422
          {
423
            if(auto* sw = dynamic_cast<Switch*>(it->second.get()))
×
424
            {
425
              sw->setState(state);
×
426
            }
427
          }
428
        });
×
429
      return true;
×
430
    }
431
    default: /*[[unlikely]]*/
×
432
      assert(false);
×
433
      break;
434
  }
435

436
  return false;
437
}
438

439
void Kernel::simulateInputChange(InputChannel channel, uint32_t address, SimulateInputAction action)
×
440
{
441
  if(!m_simulation)
×
442
    return;
×
443

NEW
444
  boost::asio::post(m_ioContext,
×
445
    [this, channel, address, action]()
×
446
    {
447
      switch(channel)
×
448
      {
449
        case InputChannel::S88:
×
450
        {
451
          uint16_t id = ObjectId::s88;
×
452
          uint32_t port = address - 1;
×
453
          auto it = m_objects.find(id);
×
454
          while(it != m_objects.end())
×
455
          {
456
            if(const auto* feedback = dynamic_cast<const Feedback*>(it->second.get()))
×
457
            {
458
              const auto ports = feedback->ports();
×
459
              if(port < ports)
×
460
              {
461
                uint16_t mask = 0;
×
462
                for(uint8_t i = 0; i < ports; i++)
×
463
                {
464
                  TriState value = feedback->operator[](i);
×
465
                  if(port == i)
×
466
                  {
467
                    switch(action)
×
468
                    {
469
                      case SimulateInputAction::SetFalse:
×
470
                        if(value == TriState::False)
×
471
                          return; // no change
×
472
                        value = TriState::False;
×
473
                        break;
×
474

475
                      case SimulateInputAction::SetTrue:
×
476
                        if(value == TriState::True)
×
477
                          return; // no change
×
478
                        value = TriState::True;
×
479
                        break;
×
480

481
                      case SimulateInputAction::Toggle:
×
482
                        value = (value == TriState::True) ? TriState::False : TriState::True;
×
483
                        break;
×
484
                    }
485
                  }
486
                  if(value == TriState::True)
×
487
                    mask |= 1 << i;
×
488
                }
489

490
                receive(
×
491
                  std::string("<EVENT ").append(std::to_string(feedback->id())).append(">\r\n")
×
492
                    .append(std::to_string(feedback->id())).append(" state[0x").append(mask != 0 ? ltrim(toHex(mask), '0') : std::string_view{"0"}).append("]>\r\n")
×
493
                    .append("<END 0 (OK)>\r\n"));
×
494

495
                break;
×
496
              }
497
              port -= ports;
×
498
            }
499
            it = m_objects.find(++id);
×
500
          }
501
          break;
×
502
        }
503
        case InputChannel::ECoSDetector:
×
504
          //! \todo Implement ECoS detector simulation
505
          break;
×
506

507
        default: [[unlikely]]
×
508
          assert(false);
×
509
          break;
510
      }
511
    });
512
}
513

514
void Kernel::switchManagerSwitched(SwitchProtocol protocol, uint16_t address, OutputPairValue value)
×
515
{
516
  ASSERT_IS_KERNEL_THREAD;
×
517

518
  if(!m_outputController)
×
519
    return;
×
520

521
  switch(protocol)
×
522
  {
523
    case SwitchProtocol::DCC:
×
524
      EventLoop::call(
×
525
        [this, address, value]()
×
526
        {
527
          m_outputController->updateOutputValue(OutputChannel::AccessoryDCC, OutputAddress(address), value);
×
528
        });
×
529
      break;
×
530

531
    case SwitchProtocol::Motorola:
×
532
      EventLoop::call(
×
533
        [this, address, value]()
×
534
        {
535
          m_outputController->updateOutputValue(OutputChannel::AccessoryMotorola, OutputAddress(address), value);
×
536
        });
×
537
      break;
×
538

539
    case SwitchProtocol::Unknown:
×
540
      assert(false);
×
541
      break;
542
  }
543
}
544

545
void Kernel::switchStateChanged(uint16_t objectId, uint8_t state)
×
546
{
547
  ASSERT_IS_KERNEL_THREAD;
×
548

549
  if(!m_outputController)
×
550
    return;
×
551

552
  EventLoop::call(
×
553
    [this, objectId, state]()
×
554
    {
555
      m_outputController->updateOutputValue(OutputChannel::ECoSObject, OutputECoSObject(objectId), state);
×
556
    });
×
557
}
558

559
void Kernel::feedbackStateChanged(Feedback& object, uint8_t port, TriState value)
×
560
{
561
  if(!m_inputController)
×
562
    return;
×
563

564
  if(isS88FeedbackId(object.id()))
×
565
  {
566
    uint32_t offset = 1;
×
567
    for(uint16_t id = ObjectId::s88; id < object.id(); id++)
×
568
    {
569
      auto it = m_objects.find(id);
×
570
      if(it == m_objects.end())
×
571
      {
572
        assert(false);
×
573
        return;
574
      }
575

576
      const auto* feedback = dynamic_cast<const Feedback*>(it->second.get());
×
577
      if(!feedback)
×
578
      {
579
        assert(false);
×
580
        return;
581
      }
582

583
      offset += feedback->ports();
×
584
    }
585

586
    EventLoop::call(
×
587
      [this, address=offset + port, value]()
×
588
      {
589
        m_inputController->updateInputValue(InputChannel::S88, InputAddress(address), value);
×
590
      });
×
591
  }
592
  else // ECoS Detector
593
  {
594
    const uint16_t portsPerObject = 16;
×
595
    const uint16_t address = 1 + port + portsPerObject * (object.id() - ObjectId::ecosDetector);
×
596

597
    EventLoop::call(
×
598
      [this, address, value]()
×
599
      {
600
        m_inputController->updateInputValue(InputChannel::ECoSDetector, InputAddress(address), value);
×
601
      });
×
602
  }
603
}
604

605
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
606
{
607
  assert(handler);
×
608
  assert(!m_ioHandler);
×
609
  m_ioHandler = std::move(handler);
×
610
}
×
611

612
bool Kernel::objectExists(uint16_t objectId) const
×
613
{
614
  return m_objects.find(objectId) != m_objects.end();
×
615
}
616

617
void Kernel::addObject(std::unique_ptr<Object> object)
×
618
{
619
  objectChanged(*object);
×
620
  m_objects.add(std::move(object));
×
621
}
×
622

623
void Kernel::objectChanged(Object& object)
×
624
{
625
  if(!m_onObjectChanged) /*[[unlikely]]*/
×
626
  {
627
    return;
×
628
  }
629

630
  std::string objectName;
×
631
  if(const auto* sw = dynamic_cast<const Switch*>(&object))
×
632
  {
633
    objectName = sw->nameUI();
×
634
  }
635

636
  EventLoop::call(
×
637
    [this, typeHash=typeid(object).hash_code(), objectId=object.id(), objectName]()
×
638
    {
639
      m_onObjectChanged(typeHash, objectId, objectName);
×
640
    });
×
641
}
×
642

643
void Kernel::removeObject(uint16_t objectId)
×
644
{
645
  m_objects.erase(objectId);
×
646
  if(m_onObjectRemoved) /*[[likely]]*/
×
647
  {
648
    m_onObjectRemoved(objectId);
×
649
  }
650
}
×
651

652
void Kernel::send(std::string_view message)
×
653
{
654
  if(m_ioHandler->send(message))
×
655
  {
656
    if(m_config.debugLogRXTX)
×
657
      EventLoop::call(
×
658
        [this, msg=std::string(rtrim(message, '\n'))]()
×
659
        {
660
          Log::log(logId, LogMessage::D2001_TX_X, msg);
×
661
        });
×
662
  }
663
  else
664
  {} // log message and go to error state
665
}
×
666

667
}
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