diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index e98e803c..899c6e4f 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -1774,6 +1774,10 @@ class TMoverParameters bool UniCtrlIntegratedBrakeCtrl = false; /*zintegrowany nastawnik JH obsluguje hamowanie*/ bool UniCtrlIntegratedLocalBrakeCtrl = false; /*zintegrowany nastawnik JH obsluguje hamowanie hamulcem pomocniczym*/ int UniCtrlNoPowerPos{0}; // cached highesr position not generating traction force + // UCList CtrlValStep: the universal controller (EIMCtrlType 3) changes its setting in steps of this size (e.g. 0.05 for the + // 5% steps of the 6Dg), one at once when moved to a raising or lowering position, then one each time its SpeedUp / SpeedDown + // covers a step; 0 (the default) changes it continuously + double UniCtrlValStep{0.0}; std::pair> PantsPreset{"0132", {0, 0}}; // pantograph preset switches; .first holds possible setups as chars, .second holds currently selected preset in each cab /*-sekcja parametrow dla lokomotywy elektrycznej*/ @@ -2250,6 +2254,7 @@ class TMoverParameters double eimic = 0; /*aktualna pozycja zintegrowanego sterowania jazda i hamowaniem*/ double eimic_analog = 0; /*pozycja zadajnika analogowa*/ double eimic_real = 0; /*faktycznie uzywana pozycja zintegrowanego sterowania jazda i hamowaniem*/ + double eimic_step_progress{0.0}; // the part of the next UniCtrlValStep the controller has covered so far double eim_localbrake = 0; /*nastawa hamowania dodatkowego pneumatycznego lokomotywy*/ int EIMCtrlType = 0; /*rodzaj wariantu zadajnika jazdy*/ bool SpeedCtrlTypeTime = false; /*czy tempomat sterowany czasowo*/ diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 628ef727..e0940125 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -7409,6 +7409,60 @@ bool TMoverParameters::DropAllPantographs(bool const State, range_t const Notify return State != initialstate; } +namespace +{ +// the setting of the universal controller one UniCtrlValStep further from Value towards Limit, never past it +double next_uc_step(double const Value, double const Limit, double const Step) +{ + constexpr double epsilon{1e-6}; + if (Limit > Value) + { + return std::min((std::floor(Value / Step + epsilon) + 1.0) * Step, Limit); + } + return std::max((std::ceil(Value / Step - epsilon) - 1.0) * Step, Limit); +} + +// the universal controller with UniCtrlValStep: the setting the continuous ramp got to (Ramped) turned into whole steps. +// a move of the controller onto a position which changes the setting makes one step at once, the ramp then one step each +// time it covers a step's length; the steps stop at the position's set value (or its bound, past which the ramp doesn't go) +double stepped_uc_value(double const Current, double const Ramped, TUniversalCtrl const &Position, double const Step, bool const Moved, double &Progress) +{ + if (Moved) + { + Progress = 0.0; + } + double const delta{Ramped - Current}; + if (std::abs(delta) < 1e-9) + { + Progress = 0.0; + return Ramped; + } + double limit{0.0}; + if (delta > 0.0) + { + limit = Position.SetCtrlVal > Current ? Position.SetCtrlVal : Position.MaxCtrlVal; + } + else + { + limit = Position.SetCtrlVal < Current ? Position.SetCtrlVal : Position.MinCtrlVal; + } + Progress = Moved ? Step : Progress + std::abs(delta); + double value{Current}; + bool reached{std::abs(value - limit) < 1e-9}; + while (!reached && Progress >= Step - 1e-9) + { + value = next_uc_step(value, limit, Step); + Progress -= Step; + reached = std::abs(value - limit) < 1e-9; + } + if (reached) + { + Progress = 0.0; + } + return safe_clamp(value, Position.MinCtrlVal, Position.MaxCtrlVal); +} +} // namespace + void TMoverParameters::CheckEIMIC(double dt) { double offset = EIMCtrlAdditionalZeros ? 1.0 : 0.0; @@ -7533,11 +7587,14 @@ void TMoverParameters::CheckEIMIC(double dt) if (MainCtrlActualPos != MainCtrlPos || LastRelayTime > InitialCtrlDelay) { - eimic -= safe_clamp(-UniCtrlList[MainCtrlPos].SetCtrlVal + eimic, 0.0, - MainCtrlActualPos == MainCtrlPos ? dt * UniCtrlList[MainCtrlPos].SpeedDown : sign(UniCtrlList[MainCtrlPos].SpeedDown) * 0.01); // odejmuj do X - eimic += safe_clamp(UniCtrlList[MainCtrlPos].SetCtrlVal - eimic, 0.0, - MainCtrlActualPos == MainCtrlPos ? dt * UniCtrlList[MainCtrlPos].SpeedUp : sign(UniCtrlList[MainCtrlPos].SpeedUp) * 0.01); // dodawaj do X - eimic = safe_clamp(eimic, UniCtrlList[MainCtrlPos].MinCtrlVal, UniCtrlList[MainCtrlPos].MaxCtrlVal); + auto const &position{UniCtrlList[MainCtrlPos]}; + double ramped{eimic}; + ramped -= safe_clamp(-position.SetCtrlVal + ramped, 0.0, + MainCtrlActualPos == MainCtrlPos ? dt * position.SpeedDown : sign(position.SpeedDown) * 0.01); // odejmuj do X + ramped += safe_clamp(position.SetCtrlVal - ramped, 0.0, + MainCtrlActualPos == MainCtrlPos ? dt * position.SpeedUp : sign(position.SpeedUp) * 0.01); // dodawaj do X + ramped = safe_clamp(ramped, position.MinCtrlVal, position.MaxCtrlVal); + eimic = UniCtrlValStep > 0.0 ? stepped_uc_value(eimic, ramped, position, UniCtrlValStep, MainCtrlActualPos != MainCtrlPos, eimic_step_progress) : ramped; } if (MainCtrlActualPos == MainCtrlPos) LastRelayTime += dt; @@ -11765,6 +11822,8 @@ void TMoverParameters::LoadFIZ_UCList(std::string const &Input) extract_value(UniCtrlIntegratedBrakeCtrl, "IntegratedBrake", Input, ""); extract_value(UniCtrlIntegratedLocalBrakeCtrl, "IntegratedLocBrake", Input, ""); extract_value(UniCtrlIntegratedBrakePNCtrl, "IntegratedBrakePN", Input, ""); + extract_value(UniCtrlValStep, "CtrlValStep", Input, ""); + UniCtrlValStep = std::max(UniCtrlValStep, 0.0); } void TMoverParameters::LoadFIZ_DList(std::string const &Input) diff --git a/tests/unit/test_uc_steps.cpp b/tests/unit/test_uc_steps.cpp new file mode 100644 index 00000000..eb1b26fa --- /dev/null +++ b/tests/unit/test_uc_steps.cpp @@ -0,0 +1,115 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" +#include "drive.h" + +#include + +// the universal controller (EIMCtrlType 3) setting its power demand in steps (UCList CtrlValStep), as the 6Dg does +// in steps of 5% (eu07/maszyna#412). the 214M railbus's controller: 3 lowers the power to 0, 4 holds it, 5 raises it +// to 1, at 0.15 a second after IniCDelay (1 s) + +using test::definition; +using test::vehicle; + +namespace { + +constexpr int lower{3}; +constexpr int hold{4}; +constexpr int raise{5}; + +// the railbus started up, its brakes released and its direction set, its controller holding no power +void get_ready(vehicle &Railbus, test::track const &Track) { + auto driven{test::driving(Railbus)}; + REQUIRE(test::start_up(driven, Track)); + test::release_brakes(driven.cab); + for (int i = 0; i < 3 && Railbus->DirActive <= 0; ++i) { + Railbus->DirectionForward(); + } + while (Railbus->MainCtrlPos < hold && Railbus->IncMainCtrl(1)) { + ; + } + while (Railbus->MainCtrlPos > hold && Railbus->DecMainCtrl(1)) { + ; + } + REQUIRE(Railbus->MainCtrlPos == hold); + Railbus.run(2.0, Track); + REQUIRE(Railbus->eimic == doctest::Approx(0.0)); +} + +void move_to(vehicle &Railbus, int const Position) { + while (Railbus->MainCtrlPos < Position && Railbus->IncMainCtrl(1)) { + ; + } + while (Railbus->MainCtrlPos > Position && Railbus->DecMainCtrl(1)) { + ; + } + REQUIRE(Railbus->MainCtrlPos == Position); +} + +bool on_step(double const Value, double const Step) { + return std::abs(Value / Step - std::round(Value / Step)) < 1e-6; +} + +} // namespace + +TEST_CASE("controls: the universal controller with CtrlValStep sets its power in whole steps") { + test::track const line{0.15, 0.0}; + vehicle railbus{definition::fiz("21xm_v1/214ma").set("UCList", "CtrlValStep", 0.05), 0.0, "test", 1}; + REQUIRE(railbus->UniCtrlValStep == doctest::Approx(0.05)); + get_ready(railbus, line); + + // a tap on the raising position: one step at once, kept by the holding one + move_to(railbus, raise); + railbus.step(line); + CHECK(railbus->eimic == doctest::Approx(0.05)); + move_to(railbus, hold); + railbus.run(2.0, line); + CHECK(railbus->eimic == doctest::Approx(0.05)); + + // held there: after the initial delay a step each third of a second (0.15 a second), never in between + move_to(railbus, raise); + for (int i = 0; i < 310; ++i) { // 3.1 s + railbus.step(line); + CAPTURE(i); + CHECK(on_step(railbus->eimic, 0.05)); + } + // the first step at once, 1 s of the initial delay, then 2.1 s of 0.15 a second (6 steps) + CHECK(railbus->eimic == doctest::Approx(0.40)); + move_to(railbus, hold); + railbus.run(2.0, line); + CHECK(railbus->eimic == doctest::Approx(0.40)); + + // up to the full power, and not past it + move_to(railbus, raise); + railbus.run(10.0, line); + CHECK(railbus->eimic == doctest::Approx(1.0)); + + // down the same way: a tap is one step, holding it goes on in steps down to nothing + move_to(railbus, lower); + railbus.step(line); + CHECK(railbus->eimic == doctest::Approx(0.95)); + move_to(railbus, hold); + railbus.run(1.0, line); + CHECK(railbus->eimic == doctest::Approx(0.95)); + move_to(railbus, lower); + for (int i = 0; i < 800; ++i) { // 8 s + railbus.step(line); + CAPTURE(i); + CHECK(on_step(railbus->eimic, 0.05)); + } + CHECK(railbus->eimic == doctest::Approx(0.0)); +} + +TEST_CASE("controls: the universal controller without CtrlValStep sets its power continuously, as before") { + test::track const line{0.15, 0.0}; + vehicle railbus{definition::fiz("21xm_v1/214ma"), 0.0, "test", 1}; + REQUIRE(railbus->UniCtrlValStep == 0.0); + get_ready(railbus, line); + // a tap moves it by 0.01, held it goes on at 0.15 a second after the initial delay + move_to(railbus, raise); + railbus.step(line); + CHECK(railbus->eimic == doctest::Approx(0.01)); + railbus.run(2.0 - 0.01, line); + CHECK(railbus->eimic == doctest::Approx(0.01 + 0.15).epsilon(0.05)); +}