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

traintastic / traintastic / 23391602376

21 Mar 2026 11:53PM UTC coverage: 26.259% (-0.2%) from 26.481%
23391602376

push

github

reinder
[cbus] added node list

0 of 264 new or added lines in 7 files covered. (0.0%)

15 existing lines in 2 files now uncovered.

8246 of 31402 relevant lines covered (26.26%)

183.04 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
using namespace std::chrono_literals;
36

37
static constexpr auto queryNodeNumberTimeout = 100ms;
38
static constexpr auto requestCommandStationStatusTimeout = 100ms;
39

40
constexpr uint16_t makeAddressKey(uint16_t address, bool longAddress)
×
41
{
42
  return longAddress ? (0xC000 | (address & 0x3FFF)) : (address & 0x7F);
×
43
}
44

45
constexpr CBUS::SetEngineSessionMode::SpeedMode toSpeedMode(uint8_t speedSteps)
×
46
{
47
  using enum CBUS::SetEngineSessionMode::SpeedMode;
48

49
  switch(speedSteps)
×
50
  {
51
    case 14:
×
52
      return SpeedMode14;
×
53

54
    case 28:
×
55
      return SpeedMode28;
×
56

57
    default:
×
58
      return SpeedMode128;
×
59
  }
60
}
61

62
}
63

64
namespace CBUS {
65

66
Kernel::Kernel(std::string logId_, const Config& config, bool simulation)
×
67
  : KernelBase(std::move(logId_))
×
68
  , m_simulation{simulation}
×
69
  , m_initializationTimer{ioContext()}
×
70
  , m_config{config}
×
71
  , m_engineKeepAliveTimer{ioContext()}
×
72
  , m_dccAccessoryTimer{ioContext()}
×
73
{
74
  assert(isEventLoopThread());
×
75
}
×
76

77
void Kernel::setConfig(const Config& config)
×
78
{
79
  assert(isEventLoopThread());
×
80

81
  m_ioContext.post(
×
82
    [this, newConfig=config]()
×
83
    {
84
      m_config = newConfig;
×
85
    });
×
86
}
×
87

88
void Kernel::start()
×
89
{
90
  assert(isEventLoopThread());
×
91
  assert(m_ioHandler);
×
92

93
  m_thread = std::thread(
×
94
    [this]()
×
95
    {
96
      setThreadName("cbus");
×
97
      auto work = std::make_shared<boost::asio::io_context::work>(m_ioContext);
×
98
      m_ioContext.run();
×
99
    });
×
100

101
  m_ioContext.post(
×
102
    [this]()
×
103
    {
104
      try
105
      {
106
        m_ioHandler->start();
×
107
      }
108
      catch(const LogMessageException& e)
×
109
      {
110
        EventLoop::call(
×
111
          [this, e]()
×
112
          {
113
            Log::log(logId, e.message(), e.args());
×
114
            error();
×
115
          });
×
116
        return;
×
117
      }
×
118
    });
119
}
×
120

121
void Kernel::stop()
×
122
{
123
  assert(isEventLoopThread());
×
124

125
  m_ioContext.post(
×
126
    [this]()
×
127
    {
128
      m_ioHandler->stop();
×
129
    });
×
130

131
  m_ioContext.stop();
×
132

133
  m_thread.join();
×
134
}
×
135

136
void Kernel::started()
×
137
{
138
  assert(isKernelThread());
×
139

140
  nextState();
×
141
}
×
142

NEW
143
void Kernel::receive(uint8_t canId, const Message& message)
×
144
{
145
  assert(isKernelThread());
×
146

147
  if(m_config.debugLogRXTX)
×
148
  {
149
    EventLoop::call(
×
150
      [this, msg=toString(message)]()
×
151
      {
152
        Log::log(logId, LogMessage::D2002_RX_X, msg);
×
153
      });
×
154
  }
155

156
  switch(message.opCode)
×
157
  {
158
    case OpCode::TOF:
×
159
      m_trackOn = false;
×
160
      if(onTrackOff) [[likely]]
×
161
      {
162
        EventLoop::call(onTrackOff);
×
163
      }
164
      break;
×
165

166
    case OpCode::TON:
×
167
      m_trackOn = true;
×
168
      if(onTrackOn) [[likely]]
×
169
      {
170
        EventLoop::call(onTrackOn);
×
171
      }
172
      break;
×
173

174
    case OpCode::ESTOP:
×
175
      if(onEmergencyStop) [[likely]]
×
176
      {
177
        EventLoop::call(onEmergencyStop);
×
178
      }
179
      break;
×
180

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

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

233
            if(m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *it->second.session)
×
234
            {
235
              restartEngineKeepAliveTimer();
×
236
            }
237

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

249
        case LocoAddressTaken:
×
250
        case SessionNotPresent:
251
        case ConsistEmpty:
252
        case LocoNotFound:
253
        case CANBusError:
254
        case InvalidRequest:
255
          break;
×
256
      }
257
      break;
×
258
    }
259
    case OpCode::ACON:
×
260
    {
261
      const auto& acon = static_cast<const AccessoryOn&>(message);
×
262
      EventLoop::call(
×
263
        [this, nodeNumber=acon.nodeNumber(), eventNumber=acon.eventNumber()]()
×
264
        {
265
          if(onLongEvent) [[likely]]
×
266
          {
267
            onLongEvent(nodeNumber, eventNumber, true);
×
268
          }
269
        });
×
270
      break;
×
271
    }
272
    case OpCode::ACOF:
×
273
    {
274
      const auto& acof = static_cast<const AccessoryOff&>(message);
×
275
      EventLoop::call(
×
276
        [this, nodeNumber=acof.nodeNumber(), eventNumber=acof.eventNumber()]()
×
277
        {
278
          if(onLongEvent) [[likely]]
×
279
          {
280
            onLongEvent(nodeNumber, eventNumber, false);
×
281
          }
282
        });
×
283
      break;
×
284
    }
285
    case OpCode::PNN:
×
286
      if(m_state == State::QueryNodes)
×
287
      {
288
        restartInitializationTimer(queryNodeNumberTimeout);
×
NEW
289
        EventLoop::call(
×
NEW
290
          [this, canId, pnn=static_cast<const PresenceOfNode&>(message)]()
×
291
          {
NEW
292
            if(onPresenceOfNode) [[likely]]
×
293
            {
NEW
294
              onPresenceOfNode(canId, pnn.nodeNumber(), pnn.manufacturerId, pnn.moduleId);
×
295
            }
NEW
296
          });
×
297
      }
298
      break;
×
299

300
    case OpCode::PLOC:
×
301
    {
302
      const auto& ploc = static_cast<const EngineReport&>(message);
×
303
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
304
      if(m_engineGLOCs.contains(key))
×
305
      {
306
        m_engineGLOCs.erase(key);
×
307

308
        if(auto it = m_engines.find(key); it != m_engines.end())
×
309
        {
310
          auto& engine = it->second;
×
311
          engine.session = ploc.session;
×
312

313
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
314
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
315

316
          for(const auto& [number, value] : engine.functions)
×
317
          {
318
            sendSetEngineFunction(ploc.session, number, value);
×
319
          }
320

321
          engine.lastCommand = std::chrono::steady_clock::now();
×
322

323
          if(!m_engineKeepAliveTimerActive)
×
324
          {
325
            restartEngineKeepAliveTimer();
×
326
          }
327
        }
328
        else // we're no longer in need of control (rare but possible)
329
        {
330
          send(ReleaseEngine(ploc.session));
×
331
        }
332
      }
333
      break;
×
334
    }
335
    case OpCode::STAT:
×
336
      if(m_state == State::GetCommandStationStatus)
×
337
      {
338
        m_initializationTimer.cancel();
×
339
        nextState();
×
340
      }
341
      break;
×
342

343
    default:
×
344
      break;
×
345
  }
346
}
×
347

348
void Kernel::trackOff()
×
349
{
350
  assert(isEventLoopThread());
×
351

352
  m_ioContext.post(
×
353
    [this]()
×
354
    {
355
      if(m_trackOn)
×
356
      {
357
        send(RequestTrackOff());
×
358
      }
359
    });
×
360
}
×
361

362
void Kernel::trackOn()
×
363
{
364
  assert(isEventLoopThread());
×
365

366
  m_ioContext.post(
×
367
    [this]()
×
368
    {
369
      if(!m_trackOn)
×
370
      {
371
        send(RequestTrackOn());
×
372
      }
373
    });
×
374
}
×
375

376
void Kernel::requestEmergencyStop()
×
377
{
378
  assert(isEventLoopThread());
×
379

380
  m_ioContext.post(
×
381
    [this]()
×
382
    {
383
      send(RequestEmergencyStop());
×
384
    });
×
385
}
×
386

387
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
388
{
389
  assert(isEventLoopThread());
×
390

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

393
  m_ioContext.post(
×
394
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
395
    {
396
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
397
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
398
      engine.speedSteps = speedSteps;
×
399
      engine.speed = speed;
×
400
      engine.directionForward = directionForward;
×
401

402
      if(engine.session) // we're in control
×
403
      {
404
        if(speedStepsChanged)
×
405
        {
406
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
407
        }
408
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
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::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
425
{
426
  assert(isEventLoopThread());
×
427

428
  m_ioContext.post(
×
429
    [this, address, longAddress, number, value]()
×
430
    {
431
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
432
      engine.functions[number] = value;
×
433
      if(engine.session) // we're in control
×
434
      {
435
        sendSetEngineFunction(*engine.session, number, value);
×
436

437
        engine.lastCommand = std::chrono::steady_clock::now();
×
438

439
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
440
        {
441
          restartEngineKeepAliveTimer();
×
442
        }
443
      }
444
      else // take control
445
      {
446
        sendGetEngineSession(address, longAddress);
×
447
      }
448
    });
×
449
}
×
450

451
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
452
{
453
  assert(isEventLoopThread());
×
454

455
  m_ioContext.post(
×
456
    [this, deviceNumber, on]()
×
457
    {
458
      if(on)
×
459
      {
460
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
461
      }
462
      else
463
      {
464
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
465
      }
466
    });
×
467
}
×
468

469
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
470
{
471
  assert(isEventLoopThread());
×
472

473
  m_ioContext.post(
×
474
    [this, nodeNumber, eventNumber, on]()
×
475
    {
476
      if(on)
×
477
      {
478
        send(AccessoryOn(nodeNumber, eventNumber));
×
479
      }
480
      else
481
      {
482
        send(AccessoryOff(nodeNumber, eventNumber));
×
483
      }
484
    });
×
485
}
×
486

487
void Kernel::setDccAccessory(uint16_t address, bool secondOutput)
×
488
{
489
  assert(isEventLoopThread());
×
490

491
  m_ioContext.post(
×
492
    [this, address, secondOutput]()
×
493
    {
494
      send(RequestDCCPacket<sizeof(DCC::SetSimpleAccessory) + 1>(DCC::SetSimpleAccessory(address, secondOutput, true), Config::dccAccessoryRepeat));
×
495
      const bool wasEmpty = m_dccAccessoryQueue.empty();
×
496
      m_dccAccessoryQueue.emplace(std::make_pair(
×
497
        std::chrono::steady_clock::now() + m_config.dccAccessorySwitchTime,
×
498
        DCC::SetSimpleAccessory(address, secondOutput, false)
×
499
      ));
500
      if(wasEmpty)
×
501
      {
502
        startDccAccessoryTimer();
×
503
      }
504
    });
×
505
}
×
506

507
void Kernel::setDccAdvancedAccessoryValue(uint16_t address, uint8_t aspect)
×
508
{
509
  assert(isEventLoopThread());
×
510

511
  m_ioContext.post(
×
512
    [this, address, aspect]()
×
513
    {
514
      send(RequestDCCPacket<sizeof(DCC::SetAdvancedAccessoryValue) + 1>(DCC::SetAdvancedAccessoryValue(address, aspect), Config::dccExtRepeat));
×
515
    });
×
516
}
×
517

518
bool Kernel::send(std::vector<uint8_t> message)
×
519
{
520
  assert(isEventLoopThread());
×
521

522
  if(!inRange<size_t>(message.size(), 1, 8))
×
523
  {
524
    return false;
×
525
  }
526

527
  m_ioContext.post(
×
528
    [this, msg=std::move(message)]()
×
529
    {
530
      send(*reinterpret_cast<const Message*>(msg.data()));
×
531
    });
×
532

533
  return true;
×
534
}
535

536
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
537
{
538
  assert(isEventLoopThread());
×
539

540
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
541
  {
542
    return false;
×
543
  }
544

545
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
546

547
  m_ioContext.post(
×
548
    [this, packet=std::move(dccPacket), repeat]()
×
549
    {
550
      switch(packet.size())
×
551
      {
552
        case 3:
×
553
          send(RequestDCCPacket<3>(packet, repeat));
×
554
          break;
×
555

556
        case 4:
×
557
          send(RequestDCCPacket<4>(packet, repeat));
×
558
          break;
×
559

560
        case 5:
×
561
          send(RequestDCCPacket<5>(packet, repeat));
×
562
          break;
×
563

564
        case 6:
×
565
          send(RequestDCCPacket<6>(packet, repeat));
×
566
          break;
×
567

568
        default: [[unlikely]]
×
569
          assert(false);
×
570
          break;
571
      }
572
    });
×
573

574
  return true;
×
575
}
576

577
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
578
{
579
  assert(isEventLoopThread());
×
580
  assert(handler);
×
581
  assert(!m_ioHandler);
×
582
  m_ioHandler = std::move(handler);
×
583
}
×
584

585
void Kernel::send(const Message& message)
×
586
{
587
  assert(isKernelThread());
×
588

589
  if(m_config.debugLogRXTX)
×
590
  {
591
    EventLoop::call(
×
592
      [this, msg=toString(message)]()
×
593
      {
594
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
595
      });
×
596
  }
597

598
  if(auto ec = m_ioHandler->send(message); ec)
×
599
  {
600
    (void)ec; // FIXME: handle error
601
  }
602
}
×
603

604
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
605
{
606
  assert(isKernelThread());
×
607
  const auto key = makeAddressKey(address, longAddress);
×
608
  if(!m_engineGLOCs.contains(key))
×
609
  {
610
    m_engineGLOCs.emplace(key);
×
611
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
612
  }
613
}
×
614

615
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
616
{
617
  assert(isKernelThread());
×
618
  // FIXME: what to do with: serviceMode and soundControlMode?
619
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
620
}
×
621

622
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
623
{
624
  assert(isKernelThread());
×
625
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
626
}
×
627

628
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
629
{
630
  assert(isKernelThread());
×
631
  if(value)
×
632
  {
633
    send(SetEngineFunctionOn(session, number));
×
634
  }
635
  else
636
  {
637
    send(SetEngineFunctionOff(session, number));
×
638
  }
639
}
×
640

641
void Kernel::changeState(State value)
×
642
{
643
  assert(isKernelThread());
×
644
  assert(m_state != value);
×
645

646
  m_state = value;
×
647

648
  switch(m_state)
×
649
  {
650
    case State::Initial:
×
651
      break;
×
652

653
    case State::QueryNodes:
×
654
      send(QueryNodeNumber());
×
655
      restartInitializationTimer(queryNodeNumberTimeout);
×
656
      break;
×
657

658
    case State::GetCommandStationStatus:
×
659
      send(RequestCommandStationStatus());
×
660
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
661
      break;
×
662

663
    case State::Started:
×
664
      KernelBase::started();
×
665
      break;
×
666
  }
667
}
×
668

669
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
670
{
671
  assert(isKernelThread());
×
672

673
  m_initializationTimer.cancel();
×
674

675
  m_initializationTimer.expires_after(timeout);
×
676
  m_initializationTimer.async_wait(
×
677
    [this](std::error_code ec)
×
678
    {
679
      if(ec)
×
680
      {
681
        return;
×
682
      }
683

684
      switch(m_state)
×
685
      {
686
        case State::QueryNodes:
×
687
          nextState();
×
688
          break;
×
689

690
        case State::GetCommandStationStatus:
×
691
          nextState();
×
692
          break;
×
693

694
        case State::Initial:
×
695
        case State::Started:
696
          assert(false);
×
697
          break;
698
      }
699
    });
700
}
×
701

702
void Kernel::restartEngineKeepAliveTimer()
×
703
{
704
  assert(isKernelThread());
×
705

706
  m_engineKeepAliveTimer.cancel();
×
707

708
  // find first expiring engine:
709
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
710
  for(const auto& [_, engine] : m_engines)
×
711
  {
712
    if(engine.session && engine.lastCommand < lastUpdate)
×
713
    {
714
      lastUpdate = engine.lastCommand;
×
715
      m_engineKeepAliveSession = *engine.session;
×
716
    }
717
  }
718

719
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
720

721
  if(m_engineKeepAliveTimerActive)
×
722
  {
723
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
724
    m_engineKeepAliveTimer.async_wait(
×
725
      [this](std::error_code ec)
×
726
      {
727
        if(ec)
×
728
        {
729
          return;
×
730
        }
731

732
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
733

734
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
735
            [session=m_engineKeepAliveSession](const auto& item)
×
736
            {
737
              return item.second.session && *item.second.session == session;
×
738
            }); it != m_engines.end()) [[likely]]
×
739
        {
740
          it->second.lastCommand = std::chrono::steady_clock::now();
×
741
        }
742

743
        restartEngineKeepAliveTimer();
×
744
      });
745
  }
746
}
×
747

748
void Kernel::startDccAccessoryTimer()
×
749
{
750
  assert(isKernelThread());
×
751

752
  if(m_dccAccessoryQueue.empty()) [[unlikely]]
×
753
  {
754
    return;
×
755
  }
756

757
  m_dccAccessoryTimer.expires_at(m_dccAccessoryQueue.front().first);
×
758
  m_dccAccessoryTimer.async_wait(
×
759
    [this](std::error_code ec)
×
760
    {
761
      if(ec)
×
762
      {
763
        return;
×
764
      }
765

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

768
      m_dccAccessoryQueue.pop();
×
769

770
      if(!m_dccAccessoryQueue.empty())
×
771
      {
772
        startDccAccessoryTimer();
×
773
      }
774
    });
775
}
776

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