diff --git a/.clang-tidy b/.clang-tidy deleted file mode 100644 index f9bb9ef8..00000000 --- a/.clang-tidy +++ /dev/null @@ -1,24 +0,0 @@ ---- -Checks: > - -*, - misc-*, - -misc-no-recursion, - modernize-*, - -modernize-avoid-bind, - -modernize-avoid-c-arrays, - -modernize-use-trailing-return-type, - readability-*, - -readability-braces-around-statements, - -readability-implicit-bool-conversion, - -readability-identifier-length, - -readability-magic-numbers, - -readability-suspicious-call-argument - -WarningsAsErrors: > - readability-*, - -readability-function-cognitive-complexity - -CheckOptions: - - {key: readability-identifier-naming.NamespaceCase, value: CamelCase} - - {key: readability-identifier-naming.ClassCase, value: CamelCase} - - {key: readability-identifier-naming.StructCase, value: CamelCase} diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index a5868e9c..00000000 --- a/.editorconfig +++ /dev/null @@ -1,23 +0,0 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{hpp|cpp|lua|yml}] -indent_style = space -indent_size = 2 - -[*.py] -indent_style = space -indent_size = 4 - -[*.js] -charset = utf-8 -indent_style = space -indent_size = 2 - -[**CMakeLists.txt] -indent_style = space -indent_size = 2 diff --git a/.gitignore b/.gitignore index 393b76a3..69135125 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ CMakeLists.txt.* shared/translations/*.lang CMakeSettings.json .venv +traintastic.geany diff --git a/client/data/wizard/add_interface.json b/client/data/wizard/add_interface.json index 58213ef1..8c463cf0 100644 --- a/client/data/wizard/add_interface.json +++ b/client/data/wizard/add_interface.json @@ -286,6 +286,33 @@ } ] }, + "connection_cbus": { + "title": "$wizard.add_interface.connection:title$", + "text": "$wizard.add_interface.connection:text$", + "type": "radio", + "options": [ + { + "name": "$wizard.add_interface.connection:command_station_usb_port$", + "next": "serial_port", + "actions": { + "create_interface": { + "class_id": "interface.cbus", + "properties": { + "name": "%command_station%", + "type": "serial", + "baudrate": 115200, + "flow_control": "hardware", + "cbus.command_station": "%cbus_command_station%" + } + } + } + }, + { + "name": "$wizard.add_interface.connection:cbus_interface$", + "next": "interface_cbus" + } + ] + }, "connection_z21": { "title": "$wizard.add_interface.connection:title$", "text": "$wizard.add_interface.connection:text$", diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 1265d79f..d2816865 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -119,6 +119,10 @@ file(GLOB SOURCES "src/hardware/programming/lncv/*.cpp" "src/hardware/protocol/*.hpp" "src/hardware/protocol/*.cpp" + "src/hardware/protocol/cbus/*.hpp" + "src/hardware/protocol/cbus/*.cpp" + "src/hardware/protocol/cbus/iohandler/*.hpp" + "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/enum/cbusinterfacetype.hpp b/server/src/enum/cbusinterfacetype.hpp new file mode 100644 index 00000000..07ea43ad --- /dev/null +++ b/server/src/enum/cbusinterfacetype.hpp @@ -0,0 +1,35 @@ +/** + * server/src/enum/cbusinterface.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2021 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_ENUM_CBUSINTERFACETYPE_HPP +#define TRAINTASTIC_SERVER_ENUM_CBUSINTERFACETYPE_HPP + +#include +#include + +inline constexpr std::array CBUSInterfaceTypeValues{{ + CBUSInterfaceType::Serial, + CBUSInterfaceType::NetworkTCP, +}}; + +#endif + diff --git a/server/src/hardware/interface/cbusinterface.cpp b/server/src/hardware/interface/cbusinterface.cpp new file mode 100644 index 00000000..c9ce397e --- /dev/null +++ b/server/src/hardware/interface/cbusinterface.cpp @@ -0,0 +1,163 @@ +/** + * server/src/hardware/interface/cbus/cbusinterface.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2019-2025 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 "../input/list/inputlist.hpp" +#include "../output/list/outputlist.hpp" +#include "../protocol/cbus/kernel.hpp" +#include "../protocol/cbus/settings.hpp" +#include "../protocol/cbus/messages.hpp" +#include "../protocol/cbus/iohandler/serialiohandler.hpp" +#include "../protocol/cbus/iohandler/simulationiohandler.hpp" +#include "../protocol/cbus/iohandler/tcpiohandler.hpp" +#include "../../core/attributes.hpp" +#include "../../core/eventloop.hpp" +#include "../../core/objectproperty.tpp" +#include "../../log/log.hpp" +#include "../../log/logmessageexception.hpp" +#include "../../utils/displayname.hpp" +#include "../../utils/inrange.hpp" +#include "../../utils/makearray.hpp" +#include "../../world/world.hpp" + +constexpr auto inputListColumns = InputListColumn::Address; +constexpr auto outputListColumns = OutputListColumn::Channel | OutputListColumn::Address; + +CREATE_IMPL(CBUSInterface) + +CBUSInterface::CBUSInterface(World& world, std::string_view _id) + : Interface(world, _id) + , InputController(static_cast(*this)) + , OutputController(static_cast(*this)) + , type{this, "type", CBUSInterfaceType::Serial, PropertyFlags::ReadWrite | PropertyFlags::Store, + [this](CBUSInterfaceType /*value*/) + { + updateVisible(); + }} + , device{this, "device", "", PropertyFlags::ReadWrite | PropertyFlags::Store} + , baudrate{this, "baudrate", 115200, PropertyFlags::ReadWrite | PropertyFlags::Store} + , flowControl{this, "flow_control", SerialFlowControl::None, PropertyFlags::ReadWrite | PropertyFlags::Store} + , hostname{this, "hostname", "", PropertyFlags::ReadWrite | PropertyFlags::Store} + , port{this, "port", 5550, PropertyFlags::ReadWrite | PropertyFlags::Store} + , cbus{this, "cbus", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject} +{ + name = "CBUS"; + 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(baudrate, DisplayName::Serial::baudrate); + Attributes::addEnabled(baudrate, !online); + Attributes::addVisible(baudrate, false); + Attributes::addDisplayName(flowControl, DisplayName::Serial::flowControl); + Attributes::addEnabled(flowControl, !online); + Attributes::addValues(flowControl, SerialFlowControlValues); + Attributes::addVisible(flowControl, false); + m_interfaceItems.insertBefore(flowControl, 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); + + Attributes::addDisplayName(cbus, DisplayName::Hardware::cbus); + m_interfaceItems.insertBefore(cbus, notes); + + m_interfaceItems.insertBefore(inputs, notes); + + m_interfaceItems.insertBefore(outputs, notes); + + updateVisible(); +} + +CBUSInterface::~CBUSInterface() = default; + +void CBUSInterface::addToWorld() +{ + Interface::addToWorld(); + InputController::addToWorld(inputListColumns); + OutputController::addToWorld(outputListColumns); +} + +void CBUSInterface::loaded() +{ + Interface::loaded(); + updateVisible(); +} + +std::span CBUSInterface::inputChannels() const { + return {}; +} + +std::pair CBUSInterface::inputAddressMinMax(InputChannel) const { + return {0,0}; +} + +std::span CBUSInterface::outputChannels() const { + return {}; +} + +std::pair CBUSInterface::outputAddressMinMax(OutputChannel) const { + return {0,0}; +} + +bool CBUSInterface::setOutputValue(OutputChannel, uint32_t, OutputValue) { + return false; +} + +bool CBUSInterface::setOnline(bool& value, bool /*simulation*/) +{ + value = false; + return true; +} + + +void CBUSInterface::destroying() +{ + OutputController::destroying(); + InputController::destroying(); + Interface::destroying(); +} + +void CBUSInterface::updateVisible() +{ + const bool isSerial = (type == CBUSInterfaceType::Serial); + Attributes::setVisible(device, isSerial); + Attributes::setVisible(baudrate, isSerial); + Attributes::setVisible(flowControl, isSerial); + + const bool isNetwork = (type == CBUSInterfaceType::NetworkTCP); + 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..558c7351 --- /dev/null +++ b/server/src/hardware/interface/cbusinterface.hpp @@ -0,0 +1,84 @@ +/** + * server/src/hardware/interface/cbus/cbusinterface.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2019-2025 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 "../input/inputcontroller.hpp" +#include "../output/outputcontroller.hpp" +#include "../../core/serialdeviceproperty.hpp" +#include "../../core/objectproperty.hpp" +#include "../../enum/cbusinterfacetype.hpp" +#include "../../enum/serialflowcontrol.hpp" +#include + +namespace CBUS { +class Kernel; +class Settings; +} + +/** + * @brief CBUS hardware interface + */ +class CBUSInterface final + : public Interface + , public InputController + , public OutputController +{ + CLASS_ID("interface.cbus") + DEFAULT_ID("cbus") + CREATE_DEF(CBUSInterface) + + private: + std::unique_ptr m_kernel; + boost::signals2::connection m_cbusPropertyChanged; + + void addToWorld() final; + void loaded() final; + void destroying() final; + void updateVisible(); + + public: + Property type; + SerialDeviceProperty device; + Property baudrate; + Property flowControl; + Property hostname; + Property port; + ObjectProperty cbus; + + CBUSInterface(World& world, std::string_view _id); + ~CBUSInterface() final; + + bool setOnline(bool& value, bool simulation) final; + + std::span inputChannels() const final; + std::pair inputAddressMinMax(InputChannel) const final; + + std::span outputChannels() const final; + std::pair outputAddressMinMax(OutputChannel) const final; + [[nodiscard]] bool setOutputValue(OutputChannel channel, uint32_t address, OutputValue value) final; + +}; + +#endif diff --git a/server/src/hardware/protocol/cbus/config.hpp b/server/src/hardware/protocol/cbus/config.hpp new file mode 100644 index 00000000..1fb50f69 --- /dev/null +++ b/server/src/hardware/protocol/cbus/config.hpp @@ -0,0 +1,41 @@ +/** + * server/src/hardware/protocol/cbus/config.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 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_CONFIG_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_CONFIG_HPP + +#include + +namespace CBUS { + +struct Config +{ + std::chrono::milliseconds heartbeatTimeout; + std::chrono::milliseconds startupDelay; + + bool debugLogRXTX; + bool debugLogHeartbeat; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/featureflags.hpp b/server/src/hardware/protocol/cbus/featureflags.hpp new file mode 100644 index 00000000..394f3ffd --- /dev/null +++ b/server/src/hardware/protocol/cbus/featureflags.hpp @@ -0,0 +1,85 @@ +/** + * server/src/hardware/protocol/cbus/featureflags.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 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_FEATUREFLAGS_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_FEATUREFLAGS_HPP + +#include + +namespace CBUS { + +enum class FeatureFlags1 : uint8_t +{ + None = 0x00, + Input = 0x01, + Output = 0x02, + Throttle = 0x04, +}; + +enum class FeatureFlags2 : uint8_t +{ + None = 0x00, +}; + +enum class FeatureFlags3 : uint8_t +{ + None = 0x00, +}; + +enum class FeatureFlags4 : uint8_t +{ + None = 0x00, +}; + +template +constexpr bool isFeatureFlagType() +{ + return + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v; +} + +} + +template()>* = nullptr> +constexpr T operator&(const T& lhs, const T& rhs) +{ + using UT = std::underlying_type_t; + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +template()>* = nullptr> +constexpr T operator|(const T& lhs, const T& rhs) +{ + using UT = std::underlying_type_t; + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +template()>* = nullptr> +constexpr bool contains(T set, T value) +{ + return (set & value) == value; +} + +#endif diff --git a/server/src/hardware/protocol/cbus/inputstate.hpp b/server/src/hardware/protocol/cbus/inputstate.hpp new file mode 100644 index 00000000..21a38491 --- /dev/null +++ b/server/src/hardware/protocol/cbus/inputstate.hpp @@ -0,0 +1,62 @@ +/** + * server/src/hardware/protocol/cbus/inputstate.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 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_TRAINTASTICDIY_INPUTSTATE_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_INPUTSTATE_HPP + +#include +#include + +namespace CBUS { + +enum class InputState : uint8_t +{ + Undefined = 0, + False = 1, + True = 2, + Invalid = 3, +}; + +} + +constexpr std::string_view toString(CBUS::InputState value) +{ + using InputState = CBUS::InputState; + + switch(value) + { + case InputState::Undefined: + return "Undefined"; + + case InputState::False: + return "False"; + + case InputState::True: + return "True"; + + case InputState::Invalid: + return "Invalid"; + } + return {}; +} + +#endif diff --git a/server/src/hardware/protocol/cbus/iohandler/hardwareiohandler.cpp b/server/src/hardware/protocol/cbus/iohandler/hardwareiohandler.cpp new file mode 100644 index 00000000..2b8e9ad5 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/hardwareiohandler.cpp @@ -0,0 +1,101 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/hardwareiohandler.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2023 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 "hardwareiohandler.hpp" +#include "../kernel.hpp" +#include "../messages.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" +#include "../../../../utils/tohex.hpp" + +namespace CBUS { + +HardwareIOHandler::HardwareIOHandler(Kernel& kernel) + : IOHandler{kernel} + , m_readBufferOffset{0} + , m_writeBufferOffset{0} +{ +} + +bool HardwareIOHandler::send(const Message& message) +{ + if(m_writeBufferOffset + message.size() > m_writeBuffer.size()) + return false; + + const bool wasEmpty = m_writeBufferOffset == 0; + + memcpy(m_writeBuffer.data() + m_writeBufferOffset, &message, message.size()); + m_writeBufferOffset += message.size(); + + if(wasEmpty) + write(); + + return true; +} + +void HardwareIOHandler::processRead(size_t bytesTransferred) +{ + const std::byte* pos = m_readBuffer.data(); + bytesTransferred += m_readBufferOffset; + + while(bytesTransferred > 1) + { + const Message* message = nullptr; + size_t drop = 0; + + while(drop < bytesTransferred) + { + message = reinterpret_cast(pos); + if(message->size() > bytesTransferred || isChecksumValid(*message)) + break; + + drop++; + pos++; + bytesTransferred--; + } + + if(drop != 0) + { + EventLoop::call( + [this, drop, bytes=toHex(pos - drop, drop, true)]() + { + Log::log(m_kernel.logId, LogMessage::W2003_RECEIVED_MALFORMED_DATA_DROPPED_X_BYTES_X, drop, bytes); + }); + } + + assert(message); + if(message->size() <= bytesTransferred) + { + m_kernel.receive(*message); + pos += message->size(); + bytesTransferred -= message->size(); + } + else + break; + } + + if(bytesTransferred != 0) + memmove(m_readBuffer.data(), pos, bytesTransferred); + m_readBufferOffset = bytesTransferred; +} + +} diff --git a/server/src/hardware/protocol/cbus/iohandler/hardwareiohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/hardwareiohandler.hpp new file mode 100644 index 00000000..2dc14d23 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/hardwareiohandler.hpp @@ -0,0 +1,54 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/hardwareiohandler.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 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_HARDWAREIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_HARDWAREIOHANDLER_HPP + +#include +#include +#include "iohandler.hpp" + +namespace CBUS { + +class Kernel; +struct Message; + +class HardwareIOHandler : public IOHandler +{ + protected: + std::array m_readBuffer; + size_t m_readBufferOffset; + std::array m_writeBuffer; + size_t m_writeBufferOffset; + + HardwareIOHandler(Kernel& kernel); + + void processRead(size_t bytesTransferred); + virtual void write() = 0; + + public: + bool send(const Message& message) final; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/iohandler/iohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/iohandler.hpp new file mode 100644 index 00000000..5753bb96 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/iohandler.hpp @@ -0,0 +1,61 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/iohandler.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 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_IOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_IOHANDLER_HPP + +namespace CBUS { + +class Kernel; +struct Message; + +class IOHandler +{ + protected: + Kernel& m_kernel; + + IOHandler(Kernel& kernel) + : m_kernel{kernel} + { + } + + public: + IOHandler(const IOHandler&) = delete; + IOHandler& operator =(const IOHandler&) = delete; + + virtual ~IOHandler() = default; + + virtual void start() = 0; + virtual void stop() = 0; + + virtual bool send(const Message& message) = 0; +}; + +template +constexpr bool isSimulation() +{ + return false; +} + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/iohandler/serialiohandler.cpp b/server/src/hardware/protocol/cbus/iohandler/serialiohandler.cpp new file mode 100644 index 00000000..e6819486 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/serialiohandler.cpp @@ -0,0 +1,110 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/serialiohandler.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2024 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 "serialiohandler.hpp" +#include "../kernel.hpp" +#include "../messages.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" +#include "../../../../utils/serialport.hpp" + +namespace CBUS { + +SerialIOHandler::SerialIOHandler(Kernel& kernel, const std::string& device, uint32_t baudrate, SerialFlowControl flowControl) + : HardwareIOHandler(kernel) + , m_serialPort{m_kernel.ioContext()} +{ + SerialPort::open(m_serialPort, device, baudrate, 8, SerialParity::None, SerialStopBits::One, flowControl); +} + +SerialIOHandler::~SerialIOHandler() +{ + if(m_serialPort.is_open()) + { + boost::system::error_code ec; + m_serialPort.close(ec); + // ignore the error + } +} + +void SerialIOHandler::start() +{ + read(); + m_kernel.started(); +} + +void SerialIOHandler::stop() +{ + m_serialPort.close(); +} + +void SerialIOHandler::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 SerialIOHandler::write() +{ + m_serialPort.async_write_some(boost::asio::buffer(m_writeBuffer.data(), m_writeBufferOffset), + [this](const boost::system::error_code& ec, std::size_t bytesTransferred) + { + if(!ec) + { + if(bytesTransferred < m_writeBufferOffset) + { + m_writeBufferOffset -= bytesTransferred; + memmove(m_writeBuffer.data(), m_writeBuffer.data() + bytesTransferred, m_writeBufferOffset); + write(); + } + else + m_writeBufferOffset = 0; + } + 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/serialiohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/serialiohandler.hpp new file mode 100644 index 00000000..9deaa208 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/serialiohandler.hpp @@ -0,0 +1,50 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/serialiohandler.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 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_SERIALIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_SERIALIOHANDLER_HPP + +#include "hardwareiohandler.hpp" +#include +#include "../../../../enum/serialflowcontrol.hpp" + +namespace CBUS { + +class SerialIOHandler final : public HardwareIOHandler +{ + private: + boost::asio::serial_port m_serialPort; + + void read(); + void write() final; + + public: + SerialIOHandler(Kernel& kernel, const std::string& device, uint32_t baudrate, SerialFlowControl flowControl); + ~SerialIOHandler() final; + + void start() final; + void stop() final; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/iohandler/simulationiohandler.cpp b/server/src/hardware/protocol/cbus/iohandler/simulationiohandler.cpp new file mode 100644 index 00000000..ffe2f44a --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/simulationiohandler.cpp @@ -0,0 +1,130 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/simulationiohandler.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2024 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 "simulationiohandler.hpp" +#include "../kernel.hpp" +#include "../messages.hpp" +#include + +namespace CBUS { + +static std::shared_ptr copy(const Message& message) +{ + auto* bytes = new std::byte[message.size()]; + std::memcpy(bytes, &message, message.size()); + return std::shared_ptr{bytes}; +} + +SimulationIOHandler::SimulationIOHandler(Kernel& kernel) + : IOHandler(kernel) +{ +} + +void SimulationIOHandler::start() +{ + m_kernel.started(); +} + +bool SimulationIOHandler::send(const Message& message) +{ + switch(message.opCode) + { + case OpCode::Heartbeat: + reply(Heartbeat()); + break; + + case OpCode::GetInputState: + { + const auto& getInputState = static_cast(message); + reply(SetInputState(getInputState.address(), InputState::Invalid)); + break; + } + case OpCode::GetOutputState: + { + const auto& getOutputState = static_cast(message); + reply(SetOutputState(getOutputState.address(), OutputState::Invalid)); + break; + } + case OpCode::SetOutputState: + { +#ifndef NDEBUG + const auto& setOutputState = static_cast(message); + assert(setOutputState.state == OutputState::False || setOutputState.state == OutputState::True); +#endif + reply(message); + break; + } + case OpCode::ThrottleSubUnsub: + { + // TODO + break; + } + case OpCode::ThrottleSetFunction: + { + // TODO + break; + } + case OpCode::ThrottleSetSpeedDirection: + { + // TODO + break; + } + case OpCode::GetFeatures: + { + reply(Features(FeatureFlags1::Input | FeatureFlags1::Output)); + break; + } + case OpCode::GetInfo: + { + constexpr std::string_view text{"Traintastic DIY simulator v" TRAINTASTIC_VERSION}; + static_assert(text.size() <= 255); + auto info = std::make_unique(sizeof(InfoBase) + text.size() + sizeof(Checksum)); + auto& infoBase = *reinterpret_cast(info.get()); + infoBase.opCode = OpCode::Info; + infoBase.length = text.length(); + std::memcpy(info.get() + sizeof(InfoBase), text.data(), text.size()); + updateChecksum(infoBase); + reply(infoBase); + break; + } + case OpCode::SetInputState: + case OpCode::Features: + case OpCode::Info: + assert(false); // only send by device + break; + } + + return true; +} + +void SimulationIOHandler::reply(const Message& message) +{ + // post the reply, so it has some delay + //! \todo better delay simulation? at least message transfer time? + m_kernel.ioContext().post( + [this, data=copy(message)]() + { + m_kernel.receive(*reinterpret_cast(data.get())); + }); +} + +} diff --git a/server/src/hardware/protocol/cbus/iohandler/simulationiohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/simulationiohandler.hpp new file mode 100644 index 00000000..9ed71800 --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/simulationiohandler.hpp @@ -0,0 +1,55 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/simulationiohandler.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022,2024 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_SIMULATIONIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_SIMULATIONIOHANDLER_HPP + +#include "iohandler.hpp" +#include +#include + +namespace CBUS { + +class SimulationIOHandler final : public IOHandler +{ + private: + void reply(const Message& message); + + public: + SimulationIOHandler(Kernel& kernel); + + void start() final; + void stop() final {} + + bool send(const Message& message) final; +}; + +template<> +constexpr bool isSimulation() +{ + return true; +} + +} + +#endif + diff --git a/server/src/hardware/protocol/cbus/iohandler/tcpiohandler.cpp b/server/src/hardware/protocol/cbus/iohandler/tcpiohandler.cpp new file mode 100644 index 00000000..722db23d --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/tcpiohandler.cpp @@ -0,0 +1,145 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/tcpiohandler.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2024 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 "tcpiohandler.hpp" +#include +#include "../kernel.hpp" +#include "../messages.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" +#include "../../../../log/logmessageexception.hpp" + +namespace CBUS { + +TCPIOHandler::TCPIOHandler(Kernel& kernel, std::string hostname, uint16_t port) + : HardwareIOHandler(kernel) + , m_hostname{std::move(hostname)} + , m_port{port} + , m_socket{m_kernel.ioContext()} +{ +} + +TCPIOHandler::~TCPIOHandler() +{ + assert(!m_socket.is_open()); +} + +void TCPIOHandler::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 TCPIOHandler::stop() +{ + boost::system::error_code ec; + m_socket.cancel(ec); + m_socket.close(ec); + // ignore errors + m_connected = false; +} + +void TCPIOHandler::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 TCPIOHandler::write() +{ + if(m_writeBufferOffset == 0 || !m_connected) + { + return; + } + + m_socket.async_write_some(boost::asio::buffer(m_writeBuffer.data(), m_writeBufferOffset), + [this](const boost::system::error_code& ec, std::size_t bytesTransferred) + { + if(!ec) + { + m_writeBufferOffset -= bytesTransferred; + + if(m_writeBufferOffset > 0) + { + memmove(m_writeBuffer.data(), m_writeBuffer.data() + bytesTransferred, m_writeBufferOffset); + 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/tcpiohandler.hpp b/server/src/hardware/protocol/cbus/iohandler/tcpiohandler.hpp new file mode 100644 index 00000000..13daffdc --- /dev/null +++ b/server/src/hardware/protocol/cbus/iohandler/tcpiohandler.hpp @@ -0,0 +1,53 @@ +/** + * server/src/hardware/protocol/cbus/iohandler/tcpiohandler.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2024 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_TCPIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_IOHANDLER_TCPIOHANDLER_HPP + +#include "hardwareiohandler.hpp" +#include + +namespace CBUS { + +class TCPIOHandler final : public HardwareIOHandler +{ + private: + 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; + + void read(); + void write() final; + + public: + TCPIOHandler(Kernel& kernel, std::string hostname, uint16_t port); + ~TCPIOHandler() final; + + void start() final; + void stop() final; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/kernel.cpp b/server/src/hardware/protocol/cbus/kernel.cpp new file mode 100644 index 00000000..4cd51bff --- /dev/null +++ b/server/src/hardware/protocol/cbus/kernel.cpp @@ -0,0 +1,595 @@ +/** + * server/src/hardware/protocol/cbus/kernel.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2025 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 "kernel.hpp" +#include "messages.hpp" +#include "../../decoder/decoder.hpp" +#include "../../decoder/decoderchangeflags.hpp" +#include "../../decoder/list/decoderlist.hpp" +#include "../../input/inputcontroller.hpp" +#include "../../output/outputcontroller.hpp" +#include "../../../utils/inrange.hpp" +#include "../../../utils/setthreadname.hpp" +#include "../../../core/eventloop.hpp" +#include "../../../core/objectproperty.tpp" +#include "../../../log/log.hpp" +#include "../../../log/logmessageexception.hpp" +#include "../../../world/world.hpp" + +namespace CBUS { + +constexpr TriState toTriState(InputState value) +{ + switch(value) + { + case InputState::False: + return TriState::False; + + case InputState::True: + return TriState::True; + + case InputState::Undefined: + case InputState::Invalid: + break; + } + return TriState::Undefined; +} + +constexpr TriState toTriState(OutputState value) +{ + switch(value) + { + case OutputState::False: + return TriState::False; + return TriState::False; + + case OutputState::True: + return TriState::True; + + case OutputState::Undefined: + case OutputState::Invalid: + break; + } + return TriState::Undefined; +} + +Kernel::Kernel(std::string logId_, World& world, const Config& config, bool simulation) + : KernelBase(std::move(logId_)) + , m_world{world} + , m_simulation{simulation} + , m_startupDelayTimer{m_ioContext} + , m_heartbeatTimeout{m_ioContext} + , m_inputController{nullptr} + , m_outputController{nullptr} + , m_config{config} +{ +} + +void Kernel::setConfig(const Config& config) +{ + m_ioContext.post( + [this, newConfig=config]() + { + m_config = newConfig; + }); +} + +void Kernel::start() +{ + assert(m_ioHandler); + assert(!m_started); + assert(m_inputValues.empty()); + assert(m_outputValues.empty()); + assert(m_throttleSubscriptions.empty()); + assert(m_decoderSubscriptions.empty()); + + m_featureFlagsSet = false; + m_featureFlags1 = FeatureFlags1::None; + m_featureFlags2 = FeatureFlags2::None; + m_featureFlags3 = FeatureFlags3::None; + m_featureFlags4 = FeatureFlags4::None; + + 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; + } + }); + +#ifndef NDEBUG + m_started = true; +#endif +} + +void Kernel::stop() +{ + for(auto& it : m_decoderSubscriptions) + it.second.connection.disconnect(); + + m_ioContext.post( + [this]() + { + m_heartbeatTimeout.cancel(); + m_ioHandler->stop(); + }); + + m_ioContext.stop(); + + m_thread.join(); + + m_inputValues.clear(); + m_outputValues.clear(); + m_throttleSubscriptions.clear(); + m_decoderSubscriptions.clear(); + +#ifndef NDEBUG + m_started = false; +#endif +} + +void Kernel::started() +{ + assert(isKernelThread()); + + m_startupDelayTimer.expires_after(boost::asio::chrono::milliseconds(m_config.startupDelay)); + m_startupDelayTimer.async_wait(std::bind(&Kernel::startupDelayExpired, this, std::placeholders::_1)); +} + +void Kernel::receive(const Message& message) +{ + if(m_config.debugLogRXTX && (message != Heartbeat() || m_config.debugLogHeartbeat)) + EventLoop::call( + [this, msg=toString(message)]() + { + Log::log(logId, LogMessage::D2002_RX_X, msg); + }); + + restartHeartbeatTimeout(); + + switch(message.opCode) + { + case OpCode::Heartbeat: + break; + + case OpCode::SetInputState: + { + if(!m_featureFlagsSet || !hasFeatureInput()) + break; + + const auto& setInputState = static_cast(message); + const uint16_t address = setInputState.address(); + if(inRange(address, ioAddressMin, ioAddressMax)) + { + auto it = m_inputValues.find(address); + if(it == m_inputValues.end() || it->second != setInputState.state) + { + m_inputValues[address] = setInputState.state; + + EventLoop::call( + [this, address, state=setInputState.state]() + { + if(state == InputState::Invalid) + { + if(m_inputController->inputMap().count({InputChannel::Input, address}) != 0) + Log::log(logId, LogMessage::W2004_INPUT_ADDRESS_X_IS_INVALID, address); + } + else + m_inputController->updateInputValue(InputChannel::Input, address, toTriState(state)); + }); + } + } + break; + } + case OpCode::SetOutputState: + { + if(!m_featureFlagsSet || !hasFeatureOutput()) + break; + + const auto& setOutputState = static_cast(message); + const uint16_t address = setOutputState.address(); + if(inRange(address, ioAddressMin, ioAddressMax)) + { + auto it = m_outputValues.find(address); + if(it == m_outputValues.end() || it->second != setOutputState.state) + { + m_outputValues[address] = setOutputState.state; + + EventLoop::call( + [this, address, state=setOutputState.state]() + { + if(state == OutputState::Invalid) + { + if(m_outputController->outputMap().count({OutputChannel::Output, address}) != 0) + Log::log(logId, LogMessage::W2005_OUTPUT_ADDRESS_X_IS_INVALID, address); + } + else + m_outputController->updateOutputValue(OutputChannel::Output, address, toTriState(state)); + }); + } + } + break; + } + case OpCode::ThrottleSubUnsub: + { + if(!m_featureFlagsSet || !hasFeatureThrottle()) + break; + + const auto& subUnsub = static_cast(message); + switch(subUnsub.action()) + { + case ThrottleSubUnsub::Unsubscribe: + throttleUnsubscribe(subUnsub.throttleId(), {subUnsub.address(), subUnsub.isLongAddress()}); + send(subUnsub); + break; + + case ThrottleSubUnsub::Subscribe: + throttleSubscribe(subUnsub.throttleId(), {subUnsub.address(), subUnsub.isLongAddress()}); + EventLoop::call( + [this, subUnsub]() + { + if(auto decoder = getDecoder(subUnsub.address(), subUnsub.isLongAddress())) + { + uint8_t speedMax = 0; + uint8_t speed = 0; + + if(!decoder->emergencyStop) + { + speedMax = decoder->speedSteps.value(); + if(speedMax == Decoder::speedStepsAuto) + speedMax = std::numeric_limits::max(); + speed = Decoder::throttleToSpeedStep(decoder->throttle, speedMax); + } + + postSend(ThrottleSetSpeedDirection(subUnsub.throttleId(), subUnsub.address(), subUnsub.isLongAddress(), speed, speedMax, decoder->direction)); + for(const auto& function : *decoder->functions) + postSend(ThrottleSetFunction(subUnsub.throttleId(), subUnsub.address(), subUnsub.isLongAddress(), function->number, function->value)); + } + }); + break; + } + break; + } + case OpCode::ThrottleSetFunction: + { + if(!m_featureFlagsSet || !hasFeatureThrottle()) + break; + + const auto& throttleSetFunction = static_cast(message); + + throttleSubscribe(throttleSetFunction.throttleId(), {throttleSetFunction.address(), throttleSetFunction.isLongAddress()}); + + EventLoop::call( + [this, throttleSetFunction]() + { + if(auto decoder = getDecoder(throttleSetFunction.address(), throttleSetFunction.isLongAddress())) + { + bool value = false; + + if(auto function = decoder->getFunction(throttleSetFunction.functionNumber())) + { + function->value = throttleSetFunction.functionValue(); + if(function->value != throttleSetFunction.functionValue()) + { + send(ThrottleSetFunction( + throttleSetFunction.throttleId(), + throttleSetFunction.address(), + throttleSetFunction.isLongAddress(), + throttleSetFunction.functionNumber(), + function->value)); + } + } + else + { + // warning or debug? + send(ThrottleSetFunction( + throttleSetFunction.throttleId(), + throttleSetFunction.address(), + throttleSetFunction.isLongAddress(), + throttleSetFunction.functionNumber(), + value)); + } + } + }); + break; + } + case OpCode::ThrottleSetSpeedDirection: + { + if(!m_featureFlagsSet || !hasFeatureThrottle()) + break; + + const auto& throttleSetSpeedDirection = static_cast(message); + + throttleSubscribe(throttleSetSpeedDirection.throttleId(), {throttleSetSpeedDirection.address(), throttleSetSpeedDirection.isLongAddress()}); + + EventLoop::call( + [this, throttleSetSpeedDirection]() + { + if(auto decoder = getDecoder(throttleSetSpeedDirection.address(), throttleSetSpeedDirection.isLongAddress())) + { + if(throttleSetSpeedDirection.isSpeedSet()) + { + decoder->emergencyStop = throttleSetSpeedDirection.isEmergencyStop(); + if(!throttleSetSpeedDirection.isEmergencyStop()) + decoder->throttle = throttleSetSpeedDirection.throttle(); + } + if(throttleSetSpeedDirection.isDirectionSet()) + decoder->direction = throttleSetSpeedDirection.direction(); + } + }); + break; + } + case OpCode::Features: + { + const auto& features = static_cast(message); + m_featureFlagsSet = true; + m_featureFlags1 = features.featureFlags1; + m_featureFlags2 = features.featureFlags2; + m_featureFlags3 = features.featureFlags3; + m_featureFlags4 = features.featureFlags4; + + if(hasFeatureInput()) + EventLoop::call( + [this]() + { + for(const auto& it : m_inputController->inputMap()) + postSend(GetInputState(static_cast(it.first.address))); + }); + + if(hasFeatureOutput()) + EventLoop::call( + [this]() + { + for(const auto& it : m_outputController->outputMap()) + postSend(GetOutputState(static_cast(it.first.id))); + }); + break; + } + case OpCode::Info: + { + const auto& info = static_cast(message); + EventLoop::call( + [this, text=std::string(info.text())]() + { + Log::log(logId, LogMessage::I2005_X, text); + }); + break; + } + case OpCode::GetInfo: + case OpCode::GetFeatures: + case OpCode::GetOutputState: + case OpCode::GetInputState: + assert(false); + break; + } +} + +bool Kernel::setOutput(uint16_t address, bool value) +{ + postSend(SetOutputState(address, value ? OutputState::True : OutputState::False)); + return true; +} + +void Kernel::simulateInputChange(uint16_t address, SimulateInputAction action) +{ + if(m_simulation) + m_ioContext.post( + [this, address, action]() + { + CBUS::InputState state; + auto it = m_inputValues.find(address); + switch(action) + { + case SimulateInputAction::SetFalse: + if(it != m_inputValues.end() && it->second == InputState::False) + return; // no change + state = InputState::False; + break; + + case SimulateInputAction::SetTrue: + if(it != m_inputValues.end() && it->second == InputState::True) + return; // no change + state = InputState::True; + break; + + case SimulateInputAction::Toggle: + state = (it == m_inputValues.end() || it->second == InputState::True) ? InputState::False : InputState::True; + break; + + default: + assert(false); + return; + } + receive(SetInputState(address, state)); + }); +} + +void Kernel::setIOHandler(std::unique_ptr handler) +{ + assert(handler); + assert(!m_ioHandler); + m_ioHandler = std::move(handler); +} + +void Kernel::send(const Message& message) +{ + if(m_ioHandler->send(message)) + { + if(m_config.debugLogRXTX && (message != Heartbeat() || m_config.debugLogHeartbeat)) + EventLoop::call( + [this, msg=toString(message)]() + { + Log::log(logId, LogMessage::D2001_TX_X, msg); + }); + } + else + {} // log message and go to error state +} + +void Kernel::startupDelayExpired(const boost::system::error_code& ec) +{ + if(ec) + return; + + send(GetInfo()); + send(GetFeatures()); + + restartHeartbeatTimeout(); + + KernelBase::started(); +} + +void Kernel::restartHeartbeatTimeout() +{ + m_heartbeatTimeout.expires_after(m_config.heartbeatTimeout); + m_heartbeatTimeout.async_wait(std::bind(&Kernel::heartbeatTimeoutExpired, this, std::placeholders::_1)); +} + +void Kernel::heartbeatTimeoutExpired(const boost::system::error_code& ec) +{ + if(ec) + return; + m_heartbeatTimeout.cancel(); + send(Heartbeat()); + restartHeartbeatTimeout(); +} + +std::shared_ptr Kernel::getDecoder(uint16_t address, bool longAddress) const +{ + const auto& decoderList = *m_world.decoders; + std::shared_ptr decoder = decoderList.getDecoder(longAddress ? DecoderProtocol::DCCLong : DecoderProtocol::DCCShort, address); + if(!decoder) + decoder = decoderList.getDecoder(address); + return decoder; +} + +void Kernel::throttleSubscribe(uint16_t throttleId, std::pair key) +{ + auto [unused, added] = m_throttleSubscriptions[throttleId].insert(key); + if(added) + { + EventLoop::call( + [this, key]() + { + if(auto it = m_decoderSubscriptions.find(key); it == m_decoderSubscriptions.end()) + { + if(auto decoder = getDecoder(key.first, key.second)) + m_decoderSubscriptions.emplace(key, DecoderSubscription{decoder->decoderChanged.connect(std::bind(&Kernel::throttleDecoderChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), 1}); + } + else + { + it->second.count++; + } + }); + } +} + +void Kernel::throttleUnsubscribe(uint16_t throttleId, std::pair key) +{ + { + auto& subscriptions = m_throttleSubscriptions[throttleId]; + subscriptions.erase(key); + if(subscriptions.empty()) + m_throttleSubscriptions.erase(throttleId); + } + + EventLoop::call( + [this, key]() + { + if(auto it = m_decoderSubscriptions.find(key); it != m_decoderSubscriptions.end()) + { + assert(it->second.count > 0); + if(--it->second.count == 0) + { + it->second.connection.disconnect(); + m_decoderSubscriptions.erase(it); + } + } + }); +} + +void Kernel::throttleDecoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber) +{ + const std::pair key(decoder.address, decoder.protocol == DecoderProtocol::DCCLong); + + if(has(changes, DecoderChangeFlags::Direction | DecoderChangeFlags::EmergencyStop | DecoderChangeFlags::SpeedSteps | DecoderChangeFlags::Throttle)) + { + const bool emergencyStop = decoder.emergencyStop.value(); + + uint8_t speedMax = 0; + if(!emergencyStop) + { + speedMax = decoder.speedSteps.value(); + if(speedMax == Decoder::speedStepsAuto) + speedMax = std::numeric_limits::max(); + } + + m_ioContext.post( + [this, + key, + direction=decoder.direction.value(), + speed=speedMax > 0 ? Decoder::throttleToSpeedStep(decoder.throttle, speedMax) : 0, + speedMax]() + { + for(const auto& it : m_throttleSubscriptions) + if(it.second.count(key) != 0) + send(ThrottleSetSpeedDirection(it.first, key.first, key.second, speed, speedMax, direction)); + }); + } + + if(has(changes, DecoderChangeFlags::FunctionValue)) + { + assert(functionNumber <= std::numeric_limits::max()); + + m_ioContext.post( + [this, + key, + number=static_cast(functionNumber), + value=decoder.getFunctionValue(functionNumber)]() + { + for(const auto& it : m_throttleSubscriptions) + if(it.second.count(key) != 0) + send(ThrottleSetFunction(it.first, key.first, key.second, number, value)); + }); + } +} + +} diff --git a/server/src/hardware/protocol/cbus/kernel.hpp b/server/src/hardware/protocol/cbus/kernel.hpp new file mode 100644 index 00000000..076736eb --- /dev/null +++ b/server/src/hardware/protocol/cbus/kernel.hpp @@ -0,0 +1,231 @@ +/** + * server/src/hardware/protocol/cbus/kernel.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2024 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_KERNEL_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_KERNEL_HPP + +#include "../kernelbase.hpp" +#include +#include +#include +#include +#include +#include "config.hpp" +#include "featureflags.hpp" +#include "inputstate.hpp" +#include "outputstate.hpp" +#include "iohandler/iohandler.hpp" + +class World; +enum class SimulateInputAction; +class InputController; +class OutputController; +class Decoder; +enum class DecoderChangeFlags; + +namespace CBUS { + +struct Message; + +class Kernel : public ::KernelBase +{ + private: + struct DecoderSubscription + { + boost::signals2::connection connection; + size_t count; //!< number of throttles subscribed to the decoder + }; + + World& m_world; + std::unique_ptr m_ioHandler; + const bool m_simulation; + std::string m_logId; + boost::asio::steady_timer m_startupDelayTimer; + boost::asio::steady_timer m_heartbeatTimeout; + + bool m_featureFlagsSet; + FeatureFlags1 m_featureFlags1; + FeatureFlags2 m_featureFlags2; + FeatureFlags3 m_featureFlags3; + FeatureFlags4 m_featureFlags4; + + InputController* m_inputController; + std::unordered_map m_inputValues; + + OutputController* m_outputController; + std::unordered_map m_outputValues; + + std::unordered_map>> m_throttleSubscriptions; + std::map, DecoderSubscription> m_decoderSubscriptions; + + Config m_config; + + Kernel(std::string logId, World& world, const Config& config, bool simulation); + + void setIOHandler(std::unique_ptr handler); + + template + void postSend(const T& message) + { + m_ioContext.post( + [this, message]() + { + send(message); + }); + } + + void send(const Message& message); + + inline bool hasFeatureInput() const { return contains(m_featureFlags1, FeatureFlags1::Input); } + inline bool hasFeatureOutput() const { return contains(m_featureFlags1, FeatureFlags1::Output); } + inline bool hasFeatureThrottle() const { return contains(m_featureFlags1, FeatureFlags1::Throttle); } + + void startupDelayExpired(const boost::system::error_code& ec); + + void restartHeartbeatTimeout(); + void heartbeatTimeoutExpired(const boost::system::error_code& ec); + + std::shared_ptr getDecoder(uint16_t address, bool longAddress) const; + + void throttleSubscribe(uint16_t throttleId, std::pair key); + void throttleUnsubscribe(uint16_t throttleId, std::pair key); + void throttleDecoderChanged(const Decoder& decoder, DecoderChangeFlags changes, uint32_t functionNumber); + + public: + static constexpr uint16_t ioAddressMin = 1; + static constexpr uint16_t ioAddressMax = std::numeric_limits::max(); + + Kernel(const Kernel&) = delete; + Kernel& operator =(const Kernel&) = delete; + +#ifndef NDEBUG + bool isKernelThread() const + { + return std::this_thread::get_id() == m_thread.get_id(); + } +#endif + + /** + * \brief Create kernel and IO handler + * + * \param[in] config TraintasticDIY configuration + * \param[in] args IO handler arguments + * \return The kernel instance + */ + template + static std::unique_ptr create(std::string logId_, World& world, const Config& config, Args... args) + { + static_assert(std::is_base_of_v); + std::unique_ptr kernel{new Kernel(std::move(logId_), world, config, isSimulation())}; + kernel->setIOHandler(std::make_unique(*kernel, std::forward(args)...)); + return kernel; + } + + /** + * \brief Access the IO handler + * + * \return The IO handler + * \note The IO handler runs in the kernel's IO context, not all functions can be called safely! + */ + template + T& ioHandler() + { + assert(dynamic_cast(m_ioHandler.get())); + return static_cast(*m_ioHandler); + } + + /** + * \brief Set TraintasticDIY configuration + * + * \param[in] config The TraintasticDIY configuration + */ + void setConfig(const Config& config); + + /** + * \brief Set the input controller + * + * \param[in] inputController The input controller + * \note This function may not be called when the kernel is running. + */ + inline void setInputController(InputController* inputController) + { + assert(!m_started); + m_inputController = inputController; + } + + /** + * \brief Set the output controller + * + * \param[in] outputController The output controller + * \note This function may not be called when the kernel is running. + */ + inline void setOutputController(OutputController* outputController) + { + assert(!m_started); + m_outputController = outputController; + } + + /** + * \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; + + /** + * \brief ... + * + * This must be called by the IO handler whenever a TraintasticDIY message is received. + * + * \param[in] message The received TraintasticDIY message + * \note This function must run in the kernel's IO context + */ + void receive(const Message& message); + + /** + * + * \param[in] address Output address, #ioAddressMin..#ioAddressMax + * \param[in] value Output value: \c true is on, \c false is off. + * \return \c true if send successful, \c false otherwise. + */ + bool setOutput(uint16_t address, bool value); + + /** + * \brief Simulate input change + * \param[in] address Input address, #ioAddressMin..#ioAddressMax + * \param[in] action Simulation action to perform + */ + void simulateInputChange(uint16_t address, SimulateInputAction action); +}; + +} + +#endif diff --git a/server/src/hardware/protocol/cbus/messages.cpp b/server/src/hardware/protocol/cbus/messages.cpp new file mode 100644 index 00000000..b6edc125 --- /dev/null +++ b/server/src/hardware/protocol/cbus/messages.cpp @@ -0,0 +1,159 @@ +/** + * server/src/hardware/protocol/cbus/messages.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2023 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 "messages.hpp" +#include +#include "../../../utils/tohex.hpp" + +namespace CBUS { + +static constexpr std::string_view toString(ThrottleSubUnsub::Action action) +{ + switch(action) + { + case ThrottleSubUnsub::Unsubscribe: + return "unsub"; + + case ThrottleSubUnsub::Subscribe: + return "sub"; + } + return {}; +} + +Checksum calcChecksum(const Message& message) +{ + const auto* p = reinterpret_cast(&message); + const size_t dataSize = message.dataSize(); + uint8_t checksum = p[0]; + for(size_t i = 1; i <= dataSize; i++) + checksum ^= p[i]; + return static_cast(checksum); +} + +void updateChecksum(Message& message) +{ + *(reinterpret_cast(&message) + message.dataSize() + 1) = calcChecksum(message); +} + +bool isChecksumValid(const Message& message) +{ + return calcChecksum(message) == *(reinterpret_cast(&message) + message.dataSize() + 1); +} + +std::string toString(const Message& message) +{ + std::string s{::toString(message.opCode)}; + + switch(message.opCode) + { + case OpCode::Heartbeat: + case OpCode::GetInfo: + case OpCode::GetFeatures: + assert(message.dataSize() == 0); + break; + + case OpCode::GetInputState: + { + const auto& getInputState = static_cast(message); + s.append(" address=").append(std::to_string(getInputState.address())); + break; + } + case OpCode::SetInputState: + { + const auto& setInputState = static_cast(message); + s.append(" address=").append(std::to_string(setInputState.address())); + s.append(" state=").append(::toString(setInputState.state)); + break; + } + case OpCode::GetOutputState: + { + const auto& getOutputState = static_cast(message); + s.append(" address=").append(std::to_string(getOutputState.address())); + break; + } + case OpCode::SetOutputState: + { + const auto& setOutputState = static_cast(message); + s.append(" address=").append(std::to_string(setOutputState.address())); + s.append(" state=").append(::toString(setOutputState.state)); + break; + } + case OpCode::ThrottleSubUnsub: + { + const auto& throttleSubUnsub = static_cast(message); + s.append(" throttle=").append(std::to_string(throttleSubUnsub.throttleId())); + s.append(" address=").append(std::to_string(throttleSubUnsub.address())); + s.append(" action=").append(toString(throttleSubUnsub.action())); + break; + } + case OpCode::ThrottleSetFunction: + { + const auto& throttleSetFunction = static_cast(message); + s.append(" throttle=").append(std::to_string(throttleSetFunction.throttleId())); + s.append(" address=").append(std::to_string(throttleSetFunction.address())); + s.append(" function=").append(std::to_string(throttleSetFunction.functionNumber())); + s.append(" value=").append(throttleSetFunction.functionValue() ? "on" : "off"); + break; + } + case OpCode::ThrottleSetSpeedDirection: + { + const auto& throttleSetSpeedDirection = static_cast(message); + s.append(" throttle=").append(std::to_string(throttleSetSpeedDirection.throttleId())); + s.append(" address=").append(std::to_string(throttleSetSpeedDirection.address())); + if(throttleSetSpeedDirection.isSpeedSet()) + { + if(!throttleSetSpeedDirection.isEmergencyStop()) + { + s.append(" speed=").append(std::to_string(throttleSetSpeedDirection.speed)); + s.append(" speed_max=").append(std::to_string(throttleSetSpeedDirection.speedMax)); + } + else + s.append(" estop"); + } + if(throttleSetSpeedDirection.isDirectionSet()) + s.append(" direction=").append((throttleSetSpeedDirection.direction() == Direction::Forward) ? "fwd" : "rev"); + break; + } + case OpCode::Features: + { + break; + } + case OpCode::Info: + { + break; + } + } + + s.append(" ["); + const auto* bytes = reinterpret_cast(&message); + for(size_t i = 0; i < message.size(); i++) + { + if(i != 0) + s.append(" "); + s.append(toHex(bytes[i])); + } + s.append("]"); + + return s; +} + +} diff --git a/server/src/hardware/protocol/cbus/messages.hpp b/server/src/hardware/protocol/cbus/messages.hpp new file mode 100644 index 00000000..bea29b50 --- /dev/null +++ b/server/src/hardware/protocol/cbus/messages.hpp @@ -0,0 +1,410 @@ +/** + * server/src/hardware/protocol/cbus/messages.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2023 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_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_MESSAGES_HPP + +#include +#include +#include "opcode.hpp" +#include "inputstate.hpp" +#include "outputstate.hpp" +#include "featureflags.hpp" +#include "../../../enum/direction.hpp" +#include "../../../utils/byte.hpp" + +namespace CBUS { + +using Checksum = std::byte; +struct Message; + +Checksum calcChecksum(const Message& message); +void updateChecksum(Message& message); +bool isChecksumValid(const Message& message); +std::string toString(const Message& message); + +struct Message +{ + OpCode opCode; + + Message(OpCode opCode_) + : opCode{opCode_} + { + } + + size_t dataSize() const + { + if(const uint8_t len = (static_cast(opCode) & 0x0F); len != 0x0F) + return len; + return sizeof(uint8_t) + *(reinterpret_cast(this) + 1); + } + + size_t size() const + { + return sizeof(Message) + dataSize() + 1; + } +}; + +struct Heartbeat : Message +{ + Checksum checksum; + + Heartbeat() + : Message(OpCode::Heartbeat) + , checksum{calcChecksum(*this)} + { + } +}; +static_assert(sizeof(Heartbeat) == 2); + +struct GetInputState : Message +{ + uint8_t addressHigh; + uint8_t addressLow; + Checksum checksum; + + GetInputState(uint16_t address_ = 0) + : Message(OpCode::GetInputState) + , addressHigh{high8(address_)} + , addressLow{low8(address_)} + , checksum{calcChecksum(*this)} + { + } + + uint16_t address() const + { + return to16(addressLow, addressHigh); + } +}; +static_assert(sizeof(GetInputState) == 4); + +struct SetInputState : Message +{ + uint8_t addressHigh; + uint8_t addressLow; + InputState state; + Checksum checksum; + + SetInputState(uint16_t address_, InputState state_) + : Message(OpCode::SetInputState) + , addressHigh{high8(address_)} + , addressLow{low8(address_)} + , state{state_} + , checksum{calcChecksum(*this)} + { + } + + uint16_t address() const + { + return to16(addressLow, addressHigh); + } +}; +static_assert(sizeof(SetInputState) == 5); + +struct GetOutputState : Message +{ + uint8_t addressHigh; + uint8_t addressLow; + Checksum checksum; + + GetOutputState(uint16_t address_ = 0) + : Message(OpCode::GetOutputState) + , addressHigh{high8(address_)} + , addressLow{low8(address_)} + , checksum{calcChecksum(*this)} + { + } + + uint16_t address() const + { + return to16(addressLow, addressHigh); + } +}; +static_assert(sizeof(GetOutputState) == 4); + +struct SetOutputState : Message +{ + uint8_t addressHigh; + uint8_t addressLow; + OutputState state; + Checksum checksum; + + SetOutputState(uint16_t address_, OutputState state_) + : Message(OpCode::SetOutputState) + , addressHigh{high8(address_)} + , addressLow{low8(address_)} + , state{state_} + , checksum{calcChecksum(*this)} + { + } + + uint16_t address() const + { + return to16(addressLow, addressHigh); + } +}; +static_assert(sizeof(SetOutputState) == 5); + +struct ThrottleMessage : Message +{ + static constexpr uint8_t addressHighMask = 0x3F; + + uint8_t throttleIdHigh; + uint8_t throttleIdLow; + uint8_t addressHigh; + uint8_t addressLow; + + ThrottleMessage(OpCode opCode_, uint16_t throttleId_, uint16_t address_, bool longAddress) + : Message(opCode_) + , throttleIdHigh{high8(throttleId_)} + , throttleIdLow{low8(throttleId_)} + , addressHigh((high8(address_) & addressHighMask) | (longAddress ? 0x80 : 0x00)) + , addressLow{low8(address_)} + { + } + + uint16_t throttleId() const + { + return to16(throttleIdLow, throttleIdHigh); + } + + uint16_t address() const + { + return to16(addressLow, addressHigh & addressHighMask); + } + + bool isLongAddress() const + { + return (addressHigh & 0x80); + } +}; +static_assert(sizeof(ThrottleMessage) == 5); + +struct ThrottleSubUnsub : ThrottleMessage +{ + static constexpr uint8_t addressHighSubUnsubBit = 0x40; + + enum Action + { + Unsubscribe = 0, + Subscribe = 1 + }; + + Checksum checksum; + + ThrottleSubUnsub(uint16_t throttleId_, uint16_t address_, bool longAddress, Action action_) + : ThrottleMessage(OpCode::ThrottleSubUnsub, throttleId_, address_, longAddress) + , checksum{calcChecksum(*this)} + { + setAction(action_); + } + + Action action() const + { + return (addressHigh & addressHighSubUnsubBit) ? Subscribe : Unsubscribe; + } + + void setAction(Action value) + { + assert(value == Subscribe || value == Unsubscribe); + switch(value) + { + case Subscribe: + addressHigh |= addressHighSubUnsubBit; // set + break; + + case Unsubscribe: + addressHigh &= ~addressHighSubUnsubBit; // clear + break; + } + } +}; +static_assert(sizeof(ThrottleSubUnsub) == 6); + +struct ThrottleSetSpeedDirection : ThrottleMessage +{ + static constexpr uint8_t flagDirectionForward = 0x01; + static constexpr uint8_t flagDirectionSet = 0x40; + static constexpr uint8_t flagSpeedSet = 0x80; + + uint8_t speed; + uint8_t speedMax; + uint8_t flags; + Checksum checksum; + + ThrottleSetSpeedDirection(uint16_t throttleId_, uint16_t address_, bool longAddress, uint8_t speed_, uint8_t speedMax_, Direction direction_) + : ThrottleMessage(OpCode::ThrottleSetSpeedDirection, throttleId_, address_, longAddress) + , speed{speed_} + , speedMax{speedMax_} + , flags{flagSpeedSet | flagDirectionSet} + { + assert(direction_ != Direction::Unknown); + if(direction_ == Direction::Forward) + flags |= flagDirectionForward; + updateChecksum(*this); + } + + ThrottleSetSpeedDirection(uint16_t throttleId_, uint16_t address_, bool longAddress, uint8_t speed_, uint8_t speedMax_) + : ThrottleMessage(OpCode::ThrottleSetSpeedDirection, throttleId_, address_, longAddress) + , speed{speed_} + , speedMax{speedMax_} + , flags{flagSpeedSet} + , checksum{calcChecksum(*this)} + { + } + + ThrottleSetSpeedDirection(uint16_t throttleId_, uint16_t address_, bool longAddress, Direction direction_) + : ThrottleMessage(OpCode::ThrottleSetSpeedDirection, throttleId_, address_, longAddress) + , speed{0} + , speedMax{0} + , flags{flagDirectionSet} + { + assert(direction_ != Direction::Unknown); + if(direction_ == Direction::Forward) + flags |= flagDirectionForward; + updateChecksum(*this); + } + + bool isEmergencyStop() const + { + return speedMax == 0; + } + + float throttle() const + { + return (speedMax > 0) ? std::min(static_cast(speed) / static_cast(speedMax), 1) : 0; + } + + bool isSpeedSet() const + { + return (flags & flagSpeedSet); + } + + bool isDirectionSet() const + { + return (flags & flagDirectionSet); + } + + Direction direction() const + { + return (flags & flagDirectionForward) ? Direction::Forward : Direction::Reverse; + } +}; +static_assert(sizeof(ThrottleSetSpeedDirection) == 9); + +struct ThrottleSetFunction : ThrottleMessage +{ + static constexpr uint8_t functionNumberMask = 0x7F; + static constexpr uint8_t functionValueMask = 0x80; + static constexpr uint8_t functionValueOn = 0x80; + static constexpr uint8_t functionValueOff = 0x00; + + uint8_t function; + Checksum checksum; + + ThrottleSetFunction(uint16_t throttleId_, uint16_t address_, bool longAddress, uint8_t functionNumber_, bool functionValue_) + : ThrottleMessage(OpCode::ThrottleSetFunction, throttleId_, address_, longAddress) + , function((functionNumber_ & functionNumberMask) | (functionValue_ ? functionValueOn : functionValueOff)) + , checksum{calcChecksum(*this)} + { + } + + uint8_t functionNumber() const + { + return function & functionNumberMask; + } + + bool functionValue() const + { + return (function & functionValueMask) == functionValueOn; + } +}; +static_assert(sizeof(ThrottleSetFunction) == 7); + +struct GetFeatures : Message +{ + Checksum checksum; + + GetFeatures() + : Message(OpCode::GetFeatures) + , checksum{calcChecksum(*this)} + { + } +}; +static_assert(sizeof(GetFeatures) == 2); + +struct Features : Message +{ + FeatureFlags1 featureFlags1; + FeatureFlags2 featureFlags2; + FeatureFlags3 featureFlags3; + FeatureFlags4 featureFlags4; + Checksum checksum; + + Features(FeatureFlags1 ff1 = FeatureFlags1::None, FeatureFlags2 ff2 = FeatureFlags2::None, FeatureFlags3 ff3 = FeatureFlags3::None, FeatureFlags4 ff4 = FeatureFlags4::None) + : Message(OpCode::Features) + , featureFlags1{ff1} + , featureFlags2{ff2} + , featureFlags3{ff3} + , featureFlags4{ff4} + , checksum{calcChecksum(*this)} + { + } +}; +static_assert(sizeof(Features) == 6); + +struct GetInfo : Message +{ + Checksum checksum; + + GetInfo() + : Message(OpCode::GetInfo) + , checksum{calcChecksum(*this)} + { + } +}; +static_assert(sizeof(GetInfo) == 2); + +struct InfoBase : Message +{ + uint8_t length; + + std::string_view text() const + { + return {reinterpret_cast(this) + sizeof(Message) + sizeof(length), length}; + } +}; +static_assert(sizeof(InfoBase) == 2); + +} + +inline bool operator ==(const CBUS::Message& lhs, const CBUS::Message& rhs) +{ + return lhs.size() == rhs.size() && std::memcmp(&lhs, &rhs, lhs.size()) == 0; +} + +inline bool operator !=(const CBUS::Message& lhs, const CBUS::Message& rhs) +{ + return lhs.size() != rhs.size() || std::memcmp(&lhs, &rhs, lhs.size()) != 0; +} + +#endif diff --git a/server/src/hardware/protocol/cbus/opcode.hpp b/server/src/hardware/protocol/cbus/opcode.hpp new file mode 100644 index 00000000..9e8dbff5 --- /dev/null +++ b/server/src/hardware/protocol/cbus/opcode.hpp @@ -0,0 +1,93 @@ +/** + * server/src/hardware/protocol/cbus/opcode.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2023 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_OPCODE_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_OPCODE_HPP + +#include + +namespace CBUS { + +enum class OpCode : uint8_t +{ + Heartbeat = 0x00, + GetInputState = 0x12, + SetInputState = 0x13, + GetOutputState = 0x22, + SetOutputState = 0x23, + ThrottleSubUnsub = 0x34, + ThrottleSetFunction = 0x35, + ThrottleSetSpeedDirection = 0x37, + GetFeatures = 0xE0, + Features = 0xE4, + GetInfo = 0xF0, + Info = 0xFF, +}; + +} + +constexpr std::string_view toString(CBUS::OpCode value) +{ + using OpCode = CBUS::OpCode; + + switch(value) + { + case OpCode::Heartbeat: + return "Heartbeat"; + + case OpCode::GetInputState: + return "GetInputState"; + + case OpCode::SetInputState: + return "SetInputState"; + + case OpCode::GetOutputState: + return "GetOutputState"; + + case OpCode::SetOutputState: + return "SetOutputState"; + + case OpCode::ThrottleSubUnsub: + return "ThrottleSubUnsub"; + + case OpCode::ThrottleSetFunction: + return "ThrottleSetFunction"; + + case OpCode::ThrottleSetSpeedDirection: + return "ThrottleSetSpeedDirection"; + + case OpCode::GetFeatures: + return "GetFeatures"; + + case OpCode::Features: + return "Features"; + + case OpCode::GetInfo: + return "GetInfo"; + + case OpCode::Info: + return "Info"; + } + return {}; +} + +#endif diff --git a/server/src/hardware/protocol/cbus/outputstate.hpp b/server/src/hardware/protocol/cbus/outputstate.hpp new file mode 100644 index 00000000..380583d2 --- /dev/null +++ b/server/src/hardware/protocol/cbus/outputstate.hpp @@ -0,0 +1,62 @@ +/** + * server/src/hardware/protocol/cbus/outputstate.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 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_OUTPUTSTATE_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_OUTPUTSTATE_HPP + +#include +#include + +namespace CBUS { + +enum class OutputState : uint8_t +{ + Undefined = 0, + False = 1, + True = 2, + Invalid = 3, +}; + +} + +constexpr std::string_view toString(CBUS::OutputState value) +{ + using OutputState = CBUS::OutputState; + + switch(value) + { + case OutputState::Undefined: + return "Undefined"; + + case OutputState::False: + return "False"; + + case OutputState::True: + return "True"; + + case OutputState::Invalid: + return "Invalid"; + } + return {}; +} + +#endif diff --git a/server/src/hardware/protocol/cbus/settings.cpp b/server/src/hardware/protocol/cbus/settings.cpp new file mode 100644 index 00000000..f0e63ae6 --- /dev/null +++ b/server/src/hardware/protocol/cbus/settings.cpp @@ -0,0 +1,61 @@ +/** + * server/src/hardware/protocol/cbus/settings.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2023 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 "settings.hpp" +#include "../../../core/attributes.hpp" +#include "../../../utils/displayname.hpp" + +namespace CBUS { + +Settings::Settings(Object& _parent, std::string_view parentPropertyName) + : SubObject(_parent, parentPropertyName) + , startupDelay{this, "startup_delay", startupDelayDefault, PropertyFlags::ReadWrite | PropertyFlags::Store} + , heartbeatTimeout{this, "heartbeat_timeout", heartbeatTimeoutDefault, PropertyFlags::ReadWrite | PropertyFlags::Store} + , debugLogRXTX{this, "debug_log_rx_tx", false, PropertyFlags::ReadWrite | PropertyFlags::Store} + , debugLogHeartbeat{this, "debug_log_heartbeat", false, PropertyFlags::ReadWrite | PropertyFlags::Store} +{ + Attributes::addMinMax(startupDelay, startupDelayMin, startupDelayMax); + m_interfaceItems.add(startupDelay); + + Attributes::addMinMax(heartbeatTimeout, heartbeatTimeoutMin, heartbeatTimeoutMax); + m_interfaceItems.add(heartbeatTimeout); + + Attributes::addDisplayName(debugLogRXTX, DisplayName::Hardware::debugLogRXTX); + m_interfaceItems.add(debugLogRXTX); + + m_interfaceItems.add(debugLogHeartbeat); +} + +Config Settings::config() const +{ + Config config; + + config.startupDelay = std::chrono::milliseconds(startupDelay); + config.heartbeatTimeout = std::chrono::milliseconds(heartbeatTimeout); + + config.debugLogRXTX = debugLogRXTX; + config.debugLogHeartbeat = debugLogHeartbeat; + + return config; +} + +} diff --git a/server/src/hardware/protocol/cbus/settings.hpp b/server/src/hardware/protocol/cbus/settings.hpp new file mode 100644 index 00000000..26213005 --- /dev/null +++ b/server/src/hardware/protocol/cbus/settings.hpp @@ -0,0 +1,57 @@ +/** + * server/src/hardware/protocol/cbus/settings.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022-2023 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_SETTINGS_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_CBUS_SETTINGS_HPP + +#include "../../../core/subobject.hpp" +#include "../../../core/property.hpp" +#include "config.hpp" + +namespace CBUS { + +class Settings final : public SubObject +{ + CLASS_ID("cbus_settings") + + private: + static constexpr uint16_t startupDelayMin = 0; + static constexpr uint16_t startupDelayDefault = 500; + static constexpr uint16_t startupDelayMax = 60'000; + static constexpr uint16_t heartbeatTimeoutMin = 100; + static constexpr uint16_t heartbeatTimeoutDefault = 1'000; + static constexpr uint16_t heartbeatTimeoutMax = 60'000; + + public: + Property startupDelay; + Property heartbeatTimeout; + Property debugLogRXTX; + Property debugLogHeartbeat; + + Settings(Object& _parent, std::string_view parentPropertyName); + + Config config() const; +}; + +} + +#endif diff --git a/server/src/utils/displayname.hpp b/server/src/utils/displayname.hpp index 5ca56f58..bc7ee948 100644 --- a/server/src/utils/displayname.hpp +++ b/server/src/utils/displayname.hpp @@ -65,6 +65,7 @@ namespace DisplayName constexpr std::string_view throttles = "hardware:throttles"; constexpr std::string_view xpressnet = "hardware:xpressnet"; constexpr std::string_view z21 = "hardware:z21"; + constexpr std::string_view cbus = "hardware:cbus"; } namespace Interface { diff --git a/server/src/world/world.cpp b/server/src/world/world.cpp index 06e855a3..b2b1b90a 100644 --- a/server/src/world/world.cpp +++ b/server/src/world/world.cpp @@ -126,7 +126,6 @@ void World::init(World& world) world.identificationControllers.setValueInternal(std::make_shared>(world, world.identificationControllers.name())); world.lncvProgrammingControllers.setValueInternal(std::make_shared>(world, world.lncvProgrammingControllers.name())); world.loconetInterfaces.setValueInternal(std::make_shared>(world, world.loconetInterfaces.name())); - world.interfaces.setValueInternal(std::make_shared(world, world.interfaces.name())); world.decoders.setValueInternal(std::make_shared(world, world.decoders.name(), decoderListColumns)); world.inputs.setValueInternal(std::make_shared(world, world.inputs.name(), inputListColumns)); diff --git a/server/vcpkg-configuration.json b/server/vcpkg-configuration.json deleted file mode 100644 index d43be883..00000000 --- a/server/vcpkg-configuration.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "default-registry": { - "kind": "git", - "baseline": "b322364f06308bdd24823f9d8f03fe0cc86fd46f", - "repository": "https://github.com/microsoft/vcpkg" - }, - "registries": [ - { - "kind": "artifact", - "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", - "name": "microsoft" - } - ] -} diff --git a/server/vcpkg.json b/server/vcpkg.json deleted file mode 100644 index 06d7c8f3..00000000 --- a/server/vcpkg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "dependencies": [ - "boost-asio", - "boost-beast", - "boost-program-options", - "boost-signals2", - "boost-uuid", - "boost-url", - { - "name": "libarchive", - "default-features": false, - "features": ["lzma"] - }, - "lua", - "zlib" - ] -} diff --git a/shared/src/traintastic/enum/cbusinterfacetype.hpp b/shared/src/traintastic/enum/cbusinterfacetype.hpp new file mode 100644 index 00000000..08f81b93 --- /dev/null +++ b/shared/src/traintastic/enum/cbusinterfacetype.hpp @@ -0,0 +1,43 @@ +/** + * shared/src/traintastic/enum/cbusinterfacetype.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2021-2022 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 "enum.hpp" + +enum class CBUSInterfaceType : uint8_t +{ + Serial = 0, + NetworkTCP = 1, +}; + +TRAINTASTIC_ENUM(CBUSInterfaceType, "cbus_interface_type", 2, +{ + {CBUSInterfaceType::Serial, "serial"}, + {CBUSInterfaceType::NetworkTCP, "network_tcp"}, +}); + +#endif + + diff --git a/shared/translations/de-de.json b/shared/translations/de-de.json index d3a47cc1..0ee55573 100644 --- a/shared/translations/de-de.json +++ b/shared/translations/de-de.json @@ -757,7 +757,7 @@ }, { "term": "hardware:throttles", - "definition": "Drosseln" + "definition": "Handregler" }, { "term": "hsi88:s88_left", @@ -823,6 +823,10 @@ "term": "interface.loconet:interface", "definition": "Schnittstelle" }, + { + "term": "interface.cbus:interface", + "definition": "Schnittstelle" + }, { "term": "interface.marklin_can:marklin_can_locomotive_list", "definition": "M\u00e4rklin CAN: Lokomotivenliste" @@ -873,7 +877,7 @@ }, { "term": "interface_list:list_is_empty", - "definition": "Schnittstelle ist nicht leer.\nDr\u00fccke dem Plus-Knopf um eine \nneue Schnittstelle zu erstellen." + "definition": "Keine Schnittstelle definiert.\nDr\u00fccke dem Plus-Knopf um eine \nneue Schnittstelle zu erstellen." }, { "term": "interface_state:error", @@ -2717,7 +2721,7 @@ }, { "term": "tray_icon.language_changed_message_box:text", - "definition": "Tritastic server muss neu gestartet werden, damit die Sprach\u00e4nderung wirksam wird. Jetzt Neustarten?" + "definition": "Traitastic server muss neu gestartet werden, damit die Sprach\u00e4nderung wirksam wird. Jetzt Neustarten?" }, { "term": "tray_icon.menu:advanced", @@ -3299,4 +3303,4 @@ "term": "zone:speed_limit", "definition": "Geschwindigkeitbegrenzung" } -] \ No newline at end of file +]