Merge pull request #115 from gfgit/work/gfgit/path_release

Path reservation fixes
Dieser Commit ist enthalten in:
Reinder Feenstra 2024-08-01 23:09:18 +02:00 committet von GitHub
Commit 99a3fb56d9
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: B5690EEEBB952194
24 geänderte Dateien mit 850 neuen und 69 gelöschten Zeilen

Datei anzeigen

@ -34,7 +34,6 @@
#include <QApplication>
#include <QKeyEvent>
#include <QPainter>
#include <QTimer>
#include <traintastic/locale/locale.hpp>
#include "getboardcolorscheme.hpp"
#include "tilepainter.hpp"
@ -92,6 +91,7 @@ BoardWidget::BoardWidget(std::shared_ptr<Board> object, QWidget* parent) :
m_editActions{new QActionGroup(this)}
, m_tileMoveStarted{false}
, m_tileResizeStarted{false}
, m_nxButtonTimerId(0)
{
setWindowIcon(Theme::getIconForClassId(object->classId));
setFocusPolicy(Qt::StrongFocus);
@ -447,6 +447,8 @@ BoardWidget::BoardWidget(std::shared_ptr<Board> object, QWidget* parent) :
BoardWidget::~BoardWidget()
{
stopTimerAndReleaseButtons();
if(m_nxManagerRequestId != Connection::invalidRequestId)
{
m_object->connection()->cancelRequest(m_nxManagerRequestId);
@ -459,6 +461,10 @@ void BoardWidget::worldEditChanged(bool value)
m_editActionNone->activate(QAction::Trigger);
m_toolbarEdit->setVisible(value);
m_statusBar->setVisible(value);
// Stop timers in edit mode
if(value)
stopTimerAndReleaseButtons();
}
void BoardWidget::gridChanged(BoardAreaWidget::Grid value)
@ -650,7 +656,8 @@ void BoardWidget::tileClicked(int16_t x, int16_t y)
if(nxButton->isPressed())
{
releaseNXButton(nxButton);
stopTimerAndReleaseButtons();
return;
}
else
{
@ -669,31 +676,13 @@ void BoardWidget::tileClicked(int16_t x, int16_t y)
m_nxButtonPressed.reset();
QTimer::singleShot(nxButtonReleaseDelay, this,
[this, weak1=std::weak_ptr<NXButtonRailTile>(firstButton), weak2=std::weak_ptr<NXButtonRailTile>(nxButton)]()
{
if(auto btn = weak1.lock())
{
releaseNXButton(btn);
}
if(auto btn = weak2.lock())
{
releaseNXButton(btn);
}
});
startReleaseTimer(firstButton, nxButton);
}
else
{
m_nxButtonPressed = nxButton;
QTimer::singleShot(nxButtonHoldTime, this,
[this, weak=std::weak_ptr<NXButtonRailTile>(nxButton)]()
{
if(auto btn = weak.lock())
{
releaseNXButton(btn);
}
});
startHoldTimer(nxButton);
}
}
}
@ -778,6 +767,53 @@ void BoardWidget::rightClicked()
rotateTile(true);
}
void BoardWidget::startHoldTimer(const std::shared_ptr<NXButtonRailTile>& nxButton)
{
stopTimerAndReleaseButtons();
m_releaseButton1 = nxButton;
assert(m_nxButtonTimerId == 0);
m_nxButtonTimerId = startTimer(nxButtonHoldTime);
}
void BoardWidget::startReleaseTimer(const std::shared_ptr<NXButtonRailTile> &firstButton,
const std::shared_ptr<NXButtonRailTile> &nxButton)
{
// Do not release first button yet
m_releaseButton1.reset();
stopTimerAndReleaseButtons();
m_releaseButton1 = firstButton;
m_releaseButton2 = nxButton;
assert(m_nxButtonTimerId == 0);
m_nxButtonTimerId = startTimer(nxButtonReleaseDelay);
}
void BoardWidget::stopTimerAndReleaseButtons()
{
if(m_nxButtonTimerId)
{
// Instantly release buttons
if(auto btn = m_releaseButton1.lock())
{
releaseNXButton(btn);
}
m_releaseButton1.reset();
if(auto btn = m_releaseButton2.lock())
{
releaseNXButton(btn);
}
m_releaseButton2.reset();
killTimer(m_nxButtonTimerId);
m_nxButtonTimerId = 0;
}
}
void BoardWidget::actionSelected(const Board::TileInfo* info)
{
m_boardArea->setMouseMoveAction(BoardAreaWidget::MouseMoveAction::None);
@ -825,6 +861,17 @@ void BoardWidget::keyPressEvent(QKeyEvent* event)
}
}
void BoardWidget::timerEvent(QTimerEvent *e)
{
if(e->timerId() == m_nxButtonTimerId)
{
stopTimerAndReleaseButtons();
return;
}
QWidget::timerEvent(e);
}
void BoardWidget::rotateTile(bool ccw)
{
if(m_tileRotates != 0)

Datei anzeigen

@ -78,11 +78,25 @@ class BoardWidget : public QWidget
TileRotate m_tileRotateLast = TileRotate::Deg0; //!< Last used tile rotate for add/move
std::weak_ptr<NXButtonRailTile> m_nxButtonPressed;
int m_nxButtonTimerId;
std::weak_ptr<NXButtonRailTile> m_releaseButton1;
std::weak_ptr<NXButtonRailTile> m_releaseButton2;
void startHoldTimer(const std::shared_ptr<NXButtonRailTile> &nxButton);
void startReleaseTimer(const std::shared_ptr<NXButtonRailTile> &firstButton,
const std::shared_ptr<NXButtonRailTile> &nxButton);
void actionSelected(const Board::TileInfo* tile);
void keyPressEvent(QKeyEvent* event) override;
void timerEvent(QTimerEvent *e) override;
void rotateTile(bool ccw = false);
void releaseNXButton(const std::shared_ptr<NXButtonRailTile>& nxButton);
protected slots:
void stopTimerAndReleaseButtons();
protected slots:
void worldEditChanged(bool value);
void gridChanged(BoardAreaWidget::Grid value);

Datei anzeigen

@ -33,6 +33,7 @@
#include <traintastic/enum/decouplerstate.hpp>
#include <traintastic/enum/direction.hpp>
#include <traintastic/enum/directioncontrolstate.hpp>
#include <traintastic/enum/externaloutputchangeaction.hpp>
#include <traintastic/enum/lengthunit.hpp>
#include <traintastic/enum/loconetf9f28.hpp>
#include <traintastic/enum/loconetfastclock.hpp>
@ -105,6 +106,7 @@ QString translateEnum(const QString& enumName, qint64 value)
TRANSLATE_ENUM(DecouplerState)
TRANSLATE_ENUM(Direction)
TRANSLATE_ENUM(DirectionControlState)
TRANSLATE_ENUM(ExternalOutputChangeAction)
TRANSLATE_ENUM(LengthUnit)
TRANSLATE_ENUM(LocoNetF9F28)
TRANSLATE_ENUM(LocoNetFastClock)

Datei anzeigen

@ -33,6 +33,8 @@
#include "../tile/rail/turnout/turnoutrailtile.hpp"
#include "../tile/rail/linkrailtile.hpp"
#include "../tile/rail/nxbuttonrailtile.hpp"
#include "../../train/trainblockstatus.hpp"
#include "../../core/eventloop.hpp"
#include "../../core/objectproperty.tpp"
#include "../../enum/bridgepath.hpp"
@ -296,9 +298,34 @@ BlockPath::BlockPath(BlockRailTile& block, BlockSide side)
: m_fromBlock{block}
, m_fromSide{side}
, m_toSide{static_cast<BlockSide>(-1)}
, m_delayReleaseTimer{EventLoop::ioContext}
, m_isReserved(false)
, m_delayedReleaseScheduled(false)
{
}
BlockPath::BlockPath(const BlockPath &other)
: Path(other)
, std::enable_shared_from_this<BlockPath>()
, m_fromBlock(other.m_fromBlock)
, m_fromSide(other.m_fromSide)
, m_toBlock(other.m_toBlock)
, m_toSide(other.m_toSide)
, m_tiles(other.m_tiles)
, m_turnouts(other.m_turnouts)
, m_directionControls(other.m_directionControls)
, m_crossings(other.m_crossings)
, m_bridges(other.m_bridges)
, m_signals(other.m_signals)
, m_nxButtonFrom(other.m_nxButtonFrom)
, m_nxButtonTo(other.m_nxButtonTo)
, m_delayReleaseTimer{EventLoop::ioContext}
, m_isReserved(false)
, m_delayedReleaseScheduled(false)
{
}
bool BlockPath::operator ==(const BlockPath& other) const noexcept
{
return
@ -387,7 +414,7 @@ bool BlockPath::reserve(const std::shared_ptr<Train>& train, bool dryRun)
{
if(auto turnout = turnoutWeak.lock())
{
if(!turnout->reserve(position, dryRun))
if(!turnout->reserve(shared_from_this(), position, dryRun))
{
assert(dryRun);
return false;
@ -489,6 +516,9 @@ bool BlockPath::reserve(const std::shared_ptr<Train>& train, bool dryRun)
}
}
if(!dryRun)
m_isReserved = true;
return true;
}
@ -499,24 +529,49 @@ bool BlockPath::release(bool dryRun)
return false;
}
if(!dryRun)
m_delayReleaseTimer.cancel();
auto toBlock = m_toBlock.lock();
if(!toBlock) /*[[unlikely]]*/
return false;
BlockState fromState = m_fromBlock.state.value();
BlockState toState = toBlock->state.value();
if((fromState == BlockState::Occupied || fromState == BlockState::Unknown)
&& (toState == BlockState::Occupied || toState == BlockState::Unknown)
&& !m_fromBlock.trains.empty() && !toBlock->trains.empty())
{
// Check if train head is beyond toBlock while its end is still in fromBlock
const auto& status1 = fromSide() == BlockSide::A ? m_fromBlock.trains.front() : m_fromBlock.trains.back();
const auto& status2 = toSide() == BlockSide::A ? toBlock->trains.front() : toBlock->trains.back();
if(status1->train.value() == status2->train.value())
return false;
}
if(!m_fromBlock.release(m_fromSide, dryRun))
{
assert(dryRun);
return false;
}
if(auto toBlock = m_toBlock.lock()) /*[[likely]]*/
if(!dryRun && toBlock->state.value() == BlockState::Reserved)
{
if(toBlock->trains.size() == 1)
{
//TODO: this bypasses some checks
toBlock->removeTrainInternal(toBlock->trains[0]);
//TODO: dryRun? what if it fails?
}
}
if(!toBlock->release(m_toSide, dryRun))
{
assert(dryRun);
return false;
}
}
else
{
return false;
}
for(const auto& item : m_turnouts)
{
@ -597,5 +652,28 @@ bool BlockPath::release(bool dryRun)
}
}
if(!dryRun)
m_isReserved = false;
return true;
}
bool BlockPath::delayedRelease(uint16_t timeoutMillis)
{
if(m_delayedReleaseScheduled)
return false;
m_delayedReleaseScheduled = true;
m_delayReleaseTimer.expires_after(boost::asio::chrono::milliseconds(timeoutMillis));
m_delayReleaseTimer.async_wait([this](const boost::system::error_code& ec)
{
m_delayedReleaseScheduled = false;
if(ec)
return;
release();
});
return true;
}

Datei anzeigen

@ -29,6 +29,7 @@
#include <array>
#include <vector>
#include <utility>
#include <boost/asio/steady_timer.hpp>
#include "../../enum/blockside.hpp"
class RailTile;
@ -64,10 +65,15 @@ class BlockPath : public Path, public std::enable_shared_from_this<BlockPath>
std::weak_ptr<NXButtonRailTile> m_nxButtonFrom;
std::weak_ptr<NXButtonRailTile> m_nxButtonTo;
boost::asio::steady_timer m_delayReleaseTimer;
bool m_isReserved;
bool m_delayedReleaseScheduled;
public:
static std::vector<std::shared_ptr<BlockPath>> find(BlockRailTile& block);
BlockPath(BlockRailTile& block, BlockSide side);
BlockPath(const BlockPath& other);
bool operator ==(const BlockPath& other) const noexcept;
@ -99,11 +105,17 @@ class BlockPath : public Path, public std::enable_shared_from_this<BlockPath>
return m_toSide;
}
inline bool isReserved() const
{
return m_isReserved;
}
std::shared_ptr<NXButtonRailTile> nxButtonFrom() const;
std::shared_ptr<NXButtonRailTile> nxButtonTo() const;
bool reserve(const std::shared_ptr<Train>& train, bool dryRun = false);
bool release(bool dryRun = false);
bool delayedRelease(uint16_t timeoutMillis);
};
#endif

Datei anzeigen

@ -98,6 +98,15 @@ bool NXManager::selectPath(const NXButtonRailTile& from, const NXButtonRailTile&
{
LOG_DEBUG("Path found:", path->fromBlock().name.value(), "->", path->toBlock()->name.value());
if(path->isReserved())
{
// If user clicked an already reserved path we release it.
// TODO: make some logic to prevent releasing a path while train is inside it?
// Also releasing a path when we already set signal to "Proceed" is dangerous because train might
// have already started moving towards out path
return path->release();
}
if(from.block->trains.empty())
{
continue; // no train in from block

Datei anzeigen

@ -133,35 +133,7 @@ BlockRailTile::BlockRailTile(World& world, std::string_view _id) :
throw LogMessageException(LogMessage::E3006_CANT_REMOVE_TRAIN_TRAIN_CAN_ONLY_BE_REMOVED_FROM_HEAD_OR_TAIL_BLOCK);
}
status->destroy();
status.reset();
updateTrainMethodEnabled();
if(state == BlockState::Reserved)
updateState();
Log::log(*this, LogMessage::N3002_REMOVED_TRAIN_X_FROM_BLOCK_X, oldTrain->name.value(), name.value());
if(oldTrain->blocks.empty())
{
oldTrain->active = false;
}
if(m_world.simulation)
{
for(const auto& item : *inputMap)
{
if(item->input && item->input->interface)
{
if(item->type == SensorType::OccupancyDetector)
item->input->simulateChange(item->invert.value() ? SimulateInputAction::SetTrue : SimulateInputAction::SetFalse);
else
assert(false); // not yet implemented
}
}
}
oldTrain->fireBlockRemoved(shared_ptr<BlockRailTile>());
fireEvent(onTrainRemoved, oldTrain, self);
removeTrainInternal(status);
}
}}
, flipTrain{*this, "flip_train",
@ -374,6 +346,7 @@ void BlockRailTile::identificationEvent(BlockInputMapItem& /*item*/, Identificat
case IdentificationEventType::Present:
//!< \todo assign train (if allowed and possible)
trains.appendInternal(TrainBlockStatus::create(*this, std::string("#").append(std::to_string(identifier)), blockDirection));
updateTrainMethodEnabled();
if(state == BlockState::Free || state == BlockState::Unknown)
updateState();
break;
@ -475,6 +448,46 @@ bool BlockRailTile::release(BlockSide side, bool dryRun)
return true;
}
bool BlockRailTile::removeTrainInternal(const std::shared_ptr<TrainBlockStatus> &status)
{
if(!status)
return false;
const auto self = shared_ptr<BlockRailTile>();
const std::shared_ptr<Train> oldTrain = status->train.value();
status->destroy();
updateTrainMethodEnabled();
if(state == BlockState::Reserved)
updateState();
Log::log(*this, LogMessage::N3002_REMOVED_TRAIN_X_FROM_BLOCK_X, oldTrain->name.value(), name.value());
if(oldTrain->blocks.empty())
{
oldTrain->active = false;
}
if(m_world.simulation)
{
for(const auto& item : *inputMap)
{
if(item->input && item->input->interface)
{
if(item->type == SensorType::OccupancyDetector)
item->input->simulateChange(item->invert.value() ? SimulateInputAction::SetTrue : SimulateInputAction::SetFalse);
else
assert(false); // not yet implemented
}
}
}
oldTrain->fireBlockRemoved(shared_ptr<BlockRailTile>());
fireEvent(onTrainRemoved, oldTrain, self);
return true;
}
void BlockRailTile::updateState()
{
if(!inputMap->items.empty())

Datei anzeigen

@ -110,6 +110,8 @@ class BlockRailTile : public RailTile
const std::shared_ptr<BlockPath> getReservedPath(BlockSide side) const;
bool reserve(const std::shared_ptr<BlockPath>& blockPath, const std::shared_ptr<Train>& train, BlockSide side, bool dryRun = false);
bool release(BlockSide side, bool dryRun = false);
bool removeTrainInternal(const std::shared_ptr<TrainBlockStatus>& status);
};
#endif

Datei anzeigen

@ -53,6 +53,7 @@ bool CrossRailTile::release(bool dryRun)
if(!dryRun)
{
m_crossState = CrossState::Unset;
RailTile::release();
}
return true;

Datei anzeigen

@ -32,6 +32,7 @@ CREATE_IMPL(DirectionControlRailTile)
DirectionControlRailTile::DirectionControlRailTile(World& world, std::string_view _id)
: StraightRailTile(world, _id, TileId::RailDirectionControl)
, m_node{*this, 2}
, m_reservedState(DirectionControlState::None)
, name{this, "name", id, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly}
, useNone{this, "use_none", true, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly,
[this](const bool /*value*/)
@ -61,6 +62,14 @@ DirectionControlRailTile::DirectionControlRailTile(World& world, std::string_vie
, setState{*this, "set_state", MethodFlags::ScriptCallable,
[this](DirectionControlState newState)
{
if(reservedState() && newState != m_reservedState)
{
// Direction control is currently locked by reserved path
// Allow setting to Both directions unless reserved direction is None
if(m_reservedState == DirectionControlState::None || newState != DirectionControlState::Both)
return false;
}
const auto& states = setState.getVectorAttribute<DirectionControlState>(AttributeName::Values);
if(std::find(states.begin(), states.end(), newState) == states.end())
return false;
@ -121,13 +130,14 @@ DirectionControlRailTile::DirectionControlRailTile(World& world, std::string_vie
bool DirectionControlRailTile::reserve(DirectionControlState directionControlState, bool dryRun)
{
if(state != directionControlState && state != DirectionControlState::Both)
if(reservedState() || (state != directionControlState && state != DirectionControlState::Both))
{
return false;
}
if(!dryRun)
{
m_reservedState = directionControlState;
StraightRailTile::reserve();
}

Datei anzeigen

@ -36,6 +36,7 @@ class DirectionControlRailTile final : public StraightRailTile
private:
Node m_node;
DirectionControlState m_reservedState;
void updateEnabled();
void updateStateValues();

Datei anzeigen

@ -22,11 +22,16 @@
#include "signalrailtile.hpp"
#include "../../../map/abstractsignalpath.hpp"
#include "../../../map/blockpath.hpp"
#include "../../../../core/attributes.hpp"
#include "../../../../core/method.tpp"
#include "../../../../core/objectproperty.tpp"
#include "../../../../world/getworld.hpp"
#include "../../../../utils/displayname.hpp"
#include "../blockrailtile.hpp"
#include "../../../../train/trainblockstatus.hpp"
#include "../../../../train/train.hpp"
#include "../../../../log/log.hpp"
std::optional<OutputActionValue> SignalRailTile::getDefaultActionValue(SignalAspect signalAspect, OutputType outputType, size_t outputIndex)
{
@ -79,6 +84,7 @@ std::optional<OutputActionValue> SignalRailTile::getDefaultActionValue(SignalAsp
SignalRailTile::SignalRailTile(World& world, std::string_view _id, TileId tileId_) :
StraightRailTile(world, _id, tileId_),
m_node{*this, 2},
m_retryCount(0),
name{this, "name", std::string(_id), PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
requireReservation{this, "require_reservation", AutoYesNo::Auto, PropertyFlags::ReadWrite | PropertyFlags::Store},
aspect{this, "aspect", SignalAspect::Unknown, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly},
@ -203,14 +209,89 @@ void SignalRailTile::connectOutputMap()
{
outputMap->onOutputStateMatchFound.connect([this](SignalAspect value)
{
bool changed = (value == aspect);
if(doSetAspect(value, true))
{
bool changed = (value != aspect);
if(!doSetAspect(value, true) || !changed)
return; // No change
if(!m_signalPath || !hasReservedPath())
return; // Not locked
// If we are in a signal path, re-evaluate our aspect
// This corrects accidental modifications of aspect done
// by the user with an handset or command station.
if(changed && m_signalPath)
Log::log(id, LogMessage::W3004_LOCKED_SIGNAL_CHANGED);
if(m_world.correctOutputPosWhenLocked)
{
auto now = std::chrono::steady_clock::now();
if((now - m_lastRetryStart) >= RETRY_DURATION)
{
// Reset retry count
m_lastRetryStart = now;
m_retryCount = 0;
}
if(m_retryCount < MAX_RETRYCOUNT)
{
// Try to reset output to reseved state
m_retryCount++;
evaluate();
Log::log(id, LogMessage::N3004_SIGNAL_RESET_TO_RESERVED_ASPECT);
return;
}
}
// We reached maximum retry count
// We cannot lock this signal. Take action.
switch (m_world.extOutputChangeAction.value())
{
default:
case ExternalOutputChangeAction::DoNothing:
{
// Do nothing
break;
}
case ExternalOutputChangeAction::EmergencyStopTrain:
{
if(auto blockPath = reservedPath())
{
std::vector<std::shared_ptr<Train>> alreadyStoppedTrains;
for(auto it : blockPath->fromBlock().trains)
{
it->train.value()->emergencyStop.setValue(true);
alreadyStoppedTrains.push_back(it->train.value());
Log::log(it->train->id, LogMessage::E3004_TRAIN_STOPPED_ON_SIGNAL_X_CHANGED, id.value());
}
auto toBlock = blockPath->toBlock();
if(toBlock)
{
for(auto it : blockPath->toBlock()->trains)
{
if(std::find(alreadyStoppedTrains.cbegin(), alreadyStoppedTrains.cend(), it->train.value()) != alreadyStoppedTrains.cend())
continue; // Do not stop train twice
it->train.value()->emergencyStop.setValue(true);
Log::log(it->train->id, LogMessage::E3004_TRAIN_STOPPED_ON_SIGNAL_X_CHANGED, id.value());
}
}
}
break;
}
case ExternalOutputChangeAction::EmergencyStopWorld:
{
m_world.stop();
Log::log(m_world, LogMessage::E3008_WORLD_STOPPED_ON_SIGNAL_X_CHANGED, id.value());
break;
}
case ExternalOutputChangeAction::PowerOffWorld:
{
m_world.powerOff();
Log::log(m_world, LogMessage::E3010_WORLD_POWER_OFF_ON_SIGNAL_X_CHANGED, id.value());
break;
}
}
});

Datei anzeigen

@ -23,6 +23,7 @@
#ifndef TRAINTASTIC_SERVER_BOARD_TILE_RAIL_SIGNAL_SIGNALRAILTILE_HPP
#define TRAINTASTIC_SERVER_BOARD_TILE_RAIL_SIGNAL_SIGNALRAILTILE_HPP
#include <chrono>
#include "../straightrailtile.hpp"
#include <traintastic/enum/autoyesno.hpp>
#include "../../../map/node.hpp"
@ -43,6 +44,10 @@ class SignalRailTile : public StraightRailTile
Node m_node;
std::unique_ptr<AbstractSignalPath> m_signalPath;
std::weak_ptr<BlockPath> m_blockPath;
std::chrono::steady_clock::time_point m_lastRetryStart;
uint8_t m_retryCount;
static constexpr uint8_t MAX_RETRYCOUNT = 3;
static constexpr std::chrono::steady_clock::duration RETRY_DURATION = std::chrono::minutes(1);
SignalRailTile(World& world, std::string_view _id, TileId tileId_);

Datei anzeigen

@ -26,6 +26,11 @@
#include "../../../../core/method.tpp"
#include "../../../../world/world.hpp"
#include "../../../../utils/displayname.hpp"
#include "../../../map/blockpath.hpp"
#include "../blockrailtile.hpp"
#include "../../../../train/trainblockstatus.hpp"
#include "../../../../train/train.hpp"
#include "../../../../log/log.hpp"
TurnoutRailTile::TurnoutRailTile(World& world, std::string_view _id, TileId tileId_, size_t connectors) :
RailTile(world, _id, tileId_),
@ -33,7 +38,13 @@ TurnoutRailTile::TurnoutRailTile(World& world, std::string_view _id, TileId tile
name{this, "name", std::string(_id), PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
position{this, "position", TurnoutPosition::Unknown, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly},
outputMap{this, "output_map", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject | PropertyFlags::NoScript},
setPosition{*this, "set_position", MethodFlags::ScriptCallable, [this](TurnoutPosition value) { return doSetPosition(value); }}
setPosition{*this, "set_position", MethodFlags::ScriptCallable, [this](TurnoutPosition value)
{
TurnoutPosition reservedPosition = getReservedPosition();
if(reservedPosition != TurnoutPosition::Unknown && reservedPosition != value)
return false; // Turnout is locked by reservation path
return doSetPosition(value);
}}
{
assert(isRailTurnout(tileId_));
@ -54,12 +65,23 @@ TurnoutRailTile::TurnoutRailTile(World& world, std::string_view _id, TileId tile
// setPosition is added by sub class
}
bool TurnoutRailTile::reserve(TurnoutPosition turnoutPosition, bool dryRun)
bool TurnoutRailTile::reserve(const std::shared_ptr<BlockPath> &blockPath, TurnoutPosition turnoutPosition, bool dryRun)
{
if(!isValidPosition(turnoutPosition))
{
return false;
}
const TurnoutPosition reservedPos = getReservedPosition();
if(reservedPos != TurnoutPosition::Unknown && reservedPos != turnoutPosition)
{
// TODO: what if 2 path reserve same turnout for same position?
// Upon release one path it will make turnout free while it's still reserved by second path
// Turnout is already reserved for another position
return false;
}
if(!dryRun)
{
if(!doSetPosition(turnoutPosition)) /*[[unlikely]]*/
@ -67,6 +89,8 @@ bool TurnoutRailTile::reserve(TurnoutPosition turnoutPosition, bool dryRun)
return false;
}
m_reservedPath = blockPath;
RailTile::setReservedState(static_cast<uint8_t>(turnoutPosition));
}
return true;
@ -78,6 +102,8 @@ bool TurnoutRailTile::release(bool dryRun)
if(!dryRun)
{
m_reservedPath.reset();
RailTile::release();
}
return true;
@ -95,6 +121,11 @@ void TurnoutRailTile::addToWorld()
RailTile::addToWorld();
}
TurnoutPosition TurnoutRailTile::getReservedPosition() const
{
return static_cast<TurnoutPosition>(RailTile::reservedState());
}
void TurnoutRailTile::worldEvent(WorldState state, WorldEvent event)
{
RailTile::worldEvent(state, event);
@ -128,7 +159,91 @@ void TurnoutRailTile::connectOutputMap()
{
outputMap->onOutputStateMatchFound.connect([this](TurnoutPosition pos)
{
doSetPosition(pos, true);
bool changed = (pos != position);
if(!doSetPosition(pos, true) || !changed)
return; // No change
TurnoutPosition reservedPosition = getReservedPosition();
if(reservedPosition == TurnoutPosition::Unknown || reservedPosition == position.value())
return; // Not locked
// If turnout is inside a reserved path, force it to reserved position
// This corrects accidental modifications of position done
// by the user with an handset or command station.
Log::log(id, LogMessage::W3003_LOCKED_TURNOUT_CHANGED);
if(m_world.correctOutputPosWhenLocked)
{
auto now = std::chrono::steady_clock::now();
if((now - m_lastRetryStart) >= RETRY_DURATION)
{
// Reset retry count
m_lastRetryStart = now;
m_retryCount = 0;
}
if(m_retryCount < MAX_RETRYCOUNT)
{
// Try to reset output to reseved state
m_retryCount++;
doSetPosition(reservedPosition, false);
Log::log(id, LogMessage::N3003_TURNOUT_RESET_TO_RESERVED_POSITION);
return;
}
}
// We reached maximum retry count
// We cannot lock this turnout. Take action.
switch (m_world.extOutputChangeAction.value())
{
default:
case ExternalOutputChangeAction::DoNothing:
{
// Do nothing
break;
}
case ExternalOutputChangeAction::EmergencyStopTrain:
{
if(auto blockPath = m_reservedPath.lock())
{
std::vector<std::shared_ptr<Train>> alreadyStoppedTrains;
for(auto it : blockPath->fromBlock().trains)
{
it->train.value()->emergencyStop.setValue(true);
alreadyStoppedTrains.push_back(it->train.value());
Log::log(it->train->id, LogMessage::E3003_TRAIN_STOPPED_ON_TURNOUT_X_CHANGED, id.value());
}
auto toBlock = blockPath->toBlock();
if(toBlock)
{
for(auto it : blockPath->toBlock()->trains)
{
if(std::find(alreadyStoppedTrains.cbegin(), alreadyStoppedTrains.cend(), it->train.value()) != alreadyStoppedTrains.cend())
continue; // Do not stop train twice
it->train.value()->emergencyStop.setValue(true);
Log::log(it->train->id, LogMessage::E3003_TRAIN_STOPPED_ON_TURNOUT_X_CHANGED, id.value());
}
}
}
break;
}
case ExternalOutputChangeAction::EmergencyStopWorld:
{
m_world.stop();
Log::log(m_world, LogMessage::E3007_WORLD_STOPPED_ON_TURNOUT_X_CHANGED, id.value());
break;
}
case ExternalOutputChangeAction::PowerOffWorld:
{
m_world.powerOff();
Log::log(m_world, LogMessage::E3009_WORLD_POWER_OFF_ON_TURNOUT_X_CHANGED, id.value());
break;
}
}
});
//TODO: disconnect somewhere?

Datei anzeigen

@ -23,6 +23,7 @@
#ifndef TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTRAILTILE_HPP
#define TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTRAILTILE_HPP
#include <chrono>
#include "../railtile.hpp"
#include "../../../map/node.hpp"
#include "../../../../core/objectproperty.hpp"
@ -30,12 +31,20 @@
#include "../../../../enum/turnoutposition.hpp"
#include "../../../../hardware/output/map/turnoutoutputmap.hpp"
class BlockPath;
class TurnoutRailTile : public RailTile
{
DEFAULT_ID("turnout")
private:
Node m_node;
std::weak_ptr<BlockPath> m_reservedPath;
std::chrono::steady_clock::time_point m_lastRetryStart;
uint8_t m_retryCount;
static constexpr uint8_t MAX_RETRYCOUNT = 3;
static constexpr std::chrono::steady_clock::duration RETRY_DURATION = std::chrono::minutes(1);
protected:
TurnoutRailTile(World& world, std::string_view _id, TileId tileId_, size_t connectors);
@ -60,8 +69,10 @@ class TurnoutRailTile : public RailTile
std::optional<std::reference_wrapper<const Node>> node() const final { return m_node; }
std::optional<std::reference_wrapper<Node>> node() final { return m_node; }
virtual bool reserve(TurnoutPosition turnoutPosition, bool dryRun = false);
virtual bool reserve(const std::shared_ptr<BlockPath>& blockPath, TurnoutPosition turnoutPosition, bool dryRun = false);
bool release(bool dryRun = false);
TurnoutPosition getReservedPosition() const;
};
#endif

Datei anzeigen

@ -24,11 +24,15 @@
#include "train.hpp"
#include "trainblockstatus.hpp"
#include "../board/tile/rail/blockrailtile.hpp"
#include "../board/map/blockpath.hpp"
#include "../core/objectproperty.tpp"
#include "../world/world.hpp"
void TrainTracking::reserve(const std::shared_ptr<Train>& train, const std::shared_ptr<BlockRailTile>& block, BlockTrainDirection direction)
{
block->trains.appendInternal(TrainBlockStatus::create(*block, *train, direction));
block->updateTrainMethodEnabled();
train->fireBlockReserved(block, direction);
block->fireTrainReserved(train, direction);
}
@ -88,6 +92,21 @@ void TrainTracking::left(std::shared_ptr<TrainBlockStatus> status)
train->fireBlockLeft(block, direction);
block->fireTrainLeft(train, direction);
const auto pathA = block->getReservedPath(BlockSide::A);
const bool exitA = pathA && &pathA->fromBlock() == block.get();
const auto pathB = block->getReservedPath(BlockSide::B);
const bool exitB = pathB && &pathB->fromBlock() == block.get();
if(direction == BlockTrainDirection::TowardsA && exitA)
{
pathA->delayedRelease(block->world().pathReleaseDelay);
}
else if(direction == BlockTrainDirection::TowardsB && exitB)
{
pathB->delayedRelease(block->world().pathReleaseDelay);
}
status->destroy();
#ifndef NDEBUG
std::weak_ptr<TrainBlockStatus> statusWeak = status;

Datei anzeigen

@ -32,6 +32,7 @@ namespace Category
constexpr std::string_view info = "category:info";
constexpr std::string_view log = "category:log";
constexpr std::string_view network = "category:network";
constexpr std::string_view trains = "category:trains";
}
#endif

Datei anzeigen

@ -67,6 +67,7 @@
#include "../train/trainlist.hpp"
#include "../vehicle/rail/railvehiclelist.hpp"
#include "../lua/scriptlist.hpp"
#include "../utils/category.hpp"
using nlohmann::json;
@ -144,6 +145,9 @@ World::World(Private /*unused*/) :
powerOnWhenLoaded = true; // can't run without power
}
}},
correctOutputPosWhenLocked{this, "correct_output_pos_when_locked", true, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript},
extOutputChangeAction{this, "ext_output_change_action", ExternalOutputChangeAction::EmergencyStopTrain, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript},
pathReleaseDelay{this, "path_release_delay", 5000, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript},
decoderControllers{this, "input_controllers", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
inputControllers{this, "input_controllers", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
outputControllers{this, "output_controllers", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
@ -308,6 +312,20 @@ World::World(Private /*unused*/) :
m_interfaceItems.add(powerOnWhenLoaded);
m_interfaceItems.add(runWhenLoaded);
Attributes::addCategory(correctOutputPosWhenLocked, Category::trains);
Attributes::addEnabled(correctOutputPosWhenLocked, true);
m_interfaceItems.add(correctOutputPosWhenLocked);
Attributes::addCategory(extOutputChangeAction, Category::trains);
Attributes::addEnabled(extOutputChangeAction, true);
Attributes::addValues(extOutputChangeAction, extOutputChangeActionValues);
m_interfaceItems.add(extOutputChangeAction);
Attributes::addCategory(pathReleaseDelay, Category::trains);
Attributes::addEnabled(pathReleaseDelay, true);
Attributes::addMinMax(pathReleaseDelay, {0, 15000}); // Up to 15 seconds
m_interfaceItems.add(pathReleaseDelay);
Attributes::addObjectEditor(decoderControllers, false);
m_interfaceItems.add(decoderControllers);
Attributes::addObjectEditor(inputControllers, false);

Datei anzeigen

@ -31,6 +31,7 @@
#include "../core/event.hpp"
#include <unordered_map>
#include <boost/uuid/uuid.hpp>
#include <traintastic/enum/externaloutputchangeaction.hpp>
#include <traintastic/enum/worldevent.hpp>
#include "../enum/worldscale.hpp"
#include "../status/status.hpp"
@ -103,6 +104,10 @@ class World : public Object
Property<bool> powerOnWhenLoaded;
Property<bool> runWhenLoaded;
Property<bool> correctOutputPosWhenLocked;
Property<ExternalOutputChangeAction> extOutputChangeAction;
Property<uint16_t> pathReleaseDelay;
ObjectProperty<ControllerList<DecoderController>> decoderControllers;
ObjectProperty<ControllerList<InputController>> inputControllers;
ObjectProperty<ControllerList<OutputController>> outputControllers;

Datei anzeigen

@ -29,6 +29,7 @@
#include "../../src/board/nx/nxmanager.hpp"
#include "../../src/board/tile/rail/blockrailtile.hpp"
#include "../../src/board/tile/rail/bridge90railtile.hpp"
#include "../../src/board/tile/rail/directioncontrolrailtile.hpp"
#include "../../src/board/tile/rail/nxbuttonrailtile.hpp"
#include "../../src/board/tile/rail/turnout/turnoutleft45railtile.hpp"
#include "../../src/board/tile/rail/turnout/turnoutright45railtile.hpp"
@ -157,3 +158,100 @@ TEST_CASE("Board: Bridge path resevation using NX", "[board][board-path]")
REQUIRE(train1.expired());
REQUIRE(train2.expired());
}
TEST_CASE("Board: Direction path reservation using NX and change direction state", "[board][board-path]")
{
auto world = World::create();
std::weak_ptr<World> worldWeak = world;
// Board:
// +--------+ +--------+
// | block1 |--(nx1)--(-)--(nx2)--| block2 |
// +--------+ +--------+
std::weak_ptr<Board> boardWeak = world->boards->create();
REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false));
REQUIRE(boardWeak.lock()->addTile(1, 0, TileRotate::Deg90, NXButtonRailTile::classId, false));
REQUIRE(boardWeak.lock()->addTile(2, 0, TileRotate::Deg90, DirectionControlRailTile::classId, false));
REQUIRE(boardWeak.lock()->addTile(3, 0, TileRotate::Deg90, NXButtonRailTile::classId, false));
REQUIRE(boardWeak.lock()->addTile(4, 0, TileRotate::Deg90, BlockRailTile::classId, false));
std::weak_ptr<BlockRailTile> block1 = std::dynamic_pointer_cast<BlockRailTile>(boardWeak.lock()->getTile({0, 0}));
REQUIRE_FALSE(block1.expired());
std::weak_ptr<BlockRailTile> block2 = std::dynamic_pointer_cast<BlockRailTile>(boardWeak.lock()->getTile({4, 0}));
REQUIRE_FALSE(block2.expired());
std::weak_ptr<NXButtonRailTile> nx1 = std::dynamic_pointer_cast<NXButtonRailTile>(boardWeak.lock()->getTile({1, 0}));
REQUIRE_FALSE(nx1.expired());
std::weak_ptr<NXButtonRailTile> nx2 = std::dynamic_pointer_cast<NXButtonRailTile>(boardWeak.lock()->getTile({3, 0}));
REQUIRE_FALSE(nx2.expired());
std::weak_ptr<DirectionControlRailTile> directionControl = std::dynamic_pointer_cast<DirectionControlRailTile>(boardWeak.lock()->getTile({2, 0}));
REQUIRE_FALSE(directionControl.expired());
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::Both);
// Change direction control states (no reservation, so all valuese are allowed):
REQUIRE(directionControl.lock()->setState(DirectionControlState::AtoB));
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::AtoB);
REQUIRE(directionControl.lock()->setState(DirectionControlState::BtoA));
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::BtoA);
REQUIRE(directionControl.lock()->setState(DirectionControlState::None));
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::None);
REQUIRE(directionControl.lock()->setState(DirectionControlState::Both));
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::Both);
// Set block free:
REQUIRE(block2.lock()->state == BlockState::Unknown);
REQUIRE(block2.lock()->setStateFree());
REQUIRE(block2.lock()->state == BlockState::Free);
// Create train:
std::weak_ptr<RailVehicle> locomotive = world->railVehicles->create(Locomotive::classId);
std::weak_ptr<Train> train = world->trains->create();
REQUIRE(train.lock()->vehicles->length == 0);
train.lock()->vehicles->add(locomotive.lock());
REQUIRE(train.lock()->vehicles->length == 1);
// Assign train to block 1:
block1.lock()->assignTrain(train.lock());
REQUIRE(block1.lock()->state == BlockState::Reserved);
block1.lock()->flipTrain();
// Set world in RUN state (required for selecting paths using NX buttons):
world->run();
// Set path for train from block 1 to block 2:
world->nxManager->select(nx1.lock(), nx2.lock());
REQUIRE(block2.lock()->state == BlockState::Reserved);
REQUIRE(block2.lock()->trains.size() == 1);
REQUIRE(block2.lock()->trains[0]->train.value() == train.lock());
// Change direction control state:
// A -> B = allowed as that is the train direction.
REQUIRE(directionControl.lock()->setState(DirectionControlState::AtoB));
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::AtoB);
// B -> A = not allowed as that is opposite of the the train direction.
REQUIRE_FALSE(directionControl.lock()->setState(DirectionControlState::BtoA));
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::AtoB);
// None = not allowed when reserved.
REQUIRE_FALSE(directionControl.lock()->setState(DirectionControlState::None));
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::AtoB);
// Both = always allowed.
REQUIRE(directionControl.lock()->setState(DirectionControlState::Both));
REQUIRE(directionControl.lock()->state.value() == DirectionControlState::Both);
world.reset();
REQUIRE(worldWeak.expired());
REQUIRE(boardWeak.expired());
REQUIRE(block1.expired());
REQUIRE(block2.expired());
REQUIRE(nx1.expired());
REQUIRE(nx2.expired());
REQUIRE(directionControl.expired());
REQUIRE(locomotive.expired());
REQUIRE(train.expired());
}

Datei anzeigen

@ -0,0 +1,53 @@
/**
* shared/src/traintastic/enum/externaloutputchangeaction.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Filippo Gentile
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SHARED_TRAINTASTIC_ENUM_EXTERNALOUTPUTCHANGEACTION_HPP
#define TRAINTASTIC_SHARED_TRAINTASTIC_ENUM_EXTERNALOUTPUTCHANGEACTION_HPP
#include <cstdint>
#include <array>
#include "enum.hpp"
enum class ExternalOutputChangeAction : uint8_t
{
DoNothing = 0,
EmergencyStopTrain = 1,
EmergencyStopWorld = 2,
PowerOffWorld = 3,
};
TRAINTASTIC_ENUM(ExternalOutputChangeAction, "ext_output_change_action", 4,
{
{ExternalOutputChangeAction::DoNothing, "do_nothing"},
{ExternalOutputChangeAction::EmergencyStopTrain, "estop_train"},
{ExternalOutputChangeAction::EmergencyStopWorld, "estop_world"},
{ExternalOutputChangeAction::PowerOffWorld, "poweroff_world"},
});
inline constexpr std::array<ExternalOutputChangeAction, 4> extOutputChangeActionValues{{
ExternalOutputChangeAction::DoNothing,
ExternalOutputChangeAction::EmergencyStopTrain,
ExternalOutputChangeAction::EmergencyStopWorld,
ExternalOutputChangeAction::PowerOffWorld,
}};
#endif // TRAINTASTIC_SHARED_TRAINTASTIC_ENUM_EXTERNALOUTPUTCHANGEACTION_HPP

Datei anzeigen

@ -130,6 +130,8 @@ enum class LogMessage : uint32_t
N2007_LISTEN_ONLY_MODE_DEACTIVATED = LogMessageOffset::notice + 2007,
N3001_ASSIGNED_TRAIN_X_TO_BLOCK_X = LogMessageOffset::notice + 3001,
N3002_REMOVED_TRAIN_X_FROM_BLOCK_X = LogMessageOffset::notice + 3002,
N3003_TURNOUT_RESET_TO_RESERVED_POSITION = LogMessageOffset::notice + 3003,
N3004_SIGNAL_RESET_TO_RESERVED_ASPECT = LogMessageOffset::notice + 3004,
N9001_STARTING_SCRIPT = LogMessageOffset::notice + 9001,
N9999_X = LogMessageOffset::notice + 9999,
@ -149,6 +151,8 @@ enum class LogMessage : uint32_t
W2020_DCCEXT_RCN213_IS_NOT_SUPPORTED = LogMessageOffset::warning + 2020,
W3001_NX_BUTTON_CONNECTED_TO_TWO_BLOCKS = LogMessageOffset::warning + 3001,
W3002_NX_BUTTON_NOT_CONNECTED_TO_ANY_BLOCK = LogMessageOffset::warning + 3002,
W3003_LOCKED_TURNOUT_CHANGED = LogMessageOffset::warning + 3003,
W3004_LOCKED_SIGNAL_CHANGED = LogMessageOffset::warning + 3004,
W9001_EXECUTION_TOOK_X_US = LogMessageOffset::warning + 9001,
W9999_X = LogMessageOffset::warning + 9999,
@ -187,8 +191,14 @@ enum class LogMessage : uint32_t
E2024_UNKNOWN_LOCOMOTIVE_MFX_UID_X = LogMessageOffset::error + 2024,
E3001_CANT_DELETE_RAIL_VEHICLE_WHEN_IN_ACTIVE_TRAIN = LogMessageOffset::error + 3001,
E3002_CANT_DELETE_ACTIVE_TRAIN = LogMessageOffset::error + 3002,
E3003_TRAIN_STOPPED_ON_TURNOUT_X_CHANGED = LogMessageOffset::error + 3003,
E3004_TRAIN_STOPPED_ON_SIGNAL_X_CHANGED = LogMessageOffset::error + 3004,
E3005_CANT_REMOVE_TRAIN_TRAIN_MUST_BE_STOPPED_FIRST = LogMessageOffset::error + 3005,
E3006_CANT_REMOVE_TRAIN_TRAIN_CAN_ONLY_BE_REMOVED_FROM_HEAD_OR_TAIL_BLOCK = LogMessageOffset::error + 3006,
E3007_WORLD_STOPPED_ON_TURNOUT_X_CHANGED = LogMessageOffset::error + 3007,
E3008_WORLD_STOPPED_ON_SIGNAL_X_CHANGED = LogMessageOffset::error + 3008,
E3009_WORLD_POWER_OFF_ON_TURNOUT_X_CHANGED = LogMessageOffset::error + 3009,
E3010_WORLD_POWER_OFF_ON_SIGNAL_X_CHANGED = LogMessageOffset::error + 3010,
E9001_X_DURING_EXECUTION_OF_X_EVENT_HANDLER = LogMessageOffset::error + 9001,
E9999_X = LogMessageOffset::error + 9999,

Datei anzeigen

@ -242,6 +242,14 @@
"comment": "",
"fuzzy": 0
},
{
"term": "category:trains",
"definition": "Trains",
"context": "",
"term_plural": "",
"reference": "",
"comment": ""
},
{
"term": "class_id:board_tile.misc.push_button",
"definition": "Push button",
@ -953,6 +961,22 @@
"comment": "",
"fuzzy": 0
},
{
"term": "ext_output_change_action:do_nothing",
"definition": "Do nothing"
},
{
"term": "ext_output_change_action:estop_train",
"definition": "Emergency stop trains in block path"
},
{
"term": "ext_output_change_action:estop_world",
"definition": "Emergency stop World"
},
{
"term": "ext_output_change_action:poweroff_world",
"definition": "Power off World"
},
{
"term": "ecos_channel:ecos_detector",
"definition": "ECoS Detector",
@ -2825,6 +2849,14 @@
"comment": "",
"fuzzy": 0
},
{
"term": "message:N3003",
"definition": "Turnout position was reset to reserved one"
},
{
"term": "message:N3004",
"definition": "Signal aspect was reset to reserved one"
},
{
"term": "message:N9001",
"definition": "Starting script",
@ -4454,6 +4486,30 @@
"comment": "",
"fuzzy": 0
},
{
"term": "world:correct_output_pos_when_locked",
"definition": "Try to correct Ouput state when externally chaged while locked",
"context": "",
"term_plural": "",
"reference": "",
"comment": ""
},
{
"term": "world:ext_output_change_action",
"definition": "External Output Change Action",
"context": "",
"term_plural": "",
"reference": "",
"comment": ""
},
{
"term": "world:path_release_delay",
"definition": "Block Path release delay",
"context": "",
"term_plural": "",
"reference": "",
"comment": ""
},
{
"term": "world_scale:custom",
"definition": "Custom",
@ -4958,6 +5014,30 @@
"comment": "",
"fuzzy": 0
},
{
"term": "message:E3003",
"definition": "Train was emergency stopped due to turnout %1 position externally changed"
},
{
"term": "message:E3004",
"definition": "Train was emergency stopped due to signal %1 aspect externally changed"
},
{
"term": "message:E3007",
"definition": "World was emergency stopped due to turnout %1 position externally changed"
},
{
"term": "message:E3008",
"definition": "World was emergency stopped due to signal %1 aspect externally changed"
},
{
"term": "message:E3009",
"definition": "World was emergency powered off due to turnout %1 position externally changed"
},
{
"term": "message:E3010",
"definition": "World was emergency powered off due to signal %1 aspect externally changed"
},
{
"term": "message:C1014",
"definition": "Invalid command",
@ -5030,6 +5110,14 @@
"comment": "",
"fuzzy": 0
},
{
"term": "message:W3003",
"definition": "Turnout position externally changed while locked in a path"
},
{
"term": "message:W3004",
"definition": "Signal aspect externally changed while locked in a path"
},
{
"term": "auto_yes_no:auto",
"definition": "Auto detect",

Datei anzeigen

@ -242,6 +242,14 @@
"comment": "",
"fuzzy": 0
},
{
"term": "category:trains",
"definition": "Treni",
"context": "",
"term_plural": "",
"reference": "",
"comment": ""
},
{
"term": "class_id:board_tile.misc.push_button",
"definition": "Bottone",
@ -953,6 +961,22 @@
"comment": "",
"fuzzy": 0
},
{
"term": "ext_output_change_action:do_nothing",
"definition": "Non fare niente"
},
{
"term": "ext_output_change_action:estop_train",
"definition": "Stop di emergenza per i treni nell'itinerario"
},
{
"term": "ext_output_change_action:estop_world",
"definition": "Stop di emergenza Mondo"
},
{
"term": "ext_output_change_action:poweroff_world",
"definition": "Spegni Mondo"
},
{
"term": "ecos_channel:ecos_detector",
"definition": "ECoS Detector",
@ -2825,6 +2849,14 @@
"comment": "",
"fuzzy": 0
},
{
"term": "message:N3003",
"definition": "La posizione del deviatoio è stata riportata a quella riservata"
},
{
"term": "message:N3004",
"definition": "L'aspetto del segnale è stato riportato a quello riservato"
},
{
"term": "message:N9001",
"definition": "Avvio script",
@ -4454,6 +4486,30 @@
"comment": "",
"fuzzy": 0
},
{
"term": "world:correct_output_pos_when_locked",
"definition": "Prova a correggere lo stato di un output modificato esternamente mentre bloccato",
"context": "",
"term_plural": "",
"reference": "",
"comment": ""
},
{
"term": "world:ext_output_change_action",
"definition": "Azione per Modifica Esterna di un Output",
"context": "",
"term_plural": "",
"reference": "",
"comment": ""
},
{
"term": "world:path_release_delay",
"definition": "Ritardo nella liberazione dell'itinerario",
"context": "",
"term_plural": "",
"reference": "",
"comment": ""
},
{
"term": "world_scale:custom",
"definition": "Custom",
@ -4958,6 +5014,30 @@
"comment": "",
"fuzzy": 0
},
{
"term": "message:E3003",
"definition": "Il treno \u00e8 stato stoppato d'emergenza per la modifica esterna della posizione del deviatoio %1"
},
{
"term": "message:E3004",
"definition": "Il treno \u00e8 stato stoppato d'emergenza per la modifica esterna dell'aspetto del segnale %1"
},
{
"term": "message:E3007",
"definition": "Il mondo \u00e8 stato stoppato d'emergenza per la modifica esterna della posizione del deviatoio %1"
},
{
"term": "message:E3008",
"definition": "Il mondo \u00e8 stato stoppato d'emergenza per la modifica esterna dell'aspetto del segnale %1"
},
{
"term": "message:E3009",
"definition": "Il mondo \u00e8 stato spento per la modifica esterna della posizione del deviatoio %1"
},
{
"term": "message:E3010",
"definition": "Il mondo \u00e8 stato spento per la modifica esterna dell'aspetto del segnale %1"
},
{
"term": "message:C1014",
"definition": "Comando non valido",
@ -5030,6 +5110,14 @@
"comment": "",
"fuzzy": 0
},
{
"term": "message:W3003",
"definition": "Posizione del deviatoio modificata dall'esterno mentre era bloccato in un itinerario"
},
{
"term": "message:W3004",
"definition": "Aspetto del segnale modificato dall'esterno mentre era bloccato in un itinerario"
},
{
"term": "auto_yes_no:auto",
"definition": "Auto rileva",