diff --git a/manual/build.py b/manual/build.py index dc8ff4fb..d19563de 100755 --- a/manual/build.py +++ b/manual/build.py @@ -221,6 +221,7 @@ class TraintasticHelp: ]}, {'Product index': 'appendix/supported-hardware/product-index.md'} ]}, + {'CBUS/VLCB reference': 'appendix/cbus-vlcb.md'}, {'LocoNet reference': 'appendix/loconet.md'}, {'XpressNet reference': 'appendix/xpressnet.md'}, {'Lua scripting reference': lua_ref}, diff --git a/manual/docs/en/appendix/cbus-vlcb.md b/manual/docs/en/appendix/cbus-vlcb.md new file mode 100644 index 00000000..c1cbbaaf --- /dev/null +++ b/manual/docs/en/appendix/cbus-vlcb.md @@ -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 Traintastic’s 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. diff --git a/manual/luadoc/class.json b/manual/luadoc/class.json index 7d52030a..5d67cc02 100644 --- a/manual/luadoc/class.json +++ b/manual/luadoc/class.json @@ -106,6 +106,9 @@ "CLOCK": { "type": "constant" }, + "CBUS_INTERFACE": { + "type": "constant" + }, "DCCEX_INTERFACE": { "type": "constant" }, @@ -228,4 +231,4 @@ "type": "constant", "since": "0.3" } -} \ No newline at end of file +} diff --git a/manual/luadoc/object/cbusinterface.json b/manual/luadoc/object/cbusinterface.json new file mode 100644 index 00000000..68c7f82c --- /dev/null +++ b/manual/luadoc/object/cbusinterface.json @@ -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 + } +} diff --git a/manual/luadoc/terms/en-us.json b/manual/luadoc/terms/en-us.json index 7220c046..5cc27c62 100644 --- a/manual/luadoc/terms/en-us.json +++ b/manual/luadoc/terms/en-us.json @@ -3190,5 +3190,58 @@ { "term": "object.category.signals:title", "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 Traintastic’s 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 Traintastic’s 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." } -] \ No newline at end of file +] diff --git a/server/src/hardware/interface/cbusinterface.cpp b/server/src/hardware/interface/cbusinterface.cpp index 945d31cf..86fe68a3 100644 --- a/server/src/hardware/interface/cbusinterface.cpp +++ b/server/src/hardware/interface/cbusinterface.cpp @@ -36,6 +36,10 @@ #include "../../utils/displayname.hpp" #include "../../world/world.hpp" +namespace CBUS { +class Simulator{}; +} + CREATE_IMPL(CBUSInterface) CBUSInterface::CBUSInterface(World& world, std::string_view _id) @@ -88,6 +92,24 @@ CBUSInterface::CBUSInterface(World& world, std::string_view _id) CBUSInterface::~CBUSInterface() = default; +bool CBUSInterface::send(std::vector message) +{ + if(m_kernel) + { + return m_kernel->send(std::move(message)); + } + return false; +} + +bool CBUSInterface::sendDCC(std::vector dccPacket, uint8_t repeat) +{ + if(m_kernel) + { + return m_kernel->sendDCC(std::move(dccPacket), repeat); + } + return false; +} + void CBUSInterface::addToWorld() { Interface::addToWorld(); diff --git a/server/src/hardware/interface/cbusinterface.hpp b/server/src/hardware/interface/cbusinterface.hpp index f60be027..a14d8495 100644 --- a/server/src/hardware/interface/cbusinterface.hpp +++ b/server/src/hardware/interface/cbusinterface.hpp @@ -30,7 +30,7 @@ class CBUSSettings; namespace CBUS { class Kernel; -class Simulator {}; +class Simulator; } /** @@ -53,6 +53,17 @@ public: CBUSInterface(World& world, std::string_view _id); ~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 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 dccPacket, uint8_t repeat); + protected: void addToWorld() final; void loaded() final; diff --git a/server/src/hardware/protocol/cbus/cbuskernel.cpp b/server/src/hardware/protocol/cbus/cbuskernel.cpp index b9f55583..a81930d4 100644 --- a/server/src/hardware/protocol/cbus/cbuskernel.cpp +++ b/server/src/hardware/protocol/cbus/cbuskernel.cpp @@ -25,9 +25,11 @@ /* #include "simulator/cbussimulator.hpp" */ +#include "../dcc/dcc.hpp" #include "../../../core/eventloop.hpp" #include "../../../log/log.hpp" #include "../../../log/logmessageexception.hpp" +#include "../../../utils/inrange.hpp" #include "../../../utils/setthreadname.hpp" namespace CBUS { @@ -191,6 +193,65 @@ void Kernel::requestEmergencyStop() }); } +bool Kernel::send(std::vector message) +{ + assert(isEventLoopThread()); + + if(!inRange(message.size(), 1, 8)) + { + return false; + } + + m_ioContext.post( + [this, msg=std::move(message)]() + { + send(*reinterpret_cast(msg.data())); + }); + + return true; +} + +bool Kernel::sendDCC(std::vector dccPacket, uint8_t repeat) +{ + assert(isEventLoopThread()); + + if(!inRange(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 handler) { assert(isEventLoopThread()); diff --git a/server/src/hardware/protocol/cbus/cbuskernel.hpp b/server/src/hardware/protocol/cbus/cbuskernel.hpp index 69ea581b..46326b48 100644 --- a/server/src/hardware/protocol/cbus/cbuskernel.hpp +++ b/server/src/hardware/protocol/cbus/cbuskernel.hpp @@ -89,6 +89,9 @@ public: void trackOn(); void requestEmergencyStop(); + bool send(std::vector message); + bool sendDCC(std::vector dccPacket, uint8_t repeat); + private: std::unique_ptr m_ioHandler; const bool m_simulation; diff --git a/server/src/hardware/protocol/cbus/cbusmessages.hpp b/server/src/hardware/protocol/cbus/cbusmessages.hpp index 70099189..9bbf12a6 100644 --- a/server/src/hardware/protocol/cbus/cbusmessages.hpp +++ b/server/src/hardware/protocol/cbus/cbusmessages.hpp @@ -24,5 +24,6 @@ #include "messages/cbusenginemessages.hpp" #include "messages/cbusgeneralmessages.hpp" +#include "messages/cbusrequestdccpacketmessage.hpp" #endif diff --git a/server/src/hardware/protocol/cbus/messages/cbusrequestdccpacketmessage.hpp b/server/src/hardware/protocol/cbus/messages/cbusrequestdccpacketmessage.hpp new file mode 100644 index 00000000..a7f42db6 --- /dev/null +++ b/server/src/hardware/protocol/cbus/messages/cbusrequestdccpacketmessage.hpp @@ -0,0 +1,67 @@ +/** + * This file is part of Traintastic, + * see . + * + * 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 +#include +#include + +namespace CBUS { + +template +requires(N >= 3 && N <= 6) +struct RequestDCCPacket : Message +{ + uint8_t repeat; + uint8_t data[N]; + + RequestDCCPacket(std::span 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(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 diff --git a/server/src/hardware/protocol/dcc/dcc.hpp b/server/src/hardware/protocol/dcc/dcc.hpp index a4d405af..fa370687 100644 --- a/server/src/hardware/protocol/dcc/dcc.hpp +++ b/server/src/hardware/protocol/dcc/dcc.hpp @@ -1,9 +1,8 @@ /** - * server/src/hardware/protocol/dcc/dcc.hpp + * This file is part of Traintastic, + * see . * - * This file is part of the traintastic source code. - * - * Copyright (C) 2021,2023-2024 Reinder Feenstra + * Copyright (C) 2021-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 @@ -24,6 +23,7 @@ #define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_DCC_DCC_HPP #include +#include #include #include "../../../utils/inrange.hpp" @@ -45,6 +45,20 @@ constexpr DecoderProtocol getProtocol(uint16_t address) return isLongAddress(address) ? DecoderProtocol::DCCLong : DecoderProtocol::DCCShort; } +constexpr uint8_t calcChecksum(std::span 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 { constexpr uint16_t addressMin = 1; diff --git a/server/src/lua/class.cpp b/server/src/lua/class.cpp index 5a83db4d..7decdc3a 100644 --- a/server/src/lua/class.cpp +++ b/server/src/lua/class.cpp @@ -62,6 +62,7 @@ #include "../clock/clock.hpp" +#include "../hardware/interface/cbusinterface.hpp" #include "../hardware/interface/dccexinterface.hpp" #include "../hardware/interface/ecosinterface.hpp" #include "../hardware/interface/hsi88.hpp" @@ -196,6 +197,7 @@ void Class::registerValues(lua_State* L) registerValue(L, "CLOCK"); // hardware - interface: + registerValue(L, "CBUS_INTERFACE"); registerValue(L, "DCCEX_INTERFACE"); registerValue(L, "ECOS_INTERFACE"); registerValue(L, "HSI88_INTERFACE"); diff --git a/server/src/lua/object.cpp b/server/src/lua/object.cpp index 1e000d52..d785e230 100644 --- a/server/src/lua/object.cpp +++ b/server/src/lua/object.cpp @@ -1,9 +1,8 @@ /** - * server/src/lua/object.cpp + * This file is part of Traintastic, + * see . * - * This file is part of the traintastic source code. - * - * Copyright (C) 2019-2025 Reinder Feenstra + * Copyright (C) 2019-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 @@ -24,6 +23,7 @@ #include "object/object.hpp" #include "object/objectlist.hpp" #include "object/interface.hpp" +#include "object/cbusinterface.hpp" #include "object/loconetinterface.hpp" #include "object/scriptthrottle.hpp" @@ -36,6 +36,7 @@ void registerTypes(lua_State* L) Object::registerType(L); ObjectList::registerType(L); Interface::registerType(L); + CBUSInterface::registerType(L); LocoNetInterface::registerType(L); ScriptThrottle::registerType(L); @@ -65,7 +66,11 @@ void push(lua_State* L, const ObjectPtr& value) lua_pop(L, 1); // remove nil 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); else if(dynamic_cast(value.get())) luaL_setmetatable(L, ObjectList::metaTableName); diff --git a/server/src/lua/object/cbusinterface.cpp b/server/src/lua/object/cbusinterface.cpp new file mode 100644 index 00000000..e0fa105d --- /dev/null +++ b/server/src/lua/object/cbusinterface.cpp @@ -0,0 +1,75 @@ +/** + * This file is part of Traintastic, + * see . + * + * 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(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(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(L, 1); + auto repeat = (argc >= 2) ? check(L, 2) : defaultRepeat; + Lua::push(L, interface->sendDCC(std::move(dccPacket), repeat)); + return 1; +} + +} diff --git a/server/src/lua/object/cbusinterface.hpp b/server/src/lua/object/cbusinterface.hpp new file mode 100644 index 00000000..40fadb1a --- /dev/null +++ b/server/src/lua/object/cbusinterface.hpp @@ -0,0 +1,48 @@ +/** + * This file is part of Traintastic, + * see . + * + * 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 +#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