From 5f0d78ae1470be30e2e2c46085281513b1853d4c Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 13 Mar 2024 23:45:23 +0100 Subject: [PATCH] OutputController: Added support for ECoS object, see #95 --- client/src/network/outputkeyboard.cpp | 2 + client/src/widget/outputkeyboardwidget.cpp | 3 + client/src/widget/outputmapwidget.cpp | 35 ++- client/src/widget/outputmapwidget.hpp | 3 + manual/luadoc/object/addressoutput.json | 3 + manual/luadoc/object/output.json | 3 +- server/src/core/attributes.hpp | 16 + .../src/hardware/interface/ecosinterface.cpp | 63 +++- .../src/hardware/interface/ecosinterface.hpp | 6 +- server/src/hardware/output/addressoutput.cpp | 30 ++ server/src/hardware/output/addressoutput.hpp | 44 +++ server/src/hardware/output/aspectoutput.cpp | 2 +- server/src/hardware/output/aspectoutput.hpp | 4 +- .../src/hardware/output/ecosstateoutput.cpp | 56 ++++ .../src/hardware/output/ecosstateoutput.hpp | 53 ++++ .../output/keyboard/pairoutputkeyboard.cpp | 3 +- .../output/keyboard/singleoutputkeyboard.cpp | 5 +- .../output/list/outputlisttablemodel.cpp | 7 +- server/src/hardware/output/map/outputmap.cpp | 280 +++++++++++++++--- server/src/hardware/output/map/outputmap.hpp | 12 + .../map/outputmapecosstateoutputaction.cpp | 57 ++++ .../map/outputmapecosstateoutputaction.hpp | 48 +++ server/src/hardware/output/output.cpp | 5 +- server/src/hardware/output/output.hpp | 13 +- .../src/hardware/output/outputcontroller.cpp | 75 ++++- .../src/hardware/output/outputcontroller.hpp | 37 ++- server/src/hardware/output/outputvalue.hpp | 2 +- server/src/hardware/output/pairoutput.cpp | 2 +- server/src/hardware/output/pairoutput.hpp | 4 +- server/src/hardware/output/singleoutput.cpp | 4 +- server/src/hardware/output/singleoutput.hpp | 4 +- server/src/hardware/protocol/ecos/kernel.cpp | 111 +++++-- server/src/hardware/protocol/ecos/kernel.hpp | 19 +- .../hardware/protocol/ecos/object/object.cpp | 11 +- .../hardware/protocol/ecos/object/object.hpp | 1 + .../hardware/protocol/ecos/object/switch.cpp | 16 + .../hardware/protocol/ecos/object/switch.hpp | 1 + .../protocol/traintasticdiy/kernel.cpp | 2 +- server/src/lua/object/interface.cpp | 6 +- server/src/network/session.cpp | 1 + server/test/hardware/outputmap.cpp | 192 ++++++++++++ shared/src/traintastic/enum/outputchannel.hpp | 7 +- shared/src/traintastic/enum/outputtype.hpp | 7 +- shared/translations/en-us.json | 20 ++ 44 files changed, 1129 insertions(+), 146 deletions(-) create mode 100644 manual/luadoc/object/addressoutput.json create mode 100644 server/src/hardware/output/addressoutput.cpp create mode 100644 server/src/hardware/output/addressoutput.hpp create mode 100644 server/src/hardware/output/ecosstateoutput.cpp create mode 100644 server/src/hardware/output/ecosstateoutput.hpp create mode 100644 server/src/hardware/output/map/outputmapecosstateoutputaction.cpp create mode 100644 server/src/hardware/output/map/outputmapecosstateoutputaction.hpp create mode 100644 server/test/hardware/outputmap.cpp diff --git a/client/src/network/outputkeyboard.cpp b/client/src/network/outputkeyboard.cpp index 7be2e1ea..a6c48283 100644 --- a/client/src/network/outputkeyboard.cpp +++ b/client/src/network/outputkeyboard.cpp @@ -54,6 +54,7 @@ OutputKeyboard::OutputKeyboard(std::shared_ptr connection, Handle ha break; case OutputType::Aspect: /*[[unlikely]]*/ + case OutputType::ECoSState: /*[[unlikely]]*/ assert(false); break; } @@ -118,6 +119,7 @@ void OutputKeyboard::created() break; case OutputType::Aspect: /*[[unlikely]]*/ + case OutputType::ECoSState: /*[[unlikely]]*/ assert(false); break; } diff --git a/client/src/widget/outputkeyboardwidget.cpp b/client/src/widget/outputkeyboardwidget.cpp index f3df5a9f..9bc599cb 100644 --- a/client/src/widget/outputkeyboardwidget.cpp +++ b/client/src/widget/outputkeyboardwidget.cpp @@ -171,6 +171,7 @@ OutputKeyboardWidget::OutputKeyboardWidget(std::shared_ptr objec break; case OutputType::Aspect: + case OutputType::ECoSState: assert(false); // not (yet) supported break; } @@ -210,6 +211,7 @@ OutputKeyboardWidget::OutputKeyboardWidget(std::shared_ptr objec break; } case OutputType::Aspect: /*[[unlikely]]*/ + case OutputType::ECoSState: /*[[unlikely]]*/ assert(false); break; } @@ -350,6 +352,7 @@ void OutputKeyboardWidget::updateLEDs() break; } case OutputType::Aspect: /*[[unlikely]]*/ + case OutputType::ECoSState: /*[[unlikely]]*/ assert(false); break; } diff --git a/client/src/widget/outputmapwidget.cpp b/client/src/widget/outputmapwidget.cpp index 7130c160..83d412ce 100644 --- a/client/src/widget/outputmapwidget.cpp +++ b/client/src/widget/outputmapwidget.cpp @@ -54,6 +54,7 @@ OutputMapWidget::OutputMapWidget(ObjectPtr object, QWidget* parent) : QWidget(parent) , m_object{std::move(object)} , m_addresses{m_object->getVectorProperty("addresses")} + , m_ecosObject{dynamic_cast(m_object->getProperty("ecos_object"))} , m_items{m_object->getObjectVectorProperty("items")} , m_table{new QTableWidget(this)} { @@ -73,6 +74,11 @@ OutputMapWidget::OutputMapWidget(ObjectPtr object, QWidget* parent) form->addRow(new InterfaceItemNameLabel(*m_addresses, this), new PropertyAddresses(*m_addresses, m_object->getMethod("add_address"), m_object->getMethod("remove_address"), this)); connect(m_addresses, &AbstractVectorProperty::valueChanged, this, &OutputMapWidget::updateTableOutputColumns); } + if(m_ecosObject) + { + form->addRow(new InterfaceItemNameLabel(*m_ecosObject, this), new PropertyComboBox(*m_ecosObject, this)); + connect(m_ecosObject, &AbstractVectorProperty::valueChanged, this, &OutputMapWidget::updateTableOutputColumns); + } l->addLayout(form); m_table->setColumnCount(columnCountNonOutput); @@ -135,18 +141,25 @@ void OutputMapWidget::updateItems(const std::vector& items) void OutputMapWidget::updateTableOutputColumns() { - if(!m_addresses) /*[[unlikely]]*/ + if(m_addresses && m_addresses->getAttributeBool(AttributeName::Visible, true)) { - return; + const auto size = m_addresses->size(); + + m_table->setColumnCount(columnCountNonOutput + size); + for(int i = 0; i < size; i++) + { + const int column = columnCountNonOutput + i; + m_table->setHorizontalHeaderItem(column, new QTableWidgetItem(QString("#%1").arg(m_addresses->getInt(i)))); + } } - - const auto size = m_addresses->size(); - - m_table->setColumnCount(columnCountNonOutput + size); - for(int i = 0; i < size; i++) + else if(m_ecosObject && m_ecosObject->getAttributeBool(AttributeName::Visible, true)) { - const int column = columnCountNonOutput + i; - m_table->setHorizontalHeaderItem(column, new QTableWidgetItem(QString("#%1").arg(m_addresses->getInt(i)))); + m_table->setColumnCount(columnCountNonOutput + 1); + m_table->setHorizontalHeaderItem(columnCountNonOutput, new QTableWidgetItem(Locale::tr("output.ecos_object:state"))); + } + else + { + m_table->setColumnCount(columnCountNonOutput); } } @@ -171,6 +184,10 @@ void OutputMapWidget::updateTableOutputActions(ObjectVectorProperty& property, i { m_table->setCellWidget(row, column, new PropertySpinBox(*aspect, this)); } + else if(auto* state = dynamic_cast(object->getProperty("state"))) + { + m_table->setCellWidget(row, column, new PropertySpinBox(*state, this)); + } } column++; } diff --git a/client/src/widget/outputmapwidget.hpp b/client/src/widget/outputmapwidget.hpp index 326bfe97..7d31e5f2 100644 --- a/client/src/widget/outputmapwidget.hpp +++ b/client/src/widget/outputmapwidget.hpp @@ -29,8 +29,10 @@ class QTableWidget; class Method; class MethodAction; +class AbstractProperty; class AbstractVectorProperty; class ObjectVectorProperty; +class Property; class OutputMapWidget : public QWidget { @@ -39,6 +41,7 @@ class OutputMapWidget : public QWidget protected: ObjectPtr m_object; AbstractVectorProperty* m_addresses; + Property* m_ecosObject; ObjectVectorProperty* m_items; QTableWidget* m_table; std::vector m_itemObjects; diff --git a/manual/luadoc/object/addressoutput.json b/manual/luadoc/object/addressoutput.json new file mode 100644 index 00000000..b11cf31a --- /dev/null +++ b/manual/luadoc/object/addressoutput.json @@ -0,0 +1,3 @@ +{ + "address": {} +} \ No newline at end of file diff --git a/manual/luadoc/object/output.json b/manual/luadoc/object/output.json index ca2fa0de..2f9f1781 100644 --- a/manual/luadoc/object/output.json +++ b/manual/luadoc/object/output.json @@ -2,6 +2,5 @@ "name": {}, "type": {}, "interface": {}, - "channel": {}, - "address": {} + "channel": {} } \ No newline at end of file diff --git a/server/src/core/attributes.hpp b/server/src/core/attributes.hpp index 7470f3a6..20712c06 100644 --- a/server/src/core/attributes.hpp +++ b/server/src/core/attributes.hpp @@ -58,6 +58,22 @@ struct Attributes property.setAttribute(AttributeName::AliasValues, values); } + template + static inline void addAliases(Property& property, tcb::span keys, tcb::span values) + { + assert(keys.size() == values.size()); + property.addAttribute(AttributeName::AliasKeys, keys); + property.addAttribute(AttributeName::AliasValues, values); + } + + template + static inline void setAliases(Property& property, tcb::span keys, tcb::span values) + { + assert(keys.size() == values.size()); + property.setAttribute(AttributeName::AliasKeys, keys); + property.setAttribute(AttributeName::AliasValues, values); + } + static inline void addCategory(InterfaceItem& item, std::string_view value) { item.addAttribute(AttributeName::Category, value); diff --git a/server/src/hardware/interface/ecosinterface.cpp b/server/src/hardware/interface/ecosinterface.cpp index bfdfd677..c5b26668 100644 --- a/server/src/hardware/interface/ecosinterface.cpp +++ b/server/src/hardware/interface/ecosinterface.cpp @@ -30,6 +30,7 @@ #include "../protocol/ecos/messages.hpp" #include "../protocol/ecos/iohandler/tcpiohandler.hpp" #include "../protocol/ecos/iohandler/simulationiohandler.hpp" +#include "../protocol/ecos/object/switch.hpp" #include "../../core/attributes.hpp" #include "../../core/method.tpp" #include "../../core/objectproperty.tpp" @@ -119,16 +120,34 @@ void ECoSInterface::inputSimulateChange(uint32_t channel, uint32_t address, Simu tcb::span ECoSInterface::outputChannels() const { - static const auto values = makeArray(OutputChannel::AccessoryDCC, OutputChannel::AccessoryMotorola); + static const auto values = makeArray(OutputChannel::AccessoryDCC, OutputChannel::AccessoryMotorola, OutputChannel::ECoSObject); return values; } -bool ECoSInterface::setOutputValue(OutputChannel channel, uint32_t address, OutputValue value) +std::pair, tcb::span> ECoSInterface::getOutputECoSObjects(OutputChannel channel) const +{ + if(channel == OutputChannel::ECoSObject) /*[[likely]]*/ + { + return {m_outputECoSObjectIds, m_outputECoSObjectNames}; + } + return OutputController::getOutputECoSObjects(channel); +} + +bool ECoSInterface::isOutputId(OutputChannel channel, uint32_t outputId) const +{ + if(channel == OutputChannel::ECoSObject) + { + return inRange(outputId, ECoS::ObjectId::switchMin, ECoS::ObjectId::switchMax); + } + return OutputController::isOutputId(channel, outputId); +} + +bool ECoSInterface::setOutputValue(OutputChannel channel, uint32_t outputId, OutputValue value) { return m_kernel && - inRange(address, outputAddressMinMax(channel)) && - m_kernel->setOutput(channel, static_cast(address), std::get(value)); + isOutputId(channel, outputId) && + m_kernel->setOutput(channel, outputId, value); } bool ECoSInterface::setOnline(bool& value, bool simulation) @@ -176,6 +195,38 @@ bool ECoSInterface::setOnline(bool& value, bool simulation) if(!contains(m_world.state.value(), WorldState::Run)) m_world.run(); }); + m_kernel->setOnObjectChanged( + [this](std::size_t typeHash, uint16_t objectId, const std::string& objectName) + { + if(typeHash == typeid(ECoS::Switch).hash_code()) + { + if(auto it = std::find(m_outputECoSObjectIds.begin(), m_outputECoSObjectIds.end(), objectId); it != m_outputECoSObjectIds.end()) + { + const std::size_t index = std::distance(m_outputECoSObjectIds.begin(), it); + m_outputECoSObjectNames[index] = objectName; + } + else + { + m_outputECoSObjectIds.emplace_back(objectId); + m_outputECoSObjectNames.emplace_back(objectName); + } + assert(m_outputECoSObjectIds.size() == m_outputECoSObjectNames.size()); + outputECoSObjectsChanged(); + } + }); + m_kernel->setOnObjectRemoved( + [this](uint16_t objectId) + { + assert(objectId == 0); + if(auto it = std::find(m_outputECoSObjectIds.begin(), m_outputECoSObjectIds.end(), objectId); it != m_outputECoSObjectIds.end()) + { + const std::size_t index = std::distance(m_outputECoSObjectIds.begin(), it); + m_outputECoSObjectIds.erase(it); + m_outputECoSObjectNames.erase(std::next(m_outputECoSObjectNames.begin(), index)); + assert(m_outputECoSObjectIds.size() == m_outputECoSObjectNames.size()); + outputECoSObjectsChanged(); + } + }); m_kernel->setDecoderController(this); m_kernel->setInputController(this); m_kernel->setOutputController(this); @@ -187,6 +238,10 @@ bool ECoSInterface::setOnline(bool& value, bool simulation) m_kernel->setConfig(ecos->config()); }); + // Reset output object list: + m_outputECoSObjectIds.assign({0}); + m_outputECoSObjectNames.assign({{}}); + Attributes::setEnabled(hostname, false); } catch(const LogMessageException& e) diff --git a/server/src/hardware/interface/ecosinterface.hpp b/server/src/hardware/interface/ecosinterface.hpp index 553c677c..c8810771 100644 --- a/server/src/hardware/interface/ecosinterface.hpp +++ b/server/src/hardware/interface/ecosinterface.hpp @@ -53,6 +53,8 @@ class ECoSInterface final std::unique_ptr m_kernel; boost::signals2::connection m_ecosPropertyChanged; ECoS::Simulation m_simulation; + std::vector m_outputECoSObjectIds; + std::vector m_outputECoSObjectNames; void addToWorld() final; void destroying() final; @@ -85,7 +87,9 @@ class ECoSInterface final // OutputController: tcb::span outputChannels() const final; - [[nodiscard]] bool setOutputValue(OutputChannel channel, uint32_t address, OutputValue value) final; + std::pair, tcb::span> getOutputECoSObjects(OutputChannel channel) const final; + bool isOutputId(OutputChannel channel, uint32_t id) const final; + [[nodiscard]] bool setOutputValue(OutputChannel channel, uint32_t outputId, OutputValue value) final; }; #endif diff --git a/server/src/hardware/output/addressoutput.cpp b/server/src/hardware/output/addressoutput.cpp new file mode 100644 index 00000000..40ad0650 --- /dev/null +++ b/server/src/hardware/output/addressoutput.cpp @@ -0,0 +1,30 @@ +/** + * server/src/hardware/output/output.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2019-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 "addressoutput.hpp" + +AddressOutput::AddressOutput(std::shared_ptr outputController, OutputChannel channel_, OutputType type_, uint32_t address_) + : Output(std::move(outputController), channel_, type_) + , address{this, "address", address_, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} +{ + m_interfaceItems.add(address); +} diff --git a/server/src/hardware/output/addressoutput.hpp b/server/src/hardware/output/addressoutput.hpp new file mode 100644 index 00000000..0052de6e --- /dev/null +++ b/server/src/hardware/output/addressoutput.hpp @@ -0,0 +1,44 @@ +/** + * server/src/hardware/output/addressoutput.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 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_OUTPUT_ADDRESSOUTPUT_HPP +#define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_ADDRESSOUTPUT_HPP + +#include "output.hpp" + +class AddressOutput : public Output +{ + friend class OutputController; + + protected: + AddressOutput(std::shared_ptr outputController, OutputChannel channel_, OutputType type_, uint32_t address_); + + public: + Property address; + + uint32_t id() const final + { + return address.value(); + } +}; + +#endif diff --git a/server/src/hardware/output/aspectoutput.cpp b/server/src/hardware/output/aspectoutput.cpp index c98848ee..08f278f1 100644 --- a/server/src/hardware/output/aspectoutput.cpp +++ b/server/src/hardware/output/aspectoutput.cpp @@ -28,7 +28,7 @@ #include "../../utils/inrange.hpp" AspectOutput::AspectOutput(std::shared_ptr outputController, OutputChannel channel_, uint32_t address_) - : Output(std::move(outputController), channel_, OutputType::Pair, address_) + : AddressOutput(std::move(outputController), channel_, OutputType::Pair, address_) , value{this, "value", -1, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} , setValue{*this, "set_value", MethodFlags::ScriptCallable, [this](int16_t newValue) diff --git a/server/src/hardware/output/aspectoutput.hpp b/server/src/hardware/output/aspectoutput.hpp index 2315d597..8a399233 100644 --- a/server/src/hardware/output/aspectoutput.hpp +++ b/server/src/hardware/output/aspectoutput.hpp @@ -23,11 +23,11 @@ #ifndef TRAINTASTIC_SERVER_HARDWARE_OUTPUT_ASPECTOUTPUT_HPP #define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_ASPECTOUTPUT_HPP -#include "output.hpp" +#include "addressoutput.hpp" #include "../../core/method.hpp" #include "../../core/event.hpp" -class AspectOutput final : public Output +class AspectOutput final : public AddressOutput { friend class OutputController; diff --git a/server/src/hardware/output/ecosstateoutput.cpp b/server/src/hardware/output/ecosstateoutput.cpp new file mode 100644 index 00000000..c067d09c --- /dev/null +++ b/server/src/hardware/output/ecosstateoutput.cpp @@ -0,0 +1,56 @@ +/** + * server/src/hardware/output/ecosstateoutput.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 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 "ecosstateoutput.hpp" +#include "outputcontroller.hpp" +#include "../../core/attributes.hpp" +#include "../../core/method.tpp" +#include "../../core/objectproperty.tpp" +#include "../../utils/inrange.hpp" + +ECoSStateOutput::ECoSStateOutput(std::shared_ptr outputController, OutputChannel channel_, uint16_t ecosObjectId_) + : Output(std::move(outputController), channel_, OutputType::ECoSState) + , ecosObjectId{this, "ecos_object_id", ecosObjectId_, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} + , value{this, "value", 0, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} + , setValue{*this, "set_value", MethodFlags::ScriptCallable, + [this](uint8_t newValue) + { + assert(interface); + return interface->setOutputValue(channel, ecosObjectId, newValue); + }} + , onValueChanged{*this, "on_value_changed", EventFlags::Scriptable} +{ + Attributes::addObjectEditor(value, false); + Attributes::addMinMax(value, std::numeric_limits::min(), std::numeric_limits::max()); + m_interfaceItems.add(value); + + Attributes::addObjectEditor(setValue, false); + m_interfaceItems.add(setValue); + + m_interfaceItems.add(onValueChanged); +} + +void ECoSStateOutput::updateValue(uint8_t newValue) +{ + value.setValueInternal(newValue); + fireEvent(onValueChanged, newValue, shared_ptr()); +} diff --git a/server/src/hardware/output/ecosstateoutput.hpp b/server/src/hardware/output/ecosstateoutput.hpp new file mode 100644 index 00000000..6693a109 --- /dev/null +++ b/server/src/hardware/output/ecosstateoutput.hpp @@ -0,0 +1,53 @@ +/** + * server/src/hardware/output/ecosstateoutput.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 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_OUTPUT_ECOSSTATEOUTPUT_HPP +#define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_ECOSSTATEOUTPUT_HPP + +#include "output.hpp" +#include "../../core/method.hpp" +#include "../../core/event.hpp" + +class ECoSStateOutput final : public Output +{ + friend class OutputController; + + CLASS_ID("output.ecos_state") + + protected: + void updateValue(uint8_t newValue); + + public: + Property ecosObjectId; + Property value; + Method setValue; + Event&> onValueChanged; + + ECoSStateOutput(std::shared_ptr outputController, OutputChannel channel_, uint16_t ecosObjectId_); + + uint32_t id() const final + { + return ecosObjectId.value(); + } +}; + +#endif diff --git a/server/src/hardware/output/keyboard/pairoutputkeyboard.cpp b/server/src/hardware/output/keyboard/pairoutputkeyboard.cpp index e7aa15b7..ef287d26 100644 --- a/server/src/hardware/output/keyboard/pairoutputkeyboard.cpp +++ b/server/src/hardware/output/keyboard/pairoutputkeyboard.cpp @@ -45,7 +45,8 @@ std::vector PairOutputKeyboard::getOutputInfo() cons { if(it.second->channel == channel) { - states.emplace_back(OutputInfo{it.second->address.value(), true, static_cast(*it.second).value.value()}); + const auto& output = static_cast(*it.second); + states.emplace_back(OutputInfo{output.address.value(), true, output.value.value()}); } } return states; diff --git a/server/src/hardware/output/keyboard/singleoutputkeyboard.cpp b/server/src/hardware/output/keyboard/singleoutputkeyboard.cpp index ef01c205..d4a25f5b 100644 --- a/server/src/hardware/output/keyboard/singleoutputkeyboard.cpp +++ b/server/src/hardware/output/keyboard/singleoutputkeyboard.cpp @@ -30,7 +30,7 @@ SingleOutputKeyboard::SingleOutputKeyboard(OutputController& controller, OutputC , setOutputValue(*this, "set_output_value", [this](uint32_t address, bool value) { - return m_controller.setOutputValue(channel, address, value); + return m_controller.setOutputValue(channel, address, toTriState(value)); }) , outputValueChanged(*this, "output_value_changed", EventFlags::Public) { @@ -45,7 +45,8 @@ std::vector SingleOutputKeyboard::getOutputInfo() co { if(it.second->channel == channel) { - states.emplace_back(OutputInfo{it.second->address.value(), true, static_cast(*it.second).value.value()}); + const auto& output = static_cast(*it.second); + states.emplace_back(OutputInfo{output.address.value(), true, output.value.value()}); } } return states; diff --git a/server/src/hardware/output/list/outputlisttablemodel.cpp b/server/src/hardware/output/list/outputlisttablemodel.cpp index 4b5dd61d..08464cbb 100644 --- a/server/src/hardware/output/list/outputlisttablemodel.cpp +++ b/server/src/hardware/output/list/outputlisttablemodel.cpp @@ -22,6 +22,7 @@ #include "outputlisttablemodel.hpp" #include "outputlist.hpp" +#include "../addressoutput.hpp" #include "../outputcontroller.hpp" #include "../../../core/objectproperty.tpp" #include "../../../utils/displayname.hpp" @@ -95,7 +96,11 @@ std::string OutputListTableModel::getText(uint32_t column, uint32_t row) const break; case OutputListColumn::Address: - return std::to_string(output.address.value()); + if(auto* addressOutput = dynamic_cast(&output)) + { + return std::to_string(addressOutput->address.value()); + } + return {}; } assert(false); } diff --git a/server/src/hardware/output/map/outputmap.cpp b/server/src/hardware/output/map/outputmap.cpp index d1618da0..38f0819e 100644 --- a/server/src/hardware/output/map/outputmap.cpp +++ b/server/src/hardware/output/map/outputmap.cpp @@ -26,7 +26,9 @@ #include "outputmapsingleoutputaction.hpp" #include "outputmappairoutputaction.hpp" #include "outputmapaspectoutputaction.hpp" +#include "outputmapecosstateoutputaction.hpp" #include "../outputcontroller.hpp" +#include "../../interface/interface.hpp" #include "../../../core/attributes.hpp" #include "../../../core/method.tpp" #include "../../../core/objectproperty.tpp" @@ -46,8 +48,19 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) }, [this](const std::shared_ptr& newValue) { + m_interfaceDestroying.disconnect(); + if(newValue) { + if(auto* object = dynamic_cast(newValue.get())) /*[[likely]]*/ + { + m_interfaceDestroying = object->onDestroying.connect( + [this](Object& /*object*/) + { + interface = nullptr; + }); + } + if(!interface) { // No interface was assigned. @@ -81,21 +94,36 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) { assert(addresses.empty()); assert(m_outputs.empty()); - const uint32_t address = newValue->getUnusedOutputAddress(channel); - addresses.appendInternal(address); - m_outputs.emplace_back(newValue->getOutput(channel, address, parent())); - const auto outputType = newValue->outputType(channel); - for(auto& item : items) + + switch(channel) { - item->outputActions.appendInternal(createOutputAction(outputType, 0)); + case OutputChannel::Output: + case OutputChannel::Accessory: + case OutputChannel::AccessoryDCC: + case OutputChannel::AccessoryMotorola: + case OutputChannel::DCCext: + case OutputChannel::Turnout: + { + const uint32_t address = newValue->getUnusedOutputAddress(channel); + addresses.appendInternal(address); + addOutput(channel, address, *newValue); + break; + } + case OutputChannel::ECoSObject: + if(newValue->isOutputId(channel, ecosObject)) + { + addOutput(channel, ecosObject, *newValue); + } + break; } + updateOutputActions(newValue->outputType(channel)); } } else // no interface { for(auto& output : m_outputs) { - if(output) + if(output) /*[[likely]]*/ { interface->releaseOutput(*output, parent()); } @@ -120,9 +148,28 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) } // Get outputs for current channel: - for(uint32_t address : addresses) + switch(newValue) { - m_outputs.emplace_back(interface->getOutput(newValue, address, parent())); + case OutputChannel::Output: + case OutputChannel::Accessory: + case OutputChannel::AccessoryDCC: + case OutputChannel::AccessoryMotorola: + case OutputChannel::DCCext: + case OutputChannel::Turnout: + ecosObject.setValueInternal(0); + for(uint32_t address : addresses) + { + addOutput(newValue, address); + } + break; + + case OutputChannel::ECoSObject: + addresses.clearInternal(); + if(interface->isOutputId(newValue, ecosObject)) + { + addOutput(newValue, ecosObject); + } + break; } channelChanged(); @@ -160,7 +207,7 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) return false; // Duplicate addresses aren't allowed. } - auto output = interface->getOutput(channel, value, parent()); + auto output = getOutput(channel, value, *interface); if(!output) /*[[unlikely]]*/ { return false; // Output doesn't exist. @@ -168,13 +215,39 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) if(index < m_outputs.size() && m_outputs[index]) { - interface->releaseOutput(*m_outputs[index], parent()); + releaseOutput(*m_outputs[index]); } m_outputs[index] = output; return true; }} + , ecosObject{this, "ecos_object", 0, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript, + nullptr, + [this](uint16_t& value) + { + if(!interface) /*[[unlikely]]*/ + { + return false; + } + + if(value == 0 || interface->isOutputId(channel, value)) // 0 = no object + { + if(!m_outputs.empty()) + { + releaseOutput(*m_outputs.front()); + m_outputs.clear(); + } + if(value != 0) + { + addOutput(channel, value); + } + updateOutputActions(); + return true; + } + + return false; + }} , items{*this, "items", {}, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject} , addAddress{*this, "add_address", MethodFlags::NoScript, [this]() @@ -189,7 +262,7 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) } const uint32_t address = getUnusedAddress(); addresses.appendInternal(address); - m_outputs.emplace_back(interface->getOutput(channel, address, parent())); + addOutput(channel, address); addressesSizeChanged(); } }} @@ -201,7 +274,7 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) addresses.eraseInternal(addresses.size() - 1); if(m_outputs.back()) { - interface->releaseOutput(*m_outputs.back(), parent()); + releaseOutput(*m_outputs.back()); } m_outputs.pop_back(); addressesSizeChanged(); @@ -228,6 +301,12 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) Attributes::addMinMax(addresses, 0, 0); m_interfaceItems.add(addresses); + Attributes::addAliases(ecosObject, tcb::span{}, tcb::span{}); + Attributes::addEnabled(ecosObject, editable); + Attributes::addValues(ecosObject, tcb::span{}); + Attributes::addVisible(ecosObject, false); + m_interfaceItems.add(ecosObject); + m_interfaceItems.add(items); Attributes::addEnabled(addAddress, false); @@ -241,26 +320,41 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) updateEnabled(); } +OutputMap::~OutputMap() +{ + m_interfaceDestroying.disconnect(); + m_outputECoSObjectsChanged.disconnect(); +} + void OutputMap::load(WorldLoader& loader, const nlohmann::json& data) { SubObject::load(loader, data); if(interface) { - for(uint32_t address : addresses) + switch(channel) { - m_outputs.emplace_back(interface->getOutput(channel, address, parent())); + case OutputChannel::Output: + case OutputChannel::Accessory: + case OutputChannel::AccessoryDCC: + case OutputChannel::AccessoryMotorola: + case OutputChannel::DCCext: + case OutputChannel::Turnout: + for(uint32_t address : addresses) + { + addOutput(channel, address); + } + break; + + case OutputChannel::ECoSObject: + if(interface->isOutputId(channel, ecosObject)) + { + addOutput(channel, ecosObject); + } + break; } - const auto outputType = interface->outputType(channel); - const auto addressCount = addresses.size(); - for(auto& item : items) - { - for(size_t i = 0; i < addressCount; i++) - { - item->outputActions.appendInternal(createOutputAction(outputType, i)); - } - } + updateOutputActions(); } } @@ -282,11 +376,22 @@ void OutputMap::worldEvent(WorldState state, WorldEvent event) void OutputMap::interfaceChanged() { - Attributes::setValues(channel, interface ? interface->outputChannels() : tcb::span{}); + const auto outputChannels = interface ? interface->outputChannels() : tcb::span{}; + Attributes::setValues(channel, outputChannels); Attributes::setVisible(channel, interface); - Attributes::setVisible(addresses, interface); - Attributes::setVisible(addAddress, interface); - Attributes::setVisible(removeAddress, interface); + + m_outputECoSObjectsChanged.disconnect(); + + if(std::find(outputChannels.begin(), outputChannels.end(), OutputChannel::ECoSObject)) + { + m_outputECoSObjectsChanged = interface->outputECoSObjectsChanged.connect( + [this]() + { + const auto aliases = interface->getOutputECoSObjects(OutputChannel::ECoSObject); + Attributes::setAliases(ecosObject, aliases.first, aliases.second); + Attributes::setValues(ecosObject, aliases.first); + }); + } channelChanged(); } @@ -295,29 +400,68 @@ void OutputMap::channelChanged() { if(interface) { - const auto addressRange = interface->outputAddressMinMax(channel); - const uint32_t addressCount = (addressRange.second - addressRange.first + 1); - Attributes::setMinMax(addresses, addressRange); - - while(addressCount < addresses.size()) // Reduce number of addresses if larger than address space. + switch(channel.value()) { - addresses.eraseInternal(addresses.size() - 1); - } - - // Make sure all addresses are in range: - for(size_t i = 0; i < addresses.size(); i++) - { - if(!inRange(addresses[i], addressRange)) + case OutputChannel::Output: + case OutputChannel::Accessory: + case OutputChannel::AccessoryDCC: + case OutputChannel::AccessoryMotorola: + case OutputChannel::DCCext: + case OutputChannel::Turnout: { - addresses.setValueInternal(i, getUnusedAddress()); + Attributes::setVisible({addresses, addAddress, removeAddress}, true); + Attributes::setVisible(ecosObject, false); + Attributes::setAliases(ecosObject, tcb::span{}, tcb::span{}); + Attributes::setValues(ecosObject, tcb::span{}); + + if(addresses.empty()) + { + const auto address = interface->getUnusedOutputAddress(channel); + addresses.appendInternal(address); + addOutput(channel, address); + updateOutputActions(); + } + + const auto addressRange = interface->outputAddressMinMax(channel); + const uint32_t addressCount = (addressRange.second - addressRange.first + 1); + Attributes::setMinMax(addresses, addressRange); + + while(addressCount < addresses.size()) // Reduce number of addresses if larger than address space. + { + addresses.eraseInternal(addresses.size() - 1); + } + + // Make sure all addresses are in range: + for(size_t i = 0; i < addresses.size(); i++) + { + if(!inRange(addresses[i], addressRange)) + { + addresses.setValueInternal(i, getUnusedAddress()); + } + } + + addressesSizeChanged(); + break; + } + case OutputChannel::ECoSObject: + { + Attributes::setVisible({addresses, addAddress, removeAddress}, false); + Attributes::setVisible(ecosObject, true); + const auto aliases = interface->getOutputECoSObjects(channel); + Attributes::setAliases(ecosObject, aliases.first, aliases.second); + Attributes::setValues(ecosObject, aliases.first); + + updateOutputActions(); + break; } } - - addressesSizeChanged(); } else { + Attributes::setVisible({addresses, addAddress, removeAddress, ecosObject}, false); Attributes::setMinMax(addresses, std::numeric_limits::min(), std::numeric_limits::max()); + Attributes::setAliases(ecosObject, tcb::span{}, tcb::span{}); + Attributes::setValues(ecosObject, tcb::span{}); } } @@ -326,26 +470,36 @@ void OutputMap::addressesSizeChanged() Attributes::setDisplayName(addresses, addresses.size() == 1 ? DisplayName::Hardware::address : DisplayName::Hardware::addresses); assert(addresses.size() == m_outputs.size()); - const auto outputType = interface->outputType(channel); + updateOutputActions(); + + updateEnabled(); +} + +void OutputMap::updateOutputActions() +{ + assert(interface); + updateOutputActions(interface->outputType(channel)); +} + +void OutputMap::updateOutputActions(OutputType outputType) +{ for(const auto& item : items) { - while(addresses.size() > item->outputActions.size()) + while(m_outputs.size() > item->outputActions.size()) { std::shared_ptr outputAction = createOutputAction(outputType, item->outputActions.size()); assert(outputAction); item->outputActions.appendInternal(outputAction); } - while(addresses.size() < item->outputActions.size()) + while(m_outputs.size() < item->outputActions.size()) { item->outputActions.back()->destroy(); item->outputActions.removeInternal(item->outputActions.back()); } - assert(addresses.size() == item->outputActions.size()); + assert(m_outputs.size() == item->outputActions.size()); } - - updateEnabled(); } void OutputMap::updateEnabled() @@ -357,6 +511,7 @@ void OutputMap::updateEnabled() Attributes::setEnabled(addresses, editable); Attributes::setEnabled(addAddress, editable && addresses.size() < addressesSizeMax); Attributes::setEnabled(removeAddress, editable && addresses.size() > addressesSizeMin); + Attributes::setEnabled(ecosObject, editable); } uint32_t OutputMap::getUnusedAddress() const @@ -390,7 +545,34 @@ std::shared_ptr OutputMap::createOutputAction(OutputType case OutputType::Aspect: return std::make_shared(*this, index); + + case OutputType::ECoSState: + return std::make_shared(*this, index); } assert(false); return {}; } + +void OutputMap::addOutput(OutputChannel ch, uint32_t id) +{ + addOutput(ch, id, *interface); +} + +void OutputMap::addOutput(OutputChannel ch, uint32_t id, OutputController& outputController) +{ + m_outputs.emplace_back(getOutput(ch, id, outputController)); + assert(m_outputs.back()); +} + +std::shared_ptr OutputMap::getOutput(OutputChannel ch, uint32_t id, OutputController& outputController) +{ + auto output = outputController.getOutput(ch, id, parent()); + // TODO: connect output value changed + return output; +} + +void OutputMap::releaseOutput(Output& output) +{ + // TODO: disconnect output value changed + interface->releaseOutput(output, parent()); +} diff --git a/server/src/hardware/output/map/outputmap.hpp b/server/src/hardware/output/map/outputmap.hpp index 67ba9976..f67b18d1 100644 --- a/server/src/hardware/output/map/outputmap.hpp +++ b/server/src/hardware/output/map/outputmap.hpp @@ -47,6 +47,14 @@ class OutputMap : public SubObject static constexpr size_t addressesSizeMin = 1; static constexpr size_t addressesSizeMax = 8; + boost::signals2::connection m_interfaceDestroying; + boost::signals2::connection m_outputECoSObjectsChanged; + + void addOutput(OutputChannel ch, uint32_t id); + void addOutput(OutputChannel ch, uint32_t id, OutputController& outputController); + std::shared_ptr getOutput(OutputChannel ch, uint32_t id, OutputController& outputController); + void releaseOutput(Output& output); + protected: Outputs m_outputs; @@ -57,6 +65,8 @@ class OutputMap : public SubObject void interfaceChanged(); void channelChanged(); void addressesSizeChanged(); + void updateOutputActions(); + void updateOutputActions(OutputType outputType); void updateEnabled(); uint32_t getUnusedAddress() const; @@ -66,11 +76,13 @@ class OutputMap : public SubObject ObjectProperty interface; Property channel; VectorProperty addresses; + Property ecosObject; ObjectVectorProperty items; Method addAddress; Method removeAddress; OutputMap(Object& _parent, std::string_view parentPropertyName); + ~OutputMap() override; const std::shared_ptr& output(size_t index) const { diff --git a/server/src/hardware/output/map/outputmapecosstateoutputaction.cpp b/server/src/hardware/output/map/outputmapecosstateoutputaction.cpp new file mode 100644 index 00000000..0d935900 --- /dev/null +++ b/server/src/hardware/output/map/outputmapecosstateoutputaction.cpp @@ -0,0 +1,57 @@ +/** + * server/src/hardware/output/map/outputmapecosstateoutputaction.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 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 "outputmapecosstateoutputaction.hpp" +#include "../ecosstateoutput.hpp" +#include "../../../core/attributes.hpp" +#include "../../../core/method.tpp" +#include "../../../world/world.hpp" + +OutputMapECoSStateOutputAction::OutputMapECoSStateOutputAction(OutputMap& parent_, size_t outputIndex) + : OutputMapOutputAction(parent_, outputIndex) + , state{this, "state", 0, PropertyFlags::ReadWrite | PropertyFlags::Store} +{ + const bool editable = contains(world().state.value(), WorldState::Edit); + + Attributes::addEnabled(state, editable); + m_interfaceItems.add(state); +} + +void OutputMapECoSStateOutputAction::execute() +{ + ecosStateOutput().setValue(state); +} + +void OutputMapECoSStateOutputAction::worldEvent(WorldState worldState, WorldEvent event) +{ + OutputMapOutputAction::worldEvent(worldState, event); + + const bool editable = contains(worldState, WorldState::Edit); + + Attributes::setEnabled(state, editable); +} + +ECoSStateOutput& OutputMapECoSStateOutputAction::ecosStateOutput() +{ + assert(dynamic_cast(&output())); + return static_cast(output()); +} diff --git a/server/src/hardware/output/map/outputmapecosstateoutputaction.hpp b/server/src/hardware/output/map/outputmapecosstateoutputaction.hpp new file mode 100644 index 00000000..e6c4a1a4 --- /dev/null +++ b/server/src/hardware/output/map/outputmapecosstateoutputaction.hpp @@ -0,0 +1,48 @@ +/** + * server/src/hardware/output/map/outputmapecosstateoutputaction.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 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_OUTPUT_MAP_OUTPUTMAPECOSSTATEOUTPUTACTION_HPP +#define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_MAP_OUTPUTMAPECOSSTATEOUTPUTACTION_HPP + +#include "outputmapoutputaction.hpp" + +class ECoSStateOutput; + +class OutputMapECoSStateOutputAction final : public OutputMapOutputAction +{ + CLASS_ID("output_map_output_action.aspect") + + private: + ECoSStateOutput& ecosStateOutput(); + + protected: + void worldEvent(WorldState worldState, WorldEvent event) final; + + public: + Property state; + + OutputMapECoSStateOutputAction(OutputMap& _parent, size_t outputIndex); + + void execute() final; +}; + +#endif diff --git a/server/src/hardware/output/output.cpp b/server/src/hardware/output/output.cpp index 9029f999..350f5bef 100644 --- a/server/src/hardware/output/output.cpp +++ b/server/src/hardware/output/output.cpp @@ -31,18 +31,15 @@ #include "../../log/log.hpp" #include "../../utils/displayname.hpp" -Output::Output(std::shared_ptr outputController, OutputChannel channel_, OutputType type_, uint32_t address_) +Output::Output(std::shared_ptr outputController, OutputChannel channel_, OutputType type_) : interface{this, "interface", std::move(outputController), PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} , channel{this, "channel", channel_, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} , type{this, "type", type_, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} - , address{this, "address", address_, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} { m_interfaceItems.add(interface); Attributes::addValues(channel, outputChannelValues); m_interfaceItems.add(channel); - - m_interfaceItems.add(address); } std::string Output::getObjectId() const diff --git a/server/src/hardware/output/output.hpp b/server/src/hardware/output/output.hpp index 57a80be1..019fe73d 100644 --- a/server/src/hardware/output/output.hpp +++ b/server/src/hardware/output/output.hpp @@ -41,13 +41,10 @@ class Output : public Object friend class OutputController; private: - static constexpr uint32_t addressMinDefault = 0; - static constexpr uint32_t addressMaxDefault = 1'000'000; - std::set> m_usedBy; //!< Objects that use the output. protected: - Output(std::shared_ptr outputController, OutputChannel channel_, OutputType type_, uint32_t address_); + Output(std::shared_ptr outputController, OutputChannel channel_, OutputType type_); public: static constexpr uint32_t invalidAddress = std::numeric_limits::max(); @@ -55,7 +52,13 @@ class Output : public Object ObjectProperty interface; Property channel; Property type; - Property address; + + /** + * \brief Unique identifier for the output within the channel. + * + * \return Unique identifier, can be any number/mask. + */ + virtual uint32_t id() const = 0; std::string getObjectId() const final; }; diff --git a/server/src/hardware/output/outputcontroller.cpp b/server/src/hardware/output/outputcontroller.cpp index 7508d6df..ddf1f43a 100644 --- a/server/src/hardware/output/outputcontroller.cpp +++ b/server/src/hardware/output/outputcontroller.cpp @@ -24,6 +24,7 @@ #include "singleoutput.hpp" #include "pairoutput.hpp" #include "aspectoutput.hpp" +#include "ecosstateoutput.hpp" #include "list/outputlist.hpp" #include "list/outputlisttablemodel.hpp" #include "keyboard/singleoutputkeyboard.hpp" @@ -59,6 +60,9 @@ OutputType OutputController::outputType(OutputChannel channel) const case OutputChannel::DCCext: return OutputType::Aspect; + + case OutputChannel::ECoSObject: + return OutputType::ECoSState; } assert(false); return static_cast(0); @@ -80,11 +84,20 @@ std::pair OutputController::outputAddressMinMax(OutputChanne case OutputChannel::Accessory: case OutputChannel::Turnout: break; + + case OutputChannel::ECoSObject: + return noAddressMinMax; } assert(false); return {0, 0}; } +std::pair, tcb::span> OutputController::getOutputECoSObjects(OutputChannel /*channel*/) const +{ + assert(false); + return {{}, {}}; +} + bool OutputController::isOutputChannel(OutputChannel channel) const { const auto channels = outputChannels(); @@ -99,12 +112,31 @@ bool OutputController::isOutputChannel(OutputChannel channel) const return false; } -bool OutputController::isOutputAddressAvailable(OutputChannel channel, uint32_t address) const +bool OutputController::isOutputId(OutputChannel channel, uint32_t id) const +{ + switch(channel) + { + case OutputChannel::AccessoryDCC: + case OutputChannel::DCCext: + case OutputChannel::AccessoryMotorola: + case OutputChannel::Output: + case OutputChannel::Accessory: + case OutputChannel::Turnout: + return inRange(id, outputAddressMinMax(channel)); // id == address + + case OutputChannel::ECoSObject: + assert(false); + break; + } + return false; +} + +bool OutputController::isOutputAvailable(OutputChannel channel, uint32_t id) const { assert(isOutputChannel(channel)); return - inRange(address, outputAddressMinMax(channel)) && - m_outputs.find({channel, address}) == m_outputs.end(); + isOutputId(channel, id) && + m_outputs.find({channel, id}) == m_outputs.end(); } uint32_t OutputController::getUnusedOutputAddress(OutputChannel channel) const @@ -115,18 +147,18 @@ uint32_t OutputController::getUnusedOutputAddress(OutputChannel channel) const for(uint32_t address = range.first; address < range.second; address++) if(m_outputs.find({channel, address}) == end) return address; - return Output::invalidAddress; + return AddressOutput::invalidAddress; } -std::shared_ptr OutputController::getOutput(OutputChannel channel, uint32_t address, Object& usedBy) +std::shared_ptr OutputController::getOutput(OutputChannel channel, uint32_t id, Object& usedBy) { - if(!isOutputChannel(channel) || !inRange(address, outputAddressMinMax(channel))) + if(!isOutputChannel(channel) || !isOutputId(channel, id)) { return {}; } // Check if already exists: - if(auto it = m_outputs.find({channel, address}); it != m_outputs.end()) + if(auto it = m_outputs.find({channel, id}); it != m_outputs.end()) { it->second->m_usedBy.emplace(usedBy.shared_from_this()); return it->second; @@ -137,20 +169,24 @@ std::shared_ptr OutputController::getOutput(OutputChannel channel, uint3 switch(outputType(channel)) { case OutputType::Single: - output = std::make_shared(shared_ptr(), channel, address); + output = std::make_shared(shared_ptr(), channel, id); break; case OutputType::Pair: - output = std::make_shared(shared_ptr(), channel, address); + output = std::make_shared(shared_ptr(), channel, id); break; case OutputType::Aspect: - output = std::make_shared(shared_ptr(), channel, address); + output = std::make_shared(shared_ptr(), channel, id); + break; + + case OutputType::ECoSState: + output = std::make_shared(shared_ptr(), channel, id); break; } assert(output); output->m_usedBy.emplace(usedBy.shared_from_this()); - m_outputs.emplace(OutputMapKey{channel, address}, output); + m_outputs.emplace(OutputMapKey{channel, id}, output); outputs->addObject(output); getWorld(outputs.object()).outputs->addObject(output); return output; @@ -162,7 +198,7 @@ void OutputController::releaseOutput(Output& output, Object& usedBy) output.m_usedBy.erase(usedBy.shared_from_this()); if(output.m_usedBy.empty()) { - m_outputs.erase({output.channel.value(), output.address.value()}); + m_outputs.erase({output.channel.value(), output.id()}); outputs->removeObject(outputShared); getWorld(outputs.object()).outputs->removeObject(outputShared); outputShared->destroy(); @@ -170,10 +206,10 @@ void OutputController::releaseOutput(Output& output, Object& usedBy) } } -void OutputController::updateOutputValue(OutputChannel channel, uint32_t address, OutputValue value) +void OutputController::updateOutputValue(OutputChannel channel, uint32_t id, OutputValue value) { assert(isOutputChannel(channel)); - if(auto it = m_outputs.find({channel, address}); it != m_outputs.end()) + if(auto it = m_outputs.find({channel, id}); it != m_outputs.end()) { if(auto* single = dynamic_cast(it->second.get())) { @@ -187,11 +223,15 @@ void OutputController::updateOutputValue(OutputChannel channel, uint32_t address { aspect->updateValue(std::get(value)); } + else if(auto* ecosState = dynamic_cast(it->second.get())) + { + ecosState->updateValue(std::get(value)); + } } if(auto keyboard = m_outputKeyboards[channel].lock()) { - keyboard->fireOutputValueChanged(address, value); + keyboard->fireOutputValueChanged(id, value); } } @@ -205,6 +245,7 @@ bool OutputController::hasOutputKeyboard(OutputChannel channel) const return true; case OutputType::Aspect: + case OutputType::ECoSState: return false; } assert(false); @@ -228,6 +269,7 @@ std::shared_ptr OutputController::outputKeyboard(OutputChannel c break; case OutputType::Aspect: /*[[unlikely]]*/ + case OutputType::ECoSState: /*[[unlikely]]*/ break; // not supported (yet) } assert(keyboard); @@ -250,7 +292,8 @@ void OutputController::destroying() { const auto& output = outputs->front(); assert(output->interface.value() == std::dynamic_pointer_cast(object.shared_from_this())); - output->interface = nullptr; // removes object form the list + output->interface.setValueInternal(nullptr); + outputs->removeObject(output); } object.world().outputControllers->remove(std::dynamic_pointer_cast(object.shared_from_this())); } diff --git a/server/src/hardware/output/outputcontroller.hpp b/server/src/hardware/output/outputcontroller.hpp index 6fd8c31a..5c89f180 100644 --- a/server/src/hardware/output/outputcontroller.hpp +++ b/server/src/hardware/output/outputcontroller.hpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -46,14 +47,16 @@ enum class OutputListColumn; class OutputController { public: + static constexpr std::pair noAddressMinMax{std::numeric_limits::max(), std::numeric_limits::min()}; + struct OutputMapKey { OutputChannel channel; - uint32_t address; + uint32_t id; inline bool operator ==(const OutputMapKey other) const noexcept { - return channel == other.channel && address == other.address; + return channel == other.channel && id == other.id; } }; static_assert(sizeof(OutputMapKey) == sizeof(uint64_t)); @@ -62,7 +65,7 @@ class OutputController { std::size_t operator()(const OutputMapKey& value) const noexcept { - return (static_cast(value.channel) << (8 * sizeof(value.address))) | value.address; + return (static_cast(value.channel) << (8 * sizeof(value.id))) | value.id; } }; @@ -82,6 +85,8 @@ class OutputController void destroying(); public: + boost::signals2::signal outputECoSObjectsChanged; + ObjectProperty outputs; /** @@ -99,6 +104,11 @@ class OutputController */ bool isOutputChannel(OutputChannel channel) const; + /** + * + */ + virtual bool isOutputId(OutputChannel channel, uint32_t id) const; + /** * */ @@ -112,7 +122,12 @@ class OutputController /** * */ - [[nodiscard]] virtual bool isOutputAddressAvailable(OutputChannel channel, uint32_t address) const; + virtual std::pair, tcb::span> getOutputECoSObjects(OutputChannel channel) const; + + /** + * + */ + [[nodiscard]] virtual bool isOutputAvailable(OutputChannel channel, uint32_t id) const; /** * \brief Get the next unused output address @@ -125,18 +140,18 @@ class OutputController /** * \brief Get an output. * - * For each channel/address combination an output object is create once, + * For each channel/id combination an output object is create once, * if an output is requested multiple time they all share the same instance. * Once the object the uses the output is no longer using it, * it must be released using \ref releaseOutput . * The output object will be destroyed when the are zero users. * * \param[in] channel The output channel. - * \param[in] address The output address. + * \param[in] id The output id. * \param[in] usedBy The object the will use the output. - * \return An output object if the channel/address combination is valid, \c nullptr otherwise. + * \return An output object if the channel/id combination is valid, \c nullptr otherwise. */ - std::shared_ptr getOutput(OutputChannel channel, uint32_t address, Object& usedBy); + std::shared_ptr getOutput(OutputChannel channel, uint32_t id, Object& usedBy); /** * \brief Release an output. @@ -150,7 +165,7 @@ class OutputController /** * @brief ... */ - [[nodiscard]] virtual bool setOutputValue(OutputChannel /*channel*/, uint32_t /*address*/, OutputValue /*value*/) = 0; + [[nodiscard]] virtual bool setOutputValue(OutputChannel /*channel*/, uint32_t /*id*/, OutputValue /*value*/) = 0; /** * @brief Update the output value @@ -158,10 +173,10 @@ class OutputController * This function should be called by the hardware layer whenever the output value changes. * * @param[in] channel Output channel - * @param[in] address Output address + * @param[in] id Output id * @param[in] value New output value */ - void updateOutputValue(OutputChannel channel, uint32_t address, OutputValue value); + void updateOutputValue(OutputChannel channel, uint32_t id, OutputValue value); /** * \brief Check is there is an output keyboard available for the channel. diff --git a/server/src/hardware/output/outputvalue.hpp b/server/src/hardware/output/outputvalue.hpp index 02c48bd8..6dfecc19 100644 --- a/server/src/hardware/output/outputvalue.hpp +++ b/server/src/hardware/output/outputvalue.hpp @@ -28,6 +28,6 @@ #include #include -using OutputValue = std::variant; +using OutputValue = std::variant; #endif diff --git a/server/src/hardware/output/pairoutput.cpp b/server/src/hardware/output/pairoutput.cpp index 1d132646..88662469 100644 --- a/server/src/hardware/output/pairoutput.cpp +++ b/server/src/hardware/output/pairoutput.cpp @@ -27,7 +27,7 @@ #include "../../core/objectproperty.tpp" PairOutput::PairOutput(std::shared_ptr outputController, OutputChannel channel_, uint32_t address_) - : Output(std::move(outputController), channel_, OutputType::Pair, address_) + : AddressOutput(std::move(outputController), channel_, OutputType::Pair, address_) , value{this, "value", OutputPairValue::Undefined, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} , setValue{*this, "set_value", MethodFlags::ScriptCallable, [this](OutputPairValue newValue) diff --git a/server/src/hardware/output/pairoutput.hpp b/server/src/hardware/output/pairoutput.hpp index a40a86a3..d66a18c4 100644 --- a/server/src/hardware/output/pairoutput.hpp +++ b/server/src/hardware/output/pairoutput.hpp @@ -23,12 +23,12 @@ #ifndef TRAINTASTIC_SERVER_HARDWARE_OUTPUT_PAIROUTPUT_HPP #define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_PAIROUTPUT_HPP -#include "output.hpp" +#include "addressoutput.hpp" #include #include "../../core/method.hpp" #include "../../core/event.hpp" -class PairOutput final : public Output +class PairOutput final : public AddressOutput { friend class OutputController; diff --git a/server/src/hardware/output/singleoutput.cpp b/server/src/hardware/output/singleoutput.cpp index 0fd7cc8b..0c1525e2 100644 --- a/server/src/hardware/output/singleoutput.cpp +++ b/server/src/hardware/output/singleoutput.cpp @@ -27,14 +27,14 @@ #include "../../core/objectproperty.tpp" SingleOutput::SingleOutput(std::shared_ptr outputController, OutputChannel channel_, uint32_t address_) - : Output(std::move(outputController), channel_, OutputType::Pair, address_) + : AddressOutput(std::move(outputController), channel_, OutputType::Pair, address_) , value{this, "value", TriState::Undefined, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} , setValue{*this, "set_value", MethodFlags::ScriptCallable, [this](bool newValue) { return interface && - interface->setOutputValue(channel, address, newValue); + interface->setOutputValue(channel, address, toTriState(newValue)); }} , onValueChanged{*this, "on_value_changed", EventFlags::Scriptable} { diff --git a/server/src/hardware/output/singleoutput.hpp b/server/src/hardware/output/singleoutput.hpp index a042725b..6d0593e1 100644 --- a/server/src/hardware/output/singleoutput.hpp +++ b/server/src/hardware/output/singleoutput.hpp @@ -23,12 +23,12 @@ #ifndef TRAINTASTIC_SERVER_HARDWARE_OUTPUT_SINGLEOUTPUT_HPP #define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_SINGLEOUTPUT_HPP -#include "output.hpp" +#include "addressoutput.hpp" #include "../../core/method.hpp" #include "../../core/event.hpp" #include "../../enum/tristate.hpp" -class SingleOutput : public Output +class SingleOutput : public AddressOutput { friend class OutputController; diff --git a/server/src/hardware/protocol/ecos/kernel.cpp b/server/src/hardware/protocol/ecos/kernel.cpp index b90a0981..d39322b8 100644 --- a/server/src/hardware/protocol/ecos/kernel.cpp +++ b/server/src/hardware/protocol/ecos/kernel.cpp @@ -22,6 +22,7 @@ #include "kernel.hpp" #include +#include #include "messages.hpp" #include "simulation.hpp" #include "object/ecos.hpp" @@ -104,6 +105,17 @@ void Kernel::setOnGo(std::function callback) m_onGo = std::move(callback); } +void Kernel::setOnObjectChanged(OnObjectChanged callback) +{ + assert(!m_started); + m_onObjectChanged = std::move(callback); +} + +void Kernel::setOnObjectRemoved(OnObjectRemoved callback) +{ + assert(!m_started); + m_onObjectRemoved = std::move(callback); +} void Kernel::setDecoderController(DecoderController* decoderController) { assert(!m_started); @@ -379,33 +391,44 @@ void Kernel::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, } } -bool Kernel::setOutput(OutputChannel channel, uint16_t address, OutputPairValue value) +bool Kernel::setOutput(OutputChannel channel, uint32_t id, OutputValue value) { - auto switchProtocol = SwitchProtocol::Unknown; + assert(isEventLoopThread()); switch(channel) { case OutputChannel::AccessoryDCC: - switchProtocol = SwitchProtocol::DCC; - break; - case OutputChannel::AccessoryMotorola: - switchProtocol = SwitchProtocol::Motorola; - break; - + { + const auto switchProtocol = (channel == OutputChannel::AccessoryDCC) ? SwitchProtocol::DCC : SwitchProtocol::Motorola; + m_ioContext.post( + [this, switchProtocol, address=id, port=(std::get(value) == OutputPairValue::Second)]() + { + switchManager().setSwitch(switchProtocol, address, port); + }); + return true; + } + case OutputChannel::ECoSObject: + { + m_ioContext.post( + [this, id, state=std::get(value)]() + { + if(auto it = m_objects.find(id); it != m_objects.end()) + { + if(auto* sw = dynamic_cast(it->second.get())) + { + sw->setState(state); + } + } + }); + return true; + } default: /*[[unlikely]]*/ assert(false); - return false; + break; } - assert(switchProtocol != SwitchProtocol::Unknown); - - m_ioContext.post( - [this, switchProtocol, address, value]() - { - switchManager().setSwitch(switchProtocol, address, value == OutputPairValue::Second); - }); - return true; + return false; } void Kernel::simulateInputChange(uint32_t channel, uint32_t address, SimulateInputAction action) @@ -510,6 +533,20 @@ void Kernel::switchManagerSwitched(SwitchProtocol protocol, uint16_t address, Ou } } +void Kernel::switchStateChanged(uint16_t objectId, uint8_t state) +{ + ASSERT_IS_KERNEL_THREAD; + + if(!m_outputController) + return; + + EventLoop::call( + [this, objectId, state]() + { + m_outputController->updateOutputValue(OutputChannel::ECoSObject, objectId, state); + }); +} + void Kernel::feedbackStateChanged(Feedback& object, uint8_t port, TriState value) { if(!m_inputController) @@ -563,6 +600,46 @@ void Kernel::setIOHandler(std::unique_ptr handler) m_ioHandler = std::move(handler); } +bool Kernel::objectExists(uint16_t objectId) const +{ + return m_objects.find(objectId) != m_objects.end(); +} + +void Kernel::addObject(std::unique_ptr object) +{ + objectChanged(*object); + m_objects.add(std::move(object)); +} + +void Kernel::objectChanged(Object& object) +{ + if(!m_onObjectChanged) /*[[unlikely]]*/ + { + return; + } + + std::string objectName; + if(auto* sw = dynamic_cast(&object)) + { + objectName = sw->nameUI(); + } + + EventLoop::call( + [this, typeHash=typeid(object).hash_code(), objectId=object.id(), objectName]() + { + m_onObjectChanged(typeHash, objectId, objectName); + }); +} + +void Kernel::removeObject(uint16_t objectId) +{ + m_objects.erase(objectId); + if(m_onObjectRemoved) /*[[likely]]*/ + { + m_onObjectRemoved(objectId); + } +} + void Kernel::send(std::string_view message) { if(m_ioHandler->send(message)) diff --git a/server/src/hardware/protocol/ecos/kernel.hpp b/server/src/hardware/protocol/ecos/kernel.hpp index 7acf677e..51df5e20 100644 --- a/server/src/hardware/protocol/ecos/kernel.hpp +++ b/server/src/hardware/protocol/ecos/kernel.hpp @@ -55,6 +55,9 @@ class Kernel : public ::KernelBase friend class ECoS; public: + using OnObjectChanged = std::function; + using OnObjectRemoved = std::function; + static constexpr uint16_t s88AddressMin = 1; static constexpr uint16_t s88AddressMax = 1000; //!< \todo what is the maximum static constexpr uint16_t ecosDetectorAddressMin = 1; @@ -95,6 +98,8 @@ class Kernel : public ::KernelBase std::function m_onGo; std::function m_onEmergencyStop; + OnObjectChanged m_onObjectChanged; + OnObjectRemoved m_onObjectRemoved; DecoderController* m_decoderController; InputController* m_inputController; @@ -106,6 +111,12 @@ class Kernel : public ::KernelBase void setIOHandler(std::unique_ptr handler); + bool objectExists(uint16_t objectId) const; + void addObject(std::unique_ptr object); + void objectChanged(Object& object); + void removeObject(uint16_t objectId); + + //const std::unique_ptr ecos(); ECoS& ecos(); void ecosGoChanged(TriState value); @@ -183,6 +194,9 @@ class Kernel : public ::KernelBase */ void setOnGo(std::function callback); + void setOnObjectChanged(OnObjectChanged callback); + void setOnObjectRemoved(OnObjectRemoved callback); + /** * @brief Set the decoder controller * @param[in] decoderController The decoder controller @@ -246,15 +260,16 @@ class Kernel : public ::KernelBase /** * @brief ... * @param[in] channel Channel - * @param[in] address Output address + * @param[in] id Output id/address * @param[in] value Output value * @return \c true if send successful, \c false otherwise. */ - bool setOutput(OutputChannel channel, uint16_t address, OutputPairValue value); + bool setOutput(OutputChannel channel, uint32_t id, OutputValue value); void simulateInputChange(uint32_t channel, uint32_t address, SimulateInputAction action); void switchManagerSwitched(SwitchProtocol protocol, uint16_t address, OutputPairValue value); + void switchStateChanged(uint16_t objectId, uint8_t state); void feedbackStateChanged(Feedback& object, uint8_t port, TriState value); }; diff --git a/server/src/hardware/protocol/ecos/object/object.cpp b/server/src/hardware/protocol/ecos/object/object.cpp index 2390c784..f62df460 100644 --- a/server/src/hardware/protocol/ecos/object/object.cpp +++ b/server/src/hardware/protocol/ecos/object/object.cpp @@ -75,17 +75,22 @@ void Object::send(std::string_view message) bool Object::objectExists(uint16_t objectId) const { - return m_kernel.m_objects.find(objectId) != m_kernel.m_objects.end(); + return m_kernel.objectExists(objectId); } void Object::addObject(std::unique_ptr object) { - m_kernel.m_objects.add(std::move(object)); + m_kernel.addObject(std::move(object)); +} + +void Object::nameChanged() +{ + m_kernel.objectChanged(*this); } void Object::removeObject(uint16_t objectId) { - m_kernel.m_objects.erase(objectId); + m_kernel.removeObject(objectId); } void Object::update(const std::vector& lines) diff --git a/server/src/hardware/protocol/ecos/object/object.hpp b/server/src/hardware/protocol/ecos/object/object.hpp index 2caffb67..500df035 100644 --- a/server/src/hardware/protocol/ecos/object/object.hpp +++ b/server/src/hardware/protocol/ecos/object/object.hpp @@ -48,6 +48,7 @@ class Object bool objectExists(uint16_t objectId) const; void addObject(std::unique_ptr object); + void nameChanged(); void removeObject(uint16_t objectId); virtual void update(std::string_view /*option*/, std::string_view /*value*/){}// = 0; diff --git a/server/src/hardware/protocol/ecos/object/switch.cpp b/server/src/hardware/protocol/ecos/object/switch.cpp index a28275c2..3f7468e2 100644 --- a/server/src/hardware/protocol/ecos/object/switch.cpp +++ b/server/src/hardware/protocol/ecos/object/switch.cpp @@ -79,6 +79,17 @@ bool Switch::receiveEvent(const Event& event) return Object::receiveEvent(event); } +std::string Switch::nameUI() const +{ + return + std::string(m_name1) + .append(" (") + .append(toString(m_protocol)) + .append(" ") + .append(std::to_string(m_address)) + .append(")"); +} + void Switch::setState(uint8_t value) { send(set(m_id, Option::state, value)); @@ -89,6 +100,7 @@ void Switch::update(std::string_view option, std::string_view value) if(option == Option::name1) { m_name1 = value; + nameChanged(); } else if(option == Option::name2) { @@ -101,6 +113,7 @@ void Switch::update(std::string_view option, std::string_view value) else if(option == Option::addr) { fromChars(value, m_address); + nameChanged(); } else if(option == Option::addrext) { @@ -141,11 +154,14 @@ void Switch::update(std::string_view option, std::string_view value) else if(option == Option::protocol) { fromString(value, m_protocol); + nameChanged(); } else if(option == Option::state) { fromChars(value, m_state); + m_kernel.switchStateChanged(m_id, m_state); + if(m_state < m_addrext.size()) { const auto& port = m_addrext[m_state]; diff --git a/server/src/hardware/protocol/ecos/object/switch.hpp b/server/src/hardware/protocol/ecos/object/switch.hpp index 4467a4cc..66b281b7 100644 --- a/server/src/hardware/protocol/ecos/object/switch.hpp +++ b/server/src/hardware/protocol/ecos/object/switch.hpp @@ -77,6 +77,7 @@ class Switch final : public Object bool receiveReply(const Reply& reply) final; bool receiveEvent(const Event& event) final; + std::string nameUI() const; const std::string& name1() const { return m_name1; } const std::string& name2() const { return m_name2; } const std::string& name3() const { return m_name3; } diff --git a/server/src/hardware/protocol/traintasticdiy/kernel.cpp b/server/src/hardware/protocol/traintasticdiy/kernel.cpp index 925ebc99..a7d7eb3d 100644 --- a/server/src/hardware/protocol/traintasticdiy/kernel.cpp +++ b/server/src/hardware/protocol/traintasticdiy/kernel.cpp @@ -373,7 +373,7 @@ void Kernel::receive(const Message& message) [this]() { for(const auto& it : m_outputController->outputMap()) - postSend(GetOutputState(static_cast(it.first.address))); + postSend(GetOutputState(static_cast(it.first.id))); }); break; } diff --git a/server/src/lua/object/interface.cpp b/server/src/lua/object/interface.cpp index 033446aa..569fcada 100644 --- a/server/src/lua/object/interface.cpp +++ b/server/src/lua/object/interface.cpp @@ -78,9 +78,9 @@ int Interface::get_output(lua_State* L) auto outputController = std::dynamic_pointer_cast<::OutputController>(check<::Interface>(L, lua_upvalueindex(1))); assert(outputController); auto channel = check<::OutputChannel>(L, 1); - auto address = check(L, 2); - auto& stateData = Lua::Sandbox::getStateData(L); - auto output = outputController->getOutput(channel, address, stateData.script()); + auto id = check(L, 2); + auto& stateData = Lua::Sandbox::getStateData(L); + auto output = outputController->getOutput(channel, id, stateData.script()); if(output) { stateData.registerOutput(outputController, output); diff --git a/server/src/network/session.cpp b/server/src/network/session.cpp index fce03b81..85ab8c13 100644 --- a/server/src/network/session.cpp +++ b/server/src/network/session.cpp @@ -590,6 +590,7 @@ bool Session::processMessage(const Message& message) break; case OutputType::Aspect: /*[[unlikely]]*/ + case OutputType::ECoSState: /*[[unlikely]]*/ assert(false); break; } diff --git a/server/test/hardware/outputmap.cpp b/server/test/hardware/outputmap.cpp new file mode 100644 index 00000000..13fee9ac --- /dev/null +++ b/server/test/hardware/outputmap.cpp @@ -0,0 +1,192 @@ +/** + * server/test/hardware/outputmap.cpp + * + * This file is part of the traintastic test suite. + * + * Copyright (C) 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 +#include "../../src/core/objectproperty.tpp" +#include "../../src/world/world.hpp" +#include "../../src/hardware/interface/interfacelist.hpp" +#include "../../src/hardware/interface/ecosinterface.hpp" +#include "../../src/hardware/output/map/outputmappairoutputaction.hpp" +#include "../../src/hardware/output/map/outputmapecosstateoutputaction.hpp" +#include "../../src/board/board.hpp" +#include "../../src/board/boardlist.hpp" +#include "../../src/board/tile/rail/signal/signal3aspectrailtile.hpp" +#include "../../src/board/tile/rail/turnout/turnoutleft45railtile.hpp" + +TEST_CASE("OutputMap: Channel: Accessory -> ECoSObject -> Accessory", "[outputmap]") +{ + // Create world: + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE(worldWeak.lock()->interfaces->length == 0); + REQUIRE(worldWeak.lock()->boards->length == 0); + + // Create interface: + std::weak_ptr interfaceWeak = std::dynamic_pointer_cast(world->interfaces->create(ECoSInterface::classId)); + REQUIRE_FALSE(interfaceWeak.expired()); + REQUIRE(worldWeak.lock()->interfaces->length == 1); + + // Create board: + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 1); + + // Create signal: + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg0, Signal3AspectRailTile::classId, false)); + std::weak_ptr signalWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + std::weak_ptr outputMapWeak = signalWeak.lock()->outputMap.value(); + REQUIRE_FALSE(signalWeak.expired()); + REQUIRE_FALSE(outputMapWeak.lock()->interface); + + // Assign interface: + outputMapWeak.lock()->interface = interfaceWeak.lock(); + REQUIRE(outputMapWeak.lock()->interface.value() == interfaceWeak.lock()); + REQUIRE(isAccessory(outputMapWeak.lock()->channel.value())); + REQUIRE(outputMapWeak.lock()->addresses.size() == 1); + REQUIRE(outputMapWeak.lock()->ecosObject == 0); + for(const auto& item : outputMapWeak.lock()->items) + { + REQUIRE(item->outputActions.size() == 1); + REQUIRE(std::dynamic_pointer_cast(item->outputActions[0])); + } + + // Add address: + outputMapWeak.lock()->addAddress(); + REQUIRE(outputMapWeak.lock()->addresses.size() == 2); + REQUIRE(outputMapWeak.lock()->addresses[0] != outputMapWeak.lock()->addresses[1]); + for(const auto& item : outputMapWeak.lock()->items) + { + REQUIRE(item->outputActions.size() == 2); + REQUIRE(std::dynamic_pointer_cast(item->outputActions[0])); + REQUIRE(std::dynamic_pointer_cast(item->outputActions[1])); + } + + // Change channel to ECoSObject: + outputMapWeak.lock()->channel = OutputChannel::ECoSObject; + REQUIRE(outputMapWeak.lock()->channel.value() == OutputChannel::ECoSObject); + REQUIRE(outputMapWeak.lock()->addresses.size() == 0); + REQUIRE(outputMapWeak.lock()->ecosObject == 0); + for(const auto& item : outputMapWeak.lock()->items) + { + REQUIRE(item->outputActions.empty()); + } + + // Select ECoS object: + outputMapWeak.lock()->ecosObject = 20000; + REQUIRE(outputMapWeak.lock()->ecosObject == 20000); + for(const auto& item : outputMapWeak.lock()->items) + { + REQUIRE(item->outputActions.size() == 1); + REQUIRE(std::dynamic_pointer_cast(item->outputActions[0])); + } + + // Change channel to AccessoryMotorola: + outputMapWeak.lock()->channel = OutputChannel::AccessoryMotorola; + REQUIRE(outputMapWeak.lock()->channel.value() == OutputChannel::AccessoryMotorola); + REQUIRE(outputMapWeak.lock()->addresses.size() == 1); + REQUIRE(outputMapWeak.lock()->ecosObject == 0); + for(const auto& item : outputMapWeak.lock()->items) + { + REQUIRE(item->outputActions.size() == 1); + REQUIRE(std::dynamic_pointer_cast(item->outputActions[0])); + } + + // cleanup: + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(interfaceWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(signalWeak.expired()); + REQUIRE(outputMapWeak.expired()); +} + +TEST_CASE("OutputMap: Channel: preserve mapping", "[outputmap]") +{ + // Create world: + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE(worldWeak.lock()->interfaces->length == 0); + REQUIRE(worldWeak.lock()->boards->length == 0); + + // Create interface: + std::weak_ptr interfaceWeak = std::dynamic_pointer_cast(world->interfaces->create(ECoSInterface::classId)); + REQUIRE_FALSE(interfaceWeak.expired()); + REQUIRE(worldWeak.lock()->interfaces->length == 1); + + // Create board: + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 1); + + // Create turnout: + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg0, TurnoutLeft45RailTile::classId, false)); + std::weak_ptr turnoutWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + std::weak_ptr outputMapWeak = turnoutWeak.lock()->outputMap.value(); + REQUIRE_FALSE(turnoutWeak.expired()); + REQUIRE_FALSE(outputMapWeak.lock()->interface); + + // Assign interface: + outputMapWeak.lock()->interface = interfaceWeak.lock(); + REQUIRE(outputMapWeak.lock()->interface.value() == interfaceWeak.lock()); + REQUIRE(outputMapWeak.lock()->channel.value() == OutputChannel::AccessoryDCC); + REQUIRE(outputMapWeak.lock()->addresses.size() == 1); + REQUIRE(outputMapWeak.lock()->ecosObject == 0); + for(const auto& item : outputMapWeak.lock()->items) + { + REQUIRE(item->outputActions.size() == 1); + REQUIRE(std::dynamic_pointer_cast(item->outputActions[0])); + } + + // Assign output actions: + std::weak_ptr outputActionWeakA = std::dynamic_pointer_cast(outputMapWeak.lock()->items[0]->outputActions[0]); + std::weak_ptr outputActionWeakB = std::dynamic_pointer_cast(outputMapWeak.lock()->items[1]->outputActions[0]); + outputActionWeakA.lock()->action = PairOutputAction::First; + outputActionWeakB.lock()->action = PairOutputAction::Second; + REQUIRE(outputActionWeakA.lock()->action.value() == PairOutputAction::First); + REQUIRE(outputActionWeakB.lock()->action.value() == PairOutputAction::Second); + + // Change channel to AccessoryMotorola: + outputMapWeak.lock()->channel = OutputChannel::AccessoryMotorola; + REQUIRE(outputMapWeak.lock()->channel.value() == OutputChannel::AccessoryMotorola); + REQUIRE_FALSE(outputActionWeakA.expired()); + REQUIRE_FALSE(outputActionWeakB.expired()); + REQUIRE(outputActionWeakA.lock() == std::dynamic_pointer_cast(outputMapWeak.lock()->items[0]->outputActions[0])); + REQUIRE(outputActionWeakB.lock() == std::dynamic_pointer_cast(outputMapWeak.lock()->items[1]->outputActions[0])); + REQUIRE(outputActionWeakA.lock()->action.value() == PairOutputAction::First); + REQUIRE(outputActionWeakB.lock()->action.value() == PairOutputAction::Second); + REQUIRE(outputMapWeak.lock()->addresses.size() == 1); + REQUIRE(outputMapWeak.lock()->addresses[0] == 1); + REQUIRE(outputMapWeak.lock()->ecosObject == 0); + for(const auto& item : outputMapWeak.lock()->items) + { + REQUIRE(item->outputActions.size() == 1); + REQUIRE(std::dynamic_pointer_cast(item->outputActions[0])); + } + + // cleanup: + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(interfaceWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(turnoutWeak.expired()); + REQUIRE(outputMapWeak.expired()); +} diff --git a/shared/src/traintastic/enum/outputchannel.hpp b/shared/src/traintastic/enum/outputchannel.hpp index 19328642..6a1948fe 100644 --- a/shared/src/traintastic/enum/outputchannel.hpp +++ b/shared/src/traintastic/enum/outputchannel.hpp @@ -35,9 +35,10 @@ enum class OutputChannel : uint16_t AccessoryMotorola = 4, DCCext = 5, //!< DCCext, see RCN-213 Turnout = 6, //!< DCC-EX turnout + ECoSObject = 7, //!< ECoS switch object }; -TRAINTASTIC_ENUM(OutputChannel, "output_channel", 6, +TRAINTASTIC_ENUM(OutputChannel, "output_channel", 7, { {OutputChannel::Output, "output"}, {OutputChannel::Accessory, "accessory"}, @@ -45,15 +46,17 @@ TRAINTASTIC_ENUM(OutputChannel, "output_channel", 6, {OutputChannel::AccessoryMotorola, "accessory_motorola"}, {OutputChannel::DCCext, "dcc_ext"}, {OutputChannel::Turnout, "turnout"}, + {OutputChannel::ECoSObject, "ecos_object"}, }); -inline constexpr std::array outputChannelValues{{ +inline constexpr std::array outputChannelValues{{ OutputChannel::Output, OutputChannel::Accessory, OutputChannel::AccessoryDCC, OutputChannel::AccessoryMotorola, OutputChannel::DCCext, OutputChannel::Turnout, + OutputChannel::ECoSObject, }}; constexpr bool isAccessory(OutputChannel value) diff --git a/shared/src/traintastic/enum/outputtype.hpp b/shared/src/traintastic/enum/outputtype.hpp index e67284a8..0682eeeb 100644 --- a/shared/src/traintastic/enum/outputtype.hpp +++ b/shared/src/traintastic/enum/outputtype.hpp @@ -33,19 +33,22 @@ enum class OutputType : uint8_t Single = 1, Pair = 2, Aspect = 3, + ECoSState = 4, }; -TRAINTASTIC_ENUM(OutputType, "output_type", 3, +TRAINTASTIC_ENUM(OutputType, "output_type", 4, { {OutputType::Single, "single"}, {OutputType::Pair, "pair"}, {OutputType::Aspect, "aspect"}, + {OutputType::ECoSState, "ecos_state"} }); -inline constexpr std::array outputTypeValues{{ +inline constexpr std::array outputTypeValues{{ OutputType::Single, OutputType::Pair, OutputType::Aspect, + OutputType::ECoSState, }}; #endif diff --git a/shared/translations/en-us.json b/shared/translations/en-us.json index 83bc5418..19de044f 100644 --- a/shared/translations/en-us.json +++ b/shared/translations/en-us.json @@ -4466,5 +4466,25 @@ { "term": "output_channel:accessory_motorola", "definition": "Accessory (Motorola)" + }, + { + "term": "output_channel:ecos_object", + "definition": "ECoS object" + }, + { + "term": "output_map.signal:add_address", + "definition": "Add address" + }, + { + "term": "output_map.signal:remove_address", + "definition": "Remove address" + }, + { + "term": "output_map.signal:ecos_object", + "definition": "ECoS object" + }, + { + "term": "output.ecos_object:state", + "definition": "State" } ] \ No newline at end of file