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

traintastic / traintastic / 23170091702

16 Mar 2026 10:43PM UTC coverage: 26.544% (-0.07%) from 26.616%
23170091702

push

github

reinder
[cbus] added initialization states (for future proof smooth startup without flooding the bus with messages)

0 of 88 new or added lines in 5 files covered. (0.0%)

22 existing lines in 5 files now uncovered.

8246 of 31065 relevant lines covered (26.54%)

185.02 hits per line

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

0.0
/server/src/hardware/protocol/cbus/cbuskernel.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 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 "cbuskernel.hpp"
23
#include "cbusmessages.hpp"
24
#include "cbustostring.hpp"
25
#include "simulator/cbussimulator.hpp"
26
#include "../dcc/dcc.hpp"
27
#include "../../../core/eventloop.hpp"
28
#include "../../../log/log.hpp"
29
#include "../../../log/logmessageexception.hpp"
30
#include "../../../utils/inrange.hpp"
31
#include "../../../utils/setthreadname.hpp"
32

33
namespace {
34

35
using namespace std::chrono_literals;
36

37
static constexpr auto queryNodeNumberTimeout = 100ms;
38
static constexpr auto requestCommandStationStatusTimeout = 100ms;
39

UNCOV
40
constexpr uint16_t makeAddressKey(uint16_t address, bool longAddress)
×
41
{
42
  return longAddress ? (0xC000 | (address & 0x3FFF)) : (address & 0x7F);
×
43
}
44

45
constexpr CBUS::SetEngineSessionMode::SpeedMode toSpeedMode(uint8_t speedSteps)
×
46
{
47
  using enum CBUS::SetEngineSessionMode::SpeedMode;
48

49
  switch(speedSteps)
×
50
  {
51
    case 14:
×
52
      return SpeedMode14;
×
53

54
    case 28:
×
55
      return SpeedMode28;
×
56

57
    default:
×
58
      return SpeedMode128;
×
59
  }
60
}
61

62
}
63

64
namespace CBUS {
65

66
Kernel::Kernel(std::string logId_, const Config& config, bool simulation)
×
67
  : KernelBase(std::move(logId_))
×
68
  , m_simulation{simulation}
×
NEW
69
  , m_initializationTimer{ioContext()}
×
70
  , m_config{config}
×
71
  , m_engineKeepAliveTimer{ioContext()}
×
72
{
73
  assert(isEventLoopThread());
×
74
}
×
75

76
void Kernel::setConfig(const Config& config)
×
77
{
78
  assert(isEventLoopThread());
×
79

80
  m_ioContext.post(
×
81
    [this, newConfig=config]()
×
82
    {
83
      m_config = newConfig;
×
84
    });
×
85
}
×
86

87
void Kernel::start()
×
88
{
89
  assert(isEventLoopThread());
×
90
  assert(m_ioHandler);
×
91

92
  m_thread = std::thread(
×
93
    [this]()
×
94
    {
95
      setThreadName("cbus");
×
96
      auto work = std::make_shared<boost::asio::io_context::work>(m_ioContext);
×
97
      m_ioContext.run();
×
98
    });
×
99

100
  m_ioContext.post(
×
101
    [this]()
×
102
    {
103
      try
104
      {
105
        m_ioHandler->start();
×
106
      }
107
      catch(const LogMessageException& e)
×
108
      {
109
        EventLoop::call(
×
110
          [this, e]()
×
111
          {
112
            Log::log(logId, e.message(), e.args());
×
113
            error();
×
114
          });
×
115
        return;
×
116
      }
×
117
    });
118
}
×
119

120
void Kernel::stop()
×
121
{
122
  assert(isEventLoopThread());
×
123

124
  m_ioContext.post(
×
125
    [this]()
×
126
    {
127
      m_ioHandler->stop();
×
128
    });
×
129

130
  m_ioContext.stop();
×
131

132
  m_thread.join();
×
133
}
×
134

135
void Kernel::started()
×
136
{
137
  assert(isKernelThread());
×
138

NEW
139
  nextState();
×
140
}
×
141

142
void Kernel::receive(uint8_t /*canId*/, const Message& message)
×
143
{
144
  assert(isKernelThread());
×
145

146
  if(m_config.debugLogRXTX)
×
147
  {
148
    EventLoop::call(
×
149
      [this, msg=toString(message)]()
×
150
      {
151
        Log::log(logId, LogMessage::D2002_RX_X, msg);
×
152
      });
×
153
  }
154

155
  switch(message.opCode)
×
156
  {
157
    case OpCode::TOF:
×
158
      m_trackOn = false;
×
159
      if(onTrackOff) [[likely]]
×
160
      {
161
        EventLoop::call(onTrackOff);
×
162
      }
163
      break;
×
164

165
    case OpCode::TON:
×
166
      m_trackOn = true;
×
167
      if(onTrackOn) [[likely]]
×
168
      {
169
        EventLoop::call(onTrackOn);
×
170
      }
171
      break;
×
172

173
    case OpCode::ESTOP:
×
174
      if(onEmergencyStop) [[likely]]
×
175
      {
176
        EventLoop::call(onEmergencyStop);
×
177
      }
178
      break;
×
179

180
    case OpCode::ASON:
×
181
    {
182
      const auto& ason = static_cast<const AccessoryShortOn&>(message);
×
183
      EventLoop::call(
×
184
        [this, eventNumber=ason.deviceNumber()]()
×
185
        {
186
          if(onShortEvent) [[likely]]
×
187
          {
188
            onShortEvent(eventNumber, true);
×
189
          }
190
        });
×
191
      break;
×
192
    }
193
    case OpCode::ASOF:
×
194
    {
195
      const auto& asof = static_cast<const AccessoryShortOff&>(message);
×
196
      EventLoop::call(
×
197
        [this, eventNumber=asof.deviceNumber()]()
×
198
        {
199
          if(onShortEvent) [[likely]]
×
200
          {
201
            onShortEvent(eventNumber, false);
×
202
          }
203
        });
×
204
      break;
×
205
    }
206
    case OpCode::ERR:
×
207
    {
208
      switch(static_cast<const CommandStationErrorMessage&>(message).errorCode)
×
209
      {
210
        using enum DCCErr;
211

212
        case LocoStackFull:
×
213
        {
214
          const auto& err = static_cast<const CommandStationLocoStackFullError&>(message);
×
215
          const auto key = makeAddressKey(err.address(), err.isLongAddress());
×
216
          if(m_engineGLOCs.contains(key))
×
217
          {
218
            m_engineGLOCs.erase(key);
×
219
            // FIXME: log error
220
          }
221
          break;
×
222
        }
223
        case SessionCancelled:
×
224
          if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
225
            [session=static_cast<const CommandStationSessionCancelled&>(message).session()](const auto& item)
×
226
            {
227
              return item.second.session && *item.second.session == session;
×
228
            }); it != m_engines.end())
×
229
          {
230
            it->second.session = std::nullopt;
×
231

232
            if(m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *it->second.session)
×
233
            {
234
              restartEngineKeepAliveTimer();
×
235
            }
236

237
            EventLoop::call(
×
238
              [this, key=it->first]()
×
239
              {
240
                if(onEngineSessionCancelled) [[likely]]
×
241
                {
242
                  onEngineSessionCancelled(key & 0x3FFF, (key & 0xC000) == 0xC000);
×
243
                }
244
              });
×
245
          }
246
          break;
×
247

248
        case LocoAddressTaken:
×
249
        case SessionNotPresent:
250
        case ConsistEmpty:
251
        case LocoNotFound:
252
        case CANBusError:
253
        case InvalidRequest:
254
          break;
×
255
      }
256
      break;
×
257
    }
258
    case OpCode::ACON:
×
259
    {
260
      const auto& acon = static_cast<const AccessoryOn&>(message);
×
261
      EventLoop::call(
×
262
        [this, nodeNumber=acon.nodeNumber(), eventNumber=acon.eventNumber()]()
×
263
        {
264
          if(onLongEvent) [[likely]]
×
265
          {
266
            onLongEvent(nodeNumber, eventNumber, true);
×
267
          }
268
        });
×
269
      break;
×
270
    }
271
    case OpCode::ACOF:
×
272
    {
273
      const auto& acof = static_cast<const AccessoryOff&>(message);
×
274
      EventLoop::call(
×
275
        [this, nodeNumber=acof.nodeNumber(), eventNumber=acof.eventNumber()]()
×
276
        {
277
          if(onLongEvent) [[likely]]
×
278
          {
279
            onLongEvent(nodeNumber, eventNumber, false);
×
280
          }
281
        });
×
282
      break;
×
283
    }
NEW
284
    case OpCode::PNN:
×
NEW
285
      if(m_state == State::QueryNodes)
×
286
      {
NEW
287
        restartInitializationTimer(queryNodeNumberTimeout);
×
288
      }
NEW
289
      break;
×
290

UNCOV
291
    case OpCode::PLOC:
×
292
    {
293
      const auto& ploc = static_cast<const EngineReport&>(message);
×
294
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
295
      if(m_engineGLOCs.contains(key))
×
296
      {
297
        m_engineGLOCs.erase(key);
×
298

299
        if(auto it = m_engines.find(key); it != m_engines.end())
×
300
        {
301
          auto& engine = it->second;
×
302
          engine.session = ploc.session;
×
303

304
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
305
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
306

307
          for(const auto& [number, value] : engine.functions)
×
308
          {
309
            sendSetEngineFunction(ploc.session, number, value);
×
310
          }
311

312
          engine.lastCommand = std::chrono::steady_clock::now();
×
313

314
          if(!m_engineKeepAliveTimerActive)
×
315
          {
316
            restartEngineKeepAliveTimer();
×
317
          }
318
        }
319
        else // we're no longer in need of control (rare but possible)
320
        {
321
          send(ReleaseEngine(ploc.session));
×
322
        }
323
      }
324
      break;
×
325
    }
NEW
326
    case OpCode::STAT:
×
NEW
327
      if(m_state == State::GetCommandStationStatus)
×
328
      {
NEW
329
        m_initializationTimer.cancel();
×
NEW
330
        nextState();
×
331
      }
NEW
332
      break;
×
333

334
    default:
×
335
      break;
×
336
  }
337
}
×
338

339
void Kernel::trackOff()
×
340
{
341
  assert(isEventLoopThread());
×
342

343
  m_ioContext.post(
×
344
    [this]()
×
345
    {
346
      if(m_trackOn)
×
347
      {
348
        send(RequestTrackOff());
×
349
      }
350
    });
×
351
}
×
352

353
void Kernel::trackOn()
×
354
{
355
  assert(isEventLoopThread());
×
356

357
  m_ioContext.post(
×
358
    [this]()
×
359
    {
360
      if(!m_trackOn)
×
361
      {
362
        send(RequestTrackOn());
×
363
      }
364
    });
×
365
}
×
366

367
void Kernel::requestEmergencyStop()
×
368
{
369
  assert(isEventLoopThread());
×
370

371
  m_ioContext.post(
×
372
    [this]()
×
373
    {
374
      send(RequestEmergencyStop());
×
375
    });
×
376
}
×
377

378
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
379
{
380
  assert(isEventLoopThread());
×
381

382
  const uint8_t speed = eStop ? 1 : (speedStep > 0 ? speedStep + 1 : 0);
×
383

384
  m_ioContext.post(
×
385
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
386
    {
387
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
388
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
389
      engine.speedSteps = speedSteps;
×
390
      engine.speed = speed;
×
391
      engine.directionForward = directionForward;
×
392

393
      if(engine.session) // we're in control
×
394
      {
395
        if(speedStepsChanged)
×
396
        {
397
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
398
        }
399
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
400

401
        engine.lastCommand = std::chrono::steady_clock::now();
×
402

403
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
404
        {
405
          restartEngineKeepAliveTimer();
×
406
        }
407
      }
408
      else // take control
409
      {
410
        sendGetEngineSession(address, longAddress);
×
411
      }
412
    });
×
413
}
×
414

415
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
416
{
417
  assert(isEventLoopThread());
×
418

419
  m_ioContext.post(
×
420
    [this, address, longAddress, number, value]()
×
421
    {
422
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
423
      engine.functions[number] = value;
×
424
      if(engine.session) // we're in control
×
425
      {
426
        sendSetEngineFunction(*engine.session, number, value);
×
427

428
        engine.lastCommand = std::chrono::steady_clock::now();
×
429

430
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
431
        {
432
          restartEngineKeepAliveTimer();
×
433
        }
434
      }
435
      else // take control
436
      {
437
        sendGetEngineSession(address, longAddress);
×
438
      }
439
    });
×
440
}
×
441

442
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
443
{
444
  assert(isEventLoopThread());
×
445

446
  m_ioContext.post(
×
447
    [this, deviceNumber, on]()
×
448
    {
449
      if(on)
×
450
      {
451
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
452
      }
453
      else
454
      {
455
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
456
      }
457
    });
×
458
}
×
459

460
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
461
{
462
  assert(isEventLoopThread());
×
463

464
  m_ioContext.post(
×
465
    [this, nodeNumber, eventNumber, on]()
×
466
    {
467
      if(on)
×
468
      {
469
        send(AccessoryOn(nodeNumber, eventNumber));
×
470
      }
471
      else
472
      {
473
        send(AccessoryOff(nodeNumber, eventNumber));
×
474
      }
475
    });
×
476
}
×
477

478
bool Kernel::send(std::vector<uint8_t> message)
×
479
{
480
  assert(isEventLoopThread());
×
481

482
  if(!inRange<size_t>(message.size(), 1, 8))
×
483
  {
484
    return false;
×
485
  }
486

487
  m_ioContext.post(
×
488
    [this, msg=std::move(message)]()
×
489
    {
490
      send(*reinterpret_cast<const Message*>(msg.data()));
×
491
    });
×
492

493
  return true;
×
494
}
495

496
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
497
{
498
  assert(isEventLoopThread());
×
499

500
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
501
  {
502
    return false;
×
503
  }
504

505
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
506

507
  m_ioContext.post(
×
508
    [this, packet=std::move(dccPacket), repeat]()
×
509
    {
510
      switch(packet.size())
×
511
      {
512
        case 3:
×
513
          send(RequestDCCPacket<3>(packet, repeat));
×
514
          break;
×
515

516
        case 4:
×
517
          send(RequestDCCPacket<4>(packet, repeat));
×
518
          break;
×
519

520
        case 5:
×
521
          send(RequestDCCPacket<5>(packet, repeat));
×
522
          break;
×
523

524
        case 6:
×
525
          send(RequestDCCPacket<6>(packet, repeat));
×
526
          break;
×
527

528
        default: [[unlikely]]
×
529
          assert(false);
×
530
          break;
531
      }
532
    });
×
533

534
  return true;
×
535
}
536

537
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
538
{
539
  assert(isEventLoopThread());
×
540
  assert(handler);
×
541
  assert(!m_ioHandler);
×
542
  m_ioHandler = std::move(handler);
×
543
}
×
544

545
void Kernel::send(const Message& message)
×
546
{
547
  assert(isKernelThread());
×
548

549
  if(m_config.debugLogRXTX)
×
550
  {
551
    EventLoop::call(
×
552
      [this, msg=toString(message)]()
×
553
      {
554
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
555
      });
×
556
  }
557

558
  if(auto ec = m_ioHandler->send(message); ec)
×
559
  {
560
    (void)ec; // FIXME: handle error
561
  }
562
}
×
563

564
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
565
{
566
  assert(isKernelThread());
×
567
  const auto key = makeAddressKey(address, longAddress);
×
568
  if(!m_engineGLOCs.contains(key))
×
569
  {
570
    m_engineGLOCs.emplace(key);
×
571
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
572
  }
573
}
×
574

575
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
576
{
577
  assert(isKernelThread());
×
578
  // FIXME: what to do with: serviceMode and soundControlMode?
579
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
580
}
×
581

582
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
583
{
584
  assert(isKernelThread());
×
585
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
586
}
×
587

588
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
589
{
590
  assert(isKernelThread());
×
591
  if(value)
×
592
  {
593
    send(SetEngineFunctionOn(session, number));
×
594
  }
595
  else
596
  {
597
    send(SetEngineFunctionOff(session, number));
×
598
  }
599
}
×
600

NEW
601
void Kernel::changeState(State value)
×
602
{
NEW
603
  assert(isKernelThread());
×
NEW
604
  assert(m_state != value);
×
605

NEW
606
  m_state = value;
×
607

NEW
608
  switch(m_state)
×
609
  {
NEW
610
    case State::Initial:
×
NEW
611
      break;
×
612

NEW
613
    case State::QueryNodes:
×
NEW
614
      send(QueryNodeNumber());
×
NEW
615
      restartInitializationTimer(queryNodeNumberTimeout);
×
NEW
616
      break;
×
617

NEW
618
    case State::GetCommandStationStatus:
×
NEW
619
      send(RequestCommandStationStatus());
×
NEW
620
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
NEW
621
      break;
×
622

NEW
623
    case State::Started:
×
NEW
624
      KernelBase::started();
×
NEW
625
      break;
×
626
  }
NEW
627
}
×
628

NEW
629
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
630
{
NEW
631
  assert(isKernelThread());
×
632

NEW
633
  m_initializationTimer.cancel();
×
634

NEW
635
  m_initializationTimer.expires_after(timeout);
×
NEW
636
  m_initializationTimer.async_wait(
×
NEW
637
    [this](std::error_code ec)
×
638
    {
NEW
639
      if(ec)
×
640
      {
NEW
641
        return;
×
642
      }
643

NEW
644
      switch(m_state)
×
645
      {
NEW
646
        case State::QueryNodes:
×
NEW
647
          nextState();
×
NEW
648
          break;
×
649

NEW
650
        case State::GetCommandStationStatus:
×
NEW
651
          nextState();
×
NEW
652
          break;
×
653

NEW
654
        case State::Initial:
×
655
        case State::Started:
NEW
656
          assert(false);
×
657
          break;
658
      }
659
    });
NEW
660
}
×
661

UNCOV
662
void Kernel::restartEngineKeepAliveTimer()
×
663
{
NEW
664
  assert(isKernelThread());
×
665

UNCOV
666
  m_engineKeepAliveTimer.cancel();
×
667

668
  // find first expiring engine:
669
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
670
  for(const auto& [_, engine] : m_engines)
×
671
  {
672
    if(engine.session && engine.lastCommand < lastUpdate)
×
673
    {
674
      lastUpdate = engine.lastCommand;
×
675
      m_engineKeepAliveSession = *engine.session;
×
676
    }
677
  }
678

679
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
680

681
  if(m_engineKeepAliveTimerActive)
×
682
  {
683
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
684
    m_engineKeepAliveTimer.async_wait(
×
685
      [this](std::error_code ec)
×
686
      {
687
        if(ec)
×
688
        {
689
          return;
×
690
        }
691

692
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
693

694
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
695
            [session=m_engineKeepAliveSession](const auto& item)
×
696
            {
697
              return item.second.session && *item.second.session == session;
×
698
            }); it != m_engines.end()) [[likely]]
×
699
        {
700
          it->second.lastCommand = std::chrono::steady_clock::now();
×
701
        }
702

703
        restartEngineKeepAliveTimer();
×
704
      });
705
  }
706
}
×
707

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