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