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

traintastic / traintastic / 26121453997

19 May 2026 07:52PM UTC coverage: 25.063% (-0.6%) from 25.624%
26121453997

Pull #221

github

web-flow
Merge 598936246 into 15e38bcf7
Pull Request #221: Xpressnet new messages

49 of 1129 new or added lines in 16 files covered. (4.34%)

782 existing lines in 21 files now uncovered.

8483 of 33847 relevant lines covered (25.06%)

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

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

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

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

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

263
    case OpCode::ARSON:
×
264
    {
265
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
266
      receiveShortEvent(eventNumber, true);
×
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::ARSOF:
×
276
    {
277
      const auto eventNumber = static_cast<const AccessoryShortResponseEventOff&>(message).deviceNumber();
×
UNCOV
278
      receiveShortEvent(eventNumber, false);
×
UNCOV
279
      if(m_state == State::RequestShortEvents &&
×
UNCOV
280
          m_initializationRequestShortEvents.back() == eventNumber)
×
281
      {
UNCOV
282
        m_initializationRequestShortEvents.pop_back();
×
283
        requestShortEvent();
×
284
      }
285
      break;
×
286
    }
287
    case OpCode::ERR:
×
288
    {
UNCOV
289
      switch(static_cast<const CommandStationErrorMessage&>(message).errorCode)
×
290
      {
291
        using enum DCCErr;
292

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

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

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

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

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

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

397
          readNodeParameter();
×
398
        }
399
      }
UNCOV
400
      break;
×
401

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

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

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

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

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

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

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

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

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

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

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

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

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

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

510
    default:
×
UNCOV
511
      break;
×
512
  }
513

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

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

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

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

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

UNCOV
548
  return m_onReceiveHandle;
×
549
}
550

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

555
  m_onReceiveCallbacks.erase(handle);
×
556

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
749
  return true;
×
750
}
751

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

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

UNCOV
761
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
762

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

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

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

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

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

UNCOV
790
  return true;
×
791
}
792

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

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

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

814
  const auto canMessage = toCANMessage(message, m_canId);
×
815

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1009
  m_state = value;
×
1010

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

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

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

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

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

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

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

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

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

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

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

1085
  m_initializationTimer.cancel();
×
1086

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

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

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

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

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

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

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

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

UNCOV
1133
  m_engineKeepAliveTimer.cancel();
×
1134

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

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

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

UNCOV
1159
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
1160

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

UNCOV
1170
        restartEngineKeepAliveTimer();
×
1171
      });
1172
  }
UNCOV
1173
}
×
1174

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

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

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

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

UNCOV
1195
      m_dccAccessoryQueue.pop();
×
1196

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

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