From 91be90548fb20f129ccd4e3778008eee209d37ad Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 5 Sep 2026 20:35:04 +0000 Subject: [PATCH] hardware protocol v2: configuration, wiring and debug panel Adds the WITH_HARDWARE_PROTOCOL_V2 build option and the eu07.ini entries which configure it: uart2 declares a serial link and may be repeated for several controllers, the remaining uart2* entries tune frame size, heartbeat, rate limits and logging. UART v1 is untouched and both may be active at the same time on different ports. The driver mode starts the manager when at least one link is configured and pumps it from the input polling loop, and the debug panel gains a Hardware Protocol v2 section reporting link and session condition, packet rates, round trip time, protocol and transport error counters and whatever diagnostics the device itself reports, with switches for protocol and frame logging. --- CMakeLists.txt | 19 +++++ application/drivermode.cpp | 13 +++ application/drivermode.h | 6 ++ application/driveruipanels.cpp | 113 +++++++++++++++++++++++++ application/driveruipanels.h | 6 +- hardware/hardware_config.h | 10 +++ hardware/hardware_manager.cpp | 19 ++++- hardware/hardware_manager.h | 6 +- hardware/protocol/protocol_session.cpp | 8 +- utilities/Globals.cpp | 71 ++++++++++++++++ utilities/Globals.h | 7 ++ 11 files changed, 269 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 56cf1bce..96441e04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ option(WITH_OPENGL_MODERN "Compile with OpenGL modern renderer" ON) option(WITH_OPENGL_LEGACY "Compile with OpenGL legacy renderer" ON) option(WITH_PYTHON "Python 3.14 embedded scripting" ON) option(WITH_UART "Compile with libserialport" ON) +option(WITH_HARDWARE_PROTOCOL_V2 "Compile with MaSzyna Hardware Protocol v2 (UART v2)" ON) option(WITH_LUA "Compile with lua scripting support" ON) option(WITH_OPENVR "Compile with OpenVR" OFF) option(WITH_ZMQ "Compile with cppzmq" OFF) @@ -267,6 +268,24 @@ if (WITH_UART) set(SOURCES ${SOURCES} "utilities/uart.cpp") endif() +if (WITH_HARDWARE_PROTOCOL_V2) + set(DEFINITIONS ${DEFINITIONS} "WITH_HARDWARE_PROTOCOL_V2") + set(SOURCES ${SOURCES} + "hardware/hardware_manager.cpp" + "hardware/diagnostics/hardware_diagnostics.cpp" + "hardware/protocol/protocol_codec.cpp" + "hardware/protocol/protocol_defs.cpp" + "hardware/protocol/protocol_session.cpp" + "hardware/registry/command_registry.cpp" + "hardware/registry/hardware_symbol_registry.cpp" + "hardware/registry/state_registry.cpp" + "hardware/subscriptions/subscription_manager.cpp") + if (WITH_UART) + # the serial transport is the only one which needs libserialport + set(SOURCES ${SOURCES} "hardware/transport/serial_transport.cpp") + endif() +endif() + if (WITH_ZMQ) set(DEFINITIONS ${DEFINITIONS} "WITH_ZMQ") set(SOURCES ${SOURCES} "input/zmq_input.cpp") diff --git a/application/drivermode.cpp b/application/drivermode.cpp index 80fd3b17..9e15873a 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -55,6 +55,13 @@ void driver_mode::drivermode_input::poll() uart->poll(); } #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 + if (hardware != nullptr) + { + // exchanges frames already gathered by the hardware worker thread, never touches the port itself + hardware->update(); + } +#endif #ifdef WITH_ZMQ if (zmq != nullptr) { @@ -86,6 +93,12 @@ bool driver_mode::drivermode_input::init() uart->init(); } #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 + if (true == Global.hardware_conf.enable) + { + hardware = std::make_unique(Global.hardware_conf); + } +#endif #ifdef WITH_ZMQ if (!Global.zmq_address.empty()) { diff --git a/application/drivermode.h b/application/drivermode.h index 66ff6936..17edc575 100644 --- a/application/drivermode.h +++ b/application/drivermode.h @@ -20,6 +20,9 @@ http://mozilla.org/MPL/2.0/. #ifdef WITH_UART #include "utilities/uart.h" #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 +#include "hardware/hardware_manager.h" +#endif #ifdef WITH_ZMQ #include "input/zmq_input.h" #endif @@ -76,6 +79,9 @@ private: #ifdef WITH_UART std::unique_ptr uart; #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 + std::unique_ptr hardware; +#endif #ifdef WITH_ZMQ std::unique_ptr zmq; #endif diff --git a/application/driveruipanels.cpp b/application/driveruipanels.cpp index 729b854c..46eb96f8 100644 --- a/application/driveruipanels.cpp +++ b/application/driveruipanels.cpp @@ -38,6 +38,10 @@ http://mozilla.org/MPL/2.0/. #include "utilities/uart.h" #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 +#include "hardware/hardware_manager.h" +#endif + void drivingaid_panel::update() { @@ -544,6 +548,9 @@ void debug_panel::update() #ifdef WITH_UART m_uartlines.clear(); #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 + m_hardwarelines.clear(); +#endif update_section_vehicle( m_vehiclelines ); update_section_engine( m_enginelines ); @@ -557,6 +564,9 @@ void debug_panel::update() #ifdef WITH_UART update_section_uart(m_uartlines); #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 + update_section_hardware(m_hardwarelines); +#endif } void @@ -620,6 +630,18 @@ debug_panel::render() { ImGui::Combo("Baud", &UartStatus.selected_baud_index, uart_baudrates_list, uart_baudrates_list_num); ImGui::Checkbox("Enabled", &UartStatus.enabled); } +#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; + } + } #endif // toggles ImGui::Separator(); @@ -1311,6 +1333,97 @@ debug_panel::update_section_uart( std::vector &Output ) { } #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 +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; + } + + for( auto const &report : manager->reports() ) { + + auto const &protocol = report.protocol; + + std::string textline = + ( report.device_name.empty() ? std::string( "(waiting for device)" ) : report.device_name ) + + ( report.device_role.empty() ? "" : " [" + report.device_role + "]" ) + + ( report.device_id.empty() ? "" : " id: " + report.device_id ) + + ( report.firmware_version.empty() ? "" : " fw: " + report.firmware_version ); + + textline += + "\n" + report.transport_kind + " " + report.endpoint + + " " + hardware::to_string( report.state ) + + " / " + hardware::to_string( report.session ); + + if( report.session_id != 0 ) { + textline += + "\nprotocol v" + std::to_string( report.protocol_major ) + "." + std::to_string( report.protocol_minor ) + + " session " + std::to_string( report.session_id ) + + " frame " + std::to_string( report.frame_size ) + " b" + + " handles " + std::to_string( report.handles ) + + " subscriptions " + std::to_string( report.subscriptions ); + } + + textline += + "\nrx " + to_string( protocol.rx_rate, 0 ) + " pkt/s" + + " tx " + to_string( protocol.tx_rate, 0 ) + " pkt/s" + + " rtt " + to_string( protocol.round_trip_time_ms, 1 ) + " ms" + + " last frame " + to_string( protocol.last_rx_age, 2 ) + " s ago"; + + textline += + "\nframes rx " + std::to_string( protocol.rx_frames ) + " tx " + std::to_string( protocol.tx_frames ) + + " bytes rx " + std::to_string( report.transport.rx_bytes ) + " tx " + std::to_string( report.transport.tx_bytes ); + + textline += + "\nprotocol errors: crc " + std::to_string( protocol.crc_errors ) + + " frame " + std::to_string( protocol.frame_errors ) + + " length " + std::to_string( protocol.length_errors ) + + " version " + std::to_string( protocol.version_errors ) + + " gaps " + std::to_string( protocol.sequence_gaps ); + + textline += + "\nunknown: message " + std::to_string( protocol.unknown_messages ) + + " symbol " + std::to_string( protocol.unknown_symbols ) + + " handle " + std::to_string( protocol.unknown_handles ) + + " duplicates " + std::to_string( protocol.duplicate_commands ) + + " rate limited " + std::to_string( protocol.rate_limited ); + + textline += + "\nack " + std::to_string( protocol.acks ) + + " nack " + std::to_string( protocol.nacks ) + + " state updates " + std::to_string( protocol.state_updates ) + + " commands " + std::to_string( protocol.commands_executed ) + + " controls " + std::to_string( protocol.controls_applied ); + + textline += + "\nsession uptime " + to_string( protocol.connection_uptime, 0 ) + " s" + + " transport errors " + std::to_string( report.transport.transport_errors ) + + " reconnects " + std::to_string( report.transport.reconnects ); + + if( false == report.transport.last_error.empty() ) { + textline += "\nlast transport error: " + report.transport.last_error; + } + + if( true == report.device.available ) { + textline += + "\ndevice: uptime " + std::to_string( report.device.uptime_ms / 1000 ) + " s" + + ", supply " + to_string( report.device.supply_voltage, 2 ) + " V" + + ", temperature " + to_string( report.device.temperature, 1 ) + " deg C" + + ", overflows rx " + std::to_string( report.device.rx_overflows ) + " tx " + std::to_string( report.device.tx_overflows ) + + ", watchdog resets " + std::to_string( report.device.watchdog_resets ); + if( false == report.device.firmware_build.empty() ) { + textline += ", build " + report.device.firmware_build; + } + } + + Output.emplace_back( textline, Global.UITextColor ); + } +} +#endif + void debug_panel::update_section_scenario( std::vector &Output ) { diff --git a/application/driveruipanels.h b/application/driveruipanels.h index da4cad87..2af4513f 100644 --- a/application/driveruipanels.h +++ b/application/driveruipanels.h @@ -91,6 +91,9 @@ private: void update_section_renderer( std::vector &Output ); #ifdef WITH_UART void update_section_uart( std::vector &Output ); +#endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 + void update_section_hardware( std::vector &Output ); #endif // section update helpers std::string update_vehicle_coupler( int const Side ); @@ -116,7 +119,8 @@ private: m_eventqueuelines, m_powergridlines, m_rendererlines, - m_uartlines; + m_uartlines, + m_hardwarelines; double last_time = std::numeric_limits::quiet_NaN(); diff --git a/hardware/hardware_config.h b/hardware/hardware_config.h index e6283a3b..3010cb32 100644 --- a/hardware/hardware_config.h +++ b/hardware/hardware_config.h @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include @@ -53,4 +54,13 @@ struct config bool debug_frames = false; }; +// logging switches which can be flipped at run time from the simulator debug panel +struct debug_switches +{ + std::atomic log_messages { false }; + std::atomic log_frames { false }; +}; + +extern debug_switches debug_flags; + } // namespace hardware diff --git a/hardware/hardware_manager.cpp b/hardware/hardware_manager.cpp index a9c701ba..a2443aa1 100644 --- a/hardware/hardware_manager.cpp +++ b/hardware/hardware_manager.cpp @@ -30,6 +30,8 @@ constexpr std::size_t read_chunk_size = 4096; } // anonymous namespace +debug_switches debug_flags; + hardware_manager *hardware_manager::s_instance = nullptr; hardware_link::hardware_link( std::unique_ptr Transport, config const &Config ) : m_transport( std::move( Transport ) ), m_config( Config ), m_session( *this, Config ) @@ -224,6 +226,9 @@ hardware_manager::hardware_manager( config const &Config ) : m_config( Config ) } #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() ) @@ -259,18 +264,28 @@ void hardware_manager::worker() } } -void hardware_manager::update( double const Deltatime ) +void hardware_manager::update() { if( true == m_links.empty() ) { return; } + // the input polling code doesn't carry a time step, so the sessions get their own + auto const now = std::chrono::steady_clock::now(); + double deltatime = 0.0; + if( true == m_updatestarted ) + { + deltatime = std::clamp( std::chrono::duration( now - m_lastupdate ).count(), 0.0, 0.5 ); + } + m_updatestarted = true; + m_lastupdate = now; + state_registry::capture( m_snapshot ); for( std::size_t index = 0; index < m_links.size(); ++index ) { - m_links[ index ]->update( Deltatime, m_snapshot ); + m_links[ index ]->update( deltatime, m_snapshot ); m_reports[ index ] = m_links[ index ]->report(); } } diff --git a/hardware/hardware_manager.h b/hardware/hardware_manager.h index 7647c6ea..058cb822 100644 --- a/hardware/hardware_manager.h +++ b/hardware/hardware_manager.h @@ -82,8 +82,8 @@ public: explicit hardware_manager( config const &Config ); ~hardware_manager(); - // called once per simulation frame from the input polling code - void update( double const Deltatime ); + // called once per simulation frame from the input polling code; the time step is measured here + void update(); std::vector const &reports() const { return m_reports; } bool active() const { return false == m_links.empty(); } @@ -102,6 +102,8 @@ private: std::vector> m_links; std::vector m_reports; state_snapshot m_snapshot; + std::chrono::steady_clock::time_point m_lastupdate; + bool m_updatestarted = false; std::thread m_thread; std::atomic m_quit { false }; }; diff --git a/hardware/protocol/protocol_session.cpp b/hardware/protocol/protocol_session.cpp index 3ac77e9b..3b650a78 100644 --- a/hardware/protocol/protocol_session.cpp +++ b/hardware/protocol/protocol_session.cpp @@ -182,7 +182,7 @@ void protocol_session::send( message_type const Type, std::uint8_t const Flags, ++m_diagnostics.tx_frames; m_txrate.add(); - if( true == m_config.debug_frames ) + if( true == debug_flags.log_frames ) { WriteLog( "hardware: tx " + std::string( to_string( Type ) ) + ", " + std::to_string( Payload.size() ) + " bytes" ); } @@ -206,7 +206,7 @@ void protocol_session::send_nack( std::uint32_t const Transaction, message_type send( message_type::nack, packetflag_response | packetflag_error, writer ); ++m_diagnostics.nacks; - if( true == m_config.debug ) + if( true == debug_flags.log_messages ) { WriteLog( "hardware: nack " + std::string( to_string( Type ) ) + ", " + std::string( to_string( Error ) ) ); } @@ -318,7 +318,7 @@ void protocol_session::handle_packet( decoded_packet const &Packet ) m_lastreceivedsequence = Packet.header.sequence; m_sequenceseen = true; - if( true == m_config.debug_frames ) + if( true == debug_flags.log_frames ) { WriteLog( "hardware: rx " + std::string( to_string( Packet.header.type ) ) + ", " + std::to_string( Packet.payload.size() ) + " bytes" ); } @@ -366,7 +366,7 @@ void protocol_session::handle_packet( decoded_packet const &Packet ) case message_type::device_ready: m_state = session_state::ready; - if( true == m_config.debug ) + if( true == debug_flags.log_messages ) { WriteLog( "hardware: device '" + m_devicename + "' is ready" ); } diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 0ab064b4..3c40ee5d 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1040,6 +1040,77 @@ bool global_settings::ConfigParseHardware(cParser& Parser, const std::string& to } #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 + // hardware protocol v2 (uart v2). the legacy uart entries above keep working unchanged, both + // protocols may be used at the same time as long as they don't share a port + if (token == "uart2") + { + Parser.getTokens(2, false); + hardware::serial_link_config link; + Parser >> link.port + >> link.baud; + if (false == link.port.empty()) + { + hardware_conf.serial_links.emplace_back(link); + hardware_conf.enable = true; + } + return true; + } + + if (token == "uart2framesize") + { + int framesize = static_cast(hardware_conf.frame_size); + ParseOneClamped(Parser, framesize, static_cast(hardware::protocol_frame_size_min), static_cast(hardware::protocol_frame_size_max)); + hardware_conf.frame_size = static_cast(framesize); + return true; + } + + if (token == "uart2heartbeat") + { + ParseOneClamped(Parser, hardware_conf.heartbeat_interval_ms, 50, 10000); + return true; + } + + if (token == "uart2timeout") + { + ParseOneClamped(Parser, hardware_conf.heartbeat_timeout_count, 2, 100); + return true; + } + + if (token == "uart2ratelimit") + { + Parser.getTokens(3); + Parser >> hardware_conf.max_frames_per_second + >> hardware_conf.max_payload_bytes_per_second + >> hardware_conf.max_commands_per_second; + return true; + } + + if (token == "uart2pollinterval") + { + ParseOneClamped(Parser, hardware_conf.poll_interval_ms, 1, 100); + return true; + } + + if (token == "uart2reconnect") + { + ParseOneClamped(Parser, hardware_conf.reconnect_interval, 0.1f, 60.0f); + return true; + } + + if (token == "uart2debug") + { + ParseOne(Parser, hardware_conf.debug, 1); + return true; + } + + if (token == "uart2debugframes") + { + ParseOne(Parser, hardware_conf.debug_frames, 1); + return true; + } +#endif + #ifdef WITH_ZMQ if (token == "zmq.address") { diff --git a/utilities/Globals.h b/utilities/Globals.h index c4bd26f6..0df9f818 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -20,6 +20,9 @@ http://mozilla.org/MPL/2.0/. #ifdef WITH_UART #include "utilities/uart.h" #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 +#include "hardware/hardware_config.h" +#endif #ifdef WITH_ZMQ #include "input/zmq_input.h" #endif @@ -228,6 +231,10 @@ struct global_settings { {"dynamicbrake", &uart_conf.dynamicenable}, }; #endif +#ifdef WITH_HARDWARE_PROTOCOL_V2 + // hardware protocol v2; independent of the legacy uart link above, both may run at the same time + hardware::config hardware_conf; +#endif #ifdef WITH_ZMQ std::string zmq_address; #endif