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

traintastic / traintastic / 23769213849

30 Mar 2026 09:43PM UTC coverage: 25.799% (-0.07%) from 25.87%
23769213849

push

github

reinder
[cbus] request all used short/long event during interface initialization

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

11 existing lines in 3 files now uncovered.

8256 of 32001 relevant lines covered (25.8%)

179.77 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

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

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

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

UNCOV
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:
×
NEW
213
      receiveShortEvent(static_cast<const AccessoryShortOn&>(message).deviceNumber(), true);
×
NEW
214
      break;
×
215

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

NEW
220
    case OpCode::ARSON:
×
221
    {
NEW
222
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
NEW
223
      receiveShortEvent(eventNumber, true);
×
NEW
224
      if(m_state == State::RequestShortEvents &&
×
NEW
225
          m_initializationRequestShortEvents.back() == eventNumber)
×
226
      {
NEW
227
        m_initializationRequestShortEvents.pop_back();
×
NEW
228
        requestShortEvent();
×
229
      }
UNCOV
230
      break;
×
231
    }
NEW
232
    case OpCode::ARSOF:
×
233
    {
NEW
234
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
NEW
235
      receiveShortEvent(eventNumber, false);
×
NEW
236
      if(m_state == State::RequestShortEvents &&
×
NEW
237
          m_initializationRequestShortEvents.back() == eventNumber)
×
238
      {
NEW
239
        m_initializationRequestShortEvents.pop_back();
×
NEW
240
        requestShortEvent();
×
241
      }
UNCOV
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);
×
NEW
299
      receiveLongEvent(acon.nodeNumber(), acon.eventNumber(), true);
×
300
      break;
×
301
    }
302
    case OpCode::ACOF:
×
303
    {
304
      const auto& acof = static_cast<const AccessoryOff&>(message);
×
NEW
305
      receiveLongEvent(acof.nodeNumber(), acof.eventNumber(), false);
×
NEW
306
      break;
×
307
    }
NEW
308
    case OpCode::ARON:
×
309
    {
NEW
310
      const auto& aron = static_cast<const AccessoryResponseEventOn&>(message);
×
NEW
311
      receiveLongEvent(aron.nodeNumber(), aron.eventNumber(), true);
×
NEW
312
      if(m_state == State::RequestShortEvents &&
×
NEW
313
          m_initializationRequestLongEvents.back().first == aron.nodeNumber() &&
×
NEW
314
          m_initializationRequestLongEvents.back().second == aron.eventNumber())
×
315
      {
NEW
316
        m_initializationRequestLongEvents.pop_back();
×
NEW
317
        requestLongEvent();
×
318
      }
NEW
319
      break;
×
320
    }
NEW
321
    case OpCode::AROF:
×
322
    {
NEW
323
      const auto& arof = static_cast<const AccessoryResponseEventOff&>(message);
×
NEW
324
      receiveLongEvent(arof.nodeNumber(), arof.eventNumber(), false);
×
NEW
325
      if(m_state == State::RequestShortEvents &&
×
NEW
326
          m_initializationRequestLongEvents.back().first == arof.nodeNumber() &&
×
NEW
327
          m_initializationRequestLongEvents.back().second == arof.eventNumber())
×
328
      {
NEW
329
        m_initializationRequestLongEvents.pop_back();
×
NEW
330
        requestLongEvent();
×
331
      }
UNCOV
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

439
void Kernel::trackOff()
×
440
{
441
  assert(isEventLoopThread());
×
442

443
  boost::asio::post(m_ioContext,
×
444
    [this]()
×
445
    {
446
      if(m_trackOn)
×
447
      {
448
        send(RequestTrackOff());
×
449
      }
450
    });
×
451
}
×
452

453
void Kernel::trackOn()
×
454
{
455
  assert(isEventLoopThread());
×
456

457
  boost::asio::post(m_ioContext,
×
458
    [this]()
×
459
    {
460
      if(!m_trackOn)
×
461
      {
462
        send(RequestTrackOn());
×
463
      }
464
    });
×
465
}
×
466

467
void Kernel::requestEmergencyStop()
×
468
{
469
  assert(isEventLoopThread());
×
470

471
  boost::asio::post(m_ioContext,
×
472
    [this]()
×
473
    {
474
      send(RequestEmergencyStop());
×
475
    });
×
476
}
×
477

478
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
479
{
480
  assert(isEventLoopThread());
×
481

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

484
  boost::asio::post(m_ioContext,
×
485
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
486
    {
487
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
488
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
489
      engine.speedSteps = speedSteps;
×
490
      engine.speed = speed;
×
491
      engine.directionForward = directionForward;
×
492

493
      if(engine.session) // we're in control
×
494
      {
495
        if(speedStepsChanged)
×
496
        {
497
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
498
        }
499
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
500

501
        engine.lastCommand = std::chrono::steady_clock::now();
×
502

503
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
504
        {
505
          restartEngineKeepAliveTimer();
×
506
        }
507
      }
508
      else // take control
509
      {
510
        sendGetEngineSession(address, longAddress);
×
511
      }
512
    });
×
513
}
×
514

515
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
516
{
517
  assert(isEventLoopThread());
×
518

519
  boost::asio::post(m_ioContext,
×
520
    [this, address, longAddress, number, value]()
×
521
    {
522
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
523
      engine.functions[number] = value;
×
524
      if(engine.session) // we're in control
×
525
      {
526
        sendSetEngineFunction(*engine.session, number, value);
×
527

528
        engine.lastCommand = std::chrono::steady_clock::now();
×
529

530
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
531
        {
532
          restartEngineKeepAliveTimer();
×
533
        }
534
      }
535
      else // take control
536
      {
537
        sendGetEngineSession(address, longAddress);
×
538
      }
539
    });
×
540
}
×
541

542
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
543
{
544
  assert(isEventLoopThread());
×
545

546
  boost::asio::post(m_ioContext,
×
547
    [this, deviceNumber, on]()
×
548
    {
549
      if(on)
×
550
      {
551
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
552
      }
553
      else
554
      {
555
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
556
      }
557
    });
×
558
}
×
559

560
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
561
{
562
  assert(isEventLoopThread());
×
563

564
  boost::asio::post(m_ioContext,
×
565
    [this, nodeNumber, eventNumber, on]()
×
566
    {
567
      if(on)
×
568
      {
569
        send(AccessoryOn(nodeNumber, eventNumber));
×
570
      }
571
      else
572
      {
573
        send(AccessoryOff(nodeNumber, eventNumber));
×
574
      }
575
    });
×
576
}
×
577

578
void Kernel::setDccAccessory(uint16_t address, bool secondOutput)
×
579
{
580
  assert(isEventLoopThread());
×
581

582
  boost::asio::post(m_ioContext,
×
583
    [this, address, secondOutput]()
×
584
    {
585
      send(RequestDCCPacket<sizeof(DCC::SetSimpleAccessory) + 1>(DCC::SetSimpleAccessory(address, secondOutput, true), Config::dccAccessoryRepeat));
×
586
      const bool wasEmpty = m_dccAccessoryQueue.empty();
×
587
      m_dccAccessoryQueue.emplace(std::make_pair(
×
588
        std::chrono::steady_clock::now() + m_config.dccAccessorySwitchTime,
×
589
        DCC::SetSimpleAccessory(address, secondOutput, false)
×
590
      ));
591
      if(wasEmpty)
×
592
      {
593
        startDccAccessoryTimer();
×
594
      }
595
    });
×
596
}
×
597

598
void Kernel::setDccAdvancedAccessoryValue(uint16_t address, uint8_t aspect)
×
599
{
600
  assert(isEventLoopThread());
×
601

602
  boost::asio::post(m_ioContext,
×
603
    [this, address, aspect]()
×
604
    {
605
      send(RequestDCCPacket<sizeof(DCC::SetAdvancedAccessoryValue) + 1>(DCC::SetAdvancedAccessoryValue(address, aspect), Config::dccExtRepeat));
×
606
    });
×
607
}
×
608

609
bool Kernel::send(std::vector<uint8_t> message)
×
610
{
611
  assert(isEventLoopThread());
×
612

613
  if(!inRange<size_t>(message.size(), 1, 8))
×
614
  {
615
    return false;
×
616
  }
617

618
  boost::asio::post(m_ioContext,
×
619
    [this, msg=std::move(message)]()
×
620
    {
621
      send(*reinterpret_cast<const Message*>(msg.data()));
×
622
    });
×
623

624
  return true;
×
625
}
626

627
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
628
{
629
  assert(isEventLoopThread());
×
630

631
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
632
  {
633
    return false;
×
634
  }
635

636
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
637

638
  boost::asio::post(m_ioContext,
×
639
    [this, packet=std::move(dccPacket), repeat]()
×
640
    {
641
      switch(packet.size())
×
642
      {
643
        case 3:
×
644
          send(RequestDCCPacket<3>(packet, repeat));
×
645
          break;
×
646

647
        case 4:
×
648
          send(RequestDCCPacket<4>(packet, repeat));
×
649
          break;
×
650

651
        case 5:
×
652
          send(RequestDCCPacket<5>(packet, repeat));
×
653
          break;
×
654

655
        case 6:
×
656
          send(RequestDCCPacket<6>(packet, repeat));
×
657
          break;
×
658

659
        default: [[unlikely]]
×
660
          assert(false);
×
661
          break;
662
      }
663
    });
×
664

665
  return true;
×
666
}
667

668
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
669
{
670
  assert(isEventLoopThread());
×
671
  assert(handler);
×
672
  assert(!m_ioHandler);
×
673
  m_ioHandler = std::move(handler);
×
674
}
×
675

676
void Kernel::send(const Message& message)
×
677
{
678
  assert(isKernelThread());
×
679

680
  if(m_config.debugLogRXTX)
×
681
  {
682
    EventLoop::call(
×
683
      [this, msg=toString(message)]()
×
684
      {
685
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
686
      });
×
687
  }
688

689
  if(auto ec = m_ioHandler->send(message); ec)
×
690
  {
691
    (void)ec; // FIXME: handle error
692
  }
693
}
×
694

695
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
696
{
697
  assert(isKernelThread());
×
698
  const auto key = makeAddressKey(address, longAddress);
×
699
  if(!m_engineGLOCs.contains(key))
×
700
  {
701
    m_engineGLOCs.emplace(key);
×
702
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
703
  }
704
}
×
705

706
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
707
{
708
  assert(isKernelThread());
×
709
  // FIXME: what to do with: serviceMode and soundControlMode?
710
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
711
}
×
712

713
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
714
{
715
  assert(isKernelThread());
×
716

717
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
718

719
  EventLoop::call(
×
720
    [this, session, speed, directionForward]()
×
721
    {
722
      if(onEngineSpeedDirectionChanged) [[likely]]
×
723
      {
724
        onEngineSpeedDirectionChanged(session, speed, directionForward);
×
725
      }
726
    });
×
727
}
×
728

729
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
730
{
731
  assert(isKernelThread());
×
732

733
  send(SetEngineFunction(session, number, value));
×
734

735
  EventLoop::call(
×
736
    [this, session, number, value]()
×
737
    {
738
      if(onEngineFunctionChanged) [[likely]]
×
739
      {
740
        onEngineFunctionChanged(session, number, value);
×
741
      }
742
    });
×
743
}
×
744

745
void Kernel::receiveDFNOx(const SetEngineFunction& message)
×
746
{
747
  assert(isKernelThread());
×
748
  EventLoop::call(
×
749
    [this, message]()
×
750
    {
751
      if(onEngineFunctionChanged) [[likely]]
×
752
      {
753
        onEngineFunctionChanged(message.session, message.number, message.on());
×
754
      }
755
    });
×
756
}
×
757

758
void Kernel::receiveDFUN(const CBUS::SetEngineFunctions& message)
×
759
{
760
  assert(isKernelThread());
×
761
  EventLoop::call(
×
762
    [this, message]()
×
763
    {
764
      if(onEngineFunctionChanged) [[likely]]
×
765
      {
766
        switch(message.range)
×
767
        {
768
          using enum SetEngineFunctions::Range;
769

770
          case F0F4:
×
771
            for(auto fn : message.numbers())
×
772
            {
773
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF0F4&>(message).f(fn));
×
774
            }
775
            break;
×
776

777
          case F5F8:
×
778
            for(auto fn : message.numbers())
×
779
            {
780
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF5F8&>(message).f(fn));
×
781
            }
782
            break;
×
783

784
          case F9F12:
×
785
            for(auto fn : message.numbers())
×
786
            {
787
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF9F12&>(message).f(fn));
×
788
            }
789
            break;
×
790

791
          case F13F20:
×
792
            for(auto fn : message.numbers())
×
793
            {
794
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF13F20&>(message).f(fn));
×
795
            }
796
            break;
×
797

798
          case F21F28:
×
799
            for(auto fn : message.numbers())
×
800
            {
801
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF21F28&>(message).f(fn));
×
802
            }
803
            break;
×
804
        }
805
      }
806
    });
×
807
}
×
808

809
void Kernel::receiveDSPD(const SetEngineSpeedDirection& message)
×
810
{
811
  assert(isKernelThread());
×
812
  EventLoop::call(
×
813
    [this, message]()
×
814
    {
815
      if(onEngineSpeedDirectionChanged) [[likely]]
×
816
      {
817
        onEngineSpeedDirectionChanged(message.session, message.speed(), message.directionForward());
×
818
      }
819
    });
×
820
}
×
821

822
void Kernel::receiveKLOC(const ReleaseEngine& message)
×
823
{
824
  assert(isKernelThread());
×
825
  EventLoop::call(
×
826
    [this, session=message.session]()
×
827
    {
828
      if(onEngineSessionReleased) [[likely]]
×
829
      {
830
        onEngineSessionReleased(session);
×
831
      }
832
    });
×
833
}
×
834

NEW
835
void Kernel::receiveShortEvent(uint16_t eventNumber, bool on)
×
836
{
NEW
837
  assert(isKernelThread());
×
NEW
838
  EventLoop::call(
×
NEW
839
    [this, eventNumber, on]()
×
840
    {
NEW
841
      if(onShortEvent) [[likely]]
×
842
      {
NEW
843
        onShortEvent(eventNumber, on);
×
844
      }
NEW
845
    });
×
NEW
846
}
×
847

NEW
848
void Kernel::receiveLongEvent(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
849
{
NEW
850
  assert(isKernelThread());
×
NEW
851
  EventLoop::call(
×
NEW
852
    [this, nodeNumber, eventNumber, on]()
×
853
    {
NEW
854
      if(onLongEvent) [[likely]]
×
855
      {
NEW
856
        onLongEvent(nodeNumber, eventNumber, on);
×
857
      }
NEW
858
    });
×
NEW
859
}
×
860

UNCOV
861
void Kernel::changeState(State value)
×
862
{
863
  assert(isKernelThread());
×
864
  assert(m_state != value);
×
865

866
  m_state = value;
×
867

868
  switch(m_state)
×
869
  {
870
    case State::Initial: [[unlikely]]
×
871
      assert(false);
×
872
      break;
873

874
    case State::QueryNodes:
×
875
      send(QueryNodeNumber());
×
876
      restartInitializationTimer(queryNodeNumberTimeout);
×
877
      break;
×
878

879
    case State::ReadNodeParameters:
×
880
      readNodeParameter();
×
881
      break;
×
882

883
    case State::GetCommandStationStatus:
×
884
      send(RequestCommandStationStatus());
×
885
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
886
      break;
×
887

NEW
888
    case State::RequestShortEvents:
×
NEW
889
      requestShortEvent();
×
NEW
890
      break;
×
891

NEW
892
    case State::RequestLongEvents:
×
NEW
893
      requestLongEvent();
×
NEW
894
      break;
×
895

896
    case State::Started:
×
897
      KernelBase::started();
×
898
      break;
×
899
  }
900
}
×
901

902
void Kernel::readNodeParameter()
×
903
{
904
  assert(m_state == State::ReadNodeParameters);
×
905
  if(m_readNodeParameters.empty())
×
906
  {
907
    nextState();
×
908
    return;
×
909
  }
910
  send(m_readNodeParameters.front());
×
911
  restartInitializationTimer(readNodeParameterTimeout);
×
912
}
913

NEW
914
void Kernel::requestShortEvent()
×
915
{
NEW
916
  assert(m_state == State::RequestShortEvents);
×
NEW
917
  if(m_initializationRequestShortEvents.empty())
×
918
  {
NEW
919
    nextState();
×
NEW
920
    return;
×
921
  }
NEW
922
  send(AccessoryShortRequestEvent(Config::nodeId, m_initializationRequestShortEvents.back()));
×
NEW
923
  restartInitializationTimer(requestShortEventTimeout);
×
924
}
925

NEW
926
void Kernel::requestLongEvent()
×
927
{
NEW
928
  assert(m_state == State::RequestLongEvents);
×
NEW
929
  if(m_initializationRequestLongEvents.empty())
×
930
  {
NEW
931
    nextState();
×
NEW
932
    return;
×
933
  }
NEW
934
  send(AccessoryRequestEvent(m_initializationRequestLongEvents.back().first, m_initializationRequestLongEvents.back().second));
×
NEW
935
  restartInitializationTimer(requestLongEventTimeout);
×
936
}
937

UNCOV
938
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
939
{
940
  assert(isKernelThread());
×
941

942
  m_initializationTimer.cancel();
×
943

944
  m_initializationTimer.expires_after(timeout);
×
945
  m_initializationTimer.async_wait(
×
946
    [this](std::error_code ec)
×
947
    {
948
      if(ec)
×
949
      {
950
        return;
×
951
      }
952

953
      switch(m_state)
×
954
      {
955
        case State::QueryNodes:
×
956
          nextState();
×
957
          break;
×
958

959
        case State::ReadNodeParameters:
×
960
          m_readNodeParameters.pop();
×
961
          readNodeParameter();
×
962
          break;
×
963

964
        case State::GetCommandStationStatus:
×
965
          nextState();
×
966
          break;
×
967

NEW
968
        case State::RequestShortEvents:
×
NEW
969
          m_initializationRequestShortEvents.pop_back();
×
NEW
970
          requestShortEvent();
×
NEW
971
          break;
×
972

NEW
973
        case State::RequestLongEvents:
×
NEW
974
          m_initializationRequestLongEvents.pop_back();
×
NEW
975
          requestLongEvent();
×
NEW
976
          break;
×
977

978
        case State::Initial: [[unlikely]]
×
979
        case State::Started: [[unlikely]]
×
980
          assert(false);
×
981
          break;
982
      }
983
    });
984
}
×
985

986
void Kernel::restartEngineKeepAliveTimer()
×
987
{
988
  assert(isKernelThread());
×
989

990
  m_engineKeepAliveTimer.cancel();
×
991

992
  // find first expiring engine:
993
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
994
  for(const auto& [_, engine] : m_engines)
×
995
  {
996
    if(engine.session && engine.lastCommand < lastUpdate)
×
997
    {
998
      lastUpdate = engine.lastCommand;
×
999
      m_engineKeepAliveSession = *engine.session;
×
1000
    }
1001
  }
1002

1003
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
1004

1005
  if(m_engineKeepAliveTimerActive)
×
1006
  {
1007
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
1008
    m_engineKeepAliveTimer.async_wait(
×
1009
      [this](std::error_code ec)
×
1010
      {
1011
        if(ec)
×
1012
        {
1013
          return;
×
1014
        }
1015

1016
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
1017

1018
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
1019
            [session=m_engineKeepAliveSession](const auto& item)
×
1020
            {
1021
              return item.second.session && *item.second.session == session;
×
1022
            }); it != m_engines.end()) [[likely]]
×
1023
        {
1024
          it->second.lastCommand = std::chrono::steady_clock::now();
×
1025
        }
1026

1027
        restartEngineKeepAliveTimer();
×
1028
      });
1029
  }
1030
}
×
1031

1032
void Kernel::startDccAccessoryTimer()
×
1033
{
1034
  assert(isKernelThread());
×
1035

1036
  if(m_dccAccessoryQueue.empty()) [[unlikely]]
×
1037
  {
1038
    return;
×
1039
  }
1040

1041
  m_dccAccessoryTimer.expires_at(m_dccAccessoryQueue.front().first);
×
1042
  m_dccAccessoryTimer.async_wait(
×
1043
    [this](std::error_code ec)
×
1044
    {
1045
      if(ec)
×
1046
      {
1047
        return;
×
1048
      }
1049

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

1052
      m_dccAccessoryQueue.pop();
×
1053

1054
      if(!m_dccAccessoryQueue.empty())
×
1055
      {
1056
        startDccAccessoryTimer();
×
1057
      }
1058
    });
1059
}
1060

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