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

traintastic / traintastic / 22823086659

08 Mar 2026 02:30PM UTC coverage: 26.831% (+0.06%) from 26.774%
22823086659

push

github

reinder
[cbus] renamed CBUSAccessory(Short) to Long/Short event and added node setting for Long events.

0 of 15 new or added lines in 2 files covered. (0.0%)

1909 existing lines in 38 files now uncovered.

8230 of 30674 relevant lines covered (26.83%)

186.8 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

176
    case OpCode::ERR:
×
177
    {
178
      switch(static_cast<const CommandStationErrorMessage&>(message).errorCode)
×
179
      {
180
        using enum DCCErr;
181

182
        case LocoStackFull:
×
183
        {
184
          const auto& err = static_cast<const CommandStationLocoStackFullError&>(message);
×
185
          const auto key = makeAddressKey(err.address(), err.isLongAddress());
×
186
          if(m_engineGLOCs.contains(key))
×
187
          {
188
            m_engineGLOCs.erase(key);
×
189
            // FIXME: log error
190
          }
191
          break;
×
192
        }
193
        case SessionCancelled:
×
194
          if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
195
            [session=static_cast<const CommandStationSessionCancelled&>(message).session()](const auto& item)
×
196
            {
197
              return item.second.session && *item.second.session == session;
×
198
            }); it != m_engines.end())
×
199
          {
200
            it->second.session = std::nullopt;
×
201

202
            EventLoop::call(
×
203
              [this, key=it->first]()
×
204
              {
205
                if(onEngineSessionCancelled) [[likely]]
×
206
                {
207
                  onEngineSessionCancelled(key & 0x3FFF, (key & 0xC000) == 0xC000);
×
208
                }
209
              });
×
210
          }
211
          break;
×
212

213
        case LocoAddressTaken:
×
214
        case SessionNotPresent:
215
        case ConsistEmpty:
216
        case LocoNotFound:
217
        case CANBusError:
218
        case InvalidRequest:
219
          break;
×
220
      }
221
      break;
×
222
    }
223
    case OpCode::PLOC:
×
224
    {
225
      const auto& ploc = static_cast<const EngineReport&>(message);
×
226
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
227
      if(m_engineGLOCs.contains(key))
×
228
      {
229
        m_engineGLOCs.erase(key);
×
230

231
        if(auto it = m_engines.find(key); it != m_engines.end())
×
232
        {
233
          auto& engine = it->second;
×
234
          engine.session = ploc.session;
×
235

236
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
237
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
238

239
          for(const auto [number, value] : engine.functions)
×
240
          {
241
            sendSetEngineFunction(ploc.session, number, value);
×
242
          }
243
        }
244
        else // we're no longer in need of control (rare but possible)
245
        {
246
          send(ReleaseEngine(ploc.session));
×
247
        }
248
      }
249
      break;
×
250
    }
251
    default:
×
252
      break;
×
253
  }
254
}
×
255

256
void Kernel::trackOff()
×
257
{
258
  assert(isEventLoopThread());
×
259

260
  m_ioContext.post(
×
261
    [this]()
×
262
    {
263
      if(m_trackOn)
×
264
      {
265
        send(RequestTrackOff());
×
266
      }
267
    });
×
268
}
×
269

270
void Kernel::trackOn()
×
271
{
272
  assert(isEventLoopThread());
×
273

274
  m_ioContext.post(
×
275
    [this]()
×
276
    {
277
      if(!m_trackOn)
×
278
      {
279
        send(RequestTrackOn());
×
280
      }
281
    });
×
282
}
×
283

284
void Kernel::requestEmergencyStop()
×
285
{
286
  assert(isEventLoopThread());
×
287

288
  m_ioContext.post(
×
289
    [this]()
×
290
    {
291
      send(RequestEmergencyStop());
×
292
    });
×
293
}
×
294

295
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
296
{
297
  assert(isEventLoopThread());
×
298

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

301
  m_ioContext.post(
×
302
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
303
    {
304
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
305
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
306
      engine.speedSteps = speedSteps;
×
307
      engine.speed = speed;
×
308
      engine.directionForward = directionForward;
×
309

310
      if(engine.session) // we're in control
×
311
      {
312
        if(speedStepsChanged)
×
313
        {
314
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
315
        }
316
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
317
      }
318
      else // take control
319
      {
320
        sendGetEngineSession(address, longAddress);
×
321
      }
322
    });
×
323
}
×
324

325
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
326
{
327
  assert(isEventLoopThread());
×
328

329
  m_ioContext.post(
×
330
    [this, address, longAddress, number, value]()
×
331
    {
332
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
333
      engine.functions[number] = value;
×
334
      if(engine.session) // we're in control
×
335
      {
336
        sendSetEngineFunction(*engine.session, number, value);
×
337
      }
338
      else // take control
339
      {
340
        sendGetEngineSession(address, longAddress);
×
341
      }
342
    });
×
343
}
×
344

345
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
346
{
347
  assert(isEventLoopThread());
×
348

349
  m_ioContext.post(
×
350
    [this, deviceNumber, on]()
×
351
    {
352
      if(on)
×
353
      {
354
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
355
      }
356
      else
357
      {
358
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
359
      }
360
    });
×
361
}
×
362

NEW
363
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
364
{
365
  assert(isEventLoopThread());
×
366

367
  m_ioContext.post(
×
NEW
368
    [this, nodeNumber, eventNumber, on]()
×
369
    {
370
      if(on)
×
371
      {
NEW
372
        send(AccessoryOn(nodeNumber, eventNumber));
×
373
      }
374
      else
375
      {
NEW
376
        send(AccessoryOff(nodeNumber, eventNumber));
×
377
      }
378
    });
×
379
}
×
380

381
bool Kernel::send(std::vector<uint8_t> message)
×
382
{
383
  assert(isEventLoopThread());
×
384

385
  if(!inRange<size_t>(message.size(), 1, 8))
×
386
  {
387
    return false;
×
388
  }
389

390
  m_ioContext.post(
×
391
    [this, msg=std::move(message)]()
×
392
    {
393
      send(*reinterpret_cast<const Message*>(msg.data()));
×
394
    });
×
395

396
  return true;
×
397
}
398

399
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
400
{
401
  assert(isEventLoopThread());
×
402

403
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
404
  {
405
    return false;
×
406
  }
407

408
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
409

410
  m_ioContext.post(
×
411
    [this, packet=std::move(dccPacket), repeat]()
×
412
    {
413
      switch(packet.size())
×
414
      {
415
        case 3:
×
416
          send(RequestDCCPacket<3>(packet, repeat));
×
417
          break;
×
418

419
        case 4:
×
420
          send(RequestDCCPacket<4>(packet, repeat));
×
421
          break;
×
422

423
        case 5:
×
424
          send(RequestDCCPacket<5>(packet, repeat));
×
425
          break;
×
426

427
        case 6:
×
428
          send(RequestDCCPacket<6>(packet, repeat));
×
429
          break;
×
430

431
        default: [[unlikely]]
×
432
          assert(false);
×
433
          break;
434
      }
435
    });
×
436

437
  return true;
×
438
}
439

440
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
441
{
442
  assert(isEventLoopThread());
×
443
  assert(handler);
×
444
  assert(!m_ioHandler);
×
445
  m_ioHandler = std::move(handler);
×
446
}
×
447

448
void Kernel::send(const Message& message)
×
449
{
450
  assert(isKernelThread());
×
451

452
  if(m_config.debugLogRXTX)
×
453
  {
454
    EventLoop::call(
×
455
      [this, msg=toString(message)]()
×
456
      {
457
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
458
      });
×
459
  }
460

461
  if(auto ec = m_ioHandler->send(message); ec)
×
462
  {
463
    (void)ec; // FIXME: handle error
464
  }
465
}
×
466

467
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
468
{
469
  assert(isKernelThread());
×
470
  const auto key = makeAddressKey(address, longAddress);
×
471
  if(!m_engineGLOCs.contains(key))
×
472
  {
473
    m_engineGLOCs.emplace(key);
×
474
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
475
  }
476
}
×
477

478
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
479
{
480
  assert(isKernelThread());
×
481
  // FIXME: what to do with: serviceMode and soundControlMode?
482
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
483
}
×
484

485
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
486
{
487
  assert(isKernelThread());
×
488
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
489
}
×
490

491
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
492
{
493
  assert(isKernelThread());
×
494
  if(value)
×
495
  {
496
    send(SetEngineFunctionOn(session, number));
×
497
  }
498
  else
499
  {
500
    send(SetEngineFunctionOff(session, number));
×
501
  }
502
}
×
503

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