diff --git a/client/src/utils/enum.cpp b/client/src/utils/enum.cpp index c1d28bcc..ff021867 100644 --- a/client/src/utils/enum.cpp +++ b/client/src/utils/enum.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -86,6 +87,7 @@ QString translateEnum(const QString& enumName, qint64 value) TRANSLATE_ENUM(SerialFlowControl) TRANSLATE_ENUM(SignalAspect) TRANSLATE_ENUM(SpeedUnit) + TRANSLATE_ENUM(TraintasticDIYInterfaceType) TRANSLATE_ENUM(TurnoutPosition) TRANSLATE_ENUM(WeightUnit) TRANSLATE_ENUM(WorldScale) diff --git a/manual/traintasticmanual/en-us/messages/info.md b/manual/traintasticmanual/en-us/messages/info.md index 3dce2f48..19dd9ccb 100644 --- a/manual/traintasticmanual/en-us/messages/info.md +++ b/manual/traintasticmanual/en-us/messages/info.md @@ -42,6 +42,10 @@ TODO Information about the connected HSI-88 interface, e.g. *Ver. 0.62 / 08.07.02 / HSI-88 / (c) LDT*. + +## I2005: *info* {#i2005} +Information about the connected Traintastic DIY device. + ## I9001: Stopped script {#i9001} TODO diff --git a/manual/traintasticmanual/en-us/messages/warning.md b/manual/traintasticmanual/en-us/messages/warning.md index 79df75e9..ed6188ef 100644 --- a/manual/traintasticmanual/en-us/messages/warning.md +++ b/manual/traintasticmanual/en-us/messages/warning.md @@ -33,6 +33,12 @@ If not, remapping decoder functions or using a different command station is the **Solution:** The number of speed steps that can be used is determinded by the command station or interface. Changing the decoders speed steps to *Auto* should usally work. +## W2004: Input address *address* is invalid {#w2004} +The connected Traintastic DIY device does not have an input with *address*. + +## W2005: Output address *address* is invalid {#w2005} +The connected Traintastic DIY device does not have an output with *address*. + ## W9999: *message* {#w9999} Custom warning message generated by a [Lua script](../lua.md). diff --git a/manual/traintasticmanual/en-us/traintasticdiyprotocol.md b/manual/traintasticmanual/en-us/traintasticdiyprotocol.md new file mode 100644 index 00000000..d5b62044 --- /dev/null +++ b/manual/traintasticmanual/en-us/traintasticdiyprotocol.md @@ -0,0 +1,203 @@ +# Traintastic DIY protocol {#tdiyp} + +The Traintastic DIY protocol is designed to make it possible to develop custom hardware, e.g. by using the Arduino platform and use it with Traintastic. + +The Traintastic DIY protocol is currently supported via: +- Serial port: baudrate and flow control can be chosen, data format is fixed at 8N1 (8 data byte, no parity, one stop bit) +- Network connection (TCP): port number can be chosen. + +It is currently limited to: +- Reading inputs +- Controlling outputs + +Other features might be added in the future. + +## Message format {#tdiyp-message-format} + +Each Traintastic DIY protocol message starts with an opcode byte, besides the message type it also contains the data payload length in the lowest nibble. +If the lowest nibble is `0xF` the the second byte of the message determines the payload length. +The message always ends with a checksum byte, the checksum is the result of XOR-ing of all message bytes. + +Examples: +``` +0x50 0x50 +``` +The lowest nibble of the first byte is `0` indicating a zero byte payload. +The checksum is identical to the opcode, there is no data to XOR with. + +``` +0x24 0x11 0x22 0x33 0x44 0x60 +``` +The lowest nibble of the first byte is `4` indicating a 4 byte payload. +The checksum is `0x24` XOR `0x11` XOR `0x22` XOR `0x33` XOR `0x44` = `0x60`. + +``` +0x2F 0x20 ... 0x?? +``` +The lowest nibble of the first byte is `F` indicating that the second byte must be used as payload length, 32 byte. +The checksum is `0x2F` XOR `0x20` XOR *all payload bytes*. + + +## Messages {#tdiyp-messages} + +Messages are send by Traintastic to the DIY device, for every message the DIY device sends a response message. +Some messages are sent unsolicited by the DIY device to Traintastic if changes are detected by the DIY device. + +| Command | | +|---------------------------------------------|-----------------------------------------| +| [Heartbeat](#tdiyp-heartbeat) | Mandatory | +| [Get information](#tdiyp-get-information) | Mandatory | +| [Get features](#tdiyp-get-features) | Mandatory | +| [Get input state](#tdiyp-get-input-state) | Mandatory if input feature flag is set | +| [Set input state](#tdiyp-set-input-state) | Mandatory if input feature flag is set | +| [Get output state](#tdiyp-get-output-state) | Mandatory if output feature flag is set | +| [Set output state](#tdiyp-set-output-state) | Mandatory if output feature flag is set | + +**Badges**: +- The $badge:since:v0.2$ badge indicates in which version of Traintastic the message is added. + + +### Heartbeat $badge:since:v0.2$ {#tdiyp-heartbeat} + +The heartbeat message is sent by Traintastic to check if the DIY device is (still) present, the DIY device responds with a heartbeat message. +The heartbeat rate can be configured in Traintastic, by default the heartbeat message is one second after the last message is received from the DIY device. + +#### Request message +``` +0x00 +``` + +#### Response message +``` +0x00 +``` + + +### Get information $badge:since:v0.2$ {#tdiyp-get-information} + +The *get information* message is the first message sent after connecting. +The DIY device responds with an *information* message containing a description of the connected DIY device. +This is pure informational and displayed in the message console. + +#### Request message +``` +0xF0 +``` + +#### Response message +``` +0xFF +``` + +### Get features $badge:since:v0.2$ {#tdiyp-get-features} + +The *get features* message is the second message sent by Traintastic after connecting. +The DIY device responds with a *features* message containing flags which indicate what is supported by the DIY device. + +#### Request message +``` +0xE0 +``` + +#### Response message +``` +0xE4 +``` +- `` feature flags 1, OR-ed value of: + - `0x01` input feature flag: set if the DIY device has inputs $badge:since:v0.2$ + - `0x02` output feature flag: set if the DIY device has outputs $badge:since:v0.2$ + - `0x04`...`0x80` are reserved, do not use +- `` feature flags 2, reserved must be `0x00` +- `` feature flags 3, reserved must be `0x00` +- `` feature flags 4, reserved must be `0x00` + + +### Get input state $badge:since:v0.2$ {#tdiyp-get-input-state} + +Sent by Traintastic to retrieve the current input state. +Address zero has a special meaning, it is used as broadcast address to retrieve the current state of all inputs. + +#### Request message +``` +0x12 +``` + +- `` high byte of 16bit input address +- `` low byte of 16bit input address + +#### Response +If the address is non zero the DIY device responds with a *[set input state](#tdiyp-set-input-state)* message containing the current state of the input address. + +If the address is zero the DIY device responds with multiple *[set input state](#tdiyp-set-input-state)* messages, one for each know input address or +send a single *[set input state](#tdiyp-set-input-state)* message with address zero and state *invalid* to inform Traintastic that the address zero request is not supported. + + +### Set input state $badge:since:v0.2$ {#tdiyp-set-input-state} + +Sent by the DIY device as response to the *[get input state](#tdiyp-get-input-state)* message and must be sent by the DIY device whenever an input state changes. + +#### Message +``` +0x13 +``` + +- `` high byte of 16bit input address +- `` low byte of 16bit input address +- `` input state: + - `0x00` if input state is unknown + - `0x01` if input state is low/false + - `0x02` if input state is high/true + - `0x03` if input is invalid (only as response to a *[get input state](#tdiyp-get-input-state)* message) + - `0x04`...`0xFF` are reserved, do not use + +Examples: +``` +0x13 0x00 0x12 0x02 0x03 +``` +Input 18 state changed to high/true + +``` +0x13 0x02 0xA2 0x01 0xB2 +``` +Input 674 state changed to low/false + + +### Get output state $badge:since:v0.2$ {#tdiyp-get-output-state} + +Sent by Traintastic to retrieve the current output state. +Address zero has a special meaning, it is used as broadcast address to retrieve the current state of all outputs. + +#### Request message +``` +0x22 +``` + +- `` high byte of 16bit output address +- `` low byte of 16bit output address + +#### Response message +If the address is non zero the DIY device responds with a *[set output state](#tdiyp-set-output-state)* message containing the current state of the output address. + +If the address is zero the DIY device responds with multiple *[set inpoutputut state](#tdiyp-set-output-state)* messages, one for each know output address or +send a single *[set output state](#tdiyp-set-output-state)* message with address zero and state *invalid* to inform Traintastic that the address zero request is not supported. + + +### Output state changed $badge:since:v0.2$ {#tdiyp-output-state-change} + +Sent by Traintastic to change the state of an output, the DIY device responds with a *get output state* message containing the new output state, +if for some reason the output state cannot be the current state must be send. +Sent by the DIY device as response to the *[get output state](#tdiyp-get-output-state)* message and must be sent by the DIY device whenever an output state changes. + +#### Message +``` +0x23 +``` + +- `` high byte of 16bit output address +- `` low byte of 16bit output address +- `` output state: + - `0x00` if output state is unknown + - `0x01` if output state is low/false + - `0x02` if output state is high/true + - `0x03` if output is invalid (only as response to a *[get output state](#tdiyp-get-output-state)* message) + - `0x04`...`0xFF` are reserved, do not use diff --git a/manual/traintasticmanual/traintasticmanual.json b/manual/traintasticmanual/traintasticmanual.json index 884b41d6..4974a4b5 100644 --- a/manual/traintasticmanual/traintasticmanual.json +++ b/manual/traintasticmanual/traintasticmanual.json @@ -126,6 +126,10 @@ "type": "appendix", "markdown": "xpressnet.md" }, + { + "type": "appendix", + "markdown": "traintasticdiyprotocol.md" + }, { "type": "appendix", "markdown": "messages.md", diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 8c84203a..c50bf90e 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -119,6 +119,10 @@ file(GLOB SOURCES "src/hardware/protocol/loconet/*.cpp" "src/hardware/protocol/loconet/iohandler/*.hpp" "src/hardware/protocol/loconet/iohandler/*.cpp" + "src/hardware/protocol/traintasticdiy/*.hpp" + "src/hardware/protocol/traintasticdiy/*.cpp" + "src/hardware/protocol/traintasticdiy/iohandler/*.hpp" + "src/hardware/protocol/traintasticdiy/iohandler/*.cpp" "src/hardware/protocol/xpressnet/*.hpp" "src/hardware/protocol/xpressnet/*.cpp" "src/hardware/protocol/xpressnet/iohandler/*.hpp" diff --git a/server/src/enum/traintasticdiyinterfacetype.hpp b/server/src/enum/traintasticdiyinterfacetype.hpp new file mode 100644 index 00000000..414963d4 --- /dev/null +++ b/server/src/enum/traintasticdiyinterfacetype.hpp @@ -0,0 +1,34 @@ +/** + * server/src/enum/traintasticdiyinterfacetype.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_TRAINTASTICDIYINTERFACETYPE_HPP +#define TRAINTASTIC_SERVER_ENUM_TRAINTASTICDIYINTERFACETYPE_HPP + +#include +#include + +inline constexpr std::array traintasticDIYInterfaceTypeValues{{ + TraintasticDIYInterfaceType::Serial, + TraintasticDIYInterfaceType::NetworkTCP, +}}; + +#endif diff --git a/server/src/hardware/interface/interfaces.cpp b/server/src/hardware/interface/interfaces.cpp index 6e4f9ea7..b227e632 100644 --- a/server/src/hardware/interface/interfaces.cpp +++ b/server/src/hardware/interface/interfaces.cpp @@ -30,6 +30,7 @@ std::shared_ptr Interfaces::create(World& world, std::string_view cla IF_CLASSID_CREATE(ECoSInterface) IF_CLASSID_CREATE(HSI88Interface) IF_CLASSID_CREATE(LocoNetInterface) + IF_CLASSID_CREATE(TraintasticDIYInterface) IF_CLASSID_CREATE(WlanMausInterface) IF_CLASSID_CREATE(XpressNetInterface) IF_CLASSID_CREATE(Z21Interface) diff --git a/server/src/hardware/interface/interfaces.hpp b/server/src/hardware/interface/interfaces.hpp index 3d989eca..a8ab4591 100644 --- a/server/src/hardware/interface/interfaces.hpp +++ b/server/src/hardware/interface/interfaces.hpp @@ -30,6 +30,7 @@ #include "ecosinterface.hpp" #include "hsi88.hpp" #include "loconetinterface.hpp" +#include "traintasticdiyinterface.hpp" #include "wlanmausinterface.hpp" #include "xpressnetinterface.hpp" #include "z21interface.hpp" @@ -43,6 +44,7 @@ struct Interfaces ECoSInterface::classId, HSI88Interface::classId, LocoNetInterface::classId, + TraintasticDIYInterface::classId, WlanMausInterface::classId, XpressNetInterface::classId, Z21Interface::classId diff --git a/server/src/hardware/interface/traintasticdiyinterface.cpp b/server/src/hardware/interface/traintasticdiyinterface.cpp new file mode 100644 index 00000000..9bd3b3cb --- /dev/null +++ b/server/src/hardware/interface/traintasticdiyinterface.cpp @@ -0,0 +1,273 @@ +/** + * server/src/hardware/interface/traintasticdiyinterface.cpp + * + * 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. + */ + +#include "traintasticdiyinterface.hpp" +#include "../input/list/inputlisttablemodel.hpp" +#include "../output/list/outputlisttablemodel.hpp" +#include "../protocol/traintasticdiy/messages.hpp" +#include "../protocol/traintasticdiy/iohandler/serialiohandler.hpp" +#include "../protocol/traintasticdiy/iohandler/simulationiohandler.hpp" +#include "../protocol/traintasticdiy/iohandler/tcpiohandler.hpp" +#include "../../core/attributes.hpp" +#include "../../log/log.hpp" +#include "../../log/logmessageexception.hpp" +#include "../../utils/displayname.hpp" +#include "../../utils/inrange.hpp" +#include "../../world/world.hpp" + +constexpr auto inputListColumns = InputListColumn::Id | InputListColumn::Name | InputListColumn::Address; +constexpr auto outputListColumns = OutputListColumn::Id | OutputListColumn::Name | OutputListColumn::Address; + +TraintasticDIYInterface::TraintasticDIYInterface(World& world, std::string_view _id) + : Interface(world, _id) + , type{this, "type", TraintasticDIYInterfaceType::Serial, PropertyFlags::ReadWrite | PropertyFlags::Store, + [this](TraintasticDIYInterfaceType /*value*/) + { + updateVisible(); + }} + , device{this, "device", "", PropertyFlags::ReadWrite | PropertyFlags::Store} + , baudrate{this, "baudrate", 19200, PropertyFlags::ReadWrite | PropertyFlags::Store} + , flowControl{this, "flow_control", SerialFlowControl::None, PropertyFlags::ReadWrite | PropertyFlags::Store} + , hostname{this, "hostname", "192.168.1.203", PropertyFlags::ReadWrite | PropertyFlags::Store} + , port{this, "port", 5550, PropertyFlags::ReadWrite | PropertyFlags::Store} + , traintasticDIY{this, "traintastic_diy", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject} + , inputs{this, "inputs", nullptr, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::SubObject} + , outputs{this, "outputs", nullptr, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::SubObject} +{ + name = "Traintastic DIY"; + traintasticDIY.setValueInternal(std::make_shared(*this, traintasticDIY.name())); + inputs.setValueInternal(std::make_shared(*this, inputs.name(), inputListColumns)); + outputs.setValueInternal(std::make_shared(*this, outputs.name(), outputListColumns)); + + Attributes::addDisplayName(type, DisplayName::Interface::type); + Attributes::addEnabled(type, !online); + Attributes::addValues(type, traintasticDIYInterfaceTypeValues); + m_interfaceItems.insertBefore(type, notes); + + Attributes::addDisplayName(device, DisplayName::Serial::device); + 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); + m_interfaceItems.insertBefore(baudrate, notes); + + 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); + + m_interfaceItems.insertBefore(traintasticDIY, notes); + + Attributes::addDisplayName(inputs, DisplayName::Hardware::inputs); + m_interfaceItems.insertBefore(inputs, notes); + + Attributes::addDisplayName(outputs, DisplayName::Hardware::outputs); + m_interfaceItems.insertBefore(outputs, notes); + + updateVisible(); +} + +bool TraintasticDIYInterface::addInput(Input& input) +{ + const bool success = InputController::addInput(input); + if(success) + inputs->addObject(input.shared_ptr()); + return success; +} + +bool TraintasticDIYInterface::removeInput(Input& input) +{ + const bool success = InputController::removeInput(input); + if(success) + inputs->removeObject(input.shared_ptr()); + return success; +} + +void TraintasticDIYInterface::inputSimulateChange(uint32_t channel, uint32_t address) +{ + if(m_kernel && inRange(address, outputAddressMinMax(channel))) + m_kernel->simulateInputChange(address); +} + +bool TraintasticDIYInterface::addOutput(Output& output) +{ + const bool success = OutputController::addOutput(output); + if(success) + outputs->addObject(output.shared_ptr()); + return success; +} + +bool TraintasticDIYInterface::removeOutput(Output& output) +{ + const bool success = OutputController::removeOutput(output); + if(success) + outputs->removeObject(output.shared_ptr()); + return success; +} + +bool TraintasticDIYInterface::setOutputValue(uint32_t channel, uint32_t address, bool value) +{ + assert(isOutputChannel(channel)); + return + m_kernel && + inRange(address, outputAddressMinMax(channel)) && + m_kernel->setOutput(static_cast(address), value); +} + +bool TraintasticDIYInterface::setOnline(bool& value, bool simulation) +{ + if(!m_kernel && value) + { + try + { + if(simulation) + { + m_kernel = TraintasticDIY::Kernel::create(traintasticDIY->config()); + } + else + { + switch(type) + { + case TraintasticDIYInterfaceType::Serial: + m_kernel = TraintasticDIY::Kernel::create(traintasticDIY->config(), device.value(), baudrate.value(), flowControl.value()); + break; + + case TraintasticDIYInterfaceType::NetworkTCP: + m_kernel = TraintasticDIY::Kernel::create(traintasticDIY->config(), hostname.value(), port.value()); + break; + } + } + + if(!m_kernel) + { + assert(false); + return false; + } + + status.setValueInternal(InterfaceStatus::Initializing); + + m_kernel->setLogId(id.value()); + m_kernel->setOnStarted( + [this]() + { + status.setValueInternal(InterfaceStatus::Online); + }); + + m_kernel->setInputController(this); + m_kernel->setOutputController(this); + m_kernel->start(); + + m_traintasticDIYPropertyChanged = traintasticDIY->propertyChanged.connect( + [this](BaseProperty& /*property*/) + { + m_kernel->setConfig(traintasticDIY->config()); + }); + + Attributes::setEnabled({type, device, baudrate, flowControl, hostname, port}, false); + } + catch(const LogMessageException& e) + { + status.setValueInternal(InterfaceStatus::Offline); + Log::log(*this, e.message(), e.args()); + return false; + } + } + else if(m_kernel && !value) + { + Attributes::setEnabled({type, device, baudrate, flowControl, hostname, port}, true); + + m_traintasticDIYPropertyChanged.disconnect(); + + m_kernel->stop(); + m_kernel.reset(); + + status.setValueInternal(InterfaceStatus::Offline); + } + return true; +} + +void TraintasticDIYInterface::addToWorld() +{ + Interface::addToWorld(); + + m_world.inputControllers->add(std::dynamic_pointer_cast(shared_from_this())); + m_world.outputControllers->add(std::dynamic_pointer_cast(shared_from_this())); +} + +void TraintasticDIYInterface::loaded() +{ + Interface::loaded(); + + updateVisible(); +} + +void TraintasticDIYInterface::destroying() +{ + for(const auto& input : *inputs) + { + assert(input->interface.value() == std::dynamic_pointer_cast(shared_from_this())); + input->interface = nullptr; + } + + for(const auto& output : *outputs) + { + assert(output->interface.value() == std::dynamic_pointer_cast(shared_from_this())); + output->interface = nullptr; + } + + m_world.inputControllers->remove(std::dynamic_pointer_cast(shared_from_this())); + m_world.outputControllers->remove(std::dynamic_pointer_cast(shared_from_this())); + + Interface::destroying(); +} + +void TraintasticDIYInterface::idChanged(const std::string& newId) +{ + if(m_kernel) + m_kernel->setLogId(newId); +} + +void TraintasticDIYInterface::updateVisible() +{ + const bool isSerial = (type == TraintasticDIYInterfaceType::Serial); + Attributes::setVisible(device, isSerial); + Attributes::setVisible(baudrate, isSerial); + Attributes::setVisible(flowControl, isSerial); + + const bool isNetwork = (type == TraintasticDIYInterfaceType::NetworkTCP); + Attributes::setVisible(hostname, isNetwork); + Attributes::setVisible(port, isNetwork); +} diff --git a/server/src/hardware/interface/traintasticdiyinterface.hpp b/server/src/hardware/interface/traintasticdiyinterface.hpp new file mode 100644 index 00000000..968f68f2 --- /dev/null +++ b/server/src/hardware/interface/traintasticdiyinterface.hpp @@ -0,0 +1,90 @@ +/** + * server/src/hardware/interface/traintasticdiyinterface.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_INTERFACE_TRAINTASTICDIYINTERFACE_HPP +#define TRAINTASTIC_SERVER_HARDWARE_INTERFACE_TRAINTASTICDIYINTERFACE_HPP + +#include "interface.hpp" +#include "../protocol/traintasticdiy/kernel.hpp" +#include "../protocol/traintasticdiy/settings.hpp" +#include "../input/inputcontroller.hpp" +#include "../input/list/inputlist.hpp" +#include "../output/outputcontroller.hpp" +#include "../output/list/outputlist.hpp" +#include "../../core/objectproperty.hpp" +#include "../../enum/traintasticdiyinterfacetype.hpp" +#include "../../enum/serialflowcontrol.hpp" + +/** + * \brief Traintastic DIY hardware interface + */ +class TraintasticDIYInterface final + : public Interface + , public InputController + , public OutputController +{ + CLASS_ID("interface.traintastic_diy") + DEFAULT_ID("traintastic_diy") + CREATE(TraintasticDIYInterface) + + private: + std::unique_ptr m_kernel; + boost::signals2::connection m_traintasticDIYPropertyChanged; + + void addToWorld() final; + void loaded() final; + void destroying() final; + + void idChanged(const std::string& newId) final; + + void updateVisible(); + + protected: + bool setOnline(bool& value, bool simulation) final; + + public: + Property type; + Property device; + Property baudrate; + Property flowControl; + Property hostname; + Property port; + ObjectProperty traintasticDIY; + ObjectProperty inputs; + ObjectProperty outputs; + + TraintasticDIYInterface(World& world, std::string_view _id); + + // InputController: + std::pair inputAddressMinMax(uint32_t /*channel*/) const final { return {TraintasticDIY::Kernel::ioAddressMin, TraintasticDIY::Kernel::ioAddressMax}; } + [[nodiscard]] bool addInput(Input& input) final; + [[nodiscard]] bool removeInput(Input& input) final; + void inputSimulateChange(uint32_t channel, uint32_t address) final; + + // OutputController: + std::pair outputAddressMinMax(uint32_t /*channel*/) const final { return {TraintasticDIY::Kernel::ioAddressMin, TraintasticDIY::Kernel::ioAddressMax}; } + [[nodiscard]] bool addOutput(Output& output) final; + [[nodiscard]] bool removeOutput(Output& output) final; + [[nodiscard]] bool setOutputValue(uint32_t channel, uint32_t address, bool value) final; +}; + +#endif diff --git a/server/src/hardware/protocol/traintasticdiy/config.hpp b/server/src/hardware/protocol/traintasticdiy/config.hpp new file mode 100644 index 00000000..b56d37ae --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/config.hpp @@ -0,0 +1,40 @@ +/** + * server/src/hardware/protocol/traintasticdiy/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_TRAINTASTICDIY_CONFIG_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_CONFIG_HPP + +#include + +namespace TraintasticDIY { + +struct Config +{ + std::chrono::milliseconds heartbeatTimeout; + + bool debugLogRXTX; + bool debugLogHeartbeat; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/traintasticdiy/featureflags.hpp b/server/src/hardware/protocol/traintasticdiy/featureflags.hpp new file mode 100644 index 00000000..d84cf953 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/featureflags.hpp @@ -0,0 +1,84 @@ +/** + * server/src/hardware/protocol/traintasticdiy/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_TRAINTASTICDIY_FEATUREFLAGS_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_FEATUREFLAGS_HPP + +#include + +namespace TraintasticDIY { + +enum class FeatureFlags1 : uint8_t +{ + None = 0x00, + Input = 0x01, + Output = 0x02, +}; + +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/traintasticdiy/inputstate.hpp b/server/src/hardware/protocol/traintasticdiy/inputstate.hpp new file mode 100644 index 00000000..497c3131 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/inputstate.hpp @@ -0,0 +1,62 @@ +/** + * server/src/hardware/protocol/traintasticdiy/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 TraintasticDIY { + +enum class InputState : uint8_t +{ + Undefined = 0, + False = 1, + True = 2, + Invalid = 3, +}; + +} + +constexpr std::string_view toString(TraintasticDIY::InputState value) +{ + using InputState = TraintasticDIY::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/traintasticdiy/iohandler/hardwareiohandler.cpp b/server/src/hardware/protocol/traintasticdiy/iohandler/hardwareiohandler.cpp new file mode 100644 index 00000000..88370e83 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/hardwareiohandler.cpp @@ -0,0 +1,100 @@ +/** + * server/src/hardware/protocol/traintasticdiy/iohandler/hardwareiohandler.cpp + * + * 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. + */ + +#include "hardwareiohandler.hpp" +#include "../kernel.hpp" +#include "../messages.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" + +namespace TraintasticDIY { + +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]() + { + Log::log(m_kernel.logId(), LogMessage::W2001_RECEIVED_MALFORMED_DATA_DROPPED_X_BYTES, drop); + }); + } + + 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/traintasticdiy/iohandler/hardwareiohandler.hpp b/server/src/hardware/protocol/traintasticdiy/iohandler/hardwareiohandler.hpp new file mode 100644 index 00000000..4b12599f --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/hardwareiohandler.hpp @@ -0,0 +1,54 @@ +/** + * server/src/hardware/protocol/traintasticdiy/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_TRAINTASTICDIY_IOHANDLER_HARDWAREIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_IOHANDLER_HARDWAREIOHANDLER_HPP + +#include +#include +#include "iohandler.hpp" + +namespace TraintasticDIY { + +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/traintasticdiy/iohandler/iohandler.hpp b/server/src/hardware/protocol/traintasticdiy/iohandler/iohandler.hpp new file mode 100644 index 00000000..04531756 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/iohandler.hpp @@ -0,0 +1,61 @@ +/** + * server/src/hardware/protocol/traintasticdiy/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_TRAINTASTICDIY_IOHANDLER_IOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_IOHANDLER_IOHANDLER_HPP + +namespace TraintasticDIY { + +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/traintasticdiy/iohandler/serialiohandler.cpp b/server/src/hardware/protocol/traintasticdiy/iohandler/serialiohandler.cpp new file mode 100644 index 00000000..e6d427bf --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/serialiohandler.cpp @@ -0,0 +1,105 @@ +/** + * server/src/hardware/protocol/traintasticdiy/iohandler/serialiohandler.cpp + * + * 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. + */ + +#include "serialiohandler.hpp" +#include "../kernel.hpp" +#include "../messages.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" +#include "../../../../utils/serialport.hpp" + +namespace TraintasticDIY { + +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()) + m_serialPort.close(); +} + +void SerialIOHandler::start() +{ + read(); +} + +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); + // TODO interface status -> 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); + // TODO interface status -> error + }); + } + }); +} + +} diff --git a/server/src/hardware/protocol/traintasticdiy/iohandler/serialiohandler.hpp b/server/src/hardware/protocol/traintasticdiy/iohandler/serialiohandler.hpp new file mode 100644 index 00000000..ba6a03d6 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/serialiohandler.hpp @@ -0,0 +1,50 @@ +/** + * server/src/hardware/protocol/traintasticdiy/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_TRAINTASTICDIY_IOHANDLER_SERIALIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_IOHANDLER_SERIALIOHANDLER_HPP + +#include "hardwareiohandler.hpp" +#include +#include "../../../../enum/serialflowcontrol.hpp" + +namespace TraintasticDIY { + +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/traintasticdiy/iohandler/simulationiohandler.cpp b/server/src/hardware/protocol/traintasticdiy/iohandler/simulationiohandler.cpp new file mode 100644 index 00000000..2a9a0145 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/simulationiohandler.cpp @@ -0,0 +1,110 @@ +/** + * server/src/hardware/protocol/traintasticdiy/iohandler/simulationiohandler.cpp + * + * 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. + */ + +#include "simulationiohandler.hpp" +#include "../kernel.hpp" +#include "../messages.hpp" +#include + +namespace TraintasticDIY { + +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) +{ +} + +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::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/traintasticdiy/iohandler/simulationiohandler.hpp b/server/src/hardware/protocol/traintasticdiy/iohandler/simulationiohandler.hpp new file mode 100644 index 00000000..bb92565e --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/simulationiohandler.hpp @@ -0,0 +1,55 @@ +/** + * server/src/hardware/protocol/traintasticdiy/iohandler/simulationiohandler.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_IOHANDLER_SIMULATIONIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_IOHANDLER_SIMULATIONIOHANDLER_HPP + +#include "iohandler.hpp" +#include +#include + +namespace TraintasticDIY { + +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/traintasticdiy/iohandler/tcpiohandler.cpp b/server/src/hardware/protocol/traintasticdiy/iohandler/tcpiohandler.cpp new file mode 100644 index 00000000..a836dd38 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/tcpiohandler.cpp @@ -0,0 +1,114 @@ +/** + * server/src/hardware/protocol/traintasticdiy/iohandler/tcpiohandler.cpp + * + * 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. + */ + +#include "tcpiohandler.hpp" +#include +#include "../kernel.hpp" +#include "../messages.hpp" +#include "../../../../core/eventloop.hpp" +#include "../../../../log/log.hpp" +#include "../../../../log/logmessageexception.hpp" + +namespace TraintasticDIY { + +TCPIOHandler::TCPIOHandler(Kernel& kernel, const std::string& hostname, uint16_t port) + : HardwareIOHandler(kernel) + , m_socket{m_kernel.ioContext()} +{ + boost::system::error_code ec; + + m_endpoint.port(port); + m_endpoint.address(boost::asio::ip::make_address(hostname, ec)); + if(ec) + throw LogMessageException(LogMessage::E2003_MAKE_ADDRESS_FAILED_X, ec); + + m_socket.connect(m_endpoint, ec); + if(ec) + throw LogMessageException(LogMessage::E2005_SOCKET_CONNECT_FAILED_X, ec); + + m_socket.set_option(boost::asio::socket_base::linger(true, 0)); + m_socket.set_option(boost::asio::ip::tcp::no_delay(true)); +} + +TCPIOHandler::~TCPIOHandler() +{ +} + +void TCPIOHandler::start() +{ + read(); +} + +void TCPIOHandler::stop() +{ +} + +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); + // TODO interface status -> error + }); + } + }); +} + +void TCPIOHandler::write() +{ + 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); + // TODO interface status -> error + }); + } + }); +} + +} diff --git a/server/src/hardware/protocol/traintasticdiy/iohandler/tcpiohandler.hpp b/server/src/hardware/protocol/traintasticdiy/iohandler/tcpiohandler.hpp new file mode 100644 index 00000000..b5c88b0f --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/iohandler/tcpiohandler.hpp @@ -0,0 +1,50 @@ +/** + * server/src/hardware/protocol/traintasticdiy/iohandler/tcpiohandler.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_IOHANDLER_TCPIOHANDLER_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_IOHANDLER_TCPIOHANDLER_HPP + +#include "hardwareiohandler.hpp" +#include + +namespace TraintasticDIY { + +class TCPIOHandler final : public HardwareIOHandler +{ + private: + boost::asio::ip::tcp::socket m_socket; + boost::asio::ip::tcp::endpoint m_endpoint; + + void read(); + void write() final; + + public: + TCPIOHandler(Kernel& kernel, const std::string& hostname, uint16_t port); + ~TCPIOHandler() final; + + void start() final; + void stop() final; +}; + +} + +#endif diff --git a/server/src/hardware/protocol/traintasticdiy/kernel.cpp b/server/src/hardware/protocol/traintasticdiy/kernel.cpp new file mode 100644 index 00000000..c7451bfd --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/kernel.cpp @@ -0,0 +1,325 @@ +/** + * server/src/hardware/protocol/traintasticdiy/kernel.cpp + * + * 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. + */ + +#include "kernel.hpp" +#include "messages.hpp" +#include "../../decoder/decoder.hpp" +#include "../../decoder/decoderchangeflags.hpp" +#include "../../input/inputcontroller.hpp" +#include "../../output/outputcontroller.hpp" +#include "../../../utils/inrange.hpp" +#include "../../../utils/setthreadname.hpp" +#include "../../../core/eventloop.hpp" +#include "../../../log/log.hpp" + +namespace TraintasticDIY { + +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; + + case OutputState::True: + return TriState::True; + + case OutputState::Undefined: + case OutputState::Invalid: + break; + } + return TriState::Undefined; +} + +Kernel::Kernel(const Config& config, bool simulation) + : m_ioContext{1} + , m_simulation{simulation} + , m_heartbeatTimeout{m_ioContext} + , m_inputController{nullptr} + , m_outputController{nullptr} + , m_config{config} +#ifndef NDEBUG + , m_started{false} +#endif +{ +} + +void Kernel::setConfig(const Config& config) +{ + m_ioContext.post( + [this, newConfig=config]() + { + m_config = newConfig; + }); +} + +void Kernel::start() +{ + assert(m_ioHandler); + assert(!m_started); + + 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("traintasticdiy"); + auto work = std::make_shared(m_ioContext); + m_ioContext.run(); + }); + + m_ioContext.post( + [this]() + { + m_ioHandler->start(); + + send(GetInfo()); + send(GetFeatures()); + + restartHeartbeatTimeout(); + + if(m_onStarted) + EventLoop::call( + [this]() + { + m_onStarted(); + }); + }); + +#ifndef NDEBUG + m_started = true; +#endif +} + +void Kernel::stop() +{ + m_ioContext.post( + [this]() + { + m_heartbeatTimeout.cancel(); + m_ioHandler->stop(); + }); + + m_ioContext.stop(); + + m_thread.join(); + +#ifndef NDEBUG + m_started = false; +#endif +} + +void Kernel::receive(const Message& message) +{ + if(m_config.debugLogRXTX && (message != Heartbeat() || m_config.debugLogHeartbeat)) + EventLoop::call( + [this, msg=toString(message)]() + { + Log::log(m_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->inputs().count({InputController::defaultInputChannel, address}) != 0) + Log::log(m_logId, LogMessage::W2004_INPUT_ADDRESS_X_IS_INVALID, address); + } + else + m_inputController->updateInputValue(InputController::defaultInputChannel, 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->outputs().count({OutputController::defaultOutputChannel, address}) != 0) + Log::log(m_logId, LogMessage::W2005_OUTPUT_ADDRESS_X_IS_INVALID, address); + } + else + m_outputController->updateOutputValue(OutputController::defaultOutputChannel, address, toTriState(state)); + }); + } + } + 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->inputs()) + postSend(GetInputState(static_cast(it.first.address))); + }); + + if(hasFeatureOutput()) + EventLoop::call( + [this]() + { + for(const auto& it : m_outputController->outputs()) + postSend(GetOutputState(static_cast(it.first.address))); + }); + break; + } + case OpCode::Info: + { + const auto& info = static_cast(message); + EventLoop::call( + [this, text=std::string(info.text())]() + { + Log::log(m_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) +{ + if(m_simulation) + m_ioContext.post( + [this, address]() + { + auto it = m_inputValues.find(address); + receive(SetInputState(address, (it == m_inputValues.end() && it->second == InputState::True) ? InputState::False : InputState::True)); + }); +} + +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(m_logId, LogMessage::D2001_TX_X, msg); + }); + } + else + {} // log message and go to error state +} + +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(); +} + +} diff --git a/server/src/hardware/protocol/traintasticdiy/kernel.hpp b/server/src/hardware/protocol/traintasticdiy/kernel.hpp new file mode 100644 index 00000000..8aa8d855 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/kernel.hpp @@ -0,0 +1,233 @@ +/** + * server/src/hardware/protocol/traintasticdiy/kernel.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_KERNEL_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_KERNEL_HPP + +#include +#include +#include +#include +#include +#include "config.hpp" +#include "featureflags.hpp" +#include "inputstate.hpp" +#include "outputstate.hpp" +#include "iohandler/iohandler.hpp" + +class InputController; +class OutputController; + +namespace TraintasticDIY { + +struct Message; + +class Kernel +{ + private: + boost::asio::io_context m_ioContext; + std::unique_ptr m_ioHandler; + const bool m_simulation; + std::thread m_thread; + std::string m_logId; + boost::asio::steady_timer m_heartbeatTimeout; + std::function m_onStarted; + + 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; + + Config m_config; +#ifndef NDEBUG + bool m_started; +#endif + + Kernel(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); } + + void restartHeartbeatTimeout(); + void heartbeatTimeoutExpired(const boost::system::error_code& ec); + + 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; + + /** + * \brief IO context for TraintasticDIY kernel and IO handler + * + * \return The IO context + */ + boost::asio::io_context& ioContext() { return m_ioContext; } + + /** + * \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(const Config& config, Args... args) + { + static_assert(std::is_base_of_v); + std::unique_ptr kernel{new Kernel(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); + } + + /** + * + * + */ + inline const std::string& logId() { return m_logId; } + + /** + * \brief Set object id used for log messages + * + * \param[in] value The object id + */ + inline void setLogId(std::string value) + { + m_logId = std::move(value); + } + + /** + * \brief Set TraintasticDIY configuration + * + * \param[in] config The TraintasticDIY configuration + */ + void setConfig(const Config& config); + + /** + * \brief ... + * + * \param[in] callback ... + * \note This function may not be called when the kernel is running. + */ + inline void setOnStarted(std::function callback) + { + assert(!m_started); + m_onStarted = std::move(callback); + } + + /** + * \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 ... + * + * 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 + */ + void simulateInputChange(uint16_t address); +}; + +} + +#endif diff --git a/server/src/hardware/protocol/traintasticdiy/messages.cpp b/server/src/hardware/protocol/traintasticdiy/messages.cpp new file mode 100644 index 00000000..aafb39de --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/messages.cpp @@ -0,0 +1,88 @@ +#include "messages.hpp" +#include +#include "../../../utils/tohex.hpp" + +namespace TraintasticDIY { + +Checksum calcChecksum(const Message& message) +{ + const uint8_t* 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::Features: + { + break; + } + case OpCode::Info: + { + break; + } + } + + s.append(" ["); + const uint8_t* 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/traintasticdiy/messages.hpp b/server/src/hardware/protocol/traintasticdiy/messages.hpp new file mode 100644 index 00000000..5426f805 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/messages.hpp @@ -0,0 +1,233 @@ +/** + * server/src/hardware/protocol/traintasticdiy/messages.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_MESSAGES_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_MESSAGES_HPP + +#include +#include +#include "opcode.hpp" +#include "inputstate.hpp" +#include "outputstate.hpp" +#include "featureflags.hpp" +#include "../../../utils/byte.hpp" + +namespace TraintasticDIY { + +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 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 TraintasticDIY::Message& lhs, const TraintasticDIY::Message& rhs) +{ + return lhs.size() == rhs.size() && std::memcmp(&lhs, &rhs, lhs.size()) == 0; +} + +inline bool operator !=(const TraintasticDIY::Message& lhs, const TraintasticDIY::Message& rhs) +{ + return lhs.size() != rhs.size() || std::memcmp(&lhs, &rhs, lhs.size()) != 0; +} + +#endif diff --git a/server/src/hardware/protocol/traintasticdiy/opcode.hpp b/server/src/hardware/protocol/traintasticdiy/opcode.hpp new file mode 100644 index 00000000..042007e5 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/opcode.hpp @@ -0,0 +1,81 @@ +/** + * server/src/hardware/protocol/traintasticdiy/opcode.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_OPCODE_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_OPCODE_HPP + +#include + +namespace TraintasticDIY { + +enum class OpCode : uint8_t +{ + Heartbeat = 0x00, + GetInputState = 0x12, + SetInputState = 0x13, + GetOutputState = 0x22, + SetOutputState = 0x23, + GetFeatures = 0xE0, + Features = 0xE4, + GetInfo = 0xF0, + Info = 0xFF, +}; + +} + +constexpr std::string_view toString(TraintasticDIY::OpCode value) +{ + using OpCode = TraintasticDIY::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::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/traintasticdiy/outputstate.hpp b/server/src/hardware/protocol/traintasticdiy/outputstate.hpp new file mode 100644 index 00000000..57f8c78e --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/outputstate.hpp @@ -0,0 +1,62 @@ +/** + * server/src/hardware/protocol/traintasticdiy/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_TRAINTASTICDIY_OUTPUTSTATE_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_OUTPUTSTATE_HPP + +#include +#include + +namespace TraintasticDIY { + +enum class OutputState : uint8_t +{ + Undefined = 0, + False = 1, + True = 2, + Invalid = 3, +}; + +} + +constexpr std::string_view toString(TraintasticDIY::OutputState value) +{ + using OutputState = TraintasticDIY::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/traintasticdiy/settings.cpp b/server/src/hardware/protocol/traintasticdiy/settings.cpp new file mode 100644 index 00000000..eb5c754a --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/settings.cpp @@ -0,0 +1,56 @@ +/** + * server/src/hardware/protocol/traintasticdiy/settings.cpp + * + * 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. + */ + +#include "settings.hpp" +#include "../../../core/attributes.hpp" +#include "../../../utils/displayname.hpp" + +namespace TraintasticDIY { + +Settings::Settings(Object& _parent, std::string_view parentPropertyName) + : SubObject(_parent, parentPropertyName) + , 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(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.heartbeatTimeout = std::chrono::milliseconds(heartbeatTimeout); + + config.debugLogRXTX = debugLogRXTX; + config.debugLogHeartbeat = debugLogHeartbeat; + + return config; +} + +} diff --git a/server/src/hardware/protocol/traintasticdiy/settings.hpp b/server/src/hardware/protocol/traintasticdiy/settings.hpp new file mode 100644 index 00000000..eaa93f88 --- /dev/null +++ b/server/src/hardware/protocol/traintasticdiy/settings.hpp @@ -0,0 +1,53 @@ +/** + * server/src/hardware/protocol/traintasticdiy/settings.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_SETTINGS_HPP +#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_TRAINTASTICDIY_SETTINGS_HPP + +#include "../../../core/subobject.hpp" +#include "../../../core/property.hpp" +#include "config.hpp" + +namespace TraintasticDIY { + +class Settings final : public SubObject +{ + CLASS_ID("traintastic_diy_settings") + + private: + static constexpr uint16_t heartbeatTimeoutMin = 100; + static constexpr uint16_t heartbeatTimeoutDefault = 1'000; + static constexpr uint16_t heartbeatTimeoutMax = 60'000; + + public: + Property heartbeatTimeout; + Property debugLogRXTX; + Property debugLogHeartbeat; + + Settings(Object& _parent, std::string_view parentPropertyName); + + Config config() const; +}; + +} + +#endif diff --git a/server/test/objectcreatedestroy.cpp b/server/test/objectcreatedestroy.cpp index e62f4aee..7500a379 100644 --- a/server/test/objectcreatedestroy.cpp +++ b/server/test/objectcreatedestroy.cpp @@ -28,6 +28,7 @@ #include "../src/hardware/interface/ecosinterface.hpp" #include "../src/hardware/interface/hsi88.hpp" #include "../src/hardware/interface/loconetinterface.hpp" +#include "../src/hardware/interface/traintasticdiyinterface.hpp" #include "../src/hardware/interface/wlanmausinterface.hpp" #include "../src/hardware/interface/xpressnetinterface.hpp" #include "../src/hardware/interface/z21interface.hpp" @@ -83,6 +84,7 @@ TEMPLATE_TEST_CASE("Create world and interface => destroy world", "[object-creat , ECoSInterface , HSI88Interface , LocoNetInterface + , TraintasticDIYInterface , WlanMausInterface , XpressNetInterface , Z21Interface @@ -106,6 +108,7 @@ TEMPLATE_TEST_CASE("Create world and interface => destroy interface", "[object-c , ECoSInterface , HSI88Interface , LocoNetInterface + , TraintasticDIYInterface , WlanMausInterface , XpressNetInterface , Z21Interface diff --git a/shared/src/traintastic/enum/logmessage.hpp b/shared/src/traintastic/enum/logmessage.hpp index 683aa4d5..2c7e7a6f 100644 --- a/shared/src/traintastic/enum/logmessage.hpp +++ b/shared/src/traintastic/enum/logmessage.hpp @@ -73,6 +73,7 @@ enum class LogMessage : uint32_t I2002_HARDWARE_TYPE_X = LogMessageOffset::info + 2002, I2003_FIRMWARE_VERSION_X = LogMessageOffset::info + 2003, I2004_HSI_88_X = LogMessageOffset::info + 2004, + I2005_X = LogMessageOffset::info + 2005, I9001_STOPPED_SCRIPT = LogMessageOffset::info + 9001, I9999_X = LogMessageOffset::info + 9999, @@ -113,6 +114,8 @@ enum class LogMessage : uint32_t W2001_RECEIVED_MALFORMED_DATA_DROPPED_X_BYTES = LogMessageOffset::warning + 2001, W2002_COMMAND_STATION_DOESNT_SUPPORT_FUNCTIONS_ABOVE_FX = LogMessageOffset::warning + 2002, W2003_COMMAND_STATION_DOESNT_SUPPORT_X_SPEEDSTEPS_USING_X = LogMessageOffset::warning + 2003, + W2004_INPUT_ADDRESS_X_IS_INVALID = LogMessageOffset::warning + 2004, + W2005_OUTPUT_ADDRESS_X_IS_INVALID = LogMessageOffset::warning + 2005, W9999_X = LogMessageOffset::warning + 9999, // Error: diff --git a/shared/src/traintastic/enum/traintasticdiyinterfacetype.hpp b/shared/src/traintastic/enum/traintasticdiyinterfacetype.hpp new file mode 100644 index 00000000..2a992448 --- /dev/null +++ b/shared/src/traintastic/enum/traintasticdiyinterfacetype.hpp @@ -0,0 +1,41 @@ +/** + * shared/src/traintastic/enum/traintasticdiyinterfacetype.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_TRAINTASTICDIYINTERFACETYPE_HPP +#define TRAINTASTIC_SHARED_TRAINTASTIC_ENUM_TRAINTASTICDIYINTERFACETYPE_HPP + +#include +#include "enum.hpp" + +enum class TraintasticDIYInterfaceType : uint8_t +{ + Serial = 0, + NetworkTCP = 1, +}; + +TRAINTASTIC_ENUM(TraintasticDIYInterfaceType, "traintastic_diy_interface_type", 2, +{ + {TraintasticDIYInterfaceType::Serial, "serial"}, + {TraintasticDIYInterfaceType::NetworkTCP, "network_tcp"}, +}); + +#endif diff --git a/shared/translations/en-us.txt b/shared/translations/en-us.txt index 6d6bca2b..9e3d1323 100644 --- a/shared/translations/en-us.txt +++ b/shared/translations/en-us.txt @@ -61,6 +61,7 @@ class_id:interface.dccplusplus=DCC++ class_id:interface.ecos=ECoS class_id:interface.hsi88=HSI-88 class_id:interface.loconet=LocoNet +class_id:interface.traintastic_diy=Traintastic DIY class_id:interface.wlanmaus=WLANmaus class_id:interface.xpressnet=XpressNet class_id:interface.z21=Z21 @@ -167,6 +168,9 @@ input_map_item.block:type=Type interface.dccplusplus:dcc_plus_plus=DCC++(EX) interface.ecos:ecos=ECoS +interface.ecos:ecos=ECoS +interface.ecos:ecos_detector=ECoS detector +interface.ecos:s88=S88 interface.hsi88:modules_left=Modules left interface.hsi88:modules_middle=Modulles middle @@ -174,6 +178,8 @@ interface.hsi88:modules_right=Modules right interface.loconet:interface=Interface +interface.traintastic_diy:traintastic_diy=Traintastic DIY + interface.xpressnet:interface=Interface interface.xpressnet:s88_module_count=S88 module count interface.xpressnet:s88_start_address=S88 start address @@ -321,6 +327,7 @@ message:I2001=Unknown loco address: %1 message:I2002=Hardware type: %1 message:I2003=Firmware version: %1 message:I2004=HSI-88: %1 +message:I2005=%1 message:I9001=Stopped script message:I9999=%1 message:N1001=Received signal: %1 @@ -357,6 +364,8 @@ message:W1002=Setting %1 doesnt exist message:W2001=Received malformed data dropped %1 bytes message:W2002=Command station doesn't support functions above F%1 message:W2003=Command station doesn't support %1 speedsteps using %2 +message:W2004=Input address %1 is invalid +message:W2005=Output address %1 is invalid message:W9999=%1 object:id=Id @@ -507,6 +516,12 @@ train:speed_max=Maximum speed train:vehicles=Vehicles train:weight=Weight +traintastic_diy_interface_type:network_tcp=Network (TCP) +traintastic_diy_interface_type:serial=Serial + +traintastic_diy_settings:debug_log_heartbeat=Log heartbeat communication +traintastic_diy_settings:heartbeat_timeout=Heartbeat timeout + turnout_position:left=Left turnout_position:right=Right turnout_position:straight=Straight