17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-05 22:59:10 +01:00

First implementation of graph vehicles

This commit is contained in:
2026-10-14 12:20:49 +02:00
parent 70abf879b3
commit 1ef465264c
121 changed files with 12292 additions and 729 deletions

View File

@@ -43,6 +43,18 @@ std::vector<std::string> split_labels(std::string const &Text)
} // namespace
int cab_of(block_setup const &Setup)
{
auto const index{find_param(Setup.param_defs, "cab")};
if (index < 0)
{
return 0;
}
// "any", "1", "2", "0" (the engine room)
auto const &value{Setup.params[index].text};
return value == "1" ? 1 : value == "2" ? 2 : value == "0" ? -1 : 0;
}
// cab_switch
cab_switch::cab_switch(block_setup const &Setup)
@@ -50,7 +62,10 @@ cab_switch::cab_switch(block_setup const &Setup)
m_positioncount(std::max(1, static_cast<int>(std::lround(number(Setup, param::positions))))), m_min(number(Setup, param::min)), m_max(number(Setup, param::max)),
m_spring(boolean(Setup, param::spring)), m_labels(split_labels(text(Setup, param::labels))), m_animation(static_cast<animation_type>(choice(Setup, param::animation))),
m_step(number(Setup, param::step)), m_offset(number(Setup, param::offset)), m_smoothing(number(Setup, param::smoothing)), m_submodel(text(Setup, param::submodel)),
m_keyincrease(text(Setup, param::key_inc)), m_keydecrease(text(Setup, param::key_dec)), m_command(text(Setup, param::command)), m_hardwarename(text(Setup, param::hw_name))
m_soundincrease(text(Setup, param::sound_inc)), m_sounddecrease(text(Setup, param::sound_dec)),
m_soundpositions(text(Setup, param::sound_positions)),
m_keyincrease(text(Setup, param::key_inc)), m_keydecrease(text(Setup, param::key_dec)), m_command(text(Setup, param::command)), m_hardwarename(text(Setup, param::hw_name)),
m_cab(cab_of(Setup))
{
// the default is given as a value, as the author sees it; the switch starts at the position closest to it
auto const defaultvalue{number(Setup, param::default_value)};
@@ -59,6 +74,7 @@ cab_switch::cab_switch(block_setup const &Setup)
m_defaultposition = clamp_position(static_cast<int>(std::lround((defaultvalue - m_min) / (m_max - m_min) * (m_positioncount - 1))));
}
m_position = m_defaultposition;
m_display = static_cast<float>(m_position);
}
int cab_switch::clamp_position(int const Position) const
@@ -84,6 +100,7 @@ void cab_switch::update(frame &Frame)
{
set(Frame, port::value, value_at(m_position));
set(Frame, port::index, m_position);
m_display = static_cast<float>(m_position);
}
void cab_switch::increase()
@@ -181,10 +198,14 @@ block_type cab_switch::describe()
number_param("offset", 0.0, "deg/mm"),
number_param("smoothing", 0.2, "s"),
text_param("submodel"),
text_param("sound_inc"),
text_param("sound_dec"),
text_param("sound_positions"),
text_param("key_inc"),
text_param("key_dec"),
text_param("command"),
text_param("hw_name"),
cab_param(),
};
type.create = make_block<cab_switch>;
return type;
@@ -194,7 +215,8 @@ block_type cab_switch::describe()
cab_lamp::cab_lamp(block_setup const &Setup)
: block(Setup), m_supplied(is_connected(Setup, port::supply)), m_blink(boolean(Setup, param::blink)), m_blinkperiod(std::max(0.01, number(Setup, param::blink_period))),
m_minvoltage(number(Setup, param::min_voltage)), m_power(std::max(0.0, number(Setup, param::power))), m_submodel(text(Setup, param::submodel))
m_minvoltage(number(Setup, param::min_voltage)), m_power(std::max(0.0, number(Setup, param::power))), m_submodel(text(Setup, param::submodel)),
m_soundon(text(Setup, param::sound_on)), m_soundoff(text(Setup, param::sound_off)), m_cab(cab_of(Setup))
{
}
@@ -233,6 +255,9 @@ block_type cab_lamp::describe()
number_param("min_voltage", 70.0, "V"),
number_param("power", 2.0, "W"),
text_param("submodel"),
text_param("sound_on"),
text_param("sound_off"),
cab_param(),
};
type.create = make_block<cab_lamp>;
return type;
@@ -243,7 +268,7 @@ block_type cab_lamp::describe()
cab_gauge::cab_gauge(block_setup const &Setup, bool const Twoneedles)
: block(Setup), m_twoneedles(Twoneedles), m_min(number(Setup, param::min)), m_max(number(Setup, param::max)), m_anglemin(number(Setup, param::angle_min)),
m_anglemax(number(Setup, param::angle_max)), m_smoothing(number(Setup, param::smoothing)), m_submodel(text(Setup, param::submodel)),
m_submodel2(Twoneedles ? text(Setup, param::submodel2) : std::string{})
m_submodel2(Twoneedles ? text(Setup, param::submodel2) : std::string{}), m_cab(cab_of(Setup))
{
m_values[0] = m_values[1] = static_cast<float>(std::min(m_min, m_max));
}
@@ -292,6 +317,7 @@ block_type cab_gauge::describe(bool const Twoneedles)
{
type.params.push_back(text_param("submodel2"));
}
type.params.push_back(cab_param());
if (Twoneedles)
{
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<cab_gauge>(Setup, true); };
@@ -308,7 +334,7 @@ block_type cab_gauge::describe(bool const Twoneedles)
vehicle_sound::vehicle_sound(block_setup const &Setup)
: block(Setup), m_file(text(Setup, param::file)), m_basevolume(number(Setup, param::volume)), m_basepitch(number(Setup, param::pitch)), m_loop(boolean(Setup, param::loop)),
m_placement(static_cast<placement_type>(choice(Setup, param::placement))), m_range(number(Setup, param::range)),
m_offset{number(Setup, param::offset_x), number(Setup, param::offset_y), number(Setup, param::offset_z)}
m_offset{number(Setup, param::offset_x), number(Setup, param::offset_y), number(Setup, param::offset_z)}, m_cab(cab_of(Setup))
{
}
@@ -364,11 +390,35 @@ block_type vehicle_sound::describe()
number_param("offset_x", 0.0, "m"),
number_param("offset_y", 0.0, "m"),
number_param("offset_z", 0.0, "m"),
cab_param(),
};
type.create = make_block<vehicle_sound>;
return type;
}
// cab_export
cab_export::cab_export(block_setup const &Setup) : block(Setup), m_name(text(Setup, 0)), m_kind(static_cast<kind_type>(choice(Setup, 1))) {}
void cab_export::update(frame &Frame)
{
m_value = get(Frame, port::value);
}
block_type cab_export::describe()
{
block_type type;
type.id = "fb.export";
type.category = "indicators";
type.name = "Screen value";
type.description = "Publishes the value for the python screens of the cab, as an entry of the train state with the given name.";
type.origin = block_origin::cab;
type.ports = {signal_in("value", value_type::real)};
type.params = {text_param("name"), choice_param("kind", {"float", "int", "bool"}, "float")};
type.create = make_block<cab_export>;
return type;
}
void register_cab_blocks(catalog &Catalog)
{
Catalog.add(cab_switch::describe());
@@ -376,6 +426,7 @@ void register_cab_blocks(catalog &Catalog)
Catalog.add(cab_gauge::describe(false));
Catalog.add(cab_gauge::describe(true));
Catalog.add(vehicle_sound::describe());
Catalog.add(cab_export::describe());
}
} // namespace vgraph

View File

@@ -48,10 +48,14 @@ public:
offset,
smoothing,
submodel,
sound_inc,
sound_dec,
sound_positions,
key_inc,
key_dec,
command,
hw_name
hw_name,
cab
};
};
enum class behaviour_type
@@ -136,6 +140,21 @@ public:
{
return m_submodel;
}
// sounds played by the animated submodel when the position increases or decreases, in the format of
// soundinc: and sounddec: of the .mmd controls
std::string const &sound_increase() const
{
return m_soundincrease;
}
std::string const &sound_decrease() const
{
return m_sounddecrease;
}
// sounds of single positions, as soundN: entries of the .mmd controls with N the number of the position
std::string const &sound_positions() const
{
return m_soundpositions;
}
// bindings: keys, existing simulator command, name for the hardware desk
std::string const &key_increase() const
{
@@ -153,6 +172,16 @@ public:
{
return m_hardwarename;
}
// cab whose model holds the submodel: 0 any, 1 or 2
int cab() const
{
return m_cab;
}
// stable address of the value animating the submodel (the position), for TGauge::AssignFloat
float *display_state()
{
return &m_display;
}
static block_type describe();
@@ -171,11 +200,16 @@ private:
double const m_offset;
double const m_smoothing;
std::string const m_submodel;
std::string const m_soundincrease;
std::string const m_sounddecrease;
std::string const m_soundpositions;
std::string const m_keyincrease;
std::string const m_keydecrease;
std::string const m_command;
std::string const m_hardwarename;
int const m_cab;
int m_position{0};
float m_display{0.f};
};
// indicator lamp. when the graph simulates low voltage circuits and the supply input is connected, the lamp lights
@@ -200,7 +234,10 @@ public:
blink_period,
min_voltage,
power,
submodel
submodel,
sound_on,
sound_off,
cab
};
};
@@ -229,6 +266,19 @@ public:
{
return m_submodel;
}
// sounds played when the lamp lights and goes out, as soundinc: and sounddec: of the .mmd lamps
std::string const &sound_on() const
{
return m_soundon;
}
std::string const &sound_off() const
{
return m_soundoff;
}
int cab() const
{
return m_cab;
}
static block_type describe();
@@ -239,9 +289,15 @@ private:
double const m_minvoltage;
double const m_power;
std::string const m_submodel;
std::string const m_soundon;
std::string const m_soundoff;
int const m_cab;
bool m_lit{false};
};
// cab given by the "cab" parameter of a cab block: 0 any, 1 or 2, -1 the engine room
int cab_of(block_setup const &Setup);
// needle gauge with one or two needles: voltmeter, ammeter, speedometer, pressure gauge
class cab_gauge final : public block
{
@@ -318,6 +374,10 @@ public:
{
return Needle == 0 ? m_submodel : m_submodel2;
}
int cab() const
{
return m_cab;
}
static block_type describe(bool Twoneedles);
@@ -330,6 +390,7 @@ private:
double const m_smoothing;
std::string const m_submodel;
std::string const m_submodel2;
int const m_cab;
float m_values[2]{0.f, 0.f};
};
@@ -358,7 +419,8 @@ public:
range,
offset_x,
offset_y,
offset_z
offset_z,
cab
};
};
// the same placements as sound_placement of the audio subsystem
@@ -422,6 +484,11 @@ public:
{
return m_offset[2];
}
// for the sounds placed in the cab: the cab they belong to, 0 any
int cab() const
{
return m_cab;
}
static block_type describe();
@@ -433,9 +500,53 @@ private:
placement_type const m_placement;
double const m_range;
double const m_offset[3];
int const m_cab;
bool m_playing{false};
float m_volume{0.f};
float m_pitch{1.f};
};
// value published for the python screens of the cab (TTrain::GetTrainState), under the specified name
class cab_export final : public block
{
public:
struct port
{
enum : std::size_t
{
value
};
};
enum class kind_type
{
real,
integer,
boolean
};
explicit cab_export(block_setup const &Setup);
void update(frame &Frame) override;
std::string const &name() const
{
return m_name;
}
kind_type kind() const
{
return m_kind;
}
double value() const
{
return m_value;
}
static block_type describe();
private:
std::string const m_name;
kind_type const m_kind;
double m_value{0.0};
};
} // namespace vgraph

View File

@@ -0,0 +1,920 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <cmath>
// combustion engine drive: engine -> clutch and gearbox / hydraulic transmission / main generator.
//
// the mechanical connections keep the convention of the electric drives: torque forward, rotational speed back. the
// engine is a torque source like the electric motors: it reads the speed of its shaft from the back channel and gives the
// torque. the speed of the engine shaft is a state of the block connected to the engine (drive.gearbox, drive.hydro or
// drive.generator), the same way the speed of the wheels is a state of the vehicle body behind drive.axle: that block
// holds the inertia of the engine side (crankshaft, flywheel, generator rotor), integrates the engine speed with the engine
// torque and the torque of its coupling (slipping clutch, torque converter, generator load), and returns it to the engine.
// when the coupling is locked (clutch engaged without slip, lockup clutch of the converter) the engine follows the speed
// of the wheels, as in TMoverParameters::dizel_Momentum, so the chain is rigid and stays stable at any step.
// the engine shaft has to be connected to exactly one such block; its speed is in rad/s, positive, in the frame of the
// engine. the direction of the vehicle is set after the coupling (dir inputs of the gearbox and the hydraulic transmission)
namespace vgraph
{
namespace
{
double constexpr two_pi{2.0 * M_PI};
double constexpr rpm_to_rad{two_pi / 60.0};
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description, block_origin Origin)
{
block_type type;
type.id = std::move(Id);
type.category = "drive";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = Origin;
return type;
}
// direction of the drive in the frame of the vehicle: -1, 0 or 1 (as for the electric motors)
int absolute_direction(frame const &Frame, double const Dir)
{
auto const dir{Dir > 0.5 ? 1 : Dir < -0.5 ? -1 : 0};
auto const cab{Frame.body->cab};
return dir * (cab != 0 ? cab : 1);
}
double sign(double const Value)
{
return Value > 0.0 ? 1.0 : Value < 0.0 ? -1.0 : 0.0;
}
// clamp which accepts the bounds in any order (safe_clamp of the Mover)
double clamp_any(double const Value, double const Bound1, double const Bound2)
{
return std::clamp(Value, std::min(Bound1, Bound2), std::max(Bound1, Bound2));
}
// linear interpolation in the rows of a table (first column the argument, sorted); the end rows outside of it
double interpolate(std::vector<std::vector<double>> const &Rows, std::size_t const Column, double const Argument)
{
if (Rows.empty())
{
return 0.0;
}
if (Argument <= Rows.front()[0])
{
return Rows.front()[Column];
}
for (std::size_t idx = 1; idx < Rows.size(); ++idx)
{
auto const &upper{Rows[idx]};
if (Argument <= upper[0])
{
auto const &lower{Rows[idx - 1]};
auto const span{upper[0] - lower[0]};
return span > 0.0 ? lower[Column] + (upper[Column] - lower[Column]) * (Argument - lower[0]) / span : upper[Column];
}
}
return Rows.back()[Column];
}
// rows of a table parameter with at least the specified number of columns, sorted by the first one
std::vector<std::vector<double>> sorted_rows(std::vector<std::vector<double>> const &Rows, std::size_t const Columns)
{
std::vector<std::vector<double>> result;
for (auto const &row : Rows)
{
if (row.size() >= Columns)
{
result.push_back(row);
}
}
std::stable_sort(std::begin(result), std::end(result), [](auto const &Left, auto const &Right) { return Left[0] < Right[0]; });
return result;
}
// combustion engine with a speed governor and a starter (adapted from TMoverParameters::dizel_Momentum, dizel_fillcheck
// and dizel_Update)
class drive_diesel final : public block
{
public:
struct port
{
enum : std::size_t
{
start,
stop,
fuel,
demand,
shaft,
running,
starting,
rpm,
torque,
fill,
power,
fuel_rate
};
};
struct param
{
enum : std::size_t
{
idle_rpm,
max_rpm,
governor,
droop,
fill_rate,
curve,
curve_type,
m_max,
rpm_m_max,
m_nmax,
friction,
starter_torque,
start_time,
fuel_consumption,
main_engine
};
};
explicit drive_diesel(block_setup const &Setup)
: block(Setup), m_idle(std::max(1.0, number(Setup, param::idle_rpm))), m_max(std::max(m_idle + 1.0, number(Setup, param::max_rpm))),
m_allspeed(choice(Setup, param::governor) == 0), m_band(std::max(1.0, std::max(0.001, number(Setup, param::droop)) * m_max)),
m_fillrate(std::max(0.0, number(Setup, param::fill_rate))), m_curve(sorted_rows(table(Setup, param::curve), 2)), m_curvepower(choice(Setup, param::curve_type) == 1),
m_mmax(number(Setup, param::m_max)), m_nmmax(number(Setup, param::rpm_m_max)), m_mnmax(number(Setup, param::m_nmax)), m_friction(std::max(0.0, number(Setup, param::friction))),
m_startertorque(std::max(0.0, number(Setup, param::starter_torque))), m_starttime(std::max(0.0, number(Setup, param::start_time))),
m_mainengine(boolean(Setup, param::main_engine))
{
if (m_curve.size() < 2)
{
m_curve.clear();
}
if (m_nmmax == m_max)
{
// as in LoadFIZ_DList: guards the division in the torque parabola
m_nmmax = m_max - 1.0;
}
// fuel used per hour at the full power, as dizel_FuelConsumption of LoadFIZ_DList: g/kWh * kW / (850 g/l)
auto const ratedpower{std::max(0.0, full_torque(m_max) - m_friction) * m_max * rpm_to_rad / 1000.0};
m_fuelperhour = std::max(0.0, number(Setup, param::fuel_consumption)) * ratedpower / 850.0;
}
void update(frame &Frame) override
{
auto const omega{back_out(Frame, port::shaft)};
auto const rpm{omega / rpm_to_rad};
auto const start{get_bool(Frame, port::start)};
auto const stop{get_bool(Frame, port::stop)};
auto const fuel{get_bool(Frame, port::fuel)};
auto const startpressed{start && false == m_startprevious};
m_startprevious = start;
// start procedure (dizel_Update): the starter cranks the engine for start_time, then the ignition; the starter
// keeps helping until the engine reaches its idle speed (dizel_spinup)
if (false == m_running && false == m_cranking && startpressed && fuel && false == stop)
{
m_cranking = true;
m_cranktime = 0.0;
}
if (m_cranking)
{
if (stop || false == fuel)
{
m_cranking = false;
}
else
{
m_cranktime += Frame.dt;
if (m_cranktime >= m_starttime)
{
m_cranking = false;
m_running = true;
m_spinup = true;
}
}
}
if (m_running && (stop || false == fuel))
{
// knocked out without fuel, as dizel_Update does when the fuel pump stops
m_running = false;
}
m_spinup = m_spinup && m_running && rpm < 0.95 * m_idle;
if (m_running && false == m_spinup && rpm < 0.25 * m_idle)
{
// stalled, e.g. by engaging the clutch at standstill
m_running = false;
}
// fuel injection set by the governor (dizel_fillcheck), reached at fill_rate (dizel_Update: fillspeed)
auto target{0.0};
if (m_running)
{
auto const demand{std::clamp(get(Frame, port::demand), 0.0, 1.0)};
if (m_allspeed)
{
// all speed governor: the demand sets the speed between the idle and the maximum one
auto const setspeed{m_idle + demand * (m_max - m_idle)};
target = std::clamp((setspeed - rpm) / m_band, 0.0, 1.0);
}
else
{
// the demand is the injection; the governor keeps the idle speed and cuts the fuel at the maximum one
target = std::max(demand, std::clamp((m_idle - rpm) / m_band, 0.0, 1.0));
target = std::min(target, std::clamp((m_max - rpm) / m_band, 0.0, 1.0));
}
}
m_fill += (target - m_fill) * std::min(1.0, m_fillrate * Frame.dt);
// torque (dizel_Momentum): the curve at full injection times the injection, less the motion resistance of
// the engine (dizel_Mstand), which acts against the rotation and fades out at standstill
m_combustion = rpm > 0.0 ? full_torque(rpm) * m_fill : 0.0;
m_resistance = m_friction * std::clamp(rpm / (0.1 * m_idle), -1.0, 1.0);
auto starter{0.0};
if (m_cranking || m_spinup)
{
// starter, falling with the speed as in dizel_Momentum
starter = m_startertorque * 0.3 / (0.3 + std::max(0.0, rpm / m_idle));
}
m_torque = m_combustion - m_resistance + starter;
m_omega = omega;
m_netpower = (m_combustion - m_resistance) * omega / 1000.0;
m_fuelrate = m_running ? m_fuelperhour * std::max(0.0, rpm / m_max) * m_fill : 0.0;
m_fuelused += m_fuelrate * Frame.dt / 3600.0;
set_forward(Frame, port::shaft, m_torque);
set_bool(Frame, port::running, m_running);
set_bool(Frame, port::starting, m_cranking || m_spinup);
set(Frame, port::rpm, rpm);
set(Frame, port::torque, m_torque);
set(Frame, port::fill, m_fill);
set(Frame, port::power, m_netpower);
set(Frame, port::fuel_rate, m_fuelrate);
}
// the fields of the vehicle body are written after all blocks were updated, so the engine speed replaces the
// revolutions written by the traction motors of a diesel-electric vehicle (for the diesel vehicles enrot is the
// engine speed: engine sounds, cab indicators)
void late_update(frame &Frame) override
{
auto &body{vehicle(Frame)};
// for the diesel vehicles Mains is the running engine; with more engines (or line breakers) any of them
body.Mains = Frame.body->mains.any(Frame.step, m_running);
if (false == m_mainengine)
{
return;
}
body.enrot = m_omega / two_pi;
body.dizel_fill = m_fill;
body.Mm = m_combustion - m_resistance;
body.dizel_Torque = m_torque;
body.dizel_Power = m_netpower;
// as in TractionForce: (dizel_Mstand + Mm) * enrot * 2pi / 1000
body.EnginePower = m_combustion * m_omega / 1000.0;
body.dizel_startup = m_cranking;
body.dizel_spinup = m_spinup;
body.dizel_FuelConsumptionActual = m_fuelrate;
body.dizel_FuelConsumptedTotal = m_fuelused;
// speeds the sounds and indicators scale with (EngineIdleRPM, EngineMaxRPM, EngineRPMRatio), rev/s
body.dizel_nmin = m_idle / 60.0;
body.dizel_nmax = m_max / 60.0;
body.nmax = m_max / 60.0;
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "fuel_used";
}
double state_value(std::size_t) const override
{
return m_fuelused;
}
static block_type describe()
{
auto type{make_type("drive.diesel", "Diesel engine",
"Combustion engine with a speed governor and a starter (dizel_Momentum). start begins the start procedure: the starter cranks the engine for "
"start_time, then the fuel is injected. demand 0..1 sets the speed (all_speed governor) or the injection (min_max governor, idle and maximum "
"speed kept). The torque is the curve at full injection (table rpm -> Nm or kW, or the Mover parabola m_max, rpm_m_max, m_nmax) times the "
"injection, less the friction. The shaft has to go to one drive.gearbox, drive.hydro or drive.generator, which holds the engine speed.",
block_origin::extracted)};
type.ports = {signal_in("start", value_type::boolean),
signal_in("stop", value_type::boolean, 0.0, "", true),
signal_in("fuel", value_type::boolean, 1.0, "", true),
signal_in("demand", value_type::real),
physical_out("shaft", domain::mechanical),
signal_out("running", value_type::boolean),
signal_out("starting", value_type::boolean),
signal_out("rpm", value_type::real, "rpm"),
signal_out("torque", value_type::real, "Nm"),
signal_out("fill", value_type::real),
signal_out("power", value_type::real, "kW"),
signal_out("fuel_rate", value_type::real, "l/h")};
type.params = {number_param("idle_rpm", 600.0, "rpm"),
number_param("max_rpm", 1500.0, "rpm"),
choice_param("governor", {"all_speed", "min_max"}, "all_speed"),
number_param("droop", 0.05),
number_param("fill_rate", 10.0, "1/s"),
table_param("curve", {"rpm", "torque"}),
choice_param("curve_type", {"torque", "power"}, "torque"),
number_param("m_max", 3000.0, "Nm"),
number_param("rpm_m_max", 1100.0, "rpm"),
number_param("m_nmax", 2600.0, "Nm"),
number_param("friction", 300.0, "Nm"),
number_param("starter_torque", 600.0, "Nm"),
number_param("start_time", 2.0, "s"),
number_param("fuel_consumption", 220.0, "g/kWh"),
boolean_param("main_engine", true)};
type.late = true;
type.create = make_block<drive_diesel>;
return type;
}
private:
// torque at full injection, Nm
double full_torque(double const Rpm) const
{
if (false == m_curve.empty())
{
auto const value{interpolate(m_curve, 1, Rpm)};
// power curve in kW: torque = P / omega, the speed held at the first row to keep it finite
return m_curvepower ? value * 1000.0 / (std::max(Rpm, std::max(1.0, m_curve.front()[0])) * rpm_to_rad) : value;
}
// parabola of dizel_Momentum: Mmax at nMmax, Mnmax at nmax
auto const x{(Rpm - m_nmmax) / (m_nmmax - m_max)};
return std::max(0.0, m_mmax - (m_mmax - m_mnmax) * x * x);
}
double const m_idle; // rpm
double const m_max; // rpm
bool const m_allspeed;
double const m_band; // rpm
double const m_fillrate;
std::vector<std::vector<double>> m_curve;
bool const m_curvepower;
double const m_mmax;
double m_nmmax;
double const m_mnmax;
double const m_friction;
double const m_startertorque;
double const m_starttime;
bool const m_mainengine;
double m_fuelperhour{0.0}; // l/h at full power and maximum speed
bool m_startprevious{false};
bool m_cranking{false};
bool m_running{false};
bool m_spinup{false};
double m_cranktime{0.0};
double m_fill{0.0};
double m_combustion{0.0};
double m_resistance{0.0};
double m_torque{0.0};
double m_omega{0.0};
double m_netpower{0.0};
double m_fuelrate{0.0};
double m_fuelused{0.0};
};
// friction clutch and mechanical gearbox (adapted from TMoverParameters::dizel_Momentum without the converter,
// dizel_EngageChange and dizel_AutoGearCheck). holds the speed of the engine: slipping, the clutch passes its friction
// torque and the engine speed follows its own torque; synchronous, the engine follows the gearbox input and its whole
// torque passes on
class drive_gearbox final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
gear,
clutch,
dir,
out,
actual,
engage,
slip,
shifting
};
};
struct param
{
enum : std::size_t
{
gears,
clutch_torque,
engage_rate,
release_rate,
auto_clutch,
inertia,
eff
};
};
explicit drive_gearbox(block_setup const &Setup)
: block(Setup), m_clutchtorque(std::max(0.0, number(Setup, param::clutch_torque))), m_engagerate(std::max(0.0, number(Setup, param::engage_rate))),
m_releaserate(std::max(0.0, number(Setup, param::release_rate))), m_autoclutch(boolean(Setup, param::auto_clutch)),
m_inertia(std::max(0.01, number(Setup, param::inertia))), m_efficiency(std::clamp(number(Setup, param::eff), 0.01, 1.0))
{
for (auto const &row : table(Setup, param::gears))
{
if (row.size() >= 2)
{
m_gears.emplace_back(static_cast<int>(std::lround(row[0])), row[1]);
}
}
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const moment{forward_in(Frame, port::in)};
auto const requestedgear{static_cast<int>(std::lround(get(Frame, port::gear)))};
auto const requesteddir{absolute_direction(Frame, get(Frame, port::dir))};
// a change of the gear or the direction waits for the released clutch; with auto_clutch the gearbox releases it
m_shifting = (requestedgear != m_gear || requesteddir != m_dir);
auto const demand{std::clamp(get(Frame, port::clutch), 0.0, 1.0)};
m_engagestate = (m_shifting && m_autoclutch) ? 0.0 : demand;
// dizel_EngageChange: the pressure approaches the set one, and jumps to it when close
if (std::abs(m_engage - m_engagestate) < 0.11)
{
m_engage = m_engagestate;
}
else
{
m_engage += (m_engage > m_engagestate ? m_releaserate : m_engagerate) * Frame.dt * (m_engagestate - m_engage);
}
if (m_shifting && m_engage < 0.1)
{
m_gear = requestedgear;
m_dir = requesteddir;
m_shifting = false;
}
// ratio from the engine to the output shaft, in the frame of the vehicle; 0 in neutral
auto ratio{0.0};
for (auto const &gear : m_gears)
{
if (gear.first == m_gear && m_gear != 0)
{
ratio = gear.second * m_dir;
break;
}
}
auto gearmoment{0.0};
m_deltaomega = 0.0;
m_slipping = false;
if (ratio == 0.0)
{
// neutral: the input shaft turns with the engine, which runs free
auto const newomega{m_omega + moment / m_inertia * Frame.dt};
m_omega = (newomega < 0.0 && m_omega >= 0.0) ? 0.0 : newomega;
m_previous = m_omega;
}
else
{
// dizel_Momentum, in rad/s and kg m2 (the .fiz AIM is the inertia times 2pi)
auto const n{back_out(Frame, port::out) * ratio}; // speed of the gearbox input
auto const torquec{m_clutchtorque * m_engage};
if (std::abs(moment) > torquec || std::abs(m_previous - m_omega) > 0.1 * two_pi)
{
// slipping, because of the speed difference or the torque above the friction of the clutch
m_slipping = true;
m_deltaomega = m_omega - m_previous;
gearmoment = torquec * sign(m_deltaomega);
auto const eps{(moment - gearmoment) / m_inertia};
auto newomega{m_omega + eps * Frame.dt};
if ((newomega - n) * (m_omega - m_previous) < 0.0 && torquec > 0.1)
{
// the slip changed its sign: the clutch grips
newomega = n;
}
if (newomega * m_omega <= 0.0 && eps * m_omega < 0.0)
{
// no passing through zero speed
newomega = 0.0;
}
m_omega = newomega;
}
else
{
// synchronous: the engine follows the gearbox, within what the clutch can hold
gearmoment = moment;
m_omega = clamp_any(n, m_omega + (moment - torquec) / m_inertia * Frame.dt, m_omega + (moment + torquec) / m_inertia * Frame.dt);
}
m_previous = n;
}
set_forward(Frame, port::out, gearmoment * ratio * m_efficiency);
add_back(Frame, port::in, m_omega);
set(Frame, port::actual, m_gear);
set(Frame, port::engage, m_engage);
set_bool(Frame, port::slip, m_slipping && m_engage > 0.1 && std::abs(m_deltaomega) > 0.2 * two_pi);
set_bool(Frame, port::shifting, m_shifting);
// clutch state for the sounds and the cab indicators (rsEngageSlippery, clutch gauges)
body.dizel_engage = m_engage;
body.dizel_engagestate = m_engagestate;
body.dizel_engagedeltaomega = m_deltaomega / two_pi;
body.dizel_automaticgearstatus = m_shifting ? sign(requestedgear - m_gear) : 0.0;
}
static block_type describe()
{
auto type{make_type("drive.gearbox", "Gearbox with clutch",
"Friction clutch and mechanical gearbox (dizel_Momentum, dizel_EngageChange). gear selects the row of the gears table (gear number, ratio; "
"0 or a missing number is neutral, reverse gears can have negative ratios), dir reverses. A change of the gear or the direction waits until "
"the clutch is released; with auto_clutch the gearbox releases it itself. Holds the speed of the engine (inertia of the engine side).",
block_origin::extracted)};
type.ports = {physical_in("in", domain::mechanical),
signal_in("gear", value_type::integer, 1.0, "", true),
signal_in("clutch", value_type::real, 1.0, "", true),
signal_in("dir", value_type::real, 1.0, "", true),
physical_out("out", domain::mechanical),
signal_out("actual", value_type::integer),
signal_out("engage", value_type::real),
signal_out("slip", value_type::boolean),
signal_out("shifting", value_type::boolean)};
type.params = {table_param("gears", {"gear", "ratio"}),
number_param("clutch_torque", 1500.0, "Nm"),
number_param("engage_rate", 0.5, "1/s"),
number_param("release_rate", 0.9, "1/s"),
boolean_param("auto_clutch", true),
number_param("inertia", 10.0, "kg m2"),
number_param("eff", 0.95)};
type.create = make_block<drive_gearbox>;
return type;
}
private:
double const m_clutchtorque;
double const m_engagerate;
double const m_releaserate;
bool const m_autoclutch;
double const m_inertia;
double const m_efficiency;
std::vector<std::pair<int, double>> m_gears;
int m_gear{0};
int m_dir{0};
bool m_shifting{false};
bool m_slipping{false};
double m_engage{0.0};
double m_engagestate{0.0};
double m_omega{0.0}; // engine speed, rad/s
double m_previous{0.0}; // gearbox input speed in the previous step (dizel_n_old)
double m_deltaomega{0.0};
};
// hydraulic transmission: torque converter with filling, fluid coupling range and lockup clutch (adapted from the hydro_TC
// part of TMoverParameters::dizel_Momentum). holds the speed of the engine like the gearbox
class drive_hydro final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
power,
running,
dir,
out,
fill,
lockup,
ratio
};
};
struct param
{
enum : std::size_t
{
tc_tmmax,
tc_cp,
tc_table,
tc_tii,
tc_tio,
tc_too,
fill_rate,
drain_rate,
lockup_torque,
lockup_speed,
unlock_speed,
idle_rpm,
ans_speed,
inertia
};
};
explicit drive_hydro(block_setup const &Setup)
: block(Setup), m_tmmax(number(Setup, param::tc_tmmax)), m_couplingpoint(std::clamp(number(Setup, param::tc_cp), 0.01, 0.99)),
m_table(sorted_rows(table(Setup, param::tc_table), 2)), m_torqueinin(number(Setup, param::tc_tii)), m_torqueinout(number(Setup, param::tc_tio)),
m_torqueoutout(number(Setup, param::tc_too)), m_fillrate(std::max(0.0, number(Setup, param::fill_rate))), m_drainrate(std::max(0.0, number(Setup, param::drain_rate))),
m_lockuptorque(std::max(0.0, number(Setup, param::lockup_torque))), m_lockupspeed(number(Setup, param::lockup_speed)), m_unlockspeed(number(Setup, param::unlock_speed)),
m_idle(std::max(1.0, number(Setup, param::idle_rpm)) * rpm_to_rad), m_ansspeed(number(Setup, param::ans_speed)),
m_inertia(std::max(0.01, number(Setup, param::inertia)))
{
if (m_table.size() < 2)
{
m_table.clear();
}
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const moment{forward_in(Frame, port::in)};
auto const dir{absolute_direction(Frame, get(Frame, port::dir))};
auto const power{get_bool(Frame, port::power)};
// the engine running (Mains of dizel_Momentum); a stopped engine drains the converter and releases the lockup clutch
auto const running{get_bool(Frame, port::running)};
auto const vel{std::abs(Frame.body->velocity) * 3.6};
// speed of the output shaft in the frame of the engine; without a direction it turns with the engine
auto const n{dir != 0 ? back_out(Frame, port::out) * dir : m_omega};
// filling and draining of the converter, engaging and releasing of the lockup clutch
if (power && running && m_omega > 0.9 * m_idle)
{
m_fill += m_fillrate * Frame.dt;
}
if ((false == power && vel < m_ansspeed) || false == running || m_omega < 0.8 * m_idle)
{
m_fill -= m_drainrate * Frame.dt;
}
m_fill = std::clamp(m_fill, 0.0, 1.0);
if (vel > m_lockupspeed && running && m_omega > 0.9 * m_idle && power)
{
m_lockup = true;
m_lockuprate += m_fillrate * Frame.dt;
}
if (vel < (power ? m_lockupspeed : m_unlockspeed) || false == running || m_omega < 0.8 * m_idle)
{
m_lockup = false;
m_lockuprate -= m_drainrate * Frame.dt;
}
m_lockuprate = std::clamp(m_lockuprate, 0.0, 1.0);
// torques of the converter; its constants are for the speeds in rev/s, as in the .fiz files
auto const nin{m_omega / two_pi};
auto const nout{m_previous / two_pi};
auto const hydrotorque{nin * nin * m_torqueinin + (nin - nout) * m_torqueinout + nout * nout * m_torqueoutout};
auto const speedratio{nout / std::max(0.01, nin)};
double torquein;
double torqueout;
if (false == m_table.empty())
{
m_tmratio = interpolate(m_table, 1, speedratio);
torqueout = hydrotorque * m_fill * m_tmratio;
torquein = hydrotorque * m_fill * std::min(1.0, m_tmratio);
}
else if (speedratio < m_couplingpoint)
{
// converter range: the torque is multiplied
auto const x{1.0 - speedratio / m_couplingpoint};
m_tmratio = 1.0 + (m_tmmax - 1.0) * x * x;
torquein = hydrotorque * m_fill;
torqueout = hydrotorque * m_fill * m_tmratio;
}
else
{
// fluid coupling range
m_tmratio = (1.0 - speedratio) / (1.0 - m_couplingpoint);
torquein = hydrotorque * m_fill * m_tmratio;
torqueout = torquein;
}
auto const torquelock{m_lockuptorque * m_lockuprate};
// the main clutch of dizel_Momentum is taken as always engaged; the converter is drained to disconnect the engine
double gearmoment;
auto const capacity{torquelock + std::abs(torquein)};
if (std::abs(moment) > capacity || std::abs(m_previous - m_omega) > 0.1 * two_pi)
{
auto const deltaomega{m_omega - m_previous};
// NOTE: dizel_Momentum adds the lockup torque without the sign of the slip; here it acts against the slip too
gearmoment = (torquelock + std::abs(torqueout)) * sign(deltaomega);
auto const eps{(moment - capacity * sign(deltaomega)) / m_inertia};
auto newomega{m_omega + eps * Frame.dt};
if ((newomega - n) * deltaomega < 0.0 && torquelock > 0.1)
{
// the lockup clutch grips
newomega = n;
}
if (newomega * m_omega <= 0.0 && eps * m_omega < 0.0)
{
newomega = 0.0;
}
m_omega = newomega;
}
else
{
gearmoment = moment;
m_omega = clamp_any(n, m_omega + (moment - capacity) / m_inertia * Frame.dt, m_omega + (moment + capacity) / m_inertia * Frame.dt);
}
m_previous = n;
set_forward(Frame, port::out, gearmoment * dir);
add_back(Frame, port::in, m_omega);
set(Frame, port::fill, m_fill);
set_bool(Frame, port::lockup, m_lockup);
// the ratio of an empty converter means nothing (and grows without limit when the engine stops)
set(Frame, port::ratio, m_fill > 0.0 ? m_tmratio : 0.0);
body.hydro_TC_Fill = m_fill;
body.hydro_TC_Lockup = m_lockup;
body.hydro_TC_LockupRate = m_lockuprate;
body.hydro_TC_TMRatio = m_tmratio;
body.hydro_TC_TorqueIn = torquein;
body.hydro_TC_TorqueOut = gearmoment;
body.hydro_TC_nIn = nin;
body.hydro_TC_nOut = nout;
}
static block_type describe()
{
auto type{make_type("drive.hydro", "Hydraulic transmission",
"Torque converter with filling, fluid coupling range above the coupling point and a lockup clutch, as hydro_TC of the .fiz files (dizel_Momentum). "
"power fills the converter (with the engine above 0.9 of idle_rpm); without it the converter drains below ans_speed. The lockup clutch engages "
"above lockup_speed with power and releases below it (below unlock_speed without power). running (the engine's) false drains the converter and "
"releases the lockup clutch. Holds the speed of the engine (inertia of the engine side).",
block_origin::extracted)};
type.ports = {physical_in("in", domain::mechanical),
signal_in("power", value_type::boolean),
signal_in("running", value_type::boolean, 1.0, "", true),
signal_in("dir", value_type::real, 1.0, "", true),
physical_out("out", domain::mechanical),
signal_out("fill", value_type::real),
signal_out("lockup", value_type::boolean),
signal_out("ratio", value_type::real)};
type.params = {number_param("tc_tmmax", 2.0),
number_param("tc_cp", 0.85),
table_param("tc_table", {"speed_ratio", "torque_ratio"}),
number_param("tc_tii", 4.5),
number_param("tc_tio", 0.0),
number_param("tc_too", 0.0),
number_param("fill_rate", 1.0, "1/s"),
number_param("drain_rate", 1.0, "1/s"),
number_param("lockup_torque", 3000.0, "Nm"),
number_param("lockup_speed", 40.0, "km/h"),
number_param("unlock_speed", 30.0, "km/h"),
number_param("idle_rpm", 600.0, "rpm"),
number_param("ans_speed", 3.0, "km/h"),
number_param("inertia", 10.0, "kg m2")};
type.create = make_block<drive_hydro>;
return type;
}
private:
double const m_tmmax;
double const m_couplingpoint;
std::vector<std::vector<double>> m_table;
double const m_torqueinin;
double const m_torqueinout;
double const m_torqueoutout;
double const m_fillrate;
double const m_drainrate;
double const m_lockuptorque;
double const m_lockupspeed;
double const m_unlockspeed;
double const m_idle; // rad/s
double const m_ansspeed;
double const m_inertia;
double m_fill{0.0};
bool m_lockup{false};
double m_lockuprate{0.0};
double m_tmratio{1.0};
double m_omega{0.0}; // engine speed, rad/s
double m_previous{0.0}; // output speed in the previous step, rad/s
};
// main generator of a diesel-electric vehicle: separately excited dc generator (adapted from the DieselElectric part of
// TMoverParameters::TractionForce: the ellipse of the generator characteristic and the power available at the engine
// speed). holds the speed of the engine
class drive_generator final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
excitation,
out,
u,
i,
p
};
};
struct param
{
enum : std::size_t
{
u_max,
i_max,
p_max,
nominal_rpm,
eff,
field_time,
inertia
};
};
explicit drive_generator(block_setup const &Setup)
: block(Setup), m_umax(std::max(0.0, number(Setup, param::u_max))), m_imax(std::max(1.0, number(Setup, param::i_max))),
m_pmax(std::max(0.0, number(Setup, param::p_max)) * 1000.0), m_nominal(std::max(1.0, number(Setup, param::nominal_rpm)) * rpm_to_rad),
m_efficiency(std::clamp(number(Setup, param::eff), 0.01, 1.0)), m_fieldtime(std::max(0.0, number(Setup, param::field_time))),
m_inertia(std::max(0.01, number(Setup, param::inertia)))
{
}
void update(frame &Frame) override
{
auto const moment{forward_in(Frame, port::in)};
auto const current{back_out(Frame, port::out)}; // drawn by the load in the previous step
auto const excitation{std::clamp(get(Frame, port::excitation), 0.0, 1.0)};
auto const speedratio{std::clamp(m_omega / m_nominal, 0.0, 1.0)};
// the voltage at no load grows with the excitation and the speed; under load it follows the ellipse of the
// separately excited generator (zero at i_max) and the hyperbola of the power available at this speed
auto const noload{m_umax * excitation * speedratio};
auto const load{std::min(1.0, std::abs(current) / m_imax)};
auto target{noload * std::sqrt(std::max(0.0, 1.0 - load * load))};
if (std::abs(current) > 1.0)
{
target = std::min(target, m_pmax * speedratio / std::abs(current));
}
// the field of the generator follows with its time constant, which also keeps the loop with the motors stable
m_voltage += (target - m_voltage) * (m_fieldtime > 0.0 ? std::min(1.0, Frame.dt / m_fieldtime) : 1.0);
m_power = m_voltage * current;
// load of the engine: the electric power and the losses
auto const loadtorque{m_power / m_efficiency / std::max(m_omega, 1.0)};
m_omega = std::max(0.0, m_omega + (moment - loadtorque) / m_inertia * Frame.dt);
set_forward(Frame, port::out, m_voltage);
add_back(Frame, port::in, m_omega);
set(Frame, port::u, m_voltage);
set(Frame, port::i, current);
set(Frame, port::p, m_power / 1000.0);
}
static block_type describe()
{
auto type{make_type("drive.generator", "Main generator",
"Main generator of a diesel-electric vehicle. The voltage at no load is u_max times the excitation (0..1), proportional to the engine speed "
"below nominal_rpm; under load it falls on an ellipse to zero at i_max, and the power is limited to p_max scaled with the engine speed "
"(DieselElectric of TractionForce). The electric power loads the engine. Holds the speed of the engine (inertia of the engine and the rotor).",
block_origin::extracted)};
type.ports = {physical_in("in", domain::mechanical), signal_in("excitation", value_type::real), physical_out("out", domain::high_voltage),
signal_out("u", value_type::real, "V"), signal_out("i", value_type::real, "A"), signal_out("p", value_type::real, "kW")};
type.params = {number_param("u_max", 750.0, "V"),
number_param("i_max", 2400.0, "A"),
number_param("p_max", 330.0, "kW"),
number_param("nominal_rpm", 1500.0, "rpm"),
number_param("eff", 0.93),
number_param("field_time", 0.5, "s"),
number_param("inertia", 20.0, "kg m2")};
type.create = make_block<drive_generator>;
return type;
}
private:
double const m_umax;
double const m_imax;
double const m_pmax; // W
double const m_nominal; // rad/s
double const m_efficiency;
double const m_fieldtime;
double const m_inertia;
double m_voltage{0.0};
double m_power{0.0};
double m_omega{0.0}; // engine speed, rad/s
};
} // namespace
void register_diesel_blocks(catalog &Catalog)
{
Catalog.add(drive_diesel::describe());
Catalog.add(drive_gearbox::describe());
Catalog.add(drive_hydro::describe());
Catalog.add(drive_generator::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,658 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <cmath>
// drive chain: motor -> gear -> axle. the mechanical connections pass the torque forward and the rotational speed back.
// both are in the frame of the vehicle: positive torque pushes the vehicle towards its front (cab 1), positive speed
// is the rotation when moving that way. the dir input of the motors is the position of the reverser in the occupied cab,
// so the motors turn it to the frame of the vehicle with the occupied cab
namespace vgraph
{
namespace
{
double constexpr two_pi{2.0 * M_PI};
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description, block_origin Origin)
{
block_type type;
type.id = std::move(Id);
type.category = "drive";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = Origin;
return type;
}
// direction of the drive in the frame of the vehicle: -1, 0 or 1
int absolute_direction(frame const &Frame, double const Dir)
{
auto const dir{Dir > 0.5 ? 1 : Dir < -0.5 ? -1 : 0};
auto const cab{Frame.body->cab};
return dir * (cab != 0 ? cab : 1);
}
// generic motor with a torque and power limit; a stand-in for the drives not extracted yet
class drive_simple final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
demand,
dir,
shaft,
i,
p
};
};
struct param
{
enum : std::size_t
{
t_max,
p_max,
eff,
min_voltage
};
};
explicit drive_simple(block_setup const &Setup)
: block(Setup), m_supplied(is_connected(Setup, port::wn)), m_tmax(number(Setup, param::t_max)), m_pmax(number(Setup, param::p_max)),
m_efficiency(std::clamp(number(Setup, param::eff), 0.01, 1.0)), m_minvoltage(number(Setup, param::min_voltage))
{
}
void update(frame &Frame) override
{
auto const voltage{m_supplied ? forward_in(Frame, port::wn) : 0.0};
auto const powered{false == m_supplied || (voltage > 0.0 && voltage >= m_minvoltage)};
auto const dir{absolute_direction(Frame, get(Frame, port::dir))};
auto const demand{std::clamp(get(Frame, port::demand), -1.0, 1.0)};
auto const speed{back_out(Frame, port::shaft)}; // rad/s
auto torque{0.0};
if (powered && dir != 0)
{
if (demand >= 0.0)
{
// traction, limited by the power above the base speed
auto const limit{std::abs(speed) > 1e-3 ? std::min(m_tmax, m_pmax / std::abs(speed)) : m_tmax};
torque = dir * demand * limit;
}
else
{
// electric brake, against the rotation
auto const limit{std::abs(speed) > 1e-3 ? std::min(m_tmax, m_pmax / std::abs(speed)) : 0.0};
torque = (speed >= 0.0 ? -1.0 : 1.0) * (-demand) * limit * std::min(1.0, std::abs(speed));
}
}
auto const mechanical{torque * speed}; // W, positive when driving
auto const electrical{mechanical >= 0.0 ? mechanical / m_efficiency : mechanical * m_efficiency};
auto const current{m_supplied && voltage > 1.0 ? electrical / voltage : 0.0};
if (m_supplied)
{
add_back(Frame, port::wn, current);
}
set_forward(Frame, port::shaft, torque);
set(Frame, port::i, current);
set(Frame, port::p, electrical);
auto &body{vehicle(Frame)};
body.Im = current;
body.EngineVoltage = voltage * dir;
body.enrot = speed / two_pi;
}
static block_type describe()
{
auto type{make_type("drive.simple", "Simple motor",
"Motor with a torque limit (t_max) and a power limit (p_max): demand 0..1 drives, -1..0 brakes electrically. A stand-in for prototypes.",
block_origin::native)};
type.ports = {physical_in("wn", domain::high_voltage, true), signal_in("demand", value_type::real), signal_in("dir", value_type::real),
physical_out("shaft", domain::mechanical), signal_out("i", value_type::real, "A"), signal_out("p", value_type::real, "W")};
type.params = {number_param("t_max", 5000.0, "Nm"), number_param("p_max", 500000.0, "W"), number_param("eff", 0.9), number_param("min_voltage", 0.0, "V")};
type.create = make_block<drive_simple>;
return type;
}
private:
bool const m_supplied;
double const m_tmax;
double const m_pmax;
double const m_efficiency;
double const m_minvoltage;
};
class drive_gear final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
out
};
};
explicit drive_gear(block_setup const &Setup) : block(Setup), m_ratio(number(Setup, 0)), m_efficiency(number(Setup, 1)) {}
void update(frame &Frame) override
{
set_forward(Frame, port::out, forward_in(Frame, port::in) * m_ratio * m_efficiency);
add_back(Frame, port::in, back_out(Frame, port::out) * m_ratio);
}
static block_type describe()
{
auto type{make_type("drive.gear", "Gear", "Transmission: torque times ratio and efficiency, speed of the input times ratio. ratio = wheel teeth / motor teeth.",
block_origin::extracted)};
type.ports = {physical_in("in", domain::mechanical), physical_out("out", domain::mechanical)};
type.params = {number_param("ratio", 1.0), number_param("eff", 1.0)};
type.create = make_block<drive_gear>;
return type;
}
private:
double const m_ratio;
double const m_efficiency;
};
// wheelsets driven by one shaft: the torque becomes the tractive force of the vehicle
class drive_axle final : public block
{
public:
struct port
{
enum : std::size_t
{
shaft,
force,
slip
};
};
explicit drive_axle(block_setup const &Setup) : block(Setup), m_axles(std::max(0.0, number(Setup, 0))) {}
void update(frame &Frame) override
{
auto const &body{vehicle(Frame)};
auto const diameter{body.WheelDiameter > 0.0 ? body.WheelDiameter : 1.0};
auto const force{forward_in(Frame, port::shaft) * 2.0 / diameter * m_axles};
Frame.body->tractive_force += force;
add_back(Frame, port::shaft, Frame.body->wheel_speed * two_pi);
set(Frame, port::force, force);
set_bool(Frame, port::slip, Frame.body->slipping);
}
static block_type describe()
{
auto type{make_type("drive.axle", "Driven axles",
"Driven wheelsets: the torque of one shaft (of one motor, after the gear) times powered_axles gives the force at the wheel rim. All axle forces are "
"summed into the tractive force of the vehicle.",
block_origin::native)};
type.ports = {physical_in("shaft", domain::mechanical), signal_out("force", value_type::real, "N"), signal_out("slip", value_type::boolean)};
type.params = {number_param("powered_axles", 1.0)};
type.create = make_block<drive_axle>;
return type;
}
private:
double const m_axles;
};
// series dc traction motors with resistor starting (TMoverParameters::Current, Momentum)
class drive_dc_series final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
position,
shunt,
dir,
high,
reset,
shaft,
i,
u_motor,
overload
};
};
struct param
{
enum : std::size_t
{
rlist,
mpt,
volt,
winding_res,
circuit_res,
imax_lo,
imax_hi,
imin_lo,
imin_hi,
trip_delay
};
};
explicit drive_dc_series(block_setup const &Setup)
: block(Setup), m_windingres(number(Setup, param::winding_res)), m_circuitres(number(Setup, param::circuit_res)), m_imaxlo(number(Setup, param::imax_lo)),
m_imaxhi(number(Setup, param::imax_hi)), m_tripdelay(number(Setup, param::trip_delay))
{
for (auto const &row : table(Setup, param::rlist))
{
// pos, r, bn, mn, auto
scheme entry;
entry.r = row[1];
entry.bn = static_cast<int>(std::lround(row[2]));
entry.mn = static_cast<int>(std::lround(row[3]));
m_schemes.push_back(entry);
}
for (auto const &row : table(Setup, param::mpt))
{
// pos, mfi, mIsat, mfi0, fi, Isat, fi0
TMotorParameters motor;
motor.mfi = row[1];
motor.mIsat = row[2];
motor.mfi0 = row[3];
motor.fi = row[4];
motor.Isat = row[5];
motor.fi0 = row[6];
m_motors.push_back(motor);
}
if (m_schemes.empty())
{
m_schemes.emplace_back();
}
if (m_motors.empty())
{
m_motors.emplace_back();
}
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dir{absolute_direction(Frame, get(Frame, port::dir))};
auto const voltage{forward_in(Frame, port::wn) * dir};
auto const position{std::clamp<int>(static_cast<int>(std::lround(get(Frame, port::position))), 0, static_cast<int>(m_schemes.size()) - 1)};
auto const shunt{std::clamp<int>(static_cast<int>(std::lround(get(Frame, port::shunt))), 0, static_cast<int>(m_motors.size()) - 1)};
auto const &scheme{m_schemes[position]};
auto const &motor{m_motors[shunt]};
auto const revolutions{back_out(Frame, port::shaft) / two_pi}; // rev/s
if (get_bool(Frame, port::reset))
{
m_overload = false;
}
auto current{0.0};
if (scheme.bn != 0 && std::abs(voltage) > 1.0 && false == m_overload)
{
auto const resistance{scheme.r + m_circuitres};
current = series_motor_current(voltage, revolutions, scheme.mn, scheme.mn * m_windingres + resistance, motor);
}
// overload relay, as in TractionForce: trips when the current stays above the limit
auto const imax{get_bool(Frame, port::high) ? m_imaxhi : m_imaxlo};
if (imax > 0.0 && std::abs(current) > imax)
{
m_overtime += Frame.dt;
if (m_overtime > m_tripdelay)
{
m_overload = true;
}
}
else
{
m_overtime = 0.0;
}
auto const torque{series_motor_momentum(current, motor)};
auto const linecurrent{current * scheme.bn};
add_back(Frame, port::wn, std::abs(linecurrent));
set_forward(Frame, port::shaft, torque);
set(Frame, port::i, current);
set(Frame, port::u_motor, scheme.mn > 0 ? (std::abs(voltage) - std::abs(current) * (scheme.r + m_circuitres)) / scheme.mn : 0.0);
set_bool(Frame, port::overload, m_overload);
body.Im = current;
body.EngineVoltage = voltage;
body.enrot = revolutions;
body.MainCtrlActualPos = position;
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "overload";
}
double state_value(std::size_t) const override
{
return m_overload ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("drive.dc_series", "Series DC motors",
"Series wound DC traction motors with resistor starting, computed as for the .fiz vehicles. position selects the row of rlist (resistance, "
"branches, motors in series), shunt the row of mpt (motor characteristics). The shaft carries the torque of one motor. high selects imax_hi "
"for the overload relay; reset clears it.",
block_origin::extracted)};
type.ports = {physical_in("wn", domain::high_voltage),
signal_in("position", value_type::integer),
signal_in("shunt", value_type::integer, 0.0, "", true),
signal_in("dir", value_type::real),
signal_in("high", value_type::boolean, 0.0, "", true),
signal_in("reset", value_type::boolean, 0.0, "", true),
physical_out("shaft", domain::mechanical),
signal_out("i", value_type::real, "A"),
signal_out("u_motor", value_type::real, "V"),
signal_out("overload", value_type::boolean)};
type.params = {table_param("rlist", {"pos", "r", "bn", "mn", "auto"}),
table_param("mpt", {"pos", "mfi", "misat", "mfi0", "fi", "isat", "fi0"}),
number_param("volt", 1500.0, "V"),
number_param("winding_res", 0.0, "ohm"),
number_param("circuit_res", 0.0, "ohm"),
number_param("imax_lo", 0.0, "A"),
number_param("imax_hi", 0.0, "A"),
number_param("imin_lo", 0.0, "A"),
number_param("imin_hi", 0.0, "A"),
number_param("trip_delay", 0.05, "s")};
type.create = make_block<drive_dc_series>;
return type;
}
private:
struct scheme
{
double r{0.0};
int bn{0};
int mn{0};
};
double const m_windingres;
double const m_circuitres;
double const m_imaxlo;
double const m_imaxhi;
double const m_tripdelay;
std::vector<scheme> m_schemes;
std::vector<TMotorParameters> m_motors;
double m_overtime{0.0};
bool m_overload{false};
};
// induction motors fed by inverters (the ElectricInductionMotor drive of TMoverParameters::TractionForce)
class drive_async final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
demand,
dir,
shaft,
i,
f,
ed_active,
ed
};
};
// in the order of the eimc constants they're copied to
struct param
{
enum : std::size_t
{
dfic,
dfmax,
p,
cfu,
cim,
icif,
uzmax,
uzh,
du,
i0,
fcfu,
fcfuh,
f0,
a1,
pmax,
fh,
ph,
vh0,
vh1,
imax,
abed,
edep,
flat,
motors,
ratio,
eff,
log_force
};
};
explicit drive_async(block_setup const &Setup)
: block(Setup), m_flat(boolean(Setup, param::flat)), m_motors(static_cast<int>(std::lround(number(Setup, param::motors)))), m_ratio(number(Setup, param::ratio)),
m_efficiency(number(Setup, param::eff)), m_logforce(boolean(Setup, param::log_force))
{
std::fill(std::begin(m_eimc), std::end(m_eimc), 0.0);
std::fill(std::begin(m_eimv), std::end(m_eimv), 0.0);
m_eimc[eimc_s_dfic] = number(Setup, param::dfic);
m_eimc[eimc_s_dfmax] = number(Setup, param::dfmax);
m_eimc[eimc_s_p] = number(Setup, param::p);
m_eimc[eimc_s_cfu] = number(Setup, param::cfu);
m_eimc[eimc_s_cim] = number(Setup, param::cim);
m_eimc[eimc_s_icif] = number(Setup, param::icif);
m_eimc[eimc_f_Uzmax] = number(Setup, param::uzmax);
m_eimc[eimc_f_Uzh] = number(Setup, param::uzh);
m_eimc[eimc_f_DU] = number(Setup, param::du);
m_eimc[eimc_f_I0] = number(Setup, param::i0);
m_eimc[eimc_f_cfu] = number(Setup, param::fcfu);
// as in LoadFIZ_Engine: fcfuH defaults to fcfu
m_eimc[eimc_f_cfuH] = number(Setup, param::fcfuh) > 0.0 ? number(Setup, param::fcfuh) : m_eimc[eimc_f_cfu];
m_eimc[eimc_p_F0] = number(Setup, param::f0);
m_eimc[eimc_p_a1] = number(Setup, param::a1);
m_eimc[eimc_p_Pmax] = number(Setup, param::pmax);
m_eimc[eimc_p_Fh] = number(Setup, param::fh);
m_eimc[eimc_p_Ph] = number(Setup, param::ph);
m_eimc[eimc_p_Vh0] = number(Setup, param::vh0);
m_eimc[eimc_p_Vh1] = number(Setup, param::vh1);
m_eimc[eimc_p_Imax] = number(Setup, param::imax);
m_eimc[eimc_p_abed] = number(Setup, param::abed);
m_eimc[eimc_p_eped] = number(Setup, param::edep);
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const voltage{forward_in(Frame, port::wn)};
auto const dir{absolute_direction(Frame, get(Frame, port::dir))};
auto const enrot{back_out(Frame, port::shaft) / two_pi};
auto const demand{std::clamp(get(Frame, port::demand), -1.0, 1.0)};
if (voltage < 1.0 || dir == 0)
{
// without supply the inverters stop, as for the vehicles without the main circuit (Mains)
std::fill(std::begin(m_eimv), std::end(m_eimv), 0.0);
m_eimvpr = 0.0;
m_dynamicbrake = false;
add_back(Frame, port::wn, 0.0);
publish(Frame, body, 0.0, 0.0, 0.0, voltage * dir, enrot);
return;
}
auto const vel{body.Vel};
// set value of the force and the rate of its ramp, as in TractionForce
double posratio;
double rate;
if (demand < -0.001)
{
m_dynamicbrake = true;
auto const vhramp{std::clamp((vel - m_eimc[eimc_p_Vh0]) / std::max(0.001, m_eimc[eimc_p_Vh1] - m_eimc[eimc_p_Vh0]), 0.0, 1.0)};
posratio = demand * vhramp;
rate = 5.0;
}
else
{
m_dynamicbrake = false;
posratio = std::max(demand, 0.0);
if (m_flat && m_eimc[eimc_p_F0] * m_eimv[eimv_Fful] > 0)
{
posratio = std::min(posratio * m_eimc[eimc_p_F0] / m_eimv[eimv_Fful], 1.0);
}
rate = 4.0;
}
if (body.SlippingWheels)
{
// anti-slip: the set value drops quickly
posratio = 0.0;
rate = 10.0;
}
induction_motor_inputs input;
input.demand = posratio;
input.rate = rate;
input.dynamic_brake = m_dynamicbrake;
input.engine_voltage = voltage;
input.enrot = enrot;
input.vel = vel;
input.v = body.V;
input.dir_absolute = dir;
input.dir_active = dir;
input.powered_axles = m_motors;
input.inverters_ratio = 1.0;
input.ratio = m_ratio;
input.efficiency = m_efficiency;
input.wheel_diameter = body.WheelDiameter > 0.0 ? body.WheelDiameter : 1.0;
input.log_force = m_logforce;
input.max_voltage = body.EnginePowerSource.CollectorParameters.MaxV;
input.dt = Frame.dt;
induction_motor_outputs output;
induction_motor_update(input, m_eimc, m_eimv, m_eimvpr, m_vadd, output);
add_back(Frame, port::wn, output.Itot);
// share of the requested braking force the motors deliver, for the blending with the pneumatic brake
auto edshare{0.0};
if (m_dynamicbrake && m_eimc[eimc_p_Fh] > 0.0 && demand < -0.001)
{
edshare = std::clamp(std::abs(m_eimv[eimv_Fr]) / (m_eimc[eimc_p_Fh] * -demand), 0.0, 1.0);
}
publish(Frame, body, output.Mm, output.Im, edshare, voltage, enrot);
}
std::size_t state_count() const override
{
return 2;
}
char const *state_name(std::size_t Index) const override
{
return Index == 0 ? "set_value" : "force_kn";
}
double state_value(std::size_t Index) const override
{
return Index == 0 ? m_eimvpr : m_eimv[eimv_Fr];
}
static block_type describe()
{
auto type{make_type("drive.async", "Induction motors with inverters",
"Induction traction motors fed by inverters, computed as for the .fiz vehicles (Engine: ElectricInductionMotor). demand 0..1 drives, -1..0 brakes "
"electrically (faded out between vh1 and vh0). The shaft carries the torque of one motor; ratio and eff have to match the gear, and motors the "
"driven axles. ed is the share of the requested braking force the motors deliver, for the ed input of the distributor.",
block_origin::extracted)};
type.ports = {physical_in("wn", domain::high_voltage), signal_in("demand", value_type::real), signal_in("dir", value_type::real),
physical_out("shaft", domain::mechanical), signal_out("i", value_type::real, "A"), signal_out("f", value_type::real, "Hz"),
signal_out("ed_active", value_type::boolean), signal_out("ed", value_type::real)};
type.params = {number_param("dfic", 0.0),
number_param("dfmax", 0.0),
number_param("p", 2.0),
number_param("cfu", 0.0),
number_param("cim", 0.0),
number_param("icif", 0.0),
number_param("uzmax", 0.0, "V"),
number_param("uzh", 0.0, "V"),
number_param("du", 0.0, "V"),
number_param("i0", 0.0, "A"),
number_param("fcfu", 0.0),
number_param("fcfuh", 0.0),
number_param("f0", 0.0, "kN"),
number_param("a1", 0.0),
number_param("pmax", 0.0, "kW"),
number_param("fh", 0.0, "kN"),
number_param("ph", 0.0, "kW"),
number_param("vh0", 0.0, "km/h"),
number_param("vh1", 0.0, "km/h"),
number_param("imax", 0.0, "A"),
number_param("abed", 1.0),
number_param("edep", 1.5),
boolean_param("flat", false),
number_param("motors", 4.0),
number_param("ratio", 1.0),
number_param("eff", 1.0),
boolean_param("log_force", false)};
type.create = make_block<drive_async>;
return type;
}
private:
void publish(frame &Frame, TMoverParameters &Body, double const Torque, double const Current, double const Edshare, double const Voltage, double const Revolutions)
{
set_forward(Frame, port::shaft, Torque);
set(Frame, port::i, Current);
set(Frame, port::f, std::abs(m_eimv[eimv_fp]));
set_bool(Frame, port::ed_active, m_dynamicbrake);
set(Frame, port::ed, Edshare);
Body.Im = Current;
Body.EngineVoltage = Voltage;
Body.enrot = Revolutions;
Body.DynamicBrakeFlag = m_dynamicbrake;
Body.InverterFrequency = std::abs(m_eimv[eimv_fp]);
std::copy(std::begin(m_eimv), std::end(m_eimv), Body.eimv);
}
bool const m_flat;
int const m_motors;
double const m_ratio;
double const m_efficiency;
bool const m_logforce;
double m_eimc[26];
double m_eimv[21];
double m_eimvpr{0.0};
double m_vadd{0.0};
bool m_dynamicbrake{false};
};
} // namespace
void register_drive_blocks(catalog &Catalog)
{
Catalog.add(drive_simple::describe());
Catalog.add(drive_dc_series::describe());
Catalog.add(drive_async::describe());
Catalog.add(drive_gear::describe());
Catalog.add(drive_axle::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,658 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <cmath>
// high voltage circuit and auxiliary supplies. voltage goes forward through the connections, current comes back; the
// back channel lags one step, so a breaker sees the current its load drew in the previous step
namespace vgraph
{
namespace
{
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description, block_origin Origin)
{
block_type type;
type.id = std::move(Id);
type.category = "electric";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = Origin;
return type;
}
// current collector. in the simulator its contact with the catenary is the pantograph of the vehicle model
class el_pantograph final : public block
{
public:
struct port
{
enum : std::size_t
{
raise,
air,
wn,
up,
height,
u
};
};
struct param
{
enum : std::size_t
{
index,
raise_time,
lower_time,
min_pressure,
preview_voltage
};
};
explicit el_pantograph(block_setup const &Setup)
: block(Setup), m_air(is_connected(Setup, port::air)), m_index(std::clamp(static_cast<int>(std::lround(number(Setup, param::index))), 0, 1)),
m_raisetime(std::max(0.01, number(Setup, param::raise_time))), m_lowertime(std::max(0.01, number(Setup, param::lower_time))),
m_minpressure(number(Setup, param::min_pressure)), m_previewvoltage(number(Setup, param::preview_voltage))
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const pressure{m_air ? forward_in(Frame, port::air) : body.EnginePowerSource.CollectorParameters.MaxPress};
auto const raise{get_bool(Frame, port::raise) && pressure >= m_minpressure};
auto &pantograph{body.Pantographs[m_index]};
pantograph.is_active = raise;
// the model raises the pantograph at a speed depending on this pressure
body.PantPress = pressure;
m_height = std::clamp(m_height + (raise ? Frame.dt / m_raisetime : -Frame.dt / m_lowertime), 0.0, 1.0);
double voltage;
if (Frame.body->standalone)
{
voltage = m_height >= 1.0 ? m_previewvoltage : 0.0;
}
else
{
voltage = std::abs(m_index == 0 ? body.PantFrontVolt : body.PantRearVolt);
}
pantograph.voltage = voltage;
set_forward(Frame, port::wn, voltage);
// current drawn by the vehicle through this pantograph, in the previous step
Frame.body->current += back_out(Frame, port::wn);
set_bool(Frame, port::up, m_height >= 1.0);
set(Frame, port::height, m_height);
set(Frame, port::u, voltage);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "height";
}
double state_value(std::size_t) const override
{
return m_height;
}
static block_type describe()
{
auto type{make_type("el.pantograph", "Pantograph",
"Current collector: index 0 is the front pantograph of the vehicle model, 1 the rear one. Raising needs the air pressure (when connected). "
"Without the simulator the catenary has the preview voltage.",
block_origin::extracted)};
type.ports = {signal_in("raise", value_type::boolean), physical_in("air", domain::air, true), physical_out("wn", domain::high_voltage),
signal_out("up", value_type::boolean), signal_out("height", value_type::real), signal_out("u", value_type::real, "V")};
type.params = {number_param("index", 0.0), number_param("raise_time", 4.0, "s"), number_param("lower_time", 1.0, "s"), number_param("min_pressure", 3.5, "bar"),
number_param("preview_voltage", 3000.0, "V")};
type.create = make_block<el_pantograph>;
return type;
}
private:
bool const m_air;
int const m_index;
double const m_raisetime;
double const m_lowertime;
double const m_minpressure;
double const m_previewvoltage;
double m_height{0.0};
};
// joins two supplies: the higher voltage wins, the current goes back to the source(s) supplying it
class el_junction final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
b,
out
};
};
explicit el_junction(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto const a{forward_in(Frame, port::a)};
auto const b{forward_in(Frame, port::b)};
set_forward(Frame, port::out, std::max(a, b));
auto const current{back_out(Frame, port::out)};
if (std::abs(a - b) < 1.0)
{
add_back(Frame, port::a, 0.5 * current);
add_back(Frame, port::b, 0.5 * current);
}
else
{
add_back(Frame, a > b ? port::a : port::b, current);
}
}
static block_type describe()
{
auto type{make_type("el.junction", "Junction", "Joins two high voltage supplies, e.g. two pantographs.", block_origin::primitive)};
type.ports = {physical_in("a", domain::high_voltage), physical_in("b", domain::high_voltage, true), physical_out("out", domain::high_voltage)};
type.create = make_block<el_junction>;
return type;
}
};
// line breaker (WS). its state is the Mains of the vehicle body, which the sounds and the AI watch; with more breakers
// (multi-system vehicles) the Mains is closed when any of them is
class el_hsb final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
close,
open,
hold,
out,
closed,
i,
tripped
};
};
struct param
{
enum : std::size_t
{
trip_current,
close_time,
min_voltage,
init_time
};
};
explicit el_hsb(block_setup const &Setup)
: block(Setup), m_tripcurrent(number(Setup, param::trip_current)), m_closetime(number(Setup, param::close_time)), m_minvoltage(number(Setup, param::min_voltage)),
m_inittime(number(Setup, param::init_time)), m_initcountdown(m_inittime)
{
}
void update(frame &Frame) override
{
m_voltage = forward_in(Frame, port::in);
m_current = back_out(Frame, port::out);
// the current of the previous step passes to the supply
add_back(Frame, port::in, m_current);
auto const voltageok{m_voltage > 0.0 && m_voltage >= m_minvoltage};
// the breaker can be closed after the voltage was present for the init time
m_initcountdown = voltageok ? m_initcountdown - Frame.dt : m_inittime;
if (m_closed)
{
auto const overload{m_tripcurrent > 0.0 && std::abs(m_current) > m_tripcurrent};
if (overload || false == voltageok || get_bool(Frame, port::open) || false == get_bool(Frame, port::hold))
{
m_closed = false;
m_tripped = m_tripped || overload;
}
}
else if (get_bool(Frame, port::close) && voltageok && m_initcountdown <= 0.0 && get_bool(Frame, port::hold) && false == get_bool(Frame, port::open))
{
m_closing += Frame.dt;
if (m_closing >= m_closetime)
{
m_closed = true;
m_tripped = false;
m_closing = 0.0;
}
}
else
{
m_closing = 0.0;
}
// the vehicle can have more breakers, e.g. one for each supply system; its Mains is closed when any of them is
vehicle(Frame).Mains = Frame.body->mains.any(Frame.step, m_closed);
}
void commit(frame &Frame) override
{
set_forward(Frame, port::out, m_closed ? m_voltage : 0.0);
set_bool(Frame, port::closed, m_closed);
set(Frame, port::i, m_current);
set_bool(Frame, port::tripped, m_tripped);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "closed";
}
double state_value(std::size_t) const override
{
return m_closed ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("el.hsb", "Line breaker",
"Main (line) breaker. Closes after close is held for close_time, with enough voltage present for init_time; opens with open, without hold, "
"below min_voltage or above trip_current (then tripped is set until the next closing).",
block_origin::extracted)};
type.ports = {physical_in("in", domain::high_voltage),
signal_in("close", value_type::boolean),
signal_in("open", value_type::boolean, 0.0, "", true),
signal_in("hold", value_type::boolean, 1.0, "", true),
physical_out("out", domain::high_voltage),
signal_out("closed", value_type::boolean),
signal_out("i", value_type::real, "A"),
signal_out("tripped", value_type::boolean)};
type.params = {number_param("trip_current", 0.0, "A"), number_param("close_time", 0.2, "s"), number_param("min_voltage", 0.0, "V"), number_param("init_time", 0.0, "s")};
type.deferred = true;
type.create = make_block<el_hsb>;
return type;
}
private:
double const m_tripcurrent;
double const m_closetime;
double const m_minvoltage;
double const m_inittime;
double m_initcountdown;
double m_closing{0.0};
double m_voltage{0.0};
double m_current{0.0};
bool m_closed{false};
bool m_tripped{false};
};
class el_contactor final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
on,
out,
closed
};
};
explicit el_contactor(block_setup const &Setup) : block(Setup), m_delay(number(Setup, 0)) {}
void update(frame &Frame) override
{
m_voltage = forward_in(Frame, port::in);
add_back(Frame, port::in, back_out(Frame, port::out));
if (get_bool(Frame, port::on))
{
m_timer += Frame.dt;
m_closed = m_timer >= m_delay;
}
else
{
m_timer = 0.0;
m_closed = false;
}
}
void commit(frame &Frame) override
{
set_forward(Frame, port::out, m_closed ? m_voltage : 0.0);
set_bool(Frame, port::closed, m_closed);
}
static block_type describe()
{
auto type{make_type("el.contactor", "Contactor", "Connects the output to the input after on was held for the delay; disconnects at once.", block_origin::primitive)};
type.ports = {physical_in("in", domain::high_voltage), signal_in("on", value_type::boolean), physical_out("out", domain::high_voltage),
signal_out("closed", value_type::boolean)};
type.params = {number_param("delay", 0.0, "s")};
type.deferred = true;
type.create = make_block<el_contactor>;
return type;
}
private:
double const m_delay;
double m_timer{0.0};
double m_voltage{0.0};
bool m_closed{false};
};
// static converter or motor generator: auxiliary supply from the high voltage, and optionally the onboard low voltage
// (charger). its state is the ConverterFlag of the body; with more converters the flag is set when any of them runs
class el_converter final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
on,
aux,
run,
nn
};
};
struct param
{
enum : std::size_t
{
power,
start_time,
min_voltage,
out_voltage,
nn_voltage,
aux_kind
};
};
explicit el_converter(block_setup const &Setup)
: block(Setup), m_idlepower(number(Setup, param::power) * 0.05), m_starttime(number(Setup, param::start_time)), m_minvoltage(number(Setup, param::min_voltage)),
m_outvoltage(number(Setup, param::out_voltage)), m_nnvoltage(std::max(0.0, number(Setup, param::nn_voltage)))
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const voltage{forward_in(Frame, port::wn)};
auto const on{get_bool(Frame, port::on)};
if (on && voltage > 0.0 && voltage >= m_minvoltage)
{
m_timer += Frame.dt;
}
else
{
m_timer = 0.0;
}
auto const run{on && voltage > 0.0 && voltage >= m_minvoltage && m_timer >= m_starttime};
body.ConverterAllow = Frame.body->converter_allow.any(Frame.step, on);
body.ConverterFlag = Frame.body->converter.any(Frame.step, run);
set_forward(Frame, port::aux, run ? m_outvoltage : 0.0);
set_forward(Frame, port::nn, run ? m_nnvoltage : 0.0);
if (run)
{
// power taken by the auxiliaries and the low voltage loads in the previous step, and the losses
auto const nnpower{m_nnvoltage * std::max(0.0, back_out(Frame, port::nn))};
add_back(Frame, port::wn, (back_out(Frame, port::aux) + nnpower + m_idlepower) / voltage);
}
set_bool(Frame, port::run, run);
}
static block_type describe()
{
auto type{make_type("el.converter", "Converter",
"Converter of the auxiliary supply. Runs start_time after it was switched on with enough voltage; draws the power of its loads from the high "
"voltage. nn is the onboard low voltage made by its charger (off when nn_voltage is 0). aux_kind only names the auxiliary circuit.",
block_origin::extracted)};
type.ports = {physical_in("wn", domain::high_voltage), signal_in("on", value_type::boolean), physical_out("aux", domain::aux_ac), signal_out("run", value_type::boolean),
physical_out("nn", domain::low_voltage)};
// aux_kind describes the auxiliary circuit for the editor (three phase, single phase or direct current); the simulator ignores it
type.params = {number_param("power", 50000.0, "W"), number_param("start_time", 2.0, "s"), number_param("min_voltage", 0.0, "V"),
number_param("out_voltage", 400.0, "V"), number_param("nn_voltage", 0.0, "V"), choice_param("aux_kind", {"ac3", "ac1", "dc"}, "ac3")};
type.create = make_block<el_converter>;
return type;
}
private:
double const m_idlepower;
double const m_starttime;
double const m_minvoltage;
double const m_outvoltage;
double const m_nnvoltage;
double m_timer{0.0};
};
// voltage relay: tells whether the supply is within a range, e.g. to recognize the supply system of a multi-system
// vehicle. the simulator gives the vehicles only the voltage of the catenary, so the systems are told apart by it
class el_voltage_relay final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
ok,
u
};
};
struct param
{
enum : std::size_t
{
min_voltage,
max_voltage,
delay
};
};
explicit el_voltage_relay(block_setup const &Setup)
: block(Setup), m_minvoltage(number(Setup, param::min_voltage)), m_maxvoltage(number(Setup, param::max_voltage)), m_delay(number(Setup, param::delay))
{
}
void update(frame &Frame) override
{
// the relay draws no current
auto const voltage{forward_in(Frame, port::in)};
auto const inrange{voltage > 0.0 && voltage >= m_minvoltage && (m_maxvoltage <= 0.0 || voltage <= m_maxvoltage)};
m_timer = inrange ? m_timer + Frame.dt : 0.0;
set_bool(Frame, port::ok, inrange && m_timer >= m_delay);
set(Frame, port::u, voltage);
}
static block_type describe()
{
auto type{make_type("el.voltage_relay", "Voltage relay",
"ok is set when the voltage was within min_voltage and max_voltage (0: no upper limit) for the delay. Draws no current; used e.g. to recognize "
"the supply system.",
block_origin::primitive)};
type.ports = {physical_in("in", domain::high_voltage), signal_out("ok", value_type::boolean), signal_out("u", value_type::real, "V")};
type.params = {number_param("min_voltage", 2200.0, "V"), number_param("max_voltage", 4000.0, "V"), number_param("delay", 0.5, "s")};
type.create = make_block<el_voltage_relay>;
return type;
}
private:
double const m_minvoltage;
double const m_maxvoltage;
double const m_delay;
double m_timer{0.0};
};
// main transformer of an ac vehicle, or of the ac branch of a multi-system one, with the rectifier behind it implied.
// the output voltage is the input voltage times the ratio; the current drawn from the input follows from the power
class el_transformer final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
out
};
};
struct param
{
enum : std::size_t
{
ratio,
eff
};
};
explicit el_transformer(block_setup const &Setup)
: block(Setup), m_ratio(std::max(0.0, number(Setup, param::ratio))), m_efficiency(std::clamp(number(Setup, param::eff), 0.01, 1.0))
{
}
void update(frame &Frame) override
{
set_forward(Frame, port::out, forward_in(Frame, port::in) * m_ratio);
add_back(Frame, port::in, back_out(Frame, port::out) * m_ratio / m_efficiency);
}
static block_type describe()
{
auto type{make_type("el.transformer", "Transformer",
"Output voltage is the input voltage times the ratio (out/in); the input current is the output current times ratio / eff. For ac vehicles "
"and the ac branches of multi-system vehicles, with the rectifier implied.",
block_origin::primitive)};
type.ports = {physical_in("in", domain::high_voltage), physical_out("out", domain::high_voltage)};
type.params = {number_param("ratio", 0.12, ""), number_param("eff", 0.97, "")};
type.create = make_block<el_transformer>;
return type;
}
private:
double const m_ratio;
double const m_efficiency;
};
// battery of the low voltage circuits; its switch is the Battery of the body
class el_battery final : public block
{
public:
struct port
{
enum : std::size_t
{
on,
charge,
nn,
ok,
u
};
};
struct param
{
enum : std::size_t
{
voltage,
capacity,
min_voltage
};
};
explicit el_battery(block_setup const &Setup)
: block(Setup), m_charger(is_connected(Setup, port::charge)), m_voltage(number(Setup, param::voltage)), m_capacity(std::max(0.1, number(Setup, param::capacity))),
m_minvoltage(number(Setup, param::min_voltage)), m_charge(m_capacity)
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const on{get_bool(Frame, port::on)};
if (on)
{
// current of the loads in the previous step, A
m_charge -= std::max(0.0, back_out(Frame, port::nn)) * Frame.dt / 3600.0;
}
if (m_charger && forward_in(Frame, port::charge) > 0.0)
{
// charged at a tenth of the capacity per hour
m_charge += 0.1 * m_capacity * Frame.dt / 3600.0;
}
m_charge = std::clamp(m_charge, 0.0, m_capacity);
// open circuit voltage drops with the charge
auto const voltage{on ? m_voltage * (0.85 + 0.15 * m_charge / m_capacity) : 0.0};
body.Battery = on;
body.BatteryVoltage = voltage;
set_forward(Frame, port::nn, voltage);
set_bool(Frame, port::ok, on && voltage >= m_minvoltage);
set(Frame, port::u, voltage);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "charge";
}
double state_value(std::size_t) const override
{
return m_charge;
}
static block_type describe()
{
auto type{make_type("el.battery", "Battery",
"Battery of the low voltage circuits. ok tells whether its voltage is above min_voltage; it's charged when the charge input has voltage.",
block_origin::extracted)};
type.ports = {signal_in("on", value_type::boolean), physical_in("charge", domain::aux_ac, true), physical_out("nn", domain::low_voltage),
signal_out("ok", value_type::boolean), signal_out("u", value_type::real, "V")};
type.params = {number_param("voltage", 110.0, "V"), number_param("capacity", 100.0, "Ah"), number_param("min_voltage", 80.0, "V")};
type.single = true;
type.create = make_block<el_battery>;
return type;
}
private:
bool const m_charger;
double const m_voltage;
double const m_capacity;
double const m_minvoltage;
double m_charge;
};
} // namespace
void register_electric_blocks(catalog &Catalog)
{
Catalog.add(el_pantograph::describe());
Catalog.add(el_junction::describe());
Catalog.add(el_hsb::describe());
Catalog.add(el_contactor::describe());
Catalog.add(el_converter::describe());
Catalog.add(el_battery::describe());
Catalog.add(el_voltage_relay::describe());
Catalog.add(el_transformer::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,468 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "vehicle/graph/graph_instance.h"
#include "McZapkie/MOVER.h"
#include <array>
#include <cmath>
#include <limits>
// standard interfaces: between the vehicle and its neighbours (brake pipe, feed pipe, train heating), and between the
// vehicle and the AI. the neighbours may be .fiz vehicles, so these blocks work through the couplers of the vehicle body
namespace vgraph
{
namespace
{
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description)
{
block_type type;
type.id = std::move(Id);
type.category = "interfaces";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = block_origin::interface_profile;
return type;
}
param_def side_param()
{
return choice_param("side", {"front", "rear"}, "front");
}
// end cock of the brake pipe, or of the feed pipe, at one end of the vehicle
class if_cock final : public block
{
public:
struct port
{
enum : std::size_t
{
pipe,
cock
};
};
struct param
{
enum : std::size_t
{
side,
cock
};
};
if_cock(block_setup const &Setup, bool const Brakepipe)
: block(Setup), m_brakepipe(Brakepipe), m_side(choice(Setup, param::side) == 0 ? end::front : end::rear), m_switched(is_connected(Setup, port::cock)),
m_open(boolean(Setup, param::cock))
{
}
void update(frame &Frame) override
{
auto const open{m_switched ? get_bool(Frame, port::cock) : m_open};
auto &coupler{vehicle(Frame).Couplers[m_side]};
if (m_brakepipe)
{
coupler.BrakePipeCock = open;
}
else
{
coupler.FeedPipeCock = open;
}
}
static block_type describe(bool const Brakepipe)
{
auto type{Brakepipe ? make_type("if.pg", "Brake pipe coupling",
"End cock of the brake pipe at one end of the vehicle. Open (the default), the brake pipe exchanges air with the neighbouring vehicle.")
: make_type("if.pz", "Feed pipe coupling",
"End cock (yellow) of the feed pipe at one end of the vehicle. Closed by default; open, the feed pipe exchanges air with the neighbour.")};
type.ports = {Brakepipe ? physical_in("pg", domain::brake_pipe) : physical_in("pz", domain::feed_pipe), signal_in("cock", value_type::boolean, 0.0, "", true)};
type.params = {side_param(), boolean_param("cock", Brakepipe)};
if (Brakepipe)
{
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<if_cock>(Setup, true); };
}
else
{
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<if_cock>(Setup, false); };
}
return type;
}
private:
bool const m_brakepipe;
int const m_side;
bool const m_switched;
bool const m_open;
};
// train heating supply through the couplers. the voltage comes from the main circuit of the vehicle (Mains)
class if_heat final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
on,
i
};
};
explicit if_heat(block_setup const &Setup) : block(Setup), m_side(choice(Setup, 0) == 0 ? end::front : end::rear) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const voltage{forward_in(Frame, port::wn)};
auto const on{get_bool(Frame, port::on)};
body.HeatingAllow = on;
body.Heating = on && voltage > 0.0;
auto const current{std::abs(body.Couplers[m_side].power_high.current)};
add_back(Frame, port::wn, current);
set(Frame, port::i, current);
}
static block_type describe()
{
auto type{make_type("if.heat", "Train heating",
"Train heating supply through the coupler at one end: on connects the main circuit (behind the line breaker) to the heating line. i is the "
"current the train draws.")};
type.ports = {physical_in("wn", domain::high_voltage), signal_in("on", value_type::boolean, 1.0, "", true), signal_out("i", value_type::real, "A")};
type.params = {side_param()};
type.create = make_block<if_heat>;
return type;
}
private:
int const m_side;
};
// intents of the AI driver, and the feedback it reads. graph vehicles aren't driven by the AI yet: the outputs stay
// at rest until the AI adapter of the graph vehicles is in place
class if_ai final : public block
{
public:
struct port
{
enum : std::size_t
{
ready,
i_ratio,
slip,
pg,
zg,
bc,
vmax,
dir,
traction,
ed_brake,
brake,
emergency,
local_brake,
prepare,
shutdown,
doors_left,
doors_right,
lights,
horn,
pantographs,
sand
};
};
explicit if_ai(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto const &intents{Frame.body->ai};
set(Frame, port::dir, intents.dir);
set(Frame, port::traction, intents.traction);
set(Frame, port::ed_brake, intents.ed_brake);
set(Frame, port::brake, intents.brake);
set_bool(Frame, port::emergency, intents.emergency);
set(Frame, port::local_brake, intents.local_brake);
set_bool(Frame, port::prepare, intents.prepare);
set_bool(Frame, port::shutdown, intents.shutdown);
set_bool(Frame, port::doors_left, intents.doors_left);
set_bool(Frame, port::doors_right, intents.doors_right);
set(Frame, port::lights, intents.lights);
set_bool(Frame, port::horn, intents.horn);
set_bool(Frame, port::pantographs, intents.pantographs);
set_bool(Frame, port::sand, intents.sand);
auto &feedback{Frame.body->ai_reply};
feedback.ready = get_bool(Frame, port::ready);
feedback.i_ratio = get(Frame, port::i_ratio);
feedback.slip = get_bool(Frame, port::slip);
feedback.pg = get(Frame, port::pg);
feedback.zg = get(Frame, port::zg);
feedback.bc = get(Frame, port::bc);
feedback.vmax = get(Frame, port::vmax);
}
static block_type describe()
{
auto type{make_type("if.ai", "AI interface",
"What the AI driver wants (outputs) and what it needs to know (inputs). The vehicle's graph translates the intents to its controls. Not "
"used by the simulator yet: the AI doesn't drive graph vehicles.")};
type.ports = {signal_in("ready", value_type::boolean, 0.0, "", true),
signal_in("i_ratio", value_type::real, 0.0, "", true),
signal_in("slip", value_type::boolean, 0.0, "", true),
signal_in("pg", value_type::real, 0.0, "bar", true),
signal_in("zg", value_type::real, 0.0, "bar", true),
signal_in("bc", value_type::real, 0.0, "bar", true),
signal_in("vmax", value_type::real, 0.0, "km/h", true),
signal_out("dir", value_type::real),
signal_out("traction", value_type::real),
signal_out("ed_brake", value_type::real),
signal_out("brake", value_type::real),
signal_out("emergency", value_type::boolean),
signal_out("local_brake", value_type::real),
signal_out("prepare", value_type::boolean),
signal_out("shutdown", value_type::boolean),
signal_out("doors_left", value_type::boolean),
signal_out("doors_right", value_type::boolean),
signal_out("lights", value_type::integer),
signal_out("horn", value_type::boolean),
signal_out("pantographs", value_type::boolean),
signal_out("sand", value_type::boolean)};
type.single = true;
type.create = make_block<if_ai>;
return type;
}
};
// control line of the multiple unit control: a named train wire shared by the coupled graph vehicles. every vehicle
// publishes its contribution in its body state (body_state::lines); the others read what it published last, so the
// values pass between the vehicles with up to one step of delay. the line ends at a vehicle without a graph, at
// a vehicle without the line, or at a coupling without the required flag
class if_line final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
active,
out,
vehicles,
drivers
};
};
struct param
{
enum : std::size_t
{
name,
combine,
reverse,
pair,
coupling
};
};
enum class combination
{
max,
min,
sum,
maxabs,
any
};
explicit if_line(block_setup const &Setup)
: block(Setup), m_name(text(Setup, param::name)), m_pair(text(Setup, param::pair).empty() ? m_name : text(Setup, param::pair)),
m_combine(static_cast<combination>(std::max(0, choice(Setup, param::combine)))), m_reverse(boolean(Setup, param::reverse)),
m_flag(coupling_flag(choice(Setup, param::coupling))), m_drives(is_connected(Setup, port::in))
{
m_hints.fill(0);
}
void update(frame &Frame) override
{
auto &lines{Frame.body->lines};
if (m_entry < 0)
{
// the entry of the line in this vehicle, shared with the other blocks of the same line
m_entry = find(lines, m_name, 0);
if (m_entry < 0)
{
lines.push_back({m_name, 0.0, false});
m_entry = static_cast<int>(lines.size()) - 1;
}
}
auto &own{lines[m_entry]};
if (m_drives)
{
own.value = get(Frame, port::in);
own.active = get_bool(Frame, port::active);
}
m_drivers = 0;
m_vehicles = 1;
if (own.active)
{
add(own.value);
}
auto const *body{Frame.body->mover};
for (int side = 0; side < 2; ++side)
{
walk(*body, side, m_hints.data() + side * max_vehicles);
}
set(Frame, port::out, m_drivers > 0 ? m_result : 0.0);
set(Frame, port::vehicles, m_vehicles);
set(Frame, port::drivers, m_drivers);
}
static block_type describe()
{
auto type{make_type("if.line", "Train line",
"Named control line of the multiple unit control, shared by the coupled vehicles with the line (the coupling needs the selected flag). out "
"combines the values of the vehicles driving the line (in connected and active set), in the orientation of this vehicle: with reverse the "
"value of a vehicle facing the other way changes sign, and such a vehicle gives its pair line (if set) in place of this one.")};
type.ports = {signal_in("in", value_type::real, 0.0, "", true), signal_in("active", value_type::boolean, 1.0, "", true), signal_out("out", value_type::real),
signal_out("vehicles", value_type::integer), signal_out("drivers", value_type::integer)};
type.params = {text_param("name", "linia"), choice_param("combine", {"max", "min", "sum", "maxabs", "or"}, "max"), boolean_param("reverse", false), text_param("pair"),
choice_param("coupling", {"control", "permanent", "coupler", "highvoltage", "heating", "power110v", "power24v", "power3x400v"}, "control")};
type.create = make_block<if_line>;
return type;
}
private:
// longest chain of vehicles followed in each direction, also the guard against coupling loops
static constexpr int max_vehicles{64};
static int coupling_flag(int const Choice)
{
static int const flags[]{coupling::control, coupling::permanent, coupling::coupler, coupling::highvoltage, coupling::heating, coupling::power110v, coupling::power24v,
coupling::power3x400v};
return flags[std::clamp(Choice, 0, static_cast<int>(std::size(flags)) - 1)];
}
// index of the line with the specified name, checking the hint first, or -1
static int find(std::vector<train_line_value> const &Lines, std::string const &Name, int const Hint)
{
if (Hint >= 0 && Hint < static_cast<int>(Lines.size()) && Lines[Hint].name == Name)
{
return Hint;
}
for (std::size_t idx = 0; idx < Lines.size(); ++idx)
{
if (Lines[idx].name == Name)
{
return static_cast<int>(idx);
}
}
return -1;
}
void add(double const Value)
{
if (m_drivers == 0)
{
m_result = (m_combine == combination::any ? (Value != 0.0 ? 1.0 : 0.0) : Value);
}
else
{
switch (m_combine)
{
case combination::max:
m_result = std::max(m_result, Value);
break;
case combination::min:
m_result = std::min(m_result, Value);
break;
case combination::sum:
m_result += Value;
break;
case combination::maxabs:
m_result = (std::abs(Value) > std::abs(m_result) ? Value : m_result);
break;
case combination::any:
m_result = (m_result != 0.0 || Value != 0.0 ? 1.0 : 0.0);
break;
}
}
++m_drivers;
}
// follows the coupled vehicles from the specified side of the vehicle. a neighbour coupled by the same side as the one
// leading to it (front to front, rear to rear) faces the other way
void walk(TMoverParameters const &Body, int Side, int *Hints)
{
auto const *current{&Body};
auto facing{1}; // orientation of the current vehicle relative to this one
for (int count = 0; count < max_vehicles; ++count)
{
auto const &coupler{current->Couplers[Side]};
if (coupler.Connected == nullptr || (coupler.CouplingFlag & m_flag) == 0 || coupler.Connected == &Body)
{
return;
}
auto const *neighbour{coupler.Connected};
auto const connectedside{coupler.ConnectedNr};
facing = (Side != connectedside ? facing : -facing);
if (neighbour->Graph == nullptr)
{
return;
}
auto const &lines{neighbour->Graph->body().lines};
auto const index{find(lines, facing > 0 ? m_name : m_pair, Hints[count])};
if (index < 0)
{
return;
}
Hints[count] = index;
++m_vehicles;
auto const &line{lines[index]};
if (line.active)
{
add(m_reverse && facing < 0 ? -line.value : line.value);
}
current = neighbour;
Side = 1 - connectedside;
}
}
std::string const m_name;
std::string const m_pair;
combination const m_combine;
bool const m_reverse;
int const m_flag;
bool const m_drives;
int m_entry{-1};
std::array<int, 2 * max_vehicles> m_hints;
double m_result{0.0};
int m_drivers{0};
int m_vehicles{1};
};
} // namespace
void register_interface_blocks(catalog &Catalog)
{
Catalog.add(if_cock::describe(true));
Catalog.add(if_cock::describe(false));
Catalog.add(if_heat::describe());
Catalog.add(if_ai::describe());
Catalog.add(if_line::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,278 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_format.h"
#include "vehicle/graph/graph_lua.h"
#include <cmath>
#ifdef WITH_LUA
#include <lua.hpp>
#endif
// function block defined by a lua script. its ports and numeric parameters come from the script
namespace vgraph
{
namespace
{
bool resolve_lua_block(node_desc const &Node, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error)
{
if (false == lua_available())
{
Error = "lua blocks need a simulator built with lua support (WITH_LUA)";
return false;
}
if (Node.script.empty())
{
Error = "lua block without a script";
return false;
}
lua_block_definition definition;
if (false == parse_lua_block(Node.script, Node.id, definition, Error))
{
Error = "lua: " + Error;
return false;
}
Ports = std::move(definition.ports);
Params = std::move(definition.params);
return true;
}
#ifdef WITH_LUA
class lua_block final : public block
{
public:
explicit lua_block(block_setup const &Setup) : block(Setup), m_host(Setup.lua), m_nodeid(Setup.node_id)
{
for (std::size_t idx = 0; idx < Setup.ports.size(); ++idx)
{
auto const &port{Setup.ports[idx]};
(port.direction == port_direction::in ? m_inputs : m_outputs).push_back({idx, port.key, port.type});
}
if (m_host == nullptr || m_host->state() == nullptr)
{
fail("no lua state");
return;
}
std::string error;
m_definition = m_host->load_block(Setup.script, Setup.node_id, error);
if (m_definition < 0)
{
fail(error);
return;
}
auto *L{m_host->state()};
auto const top{lua_gettop(L)};
lua_rawgeti(L, LUA_REGISTRYINDEX, m_definition);
auto const definition{lua_gettop(L)};
lua_getfield(L, definition, "rate");
auto const rate{lua_isnumber(L, -1) ? lua_tonumber(L, -1) : 0.0};
lua_pop(L, 1);
m_period = (rate > 0.0 ? 1.0 / rate : 0.0);
// self: copy of the state table, and the parameters with values from the graph for the numeric ones
lua_newtable(L);
auto const self{lua_gettop(L)};
lua_getfield(L, definition, "state");
if (lua_istable(L, -1))
{
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
lua_pushvalue(L, -2);
lua_insert(L, -2);
lua_rawset(L, self);
}
}
lua_pop(L, 1);
lua_getfield(L, definition, "params");
if (lua_istable(L, -1))
{
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
if (lua_type(L, -2) == LUA_TSTRING && lua_type(L, -1) == LUA_TNUMBER)
{
auto const paramindex{find_param(Setup.param_defs, std::string{"lua."} + lua_tostring(L, -2))};
if (paramindex >= 0)
{
lua_pop(L, 1);
lua_pushnumber(L, Setup.params[paramindex].number);
}
}
lua_pushvalue(L, -2);
lua_insert(L, -2);
lua_rawset(L, self);
}
}
lua_pop(L, 1);
m_self = luaL_ref(L, LUA_REGISTRYINDEX);
lua_newtable(L);
m_in = luaL_ref(L, LUA_REGISTRYINDEX);
lua_newtable(L);
m_out = luaL_ref(L, LUA_REGISTRYINDEX);
lua_settop(L, top);
}
~lua_block() override
{
if (m_host != nullptr)
{
m_host->release(m_definition);
m_host->release(m_self);
m_host->release(m_in);
m_host->release(m_out);
}
}
void update(frame &Frame) override
{
if (m_fault)
{
// outputs keep their last values
return;
}
auto dt{Frame.dt};
if (m_period > 0.0)
{
m_accumulated += Frame.dt;
if (m_accumulated + 1e-9 < m_period)
{
return;
}
dt = m_accumulated;
m_accumulated = 0.0;
}
auto *L{m_host->state()};
auto const top{lua_gettop(L)};
lua_rawgeti(L, LUA_REGISTRYINDEX, m_in);
for (auto const &input : m_inputs)
{
auto const value{get(Frame, input.port)};
switch (input.type)
{
case value_type::boolean:
lua_pushboolean(L, value > 0.5);
break;
case value_type::integer:
lua_pushnumber(L, std::round(value));
break;
default:
lua_pushnumber(L, value);
break;
}
lua_setfield(L, -2, input.key.c_str());
}
lua_pop(L, 1);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_definition);
lua_getfield(L, -1, "update");
lua_remove(L, -2);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_self);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_in);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_out);
lua_pushnumber(L, dt);
std::string error;
if (false == m_host->call(4, error))
{
lua_settop(L, top);
fail(error);
return;
}
lua_rawgeti(L, LUA_REGISTRYINDEX, m_out);
for (auto const &output : m_outputs)
{
lua_getfield(L, -1, output.key.c_str());
auto value{0.0};
if (lua_isboolean(L, -1))
{
value = (lua_toboolean(L, -1) ? 1.0 : 0.0);
}
else if (lua_type(L, -1) == LUA_TNUMBER)
{
value = lua_tonumber(L, -1);
if (output.type == value_type::integer)
{
value = std::round(value);
}
}
lua_pop(L, 1);
set(Frame, output.port, value);
}
lua_settop(L, top);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "fault";
}
double state_value(std::size_t) const override
{
return m_fault ? 1.0 : 0.0;
}
private:
struct port_entry
{
std::size_t port;
std::string key;
value_type type;
};
void fail(std::string const &Reason)
{
// a failing block stops, and the rest of the vehicle keeps working
m_fault = true;
log_message("lua [" + m_nodeid + "]: block stopped: " + Reason);
}
lua_host *m_host;
std::string m_nodeid;
std::vector<port_entry> m_inputs;
std::vector<port_entry> m_outputs;
int m_definition{-1};
int m_self{-1};
int m_in{-1};
int m_out{-1};
double m_period{0.0};
double m_accumulated{0.0};
bool m_fault{false};
};
#endif
} // namespace
void register_script_blocks(catalog &Catalog)
{
block_type type;
type.id = "lua.block";
type.category = "script";
type.name = "Lua block";
type.description = "Function block defined by a lua 5.1 script: block \"name\" { inputs, outputs, params, state, rate, update = function(self, i, o, dt) ... end }.";
type.origin = block_origin::script;
type.dynamic_ports = true;
type.resolve = resolve_lua_block;
#ifdef WITH_LUA
type.create = make_block<lua_block>;
#else
// graphs with lua blocks are rejected when loaded, so blocks of this type are never created
type.create = nullptr;
#endif
Catalog.add(std::move(type));
}
} // namespace vgraph

View File

@@ -0,0 +1,186 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_format.h"
// modules: graphs of logic made once and used by many vehicles. a node of type "module" names the file of the module;
// when the vehicle graph is loaded, the node is replaced with the blocks of the module (expand_modules), connected
// through the module's interface blocks, module.input and module.output, one per port of the module.
// the interface blocks work also on their own, so a module file can be tried with -graph-run: an input gives its
// default value (forced values come from the script), an output shows the value it receives
namespace vgraph
{
namespace
{
std::vector<std::string> interface_types()
{
std::vector<std::string> result{"bool", "int", "float"};
for (std::size_t idx = 1; idx < static_cast<std::size_t>(domain::count_); ++idx)
{
result.emplace_back(info(static_cast<domain>(idx)).id);
}
return result;
}
std::vector<param_def> interface_params(bool const Input)
{
std::vector<param_def> params{choice_param("type", interface_types(), "float")};
if (Input)
{
params.push_back(number_param("default", 0.0));
}
params.push_back(text_param("label"));
return params;
}
// the port of an interface block, of the type chosen by its "type" parameter
bool interface_port(node_desc const &Node, bool const Input, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error)
{
std::string type{"float"};
for (auto const &param : Node.params)
{
if (param.first == "type")
{
type = param.second.text;
}
}
// the input of the module is an output of the interface block inside it, and the other way round
auto const key{Input ? "out" : "in"};
if (type == "bool" || type == "int" || type == "float")
{
auto const valuetype{type == "bool" ? value_type::boolean : type == "int" ? value_type::integer : value_type::real};
Ports = {Input ? signal_out(key, valuetype) : signal_in(key, valuetype, 0.0, "", true)};
}
else
{
domain dom;
if (false == parse_domain(type, dom) || dom == domain::signal)
{
Error = "unknown type \"" + type + "\" of the module port; expected bool, int, float or a physical domain";
return false;
}
Ports = {Input ? physical_out(key, dom) : physical_in(key, dom, true)};
}
Params = interface_params(Input);
return true;
}
bool resolve_input(node_desc const &Node, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error)
{
return interface_port(Node, true, Ports, Params, Error);
}
bool resolve_output(node_desc const &Node, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error)
{
return interface_port(Node, false, Ports, Params, Error);
}
bool resolve_module(node_desc const &, std::vector<port_def> &, std::vector<param_def> &, std::string &Error)
{
// expand_modules replaces these nodes when the file is loaded, so one left here means the module couldn't be read
Error = "the module wasn't expanded; graphs with modules have to be loaded from a file, next to the module files";
return false;
}
class module_input final : public block
{
public:
explicit module_input(block_setup const &Setup) : block(Setup), m_signal(Setup.ports[0].dom == domain::signal), m_value(number(Setup, 1)) {}
void update(frame &Frame) override
{
if (m_signal)
{
set(Frame, 0, m_value);
}
else
{
set_forward(Frame, 0, m_value);
}
}
private:
bool const m_signal;
double const m_value;
};
class module_output final : public block
{
public:
explicit module_output(block_setup const &Setup) : block(Setup), m_signal(Setup.ports[0].dom == domain::signal), m_connected(is_connected(Setup, 0)) {}
void update(frame &Frame) override
{
if (false == m_connected)
{
return;
}
m_value = m_signal ? get(Frame, 0) : forward_in(Frame, 0);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "value";
}
double state_value(std::size_t) const override
{
return m_value;
}
private:
bool const m_signal;
bool const m_connected;
double m_value{0.0};
};
block_type interface_type(bool const Input)
{
block_type type;
type.id = Input ? "module.input" : "module.output";
type.category = "module";
type.name = Input ? "Module input" : "Module output";
type.description = Input ? "Input of a module: the value given to the module node in the vehicle graph. On its own it gives the default value."
: "Output of a module: the value the module node gives in the vehicle graph.";
type.origin = block_origin::primitive;
type.dynamic_ports = true;
type.resolve = Input ? resolve_input : resolve_output;
type.params = interface_params(Input);
type.create = Input ? make_block<module_input> : make_block<module_output>;
return type;
}
} // namespace
void register_module_blocks(catalog &Catalog)
{
Catalog.add(interface_type(true));
Catalog.add(interface_type(false));
block_type type;
type.id = std::string{module_type};
type.category = "module";
type.name = "Module";
type.description = "Graph of logic from another file (a module), used as one block. Its ports are the module.input and module.output blocks "
"of the module; the file is given relative to the vehicle graph.";
type.origin = block_origin::primitive;
type.dynamic_ports = true;
type.resolve = resolve_module;
type.params = {text_param("file")};
type.create = nullptr;
Catalog.add(std::move(type));
}
} // namespace vgraph

View File

@@ -0,0 +1,801 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <cmath>
// air supply and brakes. the blocks drive the brake equipment of the vehicle body (TMoverParameters): its brake pipe and
// feed pipe, which exchange air with the neighbouring vehicles, and the distributor and driver's brake valves created
// by CheckLocomotiveParameters from the parameters of these blocks, with the same code as for the .fiz vehicles.
// the sequence of the operations follows TMoverParameters::UpdatePipePressure and UpdateScndPipePressure
namespace vgraph
{
namespace
{
// cross-section of the feed pipe connection, as in TMoverParameters::UpdateScndPipePressure
double constexpr feed_pipe_section{0.5067};
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Category, std::string Name, std::string Description, block_origin Origin)
{
block_type type;
type.id = std::move(Id);
type.category = std::move(Category);
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = Origin;
return type;
}
// compressor of the main reservoir
class pn_compressor final : public block
{
public:
struct port
{
enum : std::size_t
{
power,
power_hv,
on,
air,
run
};
};
struct param
{
enum : std::size_t
{
flow,
max_pressure,
min_voltage,
power
};
};
explicit pn_compressor(block_setup const &Setup)
: block(Setup), m_aux(is_connected(Setup, port::power)), m_hv(is_connected(Setup, port::power_hv)), m_flow(number(Setup, param::flow)),
m_maxpressure(number(Setup, param::max_pressure)), m_minvoltage(number(Setup, param::min_voltage)), m_power(number(Setup, param::power))
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
// without supply connections the compressor is assumed to be powered
auto powered{false == m_aux && false == m_hv};
auto const auxvoltage{m_aux ? forward_in(Frame, port::power) : 0.0};
auto const hvvoltage{m_hv ? forward_in(Frame, port::power_hv) : 0.0};
powered = powered || (m_aux && auxvoltage >= m_minvoltage && auxvoltage > 0.0) || (m_hv && hvvoltage >= m_minvoltage && hvvoltage > 0.0);
auto const run{powered && get_bool(Frame, port::on) && body.VeselVolume > 0.0 && m_maxpressure > 0.0};
body.CompressorAllow = get_bool(Frame, port::on);
body.CompressorFlag = run;
if (run)
{
// TMoverParameters::CompressorCheck, compressor working at steady pace
body.CompressedVolume += m_flow * (2.0 * m_maxpressure - body.Compressor) / m_maxpressure * Frame.dt;
if (m_aux && auxvoltage > 0.0)
{
add_back(Frame, port::power, m_power);
}
else if (m_hv && hvvoltage > 0.0)
{
add_back(Frame, port::power_hv, m_power / hvvoltage);
}
}
set_forward(Frame, port::air, body.Compressor);
set_bool(Frame, port::run, run);
}
static block_type describe()
{
auto type{make_type("pn.compressor", "pneumatics", "Compressor",
"Compressor of the main reservoir. Without a supply connected it's assumed to be powered; with both connected either one is enough.",
block_origin::extracted)};
type.ports = {physical_in("power", domain::aux_ac, true), physical_in("power_hv", domain::high_voltage, true), signal_in("on", value_type::boolean),
physical_out("air", domain::air), signal_out("run", value_type::boolean)};
type.params = {fiz(number_param("flow", 0.05, "m3/s"), "Brake:", "CompressorSpeed"), number_param("max_pressure", 10.0, "bar"), number_param("min_voltage", 0.0, "V"),
number_param("power", 10000.0, "W")};
type.single = true;
type.create = make_block<pn_compressor>;
return type;
}
private:
bool const m_aux;
bool const m_hv;
double const m_flow;
double const m_maxpressure;
double const m_minvoltage;
double const m_power;
};
// pressure switch of the compressor: asks for air below the lower pressure, until the upper one is reached
class pn_governor final : public block
{
public:
struct port
{
enum : std::size_t
{
p,
need
};
};
struct param
{
enum : std::size_t
{
min,
max
};
};
explicit pn_governor(block_setup const &Setup) : block(Setup), m_min(number(Setup, param::min)), m_max(number(Setup, param::max)) {}
void update(frame &Frame) override
{
auto const pressure{get(Frame, port::p)};
if (pressure < m_min)
{
m_need = true;
}
else if (pressure > m_max)
{
m_need = false;
}
vehicle(Frame).CompressorGovernorLock = false == m_need;
}
void commit(frame &Frame) override
{
set_bool(Frame, port::need, m_need);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "need";
}
double state_value(std::size_t) const override
{
return m_need ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("pn.governor", "pneumatics", "Pressure governor", "Pressure switch of the compressor: on below min, off above max.", block_origin::primitive)};
type.ports = {signal_in("p", value_type::real, 0.0, "bar"), signal_out("need", value_type::boolean)};
type.params = {fiz(number_param("min", 7.5, "bar"), "Brake:", "MinCP"), fiz(number_param("max", 8.5, "bar"), "Brake:", "MaxCP")};
type.deferred = true;
type.create = make_block<pn_governor>;
return type;
}
private:
double const m_min;
double const m_max;
bool m_need{true};
};
// main reservoir of the vehicle, and its connection with the feed pipe
class pn_reservoir final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
out,
pz,
p
};
};
explicit pn_reservoir(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
if (body.VeselVolume > 0.0)
{
// TMoverParameters::CompressorCheck: safety valve and leaks
body.Compressor = body.CompressedVolume / body.VeselVolume;
body.EmergencyValveOpen = body.Compressor > (body.EmergencyValveOpen ? body.EmergencyValveOff : body.EmergencyValveOn);
if (body.EmergencyValveOpen)
{
body.CompressedVolume -= PF(0, body.Compressor, body.EmergencyValveArea) * dt;
}
body.CompressedVolume = std::max(0.0, body.CompressedVolume - dt * body.AirLeakRate * 0.1);
body.Compressor = body.CompressedVolume / body.VeselVolume;
}
else
{
body.Compressor = 0.0;
}
set_forward(Frame, port::out, body.Compressor);
set_forward(Frame, port::pz, body.ScndPipePress);
set(Frame, port::p, body.Compressor);
}
// the feed pipe, after the other blocks took their air from it (TMoverParameters::UpdateScndPipePressure)
void late_update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
// the end cocks (if.pz) are checked by the exchange
auto const exchange{body.ScndPipeExchange(dt)};
if (body.VeselVolume > 0.0)
{
auto const flow{PF(body.Compressor, body.ScndPipePress, feed_pipe_section) * dt};
body.CompressedVolume += flow / 1000.0;
body.Pipe2->Flow(-flow);
}
body.Pipe2->Flow(exchange);
body.Pipe2->Act();
body.ScndPipePress = body.Pipe2->P();
if (body.ScndPipePress < -1.0)
{
body.ScndPipePress = -1.0;
body.Pipe2->CreatePress(-1.0);
body.Pipe2->Act();
}
if (body.CompressedVolume < 0.0)
{
body.CompressedVolume = 0.0;
}
}
static block_type describe()
{
auto type{make_type("pn.reservoir", "pneumatics", "Main reservoir",
"The main reservoir of the vehicle. It feeds the feed pipe (pz), which the brake valves and the neighbouring vehicles take air from.",
block_origin::simulator)};
type.ports = {physical_in("in", domain::air, true), physical_out("out", domain::air), physical_out("pz", domain::feed_pipe), signal_out("p", value_type::real, "bar")};
type.params = {fiz(number_param("volume", 0.0, "m3"), "Brake:", "Vv")};
type.single = true;
type.late = true;
type.create = make_block<pn_reservoir>;
return type;
}
};
// brake pipe of the vehicle
class br_pipe final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
vent,
dist,
couplers,
p
};
};
explicit br_pipe(block_setup const &Setup) : block(Setup), m_trainlength(std::max(0.0, number(Setup, 0))) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
if (Frame.body->standalone && m_trainlength > 0.0 && false == m_extended)
{
// without the simulator there are no neighbours: the pipe gets the volume of the train instead
auto const pressure{body.Pipe->P()};
body.Pipe->CreateCap((std::max(body.Dim.L, 14.0) + 0.5 + m_trainlength) * body.Spg);
body.Pipe->CreatePress(pressure);
m_extended = true;
}
// emergency valve: the vent input, and the emergency brakes the simulator applies to every vehicle
body.EmergencyValveFlow = 0.0;
if (get_bool(Frame, port::vent) || body.RadioStopFlag || body.AlarmChainFlag)
{
body.EmergencyValveFlow = PF(0, body.PipePress, 0.15) * dt;
}
body.Pipe->Flow(-body.EmergencyValveFlow);
body.Pipe->Flow(-body.PipePress * 0.001 * dt);
body.Pipe->Act();
body.PipePress = body.Pipe->P();
publish(Frame, body.PipePress);
}
// exchange with the neighbours, after the distributor took its air
void late_update(frame &Frame) override
{
auto &body{vehicle(Frame)};
// the end cocks (if.pg) are checked by the exchange
body.Pipe->Flow(body.GetDVc(Frame.dt));
body.Pipe->Act();
body.PipePress = body.Pipe->P();
if (body.PipePress < -1.0)
{
body.PipePress = -1.0;
body.Pipe->CreatePress(-1.0);
body.Pipe->Act();
}
}
static block_type describe()
{
auto type{make_type("br.pipe", "brakes", "Brake pipe",
"The brake pipe of the vehicle, connected through the couplers with the neighbouring vehicles. vent opens the emergency valve. "
"preview_train_length adds the pipe of a train of that length when the graph runs without the simulator.",
block_origin::native)};
type.ports = {physical_in("in", domain::brake_pipe, true), signal_in("vent", value_type::boolean, 0.0, "", true), physical_out("dist", domain::brake_pipe),
physical_out("couplers", domain::brake_pipe), signal_out("p", value_type::real, "bar")};
type.params = {number_param("preview_train_length", 0.0, "m")};
type.single = true;
type.late = true;
type.create = make_block<br_pipe>;
return type;
}
private:
double const m_trainlength; // without the simulator: length of the brake pipe of the train behind the vehicle
bool m_extended{false};
void publish(frame &Frame, double const Pressure)
{
set_forward(Frame, port::dist, Pressure);
set_forward(Frame, port::couplers, Pressure);
set(Frame, port::p, Pressure);
}
};
// driver's brake valve
class br_handle final : public block
{
public:
struct port
{
enum : std::size_t
{
zg,
pos,
emerg,
reductor,
pg,
p_ctl
};
};
explicit br_handle(block_setup const &Setup) : block(Setup), m_reductor(is_connected(Setup, port::reductor)) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
auto position{get(Frame, port::pos)};
if (get_bool(Frame, port::emerg))
{
position = body.Handle->GetPos(bh_EB);
}
body.BrakeCtrlPosR = position;
body.BrakeCtrlPos = static_cast<int>(std::floor(position + 0.5));
// brake pipe lock in the emergency state, per the .fiz parameters (LPOn, LPOff, HandlePipeUnlockPos, EmergencyCutsOffHandle)
body.LockPipe = body.PipePress < (body.LockPipe ? body.LockPipeOff : body.LockPipeOn);
auto const lock{(body.LockPipe && false == body.UnlockPipe && position > body.HandleUnlock) || (body.EmergencyCutsOffHandle && body.EmergencyValveFlow > 0)};
auto const supply{lock ? body.PipePress + 0.00001 : body.ScndPipePress};
body.Handle->SetReductor(m_reductor ? get(Frame, port::reductor) : body.BrakeCtrlPos2);
auto const flow{body.Handle->GetPF(position, body.PipePress, supply, dt, body.EqvtPipePress)};
if (flow < 0)
{
if (body.Compressor > body.ScndPipePress)
{
body.CompressedVolume += flow / 1500.0;
body.Pipe2->Flow(flow / 3.0);
}
else
{
body.Pipe2->Flow(flow);
}
}
body.Pipe->Flow(-flow);
// normalised as in UpdatePipePressure, for the sounds of the valve
body.dpMainValve = flow / (std::max(body.Dim.L, 1.0) * std::max(body.Spg, 0.01) * 20) / (100.0 * dt);
set_forward(Frame, port::pg, body.PipePress);
set(Frame, port::p_ctl, body.Handle->GetCP());
}
static block_type describe()
{
auto type{make_type("br.handle", "brakes", "Driver's brake valve",
"Driver's brake valve (TDriverHandle). pos is the position of the handle in the valve's own scale, e.g. -1..6 for FV4a; emerg moves it to the "
"emergency position. It takes air from the feed pipe of the main reservoir.",
block_origin::simulator)};
type.ports = {physical_in("zg", domain::air),
signal_in("pos", value_type::real),
signal_in("emerg", value_type::boolean, 0.0, "", true),
signal_in("reductor", value_type::real, 0.0, "bar", true),
physical_out("pg", domain::brake_pipe),
signal_out("p_ctl", value_type::real, "bar")};
type.params = {
fiz(choice_param("type", {"FV4a", "MHZ_EN57", "MHZ_K5P", "MHZ_K8P", "MHZ_6P", "M394", "Knorr", "St113", "FVel6", "FVE408", "D2", "Westinghouse", "test"}, "FV4a"), "Cntrl.",
"BrakeHandle"),
fiz(boolean_param("auto_overload", false), "Cntrl.", "HAO"),
fiz(boolean_param("manual_overload", false), "Cntrl.", "HMO"),
fiz(number_param("gdp1", 0.0), "Cntrl.", "HGDP1"),
fiz(number_param("gdp2", 0.0), "Cntrl.", "HGDP2"),
fiz(number_param("overload_max", 0.0, "bar"), "Cntrl.", "OMP"),
fiz(number_param("overload_decrease", 0.0, "bar/s"), "Cntrl.", "OPD"),
fiz(number_param("lock_on", -1.0, "bar"), "Brake:", "LPOn"),
fiz(number_param("lock_off", -1.0, "bar"), "Brake:", "LPOff"),
fiz(number_param("unlock_pos", -3.0), "Brake:", "HandlePipeUnlockPos"),
fiz(boolean_param("emergency_cuts_off", false), "Brake:", "EmergencyCutsOffHandle"),
};
type.single = true;
type.create = make_block<br_handle>;
return type;
}
private:
bool const m_reductor;
};
// independent (local) brake valve
class br_local_handle final : public block
{
public:
struct port
{
enum : std::size_t
{
zg,
pos,
bc
};
};
explicit br_local_handle(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const position{std::clamp(get(Frame, port::pos), 0.0, 1.0)};
body.LocalBrakePosA = position;
body.dpLocalValve = body.LocHandle->GetPF(position, body.Hamulec->GetBCP(), body.ScndPipePress, Frame.dt, 0);
body.LocBrakePress = body.LocHandle->GetCP();
set_forward(Frame, port::bc, body.LocBrakePress);
}
static block_type describe()
{
auto type{make_type("br.local_handle", "brakes", "Independent brake valve",
"Independent (local) brake valve of the locomotive. pos 0..1 is the position of the handle; bc is the pressure it sets for the distributor.",
block_origin::simulator)};
type.ports = {physical_in("zg", domain::air), signal_in("pos", value_type::real), physical_out("bc", domain::air)};
type.params = {fiz(choice_param("type", {"FD1", "Knorr", "Westinghouse"}, "FD1"), "Cntrl.", "LocBrakeHandle")};
type.single = true;
type.create = make_block<br_local_handle>;
return type;
}
};
// distributor (brake valve) of the vehicle, with its auxiliary reservoir and brake cylinders
class br_distributor final : public block
{
public:
struct port
{
enum : std::size_t
{
pg,
feed,
local,
ed,
release,
cyl,
bc,
aux,
crp
};
};
explicit br_distributor(block_setup const &Setup)
: block(Setup), m_local(is_connected(Setup, port::local)), m_ed(is_connected(Setup, port::ed)), m_release(is_connected(Setup, port::release))
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
auto &brake{*body.Hamulec};
brake.ForceLeak(dt * body.AirLeakRate * 0.25);
if (m_local)
{
body.LocBrakePress = forward_in(Frame, port::local);
brake.SetLBP(body.LocBrakePress);
}
// load dependent brake force, as in TMoverParameters::UpdatePipePressure
switch (body.BrakeValve)
{
case TBrakeValve::K:
case TBrakeValve::W:
case TBrakeValve::KE:
case TBrakeValve::EStED:
case TBrakeValve::ESt3AL2:
case TBrakeValve::NESt3:
case TBrakeValve::ESt4:
case TBrakeValve::ESt3:
{
brake.PLC(body.MBPM < 2 ? body.MaxBrakePress[body.LoadFlag] : body.TotalMass - body.Mred);
break;
}
case TBrakeValve::EP2:
case TBrakeValve::EP1:
{
brake.PLC(body.TotalMass - body.Mred);
break;
}
default:
{
break;
}
}
if (m_ed)
{
brake.SetED(std::clamp(get(Frame, port::ed), 0.0, 1.0));
}
if (m_release)
{
brake.Releaser(get_bool(Frame, port::release) ? 1 : 0);
}
body.CntrlPipePress = brake.GetVRP();
// the brake pipe
auto const connected{(brake.GetBrakeStatus() & b_dmg) == b_dmg ? 0.0 : 1.0};
body.Pipe->Flow(connected * brake.GetPF(connected * body.PipePress, dt, body.Vel));
if (body.ASBType == 128)
{
brake.ASB(int(body.SlippingWheels && body.Vel > 1) * (1 + 2 * int(body.nrot_eps < -0.01)));
}
// the feed pipe
body.Pipe2->Flow(brake.GetHPFlow(body.ScndPipePress, dt));
body.BrakePress = brake.GetBCP();
body.Volume = brake.GetBRP();
set_forward(Frame, port::cyl, body.BrakePress);
set(Frame, port::bc, body.BrakePress);
set(Frame, port::aux, body.Volume);
set(Frame, port::crp, brake.GetCRP());
}
static block_type describe()
{
auto type{make_type("br.distributor", "brakes", "Distributor",
"Distributor of the automatic brake (TBrake), with its auxiliary reservoir and the brake cylinders. local is the pressure of the independent "
"brake valve; ed (0..1) is the share of the electrodynamic brake for the blending of the EStED type; release operates the releaser.",
block_origin::simulator)};
type.ports = {physical_in("pg", domain::brake_pipe),
physical_in("feed", domain::feed_pipe, true),
physical_in("local", domain::air, true),
signal_in("ed", value_type::real, 0.0, "", true),
signal_in("release", value_type::boolean, 0.0, "", true),
physical_out("cyl", domain::air),
signal_out("bc", value_type::real, "bar"),
signal_out("aux", value_type::real, "bar"),
signal_out("crp", value_type::real, "bar")};
type.params = {
fiz(choice_param("type", {"W", "W_Lu_L", "W_Lu_XR", "W_Lu_VI", "K", "Kg", "Kp", "Kss", "Kkg", "Kkp", "Kks", "Hikp1", "Hikss", "Hikg1", "KE", "SW", "EStED", "NESt3", "ESt3",
"LSt", "ESt4", "ESt3AL2", "EP1", "EP2", "M483", "CV1_L_TR", "CV1", "CV1_R"},
"ESt3"),
"Brake:", "BrakeValve"),
fiz(choice_param("mode", {"G", "P", "R", "GP", "PR", "GPR", "GPR+Mg", "PR+Mg"}, "GP"), "Cntrl.", "BrakeDelays"),
fiz(number_param("max_bp", 3.8, "bar"), "Brake:", "MaxBP"),
fiz(number_param("max_lbp", 0.0, "bar"), "Brake:", "MaxLBP"),
fiz(number_param("tare_max_bp", 0.0, "bar"), "Brake:", "TareMaxBP"),
fiz(number_param("max_asbp", 0.0, "bar"), "Brake:", "MaxASBP"),
fiz(number_param("cyl_count", 2.0), "Brake:", "BCN"),
fiz(number_param("cyl_radius", 0.178, "m"), "Brake:", "BCR"),
fiz(number_param("cyl_dist", 0.1, "m"), "Brake:", "BCD"),
fiz(number_param("volume", 0.0, "l"), "Brake:", "BVV"),
fiz(number_param("rm", 1.0), "Brake:", "RM"),
fiz(number_param("rv", 55.0, "km/h"), "Brake:", "RV"),
fiz(number_param("valve_size", 0.0), "Brake:", "Size"),
fiz(number_param("delay1", 0.0, "s"), "Cntrl.", "BDelay1"),
fiz(number_param("delay2", 0.0, "s"), "Cntrl.", "BDelay2"),
fiz(number_param("delay3", 0.0, "s"), "Cntrl.", "BDelay3"),
fiz(number_param("delay4", 0.0, "s"), "Cntrl.", "BDelay4"),
fiz(choice_param("asb", {"none", "manual", "automatic", "yes"}, "none"), "Cntrl.", "ASB"),
};
type.single = true;
type.create = make_block<br_distributor>;
return type;
}
private:
bool const m_local;
bool const m_ed;
bool const m_release;
};
// brake rigging: from the cylinder pressure to the brake force at the wheels
class br_rigging final : public block
{
public:
struct port
{
enum : std::size_t
{
bc,
spring,
force
};
};
struct param
{
enum : std::size_t
{
bcm,
bcs,
bsa,
bre,
nbpa,
material
};
};
explicit br_rigging(block_setup const &Setup) : block(Setup), m_material(make_friction_material(material_id(text(Setup, param::material)))) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
// the cylinders are described by the distributor (BCR, BCN), the rigging by this block; both are in the body
brake_rigging_setup setup;
setup.press_to_force = body.P2FTrans;
setup.cylinder_spring = body.BrakeCylSpring;
setup.cylinder_mult = body.BrakeCylMult[0];
setup.slack_adjuster = body.BrakeSlckAdj;
setup.rigging_efficiency = body.BrakeRigEff;
setup.cylinder_count = body.BrakeCylNo;
setup.axles = body.NAxles;
setup.blocks_per_axle = body.NBpA;
auto const shoeforce{brake_rigging_shoe_force(setup, std::max(0.0, get(Frame, port::spring)), get(Frame, port::bc), body.Ntotal)};
auto const friction{m_material->GetFC(shoeforce, body.Vel)};
body.UnitBrakeForce = friction * shoeforce * 1000.0;
auto const force{body.UnitBrakeForce * body.NAxles * std::max(1, body.NBpA)};
Frame.body->brake_force += force;
set(Frame, port::force, force);
}
static block_type describe()
{
auto type{make_type("br.rigging", "brakes", "Brake rigging",
"Brake rigging and shoes or discs: the brake force from the cylinder pressure (bc) and the force of the spring brake (spring, kN). The force is "
"applied at the wheels of the vehicle.",
block_origin::extracted)};
type.ports = {signal_in("bc", value_type::real, 0.0, "bar"), signal_in("spring", value_type::real, 0.0, "kN", true), signal_out("force", value_type::real, "N")};
type.params = {
fiz(number_param("bcm", 1.0), "Brake:", "BCM"),
fiz(number_param("bcs", 0.0, "kN"), "Brake:", "BCS"),
fiz(number_param("bsa", 0.0, "kN"), "Brake:", "BSA"),
fiz(number_param("bre", 1.0), "Brake:", "BRE"),
fiz(number_param("nbpa", 1.0), "Brake:", "NBpA"),
fiz(choice_param("material", {"P10", "P10-Bg", "P10-Bgu", "P10yBg", "P10yBgu", "FR513", "FR510", "Cosid", "Disk1", "Disk1+Mg", "Disk2"}, "P10"), "Brake:", "BM"),
};
type.single = true;
type.create = make_block<br_rigging>;
return type;
}
private:
static int material_id(std::string const &Material)
{
// as in TMoverParameters::LoadFIZ_Brake, without the magnetic track brake
if (Material == "P10-Bg")
return bp_P10Bg;
if (Material == "P10-Bgu")
return bp_P10Bgu;
if (Material == "FR513")
return bp_FR513;
if (Material == "FR510")
return bp_FR510;
if (Material == "Cosid")
return bp_Cosid;
if (Material == "P10yBg")
return bp_P10yBg;
if (Material == "P10yBgu")
return bp_P10yBgu;
if (Material == "Disk1" || Material == "Disk1+Mg")
return bp_D1;
if (Material == "Disk2")
return bp_D2;
return bp_P10;
}
std::shared_ptr<TFricMat> m_material;
};
// spring (parking) brake
class pn_spring_brake final : public block
{
public:
struct port
{
enum : std::size_t
{
release,
feed,
force,
applied
};
};
explicit pn_spring_brake(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto &spring{body.SpringBrake};
spring.Activate = false == get_bool(Frame, port::release);
body.UpdateSpringBrake(Frame.dt);
// the force the spring adds to the shoes, as in TMoverParameters::BrakeForce
auto const force{spring.IsReady ? std::max(0.0, spring.MinForcePressure - spring.Cylinder->P()) * spring.MaxBrakeForce : 0.0};
set(Frame, port::force, force);
set_bool(Frame, port::applied, spring.IsActive);
}
static block_type describe()
{
auto type{make_type("pn.spring_brake", "brakes", "Spring brake",
"Spring (parking) brake, released with air from the feed pipe. force (kN) is meant for the spring input of the brake rigging.",
block_origin::extracted)};
type.ports = {signal_in("release", value_type::boolean), physical_in("feed", domain::air, true), signal_out("force", value_type::real, "kN"),
signal_out("applied", value_type::boolean)};
type.params = {
fiz(number_param("volume", 1.0, "l"), "SpringBrake:", "Volume"),
fiz(number_param("mbf", 0.0, "kN"), "SpringBrake:", "MBF"),
fiz(number_param("max_sp", 0.0, "bar"), "SpringBrake:", "MaxSP"),
fiz(number_param("reset_p", 0.0, "bar"), "SpringBrake:", "ResetP"),
fiz(number_param("min_fp", 0.1, "bar"), "SpringBrake:", "MinFP"),
fiz(number_param("press_off", -1.0, "bar"), "SpringBrake:", "PressOff"),
fiz(number_param("press_on", -2.0, "bar"), "SpringBrake:", "PressOn"),
fiz(number_param("valve_on_area", 0.0), "SpringBrake:", "ValveOnArea"),
fiz(number_param("valve_off_area", 0.0), "SpringBrake:", "ValveOffArea"),
fiz(number_param("valve_pnb_area", 0.0), "SpringBrake:", "ValvePNBArea"),
};
type.single = true;
type.create = make_block<pn_spring_brake>;
return type;
}
};
} // namespace
void register_pneumatic_blocks(catalog &Catalog)
{
Catalog.add(pn_compressor::describe());
Catalog.add(pn_governor::describe());
Catalog.add(pn_reservoir::describe());
Catalog.add(br_handle::describe());
Catalog.add(br_local_handle::describe());
Catalog.add(br_pipe::describe());
Catalog.add(br_distributor::describe());
Catalog.add(br_rigging::describe());
Catalog.add(pn_spring_brake::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,149 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
// vigilance device and cab signalling (CA/SHP). the block drives the TSecuritySystem of the vehicle body, configured by
// TSecuritySystem::load from the parameters of the block, so the simulator's own uses of it (cab signal magnets of the
// track, the reset commands of the cab, the AI) keep working with graph vehicles
namespace vgraph
{
namespace
{
class sec_ca final : public block
{
public:
struct port
{
enum : std::size_t
{
v,
power,
reset,
reset_shp,
active,
lamp,
buzzer,
brake,
shp_lamp
};
};
struct param
{
enum : std::size_t
{
aware,
cab_signal,
separate_ack,
sifa,
aware_delay,
sound_delay,
emergency_delay,
min_speed,
max_hold_time,
radio_stop,
cab_dependent
};
};
explicit sec_ca(block_setup const &Setup) : block(Setup), m_separate(boolean(Setup, param::separate_ack)) {}
void update(frame &Frame) override
{
auto &security{Frame.body->mover->SecuritySystem};
auto const active{get_bool(Frame, port::active)};
if (m_first || active != m_active)
{
security.set_enabled(active);
m_active = active;
m_first = false;
}
auto const reset{get_bool(Frame, port::reset)};
if (reset && false == m_reset)
{
security.acknowledge_press();
}
else if (false == reset && m_reset)
{
security.acknowledge_release();
}
m_reset = reset;
auto const resetshp{get_bool(Frame, port::reset_shp)};
if (resetshp && false == m_resetshp)
{
security.cabsignal_reset();
}
m_resetshp = resetshp;
security.update(Frame.dt, get(Frame, port::v), get_bool(Frame, port::power), Frame.body->cab);
set_bool(Frame, port::lamp, m_separate ? security.is_vigilance_blinking() : security.is_blinking());
set_bool(Frame, port::buzzer, security.is_beeping() || security.is_cabsignal_beeping());
set_bool(Frame, port::brake, security.is_braking());
set_bool(Frame, port::shp_lamp, security.is_cabsignal_blinking());
}
static block_type describe()
{
block_type type;
type.id = "sec.ca";
type.category = "security";
type.name = "Vigilance device (CA/SHP)";
type.description = "Vigilance device and cab signalling (TSecuritySystem). v in km/h; brake is meant for the vent input of the brake pipe. The cab signal is "
"set by the magnets of the track, as for the .fiz vehicles.";
type.origin = block_origin::simulator;
type.ports = {signal_in("v", value_type::real, 0.0, "km/h"),
signal_in("power", value_type::boolean),
signal_in("reset", value_type::boolean),
signal_in("reset_shp", value_type::boolean, 0.0, "", true),
signal_in("active", value_type::boolean, 1.0, "", true),
signal_out("lamp", value_type::boolean),
signal_out("buzzer", value_type::boolean),
signal_out("brake", value_type::boolean),
signal_out("shp_lamp", value_type::boolean)};
// the first four make AwareSystem; a negative min_speed leaves the default of 10% of vmax
type.params = {
boolean_param("aware", true),
boolean_param("cab_signal", false),
boolean_param("separate_ack", false),
boolean_param("sifa", false),
fiz(number_param("aware_delay", 30.0, "s"), "Security:", "AwareDelay"),
fiz(number_param("sound_delay", 5.0, "s"), "Security:", "SoundSignalDelay"),
fiz(number_param("emergency_delay", 5.0, "s"), "Security:", "EmergencyBrakeDelay"),
number_param("min_speed", -1.0, "km/h"),
fiz(number_param("max_hold_time", 1.5, "s"), "Security:", "MaxHoldTime"),
fiz(boolean_param("radio_stop", false), "Security:", "RadioStop"),
fiz(boolean_param("cab_dependent", false), "Security:", "CabDependent"),
};
type.single = true;
type.create = make_block<sec_ca>;
return type;
}
private:
bool const m_separate;
bool m_first{true};
bool m_active{false};
bool m_reset{false};
bool m_resetshp{false};
};
} // namespace
void register_security_blocks(catalog &Catalog)
{
Catalog.add(sec_ca::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,424 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <algorithm>
#include <cmath>
// the vehicle body: its motion, doors, lights, sander and the animated parts of its model, which stay native in the simulator
namespace vgraph
{
namespace
{
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description)
{
block_type type;
type.id = std::move(Id);
type.category = "vehicle";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = block_origin::native;
return type;
}
class veh_state final : public block
{
public:
struct port
{
enum : std::size_t
{
v,
v_signed,
accel,
wheel_rpm,
slipping,
cab,
time
};
};
explicit veh_state(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto const &body{*Frame.body};
set(Frame, port::v, std::abs(body.velocity) * 3.6);
set(Frame, port::v_signed, body.velocity);
set(Frame, port::accel, body.acceleration);
set(Frame, port::wheel_rpm, std::abs(body.wheel_speed) * 60.0);
set_bool(Frame, port::slipping, body.slipping);
set(Frame, port::cab, body.cab);
set(Frame, port::time, Frame.time);
}
static block_type describe()
{
auto type{make_type("veh.state", "Vehicle state",
"Motion of the vehicle: speed (km/h), signed speed (m/s, positive towards the front, cab 1), acceleration, wheel revolutions, the occupied "
"cab (1, -1, 0) and the time of the graph.")};
type.ports = {signal_out("v", value_type::real, "km/h"), signal_out("v_signed", value_type::real, "m/s"), signal_out("accel", value_type::real, "m/s2"),
signal_out("wheel_rpm", value_type::real, "1/min"), signal_out("slipping", value_type::boolean), signal_out("cab", value_type::integer),
signal_out("time", value_type::real, "s")};
type.create = make_block<veh_state>;
return type;
}
};
// passenger doors, operated by the driver through the graph; the doors of the vehicle body move them and play their sounds
class veh_doors final : public block
{
public:
struct port
{
enum : std::size_t
{
open_left,
open_right,
lock,
closed_left,
closed_right,
all_closed
};
};
explicit veh_doors(block_setup const &Setup) : block(Setup), m_lock(is_connected(Setup, port::lock)) {}
void update(frame &Frame) override
{
auto &doors{vehicle(Frame).Doors};
operate(doors.instances[side::left], get_bool(Frame, port::open_left));
operate(doors.instances[side::right], get_bool(Frame, port::open_right));
if (m_lock)
{
doors.has_lock = true;
doors.lock_enabled = get_bool(Frame, port::lock);
}
// the state from the last update of the doors; they move after the graph
auto const closedleft{doors.instances[side::left].is_closed};
auto const closedright{doors.instances[side::right].is_closed};
set_bool(Frame, port::closed_left, closedleft);
set_bool(Frame, port::closed_right, closedright);
set_bool(Frame, port::all_closed, closedleft && closedright);
}
static block_type describe()
{
auto type{make_type("veh.doors", "Doors",
"Passenger doors of the vehicle. They open while the open input is on and close when it goes off; lock blocks them above the lock speed.")};
type.ports = {signal_in("open_left", value_type::boolean, 0.0, "", true),
signal_in("open_right", value_type::boolean, 0.0, "", true),
signal_in("lock", value_type::boolean, 0.0, "", true),
signal_out("closed_left", value_type::boolean),
signal_out("closed_right", value_type::boolean),
signal_out("all_closed", value_type::boolean)};
// open_time and close_time are turned into the speeds of the .fiz with the range
type.params = {
number_param("open_time", 2.0, "s"),
number_param("close_time", 3.0, "s"),
fiz(choice_param("type", {"shift", "rotate", "fold", "plug"}, "rotate"), "Doors:", "DoorOpenMethod"),
fiz(number_param("range", 1.0, "m/deg"), "Doors:", "DoorMaxShiftL"),
fiz(number_param("range_out", 0.0, "m"), "Doors:", "DoorMaxShiftPlug"),
fiz(number_param("open_delay", 0.0, "s"), "Doors:", "DoorOpenDelay"),
fiz(number_param("close_delay", 0.0, "s"), "Doors:", "DoorCloseDelay"),
fiz(number_param("lock_speed", 10.0, "km/h"), "Doors:", "DoorLockSpeed"),
};
type.single = true;
type.create = make_block<veh_doors>;
return type;
}
private:
template <class Door_> static void operate(Door_ &Door, bool const Open)
{
Door.remote_open = Open;
Door.remote_close = false == Open;
}
bool const m_lock;
};
class veh_lights final : public block
{
public:
struct port
{
enum : std::size_t
{
front,
rear,
dim
};
};
explicit veh_lights(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
body.iLights[end::front] = static_cast<int>(std::lround(get(Frame, port::front)));
body.iLights[end::rear] = static_cast<int>(std::lround(get(Frame, port::rear)));
Frame.body->headlights_dimmed = get_bool(Frame, port::dim);
}
static block_type describe()
{
auto type{make_type("veh.lights", "Head and tail lights",
"Lights at the front and the rear end of the vehicle, as bit masks of the lights (1 left, 4 upper, 16 right white; 2 left, 32 right red...).")};
type.ports = {signal_in("front", value_type::integer, 0.0, "", true), signal_in("rear", value_type::integer, 0.0, "", true), signal_in("dim", value_type::boolean, 0.0, "", true)};
type.single = true;
type.create = make_block<veh_lights>;
return type;
}
};
class veh_sander final : public block
{
public:
struct port
{
enum : std::size_t
{
on,
sanding
};
};
explicit veh_sander(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const on{get_bool(Frame, port::on)};
if (on != m_on)
{
body.Sandbox(on, range_t::local);
m_on = on;
}
set_bool(Frame, port::sanding, body.SandDose);
}
static block_type describe()
{
auto type{make_type("veh.sander", "Sander", "Sands the rails while on, until the sand (body sand_capacity) runs out.")};
type.ports = {signal_in("on", value_type::boolean), signal_out("sanding", value_type::boolean)};
type.single = true;
type.create = make_block<veh_sander>;
return type;
}
private:
bool m_on{false};
};
// placement of the animated submodels of the vehicle's external model: the model of the vehicle body or its low poly
// interior. the vehicle (DynObj) reads these parameters by their keys and applies the block's state to the submodels
// right before the vehicle is drawn, so keep the keys and the choice ids
param_def model_param()
{
return choice_param("model", {"vehicle", "lowpoly"}, "vehicle");
}
// submodel of the external model rotated or moved by the value, like the TGauge of a cab control: the input is clamped
// to min..max, followed with the smoothing of TGauge (its friction), and gives offset + scale * value degrees
// (rotation about the submodel's y axis) or millimetres (shift along its z axis)
class veh_anim final : public block
{
public:
struct port
{
enum : std::size_t
{
value,
anim
};
};
struct param
{
enum : std::size_t
{
model,
submodel,
animation,
min,
max,
scale,
offset,
smoothing
};
};
explicit veh_anim(block_setup const &Setup)
: block(Setup), m_min(std::min(number(Setup, param::min), number(Setup, param::max))), m_max(std::max(number(Setup, param::min), number(Setup, param::max))),
m_scale(number(Setup, param::scale)), m_offset(number(Setup, param::offset)), m_smoothing(std::max(0.0, number(Setup, param::smoothing)))
{
}
void update(frame &Frame) override
{
auto const target{std::clamp(get(Frame, port::value), m_min, m_max)};
if (m_initialized && m_smoothing > 0.0 && Frame.dt < 0.5 * m_smoothing)
{
// the same as TGauge::Update
m_value += Frame.dt * (target - m_value) / m_smoothing;
if (std::abs(target - m_value) <= 0.0001)
{
m_value = target;
}
}
else
{
// the first step starts at the input, so the part doesn't move when the vehicle appears
m_value = target;
m_initialized = true;
}
set(Frame, port::anim, animation());
}
std::size_t state_count() const override
{
return 2;
}
char const *state_name(std::size_t Index) const override
{
return Index == 0 ? "value" : "anim";
}
double state_value(std::size_t Index) const override
{
return Index == 0 ? m_value : animation();
}
static block_type describe()
{
auto type{make_type("veh.anim", "Vehicle model animation",
"Rotates or moves a submodel of the vehicle's external model (or its low poly interior) by the value, like a cab control: offset + scale * value, "
"in degrees or millimetres, smoothed. Each vehicle shows its own state, although the model is shared.")};
type.ports = {signal_in("value", value_type::real), signal_out("anim", value_type::real)};
type.params = {
model_param(),
text_param("submodel"),
choice_param("animation", {"rotate", "move"}, "rotate"),
number_param("min", 0.0),
number_param("max", 1.0),
number_param("scale", 90.0, "deg/mm"),
number_param("offset", 0.0, "deg/mm"),
number_param("smoothing", 0.5, "s"),
};
type.create = make_block<veh_anim>;
return type;
}
private:
double animation() const
{
return m_offset + m_scale * m_value;
}
double const m_min;
double const m_max;
double const m_scale;
double const m_offset;
double const m_smoothing;
double m_value{0.0};
bool m_initialized{false};
};
// submodel of the external model shown while the input is on, like the TButton of a lamp: submodel visible when on,
// the optional submodel_off visible when off
class veh_visible final : public block
{
public:
struct port
{
enum : std::size_t
{
on,
shown
};
};
struct param
{
enum : std::size_t
{
model,
submodel,
submodel_off,
blink,
blink_period
};
};
explicit veh_visible(block_setup const &Setup) : block(Setup), m_blink(boolean(Setup, param::blink)), m_blinkperiod(std::max(0.01, number(Setup, param::blink_period))) {}
void update(frame &Frame) override
{
auto on{get_bool(Frame, port::on)};
if (on && m_blink)
{
// in step with the lamps of the cab, which blink with the time of the graph too
on = std::fmod(Frame.time, m_blinkperiod) < 0.5 * m_blinkperiod;
}
m_shown = on;
set_bool(Frame, port::shown, m_shown);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "on";
}
double state_value(std::size_t) const override
{
return m_shown ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("veh.visible", "Vehicle model element visibility",
"Shows a submodel of the vehicle's external model (or its low poly interior) while the input is on, and the optional off submodel while "
"it's off, e.g. a lamp, a sign or a gadget. Each vehicle shows its own state, although the model is shared.")};
type.ports = {signal_in("on", value_type::boolean), signal_out("shown", value_type::boolean)};
type.params = {
model_param(), text_param("submodel"), text_param("submodel_off"), boolean_param("blink", false), number_param("blink_period", 1.0, "s"),
};
type.create = make_block<veh_visible>;
return type;
}
private:
bool const m_blink;
double const m_blinkperiod;
bool m_shown{false};
};
} // namespace
void register_vehicle_blocks(catalog &Catalog)
{
Catalog.add(veh_state::describe());
Catalog.add(veh_doors::describe());
Catalog.add(veh_lights::describe());
Catalog.add(veh_sander::describe());
Catalog.add(veh_anim::describe());
Catalog.add(veh_visible::describe());
}
} // namespace vgraph

View File

@@ -31,9 +31,14 @@ std::string const &block::text(block_setup const &Setup, std::size_t const Param
return Setup.params[Param].text;
}
std::vector<std::vector<double>> const &block::table(block_setup const &Setup, std::size_t const Param)
{
return Setup.params[Param].rows;
}
int block::choice(block_setup const &Setup, std::size_t const Param)
{
auto const &choices{Setup.type.params[Param].choices};
auto const &choices{Setup.param_defs[Param].choices};
auto const &value{Setup.params[Param].text};
for (std::size_t idx = 0; idx < choices.size(); ++idx)
{

View File

@@ -19,6 +19,8 @@ namespace vgraph
{
struct block_type;
struct body_state;
class lua_host;
// state of a physical connection. the output writes the forward value; every connected input adds its share
// of the back value to back_next, which becomes visible to the output in the next step. thanks to this one step lag
@@ -35,19 +37,26 @@ struct frame
{
double dt{0.0};
double time{0.0}; // time elapsed since the start of the instance, after this step
std::uint64_t step{0}; // number of the step, counted from 1; for the blocks combining their effects on the body (shared_value)
bool low_voltage{false}; // graph wide switch: simulate the low voltage circuits, or treat them as always powered
double *signals{nullptr};
std::uint8_t const *forced{nullptr}; // signal slots with value forced from outside, writes from blocks are ignored
link_state *links{nullptr};
body_state *body{nullptr}; // the vehicle body; never null during update
};
// everything a block factory needs to create a block of the given node
struct block_setup
{
block_type const &type;
std::vector<param_value> const &params; // in the order of the type's parameter definitions
std::vector<int> const &slots; // in the order of the type's port definitions; signal slot or link index
std::vector<std::uint8_t> const &connected; // in the order of the type's port definitions
std::vector<port_def> const &ports; // ports of the node: the type's, or resolved for the node (lua blocks)
std::vector<param_def> const &param_defs; // parameter definitions of the node, as above
std::vector<param_value> const &params; // in the order of param_defs
std::vector<int> const &slots; // in the order of ports; signal slot or link index
std::vector<std::uint8_t> const &connected; // in the order of ports
std::string const &node_id;
std::string const &script; // source of a lua block
lua_host *lua{nullptr}; // lua state of the graph instance; null when the graph has no lua blocks
};
// base of all blocks. a block reads and writes its ports through slot indices resolved when the graph was compiled
@@ -62,6 +71,9 @@ public:
// called after all blocks were updated, by the blocks of types marked as deferred. a deferred block publishes its
// outputs here, so they become visible to other blocks in the next step; this is what allows feedback loops
virtual void commit(frame &) {}
// called after all blocks were updated and before the deferred ones commit, by the blocks of types marked as late.
// used by the blocks which have to see the effects of the whole step, e.g. the brake pipe applying all the flows
virtual void late_update(frame &) {}
// internal state shown by the inspector and written by the standalone runner, e.g. whether a lamp is lit
virtual std::size_t state_count() const
@@ -132,6 +144,7 @@ protected:
static double number(block_setup const &Setup, std::size_t Param);
static bool boolean(block_setup const &Setup, std::size_t Param);
static std::string const &text(block_setup const &Setup, std::size_t Param);
static std::vector<std::vector<double>> const &table(block_setup const &Setup, std::size_t Param);
// index of the parameter's value in the list of choices of its definition
static int choice(block_setup const &Setup, std::size_t Param);

View File

@@ -0,0 +1,133 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_body.h"
namespace vgraph
{
namespace
{
body_field number_field(std::string Key, double Default, std::string Unit, std::string Section, std::string Fizkey)
{
body_field result;
result.key = std::move(Key);
result.type = param_type::number;
result.unit = std::move(Unit);
result.default_value = param_value::make_number(Default);
result.fiz_section = std::move(Section);
result.fiz_key = std::move(Fizkey);
return result;
}
body_field text_field(std::string Key, std::string Default, std::string Section, std::string Fizkey)
{
body_field result;
result.key = std::move(Key);
result.type = param_type::text;
result.default_value = param_value::make_text(std::move(Default));
result.fiz_section = std::move(Section);
result.fiz_key = std::move(Fizkey);
return result;
}
body_field choice_field(std::string Key, std::vector<std::string> Choices, std::vector<std::string> Fizvalues, std::string Default, std::string Section, std::string Fizkey)
{
body_field result;
result.key = std::move(Key);
result.type = param_type::choice;
result.choices = std::move(Choices);
result.fiz_values = std::move(Fizvalues);
result.default_value = param_value::make_choice(std::move(Default));
result.fiz_section = std::move(Section);
result.fiz_key = std::move(Fizkey);
return result;
}
} // namespace
std::vector<body_field> const &body_schema()
{
// the defaults are the values the simulator uses when the entry is missing from the file
static std::vector<body_field> const schema{
choice_field("category", {"train", "road", "unimog", "ship"}, {"train", "road", "unimog", "ship"}, "train", "Param.", "Category"),
number_field("mass", 0.0, "kg", "Param.", "M"),
number_field("mass_rotating", 0.0, "kg", "Param.", "Mred"),
number_field("vmax", 0.0, "km/h", "Param.", "Vmax"),
number_field("power", 0.0, "kW", "Param.", "PWR"),
number_field("sand_capacity", 0.0, "kg", "Param.", "SandCap"),
number_field("heating_power", 0.0, "kW", "Param.", "HeatingP"),
number_field("length", 0.0, "m", "Dimensions:", "L"),
number_field("height", 0.0, "m", "Dimensions:", "H"),
number_field("width", 0.0, "m", "Dimensions:", "W"),
number_field("cx", 0.3, "", "Dimensions:", "Cx"),
number_field("wheel_diameter", 0.0, "m", "Wheels:", "D"),
number_field("wheel_inertia", 0.0, "kg m2", "Wheels:", "AIM"),
number_field("track_gauge", 1.435, "m", "Wheels:", "Tw"),
text_field("axle_arrangement", "", "Wheels:", "Axle"),
number_field("axle_distance", 0.0, "m", "Wheels:", "Ad"),
number_field("bogie_distance", 0.0, "m", "Wheels:", "Bd"),
number_field("min_radius", 0.0, "m", "Wheels:", "Rmin"),
choice_field("bearing", {"roll", "slide"}, {"Roll", "Slide"}, "slide", "Wheels:", "BearingType"),
text_field("coupler_type", "", "BuffCoupl.", "CType"),
number_field("coupler_kb", 0.0, "kN/m", "BuffCoupl.", "kB"),
number_field("coupler_dmax_b", 0.1, "m", "BuffCoupl.", "DmaxB"),
number_field("coupler_fmax_b", 1.0, "kN", "BuffCoupl.", "FmaxB"),
number_field("coupler_kc", 0.0, "kN/m", "BuffCoupl.", "kC"),
number_field("coupler_dmax_c", 0.1, "m", "BuffCoupl.", "DmaxC"),
number_field("coupler_fmax_c", 1.0, "kN", "BuffCoupl.", "FmaxC"),
number_field("coupler_beta", 0.0, "", "BuffCoupl.", "beta"),
number_field("coupler_allowed_flag", 3.0, "", "BuffCoupl.", "AllowedFlag"),
text_field("control_type", "", "BuffCoupl.", "ControlType"),
number_field("max_load", 0.0, "", "Load:", "MaxLoad"),
text_field("load_unit", "", "Load:", "LoadQ"),
text_field("load_accepted", "", "Load:", "LoadAccepted"),
number_field("load_speed", 0.0, "1/s", "Load:", "LoadSpeed"),
number_field("unload_speed", 0.0, "1/s", "Load:", "UnLoadSpeed"),
// three numbers, red green blue 0-255; written as LampRed, LampGreen and LampBlue
text_field("light_color", "255 255 255", "Headlights:", "Lamp"),
// current collectors; used when the graph has el.pantograph blocks
text_field("collector_type", "", "Power:", "PantType"),
number_field("collector_min_h", 0.0, "m", "Power:", "MinH"),
number_field("collector_max_h", 0.0, "m", "Power:", "MaxH"),
number_field("collector_csw", 0.0, "m", "Power:", "CSW"),
number_field("collector_max_voltage", 0.0, "V", "Power:", "MaxVoltage"),
number_field("collector_min_voltage", 0.0, "V", "Power:", "MinV"),
number_field("collector_max_current", 0.0, "A", "Power:", "MaxCurrent"),
number_field("collector_layout", 3.0, "", "Power:", "PhysicalLayout"),
};
return schema;
}
body_field const *find_body_field(std::string_view const Key)
{
for (auto const &field : body_schema())
{
if (field.key == Key)
{
return &field;
}
}
return nullptr;
}
std::vector<profile_info> const &known_profiles()
{
static std::vector<profile_info> const profiles{
{"brake_pipe", 1},
{"feed_pipe", 1},
{"heating", 1},
{"ai", 1},
};
return profiles;
}
} // namespace vgraph

144
vehicle/graph/graph_body.h Normal file
View File

@@ -0,0 +1,144 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include "vehicle/graph/graph_types.h"
#include <algorithm>
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
class TMoverParameters;
namespace vgraph
{
// entry of the "body" section of the .vhg files: parameters of the vehicle body, which stays native in the simulator
// (mass, dimensions, axles, couplers). each entry names the .fiz section and key it replaces; the simulator builds
// the .fiz lines from the entries and reads them with the same code that reads the .fiz files
struct body_field
{
std::string key;
param_type type{param_type::number};
std::string unit;
param_value default_value;
std::vector<std::string> choices; // choice fields only
std::vector<std::string> fiz_values; // choice fields only: the .fiz value of each choice
std::string fiz_section; // e.g. "Param."
std::string fiz_key; // e.g. "M"
};
std::vector<body_field> const &body_schema();
body_field const *find_body_field(std::string_view Key);
// standard interfaces between vehicles, or between the vehicle and the AI, with the versions the simulator knows
struct profile_info
{
std::string_view id;
int version;
};
std::vector<profile_info> const &known_profiles();
// what the AI driver wants from the vehicle (if.ai outputs), and what it reads back (if.ai inputs)
struct ai_intents
{
double dir{0.0};
double traction{0.0}; // 0..1
double ed_brake{0.0}; // 0..1
double brake{0.0}; // train brake demand 0..1, 1 = full service braking
bool emergency{false};
double local_brake{0.0}; // 0..1
bool prepare{false};
bool shutdown{false};
bool doors_left{false};
bool doors_right{false};
int lights{0};
bool horn{false};
bool pantographs{false};
bool sand{false};
};
struct ai_feedback
{
bool ready{false};
double i_ratio{0.0};
bool slip{false};
double pg{0.0};
double zg{0.0};
double bc{0.0};
double vmax{0.0};
};
// property of the vehicle body set by several blocks, e.g. the Mains closed by any of the line breakers. the first block
// updated in a step starts the value anew and the next ones combine theirs with it, so after the step it covers all of them
struct shared_value
{
std::uint64_t step{0};
double value{0.0};
// the largest of the values given in the step
double max(std::uint64_t const Step, double const Value)
{
value = (step != Step ? Value : std::max(value, Value));
step = Step;
return value;
}
// whether any of the values given in the step is set
bool any(std::uint64_t const Step, bool const Value)
{
return max(Step, Value ? 1.0 : 0.0) > 0.5;
}
};
// value one vehicle gives to a train line (if.line), read by the vehicles coupled with it
struct train_line_value
{
std::string name;
double value{0.0};
bool active{false}; // the vehicle drives the line
};
// state of the vehicle body shared with the blocks during a step. the owner of the graph (the simulator, or the
// standalone runner) fills the inputs before every step and reads the outputs after it
struct body_state
{
// inputs
double velocity{0.0}; // m/s, signed, positive when moving towards the front coupler
double acceleration{0.0}; // m/s^2, signed like the velocity
double wheel_speed{0.0}; // rotational speed of the wheels, rev/s, signed like the velocity
bool slipping{false};
int cab{0}; // occupied cab: 1, -1 or 0
double distance{0.0}; // m, travelled
bool standalone{false}; // the graph runs without the simulator (-graph-run): no catenary, no neighbours
// set when the graph has no br.pipe or pn.reservoir: the pipe of the body still passes the air between the neighbours
bool native_brake_pipe{true};
bool native_feed_pipe{true};
// outputs, cleared by the owner before every step
double tractive_force{0.0}; // N, at the wheel rim, sum of drive.axle blocks; positive towards the front
double brake_force{0.0}; // N, magnitude, sum of br.rigging blocks
double current{0.0}; // A, drawn from the catenary by the vehicle
bool headlights_dimmed{false};
// AI interface (if.ai)
ai_intents ai;
ai_feedback ai_reply;
// properties of the body combined from several blocks
shared_value mains; // closed line breakers (el.hsb)
shared_value converter; // running converters (el.converter)
shared_value converter_allow;
// train lines of the vehicle, published by its if.line blocks for the neighbours
std::vector<train_line_value> lines;
// body of the vehicle; the standalone runner (-graph-run) creates one as well, so it is never null during update
TMoverParameters *mover{nullptr};
};
} // namespace vgraph

View File

@@ -9,6 +9,8 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "input/command.h"
#include <nlohmann/json.hpp>
@@ -66,6 +68,15 @@ catalog const &catalog::get()
catalog result;
register_logic_blocks(result);
register_cab_blocks(result);
register_script_blocks(result);
register_vehicle_blocks(result);
register_pneumatic_blocks(result);
register_security_blocks(result);
register_electric_blocks(result);
register_drive_blocks(result);
register_diesel_blocks(result);
register_interface_blocks(result);
register_module_blocks(result);
return result;
}()};
return instance;
@@ -100,6 +111,15 @@ nlohmann::json to_json(param_value const &Value)
case param_type::text:
case param_type::choice:
return Value.text;
case param_type::table:
{
auto result{nlohmann::json::array()};
for (auto const &row : Value.rows)
{
result.push_back(row);
}
return result;
}
default:
return Value.number;
}
@@ -142,6 +162,16 @@ std::string catalog::export_json() const
item["description"] = type.description;
item["origin"] = std::string{to_string(type.origin)};
item["deferred"] = type.deferred;
if (type.single)
{
// at most one block of this type in a graph
item["single"] = true;
}
if (type.dynamic_ports)
{
// ports and parameters come from the node, e.g. from the script of a lua block
item["dynamic_ports"] = true;
}
auto &ports = (item["ports"] = nlohmann::ordered_json::array());
for (auto const &port : type.ports)
{
@@ -182,10 +212,93 @@ std::string catalog::export_json() const
{
paramitem["choices"] = param.choices;
}
if (param.type == param_type::table)
{
paramitem["columns"] = param.columns;
}
if (false == param.fiz_key.empty())
{
paramitem["fiz"] = param.fiz_section + " " + param.fiz_key;
}
params.push_back(std::move(paramitem));
}
blocks.push_back(std::move(item));
}
// parameters of the vehicle body, for the form of the editor
auto &body = (root["body"] = nlohmann::ordered_json::array());
for (auto const &field : body_schema())
{
nlohmann::ordered_json item;
item["key"] = field.key;
item["type"] = std::string{to_string(field.type)};
if (false == field.unit.empty())
{
item["unit"] = field.unit;
}
item["default"] = to_json(field.default_value);
if (field.type == param_type::choice)
{
item["choices"] = field.choices;
}
item["fiz"] = field.fiz_section + " " + field.fiz_key;
body.push_back(std::move(item));
}
auto &profiles = (root["profiles"] = nlohmann::ordered_json::array());
for (auto const &profile : known_profiles())
{
nlohmann::ordered_json item;
item["id"] = std::string{profile.id};
item["version"] = profile.version;
profiles.push_back(std::move(item));
}
// commands of the simulator, which the cab switches can be bound to (by name), for the pickers of the editor
auto &commands = (root["commands"] = nlohmann::ordered_json::array());
{
std::vector<std::pair<std::size_t, std::string>> names;
for (auto const &entry : simulation::commandMap)
{
if (entry.second != user_command::none)
{
names.emplace_back(static_cast<std::size_t>(entry.second), entry.first);
}
}
// in the order of the command list, which groups the related commands
std::sort(std::begin(names), std::end(names));
for (auto const &name : names)
{
nlohmann::ordered_json item;
item["name"] = name.second;
if (name.first < simulation::Commands_descriptions.size())
{
auto const &description{simulation::Commands_descriptions[name.first]};
switch (description.target)
{
case command_target::userinterface:
item["target"] = "userinterface";
break;
case command_target::vehicle:
item["target"] = "vehicle";
break;
case command_target::signal:
item["target"] = "signal";
break;
case command_target::entity:
item["target"] = "entity";
break;
case command_target::simulation:
item["target"] = "simulation";
break;
default:
break;
}
item["mode"] = (description.mode == command_mode::continuous ? "continuous" : "oneoff");
}
commands.push_back(std::move(item));
}
}
// live preview server of the simulator, see the README
root["preview"] = "protocol 1";
return root.dump(1, '\t');
}
@@ -270,4 +383,27 @@ param_def choice_param(std::string Key, std::vector<std::string> Choices, std::s
return result;
}
param_def table_param(std::string Key, std::vector<std::string> Columns)
{
param_def result;
result.key = std::move(Key);
result.type = param_type::table;
result.columns = std::move(Columns);
result.default_value = param_value::make_table({});
return result;
}
param_def fiz(param_def Definition, std::string Section, std::string Key)
{
Definition.fiz_section = std::move(Section);
Definition.fiz_key = std::move(Key);
return Definition;
}
param_def cab_param()
{
// "0": the engine room (cab0definition: of the .mmd files)
return choice_param("cab", {"any", "1", "2", "0"}, "any");
}
} // namespace vgraph

View File

@@ -18,6 +18,8 @@ http://mozilla.org/MPL/2.0/.
namespace vgraph
{
struct node_desc;
// where the behaviour of a block comes from; shown by the editor
enum class block_origin : std::uint8_t
{
@@ -42,9 +44,18 @@ struct block_type
// outputs of a deferred block are published after the whole step, so connections leaving it don't take part
// in ordering the blocks, and loops passing through it are allowed
bool deferred{false};
// late_update() is called for the blocks of this type after all blocks were updated
bool late{false};
// at most one block of this type in a graph, because it stands for a part of the vehicle the simulator has only one of
bool single{false};
std::vector<port_def> ports;
std::vector<param_def> params;
block_factory create{nullptr};
// blocks whose ports and parameters depend on the node, e.g. lua blocks described by their script. the resolver
// fills the node's ports and parameters in place of the ones above; returns false with the reason on failure
bool dynamic_ports{false};
using port_resolver = bool (*)(node_desc const &Node, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error);
port_resolver resolve{nullptr};
int port_index(std::string_view Key) const;
int param_index(std::string_view Key) const;
@@ -82,9 +93,23 @@ param_def number_param(std::string Key, double Default, std::string Unit = "");
param_def boolean_param(std::string Key, bool Default);
param_def text_param(std::string Key, std::string Default = "");
param_def choice_param(std::string Key, std::vector<std::string> Choices, std::string Default);
param_def table_param(std::string Key, std::vector<std::string> Columns);
// marks the parameter as standing for the specified .fiz entry, e.g. fiz(number_param("max_bp", 0.0, "bar"), "Brake:", "MaxBP")
param_def fiz(param_def Definition, std::string Section, std::string Key);
// placement of cab controls and indicators: the cab whose model holds the submodel
param_def cab_param();
// registration functions of the block families, called once when the catalog is built
void register_logic_blocks(catalog &Catalog);
void register_cab_blocks(catalog &Catalog);
void register_script_blocks(catalog &Catalog);
void register_vehicle_blocks(catalog &Catalog);
void register_pneumatic_blocks(catalog &Catalog);
void register_security_blocks(catalog &Catalog);
void register_electric_blocks(catalog &Catalog);
void register_drive_blocks(catalog &Catalog);
void register_diesel_blocks(catalog &Catalog);
void register_interface_blocks(catalog &Catalog);
void register_module_blocks(catalog &Catalog);
} // namespace vgraph

View File

@@ -10,8 +10,10 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "vehicle/graph/graph_format.h"
#include <algorithm>
#include <fstream>
#include <iterator>
#include <map>
#include <set>
#include <nlohmann/json.hpp>
@@ -57,6 +59,32 @@ bool read_value(json const &Value, param_value &Result)
Result = param_value::make_text(Value.get<std::string>());
return true;
}
if (Value.is_array())
{
// table: array of rows, each row an array of numbers
std::vector<std::vector<double>> rows;
rows.reserve(Value.size());
for (auto const &row : Value)
{
if (false == row.is_array())
{
return false;
}
std::vector<double> cells;
cells.reserve(row.size());
for (auto const &cell : row)
{
if (false == cell.is_number())
{
return false;
}
cells.push_back(cell.get<double>());
}
rows.emplace_back(std::move(cells));
}
Result = param_value::make_table(std::move(rows));
return true;
}
return false;
}
@@ -71,7 +99,7 @@ void read_values(json const &Object, std::vector<named_value> &Values, std::vect
}
else
{
report(Diagnostics, severity::error, Node, What + " \"" + item.key() + "\" has to be a number, a boolean or a text");
report(Diagnostics, severity::error, Node, What + " \"" + item.key() + "\" has to be a number, a boolean, a text or a table (array of rows of numbers)");
}
}
}
@@ -195,6 +223,7 @@ bool parse_document_(std::string const &Text, document &Document, std::vector<di
node.id = item.value("id", "");
node.type = item.value("type", "");
node.title = item.value("title", "");
node.script = item.value("lua", "");
if (false == is_valid_id(node.id))
{
report(Diagnostics, severity::error, node.id, "node id has to be made of letters, digits, '_' and '-'");
@@ -253,6 +282,224 @@ bool parse_document_(std::string const &Text, document &Document, std::vector<di
} // namespace
namespace
{
struct endpoint
{
std::string node;
std::string port;
};
std::string directory_of(std::string const &Path)
{
auto const separator{Path.find_last_of("/\\")};
return separator == std::string::npos ? std::string{} : Path.substr(0, separator + 1);
}
bool expand_modules_(document &Document, std::string const &Directory, std::vector<diagnostic> &Diagnostics, std::vector<std::string> &Chain)
{
auto ok{true};
std::vector<node_desc> nodes;
std::vector<connection_desc> connections{Document.connections};
for (auto &node : Document.nodes)
{
if (node.type != module_type)
{
nodes.emplace_back(std::move(node));
continue;
}
std::string file;
for (auto const &param : node.params)
{
if (param.first == "file")
{
file = param.second.text;
}
else
{
report(Diagnostics, severity::warning, node.id, "unknown parameter \"" + param.first + "\" of the module was ignored");
}
}
std::replace(std::begin(file), std::end(file), '\\', '/');
if (file.empty())
{
report(Diagnostics, severity::error, node.id, "module without a file");
ok = false;
continue;
}
auto const path{(file.front() == '/' || (file.size() > 1 && file[1] == ':')) ? file : Directory + file};
if (std::find(std::begin(Chain), std::end(Chain), path) != std::end(Chain) || Chain.size() >= 16)
{
report(Diagnostics, severity::error, node.id, "module \"" + file + "\" uses itself");
ok = false;
continue;
}
// problems of the module file are reported under the module node
std::vector<diagnostic> problems;
document module;
auto loaded{load_document(path, module, problems)};
if (loaded)
{
Chain.push_back(path);
loaded = expand_modules_(module, directory_of(path), problems, Chain);
Chain.pop_back();
}
if (false == module.body.empty() || false == module.profiles.empty())
{
problems.push_back({severity::warning, "", "the body and the profiles of a module are ignored"});
}
for (auto const &problem : problems)
{
report(Diagnostics, problem.level, node.id + (problem.node.empty() ? "" : "/" + problem.node), "module \"" + file + "\": " + problem.message);
}
if (false == loaded)
{
ok = false;
continue;
}
auto const prefix{node.id + "/"};
std::set<std::string> inputs, outputs;
std::map<std::string, node_desc> inputnodes;
for (auto &inner : module.nodes)
{
// the interface of this module; blocks of the modules it uses have ids with '/', and an input block kept
// by such a module (for its unconnected input) isn't an input of this one
auto const own{inner.id.find('/') == std::string::npos};
if (own && inner.type == "module.input")
{
inputs.insert(inner.id);
inputnodes.emplace(inner.id, inner);
}
else if (own && inner.type == "module.output")
{
outputs.insert(inner.id);
}
else
{
inner.id = prefix + inner.id;
nodes.emplace_back(std::move(inner));
}
}
// connections inside the module
std::map<std::string, std::vector<endpoint>> inputtargets; // input of the module -> inputs of its blocks
std::map<std::string, endpoint> outputsources; // output of the module -> output of a block, or "input" node for a passed input
for (auto const &inner : module.connections)
{
auto const frominput{inputs.count(inner.from_node) > 0};
if (outputs.count(inner.to_node) > 0)
{
outputsources[inner.to_node] = frominput ? endpoint{inner.from_node, ""} : endpoint{prefix + inner.from_node, inner.from_port};
}
else if (frominput)
{
inputtargets[inner.from_node].push_back({prefix + inner.to_node, inner.to_port});
}
else
{
connections.push_back({prefix + inner.from_node, inner.from_port, prefix + inner.to_node, inner.to_port});
}
}
// connections of the module node in the graph using it
std::map<std::string, endpoint> externalsources; // input of the module -> what feeds it
std::map<std::string, std::vector<endpoint>> externaltargets; // output of the module -> what it feeds
for (auto it = std::begin(connections); it != std::end(connections);)
{
if (it->to_node == node.id)
{
if (inputs.count(it->to_port) == 0)
{
report(Diagnostics, severity::error, node.id, "module \"" + file + "\" has no input \"" + it->to_port + "\"");
ok = false;
}
externalsources[it->to_port] = {it->from_node, it->from_port};
it = connections.erase(it);
}
else if (it->from_node == node.id)
{
if (outputs.count(it->from_port) == 0)
{
report(Diagnostics, severity::error, node.id, "module \"" + file + "\" has no output \"" + it->from_port + "\"");
ok = false;
}
externaltargets[it->from_port].push_back({it->to_node, it->to_port});
it = connections.erase(it);
}
else
{
++it;
}
}
// an input fed from an output of the same module (a loop through the module) is fed by the block inside it
for (auto &source : externalsources)
{
if (source.second.node != node.id)
{
continue;
}
auto const inner{outputsources.find(source.second.port)};
if (inner != std::end(outputsources) && false == inner->second.port.empty())
{
source.second = inner->second;
}
}
// an input without a connection keeps its interface block, which gives the default value of the input
auto const source_of = [&](std::string const &Input) -> endpoint {
auto const source{externalsources.find(Input)};
if (source != std::end(externalsources))
{
return source->second;
}
auto const id{prefix + Input};
if (std::none_of(std::begin(nodes), std::end(nodes), [&](node_desc const &Node) { return Node.id == id; }))
{
auto interfacenode{inputnodes[Input]};
interfacenode.id = id;
nodes.emplace_back(std::move(interfacenode));
}
return {id, "out"};
};
for (auto const &input : inputtargets)
{
auto const source{source_of(input.first)};
for (auto const &target : input.second)
{
connections.push_back({source.node, source.port, target.node, target.port});
}
}
for (auto const &output : externaltargets)
{
auto source{outputsources.find(output.first)};
if (source == std::end(outputsources))
{
continue;
}
auto from{source->second};
if (from.port.empty())
{
// an input passed straight to an output
from = source_of(from.node);
}
for (auto const &target : output.second)
{
connections.push_back({from.node, from.port, target.node, target.port});
}
}
}
Document.nodes = std::move(nodes);
Document.connections = std::move(connections);
return ok;
}
} // namespace
bool expand_modules(document &Document, std::string const &Directory, std::vector<diagnostic> &Diagnostics)
{
std::vector<std::string> chain;
return expand_modules_(Document, Directory, Diagnostics, chain);
}
bool parse_document(std::string const &Text, document &Document, std::vector<diagnostic> &Diagnostics)
{
try

View File

@@ -27,6 +27,7 @@ struct node_desc
std::string type;
std::string title;
std::vector<named_value> params; // as stored in the file; checked against the catalog when the graph is compiled
std::string script; // source of a lua block ("lua" in the file)
};
struct connection_desc
@@ -50,6 +51,16 @@ struct document
std::vector<connection_desc> connections;
};
// type of the nodes standing for modules: graphs of logic kept in their own files (by convention .vhgm) and used as
// one block. the "file" parameter names the module file, relative to the file of the graph using it
inline constexpr std::string_view module_type{"module"};
// replaces the module nodes of the document with the blocks of their modules, read from their files relative to
// Directory (with a trailing separator, or empty). the blocks get ids "<module node id>/<id in the module>", and the
// connections of the module node are passed to the blocks connected to its module.input and module.output blocks.
// modules can use other modules. problems are appended to the diagnostics; returns false on errors
bool expand_modules(document &Document, std::string const &Directory, std::vector<diagnostic> &Diagnostics);
// parses the content of a .vhg file. problems are appended to the diagnostics; returns false on errors
bool parse_document(std::string const &Text, document &Document, std::vector<diagnostic> &Diagnostics);
// reads and parses the specified .vhg file

View File

@@ -18,16 +18,24 @@ graph_instance::graph_instance(std::shared_ptr<graph_template const> Template)
: m_template(std::move(Template)), m_signals(m_template->signal_defaults()), m_forced(m_signals.size(), 0), m_links(m_template->link_count())
{
auto const &nodes{m_template->nodes()};
if (m_template->uses_lua())
{
m_lua = std::make_unique<lua_host>();
}
m_blocks.reserve(nodes.size());
for (auto const &node : nodes)
{
block_setup const setup{*node.type, node.params, node.slots, node.connected};
block_setup const setup{*node.type, node.ports, node.param_defs, node.params, node.slots, node.connected, node.id, node.script, m_lua.get()};
m_blocks.emplace_back(node.type->create(setup));
}
m_order.reserve(nodes.size());
for (auto const nodeindex : m_template->order())
{
m_order.push_back(m_blocks[nodeindex].get());
if (nodes[nodeindex].type->late)
{
m_late.push_back(m_blocks[nodeindex].get());
}
if (nodes[nodeindex].type->deferred)
{
m_deferred.push_back(m_blocks[nodeindex].get());
@@ -41,26 +49,40 @@ graph_instance::graph_instance(std::shared_ptr<graph_template const> Template)
}
}
// blocks release their references to the lua state before it's closed
graph_instance::~graph_instance()
{
m_blocks.clear();
m_lua.reset();
}
frame graph_instance::make_frame(double const Deltatime)
{
frame result;
result.dt = Deltatime;
result.time = m_time;
result.step = m_step;
result.low_voltage = m_template->low_voltage_simulation();
result.signals = m_signals.data();
result.forced = m_forced.data();
result.links = m_links.data();
result.body = &m_body;
return result;
}
void graph_instance::update(double const Deltatime)
{
m_time += Deltatime;
++m_step;
auto frame{make_frame(Deltatime)};
for (auto *item : m_order)
{
item->update(frame);
}
for (auto *item : m_late)
{
item->late_update(frame);
}
for (auto *item : m_deferred)
{
item->commit(frame);

View File

@@ -10,6 +10,8 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include "vehicle/graph/graph_block.h"
#include "vehicle/graph/graph_body.h"
#include "vehicle/graph/graph_lua.h"
#include "vehicle/graph/graph_template.h"
#include <memory>
@@ -24,6 +26,7 @@ class graph_instance
{
public:
explicit graph_instance(std::shared_ptr<graph_template const> Template);
~graph_instance();
// advances the graph by the specified time. the order of evaluation is fixed, and there's no other source
// of time or randomness, so the same inputs always produce the same results
@@ -37,6 +40,15 @@ public:
{
return m_time;
}
// state of the vehicle body shared with the blocks; filled by the owner of the graph before every step
body_state &body()
{
return m_body;
}
body_state const &body() const
{
return m_body;
}
// block of the node with the specified index (see graph_template::nodes())
block &node_block(std::size_t Index)
@@ -54,6 +66,10 @@ public:
{
return m_signals[Slot];
}
link_state const &link(int Index) const
{
return m_links[Index];
}
// keeps the slot at the specified value regardless of what its block writes, until released
void force(int Slot, double Value);
void release(int Slot);
@@ -66,13 +82,18 @@ private:
frame make_frame(double Deltatime);
std::shared_ptr<graph_template const> m_template;
// lua state of the vehicle, created when the graph has lua blocks. declared before the blocks, so it outlives them
std::unique_ptr<lua_host> m_lua;
std::vector<std::unique_ptr<block>> m_blocks; // in the order of the template's nodes
std::vector<block *> m_order; // blocks in the order of evaluation
std::vector<block *> m_late; // blocks updated again after all the others
std::vector<block *> m_deferred; // blocks publishing their outputs after the step
std::vector<double> m_signals;
std::vector<std::uint8_t> m_forced;
std::vector<link_state> m_links;
body_state m_body;
double m_time{0.0};
std::uint64_t m_step{0};
};
} // namespace vgraph

592
vehicle/graph/graph_lua.cpp Normal file
View File

@@ -0,0 +1,592 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_lua.h"
#include <algorithm>
#include <cmath>
#ifdef WITH_LUA
#include <lua.hpp>
#endif
namespace vgraph
{
namespace
{
log_sink &sink()
{
static log_sink instance;
return instance;
}
} // namespace
void set_log_sink(log_sink Sink)
{
sink() = std::move(Sink);
}
void log_message(std::string const &Message)
{
if (sink())
{
sink()(Message);
}
}
// block_random
block_random::block_random(std::uint64_t const Seed) : state(Seed != 0 ? Seed : 0x9E3779B97F4A7C15ull) {}
double block_random::next()
{
// xorshift64*: small, fast, and easy to reproduce exactly in the editor
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
return static_cast<double>((state * 0x2545F4914F6CDD1Dull) >> 11) * (1.0 / 9007199254740992.0);
}
std::uint64_t block_random::seed_from(std::string const &Text)
{
// fnv-1a
std::uint64_t hash{0xcbf29ce484222325ull};
for (auto const character : Text)
{
hash ^= static_cast<unsigned char>(character);
hash *= 0x100000001b3ull;
}
return hash;
}
#ifdef WITH_LUA
namespace
{
// keys of values kept out of reach of the scripts: light userdata can't be created from lua
char const block_definition_key{0};
char const block_name_key{0};
// instructions between checks of the budget, and the number of checks allowed in a single call
int constexpr hook_interval{1000};
int constexpr call_budget{2000}; // 2 million instructions; far more than any block needs in one step
thread_local int budget_remaining{0};
void budget_hook(lua_State *L, lua_Debug *)
{
if (--budget_remaining <= 0)
{
budget_remaining = call_budget; // leave room for the error handling
luaL_error(L, "instruction budget exceeded, endless loop?");
}
}
void open_library(lua_State *L, lua_CFunction Open, char const *Name)
{
lua_pushcfunction(L, Open);
lua_pushstring(L, Name);
lua_call(L, 1, 0);
}
// copies the fields of the table at Source into a new table pushed on the stack
void push_copy(lua_State *L, int const Source)
{
auto const source{Source < 0 ? lua_gettop(L) + Source + 1 : Source};
lua_newtable(L);
lua_pushnil(L);
while (lua_next(L, source) != 0)
{
lua_pushvalue(L, -2);
lua_insert(L, -2);
lua_rawset(L, -4);
}
}
// math.random and math.randomseed with the generator in the closure's upvalue, the same semantics as in lua 5.1
int script_random(lua_State *L)
{
auto *random{static_cast<block_random *>(lua_touserdata(L, lua_upvalueindex(1)))};
auto const r{random->next()};
switch (lua_gettop(L))
{
case 0:
{
lua_pushnumber(L, r);
return 1;
}
case 1:
{
auto const upper{luaL_checknumber(L, 1)};
luaL_argcheck(L, 1.0 <= upper, 1, "interval is empty");
lua_pushnumber(L, std::floor(r * std::floor(upper)) + 1.0);
return 1;
}
case 2:
{
auto const lower{std::floor(luaL_checknumber(L, 1))};
auto const upper{std::floor(luaL_checknumber(L, 2))};
luaL_argcheck(L, lower <= upper, 2, "interval is empty");
lua_pushnumber(L, std::floor(r * (upper - lower + 1.0)) + lower);
return 1;
}
default:
{
return luaL_error(L, "wrong number of arguments");
}
}
}
int script_randomseed(lua_State *L)
{
auto *random{static_cast<block_random *>(lua_touserdata(L, lua_upvalueindex(1)))};
// splitmix64 finalizer spreads small seeds over the whole state
auto x{static_cast<std::uint64_t>(static_cast<std::int64_t>(std::floor(luaL_checknumber(L, 1))))};
x += 0x9E3779B97F4A7C15ull;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
x ^= x >> 31;
*random = block_random{x};
return 0;
}
// print sends the message to the log, prefixed with the node id; limited, so a script printing in every step can't flood the log
int script_print(lua_State *L)
{
auto const count{static_cast<int>(lua_tointeger(L, lua_upvalueindex(2)))};
if (count >= 50)
{
return 0;
}
lua_pushinteger(L, count + 1);
lua_replace(L, lua_upvalueindex(2));
std::string message{"lua ["};
message += lua_tostring(L, lua_upvalueindex(1));
message += "]:";
auto const arguments{lua_gettop(L)};
lua_getglobal(L, "tostring");
for (int idx = 1; idx <= arguments; ++idx)
{
lua_pushvalue(L, -1);
lua_pushvalue(L, idx);
lua_call(L, 1, 1);
auto const *text{lua_tostring(L, -1)};
message += ' ';
message += (text != nullptr ? text : "?");
lua_pop(L, 1);
}
lua_pop(L, 1);
if (count + 1 == 50)
{
message += " (further messages of this block are suppressed)";
}
log_message(message);
return 0;
}
// block "name" returns a function, which takes the definition table and stores it in the environment
int script_block_definition(lua_State *L)
{
luaL_checktype(L, 1, LUA_TTABLE);
lua_pushvalue(L, lua_upvalueindex(1)); // environment
lua_pushlightuserdata(L, const_cast<char *>(&block_definition_key));
lua_pushvalue(L, 1);
lua_rawset(L, -3);
lua_pushlightuserdata(L, const_cast<char *>(&block_name_key));
lua_pushvalue(L, lua_upvalueindex(2));
lua_rawset(L, -3);
return 0;
}
int script_block(lua_State *L)
{
if (lua_type(L, 1) == LUA_TTABLE)
{
// block { ... } without a name
lua_pushstring(L, "");
lua_insert(L, 1);
}
luaL_checkstring(L, 1);
lua_pushvalue(L, lua_upvalueindex(1)); // environment
lua_pushvalue(L, 1); // name
lua_pushcclosure(L, script_block_definition, 2);
if (lua_gettop(L) >= 3 && lua_type(L, 2) == LUA_TTABLE)
{
// block("name", { ... })
lua_pushvalue(L, 2);
lua_call(L, 1, 0);
return 0;
}
return 1;
}
// libraries available to the scripts. everything else (io, os, package, debug, ffi, jit, loading code) stays out
char const *const base_functions[]{"assert", "error", "ipairs", "next", "pairs", "pcall", "select", "tonumber",
"tostring", "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "setmetatable",
"getmetatable", "_VERSION"};
char const *const libraries[]{"string", "table", "math", "bit", "coroutine"};
lua_State *create_state()
{
auto *L{luaL_newstate()};
if (L == nullptr)
{
return nullptr;
}
// interpreter only: instruction count hooks don't run inside compiled traces, and the budget has to stop endless loops
luaJIT_setmode(L, 0, LUAJIT_MODE_ENGINE | LUAJIT_MODE_OFF);
open_library(L, luaopen_base, "");
open_library(L, luaopen_table, LUA_TABLIBNAME);
open_library(L, luaopen_string, LUA_STRLIBNAME);
open_library(L, luaopen_math, LUA_MATHLIBNAME);
open_library(L, luaopen_bit, LUA_BITLIBNAME);
// string.dump would give access to bytecode
lua_getglobal(L, "string");
lua_pushnil(L);
lua_setfield(L, -2, "dump");
lua_pop(L, 1);
// the metatable of strings leads to the shared string library; hiding it keeps the blocks from changing it for others
lua_pushstring(L, "");
if (lua_getmetatable(L, -1) != 0)
{
lua_pushboolean(L, 0);
lua_setfield(L, -2, "__metatable");
lua_pop(L, 1);
}
lua_pop(L, 1);
lua_sethook(L, budget_hook, LUA_MASKCOUNT, hook_interval);
return L;
}
// pushes a new environment for the block with the specified node id
void push_environment(lua_State *L, std::string const &Nodeid)
{
lua_newtable(L);
auto const environment{lua_gettop(L)};
for (auto const *name : base_functions)
{
lua_getglobal(L, name);
lua_setfield(L, environment, name);
}
for (auto const *name : libraries)
{
lua_getglobal(L, name);
if (lua_istable(L, -1))
{
// copies, so changes made by one block don't leak to the others
push_copy(L, -1);
lua_setfield(L, environment, name);
}
lua_pop(L, 1);
}
// random generator of the block, seeded from the node id
lua_getfield(L, environment, "math");
auto *random{static_cast<block_random *>(lua_newuserdata(L, sizeof(block_random)))};
*random = block_random{block_random::seed_from(Nodeid)};
lua_pushvalue(L, -1);
lua_pushcclosure(L, script_random, 1);
lua_setfield(L, -3, "random");
lua_pushcclosure(L, script_randomseed, 1);
lua_setfield(L, -2, "randomseed");
lua_pop(L, 1);
// print to the log
lua_pushstring(L, Nodeid.c_str());
lua_pushinteger(L, 0);
lua_pushcclosure(L, script_print, 2);
lua_setfield(L, environment, "print");
lua_pushvalue(L, environment);
lua_setfield(L, environment, "_G");
// block definition
lua_pushvalue(L, environment);
lua_pushcclosure(L, script_block, 1);
lua_setfield(L, environment, "block");
}
// runs the chunk on top of the stack within the budget; returns false with the message on failure
bool protected_call(lua_State *L, int const Arguments, std::string &Error)
{
budget_remaining = call_budget;
if (lua_pcall(L, Arguments, 0, 0) != 0)
{
auto const *message{lua_tostring(L, -1)};
Error = (message != nullptr ? message : "unknown error");
lua_pop(L, 1);
return false;
}
return true;
}
// loads and runs the script in the environment on top of the stack; leaves the definition table on the stack, or returns false
bool run_script(lua_State *L, std::string const &Script, std::string const &Chunkname, std::string &Error)
{
auto const environment{lua_gettop(L)};
auto const chunkname{"=" + Chunkname};
if (luaL_loadbuffer(L, Script.data(), Script.size(), chunkname.c_str()) != 0)
{
Error = lua_tostring(L, -1);
lua_pop(L, 1);
return false;
}
lua_pushvalue(L, environment);
lua_setfenv(L, -2);
if (false == protected_call(L, 0, Error))
{
return false;
}
lua_pushlightuserdata(L, const_cast<char *>(&block_definition_key));
lua_rawget(L, environment);
if (false == lua_istable(L, -1))
{
lua_pop(L, 1);
Error = "the script doesn't call block \"name\" { ... }";
return false;
}
lua_getfield(L, -1, "update");
auto const hasupdate{lua_isfunction(L, -1)};
lua_pop(L, 1);
if (false == hasupdate)
{
lua_pop(L, 1);
Error = "the block has no update(self, i, o, dt) function";
return false;
}
return true;
}
bool is_identifier(std::string const &Text)
{
if (Text.empty() || (Text[0] >= '0' && Text[0] <= '9'))
{
return false;
}
return std::all_of(std::begin(Text), std::end(Text), [](char const Char) { return (Char >= 'a' && Char <= 'z') || (Char >= 'A' && Char <= 'Z') || (Char >= '0' && Char <= '9') || Char == '_'; });
}
// reads inputs or outputs of the definition table on top of the stack
bool read_ports(lua_State *L, char const *Field, port_direction const Direction, std::vector<port_def> &Ports, std::string &Error)
{
lua_getfield(L, -1, Field);
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
return true;
}
if (false == lua_istable(L, -1))
{
lua_pop(L, 1);
Error = std::string{"\""} + Field + "\" has to be a table";
return false;
}
std::vector<port_def> ports;
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
if (lua_type(L, -2) != LUA_TSTRING || lua_type(L, -1) != LUA_TSTRING)
{
lua_pop(L, 3);
Error = std::string{"entries of \""} + Field + "\" have to look like name = \"bool\" | \"int\" | \"float\"";
return false;
}
std::string const key{lua_tostring(L, -2)};
std::string type{lua_tostring(L, -1)};
std::transform(std::begin(type), std::end(type), std::begin(type), [](unsigned char const Char) { return static_cast<char>(std::tolower(Char)); });
lua_pop(L, 1);
if (false == is_identifier(key))
{
lua_pop(L, 2);
Error = "port name \"" + key + "\" has to be made of letters, digits and '_'";
return false;
}
port_def port;
port.key = key;
port.direction = Direction;
port.dom = domain::signal;
port.optional = true;
if (type == "bool")
{
port.type = value_type::boolean;
}
else if (type == "int")
{
port.type = value_type::integer;
}
else if (type == "float")
{
port.type = value_type::real;
}
else
{
lua_pop(L, 2);
Error = "port \"" + key + "\" has unknown type \"" + type + "\", expected bool, int or float";
return false;
}
ports.push_back(std::move(port));
}
lua_pop(L, 1);
std::sort(std::begin(ports), std::end(ports), [](port_def const &Left, port_def const &Right) { return Left.key < Right.key; });
Ports.insert(std::end(Ports), std::begin(ports), std::end(ports));
return true;
}
} // namespace
bool lua_available()
{
return true;
}
bool parse_lua_block(std::string const &Script, std::string const &Chunkname, lua_block_definition &Definition, std::string &Error)
{
lua_host host;
auto *L{host.state()};
if (L == nullptr)
{
Error = "can't create lua state";
return false;
}
push_environment(L, Chunkname);
if (false == run_script(L, Script, Chunkname, Error))
{
return false;
}
// definition table on top of the stack, environment below
lua_pushlightuserdata(L, const_cast<char *>(&block_name_key));
lua_rawget(L, -3);
Definition.name = (lua_isstring(L, -1) ? lua_tostring(L, -1) : "");
lua_pop(L, 1);
if (false == read_ports(L, "inputs", port_direction::in, Definition.ports, Error) || false == read_ports(L, "outputs", port_direction::out, Definition.ports, Error))
{
return false;
}
for (std::size_t first = 0; first < Definition.ports.size(); ++first)
{
for (auto second = first + 1; second < Definition.ports.size(); ++second)
{
if (Definition.ports[first].key == Definition.ports[second].key)
{
Error = "\"" + Definition.ports[first].key + "\" is both an input and an output";
return false;
}
}
}
lua_getfield(L, -1, "params");
if (lua_istable(L, -1))
{
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
// numeric parameters can be set per vehicle in the graph; others stay as the script defines them
if (lua_type(L, -2) == LUA_TSTRING && lua_type(L, -1) == LUA_TNUMBER)
{
param_def param;
param.key = std::string{"lua."} + lua_tostring(L, -2);
param.type = param_type::number;
param.default_value = param_value::make_number(lua_tonumber(L, -1));
Definition.params.push_back(std::move(param));
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
std::sort(std::begin(Definition.params), std::end(Definition.params), [](param_def const &Left, param_def const &Right) { return Left.key < Right.key; });
lua_getfield(L, -1, "rate");
if (false == lua_isnil(L, -1))
{
if (lua_type(L, -1) != LUA_TNUMBER || lua_tonumber(L, -1) < 0.0)
{
Error = "\"rate\" has to be a number of updates per second, 0 or more";
return false;
}
Definition.rate = lua_tonumber(L, -1);
}
lua_pop(L, 1);
return true;
}
lua_host::lua_host() : m_state(create_state()) {}
lua_host::~lua_host()
{
if (m_state != nullptr)
{
lua_close(m_state);
}
}
int lua_host::load_block(std::string const &Script, std::string const &Nodeid, std::string &Error)
{
if (m_state == nullptr)
{
Error = "can't create lua state";
return LUA_NOREF;
}
auto *L{m_state};
auto const top{lua_gettop(L)};
push_environment(L, Nodeid);
if (false == run_script(L, Script, Nodeid, Error))
{
lua_settop(L, top);
return LUA_NOREF;
}
auto const reference{luaL_ref(L, LUA_REGISTRYINDEX)};
lua_settop(L, top);
return reference;
}
bool lua_host::call(int const Arguments, std::string &Error)
{
return protected_call(m_state, Arguments, Error);
}
void lua_host::release(int const Reference)
{
if (m_state != nullptr && Reference != LUA_NOREF && Reference != LUA_REFNIL)
{
luaL_unref(m_state, LUA_REGISTRYINDEX, Reference);
}
}
#else
bool lua_available()
{
return false;
}
bool parse_lua_block(std::string const &, std::string const &, lua_block_definition &, std::string &Error)
{
Error = "this build of the simulator has no lua support (WITH_LUA)";
return false;
}
lua_host::lua_host() = default;
lua_host::~lua_host() = default;
int lua_host::load_block(std::string const &, std::string const &, std::string &Error)
{
Error = "this build of the simulator has no lua support (WITH_LUA)";
return -1;
}
bool lua_host::call(int, std::string &Error)
{
Error = "this build of the simulator has no lua support (WITH_LUA)";
return false;
}
void lua_host::release(int) {}
#endif
} // namespace vgraph

84
vehicle/graph/graph_lua.h Normal file
View File

@@ -0,0 +1,84 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include "vehicle/graph/graph_types.h"
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
struct lua_State;
// lua function blocks of the vehicle graph. each vehicle gets a lua state of its own, separate from the scenery's
// one, and each block an environment of its own within it. scripts run in lua 5.1 (luajit) with a restricted set of
// libraries: no files, operating system, modules, ffi or bytecode loading. the jit compiler is turned off for these
// states, so the instruction budget which stops endless loops works reliably
namespace vgraph
{
// messages of the lua blocks (print, errors). the simulator directs them to its log, the command line tools to the console
using log_sink = std::function<void(std::string const &)>;
void set_log_sink(log_sink Sink);
void log_message(std::string const &Message);
// whether this build of the simulator can run lua blocks (WITH_LUA)
bool lua_available();
// interface of a lua block, read from its script:
// block "name" { inputs = { a = "bool" }, outputs = { q = "float" }, params = { k = 1.0 }, state = { ... }, rate = 20,
// update = function(self, i, o, dt) ... end }
struct lua_block_definition
{
std::string name;
std::vector<port_def> ports; // inputs then outputs, each sorted by key so the order doesn't depend on the lua hash
std::vector<param_def> params; // numeric parameters of the script as "lua.<key>", with the script's values as defaults
double rate{0.0}; // updates per second; 0 updates the block in every step
};
// runs the script in a temporary state and reads the block interface. returns false with the reason on failure
bool parse_lua_block(std::string const &Script, std::string const &Chunkname, lua_block_definition &Definition, std::string &Error);
// lua state shared by the lua blocks of one vehicle
class lua_host
{
public:
lua_host();
~lua_host();
lua_host(lua_host const &) = delete;
lua_host &operator=(lua_host const &) = delete;
lua_State *state() const
{
return m_state;
}
// runs the script of a block in a new environment and returns the registry reference of the block definition table,
// or a negative value with the reason on failure. the environment's random generator is seeded from the node id
int load_block(std::string const &Script, std::string const &Nodeid, std::string &Error);
// calls the function placed on the stack below its arguments, within the instruction budget. returns false with the reason on failure
bool call(int Arguments, std::string &Error);
void release(int Reference);
private:
lua_State *m_state{nullptr};
};
// random generator used by math.random of the lua blocks; the external editor uses the same one, so previews match
struct block_random
{
std::uint64_t state;
explicit block_random(std::uint64_t Seed);
double next(); // [0, 1)
static std::uint64_t seed_from(std::string const &Text);
};
} // namespace vgraph

View File

@@ -0,0 +1,534 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#ifdef DBG_NEW
#undef new
#endif
#define ASIO_DISABLE_VISIBILITY
#include <asio.hpp>
#include "vehicle/graph/graph_preview.h"
#include "vehicle/graph/graph_instance.h"
#include "vehicle/graph/blocks/cab.h"
#include "vehicle/DynObj.h"
#include "vehicle/Train.h"
#include "model/MdlMngr.h"
#include "model/Model3d.h"
#include "simulation/simulation.h"
#include "utilities/parser.h"
#include "utilities/utilities.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include <array>
#include <chrono>
#include <deque>
#include <nlohmann/json.hpp>
namespace vgraph
{
namespace
{
using json = nlohmann::json;
int constexpr protocol_version{1};
// pushes of the values per second
double constexpr push_rate{10.0};
class preview_client : public std::enable_shared_from_this<preview_client>
{
public:
explicit preview_client(asio::io_context &Context) : m_socket(Context) {}
asio::ip::tcp::socket &socket()
{
return m_socket;
}
void start()
{
read();
}
bool closed() const
{
return m_closed;
}
// lines received since the last call
std::vector<std::string> take_lines()
{
std::vector<std::string> lines;
lines.swap(m_inbox);
return lines;
}
void send(json const &Message)
{
if (m_closed)
{
return;
}
m_outbox.push_back(Message.dump() + "\n");
if (false == m_writing)
{
write();
}
}
// vehicle the client watches, empty when not attached
std::string attached;
std::chrono::steady_clock::time_point lastpush;
private:
void read()
{
auto self{shared_from_this()};
asio::async_read_until(m_socket, m_buffer, '\n', [self](asio::error_code const &Error, std::size_t const Size) {
if (Error)
{
self->m_closed = true;
return;
}
std::string line{asio::buffers_begin(self->m_buffer.data()), asio::buffers_begin(self->m_buffer.data()) + Size};
self->m_buffer.consume(Size);
while (false == line.empty() && (line.back() == '\n' || line.back() == '\r'))
{
line.pop_back();
}
if (false == line.empty())
{
self->m_inbox.emplace_back(std::move(line));
}
self->read();
});
}
void write()
{
if (m_outbox.empty())
{
m_writing = false;
return;
}
m_writing = true;
auto self{shared_from_this()};
asio::async_write(m_socket, asio::buffer(m_outbox.front()), [self](asio::error_code const &Error, std::size_t) {
if (Error)
{
self->m_closed = true;
self->m_writing = false;
return;
}
self->m_outbox.pop_front();
self->write();
});
}
asio::ip::tcp::socket m_socket;
asio::streambuf m_buffer;
std::vector<std::string> m_inbox;
std::deque<std::string> m_outbox;
bool m_writing{false};
bool m_closed{false};
};
TDynamicObject *find_vehicle(std::string const &Name)
{
auto *vehicle{simulation::Vehicles.find(Name)};
return (vehicle != nullptr && vehicle->MoverParameters != nullptr && vehicle->MoverParameters->Graph ? vehicle : nullptr);
}
json error(std::string const &Message)
{
return {{"op", "error"}, {"message", Message}};
}
// names of the submodels of the model, sorted, for the pickers of the editor
json submodel_names(TModel3d *Model)
{
std::vector<std::string> names;
// the siblings are walked in a loop, only the children recursively
std::vector<TSubModel *> pending{Model->GetSMRoot()};
while (false == pending.empty())
{
auto *submodel{pending.back()};
pending.pop_back();
for (; submodel != nullptr; submodel = submodel->NextGet())
{
if (false == submodel->pName.empty())
{
names.push_back(submodel->pName);
}
if (submodel->ChildGet() != nullptr)
{
pending.push_back(submodel->ChildGet());
}
}
}
std::sort(std::begin(names), std::end(names));
names.erase(std::unique(std::begin(names), std::end(names)), std::end(names));
return names;
}
json model_entry(TModel3d *Model)
{
if (Model == nullptr)
{
return nullptr;
}
return {{"model", Model->NameGet()}, {"submodels", submodel_names(Model)}};
}
class preview_server
{
public:
explicit preview_server(int const Port) : m_acceptor(m_context)
{
asio::error_code error;
asio::ip::tcp::endpoint const endpoint{asio::ip::make_address("127.0.0.1"), static_cast<unsigned short>(Port)};
m_acceptor.open(endpoint.protocol(), error);
if (false == static_cast<bool>(error))
{
m_acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true), error);
m_acceptor.bind(endpoint, error);
}
if (false == static_cast<bool>(error))
{
m_acceptor.listen(asio::socket_base::max_listen_connections, error);
}
if (error)
{
ErrorLog("Vehicle graph preview: can't listen on 127.0.0.1:" + std::to_string(Port) + ": " + error.message());
return;
}
WriteLog("Vehicle graph preview: listening on 127.0.0.1:" + std::to_string(Port));
accept();
}
void update()
{
m_context.restart();
m_context.poll();
auto const now{std::chrono::steady_clock::now()};
for (auto &client : m_clients)
{
for (auto const &line : client->take_lines())
{
handle(*client, line);
}
if (false == client->attached.empty() && now - client->lastpush >= std::chrono::duration<double>(1.0 / push_rate))
{
client->lastpush = now;
push_values(*client);
}
}
m_clients.erase(std::remove_if(std::begin(m_clients), std::end(m_clients), [](auto const &Client) { return Client->closed(); }), std::end(m_clients));
// the writes started above
m_context.poll();
}
private:
void accept()
{
auto client{std::make_shared<preview_client>(m_context)};
m_acceptor.async_accept(client->socket(), [this, client](asio::error_code const &Error) {
if (false == static_cast<bool>(Error))
{
client->start();
m_clients.push_back(client);
}
accept();
});
}
void handle(preview_client &Client, std::string const &Line)
{
auto const message{json::parse(Line, nullptr, false)};
if (message.is_discarded() || false == message.is_object() || false == message.contains("op") || false == message["op"].is_string())
{
Client.send(error("malformed message, expected a json object with \"op\""));
return;
}
auto const op{message["op"].get<std::string>()};
try
{
if (op == "hello")
{
Client.send({{"op", "hello"}, {"version", protocol_version}, {"simulator", "MaSzyna " + Global.asVersion}});
}
else if (op == "list")
{
auto items{json::array()};
for (auto const *vehicle : simulation::Vehicles.sequence())
{
if (vehicle == nullptr || vehicle->MoverParameters == nullptr || nullptr == vehicle->MoverParameters->Graph)
{
continue;
}
items.push_back({{"name", vehicle->name()}, {"file", vehicle->MoverParameters->Graph->definition().path()}, {"graph", vehicle->MoverParameters->Graph->definition().name()}});
}
Client.send({{"op", "vehicles"}, {"items", items}});
}
else if (op == "attach")
{
auto const name{message.value("vehicle", std::string{})};
auto *vehicle{find_vehicle(name)};
if (vehicle == nullptr)
{
Client.send(error("no graph vehicle named \"" + name + "\""));
return;
}
Client.attached = name;
Client.lastpush = {};
Client.send({{"op", "attached"}, {"vehicle", name}, {"file", vehicle->MoverParameters->Graph->definition().path()}});
}
else if (op == "detach")
{
Client.attached.clear();
}
else if (op == "switch" || op == "force" || op == "unforce" || op == "reload" || op == "submodels")
{
auto *vehicle{find_vehicle(Client.attached)};
if (vehicle == nullptr)
{
Client.send(error("not attached to a graph vehicle"));
return;
}
if (op == "switch")
{
operate(Client, *vehicle->MoverParameters->Graph, message);
}
else if (op == "reload")
{
reload(Client, *vehicle);
}
else if (op == "submodels")
{
submodels(Client, *vehicle);
}
else
{
force(Client, *vehicle->MoverParameters->Graph, message, op == "force");
}
}
else
{
Client.send(error("unknown op \"" + op + "\""));
}
}
catch (json::exception const &Error)
{
Client.send(error(std::string{"malformed message: "} + Error.what()));
}
}
void operate(preview_client &Client, graph_instance &Graph, json const &Message)
{
auto const node{Message.value("node", std::string{})};
auto *control{dynamic_cast<cab_switch *>(Graph.find_block(node))};
if (control == nullptr)
{
Client.send(error("\"" + node + "\" isn't a cab switch"));
return;
}
auto const action{Message.value("action", std::string{})};
auto const value{Message.value("value", 0.0)};
if (action == "press")
control->press();
else if (action == "release")
control->release();
else if (action == "toggle")
control->toggle();
else if (action == "inc")
control->increase();
else if (action == "dec")
control->decrease();
else if (action == "set")
control->set_position(static_cast<int>(std::lround(value)));
else if (action == "value")
control->set_value(value);
else
Client.send(error("unknown switch action \"" + action + "\""));
}
void force(preview_client &Client, graph_instance &Graph, json const &Message, bool const Force)
{
auto const port{Message.value("port", std::string{})};
auto const slot{Graph.definition().find_signal(port)};
if (slot < 0)
{
Client.send(error("unknown signal port \"" + port + "\""));
return;
}
if (Force)
{
Graph.force(slot, Message.value("value", 0.0));
}
else
{
Graph.release(slot);
}
}
void reload(preview_client &Client, TDynamicObject &Vehicle)
{
auto &body{*Vehicle.MoverParameters};
std::vector<diagnostic> diagnostics;
auto const path{body.Graph->definition().path()};
auto const graph{load_graph(path, diagnostics)};
if (graph == nullptr)
{
std::string text;
for (auto const &entry : diagnostics)
{
text += (text.empty() ? "" : "\n") + to_string(entry);
}
Client.send(error(text));
return;
}
// the body keeps its state; the blocks start anew. the old graph is kept alive, as the cab and the sounds
// of the vehicle hold addresses of its blocks until they are created again below
m_retired.push_back(body.Graph);
auto const old{body.Graph->body()};
body.Graph = std::make_shared<graph_instance>(graph);
auto &state{body.Graph->body()};
state.mover = &body;
state.native_brake_pipe = old.native_brake_pipe;
state.native_feed_pipe = old.native_feed_pipe;
Vehicle.create_graph_sounds();
if (simulation::Train != nullptr && simulation::Train->Dynamic() == &Vehicle)
{
simulation::Train->InitializeCab(body.CabOccupied, body.TypeName + ".mmd");
}
WriteLog("Vehicle graph preview: reloaded " + path);
Client.send({{"op", "reloaded"}});
}
// submodels of the models of the vehicle: the vehicle, its low poly interior and the cabs of the .mmd file
void submodels(preview_client &Client, TDynamicObject &Vehicle)
{
auto cabs{json::array()};
// the cab models are loaded the same way as by TTrain::InitializeCab(), so the models switching the cab would load
// are shared with it. in a network game the cab models are separate for each train: only the cabs of the
// player's train can be looked up then
auto const networked{false == Global.network_servers.empty() || Global.network_client.has_value()};
auto const occupied{simulation::Train != nullptr && simulation::Train->Dynamic() == &Vehicle};
if (false == networked || occupied)
{
auto const uid{networked ? static_cast<int>(simulation::Train->id()) : 0};
std::array<bool, 3> listed{false, false, false};
cParser parser(Vehicle.MoverParameters->TypeName + ".mmd", cParser::buffer_FILE, Vehicle.asBaseDir, true, std::vector<std::string>(), true);
std::string token;
do
{
token = "";
parser.getTokens();
parser >> token;
if (token.size() != 10 || token.compare(0, 3, "cab") != 0 || token.compare(4, 6, "model:") != 0 || token[3] < '0' || token[3] > '2')
{
continue;
}
auto const cabindex{token[3] - '0'};
token = "";
parser.getTokens();
parser >> token;
if (token.empty() || listed[cabindex])
{
continue;
}
listed[cabindex] = true;
TModel3d *model{nullptr};
std::replace(token.begin(), token.end(), '\\', '/');
if (token != "none")
{
erase_leading_slashes(token);
auto const texturepath{Global.asCurrentTexturePath};
Global.asCurrentTexturePath = Vehicle.asBaseDir;
model = TModelsManager::GetModel(Vehicle.asBaseDir + token, true, false, uid);
Global.asCurrentTexturePath = texturepath;
}
else if (cabindex == 1)
{
// the controls of cab 1 are looked up in the model of the vehicle then
model = Vehicle.mdModel;
}
if (model != nullptr)
{
auto entry{model_entry(model)};
entry["cab"] = cabindex;
cabs.push_back(std::move(entry));
}
} while (false == token.empty());
}
Client.send({{"op", "submodels"}, {"vehicle", model_entry(Vehicle.mdModel)}, {"lowpoly", model_entry(Vehicle.mdLowPolyInt)}, {"cabs", cabs}});
}
void push_values(preview_client &Client)
{
auto *vehicle{find_vehicle(Client.attached)};
if (vehicle == nullptr)
{
Client.send(error("the vehicle \"" + Client.attached + "\" is gone"));
Client.attached.clear();
return;
}
auto const &graph{*vehicle->MoverParameters->Graph};
auto const &definition{graph.definition()};
json signals = json::object();
json states = json::object();
json links = json::object();
auto const &nodes{definition.nodes()};
for (std::size_t nodeindex = 0; nodeindex < nodes.size(); ++nodeindex)
{
auto const &node{nodes[nodeindex]};
for (std::size_t portindex = 0; portindex < node.ports.size(); ++portindex)
{
auto const &port{node.ports[portindex]};
if (port.direction != port_direction::out)
{
continue;
}
auto const name{node.id + "." + port.key};
if (port.dom == domain::signal)
{
signals[name] = graph.signal(node.slots[portindex]);
}
else
{
auto const &link{graph.link(node.slots[portindex])};
links[name] = {link.forward, link.back};
}
}
auto const &block{graph.node_block(nodeindex)};
for (std::size_t stateindex = 0; stateindex < block.state_count(); ++stateindex)
{
states[node.id + ":" + block.state_name(stateindex)] = block.state_value(stateindex);
}
}
Client.send({{"op", "values"}, {"time", graph.time()}, {"signals", signals}, {"states", states}, {"links", links}});
}
asio::io_context m_context;
asio::ip::tcp::acceptor m_acceptor;
std::vector<std::shared_ptr<preview_client>> m_clients;
std::vector<std::shared_ptr<graph_instance>> m_retired;
};
} // namespace
void preview_update()
{
if (false == Global.vehiclegraph_preview)
{
return;
}
static preview_server server{Global.vehiclegraph_preview_port};
server.update();
}
} // namespace vgraph

View File

@@ -0,0 +1,22 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
// live preview of the vehicle graphs for the external editor: a TCP server on the local machine, speaking json lines
// (protocol 1, see tools/vehicle_graph/README.md). off unless enabled with -graph-preview [port] or with
// "vehiclegraph_preview yes [port]" in eu07.ini. everything runs on the simulation thread: the connections are served
// by polling, once per frame
namespace vgraph
{
// serves the preview connections; does nothing when the preview is disabled
void preview_update();
} // namespace vgraph

View File

@@ -11,11 +11,15 @@ http://mozilla.org/MPL/2.0/.
#include "vehicle/graph/graph_standalone.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_instance.h"
#include "vehicle/graph/graph_lua.h"
#include "vehicle/graph/blocks/cab.h"
#include "McZapkie/MOVER.h"
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
// command line tools of the vehicle graph. they run without the rest of the simulator; the log service doesn't run
@@ -33,6 +37,7 @@ struct script_event
std::string target;
double value{0.0};
int line{0};
std::size_t vehicle{0}; // index of the vehicle in the consist; the target was prefixed "k:" for the vehicle k
};
bool load_script(std::string const &Path, std::vector<script_event> &Events)
@@ -140,6 +145,42 @@ bool apply_event(graph_instance &Instance, script_event const &Event)
return true;
}
// motion of the vehicle without the track: the forces of the graph, the motion resistance and the mass of the body
// (TMoverParameters::ComputeTotalForce and ComputeMovement, much simplified)
void move_body(TMoverParameters &Body, double const Deltatime)
{
auto const &state{Body.Graph->body()};
if (Body.TotalMass <= 0.0)
{
return;
}
auto const tractive{state.tractive_force};
auto const resistance{state.brake_force + Body.FrictionForce()};
double force;
if (Body.V == 0.0)
{
// standing still until the tractive force overcomes the resistance
force = std::abs(tractive) > resistance ? tractive - (tractive > 0.0 ? resistance : -resistance) : 0.0;
}
else
{
force = tractive - (Body.V > 0.0 ? resistance : -resistance);
}
auto const previous{Body.V};
Body.AccS = force / Body.TotalMass;
Body.V += Body.AccS * Deltatime;
if (previous != 0.0 && Body.V * previous <= 0.0)
{
// stopped by the brakes and the resistance
Body.V = 0.0;
}
Body.Vel = std::abs(Body.V) * 3.6;
Body.nrot = Body.WheelDiameter > 0.0 ? Body.V / (M_PI * Body.WheelDiameter) : 0.0;
Body.DistCounter += std::abs(Body.V) * Deltatime / 1000.0;
Body.FTrain = tractive;
Body.Fb = state.brake_force;
}
std::string format_number(double const Value)
{
char buffer[32];
@@ -148,6 +189,141 @@ std::string format_number(double const Value)
return buffer;
}
// vehicle run by -graph-run: the first one, and the ones coupled behind it (-consist)
struct run_vehicle
{
std::string path;
bool reversed{false}; // coupled by its rear coupler, facing the other way than the first vehicle
std::shared_ptr<graph_template const> graph;
std::unique_ptr<TMoverParameters> body;
std::vector<std::pair<std::string, int>> signals;
bool hasbody{false};
bool mains{false}; // the graph has several line breakers, so the Mains of the body is written as well
std::string prefix; // of the result columns: empty for the first vehicle, "k:" for the vehicle k
};
// path of a vehicle of the consist: as given, or relative to the directory of the first vehicle's file
std::string consist_path(std::string const &Path, std::string const &First)
{
if (std::ifstream{Path}.is_open())
{
return Path;
}
auto const relative{std::filesystem::path{First}.parent_path() / Path};
if (std::filesystem::path{Path}.is_relative() && std::ifstream{relative}.is_open())
{
return relative.string();
}
return Path;
}
bool load_vehicle(run_vehicle &Vehicle)
{
std::vector<diagnostic> diagnostics;
Vehicle.graph = load_graph(Vehicle.path, diagnostics);
for (auto const &entry : diagnostics)
{
std::printf("%s: %s\n", Vehicle.path.c_str(), to_string(entry).c_str());
}
if (Vehicle.graph == nullptr)
{
return false;
}
auto const &graph{*Vehicle.graph};
std::printf("%s: %zu blocks, %zu signals, %zu links\n", Vehicle.path.c_str(), graph.nodes().size(), graph.signal_defaults().size(), graph.link_count());
return true;
}
// the body of the vehicle, filled from the graph as in the simulator, with the body entries the graph doesn't give
// taken from the .fiz file of the same name, if there is one
bool create_body(run_vehicle &Vehicle, std::string const &Name, bool const Ready, int const Cab)
{
auto const &input{Vehicle.path};
auto const &graph{Vehicle.graph};
Vehicle.body = std::make_unique<TMoverParameters>(0.0, "graph", Name, 1);
auto &body{*Vehicle.body};
auto fizfile{input};
fizfile = fizfile.substr(0, std::min(fizfile.size(), fizfile.find_last_of('.'))) + ".fiz";
if (false == body.AssignGraph(graph, fizfile))
{
std::printf("%s: the body section or the block parameters were rejected by the vehicle body\n", input.c_str());
return false;
}
auto const vehicleblocks{std::any_of(std::begin(graph->nodes()), std::end(graph->nodes()), [](auto const &Node) {
auto const &category{Node.type->category};
return category != "logic" && category != "cab" && category != "indicators" && category != "script" && category != "module";
})};
if (vehicleblocks && (body.Dim.L <= 0.0 || body.Mass <= 0.0))
{
// the simulator doesn't load such a vehicle; the graph itself can still be run here
std::printf("%s: warning: the body has no length or mass (body section, or %s), the simulator won't load the vehicle\n", input.c_str(), fizfile.c_str());
}
body.ComputeMass();
body.CheckLocomotiveParameters(Ready, 1);
body.CabActive = Cab;
body.Graph->body().standalone = true;
Vehicle.hasbody = (false == graph->body().empty());
Vehicle.mains = std::count_if(std::begin(graph->nodes()), std::end(graph->nodes()), [](auto const &Node) { return Node.type->id == "el.hsb"; }) > 1;
Vehicle.signals = graph->signal_outputs();
return true;
}
void write_header(std::ostringstream &Results, run_vehicle const &Vehicle)
{
auto const &graph{*Vehicle.graph};
auto const &instance{*Vehicle.body->Graph};
auto const &prefix{Vehicle.prefix};
for (auto const &signal : Vehicle.signals)
{
Results << ';' << prefix << signal.first;
}
for (std::size_t nodeindex = 0; nodeindex < graph.nodes().size(); ++nodeindex)
{
auto const &item{instance.node_block(nodeindex)};
for (std::size_t stateindex = 0; stateindex < item.state_count(); ++stateindex)
{
Results << ';' << prefix << graph.nodes()[nodeindex].id << ':' << item.state_name(stateindex);
}
}
if (Vehicle.hasbody)
{
// the vehicle body, for graphs describing whole vehicles
Results << ';' << prefix << "body:v;" << prefix << "body:ft;" << prefix << "body:fb;" << prefix << "body:pg;" << prefix << "body:zg;" << prefix << "body:bc";
}
if (Vehicle.mains)
{
Results << ';' << prefix << "body:mains";
}
}
void write_values(std::ostringstream &Results, run_vehicle const &Vehicle)
{
auto const &graph{*Vehicle.graph};
auto const &body{*Vehicle.body};
auto const &instance{*body.Graph};
for (auto const &signal : Vehicle.signals)
{
Results << ';' << format_number(instance.signal(signal.second));
}
for (std::size_t nodeindex = 0; nodeindex < graph.nodes().size(); ++nodeindex)
{
auto const &item{instance.node_block(nodeindex)};
for (std::size_t stateindex = 0; stateindex < item.state_count(); ++stateindex)
{
Results << ';' << format_number(item.state_value(stateindex));
}
}
if (Vehicle.hasbody)
{
Results << ';' << format_number(body.Vel) << ';' << format_number(body.FTrain / 1000.0) << ';' << format_number(body.Fb / 1000.0) << ';' << format_number(body.PipePress)
<< ';' << format_number(body.Compressor) << ';' << format_number(body.BrakePress);
}
if (Vehicle.mains)
{
Results << ';' << (body.Mains ? 1 : 0);
}
}
} // namespace
int graph_catalog_standalone(std::string const &Output)
@@ -171,18 +347,22 @@ int graph_catalog_standalone(std::string const &Output)
int graph_run_standalone(int const Argc, char *Argv[])
{
// -graph-run <file.vhg> [-script <events.txt>] [-o <results.csv>] [-dt <step>] [-t <duration>] [-every <steps>]
// -graph-run <file.vhg> [-script <events.txt>] [-o <results.csv>] [-dt <step>] [-t <duration>] [-every <steps>] [-state braked|ready] [-consist <file.vhg>[:r]]...
if (Argc < 3)
{
std::printf("usage: -graph-run <file.vhg> [-script <events.txt>] [-o <results.csv>] [-dt <step, s>] [-t <duration, s>] [-every <steps>]\n");
std::printf("usage: -graph-run <file.vhg> [-script <events.txt>] [-o <results.csv>] [-dt <step, s>] [-t <duration, s>] [-every <steps>] [-state braked|ready] "
"[-consist <file.vhg>[:r]]...\n");
return 1;
}
std::string const input{Argv[2]};
std::vector<run_vehicle> vehicles(1);
vehicles.front().path = input;
std::string script;
std::string output;
double deltatime{0.01};
double duration{0.0};
int every{1};
bool ready{false};
for (int idx = 3; idx + 1 < Argc; idx += 2)
{
std::string const option{Argv[idx]};
@@ -207,6 +387,20 @@ int graph_run_standalone(int const Argc, char *Argv[])
{
every = std::max(1, std::atoi(value.c_str()));
}
else if (option == "-state")
{
// initial state of the vehicle body, as for a vehicle placed in a scenario standing (braked) or moving (ready)
ready = (value == "ready");
}
else if (option == "-consist")
{
// next vehicle of the consist, coupled behind the previous one by its front coupler, or with ":r" reversed (by its rear coupler)
run_vehicle vehicle;
vehicle.reversed = (value.size() > 2 && value.ends_with(":r"));
vehicle.path = consist_path(vehicle.reversed ? value.substr(0, value.size() - 2) : value, input);
vehicle.prefix = std::to_string(vehicles.size() + 1) + ":";
vehicles.push_back(std::move(vehicle));
}
else
{
std::printf("unknown option \"%s\"\n", option.c_str());
@@ -219,23 +413,39 @@ int graph_run_standalone(int const Argc, char *Argv[])
return 1;
}
std::vector<diagnostic> diagnostics;
auto const graph{load_graph(input, diagnostics)};
for (auto const &entry : diagnostics)
// messages of the lua blocks go straight to the console
set_log_sink([](std::string const &Message) { std::printf("%s\n", Message.c_str()); });
for (auto &vehicle : vehicles)
{
std::printf("%s: %s\n", input.c_str(), to_string(entry).c_str());
if (false == load_vehicle(vehicle))
{
return 1;
}
}
if (graph == nullptr)
{
return 1;
}
std::printf("%s: %zu blocks, %zu signals, %zu links\n", input.c_str(), graph->nodes().size(), graph->signal_defaults().size(), graph->link_count());
std::vector<script_event> events;
if (false == script.empty() && false == load_script(script, events))
{
return 1;
}
for (auto &event : events)
{
// "k:" before the target picks the vehicle k of the consist
auto const separator{event.target.find(':')};
if (separator == std::string::npos || separator == 0 || event.target.find_first_not_of("0123456789") != separator)
{
continue;
}
auto const number{std::atoi(event.target.substr(0, separator).c_str())};
if (number < 1 || number > static_cast<int>(vehicles.size()))
{
std::printf("script line %d: there's no vehicle %d in the consist\n", event.line, number);
return 1;
}
event.vehicle = static_cast<std::size_t>(number - 1);
event.target.erase(0, separator + 1);
}
if (duration <= 0.0 && false == events.empty())
{
// by default run a second past the last event
@@ -246,21 +456,29 @@ int graph_run_standalone(int const Argc, char *Argv[])
return 0;
}
graph_instance instance{graph};
auto const signals{graph->signal_outputs()};
for (std::size_t idx = 0; idx < vehicles.size(); ++idx)
{
// the first vehicle has the occupied cab
if (false == create_body(vehicles[idx], idx == 0 ? "graph" : "graph" + std::to_string(idx + 1), ready, idx == 0 ? 1 : 0))
{
return 1;
}
}
// the consist: every vehicle coupled to the previous one with all the couplings, so the brake pipes connect as well
auto const allcouplings{coupling::coupler | coupling::brakehose | coupling::control | coupling::highvoltage | coupling::gangway | coupling::mainhose | coupling::heating |
coupling::permanent | coupling::power24v | coupling::power110v | coupling::power3x400v};
for (std::size_t idx = 1; idx < vehicles.size(); ++idx)
{
auto &previous{vehicles[idx - 1]};
auto &vehicle{vehicles[idx]};
previous.body->Attach(previous.reversed ? end::front : end::rear, vehicle.reversed ? end::rear : end::front, vehicle.body.get(), allcouplings, true, false);
}
std::ostringstream results;
results << "time";
for (auto const &signal : signals)
for (auto const &vehicle : vehicles)
{
results << ';' << signal.first;
}
for (std::size_t nodeindex = 0; nodeindex < graph->nodes().size(); ++nodeindex)
{
auto const &item{instance.node_block(nodeindex)};
for (std::size_t stateindex = 0; stateindex < item.state_count(); ++stateindex)
{
results << ';' << graph->nodes()[nodeindex].id << ':' << item.state_name(stateindex);
}
write_header(results, vehicle);
}
results << '\n';
@@ -272,29 +490,29 @@ int graph_run_standalone(int const Argc, char *Argv[])
auto const steptime{step * deltatime};
while (nextevent < events.size() && events[nextevent].time <= steptime + 1e-9)
{
if (false == apply_event(instance, events[nextevent]))
auto const &event{events[nextevent]};
if (false == apply_event(*vehicles[event.vehicle].body->Graph, event))
{
return 1;
}
++nextevent;
}
instance.update(deltatime);
// the vehicles in the order of the consist; each one moves on its own, without the forces of the couplers
for (auto &vehicle : vehicles)
{
auto &body{*vehicle.body};
body.EqvtPipePress = body.PipePress;
body.compute_movement_graph_(deltatime);
move_body(body, deltatime);
}
if ((step + 1) % every != 0 && step + 1 != stepcount)
{
continue;
}
results << format_number((step + 1) * deltatime);
for (auto const &signal : signals)
for (auto const &vehicle : vehicles)
{
results << ';' << format_number(instance.signal(signal.second));
}
for (std::size_t nodeindex = 0; nodeindex < graph->nodes().size(); ++nodeindex)
{
auto const &item{instance.node_block(nodeindex)};
for (std::size_t stateindex = 0; stateindex < item.state_count(); ++stateindex)
{
results << ';' << format_number(item.state_value(stateindex));
}
write_values(results, vehicle);
}
results << '\n';
}

View File

@@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "vehicle/graph/graph_template.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include <functional>
#include <queue>
@@ -85,6 +86,28 @@ bool resolve_param(param_def const &Definition, param_value const &Value, param_
Result = param_value::make_choice(Value.text);
return true;
}
case param_type::table:
{
if (Value.type != param_type::table)
{
Problem = "has to be a table: an array of rows, each an array of numbers";
return false;
}
if (false == Definition.columns.empty())
{
for (std::size_t rowindex = 0; rowindex < Value.rows.size(); ++rowindex)
{
if (Value.rows[rowindex].size() != Definition.columns.size())
{
Problem = "row " + std::to_string(rowindex + 1) + " has " + std::to_string(Value.rows[rowindex].size()) + " values, expected " +
std::to_string(Definition.columns.size()) + " (" + join_choices(Definition.columns) + ")";
return false;
}
}
}
Result = Value;
return true;
}
default:
{
Problem = "has unsupported type";
@@ -107,9 +130,46 @@ std::shared_ptr<graph_template const> graph_template::compile(document const &Do
auto result{std::make_shared<graph_template>()};
result->m_name = Document.name;
result->m_lowvoltage = Document.low_voltage_simulation;
result->m_body = Document.body;
result->m_profiles = Document.profiles;
// body of the vehicle
for (auto const &entry : Document.body)
{
auto const *field{find_body_field(entry.first)};
if (field == nullptr)
{
report(Diagnostics, severity::warning, "", "unknown body parameter \"" + entry.first + "\" was ignored");
continue;
}
param_def definition;
definition.key = field->key;
definition.type = field->type;
definition.choices = field->choices;
param_value value;
std::string problem;
if (false == resolve_param(definition, entry.second, value, problem))
{
report(Diagnostics, severity::error, "", "body parameter \"" + entry.first + "\" " + problem);
continue;
}
result->m_body.emplace_back(entry.first, std::move(value));
}
// interface profiles
for (auto const &profile : Document.profiles)
{
auto const &known{known_profiles()};
auto const lookup{std::find_if(std::begin(known), std::end(known), [&](profile_info const &Info) { return Info.id == profile.first; })};
if (lookup == std::end(known))
{
report(Diagnostics, severity::warning, "", "unknown interface profile \"" + profile.first + "\" was ignored");
}
else if (profile.second > lookup->version)
{
report(Diagnostics, severity::error, "",
"interface profile \"" + profile.first + "\" version " + std::to_string(profile.second) + " is newer than the supported version " + std::to_string(lookup->version));
}
}
// nodes and their parameters
std::unordered_map<std::string, int> nodeindices;
std::unordered_map<std::string, bool> unresolvednodes; // nodes of unknown types, to avoid reporting their connections again
@@ -126,21 +186,41 @@ std::shared_ptr<graph_template const> graph_template::compile(document const &Do
node.id = desc.id;
node.title = desc.title;
node.type = type;
node.params.reserve(type->params.size());
for (auto const &definition : type->params)
node.script = desc.script;
if (type->resolve != nullptr)
{
std::string problem;
if (false == type->resolve(desc, node.ports, node.param_defs, problem))
{
report(Diagnostics, severity::error, desc.id, problem);
unresolvednodes.emplace(desc.id, true);
continue;
}
}
else
{
node.ports = type->ports;
node.param_defs = type->params;
}
if (type->id == "lua.block")
{
result->m_useslua = true;
}
node.params.reserve(node.param_defs.size());
for (auto const &definition : node.param_defs)
{
node.params.push_back(definition.default_value);
}
for (auto const &param : desc.params)
{
auto const paramindex{type->param_index(param.first)};
auto const paramindex{find_param(node.param_defs, param.first)};
if (paramindex < 0)
{
report(Diagnostics, severity::warning, desc.id, "unknown parameter \"" + param.first + "\" of block type \"" + type->id + "\" was ignored");
continue;
}
std::string problem;
if (false == resolve_param(type->params[paramindex], param.second, node.params[paramindex], problem))
if (false == resolve_param(node.param_defs[paramindex], param.second, node.params[paramindex], problem))
{
report(Diagnostics, severity::error, desc.id, "parameter \"" + param.first + "\" " + problem);
}
@@ -150,17 +230,33 @@ std::shared_ptr<graph_template const> graph_template::compile(document const &Do
}
auto &nodes{result->m_nodes};
// blocks standing for parts of the vehicle the simulator has only one of
{
std::unordered_map<std::string, std::string> singles;
for (auto const &node : nodes)
{
if (false == node.type->single)
{
continue;
}
auto const insert{singles.emplace(node.type->id, node.id)};
if (false == insert.second)
{
report(Diagnostics, severity::error, node.id, "the graph can have only one block of type \"" + node.type->id + "\"; the other one is \"" + insert.first->second + "\"");
}
}
}
auto &signaldefaults{result->m_signaldefaults};
auto &linkcount{result->m_linkcount};
// outputs own their slots and links
for (auto &node : nodes)
{
node.slots.assign(node.type->ports.size(), -1);
node.connected.assign(node.type->ports.size(), 0);
for (std::size_t portindex = 0; portindex < node.type->ports.size(); ++portindex)
node.slots.assign(node.ports.size(), -1);
node.connected.assign(node.ports.size(), 0);
for (std::size_t portindex = 0; portindex < node.ports.size(); ++portindex)
{
auto const &port{node.type->ports[portindex]};
auto const &port{node.ports[portindex]};
if (port.direction != port_direction::out)
{
continue;
@@ -181,7 +277,7 @@ std::shared_ptr<graph_template const> graph_template::compile(document const &Do
std::vector<std::vector<endpoint>> sources(nodes.size());
for (std::size_t nodeindex = 0; nodeindex < nodes.size(); ++nodeindex)
{
sources[nodeindex].resize(nodes[nodeindex].type->ports.size());
sources[nodeindex].resize(nodes[nodeindex].ports.size());
}
std::vector<std::vector<int>> edges(nodes.size()); // ordering constraints: source node -> consumer node
bool lowvoltageignored{false};
@@ -203,20 +299,22 @@ std::shared_ptr<graph_template const> graph_template::compile(document const &Do
auto const tonode{tolookup->second};
auto const &fromtype{*nodes[fromnode].type};
auto const &totype{*nodes[tonode].type};
auto const fromport{fromtype.port_index(connection.from_port)};
auto const toport{totype.port_index(connection.to_port)};
if (fromport < 0 || fromtype.ports[fromport].direction != port_direction::out)
auto const &fromports{nodes[fromnode].ports};
auto const &toports{nodes[tonode].ports};
auto const fromport{find_port(fromports, connection.from_port)};
auto const toport{find_port(toports, connection.to_port)};
if (fromport < 0 || fromports[fromport].direction != port_direction::out)
{
report(Diagnostics, severity::error, connection.from_node, "connection " + label + ": \"" + connection.from_port + "\" isn't an output of block type \"" + fromtype.id + "\"");
continue;
}
if (toport < 0 || totype.ports[toport].direction != port_direction::in)
if (toport < 0 || toports[toport].direction != port_direction::in)
{
report(Diagnostics, severity::error, connection.to_node, "connection " + label + ": \"" + connection.to_port + "\" isn't an input of block type \"" + totype.id + "\"");
continue;
}
auto const fromdomain{fromtype.ports[fromport].dom};
auto const todomain{totype.ports[toport].dom};
auto const fromdomain{fromports[fromport].dom};
auto const todomain{toports[toport].dom};
if (fromdomain != todomain)
{
report(Diagnostics, severity::error, connection.to_node,
@@ -249,9 +347,9 @@ std::shared_ptr<graph_template const> graph_template::compile(document const &Do
for (std::size_t nodeindex = 0; nodeindex < nodes.size(); ++nodeindex)
{
auto &node{nodes[nodeindex]};
for (std::size_t portindex = 0; portindex < node.type->ports.size(); ++portindex)
for (std::size_t portindex = 0; portindex < node.ports.size(); ++portindex)
{
auto const &port{node.type->ports[portindex]};
auto const &port{node.ports[portindex]};
if (port.direction != port_direction::in)
{
continue;
@@ -379,8 +477,8 @@ int graph_template::find_signal(std::string_view const Endpoint) const
return -1;
}
auto const &node{m_nodes[nodeindex]};
auto const portindex{node.type->port_index(Endpoint.substr(dot + 1))};
if (portindex < 0 || node.type->ports[portindex].dom != domain::signal)
auto const portindex{find_port(node.ports, Endpoint.substr(dot + 1))};
if (portindex < 0 || node.ports[portindex].dom != domain::signal)
{
return -1;
}
@@ -394,7 +492,17 @@ std::shared_ptr<graph_template const> load_graph(std::string const &Path, std::v
{
return nullptr;
}
return graph_template::compile(graph, catalog::get(), Diagnostics);
auto const separator{Path.find_last_of("/\\")};
if (false == expand_modules(graph, separator == std::string::npos ? std::string{} : Path.substr(0, separator + 1), Diagnostics))
{
return nullptr;
}
auto result{graph_template::compile(graph, catalog::get(), Diagnostics)};
if (result != nullptr)
{
std::const_pointer_cast<graph_template>(result)->m_path = Path;
}
return result;
}
std::vector<std::pair<std::string, int>> graph_template::signal_outputs() const
@@ -402,9 +510,9 @@ std::vector<std::pair<std::string, int>> graph_template::signal_outputs() const
std::vector<std::pair<std::string, int>> result;
for (auto const &node : m_nodes)
{
for (std::size_t portindex = 0; portindex < node.type->ports.size(); ++portindex)
for (std::size_t portindex = 0; portindex < node.ports.size(); ++portindex)
{
auto const &port{node.type->ports[portindex]};
auto const &port{node.ports[portindex]};
if (port.direction == port_direction::out && port.dom == domain::signal)
{
result.emplace_back(node.id + "." + port.key, node.slots[portindex]);

View File

@@ -34,9 +34,12 @@ public:
std::string id;
std::string title;
block_type const *type{nullptr};
std::vector<param_value> params; // in the order of the type's parameter definitions
std::vector<int> slots; // in the order of the type's port definitions: signal slot, or link index for physical ports
std::vector<std::uint8_t> connected; // in the order of the type's port definitions: whether the port has a connection
std::vector<port_def> ports; // the type's ports, or the ones resolved for this node (lua blocks)
std::vector<param_def> param_defs; // the type's parameters, or the ones resolved for this node
std::vector<param_value> params; // in the order of param_defs
std::vector<int> slots; // in the order of ports: signal slot, or link index for physical ports
std::vector<std::uint8_t> connected; // in the order of ports: whether the port has a connection
std::string script; // source of a lua block
};
// checks the document and builds the template. problems are appended to the diagnostics; returns null on errors
@@ -46,6 +49,11 @@ public:
{
return m_name;
}
// the file the graph was loaded from, if any
std::string const &path() const
{
return m_path;
}
bool low_voltage_simulation() const
{
return m_lowvoltage;
@@ -83,14 +91,23 @@ public:
int find_signal(std::string_view Endpoint) const;
// names of signal outputs in "node.port" form, with their slots, in the order of nodes and ports
std::vector<std::pair<std::string, int>> signal_outputs() const;
// whether any node needs the lua state
bool uses_lua() const
{
return m_useslua;
}
private:
friend std::shared_ptr<graph_template const> load_graph(std::string const &Path, std::vector<diagnostic> &Diagnostics);
std::string m_name;
std::string m_path;
bool m_lowvoltage{false};
std::vector<node_entry> m_nodes;
std::vector<int> m_order;
std::vector<double> m_signaldefaults;
std::size_t m_linkcount{0};
bool m_useslua{false};
std::vector<named_value> m_body;
std::vector<std::pair<std::string, int>> m_profiles;
};

View File

@@ -0,0 +1,276 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/Train.h"
#include "vehicle/graph/graph_instance.h"
#include "vehicle/graph/blocks/cab.h"
#include "model/Model3d.h"
#include "utilities/Logs.h"
// cab of a graph vehicle: its controls and indicators are the cab blocks of the graph. the animated submodels are found
// in the models of the vehicle the same way as for the controls of the .mmd files, and registered with the "graph:<id>"
// labels, which the mouse input turns into the vehiclegraphcontrol command
namespace
{
TSubModel *find_submodel(TDynamicObject const &Vehicle, std::string const &Name)
{
std::array<TModel3d *, 3> const sources{Vehicle.mdKabina, Vehicle.mdLowPolyInt, Vehicle.mdModel};
for (auto *source : sources)
{
if (source == nullptr)
{
continue;
}
if (auto *submodel{source->GetFromName(Name)})
{
return submodel;
}
}
return nullptr;
}
// whether a block placed in the specified cab ("cab" parameter: 0 any, 1, 2) belongs to the cab with the specified index
// placement from the cab parameter: 0 any cab, 1 or 2, -1 the engine room (cab index 0)
bool in_cab(int const Placement, int const Cabindex)
{
return Placement == 0 || (Placement < 0 ? Cabindex == 0 : Placement == Cabindex);
}
user_command find_command(std::string const &Name)
{
if (Name.empty())
{
return user_command::none;
}
auto const lookup{simulation::commandMap.find(Name)};
return lookup != simulation::commandMap.end() ? lookup->second : user_command::none;
}
} // namespace
void TTrain::initialize_graph_controls(int const Cabindex)
{
m_graphcommands.clear();
auto *graph{mvOccupied->Graph.get()};
if (graph == nullptr)
{
return;
}
// controls and indicators of the .mmd aren't used by graph vehicles
clear_cab_controls();
Cabine[Cabindex].ClearControls();
m_controlmapper.clear();
auto const &nodes{graph->definition().nodes()};
for (std::size_t idx = 0; idx < nodes.size(); ++idx)
{
auto &block{graph->node_block(idx)};
auto const &id{nodes[idx].id};
if (auto *control{dynamic_cast<vgraph::cab_switch *>(&block)})
{
// commands of the simulator operate the control in every cab
auto const bind = [&](std::string const &Name, int const Operation) {
auto const command{find_command(Name)};
if (command == user_command::none)
{
if (false == Name.empty())
{
ErrorLog("Bad vehicle graph: unknown command \"" + Name + "\" bound to control \"" + id + "\"");
}
return;
}
m_graphcommands[command].push_back({control, Operation});
};
bind(control->command(), 0);
bind(control->key_increase(), 1);
bind(control->key_decrease(), -1);
if (control->submodel().empty() || false == in_cab(control->cab(), Cabindex))
{
continue;
}
auto *submodel{find_submodel(*DynamicObject, control->submodel())};
if (submodel == nullptr)
{
ErrorLog("Bad model: failed to locate sub-model \"" + control->submodel() + "\" of control \"" + id + "\" in 3d model(s) of \"" + DynamicObject->name() + "\"",
logtype::model);
continue;
}
// the animation is given in degrees or millimetres per position
auto const rotate{control->animation() == vgraph::cab_switch::animation_type::rotate};
auto const unit{rotate ? 360.0 : 1000.0};
auto &gauge{Cabine[Cabindex].Gauge(-1)};
gauge.AssignSounds(control->sound_increase(), control->sound_decrease(), control->sound_positions(), DynamicObject);
gauge.Init(submodel, nullptr, rotate ? TGaugeAnimation::gt_Rotate : TGaugeAnimation::gt_Move, static_cast<float>(control->step() / unit),
static_cast<float>(control->offset() / unit), static_cast<float>(control->smoothing()), *control->display_state());
gauge.AssignFloat(control->display_state());
m_controlmapper.insert(gauge, "graph:" + id);
}
else if (auto *lamp{dynamic_cast<vgraph::cab_lamp *>(&block)})
{
if (lamp->submodel().empty() || false == in_cab(lamp->cab(), Cabindex))
{
continue;
}
auto &button{Cabine[Cabindex].Button(-1)};
std::array<TModel3d *, 3> const sources{DynamicObject->mdKabina, DynamicObject->mdLowPolyInt, DynamicObject->mdModel};
auto found{false};
for (auto *source : sources)
{
if (button.Init(lamp->submodel(), source, false))
{
found = true;
break;
}
}
if (false == found)
{
ErrorLog("Bad model: failed to locate sub-model \"" + lamp->submodel() + "\" of lamp \"" + id + "\" in 3d model(s) of \"" + DynamicObject->name() + "\"",
logtype::model);
}
button.AssignBool(lamp->lit_state());
button.AssignSounds(lamp->sound_on(), lamp->sound_off(), DynamicObject);
}
else if (auto *indicator{dynamic_cast<vgraph::cab_gauge *>(&block)})
{
if (false == in_cab(indicator->cab(), Cabindex))
{
continue;
}
// the needle turns from angle_min at min to angle_max at max
auto const range{indicator->max() - indicator->min()};
auto const scale{range != 0.0 ? (indicator->angle_max() - indicator->angle_min()) / 360.0 / range : 0.0};
auto const offset{indicator->angle_min() / 360.0 - indicator->min() * scale};
for (std::size_t needle = 0; needle < (indicator->two_needles() ? 2 : 1); ++needle)
{
auto const &name{indicator->submodel(needle)};
if (name.empty())
{
continue;
}
auto *submodel{find_submodel(*DynamicObject, name)};
if (submodel == nullptr)
{
ErrorLog("Bad model: failed to locate sub-model \"" + name + "\" of gauge \"" + id + "\" in 3d model(s) of \"" + DynamicObject->name() + "\"",
logtype::model);
continue;
}
auto &gauge{Cabine[Cabindex].Gauge(-1)};
gauge.Init(submodel, nullptr, TGaugeAnimation::gt_Rotate, static_cast<float>(scale), static_cast<float>(offset), static_cast<float>(indicator->smoothing()),
*indicator->value_state(needle));
gauge.AssignFloat(indicator->value_state(needle));
}
}
}
}
bool TTrain::graph_command(command_data const &Command)
{
if (Command.command == user_command::vehiclegraphcontrol)
{
// mouse on a control of the graph, or the hardware desk
auto *control{dynamic_cast<vgraph::cab_switch *>(mvOccupied->Graph->find_block(Command.payload))};
if (control == nullptr)
{
return true;
}
auto const increase{Command.param1 >= 0.0};
if (Command.action == GLFW_RELEASE)
{
control->release();
}
else if (control->behaviour() == vgraph::cab_switch::behaviour_type::multi)
{
// levers move with every press and repeat
if (increase)
{
control->increase();
}
else
{
control->decrease();
}
}
else if (Command.action == GLFW_PRESS)
{
control->press();
}
return true;
}
auto const lookup{m_graphcommands.find(Command.command)};
if (lookup == m_graphcommands.end())
{
return false;
}
for (auto const &binding : lookup->second)
{
auto &control{*binding.control};
if (Command.action == GLFW_RELEASE)
{
control.release();
continue;
}
switch (binding.operation)
{
case 1:
{
control.increase();
break;
}
case -1:
{
control.decrease();
break;
}
default:
{
if (Command.action == GLFW_PRESS)
{
control.press();
}
break;
}
}
}
return true;
}
void TTrain::export_graph_values(dictionary_source &Dictionary) const
{
auto const *graph{mvOccupied->Graph.get()};
auto const &nodes{graph->definition().nodes()};
for (std::size_t idx = 0; idx < nodes.size(); ++idx)
{
auto const *item{dynamic_cast<vgraph::cab_export const *>(&graph->node_block(idx))};
if (item == nullptr || item->name().empty())
{
continue;
}
switch (item->kind())
{
case vgraph::cab_export::kind_type::integer:
{
Dictionary.insert(item->name(), static_cast<int>(std::lround(item->value())));
break;
}
case vgraph::cab_export::kind_type::boolean:
{
Dictionary.insert(item->name(), item->value() > 0.5);
break;
}
default:
{
Dictionary.insert(item->name(), item->value());
break;
}
}
}
}

View File

@@ -75,6 +75,8 @@ std::string_view to_string(param_type const Type)
return "text";
case param_type::choice:
return "choice";
case param_type::table:
return "table";
default:
return "number";
}
@@ -112,6 +114,38 @@ param_value param_value::make_choice(std::string Value)
return result;
}
param_value param_value::make_table(std::vector<std::vector<double>> Rows)
{
param_value result;
result.type = param_type::table;
result.rows = std::move(Rows);
return result;
}
int find_port(std::vector<port_def> const &Ports, std::string_view const Key)
{
for (std::size_t idx = 0; idx < Ports.size(); ++idx)
{
if (Ports[idx].key == Key)
{
return static_cast<int>(idx);
}
}
return -1;
}
int find_param(std::vector<param_def> const &Params, std::string_view const Key)
{
for (std::size_t idx = 0; idx < Params.size(); ++idx)
{
if (Params[idx].key == Key)
{
return static_cast<int>(idx);
}
}
return -1;
}
bool has_errors(std::vector<diagnostic> const &Diagnostics)
{
return std::any_of(std::begin(Diagnostics), std::end(Diagnostics), [](diagnostic const &Diagnostic) { return Diagnostic.level == severity::error; });

View File

@@ -74,7 +74,8 @@ enum class param_type : std::uint8_t
number,
boolean,
text,
choice
choice,
table // rows of numbers, e.g. the resistor table of a series motor
};
std::string_view to_string(param_type Type);
@@ -86,11 +87,13 @@ struct param_value
double number{0.0};
bool boolean{false};
std::string text; // also holds the choice id
std::vector<std::vector<double>> rows; // table parameters only
static param_value make_number(double Value);
static param_value make_boolean(bool Value);
static param_value make_text(std::string Value);
static param_value make_choice(std::string Value);
static param_value make_table(std::vector<std::vector<double>> Rows);
};
struct port_def
@@ -111,6 +114,11 @@ struct param_def
std::string unit;
param_value default_value;
std::vector<std::string> choices; // choice parameters only, stable ascii ids
std::vector<std::string> columns; // table parameters only: names of the columns, empty for rows of free width
// .fiz entry the parameter stands for, if any, e.g. "Brake:" and "MaxBP". the simulator writes such parameters
// to the vehicle body (TMoverParameters) with the code reading the .fiz files
std::string fiz_section;
std::string fiz_key;
};
enum class severity : std::uint8_t
@@ -127,6 +135,10 @@ struct diagnostic
std::string message;
};
// index of the port or parameter with the specified key, or -1
int find_port(std::vector<port_def> const &Ports, std::string_view Key);
int find_param(std::vector<param_def> const &Params, std::string_view Key);
bool has_errors(std::vector<diagnostic> const &Diagnostics);
std::string to_string(diagnostic const &Diagnostic);

View File

@@ -0,0 +1,465 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "McZapkie/MOVER.h"
#include "vehicle/graph/graph_body.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_instance.h"
#include "vehicle/graph/graph_lua.h"
#include "utilities/Logs.h"
#include <cmath>
#include <cstdio>
#include <fstream>
#include <sstream>
// vehicle body of a graph vehicle: TMoverParameters filled from the .vhg file. the body section and the parameters of
// the blocks which stand for .fiz entries are written as .fiz lines and read with the code reading the .fiz files, so
// the derived values and the defaults are the same as for a .fiz vehicle with the same entries
namespace
{
std::string format_value(double const Value)
{
char buffer[32];
std::snprintf(buffer, sizeof(buffer), "%.10g", Value);
return buffer;
}
// .fiz lines being built, in the order of the sections
class fiz_lines
{
public:
void add(std::string const &Section, std::string const &Key, std::string const &Value)
{
auto &line{find(Section)};
line += " " + Key + "=" + Value;
}
bool has(std::string const &Section, std::string const &Key) const
{
for (auto const &entry : m_lines)
{
if (entry.first == Section)
{
return (entry.second + " ").find(" " + Key + "=") != std::string::npos;
}
}
return false;
}
// the line of the section, empty if nothing was added to it
std::string get(std::string const &Section) const
{
for (auto const &entry : m_lines)
{
if (entry.first == Section)
{
return entry.first + entry.second;
}
}
return {};
}
std::vector<std::pair<std::string, std::string>> const &lines() const
{
return m_lines;
}
private:
std::string &find(std::string const &Section)
{
for (auto &entry : m_lines)
{
if (entry.first == Section)
{
return entry.second;
}
}
m_lines.emplace_back(Section, std::string{});
return m_lines.back().second;
}
std::vector<std::pair<std::string, std::string>> m_lines;
};
// value of a parameter as written in a .fiz file; values of the .fiz files can't have spaces
std::string fiz_value(vgraph::param_value const &Value, std::vector<std::string> const &Choices, std::vector<std::string> const &Fizvalues)
{
switch (Value.type)
{
case vgraph::param_type::boolean:
return Value.boolean ? "Yes" : "No";
case vgraph::param_type::number:
return format_value(Value.number);
case vgraph::param_type::choice:
{
if (false == Fizvalues.empty())
{
for (std::size_t idx = 0; idx < Choices.size() && idx < Fizvalues.size(); ++idx)
{
if (Choices[idx] == Value.text)
{
return Fizvalues[idx];
}
}
}
return Value.text;
}
default:
{
auto text{Value.text};
std::replace(std::begin(text), std::end(text), ' ', '_');
return text;
}
}
}
// entries of the body sections of a .fiz file which the graph doesn't give. a graph made only of the cab logic, without
// the body section, can this way be tried on an existing vehicle. returns the number of entries taken
int add_fiz_body(fiz_lines &Lines, std::string const &Fizfile)
{
std::ifstream file{Fizfile, std::ios::binary};
if (false == file.is_open())
{
return 0;
}
static char const *const sections[]{"Param.", "Load:", "Dimensions:", "Wheels:", "BuffCoupl.", "Headlights:"};
int count{0};
std::string line;
while (std::getline(file, line))
{
if (false == line.empty() && line.back() == '\r')
{
line.pop_back();
}
line.erase(0, std::min(line.size(), line.find_first_not_of(" \t")));
if (line.starts_with("//"))
{
continue;
}
for (auto const *section : sections)
{
if (false == line.starts_with(section))
{
continue;
}
std::istringstream entries{line.substr(std::string_view{section}.size())};
std::string entry;
while (entries >> entry)
{
if (entry.starts_with("//"))
{
break;
}
auto const separator{entry.find('=')};
if (separator == std::string::npos || separator == 0)
{
continue;
}
auto const key{entry.substr(0, separator)};
if (false == Lines.has(section, key))
{
Lines.add(section, key, entry.substr(separator + 1));
++count;
}
}
break;
}
}
return count;
}
vgraph::param_value const *node_param(vgraph::graph_template::node_entry const &Node, std::string_view const Key)
{
auto const index{vgraph::find_param(Node.param_defs, Key)};
return index >= 0 ? &Node.params[index] : nullptr;
}
double node_number(vgraph::graph_template::node_entry const &Node, std::string_view const Key, double const Default = 0.0)
{
auto const *value{node_param(Node, Key)};
return value != nullptr ? value->number : Default;
}
bool node_boolean(vgraph::graph_template::node_entry const &Node, std::string_view const Key)
{
auto const *value{node_param(Node, Key)};
return value != nullptr && value->boolean;
}
} // namespace
bool TMoverParameters::LoadGraph(std::string const &File)
{
// messages of the graphs (lua blocks) go to the log of the simulator
static bool const logsink{[]() {
vgraph::set_log_sink([](std::string const &Message) { WriteLog(Message); });
return true;
}()};
(void)logsink;
WriteLog("LOAD GRAPH FROM " + File);
std::vector<vgraph::diagnostic> diagnostics;
auto const graph{vgraph::load_graph(File, diagnostics)};
for (auto const &entry : diagnostics)
{
if (entry.level == vgraph::severity::error)
{
ErrorLog("Bad vehicle graph: " + File + ": " + vgraph::to_string(entry));
}
else
{
WriteLog("Vehicle graph: " + File + ": " + vgraph::to_string(entry));
}
}
if (graph == nullptr)
{
ConversionError = 667;
return false;
}
// the .fiz of the same name, if there is one, gives the body entries the graph doesn't have
auto fizfile{File};
fizfile = fizfile.substr(0, std::min(fizfile.size(), fizfile.find_last_of('.'))) + ".fiz";
if (false == AssignGraph(graph, fizfile))
{
return false;
}
// without length the vehicle can't be placed on the track, and without mass it can't move
if (Dim.L <= 0.0 || Mass <= 0.0)
{
ErrorLog("Bad vehicle graph: " + File + ": the body needs the length and the mass (body section of the graph, or the .fiz file of the same name next to it)");
ConversionError = 668;
return false;
}
return true;
}
bool TMoverParameters::AssignGraph(std::shared_ptr<vgraph::graph_template const> const &Template, std::string const &Fizfile)
{
ConversionError = 0;
fiz_lines lines;
// body
for (auto const &entry : Template->body())
{
auto const *field{vgraph::find_body_field(entry.first)};
if (field == nullptr)
{
continue;
}
if (field->key == "light_color")
{
int red{255}, green{255}, blue{255};
std::sscanf(entry.second.text.c_str(), "%d %d %d", &red, &green, &blue);
lines.add(field->fiz_section, "LampRed", std::to_string(red));
lines.add(field->fiz_section, "LampGreen", std::to_string(green));
lines.add(field->fiz_section, "LampBlue", std::to_string(blue));
continue;
}
lines.add(field->fiz_section, field->fiz_key, fiz_value(entry.second, field->choices, field->fiz_values));
}
// equipment parameters of the blocks
bool nativebrakepipe{true};
bool nativefeedpipe{true};
for (auto const &node : Template->nodes())
{
auto const &id{node.type->id};
for (std::size_t idx = 0; idx < node.param_defs.size(); ++idx)
{
auto const &definition{node.param_defs[idx]};
if (definition.fiz_key.empty())
{
continue;
}
auto const &value{node.params[idx]};
if (id == "br.distributor" && definition.key == "asb" && value.text == "none")
{
continue;
}
lines.add(definition.fiz_section, definition.fiz_key, fiz_value(value, definition.choices, {}));
}
// entries made of several parameters, or implied by the presence of the block
if (id == "br.distributor")
{
lines.add("Cntrl.", "BrakeSystem", "Pneumatic");
// the number of positions of the brake controller enables the anti-slip brake setting
lines.add("Cntrl.", "BCPN", "1");
}
else if (id == "br.local_handle")
{
lines.add("Cntrl.", "LocalBrake", "PneumaticBrake");
}
else if (id == "br.pipe")
{
nativebrakepipe = false;
}
else if (id == "pn.reservoir")
{
nativefeedpipe = false;
}
else if (id == "sec.ca")
{
std::string system;
auto const addflag = [&](char const *Flag) { system += (system.empty() ? "" : ",") + std::string{Flag}; };
if (node_boolean(node, "aware"))
addflag("Active");
if (node_boolean(node, "cab_signal"))
addflag("CabSignal");
if (node_boolean(node, "separate_ack"))
addflag("SeparateAcknowledge");
if (node_boolean(node, "sifa"))
addflag("Sifa");
if (false == system.empty())
{
lines.add("Security:", "AwareSystem", system);
}
auto const minspeed{node_number(node, "min_speed", -1.0)};
if (minspeed >= 0.0)
{
lines.add("Security:", "AwareMinSpeed", format_value(minspeed));
}
}
else if (id == "veh.doors")
{
auto const range{node_number(node, "range", 1.0)};
lines.add("Doors:", "OpenCtrl", "DriverCtrl");
lines.add("Doors:", "CloseCtrl", "DriverCtrl");
lines.add("Doors:", "OpenSpeed", format_value(range / std::max(0.01, node_number(node, "open_time", 2.0))));
lines.add("Doors:", "CloseSpeed", format_value(range / std::max(0.01, node_number(node, "close_time", 3.0))));
// the graph decides whether the doors have power
lines.add("Doors:", "DoorVoltage", "0");
}
else if (id == "el.pantograph")
{
lines.add("Power:", "EnginePower", "CurrentCollector");
}
else if (id == "if.heat")
{
lines.add("Clima:", "Heating", "Main");
}
}
if (false == Fizfile.empty())
{
auto const count{add_fiz_body(lines, Fizfile)};
if (count > 0)
{
WriteLog("graph: " + std::to_string(count) + " body entries from " + Fizfile);
}
}
// sections in the order of the .fiz files
auto const read = [&](std::string const &Section, auto Reader) {
auto const line{lines.get(Section)};
if (false == line.empty())
{
WriteLog("graph: " + line);
Reader(line);
}
};
read("Param.", [this](std::string const &Line) { LoadFIZ_Param(Line); });
read("Load:", [this](std::string const &Line) { LoadFIZ_Load(Line); });
read("Dimensions:", [this](std::string const &Line) { LoadFIZ_Dimensions(Line); });
read("Wheels:", [this](std::string const &Line) { LoadFIZ_Wheels(Line); });
read("Brake:", [this](std::string const &Line) { LoadFIZ_Brake(Line); });
read("Doors:", [this](std::string const &Line) { LoadFIZ_Doors(Line); });
read("BuffCoupl.", [this](std::string const &Line) { LoadFIZ_BuffCoupl(Line, 0); });
read("Cntrl.", [this](std::string const &Line) { LoadFIZ_Cntrl(Line); });
read("Headlights:", [this](std::string const &Line) { LoadFIZ_Headlights(Line); });
read("SpringBrake:", [this](std::string const &Line) { LoadFIZ_SpringBrake(Line); });
read("Security:", [this](std::string const &Line) { SecuritySystem.load(Line, Vmax); });
read("Clima:", [this](std::string const &Line) { LoadFIZ_Clima(Line); });
read("Power:", [this](std::string const &Line) { LoadFIZ_Power(Line); });
if (EnginePowerSource.SourceType == TPowerSource::CurrentCollector && Power <= 0.0)
{
ErrorLog("Bad vehicle graph: " + Template->path() + ": the pantographs need the body power above zero");
}
Graph = std::make_shared<vgraph::graph_instance>(Template);
auto &state{Graph->body()};
state.mover = this;
state.native_brake_pipe = nativebrakepipe;
state.native_feed_pipe = nativefeedpipe;
return ConversionError == 0;
}
void TMoverParameters::compute_movement_graph_(double const Deltatime)
{
auto const dt{Deltatime};
if (LoadStatus > 0)
{
LastLoadChangeTime += dt;
}
RunInternalCommand();
TotalCurrent = 0;
// the pipes take in the flows from the neighbours since the last step, as at the start of UpdatePipePressure and
// UpdateScndPipePressure
if (PipePress > 1.0)
{
Pipe->Flow(-PipePress * AirLeakRate * dt);
Pipe->Act();
}
PipePress = Pipe->P();
if (ScndPipePress > 1.0)
{
Pipe2->Flow(-ScndPipePress * AirLeakRate * dt);
Pipe2->Act();
}
ScndPipePress = Pipe2->P();
auto &state{Graph->body()};
state.velocity = V;
state.acceleration = AccS;
state.wheel_speed = nrot;
state.slipping = SlippingWheels;
state.cab = CabActive;
state.distance = DistCounter * 1000.0;
state.tractive_force = 0.0;
state.brake_force = 0.0;
state.current = 0.0;
Graph->update(dt);
// the current of the vehicle, for the pantographs of the vehicle model
Itot = 0.0;
TotalCurrent = state.current;
// pipes without blocks still connect the neighbours
if (state.native_brake_pipe)
{
Pipe->Flow(GetDVc(dt));
Pipe->Act();
PipePress = Pipe->P();
}
if (state.native_feed_pipe)
{
Pipe2->Flow(ScndPipeExchange(dt));
Pipe2->Act();
ScndPipePress = Pipe2->P();
}
update_doors(dt);
PowerCouplersCheck(dt, coupling::highvoltage);
PowerCouplersCheck(dt, coupling::power110v);
PowerCouplersCheck(dt, coupling::power24v);
Power24vVoltage = std::max(PowerCircuits[0].first, GetTrainsetVoltage(coupling::power24v));
Power24vIsAvailable = Power24vVoltage > 0;
Power110vIsAvailable = PowerCircuits[1].first > 0 || GetTrainsetVoltage(coupling::power110v) > 0;
}
void TMoverParameters::graph_forces(double &Tractive, double &Brake) const
{
auto const &state{Graph->body()};
Tractive = state.tractive_force;
Brake = state.brake_force;
}