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

traintastic / traintastic / 24315872327

12 Apr 2026 08:37PM UTC coverage: 25.594% (-0.002%) from 25.596%
24315872327

push

github

reinder
[cbus] added setting for short event node number, default is zero

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

1 existing line in 1 file now uncovered.

8300 of 32430 relevant lines covered (25.59%)

178.27 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 "cbuscanmessageutils.hpp"
25
#include "cbustostring.hpp"
26
#include "iohub/cbusiohub.hpp"
27
#include "simulator/cbussimulator.hpp"
28
#include "../dcc/dcc.hpp"
29
#include "../../../core/eventloop.hpp"
30
#include "../../../log/log.hpp"
31
#include "../../../log/logmessageexception.hpp"
32
#include "../../../utils/inrange.hpp"
33
#include "../../../utils/setthreadname.hpp"
34

35
namespace {
36

37
using namespace std::chrono_literals;
38

39
static constexpr auto queryNodeNumberTimeout = 250ms;
40
static constexpr auto readNodeParameterTimeout = 50ms;
41
static constexpr auto requestShortEventTimeout = 50ms;
42
static constexpr auto requestLongEventTimeout = 50ms;
43
static constexpr auto requestCommandStationStatusTimeout = 100ms;
44

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

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

54
  switch(speedSteps)
×
55
  {
56
    case 14:
×
57
      return SpeedMode14;
×
58

59
    case 28:
×
60
      return SpeedMode28;
×
61

62
    default:
×
63
      return SpeedMode128;
×
64
  }
65
}
66

67
}
68

69
namespace CBUS {
70

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

83
void Kernel::setConfig(const Config& config)
×
84
{
85
  assert(isEventLoopThread());
×
86

87
  boost::asio::post(m_ioContext,
×
88
    [this, newConfig=config]()
×
89
    {
90
      m_config = newConfig;
×
91
    });
×
92
}
×
93

94
void Kernel::setRequestEventsDuringInitialize(std::vector<uint16_t> shortEvents, std::vector<std::pair<uint16_t,uint16_t>> longEvents)
×
95
{
96
  assert(isEventLoopThread());
×
97

98
  std::reverse(shortEvents.begin(), shortEvents.end());
×
99
  std::reverse(longEvents.begin(), longEvents.end());
×
100

101
  m_initializationRequestShortEvents = std::move(shortEvents);
×
102
  m_initializationRequestLongEvents = std::move(longEvents);
×
103
}
×
104

105
void Kernel::start()
×
106
{
107
  assert(isEventLoopThread());
×
108
  assert(m_ioHandler);
×
109

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

118
  boost::asio::post(m_ioContext,
×
119
    [this]()
×
120
    {
121
      try
122
      {
123
        m_ioHandler->onReceive =
×
124
          [this](const CAN::Message& canMessage)
×
125
          {
126
            receive(canMessage);
×
127
            if(m_hub)
×
128
            {
129
              m_hub->send(canMessage);
×
130
            }
131
          };
×
132

133
        m_ioHandler->start();
×
134

135
        if(m_config.hubEnabled)
×
136
        {
137
          m_hub = std::make_shared<IOHub>(m_ioContext, logId, m_config.hubLocalhostOnly, m_config.hubPort);
×
138
          m_hub->start(
×
139
            [this](const CAN::Message& message)
×
140
            {
141
              receive(message);
×
142
              (void)m_ioHandler->send(message); // FICME: add error handling
×
143
            });
×
144
        }
145
      }
146
      catch(const LogMessageException& e)
×
147
      {
148
        EventLoop::call(
×
149
          [this, e]()
×
150
          {
151
            Log::log(logId, e.message(), e.args());
×
152
            error();
×
153
          });
×
154
        return;
×
155
      }
×
156
    });
157
}
×
158

159
void Kernel::stop()
×
160
{
161
  assert(isEventLoopThread());
×
162

163
  boost::asio::post(m_ioContext,
×
164
    [this]()
×
165
    {
166
      if(m_hub)
×
167
      {
168
        m_hub->stop();
×
169
      }
170
      m_ioHandler->stop();
×
171

172
      m_ioContext.stop();
×
173
    });
×
174

175
  m_thread.join();
×
176
}
×
177

178
void Kernel::started()
×
179
{
180
  assert(isKernelThread());
×
181

182
  nextState();
×
183
}
×
184

185
void Kernel::receive(const CAN::Message& canMessage)
×
186
{
187
  assert(isKernelThread());
×
188

189
  const auto canId = getCanId(canMessage);
×
190
  const auto& message = asMessage(canMessage);
×
191

192
  if(m_config.debugLogRXTX)
×
193
  {
194
    EventLoop::call(
×
195
      [this, msg=toString(message)]()
×
196
      {
197
        Log::log(logId, LogMessage::D2002_RX_X, msg);
×
198
      });
×
199
  }
200

201
  switch(message.opCode)
×
202
  {
203
    case OpCode::TOF:
×
204
      m_trackOn = false;
×
205
      if(onTrackOff) [[likely]]
×
206
      {
207
        EventLoop::call(onTrackOff);
×
208
      }
209
      break;
×
210

211
    case OpCode::TON:
×
212
      m_trackOn = true;
×
213
      if(onTrackOn) [[likely]]
×
214
      {
215
        EventLoop::call(onTrackOn);
×
216
      }
217
      break;
×
218

219
    case OpCode::ESTOP:
×
220
      if(onEmergencyStop) [[likely]]
×
221
      {
222
        EventLoop::call(onEmergencyStop);
×
223
      }
224
      break;
×
225

226
    case OpCode::KLOC:
×
227
      receiveKLOC(static_cast<const ReleaseEngine&>(message));
×
228
      break;
×
229

230
    case OpCode::DSPD:
×
231
      receiveDSPD(static_cast<const SetEngineSpeedDirection&>(message));
×
232
      break;
×
233

234
    case OpCode::DFNON:
×
235
    case OpCode::DFNOF:
236
      receiveDFNOx(static_cast<const SetEngineFunction&>(message));
×
237
      break;
×
238

239
    case OpCode::DFUN:
×
240
      receiveDFUN(static_cast<const SetEngineFunctions&>(message));
×
241
      break;
×
242

243
    case OpCode::ASON:
×
244
      receiveShortEvent(static_cast<const AccessoryShortOn&>(message).deviceNumber(), true);
×
245
      break;
×
246

247
    case OpCode::ASOF:
×
248
      receiveShortEvent(static_cast<const AccessoryShortOff&>(message).deviceNumber(), false);
×
249
      break;
×
250

251
    case OpCode::ARSON:
×
252
    {
253
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
254
      receiveShortEvent(eventNumber, true);
×
255
      if(m_state == State::RequestShortEvents &&
×
256
          m_initializationRequestShortEvents.back() == eventNumber)
×
257
      {
258
        m_initializationRequestShortEvents.pop_back();
×
259
        requestShortEvent();
×
260
      }
261
      break;
×
262
    }
263
    case OpCode::ARSOF:
×
264
    {
265
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
266
      receiveShortEvent(eventNumber, false);
×
267
      if(m_state == State::RequestShortEvents &&
×
268
          m_initializationRequestShortEvents.back() == eventNumber)
×
269
      {
270
        m_initializationRequestShortEvents.pop_back();
×
271
        requestShortEvent();
×
272
      }
273
      break;
×
274
    }
275
    case OpCode::ERR:
×
276
    {
277
      switch(static_cast<const CommandStationErrorMessage&>(message).errorCode)
×
278
      {
279
        using enum DCCErr;
280

281
        case LocoStackFull:
×
282
        {
283
          const auto& err = static_cast<const CommandStationLocoStackFullError&>(message);
×
284
          const auto key = makeAddressKey(err.address(), err.isLongAddress());
×
285
          if(m_engineGLOCs.contains(key))
×
286
          {
287
            m_engineGLOCs.erase(key);
×
288
            // FIXME: log error
289
          }
290
          break;
×
291
        }
292
        case SessionCancelled:
×
293
          if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
294
            [session=static_cast<const CommandStationSessionCancelled&>(message).session()](const auto& item)
×
295
            {
296
              return item.second.session && *item.second.session == session;
×
297
            }); it != m_engines.end())
×
298
          {
299
            it->second.session = std::nullopt;
×
300

301
            if(m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *it->second.session)
×
302
            {
303
              restartEngineKeepAliveTimer();
×
304
            }
305

306
            EventLoop::call(
×
307
              [this, key=it->first]()
×
308
              {
309
                if(onEngineSessionCancelled) [[likely]]
×
310
                {
311
                  onEngineSessionCancelled(key & 0x3FFF, (key & 0xC000) == 0xC000);
×
312
                }
313
              });
×
314
          }
315
          break;
×
316

317
        case LocoAddressTaken:
×
318
        case SessionNotPresent:
319
        case ConsistEmpty:
320
        case LocoNotFound:
321
        case CANBusError:
322
        case InvalidRequest:
323
          break;
×
324
      }
325
      break;
×
326
    }
327
    case OpCode::ACON:
×
328
    {
329
      const auto& acon = static_cast<const AccessoryOn&>(message);
×
330
      receiveLongEvent(acon.nodeNumber(), acon.eventNumber(), true);
×
331
      break;
×
332
    }
333
    case OpCode::ACOF:
×
334
    {
335
      const auto& acof = static_cast<const AccessoryOff&>(message);
×
336
      receiveLongEvent(acof.nodeNumber(), acof.eventNumber(), false);
×
337
      break;
×
338
    }
339
    case OpCode::ARON:
×
340
    {
341
      const auto& aron = static_cast<const AccessoryResponseEventOn&>(message);
×
342
      receiveLongEvent(aron.nodeNumber(), aron.eventNumber(), true);
×
343
      if(m_state == State::RequestShortEvents &&
×
344
          m_initializationRequestLongEvents.back().first == aron.nodeNumber() &&
×
345
          m_initializationRequestLongEvents.back().second == aron.eventNumber())
×
346
      {
347
        m_initializationRequestLongEvents.pop_back();
×
348
        requestLongEvent();
×
349
      }
350
      break;
×
351
    }
352
    case OpCode::AROF:
×
353
    {
354
      const auto& arof = static_cast<const AccessoryResponseEventOff&>(message);
×
355
      receiveLongEvent(arof.nodeNumber(), arof.eventNumber(), false);
×
356
      if(m_state == State::RequestShortEvents &&
×
357
          m_initializationRequestLongEvents.back().first == arof.nodeNumber() &&
×
358
          m_initializationRequestLongEvents.back().second == arof.eventNumber())
×
359
      {
360
        m_initializationRequestLongEvents.pop_back();
×
361
        requestLongEvent();
×
362
      }
363
      break;
×
364
    }
365
    case OpCode::PARAN:
×
366
      if(m_state == State::ReadNodeParameters && !m_readNodeParameters.empty())
×
367
      {
368
        const auto& rqnpn = m_readNodeParameters.front();
×
369
        const auto& paran = static_cast<const NodeParameterResponse&>(message);
×
370

371
        if(rqnpn.nodeNumber() == paran.nodeNumber() && rqnpn.parameter == paran.parameter)
×
372
        {
373
          m_readNodeParameters.pop();
×
374
          m_initializationTimer.cancel();
×
375

376
          EventLoop::call(
×
377
            [this, canId, paran]()
×
378
            {
379
              if(onNodeParameterResponse) [[likely]]
×
380
              {
381
                onNodeParameterResponse(canId, paran.nodeNumber(), paran.parameter, paran.value);
×
382
              }
383
            });
×
384

385
          readNodeParameter();
×
386
        }
387
      }
388
      break;
×
389

390
    case OpCode::PNN:
×
391
      if(m_state == State::QueryNodes)
×
392
      {
393
        restartInitializationTimer(queryNodeNumberTimeout);
×
394

395
        const auto& pnn = static_cast<const PresenceOfNode&>(message);
×
396

397
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::VersionMajor));
×
398
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::VersionMinor));
×
399
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::BetaReleaseCode));
×
400

401
        EventLoop::call(
×
402
          [this, canId, pnn]()
×
403
          {
404
            if(onPresenceOfNode) [[likely]]
×
405
            {
406
              onPresenceOfNode(canId, pnn.nodeNumber(), pnn.manufacturerId, pnn.moduleId, pnn.flimMode(), pnn.supportsServiceDiscovery());
×
407
            }
408
          });
×
409
      }
410
      break;
×
411

412
    case OpCode::PLOC:
×
413
    {
414
      const auto& ploc = static_cast<const EngineReport&>(message);
×
415

416
      EventLoop::call(
×
417
        [this, ploc]()
×
418
        {
419
          if(onEngineSessionAcquire) [[likely]]
×
420
          {
421
            onEngineSessionAcquire(ploc.session, ploc.address(), ploc.isLongAddress());
×
422
          }
423
        });
×
424

425
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
426
      if(m_engineGLOCs.contains(key))
×
427
      {
428
        m_engineGLOCs.erase(key);
×
429

430
        if(auto it = m_engines.find(key); it != m_engines.end())
×
431
        {
432
          auto& engine = it->second;
×
433
          engine.session = ploc.session;
×
434

435
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
436
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
437

438
          for(const auto& [number, value] : engine.functions)
×
439
          {
440
            sendSetEngineFunction(ploc.session, number, value);
×
441
          }
442

443
          engine.lastCommand = std::chrono::steady_clock::now();
×
444

445
          if(!m_engineKeepAliveTimerActive)
×
446
          {
447
            restartEngineKeepAliveTimer();
×
448
          }
449
        }
450
        else // we're no longer in need of control (rare but possible)
451
        {
452
          send(ReleaseEngine(ploc.session));
×
453
        }
454
      }
455
      break;
×
456
    }
457
    case OpCode::STAT:
×
458
      if(m_state == State::GetCommandStationStatus)
×
459
      {
460
        m_initializationTimer.cancel();
×
461
        nextState();
×
462
      }
463
      break;
×
464

465
    default:
×
466
      break;
×
467
  }
468

469
  // external listeners:
470
  for(const auto& [handle, opCode] : m_onReceiveFilters)
×
471
  {
472
    if(message.opCode == opCode)
×
473
    {
474
      auto buffer = std::make_shared<std::byte[]>(message.size());
×
475
      std::memcpy(buffer.get(), &message, message.size());
×
476
      EventLoop::call(
×
477
        [this, handle, canId, data=std::move(buffer)]()
×
478
        {
479
          // check if still registered, could theoretically be unregister after filtering and before callback:
480
          if(auto it = m_onReceiveCallbacks.find(handle); it != m_onReceiveCallbacks.end())
×
481
          {
482
            it->second(canId, *reinterpret_cast<const Message*>(data.get()));
×
483
          }
484
        });
×
485
    }
×
486
  }
487
}
×
488

489
size_t Kernel::registerOnReceive(OpCode opCode, std::function<void(uint8_t, const Message&)> callback)
×
490
{
491
  assert(isEventLoopThread());
×
492

493
  while(++m_onReceiveHandle == 0);
×
494

495
  m_onReceiveCallbacks.emplace(m_onReceiveHandle, std::move(callback));
×
496

497
  m_ioContext.post(
×
498
    [this, handle=m_onReceiveHandle, opCode]()
×
499
    {
500
      m_onReceiveFilters.emplace(handle, opCode);
×
501
    });
×
502

503
  return m_onReceiveHandle;
×
504
}
505

506
void Kernel::unregisterOnReceive(size_t handle)
×
507
{
508
  assert(isEventLoopThread());
×
509

510
  m_onReceiveCallbacks.erase(handle);
×
511

512
  m_ioContext.post(
×
513
    [this, handle]()
×
514
    {
515
      m_onReceiveFilters.erase(handle);
×
516
    });
×
517
}
×
518

519
void Kernel::trackOff()
×
520
{
521
  assert(isEventLoopThread());
×
522

523
  boost::asio::post(m_ioContext,
×
524
    [this]()
×
525
    {
526
      if(m_trackOn)
×
527
      {
528
        send(RequestTrackOff());
×
529
      }
530
    });
×
531
}
×
532

533
void Kernel::trackOn()
×
534
{
535
  assert(isEventLoopThread());
×
536

537
  boost::asio::post(m_ioContext,
×
538
    [this]()
×
539
    {
540
      if(!m_trackOn)
×
541
      {
542
        send(RequestTrackOn());
×
543
      }
544
    });
×
545
}
×
546

547
void Kernel::requestEmergencyStop()
×
548
{
549
  assert(isEventLoopThread());
×
550

551
  boost::asio::post(m_ioContext,
×
552
    [this]()
×
553
    {
554
      send(RequestEmergencyStop());
×
555
    });
×
556
}
×
557

558
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
559
{
560
  assert(isEventLoopThread());
×
561

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

564
  boost::asio::post(m_ioContext,
×
565
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
566
    {
567
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
568
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
569
      engine.speedSteps = speedSteps;
×
570
      engine.speed = speed;
×
571
      engine.directionForward = directionForward;
×
572

573
      if(engine.session) // we're in control
×
574
      {
575
        if(speedStepsChanged)
×
576
        {
577
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
578
        }
579
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
580

581
        engine.lastCommand = std::chrono::steady_clock::now();
×
582

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

595
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
596
{
597
  assert(isEventLoopThread());
×
598

599
  boost::asio::post(m_ioContext,
×
600
    [this, address, longAddress, number, value]()
×
601
    {
602
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
603
      engine.functions[number] = value;
×
604
      if(engine.session) // we're in control
×
605
      {
606
        sendSetEngineFunction(*engine.session, number, value);
×
607

608
        engine.lastCommand = std::chrono::steady_clock::now();
×
609

610
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
611
        {
612
          restartEngineKeepAliveTimer();
×
613
        }
614
      }
615
      else // take control
616
      {
617
        sendGetEngineSession(address, longAddress);
×
618
      }
619
    });
×
620
}
×
621

622
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
623
{
624
  assert(isEventLoopThread());
×
625

626
  boost::asio::post(m_ioContext,
×
627
    [this, deviceNumber, on]()
×
628
    {
629
      if(on)
×
630
      {
NEW
631
        send(AccessoryShortOn(m_config.shortEventNodeNumber, deviceNumber));
×
632
      }
633
      else
634
      {
NEW
635
        send(AccessoryShortOff(m_config.shortEventNodeNumber, deviceNumber));
×
636
      }
637
    });
×
638
}
×
639

640
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
641
{
642
  assert(isEventLoopThread());
×
643

644
  boost::asio::post(m_ioContext,
×
645
    [this, nodeNumber, eventNumber, on]()
×
646
    {
647
      if(on)
×
648
      {
649
        send(AccessoryOn(nodeNumber, eventNumber));
×
650
      }
651
      else
652
      {
653
        send(AccessoryOff(nodeNumber, eventNumber));
×
654
      }
655
    });
×
656
}
×
657

658
void Kernel::setDccAccessory(uint16_t address, bool secondOutput)
×
659
{
660
  assert(isEventLoopThread());
×
661

662
  boost::asio::post(m_ioContext,
×
663
    [this, address, secondOutput]()
×
664
    {
665
      send(RequestDCCPacket<sizeof(DCC::SetSimpleAccessory) + 1>(DCC::SetSimpleAccessory(address, secondOutput, true), Config::dccAccessoryRepeat));
×
666
      const bool wasEmpty = m_dccAccessoryQueue.empty();
×
667
      m_dccAccessoryQueue.emplace(std::make_pair(
×
668
        std::chrono::steady_clock::now() + m_config.dccAccessorySwitchTime,
×
669
        DCC::SetSimpleAccessory(address, secondOutput, false)
×
670
      ));
671
      if(wasEmpty)
×
672
      {
673
        startDccAccessoryTimer();
×
674
      }
675
    });
×
676
}
×
677

678
void Kernel::setDccAdvancedAccessoryValue(uint16_t address, uint8_t aspect)
×
679
{
680
  assert(isEventLoopThread());
×
681

682
  boost::asio::post(m_ioContext,
×
683
    [this, address, aspect]()
×
684
    {
685
      send(RequestDCCPacket<sizeof(DCC::SetAdvancedAccessoryValue) + 1>(DCC::SetAdvancedAccessoryValue(address, aspect), Config::dccExtRepeat));
×
686
    });
×
687
}
×
688

689
bool Kernel::send(std::vector<uint8_t> message)
×
690
{
691
  assert(isEventLoopThread());
×
692

693
  if(!inRange<size_t>(message.size(), 1, 8))
×
694
  {
695
    return false;
×
696
  }
697

698
  boost::asio::post(m_ioContext,
×
699
    [this, msg=std::move(message)]()
×
700
    {
701
      send(*reinterpret_cast<const Message*>(msg.data()));
×
702
    });
×
703

704
  return true;
×
705
}
706

707
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
708
{
709
  assert(isEventLoopThread());
×
710

711
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
712
  {
713
    return false;
×
714
  }
715

716
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
717

718
  boost::asio::post(m_ioContext,
×
719
    [this, packet=std::move(dccPacket), repeat]()
×
720
    {
721
      switch(packet.size())
×
722
      {
723
        case 3:
×
724
          send(RequestDCCPacket<3>(packet, repeat));
×
725
          break;
×
726

727
        case 4:
×
728
          send(RequestDCCPacket<4>(packet, repeat));
×
729
          break;
×
730

731
        case 5:
×
732
          send(RequestDCCPacket<5>(packet, repeat));
×
733
          break;
×
734

735
        case 6:
×
736
          send(RequestDCCPacket<6>(packet, repeat));
×
737
          break;
×
738

739
        default: [[unlikely]]
×
740
          assert(false);
×
741
          break;
742
      }
743
    });
×
744

745
  return true;
×
746
}
747

748
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
749
{
750
  assert(isEventLoopThread());
×
751
  assert(handler);
×
752
  assert(!m_ioHandler);
×
753
  m_ioHandler = std::move(handler);
×
754
}
×
755

756
void Kernel::send(const Message& message)
×
757
{
758
  assert(isKernelThread());
×
759

760
  if(m_config.debugLogRXTX)
×
761
  {
762
    EventLoop::call(
×
763
      [this, msg=toString(message)]()
×
764
      {
765
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
766
      });
×
767
  }
768

769
  const auto canMessage = toCANMessage(message, m_canId);
×
770

771
  if(auto ec = m_ioHandler->send(canMessage); ec)
×
772
  {
773
    (void)ec; // FIXME: handle error
774
  }
775

776
  if(m_hub)
×
777
  {
778
    m_hub->send(canMessage);
×
779
  }
780
}
×
781

782
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
783
{
784
  assert(isKernelThread());
×
785
  const auto key = makeAddressKey(address, longAddress);
×
786
  if(!m_engineGLOCs.contains(key))
×
787
  {
788
    m_engineGLOCs.emplace(key);
×
789
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
790
  }
791
}
×
792

793
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
794
{
795
  assert(isKernelThread());
×
796
  // FIXME: what to do with: serviceMode and soundControlMode?
797
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
798
}
×
799

800
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
801
{
802
  assert(isKernelThread());
×
803

804
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
805

806
  EventLoop::call(
×
807
    [this, session, speed, directionForward]()
×
808
    {
809
      if(onEngineSpeedDirectionChanged) [[likely]]
×
810
      {
811
        onEngineSpeedDirectionChanged(session, speed, directionForward);
×
812
      }
813
    });
×
814
}
×
815

816
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
817
{
818
  assert(isKernelThread());
×
819

820
  send(SetEngineFunction(session, number, value));
×
821

822
  EventLoop::call(
×
823
    [this, session, number, value]()
×
824
    {
825
      if(onEngineFunctionChanged) [[likely]]
×
826
      {
827
        onEngineFunctionChanged(session, number, value);
×
828
      }
829
    });
×
830
}
×
831

832
void Kernel::receiveDFNOx(const SetEngineFunction& message)
×
833
{
834
  assert(isKernelThread());
×
835
  EventLoop::call(
×
836
    [this, message]()
×
837
    {
838
      if(onEngineFunctionChanged) [[likely]]
×
839
      {
840
        onEngineFunctionChanged(message.session, message.number, message.on());
×
841
      }
842
    });
×
843
}
×
844

845
void Kernel::receiveDFUN(const CBUS::SetEngineFunctions& message)
×
846
{
847
  assert(isKernelThread());
×
848
  EventLoop::call(
×
849
    [this, message]()
×
850
    {
851
      if(onEngineFunctionChanged) [[likely]]
×
852
      {
853
        switch(message.range)
×
854
        {
855
          using enum SetEngineFunctions::Range;
856

857
          case F0F4:
×
858
            for(auto fn : message.numbers())
×
859
            {
860
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF0F4&>(message).f(fn));
×
861
            }
862
            break;
×
863

864
          case F5F8:
×
865
            for(auto fn : message.numbers())
×
866
            {
867
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF5F8&>(message).f(fn));
×
868
            }
869
            break;
×
870

871
          case F9F12:
×
872
            for(auto fn : message.numbers())
×
873
            {
874
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF9F12&>(message).f(fn));
×
875
            }
876
            break;
×
877

878
          case F13F20:
×
879
            for(auto fn : message.numbers())
×
880
            {
881
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF13F20&>(message).f(fn));
×
882
            }
883
            break;
×
884

885
          case F21F28:
×
886
            for(auto fn : message.numbers())
×
887
            {
888
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF21F28&>(message).f(fn));
×
889
            }
890
            break;
×
891
        }
892
      }
893
    });
×
894
}
×
895

896
void Kernel::receiveDSPD(const SetEngineSpeedDirection& message)
×
897
{
898
  assert(isKernelThread());
×
899
  EventLoop::call(
×
900
    [this, message]()
×
901
    {
902
      if(onEngineSpeedDirectionChanged) [[likely]]
×
903
      {
904
        onEngineSpeedDirectionChanged(message.session, message.speed(), message.directionForward());
×
905
      }
906
    });
×
907
}
×
908

909
void Kernel::receiveKLOC(const ReleaseEngine& message)
×
910
{
911
  assert(isKernelThread());
×
912
  EventLoop::call(
×
913
    [this, session=message.session]()
×
914
    {
915
      if(onEngineSessionReleased) [[likely]]
×
916
      {
917
        onEngineSessionReleased(session);
×
918
      }
919
    });
×
920
}
×
921

922
void Kernel::receiveShortEvent(uint16_t eventNumber, bool on)
×
923
{
924
  assert(isKernelThread());
×
925
  EventLoop::call(
×
926
    [this, eventNumber, on]()
×
927
    {
928
      if(onShortEvent) [[likely]]
×
929
      {
930
        onShortEvent(eventNumber, on);
×
931
      }
932
    });
×
933
}
×
934

935
void Kernel::receiveLongEvent(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
936
{
937
  assert(isKernelThread());
×
938
  EventLoop::call(
×
939
    [this, nodeNumber, eventNumber, on]()
×
940
    {
941
      if(onLongEvent) [[likely]]
×
942
      {
943
        onLongEvent(nodeNumber, eventNumber, on);
×
944
      }
945
    });
×
946
}
×
947

948
void Kernel::changeState(State value)
×
949
{
950
  assert(isKernelThread());
×
951
  assert(m_state != value);
×
952

953
  m_state = value;
×
954

955
  switch(m_state)
×
956
  {
957
    case State::Initial: [[unlikely]]
×
958
      assert(false);
×
959
      break;
960

961
    case State::QueryNodes:
×
962
      send(QueryNodeNumber());
×
963
      restartInitializationTimer(queryNodeNumberTimeout);
×
964
      break;
×
965

966
    case State::ReadNodeParameters:
×
967
      readNodeParameter();
×
968
      break;
×
969

970
    case State::GetCommandStationStatus:
×
971
      send(RequestCommandStationStatus());
×
972
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
973
      break;
×
974

975
    case State::RequestShortEvents:
×
976
      requestShortEvent();
×
977
      break;
×
978

979
    case State::RequestLongEvents:
×
980
      requestLongEvent();
×
981
      break;
×
982

983
    case State::Started:
×
984
      KernelBase::started();
×
985
      break;
×
986
  }
987
}
×
988

989
void Kernel::readNodeParameter()
×
990
{
991
  assert(m_state == State::ReadNodeParameters);
×
992
  if(m_readNodeParameters.empty())
×
993
  {
994
    nextState();
×
995
    return;
×
996
  }
997
  send(m_readNodeParameters.front());
×
998
  restartInitializationTimer(readNodeParameterTimeout);
×
999
}
1000

1001
void Kernel::requestShortEvent()
×
1002
{
1003
  assert(m_state == State::RequestShortEvents);
×
1004
  if(m_initializationRequestShortEvents.empty())
×
1005
  {
1006
    nextState();
×
1007
    return;
×
1008
  }
NEW
1009
  send(AccessoryShortRequestEvent(m_config.shortEventNodeNumber, m_initializationRequestShortEvents.back()));
×
1010
  restartInitializationTimer(requestShortEventTimeout);
×
1011
}
1012

1013
void Kernel::requestLongEvent()
×
1014
{
1015
  assert(m_state == State::RequestLongEvents);
×
1016
  if(m_initializationRequestLongEvents.empty())
×
1017
  {
1018
    nextState();
×
1019
    return;
×
1020
  }
1021
  send(AccessoryRequestEvent(m_initializationRequestLongEvents.back().first, m_initializationRequestLongEvents.back().second));
×
1022
  restartInitializationTimer(requestLongEventTimeout);
×
1023
}
1024

1025
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
1026
{
1027
  assert(isKernelThread());
×
1028

1029
  m_initializationTimer.cancel();
×
1030

1031
  m_initializationTimer.expires_after(timeout);
×
1032
  m_initializationTimer.async_wait(
×
1033
    [this](std::error_code ec)
×
1034
    {
1035
      if(ec)
×
1036
      {
1037
        return;
×
1038
      }
1039

1040
      switch(m_state)
×
1041
      {
1042
        case State::QueryNodes:
×
1043
          nextState();
×
1044
          break;
×
1045

1046
        case State::ReadNodeParameters:
×
1047
          m_readNodeParameters.pop();
×
1048
          readNodeParameter();
×
1049
          break;
×
1050

1051
        case State::GetCommandStationStatus:
×
1052
          nextState();
×
1053
          break;
×
1054

1055
        case State::RequestShortEvents:
×
1056
          m_initializationRequestShortEvents.pop_back();
×
1057
          requestShortEvent();
×
1058
          break;
×
1059

1060
        case State::RequestLongEvents:
×
1061
          m_initializationRequestLongEvents.pop_back();
×
1062
          requestLongEvent();
×
1063
          break;
×
1064

1065
        case State::Initial: [[unlikely]]
×
1066
        case State::Started: [[unlikely]]
×
1067
          assert(false);
×
1068
          break;
1069
      }
1070
    });
1071
}
×
1072

1073
void Kernel::restartEngineKeepAliveTimer()
×
1074
{
1075
  assert(isKernelThread());
×
1076

1077
  m_engineKeepAliveTimer.cancel();
×
1078

1079
  // find first expiring engine:
1080
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
1081
  for(const auto& [_, engine] : m_engines)
×
1082
  {
1083
    if(engine.session && engine.lastCommand < lastUpdate)
×
1084
    {
1085
      lastUpdate = engine.lastCommand;
×
1086
      m_engineKeepAliveSession = *engine.session;
×
1087
    }
1088
  }
1089

1090
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
1091

1092
  if(m_engineKeepAliveTimerActive)
×
1093
  {
1094
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
1095
    m_engineKeepAliveTimer.async_wait(
×
1096
      [this](std::error_code ec)
×
1097
      {
1098
        if(ec)
×
1099
        {
1100
          return;
×
1101
        }
1102

1103
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
1104

1105
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
1106
            [session=m_engineKeepAliveSession](const auto& item)
×
1107
            {
1108
              return item.second.session && *item.second.session == session;
×
1109
            }); it != m_engines.end()) [[likely]]
×
1110
        {
1111
          it->second.lastCommand = std::chrono::steady_clock::now();
×
1112
        }
1113

1114
        restartEngineKeepAliveTimer();
×
1115
      });
1116
  }
1117
}
×
1118

1119
void Kernel::startDccAccessoryTimer()
×
1120
{
1121
  assert(isKernelThread());
×
1122

1123
  if(m_dccAccessoryQueue.empty()) [[unlikely]]
×
1124
  {
1125
    return;
×
1126
  }
1127

1128
  m_dccAccessoryTimer.expires_at(m_dccAccessoryQueue.front().first);
×
1129
  m_dccAccessoryTimer.async_wait(
×
1130
    [this](std::error_code ec)
×
1131
    {
1132
      if(ec)
×
1133
      {
1134
        return;
×
1135
      }
1136

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

1139
      m_dccAccessoryQueue.pop();
×
1140

1141
      if(!m_dccAccessoryQueue.empty())
×
1142
      {
1143
        startDccAccessoryTimer();
×
1144
      }
1145
    });
1146
}
1147

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