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

traintastic / traintastic / 23088148108

14 Mar 2026 12:40PM UTC coverage: 26.661% (-0.2%) from 26.84%
23088148108

push

github

reinder
[cbus] added input support for short/long events

0 of 133 new or added lines in 4 files covered. (0.0%)

587 existing lines in 20 files now uncovered.

8246 of 30929 relevant lines covered (26.66%)

185.83 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
{
66
  assert(isEventLoopThread());
×
67
}
×
68

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

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

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

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

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

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

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

123
  m_ioContext.stop();
×
124

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

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

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

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

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

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

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

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

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

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

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

228
            EventLoop::call(
×
229
              [this, key=it->first]()
×
230
              {
231
                if(onEngineSessionCancelled) [[likely]]
×
232
                {
233
                  onEngineSessionCancelled(key & 0x3FFF, (key & 0xC000) == 0xC000);
×
234
                }
235
              });
×
236
          }
237
          break;
×
238

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

283
        if(auto it = m_engines.find(key); it != m_engines.end())
×
284
        {
285
          auto& engine = it->second;
×
286
          engine.session = ploc.session;
×
287

288
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
289
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
290

291
          for(const auto [number, value] : engine.functions)
×
292
          {
293
            sendSetEngineFunction(ploc.session, number, value);
×
294
          }
295
        }
296
        else // we're no longer in need of control (rare but possible)
297
        {
298
          send(ReleaseEngine(ploc.session));
×
299
        }
300
      }
301
      break;
×
302
    }
303
    default:
×
304
      break;
×
305
  }
306
}
×
307

308
void Kernel::trackOff()
×
309
{
310
  assert(isEventLoopThread());
×
311

312
  m_ioContext.post(
×
313
    [this]()
×
314
    {
315
      if(m_trackOn)
×
316
      {
317
        send(RequestTrackOff());
×
318
      }
319
    });
×
320
}
×
321

322
void Kernel::trackOn()
×
323
{
324
  assert(isEventLoopThread());
×
325

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

336
void Kernel::requestEmergencyStop()
×
337
{
338
  assert(isEventLoopThread());
×
339

340
  m_ioContext.post(
×
341
    [this]()
×
342
    {
343
      send(RequestEmergencyStop());
×
344
    });
×
345
}
×
346

347
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
348
{
349
  assert(isEventLoopThread());
×
350

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

353
  m_ioContext.post(
×
354
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
355
    {
356
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
357
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
358
      engine.speedSteps = speedSteps;
×
359
      engine.speed = speed;
×
360
      engine.directionForward = directionForward;
×
361

362
      if(engine.session) // we're in control
×
363
      {
364
        if(speedStepsChanged)
×
365
        {
366
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
367
        }
368
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
369
      }
370
      else // take control
371
      {
372
        sendGetEngineSession(address, longAddress);
×
373
      }
374
    });
×
375
}
×
376

377
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
378
{
379
  assert(isEventLoopThread());
×
380

381
  m_ioContext.post(
×
382
    [this, address, longAddress, number, value]()
×
383
    {
384
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
385
      engine.functions[number] = value;
×
386
      if(engine.session) // we're in control
×
387
      {
388
        sendSetEngineFunction(*engine.session, number, value);
×
389
      }
390
      else // take control
391
      {
392
        sendGetEngineSession(address, longAddress);
×
393
      }
394
    });
×
395
}
×
396

397
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
398
{
399
  assert(isEventLoopThread());
×
400

401
  m_ioContext.post(
×
402
    [this, deviceNumber, on]()
×
403
    {
404
      if(on)
×
405
      {
406
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
407
      }
408
      else
409
      {
410
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
411
      }
412
    });
×
413
}
×
414

415
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
416
{
417
  assert(isEventLoopThread());
×
418

419
  m_ioContext.post(
×
420
    [this, nodeNumber, eventNumber, on]()
×
421
    {
422
      if(on)
×
423
      {
424
        send(AccessoryOn(nodeNumber, eventNumber));
×
425
      }
426
      else
427
      {
428
        send(AccessoryOff(nodeNumber, eventNumber));
×
429
      }
430
    });
×
431
}
×
432

433
bool Kernel::send(std::vector<uint8_t> message)
×
434
{
435
  assert(isEventLoopThread());
×
436

437
  if(!inRange<size_t>(message.size(), 1, 8))
×
438
  {
439
    return false;
×
440
  }
441

442
  m_ioContext.post(
×
443
    [this, msg=std::move(message)]()
×
444
    {
445
      send(*reinterpret_cast<const Message*>(msg.data()));
×
446
    });
×
447

448
  return true;
×
449
}
450

451
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
452
{
453
  assert(isEventLoopThread());
×
454

455
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
456
  {
457
    return false;
×
458
  }
459

460
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
461

462
  m_ioContext.post(
×
463
    [this, packet=std::move(dccPacket), repeat]()
×
464
    {
465
      switch(packet.size())
×
466
      {
467
        case 3:
×
468
          send(RequestDCCPacket<3>(packet, repeat));
×
469
          break;
×
470

471
        case 4:
×
472
          send(RequestDCCPacket<4>(packet, repeat));
×
473
          break;
×
474

475
        case 5:
×
476
          send(RequestDCCPacket<5>(packet, repeat));
×
477
          break;
×
478

479
        case 6:
×
480
          send(RequestDCCPacket<6>(packet, repeat));
×
481
          break;
×
482

483
        default: [[unlikely]]
×
484
          assert(false);
×
485
          break;
486
      }
487
    });
×
488

489
  return true;
×
490
}
491

492
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
493
{
494
  assert(isEventLoopThread());
×
495
  assert(handler);
×
496
  assert(!m_ioHandler);
×
497
  m_ioHandler = std::move(handler);
×
498
}
×
499

500
void Kernel::send(const Message& message)
×
501
{
502
  assert(isKernelThread());
×
503

504
  if(m_config.debugLogRXTX)
×
505
  {
506
    EventLoop::call(
×
507
      [this, msg=toString(message)]()
×
508
      {
509
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
510
      });
×
511
  }
512

513
  if(auto ec = m_ioHandler->send(message); ec)
×
514
  {
515
    (void)ec; // FIXME: handle error
516
  }
517
}
×
518

519
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
520
{
521
  assert(isKernelThread());
×
522
  const auto key = makeAddressKey(address, longAddress);
×
523
  if(!m_engineGLOCs.contains(key))
×
524
  {
525
    m_engineGLOCs.emplace(key);
×
526
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
527
  }
528
}
×
529

530
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
531
{
532
  assert(isKernelThread());
×
533
  // FIXME: what to do with: serviceMode and soundControlMode?
534
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
535
}
×
536

537
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
538
{
539
  assert(isKernelThread());
×
540
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
541
}
×
542

543
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
544
{
545
  assert(isKernelThread());
×
546
  if(value)
×
547
  {
548
    send(SetEngineFunctionOn(session, number));
×
549
  }
550
  else
551
  {
552
    send(SetEngineFunctionOff(session, number));
×
553
  }
554
}
×
555

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