/* 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_session.h" #include "hardware/registry/command_registry.h" #include "hardware/registry/hardware_symbol_registry.h" #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "vehicle/Train.h" namespace hardware { namespace { // how many transaction results are remembered for duplicate detection constexpr std::size_t transaction_cache_size = 128; // upper bound on session handles, one per resolved symbol constexpr std::size_t handles_max = 1024; // round trip time is measured this often constexpr double ping_interval = 5.0; std::uint32_t generate_session_id() { static std::mt19937 generator( static_cast( std::chrono::steady_clock::now().time_since_epoch().count() ) ); std::uint32_t const value = generator(); // zero is reserved for "no session" return ( value != 0 ? value : 1 ); } void write_state_value( payload_writer &Writer, state_value const &Value ) { switch( Value.type ) { case value_type::boolean: case value_type::uint8: case value_type::int8: case value_type::enumeration: Writer.write_uint8( static_cast( Value.numeric ) ); break; case value_type::uint16: case value_type::int16: Writer.write_uint16( static_cast( Value.numeric ) ); break; case value_type::uint32: case value_type::int32: case value_type::bitfield: Writer.write_uint32( static_cast( Value.numeric ) ); break; case value_type::uint64: case value_type::int64: Writer.write_uint64( static_cast( Value.numeric ) ); break; case value_type::float64: Writer.write_float64( Value.numeric ); break; case value_type::utf8_string: Writer.write_string( Value.text ); break; default: Writer.write_float32( static_cast( Value.numeric ) ); break; } } std::size_t state_value_size( state_value const &Value ) { if( Value.type == value_type::utf8_string ) { return 1 + std::min( Value.text.size(), protocol_string_max ); } auto const size = value_type_size( Value.type ); return ( size > 0 ? size : 4 ); } int glfw_action( command_action const Action ) { switch( Action ) { case command_action::release: return GLFW_RELEASE; case command_action::repeat: return GLFW_REPEAT; default: return GLFW_PRESS; } } } // anonymous namespace protocol_session::protocol_session( session_output &Output, config const &Config ) : m_output( Output ), m_config( Config ) { m_framesize = std::clamp( m_config.frame_size, protocol_frame_size_min, protocol_frame_size_max ); reset(); } void protocol_session::reset() { m_state = session_state::waiting_for_hello; m_sessionid = 0; m_sequence = 0; m_lastreceivedsequence = 0; m_sequenceseen = false; m_outgoingtransaction = 0; m_framesize = std::clamp( m_config.frame_size, protocol_frame_size_min, protocol_frame_size_max ); m_capabilities = 0; m_peermajor = 0; m_peerminor = 0; m_devicename.clear(); m_devicerole.clear(); m_deviceid.clear(); m_firmwareversion.clear(); m_hardwareversion.clear(); m_handles.clear(); m_handlelookup.clear(); m_subscriptions.clear(); m_controlsequence.clear(); m_transactions.clear(); m_transactionorder.clear(); m_uptime = 0.0; m_lastreceived = 0.0; m_heartbeattimer = 0.0; m_pingtimer = 0.0; m_pingpending = false; m_ratewindow = 0.0; m_framebudget = m_config.max_frames_per_second; m_payloadbudget = m_config.max_payload_bytes_per_second; m_commandbudget = m_config.max_commands_per_second; m_contextvalid = false; m_context = 0; m_diagnostics = protocol_diagnostics {}; m_devicediagnostics = device_diagnostics {}; m_functions.clear(); m_diagnostic = diagnostic_report {}; m_prompt = diagnostic_prompt {}; m_log.clear(); } link_state protocol_session::link_condition() const { if( m_state == session_state::waiting_for_hello ) { return link_state::connecting; } if( m_diagnostics.missed_heartbeats <= 2 ) { return link_state::healthy; } if( m_diagnostics.missed_heartbeats <= 4 ) { return link_state::degraded; } return link_state::error; } void protocol_session::add_frame_errors( frame_decoder::counters const &Delta ) { m_diagnostics.crc_errors += Delta.crc_errors; m_diagnostics.frame_errors += Delta.frame_errors; m_diagnostics.length_errors += Delta.length_errors; } void protocol_session::send( message_type const Type, std::uint8_t const Flags, payload_writer const &Payload ) { packet_header header; header.flags = Flags; header.type = Type; header.session_id = m_sessionid; header.sequence = m_sequence++; header.timestamp_ms = static_cast( m_uptime * 1000.0 ); header.payload_length = static_cast( Payload.size() ); m_output.send_packet( header, Payload.buffer() ); ++m_diagnostics.tx_frames; m_txrate.add(); if( true == debug_flags.log_frames ) { WriteLog( "hardware: tx " + std::string( to_string( Type ) ) + ", " + std::to_string( Payload.size() ) + " bytes" ); } } void protocol_session::send_ack( std::uint32_t const Transaction, message_type const Type ) { payload_writer writer; writer.write_uint32( Transaction ); writer.write_uint16( static_cast( Type ) ); send( message_type::ack, packetflag_response, writer ); ++m_diagnostics.acks; } void protocol_session::send_nack( std::uint32_t const Transaction, message_type const Type, error_code const Error ) { payload_writer writer; writer.write_uint32( Transaction ); writer.write_uint16( static_cast( Type ) ); writer.write_uint16( static_cast( Error ) ); send( message_type::nack, packetflag_response | packetflag_error, writer ); ++m_diagnostics.nacks; if( true == debug_flags.log_messages ) { WriteLog( "hardware: nack " + std::string( to_string( Type ) ) + ", " + std::string( to_string( Error ) ) ); } } std::size_t protocol_session::symbol_of_handle( std::uint16_t const Handle ) const { if( ( Handle == 0 ) || ( Handle > m_handles.size() ) ) { return symbol_registry::npos; } return m_handles[ Handle - 1 ]; } std::uint16_t protocol_session::handle_of_symbol( std::size_t const SymbolIndex ) { auto const lookup = m_handlelookup.find( SymbolIndex ); if( lookup != m_handlelookup.end() ) { return lookup->second; } if( m_handles.size() >= handles_max ) { return 0; } m_handles.emplace_back( SymbolIndex ); auto const handle = static_cast( m_handles.size() ); m_handlelookup.emplace( SymbolIndex, handle ); return handle; } bool protocol_session::consume_frame_budget( std::size_t const PayloadSize ) { if( ( m_framebudget <= 0 ) || ( m_payloadbudget < static_cast( PayloadSize ) ) ) { return false; } --m_framebudget; m_payloadbudget -= static_cast( PayloadSize ); return true; } bool protocol_session::consume_command_budget() { if( m_commandbudget <= 0 ) { return false; } --m_commandbudget; return true; } bool protocol_session::remember_transaction( std::uint32_t const Transaction, transaction_result const &Result ) { if( Transaction == 0 ) { // transaction id zero means the device doesn't care about duplicate protection return false; } m_transactions[ Transaction ] = Result; m_transactionorder.emplace_back( Transaction ); while( m_transactionorder.size() > transaction_cache_size ) { m_transactions.erase( m_transactionorder.front() ); m_transactionorder.pop_front(); } return true; } protocol_session::transaction_result const *protocol_session::recall_transaction( std::uint32_t const Transaction ) const { if( Transaction == 0 ) { return nullptr; } auto const lookup = m_transactions.find( Transaction ); return ( lookup != m_transactions.end() ? &lookup->second : nullptr ); } void protocol_session::handle_packet( decoded_packet const &Packet ) { ++m_diagnostics.rx_frames; m_rxrate.add(); m_lastreceived = m_uptime; if( false == consume_frame_budget( Packet.payload.size() ) ) { ++m_diagnostics.rate_limited; return; } if( Packet.header.protocol_major != protocol_version_major ) { ++m_diagnostics.version_errors; if( Packet.header.type == message_type::hello ) { send_nack( 0, message_type::hello, error_code::unsupported_version ); } return; } if( true == m_sequenceseen ) { if( Packet.header.sequence != m_lastreceivedsequence + 1 ) { ++m_diagnostics.sequence_gaps; } } m_lastreceivedsequence = Packet.header.sequence; m_sequenceseen = true; if( true == debug_flags.log_frames ) { WriteLog( "hardware: rx " + std::string( to_string( Packet.header.type ) ) + ", " + std::to_string( Packet.payload.size() ) + " bytes" ); } if( Packet.header.type == message_type::hello ) { handle_hello( Packet ); return; } if( Packet.header.type == message_type::ping ) { payload_reader reader( Packet.payload ); auto const token = reader.read_uint32(); payload_writer writer; writer.write_uint32( token ); send( message_type::pong, packetflag_response, writer ); return; } // everything past this point belongs to an established session if( ( m_state == session_state::waiting_for_hello ) || ( Packet.header.session_id != m_sessionid ) ) { send_nack( 0, Packet.header.type, error_code::invalid_session ); return; } switch( Packet.header.type ) { case message_type::heartbeat: handle_heartbeat( Packet ); break; case message_type::pong: { payload_reader reader( Packet.payload ); auto const token = reader.read_uint32(); if( ( true == m_pingpending ) && ( token == m_pingtoken ) ) { m_diagnostics.round_trip_time_ms = ( m_uptime - m_pingsent ) * 1000.0; m_pingpending = false; } break; } case message_type::device_ready: m_state = session_state::ready; if( true == debug_flags.log_messages ) { WriteLog( "hardware: device '" + m_devicename + "' is ready" ); } break; case message_type::device_diagnostics: handle_device_diagnostics( Packet ); break; case message_type::device_log: handle_device_log( Packet ); break; case message_type::diagnostic_functions: handle_diagnostic_functions( Packet ); break; case message_type::diagnostic_status: handle_diagnostic_status( Packet ); break; case message_type::diagnostic_prompt: handle_diagnostic_prompt( Packet ); break; case message_type::ack: handle_device_answer( Packet, true ); break; case message_type::nack: handle_device_answer( Packet, false ); break; case message_type::resolve_symbols: handle_resolve_symbols( Packet ); break; case message_type::query_symbol: handle_query_symbol( Packet ); break; case message_type::subscribe: handle_subscribe( Packet ); break; case message_type::unsubscribe: handle_unsubscribe( Packet ); break; case message_type::command_event: handle_command_event( Packet ); break; case message_type::control_set: handle_control_set( Packet ); break; case message_type::control_claim: case message_type::control_release: case message_type::property_read: case message_type::property_write: // reserved for a later revision send_nack( 0, Packet.header.type, error_code::unsupported_operation ); break; default: ++m_diagnostics.unknown_messages; if( ( Packet.header.flags & packetflag_ack_required ) != 0 ) { send_nack( 0, Packet.header.type, error_code::unknown_message ); } break; } } void protocol_session::handle_hello( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const major = reader.read_uint8(); auto const minor = reader.read_uint8(); auto const framesize = reader.read_uint16(); auto const capabilities = reader.read_uint32(); auto const name = reader.read_string(); auto const role = reader.read_string(); std::string deviceid; std::string firmware; std::string hardware; // the remaining fields are optional, a minimal controller simply doesn't send them if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { deviceid = reader.read_string(); } if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { firmware = reader.read_string(); } if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { hardware = reader.read_string(); } if( false == reader.ok() ) { ++m_diagnostics.length_errors; send_nack( 0, message_type::hello, error_code::invalid_length ); return; } if( major != protocol_version_major ) { ++m_diagnostics.version_errors; send_nack( 0, message_type::hello, error_code::unsupported_version ); return; } if( true == name.empty() ) { send_nack( 0, message_type::hello, error_code::invalid_field ); return; } // a fresh hello always starts a new session, invalidating whatever the previous one held m_handles.clear(); m_handlelookup.clear(); m_subscriptions.clear(); m_controlsequence.clear(); m_transactions.clear(); m_transactionorder.clear(); m_peermajor = major; m_peerminor = minor; m_capabilities = capabilities; m_devicename = name; m_devicerole = role; m_deviceid = deviceid; m_firmwareversion = firmware; m_hardwareversion = hardware; auto const requested = ( framesize > 0 ? static_cast( framesize ) : protocol_frame_size_default ); m_framesize = std::clamp( std::min( requested, m_config.frame_size ), protocol_frame_size_min, protocol_frame_size_max ); m_sessionid = generate_session_id(); m_state = session_state::established; m_heartbeattimer = 0.0; m_pingtimer = 0.0; m_pingpending = false; m_contextvalid = false; payload_writer writer; writer.write_uint8( protocol_version_major ); writer.write_uint8( protocol_version_minor ); writer.write_uint32( m_sessionid ); writer.write_uint16( static_cast( m_framesize ) ); writer.write_uint16( static_cast( m_config.heartbeat_interval_ms ) ); writer.write_string( Global.asVersion ); send( message_type::welcome, packetflag_response, writer ); WriteLog( "hardware: session " + std::to_string( m_sessionid ) + " established with '" + m_devicename + "' (" + m_devicerole + ")" ); } void protocol_session::handle_heartbeat( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); // the fields are informational; a device is free to send an empty heartbeat reader.read_uint32(); reader.read_uint32(); reader.read_uint16(); } void protocol_session::handle_device_diagnostics( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); device_diagnostics diagnostics; diagnostics.available = true; diagnostics.uptime_ms = reader.read_uint32(); // every field past the uptime is optional if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { diagnostics.supply_voltage = reader.read_float32(); } if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { diagnostics.temperature = reader.read_float32(); } if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { diagnostics.rx_overflows = reader.read_uint32(); } if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { diagnostics.tx_overflows = reader.read_uint32(); } if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { diagnostics.watchdog_resets = reader.read_uint32(); } if( ( true == reader.ok() ) && ( false == reader.exhausted() ) ) { diagnostics.firmware_build = reader.read_string(); } if( false == reader.ok() ) { ++m_diagnostics.length_errors; return; } m_devicediagnostics = diagnostics; } void protocol_session::handle_resolve_symbols( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const transaction = reader.read_uint32(); auto const count = reader.read_uint8(); if( ( false == reader.ok() ) || ( count > protocol_symbols_per_message_max ) ) { send_nack( transaction, message_type::resolve_symbols, error_code::invalid_field ); return; } auto const &symbols = symbol_registry::instance().entries(); payload_writer writer; writer.write_uint32( transaction ); writer.write_uint8( count ); for( std::size_t index = 0; index < count; ++index ) { auto const name = reader.read_string( protocol_symbol_name_max ); if( false == reader.ok() ) { send_nack( transaction, message_type::resolve_symbols, error_code::invalid_length ); return; } auto const symbolindex = symbol_registry::instance().find( name ); if( symbolindex == symbol_registry::npos ) { ++m_diagnostics.unknown_symbols; // handle zero tells the device the symbol isn't available in this simulator build writer.write_uint16( 0 ); writer.write_uint8( 0 ); writer.write_uint8( static_cast( value_type::none ) ); writer.write_uint8( 0 ); continue; } auto const &symbol = symbols[ symbolindex ]; writer.write_uint16( handle_of_symbol( symbolindex ) ); writer.write_uint8( static_cast( symbol.kind ) ); writer.write_uint8( static_cast( symbol.type ) ); writer.write_uint8( symbol.access ); } send( message_type::symbols_resolved, packetflag_response, writer ); } void protocol_session::handle_query_symbol( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const transaction = reader.read_uint32(); auto const handle = reader.read_uint16(); if( false == reader.ok() ) { send_nack( transaction, message_type::query_symbol, error_code::invalid_length ); return; } auto const symbolindex = symbol_of_handle( handle ); if( symbolindex == symbol_registry::npos ) { ++m_diagnostics.unknown_handles; send_nack( transaction, message_type::query_symbol, error_code::unknown_handle ); return; } auto const &symbol = symbol_registry::instance().entries()[ symbolindex ]; payload_writer writer; writer.write_uint32( transaction ); writer.write_uint16( handle ); writer.write_uint8( static_cast( symbol.kind ) ); writer.write_uint8( static_cast( symbol.type ) ); writer.write_uint8( symbol.access ); writer.write_uint8( static_cast( ( symbol.minimum ? 0x01 : 0x00 ) | ( symbol.maximum ? 0x02 : 0x00 ) ) ); writer.write_float32( static_cast( symbol.minimum.value_or( 0.0 ) ) ); writer.write_float32( static_cast( symbol.maximum.value_or( 0.0 ) ) ); writer.write_string( symbol.name ); writer.write_string( symbol.unit ); send( message_type::symbol_info, packetflag_response, writer ); } void protocol_session::handle_subscribe( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const transaction = reader.read_uint32(); auto const count = reader.read_uint8(); if( ( false == reader.ok() ) || ( count > protocol_symbols_per_message_max ) ) { send_nack( transaction, message_type::subscribe, error_code::invalid_field ); return; } auto const &symbols = symbol_registry::instance().entries(); payload_writer writer; writer.write_uint32( transaction ); writer.write_uint8( count ); for( std::size_t index = 0; index < count; ++index ) { auto const handle = reader.read_uint16(); auto const mode = static_cast( reader.read_uint8() ); auto const period = reader.read_uint16(); auto const deadband = reader.read_float32(); if( false == reader.ok() ) { send_nack( transaction, message_type::subscribe, error_code::invalid_length ); return; } auto result = error_code::none; auto const symbolindex = symbol_of_handle( handle ); if( symbolindex == symbol_registry::npos ) { ++m_diagnostics.unknown_handles; result = error_code::unknown_handle; } else if( symbols[ symbolindex ].kind != symbol_kind::state ) { result = error_code::access_denied; } else { result = m_subscriptions.subscribe( handle, symbolindex, symbols[ symbolindex ].source_index, mode, period, deadband ); } writer.write_uint16( handle ); writer.write_uint16( static_cast( result ) ); } send( message_type::subscribe_result, packetflag_response, writer ); // the device initialises its gauges and indicators from the initial snapshot std::vector items; m_subscriptions.collect( 0.0, m_snapshot, true, items ); send_state_update( items, true ); } void protocol_session::handle_unsubscribe( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const transaction = reader.read_uint32(); auto const count = reader.read_uint8(); if( ( false == reader.ok() ) || ( count > protocol_symbols_per_message_max ) ) { send_nack( transaction, message_type::unsubscribe, error_code::invalid_field ); return; } for( std::size_t index = 0; index < count; ++index ) { auto const handle = reader.read_uint16(); if( false == reader.ok() ) { send_nack( transaction, message_type::unsubscribe, error_code::invalid_length ); return; } m_subscriptions.unsubscribe( handle ); } send_ack( transaction, message_type::unsubscribe ); } void protocol_session::handle_command_event( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const transaction = reader.read_uint32(); auto const handle = reader.read_uint16(); auto const action = reader.read_uint8(); auto const parameter1 = reader.read_float32(); auto const parameter2 = reader.read_float32(); auto const entity = reader.read_uint32(); bool const acknowledge = ( ( Packet.header.flags & packetflag_ack_required ) != 0 ); if( false == reader.ok() ) { ++m_diagnostics.length_errors; if( true == acknowledge ) { send_nack( transaction, message_type::command_event, error_code::invalid_length ); } return; } // a retransmission of an already executed command must not execute it a second time if( auto const *previous = recall_transaction( transaction ) ) { ++m_diagnostics.duplicate_commands; if( true == acknowledge ) { if( true == previous->accepted ) { send_ack( transaction, message_type::command_event ); } else { send_nack( transaction, message_type::command_event, previous->error ); } } return; } auto result = error_code::none; if( m_state != session_state::ready ) { result = error_code::not_ready; } else if( false == consume_command_budget() ) { ++m_diagnostics.rate_limited; result = error_code::rate_limited; } else if( action > static_cast( command_action::repeat ) ) { result = error_code::invalid_field; } else if( ( false == std::isfinite( parameter1 ) ) || ( false == std::isfinite( parameter2 ) ) ) { result = error_code::invalid_field; } else { auto const symbolindex = symbol_of_handle( handle ); if( symbolindex == symbol_registry::npos ) { ++m_diagnostics.unknown_handles; result = error_code::unknown_handle; } else if( symbol_registry::instance().entries()[ symbolindex ].kind != symbol_kind::command ) { result = error_code::access_denied; } else { result = execute_command( symbolindex, static_cast( action ), parameter1, parameter2, static_cast( entity ) ); } } transaction_result const outcome { result == error_code::none, result }; remember_transaction( transaction, outcome ); if( true == acknowledge ) { if( true == outcome.accepted ) { send_ack( transaction, message_type::command_event ); } else { send_nack( transaction, message_type::command_event, result ); } } } void protocol_session::handle_control_set( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const handle = reader.read_uint16(); auto const sequence = reader.read_uint32(); auto const value = reader.read_float32(); auto const entity = reader.read_uint32(); if( false == reader.ok() ) { ++m_diagnostics.length_errors; return; } if( m_state != session_state::ready ) { send_nack( 0, message_type::control_set, error_code::not_ready ); return; } // updates are idempotent, so a late one is simply dropped auto const previous = m_controlsequence.find( handle ); if( previous != m_controlsequence.end() ) { if( static_cast( sequence - previous->second ) <= 0 ) { return; } } m_controlsequence[ handle ] = sequence; auto const symbolindex = symbol_of_handle( handle ); if( symbolindex == symbol_registry::npos ) { ++m_diagnostics.unknown_handles; send_nack( 0, message_type::control_set, error_code::unknown_handle ); return; } auto const &symbol = symbol_registry::instance().entries()[ symbolindex ]; if( symbol.kind != symbol_kind::command ) { send_nack( 0, message_type::control_set, error_code::access_denied ); return; } auto const &descriptor = command_registry::instance().entries()[ symbol.source_index ]; if( false == descriptor.continuous ) { send_nack( 0, message_type::control_set, error_code::wrong_type ); return; } if( false == std::isfinite( value ) ) { send_nack( 0, message_type::control_set, error_code::invalid_field ); return; } // values outside the declared range are rejected rather than silently clipped if( ( descriptor.minimum && ( value < *descriptor.minimum ) ) || ( descriptor.maximum && ( value > *descriptor.maximum ) ) ) { send_nack( 0, message_type::control_set, error_code::out_of_range ); return; } if( false == consume_command_budget() ) { ++m_diagnostics.rate_limited; return; } auto const result = execute_command( symbolindex, command_action::press, value, 0.0, static_cast( entity ) ); if( result == error_code::none ) { ++m_diagnostics.controls_applied; } else { send_nack( 0, message_type::control_set, result ); } } error_code protocol_session::execute_command( std::size_t const SymbolIndex, command_action const Action, double const Parameter1, double const Parameter2, std::uint16_t const Recipient ) { auto const &symbol = symbol_registry::instance().entries()[ SymbolIndex ]; auto const &descriptor = command_registry::instance().entries()[ symbol.source_index ]; if( descriptor.command == user_command::none ) { return error_code::unsupported_operation; } double parameter = Parameter1; if( descriptor.transform == command_transform::mastercontroller_percent ) { auto *train = simulation::Train; if( ( train == nullptr ) || ( train->Occupied() == nullptr ) ) { return error_code::not_ready; } auto const percentage = std::clamp( Parameter1, 0.0, 1.0 ); auto const positions = train->Occupied()->MainCtrlPosNo; parameter = ( percentage > 0.01 ? 1.0 + ( positions - 1 ) * percentage : 0.0 ); train->Occupied()->eimic_analog = percentage; } m_relay.post( descriptor.command, parameter, Parameter2, glfw_action( Action ), Recipient ); ++m_diagnostics.commands_executed; return error_code::none; } void protocol_session::handle_device_log( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const severity = static_cast( reader.read_uint8() ); auto const uptime = reader.read_uint32(); auto const text = reader.read_string(); if( ( false == reader.ok() ) || ( true == text.empty() ) ) { ++m_diagnostics.length_errors; return; } device_log_entry entry; entry.severity = ( severity <= log_severity::error ? severity : log_severity::info ); entry.device_uptime_ms = uptime; entry.text = text; m_log.emplace_back( std::move( entry ) ); while( m_log.size() > protocol_log_history ) { m_log.pop_front(); } // debug chatter stays in the panel, anything the device considers worth reporting // also goes to the simulator log if( ( severity != log_severity::debug ) || ( true == debug_flags.log_messages ) ) { auto const line = "hardware: [" + m_devicename + "] " + std::string( to_string( severity ) ) + ": " + text; if( severity == log_severity::error ) { ErrorLog( line ); } else { WriteLog( line ); } } } void protocol_session::handle_diagnostic_functions( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const count = reader.read_uint8(); if( ( false == reader.ok() ) || ( count > protocol_diagnostic_functions_max ) ) { send_nack( 0, message_type::diagnostic_functions, error_code::invalid_field ); return; } std::vector functions; functions.reserve( count ); for( std::size_t index = 0; index < count; ++index ) { diagnostic_function function; function.id = reader.read_uint8(); function.name = reader.read_string(); if( false == reader.ok() ) { send_nack( 0, message_type::diagnostic_functions, error_code::invalid_length ); return; } if( false == function.name.empty() ) { functions.emplace_back( std::move( function ) ); } } m_functions = std::move( functions ); if( true == debug_flags.log_messages ) { WriteLog( "hardware: device '" + m_devicename + "' offers " + std::to_string( m_functions.size() ) + " diagnostic function(s)" ); } } void protocol_session::handle_diagnostic_status( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); auto const function = reader.read_uint8(); auto const state = static_cast( reader.read_uint8() ); auto const progress = reader.read_uint8(); auto const message = reader.read_string(); if( ( false == reader.ok() ) || ( state > diagnostic_state::failed ) ) { ++m_diagnostics.length_errors; return; } m_diagnostic.function = function; m_diagnostic.state = state; m_diagnostic.progress = ( progress <= 100 ? progress : 100 ); m_diagnostic.message = message; if( state != diagnostic_state::running ) { // a routine which ended cannot be waiting for an answer any more m_prompt = diagnostic_prompt {}; WriteLog( "hardware: device '" + m_devicename + "' diagnostic " + std::to_string( function ) + " " + std::string( to_string( state ) ) + ( message.empty() ? "" : ": " + message ) ); } } void protocol_session::handle_diagnostic_prompt( decoded_packet const &Packet ) { payload_reader reader( Packet.payload ); diagnostic_prompt prompt; prompt.id = reader.read_uint32(); prompt.text = reader.read_string(); auto const options = reader.read_uint8(); if( ( false == reader.ok() ) || ( options > protocol_prompt_options_max ) ) { send_nack( 0, message_type::diagnostic_prompt, error_code::invalid_field ); return; } for( std::size_t index = 0; index < options; ++index ) { auto label = reader.read_string(); if( false == reader.ok() ) { send_nack( 0, message_type::diagnostic_prompt, error_code::invalid_length ); return; } prompt.options.emplace_back( std::move( label ) ); } if( true == prompt.options.empty() ) { prompt.options.emplace_back( "OK" ); } prompt.active = true; m_prompt = std::move( prompt ); } void protocol_session::handle_device_answer( decoded_packet const &Packet, bool const Positive ) { payload_reader reader( Packet.payload ); reader.read_uint32(); auto const message = static_cast( reader.read_uint16() ); auto const error = ( Positive ? error_code::none : static_cast( reader.read_uint16() ) ); if( false == reader.ok() ) { ++m_diagnostics.length_errors; return; } if( ( message == message_type::diagnostic_run ) && ( false == Positive ) ) { m_diagnostic.state = diagnostic_state::failed; m_diagnostic.message = to_string( error ); WriteLog( "hardware: device '" + m_devicename + "' refused to start the diagnostic: " + std::string( to_string( error ) ) ); } } bool protocol_session::start_diagnostic( std::uint8_t const Function ) { if( m_state != session_state::ready ) { return false; } auto const function = std::find_if( m_functions.begin(), m_functions.end(), [ Function ]( diagnostic_function const &Entry ) { return Entry.id == Function; } ); if( function == m_functions.end() ) { return false; } payload_writer writer; writer.write_uint32( ++m_outgoingtransaction ); writer.write_uint8( Function ); send( message_type::diagnostic_run, packetflag_ack_required, writer ); m_prompt = diagnostic_prompt {}; m_diagnostic.function = Function; m_diagnostic.state = diagnostic_state::running; m_diagnostic.progress = 0; m_diagnostic.message = "requested"; WriteLog( "hardware: device '" + m_devicename + "' asked to run diagnostic '" + function->name + "'" ); return true; } void protocol_session::cancel_diagnostic() { if( m_state != session_state::ready ) { return; } payload_writer writer; writer.write_uint8( m_diagnostic.function ); send( message_type::diagnostic_cancel, packetflag_none, writer ); m_prompt = diagnostic_prompt {}; } void protocol_session::answer_prompt( std::uint8_t const Option ) { if( false == m_prompt.active ) { return; } payload_writer writer; writer.write_uint32( m_prompt.id ); writer.write_uint8( Option ); send( message_type::diagnostic_prompt_result, packetflag_response, writer ); m_prompt = diagnostic_prompt {}; } void protocol_session::send_state_update( std::vector const &Items, bool const Snapshot ) { if( true == Items.empty() ) { return; } std::size_t const budget = ( m_framesize > protocol_header_size + protocol_crc_size ? m_framesize - protocol_header_size - protocol_crc_size : 64 ); std::uint8_t const flags = ( Snapshot ? packetflag_snapshot : packetflag_none ); payload_writer writer; writer.write_uint16( 0 ); std::uint16_t count = 0; for( auto const &item : Items ) { std::size_t const itemsize = 2 + 1 + 1 + state_value_size( item.value ); if( ( count > 0 ) && ( writer.size() + itemsize > budget ) ) { writer.patch_uint16( 0, count ); send( message_type::state_update, flags, writer ); ++m_diagnostics.state_updates; writer.clear(); writer.write_uint16( 0 ); count = 0; } writer.write_uint16( item.handle ); writer.write_uint8( static_cast( item.value.type ) ); writer.write_uint8( static_cast( item.value.quality ) ); write_state_value( writer, item.value ); ++count; } if( count > 0 ) { writer.patch_uint16( 0, count ); send( message_type::state_update, flags, writer ); ++m_diagnostics.state_updates; } } void protocol_session::send_symbol_context_changed( std::uint32_t const Context ) { payload_writer writer; writer.write_uint32( Context ); send( message_type::symbol_context_changed, packetflag_none, writer ); } void protocol_session::update( double const Deltatime, state_snapshot const &Snapshot ) { m_snapshot = Snapshot; m_uptime += Deltatime; m_rxrate.update( Deltatime ); m_txrate.update( Deltatime ); m_diagnostics.rx_rate = m_rxrate.rate(); m_diagnostics.tx_rate = m_txrate.rate(); m_diagnostics.connection_uptime = m_uptime; m_diagnostics.last_rx_age = m_uptime - m_lastreceived; m_ratewindow += Deltatime; if( m_ratewindow >= 1.0 ) { m_ratewindow = 0.0; m_framebudget = m_config.max_frames_per_second; m_payloadbudget = m_config.max_payload_bytes_per_second; m_commandbudget = m_config.max_commands_per_second; } if( m_state == session_state::waiting_for_hello ) { return; } double const heartbeatinterval = std::max( 0.05, m_config.heartbeat_interval_ms * 0.001 ); m_diagnostics.missed_heartbeats = static_cast( m_diagnostics.last_rx_age / heartbeatinterval ); if( m_diagnostics.missed_heartbeats >= m_config.heartbeat_timeout_count ) { WriteLog( "hardware: device '" + m_devicename + "' stopped responding, session closed" ); reset(); return; } // the metadata of some symbols depends on the controlled vehicle if( ( true == Snapshot.available ) && ( ( false == m_contextvalid ) || ( Snapshot.context_id != m_context ) ) ) { bool const notify = m_contextvalid; m_context = Snapshot.context_id; m_contextvalid = true; if( true == notify ) { send_symbol_context_changed( m_context ); } } m_heartbeattimer += Deltatime; if( m_heartbeattimer >= heartbeatinterval ) { m_heartbeattimer = 0.0; payload_writer writer; writer.write_uint32( static_cast( m_uptime * 1000.0 ) ); writer.write_uint32( m_lastreceivedsequence ); writer.write_uint16( static_cast( m_state ) ); send( message_type::heartbeat, packetflag_none, writer ); } m_pingtimer += Deltatime; if( ( false == m_pingpending ) && ( m_pingtimer >= ping_interval ) ) { m_pingtimer = 0.0; m_pingpending = true; m_pingsent = m_uptime; ++m_pingtoken; payload_writer writer; writer.write_uint32( m_pingtoken ); send( message_type::ping, packetflag_none, writer ); } else if( ( true == m_pingpending ) && ( m_uptime - m_pingsent > ping_interval ) ) { // the answer never arrived, try again later m_pingpending = false; m_pingtimer = 0.0; } std::vector items; m_subscriptions.collect( Deltatime, Snapshot, false, items ); send_state_update( items, false ); } void protocol_session::fill_report( link_report &Report ) const { Report.device_name = m_devicename; Report.device_role = m_devicerole; Report.device_id = m_deviceid; Report.firmware_version = m_firmwareversion; Report.session = m_state; Report.session_id = m_sessionid; Report.protocol_major = m_peermajor; Report.protocol_minor = m_peerminor; Report.handles = m_handles.size(); Report.subscriptions = m_subscriptions.size(); Report.frame_size = m_framesize; Report.protocol = m_diagnostics; Report.device = m_devicediagnostics; Report.functions = m_functions; Report.diagnostic = m_diagnostic; Report.prompt = m_prompt; Report.log.assign( m_log.begin(), m_log.end() ); } } // namespace hardware