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:
457
hardware/protocol/protocol_codec.cpp
Normal file
457
hardware/protocol/protocol_codec.cpp
Normal 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
|
||||
156
hardware/protocol/protocol_codec.h
Normal file
156
hardware/protocol/protocol_codec.h
Normal 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
|
||||
138
hardware/protocol/protocol_defs.cpp
Normal file
138
hardware/protocol/protocol_defs.cpp
Normal 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
|
||||
223
hardware/protocol/protocol_defs.h
Normal file
223
hardware/protocol/protocol_defs.h
Normal 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
|
||||
96
hardware/registry/command_registry.cpp
Normal file
96
hardware/registry/command_registry.cpp
Normal 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
|
||||
67
hardware/registry/command_registry.h
Normal file
67
hardware/registry/command_registry.h
Normal 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
|
||||
69
hardware/registry/hardware_symbol_registry.cpp
Normal file
69
hardware/registry/hardware_symbol_registry.cpp
Normal 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
|
||||
64
hardware/registry/hardware_symbol_registry.h
Normal file
64
hardware/registry/hardware_symbol_registry.h
Normal 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
|
||||
170
hardware/registry/state_registry.cpp
Normal file
170
hardware/registry/state_registry.cpp
Normal 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
|
||||
89
hardware/registry/state_registry.h
Normal file
89
hardware/registry/state_registry.h
Normal 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
|
||||
Reference in New Issue
Block a user