Merge remote-tracking branch 'origin/master' into 144-add-zone-support

Dieser Commit ist enthalten in:
Reinder Feenstra 2025-08-25 22:43:31 +02:00
Commit 230c4f6414
99 geänderte Dateien mit 2909 neuen und 1159 gelöschten Zeilen

Datei anzeigen

@ -11,8 +11,8 @@ jobs:
matrix:
config:
- name: "windows_x64_msvc"
os: windows-2019
generator: "Visual Studio 16 2019"
os: windows-latest
generator: "Visual Studio 17 2022"
arch: "-A x64"
target: traintastic-client
jobs: 4
@ -134,7 +134,7 @@ jobs:
if: startswith(matrix.config.os, 'windows')
shell: cmd
run: |
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
windeployqt --release --no-translations -no-system-d3d-compiler --no-opengl-sw ${{github.workspace}}/client/build/${{matrix.config.build_type}}/traintastic-client.exe
# Linux only:
@ -169,8 +169,8 @@ jobs:
matrix:
config:
- name: "windows_x64_clang"
os: windows-2019
generator: "Visual Studio 16 2019"
os: windows-latest
generator: "Visual Studio 17 2022"
arch: "-A x64"
toolset: "-T ClangCL"
target: ALL_BUILD
@ -495,7 +495,7 @@ jobs:
package-innosetup:
name: package innosetup
runs-on: windows-2019
runs-on: windows-latest
needs: [build-client, build-server, build-lang, build-manual, build-manual-lua]
steps:

Datei anzeigen

@ -4,7 +4,7 @@ project(traintastic-client VERSION ${TRAINTASTIC_VERSION} DESCRIPTION "Traintast
include(GNUInstallDirs)
include(../shared/translations/traintastic-lang.cmake)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)

Datei anzeigen

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96"
height="96"
viewBox="0 0 25.399999 25.400001"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="decoder_function.mute.on.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="18.660714"
inkscape:cy="40.089286"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid3713"
spacingx="0.26458333"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-nc/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-271.59998)">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.11667824;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 13.758327,272.65817 -7.4083258,7.40843 h -5.291662 v 8.46676 h 5.291662 l 7.4083258,7.40843 z"
id="path819"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 24.341666,281.12498 -6.35,6.35"
id="path1431"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 24.341666,287.47498 -6.35,-6.35"
id="path1431-7"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc"
inkscape:transform-center-x="-3.4962799"
inkscape:transform-center-y="2.5513394" />
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 3.7 KiB

Datei anzeigen

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96"
height="96"
viewBox="0 0 25.399999 25.400001"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="decoder_function.sound.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="-9.9107143"
inkscape:cy="40.089286"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid3713"
spacingx="0.26458333"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-nc/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-271.59998)">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.11667824;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 13.758327,272.65817 -7.4083258,7.40843 h -5.291662 v 8.46676 h 5.291662 l 7.4083258,7.40843 z"
id="path819"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 21.166666,274.77498 c 5.632313,8.57716 2.581995,13.72084 0,19.05"
id="path815"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1.05833333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 16.89076,278.9655 c 2.456677,3.63531 2.282522,7.18603 0,10.66896"
id="path815-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1.05833333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 19.090696,276.93182 c 3.506813,5.12618 3.055516,10.01073 0,14.73632"
id="path815-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 4.0 KiB

Datei anzeigen

@ -69,5 +69,7 @@
<file>zone.svg</file>
<file>clear_persistent_variables.svg</file>
<file>highlight_zone.svg</file>
<file>decoder_function.mute.svg</file>
<file>decoder_function.sound.svg</file>
</qresource>
</RCC>

Datei anzeigen

@ -220,6 +220,25 @@ void Connection::serverLog(ServerLogTableModel& model, bool enable)
send(request);
}
int Connection::createObject(const QString& classId, std::function<void(const ObjectPtr&, std::optional<const Error>)> callback)
{
std::unique_ptr<Message> request{Message::newRequest(Message::Command::CreateObject)};
request->write(classId.toLatin1());
send(request,
[this, callback](const std::shared_ptr<Message> message)
{
if(!message->isError())
{
callback(readObject(*message), {});
}
else
{
callback({}, *message);
}
});
return request->requestId();
}
int Connection::getObject(const QString& id, std::function<void(const ObjectPtr&, std::optional<const Error>)> callback)
{
std::unique_ptr<Message> request{Message::newRequest(Message::Command::GetObject)};
@ -281,6 +300,31 @@ int Connection::getObject(const ObjectVectorProperty& property, uint32_t index,
return request->requestId();
}
int Connection::getObjects(const Object& objectList, uint32_t startIndex, uint32_t endIndex, std::function<void(const std::vector<ObjectPtr>&, std::optional<const Error>)> callback)
{
std::unique_ptr<Message> request{Message::newRequest(Message::Command::ObjectListGetObjects)};
request->write(objectList.handle());
request->write(startIndex);
request->write(endIndex);
send(request,
[this, size=(endIndex - startIndex + 1), callback](const std::shared_ptr<Message> message)
{
if(!message->isError())
{
std::vector<ObjectPtr> objects;
objects.reserve(size);
for(uint32_t i = 0; i < size; i++)
objects.emplace_back(readObject(*message));
callback(objects, {});
}
else
{
callback({}, *message);
}
});
return request->requestId();
}
int Connection::getObjects(const ObjectVectorProperty& property, uint32_t startIndex, uint32_t endIndex, std::function<void(const std::vector<ObjectPtr>&, std::optional<const Error>)> callback)
{
std::unique_ptr<Message> request{Message::newRequest(Message::Command::ObjectGetObjectVectorPropertyObject)};
@ -485,7 +529,7 @@ ObjectPtr Connection::readObject(const Message& message)
}
else
{
p = createObject(shared_from_this(), handle, QString::fromLatin1(message.read<QByteArray>()));
p = ::createObject(shared_from_this(), handle, QString::fromLatin1(message.read<QByteArray>()));
m_handleCounter[handle] = 1;
}
@ -779,8 +823,17 @@ void Connection::processMessage(const std::shared_ptr<Message> message)
const qlonglong value = message->read<qlonglong>();
static_cast<Property*>(property)->m_value = value;
if(valueType == ValueType::Integer)
{
if(UnitProperty* unitProperty = dynamic_cast<UnitProperty*>(property))
unitProperty->m_unitValue = message->read<qint64>();
{
const auto unit = message->read<qint64>();
if(unitProperty->m_unitValue != unit)
{
unitProperty->m_unitValue = unit;
emit unitProperty->unitChanged();
}
}
}
emit property->valueChanged();
emit property->valueChangedInt64(value);
if(value >= std::numeric_limits<int>::min() && value <= std::numeric_limits<int>::max())
@ -792,7 +845,14 @@ void Connection::processMessage(const std::shared_ptr<Message> message)
const double value = message->read<double>();
static_cast<Property*>(property)->m_value = value;
if(UnitProperty* unitProperty = dynamic_cast<UnitProperty*>(property))
unitProperty->m_unitValue = message->read<qint64>();
{
const auto unit = message->read<qint64>();
if(unitProperty->m_unitValue != unit)
{
unitProperty->m_unitValue = unit;
emit unitProperty->unitChanged();
}
}
emit property->valueChanged();
emit property->valueChangedDouble(value);
break;

Datei anzeigen

@ -135,9 +135,12 @@ class Connection : public QObject, public std::enable_shared_from_this<Connectio
void serverLog(ServerLogTableModel& model, bool enable);
[[nodiscard]] int createObject(const QString& classId, std::function<void(const ObjectPtr&, std::optional<const Error>)> callback);
[[nodiscard]] int getObject(const QString& id, std::function<void(const ObjectPtr&, std::optional<const Error>)> callback);
[[nodiscard]] int getObject(const ObjectProperty& property, std::function<void(const ObjectPtr&, std::optional<const Error>)> callback);
[[nodiscard]] int getObject(const ObjectVectorProperty& property, uint32_t index, std::function<void(const ObjectPtr&, std::optional<const Error>)> callback);
[[nodiscard]] int getObjects(const Object& objectList, uint32_t startIndex, uint32_t endIndex, std::function<void(const std::vector<ObjectPtr>&, std::optional<const Error>)> callback);
[[nodiscard]] int getObjects(const ObjectVectorProperty& property, uint32_t startIndex, uint32_t endIndex, std::function<void(const std::vector<ObjectPtr>&, std::optional<const Error>)> callback);
void releaseObject(Object* object);

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2020,2023 Reinder Feenstra
* Copyright (C) 2019-2020,2023,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -53,6 +53,9 @@ class UnitProperty : public Property
}
void setUnitValue(qint64 value);
signals:
void unitChanged();
};
#endif

Datei anzeigen

@ -54,7 +54,7 @@ QWidget* createWidgetIfCustom(const ObjectPtr& object, QWidget* parent)
return new InterfaceListWidget(object, parent);
}
else if(classId == "decoder_list")
return new ThrottleObjectListWidget(object, parent); // todo remove
return new ObjectListWidget(object, parent); // todo remove
else if(classId == "controller_list")
return new ObjectListWidget(object, parent); // todo remove
else if(classId == "rail_vehicle_list")

Datei anzeigen

@ -1,92 +0,0 @@
/**
* client/src/widget/throttle/abstractthrottlebutton.cpp
*
* 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.
*/
#include "abstractthrottlebutton.hpp"
#include <QPainter>
#include <QMouseEvent>
#include <QSvgRenderer>
#include <QFile>
#include <traintastic/enum/decoderfunctiontype.hpp>
#include <traintastic/enum/decoderfunctionfunction.hpp>
AbstractThrottleButton::AbstractThrottleButton(ObjectPtr object, QWidget* parent)
: QWidget(parent)
, m_textColor{Qt::white}
, m_object{std::move(object)}
{
}
void AbstractThrottleButton::paintEvent(QPaintEvent*)
{
QPainter painter{this};
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(Qt::white);
const double size = qMin(rect().width(), rect().height());
const double borderWidth = size / 24;
const double borderRadius = size / 6;
QRectF borderRect{borderWidth / 2, borderWidth / 2, size - borderWidth, size - borderWidth};
painter.setPen(QPen(Qt::white, borderWidth));
painter.drawRoundedRect(borderRect, borderRadius, borderRadius);
if(!m_resource.isEmpty() && QFile::exists(m_resource))
{
QSvgRenderer svg{m_resource};
const double iconSize = size * 0.75;
svg.render(&painter, QRectF((borderRect.width() - iconSize) / 2, (borderRect.height() - iconSize) / 2, iconSize, iconSize));
}
else
{
painter.setPen(m_textColor);
painter.drawText(borderRect, Qt::AlignCenter, m_text);
}
}
void AbstractThrottleButton::mousePressEvent(QMouseEvent* event)
{
m_mousePressPos = event->pos();
}
void AbstractThrottleButton::mouseReleaseEvent(QMouseEvent* event)
{
if((m_mousePressPos - event->pos()).manhattanLength() < 3)
click();
}
void AbstractThrottleButton::setResource(QString resource)
{
m_resource = std::move(resource);
update();
}
void AbstractThrottleButton::setText(QString text)
{
m_text = std::move(text);
update();
}
void AbstractThrottleButton::setTextColor(QColor color)
{
m_textColor = color;
update();
}

Datei anzeigen

@ -0,0 +1,212 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "sliderwidget.hpp"
#include <QPainter>
#include <QStyleOption>
#include <QPaintEvent>
#include <QDebug>
SliderWidget::SliderWidget(QWidget* parent)
: QWidget(parent)
{
setMinimumSize(40, 0);
setMouseTracking(true);
}
SliderWidget::SliderWidget(Qt::Orientation orientation, QWidget* parent)
: SliderWidget(parent)
{
m_orientation = orientation;
if(m_orientation == Qt::Horizontal)
{
setMinimumSize(0, 40);
}
}
float SliderWidget::minimum() const
{
return m_minimum;
}
float SliderWidget::maximum() const
{
return m_maximum;
}
float SliderWidget::value() const
{
return m_value;
}
void SliderWidget::setMinimum(float min)
{
m_minimum = min;
if(m_maximum < m_minimum)
{
m_maximum = m_minimum;
}
if(m_value < m_minimum)
{
m_value = m_minimum;
}
update();
}
void SliderWidget::setMaximum(float max)
{
m_maximum = max;
if(m_minimum > m_maximum)
{
m_minimum = m_maximum;
}
if(m_value > m_maximum)
{
m_value = m_maximum;
}
update();
}
void SliderWidget::setRange(float min, float max)
{
m_minimum = min;
m_maximum = max;
if(m_value < m_minimum)
{
m_value = m_minimum;
}
else if(m_value > m_maximum)
{
m_value = m_maximum;
}
update();
}
void SliderWidget::setValue(float val)
{
float clamped = std::clamp(val, m_minimum, m_maximum);
if(qFuzzyCompare(m_value, clamped))
{
return;
}
m_value = clamped;
update();
emit valueChanged(m_value);
}
void SliderWidget::mousePressEvent(QMouseEvent* event)
{
if(event->button() == Qt::LeftButton)
{
setSliderValueFromPosition(event->pos());
m_dragging = true;
}
}
void SliderWidget::mouseMoveEvent(QMouseEvent* event)
{
if(m_dragging)
{
setSliderValueFromPosition(event->pos());
}
}
void SliderWidget::mouseReleaseEvent(QMouseEvent* event)
{
if(event->button() == Qt::LeftButton)
{
m_dragging = false;
}
}
void SliderWidget::setSliderValueFromPosition(const QPoint& pos)
{
float ratio;
if(m_orientation == Qt::Vertical)
{
const auto height = rect().height() - 2 * handleSize;
ratio = 1.0f - (float(pos.y() - handleSize) / height);
}
else
{
const auto width = rect().width() - 2 * handleSize;
ratio = float(pos.x() - handleSize) / width;
}
setValue(m_minimum + std::clamp(ratio, 0.0f, 1.0f) * (m_maximum - m_minimum));
}
void SliderWidget::wheelEvent(QWheelEvent* event)
{
const float step = 0.01f * (m_maximum - m_minimum); // 1% of range per step
const float delta = event->angleDelta().y() > 0 ? step : -step;
setValue(m_value + delta);
event->accept();
}
void SliderWidget::paintEvent(QPaintEvent* /*event*/)
{
constexpr qreal gutterSize = 5.0;
constexpr qreal gutterRadius = (gutterSize + 1) / 2;
constexpr qreal handleRadius = (handleSize + 1) / 2;
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
// Draw gutter
QRectF gutterRect = rect();
if(m_orientation == Qt::Vertical)
{
gutterRect.adjust(0.5, handleSize / 2, -0.5, -handleSize / 2);
gutterRect.setLeft((gutterRect.width() - gutterSize) / 2);
gutterRect.setWidth(9);
}
else
{
gutterRect.adjust(handleSize / 2, 0.5, -handleSize / 2, -0.5);
gutterRect.setTop((gutterRect.height() - gutterSize) / 2);
gutterRect.setHeight(9);
}
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(33, 33, 33));
painter.drawRoundedRect(gutterRect, gutterRadius, gutterRadius);
// Calculate handle position
float ratio = (m_value - m_minimum) / (m_maximum - m_minimum);
QRectF handleRect;
if(m_orientation == Qt::Vertical)
{
const float height = rect().height() - 2 * handleSize;
const float y = height * (1.0f - ratio);
handleRect = QRectF(1, handleSize / 2 + y, width() - 2, handleSize);
}
else
{
const float width = rect().width() - 2 * handleSize;
const float x = width * ratio;
handleRect = QRectF(handleSize / 2 + x, 1, handleSize, height() - 2);
}
// Draw handle
painter.setBrush(isEnabled() ? Qt::white : QColor(180, 180, 180));
painter.drawRoundedRect(handleRect, handleRadius, handleRadius);
}

Datei anzeigen

@ -0,0 +1,66 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_THROTTLE_SLIDERWIDGET_HPP
#define TRAINTASTIC_CLIENT_WIDGET_THROTTLE_SLIDERWIDGET_HPP
#include <QWidget>
class SliderWidget final : public QWidget
{
Q_OBJECT
private:
static constexpr qreal handleSize = 12.0;
Qt::Orientation m_orientation = Qt::Vertical;
float m_minimum = 0.0f;
float m_maximum = 1.0f;
float m_value = 0.0f;
bool m_dragging = false;
void setSliderValueFromPosition(const QPoint& pos);
protected:
void mousePressEvent(QMouseEvent* event) final;
void mouseMoveEvent(QMouseEvent* event) final;
void mouseReleaseEvent(QMouseEvent* event) final;
void wheelEvent(QWheelEvent* event) final;
void paintEvent(QPaintEvent* event) final;
public:
SliderWidget(QWidget* parent = nullptr);
SliderWidget(Qt::Orientation orientation, QWidget* parent = nullptr);
float minimum() const;
float maximum() const;
float value() const;
void setMinimum(float min);
void setMaximum(float max);
void setRange(float min, float max);
void setValue(float val);
signals:
void valueChanged(float val);
};
#endif

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021,2023 Reinder Feenstra
* Copyright (C) 2021,2023,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -23,6 +23,7 @@
#include "speedometerwidget.hpp"
#include <cmath>
#include <QPainter>
#include "throttlestyle.hpp"
SpeedoMeterWidget::SpeedoMeterWidget(QWidget* parent)
: QWidget(parent)
@ -168,15 +169,15 @@ void SpeedoMeterWidget::paintEvent(QPaintEvent*)
const auto eStop = QStringLiteral("ESTOP");
font.setPixelSize(size / 12);
painter.setFont(font);
QRect r = painter.fontMetrics().boundingRect(eStop);
QRect r = painter.fontMetrics().boundingRect(eStop).adjusted(-2, 0, 2, 0);
r.moveTo(borderRect.left() + (borderRect.width() - r.width()) / 2, borderRect.top() + (borderRect.height() / 2 - r.height()) / 2);
const int margin = font.pixelSize() / 6;
r.adjust(-margin, -margin, margin, margin);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::red);
painter.setBrush(ThrottleStyle::buttonEStopColor);
const qreal radius = r.height() / 4.;
painter.drawRoundedRect(r, radius, radius);
painter.setPen(Qt::white);
painter.setPen(ThrottleStyle::buttonTextColor);
painter.drawText(r, Qt::AlignCenter, eStop);
}
}

Datei anzeigen

@ -0,0 +1,144 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "throttlebutton.hpp"
#include <QPainter>
#include <QMouseEvent>
#include <QSvgRenderer>
#include <QFile>
#include <QtMath>
#include <traintastic/enum/decoderfunctiontype.hpp>
#include <traintastic/enum/decoderfunctionfunction.hpp>
#include "throttlestyle.hpp"
ThrottleButton::ThrottleButton(QWidget* parent)
: QWidget(parent)
, m_color{ThrottleStyle::buttonColor}
{
setMinimumHeight(32);
}
ThrottleButton::ThrottleButton(const QString& text_, QWidget* parent)
: ThrottleButton(parent)
{
m_text = text_;
}
void ThrottleButton::paintEvent(QPaintEvent*)
{
const double borderRadius = rect().height() * 0.3;
const QRectF buttonRect = QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5);
QPainter painter{this};
painter.setRenderHint(QPainter::Antialiasing, true);
// Draw button:
QColor c = m_color;
if(!isEnabled())
{
c = ThrottleStyle::buttonDisabledColor;
}
painter.setPen(c);
painter.setBrush(c);
painter.drawRoundedRect(buttonRect, borderRadius, borderRadius);
// Draw icon or text:
if(!m_resource.isEmpty() && QFile::exists(m_resource))
{
QSvgRenderer svg{m_resource};
const double iconSize = qMin(rect().width(), rect().height()) - qCeil(borderRadius);
svg.render(&painter, QRectF((rect().width() - iconSize) / 2, (rect().height() - iconSize) / 2, iconSize, iconSize));
}
else
{
painter.setPen(ThrottleStyle::buttonTextColor);
painter.drawText(rect(), Qt::AlignCenter, m_text);
}
}
void ThrottleButton::mousePressEvent(QMouseEvent* event)
{
m_mousePressPos = event->pos();
emit pressed();
}
void ThrottleButton::mouseReleaseEvent(QMouseEvent* event)
{
emit released();
if((m_mousePressPos - event->pos()).manhattanLength() < 3)
{
emit clicked();
}
}
QSize ThrottleButton::minimumSizeHint() const
{
if(!m_resource.isEmpty() && QFile::exists(m_resource))
{
return QSize{minimumHeight(), minimumHeight()};
}
const int w = std::max(minimumHeight(), minimumHeight() / 2 + fontMetrics().boundingRect(m_text).width());
return QSize{w, minimumHeight()};
}
const QColor& ThrottleButton::color() const
{
return m_color;
}
void ThrottleButton::setColor(const QColor& value)
{
if(m_color != value)
{
m_color = value;
update();
}
}
const QString& ThrottleButton::resource() const
{
return m_resource;
}
void ThrottleButton::setResource(const QString& value)
{
if(m_resource != value)
{
m_resource = value;
updateGeometry();
update();
}
}
const QString& ThrottleButton::text() const
{
return m_text;
}
void ThrottleButton::setText(const QString& value)
{
if(m_text != value)
{
m_text = value;
updateGeometry();
update();
}
}

Datei anzeigen

@ -1,9 +1,8 @@
/**
* client/src/widget/abstractthrottlebutton.hpp
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021 Reinder Feenstra
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -20,39 +19,45 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_THROTTLE_ABSTRACTTHROTTLEBUTTON_HPP
#define TRAINTASTIC_CLIENT_WIDGET_THROTTLE_ABSTRACTTHROTTLEBUTTON_HPP
#ifndef TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLEBUTTON_HPP
#define TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLEBUTTON_HPP
#include <QWidget>
#include "../../network/objectptr.hpp"
class AbstractThrottleButton : public QWidget
class ThrottleButton : public QWidget
{
Q_OBJECT
private:
QString m_resource;
QString m_text;
QColor m_textColor;
QColor m_color;
QPoint m_mousePressPos;
protected:
ObjectPtr m_object;
void paintEvent(QPaintEvent*) final;
void paintEvent(QPaintEvent*) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void setResource(QString resource);
void setText(QString text);
void setTextColor(QColor color);
public:
AbstractThrottleButton(ObjectPtr object, QWidget* parent = nullptr);
explicit ThrottleButton(QWidget* parent = nullptr);
explicit ThrottleButton(const QString& text_, QWidget* parent = nullptr);
QSize minimumSizeHint() const final { return QSize{32, 32}; }
bool hasHeightForWidth() const final { return true; }
int heightForWidth(int w) const final { return w; }
QSize minimumSizeHint() const override;
virtual void click() {}
const QColor& color() const;
void setColor(const QColor& value);
const QString& resource() const;
void setResource(const QString& value);
const QString& text() const;
void setText(const QString& value);
signals:
void pressed();
void released();
void clicked();
};
#endif

Datei anzeigen

@ -1,50 +0,0 @@
/**
* client/src/widget/throttle/throttledirectionbutton.cpp
*
* 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.
*/
#include "throttledirectionbutton.hpp"
#include <traintastic/locale/locale.hpp>
#include "../../network/object.hpp"
#include "../../network/abstractproperty.hpp"
ThrottleDirectionButton::ThrottleDirectionButton(ObjectPtr object, Direction direction, QWidget* parent)
: AbstractThrottleButton(std::move(object), parent)
, m_direction{direction}
, m_directionProperty{m_object->getProperty("direction")}
{
connect(m_directionProperty, &AbstractProperty::valueChanged, this, &ThrottleDirectionButton::directionChanged);
setToolTip(Locale::tr(QString(EnumName<Direction>::value).append(":").append(EnumValues<Direction>::value.at(m_direction))));
setText(m_direction == Direction::Reverse ? "<" : ">");
directionChanged();
}
void ThrottleDirectionButton::click()
{
m_directionProperty->setValueEnum(m_direction);
}
void ThrottleDirectionButton::directionChanged()
{
const bool active = m_directionProperty->toEnum<Direction>() == m_direction;
setTextColor(active ? Qt::white : Qt::gray);
//! \todo setResource();
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021 Reinder Feenstra
* Copyright (C) 2021,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -26,11 +26,13 @@
#include <QSvgRenderer>
#include <traintastic/enum/decoderfunctiontype.hpp>
#include <traintastic/enum/decoderfunctionfunction.hpp>
#include "throttlestyle.hpp"
#include "../../network/object.hpp"
#include "../../network/abstractproperty.hpp"
ThrottleFunctionButton::ThrottleFunctionButton(ObjectPtr object, QWidget* parent)
: AbstractThrottleButton(std::move(object), parent)
: ThrottleButton(parent)
, m_object{std::move(object)}
, m_number{m_object->getProperty("number")}
, m_name{m_object->getProperty("name")}
, m_type{m_object->getProperty("type")}
@ -52,6 +54,10 @@ ThrottleFunctionButton::ThrottleFunctionButton(ObjectPtr object, QWidget* parent
setToolTip(m_name->toString());
setText("F" + QString::number(m_number->toInt()));
functionOrValueChanged();
connect(this, &ThrottleButton::clicked, this, &ThrottleFunctionButton::click);
connect(this, &ThrottleButton::pressed, this, &ThrottleFunctionButton::press);
connect(this, &ThrottleButton::released, this, &ThrottleFunctionButton::release);
}
int ThrottleFunctionButton::number() const
@ -86,26 +92,9 @@ void ThrottleFunctionButton::release()
void ThrottleFunctionButton::functionOrValueChanged()
{
const bool active = m_value->toBool();
setTextColor(active ? Qt::white : Qt::gray);
setColor(active ? ThrottleStyle::buttonActiveColor : ThrottleStyle::buttonColor);
setResource(
QString(":/dark/decoder_function.")
QString(":/light/decoder_function.")
.append(EnumValues<DecoderFunctionFunction>::value.at(m_function->toEnum<DecoderFunctionFunction>()))
.append(active ? ".on" : ".off")
.append(".svg"));
}
void ThrottleFunctionButton::mousePressEvent(QMouseEvent* event)
{
if(m_type->toEnum<DecoderFunctionType>() == DecoderFunctionType::Hold)
press();
else
AbstractThrottleButton::mousePressEvent(event);
}
void ThrottleFunctionButton::mouseReleaseEvent(QMouseEvent* event)
{
if(m_type->toEnum<DecoderFunctionType>() == DecoderFunctionType::Hold)
release();
else
AbstractThrottleButton::mouseReleaseEvent(event);
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021 Reinder Feenstra
* Copyright (C) 2021,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -23,16 +23,18 @@
#ifndef TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLEFUNCTIONBUTTON_HPP
#define TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLEFUNCTIONBUTTON_HPP
#include "abstractthrottlebutton.hpp"
#include "throttlebutton.hpp"
#include <traintastic/enum/decoderfunctionfunction.hpp>
#include "../../network/objectptr.hpp"
class AbstractProperty;
class ThrottleFunctionButton : public AbstractThrottleButton
class ThrottleFunctionButton : public ThrottleButton
{
Q_OBJECT
private:
ObjectPtr m_object;
AbstractProperty* m_number;
AbstractProperty* m_name;
AbstractProperty* m_type;
@ -42,17 +44,13 @@ class ThrottleFunctionButton : public AbstractThrottleButton
private slots:
void functionOrValueChanged();
protected:
void mousePressEvent(QMouseEvent* event) final;
void mouseReleaseEvent(QMouseEvent* event) final;
public:
ThrottleFunctionButton(ObjectPtr object, QWidget* parent = nullptr);
int number() const;
DecoderFunctionFunction function() const;
void click() final;
void click();
void press();
void release();
};

Datei anzeigen

@ -1,56 +0,0 @@
/**
* client/src/widget/DecoderStopButton.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021,2023 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "throttlestopbutton.hpp"
#include <traintastic/locale/locale.hpp>
#include "../../network/object.hpp"
#include "../../network/abstractproperty.hpp"
ThrottleStopButton::ThrottleStopButton(ObjectPtr object, QWidget* parent)
: AbstractThrottleButton(std::move(object), parent)
, m_throttle{m_object->getProperty("throttle")}
, m_throttleSpeed{m_object->getProperty("throttle_speed")}
, m_emergencyStop{m_object->getProperty("emergency_stop")}
{
setText("0");
}
void ThrottleStopButton::click()
{
if(m_emergencyStop && m_emergencyStop->toBool())
{
if(m_throttleSpeed)
m_throttleSpeed->setValueDouble(0);
else if(m_throttle)
m_throttle->setValueDouble(0);
m_emergencyStop->setValueBool(false);
}
else
{
if(m_throttleSpeed && !qFuzzyIsNull(m_throttleSpeed->toDouble()))
m_throttleSpeed->setValueDouble(0);
else if(m_throttle && !qFuzzyIsNull(m_throttle->toDouble()))
m_throttle->setValueDouble(0);
else if(m_emergencyStop)
m_emergencyStop->setValueBool(true);
}
}

Datei anzeigen

@ -0,0 +1,37 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLESTYLE_HPP
#define TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLESTYLE_HPP
#include <QColor>
struct ThrottleStyle
{
inline static const QColor backgroundColor{0x10, 0x10, 0x10};
inline static const QColor buttonColor{0xBB, 0x86, 0xFC};
inline static const QColor buttonActiveColor{0x03, 0xDA, 0xC6};
inline static const QColor buttonDisabledColor{0x80, 0x80, 0x80};
inline static const QColor buttonEStopColor{0xCF, 0x66, 0x79};
inline static const QColor buttonTextColor{0x00, 0x00, 0x00};
};
#endif

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021-2023 Reinder Feenstra
* Copyright (C) 2021-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -27,36 +27,88 @@
#include <QKeyEvent>
#include <QWheelEvent>
#include <QPainter>
#include <QGuiApplication>
#include <traintastic/enum/decoderfunctionfunction.hpp>
#include <traintastic/enum/decoderfunctiontype.hpp>
#include <traintastic/enum/direction.hpp>
#include <traintastic/enum/speedunit.hpp>
#include <traintastic/locale/locale.hpp>
#include "../../network/callmethod.hpp"
#include "../../network/connection.hpp"
#include "../../network/object.hpp"
#include "../../network/object.tpp"
#include "../../network/objectproperty.hpp"
#include "../../network/abstractproperty.hpp"
#include "../../network/objectvectorproperty.hpp"
#include "../../network/method.hpp"
#include "../../network/unitproperty.hpp"
#include "../../network/error.hpp"
#include "throttledirectionbutton.hpp"
#include "throttlefunctionbutton.hpp"
#include "throttlestopbutton.hpp"
#include "throttlebutton.hpp"
#include "throttlestyle.hpp"
#include "speedometerwidget.hpp"
#include "sliderwidget.hpp"
ThrottleWidget::ThrottleWidget(ObjectPtr object, QWidget* parent)
namespace
{
constexpr auto immidiateSpeedModifier = Qt::ControlModifier;
constexpr auto releaseWithoutStopModifier = Qt::ShiftModifier;
static const ObjectPtr nullObject;
}
ThrottleWidget::ThrottleWidget(ObjectPtr train, QWidget* parent)
: QWidget(parent)
, m_object{std::move(object)}
, m_functionsRequestId{Connection::invalidRequestId}
, m_toggleDirection{m_object->getMethod("toggle_direction")}
, m_train{std::move(train)}
, m_trainHasThrottle{m_train->getProperty("has_throttle")}
, m_trainThrottleName{m_train->getProperty("throttle_name")}
, m_trainDirection{m_train->getProperty("direction")}
, m_trainSpeed{m_train->getUnitProperty("speed")}
, m_trainTargetSpeed{m_train->getUnitProperty("throttle_speed")}
, m_trainIsStopped{m_train->getProperty("is_stopped")}
, m_trainEmergencyStop{m_train->getProperty("emergency_stop")}
, m_vehiclesRequestId{Connection::invalidRequestId}
, m_nameLabel{new QLabel("", this)}
, m_functionGrid{new QGridLayout()}
, m_speedoMeter{new SpeedoMeterWidget(this)}
, m_stopButton{new ThrottleStopButton(m_object, this)}
, m_reverseButton{new ThrottleDirectionButton(m_object, Direction::Reverse, this)}
, m_forwardButton{new ThrottleDirectionButton(m_object, Direction::Forward, this)}
, m_speedSlider{new SliderWidget(Qt::Vertical, this)}
, m_stopButton{new ThrottleButton("0", this)}
, m_reverseButton{new ThrottleButton("<", this)}
, m_forwardButton{new ThrottleButton(">", this)}
, m_eStopButton{new ThrottleButton(Locale::tr("throttle.estop"), this)}
, m_throttleStatus{new QLabel("", this)}
, m_throttleAction{new ThrottleButton("", this)}
{
setFocusPolicy(Qt::StrongFocus);
if(auto* name = m_object->getProperty("name"))
m_eStopButton->setColor(ThrottleStyle::buttonEStopColor);
m_throttleAction->setEnabled(false);
fetchTrainVehicles();
m_createThrottleRequestId = m_train->connection()->createObject("throttle.client",
[this](const ObjectPtr& obj, std::optional<const Error> /*error*/)
{
m_createThrottleRequestId = Connection::invalidRequestId;
if(obj)
{
m_throttle = obj;
m_throttleTrain = m_throttle->getObjectProperty("train");
m_throttleRelease = m_throttle->getMethod("release");
m_throttleAcquire = m_throttle->getMethod("acquire");
m_throttleSetDirection = m_throttle->getMethod("set_direction");
m_throttleSetSpeed = m_throttle->getMethod("set_speed");
m_throttleFaster = m_throttle->getMethod("faster");
m_throttleSlower = m_throttle->getMethod("slower");
m_throttleEmergencyStop = m_throttle->getMethod("emergency_stop");
connect(m_throttleTrain, &AbstractProperty::valueChanged, this, &ThrottleWidget::updateThrottleControls);
m_throttleAction->setEnabled(true);
}
});
if(auto* name = m_train->getProperty("name")) [[likely]]
{
setWindowTitle(name->toString());
connect(name, &AbstractProperty::valueChangedString, this, &ThrottleWidget::setWindowTitle);
@ -64,179 +116,181 @@ ThrottleWidget::ThrottleWidget(ObjectPtr object, QWidget* parent)
connect(name, &AbstractProperty::valueChangedString, m_nameLabel, &QLabel::setText);
}
if((m_speed = m_object->getUnitProperty("speed")) &&
(m_throttleSpeed = m_object->getUnitProperty("throttle_speed"))) // train
{
m_speedoMeter->setUnit("km/h");
m_speedoMeter->setSpeedMax(m_speed->getAttributeDouble(AttributeName::Max, 0));
m_speedoMeter->setSpeed(m_speed->toDouble());
m_speedoMeter->setSpeedTarget(m_throttleSpeed->toDouble());
connect(m_speed, &AbstractProperty::valueChangedDouble, this,
[this](double value)
connect(m_trainHasThrottle, &AbstractProperty::valueChanged, this, &ThrottleWidget::updateThrottleControls);
connect(m_trainThrottleName, &AbstractProperty::valueChanged, this, &ThrottleWidget::updateThrottleControls);
connect(m_trainDirection, &AbstractProperty::valueChanged, this, &ThrottleWidget::updateThrottleControls);
connect(m_trainSpeed, &AbstractProperty::valueChangedDouble, m_speedoMeter, &SpeedoMeterWidget::setSpeed);
connect(m_trainSpeed, &AbstractProperty::attributeChanged, this,
[this](AttributeName attribute)
{
if(attribute == AttributeName::Max)
{
m_speedoMeter->setSpeed(value);
});
connect(m_speed, &InterfaceItem::attributeChanged, this,
[this](AttributeName name, const QVariant& value)
updateSpeedMax();
}
});
connect(m_trainTargetSpeed, &AbstractProperty::valueChangedDouble, m_speedoMeter, &SpeedoMeterWidget::setSpeedTarget);
connect(m_trainTargetSpeed, &AbstractProperty::valueChangedDouble, this,
[this](double value)
{
m_speedoMeter->setSpeedTarget(value);
m_speedSliderUpdateFromNetwork = true;
m_speedSlider->setValue(value);
m_speedSliderUpdateFromNetwork = false;
});
connect(m_trainTargetSpeed, &UnitProperty::unitChanged, this, &ThrottleWidget::updateSpeedUnit);
connect(m_trainIsStopped, &AbstractProperty::valueChanged, this, &ThrottleWidget::updateThrottleControls);
connect(m_trainEmergencyStop, &AbstractProperty::valueChanged, this, &ThrottleWidget::updateThrottleControls);
connect(m_trainEmergencyStop, &AbstractProperty::valueChangedBool, m_speedoMeter, &SpeedoMeterWidget::setEStop);
m_speedoMeter->setEStop(m_trainEmergencyStop->toBool());
connect(m_forwardButton, &ThrottleButton::clicked, std::bind_front(&ThrottleWidget::setDirection, this, Direction::Forward));
connect(m_reverseButton, &ThrottleButton::clicked, std::bind_front(&ThrottleWidget::setDirection, this, Direction::Reverse));
connect(m_stopButton, &ThrottleButton::clicked,
[this]()
{
setSpeed(0.0, QGuiApplication::keyboardModifiers().testFlag(immidiateSpeedModifier));
});
connect(m_eStopButton, &ThrottleButton::clicked, this, &ThrottleWidget::emergencyStop);
connect(m_speedSlider, &SliderWidget::valueChanged,
[this](float value)
{
if(!m_speedSliderUpdateFromNetwork)
{
switch(name)
{
case AttributeName::Max:
m_speedoMeter->setSpeedMax(value.toDouble());
break;
setSpeed(value, QGuiApplication::keyboardModifiers().testFlag(immidiateSpeedModifier));
}
});
default:
break;
}
});
connect(m_throttleSpeed, &AbstractProperty::valueChangedDouble, this,
[this](double value)
connect(m_throttleAction, &ThrottleButton::clicked,
[this]()
{
if(!m_throttleTrain || !m_throttleAcquire || !m_throttleRelease) [[unlikely]]
{
m_speedoMeter->setSpeedTarget(value);
});
}
else if((m_throttle = m_object->getProperty("throttle"))) // decoder
{
m_speedoMeter->setUnit("%");
m_speedoMeter->setSpeedMax(m_throttle->getAttributeDouble(AttributeName::Max, 0) * 100);
m_speedoMeter->setSpeed(m_throttle->toDouble() * 100);
return;
}
connect(m_throttle, &AbstractProperty::valueChangedDouble, this,
[this](double value)
if(m_throttleTrain->hasObject()) // release
{
m_speedoMeter->setSpeed(value * 100);
});
connect(m_throttle, &InterfaceItem::attributeChanged, this,
[this](AttributeName name, const QVariant& value)
const bool stop = !QGuiApplication::keyboardModifiers().testFlag(releaseWithoutStopModifier);
callMethod(*m_throttleRelease, nullptr, stop);
}
else // acquire / steal
{
switch(name)
{
case AttributeName::Max:
m_speedoMeter->setSpeedMax(value.toDouble() * 100);
break;
default:
break;
}
});
}
if((m_emergencyStop = m_object->getProperty("emergency_stop")))
{
m_speedoMeter->setEStop(m_emergencyStop->toBool());
connect(m_emergencyStop, &AbstractProperty::valueChangedBool, m_speedoMeter, &SpeedoMeterWidget::setEStop);
}
if(auto* p = dynamic_cast<ObjectProperty*>(m_object->getProperty("functions")))
{
m_functionsRequestId = p->getObject(
[this](const ObjectPtr& functions, std::optional<const Error> /*error*/)
{
m_functionsRequestId = Connection::invalidRequestId;
if(!functions)
return;
if(auto* items = dynamic_cast<ObjectVectorProperty*>(functions->getVectorProperty("items")))
{
int i = 0;
for(const QString& id : *items)
callMethodR<bool>(*m_throttleAcquire,
[](const bool& /*success*/, std::optional<const Error> /*error*/)
{
const int functionRequestId = functions->connection()->getObject(id,
[this, i](const ObjectPtr& function, std::optional<const Error> /*error*/)
{
m_functionRequestIds.erase(i);
},
m_train, // train
m_trainHasThrottle->toBool()); // steal
}
});
if(function)
{
static constexpr int buttonsPerRow = 6;
auto* btn = new ThrottleFunctionButton(function, this);
m_functionGrid->addWidget(btn, i / buttonsPerRow, i % buttonsPerRow);
m_functionButtons.push_back(btn);
}
});
m_functionRequestIds.emplace(i, functionRequestId);
i++;
}
}
});
}
auto* left = new QVBoxLayout();
auto* main = new QVBoxLayout();
QFont font = m_nameLabel->font();
font.setBold(true);
m_nameLabel->setFont(font);
left->addWidget(m_nameLabel);
left->addStretch();
left->addLayout(m_functionGrid);
main->addWidget(m_nameLabel);
auto* right = new QVBoxLayout();
right->addWidget(m_speedoMeter);
auto* h = new QHBoxLayout();
h->addStretch();
h->addWidget(m_reverseButton);
h->addWidget(m_stopButton);
h->addWidget(m_forwardButton);
h->addStretch();
right->addLayout(h);
auto* l = new QHBoxLayout();
l->addLayout(left);
l->addLayout(right);
setLayout(l);
auto* columns = new QHBoxLayout();
m_functions = new QVBoxLayout();
m_functions->addStretch();
columns->addLayout(m_functions, 1);
auto* right = new QGridLayout();
right->addWidget(m_speedoMeter, 0, 0, 1, 3);
right->addWidget(m_speedSlider, 0, 4, 3, 1);
right->addWidget(m_reverseButton, 1, 0);
right->addWidget(m_stopButton, 1, 1);
right->addWidget(m_forwardButton, 1, 2);
right->addWidget(m_eStopButton, 2, 0, 1, 3);
right->setRowStretch(0, 1);
columns->addLayout(right);
main->addLayout(columns, 1);
auto* bottom = new QHBoxLayout();
bottom->addWidget(m_throttleStatus, 1);
bottom->addWidget(m_throttleAction);
main->addLayout(bottom);
setLayout(main);
updateSpeedMax();
updateSpeedUnit();
updateThrottleControls();
}
ThrottleWidget::~ThrottleWidget()
{
const auto& c = m_object->connection();
if(m_functionsRequestId != Connection::invalidRequestId)
c->cancelRequest(m_functionsRequestId);
for(const auto& it : m_functionRequestIds)
c->cancelRequest(it.second);
auto& c = *m_train->connection();
if(m_createThrottleRequestId != Connection::invalidRequestId)
{
c.cancelRequest(m_createThrottleRequestId);
}
if(m_vehiclesRequestId != Connection::invalidRequestId)
{
c.cancelRequest(m_vehiclesRequestId);
}
for(const auto& it : m_vehicleDecoderRequestIds)
{
c.cancelRequest(it.second);
}
}
void ThrottleWidget::keyPressEvent(QKeyEvent* event)
{
if(ThrottleFunctionButton* btn = getFunctionButton(*event))
if(const auto& function = getFunction(*event))
{
btn->press();
if(function->getPropertyValueEnum<DecoderFunctionType>("type", DecoderFunctionType::OnOff) == DecoderFunctionType::Hold)
{
function->setPropertyValue("value", true);
}
return;
}
QWidget::keyPressEvent(event);
}
void ThrottleWidget::keyReleaseEvent(QKeyEvent* event)
{
if(ThrottleFunctionButton* btn = getFunctionButton(*event))
if(const auto& function = getFunction(*event))
{
btn->release();
if(function->getPropertyValueEnum<DecoderFunctionType>("type", DecoderFunctionType::OnOff) == DecoderFunctionType::Hold)
{
function->setPropertyValue("value", false);
}
else
{
function->setPropertyValue("value", !function->getPropertyValueBool("value", true)); // toggle
}
return;
}
else
{
switch(event->key())
{
case Qt::Key_Space:
m_stopButton->click();
emergencyStop();
return;
case Qt::Key_Return:
case Qt::Key_Enter:
if(Q_LIKELY(m_toggleDirection))
m_toggleDirection->call();
setDirection(~m_trainDirection->toEnum<Direction>()); // toggle
return;
case Qt::Key_Left:
m_reverseButton->click();
setDirection(Direction::Reverse);
return;
case Qt::Key_Right:
m_forwardButton->click();
setDirection(Direction::Forward);
return;
case Qt::Key_Up:
changeSpeed(true);
faster(event->modifiers().testFlag(immidiateSpeedModifier));
return;
case Qt::Key_Down:
changeSpeed(false);
slower(event->modifiers().testFlag(immidiateSpeedModifier));
return;
}
}
@ -245,94 +299,136 @@ void ThrottleWidget::keyReleaseEvent(QKeyEvent* event)
void ThrottleWidget::wheelEvent(QWheelEvent* event)
{
const bool immediate = event->modifiers().testFlag(immidiateSpeedModifier);
if(event->angleDelta().y() > 0)
{
changeSpeed(true);
faster(immediate);
}
else if(event->angleDelta().y() < 0)
{
changeSpeed(false);
slower(immediate);
}
}
void ThrottleWidget::paintEvent(QPaintEvent*)
{
const QColor backgroundColor{0x10, 0x10, 0x10};
QPainter painter(this);
painter.fillRect(rect(), backgroundColor);
painter.fillRect(rect(), ThrottleStyle::backgroundColor);
}
void ThrottleWidget::changeSpeed(bool up)
bool ThrottleWidget::throttleAcquired() const
{
if(up && m_emergencyStop && m_emergencyStop->toBool())
return;
return m_throttleTrain && m_throttleTrain->hasObject();
}
if(m_throttleSpeed)
void ThrottleWidget::setDirection(Direction value)
{
if(throttleAcquired() && m_throttleSetDirection)
{
double throttle = m_throttleSpeed->toDouble();
double step = 0;
switch(m_throttleSpeed->unit<SpeedUnit>())
{
case SpeedUnit::KiloMeterPerHour:
step = ((throttle < 40) || (!up && qFuzzyCompare(throttle, 40))) ? 2 : 5;
break;
case SpeedUnit::MilePerHour:
step = ((throttle < 30) || (!up && qFuzzyCompare(throttle, 30))) ? 2 : 5;
break;
case SpeedUnit::MeterPerSecond:
step = ((throttle < 10) || (!up && qFuzzyCompare(throttle, 10))) ? 0.5 : 1;
break;
}
throttle += up ? step : -step;
m_throttleSpeed->setValueDouble(std::clamp(throttle, m_throttleSpeed->getAttributeDouble(AttributeName::Min, 0), m_throttleSpeed->getAttributeDouble(AttributeName::Max, 1)));
}
else if(m_throttle)
{
static constexpr double throttleStep = 0.02; // 2%
static constexpr double throttleStepHalf = throttleStep / 2;
double throttle = m_throttle->toDouble();
if(throttle < 0.2)
{
throttle += up ? throttleStepHalf : -throttleStepHalf;
}
else
{
throttle += up ? throttleStep : -throttleStep;
}
m_throttle->setValueDouble(std::clamp(throttle, m_throttle->getAttributeDouble(AttributeName::Min, 0), m_throttle->getAttributeDouble(AttributeName::Max, 1)));
callMethodR<bool>(*m_throttleSetDirection,
[](bool /*success*/, std::optional<const Error> /*error*/)
{
},
value);
}
}
ThrottleFunctionButton* ThrottleWidget::getFunctionButton(int number)
void ThrottleWidget::setSpeed(double value, bool immediate)
{
auto it = std::find_if(m_functionButtons.begin(), m_functionButtons.end(),
[number](const auto* btn)
{
return btn->number() == number;
});
return (it != m_functionButtons.end()) ? *it : nullptr;
if(throttleAcquired() && m_throttleSetSpeed)
{
callMethodR<bool>(*m_throttleSetSpeed,
[](bool /*success*/, std::optional<const Error> /*error*/)
{
},
value,
m_trainSpeed->unit<SpeedUnit>(),
immediate);
}
}
ThrottleFunctionButton* ThrottleWidget::getFunctionButton(DecoderFunctionFunction function)
void ThrottleWidget::faster(bool immediate)
{
auto it = std::find_if(m_functionButtons.begin(), m_functionButtons.end(),
[function](const auto* btn)
{
return btn->function() == function;
});
return (it != m_functionButtons.end()) ? *it : nullptr;
if(throttleAcquired() && m_throttleFaster)
{
callMethodR<bool>(*m_throttleFaster,
[](bool /*success*/, std::optional<const Error> /*error*/)
{
},
immediate);
}
}
ThrottleFunctionButton* ThrottleWidget::getFunctionButton(const QKeyEvent& event)
void ThrottleWidget::slower(bool immediate)
{
if(throttleAcquired() && m_throttleSlower)
{
callMethodR<bool>(*m_throttleSlower,
[](bool /*success*/, std::optional<const Error> /*error*/)
{
},
immediate);
}
}
void ThrottleWidget::emergencyStop()
{
if(throttleAcquired() && m_throttleEmergencyStop)
{
callMethodR<bool>(*m_throttleEmergencyStop,
[](bool /*success*/, std::optional<const Error> /*error*/)
{
});
}
else if(m_trainEmergencyStop) [[likely]]
{
m_trainEmergencyStop->setValueBool(true);
}
}
const ObjectPtr& ThrottleWidget::getFunction(int number)
{
for(const auto& vehicle : m_trainVehicleDecoders)
{
const auto it = std::find_if(vehicle.functions.begin(), vehicle.functions.end(),
[number](const auto& f)
{
return f->getPropertyValueInt("number", -1) == number;
});
if(it != vehicle.functions.end())
{
return *it;
}
}
return nullObject;
}
const ObjectPtr& ThrottleWidget::getFunction(DecoderFunctionFunction function)
{
for(const auto& vehicle : m_trainVehicleDecoders)
{
const auto it = std::find_if(vehicle.functions.begin(), vehicle.functions.end(),
[function](const auto& f)
{
return f->getPropertyValueEnum("function", DecoderFunctionFunction::Generic) == function;
});
if(it != vehicle.functions.end())
{
return *it;
}
}
return nullObject;
}
const ObjectPtr& ThrottleWidget::getFunction(const QKeyEvent& event)
{
if(event.key() >= Qt::Key_0 && event.key() <= Qt::Key_9)
{
int n = event.key() - Qt::Key_0;
switch(event.modifiers() & Qt::ControlModifier)
switch(event.modifiers() & (Qt::ControlModifier | Qt::AltModifier))
{
case Qt::NoModifier:
break;
@ -346,17 +442,195 @@ ThrottleFunctionButton* ThrottleWidget::getFunctionButton(const QKeyEvent& event
break;
default:
return nullptr;
return nullObject;
}
return getFunctionButton(n);
return getFunction(n);
}
switch(event.key())
{
case Qt::Key_L:
return getFunctionButton(DecoderFunctionFunction::Light);
return getFunction(DecoderFunctionFunction::Light);
case Qt::Key_M:
return getFunction(DecoderFunctionFunction::Mute);
case Qt::Key_S:
return getFunction(DecoderFunctionFunction::Sound);
}
return nullptr;
return nullObject;
}
void ThrottleWidget::updateSpeedMax()
{
const auto maxSpeed = m_trainSpeed->getAttributeDouble(AttributeName::Max, 1.0);
m_speedoMeter->setSpeedMax(maxSpeed);
m_speedSlider->setMaximum(maxSpeed);
}
void ThrottleWidget::updateSpeedUnit()
{
auto unit = toDisplayUnit(m_trainTargetSpeed->unit<SpeedUnit>());
m_speedoMeter->setUnit(QString::fromUtf8(unit.data(), unit.size()));
}
void ThrottleWidget::updateThrottleControls()
{
if(!m_trainHasThrottle || !m_trainThrottleName) [[unlikely]]
{
return;
}
const bool acquired = throttleAcquired();
m_forwardButton->setEnabled(acquired && (m_trainIsStopped->toBool() || m_trainDirection->toEnum<Direction>() == Direction::Forward));
m_stopButton->setEnabled(acquired && (!m_trainIsStopped->toBool() || m_trainEmergencyStop->toBool()));
m_reverseButton->setEnabled(acquired && (m_trainIsStopped->toBool() || m_trainDirection->toEnum<Direction>() == Direction::Reverse));
m_speedSlider->setEnabled(acquired);
m_eStopButton->setEnabled(!m_trainEmergencyStop->toBool());
m_forwardButton->setColor(m_trainDirection->toEnum<Direction>() == Direction::Forward ? ThrottleStyle::buttonActiveColor : ThrottleStyle::buttonColor);
m_reverseButton->setColor(m_trainDirection->toEnum<Direction>() == Direction::Reverse ? ThrottleStyle::buttonActiveColor : ThrottleStyle::buttonColor);
if(acquired)
{
m_throttleStatus->setText(Locale::tr("throttle.in_control"));
m_throttleAction->setColor(ThrottleStyle::buttonActiveColor);
m_throttleAction->setText(Locale::tr("throttle.release"));
}
else if(m_trainHasThrottle->toBool())
{
m_throttleStatus->setText(Locale::tr("throttle.controlled_by_x").arg(m_trainThrottleName->toString()));
m_throttleAction->setColor(ThrottleStyle::buttonColor);
m_throttleAction->setText(Locale::tr("throttle.steal"));
}
else
{
m_throttleStatus->setText(Locale::tr("throttle.not_controlled"));
m_throttleAction->setColor(ThrottleStyle::buttonColor);
m_throttleAction->setText(Locale::tr("throttle.acquire"));
}
}
void ThrottleWidget::fetchTrainVehicles()
{
m_trainVehiclesList.reset();
m_trainVehicles.clear();
m_trainVehicleDecoders.clear();
if(auto* vehicles = m_train->getObjectProperty("vehicles"))
{
m_vehiclesRequestId = vehicles->getObject(
[this](const ObjectPtr& obj, std::optional<const Error> /*error*/)
{
m_vehiclesRequestId = Connection::invalidRequestId;
if(obj)
{
m_trainVehiclesList = obj;
if(int n = m_trainVehiclesList->getPropertyValueInt("length", 0) - 1; n >= 0)
{
m_vehiclesRequestId = m_trainVehiclesList->connection()->getObjects(*m_trainVehiclesList, 0, static_cast<uint32_t>(n),
[this](const std::vector<ObjectPtr>& objects, std::optional<const Error> error)
{
m_vehiclesRequestId = Connection::invalidRequestId;
if(!error)
{
m_trainVehicles = objects;
fetchTrainVehicleDecoders();
}
});
}
}
});
}
}
void ThrottleWidget::fetchTrainVehicleDecoders()
{
m_trainVehicleDecoders.clear();
m_trainVehicleDecoders.resize(m_trainVehicles.size());
for(size_t i = 0; i < m_trainVehicles.size(); ++i)
{
if(auto* decoder = m_trainVehicles[i]->getObjectProperty("decoder"); decoder && decoder->hasObject())
{
m_vehicleDecoderRequestIds[i] = decoder->getObject(
[this, i](const ObjectPtr& obj, std::optional<const Error> /*error*/)
{
m_vehicleDecoderRequestIds.erase(i);
if(obj)
{
m_trainVehicleDecoders[i].decoder = obj;
fetchTrainVehicleDecoderFunctions(i);
}
});
}
}
}
void ThrottleWidget::fetchTrainVehicleDecoderFunctions(size_t vehicleIndex)
{
if(vehicleIndex < m_trainVehicleDecoders.size() && m_trainVehicleDecoders[vehicleIndex].decoder) [[likely]]
{
assert(m_trainVehicleDecoders[vehicleIndex].functions.empty());
if(auto* functions = m_trainVehicleDecoders[vehicleIndex].decoder->getObjectProperty("functions"))
{
m_vehicleDecoderRequestIds[vehicleIndex] = functions->getObject(
[this, vehicleIndex](const ObjectPtr& obj, std::optional<const Error> /*error*/)
{
m_vehicleDecoderRequestIds[vehicleIndex] = Connection::invalidRequestId;
if(obj)
{
if(auto* items = obj->getObjectVectorProperty("items")) [[likely]]
{
[[maybe_unused]] int y = items->getObjects(
[this, vehicleIndex](const std::vector<ObjectPtr>& functionObjs, std::optional<const Error> error)
{
if(!error)
{
m_trainVehicleDecoders[vehicleIndex].functions = functionObjs;
createDecoderFunctionWidgets(vehicleIndex);
}
});
}
}
});
}
}
}
void ThrottleWidget::createDecoderFunctionWidgets(size_t vehicleIndex)
{
constexpr int functionButtonsPerRow = 5;
auto* grid = new QGridLayout();
if(auto* name = m_trainVehicles[vehicleIndex]->getProperty("name"))
{
auto* nameLabel = new QLabel(name->toString(), this);
connect(name, &AbstractProperty::valueChangedString, nameLabel, &QLabel::setText);
grid->addWidget(nameLabel, 0, 0, 1, functionButtonsPerRow + 1);
}
int index = 0;
for(const auto& function : m_trainVehicleDecoders[vehicleIndex].functions)
{
grid->addWidget(new ThrottleFunctionButton(function, this), 1 + index / functionButtonsPerRow, index % functionButtonsPerRow);
index++;
}
grid->addItem(new QSpacerItem(0, 0, QSizePolicy::Maximum), 1, functionButtonsPerRow);
int layoutIndex = 0;
for(size_t i = 0; i < vehicleIndex; ++i)
{
if(!m_trainVehicleDecoders[i].functions.empty())
{
layoutIndex++;
}
}
m_functions->insertLayout(layoutIndex, grid);
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021-2023 Reinder Feenstra
* Copyright (C) 2021-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -30,39 +30,87 @@
#include "../../network/objectptr.hpp"
class QLabel;
class QGridLayout;
class QPushButton;
class QVBoxLayout;
class SpeedoMeterWidget;
class ThrottleStopButton;
class ThrottleDirectionButton;
class SliderWidget;
class ThrottleFunctionButton;
class ThrottleButton;
class AbstractProperty;
class ObjectProperty;
class UnitProperty;
class Method;
enum class Direction : uint8_t;
class ThrottleWidget final : public QWidget
{
private:
ObjectPtr m_object;
int m_functionsRequestId;
std::map<int, int> m_functionRequestIds;
AbstractProperty* m_emergencyStop;
UnitProperty* m_speed = nullptr;
UnitProperty* m_throttleSpeed = nullptr;
AbstractProperty* m_throttle = nullptr;
Method* m_toggleDirection;
struct VehicleDecoder
{
ObjectPtr decoder;
ObjectPtr decoderFunctions;
std::vector<ObjectPtr> functions;
};
ObjectPtr m_train;
AbstractProperty* m_trainHasThrottle;
AbstractProperty* m_trainThrottleName;
AbstractProperty* m_trainDirection;
UnitProperty* m_trainSpeed;
UnitProperty* m_trainTargetSpeed;
AbstractProperty* m_trainIsStopped;
AbstractProperty* m_trainEmergencyStop;
ObjectPtr m_trainVehiclesList;
std::vector<ObjectPtr> m_trainVehicles;
std::vector<VehicleDecoder> m_trainVehicleDecoders;
ObjectPtr m_throttle;
ObjectProperty* m_throttleTrain = nullptr;
Method* m_throttleAcquire = nullptr;
Method* m_throttleRelease = nullptr;
Method* m_throttleSetSpeed = nullptr;
Method* m_throttleFaster = nullptr;
Method* m_throttleSlower = nullptr;
Method* m_throttleEmergencyStop = nullptr;
Method* m_throttleSetDirection = nullptr;
int m_createThrottleRequestId;
int m_vehiclesRequestId;
std::unordered_map<size_t, int> m_vehicleDecoderRequestIds;
QLabel* m_nameLabel;
QGridLayout* m_functionGrid;
std::list<ThrottleFunctionButton*> m_functionButtons;
QVBoxLayout* m_functions;
SpeedoMeterWidget* m_speedoMeter;
ThrottleStopButton* m_stopButton;
ThrottleDirectionButton* m_reverseButton;
ThrottleDirectionButton* m_forwardButton;
SliderWidget* m_speedSlider;
ThrottleButton* m_stopButton;
ThrottleButton* m_reverseButton;
ThrottleButton* m_forwardButton;
ThrottleButton* m_eStopButton;
QLabel* m_throttleStatus;
ThrottleButton* m_throttleAction;
bool m_speedSliderUpdateFromNetwork = false;
void changeSpeed(bool up);
bool throttleAcquired() const;
void setDirection(Direction value);
void setSpeed(double value, bool immediate);
void faster(bool immediate);
void slower(bool immediate);
void emergencyStop();
ThrottleFunctionButton* getFunctionButton(int number);
ThrottleFunctionButton* getFunctionButton(DecoderFunctionFunction function);
ThrottleFunctionButton* getFunctionButton(const QKeyEvent& event);
const ObjectPtr& getFunction(int number);
const ObjectPtr& getFunction(DecoderFunctionFunction function);
const ObjectPtr& getFunction(const QKeyEvent& event);
void updateSpeedMax();
void updateSpeedUnit();
void updateThrottleControls();
void fetchTrainVehicles();
void fetchTrainVehicleDecoders();
void fetchTrainVehicleDecoderFunctions(size_t vehicleIndex);
void createDecoderFunctionWidgets(size_t vehicleIndex);
protected:
void keyPressEvent(QKeyEvent* event) final;
@ -71,7 +119,7 @@ class ThrottleWidget final : public QWidget
void paintEvent(QPaintEvent*) final;
public:
explicit ThrottleWidget(ObjectPtr object, QWidget* parent = nullptr);
explicit ThrottleWidget(ObjectPtr train, QWidget* parent = nullptr);
~ThrottleWidget() final;
};

Datei anzeigen

@ -273,7 +273,7 @@ class LuaDoc:
name = 'class'
items = []
class_hpp = LuaDoc._read_file(os.path.join(project_root, 'server', 'src', 'lua', 'class.hpp'))
for item_name in re.findall(r'static\s+int\s+([a-zA-Z]+)\(\s*lua_State\s*\*\s*L\s*\)', class_hpp):
for item_name in re.findall(r'static\s+int\s+([a-z][a-zA-Z_]+)\(\s*lua_State\s*\*\s*L\s*\)', class_hpp):
if item_name == 'getClass':
item_name = 'get'
items.append(item_name)

Datei anzeigen

@ -1,4 +1,15 @@
{
"create_throttle": {
"type":"function",
"parameters": [
{
"name": "name",
"optional": true
}
],
"return_values": 1,
"since": "0.3"
},
"get": {
"type": "function",
"parameters": [
@ -173,6 +184,9 @@
"IDENTIFICATION_LIST": {
"type": "constant"
},
"SCRIPT_THROTTLE": {
"type": "constant"
},
"RAIL_VEHICLE_LIST": {
"type": "constant"
},
@ -207,4 +221,4 @@
"type": "constant",
"since": "0.3"
}
}
}

Datei anzeigen

@ -0,0 +1,2 @@
{
}

Datei anzeigen

@ -0,0 +1,52 @@
{
"emergency_stop": {},
"set_direction": {
"parameters": [
{
"name": "direction"
}
]
},
"change_direction": {},
"set_speed": {
"parameters": [
{
"name": "speed"
},
{
"name": "unit"
}
]
},
"set_target_speed": {
"parameters": [
{
"name": "target_speed"
},
{
"name": "unit"
}
]
},
"acquire": {
"parameters": [
{
"name": "train"
},
{
"name": "steal",
"optional": true
}
],
"return_values": 1
},
"release": {
"parameters": [
{
"name": "stop",
"optional": true
}
],
"return_values": 0
}
}

Datei anzeigen

@ -0,0 +1,21 @@
{
"name": {},
"train": {},
"on_acquire": {
"parameters": [
{
"name": "throttle"
},
{
"name": "train"
}
]
},
"on_release": {
"parameters": [
{
"name": "throttle"
}
]
}
}

Datei anzeigen

@ -8,6 +8,8 @@
"mode": {},
"mute": {},
"no_smoke": {},
"has_throttle": {},
"throttle_name": {},
"blocks": {},
"zones": {},
"on_block_assigned": {

Datei anzeigen

@ -34,8 +34,8 @@ OutputDir=output
OutputBaseFilename=traintastic-setup-v{#VersionFull}
SolidCompression=yes
WizardStyle=modern
ArchitecturesInstallIn64BitMode=x64
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64compatible
ArchitecturesAllowed=x64compatible
MinVersion=10.0
[Languages]

Datei anzeigen

@ -155,10 +155,6 @@ file(GLOB SOURCES
"src/hardware/protocol/z21/*.cpp"
"src/hardware/protocol/z21/iohandler/*.hpp"
"src/hardware/protocol/z21/iohandler/*.cpp"
"src/hardware/throttle/*.hpp"
"src/hardware/throttle/*.cpp"
"src/hardware/throttle/list/*.hpp"
"src/hardware/throttle/list/*.cpp"
"src/log/*.hpp"
"src/log/*.cpp"
"src/lua/*.hpp"
@ -173,6 +169,10 @@ file(GLOB SOURCES
"src/pcap/*.cpp"
"src/status/*.hpp"
"src/status/*.cpp"
"src/throttle/*.hpp"
"src/throttle/*.cpp"
"src/throttle/list/*.hpp"
"src/throttle/list/*.cpp"
"src/train/*.hpp"
"src/train/*.cpp"
"src/traintastic/*.hpp"

Datei anzeigen

@ -1,7 +1,6 @@
/**
* server/src/train/trainerror.cpp
*
* This file is part of the traintastic source code.
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
@ -20,45 +19,54 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "trainerror.hpp"
#include "errorcode.hpp"
namespace {
struct TrainErrorCategory : std::error_category
struct ErrorCodeCategory : std::error_category
{
const char* name() const noexcept final;
std::string message(int ev) const final;
};
const char* TrainErrorCategory::name() const noexcept
const char* ErrorCodeCategory::name() const noexcept
{
return "train";
return "traintastic";
}
std::string TrainErrorCategory::message(int ev) const
std::string ErrorCodeCategory::message(int ev) const
{
switch(static_cast<TrainError>(ev))
switch(static_cast<ErrorCode>(ev))
{
case TrainError::InvalidThrottle:
case ErrorCode::InvalidThrottle:
return "invalid throttle";
case TrainError::AlreadyAcquired:
case ErrorCode::AlreadyAcquired:
return "already acquired";
case TrainError::CanNotActivateTrain:
case ErrorCode::CanNotActivateTrain:
return "can't activate train";
case TrainError::TrainMustBeStoppedToChangeDirection:
case ErrorCode::TrainMustBeStoppedToChangeDirection:
return "train must be stopped to change direction";
case ErrorCode::UnknownDecoderAddress:
return "unknown decoder address";
case ErrorCode::DecoderNotAssignedToAVehicle:
return "decoder not assigned to a vehicle";
case ErrorCode::VehicleNotAssignedToATrain:
return "vehicle not assigned to a train";
}
return "(unrecognized error)";
}
const TrainErrorCategory trainErrorCategory{};
const ErrorCodeCategory errorCodeCategory{};
}
std::error_code make_error_code(TrainError ec)
std::error_code make_error_code(ErrorCode ec)
{
return {static_cast<int>(ec), trainErrorCategory};
return {static_cast<int>(ec), errorCodeCategory};
}

Datei anzeigen

@ -1,7 +1,6 @@
/**
* server/src/train/trainerror.hpp
*
* This file is part of the traintastic source code.
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
@ -20,23 +19,26 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_TRAIN_TRAINERROR_HPP
#define TRAINTASTIC_SERVER_TRAIN_TRAINERROR_HPP
#ifndef TRAINTASTIC_SERVER_CORE_ERRORCODE_HPP
#define TRAINTASTIC_SERVER_CORE_ERRORCODE_HPP
#include <system_error>
enum class TrainError
enum class ErrorCode
{
// zero means no error!
InvalidThrottle = 1,
AlreadyAcquired,
CanNotActivateTrain,
TrainMustBeStoppedToChangeDirection,
UnknownDecoderAddress,
DecoderNotAssignedToAVehicle,
VehicleNotAssignedToATrain,
};
template<>
struct std::is_error_code_enum<TrainError> : std::true_type {};
struct std::is_error_code_enum<ErrorCode> : std::true_type {};
std::error_code make_error_code(TrainError ec);
std::error_code make_error_code(ErrorCode ec);
#endif

Datei anzeigen

@ -0,0 +1,29 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "nonpersistentobject.hpp"
#include <cassert>
std::string NonPersistentObject::getObjectId() const
{
//assert(false); // Object is not stored or serialized for network, method may not be called.
return "";
}

Datei anzeigen

@ -1,9 +1,8 @@
/**
* client/src/widget/throttle/throttlestopbutton.hpp
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021,2023 Reinder Feenstra
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -20,24 +19,18 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLESTOPBUTTON_HPP
#define TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLESTOPBUTTON_HPP
#ifndef TRAINTASTIC_SERVER_CORE_NONPERSISTENTOBJECT_HPP
#define TRAINTASTIC_SERVER_CORE_NONPERSISTENTOBJECT_HPP
#include "abstractthrottlebutton.hpp"
#include "object.hpp"
class AbstractProperty;
class ThrottleStopButton : public AbstractThrottleButton
/**
* \brief Base class for objects that are never stored
*/
class NonPersistentObject : public Object
{
private:
AbstractProperty* m_throttle;
AbstractProperty* m_throttleSpeed;
AbstractProperty* m_emergencyStop;
public:
ThrottleStopButton(ObjectPtr object, QWidget* parent = nullptr);
void click() final;
std::string getObjectId() const final;
};
#endif

Datei anzeigen

@ -27,7 +27,7 @@
#include "decoderfunction.hpp"
#include "decoderfunctions.hpp"
#include "../protocol/dcc/dcc.hpp"
#include "../throttle/throttle.hpp"
#include "../../throttle/throttle.hpp"
#include "../../world/world.hpp"
#include "../../core/objectproperty.tpp"
#include "../../core/attributes.hpp"
@ -35,6 +35,7 @@
#include "../../log/log.hpp"
#include "../../utils/displayname.hpp"
#include "../../utils/almostzero.hpp"
#include "../../vehicle/rail/railvehicle.hpp"
CREATE_IMPL(Decoder)
@ -79,7 +80,8 @@ Decoder::Decoder(World& world, std::string_view _id) :
emergencyStop{this, "emergency_stop", false, PropertyFlags::ReadWrite,
[this](const bool& /*value*/)
{
changed(DecoderChangeFlags::EmergencyStop);
throttle.setValueInternal(0.0f);
changed(DecoderChangeFlags::EmergencyStop | DecoderChangeFlags::Throttle);
updateEditable();
}},
direction{this, "direction", Direction::Forward, PropertyFlags::ReadWrite,
@ -101,8 +103,9 @@ Decoder::Decoder(World& world, std::string_view _id) :
{
changed(DecoderChangeFlags::SpeedSteps);
}},
vehicle{this, "vehicle", nullptr, PropertyFlags::ReadOnly},
throttle{this, "throttle", throttleMin, PropertyFlags::ReadWrite,
[this](const double& /*value*/)
[this](const float& /*value*/)
{
changed(DecoderChangeFlags::Throttle);
updateEditable();
@ -155,6 +158,9 @@ Decoder::Decoder(World& world, std::string_view _id) :
Attributes::addVisible(speedSteps, false);
m_interfaceItems.add(speedSteps);
Attributes::addObjectEditor(vehicle, false);
m_interfaceItems.add(vehicle);
Attributes::addMinMax(throttle, throttleMin, throttleMax);
Attributes::addObjectEditor(throttle, false);
m_interfaceItems.add(throttle);

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2023 Reinder Feenstra
* Copyright (C) 2019-2023,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -37,6 +37,7 @@
enum class DecoderChangeFlags;
class DecoderFunction;
class RailVehicle;
class Throttle;
class Decoder : public IdObject
@ -109,6 +110,7 @@ class Decoder : public IdObject
Property<Direction> direction;
Method<void()> toggleDirection;
Property<uint8_t> speedSteps;
ObjectProperty<RailVehicle> vehicle;
Property<float> throttle;
ObjectProperty<DecoderFunctions> functions;
Property<std::string> notes;

Datei anzeigen

@ -127,6 +127,8 @@ DCCEXInterface::DCCEXInterface(World& world, std::string_view _id)
updateVisible();
}
DCCEXInterface::~DCCEXInterface() = default;
std::span<const DecoderProtocol> DCCEXInterface::decoderProtocols() const
{
static constexpr std::array<DecoderProtocol, 2> protocols{DecoderProtocol::DCCShort, DecoderProtocol::DCCLong};

Datei anzeigen

@ -78,6 +78,7 @@ class DCCEXInterface final
ObjectProperty<DCCEX::Settings> dccex;
DCCEXInterface(World& world, std::string_view _id);
~DCCEXInterface() final;
// DecoderController:
std::span<const DecoderProtocol> decoderProtocols() const final;

Datei anzeigen

@ -74,6 +74,8 @@ ECoSInterface::ECoSInterface(World& world, std::string_view _id)
m_interfaceItems.insertBefore(outputs, notes);
}
ECoSInterface::~ECoSInterface() = default;
std::span<const DecoderProtocol> ECoSInterface::decoderProtocols() const
{
static constexpr std::array<DecoderProtocol, 4> protocols{DecoderProtocol::DCCShort, DecoderProtocol::DCCLong, DecoderProtocol::Motorola, DecoderProtocol::Selectrix};

Datei anzeigen

@ -74,6 +74,7 @@ class ECoSInterface final
ObjectProperty<ECoS::Settings> ecos;
ECoSInterface(World& world, std::string_view _id);
~ECoSInterface() final;
// DecoderController:
std::span<const DecoderProtocol> decoderProtocols() const final;

Datei anzeigen

@ -120,6 +120,8 @@ LocoNetInterface::LocoNetInterface(World& world, std::string_view _id)
typeChanged();
}
LocoNetInterface::~LocoNetInterface() = default;
bool LocoNetInterface::send(std::span<uint8_t> packet)
{
if(m_kernel)

Datei anzeigen

@ -79,6 +79,7 @@ class LocoNetInterface final
ObjectProperty<LocoNet::Settings> loconet;
LocoNetInterface(World& world, std::string_view _id);
~LocoNetInterface() final;
//! \brief Send LocoNet packet
//! \param[in] packet LocoNet packet bytes, exluding checksum.

Datei anzeigen

@ -102,6 +102,8 @@ TraintasticDIYInterface::TraintasticDIYInterface(World& world, std::string_view
updateVisible();
}
TraintasticDIYInterface::~TraintasticDIYInterface() = default;
std::pair<uint32_t, uint32_t> TraintasticDIYInterface::inputAddressMinMax(uint32_t /*channel*/) const
{
return {TraintasticDIY::Kernel::ioAddressMin, TraintasticDIY::Kernel::ioAddressMax};

Datei anzeigen

@ -71,6 +71,7 @@ class TraintasticDIYInterface final
ObjectProperty<TraintasticDIY::Settings> traintasticDIY;
TraintasticDIYInterface(World& world, std::string_view _id);
~TraintasticDIYInterface() final;
// InputController:
std::pair<uint32_t, uint32_t> inputAddressMinMax(uint32_t /*channel*/) const final;

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022-2024 Reinder Feenstra
* Copyright (C) 2022-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -21,7 +21,7 @@
*/
#include "withrottleinterface.hpp"
#include "../throttle/list/throttlelistcolumn.hpp"
#include "../../throttle/list/throttlelistcolumn.hpp"
#include "../protocol/withrottle/kernel.hpp"
#include "../protocol/withrottle/settings.hpp"
#include "../protocol/withrottle/iohandler/tcpiohandler.hpp"
@ -34,7 +34,7 @@
#include "../../utils/displayname.hpp"
#include "../../world/world.hpp"
static constexpr auto throttleListColumns = ThrottleListColumn::Id | ThrottleListColumn::Name;
static constexpr auto throttleListColumns = ThrottleListColumn::Name;
CREATE_IMPL(WiThrottleInterface)

Datei anzeigen

@ -24,7 +24,7 @@
#define TRAINTASTIC_SERVER_HARDWARE_INTERFACE_WITHROTTLEINTERFACE_HPP
#include "interface.hpp"
#include "../throttle/throttlecontroller.hpp"
#include "../../throttle/throttlecontroller.hpp"
#include "../../core/objectproperty.hpp"
namespace WiThrottle {

Datei anzeigen

@ -158,6 +158,8 @@ XpressNetInterface::XpressNetInterface(World& world, std::string_view _id)
updateVisible();
}
XpressNetInterface::~XpressNetInterface() = default;
std::span<const DecoderProtocol> XpressNetInterface::decoderProtocols() const
{
static constexpr std::array<DecoderProtocol, 2> protocols{DecoderProtocol::DCCShort, DecoderProtocol::DCCLong};

Datei anzeigen

@ -78,6 +78,7 @@ class XpressNetInterface final
ObjectProperty<XpressNet::Settings> xpressnet;
XpressNetInterface(World& world, std::string_view _id);
~XpressNetInterface() final;
// DecoderController:
std::span<const DecoderProtocol> decoderProtocols() const final;

Datei anzeigen

@ -90,6 +90,8 @@ Z21Interface::Z21Interface(World& world, std::string_view _id)
m_interfaceItems.insertBefore(firmwareVersion, notes);
}
Z21Interface::~Z21Interface() = default;
std::span<const DecoderProtocol> Z21Interface::decoderProtocols() const
{
static constexpr std::array<DecoderProtocol, 3> protocols{DecoderProtocol::DCCShort, DecoderProtocol::DCCLong, DecoderProtocol::Motorola};

Datei anzeigen

@ -69,6 +69,7 @@ class Z21Interface final
Property<std::string> firmwareVersion;
Z21Interface(World& world, std::string_view _id);
~Z21Interface() final;
// DecoderController:
std::span<const DecoderProtocol> decoderProtocols() const final;

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2022,2024 Reinder Feenstra
* Copyright (C) 2019-2022,2024-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -44,9 +44,3 @@ Output::Output(std::shared_ptr<OutputController> outputController, OutputChannel
m_interfaceItems.add(onValueChangedGeneric);
}
std::string Output::getObjectId() const
{
assert(false); // Object is not stored or serialized for network, method may not be called.
return "";
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2020,2022,2024 Reinder Feenstra
* Copyright (C) 2019-2020,2022,2024-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -23,7 +23,7 @@
#ifndef TRAINTASTIC_SERVER_HARDWARE_OUTPUT_OUTPUT_HPP
#define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_OUTPUT_HPP
#include "../../core/object.hpp"
#include "../../core/nonpersistentobject.hpp"
#include <set>
#include <traintastic/enum/outputchannel.hpp>
#include <traintastic/enum/outputtype.hpp>
@ -37,7 +37,7 @@
class OutputController;
class Output : public Object
class Output : public NonPersistentObject
{
friend class OutputController;
@ -61,8 +61,6 @@ class Output : public Object
* \return Unique identifier, can be any number/mask.
*/
virtual uint32_t id() const = 0;
std::string getObjectId() const final;
};
#endif

Datei anzeigen

@ -25,14 +25,16 @@
#include "messages.hpp"
#include "../../decoder/decoder.hpp" // TODO: remove when migrated to Train control
#include "../../interface/interface.hpp"
#include "../../throttle/hardwarethrottle.hpp"
#include "../../throttle/throttlecontroller.hpp"
#include "../../../throttle/hardwarethrottle.hpp"
#include "../../../throttle/throttlecontroller.hpp"
#include "../../../core/eventloop.hpp"
#include "../../../core/method.tpp"
#include "../../../core/objectproperty.tpp"
#include "../../../clock/clock.hpp"
#include "../../../log/log.hpp"
#include "../../../log/logmessageexception.hpp"
#include "../../../train/train.hpp"
#include "../../../train/trainvehiclelist.hpp"
#include "../../../utils/fromchars.hpp"
#include "../../../utils/setthreadname.hpp"
#include "../../../utils/startswith.hpp"
@ -318,45 +320,49 @@ void Kernel::receiveFrom(std::string_view message, IOHandler::ClientId clientId)
if(!throttle)
return;
switch(throttle->acquire(address.isLong ? DecoderProtocol::DCCLong : DecoderProtocol::DCCShort, address.address, steal))
const auto ec = throttle->acquire(address.isLong ? DecoderProtocol::DCCLong : DecoderProtocol::DCCShort, address.address, steal);
if(!ec)
{
case Throttle::AcquireResult::Success:
if(auto* multiThrottle = getMultiThrottle(clientId, multiThrottleId))
{
if(auto* multiThrottle = getMultiThrottle(clientId, multiThrottleId))
{
multiThrottle->address = address.address;
multiThrottle->isLongAddress = address.isLong;
}
else
assert(false);
postSendTo(throttleCommand(multiThrottleId, '+', address.address, address.isLong), clientId);
std::unordered_map<uint32_t, std::string_view> functionNames;
for(const auto& f : *throttle->decoder()->functions)
functionNames.emplace(f->number.value(), f->name.value());
postSendTo(throttleFuctionNames(multiThrottleId, address.address, address.isLong, functionNames), clientId);
for(const auto& f : *throttle->decoder()->functions)
postSendTo(throttleFunction(multiThrottleId, address.address, address.isLong, f->number, f->value), clientId);
if(throttle->decoder()->emergencyStop)
postSendTo(throttleEstop(multiThrottleId, address.address, address.isLong), clientId);
else
postSendTo(throttleSpeed(multiThrottleId, address.address, address.isLong, std::round(throttle->throttle * speedMax)), clientId);
postSendTo(throttleDirection(multiThrottleId, address.address, address.isLong, throttle->direction), clientId);
postSendTo(throttleSpeedStepMode(multiThrottleId, address.address, address.isLong, 128), clientId);
break;
multiThrottle->address = address.address;
multiThrottle->isLongAddress = address.isLong;
}
case Throttle::AcquireResult::FailedNonExisting:
postSendTo(alert(std::string("Unknown ").append(address.isLong ? "long" : "short").append(" address: ").append(std::to_string(address.address))), clientId);
break;
else
assert(false);
case Throttle::AcquireResult::FailedInUse:
postSendTo(throttleSteal(multiThrottleId, address.address, address.isLong), clientId);
break;
postSendTo(throttleCommand(multiThrottleId, '+', address.address, address.isLong), clientId);
for(const auto& vehicle : *throttle->train->vehicles)
{
if(vehicle->decoder && vehicle->decoder->address == address.address)
{
const auto& functions = vehicle->decoder->functions;
std::unordered_map<uint32_t, std::string_view> functionNames;
for(const auto& f : *functions)
functionNames.emplace(f->number.value(), f->name.value());
postSendTo(throttleFuctionNames(multiThrottleId, address.address, address.isLong, functionNames), clientId);
for(const auto& f : *functions)
postSendTo(throttleFunction(multiThrottleId, address.address, address.isLong, f->number, f->value), clientId);
break;
}
}
if(throttle->train->emergencyStop)
postSendTo(throttleEstop(multiThrottleId, address.address, address.isLong), clientId);
else
postSendTo(throttleSpeed(multiThrottleId, address.address, address.isLong, std::round(throttle->train->speed.value() / throttle->train->speedMax.getValue(throttle->train->speed.unit()) * speedMax)), clientId);
postSendTo(throttleDirection(multiThrottleId, address.address, address.isLong, throttle->train->direction), clientId);
postSendTo(throttleSpeedStepMode(multiThrottleId, address.address, address.isLong, 128), clientId);
}
else
{
postSendTo(alert(ec.message()), clientId);
}
});
@ -500,7 +506,7 @@ const std::shared_ptr<HardwareThrottle>& Kernel::getThottle(IOHandler::ClientId
auto it = multiThrottles.insert(std::move(n)).position;
const auto& throttle = it->second.throttle;
throttle->name.setValueInternal(buildName(itClient->second.name, multiThrottleId));
throttle->released.connect(std::bind(&Kernel::throttleReleased, this, clientId, multiThrottleId));
throttle->onRelease.connect(std::bind_front(&Kernel::throttleReleased, this, clientId, multiThrottleId));
return throttle;
}
@ -512,7 +518,7 @@ const std::shared_ptr<HardwareThrottle>& Kernel::getThottle(IOHandler::ClientId
assert(success);
const auto& throttle = it->second.throttle;
throttle->name.setValueInternal(buildName(itClient->second.name, multiThrottleId));
throttle->released.connect(std::bind(&Kernel::throttleReleased, this, clientId, multiThrottleId));
throttle->onRelease.connect(std::bind_front(&Kernel::throttleReleased, this, clientId, multiThrottleId));
return throttle;
}
}
@ -520,6 +526,28 @@ const std::shared_ptr<HardwareThrottle>& Kernel::getThottle(IOHandler::ClientId
return noThrottle;
}
const std::shared_ptr<Decoder>& Kernel::getDecoder(IOHandler::ClientId clientId, char multiThrottleId)
{
assert(isEventLoopThread());
static const std::shared_ptr<Decoder> noDecoder;
const auto* multiThrottle = getMultiThrottle(clientId, multiThrottleId);
if(multiThrottle && multiThrottle->throttle->acquired())
{
for(const auto& vehicle : *multiThrottle->throttle->train->vehicles)
{
if(vehicle->decoder && vehicle->decoder->address == multiThrottle->address)
{
return vehicle->decoder.value();
}
}
}
return noDecoder;
}
void Kernel::multiThrottleAction(IOHandler::ClientId clientId, char multiThrottleId, const Address& /*address*/, ThrottleCommand throttleCommand, std::string_view message)
{
assert(isKernelThread());
@ -543,8 +571,7 @@ void Kernel::multiThrottleAction(IOHandler::ClientId clientId, char multiThrottl
}
else
{
throttle->decoder()->emergencyStop = false;
throttle->throttle = static_cast<float>(value) / speedMax;
throttle->setSpeed(throttle->train->speedMax.value() * value / speedMax, throttle->train->speedMax.unit());
}
}
});
@ -579,9 +606,9 @@ void Kernel::multiThrottleAction(IOHandler::ClientId clientId, char multiThrottl
EventLoop::call(
[this, clientId, multiThrottleId, number, force=(throttleCommand == ThrottleCommand::ForceFunction), value]()
{
if(const auto& throttle = getThottle(clientId, multiThrottleId); throttle && throttle->acquired())
if(const auto& decoder = getDecoder(clientId, multiThrottleId))
{
if(const auto& function = throttle->decoder()->getFunction(number))
if(const auto& function = decoder->getFunction(number))
{
if(force) // set
{
@ -633,8 +660,7 @@ void Kernel::multiThrottleAction(IOHandler::ClientId clientId, char multiThrottl
{
if(const auto& throttle = getThottle(clientId, multiThrottleId); throttle && throttle->acquired())
{
throttle->decoder()->emergencyStop = false;
throttle->throttle = Throttle::throttleStop;
throttle->setSpeed(0.0, throttle->train->speed.unit());
}
});
break;
@ -661,10 +687,11 @@ void Kernel::multiThrottleAction(IOHandler::ClientId clientId, char multiThrottl
{
if(const auto* multiThrottle = getMultiThrottle(clientId, multiThrottleId))
{
if(multiThrottle->throttle->decoder()->emergencyStop)
const auto& train = *multiThrottle->throttle->train;
if(train.emergencyStop)
postSendTo(throttleEstop(multiThrottleId, multiThrottle->address, multiThrottle->isLongAddress), clientId);
else
postSendTo(throttleSpeed(multiThrottleId, multiThrottle->address, multiThrottle->isLongAddress, std::round(multiThrottle->throttle->throttle * speedMax)), clientId);
postSendTo(throttleSpeed(multiThrottleId, multiThrottle->address, multiThrottle->isLongAddress, std::round(train.speed.value() / train.speedMax.getValue(train.speed.unit()) * speedMax)), clientId);
}
});
break;
@ -675,7 +702,7 @@ void Kernel::multiThrottleAction(IOHandler::ClientId clientId, char multiThrottl
{
if(const auto* multiThrottle = getMultiThrottle(clientId, multiThrottleId))
{
postSendTo(throttleDirection(multiThrottleId, multiThrottle->address, multiThrottle->isLongAddress, multiThrottle->throttle->direction), clientId);
postSendTo(throttleDirection(multiThrottleId, multiThrottle->address, multiThrottle->isLongAddress, multiThrottle->throttle->train->direction), clientId);
}
});
break;
@ -698,7 +725,7 @@ void Kernel::multiThrottleAction(IOHandler::ClientId clientId, char multiThrottl
}
}
void Kernel::throttleReleased(IOHandler::ClientId clientId, char multiThrottleId)
void Kernel::throttleReleased(IOHandler::ClientId clientId, char multiThrottleId, const std::shared_ptr<Throttle>& /*throttle*/)
{
assert(isEventLoopThread());

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022-2023 Reinder Feenstra
* Copyright (C) 2022-2023,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -31,6 +31,8 @@
#include "iohandler/iohandler.hpp"
class Clock;
class Decoder;
class Throttle;
class HardwareThrottle;
class ThrottleController;
@ -96,10 +98,11 @@ class Kernel : public ::KernelBase
MultiThrottle* getMultiThrottle(IOHandler::ClientId clientId, char multiThrottleId);
const std::shared_ptr<HardwareThrottle>& getThottle(IOHandler::ClientId clientId, char multiThrottleId = invalidMultiThrottleId);
const std::shared_ptr<Decoder>& getDecoder(IOHandler::ClientId clientId, char multiThrottleId);
void multiThrottleAction(IOHandler::ClientId clientId, char multiThrottleId, const Address& address, ThrottleCommand throttleCommand, std::string_view message);
void throttleReleased(IOHandler::ClientId clientId, char multiThrottleId);
void throttleReleased(IOHandler::ClientId clientId, char multiThrottleId, const std::shared_ptr<Throttle>& throttle);
public:
static constexpr char invalidMultiThrottleId = '\0';

Datei anzeigen

@ -1,312 +0,0 @@
/**
* server/src/hardware/throttle/throttle.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "throttle.hpp"
#include "list/throttlelist.hpp"
#include "list/throttlelisttablemodel.hpp"
#include "../../core/attributes.hpp"
#include "../../core/method.tpp"
#include "../../core/objectproperty.tpp"
#include "../../core/objectvectorproperty.tpp"
#include "../../hardware/decoder/decoder.hpp"
#include "../../log/log.hpp"
#include "../../train/train.hpp"
#include "../../utils/displayname.hpp"
#include "../../utils/valuestep.hpp"
#include "../../world/world.hpp"
namespace {
constexpr double getValueStep(double value, SpeedUnit unit)
{
switch(unit)
{
case SpeedUnit::KiloMeterPerHour:
return (value >= 40.0) ? 5.0 : 2.0;
case SpeedUnit::MilePerHour:
return (value >= 30.0) ? 5.0 : 2.0;
case SpeedUnit::MeterPerSecond:
return (value >= 10.0) ? 1.0 : 0.5;
}
return 1;
}
}
Throttle::Throttle(World& world, std::string_view _id)
: IdObject(world, _id)
, name{this, "name", id, PropertyFlags::ReadWrite | PropertyFlags::Store}
, direction{this, "direction", Direction::Forward, PropertyFlags::ReadOnly}
, throttle{this, "throttle", throttleMin, PropertyFlags::ReadWrite,
[this](const float& value)
{
if(m_decoder)
m_decoder->throttle = value;
},
[this](float& /*value*/) -> bool
{
return acquired();
}}
, train{this, "train", nullptr, PropertyFlags::ReadOnly}
, emergencyStop{*this, "emergency_stop",
[this]()
{
if(acquired())
{
if(train)
{
train->emergencyStop = true;
}
if(m_decoder)
{
m_decoder->emergencyStop = true;
}
return true;
}
return false;
}}
, stop{*this, "stop",
[this](bool immediate)
{
if(acquired())
{
if(train)
{
train->emergencyStop = false;
if(immediate)
{
train->setSpeed(*this, 0.0);
}
else
{
train->setTargetSpeed(*this, 0.0);
}
}
if(m_decoder)
{
m_decoder->emergencyStop = false;
m_decoder->throttle = throttleStop;
}
return true;
}
return false;
}}
, faster{*this, "faster",
[this](bool immediate)
{
if(acquired())
{
if(train)
{
train->emergencyStop = false;
if(immediate)
{
const double value = train->speed.value();
const double step = getValueStep(value, train->speed.unit());
train->setSpeed(*this, valueStepUp(value, step));
}
else
{
const double value = train->throttleSpeed.value();
const double step = getValueStep(value, train->throttleSpeed.unit());
train->setTargetSpeed(*this, valueStepUp(value, step));
}
}
return true;
}
return false;
}}
, slower{*this, "slower",
[this](bool immediate)
{
if(acquired())
{
if(train)
{
train->emergencyStop = false;
if(immediate)
{
const double value = train->speed.value();
const double step = getValueStep(value, train->speed.unit());
train->setSpeed(*this, valueStepDown(value, step));
}
else
{
const double value = train->throttleSpeed.value();
const double step = getValueStep(value, train->throttleSpeed.unit());
train->setTargetSpeed(*this, valueStepDown(value, step));
}
}
return true;
}
return false;
}}
, setDirection{*this, "set_direction",
[this](Direction value)
{
if(acquired() && (value == Direction::Forward || value == Direction::Reverse))
{
if(train)
{
return !train->setDirection(*this, value);
}
if(m_decoder)
{
m_decoder->direction = value;
}
return true;
}
return false;
}}
{
const bool editable = contains(m_world.state.value(), WorldState::Edit);
Attributes::addDisplayName(name, DisplayName::Object::name);
Attributes::addEnabled(name, editable);
m_interfaceItems.add(name);
Attributes::addEnabled(direction, false);
Attributes::addValues(direction, DirectionValues);
Attributes::addObjectEditor(direction, false);
m_interfaceItems.add(direction);
Attributes::addEnabled(throttle, false);
Attributes::addMinMax(throttle, throttleMin, throttleMax);
Attributes::addObjectEditor(throttle, false);
m_interfaceItems.add(throttle);
Attributes::addObjectEditor(train, false);
m_interfaceItems.add(train);
Attributes::addEnabled(emergencyStop, false);
Attributes::addObjectEditor(emergencyStop, false);
m_interfaceItems.add(emergencyStop);
Attributes::addEnabled(stop, false);
Attributes::addObjectEditor(stop, false);
m_interfaceItems.add(stop);
Attributes::addEnabled(setDirection, false);
Attributes::addObjectEditor(setDirection, false);
m_interfaceItems.add(setDirection);
}
#ifndef NDEBUG
Throttle::~Throttle()
{
assert(!acquired());
}
#endif
bool Throttle::acquired() const
{
return m_decoder.operator bool() || train.operator bool();
}
std::error_code Throttle::acquire(const std::shared_ptr<Train>& acquireTrain, bool steal)
{
assert(acquireTrain);
const auto stole = steal && acquireTrain->hasThrottle();
const std::string stoleFrom = stole ? acquireTrain->throttleName() : std::string{};
const auto ec = acquireTrain->acquire(*this, steal);
if(ec)
{
Log::log(*this, LogMessage::D3001_ACQUIRING_TRAIN_X_FAILED_X, acquireTrain->name.value(), ec.message());
return ec;
}
if(acquired())
{
release();
}
assert(!train);
train.setValueInternal(acquireTrain);
if(stole)
{
Log::log(*this, LogMessage::N3005_THROTTLE_X_STOLE_TRAIN_X_FROM_THROTTLE_X, name.value(), train->name.value(), stoleFrom);
}
else
{
Log::log(*this, LogMessage::I3001_THROTTLE_X_ACQUIRED_TRAIN_X, name.value(), train->name.value());
}
Attributes::setEnabled({emergencyStop, throttle, stop, setDirection}, true);
return {};
}
void Throttle::release(bool stopIt)
{
if(!acquired())
return;
if(stopIt)
{
emergencyStop();
}
if(m_decoder)
{
m_decoder->release(*this);
m_decoder.reset();
}
else if(train)
{
const auto logMessage = !stopIt && !train->isStopped.value() ? LogMessage::N3006_THROTTLE_X_RELEASED_TRAIN_X_WITHOUT_STOPPING_IT : LogMessage::I3002_THROTTLE_X_RELEASED_TRAIN_X;
Log::log(*this, logMessage, name.value(), train->name.value());
train->release(*this);
train.setValueInternal(nullptr);
}
Attributes::setEnabled({emergencyStop, throttle, stop, setDirection}, false);
released();
}
void Throttle::destroying()
{
m_world.throttles->removeObject(shared_ptr<Throttle>());
release();
IdObject::destroying();
}
void Throttle::addToWorld()
{
IdObject::addToWorld();
m_world.throttles->addObject(shared_ptr<Throttle>());
}
Throttle::AcquireResult Throttle::acquire(std::shared_ptr<Decoder> decoder, bool steal)
{
if(!decoder->acquire(*this, steal))
return AcquireResult::FailedInUse;
m_decoder = std::move(decoder);
Attributes::setEnabled({emergencyStop, throttle, stop, setDirection}, true);
return AcquireResult::Success;
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021-2024 Reinder Feenstra
* Copyright (C) 2021-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -25,6 +25,8 @@
#include "test.hpp"
#include "checkarguments.hpp"
#include "sandbox.hpp"
#include "script.hpp"
#include "to.hpp"
#include "../board/board.hpp"
#include "../board/boardlist.hpp"
@ -94,6 +96,8 @@
#include "../hardware/identification/identification.hpp"
#include "../hardware/identification/list/identificationlist.hpp"
#include "../throttle/scriptthrottle.hpp"
#include "../vehicle/rail/railvehiclelist.hpp"
#include "../vehicle/rail/locomotive.hpp"
#include "../vehicle/rail/freightwagon.hpp"
@ -152,6 +156,9 @@ void Class::registerValues(lua_State* L)
lua_pushcfunction(L, getClass);
lua_setfield(L, -2, "get");
lua_pushcfunction(L, create_throttle);
lua_setfield(L, -2, "create_throttle");
registerValue<Board>(L, "BOARD");
registerValue<BoardList>(L, "BOARD_LIST");
@ -216,6 +223,8 @@ void Class::registerValues(lua_State* L)
registerValue<Identification>(L, "IDENTIFICATION");
registerValue<IdentificationList>(L, "IDENTIFICATION_LIST");
registerValue<ScriptThrottle>(L, "SCRIPT_THROTTLE");
registerValue<RailVehicleList>(L, "RAIL_VEHICLE_LIST");
registerValue<Locomotive>(L, "LOCOMOTIVE");
registerValue<FreightWagon>(L, "FREIGHT_WAGON");
@ -280,4 +289,18 @@ int Class::getClass(lua_State* L)
return 1;
}
int Class::create_throttle(lua_State* L)
{
const int n = checkArguments(L, 0, 1);
auto& stateData = Sandbox::getStateData(L);
auto throttle = ScriptThrottle::create(stateData.script().world());
if(n >= 1)
{
throttle->name = to<std::string>(L, 1);
}
stateData.addThrottle(throttle);
Object::push(L, throttle);
return 1;
}
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021 Reinder Feenstra
* Copyright (C) 2021,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -46,6 +46,7 @@ struct Class
static int __tostring(lua_State* L);
static int getClass(lua_State* L);
static int create_throttle(lua_State* L);
};
}

Datei anzeigen

@ -35,6 +35,7 @@
#include <traintastic/enum/outputchannel.hpp>
#include <traintastic/enum/outputtype.hpp>
#include <traintastic/enum/outputpairvalue.hpp>
#include <traintastic/enum/speedunit.hpp>
#include <traintastic/enum/textalign.hpp>
#include "../../src/enum/tristate.hpp"
#include <traintastic/enum/turnoutposition.hpp>
@ -54,6 +55,7 @@
OutputChannel, \
OutputType, \
OutputPairValue, \
SpeedUnit, \
TextAlign, \
TriState, \
TurnoutPosition, \

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2024 Reinder Feenstra
* Copyright (C) 2019-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -25,6 +25,7 @@
#include "object/objectlist.hpp"
#include "object/interface.hpp"
#include "object/loconetinterface.hpp"
#include "object/scriptthrottle.hpp"
namespace Lua::Object {
@ -36,6 +37,7 @@ void registerTypes(lua_State* L)
ObjectList::registerType(L);
Interface::registerType(L);
LocoNetInterface::registerType(L);
ScriptThrottle::registerType(L);
// weak table for object userdata:
lua_newtable(L);
@ -67,6 +69,10 @@ void push(lua_State* L, const ObjectPtr& value)
luaL_setmetatable(L, LocoNetInterface::metaTableName);
else if(dynamic_cast<AbstractObjectList*>(value.get()))
luaL_setmetatable(L, ObjectList::metaTableName);
else if(dynamic_cast<::ScriptThrottle*>(value.get()))
{
luaL_setmetatable(L, ScriptThrottle::metaTableName);
}
else
luaL_setmetatable(L, Object::metaTableName);

Datei anzeigen

@ -0,0 +1,72 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "scriptthrottle.hpp"
#include "object.hpp"
#include "../check.hpp"
#include "../checkarguments.hpp"
#include "../push.hpp"
#include "../to.hpp"
#include "../metatable.hpp"
#include "../../throttle/scriptthrottle.hpp"
#include "../../train/train.hpp"
namespace Lua::Object {
void ScriptThrottle::registerType(lua_State* L)
{
MetaTable::clone(L, Object::metaTableName, metaTableName);
lua_pushcfunction(L, __index);
lua_setfield(L, -2, "__index");
lua_pop(L, 1);
}
int ScriptThrottle::index(lua_State* L, ::ScriptThrottle& object)
{
const auto key = to<std::string_view>(L, 2);
LUA_OBJECT_METHOD(acquire)
LUA_OBJECT_METHOD(release)
return Object::index(L, object);
}
int ScriptThrottle::__index(lua_State* L)
{
return index(L, *check<::ScriptThrottle>(L, 1));
}
int ScriptThrottle::acquire(lua_State* L)
{
const int n = checkArguments(L, 1, 2);
auto throttle = check<::ScriptThrottle>(L, lua_upvalueindex(1));
const auto ec = throttle->acquire(check<Train>(L, 1), (n >= 2) && to<bool>(L, 2));
Lua::push(L, ec.value());
return 1;
}
int ScriptThrottle::release(lua_State* L)
{
const int n = checkArguments(L, 0, 1);
check<::ScriptThrottle>(L, lua_upvalueindex(1))->release((n < 1) || to<bool>(L, 1));
return 0;
}
}

Datei anzeigen

@ -1,9 +1,8 @@
/**
* client/src/widget/throttle/throttledirectionbutton.hpp
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021 Reinder Feenstra
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -20,29 +19,30 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLEDIRECTIONBUTTON_HPP
#define TRAINTASTIC_CLIENT_WIDGET_THROTTLE_THROTTLEDIRECTIONBUTTON_HPP
#ifndef TRAINTASTIC_SERVER_LUA_OBJECT_SCRIPTTHROTTLE_HPP
#define TRAINTASTIC_SERVER_LUA_OBJECT_SCRIPTTHROTTLE_HPP
#include "abstractthrottlebutton.hpp"
#include <traintastic/enum/direction.hpp>
#include <lua.hpp>
#include "../../throttle/scriptthrottle.hpp"
class AbstractProperty;
namespace Lua::Object {
class ThrottleDirectionButton : public AbstractThrottleButton
class ScriptThrottle
{
Q_OBJECT
private:
static int __index(lua_State* L);
private:
const Direction m_direction;
AbstractProperty* m_directionProperty;
static int acquire(lua_State* L);
static int release(lua_State* L);
private slots:
void directionChanged();
public:
static constexpr char const* metaTableName = "object.scriptthrottle";
public:
ThrottleDirectionButton(ObjectPtr object, Direction direction, QWidget* parent = nullptr);
static void registerType(lua_State* L);
void click() final;
static int index(lua_State* L, ::ScriptThrottle& object);
};
}
#endif

Datei anzeigen

@ -40,6 +40,7 @@
#include <traintastic/utils/str.hpp>
#include "../world/world.hpp"
#include "../hardware/output/outputcontroller.hpp"
#include "../throttle/scriptthrottle.hpp"
#define LUA_SANDBOX "_sandbox"
#define LUA_SANDBOX_GLOBALS "_sandbox_globals"
@ -159,10 +160,10 @@ int Sandbox::__newindex(lua_State* L)
SandboxPtr Sandbox::create(Script& script)
{
lua_State* L = luaL_newstate();
auto* stateData = new StateData(script);
// create state data:
*static_cast<StateData**>(lua_getextraspace(L)) = new StateData(script);
lua_State* L = lua_newstate(alloc, stateData);
*static_cast<StateData**>(lua_getextraspace(L)) = stateData;
// register types:
PersistentVariables::registerType(L);
@ -341,6 +342,45 @@ int Sandbox::pcall(lua_State* L, int nargs, int nresults, int errfunc)
return r;
}
void* Sandbox::alloc(void* userData, void* ptr, size_t oldSize, size_t newSize)
{
auto& stateData = *static_cast<StateData*>(userData);
if(newSize == 0)
{
stateData.memoryUsed -= oldSize;
std::free(ptr);
return nullptr;
}
if(!ptr)
{
if(stateData.memoryUsed + newSize > stateData.memoryLimit)
{
return nullptr; // Memory limit reached
}
void* newptr = std::malloc(newSize);
if(newptr)
{
stateData.memoryUsed += newSize;
}
return newptr;
}
if(stateData.memoryUsed - oldSize + newSize > stateData.memoryLimit)
{
return nullptr; // Memory limit reached
}
void* newptr = std::realloc(ptr, newSize);
if(newptr)
{
stateData.memoryUsed = stateData.memoryUsed - oldSize + newSize;
}
return newptr;
}
void Sandbox::hook(lua_State* L, lua_Debug* /*ar*/)
{
if((std::chrono::steady_clock::now() - getStateData(L).pcallStart) > pcallDurationMax)
@ -373,6 +413,13 @@ Sandbox::StateData::~StateData()
}
}
}
// Destroy throttles:
while(!m_throttles.empty())
{
m_throttles.back()->destroy();
m_throttles.pop_back();
}
}
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2020,2022-2024 Reinder Feenstra
* Copyright (C) 2019-2020,2022-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -30,10 +30,12 @@
#include <limits>
#include <chrono>
#include <cassert>
#include <vector>
#include <lua.hpp>
class OutputController;
class Output;
class ScriptThrottle;
namespace Lua {
@ -52,6 +54,7 @@ class Sandbox
static int __index(lua_State* L);
static int __newindex(lua_State* L);
static void* alloc(void* ud, void* ptr, size_t osize, size_t nsize);
static void hook(lua_State* L, lua_Debug* /*ar*/);
public:
@ -66,8 +69,11 @@ class Sandbox
std::set<std::weak_ptr<Output>, std::owner_less<std::weak_ptr<Output>>>,
std::owner_less<std::weak_ptr<OutputController>>
> m_outputs;
std::vector<std::shared_ptr<ScriptThrottle>> m_throttles;
public:
static constexpr size_t memoryLimit = 1024 * 1024; // 1 MiB
size_t memoryUsed = 0;
std::chrono::time_point<std::chrono::steady_clock> pcallStart;
bool pcallExecutionTimeViolation;
@ -124,6 +130,11 @@ class Sandbox
{
m_outputs[outputController].emplace(output);
}
void addThrottle(std::shared_ptr<ScriptThrottle> throttle)
{
m_throttles.emplace_back(std::move(throttle));
}
};
static SandboxPtr create(Script& script);

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2024 Reinder Feenstra
* Copyright (C) 2019-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -269,15 +269,4 @@ void Script::stopSandbox()
}
}
bool Script::pcall(lua_State* L, int nargs, int nresults)
{
const bool success = Sandbox::pcall(L, nargs, nresults) == LUA_OK;
if(!success)
{
Log::log(*this, LogMessage::F9003_CALLING_FUNCTION_FAILED_X, lua_tostring(L, -1));
lua_pop(L, 1); // pop error message from the stack
}
return success;
}
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2024 Reinder Feenstra
* Copyright (C) 2019-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -53,7 +53,6 @@ class Script : public IdObject
void startSandbox();
void stopSandbox();
bool pcall(lua_State* L, int nargs = 0, int nresults = 0);
public:
CLASS_ID("lua.script")

Datei anzeigen

@ -47,6 +47,16 @@ class HandleList
{
}
auto begin() const
{
return m_handleToItem.begin();
}
auto end() const
{
return m_handleToItem.end();
}
Titem getItem(Handle handle) const
{
auto it = m_handleToItem.find(handle);

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2024 Reinder Feenstra
* Copyright (C) 2019-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -30,6 +30,7 @@
#ifndef NDEBUG
#include "../core/eventloop.hpp" // for: isEventLoopThread()
#endif
#include "../core/abstractobjectlist.hpp"
#include "../core/abstractunitproperty.hpp"
#include "../core/objectproperty.tpp"
#include "../core/tablemodel.hpp"
@ -40,6 +41,7 @@
#include "../board/tile/tiles.hpp"
#include "../hardware/input/monitor/inputmonitor.hpp"
#include "../hardware/output/keyboard/outputkeyboard.hpp"
#include "../throttle/clientthrottle.hpp"
#ifdef GetObject
#undef GetObject // GetObject is defined by a winapi header
@ -55,6 +57,15 @@ Session::Session(const std::shared_ptr<ClientConnection>& connection) :
Session::~Session()
{
assert(isEventLoopThread());
m_objectSignals.clear(); // disconnect all, we don't want m_handles modified during the loop
for(const auto& it : m_handles)
{
if(it.second && isSessionObject(it.second))
{
it.second->destroy();
}
}
}
bool Session::processMessage(const Message& message)
@ -120,6 +131,11 @@ bool Session::processMessage(const Message& message)
const auto counter = message.read<uint32_t>();
if(counter == m_handles.getCounter(handle))
{
if(auto object = m_handles.getItem(handle); object && isSessionObject(object))
{
object->destroy();
}
m_handles.removeHandle(handle);
auto it = m_objectSignals.find(handle);
@ -689,12 +705,81 @@ bool Session::processMessage(const Message& message)
}
break;
case Message::Command::CreateObject:
if(message.isRequest())
{
if(Traintastic::instance->world)
{
const auto classId = message.read<std::string_view>();
if(classId == ClientThrottle::classId)
{
auto throttle = ClientThrottle::create(*Traintastic::instance->world);
auto response = message.response();
writeObject(*response, throttle);
m_connection->sendMessage(std::move(response));
}
else
{
m_connection->sendMessage(message.errorResponse(LogMessage::C1015_UNKNOWN_OBJECT)); // FIXME change error
}
}
else
{
m_connection->sendMessage(message.errorResponse(LogMessage::C1015_UNKNOWN_OBJECT)); // FIXME change error
}
return true;
}
break;
case Message::Command::ObjectListGetObjects:
if(message.isRequest())
{
if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
{
if(auto* list = dynamic_cast<AbstractObjectList*>(object.get()))
{
const uint32_t startIndex = message.read<uint32_t>();
const uint32_t endIndex = message.read<uint32_t>();
if(endIndex >= startIndex && endIndex < list->length.value())
{
auto response = message.response();
for(uint32_t i = startIndex; i <= endIndex; i++)
{
writeObject(*response, list->getObject(i));
}
m_connection->sendMessage(std::move(response));
}
else // send error response
{
m_connection->sendMessage(message.errorResponse(LogMessage::C1017_INVALID_INDICES));
}
}
else
{
m_connection->sendMessage(message.errorResponse(LogMessage::C1015_UNKNOWN_OBJECT));
}
}
else
{
m_connection->sendMessage(message.errorResponse(LogMessage::C1015_UNKNOWN_OBJECT));
}
return true;
}
break;
default:
break;
}
return false;
}
bool Session::isSessionObject(const ObjectPtr& object)
{
assert(object);
return dynamic_cast<ClientThrottle*>(object.get());
}
void Session::writeObject(Message& message, const ObjectPtr& object)
{
message.writeBlock(); // object

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2024 Reinder Feenstra
* Copyright (C) 2019-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -71,6 +71,8 @@ class Session : public std::enable_shared_from_this<Session>
bool processMessage(const Message& message);
bool isSessionObject(const ObjectPtr& object);
void writeObject(Message& message, const ObjectPtr& object);
void writeTableModel(Message& message, const TableModelPtr& model);

Datei anzeigen

@ -23,14 +23,14 @@
#include "webthrottleconnection.hpp"
#include "server.hpp"
#include "../traintastic/traintastic.hpp"
#include "../core/errorcode.hpp"
#include "../core/eventloop.hpp"
#include "../core/method.tpp"
#include "../core/objectproperty.tpp"
#include "../hardware/decoder/decoder.hpp"
#include "../hardware/throttle/webthrottle.hpp"
#include "../throttle/webthrottle.hpp"
#include "../log/log.hpp"
#include "../train/train.hpp"
#include "../train/trainerror.hpp"
#include "../train/trainlist.hpp"
#include "../train/trainvehiclelist.hpp"
@ -301,7 +301,14 @@ void WebThrottleConnection::processMessage(const nlohmann::json& message)
}
else if(action == "stop")
{
throttle->stop(message.value("immediate", false));
if(message.value("immediate", false))
{
throttle->train->setSpeed(*throttle, 0.0);
}
else
{
throttle->train->setTargetSpeed(*throttle, 0.0);
}
}
else if(action == "faster")
{
@ -380,15 +387,15 @@ void WebThrottleConnection::sendError(uint32_t throttleId, std::error_code ec)
{
assert(isEventLoopThread());
if(ec == TrainError::AlreadyAcquired)
if(ec == ErrorCode::AlreadyAcquired)
{
sendError(throttleId, ec.message(), "already_acquired");
}
else if(ec == TrainError::CanNotActivateTrain)
else if(ec == ErrorCode::CanNotActivateTrain)
{
sendError(throttleId, ec.message(), "can_not_activate_train");
}
else if(ec == TrainError::TrainMustBeStoppedToChangeDirection)
else if(ec == ErrorCode::TrainMustBeStoppedToChangeDirection)
{
sendError(throttleId, ec.message(), "train_must_be_stopped_to_change_direction");
}
@ -438,8 +445,8 @@ const std::shared_ptr<WebThrottle>& WebThrottleConnection::getThrottle(uint32_t
m_throttleConnections.erase(throttleId);
m_throttles.erase(throttleId);
}));
m_throttleConnections.emplace(throttleId, it->second->released.connect(
[this, throttleId]()
m_throttleConnections.emplace(throttleId, it->second->onRelease.connect(
[this, throttleId](const std::shared_ptr<Throttle>& /*throttle*/)
{
released(throttleId);
}));

Datei anzeigen

@ -0,0 +1,74 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "clientthrottle.hpp"
#include "../core/attributes.hpp"
#include "../core/method.tpp"
#include "../core/objectproperty.tpp"
#include "../train/train.hpp"
#include "../world/world.hpp"
std::shared_ptr<ClientThrottle> ClientThrottle::create(World& world)
{
auto obj = std::make_shared<ClientThrottle>(world, getUniqueLogId("client_throttle"));
obj->addToList();
return obj;
}
ClientThrottle::ClientThrottle(World& world, std::string_view objectId)
: Throttle(world, objectId)
, m_acquire{*this, "acquire", MethodFlags::NoScript,
[this](const std::shared_ptr<Train>& acquireTrain, bool steal)
{
return acquire(acquireTrain, steal).value();
}}
, m_release{*this, "release", MethodFlags::NoScript, std::bind_front(&Throttle::release, this)}
, m_emergencyStop{*this, "emergency_stop", MethodFlags::NoScript, std::bind_front(&Throttle::emergencyStop, this)}
, m_setDirection{*this, "set_direction", MethodFlags::NoScript, std::bind_front(&Throttle::setDirection, this)}
, m_setSpeed{*this, "set_speed", MethodFlags::NoScript,
[this](double value, SpeedUnit unit, bool immediate)
{
return immediate ? setSpeed(value, unit) : setTargetSpeed(value, unit);
}}
, m_faster{*this, "faster", MethodFlags::NoScript, std::bind_front(&Throttle::faster, this)}
, m_slower{*this, "slower", MethodFlags::NoScript, std::bind_front(&Throttle::slower, this)}
{
Attributes::addObjectEditor(m_acquire, false);
m_interfaceItems.add(m_acquire);
Attributes::addObjectEditor(m_release, false);
m_interfaceItems.add(m_release);
Attributes::addObjectEditor(m_emergencyStop, false);
m_interfaceItems.add(m_emergencyStop);
Attributes::addObjectEditor(m_setDirection, false);
m_interfaceItems.add(m_setDirection);
Attributes::addObjectEditor(m_setSpeed, false);
m_interfaceItems.add(m_setSpeed);
Attributes::addObjectEditor(m_faster, false);
m_interfaceItems.add(m_faster);
Attributes::addObjectEditor(m_slower, false);
m_interfaceItems.add(m_slower);
}

Datei anzeigen

@ -0,0 +1,46 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_THROTTLE_CLIENTTHROTTLE_HPP
#define TRAINTASTIC_SERVER_THROTTLE_CLIENTTHROTTLE_HPP
#include "throttle.hpp"
class ClientThrottle : public Throttle
{
CLASS_ID("throttle.client")
private:
Method<int32_t(const std::shared_ptr<Train>&, bool)> m_acquire;
Method<void(bool)> m_release;
Method<bool()> m_emergencyStop;
Method<bool(Direction)> m_setDirection;
Method<bool(double, SpeedUnit, bool)> m_setSpeed;
Method<bool(bool)> m_faster;
Method<bool(bool)> m_slower;
public:
static std::shared_ptr<ClientThrottle> create(World& world);
ClientThrottle(World& world, std::string_view objectId);
};
#endif

Datei anzeigen

@ -1,9 +1,9 @@
/**
* server/src/hardware/throttle/hardwarethrottle.cpp
* server/src/throttle/hardwarethrottle.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022-2023 Reinder Feenstra
* Copyright (C) 2022-2023,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -21,56 +21,72 @@
*/
#include "hardwarethrottle.hpp"
#include "../../core/attributes.hpp"
#include "../../core/objectproperty.tpp"
#include "../../hardware/decoder/list/decoderlist.hpp"
#include "../../train/train.hpp"
#include "../../utils/displayname.hpp"
#include "../../world/world.hpp"
#include "../core/attributes.hpp"
#include "../core/errorcode.hpp"
#include "../core/objectproperty.tpp"
#include "../hardware/decoder/list/decoderlist.hpp"
#include "../train/train.hpp"
#include "../utils/displayname.hpp"
#include "../vehicle/rail/railvehicle.hpp"
#include "../world/world.hpp"
std::shared_ptr<HardwareThrottle> HardwareThrottle::create(std::shared_ptr<ThrottleController> controller, World& world)
{
return create(std::move(controller), world, world.getUniqueId(defaultId));
}
std::shared_ptr<HardwareThrottle> HardwareThrottle::create(std::shared_ptr<ThrottleController> controller, World& world, std::string_view _id)
{
auto obj = std::make_shared<HardwareThrottle>(std::move(controller), world, _id);
obj->addToWorld();
auto obj = std::make_shared<HardwareThrottle>(std::move(controller), world, getUniqueLogId());
obj->addToList();
return obj;
}
HardwareThrottle::HardwareThrottle(std::shared_ptr<ThrottleController> controller, World& world, std::string_view _id)
: Throttle(world, _id)
HardwareThrottle::HardwareThrottle(std::shared_ptr<ThrottleController> controller, World& world, std::string_view logId)
: Throttle(world, logId)
, interface{this, "interface", controller, PropertyFlags::ReadOnly | PropertyFlags::Store}
{
Attributes::addDisplayName(interface, DisplayName::Hardware::interface);
m_interfaceItems.add(interface);
}
Throttle::AcquireResult HardwareThrottle::acquire(DecoderProtocol protocol, uint16_t address, bool steal)
std::error_code HardwareThrottle::acquire(DecoderProtocol protocol, uint16_t address, bool steal)
{
assert(m_world.decoders.value());
auto& decoderList = *m_world.decoders.value();
auto decoder = decoderList.getDecoder(protocol, address);
if(!decoder)
{
decoder = decoderList.getDecoder(address);
}
if(!decoder)
return AcquireResult::FailedNonExisting;
return Throttle::acquire(std::move(decoder), steal);
}
void HardwareThrottle::addToWorld()
{
Throttle::addToWorld();
assert(interface);
if(interface->addThrottle(*this))
m_world.hardwareThrottles.setValueInternal(m_world.hardwareThrottles + 1);
else
assert(false);
{
return make_error_code(ErrorCode::UnknownDecoderAddress);
}
if(!decoder->vehicle)
{
return make_error_code(ErrorCode::DecoderNotAssignedToAVehicle);
}
if(decoder->vehicle->activeTrain)
{
return Throttle::acquire(decoder->vehicle->activeTrain.value(), steal);
}
if(decoder->vehicle->trains.empty())
{
return make_error_code(ErrorCode::VehicleNotAssignedToATrain);
}
for(auto& vehicleTrain : decoder->vehicle->trains)
{
assert(!vehicleTrain->active);
try
{
vehicleTrain->active = true; // try to activate train
if(vehicleTrain->active)
{
return Throttle::acquire(vehicleTrain, steal);
}
}
catch(...)
{
}
}
return make_error_code(ErrorCode::CanNotActivateTrain);
}
void HardwareThrottle::destroying()
@ -84,12 +100,12 @@ void HardwareThrottle::destroying()
Throttle::destroying();
}
void HardwareThrottle::load(WorldLoader& /*loader*/, const nlohmann::json& /*data*/)
void HardwareThrottle::addToList()
{
// do not load
}
void HardwareThrottle::save(WorldSaver& /*saver*/, nlohmann::json& /*data*/, nlohmann::json& /*state*/) const
{
// do not save
Throttle::addToList();
assert(interface);
if(interface->addThrottle(*this))
m_world.hardwareThrottles.setValueInternal(m_world.hardwareThrottles + 1);
else
assert(false);
}

Datei anzeigen

@ -1,9 +1,9 @@
/**
* server/src/hardware/throttle/hardwarethrottle.hpp
* server/src/throttle/hardwarethrottle.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022-2023 Reinder Feenstra
* Copyright (C) 2022-2023,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -20,8 +20,8 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_HARDWARE_THROTTLE_HARDWARETHROTTLE_HPP
#define TRAINTASTIC_SERVER_HARDWARE_THROTTLE_HARDWARETHROTTLE_HPP
#ifndef TRAINTASTIC_SERVER_THROTTLE_HARDWARETHROTTLE_HPP
#define TRAINTASTIC_SERVER_THROTTLE_HARDWARETHROTTLE_HPP
#include "throttle.hpp"
#include "throttlecontroller.hpp"
@ -34,10 +34,8 @@ class HardwareThrottle : public Throttle
ThrottleController& throttleController();
protected:
void addToWorld() override;
void destroying() override;
void load(WorldLoader& loader, const nlohmann::json& data) override;
void save(WorldSaver& saver, nlohmann::json& data, nlohmann::json& state) const override;
void addToList() override;
public:
static std::shared_ptr<HardwareThrottle> create(std::shared_ptr<ThrottleController> controller, World& world);
@ -45,9 +43,9 @@ class HardwareThrottle : public Throttle
ObjectProperty<ThrottleController> interface;
HardwareThrottle(std::shared_ptr<ThrottleController> controller, World& world, std::string_view _id);
HardwareThrottle(std::shared_ptr<ThrottleController> controller, World& world, std::string_view logId);
AcquireResult acquire(DecoderProtocol protocol, uint16_t address, bool steal = false);
std::error_code acquire(DecoderProtocol protocol, uint16_t address, bool steal = false);
};
#endif

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/list/throttlelist.cpp
* server/src/throttle/list/throttlelist.cpp
*
* This file is part of the traintastic source code.
*

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/list/throttlelist.hpp
* server/src/throttle/list/throttlelist.hpp
*
* This file is part of the traintastic source code.
*
@ -20,10 +20,10 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_HARDWARE_THROTTLE_LIST_THROTTLELIST_HPP
#define TRAINTASTIC_SERVER_HARDWARE_THROTTLE_LIST_THROTTLELIST_HPP
#ifndef TRAINTASTIC_SERVER_THROTTLE_LIST_THROTTLELIST_HPP
#define TRAINTASTIC_SERVER_THROTTLE_LIST_THROTTLELIST_HPP
#include "../../../core/objectlist.hpp"
#include "../../core/objectlist.hpp"
#include "throttlelistcolumn.hpp"
#include "../throttle.hpp"

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/list/throttlelistcolumn.hpp
* server/src/throttle/list/throttlelistcolumn.hpp
*
* This file is part of the traintastic source code.
*
@ -20,21 +20,19 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_HARDWARE_THROTTLE_LIST_THROTTLELISTCOLUMN_HPP
#define TRAINTASTIC_SERVER_HARDWARE_THROTTLE_LIST_THROTTLELISTCOLUMN_HPP
#ifndef TRAINTASTIC_SERVER_THROTTLE_LIST_THROTTLELISTCOLUMN_HPP
#define TRAINTASTIC_SERVER_THROTTLE_LIST_THROTTLELISTCOLUMN_HPP
#include <type_traits>
enum class ThrottleListColumn
{
Id = 1 << 0,
Name = 1 << 1,
Train = 1 << 2,
Interface = 1 << 3,
Name = 1 << 0,
Train = 1 << 1,
Interface = 1 << 2,
};
constexpr std::array<ThrottleListColumn, 4> throttleListColumnValues = {
ThrottleListColumn::Id,
constexpr std::array<ThrottleListColumn, 3> throttleListColumnValues = {
ThrottleListColumn::Name,
ThrottleListColumn::Train,
ThrottleListColumn::Interface,

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/list/throttlelisttablemodel.cpp
* server/src/throttle/list/throttlelisttablemodel.cpp
*
* This file is part of the traintastic source code.
*
@ -23,14 +23,13 @@
#include "throttlelisttablemodel.hpp"
#include "throttlelist.hpp"
#include "../webthrottle.hpp"
#include "../../../core/objectproperty.tpp"
#include "../../../train/train.hpp"
#include "../../../utils/displayname.hpp"
#include "../../core/objectproperty.tpp"
#include "../../train/train.hpp"
#include "../../utils/displayname.hpp"
bool ThrottleListTableModel::isListedProperty(std::string_view name)
{
return
name == "id" ||
name == "name" ||
name == "train" ||
name == "interface";
@ -40,9 +39,6 @@ static std::string_view displayName(ThrottleListColumn column)
{
switch(column)
{
case ThrottleListColumn::Id:
return DisplayName::Object::id;
case ThrottleListColumn::Name:
return DisplayName::Object::name;
@ -82,9 +78,6 @@ std::string ThrottleListTableModel::getText(uint32_t column, uint32_t row) const
assert(column < m_columns.size());
switch(m_columns[column])
{
case ThrottleListColumn::Id:
return throttle.id;
case ThrottleListColumn::Name:
return throttle.name;
@ -122,9 +115,7 @@ void ThrottleListTableModel::propertyChanged(BaseProperty& property, uint32_t ro
{
std::string_view name = property.name();
if(name == "id")
changed(row, ThrottleListColumn::Id);
else if(name == "name")
if(name == "name")
changed(row, ThrottleListColumn::Name);
else if(name == "train")
changed(row, ThrottleListColumn::Train);

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/list/throttlelisttablemodel.hpp
* server/src/throttle/list/throttlelisttablemodel.hpp
*
* This file is part of the traintastic source code.
*
@ -20,10 +20,10 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_HARDWARE_THROTTLE_LIST_THROTTLELISTTABLEMODEL_HPP
#define TRAINTASTIC_SERVER_HARDWARE_THROTTLE_LIST_THROTTLELISTTABLEMODEL_HPP
#ifndef TRAINTASTIC_SERVER_THROTTLE_LIST_THROTTLELISTTABLEMODEL_HPP
#define TRAINTASTIC_SERVER_THROTTLE_LIST_THROTTLELISTTABLEMODEL_HPP
#include "../../../core/objectlisttablemodel.hpp"
#include "../../core/objectlisttablemodel.hpp"
#include "throttlelistcolumn.hpp"
#include "../throttle.hpp"

Datei anzeigen

@ -0,0 +1,62 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "scriptthrottle.hpp"
#include "../core/attributes.hpp"
#include "../core/method.tpp"
#include "../core/objectproperty.tpp"
#include "../train/train.hpp"
#include "../world/world.hpp"
std::shared_ptr<ScriptThrottle> ScriptThrottle::create(World& world)
{
auto obj = std::make_shared<ScriptThrottle>(world, getUniqueLogId("scriptthrottle"));
obj->addToList();
return obj;
}
ScriptThrottle::ScriptThrottle(World& world, std::string_view objectId)
: Throttle(world, objectId)
, m_emergencyStop{*this, "emergency_stop", MethodFlags::ScriptCallable, std::bind_front(&Throttle::emergencyStop, this)}
, m_setDirection{*this, "set_direction", MethodFlags::ScriptCallable, std::bind_front(&Throttle::setDirection, this)}
, m_changeDirection{*this, "change_direction", MethodFlags::ScriptCallable,
[this]()
{
return acquired() && setDirection(~train->direction.value());
}}
, m_setSpeed{*this, "set_speed", MethodFlags::ScriptCallable, std::bind_front(&Throttle::setSpeed, this)}
, m_setTargetSpeed{*this, "set_target_speed", MethodFlags::ScriptCallable, std::bind_front(&Throttle::setTargetSpeed, this)}
{
Attributes::addObjectEditor(m_emergencyStop, false);
m_interfaceItems.add(m_emergencyStop);
Attributes::addObjectEditor(m_setDirection, false);
m_interfaceItems.add(m_setDirection);
Attributes::addObjectEditor(m_changeDirection, false);
m_interfaceItems.add(m_changeDirection);
Attributes::addObjectEditor(m_setSpeed, false);
m_interfaceItems.add(m_setSpeed);
Attributes::addObjectEditor(m_setTargetSpeed, false);
m_interfaceItems.add(m_setTargetSpeed);
}

Datei anzeigen

@ -0,0 +1,44 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_THROTTLE_SCRIPTTHROTTLE_HPP
#define TRAINTASTIC_SERVER_THROTTLE_SCRIPTTHROTTLE_HPP
#include "throttle.hpp"
class ScriptThrottle : public Throttle
{
CLASS_ID("throttle.script")
private:
Method<bool()> m_emergencyStop;
Method<bool(Direction)> m_setDirection;
Method<bool()> m_changeDirection;
Method<bool(double, SpeedUnit)> m_setSpeed;
Method<bool(double, SpeedUnit)> m_setTargetSpeed;
public:
static std::shared_ptr<ScriptThrottle> create(World& world);
ScriptThrottle(World& world, std::string_view objectId);
};
#endif

Datei anzeigen

@ -0,0 +1,241 @@
/**
* server/src/throttle/throttle.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "throttle.hpp"
#include "list/throttlelist.hpp"
#include "list/throttlelisttablemodel.hpp"
#include "../core/attributes.hpp"
#include "../core/method.tpp"
#include "../core/objectproperty.tpp"
#include "../core/objectvectorproperty.tpp"
#include "../hardware/decoder/decoder.hpp"
#include "../log/log.hpp"
#include "../train/train.hpp"
#include "../utils/displayname.hpp"
#include "../utils/valuestep.hpp"
#include "../world/world.hpp"
namespace {
constexpr double getValueStep(double value, SpeedUnit unit)
{
switch(unit)
{
case SpeedUnit::KiloMeterPerHour:
return (value >= 40.0) ? 5.0 : 2.0;
case SpeedUnit::MilePerHour:
return (value >= 30.0) ? 5.0 : 2.0;
case SpeedUnit::MeterPerSecond:
return (value >= 10.0) ? 1.0 : 0.5;
}
return 1;
}
}
std::unordered_set<std::string, StringHash, StringEqual> Throttle::s_logIds;
std::string_view Throttle::getUniqueLogId(std::string_view prefix)
{
std::string uniqueLogId{prefix};
uniqueLogId.append("_");
uint32_t number = 0;
do
{
uniqueLogId.resize(prefix.size() + 1);
uniqueLogId.append(std::to_string(++number));
}
while(s_logIds.contains(uniqueLogId));
return *s_logIds.emplace(std::move(uniqueLogId)).first;
}
Throttle::Throttle(World& world, std::string_view logId)
: m_world{world}
, m_logId{logId}
, name{this, "name", std::string(logId), PropertyFlags::ReadWrite | PropertyFlags::NoStore | PropertyFlags::ScriptReadWrite}
, train{this, "train", nullptr, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
, onAcquire{*this, "on_acquire", EventFlags::Scriptable | EventFlags::Public}
, onRelease{*this, "on_release", EventFlags::Scriptable | EventFlags::Public}
{
Attributes::addDisplayName(name, DisplayName::Object::name);
m_interfaceItems.add(name);
Attributes::addObjectEditor(train, false);
m_interfaceItems.add(train);
}
Throttle::~Throttle()
{
assert(!acquired());
if(auto it = s_logIds.find(m_logId); it != s_logIds.end()) [[likely]]
{
s_logIds.erase(it);
}
}
bool Throttle::acquired() const
{
return train.operator bool();
}
std::error_code Throttle::acquire(const std::shared_ptr<Train>& acquireTrain, bool steal)
{
assert(acquireTrain);
const auto stole = steal && acquireTrain->hasThrottle.value();
const std::string stoleFrom = stole ? acquireTrain->throttleName.value() : std::string{};
const auto ec = acquireTrain->acquire(*this, steal);
if(ec)
{
Log::log(m_logId, LogMessage::D3001_ACQUIRING_TRAIN_X_FAILED_X, acquireTrain->name.value(), ec.message());
return ec;
}
if(acquired())
{
release();
}
assert(!train);
train.setValueInternal(acquireTrain);
if(stole)
{
Log::log(m_logId, LogMessage::N3005_THROTTLE_X_STOLE_TRAIN_X_FROM_THROTTLE_X, name.value(), train->name.value(), stoleFrom);
}
else
{
Log::log(m_logId, LogMessage::I3001_THROTTLE_X_ACQUIRED_TRAIN_X, name.value(), train->name.value());
}
trainChanged();
fireEvent(onAcquire, shared_ptr<Throttle>(), acquireTrain);
return {};
}
void Throttle::release(bool stopIt)
{
if(!acquired())
return;
if(stopIt)
{
emergencyStop();
}
const auto logMessage = !stopIt && !train->isStopped.value() ? LogMessage::N3006_THROTTLE_X_RELEASED_TRAIN_X_WITHOUT_STOPPING_IT : LogMessage::I3002_THROTTLE_X_RELEASED_TRAIN_X;
Log::log(m_logId, logMessage, name.value(), train->name.value());
train->release(*this);
train.setValueInternal(nullptr);
trainChanged();
fireEvent(onRelease, shared_ptr<Throttle>());
}
bool Throttle::emergencyStop()
{
if(acquired())
{
train->emergencyStop = true;
return true;
}
return false;
}
bool Throttle::setDirection(Direction value)
{
if(acquired() && (value == Direction::Forward || value == Direction::Reverse))
{
return !train->setDirection(*this, value);
}
return false;
}
bool Throttle::setSpeed(double value, SpeedUnit unit)
{
if(acquired())
{
train->emergencyStop = false;
return !train->setSpeed(*this, convertUnit(value, unit, train->speed.unit()));
}
return false;
}
bool Throttle::setTargetSpeed(double value, SpeedUnit unit)
{
if(acquired())
{
train->emergencyStop = false;
return !train->setTargetSpeed(*this, convertUnit(value, unit, train->throttleSpeed.unit()));
}
return false;
}
bool Throttle::slower(bool immediate)
{
if(acquired())
{
train->emergencyStop = false;
if(immediate)
{
const double value = train->speed.value();
const double step = getValueStep(value, train->speed.unit());
return !train->setSpeed(*this, valueStepDown(value, step));
}
const double value = train->throttleSpeed.value();
const double step = getValueStep(value, train->throttleSpeed.unit());
return !train->setTargetSpeed(*this, valueStepDown(value, step));
}
return false;
}
bool Throttle::faster(bool immediate)
{
if(acquired())
{
train->emergencyStop = false;
if(immediate)
{
const double value = train->speed.value();
const double step = getValueStep(value, train->speed.unit());
return !train->setSpeed(*this, valueStepUp(value, step));
}
const double value = train->throttleSpeed.value();
const double step = getValueStep(value, train->throttleSpeed.unit());
return !train->setTargetSpeed(*this, valueStepUp(value, step));
}
return false;
}
void Throttle::destroying()
{
m_world.throttles->removeObject(shared_ptr<Throttle>());
release();
NonPersistentObject::destroying();
}
void Throttle::addToList()
{
m_world.throttles->addObject(shared_ptr<Throttle>());
}

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/throttle.hpp
* server/src/throttle/throttle.hpp
*
* This file is part of the traintastic source code.
*
@ -20,27 +20,28 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_HARDWARE_THROTTLE_THROTTLE_HPP
#define TRAINTASTIC_SERVER_HARDWARE_THROTTLE_THROTTLE_HPP
#ifndef TRAINTASTIC_SERVER_THROTTLE_THROTTLE_HPP
#define TRAINTASTIC_SERVER_THROTTLE_THROTTLE_HPP
#include "../../core/idobject.hpp"
#include "../core/nonpersistentobject.hpp"
#include <unordered_set>
#include <traintastic/enum/direction.hpp>
#include "../../core/property.hpp"
#include "../../core/objectproperty.hpp"
#include "../../core/objectvectorproperty.hpp"
#include "../../core/method.hpp"
#include <traintastic/enum/speedunit.hpp>
#include "../core/event.hpp"
#include "../core/property.hpp"
#include "../core/objectproperty.hpp"
#include "../core/objectvectorproperty.hpp"
#include "../core/method.hpp"
#include "../utils/stringequal.hpp"
#include "../utils/stringhash.hpp"
class Decoder;
enum class DecoderProtocol : uint8_t;
class ThrottleFunction;
class Train;
class World;
class Throttle : public IdObject
class Throttle : public NonPersistentObject
{
friend class ThrottleFunction;
DEFAULT_ID("throttle")
public:
enum class AcquireResult
{
@ -50,45 +51,39 @@ class Throttle : public IdObject
};
private:
std::shared_ptr<Decoder> m_decoder;
static std::unordered_set<std::string, StringHash, StringEqual> s_logIds;
protected:
Throttle(World& world, std::string_view _id);
static std::string_view getUniqueLogId(std::string_view prefix = "throttle");
World& m_world;
const std::string_view m_logId;
Throttle(World& world, std::string_view logId);
void destroying() override;
void addToWorld() override;
virtual void addToList();
AcquireResult acquire(std::shared_ptr<Decoder> decoder, bool steal = false);
virtual void trainChanged() {}
public:
static constexpr float throttleMin = 0;
static constexpr float throttleStop = throttleMin;
static constexpr float throttleMax = 1;
boost::signals2::signal<void()> released;
Property<std::string> name;
Property<Direction> direction;
Property<float> throttle;
ObjectProperty<Train> train;
Method<bool()> emergencyStop;
Method<bool(bool)> stop;
Method<bool(bool)> faster;
Method<bool(bool)> slower;
Method<bool(Direction)> setDirection;
Event<const std::shared_ptr<Throttle>&, const std::shared_ptr<Train>&> onAcquire;
Event<const std::shared_ptr<Throttle>&> onRelease;
#ifndef NDEBUG
~Throttle() override;
#endif
bool acquired() const;
std::error_code acquire(const std::shared_ptr<Train>& acquireTrain, bool steal = false);
void release(bool stopIt = true);
const std::shared_ptr<Decoder>& decoder() const // TODO: remove once WiThrottle is migrated to train control
{
return m_decoder;
}
bool emergencyStop();
bool setDirection(Direction value);
bool setSpeed(double value, SpeedUnit unit);
bool setTargetSpeed(double value, SpeedUnit unit);
bool slower(bool immediate);
bool faster(bool immediate);
};
#endif

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/throttlecontroller.cpp
* server/src/throttle/throttlecontroller.cpp
*
* This file is part of the traintastic source code.
*
@ -24,9 +24,9 @@
#include "throttle.hpp"
#include "list/throttlelist.hpp"
#include "list/throttlelisttablemodel.hpp"
#include "../../core/attributes.hpp"
#include "../../core/objectproperty.tpp"
#include "../../utils/displayname.hpp"
#include "../core/attributes.hpp"
#include "../core/objectproperty.tpp"
#include "../utils/displayname.hpp"
ThrottleController::ThrottleController(IdObject& interface, ThrottleListColumn columns)
: throttles{&interface, "throttles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::SubObject}

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/throttlecontroller.hpp
* server/src/throttle/throttlecontroller.hpp
*
* This file is part of the traintastic source code.
*
@ -20,10 +20,10 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_HARDWARE_THROTTLE_THROTTLECONTROLLER_HPP
#define TRAINTASTIC_SERVER_HARDWARE_THROTTLE_THROTTLECONTROLLER_HPP
#ifndef TRAINTASTIC_SERVER_THROTTLE_THROTTLECONTROLLER_HPP
#define TRAINTASTIC_SERVER_THROTTLE_THROTTLECONTROLLER_HPP
#include "../../core/objectproperty.hpp"
#include "../core/objectproperty.hpp"
#ifdef interface
#undef interface // interface is defined in combaseapi.h

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/webthrottle.cpp
* server/src/throttle/webthrottle.cpp
*
* This file is part of the traintastic source code.
*
@ -21,31 +21,16 @@
*/
#include "webthrottle.hpp"
#include "../../world/world.hpp"
#include "../world/world.hpp"
std::shared_ptr<WebThrottle> WebThrottle::create(World& world)
{
return create(world, world.getUniqueId(defaultId));
}
std::shared_ptr<WebThrottle> WebThrottle::create(World& world, std::string_view objectId)
{
auto obj = std::make_shared<WebThrottle>(world, objectId);
obj->addToWorld();
auto obj = std::make_shared<WebThrottle>(world, getUniqueLogId("webthrottle"));
obj->addToList();
return obj;
}
WebThrottle::WebThrottle(World& world, std::string_view objectId)
: Throttle(world, objectId)
WebThrottle::WebThrottle(World& world, std::string_view logId)
: Throttle(world, logId)
{
}
void WebThrottle::load(WorldLoader& /*loader*/, const nlohmann::json& /*data*/)
{
// do not load
}
void WebThrottle::save(WorldSaver& /*saver*/, nlohmann::json& /*data*/, nlohmann::json& /*state*/) const
{
// do not save
}

Datei anzeigen

@ -1,5 +1,5 @@
/**
* server/src/hardware/throttle/webthrottle.hpp
* server/src/throttle/webthrottle.hpp
*
* This file is part of the traintastic source code.
*
@ -20,8 +20,8 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_HARDWARE_THROTTLE_WEBTHROTTLE_HPP
#define TRAINTASTIC_SERVER_HARDWARE_THROTTLE_WEBTHROTTLE_HPP
#ifndef TRAINTASTIC_SERVER_THROTTLE_WEBTHROTTLE_HPP
#define TRAINTASTIC_SERVER_THROTTLE_WEBTHROTTLE_HPP
#include "throttle.hpp"
#include "throttlecontroller.hpp"
@ -29,17 +29,11 @@
class WebThrottle : public Throttle
{
CLASS_ID("throttle.web")
DEFAULT_ID("webthrottle")
protected:
void load(WorldLoader& loader, const nlohmann::json& data) override;
void save(WorldSaver& saver, nlohmann::json& data, nlohmann::json& state) const override;
public:
static std::shared_ptr<WebThrottle> create(World& world);
static std::shared_ptr<WebThrottle> create(World& world, std::string_view objectId);
WebThrottle(World& world, std::string_view objectId);
WebThrottle(World& world, std::string_view logId);
using Throttle::acquire;
};

Datei anzeigen

@ -21,13 +21,13 @@
*/
#include "train.hpp"
#include "trainerror.hpp"
#include "trainlist.hpp"
#include "trainvehiclelist.hpp"
#include "../world/world.hpp"
#include "trainblockstatus.hpp"
#include "trainlisttablemodel.hpp"
#include "../core/attributes.hpp"
#include "../core/errorcode.hpp"
#include "../core/method.tpp"
#include "../core/objectproperty.tpp"
#include "../core/objectvectorproperty.tpp"
@ -35,7 +35,7 @@
#include "../board/tile/rail/blockrailtile.hpp"
#include "../vehicle/rail/poweredrailvehicle.hpp"
#include "../hardware/decoder/decoder.hpp"
#include "../hardware/throttle/throttle.hpp"
#include "../throttle/throttle.hpp"
#include "../utils/almostzero.hpp"
#include "../utils/displayname.hpp"
#include "../zone/zone.hpp"
@ -150,13 +150,15 @@ Train::Train(World& world, std::string_view _id) :
m_throttle->release();
}
},
std::bind(&Train::setTrainActive, this, std::placeholders::_1)},
mode{this, "mode", TrainMode::ManualUnprotected, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
std::bind(&Train::setTrainActive, this, std::placeholders::_1)}
, mode{this, "mode", TrainMode::ManualUnprotected, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
, mute{this, "mute", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
, noSmoke{this, "no_smoke", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
, blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly},
zones{*this, "zones", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly},
notes{this, "notes", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
, hasThrottle{this, "has_throttle", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
, throttleName{this, "throttle_name", "", PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
, blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
, zones{*this, "zones", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
, notes{this, "notes", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
, onBlockAssigned{*this, "on_block_assigned", EventFlags::Scriptable}
, onBlockReserved{*this, "on_block_reserved", EventFlags::Scriptable}
, onBlockEntered{*this, "on_block_entered", EventFlags::Scriptable}
@ -223,6 +225,12 @@ Train::Train(World& world, std::string_view _id) :
Attributes::addObjectEditor(noSmoke, false);
m_interfaceItems.add(noSmoke);
Attributes::addObjectEditor(hasThrottle, false);
m_interfaceItems.add(hasThrottle);
Attributes::addObjectEditor(throttleName, false);
m_interfaceItems.add(throttleName);
Attributes::addObjectEditor(blocks, false);
m_interfaceItems.add(blocks);
@ -349,6 +357,12 @@ void Train::loaded()
Attributes::setEnabled(lob, overrideLength);
Attributes::setEnabled(weight, overrideWeight);
auto self = shared_ptr<Train>();
for(auto& vehicle : *vehicles)
{
vehicle->trains.appendInternal(self);
}
vehiclesChanged();
updateMute();
updateNoSmoke();
@ -576,22 +590,13 @@ bool Train::setTrainActive(bool val)
return true;
}
std::string Train::throttleName() const
{
if(m_throttle)
{
return m_throttle->name;
}
return {};
}
std::error_code Train::acquire(Throttle& throttle, bool steal)
{
if(m_throttle)
{
if(!steal)
{
return make_error_code(TrainError::AlreadyAcquired);
return make_error_code(ErrorCode::AlreadyAcquired);
}
m_throttle->release();
}
@ -606,11 +611,13 @@ std::error_code Train::acquire(Throttle& throttle, bool steal)
}
if(!active)
{
return make_error_code(TrainError::CanNotActivateTrain);
return make_error_code(ErrorCode::CanNotActivateTrain);
}
}
assert(!m_throttle);
m_throttle = throttle.shared_ptr<Throttle>();
hasThrottle.setValueInternal(true);
throttleName.setValueInternal(m_throttle->name);
return {};
}
@ -618,9 +625,11 @@ std::error_code Train::release(Throttle& throttle)
{
if(m_throttle.get() != &throttle)
{
return make_error_code(TrainError::InvalidThrottle);
return make_error_code(ErrorCode::InvalidThrottle);
}
m_throttle.reset();
hasThrottle.setValueInternal(false);
throttleName.setValueInternal("");
if(isStopped && blocks.empty())
{
active = false; // deactive train if it is stopped and not assigned to a block
@ -632,7 +641,7 @@ std::error_code Train::setSpeed(Throttle& throttle, double value)
{
if(m_throttle.get() != &throttle)
{
return make_error_code(TrainError::InvalidThrottle);
return make_error_code(ErrorCode::InvalidThrottle);
}
assert(active);
@ -656,7 +665,7 @@ std::error_code Train::setTargetSpeed(Throttle& throttle, double value)
{
if(m_throttle.get() != &throttle)
{
return make_error_code(TrainError::InvalidThrottle);
return make_error_code(ErrorCode::InvalidThrottle);
}
assert(active);
throttleSpeed.setValue(std::clamp(value, Attributes::getMin(throttleSpeed), Attributes::getMax(throttleSpeed)));
@ -667,13 +676,13 @@ std::error_code Train::setDirection(Throttle& throttle, Direction value)
{
if(m_throttle.get() != &throttle)
{
return make_error_code(TrainError::InvalidThrottle);
return make_error_code(ErrorCode::InvalidThrottle);
}
if(direction != value)
{
if(!isStopped)
{
return make_error_code(TrainError::TrainMustBeStoppedToChangeDirection);
return make_error_code(ErrorCode::TrainMustBeStoppedToChangeDirection);
}
assert(active);
direction = value;

Datei anzeigen

@ -114,6 +114,8 @@ class Train : public IdObject
Property<TrainMode> mode;
Property<bool> mute;
Property<bool> noSmoke;
Property<bool> hasThrottle;
Property<std::string> throttleName;
//! \brief List of block status the train is in
//! Index 0 is the block where the head of the train is.
@ -139,12 +141,6 @@ class Train : public IdObject
void updateNoSmoke();
void updateSpeedLimit();
bool hasThrottle() const
{
return m_throttle.operator bool();
}
std::string throttleName() const;
std::error_code acquire(Throttle& throttle, bool steal = false);
std::error_code release(Throttle& throttle);
std::error_code setSpeed(Throttle& throttle, double value);

Datei anzeigen

@ -87,6 +87,8 @@ TrainVehicleList::TrainVehicleList(Train& train_, std::string_view parentPropert
{
const auto& world = getWorld(parent());
m_interfaceItems.add(length);
Attributes::addDisplayName(add, DisplayName::List::add);
Attributes::addEnabled(add, false);
Attributes::addObjectList(add, world.railVehicles);

Datei anzeigen

@ -0,0 +1,48 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_UTILS_STRINGEQUAL_HPP
#define TRAINTASTIC_SERVER_UTILS_STRINGEQUAL_HPP
#include <string>
#include <string_view>
struct StringEqual
{
using is_transparent = void;
bool operator()(std::string_view lhs, std::string_view rhs) const noexcept
{
return lhs == rhs;
}
bool operator()(const char* lhs, std::string_view rhs) const noexcept
{
return std::string_view(lhs) == rhs;
}
bool operator()(std::string_view lhs, const char* rhs) const noexcept
{
return lhs == std::string_view(rhs);
}
};
#endif

Datei anzeigen

@ -0,0 +1,48 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_UTILS_STRINGHASH_HPP
#define TRAINTASTIC_SERVER_UTILS_STRINGHASH_HPP
#include <string>
#include <string_view>
struct StringHash
{
using is_transparent = void;
[[nodiscard]] size_t operator()(const char* value) const noexcept
{
return std::hash<std::string_view>{}(value);
}
[[nodiscard]] size_t operator()(std::string_view value) const noexcept
{
return std::hash<std::string_view>{}(value);
}
[[nodiscard]] size_t operator()(const std::string& value) const noexcept
{
return std::hash<std::string>{}(value);
}
};
#endif

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2021,2023-2024 Reinder Feenstra
* Copyright (C) 2019-2021,2023-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -35,7 +35,23 @@
RailVehicle::RailVehicle(World& world, std::string_view _id) :
Vehicle(world, _id),
decoder{this, "decoder", nullptr, PropertyFlags::ReadWrite | PropertyFlags::Store},
decoder{this, "decoder", nullptr, PropertyFlags::ReadWrite | PropertyFlags::Store, nullptr,
[this](const std::shared_ptr<Decoder>& value)
{
if(decoder)
{
decoder->vehicle.setValueInternal(nullptr);
}
if(value)
{
if(value->vehicle)
{
value->vehicle->decoder = nullptr;
}
value->vehicle.setValueInternal(shared_ptr<RailVehicle>());
}
return true;
}},
lob{*this, "lob", 0, LengthUnit::MilliMeter, PropertyFlags::ReadWrite | PropertyFlags::Store},
speedMax{*this, "speed_max", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadWrite | PropertyFlags::Store},
weight{*this, "weight", 0, WeightUnit::Ton, PropertyFlags::ReadWrite | PropertyFlags::Store, [this](double /*value*/, WeightUnit /*unit*/){ updateTotalWeight(); }},
@ -140,6 +156,11 @@ void RailVehicle::loaded()
{
Vehicle::loaded();
if(decoder)
{
decoder->vehicle.setValueInternal(shared_ptr<RailVehicle>());
}
updateTotalWeight();
}

Datei anzeigen

@ -67,7 +67,7 @@
#include "../zone/zone.hpp"
#include "../zone/zonelist.hpp"
#include "../hardware/throttle/list/throttlelist.hpp"
#include "../throttle/list/throttlelist.hpp"
#include "../train/train.hpp"
#include "../train/trainlist.hpp"
#include "../vehicle/rail/railvehiclelist.hpp"
@ -81,7 +81,7 @@ constexpr auto decoderListColumns = DecoderListColumn::Id | DecoderListColumn::N
constexpr auto inputListColumns = InputListColumn::Id | InputListColumn::Name | InputListColumn::Interface | InputListColumn::Channel | InputListColumn::Address;
constexpr auto outputListColumns = OutputListColumn::Interface | OutputListColumn::Channel | OutputListColumn::Address;
constexpr auto identificationListColumns = IdentificationListColumn::Id | IdentificationListColumn::Name | IdentificationListColumn::Interface /*| IdentificationListColumn::Channel*/ | IdentificationListColumn::Address;
constexpr auto throttleListColumns = ThrottleListColumn::Id | ThrottleListColumn::Name | ThrottleListColumn::Train | ThrottleListColumn::Interface;
constexpr auto throttleListColumns = ThrottleListColumn::Name | ThrottleListColumn::Train | ThrottleListColumn::Interface;
template<class T>
inline static void deleteAll(T& objectList)

Datei anzeigen

@ -1,3 +1,24 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <catch2/catch_template_test_macros.hpp>
#include "../src/world/world.hpp"
#include "../src/core/method.tpp"

104
server/test/train/saveload.cpp Normale Datei
Datei anzeigen

@ -0,0 +1,104 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <catch2/catch_test_macros.hpp>
#include "../src/world/world.hpp"
#include "../src/world/worldloader.hpp"
#include "../src/world/worldsaver.hpp"
#include "../src/core/method.tpp"
#include "../src/core/objectproperty.tpp"
#include "../src/hardware/decoder/list/decoderlist.hpp"
#include "../src/vehicle/rail/locomotive.hpp"
#include "../src/vehicle/rail/railvehiclelist.hpp"
#include "../src/train/train.hpp"
#include "../src/train/trainlist.hpp"
#include "../src/train/trainvehiclelist.hpp"
TEST_CASE("Train: Save/Load", "[train][train-saveload]")
{
std::filesystem::path ctw;
std::string worldUUID;
{
INFO("Create world");
auto world = World::create();
{
worldUUID = world->uuid;
auto decoder = world->decoders->create();
auto locomotive = world->railVehicles->create(Locomotive::classId);
locomotive->decoder = decoder;
REQUIRE(decoder->vehicle.value() == locomotive);
auto train = world->trains->create();
train->vehicles->add(locomotive);
REQUIRE(locomotive->trains[0] == train);
}
INFO("Saving...");
{
ctw = std::filesystem::temp_directory_path() / std::string(world->uuid.value()).append(World::dotCTW);
WorldSaver saver(*world, ctw);
}
INFO("Saved");
}
{
std::shared_ptr<World> world;
{
INFO("Loading...");
WorldLoader loader(ctw);
world = loader.world();
REQUIRE(world);
INFO("Loaded");
}
{
REQUIRE(world->uuid.value() == worldUUID);
REQUIRE(world->decoders->length == 1);
auto decoder = world->decoders->operator[](0);
REQUIRE(decoder);
REQUIRE(world->railVehicles->length == 1);
auto locomotive = std::dynamic_pointer_cast<Locomotive>(world->railVehicles->operator[](0));
REQUIRE(locomotive);
REQUIRE(world->trains->length == 1);
auto train = world->trains->operator[](0);
REQUIRE(train);
REQUIRE(decoder->vehicle.value() == locomotive);
REQUIRE(locomotive->decoder.value() == decoder);
REQUIRE(locomotive->trains.size() == 1);
REQUIRE(locomotive->trains[0] == train);
REQUIRE(train->vehicles->length == 1);
REQUIRE(train->vehicles->operator[](0) == locomotive);
}
}
INFO("Remove saved world");
REQUIRE(std::filesystem::remove(ctw));
}

Datei anzeigen

@ -40,4 +40,17 @@ TRAINTASTIC_ENUM(Direction, "direction", 3,
{Direction::Unknown, "unknown"}
});
constexpr Direction operator~(const Direction value)
{
switch(value)
{
case Direction::Forward:
return Direction::Reverse;
case Direction::Reverse:
return Direction::Forward;
default:
return Direction::Unknown;
}
}
#endif

Datei anzeigen

@ -40,4 +40,20 @@ TRAINTASTIC_ENUM(SpeedUnit, "speed_unit", 3,
{SpeedUnit::MilePerHour, "mph"},
});
constexpr std::string_view toDisplayUnit(SpeedUnit value)
{
switch(value)
{
case SpeedUnit::MeterPerSecond:
return "m/s";
case SpeedUnit::KiloMeterPerHour:
return "km/h";
case SpeedUnit::MilePerHour:
return "mph";
}
return {};
}
#endif

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2024 Reinder Feenstra
* Copyright (C) 2019-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -83,6 +83,8 @@ class Message
ObjectGetObjectVectorPropertyObject = 45,
ObjectSetVectorProperty = 46,
ObjectListGetObjects = 47,
Discover = 255,
};
@ -214,6 +216,30 @@ class Message
{
}
inline std::unique_ptr<Message> response(size_t capacity = 0) const
{
assert(isRequest());
return newResponse(command(), requestId(), capacity);
}
inline std::unique_ptr<Message> errorResponse(LogMessage code) const
{
assert(isRequest());
return newErrorResponse(command(), requestId(), code);
}
inline std::unique_ptr<Message> errorResponse(LogMessage code, std::string_view arg) const
{
assert(isRequest());
return newErrorResponse(command(), requestId(), code, arg);
}
inline std::unique_ptr<Message> errorResponse(LogMessage code, const std::vector<std::string>& args) const
{
assert(isRequest());
return newErrorResponse(command(), requestId(), code, args);
}
inline Command command() const { return header().command; }
inline Type type() const { return static_cast<Type>(header().flags.type); }
inline bool isRequest() const { return type() == Type::Request; }
@ -264,6 +290,12 @@ class Message
memcpy(value.data(), p + sizeof(Length), length);
m_readPosition += length;
}
else if constexpr(std::is_same_v<T, std::string_view>)
{
const Length length = read<Length>();
value = std::string_view(reinterpret_cast<const char*>(p + sizeof(Length)), static_cast<size_t>(length));
m_readPosition += length;
}
else if constexpr(std::is_trivially_copyable_v<T>)
{
memcpy(&value, p, sizeof(value));

Datei anzeigen

@ -6731,7 +6731,7 @@
"definition": "Recommended"
},
{
"term":"wizard.add_interface.protocol_dr5000_usb:bottom_text",
"term": "wizard.add_interface.protocol_dr5000_usb:bottom_text",
"definition": "The %command_station% supports two different protocols for communication. Although both are supported by Traintastic, using LocoNet is recommended as it is more versatile."
},
{
@ -6781,6 +6781,33 @@
{
"term": "qtapp.world_list_dialog:search",
"definition": "Search"
},
{
"term": "throttle.estop",
"definition": "EStop"
},
{
"term": "throttle.steal",
"definition": "Steal"
},
{
"term": "throttle.in_control",
"definition": "You're in control"
},
{
"term": "throttle.release",
"definition": "Release"
},
{
"term": "throttle.not_controlled",
"definition": "Not controlled"
},
{
"term": "throttle.acquire",
"definition": "Acquire"
},
{
"term": "throttle.controlled_by_x",
"definition": "Controlled by %1"
}
]
]