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

traintastic / traintastic / 26124759034

19 May 2026 08:54PM UTC coverage: 25.552% (-0.003%) from 25.555%
26124759034

push

github

reinder
[cbus] added CAN ID setting, defaults to 0x7A (122), official assignment is pending

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

1 existing line in 1 file now uncovered.

8436 of 33015 relevant lines covered (25.55%)

176.82 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

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

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

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

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

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

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

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

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

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

132
        m_ioHandler->start();
×
133

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

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

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

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

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

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

181
  nextState();
×
182
}
×
183

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

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

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

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

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

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

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

229
    case OpCode::RLOC:
×
230
    {
231
      const auto rloc = static_cast<const RequestEngineSession&>(message);
×
232
      receiveGLOC(rloc.address(), rloc.isLongAddress(), GetEngineSession::Mode::Request);
×
233
      break;
×
234
    }
235
    case OpCode::GLOC:
×
236
    {
237
      const auto gloc = static_cast<const GetEngineSession&>(message);
×
238
      receiveGLOC(gloc.address(), gloc.isLongAddress(), gloc.mode());
×
239
      break;
×
240
    }
241
    case OpCode::DSPD:
×
242
      receiveDSPD(static_cast<const SetEngineSpeedDirection&>(message));
×
243
      break;
×
244

245
    case OpCode::DFNON:
×
246
    case OpCode::DFNOF:
247
      receiveDFNOx(static_cast<const SetEngineFunction&>(message));
×
248
      break;
×
249

250
    case OpCode::DFUN:
×
251
      receiveDFUN(static_cast<const SetEngineFunctions&>(message));
×
252
      break;
×
253

254
    case OpCode::ASON:
×
255
      receiveShortEvent(static_cast<const AccessoryShortOn&>(message).deviceNumber(), true);
×
256
      break;
×
257

258
    case OpCode::ASOF:
×
259
      receiveShortEvent(static_cast<const AccessoryShortOff&>(message).deviceNumber(), false);
×
260
      break;
×
261

262
    case OpCode::ARSON:
×
263
    {
264
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
265
      receiveShortEvent(eventNumber, true);
×
266
      if(m_state == State::RequestShortEvents &&
×
267
          m_initializationRequestShortEvents.back() == eventNumber)
×
268
      {
269
        m_initializationRequestShortEvents.pop_back();
×
270
        requestShortEvent();
×
271
      }
272
      break;
×
273
    }
274
    case OpCode::ARSOF:
×
275
    {
276
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
277
      receiveShortEvent(eventNumber, false);
×
278
      if(m_state == State::RequestShortEvents &&
×
279
          m_initializationRequestShortEvents.back() == eventNumber)
×
280
      {
281
        m_initializationRequestShortEvents.pop_back();
×
282
        requestShortEvent();
×
283
      }
284
      break;
×
285
    }
286
    case OpCode::ERR:
×
287
    {
288
      switch(static_cast<const CommandStationErrorMessage&>(message).errorCode)
×
289
      {
290
        using enum DCCErr;
291

292
        case LocoStackFull:
×
293
        {
294
          const auto& err = static_cast<const CommandStationLocoStackFullError&>(message);
×
295
          const auto key = makeAddressKey(err.address(), err.isLongAddress());
×
296
          if(m_engineGLOCs.contains(key))
×
297
          {
298
            m_engineGLOCs.erase(key);
×
299
            // FIXME: log error
300
          }
301
          break;
×
302
        }
303
        case SessionCancelled:
×
304
          if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
305
            [session=static_cast<const CommandStationSessionCancelled&>(message).session()](const auto& item)
×
306
            {
307
              return item.second.session && *item.second.session == session;
×
308
            }); it != m_engines.end())
×
309
          {
310
            it->second.session = std::nullopt;
×
311

312
            if(m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *it->second.session)
×
313
            {
314
              restartEngineKeepAliveTimer();
×
315
            }
316

317
            EventLoop::call(
×
318
              [this, key=it->first]()
×
319
              {
320
                if(onEngineSessionCancelled) [[likely]]
×
321
                {
322
                  onEngineSessionCancelled(key & 0x3FFF, (key & 0xC000) == 0xC000);
×
323
                }
324
              });
×
325
          }
326
          break;
×
327

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

382
        if(rqnpn.nodeNumber() == paran.nodeNumber() && rqnpn.parameter == paran.parameter)
×
383
        {
384
          m_readNodeParameters.pop();
×
385
          m_initializationTimer.cancel();
×
386

387
          EventLoop::call(
×
388
            [this, canId, paran]()
×
389
            {
390
              if(onNodeParameterResponse) [[likely]]
×
391
              {
392
                onNodeParameterResponse(canId, paran.nodeNumber(), paran.parameter, paran.value);
×
393
              }
394
            });
×
395

396
          readNodeParameter();
×
397
        }
398
      }
399
      break;
×
400

401
    case OpCode::PNN:
×
402
      if(m_state == State::QueryNodes)
×
403
      {
404
        restartInitializationTimer(queryNodeNumberTimeout);
×
405

406
        const auto& pnn = static_cast<const PresenceOfNode&>(message);
×
407

408
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::VersionMajor));
×
409
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::VersionMinor));
×
410
        m_readNodeParameters.emplace(ReadNodeParameter(pnn.nodeNumber(), NodeParameter::BetaReleaseCode));
×
411

412
        EventLoop::call(
×
413
          [this, canId, pnn]()
×
414
          {
415
            if(onPresenceOfNode) [[likely]]
×
416
            {
417
              onPresenceOfNode(canId, pnn.nodeNumber(), pnn.manufacturerId, pnn.moduleId, pnn.flimMode(), pnn.supportsServiceDiscovery());
×
418
            }
419
          });
×
420
      }
421
      break;
×
422

423
    case OpCode::PLOC:
×
424
    {
425
      const auto& ploc = static_cast<const EngineReport&>(message);
×
426
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
427
      auto gloc = m_engineGLOCs.find(key);
×
428
      const auto owner = (gloc != m_engineGLOCs.end()) ? gloc->second : Owner::CBUS;
×
429

430
      EventLoop::call(
×
431
        [this, ploc, owner]()
×
432
        {
433
          if(onEngineSessionAcquire) [[likely]]
×
434
          {
435
            onEngineSessionAcquire(ploc.session, owner != Owner::Traintastic, ploc.address(), ploc.isLongAddress());
×
436
          }
437
        });
×
438

439
      if(auto it = m_engines.find(key); it != m_engines.end())
×
440
      {
441
        auto& engine = it->second;
×
442
        engine.session = ploc.session;
×
443
        engine.owner = owner;
×
444

445
        if(engine.owner == Owner::Traintastic)
×
446
        {
447
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
448
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
449

450
          for(const auto& [number, value] : engine.functions)
×
451
          {
452
            sendSetEngineFunction(ploc.session, number, value);
×
453
          }
454

455
          engine.lastCommand = std::chrono::steady_clock::now();
×
456

457
          if(!m_engineKeepAliveTimerActive)
×
458
          {
459
            restartEngineKeepAliveTimer();
×
460
          }
461
        }
462
        else if(engine.owner == Owner::CBUS)
×
463
        {
464
          engine.speed = ploc.speed();
×
465
          engine.directionForward = ploc.directionForward();
×
466

467
          EventLoop::call(
×
468
            [this, session=*engine.session, speed=engine.speed, directionForward=engine.directionForward]()
×
469
            {
470
              if(onEngineSpeedDirectionChanged) [[likely]]
×
471
              {
472
                onEngineSpeedDirectionChanged(session, speed, directionForward);
×
473
              }
474
            });
×
475

476
          for(uint8_t fn : ploc.numbers())
×
477
          {
478
            engine.functions[fn] = ploc.f(fn);
×
479
            EventLoop::call(
×
480
              [this, session=*engine.session, number=fn, value=engine.functions[fn]]()
×
481
              {
482
                if(onEngineFunctionChanged) [[likely]]
×
483
                {
484
                  onEngineFunctionChanged(session, number, value);
×
485
                }
486
              });
×
487
          }
488
        }
489
      }
490
      else if(owner == Owner::Traintastic) // we're no longer in need of control (rare but possible)
×
491
      {
492
        send(ReleaseEngine(ploc.session));
×
493
      }
494

495
      if(gloc != m_engineGLOCs.end())
×
496
      {
497
        m_engineGLOCs.erase(gloc);
×
498
      }
499
      break;
×
500
    }
501
    case OpCode::STAT:
×
502
      if(m_state == State::GetCommandStationStatus)
×
503
      {
504
        m_initializationTimer.cancel();
×
505
        nextState();
×
506
      }
507
      break;
×
508

509
    default:
×
510
      break;
×
511
  }
512

513
  // external listeners:
514
  for(const auto& [handle, opCode] : m_onReceiveFilters)
×
515
  {
516
    if(message.opCode == opCode)
×
517
    {
518
      auto buffer = std::make_shared<std::byte[]>(message.size());
×
519
      std::memcpy(buffer.get(), &message, message.size());
×
520
      EventLoop::call(
×
521
        [this, handle, canId, data=std::move(buffer)]()
×
522
        {
523
          // check if still registered, could theoretically be unregister after filtering and before callback:
524
          if(auto it = m_onReceiveCallbacks.find(handle); it != m_onReceiveCallbacks.end())
×
525
          {
526
            it->second(canId, *reinterpret_cast<const Message*>(data.get()));
×
527
          }
528
        });
×
529
    }
×
530
  }
531
}
×
532

533
size_t Kernel::registerOnReceive(OpCode opCode, std::function<void(uint8_t, const Message&)> callback)
×
534
{
535
  assert(isEventLoopThread());
×
536

537
  while(++m_onReceiveHandle == 0);
×
538

539
  m_onReceiveCallbacks.emplace(m_onReceiveHandle, std::move(callback));
×
540

541
  m_ioContext.post(
×
542
    [this, handle=m_onReceiveHandle, opCode]()
×
543
    {
544
      m_onReceiveFilters.emplace(handle, opCode);
×
545
    });
×
546

547
  return m_onReceiveHandle;
×
548
}
549

550
void Kernel::unregisterOnReceive(size_t handle)
×
551
{
552
  assert(isEventLoopThread());
×
553

554
  m_onReceiveCallbacks.erase(handle);
×
555

556
  m_ioContext.post(
×
557
    [this, handle]()
×
558
    {
559
      m_onReceiveFilters.erase(handle);
×
560
    });
×
561
}
×
562

563
void Kernel::trackOff()
×
564
{
565
  assert(isEventLoopThread());
×
566

567
  boost::asio::post(m_ioContext,
×
568
    [this]()
×
569
    {
570
      if(m_trackOn)
×
571
      {
572
        send(RequestTrackOff());
×
573
      }
574
    });
×
575
}
×
576

577
void Kernel::trackOn()
×
578
{
579
  assert(isEventLoopThread());
×
580

581
  boost::asio::post(m_ioContext,
×
582
    [this]()
×
583
    {
584
      if(!m_trackOn)
×
585
      {
586
        send(RequestTrackOn());
×
587
      }
588
    });
×
589
}
×
590

591
void Kernel::requestEmergencyStop()
×
592
{
593
  assert(isEventLoopThread());
×
594

595
  boost::asio::post(m_ioContext,
×
596
    [this]()
×
597
    {
598
      send(RequestEmergencyStop());
×
599
    });
×
600
}
×
601

602
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
603
{
604
  assert(isEventLoopThread());
×
605

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

608
  boost::asio::post(m_ioContext,
×
609
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
610
    {
611
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
612
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
613
      engine.speedSteps = speedSteps;
×
614
      engine.speed = speed;
×
615
      engine.directionForward = directionForward;
×
616

617
      if(engine.session) // we're in control
×
618
      {
619
        if(speedStepsChanged)
×
620
        {
621
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
622
        }
623
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
624

625
        engine.lastCommand = std::chrono::steady_clock::now();
×
626

627
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
628
        {
629
          restartEngineKeepAliveTimer();
×
630
        }
631
      }
632
      else // take control
633
      {
634
        sendGetEngineSession(address, longAddress);
×
635
      }
636
    });
×
637
}
×
638

639
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
640
{
641
  assert(isEventLoopThread());
×
642

643
  boost::asio::post(m_ioContext,
×
644
    [this, address, longAddress, number, value]()
×
645
    {
646
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
647
      engine.functions[number] = value;
×
648
      if(engine.session) // we're in control
×
649
      {
650
        sendSetEngineFunction(*engine.session, number, value);
×
651

652
        engine.lastCommand = std::chrono::steady_clock::now();
×
653

654
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
655
        {
656
          restartEngineKeepAliveTimer();
×
657
        }
658
      }
659
      else // take control
660
      {
661
        sendGetEngineSession(address, longAddress);
×
662
      }
663
    });
×
664
}
×
665

666
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
667
{
668
  assert(isEventLoopThread());
×
669

670
  boost::asio::post(m_ioContext,
×
671
    [this, deviceNumber, on]()
×
672
    {
673
      if(on)
×
674
      {
675
        send(AccessoryShortOn(m_config.shortEventNodeNumber, deviceNumber));
×
676
      }
677
      else
678
      {
679
        send(AccessoryShortOff(m_config.shortEventNodeNumber, deviceNumber));
×
680
      }
681
    });
×
682
}
×
683

684
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
685
{
686
  assert(isEventLoopThread());
×
687

688
  boost::asio::post(m_ioContext,
×
689
    [this, nodeNumber, eventNumber, on]()
×
690
    {
691
      if(on)
×
692
      {
693
        send(AccessoryOn(nodeNumber, eventNumber));
×
694
      }
695
      else
696
      {
697
        send(AccessoryOff(nodeNumber, eventNumber));
×
698
      }
699
    });
×
700
}
×
701

702
void Kernel::setDccAccessory(uint16_t address, bool secondOutput)
×
703
{
704
  assert(isEventLoopThread());
×
705

706
  boost::asio::post(m_ioContext,
×
707
    [this, address, secondOutput]()
×
708
    {
709
      send(RequestDCCPacket<sizeof(DCC::SetSimpleAccessory) + 1>(DCC::SetSimpleAccessory(address, secondOutput, true), Config::dccAccessoryRepeat));
×
710
      const bool wasEmpty = m_dccAccessoryQueue.empty();
×
711
      m_dccAccessoryQueue.emplace(std::make_pair(
×
712
        std::chrono::steady_clock::now() + m_config.dccAccessorySwitchTime,
×
713
        DCC::SetSimpleAccessory(address, secondOutput, false)
×
714
      ));
715
      if(wasEmpty)
×
716
      {
717
        startDccAccessoryTimer();
×
718
      }
719
    });
×
720
}
×
721

722
void Kernel::setDccAdvancedAccessoryValue(uint16_t address, uint8_t aspect)
×
723
{
724
  assert(isEventLoopThread());
×
725

726
  boost::asio::post(m_ioContext,
×
727
    [this, address, aspect]()
×
728
    {
729
      send(RequestDCCPacket<sizeof(DCC::SetAdvancedAccessoryValue) + 1>(DCC::SetAdvancedAccessoryValue(address, aspect), Config::dccExtRepeat));
×
730
    });
×
731
}
×
732

733
bool Kernel::send(std::vector<uint8_t> message)
×
734
{
735
  assert(isEventLoopThread());
×
736

737
  if(!inRange<size_t>(message.size(), 1, 8))
×
738
  {
739
    return false;
×
740
  }
741

742
  boost::asio::post(m_ioContext,
×
743
    [this, msg=std::move(message)]()
×
744
    {
745
      send(*reinterpret_cast<const Message*>(msg.data()));
×
746
    });
×
747

748
  return true;
×
749
}
750

751
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
752
{
753
  assert(isEventLoopThread());
×
754

755
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
756
  {
757
    return false;
×
758
  }
759

760
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
761

762
  boost::asio::post(m_ioContext,
×
763
    [this, packet=std::move(dccPacket), repeat]()
×
764
    {
765
      switch(packet.size())
×
766
      {
767
        case 3:
×
768
          send(RequestDCCPacket<3>(packet, repeat));
×
769
          break;
×
770

771
        case 4:
×
772
          send(RequestDCCPacket<4>(packet, repeat));
×
773
          break;
×
774

775
        case 5:
×
776
          send(RequestDCCPacket<5>(packet, repeat));
×
777
          break;
×
778

779
        case 6:
×
780
          send(RequestDCCPacket<6>(packet, repeat));
×
781
          break;
×
782

783
        default: [[unlikely]]
×
784
          assert(false);
×
785
          break;
786
      }
787
    });
×
788

789
  return true;
×
790
}
791

792
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
793
{
794
  assert(isEventLoopThread());
×
795
  assert(handler);
×
796
  assert(!m_ioHandler);
×
797
  m_ioHandler = std::move(handler);
×
798
}
×
799

800
void Kernel::send(const Message& message)
×
801
{
802
  assert(isKernelThread());
×
803

804
  if(m_config.debugLogRXTX)
×
805
  {
806
    EventLoop::call(
×
807
      [this, msg=toString(message)]()
×
808
      {
809
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
810
      });
×
811
  }
812

NEW
813
  const auto canMessage = toCANMessage(message, m_config.canId);
×
814

815
  if(auto ec = m_ioHandler->send(canMessage); ec)
×
816
  {
817
    (void)ec; // FIXME: handle error
818
  }
819

820
  if(m_hub)
×
821
  {
822
    m_hub->send(canMessage);
×
823
  }
824
}
×
825

826
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
827
{
828
  assert(isKernelThread());
×
829
  const auto key = makeAddressKey(address, longAddress);
×
830
  if(!m_engineGLOCs.contains(key))
×
831
  {
832
    m_engineGLOCs.emplace(key, Owner::Traintastic);
×
833
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
834
  }
835
}
×
836

837
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
838
{
839
  assert(isKernelThread());
×
840
  // FIXME: what to do with: serviceMode and soundControlMode?
841
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
842
}
×
843

844
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
845
{
846
  assert(isKernelThread());
×
847

848
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
849

850
  EventLoop::call(
×
851
    [this, session, speed, directionForward]()
×
852
    {
853
      if(onEngineSpeedDirectionChanged) [[likely]]
×
854
      {
855
        onEngineSpeedDirectionChanged(session, speed, directionForward);
×
856
      }
857
    });
×
858
}
×
859

860
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
861
{
862
  assert(isKernelThread());
×
863

864
  send(SetEngineFunction(session, number, value));
×
865

866
  EventLoop::call(
×
867
    [this, session, number, value]()
×
868
    {
869
      if(onEngineFunctionChanged) [[likely]]
×
870
      {
871
        onEngineFunctionChanged(session, number, value);
×
872
      }
873
    });
×
874
}
×
875

876
void Kernel::receiveGLOC(uint16_t address, bool longAddress, GetEngineSession::Mode /*mode*/)
×
877
{
878
  assert(isKernelThread());
×
879
  const auto key = makeAddressKey(address, longAddress);
×
880
  if(!m_engineGLOCs.contains(key))
×
881
  {
882
    m_engineGLOCs.emplace(key, Owner::CBUS);
×
883
    (void)m_engines[makeAddressKey(address, longAddress)]; // create entry if not exists
×
884
  }
885
}
×
886

887
void Kernel::receiveDFNOx(const SetEngineFunction& message)
×
888
{
889
  assert(isKernelThread());
×
890
  EventLoop::call(
×
891
    [this, message]()
×
892
    {
893
      if(onEngineFunctionChanged) [[likely]]
×
894
      {
895
        onEngineFunctionChanged(message.session, message.number, message.on());
×
896
      }
897
    });
×
898
}
×
899

900
void Kernel::receiveDFUN(const CBUS::SetEngineFunctions& message)
×
901
{
902
  assert(isKernelThread());
×
903
  EventLoop::call(
×
904
    [this, message]()
×
905
    {
906
      if(onEngineFunctionChanged) [[likely]]
×
907
      {
908
        switch(message.range)
×
909
        {
910
          using enum SetEngineFunctions::Range;
911

912
          case F0F4:
×
913
            for(auto fn : message.numbers())
×
914
            {
915
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF0F4&>(message).f(fn));
×
916
            }
917
            break;
×
918

919
          case F5F8:
×
920
            for(auto fn : message.numbers())
×
921
            {
922
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF5F8&>(message).f(fn));
×
923
            }
924
            break;
×
925

926
          case F9F12:
×
927
            for(auto fn : message.numbers())
×
928
            {
929
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF9F12&>(message).f(fn));
×
930
            }
931
            break;
×
932

933
          case F13F20:
×
934
            for(auto fn : message.numbers())
×
935
            {
936
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF13F20&>(message).f(fn));
×
937
            }
938
            break;
×
939

940
          case F21F28:
×
941
            for(auto fn : message.numbers())
×
942
            {
943
              onEngineFunctionChanged(message.session, fn, static_cast<const SetEngineFunctionsF21F28&>(message).f(fn));
×
944
            }
945
            break;
×
946
        }
947
      }
948
    });
×
949
}
×
950

951
void Kernel::receiveDSPD(const SetEngineSpeedDirection& message)
×
952
{
953
  assert(isKernelThread());
×
954
  EventLoop::call(
×
955
    [this, message]()
×
956
    {
957
      if(onEngineSpeedDirectionChanged) [[likely]]
×
958
      {
959
        onEngineSpeedDirectionChanged(message.session, message.speed(), message.directionForward());
×
960
      }
961
    });
×
962
}
×
963

964
void Kernel::receiveKLOC(const ReleaseEngine& message)
×
965
{
966
  assert(isKernelThread());
×
967
  EventLoop::call(
×
968
    [this, session=message.session]()
×
969
    {
970
      if(onEngineSessionReleased) [[likely]]
×
971
      {
972
        onEngineSessionReleased(session);
×
973
      }
974
    });
×
975
}
×
976

977
void Kernel::receiveShortEvent(uint16_t eventNumber, bool on)
×
978
{
979
  assert(isKernelThread());
×
980
  EventLoop::call(
×
981
    [this, eventNumber, on]()
×
982
    {
983
      if(onShortEvent) [[likely]]
×
984
      {
985
        onShortEvent(eventNumber, on);
×
986
      }
987
    });
×
988
}
×
989

990
void Kernel::receiveLongEvent(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
991
{
992
  assert(isKernelThread());
×
993
  EventLoop::call(
×
994
    [this, nodeNumber, eventNumber, on]()
×
995
    {
996
      if(onLongEvent) [[likely]]
×
997
      {
998
        onLongEvent(nodeNumber, eventNumber, on);
×
999
      }
1000
    });
×
1001
}
×
1002

1003
void Kernel::changeState(State value)
×
1004
{
1005
  assert(isKernelThread());
×
1006
  assert(m_state != value);
×
1007

1008
  m_state = value;
×
1009

1010
  switch(m_state)
×
1011
  {
1012
    case State::Initial: [[unlikely]]
×
1013
      assert(false);
×
1014
      break;
1015

1016
    case State::QueryNodes:
×
1017
      send(QueryNodeNumber());
×
1018
      restartInitializationTimer(queryNodeNumberTimeout);
×
1019
      break;
×
1020

1021
    case State::ReadNodeParameters:
×
1022
      readNodeParameter();
×
1023
      break;
×
1024

1025
    case State::GetCommandStationStatus:
×
1026
      send(RequestCommandStationStatus());
×
1027
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
1028
      break;
×
1029

1030
    case State::RequestShortEvents:
×
1031
      requestShortEvent();
×
1032
      break;
×
1033

1034
    case State::RequestLongEvents:
×
1035
      requestLongEvent();
×
1036
      break;
×
1037

1038
    case State::Started:
×
1039
      KernelBase::started();
×
1040
      break;
×
1041
  }
1042
}
×
1043

1044
void Kernel::readNodeParameter()
×
1045
{
1046
  assert(m_state == State::ReadNodeParameters);
×
1047
  if(m_readNodeParameters.empty())
×
1048
  {
1049
    nextState();
×
1050
    return;
×
1051
  }
1052
  send(m_readNodeParameters.front());
×
1053
  restartInitializationTimer(readNodeParameterTimeout);
×
1054
}
1055

1056
void Kernel::requestShortEvent()
×
1057
{
1058
  assert(m_state == State::RequestShortEvents);
×
1059
  if(m_initializationRequestShortEvents.empty())
×
1060
  {
1061
    nextState();
×
1062
    return;
×
1063
  }
1064
  send(AccessoryShortRequestEvent(m_config.shortEventNodeNumber, m_initializationRequestShortEvents.back()));
×
1065
  restartInitializationTimer(requestShortEventTimeout);
×
1066
}
1067

1068
void Kernel::requestLongEvent()
×
1069
{
1070
  assert(m_state == State::RequestLongEvents);
×
1071
  if(m_initializationRequestLongEvents.empty())
×
1072
  {
1073
    nextState();
×
1074
    return;
×
1075
  }
1076
  send(AccessoryRequestEvent(m_initializationRequestLongEvents.back().first, m_initializationRequestLongEvents.back().second));
×
1077
  restartInitializationTimer(requestLongEventTimeout);
×
1078
}
1079

1080
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
1081
{
1082
  assert(isKernelThread());
×
1083

1084
  m_initializationTimer.cancel();
×
1085

1086
  m_initializationTimer.expires_after(timeout);
×
1087
  m_initializationTimer.async_wait(
×
1088
    [this](std::error_code ec)
×
1089
    {
1090
      if(ec)
×
1091
      {
1092
        return;
×
1093
      }
1094

1095
      switch(m_state)
×
1096
      {
1097
        case State::QueryNodes:
×
1098
          nextState();
×
1099
          break;
×
1100

1101
        case State::ReadNodeParameters:
×
1102
          m_readNodeParameters.pop();
×
1103
          readNodeParameter();
×
1104
          break;
×
1105

1106
        case State::GetCommandStationStatus:
×
1107
          nextState();
×
1108
          break;
×
1109

1110
        case State::RequestShortEvents:
×
1111
          m_initializationRequestShortEvents.pop_back();
×
1112
          requestShortEvent();
×
1113
          break;
×
1114

1115
        case State::RequestLongEvents:
×
1116
          m_initializationRequestLongEvents.pop_back();
×
1117
          requestLongEvent();
×
1118
          break;
×
1119

1120
        case State::Initial: [[unlikely]]
×
1121
        case State::Started: [[unlikely]]
×
1122
          assert(false);
×
1123
          break;
1124
      }
1125
    });
1126
}
×
1127

1128
void Kernel::restartEngineKeepAliveTimer()
×
1129
{
1130
  assert(isKernelThread());
×
1131

1132
  m_engineKeepAliveTimer.cancel();
×
1133

1134
  // find first expiring engine:
1135
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
1136
  for(const auto& [_, engine] : m_engines)
×
1137
  {
1138
    if(engine.session && engine.lastCommand < lastUpdate)
×
1139
    {
1140
      lastUpdate = engine.lastCommand;
×
1141
      m_engineKeepAliveSession = *engine.session;
×
1142
    }
1143
  }
1144

1145
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
1146

1147
  if(m_engineKeepAliveTimerActive)
×
1148
  {
1149
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
1150
    m_engineKeepAliveTimer.async_wait(
×
1151
      [this](std::error_code ec)
×
1152
      {
1153
        if(ec)
×
1154
        {
1155
          return;
×
1156
        }
1157

1158
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
1159

1160
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
1161
            [session=m_engineKeepAliveSession](const auto& item)
×
1162
            {
1163
              return item.second.session && *item.second.session == session;
×
1164
            }); it != m_engines.end()) [[likely]]
×
1165
        {
1166
          it->second.lastCommand = std::chrono::steady_clock::now();
×
1167
        }
1168

1169
        restartEngineKeepAliveTimer();
×
1170
      });
1171
  }
1172
}
×
1173

1174
void Kernel::startDccAccessoryTimer()
×
1175
{
1176
  assert(isKernelThread());
×
1177

1178
  if(m_dccAccessoryQueue.empty()) [[unlikely]]
×
1179
  {
1180
    return;
×
1181
  }
1182

1183
  m_dccAccessoryTimer.expires_at(m_dccAccessoryQueue.front().first);
×
1184
  m_dccAccessoryTimer.async_wait(
×
1185
    [this](std::error_code ec)
×
1186
    {
1187
      if(ec)
×
1188
      {
1189
        return;
×
1190
      }
1191

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

1194
      m_dccAccessoryQueue.pop();
×
1195

1196
      if(!m_dccAccessoryQueue.empty())
×
1197
      {
1198
        startDccAccessoryTimer();
×
1199
      }
1200
    });
1201
}
1202

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