diff --git a/application/drivermode.cpp b/application/drivermode.cpp index 9e15873a..f9a6dfe2 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -94,10 +94,8 @@ bool driver_mode::drivermode_input::init() } #endif #ifdef WITH_HARDWARE_PROTOCOL_V2 - if (true == Global.hardware_conf.enable) - { - hardware = std::make_unique(Global.hardware_conf); - } + // started even with no links configured, so that a controller can be added from the debug panel + hardware = std::make_unique(Global.hardware_conf); #endif #ifdef WITH_ZMQ if (!Global.zmq_address.empty()) diff --git a/application/driveruipanels.cpp b/application/driveruipanels.cpp index 46eb96f8..b84b1086 100644 --- a/application/driveruipanels.cpp +++ b/application/driveruipanels.cpp @@ -571,6 +571,10 @@ void debug_panel::update() void debug_panel::render() { +#ifdef WITH_HARDWARE_PROTOCOL_V2 + // a controller waiting for an answer must be visible whether the panel is open or not + render_hardware_prompt(); +#endif if( false == is_open ) { return; } ImGui::PushFont(ui_layer::font_mono); @@ -632,15 +636,8 @@ debug_panel::render() { } #endif #ifdef WITH_HARDWARE_PROTOCOL_V2 - if( true == render_section( "Hardware Protocol v2", m_hardwarelines ) ) { - bool logmessages = hardware::debug_flags.log_messages; - if( ImGui::Checkbox( "Log protocol messages", &logmessages ) ) { - hardware::debug_flags.log_messages = logmessages; - } - bool logframes = hardware::debug_flags.log_frames; - if( ImGui::Checkbox( "Log every frame", &logframes ) ) { - hardware::debug_flags.log_frames = logframes; - } + if( true == ImGui::CollapsingHeader( "Hardware Protocol v2" ) ) { + render_section_hardware(); } #endif // toggles @@ -1334,15 +1331,22 @@ debug_panel::update_section_uart( std::vector &Output ) { #endif #ifdef WITH_HARDWARE_PROTOCOL_V2 + +namespace { + +char const *const hardware_baudrates[] = { + "9600", "19200", "38400", "57600", "115200", "230400", "250000", "500000", "1000000", "2000000" }; + +} // anonymous namespace + void debug_panel::update_section_hardware( std::vector &Output ) { auto const *manager = hardware::hardware_manager::instance(); - if( ( manager == nullptr ) || ( false == manager->active() ) ) { - Output.emplace_back( "(no links configured, add an \"uart2 \" entry to eu07.ini)", Global.UITextColor ); - return; - } + if( manager == nullptr ) { return; } + // one entry per link, in the same order as the reports; render_section_hardware() + // interleaves them with the controls of each link for( auto const &report : manager->reports() ) { auto const &protocol = report.protocol; @@ -1355,7 +1359,7 @@ debug_panel::update_section_hardware( std::vector &Output ) { textline += "\n" + report.transport_kind + " " + report.endpoint - + " " + hardware::to_string( report.state ) + + " " + ( report.enabled ? hardware::to_string( report.state ) : "OFF" ) + " / " + hardware::to_string( report.session ); if( report.session_id != 0 ) { @@ -1422,6 +1426,172 @@ debug_panel::update_section_hardware( std::vector &Output ) { Output.emplace_back( textline, Global.UITextColor ); } } + +void +debug_panel::render_section_hardware() { + + auto *manager = hardware::hardware_manager::instance(); + if( manager == nullptr ) { + ImGui::TextUnformatted( "(the hardware protocol is not running)" ); + return; + } + + auto const &reports = manager->reports(); + if( m_hardwarefunction.size() < reports.size() ) { + m_hardwarefunction.resize( reports.size(), 0 ); + } + + for( std::size_t index = 0; index < reports.size(); ++index ) { + + auto const &report = reports[ index ]; + ImGui::PushID( static_cast( index ) ); + ImGui::Separator(); + + if( index < m_hardwarelines.size() ) { + auto const &line = m_hardwarelines[ index ]; + ImGui::TextColored( ImVec4( line.color.r, line.color.g, line.color.b, line.color.a ), "%s", line.data.c_str() ); + } + + // switching a link off closes its port, so the controller can be reprogrammed + if( ImGui::Button( report.enabled ? "Disconnect" : "Connect" ) ) { + manager->set_link_enabled( index, false == report.enabled ); + } + ImGui::SameLine(); + if( ImGui::Button( "Remove" ) ) { + manager->remove_link( index ); + ImGui::PopID(); + break; + } + + if( false == report.functions.empty() ) { + + auto &selection = m_hardwarefunction[ index ]; + if( selection >= static_cast( report.functions.size() ) ) { selection = 0; } + if( selection < 0 ) { selection = 0; } + + std::vector names; + names.reserve( report.functions.size() ); + for( auto const &function : report.functions ) { + names.emplace_back( function.name.c_str() ); + } + + bool const running = ( report.diagnostic.state == hardware::diagnostic_state::running ); + + ImGui::SetNextItemWidth( 260 * Global.ui_scale ); + ImGui::Combo( "###diagnosticfunction", &selection, names.data(), static_cast( names.size() ) ); + ImGui::SameLine(); + if( true == running ) { + if( ImGui::Button( "Cancel" ) ) { + manager->cancel_diagnostic( index ); + } + } + else { + if( ImGui::Button( "Run diagnostic" ) ) { + manager->start_diagnostic( index, report.functions[ selection ].id ); + } + } + } + + if( report.diagnostic.state != hardware::diagnostic_state::idle ) { + ImGui::Text( + "diagnostic %u: %s %u%% %s", + static_cast( report.diagnostic.function ), + hardware::to_string( report.diagnostic.state ), + static_cast( report.diagnostic.progress ), + report.diagnostic.message.c_str() ); + } + + if( false == report.log.empty() ) { + if( true == ImGui::TreeNode( "Device log" ) ) { + ImGui::BeginChild( "###devicelog", ImVec2S( 0, 140 ), true ); + for( auto const &entry : report.log ) { + ImGui::Text( + "[%8.3f] %-7s %s", + entry.device_uptime_ms / 1000.0, + hardware::to_string( entry.severity ), + entry.text.c_str() ); + } + ImGui::EndChild(); + ImGui::TreePop(); + } + } + + ImGui::PopID(); + } + + // adding a controller which appeared after the simulation started + ImGui::Separator(); + m_hardwareportnames = manager->available_ports(); + m_hardwareports.clear(); + for( auto const &name : m_hardwareportnames ) { + m_hardwareports.emplace_back( name.c_str() ); + } + + if( true == m_hardwareports.empty() ) { + ImGui::TextUnformatted( "(no serial ports detected)" ); + } + else { + if( m_hardwareport >= static_cast( m_hardwareports.size() ) ) { m_hardwareport = -1; } + ImGui::SetNextItemWidth( 140 * Global.ui_scale ); + ImGui::Combo( "###hardwareport", &m_hardwareport, m_hardwareports.data(), static_cast( m_hardwareports.size() ) ); + ImGui::SameLine(); + ImGui::SetNextItemWidth( 120 * Global.ui_scale ); + ImGui::Combo( "###hardwarebaud", &m_hardwarebaud, hardware_baudrates, static_cast( sizeof( hardware_baudrates ) / sizeof( hardware_baudrates[ 0 ] ) ) ); + ImGui::SameLine(); + if( ImGui::Button( "Add controller" ) ) { + if( ( m_hardwareport >= 0 ) && ( m_hardwareport < static_cast( m_hardwareportnames.size() ) ) ) { + manager->add_serial_link( m_hardwareportnames[ m_hardwareport ], std::atoi( hardware_baudrates[ m_hardwarebaud ] ) ); + } + } + } + + ImGui::Separator(); + bool logmessages = hardware::debug_flags.log_messages; + if( ImGui::Checkbox( "Log protocol messages", &logmessages ) ) { + hardware::debug_flags.log_messages = logmessages; + } + bool logframes = hardware::debug_flags.log_frames; + if( ImGui::Checkbox( "Log every frame", &logframes ) ) { + hardware::debug_flags.log_frames = logframes; + } +} + +void +debug_panel::render_hardware_prompt() { + + auto *manager = hardware::hardware_manager::instance(); + if( manager == nullptr ) { return; } + + for( auto const &report : manager->reports() ) { + + if( false == report.prompt.active ) { continue; } + + auto const title = + ( report.device_name.empty() ? std::string( "Controller" ) : report.device_name ) + + "###hardwareprompt" + std::to_string( report.index ); + + bool open = true; + ImGui::SetNextWindowSize( ImVec2S( 420, 0 ), ImGuiCond_Appearing ); + if( true == ImGui::Begin( title.c_str(), &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize ) ) { + ImGui::TextWrapped( "%s", report.prompt.text.c_str() ); + ImGui::Separator(); + for( std::size_t option = 0; option < report.prompt.options.size(); ++option ) { + if( option > 0 ) { ImGui::SameLine(); } + ImGui::PushID( static_cast( option ) ); + if( ImGui::Button( report.prompt.options[ option ].c_str() ) ) { + manager->answer_prompt( report.index, static_cast( option ) ); + } + ImGui::PopID(); + } + } + ImGui::End(); + + if( false == open ) { + // closing the window without choosing tells the controller the user walked away + manager->answer_prompt( report.index, hardware::prompt_dismissed ); + } + } +} #endif void diff --git a/application/driveruipanels.h b/application/driveruipanels.h index 2af4513f..9df8acc3 100644 --- a/application/driveruipanels.h +++ b/application/driveruipanels.h @@ -94,6 +94,10 @@ private: #endif #ifdef WITH_HARDWARE_PROTOCOL_V2 void update_section_hardware( std::vector &Output ); + // interactive part of the hardware section: connect, disconnect, diagnostics + void render_section_hardware(); + // message box a controller asked the simulator to show; rendered as its own window + void render_hardware_prompt(); #endif // section update helpers std::string update_vehicle_coupler( int const Side ); @@ -122,6 +126,15 @@ private: m_uartlines, m_hardwarelines; +#ifdef WITH_HARDWARE_PROTOCOL_V2 + // debug panel state for the hardware protocol section + std::vector m_hardwarefunction; // selected diagnostic function, per link + std::vector m_hardwareports; // port names for the combo, valid for one frame + std::vector m_hardwareportnames; + int m_hardwareport { -1 }; + int m_hardwarebaud { 4 }; // 115200 +#endif + double last_time = std::numeric_limits::quiet_NaN(); struct graph_data diff --git a/hardware/diagnostics/hardware_diagnostics.h b/hardware/diagnostics/hardware_diagnostics.h index 2295c5f2..0028ced7 100644 --- a/hardware/diagnostics/hardware_diagnostics.h +++ b/hardware/diagnostics/hardware_diagnostics.h @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include "hardware/protocol/protocol_defs.h" @@ -89,6 +90,39 @@ struct device_diagnostics std::string firmware_build; }; +// one text line sent by the device +struct device_log_entry +{ + log_severity severity = log_severity::info; + std::uint32_t device_uptime_ms = 0; + std::string text; +}; + +// a diagnostic routine the device offers, listed in the debug panel +struct diagnostic_function +{ + std::uint8_t id = 0; + std::string name; +}; + +// progress of the routine currently running on the device +struct diagnostic_report +{ + diagnostic_state state = diagnostic_state::idle; + std::uint8_t function = 0; + std::uint8_t progress = 0; + std::string message; +}; + +// a message box the device asked the simulator to show +struct diagnostic_prompt +{ + bool active = false; + std::uint32_t id = 0; + std::string text; + std::vector options; +}; + // everything the debug panel needs about one connection, copied out once per frame struct link_report { @@ -106,9 +140,17 @@ struct link_report std::size_t handles = 0; std::size_t subscriptions = 0; std::size_t frame_size = 0; + // false when the link was switched off from the debug panel, which releases the port + bool enabled = true; + // index of the link, as the manager knows it + std::size_t index = 0; transport_diagnostics transport; protocol_diagnostics protocol; device_diagnostics device; + std::vector functions; + diagnostic_report diagnostic; + diagnostic_prompt prompt; + std::vector log; }; } // namespace hardware diff --git a/hardware/hardware_manager.cpp b/hardware/hardware_manager.cpp index 0254dce2..7406329a 100644 --- a/hardware/hardware_manager.cpp +++ b/hardware/hardware_manager.cpp @@ -27,6 +27,10 @@ constexpr std::size_t rx_queue_limit = 256; // frames waiting to be pushed out by the worker thread constexpr std::size_t tx_queue_limit = 512; constexpr std::size_t read_chunk_size = 4096; +// how often the list of serial ports is refreshed for the debug panel +constexpr float port_scan_interval = 2.0f; +// the worker has nothing to do when no link is configured +constexpr int idle_poll_interval_ms = 250; } // anonymous namespace @@ -40,9 +44,10 @@ hardware_link::hardware_link( std::unique_ptr Transport, con m_decoder.set_frame_size_limit( m_config.frame_size ); m_lastconnectattempt = std::chrono::steady_clock::now() - std::chrono::seconds( 10 ); + m_endpoint = m_transport->endpoint(); m_report.transport_kind = m_transport->kind(); - m_report.endpoint = m_transport->endpoint(); - m_transportstatus.endpoint = m_report.endpoint; + m_report.endpoint = m_endpoint; + m_transportstatus.endpoint = m_endpoint; } hardware_link::~hardware_link() @@ -50,6 +55,12 @@ hardware_link::~hardware_link() m_transport->disconnect(); } +void hardware_link::set_enabled( bool const Enabled ) +{ + m_enabled = Enabled; + m_report.enabled = Enabled; +} + void hardware_link::send_packet( packet_header const &Header, std::vector const &Payload ) { std::vector frame; @@ -66,6 +77,24 @@ void hardware_link::send_packet( packet_header const &Header, std::vectorconnected() ) + { + m_transport->disconnect(); + m_writebuffer.clear(); + m_decoder.reset(); + std::lock_guard lock( m_mutex ); + m_transportstatus.connected = false; + m_transportstatus.last_error.clear(); + m_transportdropped = true; + m_rxqueue.clear(); + m_txqueue.clear(); + } + return; + } + if( false == m_transport->connected() ) { auto const now = std::chrono::steady_clock::now(); @@ -213,7 +242,9 @@ void hardware_link::update( double const Deltatime, state_snapshot const &Snapsh m_session.update( Deltatime, Snapshot ); m_report.transport = status; + m_report.enabled = m_enabled; m_report.state = ( + false == m_enabled ? link_state::disconnected : false == status.connected ? link_state::disconnected : m_session.link_condition() ); m_session.fill_report( m_report ); @@ -221,23 +252,24 @@ void hardware_link::update( double const Deltatime, state_snapshot const &Snapsh hardware_manager::hardware_manager( config const &Config ) : m_config( Config ) { + debug_flags.log_messages = m_config.debug; + debug_flags.log_frames = m_config.debug_frames; + + m_lastportscan = std::chrono::steady_clock::now() - std::chrono::seconds( 10 ); + #ifdef WITH_UART for( auto const &link : m_config.serial_links ) { - m_links.emplace_back( std::make_unique( std::make_unique( link.port, link.baud ), m_config ) ); + m_links.emplace_back( std::make_shared( std::make_unique( link.port, link.baud ), m_config ) ); WriteLog( "hardware: protocol v2 link on " + link.port + " @ " + std::to_string( link.baud ) ); } #endif - debug_flags.log_messages = m_config.debug; - debug_flags.log_frames = m_config.debug_frames; - m_reports.resize( m_links.size() ); - if( false == m_links.empty() ) - { - m_thread = std::thread( &hardware_manager::worker, this ); - } + // the worker also runs with no links at all, so that ports can be scanned and controllers + // added from the debug panel while the simulation is running + m_thread = std::thread( &hardware_manager::worker, this ); s_instance = this; } @@ -250,27 +282,146 @@ hardware_manager::~hardware_manager() { m_thread.join(); } + std::lock_guard lock( m_linksmutex ); m_links.clear(); } void hardware_manager::worker() { - auto const interval = std::chrono::milliseconds( std::max( 1, m_config.poll_interval_ms ) ); - while( false == m_quit ) { - for( auto &link : m_links ) + std::vector> links; + { + std::lock_guard lock( m_linksmutex ); + links = m_links; + } + + for( auto &link : links ) { link->service(); } + +#ifdef WITH_UART + auto const now = std::chrono::steady_clock::now(); + if( std::chrono::duration( now - m_lastportscan ).count() > port_scan_interval ) + { + m_lastportscan = now; + auto ports = list_serial_ports(); + std::lock_guard lock( m_portsmutex ); + m_ports = std::move( ports ); + } +#endif + + auto const interval = std::chrono::milliseconds( links.empty() ? idle_poll_interval_ms : std::max( 1, m_config.poll_interval_ms ) ); std::this_thread::sleep_for( interval ); } } +std::shared_ptr hardware_manager::link_at( std::size_t const Index ) const +{ + std::lock_guard lock( m_linksmutex ); + return ( Index < m_links.size() ? m_links[ Index ] : nullptr ); +} + +std::vector hardware_manager::available_ports() const +{ + std::lock_guard lock( m_portsmutex ); + return m_ports; +} + +bool hardware_manager::add_serial_link( std::string const &Port, int const Baud ) +{ +#ifdef WITH_UART + if( true == Port.empty() ) + { + return false; + } + + std::lock_guard lock( m_linksmutex ); + for( auto const &link : m_links ) + { + if( link->endpoint().compare( 0, Port.size() + 1, Port + " " ) == 0 ) + { + // the port already has a link, enabled or not + return false; + } + } + + m_links.emplace_back( std::make_shared( std::make_unique( Port, Baud ), m_config ) ); + WriteLog( "hardware: protocol v2 link added on " + Port + " @ " + std::to_string( Baud ) ); + return true; +#else + ( void )Port; + ( void )Baud; + return false; +#endif +} + +bool hardware_manager::remove_link( std::size_t const Index ) +{ + std::lock_guard lock( m_linksmutex ); + if( Index >= m_links.size() ) + { + return false; + } + // the worker may still hold a reference for the rest of its pass; the port is released + // when that reference goes away + m_links[ Index ]->set_enabled( false ); + WriteLog( "hardware: protocol v2 link on " + m_links[ Index ]->endpoint() + " removed" ); + m_links.erase( m_links.begin() + Index ); + return true; +} + +void hardware_manager::set_link_enabled( std::size_t const Index, bool const Enabled ) +{ + auto const link = link_at( Index ); + if( link == nullptr ) + { + return; + } + if( link->enabled() == Enabled ) + { + return; + } + link->set_enabled( Enabled ); + WriteLog( "hardware: protocol v2 link on " + link->endpoint() + ( Enabled ? " connected" : " disconnected" ) ); +} + +bool hardware_manager::start_diagnostic( std::size_t const Index, std::uint8_t const Function ) +{ + auto const link = link_at( Index ); + return ( link != nullptr ? link->session().start_diagnostic( Function ) : false ); +} + +void hardware_manager::cancel_diagnostic( std::size_t const Index ) +{ + auto const link = link_at( Index ); + if( link != nullptr ) + { + link->session().cancel_diagnostic(); + } +} + +void hardware_manager::answer_prompt( std::size_t const Index, std::uint8_t const Option ) +{ + auto const link = link_at( Index ); + if( link != nullptr ) + { + link->session().answer_prompt( Option ); + } +} + void hardware_manager::update() { - if( true == m_links.empty() ) + std::vector> links; { + std::lock_guard lock( m_linksmutex ); + links = m_links; + } + + if( true == links.empty() ) + { + m_reports.clear(); return; } @@ -286,10 +437,12 @@ void hardware_manager::update() state_registry::capture( m_snapshot ); - for( std::size_t index = 0; index < m_links.size(); ++index ) + m_reports.resize( links.size() ); + for( std::size_t index = 0; index < links.size(); ++index ) { - m_links[ index ]->update( deltatime, m_snapshot ); - m_reports[ index ] = m_links[ index ]->report(); + links[ index ]->update( deltatime, m_snapshot ); + m_reports[ index ] = links[ index ]->report(); + m_reports[ index ].index = index; } } diff --git a/hardware/hardware_manager.h b/hardware/hardware_manager.h index 058cb822..d41e2610 100644 --- a/hardware/hardware_manager.h +++ b/hardware/hardware_manager.h @@ -28,6 +28,9 @@ http://mozilla.org/MPL/2.0/. // frames with it through small locked queues, and is the only one which ever touches simulation // state. a device which stops responding, floods the link or disappears therefore cannot stall the // simulation +// +// links may be added, switched off and removed while the simulation runs, which is what lets the +// debug panel release a port so that a controller can be reprogrammed and then reconnected namespace hardware { @@ -46,6 +49,13 @@ public: void update( double const Deltatime, state_snapshot const &Snapshot ); // simulation thread: the report gathered during the last update link_report const &report() const { return m_report; } + // simulation thread: the session, for the debug panel + protocol_session &session() { return m_session; } + + // switching a link off closes the port and stops reconnecting, which frees the device + void set_enabled( bool const Enabled ); + bool enabled() const { return m_enabled; } + std::string const &endpoint() const { return m_endpoint; } void send_packet( packet_header const &Header, std::vector const &Payload ) override; @@ -55,6 +65,8 @@ private: config m_config; protocol_session m_session; link_report m_report; + std::string m_endpoint; + std::atomic m_enabled { true }; // worker thread only frame_decoder m_decoder; @@ -86,7 +98,23 @@ public: void update(); std::vector const &reports() const { return m_reports; } - bool active() const { return false == m_links.empty(); } + bool active() const { return false == m_reports.empty(); } + + // ---- operations available from the debug panel, simulation thread only ---- + + // adds a serial link while the simulation runs; false if the port is already in use + bool add_serial_link( std::string const &Port, int const Baud ); + // removes a link; its port is released + bool remove_link( std::size_t const Index ); + // switching a link off releases its port without forgetting the link + void set_link_enabled( std::size_t const Index, bool const Enabled ); + + bool start_diagnostic( std::size_t const Index, std::uint8_t const Function ); + void cancel_diagnostic( std::size_t const Index ); + void answer_prompt( std::size_t const Index, std::uint8_t const Option ); + + // serial ports present in the system, refreshed by the worker thread + std::vector available_ports() const; // the running instance, if any; used by the debug panel static hardware_manager *instance() { return s_instance; } @@ -94,14 +122,21 @@ public: private: // methods void worker(); + std::shared_ptr link_at( std::size_t const Index ) const; // members static hardware_manager *s_instance; config m_config; - std::vector> m_links; + mutable std::mutex m_linksmutex; + std::vector> m_links; std::vector m_reports; state_snapshot m_snapshot; + + mutable std::mutex m_portsmutex; + std::vector m_ports; + std::chrono::steady_clock::time_point m_lastportscan; + std::chrono::steady_clock::time_point m_lastupdate; bool m_updatestarted = false; std::thread m_thread; diff --git a/hardware/protocol/protocol_defs.cpp b/hardware/protocol/protocol_defs.cpp index 5a752afa..6f3d8661 100644 --- a/hardware/protocol/protocol_defs.cpp +++ b/hardware/protocol/protocol_defs.cpp @@ -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 ) diff --git a/hardware/protocol/protocol_defs.h b/hardware/protocol/protocol_defs.h index 0d2b084a..10176536 100644 --- a/hardware/protocol/protocol_defs.h +++ b/hardware/protocol/protocol_defs.h @@ -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 ); diff --git a/hardware/protocol/protocol_session.cpp b/hardware/protocol/protocol_session.cpp index 3b650a78..1c54f95e 100644 --- a/hardware/protocol/protocol_session.cpp +++ b/hardware/protocol/protocol_session.cpp @@ -108,6 +108,7 @@ void protocol_session::reset() 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; @@ -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( 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() ) @@ -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 diff --git a/hardware/protocol/protocol_session.h b/hardware/protocol/protocol_session.h index f46cf90b..7bd67c2b 100644 --- a/hardware/protocol/protocol_session.h +++ b/hardware/protocol/protocol_session.h @@ -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 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 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 m_functions; + diagnostic_report m_diagnostic; + diagnostic_prompt m_prompt; + std::deque m_log; rate_counter m_rxrate; rate_counter m_txrate; };