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

traintastic / traintastic / 23719854638

29 Mar 2026 09:47PM UTC coverage: 25.872% (-0.3%) from 26.14%
23719854638

push

github

reinder
[cbus] added CANCAB node in simulation

0 of 12 new or added lines in 2 files covered. (0.0%)

648 existing lines in 6 files now uncovered.

8256 of 31911 relevant lines covered (25.87%)

180.28 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 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
  , m_dccAccessoryTimer{ioContext()}
×
74
{
75
  assert(isEventLoopThread());
×
76
}
×
77

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

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

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

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

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

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

126
  boost::asio::post(m_ioContext,
×
127
    [this]()
×
128
    {
129
      m_ioHandler->stop();
×
130
    });
×
131

132
  m_ioContext.stop();
×
133

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

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

141
  nextState();
×
142
}
×
143

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

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

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

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

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

182
    case OpCode::KLOC:
×
UNCOV
183
      receiveKLOC(static_cast<const ReleaseEngine&>(message));
×
184
      break;
×
185

186
    case OpCode::DSPD:
×
UNCOV
187
      receiveDSPD(static_cast<const SetEngineSpeedDirection&>(message));
×
188
      break;
×
189

190
    case OpCode::DFNON:
×
191
    case OpCode::DFNOF:
192
      receiveDFNOx(static_cast<const SetEngineFunction&>(message));
×
193
      break;
×
194

195
    case OpCode::DFUN:
×
UNCOV
196
      receiveDFUN(static_cast<const SetEngineFunctions&>(message));
×
197
      break;
×
198

199
    case OpCode::ASON:
×
200
    {
201
      const auto& ason = static_cast<const AccessoryShortOn&>(message);
×
UNCOV
202
      EventLoop::call(
×
203
        [this, eventNumber=ason.deviceNumber()]()
×
204
        {
205
          if(onShortEvent) [[likely]]
×
206
          {
UNCOV
207
            onShortEvent(eventNumber, true);
×
208
          }
UNCOV
209
        });
×
210
      break;
×
211
    }
UNCOV
212
    case OpCode::ASOF:
×
213
    {
214
      const auto& asof = static_cast<const AccessoryShortOff&>(message);
×
UNCOV
215
      EventLoop::call(
×
216
        [this, eventNumber=asof.deviceNumber()]()
×
217
        {
218
          if(onShortEvent) [[likely]]
×
219
          {
220
            onShortEvent(eventNumber, false);
×
221
          }
UNCOV
222
        });
×
223
      break;
×
224
    }
225
    case OpCode::ERR:
×
226
    {
227
      switch(static_cast<const CommandStationErrorMessage&>(message).errorCode)
×
228
      {
229
        using enum DCCErr;
230

UNCOV
231
        case LocoStackFull:
×
232
        {
UNCOV
233
          const auto& err = static_cast<const CommandStationLocoStackFullError&>(message);
×
234
          const auto key = makeAddressKey(err.address(), err.isLongAddress());
×
UNCOV
235
          if(m_engineGLOCs.contains(key))
×
236
          {
UNCOV
237
            m_engineGLOCs.erase(key);
×
238
            // FIXME: log error
239
          }
240
          break;
×
241
        }
242
        case SessionCancelled:
×
UNCOV
243
          if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
244
            [session=static_cast<const CommandStationSessionCancelled&>(message).session()](const auto& item)
×
245
            {
246
              return item.second.session && *item.second.session == session;
×
UNCOV
247
            }); it != m_engines.end())
×
248
          {
UNCOV
249
            it->second.session = std::nullopt;
×
250

UNCOV
251
            if(m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *it->second.session)
×
252
            {
UNCOV
253
              restartEngineKeepAliveTimer();
×
254
            }
255

256
            EventLoop::call(
×
UNCOV
257
              [this, key=it->first]()
×
258
              {
UNCOV
259
                if(onEngineSessionCancelled) [[likely]]
×
260
                {
UNCOV
261
                  onEngineSessionCancelled(key & 0x3FFF, (key & 0xC000) == 0xC000);
×
262
                }
263
              });
×
264
          }
UNCOV
265
          break;
×
266

UNCOV
267
        case LocoAddressTaken:
×
268
        case SessionNotPresent:
269
        case ConsistEmpty:
270
        case LocoNotFound:
271
        case CANBusError:
272
        case InvalidRequest:
273
          break;
×
274
      }
275
      break;
×
276
    }
277
    case OpCode::ACON:
×
278
    {
279
      const auto& acon = static_cast<const AccessoryOn&>(message);
×
UNCOV
280
      EventLoop::call(
×
281
        [this, nodeNumber=acon.nodeNumber(), eventNumber=acon.eventNumber()]()
×
282
        {
283
          if(onLongEvent) [[likely]]
×
284
          {
UNCOV
285
            onLongEvent(nodeNumber, eventNumber, true);
×
286
          }
287
        });
×
UNCOV
288
      break;
×
289
    }
290
    case OpCode::ACOF:
×
291
    {
292
      const auto& acof = static_cast<const AccessoryOff&>(message);
×
UNCOV
293
      EventLoop::call(
×
294
        [this, nodeNumber=acof.nodeNumber(), eventNumber=acof.eventNumber()]()
×
295
        {
UNCOV
296
          if(onLongEvent) [[likely]]
×
297
          {
298
            onLongEvent(nodeNumber, eventNumber, false);
×
299
          }
300
        });
×
UNCOV
301
      break;
×
302
    }
UNCOV
303
    case OpCode::PARAN:
×
304
      if(m_state == State::ReadNodeParameters && !m_readNodeParameters.empty())
×
305
      {
306
        const auto& rqnpn = m_readNodeParameters.front();
×
UNCOV
307
        const auto& paran = static_cast<const NodeParameterResponse&>(message);
×
308

309
        if(rqnpn.nodeNumber() == paran.nodeNumber() && rqnpn.parameter == paran.parameter)
×
310
        {
311
          m_readNodeParameters.pop();
×
312
          m_initializationTimer.cancel();
×
313

314
          EventLoop::call(
×
UNCOV
315
            [this, canId, paran]()
×
316
            {
UNCOV
317
              if(onNodeParameterResponse) [[likely]]
×
318
              {
319
                onNodeParameterResponse(canId, paran.nodeNumber(), paran.parameter, paran.value);
×
320
              }
UNCOV
321
            });
×
322

323
          readNodeParameter();
×
324
        }
325
      }
UNCOV
326
      break;
×
327

UNCOV
328
    case OpCode::PNN:
×
329
      if(m_state == State::QueryNodes)
×
330
      {
331
        restartInitializationTimer(queryNodeNumberTimeout);
×
332

333
        const auto& pnn = static_cast<const PresenceOfNode&>(message);
×
334

335
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::VersionMajor));
×
336
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::VersionMinor));
×
337
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::BetaReleaseCode));
×
338

339
        EventLoop::call(
×
UNCOV
340
          [this, canId, pnn]()
×
341
          {
UNCOV
342
            if(onPresenceOfNode) [[likely]]
×
343
            {
344
              onPresenceOfNode(canId, pnn.nodeNumber(), pnn.manufacturerId, pnn.moduleId, pnn.flimMode(), pnn.supportsServiceDiscovery());
×
345
            }
346
          });
×
347
      }
UNCOV
348
      break;
×
349

UNCOV
350
    case OpCode::PLOC:
×
351
    {
UNCOV
352
      const auto& ploc = static_cast<const EngineReport&>(message);
×
353

354
      EventLoop::call(
×
UNCOV
355
        [this, ploc]()
×
356
        {
UNCOV
357
          if(onEngineSessionAcquire) [[likely]]
×
358
          {
UNCOV
359
            onEngineSessionAcquire(ploc.session, ploc.address(), ploc.isLongAddress());
×
360
          }
UNCOV
361
        });
×
362

363
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
UNCOV
364
      if(m_engineGLOCs.contains(key))
×
365
      {
366
        m_engineGLOCs.erase(key);
×
367

368
        if(auto it = m_engines.find(key); it != m_engines.end())
×
369
        {
UNCOV
370
          auto& engine = it->second;
×
371
          engine.session = ploc.session;
×
372

UNCOV
373
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
374
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
375

376
          for(const auto& [number, value] : engine.functions)
×
377
          {
UNCOV
378
            sendSetEngineFunction(ploc.session, number, value);
×
379
          }
380

381
          engine.lastCommand = std::chrono::steady_clock::now();
×
382

383
          if(!m_engineKeepAliveTimerActive)
×
384
          {
385
            restartEngineKeepAliveTimer();
×
386
          }
387
        }
388
        else // we're no longer in need of control (rare but possible)
389
        {
390
          send(ReleaseEngine(ploc.session));
×
391
        }
392
      }
393
      break;
×
394
    }
395
    case OpCode::STAT:
×
UNCOV
396
      if(m_state == State::GetCommandStationStatus)
×
397
      {
UNCOV
398
        m_initializationTimer.cancel();
×
399
        nextState();
×
400
      }
UNCOV
401
      break;
×
402

UNCOV
403
    default:
×
404
      break;
×
405
  }
406
}
×
407

UNCOV
408
void Kernel::trackOff()
×
409
{
UNCOV
410
  assert(isEventLoopThread());
×
411

UNCOV
412
  boost::asio::post(m_ioContext,
×
413
    [this]()
×
414
    {
UNCOV
415
      if(m_trackOn)
×
416
      {
417
        send(RequestTrackOff());
×
418
      }
UNCOV
419
    });
×
420
}
×
421

422
void Kernel::trackOn()
×
423
{
424
  assert(isEventLoopThread());
×
425

426
  boost::asio::post(m_ioContext,
×
427
    [this]()
×
428
    {
429
      if(!m_trackOn)
×
430
      {
431
        send(RequestTrackOn());
×
432
      }
433
    });
×
UNCOV
434
}
×
435

UNCOV
436
void Kernel::requestEmergencyStop()
×
437
{
UNCOV
438
  assert(isEventLoopThread());
×
439

UNCOV
440
  boost::asio::post(m_ioContext,
×
441
    [this]()
×
442
    {
443
      send(RequestEmergencyStop());
×
UNCOV
444
    });
×
445
}
×
446

447
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
448
{
UNCOV
449
  assert(isEventLoopThread());
×
450

UNCOV
451
  const uint8_t speed = eStop ? 1 : (speedStep > 0 ? speedStep + 1 : 0);
×
452

UNCOV
453
  boost::asio::post(m_ioContext,
×
454
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
455
    {
UNCOV
456
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
457
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
UNCOV
458
      engine.speedSteps = speedSteps;
×
459
      engine.speed = speed;
×
UNCOV
460
      engine.directionForward = directionForward;
×
461

462
      if(engine.session) // we're in control
×
463
      {
464
        if(speedStepsChanged)
×
465
        {
466
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
467
        }
468
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
469

470
        engine.lastCommand = std::chrono::steady_clock::now();
×
471

472
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
473
        {
474
          restartEngineKeepAliveTimer();
×
475
        }
476
      }
477
      else // take control
478
      {
479
        sendGetEngineSession(address, longAddress);
×
480
      }
481
    });
×
482
}
×
483

484
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
485
{
486
  assert(isEventLoopThread());
×
487

488
  boost::asio::post(m_ioContext,
×
489
    [this, address, longAddress, number, value]()
×
490
    {
491
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
UNCOV
492
      engine.functions[number] = value;
×
493
      if(engine.session) // we're in control
×
494
      {
UNCOV
495
        sendSetEngineFunction(*engine.session, number, value);
×
496

497
        engine.lastCommand = std::chrono::steady_clock::now();
×
498

499
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
500
        {
UNCOV
501
          restartEngineKeepAliveTimer();
×
502
        }
503
      }
504
      else // take control
505
      {
506
        sendGetEngineSession(address, longAddress);
×
507
      }
UNCOV
508
    });
×
509
}
×
510

511
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
512
{
UNCOV
513
  assert(isEventLoopThread());
×
514

515
  boost::asio::post(m_ioContext,
×
UNCOV
516
    [this, deviceNumber, on]()
×
517
    {
518
      if(on)
×
519
      {
520
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
521
      }
522
      else
523
      {
524
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
525
      }
UNCOV
526
    });
×
527
}
×
528

529
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
530
{
531
  assert(isEventLoopThread());
×
532

533
  boost::asio::post(m_ioContext,
×
UNCOV
534
    [this, nodeNumber, eventNumber, on]()
×
535
    {
UNCOV
536
      if(on)
×
537
      {
538
        send(AccessoryOn(nodeNumber, eventNumber));
×
539
      }
540
      else
541
      {
542
        send(AccessoryOff(nodeNumber, eventNumber));
×
543
      }
544
    });
×
545
}
×
546

547
void Kernel::setDccAccessory(uint16_t address, bool secondOutput)
×
548
{
549
  assert(isEventLoopThread());
×
550

551
  boost::asio::post(m_ioContext,
×
UNCOV
552
    [this, address, secondOutput]()
×
553
    {
UNCOV
554
      send(RequestDCCPacket<sizeof(DCC::SetSimpleAccessory) + 1>(DCC::SetSimpleAccessory(address, secondOutput, true), Config::dccAccessoryRepeat));
×
555
      const bool wasEmpty = m_dccAccessoryQueue.empty();
×
UNCOV
556
      m_dccAccessoryQueue.emplace(std::make_pair(
×
557
        std::chrono::steady_clock::now() + m_config.dccAccessorySwitchTime,
×
UNCOV
558
        DCC::SetSimpleAccessory(address, secondOutput, false)
×
559
      ));
560
      if(wasEmpty)
×
561
      {
UNCOV
562
        startDccAccessoryTimer();
×
563
      }
564
    });
×
UNCOV
565
}
×
566

UNCOV
567
void Kernel::setDccAdvancedAccessoryValue(uint16_t address, uint8_t aspect)
×
568
{
569
  assert(isEventLoopThread());
×
570

571
  boost::asio::post(m_ioContext,
×
UNCOV
572
    [this, address, aspect]()
×
573
    {
UNCOV
574
      send(RequestDCCPacket<sizeof(DCC::SetAdvancedAccessoryValue) + 1>(DCC::SetAdvancedAccessoryValue(address, aspect), Config::dccExtRepeat));
×
575
    });
×
UNCOV
576
}
×
577

578
bool Kernel::send(std::vector<uint8_t> message)
×
579
{
580
  assert(isEventLoopThread());
×
581

UNCOV
582
  if(!inRange<size_t>(message.size(), 1, 8))
×
583
  {
UNCOV
584
    return false;
×
585
  }
586

587
  boost::asio::post(m_ioContext,
×
UNCOV
588
    [this, msg=std::move(message)]()
×
589
    {
590
      send(*reinterpret_cast<const Message*>(msg.data()));
×
591
    });
×
592

593
  return true;
×
594
}
595

UNCOV
596
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
597
{
598
  assert(isEventLoopThread());
×
599

UNCOV
600
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
601
  {
602
    return false;
×
603
  }
604

605
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
606

607
  boost::asio::post(m_ioContext,
×
UNCOV
608
    [this, packet=std::move(dccPacket), repeat]()
×
609
    {
610
      switch(packet.size())
×
611
      {
612
        case 3:
×
613
          send(RequestDCCPacket<3>(packet, repeat));
×
614
          break;
×
615

616
        case 4:
×
UNCOV
617
          send(RequestDCCPacket<4>(packet, repeat));
×
618
          break;
×
619

620
        case 5:
×
UNCOV
621
          send(RequestDCCPacket<5>(packet, repeat));
×
622
          break;
×
623

624
        case 6:
×
625
          send(RequestDCCPacket<6>(packet, repeat));
×
UNCOV
626
          break;
×
627

628
        default: [[unlikely]]
×
UNCOV
629
          assert(false);
×
630
          break;
631
      }
UNCOV
632
    });
×
633

UNCOV
634
  return true;
×
635
}
636

637
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
638
{
639
  assert(isEventLoopThread());
×
640
  assert(handler);
×
641
  assert(!m_ioHandler);
×
UNCOV
642
  m_ioHandler = std::move(handler);
×
643
}
×
644

UNCOV
645
void Kernel::send(const Message& message)
×
646
{
UNCOV
647
  assert(isKernelThread());
×
648

UNCOV
649
  if(m_config.debugLogRXTX)
×
650
  {
UNCOV
651
    EventLoop::call(
×
652
      [this, msg=toString(message)]()
×
653
      {
UNCOV
654
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
655
      });
×
656
  }
657

658
  if(auto ec = m_ioHandler->send(message); ec)
×
659
  {
660
    (void)ec; // FIXME: handle error
661
  }
UNCOV
662
}
×
663

664
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
665
{
666
  assert(isKernelThread());
×
UNCOV
667
  const auto key = makeAddressKey(address, longAddress);
×
UNCOV
668
  if(!m_engineGLOCs.contains(key))
×
669
  {
670
    m_engineGLOCs.emplace(key);
×
UNCOV
671
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
672
  }
UNCOV
673
}
×
674

UNCOV
675
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
676
{
677
  assert(isKernelThread());
×
678
  // FIXME: what to do with: serviceMode and soundControlMode?
679
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
UNCOV
680
}
×
681

UNCOV
682
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
683
{
684
  assert(isKernelThread());
×
685

UNCOV
686
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
687

688
  EventLoop::call(
×
689
    [this, session, speed, directionForward]()
×
690
    {
UNCOV
691
      if(onEngineSpeedDirectionChanged) [[likely]]
×
692
      {
693
        onEngineSpeedDirectionChanged(session, speed, directionForward);
×
694
      }
UNCOV
695
    });
×
696
}
×
697

698
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
699
{
UNCOV
700
  assert(isKernelThread());
×
701

702
  send(SetEngineFunction(session, number, value));
×
703

UNCOV
704
  EventLoop::call(
×
705
    [this, session, number, value]()
×
706
    {
707
      if(onEngineFunctionChanged) [[likely]]
×
708
      {
709
        onEngineFunctionChanged(session, number, value);
×
710
      }
UNCOV
711
    });
×
712
}
×
713

UNCOV
714
void Kernel::receiveDFNOx(const SetEngineFunction& message)
×
715
{
716
  assert(isKernelThread());
×
UNCOV
717
  EventLoop::call(
×
UNCOV
718
    [this, message]()
×
719
    {
UNCOV
720
      if(onEngineFunctionChanged) [[likely]]
×
721
      {
UNCOV
722
        onEngineFunctionChanged(message.session, message.number, message.on());
×
723
      }
UNCOV
724
    });
×
725
}
×
726

727
void Kernel::receiveDFUN(const CBUS::SetEngineFunctions& message)
×
728
{
729
  assert(isKernelThread());
×
UNCOV
730
  EventLoop::call(
×
731
    [this, message]()
×
732
    {
UNCOV
733
      if(onEngineFunctionChanged) [[likely]]
×
734
      {
UNCOV
735
        switch(message.range)
×
736
        {
737
          using enum SetEngineFunctions::Range;
738

UNCOV
739
          case F0F4:
×
740
            for(auto fn : message.numbers())
×
741
            {
742
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF0F4&>(message).f(fn));
×
743
            }
UNCOV
744
            break;
×
745

746
          case F5F8:
×
747
            for(auto fn : message.numbers())
×
748
            {
749
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF5F8&>(message).f(fn));
×
750
            }
751
            break;
×
752

UNCOV
753
          case F9F12:
×
UNCOV
754
            for(auto fn : message.numbers())
×
755
            {
UNCOV
756
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF9F12&>(message).f(fn));
×
757
            }
UNCOV
758
            break;
×
759

UNCOV
760
          case F13F20:
×
761
            for(auto fn : message.numbers())
×
762
            {
UNCOV
763
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF13F20&>(message).f(fn));
×
764
            }
765
            break;
×
766

767
          case F21F28:
×
UNCOV
768
            for(auto fn : message.numbers())
×
769
            {
770
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF21F28&>(message).f(fn));
×
771
            }
UNCOV
772
            break;
×
773
        }
774
      }
UNCOV
775
    });
×
776
}
×
777

778
void Kernel::receiveDSPD(const SetEngineSpeedDirection& message)
×
779
{
780
  assert(isKernelThread());
×
UNCOV
781
  EventLoop::call(
×
782
    [this, message]()
×
783
    {
784
      if(onEngineSpeedDirectionChanged) [[likely]]
×
785
      {
UNCOV
786
        onEngineSpeedDirectionChanged(message.session, message.speed(), message.directionForward());
×
787
      }
UNCOV
788
    });
×
789
}
×
790

UNCOV
791
void Kernel::receiveKLOC(const ReleaseEngine& message)
×
792
{
793
  assert(isKernelThread());
×
UNCOV
794
  EventLoop::call(
×
795
    [this, session=message.session]()
×
796
    {
UNCOV
797
      if(onEngineSessionReleased) [[likely]]
×
798
      {
UNCOV
799
        onEngineSessionReleased(session);
×
800
      }
801
    });
×
UNCOV
802
}
×
803

UNCOV
804
void Kernel::changeState(State value)
×
805
{
UNCOV
806
  assert(isKernelThread());
×
807
  assert(m_state != value);
×
808

809
  m_state = value;
×
810

UNCOV
811
  switch(m_state)
×
812
  {
813
    case State::Initial: [[unlikely]]
×
814
      assert(false);
×
815
      break;
816

UNCOV
817
    case State::QueryNodes:
×
818
      send(QueryNodeNumber());
×
UNCOV
819
      restartInitializationTimer(queryNodeNumberTimeout);
×
UNCOV
820
      break;
×
821

UNCOV
822
    case State::ReadNodeParameters:
×
823
      readNodeParameter();
×
UNCOV
824
      break;
×
825

UNCOV
826
    case State::GetCommandStationStatus:
×
827
      send(RequestCommandStationStatus());
×
UNCOV
828
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
UNCOV
829
      break;
×
830

UNCOV
831
    case State::Started:
×
UNCOV
832
      KernelBase::started();
×
UNCOV
833
      break;
×
834
  }
UNCOV
835
}
×
836

UNCOV
837
void Kernel::readNodeParameter()
×
838
{
UNCOV
839
  assert(m_state == State::ReadNodeParameters);
×
UNCOV
840
  if(m_readNodeParameters.empty())
×
841
  {
UNCOV
842
    nextState();
×
UNCOV
843
    return;
×
844
  }
UNCOV
845
  send(m_readNodeParameters.front());
×
UNCOV
846
  restartInitializationTimer(readNodeParameterTimeout);
×
847
}
848

UNCOV
849
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
850
{
UNCOV
851
  assert(isKernelThread());
×
852

UNCOV
853
  m_initializationTimer.cancel();
×
854

UNCOV
855
  m_initializationTimer.expires_after(timeout);
×
UNCOV
856
  m_initializationTimer.async_wait(
×
UNCOV
857
    [this](std::error_code ec)
×
858
    {
UNCOV
859
      if(ec)
×
860
      {
UNCOV
861
        return;
×
862
      }
863

UNCOV
864
      switch(m_state)
×
865
      {
UNCOV
866
        case State::QueryNodes:
×
UNCOV
867
          nextState();
×
UNCOV
868
          break;
×
869

UNCOV
870
        case State::ReadNodeParameters:
×
UNCOV
871
          m_readNodeParameters.pop();
×
UNCOV
872
          readNodeParameter();
×
UNCOV
873
          break;
×
874

UNCOV
875
        case State::GetCommandStationStatus:
×
UNCOV
876
          nextState();
×
UNCOV
877
          break;
×
878

UNCOV
879
        case State::Initial: [[unlikely]]
×
UNCOV
880
        case State::Started: [[unlikely]]
×
UNCOV
881
          assert(false);
×
882
          break;
883
      }
884
    });
UNCOV
885
}
×
886

UNCOV
887
void Kernel::restartEngineKeepAliveTimer()
×
888
{
UNCOV
889
  assert(isKernelThread());
×
890

UNCOV
891
  m_engineKeepAliveTimer.cancel();
×
892

893
  // find first expiring engine:
UNCOV
894
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
UNCOV
895
  for(const auto& [_, engine] : m_engines)
×
896
  {
UNCOV
897
    if(engine.session && engine.lastCommand < lastUpdate)
×
898
    {
UNCOV
899
      lastUpdate = engine.lastCommand;
×
UNCOV
900
      m_engineKeepAliveSession = *engine.session;
×
901
    }
902
  }
903

UNCOV
904
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
905

UNCOV
906
  if(m_engineKeepAliveTimerActive)
×
907
  {
UNCOV
908
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
UNCOV
909
    m_engineKeepAliveTimer.async_wait(
×
UNCOV
910
      [this](std::error_code ec)
×
911
      {
UNCOV
912
        if(ec)
×
913
        {
UNCOV
914
          return;
×
915
        }
916

UNCOV
917
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
918

UNCOV
919
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
UNCOV
920
            [session=m_engineKeepAliveSession](const auto& item)
×
921
            {
UNCOV
922
              return item.second.session && *item.second.session == session;
×
UNCOV
923
            }); it != m_engines.end()) [[likely]]
×
924
        {
UNCOV
925
          it->second.lastCommand = std::chrono::steady_clock::now();
×
926
        }
927

UNCOV
928
        restartEngineKeepAliveTimer();
×
929
      });
930
  }
UNCOV
931
}
×
932

UNCOV
933
void Kernel::startDccAccessoryTimer()
×
934
{
UNCOV
935
  assert(isKernelThread());
×
936

UNCOV
937
  if(m_dccAccessoryQueue.empty()) [[unlikely]]
×
938
  {
UNCOV
939
    return;
×
940
  }
941

UNCOV
942
  m_dccAccessoryTimer.expires_at(m_dccAccessoryQueue.front().first);
×
UNCOV
943
  m_dccAccessoryTimer.async_wait(
×
UNCOV
944
    [this](std::error_code ec)
×
945
    {
UNCOV
946
      if(ec)
×
947
      {
UNCOV
948
        return;
×
949
      }
950

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

UNCOV
953
      m_dccAccessoryQueue.pop();
×
954

UNCOV
955
      if(!m_dccAccessoryQueue.empty())
×
956
      {
UNCOV
957
        startDccAccessoryTimer();
×
958
      }
959
    });
960
}
961

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