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

traintastic / traintastic / 23259071638

18 Mar 2026 05:50PM UTC coverage: 26.526% (-0.02%) from 26.544%
23259071638

push

github

reinder
[cbus] added DCCext output channel

0 of 21 new or added lines in 3 files covered. (0.0%)

3 existing lines in 2 files now uncovered.

8246 of 31086 relevant lines covered (26.53%)

184.9 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 "../dcc/messages.hpp"
28
#include "../../../core/eventloop.hpp"
29
#include "../../../log/log.hpp"
30
#include "../../../log/logmessageexception.hpp"
31
#include "../../../utils/inrange.hpp"
32
#include "../../../utils/setthreadname.hpp"
33

34
namespace {
35

36
using namespace std::chrono_literals;
37

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

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

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

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

55
    case 28:
×
56
      return SpeedMode28;
×
57

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

63
}
64

65
namespace CBUS {
66

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

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

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

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

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

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

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

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

131
  m_ioContext.stop();
×
132

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

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

140
  nextState();
×
141
}
×
142

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
479
void Kernel::setDccAdvancedAccessoryValue(uint16_t address, uint8_t aspect)
×
480
{
NEW
481
  assert(isEventLoopThread());
×
482

NEW
483
  m_ioContext.post(
×
NEW
484
    [this, address, aspect]()
×
485
    {
NEW
486
      send(RequestDCCPacket<sizeof(DCC::SetAdvancedAccessoryValue) + 1>(DCC::SetAdvancedAccessoryValue(address, aspect), Config::dccExtRepeat));
×
NEW
487
    });
×
NEW
488
}
×
489

UNCOV
490
bool Kernel::send(std::vector<uint8_t> message)
×
491
{
492
  assert(isEventLoopThread());
×
493

494
  if(!inRange<size_t>(message.size(), 1, 8))
×
495
  {
496
    return false;
×
497
  }
498

499
  m_ioContext.post(
×
500
    [this, msg=std::move(message)]()
×
501
    {
502
      send(*reinterpret_cast<const Message*>(msg.data()));
×
503
    });
×
504

505
  return true;
×
506
}
507

508
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
509
{
510
  assert(isEventLoopThread());
×
511

512
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
513
  {
514
    return false;
×
515
  }
516

517
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
518

519
  m_ioContext.post(
×
520
    [this, packet=std::move(dccPacket), repeat]()
×
521
    {
522
      switch(packet.size())
×
523
      {
524
        case 3:
×
525
          send(RequestDCCPacket<3>(packet, repeat));
×
526
          break;
×
527

528
        case 4:
×
529
          send(RequestDCCPacket<4>(packet, repeat));
×
530
          break;
×
531

532
        case 5:
×
533
          send(RequestDCCPacket<5>(packet, repeat));
×
534
          break;
×
535

536
        case 6:
×
537
          send(RequestDCCPacket<6>(packet, repeat));
×
538
          break;
×
539

540
        default: [[unlikely]]
×
541
          assert(false);
×
542
          break;
543
      }
544
    });
×
545

546
  return true;
×
547
}
548

549
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
550
{
551
  assert(isEventLoopThread());
×
552
  assert(handler);
×
553
  assert(!m_ioHandler);
×
554
  m_ioHandler = std::move(handler);
×
555
}
×
556

557
void Kernel::send(const Message& message)
×
558
{
559
  assert(isKernelThread());
×
560

561
  if(m_config.debugLogRXTX)
×
562
  {
563
    EventLoop::call(
×
564
      [this, msg=toString(message)]()
×
565
      {
566
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
567
      });
×
568
  }
569

570
  if(auto ec = m_ioHandler->send(message); ec)
×
571
  {
572
    (void)ec; // FIXME: handle error
573
  }
574
}
×
575

576
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
577
{
578
  assert(isKernelThread());
×
579
  const auto key = makeAddressKey(address, longAddress);
×
580
  if(!m_engineGLOCs.contains(key))
×
581
  {
582
    m_engineGLOCs.emplace(key);
×
583
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
584
  }
585
}
×
586

587
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
588
{
589
  assert(isKernelThread());
×
590
  // FIXME: what to do with: serviceMode and soundControlMode?
591
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
592
}
×
593

594
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
595
{
596
  assert(isKernelThread());
×
597
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
598
}
×
599

600
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
601
{
602
  assert(isKernelThread());
×
603
  if(value)
×
604
  {
605
    send(SetEngineFunctionOn(session, number));
×
606
  }
607
  else
608
  {
609
    send(SetEngineFunctionOff(session, number));
×
610
  }
611
}
×
612

613
void Kernel::changeState(State value)
×
614
{
615
  assert(isKernelThread());
×
616
  assert(m_state != value);
×
617

618
  m_state = value;
×
619

620
  switch(m_state)
×
621
  {
622
    case State::Initial:
×
623
      break;
×
624

625
    case State::QueryNodes:
×
626
      send(QueryNodeNumber());
×
627
      restartInitializationTimer(queryNodeNumberTimeout);
×
628
      break;
×
629

630
    case State::GetCommandStationStatus:
×
631
      send(RequestCommandStationStatus());
×
632
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
633
      break;
×
634

635
    case State::Started:
×
636
      KernelBase::started();
×
637
      break;
×
638
  }
639
}
×
640

641
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
642
{
643
  assert(isKernelThread());
×
644

645
  m_initializationTimer.cancel();
×
646

647
  m_initializationTimer.expires_after(timeout);
×
648
  m_initializationTimer.async_wait(
×
649
    [this](std::error_code ec)
×
650
    {
651
      if(ec)
×
652
      {
653
        return;
×
654
      }
655

656
      switch(m_state)
×
657
      {
658
        case State::QueryNodes:
×
659
          nextState();
×
660
          break;
×
661

662
        case State::GetCommandStationStatus:
×
663
          nextState();
×
664
          break;
×
665

666
        case State::Initial:
×
667
        case State::Started:
668
          assert(false);
×
669
          break;
670
      }
671
    });
672
}
×
673

674
void Kernel::restartEngineKeepAliveTimer()
×
675
{
676
  assert(isKernelThread());
×
677

678
  m_engineKeepAliveTimer.cancel();
×
679

680
  // find first expiring engine:
681
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
682
  for(const auto& [_, engine] : m_engines)
×
683
  {
684
    if(engine.session && engine.lastCommand < lastUpdate)
×
685
    {
686
      lastUpdate = engine.lastCommand;
×
687
      m_engineKeepAliveSession = *engine.session;
×
688
    }
689
  }
690

691
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
692

693
  if(m_engineKeepAliveTimerActive)
×
694
  {
695
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
696
    m_engineKeepAliveTimer.async_wait(
×
697
      [this](std::error_code ec)
×
698
      {
699
        if(ec)
×
700
        {
701
          return;
×
702
        }
703

704
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
705

706
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
707
            [session=m_engineKeepAliveSession](const auto& item)
×
708
            {
709
              return item.second.session && *item.second.session == session;
×
710
            }); it != m_engines.end()) [[likely]]
×
711
        {
712
          it->second.lastCommand = std::chrono::steady_clock::now();
×
713
        }
714

715
        restartEngineKeepAliveTimer();
×
716
      });
717
  }
718
}
×
719

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