• 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/traintasticdiy/kernel.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2022-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 "messages.hpp"
24
#include "../../decoder/decoder.hpp"
25
#include "../../decoder/decoderchangeflags.hpp"
26
#include "../../decoder/list/decoderlist.hpp"
27
#include "../../input/inputcontroller.hpp"
28
#include "../../output/outputcontroller.hpp"
29
#include "../../../utils/inrange.hpp"
30
#include "../../../utils/setthreadname.hpp"
31
#include "../../../core/eventloop.hpp"
32
#include "../../../core/objectproperty.tpp"
33
#include "../../../log/log.hpp"
34
#include "../../../log/logmessageexception.hpp"
35
#include "../../../world/world.hpp"
36

37
namespace TraintasticDIY {
38

39
constexpr TriState toTriState(InputState value)
×
40
{
41
  switch(value)
×
42
  {
43
    case InputState::False:
×
44
      return TriState::False;
×
45

46
    case InputState::True:
×
47
      return TriState::True;
×
48

49
    case InputState::Undefined:
×
50
    case InputState::Invalid:
51
      break;
×
52
  }
53
  return TriState::Undefined;
×
54
}
55

56
constexpr TriState toTriState(OutputState value)
×
57
{
58
  switch(value)
×
59
  {
60
    case OutputState::False:
×
61
      return TriState::False;
×
62

63
    case OutputState::True:
×
64
      return TriState::True;
×
65

66
    case OutputState::Undefined:
×
67
    case OutputState::Invalid:
68
      break;
×
69
  }
70
  return TriState::Undefined;
×
71
}
72

73
Kernel::Kernel(std::string logId_, World& world, const Config& config, bool simulation)
×
74
  : KernelBase(std::move(logId_))
×
75
  , m_world{world}
×
76
  , m_simulation{simulation}
×
77
  , m_startupDelayTimer{m_ioContext}
×
78
  , m_heartbeatTimeout{m_ioContext}
×
79
  , m_inputController{nullptr}
×
80
  , m_outputController{nullptr}
×
81
  , m_config{config}
×
82
{
83
}
×
84

85
void Kernel::setConfig(const Config& config)
×
86
{
NEW
87
  boost::asio::post(m_ioContext, 
×
88
    [this, newConfig=config]()
×
89
    {
90
      m_config = newConfig;
×
91
    });
×
92
}
×
93

94
void Kernel::start()
×
95
{
96
  assert(m_ioHandler);
×
97
  assert(!m_started);
×
98
  assert(m_inputValues.empty());
×
99
  assert(m_outputValues.empty());
×
100
  assert(m_throttleSubscriptions.empty());
×
101
  assert(m_decoderSubscriptions.empty());
×
102

103
  m_featureFlagsSet = false;
×
104
  m_featureFlags1 = FeatureFlags1::None;
×
105
  m_featureFlags2 = FeatureFlags2::None;
×
106
  m_featureFlags3 = FeatureFlags3::None;
×
107
  m_featureFlags4 = FeatureFlags4::None;
×
108

109
  m_thread = std::thread(
×
110
    [this]()
×
111
    {
112
      setThreadName("traintasticdiy");
×
NEW
113
      boost::asio::executor_work_guard<decltype(m_ioContext.get_executor())> work{m_ioContext.get_executor()};
×
114
      m_ioContext.run();
×
115
    });
×
116

NEW
117
  boost::asio::post(m_ioContext, 
×
118
    [this]()
×
119
    {
120
      try
121
      {
122
        m_ioHandler->start();
×
123
      }
124
      catch(const LogMessageException& e)
×
125
      {
126
        EventLoop::call(
×
127
          [this, e]()
×
128
          {
129
            Log::log(logId, e.message(), e.args());
×
130
            error();
×
131
          });
×
132
        return;
×
133
      }
×
134
    });
135

136
#ifndef NDEBUG
137
  m_started = true;
×
138
#endif
139
}
×
140

141
void Kernel::stop()
×
142
{
143
  for(auto& it : m_decoderSubscriptions)
×
144
    it.second.connection.disconnect();
×
145

NEW
146
  boost::asio::post(m_ioContext, 
×
147
    [this]()
×
148
    {
149
      m_heartbeatTimeout.cancel();
×
150
      m_ioHandler->stop();
×
151
    });
×
152

153
  m_ioContext.stop();
×
154

155
  m_thread.join();
×
156

157
  m_inputValues.clear();
×
158
  m_outputValues.clear();
×
159
  m_throttleSubscriptions.clear();
×
160
  m_decoderSubscriptions.clear();
×
161

162
#ifndef NDEBUG
163
  m_started = false;
×
164
#endif
165
}
×
166

167
void Kernel::started()
×
168
{
169
  assert(isKernelThread());
×
170

171
  m_startupDelayTimer.expires_after(boost::asio::chrono::milliseconds(m_config.startupDelay));
×
172
  m_startupDelayTimer.async_wait(std::bind(&Kernel::startupDelayExpired, this, std::placeholders::_1));
×
173
}
×
174

175
void Kernel::receive(const Message& message)
×
176
{
177
  if(m_config.debugLogRXTX && (message != Heartbeat() || m_config.debugLogHeartbeat))
×
178
    EventLoop::call(
×
179
      [this, msg=toString(message)]()
×
180
      {
181
        Log::log(logId, LogMessage::D2002_RX_X, msg);
×
182
      });
×
183

184
  restartHeartbeatTimeout();
×
185

186
  switch(message.opCode)
×
187
  {
188
    case OpCode::Heartbeat:
×
189
      break;
×
190

191
    case OpCode::SetInputState:
×
192
    {
193
      if(!m_featureFlagsSet || !hasFeatureInput())
×
194
        break;
×
195

196
      const auto& setInputState = static_cast<const SetInputState&>(message);
×
197
      const uint16_t address = setInputState.address();
×
198
      if(inRange(address, ioAddressMin, ioAddressMax))
×
199
      {
200
        auto it = m_inputValues.find(address);
×
201
        if(it == m_inputValues.end() || it->second != setInputState.state)
×
202
        {
203
          m_inputValues[address] = setInputState.state;
×
204

205
          EventLoop::call(
×
206
            [this, address, state=setInputState.state]()
×
207
            {
208
              if(state == InputState::Invalid)
×
209
              {
210
                if(m_inputController->inputMap().count({InputChannel::Input, InputAddress(address)}) != 0)
×
211
                  Log::log(logId, LogMessage::W2004_INPUT_ADDRESS_X_IS_INVALID, address);
×
212
              }
213
              else
214
                m_inputController->updateInputValue(InputChannel::Input, InputAddress(address), toTriState(state));
×
215
            });
×
216
        }
217
      }
218
      break;
×
219
    }
220
    case OpCode::SetOutputState:
×
221
    {
222
      if(!m_featureFlagsSet || !hasFeatureOutput())
×
223
        break;
×
224

225
      const auto& setOutputState = static_cast<const SetOutputState&>(message);
×
226
      const uint16_t address = setOutputState.address();
×
227
      if(inRange(address, ioAddressMin, ioAddressMax))
×
228
      {
229
        auto it = m_outputValues.find(address);
×
230
        if(it == m_outputValues.end() || it->second != setOutputState.state)
×
231
        {
232
          m_outputValues[address] = setOutputState.state;
×
233

234
          EventLoop::call(
×
235
            [this, address, state=setOutputState.state]()
×
236
            {
237
              if(state == OutputState::Invalid)
×
238
              {
239
                if(m_outputController->outputMap().count({OutputChannel::Output, OutputAddress(address)}) != 0)
×
240
                  Log::log(logId, LogMessage::W2005_OUTPUT_ADDRESS_X_IS_INVALID, address);
×
241
              }
242
              else
243
                m_outputController->updateOutputValue(OutputChannel::Output, OutputAddress(address), toTriState(state));
×
244
            });
×
245
        }
246
      }
247
      break;
×
248
    }
249
    case OpCode::ThrottleSubUnsub:
×
250
    {
251
      if(!m_featureFlagsSet || !hasFeatureThrottle())
×
252
        break;
×
253

254
      const auto& subUnsub = static_cast<const ThrottleSubUnsub&>(message);
×
255
      switch(subUnsub.action())
×
256
      {
257
        case ThrottleSubUnsub::Unsubscribe:
×
258
          throttleUnsubscribe(subUnsub.throttleId(), {subUnsub.address(), subUnsub.isLongAddress()});
×
259
          send(subUnsub);
×
260
          break;
×
261

262
        case ThrottleSubUnsub::Subscribe:
×
263
          throttleSubscribe(subUnsub.throttleId(), {subUnsub.address(), subUnsub.isLongAddress()});
×
264
          EventLoop::call(
×
265
            [this, subUnsub]()
×
266
            {
267
              if(auto decoder = getDecoder(subUnsub.address(), subUnsub.isLongAddress()))
×
268
              {
269
                uint8_t speedMax = 0;
×
270
                uint8_t speed = 0;
×
271

272
                if(!decoder->emergencyStop)
×
273
                {
274
                  speedMax = decoder->speedSteps.value();
×
275
                  if(speedMax == Decoder::speedStepsAuto)
×
276
                    speedMax = std::numeric_limits<uint8_t>::max();
×
277
                  speed = Decoder::throttleToSpeedStep(decoder->throttle, speedMax);
×
278
                }
279

280
                postSend(ThrottleSetSpeedDirection(subUnsub.throttleId(), subUnsub.address(), subUnsub.isLongAddress(), speed, speedMax, decoder->direction));
×
281
                for(const auto& function : *decoder->functions)
×
282
                  postSend(ThrottleSetFunction(subUnsub.throttleId(), subUnsub.address(), subUnsub.isLongAddress(), function->number, function->value));
×
283
              }
×
284
            });
×
285
          break;
×
286
      }
287
      break;
×
288
    }
289
    case OpCode::ThrottleSetFunction:
×
290
    {
291
      if(!m_featureFlagsSet || !hasFeatureThrottle())
×
292
        break;
×
293

294
      const auto& throttleSetFunction = static_cast<const ThrottleSetFunction&>(message);
×
295

296
      throttleSubscribe(throttleSetFunction.throttleId(), {throttleSetFunction.address(), throttleSetFunction.isLongAddress()});
×
297

298
      EventLoop::call(
×
299
        [this, throttleSetFunction]()
×
300
        {
301
          if(auto decoder = getDecoder(throttleSetFunction.address(), throttleSetFunction.isLongAddress()))
×
302
          {
303
            bool value = false;
×
304

305
            if(auto function = decoder->getFunction(throttleSetFunction.functionNumber()))
×
306
            {
307
              function->value = throttleSetFunction.functionValue();
×
308
              if(function->value != throttleSetFunction.functionValue())
×
309
              {
310
                send(ThrottleSetFunction(
×
311
                  throttleSetFunction.throttleId(),
×
312
                  throttleSetFunction.address(),
×
313
                  throttleSetFunction.isLongAddress(),
×
314
                  throttleSetFunction.functionNumber(),
×
315
                  function->value));
×
316
              }
317
            }
318
            else
319
            {
320
              // warning or debug?
321
              send(ThrottleSetFunction(
×
322
                throttleSetFunction.throttleId(),
×
323
                throttleSetFunction.address(),
×
324
                throttleSetFunction.isLongAddress(),
×
325
                throttleSetFunction.functionNumber(),
×
326
                value));
327
            }
×
328
          }
×
329
        });
×
330
      break;
×
331
    }
332
    case OpCode::ThrottleSetSpeedDirection:
×
333
    {
334
      if(!m_featureFlagsSet || !hasFeatureThrottle())
×
335
        break;
×
336

337
      const auto& throttleSetSpeedDirection = static_cast<const ThrottleSetSpeedDirection&>(message);
×
338

339
      throttleSubscribe(throttleSetSpeedDirection.throttleId(), {throttleSetSpeedDirection.address(), throttleSetSpeedDirection.isLongAddress()});
×
340

341
      EventLoop::call(
×
342
        [this, throttleSetSpeedDirection]()
×
343
        {
344
          if(auto decoder = getDecoder(throttleSetSpeedDirection.address(), throttleSetSpeedDirection.isLongAddress()))
×
345
          {
346
            if(throttleSetSpeedDirection.isSpeedSet())
×
347
            {
348
              decoder->emergencyStop = throttleSetSpeedDirection.isEmergencyStop();
×
349
              if(!throttleSetSpeedDirection.isEmergencyStop())
×
350
                decoder->throttle = throttleSetSpeedDirection.throttle();
×
351
            }
352
            if(throttleSetSpeedDirection.isDirectionSet())
×
353
              decoder->direction = throttleSetSpeedDirection.direction();
×
354
          }
×
355
        });
×
356
      break;
×
357
    }
358
    case OpCode::Features:
×
359
    {
360
      const auto& features = static_cast<const Features&>(message);
×
361
      m_featureFlagsSet = true;
×
362
      m_featureFlags1 = features.featureFlags1;
×
363
      m_featureFlags2 = features.featureFlags2;
×
364
      m_featureFlags3 = features.featureFlags3;
×
365
      m_featureFlags4 = features.featureFlags4;
×
366

367
      if(hasFeatureInput())
×
368
        EventLoop::call(
×
369
          [this]()
×
370
          {
371
            for(const auto& it : m_inputController->inputMap())
×
372
              postSend(GetInputState(static_cast<uint16_t>(std::get<InputAddress>(it.first.location).address)));
×
373
          });
×
374

375
      if(hasFeatureOutput())
×
376
        EventLoop::call(
×
377
          [this]()
×
378
          {
379
            for(const auto& it : m_outputController->outputMap())
×
380
              postSend(GetOutputState(static_cast<uint16_t>(std::get<OutputAddress>(it.first.location).address)));
×
381
          });
×
382
      break;
×
383
    }
384
    case OpCode::Info:
×
385
    {
386
      const auto& info = static_cast<const InfoBase&>(message);
×
387
      EventLoop::call(
×
388
        [this, text=std::string(info.text())]()
×
389
        {
390
          Log::log(logId, LogMessage::I2005_X, text);
×
391
        });
×
392
      break;
×
393
    }
394
    case OpCode::GetInfo:
×
395
    case OpCode::GetFeatures:
396
    case OpCode::GetOutputState:
397
    case OpCode::GetInputState:
398
      assert(false);
×
399
      break;
400
  }
401
}
×
402

403
bool Kernel::setOutput(uint16_t address, bool value)
×
404
{
405
  postSend(SetOutputState(address, value ? OutputState::True : OutputState::False));
×
406
  return true;
×
407
}
408

409
void Kernel::simulateInputChange(uint16_t address, SimulateInputAction action)
×
410
{
411
  if(m_simulation)
×
NEW
412
    boost::asio::post(m_ioContext, 
×
413
      [this, address, action]()
×
414
      {
415
        TraintasticDIY::InputState state;
416
        auto it = m_inputValues.find(address);
×
417
        switch(action)
×
418
        {
419
          case SimulateInputAction::SetFalse:
×
420
            if(it != m_inputValues.end() && it->second == InputState::False)
×
421
              return; // no change
×
422
            state = InputState::False;
×
423
            break;
×
424

425
          case SimulateInputAction::SetTrue:
×
426
            if(it != m_inputValues.end() && it->second == InputState::True)
×
427
              return; // no change
×
428
            state = InputState::True;
×
429
            break;
×
430

431
          case SimulateInputAction::Toggle:
×
432
            state = (it == m_inputValues.end() || it->second == InputState::True) ? InputState::False : InputState::True;
×
433
            break;
×
434

435
          default:
×
436
            assert(false);
×
437
            return;
438
        }
439
        receive(SetInputState(address, state));
×
440
      });
441
}
×
442

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

450
void Kernel::send(const Message& message)
×
451
{
452
  if(m_ioHandler->send(message))
×
453
  {
454
    if(m_config.debugLogRXTX && (message != Heartbeat() || m_config.debugLogHeartbeat))
×
455
      EventLoop::call(
×
456
        [this, msg=toString(message)]()
×
457
        {
458
          Log::log(logId, LogMessage::D2001_TX_X, msg);
×
459
        });
×
460
  }
461
  else
462
  {} // log message and go to error state
463
}
×
464

465
void Kernel::startupDelayExpired(const boost::system::error_code& ec)
×
466
{
467
  if(ec)
×
468
    return;
×
469

470
  send(GetInfo());
×
471
  send(GetFeatures());
×
472

473
  restartHeartbeatTimeout();
×
474

475
  KernelBase::started();
×
476
}
477

478
void Kernel::restartHeartbeatTimeout()
×
479
{
480
  m_heartbeatTimeout.expires_after(m_config.heartbeatTimeout);
×
481
  m_heartbeatTimeout.async_wait(std::bind(&Kernel::heartbeatTimeoutExpired, this, std::placeholders::_1));
×
482
}
×
483

484
void Kernel::heartbeatTimeoutExpired(const boost::system::error_code& ec)
×
485
{
486
  if(ec)
×
487
    return;
×
488
  m_heartbeatTimeout.cancel();
×
489
  send(Heartbeat());
×
490
  restartHeartbeatTimeout();
×
491
}
492

493
std::shared_ptr<Decoder> Kernel::getDecoder(uint16_t address, bool longAddress) const
×
494
{
495
  const auto& decoderList = *m_world.decoders;
×
496
  std::shared_ptr<Decoder> decoder = decoderList.getDecoder(longAddress ? DecoderProtocol::DCCLong : DecoderProtocol::DCCShort, address);
×
497
  if(!decoder)
×
498
    decoder = decoderList.getDecoder(address);
×
499
  return decoder;
×
500
}
×
501

502
void Kernel::throttleSubscribe(uint16_t throttleId, std::pair<uint16_t, bool> key)
×
503
{
504
  auto [unused, added] = m_throttleSubscriptions[throttleId].insert(key);
×
505
  if(added)
×
506
  {
507
    EventLoop::call(
×
508
      [this, key]()
×
509
      {
510
        if(auto it = m_decoderSubscriptions.find(key); it == m_decoderSubscriptions.end())
×
511
        {
512
          if(auto decoder = getDecoder(key.first, key.second))
×
513
            m_decoderSubscriptions.emplace(key, DecoderSubscription{decoder->decoderChanged.connect(std::bind(&Kernel::throttleDecoderChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), 1});
×
514
        }
515
        else
516
        {
517
          it->second.count++;
×
518
        }
519
      });
×
520
  }
521
}
×
522

523
void Kernel::throttleUnsubscribe(uint16_t throttleId, std::pair<uint16_t, bool> key)
×
524
{
525
  {
526
    auto& subscriptions = m_throttleSubscriptions[throttleId];
×
527
    subscriptions.erase(key);
×
528
    if(subscriptions.empty())
×
529
      m_throttleSubscriptions.erase(throttleId);
×
530
  }
531

532
  EventLoop::call(
×
533
    [this, key]()
×
534
    {
535
      if(auto it = m_decoderSubscriptions.find(key); it != m_decoderSubscriptions.end())
×
536
      {
537
        assert(it->second.count > 0);
×
538
        if(--it->second.count == 0)
×
539
        {
540
          it->second.connection.disconnect();
×
541
          m_decoderSubscriptions.erase(it);
×
542
        }
543
      }
544
    });
×
545
}
×
546

547
void Kernel::throttleDecoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber)
×
548
{
549
  const std::pair<uint16_t, bool> key(decoder.address, decoder.protocol == DecoderProtocol::DCCLong);
×
550

551
  if(has(changes, DecoderChangeFlags::Direction | DecoderChangeFlags::EmergencyStop | DecoderChangeFlags::SpeedSteps | DecoderChangeFlags::Throttle))
×
552
  {
553
    const bool emergencyStop = decoder.emergencyStop.value();
×
554

555
    uint8_t speedMax = 0;
×
556
    if(!emergencyStop)
×
557
    {
558
      speedMax = decoder.speedSteps.value();
×
559
      if(speedMax == Decoder::speedStepsAuto)
×
560
        speedMax = std::numeric_limits<uint8_t>::max();
×
561
    }
562

NEW
563
    boost::asio::post(m_ioContext, 
×
564
      [this,
×
565
        key,
566
        direction=decoder.direction.value(),
×
567
        speed=speedMax > 0 ? Decoder::throttleToSpeedStep(decoder.throttle, speedMax) : 0,
×
568
        speedMax]()
569
      {
570
        for(const auto& it : m_throttleSubscriptions)
×
571
          if(it.second.count(key) != 0)
×
572
            send(ThrottleSetSpeedDirection(it.first, key.first, key.second, speed, speedMax, direction));
×
573
      });
×
574
  }
575

576
  if(has(changes, DecoderChangeFlags::FunctionValue))
×
577
  {
578
    assert(functionNumber <= std::numeric_limits<uint8_t>::max());
×
579

NEW
580
    boost::asio::post(m_ioContext, 
×
581
      [this,
×
582
        key,
583
        number=static_cast<uint8_t>(functionNumber),
584
        value=decoder.getFunctionValue(functionNumber)]()
×
585
      {
586
        for(const auto& it : m_throttleSubscriptions)
×
587
          if(it.second.count(key) != 0)
×
588
            send(ThrottleSetFunction(it.first, key.first, key.second, number, value));
×
589
      });
×
590
  }
591
}
×
592

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