mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-05 05:14:27 +01:00
Add vehicle graph core: .vhg loader, logic and cab blocks, -graph-run and -graph-catalog
This commit is contained in:
381
vehicle/graph/blocks/cab.cpp
Normal file
381
vehicle/graph/blocks/cab.cpp
Normal file
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
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/blocks/cab.h"
|
||||
#include "vehicle/graph/graph_catalog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<std::string> split_labels(std::string const &Text)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
if (Text.empty())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
std::string::size_type start{0};
|
||||
while (true)
|
||||
{
|
||||
auto const separator{Text.find(';', start)};
|
||||
result.emplace_back(Text.substr(start, separator == std::string::npos ? std::string::npos : separator - start));
|
||||
if (separator == std::string::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
start = separator + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// cab_switch
|
||||
|
||||
cab_switch::cab_switch(block_setup const &Setup)
|
||||
: block(Setup), m_behaviour(static_cast<behaviour_type>(choice(Setup, param::behaviour))),
|
||||
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))
|
||||
{
|
||||
// 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)};
|
||||
if (m_positioncount > 1 && m_max != m_min)
|
||||
{
|
||||
m_defaultposition = clamp_position(static_cast<int>(std::lround((defaultvalue - m_min) / (m_max - m_min) * (m_positioncount - 1))));
|
||||
}
|
||||
m_position = m_defaultposition;
|
||||
}
|
||||
|
||||
int cab_switch::clamp_position(int const Position) const
|
||||
{
|
||||
return std::clamp(Position, 0, m_positioncount - 1);
|
||||
}
|
||||
|
||||
double cab_switch::value_at(int const Position) const
|
||||
{
|
||||
if (m_positioncount < 2)
|
||||
{
|
||||
return m_min;
|
||||
}
|
||||
return m_min + (m_max - m_min) * clamp_position(Position) / (m_positioncount - 1);
|
||||
}
|
||||
|
||||
std::string cab_switch::label(int const Position) const
|
||||
{
|
||||
return (Position >= 0 && Position < static_cast<int>(m_labels.size()) ? m_labels[Position] : std::string{});
|
||||
}
|
||||
|
||||
void cab_switch::update(frame &Frame)
|
||||
{
|
||||
set(Frame, port::value, value_at(m_position));
|
||||
set(Frame, port::index, m_position);
|
||||
}
|
||||
|
||||
void cab_switch::increase()
|
||||
{
|
||||
m_position = clamp_position(m_position + 1);
|
||||
}
|
||||
|
||||
void cab_switch::decrease()
|
||||
{
|
||||
m_position = clamp_position(m_position - 1);
|
||||
}
|
||||
|
||||
void cab_switch::toggle()
|
||||
{
|
||||
switch (m_behaviour)
|
||||
{
|
||||
case behaviour_type::monostable:
|
||||
{
|
||||
// a momentary switch can't stay toggled; a toggle is treated as pressing it, the release brings it back
|
||||
press();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
auto const other{m_defaultposition == m_positioncount - 1 ? 0 : m_positioncount - 1};
|
||||
m_position = (m_position == m_defaultposition ? other : m_defaultposition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cab_switch::press()
|
||||
{
|
||||
switch (m_behaviour)
|
||||
{
|
||||
case behaviour_type::multi:
|
||||
{
|
||||
// levers are moved with increase and decrease; pressing them has no meaning
|
||||
break;
|
||||
}
|
||||
case behaviour_type::monostable:
|
||||
{
|
||||
m_position = (m_defaultposition == m_positioncount - 1 ? 0 : m_positioncount - 1);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
toggle();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cab_switch::release()
|
||||
{
|
||||
if (m_behaviour == behaviour_type::monostable || (m_behaviour == behaviour_type::multi && m_spring))
|
||||
{
|
||||
m_position = m_defaultposition;
|
||||
}
|
||||
}
|
||||
|
||||
void cab_switch::set_position(int const Position)
|
||||
{
|
||||
m_position = clamp_position(Position);
|
||||
}
|
||||
|
||||
void cab_switch::set_value(double const Value)
|
||||
{
|
||||
if (m_positioncount < 2 || m_max == m_min)
|
||||
{
|
||||
return;
|
||||
}
|
||||
set_position(static_cast<int>(std::lround((Value - m_min) / (m_max - m_min) * (m_positioncount - 1))));
|
||||
}
|
||||
|
||||
block_type cab_switch::describe()
|
||||
{
|
||||
block_type type;
|
||||
type.id = "cab.switch";
|
||||
type.category = "cab";
|
||||
type.name = "Switch";
|
||||
type.description = "Switch, button or lever of the cab. Its behaviour and animation are defined here, and its value can be connected to any block.";
|
||||
type.origin = block_origin::cab;
|
||||
type.ports = {signal_out("pos", value_type::real), signal_out("index", value_type::integer)};
|
||||
type.params = {
|
||||
choice_param("behaviour", {"bistable", "monostable", "multi"}, "bistable"),
|
||||
number_param("positions", 2.0),
|
||||
number_param("min", 0.0),
|
||||
number_param("max", 1.0),
|
||||
number_param("default", 0.0),
|
||||
boolean_param("spring", false),
|
||||
text_param("labels"),
|
||||
choice_param("animation", {"rotate", "move"}, "rotate"),
|
||||
number_param("step", 45.0, "deg/mm"),
|
||||
number_param("offset", 0.0, "deg/mm"),
|
||||
number_param("smoothing", 0.2, "s"),
|
||||
text_param("submodel"),
|
||||
text_param("key_inc"),
|
||||
text_param("key_dec"),
|
||||
text_param("command"),
|
||||
text_param("hw_name"),
|
||||
};
|
||||
type.create = make_block<cab_switch>;
|
||||
return type;
|
||||
}
|
||||
|
||||
// cab_lamp
|
||||
|
||||
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))
|
||||
{
|
||||
}
|
||||
|
||||
void cab_lamp::update(frame &Frame)
|
||||
{
|
||||
// without low voltage simulation, or without the supply connected, the lamp is assumed to be powered
|
||||
auto const lowvoltage{Frame.low_voltage && m_supplied};
|
||||
auto const voltage{lowvoltage ? forward_in(Frame, port::supply) : 0.0};
|
||||
auto const powered{false == lowvoltage || voltage >= m_minvoltage};
|
||||
auto lit{powered && get_bool(Frame, port::on)};
|
||||
if (lit && m_blink)
|
||||
{
|
||||
// blinking follows the time of the graph, so all lamps of the vehicle blink in step
|
||||
lit = std::fmod(Frame.time, m_blinkperiod) < 0.5 * m_blinkperiod;
|
||||
}
|
||||
m_lit = lit;
|
||||
if (lowvoltage && m_lit && voltage > 1.0)
|
||||
{
|
||||
add_back(Frame, port::supply, m_power / voltage);
|
||||
}
|
||||
}
|
||||
|
||||
block_type cab_lamp::describe()
|
||||
{
|
||||
block_type type;
|
||||
type.id = "fb.lamp";
|
||||
type.category = "indicators";
|
||||
type.name = "Lamp";
|
||||
type.description = "Indicator lamp. With low voltage simulation enabled and the supply connected, it needs enough voltage to light, and draws current.";
|
||||
type.origin = block_origin::cab;
|
||||
type.ports = {signal_in("on", value_type::boolean), physical_in("supply", domain::low_voltage, true)};
|
||||
type.params = {
|
||||
choice_param("color", {"green", "yellow", "red", "white", "blue"}, "green"),
|
||||
boolean_param("blink", false),
|
||||
number_param("blink_period", 1.0, "s"),
|
||||
number_param("min_voltage", 70.0, "V"),
|
||||
number_param("power", 2.0, "W"),
|
||||
text_param("submodel"),
|
||||
};
|
||||
type.create = make_block<cab_lamp>;
|
||||
return type;
|
||||
}
|
||||
|
||||
// cab_gauge
|
||||
|
||||
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_values[0] = m_values[1] = static_cast<float>(std::min(m_min, m_max));
|
||||
}
|
||||
|
||||
void cab_gauge::update(frame &Frame)
|
||||
{
|
||||
// the needle stops at the ends of the scale
|
||||
auto const low{std::min(m_min, m_max)};
|
||||
auto const high{std::max(m_min, m_max)};
|
||||
m_values[0] = static_cast<float>(std::clamp(get(Frame, port::value), low, high));
|
||||
if (m_twoneedles)
|
||||
{
|
||||
m_values[1] = static_cast<float>(std::clamp(get(Frame, port::value2), low, high));
|
||||
}
|
||||
}
|
||||
|
||||
block_type cab_gauge::describe(bool const Twoneedles)
|
||||
{
|
||||
block_type type;
|
||||
type.category = "indicators";
|
||||
type.origin = block_origin::cab;
|
||||
if (Twoneedles)
|
||||
{
|
||||
type.id = "fb.manometer";
|
||||
type.name = "Two needle gauge";
|
||||
type.description = "Gauge with two needles, e.g. pressure gauge of the brake pipe (black) and the main reservoir (red).";
|
||||
type.ports = {signal_in("black", value_type::real, 0.0, "bar"), signal_in("red", value_type::real, 0.0, "bar", true)};
|
||||
}
|
||||
else
|
||||
{
|
||||
type.id = "fb.gauge";
|
||||
type.name = "Gauge";
|
||||
type.description = "Needle gauge: voltmeter, ammeter, speedometer.";
|
||||
type.ports = {signal_in("value", value_type::real)};
|
||||
}
|
||||
type.params = {
|
||||
number_param("min", 0.0),
|
||||
number_param("max", Twoneedles ? 10.0 : 100.0),
|
||||
text_param("unit", Twoneedles ? "bar" : ""),
|
||||
number_param("angle_min", -135.0, "deg"),
|
||||
number_param("angle_max", 135.0, "deg"),
|
||||
number_param("smoothing", 0.3, "s"),
|
||||
text_param("submodel"),
|
||||
};
|
||||
if (Twoneedles)
|
||||
{
|
||||
type.params.push_back(text_param("submodel2"));
|
||||
}
|
||||
if (Twoneedles)
|
||||
{
|
||||
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<cab_gauge>(Setup, true); };
|
||||
}
|
||||
else
|
||||
{
|
||||
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<cab_gauge>(Setup, false); };
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
// vehicle_sound
|
||||
|
||||
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)}
|
||||
{
|
||||
}
|
||||
|
||||
void vehicle_sound::update(frame &Frame)
|
||||
{
|
||||
m_playing = get_bool(Frame, port::play);
|
||||
m_volume = static_cast<float>(std::max(0.0, m_basevolume * get(Frame, port::volume)));
|
||||
m_pitch = static_cast<float>(std::max(0.0, m_basepitch * get(Frame, port::pitch)));
|
||||
}
|
||||
|
||||
char const *vehicle_sound::state_name(std::size_t const Index) const
|
||||
{
|
||||
switch (Index)
|
||||
{
|
||||
case 0:
|
||||
return "playing";
|
||||
case 1:
|
||||
return "volume";
|
||||
default:
|
||||
return "pitch";
|
||||
}
|
||||
}
|
||||
|
||||
double vehicle_sound::state_value(std::size_t const Index) const
|
||||
{
|
||||
switch (Index)
|
||||
{
|
||||
case 0:
|
||||
return m_playing ? 1.0 : 0.0;
|
||||
case 1:
|
||||
return m_volume;
|
||||
default:
|
||||
return m_pitch;
|
||||
}
|
||||
}
|
||||
|
||||
block_type vehicle_sound::describe()
|
||||
{
|
||||
block_type type;
|
||||
type.id = "fb.sound";
|
||||
type.category = "indicators";
|
||||
type.name = "Sound";
|
||||
type.description = "Sound of the vehicle or of its cab. Volume and pitch are the parameters multiplied by the inputs; looped sounds play while the play input is on.";
|
||||
type.origin = block_origin::cab;
|
||||
type.ports = {signal_in("play", value_type::boolean), signal_in("volume", value_type::real, 1.0, "", true), signal_in("pitch", value_type::real, 1.0, "", true)};
|
||||
type.params = {
|
||||
text_param("file"),
|
||||
number_param("volume", 1.0),
|
||||
number_param("pitch", 1.0),
|
||||
boolean_param("loop", true),
|
||||
choice_param("placement", {"general", "internal", "engine", "external", "external_ambient"}, "internal"),
|
||||
number_param("range", -1.0, "m"),
|
||||
number_param("offset_x", 0.0, "m"),
|
||||
number_param("offset_y", 0.0, "m"),
|
||||
number_param("offset_z", 0.0, "m"),
|
||||
};
|
||||
type.create = make_block<vehicle_sound>;
|
||||
return type;
|
||||
}
|
||||
|
||||
void register_cab_blocks(catalog &Catalog)
|
||||
{
|
||||
Catalog.add(cab_switch::describe());
|
||||
Catalog.add(cab_lamp::describe());
|
||||
Catalog.add(cab_gauge::describe(false));
|
||||
Catalog.add(cab_gauge::describe(true));
|
||||
Catalog.add(vehicle_sound::describe());
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
441
vehicle/graph/blocks/cab.h
Normal file
441
vehicle/graph/blocks/cab.h
Normal file
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
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_block.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// cab controls and indicators. the blocks hold the logic and the values; the cab (TTrain) creates the animated
|
||||
// submodels from their parameters and binds them to the values these blocks expose
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
// switch, button or lever of the cab. its behaviour and animation are defined in the graph, and its value
|
||||
// is available to the other blocks
|
||||
class cab_switch final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
value, // "pos": value of the current position, between min and max
|
||||
index // number of the current position, from 0
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
behaviour,
|
||||
positions,
|
||||
min,
|
||||
max,
|
||||
default_value,
|
||||
spring,
|
||||
labels,
|
||||
animation,
|
||||
step,
|
||||
offset,
|
||||
smoothing,
|
||||
submodel,
|
||||
key_inc,
|
||||
key_dec,
|
||||
command,
|
||||
hw_name
|
||||
};
|
||||
};
|
||||
enum class behaviour_type
|
||||
{
|
||||
bistable, // stays where it was put
|
||||
monostable, // returns to the default position when released, e.g. a push button
|
||||
multi // several positions; with the spring enabled it returns to the default position when released
|
||||
};
|
||||
enum class animation_type
|
||||
{
|
||||
rotate,
|
||||
move
|
||||
};
|
||||
|
||||
explicit cab_switch(block_setup const &Setup);
|
||||
|
||||
void update(frame &Frame) override;
|
||||
std::size_t state_count() const override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
char const *state_name(std::size_t) const override
|
||||
{
|
||||
return "position";
|
||||
}
|
||||
double state_value(std::size_t) const override
|
||||
{
|
||||
return m_position;
|
||||
}
|
||||
|
||||
// operations of the player, AI or hardware. they take effect in the next update
|
||||
void increase();
|
||||
void decrease();
|
||||
void toggle();
|
||||
void press();
|
||||
void release();
|
||||
void set_position(int Position);
|
||||
// moves to the position with the value closest to the specified one, e.g. for hardware levers
|
||||
void set_value(double Value);
|
||||
|
||||
int position() const
|
||||
{
|
||||
return m_position;
|
||||
}
|
||||
int position_count() const
|
||||
{
|
||||
return m_positioncount;
|
||||
}
|
||||
int default_position() const
|
||||
{
|
||||
return m_defaultposition;
|
||||
}
|
||||
double value_at(int Position) const;
|
||||
behaviour_type behaviour() const
|
||||
{
|
||||
return m_behaviour;
|
||||
}
|
||||
bool spring() const
|
||||
{
|
||||
return m_spring;
|
||||
}
|
||||
// description of the specified position, from the labels parameter; empty if not specified
|
||||
std::string label(int Position) const;
|
||||
// animation of the submodel: offset + step * position, in degrees or millimetres
|
||||
animation_type animation() const
|
||||
{
|
||||
return m_animation;
|
||||
}
|
||||
double step() const
|
||||
{
|
||||
return m_step;
|
||||
}
|
||||
double offset() const
|
||||
{
|
||||
return m_offset;
|
||||
}
|
||||
double smoothing() const
|
||||
{
|
||||
return m_smoothing;
|
||||
}
|
||||
std::string const &submodel() const
|
||||
{
|
||||
return m_submodel;
|
||||
}
|
||||
// bindings: keys, existing simulator command, name for the hardware desk
|
||||
std::string const &key_increase() const
|
||||
{
|
||||
return m_keyincrease;
|
||||
}
|
||||
std::string const &key_decrease() const
|
||||
{
|
||||
return m_keydecrease;
|
||||
}
|
||||
std::string const &command() const
|
||||
{
|
||||
return m_command;
|
||||
}
|
||||
std::string const &hardware_name() const
|
||||
{
|
||||
return m_hardwarename;
|
||||
}
|
||||
|
||||
static block_type describe();
|
||||
|
||||
private:
|
||||
int clamp_position(int Position) const;
|
||||
|
||||
behaviour_type const m_behaviour;
|
||||
int const m_positioncount;
|
||||
double const m_min;
|
||||
double const m_max;
|
||||
int m_defaultposition{0};
|
||||
bool const m_spring;
|
||||
std::vector<std::string> m_labels;
|
||||
animation_type const m_animation;
|
||||
double const m_step;
|
||||
double const m_offset;
|
||||
double const m_smoothing;
|
||||
std::string const m_submodel;
|
||||
std::string const m_keyincrease;
|
||||
std::string const m_keydecrease;
|
||||
std::string const m_command;
|
||||
std::string const m_hardwarename;
|
||||
int m_position{0};
|
||||
};
|
||||
|
||||
// indicator lamp. when the graph simulates low voltage circuits and the supply input is connected, the lamp lights
|
||||
// only with enough voltage, and draws current from the supply
|
||||
class cab_lamp final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
on,
|
||||
supply
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
color,
|
||||
blink,
|
||||
blink_period,
|
||||
min_voltage,
|
||||
power,
|
||||
submodel
|
||||
};
|
||||
};
|
||||
|
||||
explicit cab_lamp(block_setup const &Setup);
|
||||
|
||||
void update(frame &Frame) override;
|
||||
std::size_t state_count() const override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
char const *state_name(std::size_t) const override
|
||||
{
|
||||
return "lit";
|
||||
}
|
||||
double state_value(std::size_t) const override
|
||||
{
|
||||
return m_lit ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
// stable address of the state, for TButton::AssignBool
|
||||
bool const *lit_state() const
|
||||
{
|
||||
return &m_lit;
|
||||
}
|
||||
std::string const &submodel() const
|
||||
{
|
||||
return m_submodel;
|
||||
}
|
||||
|
||||
static block_type describe();
|
||||
|
||||
private:
|
||||
bool const m_supplied; // whether the supply input is connected
|
||||
bool const m_blink;
|
||||
double const m_blinkperiod;
|
||||
double const m_minvoltage;
|
||||
double const m_power;
|
||||
std::string const m_submodel;
|
||||
bool m_lit{false};
|
||||
};
|
||||
|
||||
// needle gauge with one or two needles: voltmeter, ammeter, speedometer, pressure gauge
|
||||
class cab_gauge final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
value,
|
||||
value2
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
min,
|
||||
max,
|
||||
unit,
|
||||
angle_min,
|
||||
angle_max,
|
||||
smoothing,
|
||||
submodel,
|
||||
submodel2
|
||||
};
|
||||
};
|
||||
|
||||
cab_gauge(block_setup const &Setup, bool Twoneedles);
|
||||
|
||||
void update(frame &Frame) override;
|
||||
std::size_t state_count() const override
|
||||
{
|
||||
return m_twoneedles ? 2 : 1;
|
||||
}
|
||||
char const *state_name(std::size_t Index) const override
|
||||
{
|
||||
return Index == 0 ? "value" : "value2";
|
||||
}
|
||||
double state_value(std::size_t Index) const override
|
||||
{
|
||||
return m_values[Index];
|
||||
}
|
||||
|
||||
// stable addresses of the values shown by the needles, clamped to the scale, for TGauge::AssignFloat
|
||||
float *value_state(std::size_t Needle)
|
||||
{
|
||||
return &m_values[Needle];
|
||||
}
|
||||
bool two_needles() const
|
||||
{
|
||||
return m_twoneedles;
|
||||
}
|
||||
double min() const
|
||||
{
|
||||
return m_min;
|
||||
}
|
||||
double max() const
|
||||
{
|
||||
return m_max;
|
||||
}
|
||||
double angle_min() const
|
||||
{
|
||||
return m_anglemin;
|
||||
}
|
||||
double angle_max() const
|
||||
{
|
||||
return m_anglemax;
|
||||
}
|
||||
double smoothing() const
|
||||
{
|
||||
return m_smoothing;
|
||||
}
|
||||
std::string const &submodel(std::size_t Needle) const
|
||||
{
|
||||
return Needle == 0 ? m_submodel : m_submodel2;
|
||||
}
|
||||
|
||||
static block_type describe(bool Twoneedles);
|
||||
|
||||
private:
|
||||
bool const m_twoneedles;
|
||||
double const m_min;
|
||||
double const m_max;
|
||||
double const m_anglemin;
|
||||
double const m_anglemax;
|
||||
double const m_smoothing;
|
||||
std::string const m_submodel;
|
||||
std::string const m_submodel2;
|
||||
float m_values[2]{0.f, 0.f};
|
||||
};
|
||||
|
||||
// sound of the vehicle or of its cab. sounds of a graph vehicle are defined in the graph instead of the .mmd file
|
||||
class vehicle_sound final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
play,
|
||||
volume,
|
||||
pitch
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
file,
|
||||
volume,
|
||||
pitch,
|
||||
loop,
|
||||
placement,
|
||||
range,
|
||||
offset_x,
|
||||
offset_y,
|
||||
offset_z
|
||||
};
|
||||
};
|
||||
// the same placements as sound_placement of the audio subsystem
|
||||
enum class placement_type
|
||||
{
|
||||
general,
|
||||
internal,
|
||||
engine,
|
||||
external,
|
||||
external_ambient
|
||||
};
|
||||
|
||||
explicit vehicle_sound(block_setup const &Setup);
|
||||
|
||||
void update(frame &Frame) override;
|
||||
std::size_t state_count() const override
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
char const *state_name(std::size_t Index) const override;
|
||||
double state_value(std::size_t Index) const override;
|
||||
|
||||
bool playing() const
|
||||
{
|
||||
return m_playing;
|
||||
}
|
||||
// volume and pitch: parameter multiplied by the input
|
||||
float volume() const
|
||||
{
|
||||
return m_volume;
|
||||
}
|
||||
float pitch() const
|
||||
{
|
||||
return m_pitch;
|
||||
}
|
||||
std::string const &file() const
|
||||
{
|
||||
return m_file;
|
||||
}
|
||||
bool loop() const
|
||||
{
|
||||
return m_loop;
|
||||
}
|
||||
placement_type placement() const
|
||||
{
|
||||
return m_placement;
|
||||
}
|
||||
double range() const
|
||||
{
|
||||
return m_range;
|
||||
}
|
||||
double offset_x() const
|
||||
{
|
||||
return m_offset[0];
|
||||
}
|
||||
double offset_y() const
|
||||
{
|
||||
return m_offset[1];
|
||||
}
|
||||
double offset_z() const
|
||||
{
|
||||
return m_offset[2];
|
||||
}
|
||||
|
||||
static block_type describe();
|
||||
|
||||
private:
|
||||
std::string const m_file;
|
||||
double const m_basevolume;
|
||||
double const m_basepitch;
|
||||
bool const m_loop;
|
||||
placement_type const m_placement;
|
||||
double const m_range;
|
||||
double const m_offset[3];
|
||||
bool m_playing{false};
|
||||
float m_volume{0.f};
|
||||
float m_pitch{1.f};
|
||||
};
|
||||
|
||||
} // namespace vgraph
|
||||
625
vehicle/graph/blocks/logic.cpp
Normal file
625
vehicle/graph/blocks/logic.cpp
Normal file
@@ -0,0 +1,625 @@
|
||||
/*
|
||||
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 <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// generic logic and arithmetic blocks. boolean signals are stored as 0 and 1, and read as true above 0.5
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// tolerance of the equality comparisons; switch positions and constants are compared, not results of long computations
|
||||
double constexpr equality_tolerance{1e-6};
|
||||
|
||||
block_type make_type(std::string Id, std::string Name, std::string Description)
|
||||
{
|
||||
block_type type;
|
||||
type.id = std::move(Id);
|
||||
type.category = "logic";
|
||||
type.name = std::move(Name);
|
||||
type.description = std::move(Description);
|
||||
type.origin = block_origin::primitive;
|
||||
return type;
|
||||
}
|
||||
|
||||
class logic_compare final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
a,
|
||||
q
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
op,
|
||||
value
|
||||
};
|
||||
};
|
||||
enum class operation
|
||||
{
|
||||
ne,
|
||||
eq,
|
||||
lt,
|
||||
le,
|
||||
gt,
|
||||
ge
|
||||
};
|
||||
|
||||
explicit logic_compare(block_setup const &Setup) : block(Setup), m_operation(static_cast<operation>(choice(Setup, param::op))), m_value(number(Setup, param::value)) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
auto const a{get(Frame, port::a)};
|
||||
auto result{false};
|
||||
switch (m_operation)
|
||||
{
|
||||
case operation::ne:
|
||||
result = std::abs(a - m_value) > equality_tolerance;
|
||||
break;
|
||||
case operation::eq:
|
||||
result = std::abs(a - m_value) <= equality_tolerance;
|
||||
break;
|
||||
case operation::lt:
|
||||
result = a < m_value;
|
||||
break;
|
||||
case operation::le:
|
||||
result = a <= m_value + equality_tolerance;
|
||||
break;
|
||||
case operation::gt:
|
||||
result = a > m_value;
|
||||
break;
|
||||
case operation::ge:
|
||||
result = a >= m_value - equality_tolerance;
|
||||
break;
|
||||
}
|
||||
set_bool(Frame, port::q, result);
|
||||
}
|
||||
|
||||
static block_type describe()
|
||||
{
|
||||
auto type{make_type("logic.compare", "Compare", "Compares the input with a constant value.")};
|
||||
type.ports = {signal_in("a", value_type::real), signal_out("q", value_type::boolean)};
|
||||
type.params = {choice_param("op", {"ne", "eq", "lt", "le", "gt", "ge"}, "ne"), number_param("value", 0.0)};
|
||||
type.create = make_block<logic_compare>;
|
||||
return type;
|
||||
}
|
||||
|
||||
private:
|
||||
operation const m_operation;
|
||||
double const m_value;
|
||||
};
|
||||
|
||||
enum class gate
|
||||
{
|
||||
and_,
|
||||
or_,
|
||||
xor_,
|
||||
nand_,
|
||||
nor_
|
||||
};
|
||||
|
||||
class logic_gate final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
a,
|
||||
b,
|
||||
q
|
||||
};
|
||||
};
|
||||
|
||||
logic_gate(block_setup const &Setup, gate const Gate) : block(Setup), m_gate(Gate) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
auto const a{get_bool(Frame, port::a)};
|
||||
auto const b{get_bool(Frame, port::b)};
|
||||
auto result{false};
|
||||
switch (m_gate)
|
||||
{
|
||||
case gate::and_:
|
||||
result = a && b;
|
||||
break;
|
||||
case gate::or_:
|
||||
result = a || b;
|
||||
break;
|
||||
case gate::xor_:
|
||||
result = a != b;
|
||||
break;
|
||||
case gate::nand_:
|
||||
result = false == (a && b);
|
||||
break;
|
||||
case gate::nor_:
|
||||
result = false == (a || b);
|
||||
break;
|
||||
}
|
||||
set_bool(Frame, port::q, result);
|
||||
}
|
||||
|
||||
template <gate Gate_> static std::unique_ptr<block> create(block_setup const &Setup)
|
||||
{
|
||||
return std::make_unique<logic_gate>(Setup, Gate_);
|
||||
}
|
||||
|
||||
template <gate Gate_> static block_type describe(std::string Id, std::string Name, double const Default)
|
||||
{
|
||||
// unconnected inputs default to the neutral value of the gate, so a gate can be used with one input
|
||||
auto type{make_type(std::move(Id), std::move(Name), "Logic gate.")};
|
||||
type.ports = {signal_in("a", value_type::boolean, Default), signal_in("b", value_type::boolean, Default, "", true), signal_out("q", value_type::boolean)};
|
||||
type.create = create<Gate_>;
|
||||
return type;
|
||||
}
|
||||
|
||||
private:
|
||||
gate const m_gate;
|
||||
};
|
||||
|
||||
class logic_not final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
a,
|
||||
q
|
||||
};
|
||||
};
|
||||
|
||||
explicit logic_not(block_setup const &Setup) : block(Setup) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
set_bool(Frame, port::q, false == get_bool(Frame, port::a));
|
||||
}
|
||||
|
||||
static block_type describe()
|
||||
{
|
||||
auto type{make_type("logic.not", "NOT", "Negation.")};
|
||||
type.ports = {signal_in("a", value_type::boolean), signal_out("q", value_type::boolean)};
|
||||
type.create = make_block<logic_not>;
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
// set-reset latch. it's deferred, so it can close loops, e.g. self-holding contactor circuits
|
||||
class logic_sr final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
s,
|
||||
r,
|
||||
q
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
reset_priority,
|
||||
initial
|
||||
};
|
||||
};
|
||||
|
||||
explicit logic_sr(block_setup const &Setup) : block(Setup), m_resetpriority(boolean(Setup, param::reset_priority)), m_state(boolean(Setup, param::initial)) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
auto const s{get_bool(Frame, port::s)};
|
||||
auto const r{get_bool(Frame, port::r)};
|
||||
if (s && r)
|
||||
{
|
||||
m_state = (false == m_resetpriority);
|
||||
}
|
||||
else if (s)
|
||||
{
|
||||
m_state = true;
|
||||
}
|
||||
else if (r)
|
||||
{
|
||||
m_state = false;
|
||||
}
|
||||
}
|
||||
|
||||
void commit(frame &Frame) override
|
||||
{
|
||||
set_bool(Frame, port::q, m_state);
|
||||
}
|
||||
|
||||
std::size_t state_count() const override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
char const *state_name(std::size_t) const override
|
||||
{
|
||||
return "state";
|
||||
}
|
||||
double state_value(std::size_t) const override
|
||||
{
|
||||
return m_state ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
static block_type describe()
|
||||
{
|
||||
auto type{make_type("logic.sr", "SR latch", "Set sets the output, reset clears it. The output changes in the next step, so the block can close loops.")};
|
||||
type.deferred = true;
|
||||
type.ports = {signal_in("s", value_type::boolean), signal_in("r", value_type::boolean, 0.0, "", true), signal_out("q", value_type::boolean)};
|
||||
type.params = {boolean_param("reset_priority", true), boolean_param("initial", false)};
|
||||
type.create = make_block<logic_sr>;
|
||||
return type;
|
||||
}
|
||||
|
||||
private:
|
||||
bool const m_resetpriority;
|
||||
bool m_state;
|
||||
};
|
||||
|
||||
// on-delay (TON) and off-delay (TOF) timers
|
||||
class logic_timer final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
a,
|
||||
q
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
time
|
||||
};
|
||||
};
|
||||
|
||||
logic_timer(block_setup const &Setup, bool const Ondelay) : block(Setup), m_ondelay(Ondelay), m_delay(std::max(0.0, number(Setup, param::time))) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
auto const a{get_bool(Frame, port::a)};
|
||||
if (a == m_ondelay)
|
||||
{
|
||||
m_elapsed = std::min(m_elapsed + Frame.dt, m_delay);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_elapsed = 0.0;
|
||||
}
|
||||
auto const expired{m_elapsed >= m_delay};
|
||||
m_output = (m_ondelay ? (a && expired) : (a || false == expired));
|
||||
}
|
||||
|
||||
void commit(frame &Frame) override
|
||||
{
|
||||
set_bool(Frame, port::q, m_output);
|
||||
}
|
||||
|
||||
std::size_t state_count() const override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
char const *state_name(std::size_t) const override
|
||||
{
|
||||
return "elapsed";
|
||||
}
|
||||
double state_value(std::size_t) const override
|
||||
{
|
||||
return m_elapsed;
|
||||
}
|
||||
|
||||
template <bool Ondelay_> static std::unique_ptr<block> create(block_setup const &Setup)
|
||||
{
|
||||
return std::make_unique<logic_timer>(Setup, Ondelay_);
|
||||
}
|
||||
|
||||
template <bool Ondelay_> static block_type describe()
|
||||
{
|
||||
auto type{Ondelay_ ? make_type("logic.ton", "On delay", "The output follows the input switching on after the specified time, and switching off immediately.")
|
||||
: make_type("logic.tof", "Off delay", "The output follows the input switching on immediately, and switching off after the specified time.")};
|
||||
type.deferred = true;
|
||||
type.ports = {signal_in("a", value_type::boolean), signal_out("q", value_type::boolean)};
|
||||
type.params = {number_param("time", 1.0, "s")};
|
||||
type.create = create<Ondelay_>;
|
||||
return type;
|
||||
}
|
||||
|
||||
private:
|
||||
bool const m_ondelay;
|
||||
double const m_delay;
|
||||
// for the on-delay: time the input is on; for the off-delay: time the input is off
|
||||
// (both start expired for the off-delay, so the output is off until the input is switched on)
|
||||
double m_elapsed{m_ondelay ? 0.0 : m_delay};
|
||||
bool m_output{false};
|
||||
};
|
||||
|
||||
// delays the signal by one step; the explicit way to close a loop
|
||||
class logic_delay final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
a,
|
||||
q
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
initial
|
||||
};
|
||||
};
|
||||
|
||||
explicit logic_delay(block_setup const &Setup) : block(Setup), m_value(number(Setup, param::initial)) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
m_value = get(Frame, port::a);
|
||||
}
|
||||
|
||||
void commit(frame &Frame) override
|
||||
{
|
||||
set(Frame, port::q, m_value);
|
||||
}
|
||||
|
||||
static block_type describe()
|
||||
{
|
||||
auto type{make_type("logic.delay", "Unit delay", "Passes the input to the output one step later. Use it to close loops.")};
|
||||
type.deferred = true;
|
||||
type.ports = {signal_in("a", value_type::real), signal_out("q", value_type::real)};
|
||||
type.params = {number_param("initial", 0.0)};
|
||||
type.create = make_block<logic_delay>;
|
||||
return type;
|
||||
}
|
||||
|
||||
private:
|
||||
double m_value;
|
||||
};
|
||||
|
||||
class logic_hysteresis final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
a,
|
||||
q
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
lo,
|
||||
hi,
|
||||
initial
|
||||
};
|
||||
};
|
||||
|
||||
explicit logic_hysteresis(block_setup const &Setup)
|
||||
: block(Setup), m_low(number(Setup, param::lo)), m_high(number(Setup, param::hi)), m_state(boolean(Setup, param::initial))
|
||||
{
|
||||
}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
auto const a{get(Frame, port::a)};
|
||||
if (a >= m_high)
|
||||
{
|
||||
m_state = true;
|
||||
}
|
||||
else if (a <= m_low)
|
||||
{
|
||||
m_state = false;
|
||||
}
|
||||
set_bool(Frame, port::q, m_state);
|
||||
}
|
||||
|
||||
static block_type describe()
|
||||
{
|
||||
auto type{make_type("logic.hyst", "Hysteresis", "Switches on when the input reaches the upper threshold, and off when it drops to the lower one.")};
|
||||
type.ports = {signal_in("a", value_type::real), signal_out("q", value_type::boolean)};
|
||||
type.params = {number_param("lo", 0.0), number_param("hi", 1.0), boolean_param("initial", false)};
|
||||
type.create = make_block<logic_hysteresis>;
|
||||
return type;
|
||||
}
|
||||
|
||||
private:
|
||||
double const m_low;
|
||||
double const m_high;
|
||||
bool m_state;
|
||||
};
|
||||
|
||||
class logic_const final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
q
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
value
|
||||
};
|
||||
};
|
||||
|
||||
explicit logic_const(block_setup const &Setup) : block(Setup), m_value(number(Setup, param::value)) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
set(Frame, port::q, m_value);
|
||||
}
|
||||
|
||||
static block_type describe()
|
||||
{
|
||||
auto type{make_type("logic.const", "Constant", "Constant value.")};
|
||||
type.ports = {signal_out("q", value_type::real)};
|
||||
type.params = {number_param("value", 1.0)};
|
||||
type.create = make_block<logic_const>;
|
||||
return type;
|
||||
}
|
||||
|
||||
private:
|
||||
double const m_value;
|
||||
};
|
||||
|
||||
class logic_math final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
a,
|
||||
b,
|
||||
q
|
||||
};
|
||||
};
|
||||
struct param
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
op
|
||||
};
|
||||
};
|
||||
enum class operation
|
||||
{
|
||||
add,
|
||||
sub,
|
||||
mul,
|
||||
div,
|
||||
min,
|
||||
max
|
||||
};
|
||||
|
||||
explicit logic_math(block_setup const &Setup) : block(Setup), m_operation(static_cast<operation>(choice(Setup, param::op))) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
auto const a{get(Frame, port::a)};
|
||||
auto const b{get(Frame, port::b)};
|
||||
auto result{0.0};
|
||||
switch (m_operation)
|
||||
{
|
||||
case operation::add:
|
||||
result = a + b;
|
||||
break;
|
||||
case operation::sub:
|
||||
result = a - b;
|
||||
break;
|
||||
case operation::mul:
|
||||
result = a * b;
|
||||
break;
|
||||
case operation::div:
|
||||
// division by zero gives zero rather than infinity, which would spread through the whole graph
|
||||
result = (b != 0.0 ? a / b : 0.0);
|
||||
break;
|
||||
case operation::min:
|
||||
result = std::min(a, b);
|
||||
break;
|
||||
case operation::max:
|
||||
result = std::max(a, b);
|
||||
break;
|
||||
}
|
||||
set(Frame, port::q, result);
|
||||
}
|
||||
|
||||
static block_type describe()
|
||||
{
|
||||
auto type{make_type("logic.math", "Arithmetic", "Arithmetic operation on two inputs. Division by zero gives zero.")};
|
||||
type.ports = {signal_in("a", value_type::real), signal_in("b", value_type::real, 1.0), signal_out("q", value_type::real)};
|
||||
type.params = {choice_param("op", {"add", "sub", "mul", "div", "min", "max"}, "mul")};
|
||||
type.create = make_block<logic_math>;
|
||||
return type;
|
||||
}
|
||||
|
||||
private:
|
||||
operation const m_operation;
|
||||
};
|
||||
|
||||
class logic_select final : public block
|
||||
{
|
||||
public:
|
||||
struct port
|
||||
{
|
||||
enum : std::size_t
|
||||
{
|
||||
sel,
|
||||
a,
|
||||
b,
|
||||
q
|
||||
};
|
||||
};
|
||||
|
||||
explicit logic_select(block_setup const &Setup) : block(Setup) {}
|
||||
|
||||
void update(frame &Frame) override
|
||||
{
|
||||
set(Frame, port::q, get_bool(Frame, port::sel) ? get(Frame, port::b) : get(Frame, port::a));
|
||||
}
|
||||
|
||||
static block_type describe()
|
||||
{
|
||||
auto type{make_type("logic.select", "Select", "Passes input a when sel is off, and input b when sel is on.")};
|
||||
type.ports = {signal_in("sel", value_type::boolean), signal_in("a", value_type::real), signal_in("b", value_type::real), signal_out("q", value_type::real)};
|
||||
type.create = make_block<logic_select>;
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void register_logic_blocks(catalog &Catalog)
|
||||
{
|
||||
Catalog.add(logic_compare::describe());
|
||||
Catalog.add(logic_gate::describe<gate::and_>("logic.and", "AND", 1.0));
|
||||
Catalog.add(logic_gate::describe<gate::or_>("logic.or", "OR", 0.0));
|
||||
Catalog.add(logic_gate::describe<gate::xor_>("logic.xor", "XOR", 0.0));
|
||||
Catalog.add(logic_gate::describe<gate::nand_>("logic.nand", "NAND", 1.0));
|
||||
Catalog.add(logic_gate::describe<gate::nor_>("logic.nor", "NOR", 0.0));
|
||||
Catalog.add(logic_not::describe());
|
||||
Catalog.add(logic_sr::describe());
|
||||
Catalog.add(logic_timer::describe<true>());
|
||||
Catalog.add(logic_timer::describe<false>());
|
||||
Catalog.add(logic_delay::describe());
|
||||
Catalog.add(logic_hysteresis::describe());
|
||||
Catalog.add(logic_const::describe());
|
||||
Catalog.add(logic_math::describe());
|
||||
Catalog.add(logic_select::describe());
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
49
vehicle/graph/graph_block.cpp
Normal file
49
vehicle/graph/graph_block.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
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_block.h"
|
||||
#include "vehicle/graph/graph_catalog.h"
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
block::block(block_setup const &Setup) : m_type(Setup.type), m_slots(Setup.slots) {}
|
||||
|
||||
double block::number(block_setup const &Setup, std::size_t const Param)
|
||||
{
|
||||
return Setup.params[Param].number;
|
||||
}
|
||||
|
||||
bool block::boolean(block_setup const &Setup, std::size_t const Param)
|
||||
{
|
||||
return Setup.params[Param].boolean;
|
||||
}
|
||||
|
||||
std::string const &block::text(block_setup const &Setup, std::size_t const Param)
|
||||
{
|
||||
return Setup.params[Param].text;
|
||||
}
|
||||
|
||||
int block::choice(block_setup const &Setup, std::size_t const Param)
|
||||
{
|
||||
auto const &choices{Setup.type.params[Param].choices};
|
||||
auto const &value{Setup.params[Param].text};
|
||||
for (std::size_t idx = 0; idx < choices.size(); ++idx)
|
||||
{
|
||||
if (choices[idx] == value)
|
||||
{
|
||||
return static_cast<int>(idx);
|
||||
}
|
||||
}
|
||||
// the loader rejects values outside of the list, so this only happens with a broken catalog
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
150
vehicle/graph/graph_block.h
Normal file
150
vehicle/graph/graph_block.h
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
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 <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
struct block_type;
|
||||
|
||||
// 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
|
||||
// the back channel never constrains the order of evaluation, and the result doesn't depend on the order of the inputs
|
||||
struct link_state
|
||||
{
|
||||
double forward{0.0};
|
||||
double back{0.0};
|
||||
double back_next{0.0};
|
||||
};
|
||||
|
||||
// data shared by all blocks of one graph instance during a single step
|
||||
struct frame
|
||||
{
|
||||
double dt{0.0};
|
||||
double time{0.0}; // time elapsed since the start of the instance, after this step
|
||||
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};
|
||||
};
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
// base of all blocks. a block reads and writes its ports through slot indices resolved when the graph was compiled
|
||||
class block
|
||||
{
|
||||
public:
|
||||
explicit block(block_setup const &Setup);
|
||||
virtual ~block() = default;
|
||||
|
||||
// called once per step in the order resolved from the connections
|
||||
virtual void update(frame &Frame) = 0;
|
||||
// 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 &) {}
|
||||
|
||||
// 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
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
virtual char const *state_name(std::size_t) const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
virtual double state_value(std::size_t) const
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
block_type const &type() const
|
||||
{
|
||||
return m_type;
|
||||
}
|
||||
|
||||
protected:
|
||||
// signal ports
|
||||
double get(frame const &Frame, std::size_t Port) const
|
||||
{
|
||||
return Frame.signals[m_slots[Port]];
|
||||
}
|
||||
bool get_bool(frame const &Frame, std::size_t Port) const
|
||||
{
|
||||
return Frame.signals[m_slots[Port]] > 0.5;
|
||||
}
|
||||
void set(frame &Frame, std::size_t Port, double const Value) const
|
||||
{
|
||||
auto const slot{m_slots[Port]};
|
||||
if (Frame.forced[slot] == 0)
|
||||
{
|
||||
Frame.signals[slot] = Value;
|
||||
}
|
||||
}
|
||||
void set_bool(frame &Frame, std::size_t Port, bool const Value) const
|
||||
{
|
||||
set(Frame, Port, Value ? 1.0 : 0.0);
|
||||
}
|
||||
// physical inputs
|
||||
double forward_in(frame const &Frame, std::size_t Port) const
|
||||
{
|
||||
return Frame.links[m_slots[Port]].forward;
|
||||
}
|
||||
void add_back(frame &Frame, std::size_t Port, double const Value) const
|
||||
{
|
||||
Frame.links[m_slots[Port]].back_next += Value;
|
||||
}
|
||||
// physical outputs
|
||||
void set_forward(frame &Frame, std::size_t Port, double const Value) const
|
||||
{
|
||||
Frame.links[m_slots[Port]].forward = Value;
|
||||
}
|
||||
double back_out(frame const &Frame, std::size_t Port) const
|
||||
{
|
||||
return Frame.links[m_slots[Port]].back;
|
||||
}
|
||||
|
||||
// whether the port has a connection; known when the block is created
|
||||
static bool is_connected(block_setup const &Setup, std::size_t Port)
|
||||
{
|
||||
return Setup.connected[Port] != 0;
|
||||
}
|
||||
// parameters, read when the block is created
|
||||
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);
|
||||
// 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);
|
||||
|
||||
private:
|
||||
block_type const &m_type;
|
||||
std::vector<int> m_slots;
|
||||
};
|
||||
|
||||
using block_factory = std::unique_ptr<block> (*)(block_setup const &);
|
||||
|
||||
template <class Block_> std::unique_ptr<block> make_block(block_setup const &Setup)
|
||||
{
|
||||
return std::make_unique<Block_>(Setup);
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
273
vehicle/graph/graph_catalog.cpp
Normal file
273
vehicle/graph/graph_catalog.cpp
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
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 <nlohmann/json.hpp>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
std::string_view to_string(block_origin const Origin)
|
||||
{
|
||||
switch (Origin)
|
||||
{
|
||||
case block_origin::cab:
|
||||
return "cab";
|
||||
case block_origin::simulator:
|
||||
return "simulator";
|
||||
case block_origin::extracted:
|
||||
return "extracted";
|
||||
case block_origin::native:
|
||||
return "native";
|
||||
case block_origin::interface_profile:
|
||||
return "interface";
|
||||
case block_origin::script:
|
||||
return "script";
|
||||
default:
|
||||
return "primitive";
|
||||
}
|
||||
}
|
||||
|
||||
int block_type::port_index(std::string_view const Key) const
|
||||
{
|
||||
for (std::size_t idx = 0; idx < ports.size(); ++idx)
|
||||
{
|
||||
if (ports[idx].key == Key)
|
||||
{
|
||||
return static_cast<int>(idx);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int block_type::param_index(std::string_view const Key) const
|
||||
{
|
||||
for (std::size_t idx = 0; idx < params.size(); ++idx)
|
||||
{
|
||||
if (params[idx].key == Key)
|
||||
{
|
||||
return static_cast<int>(idx);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
catalog const &catalog::get()
|
||||
{
|
||||
static catalog const instance{[]() {
|
||||
catalog result;
|
||||
register_logic_blocks(result);
|
||||
register_cab_blocks(result);
|
||||
return result;
|
||||
}()};
|
||||
return instance;
|
||||
}
|
||||
|
||||
block_type const *catalog::find(std::string_view const Id) const
|
||||
{
|
||||
for (auto const &type : m_types)
|
||||
{
|
||||
if (type.id == Id)
|
||||
{
|
||||
return &type;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void catalog::add(block_type Type)
|
||||
{
|
||||
m_types.emplace_back(std::move(Type));
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
nlohmann::json to_json(param_value const &Value)
|
||||
{
|
||||
switch (Value.type)
|
||||
{
|
||||
case param_type::boolean:
|
||||
return Value.boolean;
|
||||
case param_type::text:
|
||||
case param_type::choice:
|
||||
return Value.text;
|
||||
default:
|
||||
return Value.number;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string catalog::export_json() const
|
||||
{
|
||||
nlohmann::ordered_json root;
|
||||
root["format"] = "maszyna-vehicle-graph-catalog";
|
||||
root["version"] = 1;
|
||||
root["graph_format"] = std::string{file_format};
|
||||
root["graph_version"] = file_version;
|
||||
|
||||
auto &domains = (root["domains"] = nlohmann::ordered_json::array());
|
||||
for (std::size_t idx = 0; idx < static_cast<std::size_t>(domain::count_); ++idx)
|
||||
{
|
||||
auto const &domaininfo{info(static_cast<domain>(idx))};
|
||||
nlohmann::ordered_json item;
|
||||
item["id"] = std::string{domaininfo.id};
|
||||
item["physical"] = domaininfo.physical;
|
||||
item["forward"] = std::string{domaininfo.forward};
|
||||
item["forward_unit"] = std::string{domaininfo.forward_unit};
|
||||
if (domaininfo.physical)
|
||||
{
|
||||
item["back"] = std::string{domaininfo.back};
|
||||
item["back_unit"] = std::string{domaininfo.back_unit};
|
||||
}
|
||||
domains.push_back(std::move(item));
|
||||
}
|
||||
|
||||
auto &blocks = (root["blocks"] = nlohmann::ordered_json::array());
|
||||
for (auto const &type : m_types)
|
||||
{
|
||||
nlohmann::ordered_json item;
|
||||
item["id"] = type.id;
|
||||
item["category"] = type.category;
|
||||
item["name"] = type.name;
|
||||
item["description"] = type.description;
|
||||
item["origin"] = std::string{to_string(type.origin)};
|
||||
item["deferred"] = type.deferred;
|
||||
auto &ports = (item["ports"] = nlohmann::ordered_json::array());
|
||||
for (auto const &port : type.ports)
|
||||
{
|
||||
nlohmann::ordered_json portitem;
|
||||
portitem["key"] = port.key;
|
||||
portitem["direction"] = (port.direction == port_direction::in ? "in" : "out");
|
||||
portitem["domain"] = std::string{info(port.dom).id};
|
||||
if (port.dom == domain::signal)
|
||||
{
|
||||
portitem["type"] = std::string{to_string(port.type)};
|
||||
}
|
||||
if (false == port.unit.empty())
|
||||
{
|
||||
portitem["unit"] = port.unit;
|
||||
}
|
||||
if (port.direction == port_direction::in)
|
||||
{
|
||||
portitem["optional"] = port.optional;
|
||||
if (port.dom == domain::signal)
|
||||
{
|
||||
portitem["default"] = port.default_value;
|
||||
}
|
||||
}
|
||||
ports.push_back(std::move(portitem));
|
||||
}
|
||||
auto ¶ms = (item["params"] = nlohmann::ordered_json::array());
|
||||
for (auto const ¶m : type.params)
|
||||
{
|
||||
nlohmann::ordered_json paramitem;
|
||||
paramitem["key"] = param.key;
|
||||
paramitem["type"] = std::string{to_string(param.type)};
|
||||
if (false == param.unit.empty())
|
||||
{
|
||||
paramitem["unit"] = param.unit;
|
||||
}
|
||||
paramitem["default"] = to_json(param.default_value);
|
||||
if (param.type == param_type::choice)
|
||||
{
|
||||
paramitem["choices"] = param.choices;
|
||||
}
|
||||
params.push_back(std::move(paramitem));
|
||||
}
|
||||
blocks.push_back(std::move(item));
|
||||
}
|
||||
return root.dump(1, '\t');
|
||||
}
|
||||
|
||||
port_def signal_in(std::string Key, value_type const Type, double const Default, std::string Unit, bool const Optional)
|
||||
{
|
||||
port_def result;
|
||||
result.key = std::move(Key);
|
||||
result.direction = port_direction::in;
|
||||
result.dom = domain::signal;
|
||||
result.type = Type;
|
||||
result.unit = std::move(Unit);
|
||||
result.optional = Optional;
|
||||
result.default_value = Default;
|
||||
return result;
|
||||
}
|
||||
|
||||
port_def signal_out(std::string Key, value_type const Type, std::string Unit)
|
||||
{
|
||||
port_def result;
|
||||
result.key = std::move(Key);
|
||||
result.direction = port_direction::out;
|
||||
result.dom = domain::signal;
|
||||
result.type = Type;
|
||||
result.unit = std::move(Unit);
|
||||
return result;
|
||||
}
|
||||
|
||||
port_def physical_in(std::string Key, domain const Domain, bool const Optional)
|
||||
{
|
||||
port_def result;
|
||||
result.key = std::move(Key);
|
||||
result.direction = port_direction::in;
|
||||
result.dom = Domain;
|
||||
result.optional = Optional;
|
||||
return result;
|
||||
}
|
||||
|
||||
port_def physical_out(std::string Key, domain const Domain)
|
||||
{
|
||||
port_def result;
|
||||
result.key = std::move(Key);
|
||||
result.direction = port_direction::out;
|
||||
result.dom = Domain;
|
||||
return result;
|
||||
}
|
||||
|
||||
param_def number_param(std::string Key, double const Default, std::string Unit)
|
||||
{
|
||||
param_def result;
|
||||
result.key = std::move(Key);
|
||||
result.type = param_type::number;
|
||||
result.unit = std::move(Unit);
|
||||
result.default_value = param_value::make_number(Default);
|
||||
return result;
|
||||
}
|
||||
|
||||
param_def boolean_param(std::string Key, bool const Default)
|
||||
{
|
||||
param_def result;
|
||||
result.key = std::move(Key);
|
||||
result.type = param_type::boolean;
|
||||
result.default_value = param_value::make_boolean(Default);
|
||||
return result;
|
||||
}
|
||||
|
||||
param_def text_param(std::string Key, std::string Default)
|
||||
{
|
||||
param_def result;
|
||||
result.key = std::move(Key);
|
||||
result.type = param_type::text;
|
||||
result.default_value = param_value::make_text(std::move(Default));
|
||||
return result;
|
||||
}
|
||||
|
||||
param_def choice_param(std::string Key, std::vector<std::string> Choices, std::string Default)
|
||||
{
|
||||
param_def result;
|
||||
result.key = std::move(Key);
|
||||
result.type = param_type::choice;
|
||||
result.choices = std::move(Choices);
|
||||
result.default_value = param_value::make_choice(std::move(Default));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
90
vehicle/graph/graph_catalog.h
Normal file
90
vehicle/graph/graph_catalog.h
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
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_block.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
// where the behaviour of a block comes from; shown by the editor
|
||||
enum class block_origin : std::uint8_t
|
||||
{
|
||||
primitive, // generic building block of the graph
|
||||
cab, // cab control or indicator
|
||||
simulator, // wraps existing simulator code without changing it
|
||||
extracted, // simulator code extracted from Mover, shared with the .fiz vehicles
|
||||
native, // part of the simulator the graph connects to (vehicle body, couplers)
|
||||
interface_profile, // standard interface between vehicles, or between the vehicle and the AI
|
||||
script // lua function block
|
||||
};
|
||||
|
||||
std::string_view to_string(block_origin Origin);
|
||||
|
||||
struct block_type
|
||||
{
|
||||
std::string id; // e.g. "logic.and"; stored in the .vhg files, never change it once released
|
||||
std::string category;
|
||||
std::string name;
|
||||
std::string description;
|
||||
block_origin origin{block_origin::primitive};
|
||||
// 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};
|
||||
std::vector<port_def> ports;
|
||||
std::vector<param_def> params;
|
||||
block_factory create{nullptr};
|
||||
|
||||
int port_index(std::string_view Key) const;
|
||||
int param_index(std::string_view Key) const;
|
||||
};
|
||||
|
||||
// registry of all block types known to the simulator. it's also exported for the external editor
|
||||
class catalog
|
||||
{
|
||||
public:
|
||||
static catalog const &get();
|
||||
|
||||
block_type const *find(std::string_view Id) const;
|
||||
std::vector<block_type> const &types() const
|
||||
{
|
||||
return m_types;
|
||||
}
|
||||
// description of all block types and domains, for the editor
|
||||
std::string export_json() const;
|
||||
|
||||
// used by the block registration functions
|
||||
void add(block_type Type);
|
||||
|
||||
private:
|
||||
catalog() = default;
|
||||
|
||||
std::vector<block_type> m_types;
|
||||
};
|
||||
|
||||
// helpers for the definitions of block types
|
||||
port_def signal_in(std::string Key, value_type Type, double Default = 0.0, std::string Unit = "", bool Optional = false);
|
||||
port_def signal_out(std::string Key, value_type Type, std::string Unit = "");
|
||||
port_def physical_in(std::string Key, domain Domain, bool Optional = false);
|
||||
port_def physical_out(std::string Key, domain Domain);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
} // namespace vgraph
|
||||
282
vehicle/graph/graph_format.cpp
Normal file
282
vehicle/graph/graph_format.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
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_format.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <set>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void report(std::vector<diagnostic> &Diagnostics, severity const Level, std::string Node, std::string Message)
|
||||
{
|
||||
Diagnostics.push_back({Level, std::move(Node), std::move(Message)});
|
||||
}
|
||||
|
||||
bool is_valid_id(std::string const &Id)
|
||||
{
|
||||
if (Id.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return std::all_of(std::begin(Id), std::end(Id), [](char const Char) {
|
||||
return (Char >= 'a' && Char <= 'z') || (Char >= 'A' && Char <= 'Z') || (Char >= '0' && Char <= '9') || Char == '_' || Char == '-';
|
||||
});
|
||||
}
|
||||
|
||||
// reads a json scalar as a parameter value. text values become choices later, if their definition says so
|
||||
bool read_value(json const &Value, param_value &Result)
|
||||
{
|
||||
if (Value.is_boolean())
|
||||
{
|
||||
Result = param_value::make_boolean(Value.get<bool>());
|
||||
return true;
|
||||
}
|
||||
if (Value.is_number())
|
||||
{
|
||||
Result = param_value::make_number(Value.get<double>());
|
||||
return true;
|
||||
}
|
||||
if (Value.is_string())
|
||||
{
|
||||
Result = param_value::make_text(Value.get<std::string>());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void read_values(json const &Object, std::vector<named_value> &Values, std::vector<diagnostic> &Diagnostics, std::string const &Node, std::string const &What)
|
||||
{
|
||||
for (auto const &item : Object.items())
|
||||
{
|
||||
param_value value;
|
||||
if (read_value(item.value(), value))
|
||||
{
|
||||
Values.emplace_back(item.key(), std::move(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
report(Diagnostics, severity::error, Node, What + " \"" + item.key() + "\" has to be a number, a boolean or a text");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// splits "node.port" into its parts
|
||||
bool split_endpoint(json const &Value, std::string &Node, std::string &Port)
|
||||
{
|
||||
if (false == Value.is_string())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto const text{Value.get<std::string>()};
|
||||
auto const dot{text.find('.')};
|
||||
if (dot == std::string::npos || dot == 0 || dot + 1 == text.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Node = text.substr(0, dot);
|
||||
Port = text.substr(dot + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_document_(std::string const &Text, document &Document, std::vector<diagnostic> &Diagnostics)
|
||||
{
|
||||
auto const errorcount{std::count_if(std::begin(Diagnostics), std::end(Diagnostics), [](diagnostic const &Diagnostic) { return Diagnostic.level == severity::error; })};
|
||||
|
||||
// comments are allowed, to make hand editing of the files easier
|
||||
auto const root = json::parse(Text, nullptr, false, true);
|
||||
if (root.is_discarded() || false == root.is_object())
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "the file isn't a valid json object");
|
||||
return false;
|
||||
}
|
||||
if (root.value("format", std::string{}) != file_format)
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "the file isn't a vehicle graph (missing \"format\": \"" + std::string{file_format} + "\")");
|
||||
return false;
|
||||
}
|
||||
auto const version{root.value("version", 0)};
|
||||
if (version != file_version)
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "unsupported file version " + std::to_string(version) + ", expected " + std::to_string(file_version) +
|
||||
(version < file_version ? "; open and save the file in the current editor to convert it" : ""));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root.contains("vehicle") && root["vehicle"].is_object())
|
||||
{
|
||||
Document.name = root["vehicle"].value("name", "");
|
||||
}
|
||||
if (root.contains("settings"))
|
||||
{
|
||||
auto const &settings = root["settings"];
|
||||
if (settings.is_object())
|
||||
{
|
||||
for (auto const &item : settings.items())
|
||||
{
|
||||
if (item.key() == "low_voltage_simulation" && item.value().is_boolean())
|
||||
{
|
||||
Document.low_voltage_simulation = item.value().get<bool>();
|
||||
}
|
||||
else
|
||||
{
|
||||
report(Diagnostics, severity::warning, "", "unknown or malformed setting \"" + item.key() + "\" was ignored");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "\"settings\" has to be an object");
|
||||
}
|
||||
}
|
||||
if (root.contains("body"))
|
||||
{
|
||||
if (root["body"].is_object())
|
||||
{
|
||||
read_values(root["body"], Document.body, Diagnostics, "", "body parameter");
|
||||
}
|
||||
else
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "\"body\" has to be an object");
|
||||
}
|
||||
}
|
||||
if (root.contains("profiles"))
|
||||
{
|
||||
if (root["profiles"].is_object())
|
||||
{
|
||||
for (auto const &item : root["profiles"].items())
|
||||
{
|
||||
if (item.value().is_number_integer())
|
||||
{
|
||||
Document.profiles.emplace_back(item.key(), item.value().get<int>());
|
||||
}
|
||||
else
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "version of the interface profile \"" + item.key() + "\" has to be an integer");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "\"profiles\" has to be an object");
|
||||
}
|
||||
}
|
||||
|
||||
// nodes
|
||||
if (false == root.contains("nodes") || false == root["nodes"].is_array())
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "missing \"nodes\" array");
|
||||
return false;
|
||||
}
|
||||
std::set<std::string> ids;
|
||||
for (auto const &item : root["nodes"])
|
||||
{
|
||||
if (false == item.is_object())
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "every entry of \"nodes\" has to be an object");
|
||||
continue;
|
||||
}
|
||||
node_desc node;
|
||||
node.id = item.value("id", "");
|
||||
node.type = item.value("type", "");
|
||||
node.title = item.value("title", "");
|
||||
if (false == is_valid_id(node.id))
|
||||
{
|
||||
report(Diagnostics, severity::error, node.id, "node id has to be made of letters, digits, '_' and '-'");
|
||||
continue;
|
||||
}
|
||||
if (false == ids.insert(node.id).second)
|
||||
{
|
||||
report(Diagnostics, severity::error, node.id, "duplicate node id");
|
||||
continue;
|
||||
}
|
||||
if (node.type.empty())
|
||||
{
|
||||
report(Diagnostics, severity::error, node.id, "missing block type");
|
||||
continue;
|
||||
}
|
||||
if (item.contains("params"))
|
||||
{
|
||||
if (item["params"].is_object())
|
||||
{
|
||||
read_values(item["params"], node.params, Diagnostics, node.id, "parameter");
|
||||
}
|
||||
else
|
||||
{
|
||||
report(Diagnostics, severity::error, node.id, "\"params\" has to be an object");
|
||||
}
|
||||
}
|
||||
Document.nodes.emplace_back(std::move(node));
|
||||
}
|
||||
|
||||
// connections
|
||||
if (root.contains("connections"))
|
||||
{
|
||||
if (root["connections"].is_array())
|
||||
{
|
||||
for (auto const &item : root["connections"])
|
||||
{
|
||||
connection_desc connection;
|
||||
if (false == item.is_object() || false == split_endpoint(item.value("from", json{}), connection.from_node, connection.from_port) ||
|
||||
false == split_endpoint(item.value("to", json{}), connection.to_node, connection.to_port))
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "malformed connection " + item.dump() + ", expected {\"from\": \"node.port\", \"to\": \"node.port\"}");
|
||||
continue;
|
||||
}
|
||||
Document.connections.emplace_back(std::move(connection));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "\"connections\" has to be an array");
|
||||
}
|
||||
}
|
||||
// "layout" holds the positions of the blocks in the editor and is of no interest to the simulator
|
||||
|
||||
return errorcount == std::count_if(std::begin(Diagnostics), std::end(Diagnostics), [](diagnostic const &Diagnostic) { return Diagnostic.level == severity::error; });
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool parse_document(std::string const &Text, document &Document, std::vector<diagnostic> &Diagnostics)
|
||||
{
|
||||
try
|
||||
{
|
||||
return parse_document_(Text, Document, Diagnostics);
|
||||
}
|
||||
catch (json::exception const &Error)
|
||||
{
|
||||
// values of unexpected type, e.g. a number where the id should be
|
||||
report(Diagnostics, severity::error, "", std::string{"malformed file: "} + Error.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool load_document(std::string const &Path, document &Document, std::vector<diagnostic> &Diagnostics)
|
||||
{
|
||||
std::ifstream file{Path, std::ios::binary};
|
||||
if (false == file.is_open())
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "can't open file \"" + Path + "\"");
|
||||
return false;
|
||||
}
|
||||
std::string const text{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||
return parse_document(text, Document, Diagnostics);
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
58
vehicle/graph/graph_format.h
Normal file
58
vehicle/graph/graph_format.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
using named_value = std::pair<std::string, param_value>;
|
||||
|
||||
struct node_desc
|
||||
{
|
||||
std::string id;
|
||||
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
|
||||
};
|
||||
|
||||
struct connection_desc
|
||||
{
|
||||
std::string from_node;
|
||||
std::string from_port;
|
||||
std::string to_node;
|
||||
std::string to_port;
|
||||
};
|
||||
|
||||
// content of a .vhg file, before it's checked against the block catalog
|
||||
struct document
|
||||
{
|
||||
std::string name;
|
||||
// when enabled, blocks with low voltage ports simulate the voltages and currents of the control circuits.
|
||||
// when disabled, low voltage connections are ignored and the apparatus behaves as if always powered
|
||||
bool low_voltage_simulation{false};
|
||||
std::vector<named_value> body; // parameters of the vehicle body (mass, dimensions, axles...); used by the simulator integration
|
||||
std::vector<std::pair<std::string, int>> profiles; // standard interfaces used by the vehicle, with their versions
|
||||
std::vector<node_desc> nodes;
|
||||
std::vector<connection_desc> connections;
|
||||
};
|
||||
|
||||
// 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
|
||||
bool load_document(std::string const &Path, document &Document, std::vector<diagnostic> &Diagnostics);
|
||||
|
||||
} // namespace vgraph
|
||||
92
vehicle/graph/graph_instance.cpp
Normal file
92
vehicle/graph/graph_instance.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
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_instance.h"
|
||||
#include "vehicle/graph/graph_catalog.h"
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
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()};
|
||||
m_blocks.reserve(nodes.size());
|
||||
for (auto const &node : nodes)
|
||||
{
|
||||
block_setup const setup{*node.type, node.params, node.slots, node.connected};
|
||||
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->deferred)
|
||||
{
|
||||
m_deferred.push_back(m_blocks[nodeindex].get());
|
||||
}
|
||||
}
|
||||
// deferred blocks publish their initial state, so the blocks they feed don't see zeros in the first step
|
||||
auto frame{make_frame(0.0)};
|
||||
for (auto *item : m_deferred)
|
||||
{
|
||||
item->commit(frame);
|
||||
}
|
||||
}
|
||||
|
||||
frame graph_instance::make_frame(double const Deltatime)
|
||||
{
|
||||
frame result;
|
||||
result.dt = Deltatime;
|
||||
result.time = m_time;
|
||||
result.low_voltage = m_template->low_voltage_simulation();
|
||||
result.signals = m_signals.data();
|
||||
result.forced = m_forced.data();
|
||||
result.links = m_links.data();
|
||||
return result;
|
||||
}
|
||||
|
||||
void graph_instance::update(double const Deltatime)
|
||||
{
|
||||
m_time += Deltatime;
|
||||
auto frame{make_frame(Deltatime)};
|
||||
for (auto *item : m_order)
|
||||
{
|
||||
item->update(frame);
|
||||
}
|
||||
for (auto *item : m_deferred)
|
||||
{
|
||||
item->commit(frame);
|
||||
}
|
||||
for (auto &link : m_links)
|
||||
{
|
||||
link.back = link.back_next;
|
||||
link.back_next = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
block *graph_instance::find_block(std::string_view const Id)
|
||||
{
|
||||
auto const nodeindex{m_template->find_node(Id)};
|
||||
return (nodeindex >= 0 ? m_blocks[nodeindex].get() : nullptr);
|
||||
}
|
||||
|
||||
void graph_instance::force(int const Slot, double const Value)
|
||||
{
|
||||
m_signals[Slot] = Value;
|
||||
m_forced[Slot] = 1;
|
||||
}
|
||||
|
||||
void graph_instance::release(int const Slot)
|
||||
{
|
||||
m_forced[Slot] = 0;
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
78
vehicle/graph/graph_instance.h
Normal file
78
vehicle/graph/graph_instance.h
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
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_block.h"
|
||||
#include "vehicle/graph/graph_template.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
// simulation state of the graph of a single vehicle
|
||||
class graph_instance
|
||||
{
|
||||
public:
|
||||
explicit graph_instance(std::shared_ptr<graph_template const> Template);
|
||||
|
||||
// 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
|
||||
void update(double Deltatime);
|
||||
|
||||
graph_template const &definition() const
|
||||
{
|
||||
return *m_template;
|
||||
}
|
||||
double time() const
|
||||
{
|
||||
return m_time;
|
||||
}
|
||||
|
||||
// block of the node with the specified index (see graph_template::nodes())
|
||||
block &node_block(std::size_t Index)
|
||||
{
|
||||
return *m_blocks[Index];
|
||||
}
|
||||
block const &node_block(std::size_t Index) const
|
||||
{
|
||||
return *m_blocks[Index];
|
||||
}
|
||||
// block of the node with the specified id, or null
|
||||
block *find_block(std::string_view Id);
|
||||
|
||||
double signal(int Slot) const
|
||||
{
|
||||
return m_signals[Slot];
|
||||
}
|
||||
// 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);
|
||||
bool is_forced(int Slot) const
|
||||
{
|
||||
return m_forced[Slot] != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
frame make_frame(double Deltatime);
|
||||
|
||||
std::shared_ptr<graph_template const> m_template;
|
||||
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_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;
|
||||
double m_time{0.0};
|
||||
};
|
||||
|
||||
} // namespace vgraph
|
||||
318
vehicle/graph/graph_standalone.cpp
Normal file
318
vehicle/graph/graph_standalone.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
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_standalone.h"
|
||||
#include "vehicle/graph/graph_catalog.h"
|
||||
#include "vehicle/graph/graph_instance.h"
|
||||
#include "vehicle/graph/blocks/cab.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
// command line tools of the vehicle graph. they run without the rest of the simulator; the log service doesn't run
|
||||
// in this mode, so the results are reported directly to the console
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct script_event
|
||||
{
|
||||
double time{0.0};
|
||||
std::string operation;
|
||||
std::string target;
|
||||
double value{0.0};
|
||||
int line{0};
|
||||
};
|
||||
|
||||
bool load_script(std::string const &Path, std::vector<script_event> &Events)
|
||||
{
|
||||
std::ifstream file{Path};
|
||||
if (false == file.is_open())
|
||||
{
|
||||
std::printf("can't open script file \"%s\"\n", Path.c_str());
|
||||
return false;
|
||||
}
|
||||
std::string line;
|
||||
int linenumber{0};
|
||||
while (std::getline(file, line))
|
||||
{
|
||||
++linenumber;
|
||||
auto const comment{line.find('#')};
|
||||
if (comment != std::string::npos)
|
||||
{
|
||||
line.erase(comment);
|
||||
}
|
||||
std::istringstream stream{line};
|
||||
script_event event;
|
||||
event.line = linenumber;
|
||||
if (false == static_cast<bool>(stream >> event.time))
|
||||
{
|
||||
// empty line
|
||||
continue;
|
||||
}
|
||||
if (false == static_cast<bool>(stream >> event.operation >> event.target))
|
||||
{
|
||||
std::printf("%s:%d: expected: <time> <operation> <target> [value]\n", Path.c_str(), linenumber);
|
||||
return false;
|
||||
}
|
||||
auto const needsvalue{event.operation == "set" || event.operation == "value" || event.operation == "force"};
|
||||
if (needsvalue && false == static_cast<bool>(stream >> event.value))
|
||||
{
|
||||
std::printf("%s:%d: operation \"%s\" needs a value\n", Path.c_str(), linenumber, event.operation.c_str());
|
||||
return false;
|
||||
}
|
||||
Events.push_back(event);
|
||||
}
|
||||
std::stable_sort(std::begin(Events), std::end(Events), [](script_event const &Left, script_event const &Right) { return Left.time < Right.time; });
|
||||
return true;
|
||||
}
|
||||
|
||||
bool apply_event(graph_instance &Instance, script_event const &Event)
|
||||
{
|
||||
if (Event.operation == "force" || Event.operation == "unforce")
|
||||
{
|
||||
auto const slot{Instance.definition().find_signal(Event.target)};
|
||||
if (slot < 0)
|
||||
{
|
||||
std::printf("script line %d: unknown signal port \"%s\"\n", Event.line, Event.target.c_str());
|
||||
return false;
|
||||
}
|
||||
if (Event.operation == "force")
|
||||
{
|
||||
Instance.force(slot, Event.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance.release(slot);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
auto *control{dynamic_cast<cab_switch *>(Instance.find_block(Event.target))};
|
||||
if (control == nullptr)
|
||||
{
|
||||
std::printf("script line %d: \"%s\" isn't a cab switch\n", Event.line, Event.target.c_str());
|
||||
return false;
|
||||
}
|
||||
if (Event.operation == "press")
|
||||
{
|
||||
control->press();
|
||||
}
|
||||
else if (Event.operation == "release")
|
||||
{
|
||||
control->release();
|
||||
}
|
||||
else if (Event.operation == "toggle")
|
||||
{
|
||||
control->toggle();
|
||||
}
|
||||
else if (Event.operation == "inc")
|
||||
{
|
||||
control->increase();
|
||||
}
|
||||
else if (Event.operation == "dec")
|
||||
{
|
||||
control->decrease();
|
||||
}
|
||||
else if (Event.operation == "set")
|
||||
{
|
||||
control->set_position(static_cast<int>(std::lround(Event.value)));
|
||||
}
|
||||
else if (Event.operation == "value")
|
||||
{
|
||||
control->set_value(Event.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::printf("script line %d: unknown operation \"%s\"\n", Event.line, Event.operation.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string format_number(double const Value)
|
||||
{
|
||||
char buffer[32];
|
||||
// negative zero is written as zero, so equal results give equal files
|
||||
std::snprintf(buffer, sizeof(buffer), "%.9g", Value == 0.0 ? 0.0 : Value);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int graph_catalog_standalone(std::string const &Output)
|
||||
{
|
||||
auto const text{catalog::get().export_json()};
|
||||
if (Output.empty())
|
||||
{
|
||||
std::printf("%s\n", text.c_str());
|
||||
return 0;
|
||||
}
|
||||
std::ofstream file{Output, std::ios::binary};
|
||||
if (false == file.is_open())
|
||||
{
|
||||
std::printf("can't create file \"%s\"\n", Output.c_str());
|
||||
return 1;
|
||||
}
|
||||
file << text << '\n';
|
||||
std::printf("block catalog written to %s (%zu block types)\n", Output.c_str(), catalog::get().types().size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
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>]
|
||||
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");
|
||||
return 1;
|
||||
}
|
||||
std::string const input{Argv[2]};
|
||||
std::string script;
|
||||
std::string output;
|
||||
double deltatime{0.01};
|
||||
double duration{0.0};
|
||||
int every{1};
|
||||
for (int idx = 3; idx + 1 < Argc; idx += 2)
|
||||
{
|
||||
std::string const option{Argv[idx]};
|
||||
std::string const value{Argv[idx + 1]};
|
||||
if (option == "-script")
|
||||
{
|
||||
script = value;
|
||||
}
|
||||
else if (option == "-o")
|
||||
{
|
||||
output = value;
|
||||
}
|
||||
else if (option == "-dt")
|
||||
{
|
||||
deltatime = std::atof(value.c_str());
|
||||
}
|
||||
else if (option == "-t")
|
||||
{
|
||||
duration = std::atof(value.c_str());
|
||||
}
|
||||
else if (option == "-every")
|
||||
{
|
||||
every = std::max(1, std::atoi(value.c_str()));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::printf("unknown option \"%s\"\n", option.c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (deltatime <= 0.0)
|
||||
{
|
||||
std::printf("the step has to be positive\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<diagnostic> diagnostics;
|
||||
auto const graph{load_graph(input, diagnostics)};
|
||||
for (auto const &entry : diagnostics)
|
||||
{
|
||||
std::printf("%s: %s\n", input.c_str(), to_string(entry).c_str());
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (duration <= 0.0 && false == events.empty())
|
||||
{
|
||||
// by default run a second past the last event
|
||||
duration = events.back().time + 1.0;
|
||||
}
|
||||
if (duration <= 0.0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
graph_instance instance{graph};
|
||||
auto const signals{graph->signal_outputs()};
|
||||
std::ostringstream results;
|
||||
results << "time";
|
||||
for (auto const &signal : signals)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
results << '\n';
|
||||
|
||||
auto const stepcount{static_cast<long long>(std::ceil(duration / deltatime - 1e-9))};
|
||||
std::size_t nextevent{0};
|
||||
for (long long step = 0; step < stepcount; ++step)
|
||||
{
|
||||
// events take effect at the beginning of the first step which starts at or after their time
|
||||
auto const steptime{step * deltatime};
|
||||
while (nextevent < events.size() && events[nextevent].time <= steptime + 1e-9)
|
||||
{
|
||||
if (false == apply_event(instance, events[nextevent]))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
++nextevent;
|
||||
}
|
||||
instance.update(deltatime);
|
||||
if ((step + 1) % every != 0 && step + 1 != stepcount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
results << format_number((step + 1) * deltatime);
|
||||
for (auto const &signal : 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));
|
||||
}
|
||||
}
|
||||
results << '\n';
|
||||
}
|
||||
|
||||
if (output.empty())
|
||||
{
|
||||
std::printf("%s", results.str().c_str());
|
||||
return 0;
|
||||
}
|
||||
std::ofstream file{output, std::ios::binary};
|
||||
if (false == file.is_open())
|
||||
{
|
||||
std::printf("can't create file \"%s\"\n", output.c_str());
|
||||
return 1;
|
||||
}
|
||||
file << results.str();
|
||||
std::printf("%lld steps written to %s\n", stepcount, output.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
24
vehicle/graph/graph_standalone.h
Normal file
24
vehicle/graph/graph_standalone.h
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
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 <string>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
// -graph-catalog [<file.json>]: writes the description of all block types, for the external editor.
|
||||
// returns: 0 on success, 1 otherwise
|
||||
int graph_catalog_standalone(std::string const &Output);
|
||||
// -graph-run <file.vhg> [options]: checks the graph and runs it without the simulator, driven by a script of events.
|
||||
// returns: 0 on success, 1 otherwise
|
||||
int graph_run_standalone(int Argc, char *Argv[]);
|
||||
|
||||
} // namespace vgraph
|
||||
417
vehicle/graph/graph_template.cpp
Normal file
417
vehicle/graph/graph_template.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
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_template.h"
|
||||
#include "vehicle/graph/graph_catalog.h"
|
||||
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
void report(std::vector<diagnostic> &Diagnostics, severity const Level, std::string Node, std::string Message)
|
||||
{
|
||||
Diagnostics.push_back({Level, std::move(Node), std::move(Message)});
|
||||
}
|
||||
|
||||
std::string join_choices(std::vector<std::string> const &Choices)
|
||||
{
|
||||
std::string result;
|
||||
for (auto const &choice : Choices)
|
||||
{
|
||||
if (false == result.empty())
|
||||
{
|
||||
result += ", ";
|
||||
}
|
||||
result += choice;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// checks a value read from the file against the parameter definition; text values become choices here
|
||||
bool resolve_param(param_def const &Definition, param_value const &Value, param_value &Result, std::string &Problem)
|
||||
{
|
||||
switch (Definition.type)
|
||||
{
|
||||
case param_type::number:
|
||||
{
|
||||
if (Value.type != param_type::number)
|
||||
{
|
||||
Problem = "has to be a number";
|
||||
return false;
|
||||
}
|
||||
Result = Value;
|
||||
return true;
|
||||
}
|
||||
case param_type::boolean:
|
||||
{
|
||||
if (Value.type != param_type::boolean)
|
||||
{
|
||||
Problem = "has to be true or false";
|
||||
return false;
|
||||
}
|
||||
Result = Value;
|
||||
return true;
|
||||
}
|
||||
case param_type::text:
|
||||
{
|
||||
if (Value.type != param_type::text)
|
||||
{
|
||||
Problem = "has to be a text";
|
||||
return false;
|
||||
}
|
||||
Result = Value;
|
||||
return true;
|
||||
}
|
||||
case param_type::choice:
|
||||
{
|
||||
if (Value.type != param_type::text || std::find(std::begin(Definition.choices), std::end(Definition.choices), Value.text) == std::end(Definition.choices))
|
||||
{
|
||||
Problem = "has to be one of: " + join_choices(Definition.choices);
|
||||
return false;
|
||||
}
|
||||
Result = param_value::make_choice(Value.text);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
{
|
||||
Problem = "has unsupported type";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct endpoint
|
||||
{
|
||||
int node{-1};
|
||||
int port{-1};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<graph_template const> graph_template::compile(document const &Document, catalog const &Catalog, std::vector<diagnostic> &Diagnostics)
|
||||
{
|
||||
auto const errorcount{std::count_if(std::begin(Diagnostics), std::end(Diagnostics), [](diagnostic const &Diagnostic) { return Diagnostic.level == severity::error; })};
|
||||
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;
|
||||
|
||||
// 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
|
||||
for (auto const &desc : Document.nodes)
|
||||
{
|
||||
auto const *type{Catalog.find(desc.type)};
|
||||
if (type == nullptr)
|
||||
{
|
||||
report(Diagnostics, severity::error, desc.id, "unknown block type \"" + desc.type + "\"");
|
||||
unresolvednodes.emplace(desc.id, true);
|
||||
continue;
|
||||
}
|
||||
node_entry node;
|
||||
node.id = desc.id;
|
||||
node.title = desc.title;
|
||||
node.type = type;
|
||||
node.params.reserve(type->params.size());
|
||||
for (auto const &definition : type->params)
|
||||
{
|
||||
node.params.push_back(definition.default_value);
|
||||
}
|
||||
for (auto const ¶m : desc.params)
|
||||
{
|
||||
auto const paramindex{type->param_index(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))
|
||||
{
|
||||
report(Diagnostics, severity::error, desc.id, "parameter \"" + param.first + "\" " + problem);
|
||||
}
|
||||
}
|
||||
nodeindices.emplace(node.id, static_cast<int>(result->m_nodes.size()));
|
||||
result->m_nodes.emplace_back(std::move(node));
|
||||
}
|
||||
|
||||
auto &nodes{result->m_nodes};
|
||||
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)
|
||||
{
|
||||
auto const &port{node.type->ports[portindex]};
|
||||
if (port.direction != port_direction::out)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (port.dom == domain::signal)
|
||||
{
|
||||
node.slots[portindex] = static_cast<int>(signaldefaults.size());
|
||||
signaldefaults.push_back(0.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
node.slots[portindex] = static_cast<int>(linkcount++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connections
|
||||
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());
|
||||
}
|
||||
std::vector<std::vector<int>> edges(nodes.size()); // ordering constraints: source node -> consumer node
|
||||
bool lowvoltageignored{false};
|
||||
for (auto const &connection : Document.connections)
|
||||
{
|
||||
auto const label{connection.from_node + "." + connection.from_port + " -> " + connection.to_node + "." + connection.to_port};
|
||||
auto const fromlookup{nodeindices.find(connection.from_node)};
|
||||
auto const tolookup{nodeindices.find(connection.to_node)};
|
||||
if (fromlookup == nodeindices.end() || tolookup == nodeindices.end())
|
||||
{
|
||||
auto const &missing{fromlookup == nodeindices.end() ? connection.from_node : connection.to_node};
|
||||
if (unresolvednodes.find(missing) == unresolvednodes.end())
|
||||
{
|
||||
report(Diagnostics, severity::error, "", "connection " + label + " refers to unknown node \"" + missing + "\"");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
auto const fromnode{fromlookup->second};
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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};
|
||||
if (fromdomain != todomain)
|
||||
{
|
||||
report(Diagnostics, severity::error, connection.to_node,
|
||||
"connection " + label + " joins different domains: " + std::string{info(fromdomain).id} + " and " + std::string{info(todomain).id});
|
||||
continue;
|
||||
}
|
||||
if (sources[tonode][toport].node >= 0)
|
||||
{
|
||||
report(Diagnostics, severity::error, connection.to_node, "input \"" + connection.to_port + "\" has more than one source; use a junction block to join them");
|
||||
continue;
|
||||
}
|
||||
if (fromdomain == domain::low_voltage && false == result->m_lowvoltage)
|
||||
{
|
||||
lowvoltageignored = true;
|
||||
}
|
||||
sources[tonode][toport] = {fromnode, fromport};
|
||||
nodes[fromnode].connected[fromport] = 1;
|
||||
nodes[tonode].connected[toport] = 1;
|
||||
if (false == fromtype.deferred)
|
||||
{
|
||||
edges[fromnode].push_back(tonode);
|
||||
}
|
||||
}
|
||||
if (lowvoltageignored)
|
||||
{
|
||||
report(Diagnostics, severity::warning, "", "low voltage connections have no effect, because low voltage simulation is disabled in the graph settings");
|
||||
}
|
||||
|
||||
// inputs read their sources, or slots of their own holding the default values
|
||||
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)
|
||||
{
|
||||
auto const &port{node.type->ports[portindex]};
|
||||
if (port.direction != port_direction::in)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto const &source{sources[nodeindex][portindex]};
|
||||
if (source.node >= 0)
|
||||
{
|
||||
node.slots[portindex] = nodes[source.node].slots[source.port];
|
||||
continue;
|
||||
}
|
||||
if (false == port.optional)
|
||||
{
|
||||
report(Diagnostics, severity::warning, node.id, "required input \"" + port.key + "\" isn't connected");
|
||||
}
|
||||
if (port.dom == domain::signal)
|
||||
{
|
||||
node.slots[portindex] = static_cast<int>(signaldefaults.size());
|
||||
signaldefaults.push_back(port.default_value);
|
||||
}
|
||||
else
|
||||
{
|
||||
node.slots[portindex] = static_cast<int>(linkcount++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// order of evaluation. ties are resolved by the order of nodes in the file, so the result is stable
|
||||
std::vector<int> indegree(nodes.size(), 0);
|
||||
for (auto const &targets : edges)
|
||||
{
|
||||
for (auto const target : targets)
|
||||
{
|
||||
++indegree[target];
|
||||
}
|
||||
}
|
||||
std::priority_queue<int, std::vector<int>, std::greater<int>> ready;
|
||||
for (std::size_t nodeindex = 0; nodeindex < nodes.size(); ++nodeindex)
|
||||
{
|
||||
if (indegree[nodeindex] == 0)
|
||||
{
|
||||
ready.push(static_cast<int>(nodeindex));
|
||||
}
|
||||
}
|
||||
while (false == ready.empty())
|
||||
{
|
||||
auto const nodeindex{ready.top()};
|
||||
ready.pop();
|
||||
result->m_order.push_back(nodeindex);
|
||||
for (auto const target : edges[nodeindex])
|
||||
{
|
||||
if (--indegree[target] == 0)
|
||||
{
|
||||
ready.push(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result->m_order.size() != nodes.size())
|
||||
{
|
||||
// strip nodes merely fed by a loop, so the report lists only the nodes the loops pass through
|
||||
std::vector<bool> remaining(nodes.size(), false);
|
||||
for (std::size_t nodeindex = 0; nodeindex < nodes.size(); ++nodeindex)
|
||||
{
|
||||
remaining[nodeindex] = (indegree[nodeindex] > 0);
|
||||
}
|
||||
bool changed{true};
|
||||
while (changed)
|
||||
{
|
||||
changed = false;
|
||||
for (std::size_t nodeindex = 0; nodeindex < nodes.size(); ++nodeindex)
|
||||
{
|
||||
if (false == remaining[nodeindex])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto const feedsloop{std::any_of(std::begin(edges[nodeindex]), std::end(edges[nodeindex]), [&](int const Target) { return remaining[Target]; })};
|
||||
if (false == feedsloop)
|
||||
{
|
||||
remaining[nodeindex] = false;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string loop;
|
||||
for (std::size_t nodeindex = 0; nodeindex < nodes.size(); ++nodeindex)
|
||||
{
|
||||
if (remaining[nodeindex])
|
||||
{
|
||||
loop += (loop.empty() ? "" : ", ") + nodes[nodeindex].id;
|
||||
}
|
||||
}
|
||||
report(Diagnostics, severity::error, "",
|
||||
"connections form a loop without a deferred block (e.g. logic.sr, logic.delay, logic.ton), so the order of evaluation can't be resolved; nodes in the loop: " + loop);
|
||||
}
|
||||
|
||||
auto const failed{errorcount != std::count_if(std::begin(Diagnostics), std::end(Diagnostics), [](diagnostic const &Diagnostic) { return Diagnostic.level == severity::error; })};
|
||||
if (failed)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int graph_template::find_node(std::string_view const Id) const
|
||||
{
|
||||
for (std::size_t idx = 0; idx < m_nodes.size(); ++idx)
|
||||
{
|
||||
if (m_nodes[idx].id == Id)
|
||||
{
|
||||
return static_cast<int>(idx);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int graph_template::find_signal(std::string_view const Endpoint) const
|
||||
{
|
||||
auto const dot{Endpoint.find('.')};
|
||||
if (dot == std::string_view::npos)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
auto const nodeindex{find_node(Endpoint.substr(0, dot))};
|
||||
if (nodeindex < 0)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return node.slots[portindex];
|
||||
}
|
||||
|
||||
std::shared_ptr<graph_template const> load_graph(std::string const &Path, std::vector<diagnostic> &Diagnostics)
|
||||
{
|
||||
document graph;
|
||||
if (false == load_document(Path, graph, Diagnostics))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return graph_template::compile(graph, catalog::get(), Diagnostics);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
auto const &port{node.type->ports[portindex]};
|
||||
if (port.direction == port_direction::out && port.dom == domain::signal)
|
||||
{
|
||||
result.emplace_back(node.id + "." + port.key, node.slots[portindex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
102
vehicle/graph/graph_template.h
Normal file
102
vehicle/graph/graph_template.h
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
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_format.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
struct block_type;
|
||||
class catalog;
|
||||
|
||||
// graph checked against the catalog and resolved to flat arrays: node parameters in definition order, ports bound to
|
||||
// signal slots or physical links, and the order of evaluation. it holds no simulation state and is shared by all
|
||||
// vehicles of the same type
|
||||
class graph_template
|
||||
{
|
||||
public:
|
||||
struct node_entry
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
// checks the document and builds the template. problems are appended to the diagnostics; returns null on errors
|
||||
static std::shared_ptr<graph_template const> compile(document const &Document, catalog const &Catalog, std::vector<diagnostic> &Diagnostics);
|
||||
|
||||
std::string const &name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
bool low_voltage_simulation() const
|
||||
{
|
||||
return m_lowvoltage;
|
||||
}
|
||||
std::vector<node_entry> const &nodes() const
|
||||
{
|
||||
return m_nodes;
|
||||
}
|
||||
// node indices in the order of evaluation
|
||||
std::vector<int> const &order() const
|
||||
{
|
||||
return m_order;
|
||||
}
|
||||
// initial values of all signal slots; for unconnected inputs these are the values the blocks will see
|
||||
std::vector<double> const &signal_defaults() const
|
||||
{
|
||||
return m_signaldefaults;
|
||||
}
|
||||
std::size_t link_count() const
|
||||
{
|
||||
return m_linkcount;
|
||||
}
|
||||
std::vector<named_value> const &body() const
|
||||
{
|
||||
return m_body;
|
||||
}
|
||||
std::vector<std::pair<std::string, int>> const &profiles() const
|
||||
{
|
||||
return m_profiles;
|
||||
}
|
||||
|
||||
// index of the node with the specified id, or -1
|
||||
int find_node(std::string_view Id) const;
|
||||
// signal slot of the specified port, given as "node.port", or -1 if there's no such signal port
|
||||
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;
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
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};
|
||||
std::vector<named_value> m_body;
|
||||
std::vector<std::pair<std::string, int>> m_profiles;
|
||||
};
|
||||
|
||||
// loads the specified .vhg file and compiles it against the simulator's catalog.
|
||||
// problems are appended to the diagnostics; returns null on errors
|
||||
std::shared_ptr<graph_template const> load_graph(std::string const &Path, std::vector<diagnostic> &Diagnostics);
|
||||
|
||||
} // namespace vgraph
|
||||
131
vehicle/graph/graph_types.cpp
Normal file
131
vehicle/graph/graph_types.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
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_types.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::array<domain_info, static_cast<std::size_t>(domain::count_)> const domains{{
|
||||
{"signal", "value", "", "", "", false},
|
||||
{"high_voltage", "voltage", "V", "current", "A", true},
|
||||
{"low_voltage", "voltage", "V", "current", "A", true},
|
||||
{"aux_ac", "voltage", "V", "power", "W", true},
|
||||
{"drive", "demand", "", "power", "W", true},
|
||||
{"mechanical", "torque", "Nm", "rotational speed", "rad/s", true},
|
||||
{"traction", "force", "N", "velocity", "m/s", true},
|
||||
{"air", "pressure", "bar", "flow", "", true},
|
||||
{"brake_pipe", "pressure", "bar", "flow", "", true},
|
||||
{"feed_pipe", "pressure", "bar", "flow", "", true},
|
||||
}};
|
||||
|
||||
} // namespace
|
||||
|
||||
domain_info const &info(domain const Domain)
|
||||
{
|
||||
return domains[static_cast<std::size_t>(Domain)];
|
||||
}
|
||||
|
||||
bool parse_domain(std::string_view const Text, domain &Domain)
|
||||
{
|
||||
for (std::size_t idx = 0; idx < domains.size(); ++idx)
|
||||
{
|
||||
if (domains[idx].id == Text)
|
||||
{
|
||||
Domain = static_cast<domain>(idx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string_view to_string(value_type const Type)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case value_type::boolean:
|
||||
return "bool";
|
||||
case value_type::integer:
|
||||
return "int";
|
||||
default:
|
||||
return "float";
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view to_string(param_type const Type)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case param_type::boolean:
|
||||
return "bool";
|
||||
case param_type::text:
|
||||
return "text";
|
||||
case param_type::choice:
|
||||
return "choice";
|
||||
default:
|
||||
return "number";
|
||||
}
|
||||
}
|
||||
|
||||
param_value param_value::make_number(double const Value)
|
||||
{
|
||||
param_value result;
|
||||
result.type = param_type::number;
|
||||
result.number = Value;
|
||||
return result;
|
||||
}
|
||||
|
||||
param_value param_value::make_boolean(bool const Value)
|
||||
{
|
||||
param_value result;
|
||||
result.type = param_type::boolean;
|
||||
result.boolean = Value;
|
||||
return result;
|
||||
}
|
||||
|
||||
param_value param_value::make_text(std::string Value)
|
||||
{
|
||||
param_value result;
|
||||
result.type = param_type::text;
|
||||
result.text = std::move(Value);
|
||||
return result;
|
||||
}
|
||||
|
||||
param_value param_value::make_choice(std::string Value)
|
||||
{
|
||||
param_value result;
|
||||
result.type = param_type::choice;
|
||||
result.text = std::move(Value);
|
||||
return result;
|
||||
}
|
||||
|
||||
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; });
|
||||
}
|
||||
|
||||
std::string to_string(diagnostic const &Diagnostic)
|
||||
{
|
||||
std::string result{Diagnostic.level == severity::error ? "error" : "warning"};
|
||||
if (false == Diagnostic.node.empty())
|
||||
{
|
||||
result += " [" + Diagnostic.node + "]";
|
||||
}
|
||||
result += ": " + Diagnostic.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace vgraph
|
||||
133
vehicle/graph/graph_types.h
Normal file
133
vehicle/graph/graph_types.h
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/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// vehicle graph: block based description of vehicle logic and physics, loaded from .vhg files
|
||||
// in place of .fiz. the core of the graph depends only on the standard library and nlohmann json
|
||||
// (the latter only in the .cpp files), so it can be built and tested without the rest of the simulator
|
||||
namespace vgraph
|
||||
{
|
||||
|
||||
// format identification of the .vhg (vehicle graph) files
|
||||
inline constexpr std::string_view file_format{"maszyna-vehicle-graph"};
|
||||
inline constexpr int file_version{2};
|
||||
inline constexpr std::string_view file_extension{".vhg"};
|
||||
|
||||
// signal ports pass one value in one direction. physical ports pass a pair of values in opposite directions,
|
||||
// e.g. voltage from the source towards the load and current from the load back towards the source
|
||||
enum class domain : std::uint8_t
|
||||
{
|
||||
signal,
|
||||
high_voltage, // U [V] forward, I [A] back
|
||||
low_voltage, // U [V] forward, I [A] back; only simulated when the graph enables low voltage simulation
|
||||
aux_ac, // U [V] forward, P [W] back
|
||||
drive, // demand [0-1] forward, P [W] back
|
||||
mechanical, // torque [Nm] forward, rotational speed [rad/s] back
|
||||
traction, // force at the wheel rim [N] forward, velocity [m/s] back
|
||||
air, // pressure [bar] forward, flow back
|
||||
brake_pipe, // pressure [bar] forward, flow back
|
||||
feed_pipe, // pressure [bar] forward, flow back
|
||||
count_
|
||||
};
|
||||
|
||||
enum class value_type : std::uint8_t
|
||||
{
|
||||
boolean,
|
||||
integer,
|
||||
real
|
||||
};
|
||||
|
||||
enum class port_direction : std::uint8_t
|
||||
{
|
||||
in,
|
||||
out
|
||||
};
|
||||
|
||||
struct domain_info
|
||||
{
|
||||
std::string_view id;
|
||||
std::string_view forward; // quantity passed from the output towards the inputs
|
||||
std::string_view forward_unit;
|
||||
std::string_view back; // quantity passed from the inputs back towards the output
|
||||
std::string_view back_unit;
|
||||
bool physical;
|
||||
};
|
||||
|
||||
domain_info const &info(domain Domain);
|
||||
bool parse_domain(std::string_view Text, domain &Domain);
|
||||
std::string_view to_string(value_type Type);
|
||||
|
||||
enum class param_type : std::uint8_t
|
||||
{
|
||||
number,
|
||||
boolean,
|
||||
text,
|
||||
choice
|
||||
};
|
||||
|
||||
std::string_view to_string(param_type Type);
|
||||
|
||||
// value of a block parameter, as read from the file or taken from the catalog default
|
||||
struct param_value
|
||||
{
|
||||
param_type type{param_type::number};
|
||||
double number{0.0};
|
||||
bool boolean{false};
|
||||
std::string text; // also holds the choice id
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
struct port_def
|
||||
{
|
||||
std::string key;
|
||||
port_direction direction{port_direction::in};
|
||||
domain dom{domain::signal};
|
||||
value_type type{value_type::real}; // signal ports only
|
||||
std::string unit;
|
||||
bool optional{false}; // inputs only; unconnected required inputs are reported as warnings
|
||||
double default_value{0.0}; // signal inputs only; value seen by the block when the input isn't connected
|
||||
};
|
||||
|
||||
struct param_def
|
||||
{
|
||||
std::string key;
|
||||
param_type type{param_type::number};
|
||||
std::string unit;
|
||||
param_value default_value;
|
||||
std::vector<std::string> choices; // choice parameters only, stable ascii ids
|
||||
};
|
||||
|
||||
enum class severity : std::uint8_t
|
||||
{
|
||||
warning,
|
||||
error
|
||||
};
|
||||
|
||||
// problem found while loading or compiling a graph. the node is empty for problems of the whole file
|
||||
struct diagnostic
|
||||
{
|
||||
severity level{severity::error};
|
||||
std::string node;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
bool has_errors(std::vector<diagnostic> const &Diagnostics);
|
||||
std::string to_string(diagnostic const &Diagnostic);
|
||||
|
||||
} // namespace vgraph
|
||||
Reference in New Issue
Block a user