diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 1265d79f..dda80678 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -105,6 +105,7 @@ file(GLOB SOURCES "src/hardware/input/monitor/*.cpp" "src/hardware/interface/*.hpp" "src/hardware/interface/*.cpp" + "src/hardware/interface/cbus/*.cpp" "src/hardware/interface/marklincan/*.hpp" "src/hardware/interface/marklincan/*.cpp" "src/hardware/output/*.hpp" @@ -119,6 +120,8 @@ file(GLOB SOURCES "src/hardware/programming/lncv/*.cpp" "src/hardware/protocol/*.hpp" "src/hardware/protocol/*.cpp" + "src/hardware/protocol/cbus/*.cpp" + "src/hardware/protocol/cbus/iohandler/*.cpp" "src/hardware/protocol/dccex/*.hpp" "src/hardware/protocol/dccex/*.cpp" "src/hardware/protocol/dccex/iohandler/*.hpp" diff --git a/server/src/hardware/interface/cbus/cbussettings.cpp b/server/src/hardware/interface/cbus/cbussettings.cpp new file mode 100644 index 00000000..4117bb9e --- /dev/null +++ b/server/src/hardware/interface/cbus/cbussettings.cpp @@ -0,0 +1,40 @@ +/** + * 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 "cbussettings.hpp" +#include "../../../core/attributes.hpp" +#include "../../../utils/displayname.hpp" + +CBUSSettings::CBUSSettings(Object& _parent, std::string_view parentPropertyName) + : SubObject(_parent, parentPropertyName) + , debugLogRXTX{this, "debug_log_rx_tx", false, PropertyFlags::ReadWrite | PropertyFlags::Store} +{ + Attributes::addDisplayName(debugLogRXTX, DisplayName::Hardware::debugLogRXTX); + //Attributes::addGroup(debugLogRXTX, Group::debug); + m_interfaceItems.add(debugLogRXTX); +} + +CBUS::Config CBUSSettings::config() const +{ + return CBUS::Config{ + .debugLogRXTX = debugLogRXTX, + }; +} diff --git a/server/src/hardware/interface/cbus/cbussettings.hpp b/server/src/hardware/interface/cbus/cbussettings.hpp new file mode 100644 index 00000000..c99f9dd2 --- /dev/null +++ b/server/src/hardware/interface/cbus/cbussettings.hpp @@ -0,0 +1,41 @@ +/** + * 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_INTERFACE_CBUS_CBUSSETTINGS_HPP +#define TRAINTASTIC_SERVER_HARDWARE_INTERFACE_CBUS_CBUSSETTINGS_HPP + +#include "../../../core/subobject.hpp" +#include "../../../core/property.hpp" +#include "../../../hardware/protocol/cbus/cbusconfig.hpp" + +class CBUSSettings final : public SubObject +{ + CLASS_ID("cbus_settings") + +public: + Property debugLogRXTX; + + CBUSSettings(Object& _parent, std::string_view parentPropertyName); + + CBUS::Config config() const; +}; + +#endif diff --git a/server/src/hardware/interface/cbusinterface.cpp b/server/src/hardware/interface/cbusinterface.cpp new file mode 100644 index 00000000..945d31cf --- /dev/null +++ b/server/src/hardware/interface/cbusinterface.cpp @@ -0,0 +1,238 @@ +/** + * 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 "cbus/cbussettings.hpp" +#include "../protocol/cbus/cbuskernel.hpp" +#include "../protocol/cbus/iohandler/cbuscanusbiohandler.hpp" +#include "../protocol/cbus/iohandler/cbuscanetheriohandler.hpp" +/* +#include "../protocol/cbus/simulator/cbussimulator.hpp" +*/ +#include "../../core/attributes.hpp" +#include "../../core/eventloop.hpp" +#include "../../core/method.tpp" +#include "../../core/objectproperty.tpp" +#include "../../log/log.hpp" +#include "../../log/logmessageexception.hpp" +#include "../../utils/displayname.hpp" +#include "../../world/world.hpp" + +CREATE_IMPL(CBUSInterface) + +CBUSInterface::CBUSInterface(World& world, std::string_view _id) + : Interface(world, _id) + , type{this, "type", CBUSInterfaceType::CANUSB, PropertyFlags::ReadWrite | PropertyFlags::Store, + [this](CBUSInterfaceType /*value*/) + { + updateVisible(); + }} + , device{this, "device", "", PropertyFlags::ReadWrite | PropertyFlags::Store} + , hostname{this, "hostname", "", PropertyFlags::ReadWrite | PropertyFlags::Store} + , port{this, "port", 0, PropertyFlags::ReadWrite | PropertyFlags::Store} + , cbus{this, "cbus", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject} +{ + name = "CBUS/VLCB"; + cbus.setValueInternal(std::make_shared(*this, cbus.name())); + + Attributes::addDisplayName(type, DisplayName::Interface::type); + Attributes::addEnabled(type, !online); + Attributes::addValues(type, CBUSInterfaceTypeValues); + m_interfaceItems.insertBefore(type, notes); + + Attributes::addEnabled(device, !online); + Attributes::addVisible(device, false); + m_interfaceItems.insertBefore(device, notes); + + Attributes::addDisplayName(hostname, DisplayName::IP::hostname); + Attributes::addEnabled(hostname, !online); + Attributes::addVisible(hostname, false); + m_interfaceItems.insertBefore(hostname, notes); + + Attributes::addDisplayName(port, DisplayName::IP::port); + Attributes::addEnabled(port, !online); + Attributes::addVisible(port, false); + m_interfaceItems.insertBefore(port, notes); + + m_interfaceItems.insertBefore(cbus, notes); + + m_cbusPropertyChanged = cbus->propertyChanged.connect( + [this](BaseProperty& /*property*/) + { + if(m_kernel) + { + m_kernel->setConfig(cbus->config()); + } + }); + + updateVisible(); +} + +CBUSInterface::~CBUSInterface() = default; + +void CBUSInterface::addToWorld() +{ + Interface::addToWorld(); +} + +void CBUSInterface::loaded() +{ + Interface::loaded(); + + updateVisible(); +} + +void CBUSInterface::destroying() +{ + m_cbusPropertyChanged.disconnect(); + Interface::destroying(); +} + +void CBUSInterface::worldEvent(WorldState state, WorldEvent event) +{ + Interface::worldEvent(state, event); + + switch(event) + { + case WorldEvent::PowerOff: + if(m_kernel) + { + m_kernel->trackOff(); + } + break; + + case WorldEvent::PowerOn: + if(m_kernel) + { + m_kernel->trackOn(); + } + break; + + case WorldEvent::Stop: + if(m_kernel) + { + m_kernel->requestEmergencyStop(); + } + break; + + case WorldEvent::Run: + if(m_kernel) + { + // TODO: send all known speed values + } + break; + + default: + break; + } +} + +bool CBUSInterface::setOnline(bool& value, bool simulation) +{ + if(!m_kernel && value) + { + try + { + (void)simulation; // silence warning until simulation is added + /* TODO: simulation support + if(simulation) + { + m_simulator = std::make_unique(); + m_kernel = CBUS::Kernel::create(id.value(), cbus->config(), std::ref(*m_simulator)); + } + else + */ + { + switch(type) + { + case CBUSInterfaceType::CANUSB: + m_kernel = CBUS::Kernel::create(id.value(), cbus->config(), device.value()); + break; + + case CBUSInterfaceType::CANEther: + m_kernel = CBUS::Kernel::create(id.value(), cbus->config(), hostname.value(), port.value()); + break; + } + } + + setState(InterfaceState::Initializing); + + m_kernel->setOnStarted( + [this]() + { + setState(InterfaceState::Online); + }); + m_kernel->setOnError( + [this]() + { + setState(InterfaceState::Error); + online = false; // communication no longer possible + }); + m_kernel->onTrackOff = + [this]() + { + m_world.powerOff(); + }; + m_kernel->onTrackOn = + [this]() + { + m_world.powerOn(); + }; + m_kernel->onEmergencyStop = + [this]() + { + m_world.stop(); + }; + + m_kernel->start(); + } + catch(const LogMessageException& e) + { + setState(InterfaceState::Offline); + Log::log(*this, e.message(), e.args()); + return false; + } + } + else if(m_kernel && !value) + { + m_kernel->stop(); + EventLoop::deleteLater(m_kernel.release()); + /* + EventLoop::deleteLater(m_simulator.release()); + */ + + if(status->state != InterfaceState::Error) + { + setState(InterfaceState::Offline); + } + } + return true; +} + +void CBUSInterface::updateVisible() +{ + const bool isSerial = (type == CBUSInterfaceType::CANUSB); + Attributes::setVisible(device, isSerial); + + const bool isNetwork = (type == CBUSInterfaceType::CANEther); + Attributes::setVisible(hostname, isNetwork); + Attributes::setVisible(port, isNetwork); +} diff --git a/server/src/hardware/interface/cbusinterface.hpp b/server/src/hardware/interface/cbusinterface.hpp new file mode 100644 index 00000000..f60be027 --- /dev/null +++ b/server/src/hardware/interface/cbusinterface.hpp @@ -0,0 +1,72 @@ +/** + * 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_INTERFACE_CBUSINTERFACE_HPP +#define TRAINTASTIC_SERVER_HARDWARE_INTERFACE_CBUSINTERFACE_HPP + +#include "interface.hpp" +#include "../../core/serialdeviceproperty.hpp" +#include + +class CBUSSettings; + +namespace CBUS { +class Kernel; +class Simulator {}; +} + +/** + * @brief CBUS hardware interface + */ +class CBUSInterface final + : public Interface +{ + CLASS_ID("interface.cbus") + DEFAULT_ID("cbus") + CREATE_DEF(CBUSInterface) + +public: + Property type; + SerialDeviceProperty device; + Property hostname; + Property port; + ObjectProperty cbus; + + CBUSInterface(World& world, std::string_view _id); + ~CBUSInterface() final; + +protected: + void addToWorld() final; + void loaded() final; + void destroying() final; + void worldEvent(WorldState state, WorldEvent event) final; + + bool setOnline(bool& value, bool simulation) final; + +private: + std::unique_ptr m_kernel; + std::unique_ptr m_simulator; + boost::signals2::connection m_cbusPropertyChanged; + + void updateVisible(); +}; + +#endif diff --git a/server/src/hardware/interface/interfaces.cpp b/server/src/hardware/interface/interfaces.cpp index a1e4cac4..7e07dff0 100644 --- a/server/src/hardware/interface/interfaces.cpp +++ b/server/src/hardware/interface/interfaces.cpp @@ -1,9 +1,8 @@ /** - * server/src/hardware/interface/interfaces.cpp + * This file is part of Traintastic, + * see . * - * This file is part of the traintastic source code. - * - * Copyright (C) 2021-2025 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 @@ -25,6 +24,7 @@ #include "../../world/world.hpp" #include "../../utils/makearray.hpp" +#include "cbusinterface.hpp" #include "dccexinterface.hpp" #include "ecosinterface.hpp" #include "hsi88.hpp" @@ -39,6 +39,7 @@ std::span Interfaces::classList() { static constexpr auto classes = makeArray( + CBUSInterface::classId, DCCEXInterface::classId, ECoSInterface::classId, HSI88Interface::classId, @@ -55,6 +56,7 @@ std::span Interfaces::classList() std::shared_ptr Interfaces::create(World& world, std::string_view classId, std::string_view id) { + IF_CLASSID_CREATE(CBUSInterface) IF_CLASSID_CREATE(DCCEXInterface) IF_CLASSID_CREATE(ECoSInterface) IF_CLASSID_CREATE(HSI88Interface) diff --git a/server/src/hardware/protocol/cbus/cbusgetminorpriority.hpp b/server/src/hardware/protocol/cbus/cbusgetminorpriority.hpp new file mode 100644 index 00000000..441bac3c --- /dev/null +++ b/server/src/hardware/protocol/cbus/cbusgetminorpriority.hpp @@ -0,0 +1,168 @@ +/** + * 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_CBUSGETMINORPRIORITY_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_CBUSGETMINORPRIORITY_HPP + +#include "cbusopcode.hpp" +#include "cbuspriority.hpp" + +namespace CBUS { + +constexpr MinorPriority getMinorPriority(OpCode opCode) +{ + switch(opCode) + { + using enum OpCode; + + case HLT: + case ARST: + case RESTP: + return MinorPriority::High; + + case BON: + case TOF: + case TON: + case ESTOP: + case RTOF: + case RTON: + return MinorPriority::AboveNormal; + + case ACK: + case NAK: + case RSTAT: + case RQMN: + case KLOC: + case QLOC: + case DKEEP: + case DBG1: + case RLOC: + case QCON: + case ALOC: + case STMOD: + case PCON: + case KCON: + case DSPD: + case DFLG: + case DFNON: + case DFNOF: + case DFUN: + case GLOC: + case ERR: + case RDCC3: + case WCVO: + case WCVB: + case QCVS: + case PCVS: + case RDCC4: + case WCVS: + case RDCC5: + case WCVOA: + case CABDAT: + case FCLK: + case RDCC6: + case PLOC: + case STAT: + return MinorPriority::Normal; + + case QNN: + case RQNP: + case EXTC: + case SNN: + case SSTAT: + case NNRSM: + case RQNN: + case NNREL: + case NNACK: + case NNLRN: + case NNULN: + case NNCLR: + case NNEVN: + case NERD: + case RQEVN: + case WRACK: + case RQDAT: + case RQDDS: + case BOOTM: + case ENUM: + case NNRST: + case EXTC1: + case CMDERR: + case EVNLF: + case NVRD: + case NENRD: + case RQNPN: + case NUMEV: + case CANID: + case EXTC2: + case ACON: + case ACOF: + case AREQ: + case ARON: + case AROF: + case EVULN: + case NVSET: + case NVANS: + case ASON: + case ASOF: + case ASRQ: + case PARAN: + case REVAL: + case ARSON: + case ARSOF: + case EXTC3: + case ACON1: + case ACOF1: + case REQEV: + case ARON1: + case AROF1: + case NEVAL: + case PNN: + case ASON1: + case ASOF1: + case ARSON1: + case ARSOF1: + case EXTC4: + case NAME: + case PARAMS: + case ACON3: + case ACOF3: + case ENRSP: + case ARON3: + case AROF3: + case EVLRNI: + case ACDAT: + case ARDAT: + case ASON3: + case ASOF3: + case DDES: + case DDRS: + case ARSON3: + case ARSOF3: + case EXTC6: + return MinorPriority::Low; + } + return MinorPriority::Low; +} + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/cbuskernel.cpp b/server/src/hardware/protocol/cbus/cbuskernel.cpp new file mode 100644 index 00000000..b9f55583 --- /dev/null +++ b/server/src/hardware/protocol/cbus/cbuskernel.cpp @@ -0,0 +1,221 @@ +/** + * 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 "cbuskernel.hpp" +#include "cbusmessages.hpp" +#include "cbustostring.hpp" +/* +#include "simulator/cbussimulator.hpp" +*/ +#include "../../../core/eventloop.hpp" +#include "../../../log/log.hpp" +#include "../../../log/logmessageexception.hpp" +#include "../../../utils/setthreadname.hpp" + +namespace CBUS { + +Kernel::Kernel(std::string logId_, const Config& config, bool simulation) + : KernelBase(std::move(logId_)) + , m_simulation{simulation} + , m_config{config} +{ + assert(isEventLoopThread()); +} + +void Kernel::setConfig(const Config& config) +{ + assert(isEventLoopThread()); + + m_ioContext.post( + [this, newConfig=config]() + { + m_config = newConfig; + }); +} + +void Kernel::start() +{ + assert(isEventLoopThread()); + assert(m_ioHandler); + + m_thread = std::thread( + [this]() + { + setThreadName("cbus"); + auto work = std::make_shared(m_ioContext); + m_ioContext.run(); + }); + + m_ioContext.post( + [this]() + { + try + { + m_ioHandler->start(); + } + catch(const LogMessageException& e) + { + EventLoop::call( + [this, e]() + { + Log::log(logId, e.message(), e.args()); + error(); + }); + return; + } + }); +} + +void Kernel::stop() +{ + assert(isEventLoopThread()); + + m_ioContext.post( + [this]() + { + m_ioHandler->stop(); + }); + + m_ioContext.stop(); + + m_thread.join(); +} + +void Kernel::started() +{ + assert(isKernelThread()); + + send(RequestCommandStationStatus()); + send(QueryNodeNumber()); + + ::KernelBase::started(); +} + +void Kernel::receive(uint8_t /*canId*/, const Message& message) +{ + assert(isKernelThread()); + + if(m_config.debugLogRXTX) + { + EventLoop::call( + [this, msg=toString(message)]() + { + Log::log(logId, LogMessage::D2002_RX_X, msg); + }); + } + + switch(message.opCode) + { + case OpCode::TOF: + m_trackOn = false; + if(onTrackOff) [[likely]] + { + EventLoop::call(onTrackOff); + } + break; + + case OpCode::TON: + m_trackOn = true; + if(onTrackOn) [[likely]] + { + EventLoop::call(onTrackOn); + } + break; + + case OpCode::ESTOP: + if(onEmergencyStop) [[likely]] + { + EventLoop::call(onEmergencyStop); + } + break; + + default: + break; + } +} + +void Kernel::trackOff() +{ + assert(isEventLoopThread()); + + m_ioContext.post( + [this]() + { + if(m_trackOn) + { + send(RequestTrackOff()); + } + }); +} + +void Kernel::trackOn() +{ + assert(isEventLoopThread()); + + m_ioContext.post( + [this]() + { + if(!m_trackOn) + { + send(RequestTrackOn()); + } + }); +} + +void Kernel::requestEmergencyStop() +{ + assert(isEventLoopThread()); + + m_ioContext.post( + [this]() + { + send(RequestEmergencyStop()); + }); +} + +void Kernel::setIOHandler(std::unique_ptr handler) +{ + assert(isEventLoopThread()); + assert(handler); + assert(!m_ioHandler); + m_ioHandler = std::move(handler); +} + +void Kernel::send(const Message& message) +{ + assert(isKernelThread()); + + if(m_config.debugLogRXTX) + { + EventLoop::call( + [this, msg=toString(message)]() + { + Log::log(logId, LogMessage::D2001_TX_X, msg); + }); + } + + if(auto ec = m_ioHandler->send(message); ec) + { + (void)ec; // FIXME: handle error + } +} + +} diff --git a/server/src/hardware/protocol/cbus/cbuskernel.hpp b/server/src/hardware/protocol/cbus/cbuskernel.hpp new file mode 100644 index 00000000..69ea581b --- /dev/null +++ b/server/src/hardware/protocol/cbus/cbuskernel.hpp @@ -0,0 +1,110 @@ +/** + * 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_CBUSKERNEL_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_CBUSKERNEL_HPP + +#include "../kernelbase.hpp" +#include +#include +#include "cbusconfig.hpp" +#include "iohandler/cbusiohandler.hpp" + +namespace CBUS { + +class Kernel : public ::KernelBase +{ +public: + std::function onTrackOff; + std::function onTrackOn; + std::function onEmergencyStop; + + /** + * @brief Create kernel and IO handler + * + * @param[in] config CBUS configuration + * @param[in] args IO handler arguments + * @return The kernel instance + */ + template + static std::unique_ptr create(std::string logId_, const Config& config, Args... args) + { + static_assert(std::is_base_of_v); + std::unique_ptr kernel{new Kernel(std::move(logId_), config, isSimulation())}; + kernel->setIOHandler(std::make_unique(*kernel, std::forward(args)...)); + return kernel; + } + +#ifndef NDEBUG + bool isKernelThread() const + { + return std::this_thread::get_id() == m_thread.get_id(); + } +#endif + + /** + * @brief Set CBUS configuration + * + * @param[in] config The CBUS configuration + */ + void setConfig(const Config& config); + + /** + * @brief Start the kernel and IO handler + */ + void start(); + + /** + * @brief Stop the kernel and IO handler + */ + void stop(); + + /** + * \brief Notify kernel the IO handler is started. + * \note This function must run in the kernel's IO context + */ + void started() final; + + void receive(uint8_t canId, const Message& message); + + void trackOff(); + void trackOn(); + void requestEmergencyStop(); + +private: + std::unique_ptr m_ioHandler; + const bool m_simulation; + Config m_config; + bool m_trackOn = false; + + Kernel(std::string logId_, const Config& config, bool simulation); + + Kernel(const Kernel&) = delete; + Kernel& operator =(const Kernel&) = delete; + + void setIOHandler(std::unique_ptr handler); + + void send(const Message& message); +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/cbusopcode.hpp b/server/src/hardware/protocol/cbus/cbusopcode.hpp index cc1be0f0..fe63ccb6 100644 --- a/server/src/hardware/protocol/cbus/cbusopcode.hpp +++ b/server/src/hardware/protocol/cbus/cbusopcode.hpp @@ -167,6 +167,11 @@ enum class OpCode : uint8_t EXTC6 = 0xFF, //!< Extended op-code with 6 data bytes }; +constexpr uint8_t dataSize(OpCode opc) +{ + return static_cast(opc) >> 5; // highest 3 bits determine data length +} + } #endif diff --git a/server/src/hardware/protocol/cbus/cbuspriority.hpp b/server/src/hardware/protocol/cbus/cbuspriority.hpp new file mode 100644 index 00000000..971872f4 --- /dev/null +++ b/server/src/hardware/protocol/cbus/cbuspriority.hpp @@ -0,0 +1,46 @@ +/** + * 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_CBUSPRIORITY_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_CBUSPRIORITY_HPP + +#include + +namespace CBUS { + +enum class MajorPriority : uint8_t +{ + Highest = 0b00, + Next = 0b01, + Lowest = 0b10, +}; + +enum class MinorPriority : uint8_t +{ + High = 0b00, + AboveNormal = 0b01, + Normal = 0b10, + Low = 0b11, +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/cbustostring.cpp b/server/src/hardware/protocol/cbus/cbustostring.cpp new file mode 100644 index 00000000..229da70b --- /dev/null +++ b/server/src/hardware/protocol/cbus/cbustostring.cpp @@ -0,0 +1,381 @@ +/** + * 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 "cbustostring.hpp" +#include +#include "cbusmessages.hpp" +#include "../../../utils/tohex.hpp" + +namespace CBUS { + +std::string toString(const Message& message) +{ + std::string s(toString(message.opCode)); + + switch (message.opCode) + { + using enum OpCode; + + // 00-1F – 0 data byte packets: + case ACK: + case NAK: + case HLT: + case BON: + case TOF: + case TON: + case ESTOP: + case ARST: + case RTOF: + case RTON: + case RESTP: + case RSTAT: + case QNN: + case RQNP: + case RQMN: + break; // no data + + // 20–3F - 1 data byte packets: + case KLOC: + case QLOC: + case DKEEP: + { + const auto& m = static_cast(message); + s.append(std::format(" session={}", m.session)); + break; + } + case DBG1: + break; + + case EXTC: + break; + + // 40–5F - 2 data byte packets: + case RLOC: + break; + + case QCON: + break; + + case SNN: + break; + + case ALOC: + break; + + case STMOD: + break; + + case PCON: + break; + + case KCON: + break; + + case DSPD: + break; + + case DFLG: + break; + + case DFNON: + break; + + case DFNOF: + break; + + case SSTAT: + break; + + case NNRSM: + break; + + case RQNN: + break; + + case NNREL: + break; + + case NNACK: + break; + + case NNLRN: + break; + + case NNULN: + break; + + case NNCLR: + break; + + case NNEVN: + break; + + case NERD: + break; + + case RQEVN: + break; + + case WRACK: + break; + + case RQDAT: + break; + + case RQDDS: + break; + + case BOOTM: + break; + + case ENUM: + break; + + case NNRST: + break; + + case EXTC1: + break; + + // 60-7F - 3 data byte packets: + case DFUN: + break; + + case GLOC: + break; + + case ERR: + break; + + case CMDERR: + break; + + case EVNLF: + break; + + case NVRD: + break; + + case NENRD: + break; + + case RQNPN: + break; + + case NUMEV: + break; + + case CANID: + break; + + case EXTC2: + break; + + // 80-9F - 4 data byte packets: + case RDCC3: + break; + + case WCVO: + break; + + case WCVB: + break; + + case QCVS: + break; + + case PCVS: + break; + + case ACON: + break; + + case ACOF: + break; + + case AREQ: + break; + + case ARON: + break; + + case AROF: + break; + + case EVULN: + break; + + case NVSET: + break; + + case NVANS: + break; + + case ASON: + break; + + case ASOF: + break; + + case ASRQ: + break; + + case PARAN: + break; + + case REVAL: + break; + + case ARSON: + break; + + case ARSOF: + break; + + case EXTC3: + break; + + // A0-BF - 5 data byte packets: + case RDCC4: + break; + + case WCVS: + break; + + case ACON1: + break; + + case ACOF1: + break; + + case REQEV: + break; + + case ARON1: + break; + + case AROF1: + break; + + case NEVAL: + break; + + case PNN: + break; + + case ASON1: + break; + + case ASOF1: + break; + + case ARSON1: + break; + + case ARSOF1: + break; + + case EXTC4: + break; + + // C0-DF - 6 data byte packets: + case RDCC5: + break; + + case WCVOA: + break; + + case CABDAT: + break; + + case FCLK: + break; + + // E0-FF - 7 data byte packets: + case RDCC6: + break; + + case PLOC: + break; + + case NAME: + break; + + case STAT: + break; + + case PARAMS: + break; + + case ACON3: + break; + + case ACOF3: + break; + + case ENRSP: + break; + + case ARON3: + break; + + case AROF3: + break; + + case EVLRNI: + break; + + case ACDAT: + break; + + case ARDAT: + break; + + case ASON3: + break; + + case ASOF3: + break; + + case DDES: + break; + + case DDRS: + break; + + case ARSON3: + break; + + case ARSOF3: + break; + + case EXTC6: + break; + } + + s.append(" ["); + s.append(toHex(&message, message.size())); + s.append("]"); + + return s; +} + +} diff --git a/server/src/hardware/protocol/cbus/cbustostring.hpp b/server/src/hardware/protocol/cbus/cbustostring.hpp index c15dc9da..3cabd773 100644 --- a/server/src/hardware/protocol/cbus/cbustostring.hpp +++ b/server/src/hardware/protocol/cbus/cbustostring.hpp @@ -27,13 +27,15 @@ namespace CBUS { +struct Message; + constexpr std::string_view toString(OpCode opCode) { switch(opCode) { using enum OpCode; - // 00-1F – 0 data bytes packets: + // 00-1F – 0 data byte packets: case ACK: return "ACK"; case NAK: return "NAK"; case HLT: return "HLT"; @@ -171,6 +173,8 @@ constexpr std::string_view toString(OpCode opCode) return {}; } +std::string toString(const Message& message); + } #endif diff --git a/server/src/hardware/protocol/cbus/iohandler/cbusasciiiohandler.cpp b/server/src/hardware/protocol/cbus/iohandler/cbusasciiiohandler.cpp new file mode 100644 index 00000000..9b4fb65f --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/cbusasciiiohandler.cpp @@ -0,0 +1,165 @@ +/** + * 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 "cbusasciiiohandler.hpp" +#include "../cbusgetminorpriority.hpp" +#include "../cbuskernel.hpp" +#include "../messages/cbusmessage.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" +#include "../../../../utils/tohex.hpp" +#include "../../../../utils/fromchars.hpp" + +namespace { + +std::string buildFrame(CBUS::MajorPriority majorPriority, CBUS::MinorPriority minorPriority, uint8_t canId, const CBUS::Message& message) +{ + const uint16_t sid = + (static_cast(majorPriority) << 14) | + (static_cast(minorPriority) << 12) | + (static_cast(canId) << 5); + + std::string frame(":S"); + frame.append(toHex(sid)); + frame.append("N"); + frame.append(toHex(&message, message.size())); + frame.append(";"); + return frame; +} + +} + +namespace CBUS { + +ASCIIIOHandler::ASCIIIOHandler(Kernel& kernel, uint8_t canId) + : IOHandler(kernel, canId) +{ +} + +std::error_code ASCIIIOHandler::send(const Message& message) +{ + const bool wasEmpty = m_writeQueue.empty(); + + // FIXME: handle priority queueing + // FIXME: what to do with MajorPriority? + m_writeQueue.emplace(buildFrame(MajorPriority::Lowest, getMinorPriority(message.opCode), m_canId, message)); + + if(wasEmpty) + { + write(); + } + + return {}; +} + +void ASCIIIOHandler::logDropIfNonZeroAndReset(size_t& drop) +{ + if(drop != 0) + { + EventLoop::call( + [this, drop]() + { + Log::log(m_kernel.logId, LogMessage::W2001_RECEIVED_MALFORMED_DATA_DROPPED_X_BYTES, drop); + }); + drop = 0; + } +} + +void ASCIIIOHandler::processRead(std::size_t bytesTransferred) +{ + constexpr size_t maxFrameSize = 24; // :SXXXXNXXXXXXXXXXXXXXXX; + + std::string_view buffer{m_readBuffer.data(), m_readBufferOffset + bytesTransferred}; + + size_t drop = 0; + + while(!buffer.empty()) + { + logDropIfNonZeroAndReset(drop); + + if(auto pos = buffer.find(':'); pos != 0) + { + if(pos == std::string_view::npos) + { + // no start marker drop all bytes: + drop += buffer.size(); + buffer = {}; + break; + } + + // drop bytes before start marker: + drop += pos; + buffer.remove_prefix(pos); + } + + auto end = buffer.find(';'); + if(end == std::string_view::npos) + { + // no end marker yet, wait for more data + break; + } + + std::string_view frame{buffer.data(), end + 1}; + + buffer.remove_prefix(frame.size()); // consume frame + + if(frame.size() > maxFrameSize) + { + drop += frame.size(); + } + else if(frame[1] == 'S' && frame[6] == 'N') // CBUS only uses standard non RTR CAN frames + { + uint16_t sid; + if(fromChars(frame.substr(2, sizeof(sid) * 2), sid, 16).ec != std::errc()) + { + continue; // error reading, ignore frame + } + + const auto dataLength = (frame.size() - 7) / 2; + std::array data; + std::errc ec = std::errc(); + for(size_t i = 0; i < dataLength; ++i) + { + ec = fromChars(frame.substr(7 + i * 2, 2), data[i], 16).ec; + if(ec != std::errc()) + { + break; + } + } + if(ec != std::errc()) + { + continue; // error reading, ignore frame + } + + m_kernel.receive((sid >> 5) & 0x7F, *reinterpret_cast(data.data())); + } + } + + logDropIfNonZeroAndReset(drop); + + if(buffer.size() != 0) + { + memmove(m_readBuffer.data(), buffer.data(), buffer.size()); + } + m_readBufferOffset = buffer.size(); +} + +} diff --git a/server/src/hardware/protocol/cbus/iohandler/cbusasciiiohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/cbusasciiiohandler.hpp new file mode 100644 index 00000000..48d8288e --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/cbusasciiiohandler.hpp @@ -0,0 +1,53 @@ +/** + * 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_IOHANDLER_CBUSASCIIIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_CBUSASCIIIOHANDLER_HPP + +#include "cbusiohandler.hpp" +#include +#include +#include + +namespace CBUS { + +class ASCIIIOHandler : public IOHandler +{ +public: + [[nodiscard]] std::error_code send(const Message& message) override; + +protected: + std::array m_readBuffer; + size_t m_readBufferOffset; + std::queue m_writeQueue; + + ASCIIIOHandler(Kernel& kernel, uint8_t canId); + + void logDropIfNonZeroAndReset(size_t& drop); + void processRead(std::size_t bytesTransferred); + + virtual void read() = 0; + virtual void write() = 0; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/iohandler/cbuscanetheriohandler.cpp b/server/src/hardware/protocol/cbus/iohandler/cbuscanetheriohandler.cpp new file mode 100644 index 00000000..f6c5f337 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/cbuscanetheriohandler.cpp @@ -0,0 +1,135 @@ +/** + * 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 "cbuscanetheriohandler.hpp" +#include +#include "../cbuskernel.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" +#include "../../../../log/logmessageexception.hpp" + +namespace CBUS { + +CANEtherIOHandler::CANEtherIOHandler(Kernel& kernel, std::string hostname, uint16_t port) + : ASCIIIOHandler(kernel, canId) + , m_hostname{std::move(hostname)} + , m_port{port} + , m_socket{m_kernel.ioContext()} +{ +} + +void CANEtherIOHandler::start() +{ + boost::system::error_code ec; + + m_endpoint.port(m_port); + m_endpoint.address(boost::asio::ip::make_address(m_hostname, ec)); + if(ec) + { + throw LogMessageException(LogMessage::E2003_MAKE_ADDRESS_FAILED_X, ec); + } + + m_socket.async_connect(m_endpoint, + [this](const boost::system::error_code& err) + { + if(!err) + { + m_socket.set_option(boost::asio::socket_base::linger(true, 0)); + m_socket.set_option(boost::asio::ip::tcp::no_delay(true)); + + m_connected = true; + + read(); + write(); + + m_kernel.started(); + } + else if(err != boost::asio::error::operation_aborted) + { + EventLoop::call( + [this, err]() + { + Log::log(m_kernel.logId, LogMessage::E2005_SOCKET_CONNECT_FAILED_X, err); + m_kernel.error(); + }); + } + }); +} + +void CANEtherIOHandler::stop() +{ + boost::system::error_code ec; + m_socket.cancel(ec); + m_socket.close(ec); + // ignore errors + m_connected = false; +} + +void CANEtherIOHandler::read() +{ + m_socket.async_read_some(boost::asio::buffer(m_readBuffer.data() + m_readBufferOffset, m_readBuffer.size() - m_readBufferOffset), + [this](const boost::system::error_code& ec, std::size_t bytesTransferred) + { + if(!ec) + { + processRead(bytesTransferred); + read(); + } + else if(ec != boost::asio::error::operation_aborted) + { + EventLoop::call( + [this, ec]() + { + Log::log(m_kernel.logId, LogMessage::E1007_SOCKET_READ_FAILED_X, ec); + m_kernel.error(); + }); + } + }); +} + +void CANEtherIOHandler::write() +{ + assert(!m_writeQueue.empty()); + const auto& message = m_writeQueue.front(); + boost::asio::async_write(m_socket, boost::asio::buffer(message.data(), message.size()), + [this](const boost::system::error_code& ec, std::size_t /*bytesTransferred*/) + { + if(!ec) + { + m_writeQueue.pop(); + if(!m_writeQueue.empty()) + { + write(); + } + } + else if(ec != boost::asio::error::operation_aborted) + { + EventLoop::call( + [this, ec]() + { + Log::log(m_kernel.logId, LogMessage::E1006_SOCKET_WRITE_FAILED_X, ec); + m_kernel.error(); + }); + } + }); +} + +} diff --git a/server/src/hardware/protocol/cbus/iohandler/cbuscanetheriohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/cbuscanetheriohandler.hpp new file mode 100644 index 00000000..0634937f --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/cbuscanetheriohandler.hpp @@ -0,0 +1,54 @@ +/** + * 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_IOHANDLER_CBUSCANETHERIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_CBUSCANETHERIOHANDLER_HPP + +#include "cbusasciiiohandler.hpp" +#include + +namespace CBUS { + +class CANEtherIOHandler final : public ASCIIIOHandler +{ +public: + CANEtherIOHandler(Kernel& kernel, std::string hostname, uint16_t port); + + void start() final; + void stop() final; + +protected: + void read() final; + void write() final; + +private: + static constexpr uint8_t canId = 0x7D; //!< CANEther fixed CAN_ID + + const std::string m_hostname; + const uint16_t m_port; + boost::asio::ip::tcp::socket m_socket; + boost::asio::ip::tcp::endpoint m_endpoint; + bool m_connected = false; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/iohandler/cbuscanusbiohandler.cpp b/server/src/hardware/protocol/cbus/iohandler/cbuscanusbiohandler.cpp new file mode 100644 index 00000000..b34b1be5 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/cbuscanusbiohandler.cpp @@ -0,0 +1,109 @@ +/** + * 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 "cbuscanusbiohandler.hpp" +#include +#include "../cbuskernel.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" +#include "../../../../utils/serialport.hpp" + +namespace CBUS { + +CANUSBIOHandler::CANUSBIOHandler(Kernel& kernel, const std::string& device) + : ASCIIIOHandler(kernel, canId) + , m_serialPort{m_kernel.ioContext()} +{ + // FIXME: check serial settings, just a guess, if more settings are needed add them to the interface + SerialPort::open(m_serialPort, device, 115'200, 8, SerialParity::None, SerialStopBits::One, SerialFlowControl::None); +} + +CANUSBIOHandler::~CANUSBIOHandler() +{ + if(m_serialPort.is_open()) + { + boost::system::error_code ec; + m_serialPort.close(ec); + // ignore the error + } +} + +void CANUSBIOHandler::start() +{ + read(); + m_kernel.started(); +} + +void CANUSBIOHandler::stop() +{ + m_serialPort.close(); +} + +void CANUSBIOHandler::read() +{ + m_serialPort.async_read_some(boost::asio::buffer(m_readBuffer.data() + m_readBufferOffset, m_readBuffer.size() - m_readBufferOffset), + [this](const boost::system::error_code& ec, std::size_t bytesTransferred) + { + if(!ec) + { + processRead(bytesTransferred); + read(); + } + else if(ec != boost::asio::error::operation_aborted) + { + EventLoop::call( + [this, ec]() + { + Log::log(m_kernel.logId, LogMessage::E2002_SERIAL_READ_FAILED_X, ec); + m_kernel.error(); + }); + } + }); +} + +void CANUSBIOHandler::write() +{ + assert(!m_writeQueue.empty()); + const auto& message = m_writeQueue.front(); + boost::asio::async_write(m_serialPort, boost::asio::buffer(message.data(), message.size()), + [this](const boost::system::error_code& ec, std::size_t /*bytesTransferred*/) + { + if(!ec) + { + m_writeQueue.pop(); + if(!m_writeQueue.empty()) + { + write(); + } + } + else if(ec != boost::asio::error::operation_aborted) + { + EventLoop::call( + [this, ec]() + { + Log::log(m_kernel.logId, LogMessage::E2001_SERIAL_WRITE_FAILED_X, ec); + m_kernel.error(); + }); + } + }); +} + +} diff --git a/server/src/hardware/protocol/cbus/iohandler/cbuscanusbiohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/cbuscanusbiohandler.hpp new file mode 100644 index 00000000..294bc672 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/cbuscanusbiohandler.hpp @@ -0,0 +1,51 @@ +/** + * 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_IOHANDLER_CBUSCANUSBIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_CBUSCANUSBIOHANDLER_HPP + +#include "cbusasciiiohandler.hpp" +#include + +namespace CBUS { + +class CANUSBIOHandler final : public ASCIIIOHandler +{ +public: + CANUSBIOHandler(Kernel& kernel, const std::string& device); + ~CANUSBIOHandler() final; + + void start() final; + void stop() final; + +protected: + void read() final; + void write() final; + +private: + static constexpr uint8_t canId = 0x7C; //!< CANUSB fixed CAN_ID + + boost::asio::serial_port m_serialPort; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/iohandler/cbusiohandler.cpp b/server/src/hardware/protocol/cbus/iohandler/cbusiohandler.cpp new file mode 100644 index 00000000..74f65592 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/cbusiohandler.cpp @@ -0,0 +1,32 @@ +/** + * 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 "cbusiohandler.hpp" + +namespace CBUS { + +IOHandler::IOHandler(Kernel& kernel, uint8_t canId) + : m_kernel{kernel} + , m_canId{canId} +{ +} + +} diff --git a/server/src/hardware/protocol/cbus/iohandler/cbusiohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/cbusiohandler.hpp new file mode 100644 index 00000000..075868a9 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/cbusiohandler.hpp @@ -0,0 +1,61 @@ +/** + * 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_IOHANDLER_CBUSIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_CBUSIOHANDLER_HPP + +#include +#include + +namespace CBUS { + +class Kernel; +struct Message; + +class IOHandler +{ +public: + IOHandler(const IOHandler&) = delete; + IOHandler& operator =(const IOHandler&) = delete; + + virtual ~IOHandler() = default; + + virtual void start() = 0; + virtual void stop() = 0; + + [[nodiscard]] virtual std::error_code send(const Message& message) = 0; + +protected: + Kernel& m_kernel; + const uint8_t m_canId; + + IOHandler(Kernel& kernel, uint8_t canId); +}; + +template +constexpr bool isSimulation() +{ + return false; +} + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/messages/cbusmessage.hpp b/server/src/hardware/protocol/cbus/messages/cbusmessage.hpp index 24fb77b5..4d0793f9 100644 --- a/server/src/hardware/protocol/cbus/messages/cbusmessage.hpp +++ b/server/src/hardware/protocol/cbus/messages/cbusmessage.hpp @@ -30,6 +30,11 @@ struct Message { OpCode opCode; + constexpr uint8_t size() const + { + return sizeof(OpCode) + dataSize(opCode); + } + protected: Message(OpCode opc) : opCode{opc} diff --git a/shared/src/traintastic/enum/cbusinterfacetype.hpp b/shared/src/traintastic/enum/cbusinterfacetype.hpp new file mode 100644 index 00000000..8a4f15fb --- /dev/null +++ b/shared/src/traintastic/enum/cbusinterfacetype.hpp @@ -0,0 +1,46 @@ +/** + * 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_SHARED_TRAINTASTIC_ENUM_CBUSINTERFACETYPE_HPP +#define TRAINTASTIC_SHARED_TRAINTASTIC_ENUM_CBUSINTERFACETYPE_HPP + +#include +#include +#include "enum.hpp" + +enum class CBUSInterfaceType : uint8_t +{ + CANUSB = 0, + CANEther = 1, +}; + +TRAINTASTIC_ENUM(CBUSInterfaceType, "cbus_interface_type", 2, +{ + {CBUSInterfaceType::CANUSB, "canusb"}, + {CBUSInterfaceType::CANEther, "canether"}, +}); + +inline constexpr std::array CBUSInterfaceTypeValues{{ + CBUSInterfaceType::CANUSB, + CBUSInterfaceType::CANEther, +}}; + +#endif diff --git a/shared/translations/neutral.json b/shared/translations/neutral.json index ee0b87e5..991b2b64 100644 --- a/shared/translations/neutral.json +++ b/shared/translations/neutral.json @@ -1,4 +1,8 @@ [ + { + "term": "class_id:interface.cbus", + "definition": "CBUS/VLCB" + }, { "term": "class_id:interface.dccex", "definition": "DCC-EX"