diff --git a/hardware/protocol/protocol_codec.cpp b/hardware/protocol/protocol_codec.cpp new file mode 100644 index 00000000..75f69162 --- /dev/null +++ b/hardware/protocol/protocol_codec.cpp @@ -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 + +namespace hardware +{ + +namespace +{ + +// castagnoli polynomial in reflected form +constexpr std::uint32_t crc32c_polynomial = 0x82F63B78u; + +std::array const &crc32c_table() +{ + static std::array const table = []() + { + std::array 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 &Buffer, std::uint16_t const Value ) +{ + Buffer.emplace_back( static_cast( Value ) ); + Buffer.emplace_back( static_cast( Value >> 8 ) ); +} + +void append_uint32( std::vector &Buffer, std::uint32_t const Value ) +{ + Buffer.emplace_back( static_cast( Value ) ); + Buffer.emplace_back( static_cast( Value >> 8 ) ); + Buffer.emplace_back( static_cast( Value >> 16 ) ); + Buffer.emplace_back( static_cast( Value >> 24 ) ); +} + +std::uint16_t extract_uint16( std::uint8_t const *Data ) +{ + return static_cast( static_cast( Data[ 0 ] ) | static_cast( Data[ 1 ] ) << 8 ); +} + +std::uint32_t extract_uint32( std::uint8_t const *Data ) +{ + return static_cast( Data[ 0 ] ) | static_cast( Data[ 1 ] ) << 8 | static_cast( Data[ 2 ] ) << 16 | static_cast( 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 &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 &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( 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 &Output ) +{ + std::vector 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( protocol_header_size ) ); + append_uint16( packet, static_cast( Header.type ) ); + append_uint32( packet, Header.session_id ); + append_uint32( packet, Header.sequence ); + append_uint32( packet, Header.timestamp_ms ); + append_uint16( packet, static_cast( 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( 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( 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( Value ) ); +} + +void payload_writer::write_uint64( std::uint64_t const Value ) +{ + append_uint32( m_data, static_cast( Value ) ); + append_uint32( m_data, static_cast( Value >> 32 ) ); +} + +void payload_writer::write_int64( std::int64_t const Value ) +{ + write_uint64( static_cast( 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( 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( Value ); + m_data[ Offset + 1 ] = static_cast( 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( 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( 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( 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( 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( 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( 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 &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( 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( 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 diff --git a/hardware/protocol/protocol_codec.h b/hardware/protocol/protocol_codec.h new file mode 100644 index 00000000..c983281a --- /dev/null +++ b/hardware/protocol/protocol_codec.h @@ -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 +#include +#include + +#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 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 &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 &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 &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 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 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 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 &Output ); + + counters const &stats() const { return m_stats; } + +private: +// members + std::vector m_buffer; + std::vector m_scratch; + std::size_t m_framesizelimit = protocol_frame_size_default; + bool m_overrun = false; + counters m_stats; +}; + +} // namespace hardware diff --git a/hardware/protocol/protocol_defs.cpp b/hardware/protocol/protocol_defs.cpp new file mode 100644 index 00000000..5a752afa --- /dev/null +++ b/hardware/protocol/protocol_defs.cpp @@ -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 diff --git a/hardware/protocol/protocol_defs.h b/hardware/protocol/protocol_defs.h new file mode 100644 index 00000000..0d2b084a --- /dev/null +++ b/hardware/protocol/protocol_defs.h @@ -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 +#include + +// 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 diff --git a/hardware/registry/command_registry.cpp b/hardware/registry/command_registry.cpp new file mode 100644 index 00000000..b083b24a --- /dev/null +++ b/hardware/registry/command_registry.cpp @@ -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 minimum; + std::optional 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( 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 diff --git a/hardware/registry/command_registry.h b/hardware/registry/command_registry.h new file mode 100644 index 00000000..7b5bbe44 --- /dev/null +++ b/hardware/registry/command_registry.h @@ -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 +#include +#include +#include + +#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 minimum; + std::optional maximum; + command_transform transform = command_transform::none; +}; + +class command_registry +{ + +public: +// methods + static command_registry const &instance(); + + std::vector const &entries() const { return m_entries; } + command_descriptor const *find( std::string const &Name ) const; + +private: +// methods + command_registry(); + +// members + std::vector m_entries; + std::unordered_map m_index; +}; + +} // namespace hardware diff --git a/hardware/registry/hardware_symbol_registry.cpp b/hardware/registry/hardware_symbol_registry.cpp new file mode 100644 index 00000000..a027a1f4 --- /dev/null +++ b/hardware/registry/hardware_symbol_registry.cpp @@ -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 diff --git a/hardware/registry/hardware_symbol_registry.h b/hardware/registry/hardware_symbol_registry.h new file mode 100644 index 00000000..d78ffc18 --- /dev/null +++ b/hardware/registry/hardware_symbol_registry.h @@ -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 +#include +#include +#include +#include + +#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 minimum; + std::optional 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::max(); + +// methods + static symbol_registry const &instance(); + + std::vector 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 m_entries; + std::unordered_map m_index; +}; + +} // namespace hardware diff --git a/hardware/registry/state_registry.cpp b/hardware/registry/state_registry.cpp new file mode 100644 index 00000000..47151f92 --- /dev/null +++ b/hardware/registry/state_registry.cpp @@ -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 const Minimum, std::optional const Maximum, std::function 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::hash()( train->name() ) ); +} + +} // namespace hardware diff --git a/hardware/registry/state_registry.h b/hardware/registry/state_registry.h new file mode 100644 index 00000000..2a2845d7 --- /dev/null +++ b/hardware/registry/state_registry.h @@ -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 +#include +#include +#include +#include + +#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 minimum; + std::optional maximum; + std::function getter; +}; + +class state_registry +{ + +public: +// methods + static state_registry const &instance(); + + std::vector 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 const Minimum, std::optional const Maximum, std::function Getter ); + +// members + std::vector m_entries; + std::unordered_map m_index; +}; + +} // namespace hardware