17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-19 04:59:20 +02:00

hardware protocol v2.1: device log, diagnostic routines, manual links

Three additions, all driven from the simulator debug panel.

DEVICE_LOG lets a controller send text lines; they reach the simulator log
and a per connection view in the panel which keeps the last 64 of them.

Diagnostic routines: a controller lists the procedures it offers, the
operator picks one from a drop down and starts it, and the controller
reports progress and the result. While a routine runs the controller can
ask the simulator to show a message box with its own text and buttons and
gets back which one was pressed, which is what a valve or gauge
calibration needs. The message box appears whether the debug panel is
open or not.

Links can now be disconnected and reconnected by hand: disconnecting
closes the serial port so the controller can be reprogrammed, and the
session is rebuilt from scratch on reconnect. Ports which appear while
the simulation runs can be picked from a refreshed list, given a baud
rate and added as a new controller, so the manager now starts even with
no uart2 entries in eu07.ini.
This commit is contained in:
2026-09-06 11:29:26 +00:00
parent da962cd91b
commit eed44dc858
10 changed files with 777 additions and 40 deletions

View File

@@ -27,6 +27,7 @@ char const *to_string( message_type const Type )
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::device_log: return "DEVICE_LOG";
case message_type::resolve_symbols: return "RESOLVE_SYMBOLS";
case message_type::symbols_resolved: return "SYMBOLS_RESOLVED";
case message_type::query_symbol: return "QUERY_SYMBOL";
@@ -44,6 +45,12 @@ char const *to_string( message_type const Type )
case message_type::property_read: return "PROPERTY_READ";
case message_type::property_value: return "PROPERTY_VALUE";
case message_type::property_write: return "PROPERTY_WRITE";
case message_type::diagnostic_functions: return "DIAGNOSTIC_FUNCTIONS";
case message_type::diagnostic_run: return "DIAGNOSTIC_RUN";
case message_type::diagnostic_cancel: return "DIAGNOSTIC_CANCEL";
case message_type::diagnostic_status: return "DIAGNOSTIC_STATUS";
case message_type::diagnostic_prompt: return "DIAGNOSTIC_PROMPT";
case message_type::diagnostic_prompt_result: return "DIAGNOSTIC_PROMPT_RESULT";
default: return "UNKNOWN";
}
}
@@ -108,6 +115,28 @@ char const *to_string( link_state const State )
}
}
char const *to_string( log_severity const Severity )
{
switch( Severity )
{
case log_severity::debug: return "debug";
case log_severity::info: return "info";
case log_severity::warning: return "warning";
default: return "error";
}
}
char const *to_string( diagnostic_state const State )
{
switch( State )
{
case diagnostic_state::idle: return "IDLE";
case diagnostic_state::running: return "RUNNING";
case diagnostic_state::succeeded: return "SUCCEEDED";
default: return "FAILED";
}
}
std::size_t value_type_size( value_type const Type )
{
switch( Type )

View File

@@ -20,7 +20,7 @@ namespace hardware
// protocol version implemented by this build
constexpr std::uint8_t protocol_version_major = 2;
constexpr std::uint8_t protocol_version_minor = 0;
constexpr std::uint8_t protocol_version_minor = 1;
// 'M2', stored little-endian in the frame header
constexpr std::uint16_t protocol_magic = 0x324D;
@@ -36,6 +36,10 @@ constexpr std::size_t protocol_frame_size_max = 4096;
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;
constexpr std::size_t protocol_diagnostic_functions_max = 32;
constexpr std::size_t protocol_prompt_options_max = 4;
// device log lines kept per connection for the debug panel
constexpr std::size_t protocol_log_history = 64;
// message identifiers; the numbering is part of the public protocol and must not be reordered
enum class message_type : std::uint16_t
@@ -56,6 +60,7 @@ enum class message_type : std::uint16_t
device_info = 0x0010,
device_diagnostics = 0x0011,
device_log = 0x0012,
resolve_symbols = 0x0020,
symbols_resolved = 0x0021,
@@ -77,7 +82,14 @@ enum class message_type : std::uint16_t
property_read = 0x0050,
property_value = 0x0051,
property_write = 0x0052
property_write = 0x0052,
diagnostic_functions = 0x0060,
diagnostic_run = 0x0061,
diagnostic_cancel = 0x0062,
diagnostic_status = 0x0063,
diagnostic_prompt = 0x0064,
diagnostic_prompt_result = 0x0065
};
// header flag bits
@@ -189,9 +201,32 @@ enum capability_flag : std::uint32_t
capability_send_controls = 0x00000004,
capability_diagnostics = 0x00000008,
capability_soft_takeover = 0x00000010,
capability_control_leases = 0x00000020
capability_control_leases = 0x00000020,
// the device offers diagnostic routines which can be started from the debug panel
capability_diagnostic_functions = 0x00000040
};
// severity of a text line sent by the device
enum class log_severity : std::uint8_t
{
debug = 0,
info = 1,
warning = 2,
error = 3
};
// progress of a diagnostic routine running on the device
enum class diagnostic_state : std::uint8_t
{
idle = 0,
running = 1,
succeeded = 2,
failed = 3
};
// answer index reported when the user closes a prompt without choosing
constexpr std::uint8_t prompt_dismissed = 0xFF;
// session progress, mirrors the connection lifecycle of the specification
enum class session_state : std::uint8_t
{
@@ -216,6 +251,8 @@ 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 );
char const *to_string( log_severity const Severity );
char const *to_string( diagnostic_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 );

View File

@@ -108,6 +108,7 @@ void protocol_session::reset()
m_sequence = 0;
m_lastreceivedsequence = 0;
m_sequenceseen = false;
m_outgoingtransaction = 0;
m_framesize = std::clamp<std::size_t>( m_config.frame_size, protocol_frame_size_min, protocol_frame_size_max );
m_capabilities = 0;
m_peermajor = 0;
@@ -141,6 +142,10 @@ void protocol_session::reset()
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
@@ -376,6 +381,30 @@ void protocol_session::handle_packet( decoded_packet const &Packet )
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;
@@ -906,6 +935,212 @@ error_code protocol_session::execute_command( std::size_t const SymbolIndex, com
return error_code::none;
}
void protocol_session::handle_device_log( decoded_packet const &Packet )
{
payload_reader reader( Packet.payload );
auto const severity = static_cast<log_severity>( 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<diagnostic_function> 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<diagnostic_state>( 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<message_type>( reader.read_uint16() );
auto const error = ( Positive ? error_code::none : static_cast<error_code>( 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<state_item> const &Items, bool const Snapshot )
{
if( true == Items.empty() )
@@ -1052,6 +1287,10 @@ void protocol_session::fill_report( link_report &Report ) const
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

View File

@@ -56,6 +56,16 @@ public:
// copies the current condition into the debug report
void fill_report( link_report &Report ) const;
// ---- diagnostics offered by the device, driven from the debug panel ----
// starts one of the routines the device advertised; false when it isn't available
bool start_diagnostic( std::uint8_t const Function );
void cancel_diagnostic();
// answers the message box the device asked the simulator to show
void answer_prompt( std::uint8_t const Option );
bool has_prompt() const { return m_prompt.active; }
diagnostic_prompt const &prompt() const { return m_prompt; }
std::vector<diagnostic_function> const &functions() const { return m_functions; }
session_state state() const { return m_state; }
link_state link_condition() const;
protocol_diagnostics const &diagnostics() const { return m_diagnostics; }
@@ -84,6 +94,11 @@ private:
void handle_control_set( decoded_packet const &Packet );
void handle_device_diagnostics( decoded_packet const &Packet );
void handle_heartbeat( decoded_packet const &Packet );
void handle_device_log( decoded_packet const &Packet );
void handle_diagnostic_functions( decoded_packet const &Packet );
void handle_diagnostic_status( decoded_packet const &Packet );
void handle_diagnostic_prompt( decoded_packet const &Packet );
void handle_device_answer( decoded_packet const &Packet, bool const Positive );
void send_state_update( std::vector<state_item> const &Items, bool const Snapshot );
void send_symbol_context_changed( std::uint32_t const Context );
@@ -109,6 +124,8 @@ private:
std::uint32_t m_sessionid = 0;
std::uint32_t m_sequence = 0;
std::uint32_t m_lastreceivedsequence = 0;
// transaction numbering for the few requests the simulator itself makes
std::uint32_t m_outgoingtransaction = 0;
bool m_sequenceseen = false;
std::size_t m_framesize = protocol_frame_size_default;
std::uint32_t m_capabilities = 0;
@@ -150,6 +167,10 @@ private:
protocol_diagnostics m_diagnostics;
device_diagnostics m_devicediagnostics;
std::vector<diagnostic_function> m_functions;
diagnostic_report m_diagnostic;
diagnostic_prompt m_prompt;
std::deque<device_log_entry> m_log;
rate_counter m_rxrate;
rate_counter m_txrate;
};