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

traintastic / traintastic / 24288605255

11 Apr 2026 06:17PM UTC coverage: 25.599% (-2.4%) from 27.99%
24288605255

push

github

web-flow
Merge pull request #222 from traintastic/cbus

Added CBUS/VLCB hardware support

169 of 3369 new or added lines in 99 files covered. (5.02%)

5 existing lines in 4 files now uncovered.

8300 of 32423 relevant lines covered (25.6%)

178.31 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

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

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

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

NEW
59
    case 28:
×
NEW
60
      return SpeedMode28;
×
61

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

67
}
68

69
namespace CBUS {
70

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

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

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

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

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

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

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

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

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

NEW
133
        m_ioHandler->start();
×
134

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

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

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

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

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

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

NEW
182
  nextState();
×
NEW
183
}
×
184

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
465
    default:
×
NEW
466
      break;
×
467
  }
468

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

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

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

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

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

NEW
503
  return m_onReceiveHandle;
×
504
}
505

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

NEW
510
  m_onReceiveCallbacks.erase(handle);
×
511

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
704
  return true;
×
705
}
706

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

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

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

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

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

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

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

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

NEW
745
  return true;
×
746
}
747

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
953
  m_state = value;
×
954

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

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

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

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

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

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

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

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

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

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

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

NEW
1029
  m_initializationTimer.cancel();
×
1030

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

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

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

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

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

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

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

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

NEW
1077
  m_engineKeepAliveTimer.cancel();
×
1078

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

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

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

NEW
1103
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
1104

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

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

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

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

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

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

NEW
1139
      m_dccAccessoryQueue.pop();
×
1140

NEW
1141
      if(!m_dccAccessoryQueue.empty())
×
1142
      {
NEW
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