17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-01 21:39:16 +01:00

hardware protocol v2: frame layer and shared symbol registry

Adds the transport-independent core of the MaSzyna Hardware Protocol v2:
COBS framing with a CRC32C protected 24 byte header, bounds checked
payload readers and writers, and a resynchronising stream decoder.

The shared TrainState registry describes simulator state in engineering
units with per-value quality, the command registry generates the command
namespace from Commands_descriptions, and the symbol registry merges both
into the public, name-addressed hardware api.
This commit is contained in:
2026-09-05 20:22:08 +00:00
parent 7a469190ca
commit 7cb6f789f5
10 changed files with 1529 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
/*
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 "hardware/registry/command_registry.h"
namespace hardware
{
namespace
{
struct command_metadata
{
char const *name;
bool continuous;
std::optional<double> minimum;
std::optional<double> maximum;
};
// hardware metadata for the commands which carry an analogue value; everything else is an event command
command_metadata const continuous_commands[] = {
{ "mastercontrollerset", true, 0.0, std::nullopt },
{ "secondcontrollerset", true, 0.0, std::nullopt },
{ "jointcontrollerset", true, 0.0, std::nullopt },
{ "trainbrakeset", true, 0.0, 1.0 },
{ "independentbrakeset", true, 0.0, 1.0 },
{ "dynamicbrakecontrollerset", true, 0.0, 1.0 },
{ "radiovolumeset", true, 0.0, 1.0 },
{ "radiochannelset", true, 1.0, 15.0 }
};
} // anonymous namespace
command_registry const &command_registry::instance()
{
static command_registry const registry;
return registry;
}
command_descriptor const *command_registry::find( std::string const &Name ) const
{
auto const lookup = m_index.find( Name );
return ( lookup != m_index.end() ? &m_entries[ lookup->second ] : nullptr );
}
command_registry::command_registry()
{
std::size_t commandid = 0;
for( auto const &description : simulation::Commands_descriptions )
{
command_descriptor descriptor;
descriptor.name = description.name;
descriptor.command = static_cast<user_command>( commandid );
m_index.emplace( descriptor.name, m_entries.size() );
m_entries.emplace_back( std::move( descriptor ) );
++commandid;
}
for( auto const &metadata : continuous_commands )
{
auto const lookup = m_index.find( metadata.name );
if( lookup == m_index.end() )
{
continue;
}
auto &descriptor = m_entries[ lookup->second ];
descriptor.continuous = metadata.continuous;
descriptor.minimum = metadata.minimum;
descriptor.maximum = metadata.maximum;
}
// protocol level alias: master controller driven with a 0...1 fraction of its range, the way
// uart v1 does it in percentage mode. it maps onto the regular mastercontrollerset command
auto const mastercontroller = m_index.find( "mastercontrollerset" );
if( mastercontroller != m_index.end() )
{
command_descriptor descriptor;
descriptor.name = "mastercontrollersetpercent";
descriptor.command = m_entries[ mastercontroller->second ].command;
descriptor.continuous = true;
descriptor.minimum = 0.0;
descriptor.maximum = 1.0;
descriptor.transform = command_transform::mastercontroller_percent;
m_index.emplace( descriptor.name, m_entries.size() );
m_entries.emplace_back( std::move( descriptor ) );
}
}
} // namespace hardware

View File

@@ -0,0 +1,67 @@
/*
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 <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "hardware/protocol/protocol_defs.h"
#include "input/command.h"
// command namespace of the hardware api
// the list is generated from simulation::Commands_descriptions, with hardware metadata added on top,
// so that external devices never depend on the numeric values of the user_command enumeration
namespace hardware
{
// optional conversion applied to the value received from the device before the command is posted
enum class command_transform : std::uint8_t
{
none = 0,
// 0...1 fraction of the master controller range of the occupied vehicle
mastercontroller_percent = 1
};
struct command_descriptor
{
// registry name without namespace prefix, i.e. "trainbrakeset" rather than "command.trainbrakeset"
std::string name;
user_command command = user_command::none;
// true for controls which may be driven with CONTROL_SET, false for event-only commands
bool continuous = false;
value_type parameter_type = value_type::float32;
std::optional<double> minimum;
std::optional<double> maximum;
command_transform transform = command_transform::none;
};
class command_registry
{
public:
// methods
static command_registry const &instance();
std::vector<command_descriptor> const &entries() const { return m_entries; }
command_descriptor const *find( std::string const &Name ) const;
private:
// methods
command_registry();
// members
std::vector<command_descriptor> m_entries;
std::unordered_map<std::string, std::size_t> m_index;
};
} // namespace hardware

View File

@@ -0,0 +1,69 @@
/*
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 "hardware/registry/hardware_symbol_registry.h"
#include "hardware/registry/command_registry.h"
#include "hardware/registry/state_registry.h"
namespace hardware
{
symbol_registry const &symbol_registry::instance()
{
static symbol_registry const registry;
return registry;
}
std::size_t symbol_registry::find( std::string const &Name ) const
{
auto const lookup = m_index.find( Name );
return ( lookup != m_index.end() ? lookup->second : npos );
}
symbol_registry::symbol_registry()
{
auto const &states = state_registry::instance().entries();
m_entries.reserve( states.size() + command_registry::instance().entries().size() );
for( std::size_t index = 0; index < states.size(); ++index )
{
auto const &state = states[ index ];
symbol_descriptor descriptor;
descriptor.name = "state." + state.name;
descriptor.kind = symbol_kind::state;
descriptor.type = state.type;
descriptor.access = access_read;
descriptor.unit = state.unit;
descriptor.minimum = state.minimum;
descriptor.maximum = state.maximum;
descriptor.source_index = index;
m_index.emplace( descriptor.name, m_entries.size() );
m_entries.emplace_back( std::move( descriptor ) );
}
auto const &commands = command_registry::instance().entries();
for( std::size_t index = 0; index < commands.size(); ++index )
{
auto const &command = commands[ index ];
symbol_descriptor descriptor;
descriptor.name = "command." + command.name;
descriptor.kind = symbol_kind::command;
descriptor.type = command.parameter_type;
descriptor.access = access_write;
descriptor.minimum = command.minimum;
descriptor.maximum = command.maximum;
descriptor.source_index = index;
m_index.emplace( descriptor.name, m_entries.size() );
m_entries.emplace_back( std::move( descriptor ) );
}
}
} // namespace hardware

View File

@@ -0,0 +1,64 @@
/*
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 <limits>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "hardware/protocol/protocol_defs.h"
// public symbol table of the hardware api
// merges the state and command registries into one namespace-prefixed list; devices refer to symbols
// by these names and receive short, session scoped handles in return
namespace hardware
{
struct symbol_descriptor
{
// full public name, i.e. "state.velocity" or "command.trainbrakeset"
std::string name;
symbol_kind kind = symbol_kind::state;
value_type type = value_type::float32;
std::uint8_t access = access_read;
std::string unit;
std::optional<double> minimum;
std::optional<double> maximum;
// index into the registry the symbol originates from
std::size_t source_index = 0;
};
class symbol_registry
{
public:
// types
static constexpr std::size_t npos = std::numeric_limits<std::size_t>::max();
// methods
static symbol_registry const &instance();
std::vector<symbol_descriptor> const &entries() const { return m_entries; }
// returns index of the symbol, or npos if the name isn't known
std::size_t find( std::string const &Name ) const;
private:
// methods
symbol_registry();
// members
std::vector<symbol_descriptor> m_entries;
std::unordered_map<std::string, std::size_t> m_index;
};
} // namespace hardware

View File

@@ -0,0 +1,170 @@
/*
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 "hardware/registry/state_registry.h"
#include "simulation/simulation.h"
#include "simulation/simulationtime.h"
#include "utilities/Globals.h"
namespace hardware
{
namespace
{
state_value make_value( state_snapshot const &Snapshot, value_type const Type, double const Value, bool const Applicable = true )
{
state_value result;
result.type = Type;
result.numeric = Value;
result.quality = (
false == Snapshot.available ? value_quality::unavailable :
false == Applicable ? value_quality::not_applicable :
value_quality::valid );
if( result.quality != value_quality::valid )
{
result.numeric = 0.0;
}
return result;
}
// values which don't depend on the vehicle are always valid
state_value make_global_value( value_type const Type, double const Value )
{
state_value result;
result.type = Type;
result.numeric = Value;
result.quality = value_quality::valid;
return result;
}
} // anonymous namespace
state_registry const &state_registry::instance()
{
static state_registry const registry;
return registry;
}
state_descriptor const *state_registry::find( std::string const &Name ) const
{
auto const lookup = m_index.find( Name );
return ( lookup != m_index.end() ? &m_entries[ lookup->second ] : nullptr );
}
void state_registry::add( std::string Name, value_type const Type, std::string Unit, std::optional<double> const Minimum, std::optional<double> const Maximum, std::function<state_value( state_snapshot const & )> Getter )
{
m_index.emplace( Name, m_entries.size() );
state_descriptor descriptor;
descriptor.name = std::move( Name );
descriptor.type = Type;
descriptor.unit = std::move( Unit );
descriptor.minimum = Minimum;
descriptor.maximum = Maximum;
descriptor.getter = std::move( Getter );
m_entries.emplace_back( std::move( descriptor ) );
}
state_registry::state_registry()
{
// indicators and switch states
add( "shp", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.shp != 0 ); } );
add( "alerter", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.alerter != 0 ); } );
add( "alerter_sound", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.alerter_sound != 0 ); } );
add( "radio_stop", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.radio_stop != 0 ); } );
add( "motor_resistors", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.motor_resistors != 0 ); } );
add( "line_breaker", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.line_breaker != 0 ); } );
add( "ground_relay", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.ground_relay != 0 ); } );
add( "motor_overload", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.motor_overload != 0 ); } );
add( "motor_overload_threshold", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.motor_overload_threshold != 0 ); } );
add( "motor_connectors", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.motor_connectors != 0 ); } );
add( "wheelslip", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.wheelslip != 0 ); } );
add( "converter_overload", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.converter_overload != 0 ); } );
add( "converter_off", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.converter_off != 0 ); } );
add( "compressor_overload", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.compressor_overload != 0 ); } );
add( "ventilator_overload", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.ventilator_overload != 0 ); } );
add( "train_heating", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.train_heating != 0 ); } );
add( "coupled_hv_voltage_relays", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.coupled_hv_voltage_relays != 0 ); } );
add( "recorder_braking", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.recorder_braking != 0 ); } );
add( "recorder_power", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.recorder_power != 0 ); } );
add( "springbrake_active", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.springbrake_active != 0 ); } );
add( "epbrake_enabled", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.epbrake_enabled != 0 ); } );
add( "emergencybrake", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.emergencybrake != 0 ); } );
add( "lockpipe", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.lockpipe != 0 ); } );
add( "battery", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.battery != 0 ); } );
add( "radiomessageindicator", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.radiomessageindicator ); } );
add( "dir_forward", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.dir_forward != 0 ); } );
add( "dir_backward", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.dir_backward != 0 ); } );
add( "doorleftallowed", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.doorleftallowed != 0 ); } );
add( "doorleftopened", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.doorleftopened != 0 ); } );
add( "doorrightallowed", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.doorrightallowed != 0 ); } );
add( "doorrightopened", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.doorrightopened != 0 ); } );
add( "doorstepallowed", value_type::boolean, "", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::boolean, s.train.doorstepallowed != 0 ); } );
// enumerations and counters
add( "cab", value_type::uint8, "", 0.0, 2.0, []( state_snapshot const &s ) { return make_value( s, value_type::uint8, s.train.cab ); } );
add( "cab_indicator", value_type::uint8, "", 0.0, 1.0, []( state_snapshot const &s ) { return make_value( s, value_type::uint8, s.cab_indicator ); } );
add( "radio_channel", value_type::uint8, "", 0.0, 15.0, []( state_snapshot const &s ) { return make_value( s, value_type::uint8, s.train.radio_channel ); } );
// measurements, in engineering units
add( "velocity", value_type::float32, "km/h", 0.0, 400.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, Global.iPause ? 0.0 : s.train.velocity ); } );
add( "reservoir_pressure", value_type::float32, "bar", 0.0, 16.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.reservoir_pressure ); } );
add( "pipe_pressure", value_type::float32, "bar", 0.0, 16.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.pipe_pressure ); } );
add( "brake_pressure", value_type::float32, "bar", 0.0, 16.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.brake_pressure ); } );
add( "pantograph_pressure", value_type::float32, "bar", 0.0, 16.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.pantograph_pressure, s.pantographs ); } );
add( "hv_voltage", value_type::float32, "V", 0.0, 40000.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.hv_voltage, s.electric ); } );
add( "hv_current_1", value_type::float32, "A", -5000.0, 5000.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.hv_current[ 0 ], s.electric ); } );
add( "hv_current_2", value_type::float32, "A", -5000.0, 5000.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.hv_current[ 1 ], s.electric ); } );
add( "hv_current_3", value_type::float32, "A", -5000.0, 5000.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.hv_current[ 2 ], s.electric ); } );
add( "lv_voltage", value_type::float32, "V", 0.0, 200.0, []( state_snapshot const &s ) { return make_value( s, value_type::float32, s.train.lv_voltage ); } );
add( "distance", value_type::float64, "m", {}, {}, []( state_snapshot const &s ) { return make_value( s, value_type::float64, s.train.distance ); } );
// simulation clock, available regardless of the occupied vehicle
add( "time_month_of_era", value_type::uint32, "", {}, {}, []( state_snapshot const &s ) { return make_global_value( value_type::uint32, s.time_month_of_era ); } );
add( "time_minute_of_month", value_type::uint32, "", {}, {}, []( state_snapshot const &s ) { return make_global_value( value_type::uint32, s.time_minute_of_month ); } );
add( "time_millisecond_of_day", value_type::uint32, "", {}, {}, []( state_snapshot const &s ) { return make_global_value( value_type::uint32, s.time_millisecond_of_day ); } );
}
void state_registry::capture( state_snapshot &Snapshot )
{
SYSTEMTIME const time = simulation::Time.data();
Snapshot.time_month_of_era = ( time.wYear - 1 ) * 12 + time.wMonth - 1;
Snapshot.time_minute_of_month = ( time.wDay - 1 ) * 1440 + time.wHour * 60 + time.wMinute;
Snapshot.time_millisecond_of_day = time.wSecond * 1000 + time.wMilliseconds;
auto const *train = simulation::Train;
if( train == nullptr )
{
Snapshot.available = false;
Snapshot.electric = false;
Snapshot.pantographs = false;
Snapshot.context_id = 0;
return;
}
Snapshot.available = true;
Snapshot.train = train->get_state();
if( Snapshot.train.cab > 0 )
{
// NOTE: moving from a cab to the engine room doesn't change the cab indicator
Snapshot.cab_indicator = Snapshot.train.cab - 1;
}
auto const *controlled = train->Controlled();
auto const enginetype = ( controlled != nullptr ? controlled->EngineType : TEngineType::None );
Snapshot.electric = ( ( enginetype == TEngineType::ElectricSeriesMotor ) || ( enginetype == TEngineType::ElectricInductionMotor ) );
Snapshot.pantographs = Snapshot.electric;
Snapshot.context_id = static_cast<std::uint32_t>( std::hash<std::string>()( train->name() ) );
}
} // namespace hardware

View File

@@ -0,0 +1,89 @@
/*
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 <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "hardware/protocol/protocol_defs.h"
#include "vehicle/Train.h"
// shared TrainState registry
// one description of the simulator state exposed to the outside world, meant to be used by every
// external interface (hardware protocol v2, zmq, python) instead of each of them keeping its own list
namespace hardware
{
// values captured once per simulation frame, so that all consumers see a consistent set
struct state_snapshot
{
// false when there's no occupied vehicle; state symbols then report UNAVAILABLE
bool available = false;
// high voltage circuit is meaningful for this vehicle
bool electric = false;
// vehicle is equipped with pantographs
bool pantographs = false;
TTrain::state_t train {};
// last active cab, 0: front, 1: rear. moving to the engine room doesn't change it
std::uint8_t cab_indicator = 0;
// changes whenever the controlled vehicle changes, used to notify devices about metadata changes
std::uint32_t context_id = 0;
double time_month_of_era = 0.0;
double time_minute_of_month = 0.0;
double time_millisecond_of_day = 0.0;
};
struct state_value
{
value_type type = value_type::float32;
value_quality quality = value_quality::unavailable;
double numeric = 0.0;
std::string text;
};
struct state_descriptor
{
// registry name without namespace prefix, i.e. "velocity" rather than "state.velocity"
std::string name;
value_type type = value_type::float32;
std::string unit;
std::optional<double> minimum;
std::optional<double> maximum;
std::function<state_value( state_snapshot const & )> getter;
};
class state_registry
{
public:
// methods
static state_registry const &instance();
std::vector<state_descriptor> const &entries() const { return m_entries; }
state_descriptor const *find( std::string const &Name ) const;
// fills the snapshot from the currently occupied train; simulation thread only
static void capture( state_snapshot &Snapshot );
private:
// methods
state_registry();
void add( std::string Name, value_type const Type, std::string Unit, std::optional<double> const Minimum, std::optional<double> const Maximum, std::function<state_value( state_snapshot const & )> Getter );
// members
std::vector<state_descriptor> m_entries;
std::unordered_map<std::string, std::size_t> m_index;
};
} // namespace hardware