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

traintastic / traintastic / 23119302850

15 Mar 2026 09:04PM UTC coverage: 26.621% (-0.04%) from 26.658%
23119302850

push

github

reinder
[cbus] added missing translations

8246 of 30975 relevant lines covered (26.62%)

185.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/server/src/hardware/protocol/cbus/cbuskernel.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2026 Reinder Feenstra
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
 */
21

22
#include "cbuskernel.hpp"
23
#include "cbusmessages.hpp"
24
#include "cbustostring.hpp"
25
#include "simulator/cbussimulator.hpp"
26
#include "../dcc/dcc.hpp"
27
#include "../../../core/eventloop.hpp"
28
#include "../../../log/log.hpp"
29
#include "../../../log/logmessageexception.hpp"
30
#include "../../../utils/inrange.hpp"
31
#include "../../../utils/setthreadname.hpp"
32

33
namespace {
34

35
constexpr uint16_t makeAddressKey(uint16_t address, bool longAddress)
×
36
{
37
  return longAddress ? (0xC000 | (address & 0x3FFF)) : (address & 0x7F);
×
38
}
39

40
constexpr CBUS::SetEngineSessionMode::SpeedMode toSpeedMode(uint8_t speedSteps)
×
41
{
42
  using enum CBUS::SetEngineSessionMode::SpeedMode;
43

44
  switch(speedSteps)
×
45
  {
46
    case 14:
×
47
      return SpeedMode14;
×
48

49
    case 28:
×
50
      return SpeedMode28;
×
51

52
    default:
×
53
      return SpeedMode128;
×
54
  }
55
}
56

57
}
58

59
namespace CBUS {
60

61
Kernel::Kernel(std::string logId_, const Config& config, bool simulation)
×
62
  : KernelBase(std::move(logId_))
×
63
  , m_simulation{simulation}
×
64
  , m_config{config}
×
65
  , m_engineKeepAliveTimer{ioContext()}
×
66
{
67
  assert(isEventLoopThread());
×
68
}
×
69

70
void Kernel::setConfig(const Config& config)
×
71
{
72
  assert(isEventLoopThread());
×
73

74
  m_ioContext.post(
×
75
    [this, newConfig=config]()
×
76
    {
77
      m_config = newConfig;
×
78
    });
×
79
}
×
80

81
void Kernel::start()
×
82
{
83
  assert(isEventLoopThread());
×
84
  assert(m_ioHandler);
×
85

86
  m_thread = std::thread(
×
87
    [this]()
×
88
    {
89
      setThreadName("cbus");
×
90
      auto work = std::make_shared<boost::asio::io_context::work>(m_ioContext);
×
91
      m_ioContext.run();
×
92
    });
×
93

94
  m_ioContext.post(
×
95
    [this]()
×
96
    {
97
      try
98
      {
99
        m_ioHandler->start();
×
100
      }
101
      catch(const LogMessageException& e)
×
102
      {
103
        EventLoop::call(
×
104
          [this, e]()
×
105
          {
106
            Log::log(logId, e.message(), e.args());
×
107
            error();
×
108
          });
×
109
        return;
×
110
      }
×
111
    });
112
}
×
113

114
void Kernel::stop()
×
115
{
116
  assert(isEventLoopThread());
×
117

118
  m_ioContext.post(
×
119
    [this]()
×
120
    {
121
      m_ioHandler->stop();
×
122
    });
×
123

124
  m_ioContext.stop();
×
125

126
  m_thread.join();
×
127
}
×
128

129
void Kernel::started()
×
130
{
131
  assert(isKernelThread());
×
132

133
  send(RequestCommandStationStatus());
×
134
  send(QueryNodeNumber());
×
135

136
  ::KernelBase::started();
×
137
}
×
138

139
void Kernel::receive(uint8_t /*canId*/, const Message& message)
×
140
{
141
  assert(isKernelThread());
×
142

143
  if(m_config.debugLogRXTX)
×
144
  {
145
    EventLoop::call(
×
146
      [this, msg=toString(message)]()
×
147
      {
148
        Log::log(logId, LogMessage::D2002_RX_X, msg);
×
149
      });
×
150
  }
151

152
  switch(message.opCode)
×
153
  {
154
    case OpCode::TOF:
×
155
      m_trackOn = false;
×
156
      if(onTrackOff) [[likely]]
×
157
      {
158
        EventLoop::call(onTrackOff);
×
159
      }
160
      break;
×
161

162
    case OpCode::TON:
×
163
      m_trackOn = true;
×
164
      if(onTrackOn) [[likely]]
×
165
      {
166
        EventLoop::call(onTrackOn);
×
167
      }
168
      break;
×
169

170
    case OpCode::ESTOP:
×
171
      if(onEmergencyStop) [[likely]]
×
172
      {
173
        EventLoop::call(onEmergencyStop);
×
174
      }
175
      break;
×
176

177
    case OpCode::ASON:
×
178
    {
179
      const auto& ason = static_cast<const AccessoryShortOn&>(message);
×
180
      EventLoop::call(
×
181
        [this, eventNumber=ason.deviceNumber()]()
×
182
        {
183
          if(onShortEvent) [[likely]]
×
184
          {
185
            onShortEvent(eventNumber, true);
×
186
          }
187
        });
×
188
      break;
×
189
    }
190
    case OpCode::ASOF:
×
191
    {
192
      const auto& asof = static_cast<const AccessoryShortOff&>(message);
×
193
      EventLoop::call(
×
194
        [this, eventNumber=asof.deviceNumber()]()
×
195
        {
196
          if(onShortEvent) [[likely]]
×
197
          {
198
            onShortEvent(eventNumber, false);
×
199
          }
200
        });
×
201
      break;
×
202
    }
203
    case OpCode::ERR:
×
204
    {
205
      switch(static_cast<const CommandStationErrorMessage&>(message).errorCode)
×
206
      {
207
        using enum DCCErr;
208

209
        case LocoStackFull:
×
210
        {
211
          const auto& err = static_cast<const CommandStationLocoStackFullError&>(message);
×
212
          const auto key = makeAddressKey(err.address(), err.isLongAddress());
×
213
          if(m_engineGLOCs.contains(key))
×
214
          {
215
            m_engineGLOCs.erase(key);
×
216
            // FIXME: log error
217
          }
218
          break;
×
219
        }
220
        case SessionCancelled:
×
221
          if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
222
            [session=static_cast<const CommandStationSessionCancelled&>(message).session()](const auto& item)
×
223
            {
224
              return item.second.session && *item.second.session == session;
×
225
            }); it != m_engines.end())
×
226
          {
227
            it->second.session = std::nullopt;
×
228

229
            if(m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *it->second.session)
×
230
            {
231
              restartEngineKeepAliveTimer();
×
232
            }
233

234
            EventLoop::call(
×
235
              [this, key=it->first]()
×
236
              {
237
                if(onEngineSessionCancelled) [[likely]]
×
238
                {
239
                  onEngineSessionCancelled(key & 0x3FFF, (key & 0xC000) == 0xC000);
×
240
                }
241
              });
×
242
          }
243
          break;
×
244

245
        case LocoAddressTaken:
×
246
        case SessionNotPresent:
247
        case ConsistEmpty:
248
        case LocoNotFound:
249
        case CANBusError:
250
        case InvalidRequest:
251
          break;
×
252
      }
253
      break;
×
254
    }
255
    case OpCode::ACON:
×
256
    {
257
      const auto& acon = static_cast<const AccessoryOn&>(message);
×
258
      EventLoop::call(
×
259
        [this, nodeNumber=acon.nodeNumber(), eventNumber=acon.eventNumber()]()
×
260
        {
261
          if(onLongEvent) [[likely]]
×
262
          {
263
            onLongEvent(nodeNumber, eventNumber, false);
×
264
          }
265
        });
×
266
      break;
×
267
    }
268
    case OpCode::ACOF:
×
269
    {
270
      const auto& acof = static_cast<const AccessoryOff&>(message);
×
271
      EventLoop::call(
×
272
        [this, nodeNumber=acof.nodeNumber(), eventNumber=acof.eventNumber()]()
×
273
        {
274
          if(onLongEvent) [[likely]]
×
275
          {
276
            onLongEvent(nodeNumber, eventNumber, false);
×
277
          }
278
        });
×
279
      break;
×
280
    }
281
    case OpCode::PLOC:
×
282
    {
283
      const auto& ploc = static_cast<const EngineReport&>(message);
×
284
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
285
      if(m_engineGLOCs.contains(key))
×
286
      {
287
        m_engineGLOCs.erase(key);
×
288

289
        if(auto it = m_engines.find(key); it != m_engines.end())
×
290
        {
291
          auto& engine = it->second;
×
292
          engine.session = ploc.session;
×
293

294
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
295
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
296

297
          for(const auto& [number, value] : engine.functions)
×
298
          {
299
            sendSetEngineFunction(ploc.session, number, value);
×
300
          }
301

302
          engine.lastCommand = std::chrono::steady_clock::now();
×
303

304
          if(!m_engineKeepAliveTimerActive)
×
305
          {
306
            restartEngineKeepAliveTimer();
×
307
          }
308
        }
309
        else // we're no longer in need of control (rare but possible)
310
        {
311
          send(ReleaseEngine(ploc.session));
×
312
        }
313
      }
314
      break;
×
315
    }
316
    default:
×
317
      break;
×
318
  }
319
}
×
320

321
void Kernel::trackOff()
×
322
{
323
  assert(isEventLoopThread());
×
324

325
  m_ioContext.post(
×
326
    [this]()
×
327
    {
328
      if(m_trackOn)
×
329
      {
330
        send(RequestTrackOff());
×
331
      }
332
    });
×
333
}
×
334

335
void Kernel::trackOn()
×
336
{
337
  assert(isEventLoopThread());
×
338

339
  m_ioContext.post(
×
340
    [this]()
×
341
    {
342
      if(!m_trackOn)
×
343
      {
344
        send(RequestTrackOn());
×
345
      }
346
    });
×
347
}
×
348

349
void Kernel::requestEmergencyStop()
×
350
{
351
  assert(isEventLoopThread());
×
352

353
  m_ioContext.post(
×
354
    [this]()
×
355
    {
356
      send(RequestEmergencyStop());
×
357
    });
×
358
}
×
359

360
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
361
{
362
  assert(isEventLoopThread());
×
363

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

366
  m_ioContext.post(
×
367
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
368
    {
369
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
370
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
371
      engine.speedSteps = speedSteps;
×
372
      engine.speed = speed;
×
373
      engine.directionForward = directionForward;
×
374

375
      if(engine.session) // we're in control
×
376
      {
377
        if(speedStepsChanged)
×
378
        {
379
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
380
        }
381
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
382

383
        engine.lastCommand = std::chrono::steady_clock::now();
×
384

385
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
386
        {
387
          restartEngineKeepAliveTimer();
×
388
        }
389
      }
390
      else // take control
391
      {
392
        sendGetEngineSession(address, longAddress);
×
393
      }
394
    });
×
395
}
×
396

397
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
398
{
399
  assert(isEventLoopThread());
×
400

401
  m_ioContext.post(
×
402
    [this, address, longAddress, number, value]()
×
403
    {
404
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
405
      engine.functions[number] = value;
×
406
      if(engine.session) // we're in control
×
407
      {
408
        sendSetEngineFunction(*engine.session, number, value);
×
409

410
        engine.lastCommand = std::chrono::steady_clock::now();
×
411

412
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
413
        {
414
          restartEngineKeepAliveTimer();
×
415
        }
416
      }
417
      else // take control
418
      {
419
        sendGetEngineSession(address, longAddress);
×
420
      }
421
    });
×
422
}
×
423

424
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
425
{
426
  assert(isEventLoopThread());
×
427

428
  m_ioContext.post(
×
429
    [this, deviceNumber, on]()
×
430
    {
431
      if(on)
×
432
      {
433
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
434
      }
435
      else
436
      {
437
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
438
      }
439
    });
×
440
}
×
441

442
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
443
{
444
  assert(isEventLoopThread());
×
445

446
  m_ioContext.post(
×
447
    [this, nodeNumber, eventNumber, on]()
×
448
    {
449
      if(on)
×
450
      {
451
        send(AccessoryOn(nodeNumber, eventNumber));
×
452
      }
453
      else
454
      {
455
        send(AccessoryOff(nodeNumber, eventNumber));
×
456
      }
457
    });
×
458
}
×
459

460
bool Kernel::send(std::vector<uint8_t> message)
×
461
{
462
  assert(isEventLoopThread());
×
463

464
  if(!inRange<size_t>(message.size(), 1, 8))
×
465
  {
466
    return false;
×
467
  }
468

469
  m_ioContext.post(
×
470
    [this, msg=std::move(message)]()
×
471
    {
472
      send(*reinterpret_cast<const Message*>(msg.data()));
×
473
    });
×
474

475
  return true;
×
476
}
477

478
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
479
{
480
  assert(isEventLoopThread());
×
481

482
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
483
  {
484
    return false;
×
485
  }
486

487
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
488

489
  m_ioContext.post(
×
490
    [this, packet=std::move(dccPacket), repeat]()
×
491
    {
492
      switch(packet.size())
×
493
      {
494
        case 3:
×
495
          send(RequestDCCPacket<3>(packet, repeat));
×
496
          break;
×
497

498
        case 4:
×
499
          send(RequestDCCPacket<4>(packet, repeat));
×
500
          break;
×
501

502
        case 5:
×
503
          send(RequestDCCPacket<5>(packet, repeat));
×
504
          break;
×
505

506
        case 6:
×
507
          send(RequestDCCPacket<6>(packet, repeat));
×
508
          break;
×
509

510
        default: [[unlikely]]
×
511
          assert(false);
×
512
          break;
513
      }
514
    });
×
515

516
  return true;
×
517
}
518

519
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
520
{
521
  assert(isEventLoopThread());
×
522
  assert(handler);
×
523
  assert(!m_ioHandler);
×
524
  m_ioHandler = std::move(handler);
×
525
}
×
526

527
void Kernel::send(const Message& message)
×
528
{
529
  assert(isKernelThread());
×
530

531
  if(m_config.debugLogRXTX)
×
532
  {
533
    EventLoop::call(
×
534
      [this, msg=toString(message)]()
×
535
      {
536
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
537
      });
×
538
  }
539

540
  if(auto ec = m_ioHandler->send(message); ec)
×
541
  {
542
    (void)ec; // FIXME: handle error
543
  }
544
}
×
545

546
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
547
{
548
  assert(isKernelThread());
×
549
  const auto key = makeAddressKey(address, longAddress);
×
550
  if(!m_engineGLOCs.contains(key))
×
551
  {
552
    m_engineGLOCs.emplace(key);
×
553
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
554
  }
555
}
×
556

557
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
558
{
559
  assert(isKernelThread());
×
560
  // FIXME: what to do with: serviceMode and soundControlMode?
561
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
562
}
×
563

564
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
565
{
566
  assert(isKernelThread());
×
567
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
568
}
×
569

570
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
571
{
572
  assert(isKernelThread());
×
573
  if(value)
×
574
  {
575
    send(SetEngineFunctionOn(session, number));
×
576
  }
577
  else
578
  {
579
    send(SetEngineFunctionOff(session, number));
×
580
  }
581
}
×
582

583
void Kernel::restartEngineKeepAliveTimer()
×
584
{
585
  m_engineKeepAliveTimer.cancel();
×
586

587
  // find first expiring engine:
588
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
589
  for(const auto& [_, engine] : m_engines)
×
590
  {
591
    if(engine.session && engine.lastCommand < lastUpdate)
×
592
    {
593
      lastUpdate = engine.lastCommand;
×
594
      m_engineKeepAliveSession = *engine.session;
×
595
    }
596
  }
597

598
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
599

600
  if(m_engineKeepAliveTimerActive)
×
601
  {
602
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
603
    m_engineKeepAliveTimer.async_wait(
×
604
      [this](std::error_code ec)
×
605
      {
606
        if(ec)
×
607
        {
608
          return;
×
609
        }
610

611
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
612

613
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
614
            [session=m_engineKeepAliveSession](const auto& item)
×
615
            {
616
              return item.second.session && *item.second.session == session;
×
617
            }); it != m_engines.end()) [[likely]]
×
618
        {
619
          it->second.lastCommand = std::chrono::steady_clock::now();
×
620
        }
621

622
        restartEngineKeepAliveTimer();
×
623
      });
624
  }
625
}
×
626

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