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

traintastic / traintastic / 23378856294

21 Mar 2026 11:37AM UTC coverage: 26.481% (-0.05%) from 26.526%
23378856294

push

github

reinder
[cbus] added DCC accessory output channel

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

3 existing lines in 2 files now uncovered.

8246 of 31139 relevant lines covered (26.48%)

184.58 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()}
×
NEW
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

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);
×
289
      }
290
      break;
×
291

292
    case OpCode::PLOC:
×
293
    {
294
      const auto& ploc = static_cast<const EngineReport&>(message);
×
295
      const auto key = makeAddressKey(ploc.address(), ploc.isLongAddress());
×
296
      if(m_engineGLOCs.contains(key))
×
297
      {
298
        m_engineGLOCs.erase(key);
×
299

300
        if(auto it = m_engines.find(key); it != m_engines.end())
×
301
        {
302
          auto& engine = it->second;
×
303
          engine.session = ploc.session;
×
304

305
          sendSetEngineSessionMode(ploc.session, engine.speedSteps);
×
306
          sendSetEngineSpeedDirection(ploc.session, engine.speed, engine.directionForward);
×
307

308
          for(const auto& [number, value] : engine.functions)
×
309
          {
310
            sendSetEngineFunction(ploc.session, number, value);
×
311
          }
312

313
          engine.lastCommand = std::chrono::steady_clock::now();
×
314

315
          if(!m_engineKeepAliveTimerActive)
×
316
          {
317
            restartEngineKeepAliveTimer();
×
318
          }
319
        }
320
        else // we're no longer in need of control (rare but possible)
321
        {
322
          send(ReleaseEngine(ploc.session));
×
323
        }
324
      }
325
      break;
×
326
    }
327
    case OpCode::STAT:
×
328
      if(m_state == State::GetCommandStationStatus)
×
329
      {
330
        m_initializationTimer.cancel();
×
331
        nextState();
×
332
      }
333
      break;
×
334

335
    default:
×
336
      break;
×
337
  }
338
}
×
339

340
void Kernel::trackOff()
×
341
{
342
  assert(isEventLoopThread());
×
343

344
  m_ioContext.post(
×
345
    [this]()
×
346
    {
347
      if(m_trackOn)
×
348
      {
349
        send(RequestTrackOff());
×
350
      }
351
    });
×
352
}
×
353

354
void Kernel::trackOn()
×
355
{
356
  assert(isEventLoopThread());
×
357

358
  m_ioContext.post(
×
359
    [this]()
×
360
    {
361
      if(!m_trackOn)
×
362
      {
363
        send(RequestTrackOn());
×
364
      }
365
    });
×
366
}
×
367

368
void Kernel::requestEmergencyStop()
×
369
{
370
  assert(isEventLoopThread());
×
371

372
  m_ioContext.post(
×
373
    [this]()
×
374
    {
375
      send(RequestEmergencyStop());
×
376
    });
×
377
}
×
378

379
void Kernel::setEngineSpeedDirection(uint16_t address, bool longAddress, uint8_t speedStep, uint8_t speedSteps, bool eStop, bool directionForward)
×
380
{
381
  assert(isEventLoopThread());
×
382

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

385
  m_ioContext.post(
×
386
    [this, address, longAddress, speed, speedSteps, directionForward]()
×
387
    {
388
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
389
      const bool speedStepsChanged = engine.speedSteps != speedSteps;
×
390
      engine.speedSteps = speedSteps;
×
391
      engine.speed = speed;
×
392
      engine.directionForward = directionForward;
×
393

394
      if(engine.session) // we're in control
×
395
      {
396
        if(speedStepsChanged)
×
397
        {
398
          sendSetEngineSessionMode(*engine.session, engine.speedSteps);
×
399
        }
400
        sendSetEngineSpeedDirection(*engine.session, engine.speed, engine.directionForward);
×
401

402
        engine.lastCommand = std::chrono::steady_clock::now();
×
403

404
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
405
        {
406
          restartEngineKeepAliveTimer();
×
407
        }
408
      }
409
      else // take control
410
      {
411
        sendGetEngineSession(address, longAddress);
×
412
      }
413
    });
×
414
}
×
415

416
void Kernel::setEngineFunction(uint16_t address, bool longAddress, uint8_t number, bool value)
×
417
{
418
  assert(isEventLoopThread());
×
419

420
  m_ioContext.post(
×
421
    [this, address, longAddress, number, value]()
×
422
    {
423
      auto& engine = m_engines[makeAddressKey(address, longAddress)];
×
424
      engine.functions[number] = value;
×
425
      if(engine.session) // we're in control
×
426
      {
427
        sendSetEngineFunction(*engine.session, number, value);
×
428

429
        engine.lastCommand = std::chrono::steady_clock::now();
×
430

431
        if(!m_engineKeepAliveTimerActive || (m_engineKeepAliveTimerActive && m_engineKeepAliveSession == *engine.session))
×
432
        {
433
          restartEngineKeepAliveTimer();
×
434
        }
435
      }
436
      else // take control
437
      {
438
        sendGetEngineSession(address, longAddress);
×
439
      }
440
    });
×
441
}
×
442

443
void Kernel::setAccessoryShort(uint16_t deviceNumber, bool on)
×
444
{
445
  assert(isEventLoopThread());
×
446

447
  m_ioContext.post(
×
448
    [this, deviceNumber, on]()
×
449
    {
450
      if(on)
×
451
      {
452
        send(AccessoryShortOn(Config::nodeId, deviceNumber));
×
453
      }
454
      else
455
      {
456
        send(AccessoryShortOff(Config::nodeId, deviceNumber));
×
457
      }
458
    });
×
459
}
×
460

461
void Kernel::setAccessory(uint16_t nodeNumber, uint16_t eventNumber, bool on)
×
462
{
463
  assert(isEventLoopThread());
×
464

465
  m_ioContext.post(
×
466
    [this, nodeNumber, eventNumber, on]()
×
467
    {
468
      if(on)
×
469
      {
470
        send(AccessoryOn(nodeNumber, eventNumber));
×
471
      }
472
      else
473
      {
474
        send(AccessoryOff(nodeNumber, eventNumber));
×
475
      }
476
    });
×
477
}
×
478

NEW
479
void Kernel::setDccAccessory(uint16_t address, bool secondOutput)
×
480
{
NEW
481
  assert(isEventLoopThread());
×
482

NEW
483
  m_ioContext.post(
×
NEW
484
    [this, address, secondOutput]()
×
485
    {
NEW
486
      send(RequestDCCPacket<sizeof(DCC::SetSimpleAccessory) + 1>(DCC::SetSimpleAccessory(address, secondOutput, true), Config::dccAccessoryRepeat));
×
NEW
487
      const bool wasEmpty = m_dccAccessoryQueue.empty();
×
NEW
488
      m_dccAccessoryQueue.emplace(std::make_pair(
×
NEW
489
        std::chrono::steady_clock::now() + m_config.dccAccessorySwitchTime,
×
NEW
490
        DCC::SetSimpleAccessory(address, secondOutput, false)
×
491
      ));
NEW
492
      if(wasEmpty)
×
493
      {
NEW
494
        startDccAccessoryTimer();
×
495
      }
NEW
496
    });
×
NEW
497
}
×
498

UNCOV
499
void Kernel::setDccAdvancedAccessoryValue(uint16_t address, uint8_t aspect)
×
500
{
501
  assert(isEventLoopThread());
×
502

503
  m_ioContext.post(
×
504
    [this, address, aspect]()
×
505
    {
506
      send(RequestDCCPacket<sizeof(DCC::SetAdvancedAccessoryValue) + 1>(DCC::SetAdvancedAccessoryValue(address, aspect), Config::dccExtRepeat));
×
507
    });
×
508
}
×
509

510
bool Kernel::send(std::vector<uint8_t> message)
×
511
{
512
  assert(isEventLoopThread());
×
513

514
  if(!inRange<size_t>(message.size(), 1, 8))
×
515
  {
516
    return false;
×
517
  }
518

519
  m_ioContext.post(
×
520
    [this, msg=std::move(message)]()
×
521
    {
522
      send(*reinterpret_cast<const Message*>(msg.data()));
×
523
    });
×
524

525
  return true;
×
526
}
527

528
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
×
529
{
530
  assert(isEventLoopThread());
×
531

532
  if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
×
533
  {
534
    return false;
×
535
  }
536

537
  dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
×
538

539
  m_ioContext.post(
×
540
    [this, packet=std::move(dccPacket), repeat]()
×
541
    {
542
      switch(packet.size())
×
543
      {
544
        case 3:
×
545
          send(RequestDCCPacket<3>(packet, repeat));
×
546
          break;
×
547

548
        case 4:
×
549
          send(RequestDCCPacket<4>(packet, repeat));
×
550
          break;
×
551

552
        case 5:
×
553
          send(RequestDCCPacket<5>(packet, repeat));
×
554
          break;
×
555

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

560
        default: [[unlikely]]
×
561
          assert(false);
×
562
          break;
563
      }
564
    });
×
565

566
  return true;
×
567
}
568

569
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
570
{
571
  assert(isEventLoopThread());
×
572
  assert(handler);
×
573
  assert(!m_ioHandler);
×
574
  m_ioHandler = std::move(handler);
×
575
}
×
576

577
void Kernel::send(const Message& message)
×
578
{
579
  assert(isKernelThread());
×
580

581
  if(m_config.debugLogRXTX)
×
582
  {
583
    EventLoop::call(
×
584
      [this, msg=toString(message)]()
×
585
      {
586
        Log::log(logId, LogMessage::D2001_TX_X, msg);
×
587
      });
×
588
  }
589

590
  if(auto ec = m_ioHandler->send(message); ec)
×
591
  {
592
    (void)ec; // FIXME: handle error
593
  }
594
}
×
595

596
void Kernel::sendGetEngineSession(uint16_t address, bool longAddress)
×
597
{
598
  assert(isKernelThread());
×
599
  const auto key = makeAddressKey(address, longAddress);
×
600
  if(!m_engineGLOCs.contains(key))
×
601
  {
602
    m_engineGLOCs.emplace(key);
×
603
    send(GetEngineSession(address, longAddress, GetEngineSession::Mode::Steal));
×
604
  }
605
}
×
606

607
void Kernel::sendSetEngineSessionMode(uint8_t session, uint8_t speedSteps)
×
608
{
609
  assert(isKernelThread());
×
610
  // FIXME: what to do with: serviceMode and soundControlMode?
611
  send(SetEngineSessionMode(session, toSpeedMode(speedSteps), false, false));
×
612
}
×
613

614
void Kernel::sendSetEngineSpeedDirection(uint8_t session, uint8_t speed, bool directionForward)
×
615
{
616
  assert(isKernelThread());
×
617
  send(SetEngineSpeedDirection(session, speed, directionForward));
×
618
}
×
619

620
void Kernel::sendSetEngineFunction(uint8_t session, uint8_t number, bool value)
×
621
{
622
  assert(isKernelThread());
×
623
  if(value)
×
624
  {
625
    send(SetEngineFunctionOn(session, number));
×
626
  }
627
  else
628
  {
629
    send(SetEngineFunctionOff(session, number));
×
630
  }
631
}
×
632

633
void Kernel::changeState(State value)
×
634
{
635
  assert(isKernelThread());
×
636
  assert(m_state != value);
×
637

638
  m_state = value;
×
639

640
  switch(m_state)
×
641
  {
642
    case State::Initial:
×
643
      break;
×
644

645
    case State::QueryNodes:
×
646
      send(QueryNodeNumber());
×
647
      restartInitializationTimer(queryNodeNumberTimeout);
×
648
      break;
×
649

650
    case State::GetCommandStationStatus:
×
651
      send(RequestCommandStationStatus());
×
652
      restartInitializationTimer(requestCommandStationStatusTimeout);
×
653
      break;
×
654

655
    case State::Started:
×
656
      KernelBase::started();
×
657
      break;
×
658
  }
659
}
×
660

661
void Kernel::restartInitializationTimer(std::chrono::milliseconds timeout)
×
662
{
663
  assert(isKernelThread());
×
664

665
  m_initializationTimer.cancel();
×
666

667
  m_initializationTimer.expires_after(timeout);
×
668
  m_initializationTimer.async_wait(
×
669
    [this](std::error_code ec)
×
670
    {
671
      if(ec)
×
672
      {
673
        return;
×
674
      }
675

676
      switch(m_state)
×
677
      {
678
        case State::QueryNodes:
×
679
          nextState();
×
680
          break;
×
681

682
        case State::GetCommandStationStatus:
×
683
          nextState();
×
684
          break;
×
685

686
        case State::Initial:
×
687
        case State::Started:
688
          assert(false);
×
689
          break;
690
      }
691
    });
692
}
×
693

694
void Kernel::restartEngineKeepAliveTimer()
×
695
{
696
  assert(isKernelThread());
×
697

698
  m_engineKeepAliveTimer.cancel();
×
699

700
  // find first expiring engine:
701
  std::chrono::steady_clock::time_point lastUpdate = std::chrono::steady_clock::time_point::max();
×
702
  for(const auto& [_, engine] : m_engines)
×
703
  {
704
    if(engine.session && engine.lastCommand < lastUpdate)
×
705
    {
706
      lastUpdate = engine.lastCommand;
×
707
      m_engineKeepAliveSession = *engine.session;
×
708
    }
709
  }
710

711
  m_engineKeepAliveTimerActive = (lastUpdate < std::chrono::steady_clock::time_point::max());
×
712

713
  if(m_engineKeepAliveTimerActive)
×
714
  {
715
    m_engineKeepAliveTimer.expires_at(lastUpdate + m_config.engineKeepAlive);
×
716
    m_engineKeepAliveTimer.async_wait(
×
717
      [this](std::error_code ec)
×
718
      {
719
        if(ec)
×
720
        {
721
          return;
×
722
        }
723

724
        send(SessionKeepAlive(m_engineKeepAliveSession));
×
725

726
        if(auto it = std::find_if(m_engines.begin(), m_engines.end(),
×
727
            [session=m_engineKeepAliveSession](const auto& item)
×
728
            {
729
              return item.second.session && *item.second.session == session;
×
730
            }); it != m_engines.end()) [[likely]]
×
731
        {
732
          it->second.lastCommand = std::chrono::steady_clock::now();
×
733
        }
734

735
        restartEngineKeepAliveTimer();
×
736
      });
737
  }
738
}
×
739

NEW
740
void Kernel::startDccAccessoryTimer()
×
741
{
NEW
742
  assert(isKernelThread());
×
743

NEW
744
  if(m_dccAccessoryQueue.empty()) [[unlikely]]
×
745
  {
NEW
746
    return;
×
747
  }
748

NEW
749
  m_dccAccessoryTimer.expires_at(m_dccAccessoryQueue.front().first);
×
NEW
750
  m_dccAccessoryTimer.async_wait(
×
NEW
751
    [this](std::error_code ec)
×
752
    {
NEW
753
      if(ec)
×
754
      {
NEW
755
        return;
×
756
      }
757

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

NEW
760
      m_dccAccessoryQueue.pop();
×
761

NEW
762
      if(!m_dccAccessoryQueue.empty())
×
763
      {
NEW
764
        startDccAccessoryTimer();
×
765
      }
766
    });
767
}
768

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