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