• 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/marklincan/kernel.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2023-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 "kernel.hpp"
23
#include <nlohmann/json.hpp>
24
#include <version.hpp>
25
#include "messages.hpp"
26
#include "message/configdata.hpp"
27
#include "message/statusdataconfig.hpp"
28
#include "locomotivelist.hpp"
29
#include "uid.hpp"
30
#include "../dcc/dcc.hpp"
31
#include "../motorola/motorola.hpp"
32
#include "../../decoder/decoder.hpp"
33
#include "../../decoder/decoderchangeflags.hpp"
34
#include "../../decoder/decodercontroller.hpp"
35
#include "../../input/inputcontroller.hpp"
36
#include "../../output/outputcontroller.hpp"
37
#include "../../../core/eventloop.hpp"
38
#include "../../../log/log.hpp"
39
#include "../../../log/logmessageexception.hpp"
40
#include "../../../traintastic/traintastic.hpp"
41
#include "../../../utils/inrange.hpp"
42
#include "../../../utils/setthreadname.hpp"
43
#include "../../../utils/tohex.hpp"
44
#include "../../../utils/writefile.hpp"
45
#include "../../../utils/zlib.hpp"
46

47
namespace MarklinCAN {
48

49
static std::tuple<bool, DecoderProtocol, uint16_t> uidToProtocolAddress(uint32_t uid)
×
50
{
51
  if(inRange(uid, UID::Range::locomotiveMotorola))
×
52
    return {true, DecoderProtocol::Motorola, uid - UID::Range::locomotiveMotorola.first};
×
53
  if(inRange(uid, UID::Range::locomotiveMFX))
×
54
    return {true, DecoderProtocol::MFX, uid - UID::Range::locomotiveMFX.first};
×
55
  if(inRange(uid, UID::Range::locomotiveDCC))
×
56
  {
57
    //! \todo Handle long address < 128
58
    const uint16_t address = uid - UID::Range::locomotiveDCC.first;
×
59
    if(address <= DCC::addressShortMax)
×
60
      return {true, DecoderProtocol::DCCShort, address};
×
61

62
    return {true, DecoderProtocol::DCCLong, address};
×
63
  }
64
  return {false, DecoderProtocol::None, 0};
×
65
}
66

67
Kernel::Kernel(std::string logId_, const Config& config, bool simulation)
×
68
  : KernelBase(std::move(logId_))
×
69
  , m_simulation{simulation}
×
70
  , m_statusDataConfigRequestTimer{m_ioContext}
×
71
  , m_debugDir{Traintastic::instance->debugDir()}
×
72
  , m_config{config}
×
73
{
74
  assert(isEventLoopThread());
×
75
  (void)m_simulation;
76
}
×
77

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

NEW
82
  boost::asio::post(m_ioContext, 
×
83
    [this, newConfig=config]()
×
84
    {
85
      if(m_config.defaultSwitchTime != newConfig.defaultSwitchTime)
×
86
        send(AccessorySwitchTime(newConfig.defaultSwitchTime / 10));
×
87

88
      m_config = newConfig;
×
89
    });
×
90
}
×
91

92
void Kernel::setOnLocomotiveListChanged(std::function<void(const std::shared_ptr<LocomotiveList>&)> callback)
×
93
{
94
  assert(isEventLoopThread());
×
95
  assert(!m_started);
×
96
  m_onLocomotiveListChanged = std::move(callback);
×
97
}
×
98

99
void Kernel::setOnNodeChanged(std::function<void(const Node& node)> callback)
×
100
{
101
  assert(isEventLoopThread());
×
102
  assert(!m_started);
×
103
  m_onNodeChanged = std::move(callback);
×
104
}
×
105

106
void Kernel::setDecoderController(DecoderController* decoderController)
×
107
{
108
  assert(isEventLoopThread());
×
109
  assert(!m_started);
×
110
  m_decoderController = decoderController;
×
111
}
×
112

113
void Kernel::setInputController(InputController* inputController)
×
114
{
115
  assert(isEventLoopThread());
×
116
  assert(!m_started);
×
117
  m_inputController = inputController;
×
118
}
×
119

120
void Kernel::setOutputController(OutputController* outputController)
×
121
{
122
  assert(isEventLoopThread());
×
123
  assert(!m_started);
×
124
  m_outputController = outputController;
×
125
}
×
126

127
void Kernel::start()
×
128
{
129
  assert(isEventLoopThread());
×
130
  assert(m_ioHandler);
×
131
  assert(!m_started);
×
132

133
  // reset all state values
134
  m_inputValues.fill(TriState::Undefined);
×
135
  m_outputValuesMotorola.fill(OutputPairValue::Undefined);
×
136
  m_outputValuesDCC.fill(OutputPairValue::Undefined);
×
137

138
  m_thread = std::thread(
×
139
    [this]()
×
140
    {
141
      setThreadName("marklin_can");
×
NEW
142
      boost::asio::executor_work_guard<decltype(m_ioContext.get_executor())> work{m_ioContext.get_executor()};
×
143
      m_ioContext.run();
×
144
    });
×
145

NEW
146
  boost::asio::post(m_ioContext, 
×
147
    [this]()
×
148
    {
149
      try
150
      {
151
        m_ioHandler->start();
×
152
      }
153
      catch(const LogMessageException& e)
×
154
      {
155
        EventLoop::call(
×
156
          [this, e]()
×
157
          {
158
            Log::log(logId, e.message(), e.args());
×
159
            error();
×
160
          });
×
161
      }
×
162
    });
×
163

164
#ifndef NDEBUG
165
  m_started = true;
×
166
#endif
167
}
×
168

169
void Kernel::stop()
×
170
{
171
  assert(isEventLoopThread());
×
172

NEW
173
  boost::asio::post(m_ioContext, 
×
174
    [this]()
×
175
    {
176
      m_ioHandler->stop();
×
177
    });
×
178

179
  m_ioContext.stop();
×
180

181
  m_thread.join();
×
182

183
#ifndef NDEBUG
184
  m_started = false;
×
185
#endif
186
}
×
187

188
void Kernel::started()
×
189
{
190
  // add Traintastic to the node list
191
  {
192
    Node node;
×
193
    node.uid = m_config.nodeUID;
×
194
    node.deviceName = nodeDeviceName;
×
195
    node.articleNumber = nodeArticleNumber;
×
196
    node.serialNumber = m_config.nodeSerialNumber;
×
197
    node.softwareVersionMajor = TRAINTASTIC_VERSION_MAJOR;
×
198
    node.softwareVersionMinor = TRAINTASTIC_VERSION_MINOR;
×
199
    node.deviceId = DeviceId::Traintastic;
×
200

201
    nodeChanged(node);
×
202

203
    m_nodes.emplace(m_config.nodeUID, node);
×
204
  }
×
205

206
  nextState();
×
207
}
×
208

209
void Kernel::receive(const Message& message)
×
210
{
211
  assert(isKernelThread());
×
212

213
  if(m_config.debugLogRXTX)
×
214
    EventLoop::call([this, msg=toString(message)](){ Log::log(logId, LogMessage::D2002_RX_X, msg); });
×
215

216
  switch(message.command())
×
217
  {
218
    case Command::System:
×
219
    {
220
      const auto& system = static_cast<const SystemMessage&>(message);
×
221

222
      switch(system.subCommand())
×
223
      {
224
        case SystemSubCommand::SystemStop:
×
225
        case SystemSubCommand::SystemGo:
226
        case SystemSubCommand::SystemHalt:
227
          // not (yet) implemented
228
          break;
×
229

230
        case SystemSubCommand::LocomotiveEmergencyStop:
×
231
          if(m_decoderController && system.isResponse())
×
232
          {
233
            auto [success, proto, addr] = uidToProtocolAddress(system.uid());
×
234
            if(success)
×
235
            {
236
              EventLoop::call(
×
237
                [this, protocol=proto, address=addr]()
×
238
                {
239
                  if(const auto& decoder = m_decoderController->getDecoder(protocol, address))
×
240
                    decoder->emergencyStop.setValueInternal(true);
×
241
                });
×
242
            }
243
          }
244
          break;
×
245

246
        case SystemSubCommand::LocomotiveCycleEnd:
×
247
          // not (yet) implemented
248
          break;
×
249
        case SystemSubCommand::AccessorySwitchTime:
×
250
          if(message.isResponse() && m_state == State::SetAccessorySwitchTime)
×
251
            nextState();
×
252
          break;
×
253

254
        case SystemSubCommand::Overload:
×
255
        case SystemSubCommand::Status:
256
        case SystemSubCommand::ModelClock:
257
        case SystemSubCommand::MFXSeek:
258
          // not (yet) implemented
259
          break;
×
260
      }
261
      break;
×
262
    }
263
    case Command::Discovery:
×
264
    case Command::Bind:
265
    case Command::Verify:
266
      // not (yet) implemented
267
      break;
×
268

269
    case Command::LocomotiveSpeed:
×
270
      if(m_decoderController)
×
271
      {
272
        const auto& locomotiveSpeed = static_cast<const LocomotiveSpeed&>(message);
×
273
        if(locomotiveSpeed.isResponse() && locomotiveSpeed.hasSpeed())
×
274
        {
275
          auto [success, proto, addr] = uidToProtocolAddress(locomotiveSpeed.uid());
×
276
          if(success)
×
277
          {
278
            EventLoop::call(
×
279
              [this, protocol=proto, address=addr, throttle=Decoder::speedStepToThrottle(locomotiveSpeed.speed(), LocomotiveSpeed::speedMax)]()
×
280
              {
281
                if(const auto& decoder = m_decoderController->getDecoder(protocol, address))
×
282
                {
283
                  decoder->emergencyStop.setValueInternal(false);
×
284
                  decoder->throttle.setValueInternal(throttle);
×
285
                }
286
              });
×
287
          }
288
        }
289
      }
290
      break;
×
291

292
    case Command::LocomotiveDirection:
×
293
      if(m_decoderController)
×
294
      {
295
        const auto& locomotiveDirection = static_cast<const LocomotiveDirection&>(message);
×
296
        if(locomotiveDirection.isResponse() && locomotiveDirection.hasDirection())
×
297
        {
298
          auto [success, proto, addr] = uidToProtocolAddress(locomotiveDirection.uid());
×
299
          if(success)
×
300
          {
301
            Direction direction = Direction::Unknown;
×
302
            switch(locomotiveDirection.direction())
×
303
            {
304
              case LocomotiveDirection::Direction::Forward:
×
305
                direction = Direction::Forward;
×
306
                break;
×
307

308
              case LocomotiveDirection::Direction::Reverse:
×
309
                direction = Direction::Reverse;
×
310
                break;
×
311

312
              case LocomotiveDirection::Direction::Same:
×
313
              case LocomotiveDirection::Direction::Inverse:
314
                break;
×
315
            }
316

317
            EventLoop::call(
×
318
              [this, protocol=proto, address=addr, direction]()
×
319
              {
320
                if(const auto& decoder = m_decoderController->getDecoder(protocol, address))
×
321
                  decoder->direction.setValueInternal(direction);
×
322
              });
×
323
          }
324
        }
325
      }
326
      break;
×
327

328
    case Command::LocomotiveFunction:
×
329
      if(m_decoderController)
×
330
      {
331
        const auto& locomotiveFunction = static_cast<const LocomotiveFunction&>(message);
×
332
        if(locomotiveFunction.isResponse() && locomotiveFunction.hasValue())
×
333
        {
334
          auto [success, proto, addr] = uidToProtocolAddress(locomotiveFunction.uid());
×
335
          if(success)
×
336
          {
337
            EventLoop::call(
×
338
              [this, protocol=proto, address=addr, number=locomotiveFunction.number(), value=locomotiveFunction.isOn()]()
×
339
              {
340
                if(const auto& decoder = m_decoderController->getDecoder(protocol, address))
×
341
                  decoder->setFunctionValue(number, value);
×
342
              });
×
343
          }
344
        }
345
      }
346
      break;
×
347

348
    case Command::ReadConfig:
×
349
    case Command::WriteConfig:
350
      // not (yet) implemented
351
      break;
×
352

353
    case Command::AccessoryControl:
×
354
      if(message.isResponse() && (message.dlc == 6 || message.dlc == 8))
×
355
      {
356
        const auto& accessoryControl = static_cast<const AccessoryControl&>(message);
×
357
        if(accessoryControl.position() != AccessoryControl::positionOff &&
×
358
            accessoryControl.position() != AccessoryControl::positionOn)
×
359
          break;
×
360

361
        OutputChannel channel;
362
        uint32_t address;
363
        const auto value = accessoryControl.position() == AccessoryControl::positionOff ? OutputPairValue::First : OutputPairValue::Second;
×
364

365
        if(inRange(accessoryControl.uid(), UID::Range::accessoryMotorola))
×
366
        {
367
          channel = OutputChannel::AccessoryMotorola;
×
368
          address = 1 + (accessoryControl.uid() - UID::Range::accessoryMotorola.first);
×
369
          //if(address > m_outputValuesMotorola.size() || m_outputValuesMotorola[address - 1] == value)
370
          //  break;
371
          //m_outputValuesMotorola[address - 1] = value;
372
        }
373
        else if(inRange(accessoryControl.uid(), UID::Range::accessoryDCC))
×
374
        {
375
          channel = OutputChannel::AccessoryDCC;
×
376
          address = 1 + (accessoryControl.uid() - UID::Range::accessoryDCC.first);
×
377
          //if(address > m_outputValuesDCC.size() || m_outputValuesDCC[address - 1] == value)
378
          //  break;
379
          //m_outputValuesDCC[address - 1] = value;
380
        }
381
        else
382
        {
383
          break;
×
384
        }
385

386
        EventLoop::call(
×
387
          [this, channel, address, value]()
×
388
          {
389
            m_outputController->updateOutputValue(channel, OutputAddress(address), value);
×
390
          });
×
391
      }
392
      break;
×
393

394
    case Command::AccessoryConfig:
×
395
    case Command::S88Polling:
396
      // not (yet) implemented
397
      break;
×
398

399
    case Command::FeedbackEvent:
×
400
      if(message.dlc == 8)
×
401
      {
402
        if(m_inputController)
×
403
        {
404
          const auto& feedbackState = static_cast<const FeedbackState&>(message);
×
405

406
          if(feedbackState.deviceId() == 0) //! \todo what about other values?
×
407
          {
408
            const auto value = feedbackState.stateNew() == 0 ? TriState::False : TriState::True;
×
409
            if(inRange(feedbackState.contactId(), s88AddressMin, s88AddressMax) && m_inputValues[feedbackState.contactId() - s88AddressMin] != value)
×
410
            {
411
              m_inputValues[feedbackState.contactId() - s88AddressMin] = value;
×
412

413
              EventLoop::call(
×
414
                [this, address=feedbackState.contactId(), value]()
×
415
                {
416
                  m_inputController->updateInputValue(InputChannel::Input, InputAddress(address), value);
×
417
                });
×
418
            }
419
          }
420
        }
421
      }
422
      break;
×
423

424
    case Command::SX1Event:
×
425
      // not (yet) implemented
426
      break;
×
427

428
    case Command::Ping:
×
429
      if(message.dlc == 0 && !message.isResponse())
×
430
      {
431
        send(PingReply(m_config.nodeUID, TRAINTASTIC_VERSION_MAJOR, TRAINTASTIC_VERSION_MINOR, DeviceId::Traintastic));
×
432
      }
433
      else if(message.dlc == 8 && message.isResponse())
×
434
      {
435
        const auto& pingReply = static_cast<const PingReply&>(message);
×
436

437
        if(auto it = m_nodes.find(pingReply.uid()); it == m_nodes.end())
×
438
        {
439
          if(it == m_nodes.end()) // new node
×
440
          {
441
            Node node;
×
442
            node.uid = pingReply.uid();
×
443
            node.softwareVersionMajor = pingReply.softwareVersionMajor();
×
444
            node.softwareVersionMinor = pingReply.softwareVersionMinor();
×
445
            node.deviceId = pingReply.deviceId();
×
446
            m_nodes.emplace(pingReply.uid(), node);
×
447
          }
×
448

449
          if(pingReply.uid() != m_config.nodeUID)
×
450
          {
451
            // queue the requests and wait for some time before sending them.
452
            // multiple transfer don't work well at the same time.
453
            m_statusDataConfigRequestQueue.emplace(pingReply.uid(), 0);
×
454
            restartStatusDataConfigTimer();
×
455
          }
456
        }
457
      }
458
      break;
×
459

460
    case Command::Update:
×
461
    case Command::ReadConfigData:
462
    case Command::BootloaderCAN:
463
    case Command::BootloaderTrack:
464
      // not (yet) implemented
465
      break;
×
466

467
    case Command::StatusDataConfig:
×
468
      if(message.dlc == 5 && !message.isResponse() && static_cast<const UidMessage&>(message).uid() == m_config.nodeUID)
×
469
      {
470
        const uint32_t uid = static_cast<const UidMessage&>(message).uid();
×
471
        const uint8_t index = message.data[4];
×
472
        switch(index)
×
473
        {
474
          case 0x00:
×
475
          {
476
            StatusData::DeviceDescription desc;
×
477
            desc.serialNumber = m_config.nodeSerialNumber;
×
478
            memcpy(desc.articleNumber, nodeArticleNumber.data(), std::min(nodeArticleNumber.size(), sizeof(desc.articleNumber)));
×
479
            desc.deviceName = nodeDeviceName;
×
480
            for(const auto& reply : statusDataConfigReply(m_config.nodeUID, uid, index, desc))
×
481
              send(reply);
×
482
            break;
×
483
          }
×
484
        }
485
      }
486
      else if(message.dlc == 5 && message.isResponse())
×
487
      {
488
        const auto& configReply = static_cast<const StatusDataConfig&>(message);
×
489
        receiveStatusDataConfig(configReply.uid(), configReply.index(), m_statusConfigData);
×
490
        m_statusConfigData.clear();
×
491
      }
492
      else if(message.dlc == 6 && message.isResponse())
×
493
      {
494
        const auto& configReply = static_cast<const StatusDataConfigReply&>(message);
×
495
        if(m_statusConfigData.size() / 8 == configReply.packetCount())
×
496
        {
497
          receiveStatusDataConfig(configReply.uid(), configReply.index(), m_statusConfigData);
×
498
        }
499
        m_statusConfigData.clear();
×
500
      }
501
      else if(message.dlc == 8 && message.isResponse())
×
502
      {
503
        const auto& configData = static_cast<const StatusDataConfigReplyData&>(message);
×
504
        if(m_statusConfigData.empty() && configData.hash() == StatusDataConfigReplyData::startHash)
×
505
        {
506
          m_statusConfigData.resize(8);
×
507
          std::memcpy(m_statusConfigData.data(), configData.data, 8);
×
508
        }
509
        else if((m_statusConfigData.size() / 8 + StatusDataConfigReplyData::startHash) == configData.hash())
×
510
        {
511
          m_statusConfigData.resize(m_statusConfigData.size() + 8);
×
512
          std::memcpy(m_statusConfigData.data() + m_statusConfigData.size() - 8, configData.data, 8);
×
513
        }
514
        else
515
          m_statusConfigData.clear(); // invalid data -> reset
×
516
      }
517
      break;
×
518

519
    case Command::ConfigData:
×
520
      if(message.isResponse() && message.dlc == 8)
×
521
      {
522
        m_configDataStreamCollector = std::make_unique<ConfigDataStreamCollector>(std::string{static_cast<const ConfigData&>(message).name()});
×
523
      }
524
      break;
×
525

526
    case Command::ConfigDataStream:
×
527
      if(m_configDataStreamCollector) /*[[likely]]*/
×
528
      {
529
        const auto status = m_configDataStreamCollector->process(static_cast<const ConfigDataStream&>(message));
×
530
        if(status != ConfigDataStreamCollector::Collecting)
×
531
        {
532
          if(status == ConfigDataStreamCollector::Complete)
×
533
          {
534
            receiveConfigData(std::move(m_configDataStreamCollector));
×
535
          }
536
          else // error
537
          {
538
            m_configDataStreamCollector.reset();
×
539
          }
540
        }
541
      }
542
      break;
×
543
  }
544
}
×
545

546
void Kernel::systemStop()
×
547
{
548
  assert(isEventLoopThread());
×
NEW
549
  boost::asio::post(m_ioContext, 
×
550
    [this]()
×
551
    {
552
      send(SystemStop());
×
553
    });
×
554
}
×
555

556
void Kernel::systemGo()
×
557
{
558
  assert(isEventLoopThread());
×
NEW
559
  boost::asio::post(m_ioContext, 
×
560
    [this]()
×
561
    {
562
      send(SystemGo());
×
563
    });
×
564
}
×
565

566
void Kernel::systemHalt()
×
567
{
568
  assert(isEventLoopThread());
×
NEW
569
  boost::asio::post(m_ioContext, 
×
570
    [this]()
×
571
    {
572
        send(SystemHalt());
×
573
    });
×
574
}
×
575

576
void Kernel::getLocomotiveList()
×
577
{
578
  assert(isEventLoopThread());
×
NEW
579
  boost::asio::post(m_ioContext, 
×
580
    [this]()
×
581
    {
582
      send(ConfigData(m_config.nodeUID, ConfigDataName::loks));
×
583
    });
×
584
}
×
585

586
void Kernel::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber)
×
587
{
588
  assert(isEventLoopThread());
×
589
  uint32_t uid = 0;
×
590

591
  switch(decoder.protocol.value())
×
592
  {
593
    case DecoderProtocol::DCCShort:
×
594
    case DecoderProtocol::DCCLong:
595
      uid = UID::locomotiveDCC(decoder.address);
×
596
      break;
×
597

598
    case DecoderProtocol::Motorola:
×
599
      uid = UID::locomotiveMotorola(decoder.address);
×
600
      break;
×
601

602
    case DecoderProtocol::MFX:
×
603
      if(const auto& it = m_mfxUIDtoSID.find(decoder.mfxUID); it != m_mfxUIDtoSID.end())
×
604
      {
605
        uid = UID::locomotiveMFX(it->second);
×
606
      }
607
      else
608
      {
609
        Log::log(logId, LogMessage::E2024_UNKNOWN_LOCOMOTIVE_MFX_UID_X, toHex(decoder.mfxUID.value()));
×
610
      }
611
      break;
×
612

613
    default: [[unlikely]]
×
614
      assert(false);
×
615
      break;
616
  }
617

618
  if(uid == 0)
×
619
    return;
×
620

621
  if(has(changes, DecoderChangeFlags::Direction))
×
622
  {
623
    LocomotiveDirection::Direction direction = LocomotiveDirection::Direction::Same;
×
624

625
    switch(decoder.direction.value())
×
626
    {
627
      case Direction::Forward:
×
628
        direction = LocomotiveDirection::Direction::Forward;
×
629
        break;
×
630

631
      case Direction::Reverse:
×
632
        direction = LocomotiveDirection::Direction::Reverse;
×
633
        break;
×
634

635
      case Direction::Unknown:
×
636
        break;
×
637
    }
638

639
    if(direction != LocomotiveDirection::Direction::Same)
×
640
      postSend(LocomotiveDirection(uid, direction));
×
641
  }
642

643
  if(has(changes, DecoderChangeFlags::EmergencyStop) && decoder.emergencyStop)
×
644
    postSend(LocomotiveEmergencyStop(uid));
×
645
  else if(has(changes, DecoderChangeFlags::Throttle | DecoderChangeFlags::EmergencyStop))
×
646
    postSend(LocomotiveSpeed(uid, Decoder::throttleToSpeedStep(decoder.throttle, LocomotiveSpeed::speedMax)));
×
647

648
  if(has(changes, DecoderChangeFlags::FunctionValue) && functionNumber <= std::numeric_limits<uint8_t>::max())
×
649
    postSend(LocomotiveFunction(uid, functionNumber, decoder.getFunctionValue(functionNumber)));
×
650
}
651

652
bool Kernel::setOutput(OutputChannel channel, uint16_t address, OutputPairValue value)
×
653
{
654
  assert(isEventLoopThread());
×
655
  assert(value == OutputPairValue::First || value == OutputPairValue::Second);
×
656

NEW
657
  boost::asio::post(m_ioContext, 
×
658
    [this, channel, address, value]()
×
659
    {
660
      uint32_t uid = 0;
×
661

662
      switch(channel)
×
663
      {
664
        case OutputChannel::AccessoryMotorola:
×
665
          assert(inRange(address, Motorola::Accessory::addressMin, Motorola::Accessory::addressMax));
×
666
          if(m_outputValuesMotorola[address - Motorola::Accessory::addressMin] == value)
×
667
            return;
×
668
          uid = MarklinCAN::UID::accessoryMotorola(address);
×
669
          break;
×
670

671
        case OutputChannel::AccessoryDCC:
×
672
          assert(inRange(address, DCC::Accessory::addressMin, DCC::Accessory::addressMax));
×
673
          if(m_outputValuesDCC[address - DCC::Accessory::addressMin] == value)
×
674
            return;
×
675
          uid = MarklinCAN::UID::accessoryDCC(address);
×
676
          break;
×
677

678
        default: /*[[unlikely]]*/
×
679
          assert(false);
×
680
          return;
681
      }
682
      assert(uid != 0);
×
683

684
      MarklinCAN::AccessoryControl cmd(uid);
×
685
      cmd.setPosition(value == OutputPairValue::First ? MarklinCAN::AccessoryControl::positionOff : MarklinCAN::AccessoryControl::positionOn);
×
686
      //cmd.setCurrent(value ? 1 : 0);
687
      send(cmd);
×
688
    });
689

690
  return true;
×
691
}
692

693
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
×
694
{
695
  assert(isEventLoopThread());
×
696
  assert(handler);
×
697
  assert(!m_ioHandler);
×
698
  m_ioHandler = std::move(handler);
×
699
}
×
700

701
void Kernel::send(const Message& message)
×
702
{
703
  assert(isKernelThread());
×
704

705
  if(m_config.debugLogRXTX)
×
706
    EventLoop::call([this, msg=toString(message)](){ Log::log(logId, LogMessage::D2001_TX_X, msg); });
×
707

708
  m_ioHandler->send(message);
×
709
}
×
710

711
void Kernel::postSend(const Message& message)
×
712
{
NEW
713
  boost::asio::post(m_ioContext, 
×
714
    [this, message]()
×
715
    {
716
      send(message);
×
717
    });
×
718
}
×
719

720
void Kernel::receiveStatusDataConfig(uint32_t nodeUID, uint8_t index, const std::vector<std::byte>& statusConfigData)
×
721
{
722
  auto it = m_nodes.find(nodeUID);
×
723
  if(it == m_nodes.end())
×
724
    return;
×
725

726
  Node& node = it->second;
×
727

728
  if(index == 0)
×
729
  {
730
    const auto devDesc = StatusData::DeviceDescription::decode(statusConfigData);
×
731
    node.serialNumber = devDesc.serialNumber;
×
732
    node.articleNumber.assign(devDesc.articleNumber, strnlen(devDesc.articleNumber, sizeof(devDesc.articleNumber)));
×
733
    node.deviceName = devDesc.deviceName;
×
734
    node.numberOfReadings = devDesc.numberOfReadings;
×
735
    node.numberOfConfigurationChannels = devDesc.numberOfConfigurationChannels;
×
736

737
    // schedule read of reading/configuration descriptions:
738
    const uint8_t lastIndex = devDesc.numberOfReadings + devDesc.numberOfConfigurationChannels;
×
739
    for(uint8_t i = 1; i <= lastIndex; i++)
×
740
      m_statusDataConfigRequestQueue.emplace(node.uid, i);
×
741
  }
×
742
  else if(index <= node.numberOfReadings)
×
743
  {
744
    node.readings.emplace_back(StatusData::ReadingDescription::decode(statusConfigData));
×
745
  }
746
  else if(index <= node.numberOfReadings + node.numberOfConfigurationChannels)
×
747
  {
748
    node.configurations.emplace_back(StatusData::ConfigurationDescription::decode(statusConfigData));
×
749
  }
750

751
  if(index == node.numberOfReadings + node.numberOfConfigurationChannels)
×
752
  {
753
    nodeChanged(node);
×
754
  }
755

756
  if(!m_statusDataConfigRequestQueue.empty() && m_statusDataConfigRequestQueue.front().uid == nodeUID && m_statusDataConfigRequestQueue.front().index == index)
×
757
  {
758
    m_statusDataConfigRequestQueue.pop();
×
759
    m_statusDataConfigRequestRetries = statusDataConfigRequestRetryCount;
×
760
    if(!m_statusDataConfigRequestQueue.empty())
×
761
      restartStatusDataConfigTimer();
×
762
    else if(m_state == State::DiscoverNodes)
×
763
      nextState();
×
764
  }
765
}
766

767
void Kernel::receiveConfigData(std::unique_ptr<ConfigDataStreamCollector> configData)
×
768
{
769
  const auto basename = m_debugDir / logId / "configstream" / configData->name;
×
770
  if(m_config.debugConfigStream)
×
771
  {
772
    writeFile(std::filesystem::path(basename).concat(".bin"), configData->bytes());
×
773
  }
774

775
  if(configData->name == ConfigDataName::loks)
×
776
  {
777
    const size_t uncompressedSize = be_to_host(*reinterpret_cast<const uint32_t*>(configData->data()));
×
778
    std::string locList;
×
779
    if(ZLib::Uncompress::toString(configData->data() + sizeof(uint32_t), configData->dataSize() - sizeof(uint32_t), uncompressedSize, locList))
×
780
    {
781
      if(m_config.debugConfigStream)
×
782
      {
783
        writeFile(std::filesystem::path(basename).concat(".txt"), locList);
×
784
      }
785

786
      EventLoop::call(
×
787
        [this, list=std::make_shared<LocomotiveList>(locList)]()
×
788
        {
789
          // update MFX UID to SID list:
790
          m_mfxUIDtoSID.clear();
×
791
          for(const auto& item : *list)
×
792
            m_mfxUIDtoSID.emplace(item.mfxUID, item.sid);
×
793

794
          if(m_onLocomotiveListChanged) /*[[likely]]*/
×
795
            m_onLocomotiveListChanged(list);
×
796
        });
×
797
    }
798

799
    if(m_state == State::DownloadLokList)
×
800
      nextState();
×
801
  }
×
802
}
×
803

804
void Kernel::restartStatusDataConfigTimer()
×
805
{
806
  assert(!m_statusDataConfigRequestQueue.empty());
×
807
  m_statusDataConfigRequestTimer.cancel();
×
808
  m_statusDataConfigRequestTimer.expires_after(std::chrono::milliseconds(50));
×
809
  m_statusDataConfigRequestTimer.async_wait(
×
810
    [this](const boost::system::error_code& ec)
×
811
    {
812
      if(!ec)
×
813
      {
814
        if(!m_statusConfigData.empty())
×
815
          return; // if in progress, don't request, when finished it will start the timer again
×
816

817
        if(m_statusDataConfigRequestRetries > 0) /*[[likely]]*/
×
818
          m_statusDataConfigRequestRetries--;
×
819

820
        if(m_statusDataConfigRequestRetries == 0)
×
821
        {
822
          // give up, no response
823

824
          if(auto it = m_nodes.find(m_statusDataConfigRequestQueue.front().uid); it != m_nodes.end()) /*[[likely]]*/
×
825
          {
826
            nodeChanged(it->second);
×
827
          }
828

829
          m_statusDataConfigRequestQueue.pop();
×
830
          m_statusDataConfigRequestRetries = statusDataConfigRequestRetryCount;
×
831
        }
832
        else
833
        {
834
          const auto& request = m_statusDataConfigRequestQueue.front();
×
835
          send(StatusDataConfig(m_config.nodeUID, request.uid, request.index));
×
836
        }
837
      }
838

839
      if(ec != boost::asio::error::operation_aborted)
×
840
      {
841
        if(!m_statusDataConfigRequestQueue.empty())
×
842
          restartStatusDataConfigTimer();
×
843
        else if(m_state == State::DiscoverNodes)
×
844
          nextState();
×
845
      }
846
    });
847
}
×
848

849
void Kernel::nodeChanged(const Node& node)
×
850
{
851
  assert(isKernelThread());
×
852

853
  if(m_onNodeChanged) /*[[likely]]*/
×
854
    EventLoop::call(
×
855
      [this, node=node]()
×
856
      {
857
        static_assert(!std::is_reference_v<decltype(node)>);
858
        m_onNodeChanged(node);
×
859
      });
×
860

861
  if(m_config.debugStatusDataConfig)
×
862
  {
863
    using namespace nlohmann;
864

865
    // serialize into JSON:
866
    auto data = json::object();
×
867
    data["uid"] = node.uid;
×
868
    data["software_version"] = std::to_string(node.softwareVersionMajor).append(".").append(std::to_string(node.softwareVersionMinor));
×
869
    data["device_id"] = node.deviceId;
×
870
    if(!node.deviceName.empty() || node.serialNumber != 0 || !node.articleNumber.empty() || node.numberOfReadings != 0 || node.numberOfConfigurationChannels != 0)
×
871
    {
872
      data["serial_number"] = node.serialNumber;
×
873
      data["article_number"] = node.articleNumber;
×
874
      data["device_name"] = node.deviceName;
×
875
      data["number_of_readings"] = node.numberOfReadings;
×
876
      auto readings = json::array();
×
877
      for(const auto& reading : node.readings)
×
878
      {
879
        auto readingData = json::object();
×
880
        readingData["channel"] = reading.channel;
×
881
        readingData["power"] = reading.power;
×
882
        readingData["color"] = reading.color;
×
883
        readingData["zero"] = reading.zero;
×
884
        readingData["rangeEnd"] = reading.rangeEnd;
×
885
        readingData["description"] = reading.description;
×
886
        readingData["labelStart"] = reading.labelStart;
×
887
        readingData["labelEnd"] = reading.labelEnd;
×
888
        readingData["unit"] = reading.unit;
×
889
        readings.emplace_back(readingData);
×
890
      }
×
891
      data["readings"] = readings;
×
892
      data["number_of_configuration_channels"] = node.numberOfConfigurationChannels;
×
893
      auto configurations = json::array();
×
894
      for(const auto& configuration : node.configurations)
×
895
      {
896
        auto configurationData = json::object();
×
897
        configurationData["channel"] = configuration.channel;
×
898
        configurationData["description"] = configuration.description;
×
899
        switch(configuration.type)
×
900
        {
901
          case StatusData::ConfigurationDescription::Type::List:
×
902
          {
903
            configurationData["type"] = "list";
×
904
            configurationData["default"] = configuration.default_;
×
905
            auto items = json::array();
×
906
            for(const auto& item : configuration.listItems)
×
907
              items.emplace_back(item);
×
908
            configurationData["items"] = items;
×
909
            break;
×
910
          }
×
911
          case StatusData::ConfigurationDescription::Type::Number:
×
912
            configurationData["type"] = "number";
×
913
            configurationData["value_min"] = configuration.valueMin;
×
914
            configurationData["value_max"] = configuration.valueMax;
×
915
            configurationData["value"] = configuration.value;
×
916
            configurationData["label_start"] = configuration.labelStart;
×
917
            configurationData["label_end"] = configuration.labelEnd;
×
918
            configurationData["unit"] = configuration.unit;
×
919
            break;
×
920

921
          default:
×
922
            configurationData["type"] = static_cast<uint8_t>(configuration.type);
×
923
            assert(false);
×
924
            break;
925
        }
926
        configurations.emplace_back(configurationData);
×
927
      }
×
928
      data["configurations"] = configurations;
×
929
    }
×
930

931
    writeFileJSON(m_debugDir / logId / "statusdataconfig" / toHex(node.uid).append(".json"), data);
×
932
  }
×
933
}
×
934

935
void Kernel::changeState(State value)
×
936
{
937
  assert(isKernelThread());
×
938
  assert(m_state != value);
×
939

940
  m_state = value;
×
941

942
  switch(m_state)
×
943
  {
944
    case State::Initial:
×
945
      break;
×
946

947
    case State::DiscoverNodes:
×
948
      send(Ping());
×
949
      break;
×
950

951
    case State::SetAccessorySwitchTime:
×
952
      send(AccessorySwitchTime(m_config.defaultSwitchTime / 10));
×
953
      break;
×
954

955
    case State::DownloadLokList:
×
956
      send(ConfigData(m_config.nodeUID, ConfigDataName::loks));
×
957
      break;
×
958

959
    case State::Started:
×
960
      KernelBase::started();
×
961
      break;
×
962
  }
963
}
×
964

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