diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index 16bb207f..a1a10207 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -261,6 +261,22 @@ enum class range_t /// Affects the entire consist. consist }; +/// State of the door closure signalling (SOD) of a vehicle or a consist, ordered from the least to the most +/// severe: the state of a consist is the most severe state of its vehicles. +enum class door_closure_t +{ + /// No doors worked by the crew: the vehicle doesn't take part in the signalling. + none, + /// All doors closed and locked: they can't be opened. + locked, + /// All doors closed, but released: a permit is given, or no lock holds them shut. + released, + /// A door is open, or not fully closed yet. + open +}; +/// Whether the door closure signalling lamp is lit: steadily for locked doors, in the lit phase of the +/// blinking for released ones, never with a door open or with no doors signalled. +bool door_closure_lamp(door_closure_t const State, bool const BlinkLit); /// Possible settings for an enable/disable input pair (mutually exclusive). enum class operation_t { @@ -1439,6 +1455,7 @@ class TMoverParameters float auto_velocity{-1.f}; // automatic door closure velocity threshold bool auto_include_remote{false}; // automatic door closure applies also to remote control bool permit_needed{false}; + bool closure_signal{false}; // the door closure lamp (SOD) of the cab signals the doors of the whole consist in three states std::vector permit_presets; // permit presets selectable with preset switch float voltage{0.f}; // power type required for door movement // ld inputs @@ -2593,6 +2610,10 @@ class TMoverParameters bool LockDoors(bool const State, range_t const Notify = range_t::consist); bool signal_departure(bool const State, range_t const Notify = range_t::consist); // toggles departure warning void update_doors(double const Deltatime); // door controller update + door_closure_t door_closure() const; // state of the door closure signalling of this vehicle + // whether the door closure lamp in the cab of this vehicle is lit: with DoorClosureSignal, for the given state of the + // consist; without it, for the lock of the vehicle's own doors, as the lamp always worked + bool door_closure_lamp_lit(door_closure_t const Consist, bool const BlinkLit) const; /* funkcje dla samochodow*/ bool ChangeOffsetH(double DeltaOffset); diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 20761bac..ce3c0845 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -8699,6 +8699,48 @@ bool TMoverParameters::signal_departure(bool const State, range_t const Notify) return true; } +// state of the door closure signalling (SOD) of this vehicle: open while a door isn't fully closed, released while +// the closed doors can be opened (a permit given, or, for doors that need none, no lock engaged), locked otherwise. +// doors the crew doesn't close (autonomous) aren't signalled, the same as for the door lights of the consist +door_closure_t TMoverParameters::door_closure() const +{ + if (Doors.range == 0.f || Doors.close_control == control_t::autonomous) + { + return door_closure_t::none; + } + if (std::ranges::any_of(Doors.instances, [](basic_door const &Door) { return false == Door.is_closed; })) + { + return door_closure_t::open; + } + if (Doors.is_locked) + { + return door_closure_t::locked; + } + if (Doors.permit_needed && std::ranges::none_of(Doors.instances, [](basic_door const &Door) { return Door.open_permit; })) + { + return door_closure_t::locked; + } + return door_closure_t::released; +} + +bool TMoverParameters::door_closure_lamp_lit(door_closure_t const Consist, bool const BlinkLit) const +{ + return Doors.closure_signal ? door_closure_lamp(Consist, BlinkLit) : Doors.is_locked; +} + +bool door_closure_lamp(door_closure_t const State, bool const BlinkLit) +{ + switch (State) + { + case door_closure_t::locked: + return true; + case door_closure_t::released: + return BlinkLit; + default: + return false; + } +} + // automatic door controller update void TMoverParameters::update_doors(double const Deltatime) { @@ -10576,6 +10618,8 @@ void TMoverParameters::LoadFIZ_Doors(std::string const &line) extract_value(Doors.auto_include_remote, "DoorAutoCloseRemote", line, ""); // operation permit extract_value(Doors.permit_needed, "DoorNeedPermit", line, ""); + // the door closure lamp (SOD) of the cab signals the doors of the whole consist + extract_value(Doors.closure_signal, "DoorClosureSignal", line, ""); { auto permitpresets = Split(extract_value("DoorPermitList", line), '|'); for (auto const &permit : permitpresets) diff --git a/tests/unit/test_door_closure.cpp b/tests/unit/test_door_closure.cpp new file mode 100644 index 00000000..c92d8fef --- /dev/null +++ b/tests/unit/test_door_closure.cpp @@ -0,0 +1,158 @@ +#include "stdafx.h" +#include "doctest.h" +#include "scenario.h" +#include "vehicle.h" + +// the door closure signalling (SOD, the i-door_blocked lamp of a cab): the state of each vehicle's doors and of the +// whole train, the most severe of its vehicles, as the driver gathers it for the lamp (eu07/maszyna#288) + +using test::scenario; +using test::definition; +using test::vehicle; +using namespace test::channels; + +namespace { + +// the state of the whole train, the way TController::determine_consist_state() gathers it +door_closure_t consist_closure(scenario &Scenario) { + auto closure{door_closure_t::none}; + for (std::size_t i = 0; i < Scenario.train().size(); ++i) { + closure = std::max(closure, Scenario.vehicle(i).door_closure()); + } + return closure; +} + +} // namespace + +TEST_CASE("door closure: the lamp is lit for locked doors, blinks for released ones and is dark for open ones") { + for (auto const lit : {true, false}) { + CAPTURE(lit); + CHECK(door_closure_lamp(door_closure_t::locked, lit)); + CHECK(door_closure_lamp(door_closure_t::released, lit) == lit); + CHECK_FALSE(door_closure_lamp(door_closure_t::open, lit)); + CHECK_FALSE(door_closure_lamp(door_closure_t::none, lit)); + } + // a consist takes the most severe state of its vehicles + CHECK(std::max(door_closure_t::none, door_closure_t::locked) == door_closure_t::locked); + CHECK(std::max(door_closure_t::locked, door_closure_t::released) == door_closure_t::released); + CHECK(std::max(door_closure_t::released, door_closure_t::open) == door_closure_t::open); +} + +TEST_CASE("door closure: a unit with permits, locked, released by the permit and open, in any of its cars") { + scenario scenario{"door closure/elf", motion() + doors()}; + scenario.add("elf_v1/21we-a", 1).add("elf_v1/21we-b").add("elf_v1/21we-c"); + scenario.start_up(); + auto &cab{scenario.cab()}; + auto &last{scenario.vehicle(scenario.train().size() - 1)}; + // closed with no permit: no door can be opened + CHECK(cab.door_closure() == door_closure_t::locked); + CHECK(last.door_closure() == door_closure_t::locked); + CHECK(consist_closure(scenario) == door_closure_t::locked); + + scenario.act("permit left"); + cab.PermitDoors(side::left, true); + scenario.wait(2.0); + CHECK(last.door_closure() == door_closure_t::released); + CHECK(consist_closure(scenario) == door_closure_t::released); + + scenario.act("the last car opens its own door"); + last.OperateDoors(side::left, true, range_t::local); + scenario.wait(5.0); + // the cab's own doors stay closed, the train is open all the same + CHECK(cab.door_closure() == door_closure_t::released); + CHECK(last.door_closure() == door_closure_t::open); + CHECK(consist_closure(scenario) == door_closure_t::open); + + scenario.act("close, the steps stay out under the permit"); + last.OperateDoors(side::left, false, range_t::local); + cab.OperateDoors(side::left, false); + scenario.wait(10.0); + CHECK(last.Doors.instances[side::left].position <= 0.f); + // a step out isn't a closed door + CHECK(consist_closure(scenario) == door_closure_t::open); + + scenario.act("revoke the permit"); + cab.PermitDoors(side::left, false); + scenario.wait(10.0); + CHECK(consist_closure(scenario) == door_closure_t::locked); +} + +TEST_CASE("door closure: the doors of a unit without permits are released at a standstill") { + scenario scenario{"door closure/en57", motion() + doors()}; + scenario.add("en57_v1/5bgaiii_led_spot", 1).add("en57_v1/6ba_840").add("en57_v1/5bgb"); + scenario.start_up(); + auto &cab{scenario.cab()}; + auto &middle{scenario.vehicle(1)}; + REQUIRE_FALSE(cab.Doors.permit_needed); + REQUIRE(middle.Doors.permit_needed); + // the door lock engages only once the train runs (DoorLockSpeed), till then the end cars' doors can be opened + CHECK(cab.door_closure() == door_closure_t::released); + CHECK(middle.door_closure() == door_closure_t::locked); + CHECK(consist_closure(scenario) == door_closure_t::released); + scenario.act("open right"); + cab.OperateDoors(side::right, true); + scenario.wait(10.0); + CHECK(consist_closure(scenario) == door_closure_t::open); + scenario.act("close right"); + cab.OperateDoors(side::right, false); + scenario.wait(10.0); + CHECK(consist_closure(scenario) == door_closure_t::released); +} + +TEST_CASE("door closure: a locomotive has no doors to signal, its coach does") { + scenario scenario{"door closure/eu07 with a coach", motion() + doors()}; + scenario.add("4e_v1/4e", 1).add("bdhpumn_v3/bdhpumn"); + scenario.start_up(); + CHECK(scenario.cab().door_closure() == door_closure_t::none); + scenario.act("open right"); + scenario.cab().PermitDoors(side::right, true); + scenario.cab().OperateDoors(side::right, true); + scenario.wait(10.0); + CHECK(scenario.vehicle(1).door_closure() == door_closure_t::open); + CHECK(consist_closure(scenario) == door_closure_t::open); +} + +TEST_CASE("door closure: the signalling is switched on by DoorClosureSignal in the Doors section, off without it") { + SUBCASE("without the key") { + vehicle wagon{definition::load("wagon")}; + CHECK_FALSE(wagon->Doors.closure_signal); + } + SUBCASE("with the key") { + vehicle wagon{definition::load("wagon").set("Doors", "DoorClosureSignal", "Yes")}; + CHECK(wagon->Doors.closure_signal); + } +} + +TEST_CASE("door closure: without DoorClosureSignal the lamp shows the lock of the vehicle's own doors, as before") { + vehicle wagon{definition::load("wagon")}; + REQUIRE_FALSE(wagon->Doors.closure_signal); + for (auto const locked : {true, false}) { + wagon->Doors.is_locked = locked; + for (auto const consist : {door_closure_t::none, door_closure_t::locked, door_closure_t::released, door_closure_t::open}) { + for (auto const lit : {true, false}) { + CAPTURE(locked); + CAPTURE(static_cast(consist)); + CAPTURE(lit); + // no blinking at a standstill with the doors closed, whatever the consist does + CHECK(wagon->door_closure_lamp_lit(consist, lit) == locked); + } + } + } +} + +TEST_CASE("door closure: with DoorClosureSignal the lamp shows the consist in three states") { + vehicle wagon{definition::load("wagon").set("Doors", "DoorClosureSignal", "Yes")}; + REQUIRE(wagon->Doors.closure_signal); + for (auto const locked : {true, false}) { + // the vehicle's own lock doesn't matter, the consist does + wagon->Doors.is_locked = locked; + for (auto const lit : {true, false}) { + CAPTURE(locked); + CAPTURE(lit); + CHECK(wagon->door_closure_lamp_lit(door_closure_t::locked, lit)); + CHECK(wagon->door_closure_lamp_lit(door_closure_t::released, lit) == lit); + CHECK_FALSE(wagon->door_closure_lamp_lit(door_closure_t::open, lit)); + CHECK_FALSE(wagon->door_closure_lamp_lit(door_closure_t::none, lit)); + } + } +} diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 57ef2cb3..709cb0e4 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -6019,6 +6019,7 @@ TController::determine_consist_state() { IsAnyDoorOnlyOpen[ side::right ] = IsAnyDoorOnlyOpen[ side::left ] = false; IsAnyDoorOpen[ side::right ] = IsAnyDoorOpen[ side::left ] = false; IsAnyDoorPermitActive[ side::right ] = IsAnyDoorPermitActive[ side::left ] = false; + auto doorclosure { door_closure_t::none }; ConsistShade = 0.0; auto *p { pVehicles[ end::front ] }; // pojazd na czole składu double dy; // składowa styczna grawitacji, w przedziale <0,1> @@ -6080,12 +6081,14 @@ TController::determine_consist_state() { IsAnyDoorPermitActive[ side::right ] |= rightdoor.open_permit; IsAnyDoorPermitActive[ side::left ] |= leftdoor.open_permit; } + doorclosure = std::max( doorclosure, vehicle->door_closure() ); } // measure lighting level // TBD: apply weight (multiplier) to partially lit vehicles? ConsistShade += p->fShade > 0.0 ? p->fShade : 1.0; p = p->Next(); // pojazd podłączony z tyłu (patrząc od czoła) } + DoorClosure = doorclosure; // calculate average amount of received sunlight ConsistShade /= iVehicles; diff --git a/vehicle/Driver.h b/vehicle/Driver.h index cf791c68..38b4c9e2 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -637,6 +637,7 @@ private: bool IsAnyDoorOpen[ 2 ]; // state of door in the consist bool IsAnyDoorOnlyOpen[ 2 ]; // state of door in the consist regardless of step bool IsAnyDoorPermitActive[ 2 ]; // state of door permit in the consist + door_closure_t DoorClosure{ door_closure_t::none }; // state of the door closure signalling (SOD) of the consist bool IsAnyLineBreakerOpen{ false }; // state of line breaker in all powered vehicles under control bool IsAnyConverterOverloadRelayOpen{ false }; // state of converter overload relays in all vehicles under control bool IsAnyMotorOverloadRelayOpen{ false }; // state of motor overload relays in all vehicles under control diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp index 5f27f272..21af1a39 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -9116,7 +9116,17 @@ bool TTrain::Update(double const Deltatime) btLampkaDoorLeft.Turn(DynamicObject->Mechanik->IsAnyDoorOpen[cab_to_end() == end::front ? side::left : side::right]); btLampkaDoorRight.Turn(DynamicObject->Mechanik->IsAnyDoorOpen[cab_to_end() == end::front ? side::right : side::left]); } - btLampkaBlokadaDrzwi.Turn(mvOccupied->Doors.is_locked); + if (DynamicObject->Mechanik != nullptr) + { + // with DoorClosureSignal in the definition of the cab's vehicle, the door closure signalling (SOD) of the whole + // consist: lit with all doors closed and locked, blinking with them closed but released, dark with a door open. + // without it, the lock of the vehicle's own doors, as before + btLampkaBlokadaDrzwi.Turn(mvOccupied->door_closure_lamp_lit(DynamicObject->Mechanik->DoorClosure, simulation::Time.data().wSecond % 2 < 1)); + } + else + { + btLampkaBlokadaDrzwi.Turn(mvOccupied->Doors.is_locked); + } btLampkaDoorLockOff.Turn(false == mvOccupied->Doors.lock_enabled); btLampkaDepartureSignal.Turn(mvControlled->DepartureSignal); btLampkaNapNastHam.Turn(mvControlled->DirActive != 0 && mvOccupied->EpFuse); // napiecie na nastawniku hamulcowym