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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
920
vehicle/graph/blocks/diesel.cpp
Normal file
920
vehicle/graph/blocks/diesel.cpp
Normal 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
|
||||
658
vehicle/graph/blocks/drive.cpp
Normal file
658
vehicle/graph/blocks/drive.cpp
Normal 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
|
||||
658
vehicle/graph/blocks/electric.cpp
Normal file
658
vehicle/graph/blocks/electric.cpp
Normal 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
|
||||
468
vehicle/graph/blocks/interfaces.cpp
Normal file
468
vehicle/graph/blocks/interfaces.cpp
Normal 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
|
||||
278
vehicle/graph/blocks/lua_block.cpp
Normal file
278
vehicle/graph/blocks/lua_block.cpp
Normal 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
|
||||
186
vehicle/graph/blocks/module.cpp
Normal file
186
vehicle/graph/blocks/module.cpp
Normal 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 ¶m : 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
|
||||
801
vehicle/graph/blocks/pneumatic.cpp
Normal file
801
vehicle/graph/blocks/pneumatic.cpp
Normal 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
|
||||
149
vehicle/graph/blocks/security.cpp
Normal file
149
vehicle/graph/blocks/security.cpp
Normal 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
|
||||
424
vehicle/graph/blocks/vehicle.cpp
Normal file
424
vehicle/graph/blocks/vehicle.cpp
Normal 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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 ¶ms; // 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 ¶m_defs; // parameter definitions of the node, as above
|
||||
std::vector<param_value> const ¶ms; // 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);
|
||||
|
||||
|
||||
133
vehicle/graph/graph_body.cpp
Normal file
133
vehicle/graph/graph_body.cpp
Normal 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
144
vehicle/graph/graph_body.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¶m : 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
592
vehicle/graph/graph_lua.cpp
Normal 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
84
vehicle/graph/graph_lua.h
Normal 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
|
||||
534
vehicle/graph/graph_preview.cpp
Normal file
534
vehicle/graph/graph_preview.cpp
Normal 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
|
||||
22
vehicle/graph/graph_preview.h
Normal file
22
vehicle/graph/graph_preview.h
Normal 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
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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 ¶m : 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]);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
276
vehicle/graph/graph_train.cpp
Normal file
276
vehicle/graph/graph_train.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; });
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
465
vehicle/graph/graph_vehicle.cpp
Normal file
465
vehicle/graph/graph_vehicle.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user