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

traintastic / traintastic / 23669027368

27 Mar 2026 09:50PM UTC coverage: 26.198% (+0.02%) from 26.176%
23669027368

push

github

reinder
Merge remote-tracking branch 'origin/master' into cbus

11 of 144 new or added lines in 34 files covered. (7.64%)

1 existing line in 1 file now uncovered.

8256 of 31514 relevant lines covered (26.2%)

182.55 hits per line

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

0.0
/server/src/hardware/protocol/z21/clientkernel.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2021-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 "clientkernel.hpp"
23
#include "messages.hpp"
24
#include "../../decoder/decoder.hpp"
25
#include "../../decoder/decoderchangeflags.hpp"
26
#include "../../protocol/dcc/dcc.hpp"
27
#include "../../input/inputcontroller.hpp"
28
#include "../../output/outputcontroller.hpp"
29
#include "../../../core/eventloop.hpp"
30
#include "../../../log/log.hpp"
31
#include "../../../utils/inrange.hpp"
32

33
namespace Z21 {
34

35
ClientKernel::ClientKernel(std::string logId_, const ClientConfig& config, bool simulation)
×
36
  : Kernel(std::move(logId_))
×
37
  , m_simulation{simulation}
×
38
  , m_keepAliveTimer(m_ioContext)
×
39
  , m_inactiveDecoderPurgeTimer(m_ioContext)
×
40
  , m_schedulePendingRequestTimer(m_ioContext)
×
41
  , m_config{config}
×
42
{
43
}
×
44

45
void ClientKernel::setConfig(const ClientConfig& config)
×
46
{
NEW
47
  boost::asio::post(m_ioContext, 
×
48
    [this, newConfig=config]()
×
49
    {
50
      m_config = newConfig;
×
51
    });
×
52
}
×
53

54
void ClientKernel::receive(const Message& message)
×
55
{
56
  if(m_config.debugLogRXTX)
×
57
    EventLoop::call(
×
58
      [this, msg=toString(message)]()
×
59
      {
60
        Log::log(logId, LogMessage::D2002_RX_X, msg);
×
61
      });
×
62

63
  auto matchedRequest = matchPendingReplyAndRemove(message);
×
64

65
  switch(message.header())
×
66
  {
67
    case LAN_X:
×
68
    {
69
      const auto& lanX = static_cast<const LanX&>(message);
×
70

71
      if(!LanX::isChecksumValid(lanX))
×
72
        break;
×
73

74
      switch(lanX.xheader)
×
75
      {
76
        case LAN_X_TURNOUT_INFO:
×
77
          if(message.dataLen() == sizeof(LanXTurnoutInfo))
×
78
          {
79
            const auto& reply = static_cast<const LanXTurnoutInfo&>(message);
×
80
            OutputPairValue value = OutputPairValue::Undefined;
×
81
            if(!reply.positionUnknown())
×
82
            {
83
              value = reply.state() ? OutputPairValue::Second : OutputPairValue::First;
×
84
            }
85

86
            EventLoop::call(
×
87
              [this, address=reply.address(), value]()
×
88
              {
89
                m_outputController->updateOutputValue(OutputChannel::Accessory, OutputAddress(address), value);
×
90
              });
×
91
          }
92
          break;
×
93

94
        case LAN_X_EXT_ACCESSORY_INFO:
×
95
          if(message.dataLen() == sizeof(LanXExtAccessoryInfo))
×
96
          {
97
            const auto& reply = static_cast<const LanXExtAccessoryInfo&>(message);
×
98
            if(reply.isDataValid())
×
99
            {
100
              EventLoop::call(
×
101
                [this, address=reply.address(), value=reply.aspect()]()
×
102
                {
103
                  m_outputController->updateOutputValue(OutputChannel::DCCext, OutputAddress(address), value);
×
104
                });
×
105
            }
106
          }
107
          break;
×
108

109
        case LAN_X_BC:
×
110
          if(message == LanXBCTrackPowerOff() || message == LanXBCTrackShortCircuit())
×
111
          {
112
            EventLoop::call(
×
113
              [this]()
×
114
              {
115
                if(m_trackPowerOn != TriState::False)
×
116
                {
117
                  m_trackPowerOn = TriState::False;
×
118
                  m_emergencyStop = TriState::False;
×
119
                  if(m_onTrackPowerChanged)
×
120
                    m_onTrackPowerChanged(false, false);
×
121
                }
122
              });
×
123
          }
124
          else if(message == LanXBCTrackPowerOn())
×
125
          {
126
            EventLoop::call(
×
127
              [this]()
×
128
              {
129
                if(m_trackPowerOn != TriState::True || m_emergencyStop != TriState::False)
×
130
                {
131
                  m_trackPowerOn = TriState::True;
×
132
                  m_emergencyStop = TriState::False;
×
133
                  if(m_onTrackPowerChanged)
×
134
                    m_onTrackPowerChanged(true, false);
×
135
                }
136
              });
×
137
          }
138
          break;
×
139

140
        case LAN_X_BC_STOPPED:
×
141
          if(message == LanXBCStopped())
×
142
          {
143
            EventLoop::call(
×
144
              [this]()
×
145
              {
146
                if(m_emergencyStop != TriState::True)
×
147
                {
148
                  m_emergencyStop = TriState::True;
×
149
                  m_trackPowerOn = TriState::True;
×
150

151
                  if(m_onTrackPowerChanged)
×
152
                    m_onTrackPowerChanged(true, true);
×
153
                }
154
              });
×
155
          }
156
          break;
×
157

158
        case LAN_X_LOCO_INFO:
×
159
        {
160
          bool isAnswerToOurRequest = false;
×
161

162
          if(matchedRequest)
×
163
          {
164
            auto* msgData = matchedRequest.value().messageBytes.data();
×
165
            const LanX& requestMsg = *reinterpret_cast<const LanX *>(msgData);
×
166

167
            // If we explicitly requested loco info then we treat it as external change
168
            if(requestMsg.xheader != LAN_X_GET_LOCO_INFO)
×
169
              isAnswerToOurRequest = true;
×
170
          }
171

172
          if(message.dataLen() >= LanXLocoInfo::minMessageSize && message.dataLen() <= LanXLocoInfo::maxMessageSize)
×
173
          {
174
            const auto& reply = static_cast<const LanXLocoInfo&>(message);
×
175

176
            //NOTE: there is also a function at index 0, hence +1
177
            const int functionIndexMax = std::min(reply.functionIndexMax(), LanXLocoInfo::supportedFunctionIndexMax);
×
178
            bool val[LanXLocoInfo::supportedFunctionIndexMax + 1] = {};
×
179

180
            for(int i = 0; i <= functionIndexMax; i++)
×
181
            {
182
              val[i] = reply.getFunction(i);
×
183
            }
184

185
            LocoCache &cache = getLocoCache(reply.address());
×
186

187
            auto changes = static_cast<DecoderChangeFlags>(0);
×
188

189
            //Rescale everything to 126 steps
190
            int currentSpeedStep = reply.speedStep();
×
191
            if(reply.speedSteps() != 126)
×
192
            {
193
              currentSpeedStep = float(currentSpeedStep) / float(reply.speedSteps()) * 126.0;
×
194
              if(abs(currentSpeedStep - cache.lastReceivedSpeedStep) < 5)
×
195
                currentSpeedStep = cache.lastReceivedSpeedStep; //Consider it a rounding error
×
196
            }
197

198
            // For answers to our own requests we don't care about direction and speed step
199
            if(cache.lastReceivedSpeedStep != currentSpeedStep)
×
200
            {
201
                if(!isAnswerToOurRequest)
×
202
                  changes |= DecoderChangeFlags::SpeedSteps;
×
203
                cache.lastReceivedSpeedStep = currentSpeedStep;
×
204
            }
205

206
            if(reply.speedSteps() != cache.speedSteps)
×
207
            {
208
              if(!isAnswerToOurRequest)
×
209
                changes |= DecoderChangeFlags::SpeedSteps;
×
210
              cache.speedSteps = reply.speedSteps();
×
211
            }
212

213
            //Emergency stop is urgent so bypass timeout
214
            //Direction is not propagated back so it shouldn't start looping
215
            //We bypass timeout also for Direction change.
216
            //It can at worst cause a short flickering changing direction n times and then settle down
217
            if(reply.direction() != cache.direction)
×
218
            {
219
              if(!isAnswerToOurRequest)
×
220
                changes |= DecoderChangeFlags::Direction;
×
221
              cache.direction = reply.direction();
×
222
            }
223

224
            if(reply.isEmergencyStop() || reply.isEmergencyStop() != cache.isEStop)
×
225
            {
226
              //Force change when emergency stop is set to be sure it gets received
227
              //as soon as possible
228
              changes |= DecoderChangeFlags::EmergencyStop;
×
229
              cache.isEStop = reply.isEmergencyStop();
×
230
            }
231

232
            //Do not update last seen time to avoid ignoring genuine user commands
233
            //Store last received speed step converted to 126 steps scale
234
            cache.lastReceivedSpeedStep = currentSpeedStep;
×
235

236
            // Update last seen time to prevent decoder to be purged
237
            cache.lastSetTime = std::chrono::steady_clock::now();
×
238

239
            if(isAnswerToOurRequest && changes == DecoderChangeFlags(0))
×
240
            {
241
              // No need to notify Decoder
242
              break;
×
243
            }
244

245
            EventLoop::call(
×
246
              [this, address=reply.address(), isEStop=reply.isEmergencyStop(),
×
247
              speed = reply.speedStep(), speedMax=reply.speedSteps(),
×
248
              dir = reply.direction(), val, functionIndexMax, changes]()
×
249
              {
250
                try
251
                {
252
                  if(auto decoder = m_decoderController->getDecoder(DCC::getProtocol(address), address))
×
253
                  {
254
                    float throttle = Decoder::speedStepToThrottle(speed, speedMax);
×
255

256
                    if(has(changes, DecoderChangeFlags::EmergencyStop))
×
257
                    {
258
                      m_isUpdatingDecoderFromKernel = true;
×
259
                      decoder->emergencyStop = isEStop;
×
260
                    }
261

262
                    if(has(changes, DecoderChangeFlags::Direction))
×
263
                    {
264
                      m_isUpdatingDecoderFromKernel = true;
×
265
                      decoder->direction = dir;
×
266
                    }
267

268
                    if(has(changes, DecoderChangeFlags::Throttle | DecoderChangeFlags::SpeedSteps))
×
269
                    {
270
                      m_isUpdatingDecoderFromKernel = true;
×
271
                      decoder->throttle = throttle;
×
272
                    }
273

274
                    //Reset flag guard at end
275
                    m_isUpdatingDecoderFromKernel = false;
×
276

277
                    //Function get always updated because we do not store a copy in cache
278
                    //so there is no way to tell in advance if they changed
279
                    for(int i = 0; i <= functionIndexMax; i++)
×
280
                    {
281
                      decoder->setFunctionValue(i, val[i]);
×
282
                    }
283
                  }
×
284
                }
285
                catch(...)
×
286
                {
287

288
                }
×
289
              });
×
290
          }
291
          break;
×
292
        }
293
      }
294
      break;
×
295
    }
296
    case LAN_GET_SERIAL_NUMBER:
×
297
      if(message.dataLen() == sizeof(LanGetSerialNumberReply))
×
298
      {
299
        const auto& reply = static_cast<const LanGetSerialNumberReply&>(message);
×
300

301
        if(m_serialNumber != reply.serialNumber())
×
302
        {
303
          m_serialNumber = reply.serialNumber();
×
304
          if(m_onSerialNumberChanged)
×
305
          {
306
            EventLoop::call(
×
307
              [this, serialNumber=m_serialNumber]()
×
308
              {
309
                m_onSerialNumberChanged(serialNumber);
×
310
              });
×
311
          }
312
        }
313
      }
314
      break;
×
315

316
    case LAN_GET_HWINFO:
×
317
      if(message.dataLen() == sizeof(LanGetHardwareInfoReply))
×
318
      {
319
        const auto& reply = static_cast<const LanGetHardwareInfoReply&>(message);
×
320

321
        if(m_hardwareType != reply.hardwareType() ||
×
322
            m_firmwareVersionMajor != reply.firmwareVersionMajor() ||
×
323
            m_firmwareVersionMinor != reply.firmwareVersionMinor())
×
324
        {
325
          m_hardwareType = reply.hardwareType();
×
326
          m_firmwareVersionMajor = reply.firmwareVersionMajor();
×
327
          m_firmwareVersionMinor = reply.firmwareVersionMinor();
×
328

329
          if(m_onHardwareInfoChanged)
×
330
          {
331
            EventLoop::call(
×
332
              [this, hardwareType=m_hardwareType, firmwareVersionMajor=m_firmwareVersionMajor, firmwareVersionMinor=m_firmwareVersionMinor]()
×
333
              {
334
                m_onHardwareInfoChanged(hardwareType, firmwareVersionMajor, firmwareVersionMinor);
×
335
              });
×
336
          }
337
        }
338
      }
339
      break;
×
340

341
    case LAN_RMBUS_DATACHANGED:
×
342
      if(m_inputController && message.dataLen() == sizeof(LanRMBusDataChanged))
×
343
      {
344
        const auto& data = static_cast<const LanRMBusDataChanged&>(message);
×
345

346
        for(uint8_t i = 0; i < LanRMBusDataChanged::feedbackStatusCount; i++)
×
347
        {
348
          const uint16_t index = data.groupIndex * LanRMBusDataChanged::feedbackStatusCount + i;
×
349
          const TriState value = toTriState(data.getFeedbackStatus(i));
×
350
          if(m_rbusFeedbackStatus[index] != value)
×
351
          {
352
            m_rbusFeedbackStatus[index] = value;
×
353

354
            EventLoop::call(
×
355
              [this, address=rbusAddressMin + index, value]()
×
356
              {
357
                m_inputController->updateInputValue(InputChannel::RBus, InputAddress(address), value);
×
358
              });
×
359
          }
360
        }
361
      }
362
      break;
×
363

364
    case LAN_LOCONET_DETECTOR:
×
365
      if(m_inputController && message.dataLen() >= sizeof(LanLocoNetDetector))
×
366
      {
367
        switch(static_cast<const LanLocoNetDetector&>(message).type)
×
368
        {
369
          case LanLocoNetDetector::Type::OccupancyDetector:
×
370
          {
371
            const auto& data = static_cast<const LanLocoNetDetectorOccupancyDetector&>(message);
×
372
            const uint16_t index = data.feedbackAddress();
×
373
            const TriState value = toTriState(data.isOccupied());
×
374
            if(m_loconetFeedbackStatus[index] != value)
×
375
            {
376
              m_loconetFeedbackStatus[index] = value;
×
377

378
              EventLoop::call(
×
379
                [this, address=loconetAddressMin + index, value]()
×
380
                {
381
                  m_inputController->updateInputValue(InputChannel::LocoNet, InputAddress(address), value);
×
382
                });
×
383
            }
384
            break;
×
385
          }
386
          case LanLocoNetDetector::Type::TransponderEntersBlock:
×
387
          case LanLocoNetDetector::Type::TransponderExitsBlock:
388
          case LanLocoNetDetector::Type::LissyLocoAddress:
389
          case LanLocoNetDetector::Type::LissyBlockStatus:
390
          case LanLocoNetDetector::Type::LissySpeed:
391
          case LanLocoNetDetector::Type::StationaryInterrogateRequest:
392
          case LanLocoNetDetector::Type::ReportAddress:
393
          case LanLocoNetDetector::Type::StatusRequestLissy:
394
            break; // not (yet) supported
×
395
        }
396
      }
397
      break;
×
398

399
    case LAN_GET_BROADCASTFLAGS:
×
400
      if(message.dataLen() == sizeof(LanGetBroadcastFlagsReply))
×
401
      {
402
        const auto& reply = static_cast<const LanGetBroadcastFlagsReply&>(message);
×
403
        m_broadcastFlags = reply.broadcastFlags();
×
404

405
        if(m_broadcastFlags != requiredBroadcastFlags)
×
406
        {
407
            Log::log(logId, LogMessage::W2019_Z21_BROADCAST_FLAG_MISMATCH);
×
408
        }
409
      }
410
      break;
×
411

412
    case LAN_SYSTEMSTATE_DATACHANGED:
×
413
    {
414
      if(message.dataLen() == sizeof(LanSystemStateDataChanged))
×
415
      {
416
        const auto& reply = static_cast<const LanSystemStateDataChanged&>(message);
×
417

418
        const bool isStop = reply.centralState & Z21_CENTRALSTATE_EMERGENCYSTOP;
×
419
        const bool isTrackPowerOn = (reply.centralState & Z21_CENTRALSTATE_TRACKVOLTAGEOFF) == 0
×
420
                                    && (reply.centralState & Z21_CENTRALSTATE_SHORTCIRCUIT) == 0;
×
421

422
        const TriState trackPowerOn = toTriState(isTrackPowerOn);
×
423
        const TriState stopState = toTriState(isStop);
×
424

425
        EventLoop::call([this, trackPowerOn, stopState]()
×
426
          {
427
            if(m_trackPowerOn != trackPowerOn || m_emergencyStop != stopState)
×
428
            {
429
              m_trackPowerOn = trackPowerOn;
×
430
              m_emergencyStop = stopState;
×
431

432
              if(m_onTrackPowerChanged)
×
433
                m_onTrackPowerChanged(trackPowerOn == TriState::True,
×
434
                                      stopState == TriState::True);
435
            }
436
          });
×
437
      }
438
      break;
×
439
    }
440

441
    case LAN_GET_CODE:
×
442
    case LAN_LOGOFF:
443
    case LAN_SET_BROADCASTFLAGS:
444
    case LAN_GET_LOCO_MODE:
445
    case LAN_SET_LOCO_MODE:
446
    case LAN_GET_TURNOUTMODE:
447
    case LAN_SET_TURNOUTMODE:
448
    case LAN_RMBUS_GETDATA:
449
    case LAN_RMBUS_PROGRAMMODULE:
450
    case LAN_SYSTEMSTATE_GETDATA:
451
    case LAN_RAILCOM_DATACHANGED:
452
    case LAN_RAILCOM_GETDATA:
453
    case LAN_LOCONET_Z21_RX:
454
    case LAN_LOCONET_Z21_TX:
455
    case LAN_LOCONET_FROM_LAN:
456
    case LAN_LOCONET_DISPATCH_ADDR:
457
    case LAN_CAN_DETECTOR:
458
      break; // not (yet) supported
×
459
  }
460
}
×
461

462
void ClientKernel::trackPowerOn()
×
463
{
464
  assert(isEventLoopThread());
×
465

466
  if(m_trackPowerOn != TriState::True || m_emergencyStop != TriState::False)
×
467
  {
NEW
468
    boost::asio::post(m_ioContext, 
×
469
      [this]()
×
470
      {
471
        send(LanXSetTrackPowerOn());
×
472
      });
×
473
  }
474
}
×
475

476
void ClientKernel::trackPowerOff()
×
477
{
478
  assert(isEventLoopThread());
×
479

480
  if(m_trackPowerOn != TriState::False || m_emergencyStop != TriState::False)
×
481
  {
NEW
482
    boost::asio::post(m_ioContext, 
×
483
      [this]()
×
484
      {
485
        send(LanXSetTrackPowerOff());
×
486
      });
×
487
  }
488
}
×
489

490
void ClientKernel::emergencyStop()
×
491
{
492
  assert(isEventLoopThread());
×
493

494
  if(m_trackPowerOn != TriState::True || m_emergencyStop != TriState::True)
×
495
  {
NEW
496
    boost::asio::post(m_ioContext, 
×
497
      [this]()
×
498
      {
499
        send(LanXSetStop());
×
500
      });
×
501
  }
502
}
×
503

504
void ClientKernel::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber)
×
505
{
506
  const uint16_t address = decoder.address;
×
507
  const bool longAddress = decoder.protocol == DecoderProtocol::DCCLong;
×
508

509
  const Direction direction = decoder.direction;
×
510
  const float throttle = decoder.throttle;
×
511
  const int speedSteps = decoder.speedSteps;
×
512
  const bool isEStop = decoder.emergencyStop;
×
513

514
  TriState funcVal = TriState::Undefined;
×
515
  if(const auto& f = decoder.getFunction(functionNumber))
×
516
    funcVal = toTriState(f->value);
×
517

518
  if(m_isUpdatingDecoderFromKernel)
×
519
  {
520
    //This change was caused by Z21 message so there is not point
521
    //on informing back Z21 with another message
522
    //Skip updating LocoCache again which might already be
523
    //at a new value (EventLoop is slower to process callbacks)
524
    //But reset the guard to allow Train and other parts of code
525
    //to react to this change and further edit decoder state
526
    m_isUpdatingDecoderFromKernel = false;
×
527
    return;
×
528
  }
529

NEW
530
  boost::asio::post(m_ioContext, [this, address, longAddress, direction, throttle, speedSteps, isEStop, changes, functionNumber, funcVal]()
×
531
    {
532
      LanXSetLocoDrive cmd;
×
533
      cmd.setAddress(address, longAddress);
×
534

535
      cmd.setSpeedSteps(speedSteps);
×
536
      int speedStep = Decoder::throttleToSpeedStep(throttle, cmd.speedSteps());
×
537

538
      // Decoder max speed steps must be set for the message to be correctly
539
      // distinguished from LAN_X_SET_LOCO_FUNCTION
540
      cmd.setSpeedStep(speedStep);
×
541
      cmd.setDirection(direction);
×
542

543
      LocoCache &cache = getLocoCache(address);
×
544

545
      bool changed = false;
×
546
      if(has(changes, DecoderChangeFlags::Direction) && cache.direction != direction)
×
547
      {
548
        changed = true;
×
549
      }
550

551
      if(has(changes, DecoderChangeFlags::Throttle | DecoderChangeFlags::SpeedSteps | DecoderChangeFlags::EmergencyStop))
×
552
      {
553
        if(has(changes, DecoderChangeFlags::EmergencyStop) && isEStop != cache.isEStop)
×
554
        {
555
          if(isEStop)
×
556
            cmd.setEmergencyStop();
×
557
          changed = true;
×
558
        }
559

560
        if(!isEStop && (speedSteps != cache.speedSteps || speedStep != cache.speedStep))
×
561
        {
562
          changed = true;
×
563
        }
564
      }
565

566
      if(has(changes, DecoderChangeFlags::FunctionValue))
×
567
      {
568
        //This is independent of LanXSetLocoDrive
569
        if(functionNumber <= LanXSetLocoFunction::functionNumberMax && funcVal != TriState::Undefined)
×
570
        {
571
          send(LanXSetLocoFunction(
×
572
            address, longAddress,
573
            static_cast<uint8_t>(functionNumber),
574
            funcVal == TriState::True ? LanXSetLocoFunction::SwitchType::On : LanXSetLocoFunction::SwitchType::Off));
×
575
        }
576
      }
577

578
      if(changed)
×
579
      {
580
        cache.speedSteps = cmd.speedSteps();
×
581
        cache.speedStep = cmd.speedStep();
×
582
        cache.direction = cmd.direction();
×
583
        cache.isEStop = cmd.isEmergencyStop();
×
584

585
        // Update last seen time to prevent decoder to be purged
586
        cache.lastSetTime = std::chrono::steady_clock::now();
×
587
      }
588

589
      if(changed)
×
590
      {
591
        cmd.updateChecksum();
×
592
        send(cmd);
×
593
      }
594
    });
×
595
}
596

597
bool ClientKernel::setOutput(OutputChannel channel, uint16_t address, OutputValue value)
×
598
{
599
  assert(inRange<uint32_t>(address, outputAddressMin, outputAddressMax));
×
600

601
  if(channel == OutputChannel::Accessory)
×
602
  {
NEW
603
    boost::asio::post(m_ioContext, 
×
604
      [this, address, port=std::get<OutputPairValue>(value) == OutputPairValue::Second]()
×
605
      {
606
        send(LanXSetTurnout(address, port, true));
×
607
        // TODO: sent deactivate after switch time, at least 50ms, see documentation
608
        // TODO: add some kind of queue if queing isn't supported?? requires at least v1.24 (DR5000 v1.5.5 has v1.29)
609
      });
×
610
    return true;
×
611
  }
612
  if(channel == OutputChannel::DCCext)
×
613
  {
614
    if(m_firmwareVersionMajor == 1 && m_firmwareVersionMinor < 40)
×
615
    {
616
      Log::log(logId, LogMessage::W2020_DCCEXT_RCN213_IS_NOT_SUPPORTED);
×
617
      return false;
×
618
    }
619

620
    if(inRange<int16_t>(std::get<int16_t>(value), std::numeric_limits<uint8_t>::min(), std::numeric_limits<uint8_t>::max())) /*[[likely]]*/
×
621
    {
NEW
622
      boost::asio::post(m_ioContext, 
×
623
        [this, address, data=static_cast<uint8_t>(std::get<int16_t>(value))]()
×
624
        {
625
          send(LanXSetExtAccessory(address, data));
×
626
        });
×
627
      return true;
×
628
    }
629
  }
630
  return false;
×
631
}
632

633
void ClientKernel::simulateInputChange(InputChannel channel, uint32_t address, SimulateInputAction action)
×
634
{
635
  if(!m_simulation)
×
636
    return;
×
637

NEW
638
  boost::asio::post(m_ioContext, 
×
639
    [this, channel, address, action]()
×
640
    {
641
      (void)address;
642
      switch(channel)
×
643
      {
644
        case InputChannel::RBus:
×
645
        {
646
          if((action == SimulateInputAction::SetFalse && m_rbusFeedbackStatus[address - rbusAddressMin] == TriState::False) ||
×
647
              (action == SimulateInputAction::SetTrue && m_rbusFeedbackStatus[address - rbusAddressMin] == TriState::True))
×
648
            return; // no change
×
649

650
          LanRMBusDataChanged message;
×
651
          message.groupIndex = (address - rbusAddressMin) / LanRMBusDataChanged::feedbackStatusCount;
×
652

653
          for(uint8_t i = 0; i < LanRMBusDataChanged::feedbackStatusCount; i++)
×
654
          {
655
            const uint32_t n = static_cast<uint32_t>(message.groupIndex) * LanRMBusDataChanged::feedbackStatusCount + i;
×
656
            if(address == rbusAddressMin + n)
×
657
            {
658
              switch(action)
×
659
              {
660
                case SimulateInputAction::SetFalse:
×
661
                  message.setFeedbackStatus(i, false);
×
662
                  break;
×
663

664
                case SimulateInputAction::SetTrue:
×
665
                  message.setFeedbackStatus(i, true);
×
666
                  break;
×
667

668
                case SimulateInputAction::Toggle:
×
669
                  message.setFeedbackStatus(i, m_rbusFeedbackStatus[n] != TriState::True);
×
670
                  break;
×
671
              }
672
            }
673
            else
674
              message.setFeedbackStatus(i, m_rbusFeedbackStatus[n] == TriState::True);
×
675
          }
676

677
          receive(message);
×
678

679
          break;
×
680
        }
681
        case InputChannel::LocoNet:
×
682
        {
683
          const uint16_t feedbackAddress = address - loconetAddressMin;
×
684
          bool occupied;
685
          switch(action)
×
686
          {
687
            case SimulateInputAction::SetFalse:
×
688
              if(m_loconetFeedbackStatus[feedbackAddress] == TriState::False)
×
689
                return; // no change
×
690
              occupied = false;
×
691
              break;
×
692

693
            case SimulateInputAction::SetTrue:
×
694
              if(m_loconetFeedbackStatus[feedbackAddress] == TriState::True)
×
695
                return; // no change
×
696
              occupied = true;
×
697
              break;
×
698

699
            case SimulateInputAction::Toggle:
×
700
              occupied = m_loconetFeedbackStatus[feedbackAddress] != TriState::True;
×
701
              break;
×
702

703
            default:
×
704
              assert(false);
×
705
              return;
706
          }
707
          receive(LanLocoNetDetectorOccupancyDetector(feedbackAddress, occupied));
×
708
          break;
×
709
        }
710
        default: [[unlikely]]
×
711
          assert(false);
×
712
          break;
713
      }
714
    });
715
}
716

717
void ClientKernel::onStart()
×
718
{
719
  // reset all state values
720
  m_broadcastFlags = BroadcastFlags::None;
×
721
  m_broadcastFlagsRetryCount = 0;
×
722
  m_serialNumber = 0;
×
723
  m_hardwareType = HWT_UNKNOWN;
×
724
  m_firmwareVersionMajor = 0;
×
725
  m_firmwareVersionMinor = 0;
×
726
  m_trackPowerOn = TriState::Undefined;
×
727
  m_emergencyStop = TriState::Undefined;
×
728
  m_rbusFeedbackStatus.fill(TriState::Undefined);
×
729
  m_loconetFeedbackStatus.fill(TriState::Undefined);
×
730

731
  send(LanGetSerialNumber());
×
732
  send(LanGetHardwareInfo());
×
733

734
  send(LanSetBroadcastFlags(requiredBroadcastFlags));
×
735

736
  send(LanGetBroadcastFlags());
×
737

738
  send(LanSystemStateGetData());
×
739

740
  startKeepAliveTimer();
×
741
  startInactiveDecoderPurgeTimer();
×
742
}
×
743

744
void ClientKernel::onStop()
×
745
{
746
  send(LanLogoff());
×
747

748
  m_keepAliveTimer.cancel();
×
749
  m_inactiveDecoderPurgeTimer.cancel();
×
750
  m_schedulePendingRequestTimer.cancel();
×
751
  m_locoCache.clear();
×
752
  m_pendingRequests.clear();
×
753
}
×
754

755
void ClientKernel::send(const Message& message, bool wantReply, uint8_t customRetryCount)
×
756
{
757
  if(m_ioHandler->send(message))
×
758
  {
759
    if(m_config.debugLogRXTX)
×
760
      EventLoop::call(
×
761
        [this, msg=toString(message)]()
×
762
        {
763
          Log::log(logId, LogMessage::D2001_TX_X, msg);
×
764
        });
×
765

766
    if(wantReply)
×
767
    {
768
      PendingRequest request;
×
769
      request.reply = getReplyType(message);
×
770

771
      if(request.reply.header != MessageReplyType::noReply)
×
772
      {
773
        if(customRetryCount > 0)
×
774
        {
775
          request.retryCount = customRetryCount;
×
776
        }
777
        else
778
        {
779
          // Calculate from priority
780
          switch (request.reply.priority())
×
781
          {
782
            case MessageReplyType::Priority::Low:
×
783
              request.retryCount = 1;
×
784
              break;
×
785

786
            default:
×
787
            case MessageReplyType::Priority::Normal:
788
              request.retryCount = 2;
×
789
              break;
×
790

791
            case MessageReplyType::Priority::Urgent:
×
792
              request.retryCount = 5;
×
793
              break;
×
794
          }
795
        }
796

797
        // Save copy of original message
798
        request.messageBytes.resize(message.dataLen());
×
799
        std::memcpy(request.messageBytes.data(), &message, message.dataLen());
×
800

801
        // Enque pending request
802
        addPendingRequest(request);
×
803
      }
804
    }
×
805
  }
806
  else
807
  {} // log message and go to error state
808
}
×
809

810
void ClientKernel::startKeepAliveTimer()
×
811
{
812
  if(m_broadcastFlags == BroadcastFlags::None && m_broadcastFlagsRetryCount == maxBroadcastFlagsRetryCount)
×
813
  {
814
    Log::log(logId, LogMessage::W2019_Z21_BROADCAST_FLAG_MISMATCH);
×
815
    m_broadcastFlagsRetryCount++; //Log only once
×
816
  }
817

818
  if(m_broadcastFlags == BroadcastFlags::None && m_broadcastFlagsRetryCount < maxBroadcastFlagsRetryCount)
×
819
  {
820
    //Request BC flags as keep alive message
821
    m_keepAliveTimer.expires_after(boost::asio::chrono::seconds(2));
×
822
  }
823
  else
824
  {
825
    //Normal keep alive
826
    static_assert(ClientConfig::keepAliveInterval > 0);
827
    m_keepAliveTimer.expires_after(boost::asio::chrono::seconds(ClientConfig::keepAliveInterval));
×
828
  }
829

830
  m_keepAliveTimer.async_wait(std::bind(&ClientKernel::keepAliveTimerExpired, this, std::placeholders::_1));
×
831
}
×
832

833
void ClientKernel::keepAliveTimerExpired(const boost::system::error_code& ec)
×
834
{
835
  if(ec)
×
836
    return;
×
837

838
  if(m_broadcastFlags == BroadcastFlags::None)
×
839
  {
840
    //Request BC flags as keep alive message
841
    m_broadcastFlagsRetryCount++;
×
842
    send(LanSetBroadcastFlags(requiredBroadcastFlags));
×
843
    send(LanGetBroadcastFlags());
×
844
  }
845
  else
846
  {
847
    //Normal keep alive
848
    send(LanSystemStateGetData());
×
849
  }
850

851
  startKeepAliveTimer();
×
852
}
853

854
void ClientKernel::startInactiveDecoderPurgeTimer()
×
855
{
856
  static_assert(ClientConfig::purgeInactiveDecoderInternal > 0);
857
  m_inactiveDecoderPurgeTimer.expires_after(boost::asio::chrono::seconds(ClientConfig::purgeInactiveDecoderInternal));
×
858
  m_inactiveDecoderPurgeTimer.async_wait(std::bind(&ClientKernel::inactiveDecoderPurgeTimerExpired, this, std::placeholders::_1));
×
859
}
×
860

861
void ClientKernel::inactiveDecoderPurgeTimerExpired(const boost::system::error_code& ec)
×
862
{
863
  if(ec)
×
864
    return;
×
865

866
  const auto purgeTime = std::chrono::steady_clock::now() - std::chrono::seconds(ClientConfig::purgeInactiveDecoderInternal);
×
867

868
  auto it = m_locoCache.begin();
×
869
  while(it != m_locoCache.end())
×
870
  {
871
    if(it->second.lastSetTime < purgeTime)
×
872
    {
873
      it = m_locoCache.erase(it);
×
874
    }
875
    else
876
    {
877
      it++;
×
878
    }
879
  }
880

881
  startInactiveDecoderPurgeTimer();
×
882
}
883

884
ClientKernel::LocoCache& ClientKernel::getLocoCache(uint16_t dccAddr)
×
885
{
886
  auto it = m_locoCache.find(dccAddr);
×
887
  if(it == m_locoCache.end())
×
888
  {
889
    LocoCache item;
×
890
    item.dccAddress = dccAddr;
×
891
    it = m_locoCache.emplace(dccAddr, item).first;
×
892
  }
893

894
  return it->second;
×
895
}
896

897
void ClientKernel::addPendingRequest(const PendingRequest &request)
×
898
{
899
  //Enqueue this request to track reply from Z21
900
  bool wasEmpty = m_pendingRequests.empty();
×
901
  PendingRequest req = request;
×
902
  req.sendTime = std::chrono::steady_clock::now();
×
903
  m_pendingRequests.push_back(req);
×
904
  if(wasEmpty)
×
905
    startSchedulePendingRequestTimer();
×
906
}
×
907

908
std::optional<ClientKernel::PendingRequest> ClientKernel::matchPendingReplyAndRemove(const Message &message)
×
909
{
910
  const auto currentTime = std::chrono::steady_clock::now();
×
911

912
  //TODO: depends on priority and network estimated speed
913
  const auto timeout = std::chrono::seconds(3);
×
914

915
  for(auto request = m_pendingRequests.begin(); request != m_pendingRequests.end(); request++)
×
916
  {
917
    //If it's last retry and we exceeded timeout, skip request
918
    if(request->retryCount == 0 && (currentTime - request->sendTime) > timeout)
×
919
      continue;
×
920

921
    if(message.header() != request->reply.header)
×
922
      continue;
×
923

924
    if(message.header() == LAN_X)
×
925
    {
926
      const LanX& lanX = static_cast<const LanX&>(message);
×
927
      if(lanX.xheader != request->reply.xHeader)
×
928
        continue;
×
929

930
      if(request->reply.hasFlag(MessageReplyType::Flags::CheckDb0))
×
931
      {
932
        // Cast to any LanX message with a db0 to check its value
933
        const auto& hack = static_cast<const LanXGetStatus&>(lanX);
×
934
        if(hack.db0 != request->reply.db0)
×
935
          continue;
×
936
      }
937
    }
938

939
    if(request->reply.hasFlag(MessageReplyType::Flags::CheckAddress))
×
940
    {
941
      uint16_t address = 0;
×
942
      switch (message.header())
×
943
      {
944
        case LAN_GET_LOCO_MODE:
×
945
        {
946
          address = static_cast<const LanGetLocoMode&>(message).address();
×
947
          break;
×
948
        }
949

950
        case LAN_GET_TURNOUTMODE:
×
951
        {
952
          // NOTE: not (yet) supported
953
          break;
×
954
        }
955

956
        case LAN_X:
×
957
        {
958
          const LanX& lanX = static_cast<const LanX&>(message);
×
959
          switch (lanX.xheader)
×
960
          {
961
            case LAN_X_TURNOUT_INFO:
×
962
              address = static_cast<const LanXTurnoutInfo&>(lanX).address();
×
963
              break;
×
964

965
            case LAN_X_LOCO_INFO:
×
966
              address = static_cast<const LanXLocoInfo&>(lanX).address();
×
967
              break;
×
968

969
            default:
×
970
              break;
×
971
          }
972
        }
973

974
        default:
975
          break;
×
976
      }
977

978
      if(address != request->reply.address)
×
979
        continue;
×
980
    }
981

982
    if(request->reply.hasFlag(MessageReplyType::Flags::CheckSpeedStep))
×
983
    {
984
      if(message.header() == LAN_X
×
985
          && static_cast<const LanX&>(message).xheader == LAN_X_LOCO_INFO)
×
986
      {
987
        const auto& locoInfo = static_cast<const LanXLocoInfo&>(message);
×
988
        if(locoInfo.speedAndDirection != request->reply.speedAndDirection)
×
989
          continue;
×
990
        if(locoInfo.speedSteps() != request->reply.speedSteps())
×
991
          continue;
×
992
      }
993
    }
994

995
    // We matched a previously sent request
996
    // NOTE: In theory we could have matched a reply generated by other clients operations
997
    // But for our purposes this should be fine
998

999
    // Remove it from pending queue
1000
    PendingRequest copy = *request;
×
1001
    m_pendingRequests.erase(request);
×
1002
    return copy;
×
1003
  }
1004

1005
  return {};
×
1006
}
1007

1008
void ClientKernel::startSchedulePendingRequestTimer()
×
1009
{
1010
  //TODO: depends on priority and network estimated speed
1011
  const auto timeout = std::chrono::seconds(1);
×
1012

1013
  m_schedulePendingRequestTimer.expires_after(timeout);
×
1014
  m_schedulePendingRequestTimer.async_wait(std::bind(&ClientKernel::schedulePendingRequestTimerExpired, this, std::placeholders::_1));
×
1015
}
×
1016

1017
void ClientKernel::schedulePendingRequestTimerExpired(const boost::system::error_code &ec)
×
1018
{
1019
  if(ec)
×
1020
    return;
×
1021

1022
  rescheduleTimedoutRequests();
×
1023
}
1024

1025
void ClientKernel::rescheduleTimedoutRequests()
×
1026
{
1027
  const auto deadlineTime = std::chrono::steady_clock::now();
×
1028

1029
  //TODO: depends on priority and network estimated speed
1030
  const auto timeout = std::chrono::seconds(3);
×
1031

1032
  auto request = m_pendingRequests.begin();
×
1033
  while(request != m_pendingRequests.end())
×
1034
  {
1035
    if((deadlineTime - request->sendTime) > timeout)
×
1036
    {
1037
      if(request->retryCount <= 0)
×
1038
      {
1039
        // Give up with this request and remove it
1040
        request = m_pendingRequests.erase(request);
×
1041
        continue;
×
1042
      }
1043

1044
      // Decrement retry count
1045
      request->retryCount--;
×
1046

1047
      // Re-schedule request
1048
      const auto* msgData = request->messageBytes.data();
×
1049
      const Message& requestMsg = *reinterpret_cast<const Message *>(msgData);
×
1050

1051
      // Send original request again but without adding it to pending queue
1052
      send(requestMsg, false);
×
1053

1054
      // Restart timeout
1055
      request->sendTime = std::chrono::steady_clock::now();
×
1056
    }
1057

1058
    request++;
×
1059
  }
1060

1061
  if(!m_pendingRequests.empty())
×
1062
    startSchedulePendingRequestTimer();
×
1063
}
×
1064

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