17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 05:09:20 +02: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,457 @@
/*
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/protocol/protocol_codec.h"
#include <cstring>
namespace hardware
{
namespace
{
// castagnoli polynomial in reflected form
constexpr std::uint32_t crc32c_polynomial = 0x82F63B78u;
std::array<std::uint32_t, 256> const &crc32c_table()
{
static std::array<std::uint32_t, 256> const table = []()
{
std::array<std::uint32_t, 256> result {};
for( std::uint32_t index = 0; index < 256; ++index )
{
std::uint32_t value = index;
for( int bit = 0; bit < 8; ++bit )
{
value = ( value & 1 ? ( value >> 1 ) ^ crc32c_polynomial : value >> 1 );
}
result[ index ] = value;
}
return result;
}();
return table;
}
void append_uint16( std::vector<std::uint8_t> &Buffer, std::uint16_t const Value )
{
Buffer.emplace_back( static_cast<std::uint8_t>( Value ) );
Buffer.emplace_back( static_cast<std::uint8_t>( Value >> 8 ) );
}
void append_uint32( std::vector<std::uint8_t> &Buffer, std::uint32_t const Value )
{
Buffer.emplace_back( static_cast<std::uint8_t>( Value ) );
Buffer.emplace_back( static_cast<std::uint8_t>( Value >> 8 ) );
Buffer.emplace_back( static_cast<std::uint8_t>( Value >> 16 ) );
Buffer.emplace_back( static_cast<std::uint8_t>( Value >> 24 ) );
}
std::uint16_t extract_uint16( std::uint8_t const *Data )
{
return static_cast<std::uint16_t>( static_cast<std::uint16_t>( Data[ 0 ] ) | static_cast<std::uint16_t>( Data[ 1 ] ) << 8 );
}
std::uint32_t extract_uint32( std::uint8_t const *Data )
{
return static_cast<std::uint32_t>( Data[ 0 ] ) | static_cast<std::uint32_t>( Data[ 1 ] ) << 8 | static_cast<std::uint32_t>( Data[ 2 ] ) << 16 | static_cast<std::uint32_t>( Data[ 3 ] ) << 24;
}
} // anonymous namespace
std::uint32_t crc32c( std::uint8_t const *Data, std::size_t const Size, std::uint32_t const Seed )
{
auto const &table = crc32c_table();
std::uint32_t crc = ~Seed;
for( std::size_t index = 0; index < Size; ++index )
{
crc = table[ ( crc ^ Data[ index ] ) & 0xFF ] ^ ( crc >> 8 );
}
return ~crc;
}
void cobs_encode( std::uint8_t const *Data, std::size_t const Size, std::vector<std::uint8_t> &Output )
{
Output.clear();
// the first byte holds the distance to the first zero, it's patched once that distance is known
std::size_t codeposition = 0;
Output.emplace_back( 0 );
std::uint8_t code = 1;
for( std::size_t index = 0; index < Size; ++index )
{
if( Data[ index ] != 0 )
{
Output.emplace_back( Data[ index ] );
++code;
if( code != 0xFF )
{
continue;
}
}
Output[ codeposition ] = code;
codeposition = Output.size();
Output.emplace_back( 0 );
code = 1;
}
Output[ codeposition ] = code;
}
bool cobs_decode( std::uint8_t const *Data, std::size_t const Size, std::vector<std::uint8_t> &Output )
{
Output.clear();
std::size_t index = 0;
while( index < Size )
{
std::uint8_t const code = Data[ index ];
if( code == 0 )
{
// a zero byte can never appear inside an encoded block
return false;
}
++index;
std::size_t const runlength = static_cast<std::size_t>( code ) - 1;
if( index + runlength > Size )
{
return false;
}
Output.insert( Output.end(), Data + index, Data + index + runlength );
index += runlength;
if( ( code != 0xFF ) && ( index < Size ) )
{
Output.emplace_back( 0 );
}
}
return true;
}
void encode_frame( packet_header const &Header, std::uint8_t const *Payload, std::size_t const PayloadSize, std::vector<std::uint8_t> &Output )
{
std::vector<std::uint8_t> packet;
packet.reserve( protocol_header_size + PayloadSize + protocol_crc_size );
append_uint16( packet, protocol_magic );
packet.emplace_back( Header.protocol_major );
packet.emplace_back( Header.protocol_minor );
packet.emplace_back( Header.flags );
packet.emplace_back( static_cast<std::uint8_t>( protocol_header_size ) );
append_uint16( packet, static_cast<std::uint16_t>( Header.type ) );
append_uint32( packet, Header.session_id );
append_uint32( packet, Header.sequence );
append_uint32( packet, Header.timestamp_ms );
append_uint16( packet, static_cast<std::uint16_t>( PayloadSize ) );
append_uint16( packet, 0 );
if( PayloadSize > 0 )
{
packet.insert( packet.end(), Payload, Payload + PayloadSize );
}
append_uint32( packet, crc32c( packet.data(), packet.size() ) );
cobs_encode( packet.data(), packet.size(), Output );
Output.emplace_back( 0 );
}
void payload_writer::write_bool( bool const Value )
{
m_data.emplace_back( Value ? 1 : 0 );
}
void payload_writer::write_uint8( std::uint8_t const Value )
{
m_data.emplace_back( Value );
}
void payload_writer::write_int8( std::int8_t const Value )
{
m_data.emplace_back( static_cast<std::uint8_t>( Value ) );
}
void payload_writer::write_uint16( std::uint16_t const Value )
{
append_uint16( m_data, Value );
}
void payload_writer::write_int16( std::int16_t const Value )
{
append_uint16( m_data, static_cast<std::uint16_t>( Value ) );
}
void payload_writer::write_uint32( std::uint32_t const Value )
{
append_uint32( m_data, Value );
}
void payload_writer::write_int32( std::int32_t const Value )
{
append_uint32( m_data, static_cast<std::uint32_t>( Value ) );
}
void payload_writer::write_uint64( std::uint64_t const Value )
{
append_uint32( m_data, static_cast<std::uint32_t>( Value ) );
append_uint32( m_data, static_cast<std::uint32_t>( Value >> 32 ) );
}
void payload_writer::write_int64( std::int64_t const Value )
{
write_uint64( static_cast<std::uint64_t>( Value ) );
}
void payload_writer::write_float32( float const Value )
{
std::uint32_t bits = 0;
std::memcpy( &bits, &Value, sizeof( bits ) );
append_uint32( m_data, bits );
}
void payload_writer::write_float64( double const Value )
{
std::uint64_t bits = 0;
std::memcpy( &bits, &Value, sizeof( bits ) );
write_uint64( bits );
}
void payload_writer::write_string( std::string const &Value )
{
auto const length = std::min( Value.size(), protocol_string_max );
m_data.emplace_back( static_cast<std::uint8_t>( length ) );
m_data.insert( m_data.end(), Value.begin(), Value.begin() + length );
}
void payload_writer::write_bytes( std::uint8_t const *Data, std::size_t const Size )
{
m_data.insert( m_data.end(), Data, Data + Size );
}
void payload_writer::patch_uint16( std::size_t const Offset, std::uint16_t const Value )
{
if( Offset + 1 >= m_data.size() )
{
return;
}
m_data[ Offset ] = static_cast<std::uint8_t>( Value );
m_data[ Offset + 1 ] = static_cast<std::uint8_t>( Value >> 8 );
}
bool payload_reader::require( std::size_t const Size )
{
if( m_error )
{
return false;
}
if( m_position + Size > m_size )
{
m_error = true;
return false;
}
return true;
}
bool payload_reader::read_bool()
{
return read_uint8() != 0;
}
std::uint8_t payload_reader::read_uint8()
{
if( false == require( 1 ) )
{
return 0;
}
return m_data[ m_position++ ];
}
std::int8_t payload_reader::read_int8()
{
return static_cast<std::int8_t>( read_uint8() );
}
std::uint16_t payload_reader::read_uint16()
{
if( false == require( 2 ) )
{
return 0;
}
auto const value = extract_uint16( m_data + m_position );
m_position += 2;
return value;
}
std::int16_t payload_reader::read_int16()
{
return static_cast<std::int16_t>( read_uint16() );
}
std::uint32_t payload_reader::read_uint32()
{
if( false == require( 4 ) )
{
return 0;
}
auto const value = extract_uint32( m_data + m_position );
m_position += 4;
return value;
}
std::int32_t payload_reader::read_int32()
{
return static_cast<std::int32_t>( read_uint32() );
}
std::uint64_t payload_reader::read_uint64()
{
std::uint64_t const low = read_uint32();
std::uint64_t const high = read_uint32();
return low | high << 32;
}
std::int64_t payload_reader::read_int64()
{
return static_cast<std::int64_t>( read_uint64() );
}
float payload_reader::read_float32()
{
std::uint32_t const bits = read_uint32();
float value = 0.0f;
std::memcpy( &value, &bits, sizeof( value ) );
return value;
}
double payload_reader::read_float64()
{
std::uint64_t const bits = read_uint64();
double value = 0.0;
std::memcpy( &value, &bits, sizeof( value ) );
return value;
}
std::string payload_reader::read_string( std::size_t const Sizelimit )
{
std::size_t const length = read_uint8();
if( length > Sizelimit )
{
m_error = true;
return {};
}
if( false == require( length ) )
{
return {};
}
std::string value( reinterpret_cast<char const *>( m_data + m_position ), length );
m_position += length;
return value;
}
void frame_decoder::reset()
{
m_buffer.clear();
m_overrun = false;
}
void frame_decoder::set_frame_size_limit( std::size_t const Limit )
{
m_framesizelimit = std::clamp<std::size_t>( Limit, protocol_frame_size_min, protocol_frame_size_max );
}
void frame_decoder::feed( std::uint8_t const *Data, std::size_t const Size, std::vector<decoded_packet> &Output )
{
// worst case cobs expansion, plus the code byte
std::size_t const encodedlimit = m_framesizelimit + m_framesizelimit / 254 + 2;
for( std::size_t index = 0; index < Size; ++index )
{
std::uint8_t const byte = Data[ index ];
if( byte == 0 )
{
// frame delimiter; anything gathered so far is either a frame or debris to be dropped
if( true == m_overrun )
{
m_overrun = false;
}
else if( false == m_buffer.empty() )
{
if( true == cobs_decode( m_buffer.data(), m_buffer.size(), m_scratch ) )
{
if( m_scratch.size() < protocol_header_size + protocol_crc_size )
{
++m_stats.length_errors;
}
else if( extract_uint16( m_scratch.data() ) != protocol_magic )
{
++m_stats.frame_errors;
}
else
{
std::size_t const headersize = m_scratch[ 5 ];
std::size_t const payloadlength = extract_uint16( m_scratch.data() + 20 );
if( ( headersize < protocol_header_size ) || ( headersize + payloadlength + protocol_crc_size != m_scratch.size() ) )
{
++m_stats.length_errors;
}
else
{
std::uint32_t const expectedcrc = extract_uint32( m_scratch.data() + m_scratch.size() - protocol_crc_size );
if( expectedcrc != crc32c( m_scratch.data(), m_scratch.size() - protocol_crc_size ) )
{
++m_stats.crc_errors;
}
else
{
decoded_packet packet;
packet.header.protocol_major = m_scratch[ 2 ];
packet.header.protocol_minor = m_scratch[ 3 ];
packet.header.flags = m_scratch[ 4 ];
packet.header.type = static_cast<message_type>( extract_uint16( m_scratch.data() + 6 ) );
packet.header.session_id = extract_uint32( m_scratch.data() + 8 );
packet.header.sequence = extract_uint32( m_scratch.data() + 12 );
packet.header.timestamp_ms = extract_uint32( m_scratch.data() + 16 );
packet.header.payload_length = static_cast<std::uint16_t>( payloadlength );
packet.payload.assign( m_scratch.begin() + headersize, m_scratch.begin() + headersize + payloadlength );
++m_stats.frames;
Output.emplace_back( std::move( packet ) );
}
}
}
}
else
{
++m_stats.frame_errors;
}
}
m_buffer.clear();
continue;
}
if( true == m_overrun )
{
// still waiting for the delimiter which ends the oversized frame
continue;
}
m_buffer.emplace_back( byte );
if( m_buffer.size() > encodedlimit )
{
++m_stats.length_errors;
m_overrun = true;
m_buffer.clear();
}
}
}
} // namespace hardware

View File

@@ -0,0 +1,156 @@
/*
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 <vector>
#include "hardware/protocol/protocol_defs.h"
// MaSzyna Hardware Protocol v2, frame layer
// raw packet := header | payload | crc32c, transmitted as COBS( packet ) followed by a 0x00 delimiter
namespace hardware
{
struct packet_header
{
std::uint8_t protocol_major = protocol_version_major;
std::uint8_t protocol_minor = protocol_version_minor;
std::uint8_t flags = packetflag_none;
message_type type = message_type::invalid;
std::uint32_t session_id = 0;
std::uint32_t sequence = 0;
std::uint32_t timestamp_ms = 0;
std::uint16_t payload_length = 0;
};
struct decoded_packet
{
packet_header header;
std::vector<std::uint8_t> payload;
};
// crc32c (castagnoli), the checksum protecting header and payload
std::uint32_t crc32c( std::uint8_t const *Data, std::size_t const Size, std::uint32_t const Seed = 0 );
// consistent overhead byte stuffing; the encoded output never contains a zero byte
void cobs_encode( std::uint8_t const *Data, std::size_t const Size, std::vector<std::uint8_t> &Output );
// returns false if the input isn't a valid cobs sequence
bool cobs_decode( std::uint8_t const *Data, std::size_t const Size, std::vector<std::uint8_t> &Output );
// builds a complete, ready to transmit frame including the trailing delimiter
void encode_frame( packet_header const &Header, std::uint8_t const *Payload, std::size_t const PayloadSize, std::vector<std::uint8_t> &Output );
// bounds checked payload composer; all integers are little-endian, floats are ieee 754
class payload_writer
{
public:
// methods
void write_bool( bool const Value );
void write_uint8( std::uint8_t const Value );
void write_int8( std::int8_t const Value );
void write_uint16( std::uint16_t const Value );
void write_int16( std::int16_t const Value );
void write_uint32( std::uint32_t const Value );
void write_int32( std::int32_t const Value );
void write_uint64( std::uint64_t const Value );
void write_int64( std::int64_t const Value );
void write_float32( float const Value );
void write_float64( double const Value );
// length prefixed (uint8) utf-8 text, truncated to protocol_string_max
void write_string( std::string const &Value );
void write_bytes( std::uint8_t const *Data, std::size_t const Size );
void clear() { m_data.clear(); }
std::size_t size() const { return m_data.size(); }
std::uint8_t const *data() const { return m_data.data(); }
std::vector<std::uint8_t> const &buffer() const { return m_data; }
// lets the caller patch a count written before the items were emitted
void patch_uint16( std::size_t const Offset, std::uint16_t const Value );
private:
// members
std::vector<std::uint8_t> m_data;
};
// bounds checked payload parser; every read past the end sets the error flag and yields a zeroed result
class payload_reader
{
public:
// methods
payload_reader( std::uint8_t const *Data, std::size_t const Size ) : m_data( Data ), m_size( Size ) {}
explicit payload_reader( std::vector<std::uint8_t> const &Data ) : m_data( Data.data() ), m_size( Data.size() ) {}
bool read_bool();
std::uint8_t read_uint8();
std::int8_t read_int8();
std::uint16_t read_uint16();
std::int16_t read_int16();
std::uint32_t read_uint32();
std::int32_t read_int32();
std::uint64_t read_uint64();
std::int64_t read_int64();
float read_float32();
double read_float64();
std::string read_string( std::size_t const Sizelimit = protocol_string_max );
bool ok() const { return !m_error; }
bool exhausted() const { return m_position >= m_size; }
std::size_t remaining() const { return ( m_position < m_size ? m_size - m_position : 0 ); }
void fail() { m_error = true; }
private:
// methods
bool require( std::size_t const Size );
// members
std::uint8_t const *m_data = nullptr;
std::size_t m_size = 0;
std::size_t m_position = 0;
bool m_error = false;
};
// stream decoder; recovers from arbitrary corruption at the next delimiter
class frame_decoder
{
public:
// types
struct counters
{
std::uint64_t frames = 0;
std::uint64_t crc_errors = 0;
std::uint64_t frame_errors = 0;
std::uint64_t length_errors = 0;
std::uint64_t version_errors = 0;
};
// methods
void reset();
void set_frame_size_limit( std::size_t const Limit );
// consumes raw bytes, appends every complete and valid packet to Output
void feed( std::uint8_t const *Data, std::size_t const Size, std::vector<decoded_packet> &Output );
counters const &stats() const { return m_stats; }
private:
// members
std::vector<std::uint8_t> m_buffer;
std::vector<std::uint8_t> m_scratch;
std::size_t m_framesizelimit = protocol_frame_size_default;
bool m_overrun = false;
counters m_stats;
};
} // namespace hardware

View File

@@ -0,0 +1,138 @@
/*
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/protocol/protocol_defs.h"
namespace hardware
{
char const *to_string( message_type const Type )
{
switch( Type )
{
case message_type::hello: return "HELLO";
case message_type::welcome: return "WELCOME";
case message_type::heartbeat: return "HEARTBEAT";
case message_type::ack: return "ACK";
case message_type::nack: return "NACK";
case message_type::ping: return "PING";
case message_type::pong: return "PONG";
case message_type::device_ready: return "DEVICE_READY";
case message_type::device_info: return "DEVICE_INFO";
case message_type::device_diagnostics: return "DEVICE_DIAGNOSTICS";
case message_type::resolve_symbols: return "RESOLVE_SYMBOLS";
case message_type::symbols_resolved: return "SYMBOLS_RESOLVED";
case message_type::query_symbol: return "QUERY_SYMBOL";
case message_type::symbol_info: return "SYMBOL_INFO";
case message_type::symbol_context_changed: return "SYMBOL_CONTEXT_CHANGED";
case message_type::subscribe: return "SUBSCRIBE";
case message_type::subscribe_result: return "SUBSCRIBE_RESULT";
case message_type::unsubscribe: return "UNSUBSCRIBE";
case message_type::state_update: return "STATE_UPDATE";
case message_type::command_event: return "COMMAND_EVENT";
case message_type::control_set: return "CONTROL_SET";
case message_type::control_claim: return "CONTROL_CLAIM";
case message_type::control_claim_result: return "CONTROL_CLAIM_RESULT";
case message_type::control_release: return "CONTROL_RELEASE";
case message_type::property_read: return "PROPERTY_READ";
case message_type::property_value: return "PROPERTY_VALUE";
case message_type::property_write: return "PROPERTY_WRITE";
default: return "UNKNOWN";
}
}
char const *to_string( error_code const Code )
{
switch( Code )
{
case error_code::none: return "NONE";
case error_code::unsupported_version: return "UNSUPPORTED_VERSION";
case error_code::invalid_session: return "INVALID_SESSION";
case error_code::invalid_frame: return "INVALID_FRAME";
case error_code::invalid_length: return "INVALID_LENGTH";
case error_code::invalid_field: return "INVALID_FIELD";
case error_code::unknown_message: return "UNKNOWN_MESSAGE";
case error_code::unknown_symbol: return "UNKNOWN_SYMBOL";
case error_code::unknown_handle: return "UNKNOWN_HANDLE";
case error_code::access_denied: return "ACCESS_DENIED";
case error_code::out_of_range: return "OUT_OF_RANGE";
case error_code::wrong_type: return "WRONG_TYPE";
case error_code::not_ready: return "NOT_READY";
case error_code::busy: return "BUSY";
case error_code::control_not_owned: return "CONTROL_NOT_OWNED";
case error_code::rate_limited: return "RATE_LIMITED";
case error_code::unsupported_operation: return "UNSUPPORTED_OPERATION";
default: return "INTERNAL_ERROR";
}
}
char const *to_string( value_quality const Quality )
{
switch( Quality )
{
case value_quality::valid: return "VALID";
case value_quality::stale: return "STALE";
case value_quality::unavailable: return "UNAVAILABLE";
case value_quality::invalid: return "INVALID";
default: return "NOT_APPLICABLE";
}
}
char const *to_string( session_state const State )
{
switch( State )
{
case session_state::disconnected: return "DISCONNECTED";
case session_state::waiting_for_hello: return "WAITING FOR HELLO";
case session_state::established: return "ESTABLISHED";
default: return "READY";
}
}
char const *to_string( link_state const State )
{
switch( State )
{
case link_state::disconnected: return "DISCONNECTED";
case link_state::connecting: return "CONNECTING";
case link_state::healthy: return "HEALTHY";
case link_state::degraded: return "DEGRADED";
default: return "ERROR";
}
}
std::size_t value_type_size( value_type const Type )
{
switch( Type )
{
case value_type::boolean:
case value_type::uint8:
case value_type::int8:
case value_type::enumeration:
return 1;
case value_type::uint16:
case value_type::int16:
return 2;
case value_type::uint32:
case value_type::int32:
case value_type::float32:
case value_type::bitfield:
return 4;
case value_type::uint64:
case value_type::int64:
case value_type::float64:
return 8;
default:
// variable length, or nothing at all
return 0;
}
}
} // namespace hardware

View File

@@ -0,0 +1,223 @@
/*
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 <cstddef>
#include <cstdint>
// MaSzyna Hardware Protocol v2 (MHPv2)
// transport-independent protocol definitions shared by every transport (serial, TCP, RS-485)
namespace hardware
{
// protocol version implemented by this build
constexpr std::uint8_t protocol_version_major = 2;
constexpr std::uint8_t protocol_version_minor = 0;
// 'M2', stored little-endian in the frame header
constexpr std::uint16_t protocol_magic = 0x324D;
constexpr std::size_t protocol_header_size = 24;
constexpr std::size_t protocol_crc_size = 4;
constexpr std::size_t protocol_frame_size_min = 64;
constexpr std::size_t protocol_frame_size_default = 1024;
constexpr std::size_t protocol_frame_size_max = 4096;
// safety limits applied to anything arriving from the outside world
constexpr std::size_t protocol_symbol_name_max = 128;
constexpr std::size_t protocol_string_max = 128;
constexpr std::size_t protocol_symbols_per_message_max = 64;
// message identifiers; the numbering is part of the public protocol and must not be reordered
enum class message_type : std::uint16_t
{
invalid = 0x0000,
hello = 0x0001,
welcome = 0x0002,
heartbeat = 0x0003,
ack = 0x0004,
nack = 0x0005,
ping = 0x0006,
pong = 0x0007,
device_ready = 0x0008,
device_info = 0x0010,
device_diagnostics = 0x0011,
resolve_symbols = 0x0020,
symbols_resolved = 0x0021,
query_symbol = 0x0022,
symbol_info = 0x0023,
symbol_context_changed = 0x0024,
subscribe = 0x0030,
subscribe_result = 0x0031,
unsubscribe = 0x0032,
state_update = 0x0033,
command_event = 0x0040,
control_set = 0x0041,
control_claim = 0x0042,
control_claim_result = 0x0043,
control_release = 0x0044,
property_read = 0x0050,
property_value = 0x0051,
property_write = 0x0052
};
// header flag bits
enum packet_flag : std::uint8_t
{
packetflag_none = 0x00,
packetflag_ack_required = 0x01,
packetflag_response = 0x02,
packetflag_error = 0x04,
packetflag_high_priority = 0x08,
packetflag_snapshot = 0x10
};
// wire data types
enum class value_type : std::uint8_t
{
none = 0x00,
boolean = 0x01,
uint8 = 0x02,
int8 = 0x03,
uint16 = 0x04,
int16 = 0x05,
uint32 = 0x06,
int32 = 0x07,
uint64 = 0x08,
int64 = 0x09,
float32 = 0x0A,
float64 = 0x0B,
utf8_string = 0x0C,
byte_array = 0x0D,
enumeration = 0x0E,
bitfield = 0x0F
};
// per-value quality indicator, so a device can tell "zero" from "not measured"
enum class value_quality : std::uint8_t
{
valid = 0,
stale = 1,
unavailable = 2,
invalid = 3,
not_applicable = 4
};
enum class symbol_kind : std::uint8_t
{
state = 0,
command = 1,
property = 2,
diagnostic = 3
};
enum access_mode : std::uint8_t
{
access_read = 0x01,
access_write = 0x02,
access_readwrite = 0x03
};
enum class subscription_mode : std::uint8_t
{
periodic = 0,
on_change = 1,
periodic_or_change = 2
};
enum class command_action : std::uint8_t
{
press = 0,
release = 1,
repeat = 2
};
enum class error_code : std::uint16_t
{
none = 0,
unsupported_version = 1,
invalid_session = 2,
invalid_frame = 3,
invalid_length = 4,
invalid_field = 5,
unknown_message = 6,
unknown_symbol = 7,
unknown_handle = 8,
access_denied = 9,
out_of_range = 10,
wrong_type = 11,
not_ready = 12,
busy = 13,
control_not_owned = 14,
rate_limited = 15,
unsupported_operation = 16,
internal_error = 17
};
// capabilities advertised by the device in HELLO
enum capability_flag : std::uint32_t
{
capability_none = 0x00000000,
capability_receive_state = 0x00000001,
capability_send_commands = 0x00000002,
capability_send_controls = 0x00000004,
capability_diagnostics = 0x00000008,
capability_soft_takeover = 0x00000010,
capability_control_leases = 0x00000020
};
// session progress, mirrors the connection lifecycle of the specification
enum class session_state : std::uint8_t
{
disconnected = 0,
waiting_for_hello = 1,
established = 2,
ready = 3
};
// aggregated link condition presented in the debug ui
enum class link_state : std::uint8_t
{
disconnected = 0,
connecting = 1,
healthy = 2,
degraded = 3,
error = 4
};
char const *to_string( message_type const Type );
char const *to_string( error_code const Code );
char const *to_string( value_quality const Quality );
char const *to_string( session_state const State );
char const *to_string( link_state const State );
// size of a fixed-width type on the wire, 0 for variable-length ones
std::size_t value_type_size( value_type const Type );
} // namespace hardware

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