• 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

21.88
/server/src/hardware/interface/hsi88.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 "hsi88.hpp"
23
#include "../input/input.hpp"
24
#include "../input/list/inputlisttablemodel.hpp"
25
#include "../../core/attributes.hpp"
26
#include "../../core/eventloop.hpp"
27
#include "../../log/log.hpp"
28
#include "../../log/logmessageexception.hpp"
29
#include "../../utils/displayname.hpp"
30
#include "../../utils/makearray.hpp"
31
#include "../../utils/serialport.hpp"
32
#include "../../utils/setthreadname.hpp"
33
#include "../../utils/tohex.hpp"
34
#include "../../world/world.hpp"
35

36
constexpr auto inputListColumns = InputListColumn::Channel | InputListColumn::Address;
37

38
HSI88Interface::HSI88Interface(World& world, std::string_view _id)
3✔
39
  : Interface(world, _id)
40
  , InputController(static_cast<IdObject&>(*this))
41
  , m_ioContext{1}
3✔
42
  , m_serialPort{m_ioContext}
3✔
43
  , device{this, "device", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
6✔
44
  , modulesLeft{this, "modules_left", 2, PropertyFlags::ReadWrite | PropertyFlags::Store,
6✔
45
      [this](uint8_t /*value*/)
3✔
46
      {
47
        updateModulesMax();
×
48
      }}
×
49
  , modulesMiddle{this, "modules_middle", 2, PropertyFlags::ReadWrite | PropertyFlags::Store,
6✔
50
      [this](uint8_t /*value*/)
3✔
51
      {
52
        updateModulesMax();
×
53
      }}
×
54
  , modulesRight{this, "modules_right", 2, PropertyFlags::ReadWrite | PropertyFlags::Store,
6✔
55
      [this](uint8_t /*value*/)
3✔
56
      {
57
        updateModulesMax();
×
58
      }}
×
59
  , debugLogRXTX{this, "debug_log_rx_tx", false, PropertyFlags::ReadWrite | PropertyFlags::Store,
6✔
60
      [this](bool value)
6✔
61
      {
62
        m_debugLogRXTX = value;
×
63
      }}
6✔
64
{
65
  name = "HSI-88";
3✔
66

67
  const bool editable = contains(m_world.state, WorldState::Edit);
3✔
68

69
  Attributes::addEnabled(device, !online && editable);
3✔
70
  m_interfaceItems.insertBefore(device, notes);
3✔
71

72
  Attributes::addEnabled(modulesLeft, !online && editable);
3✔
73
  Attributes::addMinMax(modulesLeft, modulesMin, modulesMax);
3✔
74
  m_interfaceItems.insertBefore(modulesLeft, notes);
3✔
75

76
  Attributes::addEnabled(modulesMiddle, !online && editable);
3✔
77
  Attributes::addMinMax(modulesMiddle, modulesMin, modulesMax);
3✔
78
  m_interfaceItems.insertBefore(modulesMiddle, notes);
3✔
79

80
  Attributes::addEnabled(modulesRight, !online && editable);
3✔
81
  Attributes::addMinMax(modulesRight, modulesMin, modulesMax);
3✔
82
  m_interfaceItems.insertBefore(modulesRight, notes);
3✔
83

84
  m_interfaceItems.insertBefore(inputs, notes);
3✔
85

86
  Attributes::addDisplayName(debugLogRXTX, DisplayName::Hardware::debugLogRXTX);
3✔
87
  Attributes::addEnabled(debugLogRXTX, editable);
3✔
88
  m_interfaceItems.insertBefore(debugLogRXTX, notes);
3✔
89

90
  updateModulesMax();
3✔
91
}
3✔
92

93
std::span<const InputChannel> HSI88Interface::inputChannels() const
3✔
94
{
95
  static const auto values = makeArray(InputChannel::S88_Left, InputChannel::S88_Middle, InputChannel::S88_Right);
96
  return values;
3✔
97
}
98

99
std::pair<uint32_t, uint32_t> HSI88Interface::inputAddressMinMax(InputChannel /*channel*/) const
×
100
{
101
  return {inputAddressMin, inputAddressMax};
×
102
}
103

104
void HSI88Interface::inputSimulateChange(InputChannel channel, const InputLocation& location, SimulateInputAction action)
×
105
{
106
  //! \todo add simulation support
107
  (void)channel;
108
  (void)location;
109
  (void)action;
110
}
×
111

112
void HSI88Interface::addToWorld()
3✔
113
{
114
  Interface::addToWorld();
3✔
115
  InputController::addToWorld(inputListColumns);
3✔
116
}
3✔
117

118
void HSI88Interface::destroying()
3✔
119
{
120
  InputController::destroying();
3✔
121
  Interface::destroying();
3✔
122
}
3✔
123

124
void HSI88Interface::loaded()
×
125
{
126
  Interface::loaded();
×
127

128
  if(modulesLeft > modulesMax)
×
129
    modulesLeft.setValueInternal(modulesMax);
×
130
  if(modulesMiddle > modulesMax)
×
131
    modulesMiddle.setValueInternal(modulesMax);
×
132
  if(modulesRight > modulesMax)
×
133
    modulesRight.setValueInternal(modulesMax);
×
134

135
  m_debugLogRXTX = debugLogRXTX;
×
136

137
  updateModulesMax();
×
138
}
×
139

140
void HSI88Interface::worldEvent(WorldState state, WorldEvent event)
×
141
{
142
  Interface::worldEvent(state, event);
×
143

144
  switch(event)
×
145
  {
146
    case WorldEvent::EditDisabled:
×
147
    case WorldEvent::EditEnabled:
148
    {
149
      const bool editable = contains(state, WorldState::Edit);
×
150
      Attributes::setEnabled({device, modulesLeft, modulesMiddle, modulesRight}, !online && editable);
×
151
      Attributes::setEnabled(debugLogRXTX, editable);
×
152
      break;
×
153
    }
154
    default:
×
155
      break;
×
156
  }
157
}
×
158

159
bool HSI88Interface::setOnline(bool& value, bool simulation)
×
160
{
161
  if(value)
×
162
  {
163
    if(modulesLeft + modulesMiddle + modulesRight > modulesTotal)
×
164
    {
165
      Log::log(*this, LogMessage::E2020_TOTAL_NUMBER_OF_MODULES_MAY_NOT_EXCEED_X, modulesTotal);
×
166
      return false;
×
167
    }
168

169
    m_simulation = simulation;
×
170

171
    if(simulation)
×
172
    {
173
      Log::log(*this, LogMessage::N2001_SIMULATION_NOT_SUPPORTED);
×
174
      return false;
×
175
    }
176

177
    {
178
      m_thread = std::thread(
×
179
        [this]()
×
180
        {
181
          setThreadName("hsi88");
×
NEW
182
          boost::asio::executor_work_guard<decltype(m_ioContext.get_executor())> work{m_ioContext.get_executor()};
×
183
          m_ioContext.restart();
×
184
          m_ioContext.run();
×
185
        });
×
186

NEW
187
      boost::asio::post(m_ioContext, 
×
188
        [this, dev=device.value(), ml=modulesLeft.value(), mm=modulesMiddle.value(), mr=modulesRight.value()]()
×
189
        {
190
          try
191
          {
192
            SerialPort::open(m_serialPort, dev, 9'600, 8, SerialParity::None, SerialStopBits::One, SerialFlowControl::Hardware);
×
193
          }
194
          catch(const LogMessageException& e)
×
195
          {
196
            Log::log(*this, e.message(), e.args());
×
197
            //! \todo interface status -> error
198
            return;
×
199
          }
×
200

201
          // reset buffers:
202
          m_readBufferOffset = 0;
×
203
          m_writeBuffer[0] = '\r';
×
204
          m_writeBufferOffset = 1;
×
205
          while(!m_sendQueue.empty())
×
206
            m_sendQueue.pop();
×
207
          m_waitingForReply = false;
×
208

209
          read();
×
210

211
          send(std::string{versionInquiry});
×
212
          send(std::string{terminalModeOff});
×
213
          const std::array<char, 5> registerModules = {'s', static_cast<char>(ml), static_cast<char>(mm), static_cast<char>(mr), '\r'};
×
214
          send({registerModules.data(), registerModules.size()});
×
215
        });
216
    }
217

218
    Attributes::setEnabled({device, modulesLeft, modulesMiddle, modulesRight}, false);
×
219
  }
220
  else
221
  {
222
    m_ioContext.stop();
×
223
    m_thread.join();
×
224
    m_serialPort.close();
×
225

226
    Attributes::setEnabled({device, modulesLeft, modulesMiddle, modulesRight}, contains(m_world.state, WorldState::Edit));
×
227
  }
228

229
  return true;
×
230
}
231

232
void HSI88Interface::read()
×
233
{
234
  assert(isHSI88Thread());
×
235

236
  m_serialPort.async_read_some(boost::asio::buffer(m_readBuffer.data() + m_readBufferOffset, m_readBuffer.size() - m_readBufferOffset),
×
237
    [this](const boost::system::error_code& ec, std::size_t bytesTransferred)
×
238
    {
239
      if(!ec)
×
240
      {
241
        bytesTransferred += m_readBufferOffset;
×
242

243
        char* pos = m_readBuffer.data();
×
244
        char* end = pos + bytesTransferred;
×
245

246
        while(pos < end)
×
247
        {
248
          char* it = std::find(pos, end, '\r');
×
249
          if(it == end)
×
250
            break;
×
251

252
          if(it > pos)
×
253
            receive({pos, static_cast<size_t>(it - pos)});
×
254

255
          bytesTransferred -= it - pos + 1;
×
256
          pos = it + 1;
×
257
        }
258

259
        if(bytesTransferred != 0)
×
260
          std::memmove(m_readBuffer.data(), pos, bytesTransferred);
×
261
        m_readBufferOffset = bytesTransferred;
×
262

263
        read();
×
264
      }
265
      else if(ec != boost::asio::error::operation_aborted)
×
266
      {
267
        Log::log(*this, LogMessage::E2002_SERIAL_READ_FAILED_X, ec);
×
268
        //! \todo interface status -> error
269
      }
270
    });
×
271
}
×
272

273
void HSI88Interface::write()
×
274
{
275
  assert(isHSI88Thread());
×
276

277
  m_serialPort.async_write_some(boost::asio::buffer(m_writeBuffer.data(), m_writeBufferOffset),
×
278
    [this](const boost::system::error_code& ec, std::size_t bytesTransferred)
×
279
    {
280
      if(!ec)
×
281
      {
282
        if(bytesTransferred < m_writeBufferOffset)
×
283
        {
284
          m_writeBufferOffset -= bytesTransferred;
×
285
          memmove(m_writeBuffer.data(), m_writeBuffer.data() + bytesTransferred, m_writeBufferOffset);
×
286
          write();
×
287
        }
288
        else
289
          m_writeBufferOffset = 0;
×
290
      }
291
      else if(ec != boost::asio::error::operation_aborted)
×
292
      {
293
        Log::log(*this, LogMessage::E2001_SERIAL_WRITE_FAILED_X, ec);
×
294
        //! \todo interface status -> error
295
      }
296
    });
×
297
}
×
298

299
void HSI88Interface::receive(std::string_view message)
×
300
{
301
  assert(isHSI88Thread());
×
302

303
  if(m_debugLogRXTX)
×
304
    Log::log(*this, LogMessage::D2002_RX_X, toHex(message));
×
305

306
  switch(message[0])
×
307
  {
308
    case 'i':
×
309
    {
310
      const uint8_t moduleCount = message[1];
×
311
      for(uint8_t i = 0; i < moduleCount; i++)
×
312
      {
313
        const uint8_t module = message[2 + 3 * i];
×
314
        const uint16_t bits = static_cast<uint16_t>(message[3 + 3 * i]) << 8 | message[4 + 3 * i];
×
315
        for(uint8_t j = 0; j < inputsPerModule; j++)
×
316
        {
317
          const TriState value = (bits & (static_cast<uint16_t>(1) << j)) ? TriState::True : TriState::False;
×
318
          const uint32_t index = (module - 1) * inputsPerModule + j;
×
319

320
          if(m_inputValues[index] != value)
×
321
          {
322
            m_inputValues[index] = value;
×
323

324
            EventLoop::call(
×
325
              [this, index, value]()
×
326
              {
327
                const auto moduleIndex = index / inputsPerModule;
×
328
                if(moduleIndex < modulesLeft.value())
×
329
                  updateInputValue(InputChannel::S88_Left, InputAddress(inputAddressMin + index), value);
×
330
                else if(moduleIndex < (modulesLeft.value() + modulesMiddle.value()))
×
331
                  updateInputValue(InputChannel::S88_Middle, InputAddress(inputAddressMin + index - modulesLeft.value() * inputsPerModule), value);
×
332
                else
333
                  updateInputValue(InputChannel::S88_Right, InputAddress(inputAddressMin + index - (modulesLeft.value() + modulesMiddle.value()) * inputsPerModule), value);
×
334
              });
×
335
          }
336
        }
337
      }
338
      break;
×
339
    }
340
    case 's':
×
341
      if(m_waitingForReply && !m_sendQueue.empty() && m_sendQueue.front()[0] == 's')
×
342
      {
343
        m_waitingForReply = false;
×
344
        m_sendQueue.pop();
×
345
        sendNext();
×
346
      }
347
      m_inputValues.resize(message[1] * inputsPerModule);
×
348
      std::fill(m_inputValues.begin(), m_inputValues.end(), TriState::Undefined);
×
349
      break;
×
350

351
    case 't':
×
352
      if(m_waitingForReply && !m_sendQueue.empty() && m_sendQueue.front()[0] == 't')
×
353
      {
354
        m_waitingForReply = false;
×
355
        m_sendQueue.pop();
×
356
        sendNext();
×
357
      }
358
      break;
×
359

360
    case 'v':
×
361
    case 'V':
362
      if(m_waitingForReply && !m_sendQueue.empty() && m_sendQueue.front() == versionInquiry)
×
363
      {
364
        m_waitingForReply = false;
×
365
        m_sendQueue.pop();
×
366
        sendNext();
×
367
      }
368
      Log::log(*this, LogMessage::I2004_HSI_88_X, message);
×
369
      break;
×
370
  }
371
}
×
372

373
void HSI88Interface::send(std::string message)
×
374
{
375
  assert(isHSI88Thread());
×
376

377
  m_sendQueue.emplace(std::move(message));
×
378

379
  if(!m_waitingForReply)
×
380
    sendNext();
×
381
}
×
382

383
void HSI88Interface::sendNext()
×
384
{
385
  assert(isHSI88Thread());
×
386

387
  if(m_sendQueue.empty())
×
388
    return;
×
389

390
  const auto& message = m_sendQueue.front();
×
391
  memcpy(m_writeBuffer.data() + m_writeBufferOffset, message.data(), message.size());
×
392
  m_writeBufferOffset += message.size();
×
393

394
  if(m_debugLogRXTX)
×
395
    Log::log(*this, LogMessage::D2001_TX_X, toHex(message));
×
396

397
  write();
×
398

399
  m_waitingForReply = true;
×
400
}
401

402
void HSI88Interface::updateModulesMax()
3✔
403
{
404
  const uint8_t sum = modulesLeft + modulesMiddle + modulesRight;
3✔
405
  const uint8_t unused = sum < modulesTotal ? modulesTotal - sum : 0;
3✔
406

407
  Attributes::setMax<uint8_t>(modulesLeft, modulesLeft + unused);
3✔
408
  Attributes::setMax<uint8_t>(modulesMiddle, modulesMiddle + unused);
3✔
409
  Attributes::setMax<uint8_t>(modulesRight, modulesRight + unused);
3✔
410
}
3✔
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