[cbus] added Lua support for sending CBUS/VLCB message and DCC commands
Einige Prüfungen sind fehlgeschlagen
Build / client macos-15-arm64 (push) Has been cancelled
Build / client macos-15-intel (push) Has been cancelled
Build / client ubuntu_24.04 (push) Has been cancelled
Build / client ubuntu_24.04_arm64 (push) Has been cancelled
Build / client raspberrypios_arm64 (push) Has been cancelled
Build / client raspberrypios_arm7 (push) Has been cancelled
Build / client windows_x64_msvc (push) Has been cancelled
Build / server ubuntu_24.04 (debug+ccov) (push) Has been cancelled
Build / server macos-15-arm64 (push) Has been cancelled
Build / server macos-15-intel (push) Has been cancelled
Build / server raspberrypios_arm64 (push) Has been cancelled
Build / server raspberrypios_arm7 (push) Has been cancelled
Build / server ubuntu_24.04 (push) Has been cancelled
Build / server ubuntu_24.04_arm64 (push) Has been cancelled
Build / server windows_x64_clang (push) Has been cancelled
Build / language files (push) Has been cancelled
Build / manual (push) Has been cancelled
Build / Update contributers in README.md (push) Has been cancelled
Build / shared data raspberrypios_10 (push) Has been cancelled
Build / shared data ubuntu_24.04 (push) Has been cancelled
Build / package innosetup (push) Has been cancelled
Build / Deploy to website (push) Has been cancelled

Dieser Commit ist enthalten in:
Reinder Feenstra 2026-02-21 16:20:58 +01:00
Ursprung 81d452bad1
Commit 222c219fb7
16 geänderte Dateien mit 445 neuen und 12 gelöschten Zeilen

Datei anzeigen

@ -221,6 +221,7 @@ class TraintasticHelp:
]}, ]},
{'Product index': 'appendix/supported-hardware/product-index.md'} {'Product index': 'appendix/supported-hardware/product-index.md'}
]}, ]},
{'CBUS/VLCB reference': 'appendix/cbus-vlcb.md'},
{'LocoNet reference': 'appendix/loconet.md'}, {'LocoNet reference': 'appendix/loconet.md'},
{'XpressNet reference': 'appendix/xpressnet.md'}, {'XpressNet reference': 'appendix/xpressnet.md'},
{'Lua scripting reference': lua_ref}, {'Lua scripting reference': lua_ref},

Datei anzeigen

@ -0,0 +1,44 @@
# CBUS/VLCB reference
CBUS is a Layout Control Bus developed by members of the Model Electronic Railway Group (MERG).
CBUS uses the Controller Area Network (CAN) for communication between the CBUS modules.
VLCB is an CBUS extension developed by MERG members to it adds additional commands and introduced a a stricter priority system for commands.
This appendix does **not** explain the CBUS/VLCB protocol.
Instead, it **how Traintastic implements and uses CBUS/VLCB** and which protocol messages are recognized.
It is intended for advanced users who are already familiar with the basics of the CBUS/VLCB protocol.
## Supported hardware
*TODO: under development*
## Message support
### General control
- Track on/off: `TOF`, `TON`, `RTOF`, `RTON` - Supported
- Emergency stop: `ESTOP`, `RESTP` - Supported
*TODO: under development, will be expanded when implemented*
## Debugging and monitoring
Traintastic provides a debug option for CBUS/VLCB that logs all bus traffic.
Messages are shown in **hexadecimal format**, and for many message types a human-readable textual description of the content is also provided.
This is useful for:
- Diagnosing compatibility issues with specific modules.
- Verifying that messages are transmitted and received as expected.
### Sending raw messages
Through [**Lua scripting**](../advanced/scripting-basics.md), it is also possible to:
- Send **raw CBUS/VLCB messages**, see [`send()`](lua/object/cbusinterface.md#send).
- Send **raw DCC track commands** (`RDCCn`), see [`send_dcc()`](lua/object/cbusinterface.md#send_dcc).
!!! warning "Use this with caution!"
- These messages bypass Traintastics normal handling.
- You need a solid understanding of CBUS/VLCB and DCC to avoid conflicts.
- Side effects may occur that Traintastic is not aware of or cannot manage.

Datei anzeigen

@ -106,6 +106,9 @@
"CLOCK": { "CLOCK": {
"type": "constant" "type": "constant"
}, },
"CBUS_INTERFACE": {
"type": "constant"
},
"DCCEX_INTERFACE": { "DCCEX_INTERFACE": {
"type": "constant" "type": "constant"
}, },

Datei anzeigen

@ -0,0 +1,23 @@
{
"send": {
"parameters": [
{
"name": "message"
}
],
"return_values": 1
},
"send_dcc": {
"parameters": [
{
"name": "dcc_packet"
},
{
"name": "repeat",
"optional": true,
"default": 2
}
],
"return_values": 1
}
}

Datei anzeigen

@ -3190,5 +3190,58 @@
{ {
"term": "object.category.signals:title", "term": "object.category.signals:title",
"definition": "Signals" "definition": "Signals"
},
{
"term": "object.cbusinterface:title",
"definition": "CBUS/VLCB interface"
},
{
"term": "object.cbusinterface:description",
"definition": "Interface for communicating with a CBUS/VLCB network."
},
{
"term": "object.cbusinterface.send_dcc:description",
"definition": "Request the command station to send a DCC packet to the track (`RDCCn`)."
},
{
"term": "object.cbusinterface.send_dcc.parameter.dcc_packet:description",
"definition": "Table containing two to five DCC packet bytes to send excluding checksum byte."
},
{
"term": "object.cbusinterface.send_dcc.parameter.repeat:description",
"definition": "Number of times to repeat the packet on the track. (1...255)"
},
{
"term": "object.cbusinterface.send_dcc:return_values",
"definition": "`true` if send, `false` otherwise."
},
{
"term": "object.cbusinterface.send_dcc.warning:title",
"definition": "Use this with caution!"
},
{
"term": "object.cbusinterface.send_dcc.warning:description",
"definition": "These messages bypass Traintastics normal handling. You need a solid understanding of DCC to avoid conflicts. Side effects may occur that Traintastic is not aware of or cannot manage."
},
{
"term": "object.cbusinterface.send:description",
"definition": "Send CBUS/VLCB message."
},
{
"term": "object.cbusinterface.send.parameter.message:description",
"definition": "CBUS/VLCB message bytes. (1...8)"
},
{
"term": "object.cbusinterface.send:return_values",
"definition": "`true` if send, `false` otherwise."
},
{
"term": "object.cbusinterface.send.warning:title",
"definition": "Use this with caution!"
},
{
"term": "object.cbusinterface.send.warning:description",
"definition": "These messages bypass Traintastics normal handling. You need a solid understanding of CBUS/VLCB to avoid conflicts. Side effects may occur that Traintastic is not aware of or cannot manage."
} }
] ]

Datei anzeigen

@ -36,6 +36,10 @@
#include "../../utils/displayname.hpp" #include "../../utils/displayname.hpp"
#include "../../world/world.hpp" #include "../../world/world.hpp"
namespace CBUS {
class Simulator{};
}
CREATE_IMPL(CBUSInterface) CREATE_IMPL(CBUSInterface)
CBUSInterface::CBUSInterface(World& world, std::string_view _id) CBUSInterface::CBUSInterface(World& world, std::string_view _id)
@ -88,6 +92,24 @@ CBUSInterface::CBUSInterface(World& world, std::string_view _id)
CBUSInterface::~CBUSInterface() = default; CBUSInterface::~CBUSInterface() = default;
bool CBUSInterface::send(std::vector<uint8_t> message)
{
if(m_kernel)
{
return m_kernel->send(std::move(message));
}
return false;
}
bool CBUSInterface::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
{
if(m_kernel)
{
return m_kernel->sendDCC(std::move(dccPacket), repeat);
}
return false;
}
void CBUSInterface::addToWorld() void CBUSInterface::addToWorld()
{ {
Interface::addToWorld(); Interface::addToWorld();

Datei anzeigen

@ -30,7 +30,7 @@ class CBUSSettings;
namespace CBUS { namespace CBUS {
class Kernel; class Kernel;
class Simulator {}; class Simulator;
} }
/** /**
@ -53,6 +53,17 @@ public:
CBUSInterface(World& world, std::string_view _id); CBUSInterface(World& world, std::string_view _id);
~CBUSInterface() final; ~CBUSInterface() final;
//! \brief Send CBUS/VLCB message
//! \param[in] message CBUS/VLCB message bytes, 1..8 bytes.
//! \return \c true if send, \c false otherwise.
bool send(std::vector<uint8_t> message);
//! \brief Send DCC packet
//! \param[in] dccPacket DCC packet byte, exluding checksum. Length is limited to 6.
//! \param[in] repeat DCC packet repeat count 0..7
//! \return \c true if send, \c false otherwise.
bool sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat);
protected: protected:
void addToWorld() final; void addToWorld() final;
void loaded() final; void loaded() final;

Datei anzeigen

@ -25,9 +25,11 @@
/* /*
#include "simulator/cbussimulator.hpp" #include "simulator/cbussimulator.hpp"
*/ */
#include "../dcc/dcc.hpp"
#include "../../../core/eventloop.hpp" #include "../../../core/eventloop.hpp"
#include "../../../log/log.hpp" #include "../../../log/log.hpp"
#include "../../../log/logmessageexception.hpp" #include "../../../log/logmessageexception.hpp"
#include "../../../utils/inrange.hpp"
#include "../../../utils/setthreadname.hpp" #include "../../../utils/setthreadname.hpp"
namespace CBUS { namespace CBUS {
@ -191,6 +193,65 @@ void Kernel::requestEmergencyStop()
}); });
} }
bool Kernel::send(std::vector<uint8_t> message)
{
assert(isEventLoopThread());
if(!inRange<size_t>(message.size(), 1, 8))
{
return false;
}
m_ioContext.post(
[this, msg=std::move(message)]()
{
send(*reinterpret_cast<const Message*>(msg.data()));
});
return true;
}
bool Kernel::sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat)
{
assert(isEventLoopThread());
if(!inRange<size_t>(dccPacket.size(), 2, 5) || repeat == 0)
{
return false;
}
dccPacket.emplace_back(DCC::calcChecksum(dccPacket));
m_ioContext.post(
[this, packet=std::move(dccPacket), repeat]()
{
switch(packet.size())
{
case 3:
send(RequestDCCPacket<3>(packet, repeat));
break;
case 4:
send(RequestDCCPacket<4>(packet, repeat));
break;
case 5:
send(RequestDCCPacket<5>(packet, repeat));
break;
case 6:
send(RequestDCCPacket<6>(packet, repeat));
break;
default: [[unlikely]]
assert(false);
break;
}
});
return true;
}
void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler) void Kernel::setIOHandler(std::unique_ptr<IOHandler> handler)
{ {
assert(isEventLoopThread()); assert(isEventLoopThread());

Datei anzeigen

@ -89,6 +89,9 @@ public:
void trackOn(); void trackOn();
void requestEmergencyStop(); void requestEmergencyStop();
bool send(std::vector<uint8_t> message);
bool sendDCC(std::vector<uint8_t> dccPacket, uint8_t repeat);
private: private:
std::unique_ptr<IOHandler> m_ioHandler; std::unique_ptr<IOHandler> m_ioHandler;
const bool m_simulation; const bool m_simulation;

Datei anzeigen

@ -24,5 +24,6 @@
#include "messages/cbusenginemessages.hpp" #include "messages/cbusenginemessages.hpp"
#include "messages/cbusgeneralmessages.hpp" #include "messages/cbusgeneralmessages.hpp"
#include "messages/cbusrequestdccpacketmessage.hpp"
#endif #endif

Datei anzeigen

@ -0,0 +1,67 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2026 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_MESSAGES_CBUSREQUESTDCCPACKETMESSAGE_HPP
#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_MESSAGES_CBUSREQUESTDCCPACKETMESSAGE_HPP
#include "cbusmessage.hpp"
#include <cassert>
#include <cstring>
#include <span>
namespace CBUS {
template<size_t N>
requires(N >= 3 && N <= 6)
struct RequestDCCPacket : Message
{
uint8_t repeat;
uint8_t data[N];
RequestDCCPacket(std::span<const uint8_t> bytes, uint8_t repeat_)
: Message(RDCCn())
, repeat{repeat_}
{
assert(N == bytes.size());
std::memcpy(data, bytes.data(), N);
}
private:
static constexpr OpCode RDCCn() noexcept
{
switch(N)
{
case 3: return OpCode::RDCC3;
case 4: return OpCode::RDCC4;
case 5: return OpCode::RDCC5;
case 6: return OpCode::RDCC6;
}
return static_cast<OpCode>(0); // unreachable, should never happen
}
};
static_assert(sizeof(RequestDCCPacket<3>) == 5);
static_assert(sizeof(RequestDCCPacket<4>) == 6);
static_assert(sizeof(RequestDCCPacket<5>) == 7);
static_assert(sizeof(RequestDCCPacket<6>) == 8);
}
#endif

Datei anzeigen

@ -1,9 +1,8 @@
/** /**
* server/src/hardware/protocol/dcc/dcc.hpp * This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
* *
* This file is part of the traintastic source code. * Copyright (C) 2021-2026 Reinder Feenstra
*
* Copyright (C) 2021,2023-2024 Reinder Feenstra
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -24,6 +23,7 @@
#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_DCC_DCC_HPP #define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_DCC_DCC_HPP
#include <cstdint> #include <cstdint>
#include <span>
#include <traintastic/enum/decoderprotocol.hpp> #include <traintastic/enum/decoderprotocol.hpp>
#include "../../../utils/inrange.hpp" #include "../../../utils/inrange.hpp"
@ -45,6 +45,20 @@ constexpr DecoderProtocol getProtocol(uint16_t address)
return isLongAddress(address) ? DecoderProtocol::DCCLong : DecoderProtocol::DCCShort; return isLongAddress(address) ? DecoderProtocol::DCCLong : DecoderProtocol::DCCShort;
} }
constexpr uint8_t calcChecksum(std::span<const uint8_t> message)
{
if(message.empty()) [[unlikely]]
{
return 0;
}
uint8_t checksum = message[0];
for(size_t i = 1; i < message.size(); i++)
{
checksum ^= message[i];
}
return checksum;
}
namespace Accessory { namespace Accessory {
constexpr uint16_t addressMin = 1; constexpr uint16_t addressMin = 1;

Datei anzeigen

@ -62,6 +62,7 @@
#include "../clock/clock.hpp" #include "../clock/clock.hpp"
#include "../hardware/interface/cbusinterface.hpp"
#include "../hardware/interface/dccexinterface.hpp" #include "../hardware/interface/dccexinterface.hpp"
#include "../hardware/interface/ecosinterface.hpp" #include "../hardware/interface/ecosinterface.hpp"
#include "../hardware/interface/hsi88.hpp" #include "../hardware/interface/hsi88.hpp"
@ -196,6 +197,7 @@ void Class::registerValues(lua_State* L)
registerValue<Clock>(L, "CLOCK"); registerValue<Clock>(L, "CLOCK");
// hardware - interface: // hardware - interface:
registerValue<CBUSInterface>(L, "CBUS_INTERFACE");
registerValue<DCCEXInterface>(L, "DCCEX_INTERFACE"); registerValue<DCCEXInterface>(L, "DCCEX_INTERFACE");
registerValue<ECoSInterface>(L, "ECOS_INTERFACE"); registerValue<ECoSInterface>(L, "ECOS_INTERFACE");
registerValue<HSI88Interface>(L, "HSI88_INTERFACE"); registerValue<HSI88Interface>(L, "HSI88_INTERFACE");

Datei anzeigen

@ -1,9 +1,8 @@
/** /**
* server/src/lua/object.cpp * This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
* *
* This file is part of the traintastic source code. * Copyright (C) 2019-2026 Reinder Feenstra
*
* Copyright (C) 2019-2025 Reinder Feenstra
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -24,6 +23,7 @@
#include "object/object.hpp" #include "object/object.hpp"
#include "object/objectlist.hpp" #include "object/objectlist.hpp"
#include "object/interface.hpp" #include "object/interface.hpp"
#include "object/cbusinterface.hpp"
#include "object/loconetinterface.hpp" #include "object/loconetinterface.hpp"
#include "object/scriptthrottle.hpp" #include "object/scriptthrottle.hpp"
@ -36,6 +36,7 @@ void registerTypes(lua_State* L)
Object::registerType(L); Object::registerType(L);
ObjectList::registerType(L); ObjectList::registerType(L);
Interface::registerType(L); Interface::registerType(L);
CBUSInterface::registerType(L);
LocoNetInterface::registerType(L); LocoNetInterface::registerType(L);
ScriptThrottle::registerType(L); ScriptThrottle::registerType(L);
@ -65,7 +66,11 @@ void push(lua_State* L, const ObjectPtr& value)
lua_pop(L, 1); // remove nil lua_pop(L, 1); // remove nil
new(lua_newuserdata(L, sizeof(ObjectPtrWeak))) ObjectPtrWeak(value); new(lua_newuserdata(L, sizeof(ObjectPtrWeak))) ObjectPtrWeak(value);
if(dynamic_cast<::LocoNetInterface*>(value.get())) if(dynamic_cast<::CBUSInterface*>(value.get()))
{
luaL_setmetatable(L, CBUSInterface::metaTableName);
}
else if(dynamic_cast<::LocoNetInterface*>(value.get()))
luaL_setmetatable(L, LocoNetInterface::metaTableName); luaL_setmetatable(L, LocoNetInterface::metaTableName);
else if(dynamic_cast<AbstractObjectList*>(value.get())) else if(dynamic_cast<AbstractObjectList*>(value.get()))
luaL_setmetatable(L, ObjectList::metaTableName); luaL_setmetatable(L, ObjectList::metaTableName);

Datei anzeigen

@ -0,0 +1,75 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2026 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "cbusinterface.hpp"
#include "interface.hpp"
#include "object.hpp"
#include "../check.hpp"
#include "../checkarguments.hpp"
#include "../checkvector.hpp"
#include "../push.hpp"
#include "../to.hpp"
#include "../metatable.hpp"
namespace Lua::Object {
void CBUSInterface::registerType(lua_State* L)
{
MetaTable::clone(L, Interface::metaTableName, metaTableName);
lua_pushcfunction(L, __index);
lua_setfield(L, -2, "__index");
lua_pop(L, 1);
}
int CBUSInterface::index(lua_State* L, ::CBUSInterface& object)
{
const auto key = to<std::string_view>(L, 2);
LUA_OBJECT_METHOD(send)
LUA_OBJECT_METHOD(send_dcc)
return Interface::index(L, object);
}
int CBUSInterface::__index(lua_State* L)
{
return index(L, *check<::CBUSInterface>(L, 1));
}
int CBUSInterface::send(lua_State* L)
{
checkArguments(L, 1);
auto interface = check<::CBUSInterface>(L, lua_upvalueindex(1));
auto message = checkVector<uint8_t>(L, 1);
Lua::push(L, interface->send(std::move(message)));
return 1;
}
int CBUSInterface::send_dcc(lua_State* L)
{
const uint8_t defaultRepeat = 2;
const int argc = checkArguments(L, 1, 2);
auto interface = check<::CBUSInterface>(L, lua_upvalueindex(1));
auto dccPacket = checkVector<uint8_t>(L, 1);
auto repeat = (argc >= 2) ? check<uint8_t>(L, 2) : defaultRepeat;
Lua::push(L, interface->sendDCC(std::move(dccPacket), repeat));
return 1;
}
}

Datei anzeigen

@ -0,0 +1,48 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2026 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_LUA_OBJECT_CBUSINTERFACE_HPP
#define TRAINTASTIC_SERVER_LUA_OBJECT_CBUSINTERFACE_HPP
#include <lua.hpp>
#include "../../hardware/interface/cbusinterface.hpp"
namespace Lua::Object {
class CBUSInterface
{
private:
static int __index(lua_State* L);
static int send(lua_State* L);
static int send_dcc(lua_State* L);
public:
static constexpr char const* metaTableName = "object.interface.cbus";
static void registerType(lua_State* L);
static int index(lua_State* L, ::CBUSInterface& object);
};
}
#endif