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

traintastic / traintastic / 23872903160

01 Apr 2026 09:58PM UTC coverage: 25.802% (+0.003%) from 25.799%
23872903160

push

github

reinder
[cbus] added cbus booster support, uses ACON2 events

44 of 136 new or added lines in 5 files covered. (32.35%)

62 existing lines in 7 files now uncovered.

8300 of 32168 relevant lines covered (25.8%)

179.72 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 readNodeParameterTimeout = 50ms;
39
static constexpr auto requestShortEventTimeout = 50ms;
40
static constexpr auto requestLongEventTimeout = 50ms;
41
static constexpr auto requestCommandStationStatusTimeout = 100ms;
42

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

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

52
  switch(speedSteps)
×
53
  {
54
    case 14:
×
55
      return SpeedMode14;
×
56

57
    case 28:
×
58
      return SpeedMode28;
×
59

60
    default:
×
61
      return SpeedMode128;
×
62
  }
63
}
64

65
}
66

67
namespace CBUS {
68

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

80
void Kernel::setConfig(const Config& config)
×
81
{
82
  assert(isEventLoopThread());
×
83

84
  boost::asio::post(m_ioContext,
×
85
    [this, newConfig=config]()
×
86
    {
87
      m_config = newConfig;
×
88
    });
×
89
}
×
90

91
void Kernel::setRequestEventsDuringInitialize(std::vector<uint16_t> shortEvents, std::vector<std::pair<uint16_t,uint16_t>> longEvents)
×
92
{
93
  assert(isEventLoopThread());
×
94

95
  std::reverse(shortEvents.begin(), shortEvents.end());
×
96
  std::reverse(longEvents.begin(), longEvents.end());
×
97

98
  m_initializationRequestShortEvents = std::move(shortEvents);
×
99
  m_initializationRequestLongEvents = std::move(longEvents);
×
100
}
×
101

102
void Kernel::start()
×
103
{
104
  assert(isEventLoopThread());
×
105
  assert(m_ioHandler);
×
106

107
  m_thread = std::thread(
×
108
    [this]()
×
109
    {
110
      setThreadName("cbus");
×
111
      auto work = std::make_shared<boost::asio::io_context::work>(m_ioContext);
×
112
      m_ioContext.run();
×
113
    });
×
114

115
  boost::asio::post(m_ioContext,
×
116
    [this]()
×
117
    {
118
      try
119
      {
120
        m_ioHandler->start();
×
121
      }
122
      catch(const LogMessageException& e)
×
123
      {
124
        EventLoop::call(
×
125
          [this, e]()
×
126
          {
127
            Log::log(logId, e.message(), e.args());
×
128
            error();
×
129
          });
×
130
        return;
×
131
      }
×
132
    });
133
}
×
134

135
void Kernel::stop()
×
136
{
137
  assert(isEventLoopThread());
×
138

139
  boost::asio::post(m_ioContext,
×
140
    [this]()
×
141
    {
142
      m_ioHandler->stop();
×
143
    });
×
144

145
  m_ioContext.stop();
×
146

147
  m_thread.join();
×
148
}
×
149

150
void Kernel::started()
×
151
{
152
  assert(isKernelThread());
×
153

154
  nextState();
×
155
}
×
156

157
void Kernel::receive(uint8_t canId, const Message& message)
×
158
{
159
  assert(isKernelThread());
×
160

161
  if(m_config.debugLogRXTX)
×
162
  {
163
    EventLoop::call(
×
164
      [this, msg=toString(message)]()
×
165
      {
166
        Log::log(logId, LogMessage::D2002_RX_X, msg);
×
167
      });
×
168
  }
169

170
  switch(message.opCode)
×
171
  {
172
    case OpCode::TOF:
×
173
      m_trackOn = false;
×
174
      if(onTrackOff) [[likely]]
×
175
      {
176
        EventLoop::call(onTrackOff);
×
177
      }
178
      break;
×
179

180
    case OpCode::TON:
×
181
      m_trackOn = true;
×
182
      if(onTrackOn) [[likely]]
×
183
      {
184
        EventLoop::call(onTrackOn);
×
185
      }
186
      break;
×
187

188
    case OpCode::ESTOP:
×
189
      if(onEmergencyStop) [[likely]]
×
190
      {
191
        EventLoop::call(onEmergencyStop);
×
192
      }
193
      break;
×
194

195
    case OpCode::KLOC:
×
196
      receiveKLOC(static_cast<const ReleaseEngine&>(message));
×
197
      break;
×
198

199
    case OpCode::DSPD:
×
200
      receiveDSPD(static_cast<const SetEngineSpeedDirection&>(message));
×
201
      break;
×
202

203
    case OpCode::DFNON:
×
204
    case OpCode::DFNOF:
205
      receiveDFNOx(static_cast<const SetEngineFunction&>(message));
×
206
      break;
×
207

208
    case OpCode::DFUN:
×
209
      receiveDFUN(static_cast<const SetEngineFunctions&>(message));
×
210
      break;
×
211

212
    case OpCode::ASON:
×
213
      receiveShortEvent(static_cast<const AccessoryShortOn&>(message).deviceNumber(), true);
×
214
      break;
×
215

216
    case OpCode::ASOF:
×
217
      receiveShortEvent(static_cast<const AccessoryShortOff&>(message).deviceNumber(), false);
×
218
      break;
×
219

220
    case OpCode::ARSON:
×
221
    {
222
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
223
      receiveShortEvent(eventNumber, true);
×
224
      if(m_state == State::RequestShortEvents &&
×
225
          m_initializationRequestShortEvents.back() == eventNumber)
×
226
      {
227
        m_initializationRequestShortEvents.pop_back();
×
228
        requestShortEvent();
×
229
      }
230
      break;
×
231
    }
232
    case OpCode::ARSOF:
×
233
    {
234
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
235
      receiveShortEvent(eventNumber, false);
×
236
      if(m_state == State::RequestShortEvents &&
×
237
          m_initializationRequestShortEvents.back() == eventNumber)
×
238
      {
239
        m_initializationRequestShortEvents.pop_back();
×
240
        requestShortEvent();
×
241
      }
242
      break;
×
243
    }
244
    case OpCode::ERR:
×
245
    {
246
      switch(static_cast<const CommandStationErrorMessage&>(message).errorCode)
×
247
      {
248
        using enum DCCErr;
249

250
        case LocoStackFull:
×
251
        {
252
          const auto& err = static_cast<const CommandStationLocoStackFullError&>(message);
×
253
          const auto key = makeAddressKey(err.address(), err.isLongAddress());
×
254
          if(m_engineGLOCs.contains(key))
×
255
          {
256
            m_engineGLOCs.erase(key);
×
257
            // FIXME: log error
258
          }
259
          break;
×
260
        }
261
        case SessionCancelled:
×
262
          if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
263
            [session=static_cast<const CommandStationSessionCancelled&>(message).session()](const auto& item)
×
264
            {
265
              return item.second.session && *item.second.session == session;
×
266
            }); it != m_engines.end())
×
267
          {
268
            it->second.session = std::nullopt;
×
269

270
            if(m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *it->second.session)
×
271
            {
272
              restartEngineKeepAliveTimer();
×
273
            }
274

275
            EventLoop::call(
×
276
              [this, key=it->first]()
×
277
              {
278
                if(onEngineSessionCancelled) [[likely]]
×
279
                {
280
                  onEngineSessionCancelled(key & 0x3FFF, (key & 0xC000) == 0xC000);
×
281
                }
282
              });
×
283
          }
284
          break;
×
285

286
        case LocoAddressTaken:
×
287
        case SessionNotPresent:
288
        case ConsistEmpty:
289
        case LocoNotFound:
290
        case CANBusError:
291
        case InvalidRequest:
292
          break;
×
293
      }
294
      break;
×
295
    }
296
    case OpCode::ACON:
×
297
    {
298
      const auto& acon = static_cast<const AccessoryOn&>(message);
×
299
      receiveLongEvent(acon.nodeNumber(), acon.eventNumber(), true);
×
300
      break;
×
301
    }
302
    case OpCode::ACOF:
×
303
    {
304
      const auto& acof = static_cast<const AccessoryOff&>(message);
×
305
      receiveLongEvent(acof.nodeNumber(), acof.eventNumber(), false);
×
306
      break;
×
307
    }
308
    case OpCode::ARON:
×
309
    {
310
      const auto& aron = static_cast<const AccessoryResponseEventOn&>(message);
×
311
      receiveLongEvent(aron.nodeNumber(), aron.eventNumber(), true);
×
312
      if(m_state == State::RequestShortEvents &&
×
313
          m_initializationRequestLongEvents.back().first == aron.nodeNumber() &&
×
314
          m_initializationRequestLongEvents.back().second == aron.eventNumber())
×
315
      {
316
        m_initializationRequestLongEvents.pop_back();
×
317
        requestLongEvent();
×
318
      }
319
      break;
×
320
    }
321
    case OpCode::AROF:
×
322
    {
323
      const auto& arof = static_cast<const AccessoryResponseEventOff&>(message);
×
324
      receiveLongEvent(arof.nodeNumber(), arof.eventNumber(), false);
×
325
      if(m_state == State::RequestShortEvents &&
×
326
          m_initializationRequestLongEvents.back().first == arof.nodeNumber() &&
×
327
          m_initializationRequestLongEvents.back().second == arof.eventNumber())
×
328
      {
329
        m_initializationRequestLongEvents.pop_back();
×
330
        requestLongEvent();
×
331
      }
332
      break;
×
333
    }
334
    case OpCode::PARAN:
×
335
      if(m_state == State::ReadNodeParameters && !m_readNodeParameters.empty())
×
336
      {
337
        const auto& rqnpn = m_readNodeParameters.front();
×
338
        const auto& paran = static_cast<const NodeParameterResponse&>(message);
×
339

340
        if(rqnpn.nodeNumber() == paran.nodeNumber() && rqnpn.parameter == paran.parameter)
×
341
        {
342
          m_readNodeParameters.pop();
×
343
          m_initializationTimer.cancel();
×
344

345
          EventLoop::call(
×
346
            [this, canId, paran]()
×
347
            {
348
              if(onNodeParameterResponse) [[likely]]
×
349
              {
350
                onNodeParameterResponse(canId, paran.nodeNumber(), paran.parameter, paran.value);
×
351
              }
352
            });
×
353

354
          readNodeParameter();
×
355
        }
356
      }
357
      break;
×
358

359
    case OpCode::PNN:
×
360
      if(m_state == State::QueryNodes)
×
361
      {
362
        restartInitializationTimer(queryNodeNumberTimeout);
×
363

364
        const auto& pnn = static_cast<const PresenceOfNode&>(message);
×
365

366
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::VersionMajor));
×
367
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::VersionMinor));
×
368
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::BetaReleaseCode));
×
369

370
        EventLoop::call(
×
371
          [this, canId, pnn]()
×
372
          {
373
            if(onPresenceOfNode) [[likely]]
×
374
            {
375
              onPresenceOfNode(canId, pnn.nodeNumber(), pnn.manufacturerId, pnn.moduleId, pnn.flimMode(), pnn.supportsServiceDiscovery());
×
376
            }
377
          });
×
378
      }
379
      break;
×
380

381
    case OpCode::PLOC:
×
382
    {
383
      const auto& ploc = static_cast<const EngineReport&>(message);
×
384

385
      EventLoop::call(
×
386
        [this, ploc]()
×
387
        {
388
          if(onEngineSessionAcquire) [[likely]]
×
389
          {
390
            onEngineSessionAcquire(ploc.session, ploc.address(), ploc.isLongAddress());
×
391
          }
392
        });
×
393

394
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
395
      if(m_engineGLOCs.contains(key))
×
396
      {
397
        m_engineGLOCs.erase(key);
×
398

399
        if(auto it = m_engines.find(key); it != m_engines.end())
×
400
        {
401
          auto& engine = it->second;
×
402
          engine.session = ploc.session;
×
403

404
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
405
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
406

407
          for(const auto& [number, value] : engine.functions)
×
408
          {
409
            sendSetEngineFunction(ploc.session, number, value);
×
410
          }
411

412
          engine.lastCommand = std::chrono::steady_clock::now();
×
413

414
          if(!m_engineKeepAliveTimerActive)
×
415
          {
416
            restartEngineKeepAliveTimer();
×
417
          }
418
        }
419
        else // we're no longer in need of control (rare but possible)
420
        {
421
          send(ReleaseEngine(ploc.session));
×
422
        }
423
      }
424
      break;
×
425
    }
426
    case OpCode::STAT:
×
427
      if(m_state == State::GetCommandStationStatus)
×
428
      {
429
        m_initializationTimer.cancel();
×
430
        nextState();
×
431
      }
432
      break;
×
433

434
    default:
×
435
      break;
×
436
  }
437

438
  // external listeners:
NEW
439
  for(const auto& [handle, opCode] : m_onReceiveFilters)
×
440
  {
NEW
441
    if(message.opCode == opCode)
×
442
    {
NEW
443
      auto buffer = std::make_shared<std::byte[]>(message.size());
×
NEW
444
      std::memcpy(buffer.get(), &message, message.size());
×
NEW
445
      EventLoop::call(
×
NEW
446
        [this, handle, canId, data=std::move(buffer)]()
×
447
        {
448
          // check if still registered, could theoretically be unregister after filtering and before callback:
NEW
449
          if(auto it = m_onReceiveCallbacks.find(handle); it != m_onReceiveCallbacks.end())
×
450
          {
NEW
451
            it->second(canId, *reinterpret_cast<const Message*>(data.get()));
×
452
          }
NEW
453
        });
×
NEW
454
    }
×
455
  }
NEW
456
}
×
457

NEW
458
size_t Kernel::registerOnReceive(OpCode opCode, std::function<void(uint8_t, const Message&)> callback)
×
459
{
NEW
460
  assert(isEventLoopThread());
×
461

NEW
462
  while(++m_onReceiveHandle == 0);
×
463

NEW
464
  m_onReceiveCallbacks.emplace(m_onReceiveHandle, std::move(callback));
×
465

NEW
466
  m_ioContext.post(
×
NEW
467
    [this, handle=m_onReceiveHandle, opCode]()
×
468
    {
NEW
469
      m_onReceiveFilters.emplace(handle, opCode);
×
NEW
470
    });
×
471

NEW
472
  return m_onReceiveHandle;
×
473
}
474

NEW
475
void Kernel::unregisterOnReceive(size_t handle)
×
476
{
NEW
477
  assert(isEventLoopThread());
×
478

NEW
479
  m_onReceiveCallbacks.erase(handle);
×
480

NEW
481
  m_ioContext.post(
×
NEW
482
    [this, handle]()
×
483
    {
NEW
484
      m_onReceiveFilters.erase(handle);
×
NEW
485
    });
×
UNCOV
486
}
×
487

488
void Kernel::trackOff()
×
489
{
490
  assert(isEventLoopThread());
×
491

492
  boost::asio::post(m_ioContext,
×
493
    [this]()
×
494
    {
495
      if(m_trackOn)
×
496
      {
497
        send(RequestTrackOff());
×
498
      }
499
    });
×
500
}
×
501

502
void Kernel::trackOn()
×
503
{
504
  assert(isEventLoopThread());
×
505

506
  boost::asio::post(m_ioContext,
×
507
    [this]()
×
508
    {
509
      if(!m_trackOn)
×
510
      {
511
        send(RequestTrackOn());
×
512
      }
513
    });
×
514
}
×
515

516
void Kernel::requestEmergencyStop()
×
517
{
518
  assert(isEventLoopThread());
×
519

520
  boost::asio::post(m_ioContext,
×
521
    [this]()
×
522
    {
523
      send(RequestEmergencyStop());
×
524
    });
×
525
}
×
526

527
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
528
{
529
  assert(isEventLoopThread());
×
530

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

533
  boost::asio::post(m_ioContext,
×
534
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
535
    {
536
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
537
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
538
      engine.speedSteps = speedSteps;
×
539
      engine.speed = speed;
×
540
      engine.directionForward = directionForward;
×
541

542
      if(engine.session) // we're in control
×
543
      {
544
        if(speedStepsChanged)
×
545
        {
546
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
547
        }
548
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
549

550
        engine.lastCommand = std::chrono::steady_clock::now();
×
551

552
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
553
        {
554
          restartEngineKeepAliveTimer();
×
555
        }
556
      }
557
      else // take control
558
      {
559
        sendGetEngineSession(address, longAddress);
×
560
      }
561
    });
×
562
}
×
563

564
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
565
{
566
  assert(isEventLoopThread());
×
567

568
  boost::asio::post(m_ioContext,
×
569
    [this, address, longAddress, number, value]()
×
570
    {
571
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
572
      engine.functions[number] = value;
×
573
      if(engine.session) // we're in control
×
574
      {
575
        sendSetEngineFunction(*engine.session, number, value);
×
576

577
        engine.lastCommand = std::chrono::steady_clock::now();
×
578

579
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
580
        {
581
          restartEngineKeepAliveTimer();
×
582
        }
583
      }
584
      else // take control
585
      {
586
        sendGetEngineSession(address, longAddress);
×
587
      }
588
    });
×
589
}
×
590

591
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
592
{
593
  assert(isEventLoopThread());
×
594

595
  boost::asio::post(m_ioContext,
×
596
    [this, deviceNumber, on]()
×
597
    {
598
      if(on)
×
599
      {
600
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
601
      }
602
      else
603
      {
604
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
605
      }
606
    });
×
607
}
×
608

609
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
610
{
611
  assert(isEventLoopThread());
×
612

613
  boost::asio::post(m_ioContext,
×
614
    [this, nodeNumber, eventNumber, on]()
×
615
    {
616
      if(on)
×
617
      {
618
        send(AccessoryOn(nodeNumber, eventNumber));
×
619
      }
620
      else
621
      {
622
        send(AccessoryOff(nodeNumber, eventNumber));
×
623
      }
624
    });
×
625
}
×
626

627
void Kernel::setDccAccessory(uint16_t address, bool secondOutput)
×
628
{
629
  assert(isEventLoopThread());
×
630

631
  boost::asio::post(m_ioContext,
×
632
    [this, address, secondOutput]()
×
633
    {
634
      send(RequestDCCPacket<sizeof(DCC::SetSimpleAccessory) + 1>(DCC::SetSimpleAccessory(address, secondOutput, true), Config::dccAccessoryRepeat));
×
635
      const bool wasEmpty = m_dccAccessoryQueue.empty();
×
636
      m_dccAccessoryQueue.emplace(std::make_pair(
×
637
        std::chrono::steady_clock::now() + m_config.dccAccessorySwitchTime,
×
638
        DCC::SetSimpleAccessory(address, secondOutput, false)
×
639
      ));
640
      if(wasEmpty)
×
641
      {
642
        startDccAccessoryTimer();
×
643
      }
644
    });
×
645
}
×
646

647
void Kernel::setDccAdvancedAccessoryValue(uint16_t address, uint8_t aspect)
×
648
{
649
  assert(isEventLoopThread());
×
650

651
  boost::asio::post(m_ioContext,
×
652
    [this, address, aspect]()
×
653
    {
654
      send(RequestDCCPacket<sizeof(DCC::SetAdvancedAccessoryValue) + 1>(DCC::SetAdvancedAccessoryValue(address, aspect), Config::dccExtRepeat));
×
655
    });
×
656
}
×
657

658
bool Kernel::send(std::vector<uint8_t> message)
×
659
{
660
  assert(isEventLoopThread());
×
661

662
  if(!inRange<size_t>(message.size(), 1, 8))
×
663
  {
664
    return false;
×
665
  }
666

667
  boost::asio::post(m_ioContext,
×
668
    [this, msg=std::move(message)]()
×
669
    {
670
      send(*reinterpret_cast<const Message*>(msg.data()));
×
671
    });
×
672

673
  return true;
×
674
}
675

676
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
677
{
678
  assert(isEventLoopThread());
×
679

680
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
681
  {
682
    return false;
×
683
  }
684

685
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
686

687
  boost::asio::post(m_ioContext,
×
688
    [this, packet=std::move(dccPacket), repeat]()
×
689
    {
690
      switch(packet.size())
×
691
      {
692
        case 3:
×
693
          send(RequestDCCPacket<3>(packet, repeat));
×
694
          break;
×
695

696
        case 4:
×
697
          send(RequestDCCPacket<4>(packet, repeat));
×
698
          break;
×
699

700
        case 5:
×
701
          send(RequestDCCPacket<5>(packet, repeat));
×
702
          break;
×
703

704
        case 6:
×
705
          send(RequestDCCPacket<6>(packet, repeat));
×
706
          break;
×
707

708
        default: [[unlikely]]
×
709
          assert(false);
×
710
          break;
711
      }
712
    });
×
713

714
  return true;
×
715
}
716

717
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
718
{
719
  assert(isEventLoopThread());
×
720
  assert(handler);
×
721
  assert(!m_ioHandler);
×
722
  m_ioHandler = std::move(handler);
×
723
}
×
724

725
void Kernel::send(const Message& message)
×
726
{
727
  assert(isKernelThread());
×
728

729
  if(m_config.debugLogRXTX)
×
730
  {
731
    EventLoop::call(
×
732
      [this, msg=toString(message)]()
×
733
      {
734
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
735
      });
×
736
  }
737

738
  if(auto ec = m_ioHandler->send(message); ec)
×
739
  {
740
    (void)ec; // FIXME: handle error
741
  }
742
}
×
743

744
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
745
{
746
  assert(isKernelThread());
×
747
  const auto key = makeAddressKey(address, longAddress);
×
748
  if(!m_engineGLOCs.contains(key))
×
749
  {
750
    m_engineGLOCs.emplace(key);
×
751
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
752
  }
753
}
×
754

755
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
756
{
757
  assert(isKernelThread());
×
758
  // FIXME: what to do with: serviceMode and soundControlMode?
759
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
760
}
×
761

762
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
763
{
764
  assert(isKernelThread());
×
765

766
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
767

768
  EventLoop::call(
×
769
    [this, session, speed, directionForward]()
×
770
    {
771
      if(onEngineSpeedDirectionChanged) [[likely]]
×
772
      {
773
        onEngineSpeedDirectionChanged(session, speed, directionForward);
×
774
      }
775
    });
×
776
}
×
777

778
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
779
{
780
  assert(isKernelThread());
×
781

782
  send(SetEngineFunction(session, number, value));
×
783

784
  EventLoop::call(
×
785
    [this, session, number, value]()
×
786
    {
787
      if(onEngineFunctionChanged) [[likely]]
×
788
      {
789
        onEngineFunctionChanged(session, number, value);
×
790
      }
791
    });
×
792
}
×
793

794
void Kernel::receiveDFNOx(const SetEngineFunction& message)
×
795
{
796
  assert(isKernelThread());
×
797
  EventLoop::call(
×
798
    [this, message]()
×
799
    {
800
      if(onEngineFunctionChanged) [[likely]]
×
801
      {
802
        onEngineFunctionChanged(message.session, message.number, message.on());
×
803
      }
804
    });
×
805
}
×
806

807
void Kernel::receiveDFUN(const CBUS::SetEngineFunctions& message)
×
808
{
809
  assert(isKernelThread());
×
810
  EventLoop::call(
×
811
    [this, message]()
×
812
    {
813
      if(onEngineFunctionChanged) [[likely]]
×
814
      {
815
        switch(message.range)
×
816
        {
817
          using enum SetEngineFunctions::Range;
818

819
          case F0F4:
×
820
            for(auto fn : message.numbers())
×
821
            {
822
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF0F4&>(message).f(fn));
×
823
            }
824
            break;
×
825

826
          case F5F8:
×
827
            for(auto fn : message.numbers())
×
828
            {
829
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF5F8&>(message).f(fn));
×
830
            }
831
            break;
×
832

833
          case F9F12:
×
834
            for(auto fn : message.numbers())
×
835
            {
836
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF9F12&>(message).f(fn));
×
837
            }
838
            break;
×
839

840
          case F13F20:
×
841
            for(auto fn : message.numbers())
×
842
            {
843
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF13F20&>(message).f(fn));
×
844
            }
845
            break;
×
846

847
          case F21F28:
×
848
            for(auto fn : message.numbers())
×
849
            {
850
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF21F28&>(message).f(fn));
×
851
            }
852
            break;
×
853
        }
854
      }
855
    });
×
856
}
×
857

858
void Kernel::receiveDSPD(const SetEngineSpeedDirection& message)
×
859
{
860
  assert(isKernelThread());
×
861
  EventLoop::call(
×
862
    [this, message]()
×
863
    {
864
      if(onEngineSpeedDirectionChanged) [[likely]]
×
865
      {
866
        onEngineSpeedDirectionChanged(message.session, message.speed(), message.directionForward());
×
867
      }
868
    });
×
869
}
×
870

871
void Kernel::receiveKLOC(const ReleaseEngine& message)
×
872
{
873
  assert(isKernelThread());
×
874
  EventLoop::call(
×
875
    [this, session=message.session]()
×
876
    {
877
      if(onEngineSessionReleased) [[likely]]
×
878
      {
879
        onEngineSessionReleased(session);
×
880
      }
881
    });
×
882
}
×
883

884
void Kernel::receiveShortEvent(uint16_t eventNumber, bool on)
×
885
{
886
  assert(isKernelThread());
×
887
  EventLoop::call(
×
888
    [this, eventNumber, on]()
×
889
    {
890
      if(onShortEvent) [[likely]]
×
891
      {
892
        onShortEvent(eventNumber, on);
×
893
      }
894
    });
×
895
}
×
896

897
void Kernel::receiveLongEvent(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
898
{
899
  assert(isKernelThread());
×
900
  EventLoop::call(
×
901
    [this, nodeNumber, eventNumber, on]()
×
902
    {
903
      if(onLongEvent) [[likely]]
×
904
      {
905
        onLongEvent(nodeNumber, eventNumber, on);
×
906
      }
907
    });
×
908
}
×
909

910
void Kernel::changeState(State value)
×
911
{
912
  assert(isKernelThread());
×
913
  assert(m_state != value);
×
914

915
  m_state = value;
×
916

917
  switch(m_state)
×
918
  {
919
    case State::Initial: [[unlikely]]
×
920
      assert(false);
×
921
      break;
922

923
    case State::QueryNodes:
×
924
      send(QueryNodeNumber());
×
925
      restartInitializationTimer(queryNodeNumberTimeout);
×
926
      break;
×
927

928
    case State::ReadNodeParameters:
×
929
      readNodeParameter();
×
930
      break;
×
931

932
    case State::GetCommandStationStatus:
×
933
      send(RequestCommandStationStatus());
×
934
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
935
      break;
×
936

937
    case State::RequestShortEvents:
×
938
      requestShortEvent();
×
939
      break;
×
940

941
    case State::RequestLongEvents:
×
942
      requestLongEvent();
×
943
      break;
×
944

945
    case State::Started:
×
946
      KernelBase::started();
×
947
      break;
×
948
  }
949
}
×
950

951
void Kernel::readNodeParameter()
×
952
{
953
  assert(m_state == State::ReadNodeParameters);
×
954
  if(m_readNodeParameters.empty())
×
955
  {
956
    nextState();
×
957
    return;
×
958
  }
959
  send(m_readNodeParameters.front());
×
960
  restartInitializationTimer(readNodeParameterTimeout);
×
961
}
962

963
void Kernel::requestShortEvent()
×
964
{
965
  assert(m_state == State::RequestShortEvents);
×
966
  if(m_initializationRequestShortEvents.empty())
×
967
  {
968
    nextState();
×
969
    return;
×
970
  }
971
  send(AccessoryShortRequestEvent(Config::nodeId, m_initializationRequestShortEvents.back()));
×
972
  restartInitializationTimer(requestShortEventTimeout);
×
973
}
974

975
void Kernel::requestLongEvent()
×
976
{
977
  assert(m_state == State::RequestLongEvents);
×
978
  if(m_initializationRequestLongEvents.empty())
×
979
  {
980
    nextState();
×
981
    return;
×
982
  }
983
  send(AccessoryRequestEvent(m_initializationRequestLongEvents.back().first, m_initializationRequestLongEvents.back().second));
×
984
  restartInitializationTimer(requestLongEventTimeout);
×
985
}
986

987
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
988
{
989
  assert(isKernelThread());
×
990

991
  m_initializationTimer.cancel();
×
992

993
  m_initializationTimer.expires_after(timeout);
×
994
  m_initializationTimer.async_wait(
×
995
    [this](std::error_code ec)
×
996
    {
997
      if(ec)
×
998
      {
999
        return;
×
1000
      }
1001

1002
      switch(m_state)
×
1003
      {
1004
        case State::QueryNodes:
×
1005
          nextState();
×
1006
          break;
×
1007

1008
        case State::ReadNodeParameters:
×
1009
          m_readNodeParameters.pop();
×
1010
          readNodeParameter();
×
1011
          break;
×
1012

1013
        case State::GetCommandStationStatus:
×
1014
          nextState();
×
1015
          break;
×
1016

1017
        case State::RequestShortEvents:
×
1018
          m_initializationRequestShortEvents.pop_back();
×
1019
          requestShortEvent();
×
1020
          break;
×
1021

1022
        case State::RequestLongEvents:
×
1023
          m_initializationRequestLongEvents.pop_back();
×
1024
          requestLongEvent();
×
1025
          break;
×
1026

1027
        case State::Initial: [[unlikely]]
×
1028
        case State::Started: [[unlikely]]
×
1029
          assert(false);
×
1030
          break;
1031
      }
1032
    });
1033
}
×
1034

1035
void Kernel::restartEngineKeepAliveTimer()
×
1036
{
1037
  assert(isKernelThread());
×
1038

1039
  m_engineKeepAliveTimer.cancel();
×
1040

1041
  // find first expiring engine:
1042
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
1043
  for(const auto& [_, engine] : m_engines)
×
1044
  {
1045
    if(engine.session && engine.lastCommand < lastUpdate)
×
1046
    {
1047
      lastUpdate = engine.lastCommand;
×
1048
      m_engineKeepAliveSession = *engine.session;
×
1049
    }
1050
  }
1051

1052
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
1053

1054
  if(m_engineKeepAliveTimerActive)
×
1055
  {
1056
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
1057
    m_engineKeepAliveTimer.async_wait(
×
1058
      [this](std::error_code ec)
×
1059
      {
1060
        if(ec)
×
1061
        {
1062
          return;
×
1063
        }
1064

1065
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
1066

1067
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
1068
            [session=m_engineKeepAliveSession](const auto& item)
×
1069
            {
1070
              return item.second.session && *item.second.session == session;
×
1071
            }); it != m_engines.end()) [[likely]]
×
1072
        {
1073
          it->second.lastCommand = std::chrono::steady_clock::now();
×
1074
        }
1075

1076
        restartEngineKeepAliveTimer();
×
1077
      });
1078
  }
1079
}
×
1080

1081
void Kernel::startDccAccessoryTimer()
×
1082
{
1083
  assert(isKernelThread());
×
1084

1085
  if(m_dccAccessoryQueue.empty()) [[unlikely]]
×
1086
  {
1087
    return;
×
1088
  }
1089

1090
  m_dccAccessoryTimer.expires_at(m_dccAccessoryQueue.front().first);
×
1091
  m_dccAccessoryTimer.async_wait(
×
1092
    [this](std::error_code ec)
×
1093
    {
1094
      if(ec)
×
1095
      {
1096
        return;
×
1097
      }
1098

1099
      send(RequestDCCPacket<sizeof(DCC::SetSimpleAccessory) + 1>(m_dccAccessoryQueue.front().second, Config::dccAccessoryRepeat));
×
1100

1101
      m_dccAccessoryQueue.pop();
×
1102

1103
      if(!m_dccAccessoryQueue.empty())
×
1104
      {
1105
        startDccAccessoryTimer();
×
1106
      }
1107
    });
1108
}
1109

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