Merge pull request #115 from gfgit/work/gfgit/path_release
Path reservation fixes
Dieser Commit ist enthalten in:
Commit
99a3fb56d9
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,22 +529,47 @@ 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->release(m_toSide, dryRun))
|
||||
if(toBlock->trains.size() == 1)
|
||||
{
|
||||
assert(dryRun);
|
||||
return false;
|
||||
//TODO: this bypasses some checks
|
||||
toBlock->removeTrainInternal(toBlock->trains[0]);
|
||||
//TODO: dryRun? what if it fails?
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if(!toBlock->release(m_toSide, dryRun))
|
||||
{
|
||||
assert(dryRun);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
|
||||
@ -53,6 +53,7 @@ bool CrossRailTile::release(bool dryRun)
|
||||
|
||||
if(!dryRun)
|
||||
{
|
||||
m_crossState = CrossState::Unset;
|
||||
RailTile::release();
|
||||
}
|
||||
return true;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ class DirectionControlRailTile final : public StraightRailTile
|
||||
|
||||
private:
|
||||
Node m_node;
|
||||
DirectionControlState m_reservedState;
|
||||
|
||||
void updateEnabled();
|
||||
void updateStateValues();
|
||||
|
||||
@ -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.
|
||||
Log::log(id, LogMessage::W3004_LOCKED_SIGNAL_CHANGED);
|
||||
|
||||
if(m_world.correctOutputPosWhenLocked)
|
||||
{
|
||||
// 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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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_);
|
||||
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
53
shared/src/traintastic/enum/externaloutputchangeaction.hpp
Normale Datei
53
shared/src/traintastic/enum/externaloutputchangeaction.hpp
Normale Datei
@ -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
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
@ -6308,4 +6396,4 @@
|
||||
"comment": "",
|
||||
"fuzzy": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
Laden…
x
In neuem Issue referenzieren
Einen Benutzer sperren