From fcccba21aa82e53557b67ddee23de6c7eadd5b43 Mon Sep 17 00:00:00 2001 From: Adrian Del Grosso <10929341+ad3154@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:23:50 -0600 Subject: [PATCH 1/2] [VT]: Allow selection of VT client pools based on server version This change makes it possible for a consumer of the library to choose at runtime which object pool(s) to use based on the version of the VT server. --- .../isobus/isobus_virtual_terminal_client.hpp | 16 +++++++ isobus/src/isobus_virtual_terminal_client.cpp | 44 +++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/isobus/include/isobus/isobus/isobus_virtual_terminal_client.hpp b/isobus/include/isobus/isobus/isobus_virtual_terminal_client.hpp index 449c4be5..882039e4 100644 --- a/isobus/include/isobus/isobus/isobus_virtual_terminal_client.hpp +++ b/isobus/include/isobus/isobus/isobus_virtual_terminal_client.hpp @@ -330,6 +330,8 @@ namespace isobus Disconnected, ///< VT is not connected, and is not trying to connect yet WaitForPartnerVTStatusMessage, ///< VT client is initialized, waiting for a VT server to come online SendWorkingSetMasterMessage, ///< Client is sending the working state master message + SendGetMemoryInitial, ///< We send an initial get memory message for 0 bytes to get the version of the VT server + WaitForGetMemoryInitialResponse, ///< Client is waiting for a response to the initial ReadyForObjectPool, ///< Client needs an object pool before connection can continue SendGetMemory, ///< Client is sending the "get memory" message to see if VT has enough memory available WaitForGetMemoryResponse, ///< Client is waiting for a response to the "get memory" message @@ -1234,6 +1236,10 @@ namespace isobus const std::vector *pool, const std::string &version = ""); + /// @brief Returns the number of object pools that have been assigned for upload + /// @returns The number of object pools that have been assigned for upload + std::size_t get_number_of_object_pools() const; + /// @brief Configures an object pool to be automatically scaled to match the target VT server /// @param[in] poolIndex The index of the pool you want to auto-scale /// @param[in] originalDataMaskDimensions_px The data mask width that your object pool was originally designed for @@ -1252,6 +1258,15 @@ namespace isobus /// @param[in] version An optional version string. The stack will automatically store/load your pool from the VT if this is provided. void register_object_pool_data_chunk_callback(std::uint8_t poolIndex, std::uint32_t poolTotalSize, DataChunkCallback value, std::string version = ""); + /// @brief Sets a callback that will be called when the client is ready for object pool upload. You should call this before initialize if you're planning to use it. + /// @details This will be called once the VT version of the server is known so that you can select which + /// object pools you want to upload based on that information. This is optional. If you want to use this mechanism, + /// simply set your object pools in the callback using set_object_pool or register_object_pool_data_chunk_callback. + /// If you fail to do this when the callback is called, no object pools will be uploaded until you call set_object_pool, though that + /// would not be particularly thread safe unless you passed false to the initialize function. You can also just set your object pools before connecting if you want to always upload the same pools. + /// @param[in] callback The callback to call when ready for object pool upload + void set_on_ready_for_object_pool_callback(std::function callback); + /// @brief Periodic Update Function (worker thread may call this) /// @details This class can spawn a thread, or you can supply your own to run this function. /// To configure that behavior, see the initialize function. @@ -1676,6 +1691,7 @@ namespace isobus EventDispatcher auxiliaryFunctionEventDispatcher; ///< A list of all auxiliary function callbacks // Object Pool info + std::function onReadyForObjectPoolCallback; ///< The callback to get object pools for upload when the client is ready DataChunkCallback objectPoolDataCallback = nullptr; ///< The callback to use to get pool data std::uint32_t lastObjectPoolIndex = 0; ///< The last object pool index that was processed }; diff --git a/isobus/src/isobus_virtual_terminal_client.cpp b/isobus/src/isobus_virtual_terminal_client.cpp index 5646a915..193f5c4e 100644 --- a/isobus/src/isobus_virtual_terminal_client.cpp +++ b/isobus/src/isobus_virtual_terminal_client.cpp @@ -1264,6 +1264,11 @@ namespace isobus } } + std::size_t VirtualTerminalClient::get_number_of_object_pools() const + { + return objectPools.size(); + } + void VirtualTerminalClient::set_object_pool_scaling(std::uint8_t poolIndex, std::uint32_t originalDataMaskDimensions_px, std::uint32_t originalSoftKyeDesignatorHeight_px) @@ -1303,6 +1308,11 @@ namespace isobus } } + void VirtualTerminalClient::set_on_ready_for_object_pool_callback(std::function callback) + { + onReadyForObjectPoolCallback = callback; + } + void VirtualTerminalClient::update() { StateMachineState previousStateMachineState = state; // Save state to see if it changes this update @@ -1337,7 +1347,26 @@ namespace isobus { if (send_working_set_master()) { - set_state(StateMachineState::ReadyForObjectPool); + set_state(StateMachineState::SendGetMemoryInitial); + } + } + break; + + case StateMachineState::SendGetMemoryInitial: + { + if (send_get_memory(0)) + { + set_state(StateMachineState::WaitForGetMemoryInitialResponse); + } + } + break; + + case StateMachineState::WaitForGetMemoryInitialResponse: + { + if (SystemTiming::time_expired_ms(lastVTStatusTimestamp_ms, VT_STATUS_TIMEOUT_MS)) + { + LOG_ERROR("[VT]: VT server has timed out while waiting for initial get memory message. Disconnecting."); + set_state(StateMachineState::Disconnected); } } break; @@ -1352,6 +1381,11 @@ namespace isobus LOG_ERROR("[VT]: Ready to upload pool, but VT server has timed out. Disconnecting."); set_state(StateMachineState::Disconnected); } + else if (onReadyForObjectPoolCallback) + { + // Attempt to indicate to the user that we need at least one object pool to proceed. + onReadyForObjectPoolCallback(get_connected_vt_version()); + } if (!objectPools.empty()) { @@ -2774,10 +2808,14 @@ namespace isobus case static_cast(Function::GetMemoryMessage): { - if (StateMachineState::WaitForGetMemoryResponse == parentVT->state) + if (StateMachineState::WaitForGetMemoryInitialResponse == parentVT->state) { parentVT->connectedVTVersion = message.get_uint8_at(1); - + LOG_DEBUG("[VT]: Server version is " + isobus::to_string(static_cast(parentVT->connectedVTVersion))); + parentVT->set_state(StateMachineState::ReadyForObjectPool); + } + else if (StateMachineState::WaitForGetMemoryResponse == parentVT->state) + { if (0 == message.get_uint8_at(2)) { // There IS enough memory From 5861d490512bc2173b8996e7667e65b3822fe7e5 Mon Sep 17 00:00:00 2001 From: Adrian Del Grosso <10929341+ad3154@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:39:59 -0500 Subject: [PATCH 2/2] [Examples]: Add example app for choosing different (or multiple) VT object pools at runtime based on the VT server version --- CMakeLists.txt | 1 + .../multiple_object_pools/CMakeLists.txt | 31 +++ .../multiple_object_pools/console_logger.cpp | 67 ++++++ .../multiple_object_pools/main.cpp | 220 ++++++++++++++++++ .../multiple_object_pools/objectPoolObjects.h | 36 +++ .../multiple_object_pools/window_masks.iop | Bin 0 -> 55 bytes 6 files changed, 355 insertions(+) create mode 100644 examples/virtual_terminal/multiple_object_pools/CMakeLists.txt create mode 100644 examples/virtual_terminal/multiple_object_pools/console_logger.cpp create mode 100644 examples/virtual_terminal/multiple_object_pools/main.cpp create mode 100644 examples/virtual_terminal/multiple_object_pools/objectPoolObjects.h create mode 100644 examples/virtual_terminal/multiple_object_pools/window_masks.iop diff --git a/CMakeLists.txt b/CMakeLists.txt index ef101453..34c2bd4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ if(BUILD_EXAMPLES) add_subdirectory("examples/virtual_terminal/version3_object_pool") add_subdirectory("examples/virtual_terminal/aux_functions") add_subdirectory("examples/virtual_terminal/aux_inputs") + add_subdirectory("examples/virtual_terminal/multiple_object_pools") add_subdirectory("examples/task_controller_client") add_subdirectory("examples/task_controller_server") add_subdirectory("examples/guidance") diff --git a/examples/virtual_terminal/multiple_object_pools/CMakeLists.txt b/examples/virtual_terminal/multiple_object_pools/CMakeLists.txt new file mode 100644 index 00000000..f8459c37 --- /dev/null +++ b/examples/virtual_terminal/multiple_object_pools/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.16) +project(vt_client_multiple_object_pools_example) + +if(NOT BUILD_EXAMPLES) + find_package(isobus REQUIRED) +endif() +find_package(Threads REQUIRED) + +add_executable(VTClientMultiplePoolsExampleTarget main.cpp console_logger.cpp + objectPoolObjects.h) + +target_compile_features(VTClientMultiplePoolsExampleTarget PUBLIC cxx_std_11) +set_target_properties(VTClientMultiplePoolsExampleTarget + PROPERTIES CXX_EXTENSIONS OFF) + +target_link_libraries( + VTClientMultiplePoolsExampleTarget + PRIVATE isobus::Isobus isobus::HardwareIntegration Threads::Threads + isobus::Utility) + +add_custom_command( + TARGET VTClientMultiplePoolsExampleTarget + POST_BUILD + COMMENT "Copying object pools to build directory" + COMMAND + ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/../version3_object_pool/VT3TestPool.iop + $/VT3TestPool.iop + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/window_masks.iop + $/window_masks.iop +) diff --git a/examples/virtual_terminal/multiple_object_pools/console_logger.cpp b/examples/virtual_terminal/multiple_object_pools/console_logger.cpp new file mode 100644 index 00000000..2eb71263 --- /dev/null +++ b/examples/virtual_terminal/multiple_object_pools/console_logger.cpp @@ -0,0 +1,67 @@ +#include "isobus/isobus/can_stack_logger.hpp" + +#include + +// A log sink for the CAN stack +class CustomLogger : public isobus::CANStackLogger +{ +public: + void sink_CAN_stack_log(CANStackLogger::LoggingLevel level, const std::string &text) override + { + switch (level) + { + case LoggingLevel::Debug: + { + std::cout << "[" + << "\033[1;36m" + << "Debug" + << "\033[0m" + << "]"; + } + break; + + case LoggingLevel::Info: + { + std::cout << "[" + << "\033[1;32m" + << "Info" + << "\033[0m" + << "]"; + } + break; + + case LoggingLevel::Warning: + { + std::cout << "[" + << "\033[1;33m" + << "Warn" + << "\033[0m" + << "]"; + } + break; + + case LoggingLevel::Error: + { + std::cout << "[" + << "\033[1;31m" + << "Error" + << "\033[0m" + << "]"; + } + break; + + case LoggingLevel::Critical: + { + std::cout << "[" + << "\033[1;35m" + << "Critical" + << "\033[0m" + << "]"; + } + break; + } + std::cout << text << std::endl; // Write the text to stdout + } +}; + +static CustomLogger logger; diff --git a/examples/virtual_terminal/multiple_object_pools/main.cpp b/examples/virtual_terminal/multiple_object_pools/main.cpp new file mode 100644 index 00000000..2b55a56a --- /dev/null +++ b/examples/virtual_terminal/multiple_object_pools/main.cpp @@ -0,0 +1,220 @@ +#include "isobus/hardware_integration/available_can_drivers.hpp" +#include "isobus/hardware_integration/can_hardware_interface.hpp" +#include "isobus/isobus/can_network_manager.hpp" +#include "isobus/isobus/can_partnered_control_function.hpp" +#include "isobus/isobus/can_stack_logger.hpp" +#include "isobus/isobus/isobus_virtual_terminal_client.hpp" +#include "isobus/isobus/isobus_virtual_terminal_client_update_helper.hpp" +#include "isobus/utility/iop_file_interface.hpp" + +#include "console_logger.cpp" +#include "objectPoolObjects.h" + +#include +#include +#include + +//! It is discouraged to use global variables, but it is done here for simplicity. +static std::shared_ptr virtualTerminalClient = nullptr; +static std::shared_ptr virtualTerminalUpdateHelper = nullptr; +static std::atomic_bool running = { true }; + +void signal_handler(int) +{ + running = false; +} + +// This callback will provide us with event driven notifications of softkey presses from the stack +void handle_softkey_event(const isobus::VirtualTerminalClient::VTKeyEvent &event) +{ + if (event.keyNumber == 0) + { + // We have the alarm ACK code, so if we have an active alarm, acknowledge it by going back to the main runscreen + virtualTerminalUpdateHelper->set_active_data_or_alarm_mask(example_WorkingSet, mainRunscreen_DataMask); + } + + switch (event.keyEvent) + { + case isobus::VirtualTerminalClient::KeyActivationCode::ButtonUnlatchedOrReleased: + { + switch (event.objectID) + { + case alarm_SoftKey: + { + virtualTerminalUpdateHelper->set_active_data_or_alarm_mask(example_WorkingSet, example_AlarmMask); + } + break; + + case acknowledgeAlarm_SoftKey: + { + virtualTerminalUpdateHelper->set_active_data_or_alarm_mask(example_WorkingSet, mainRunscreen_DataMask); + } + break; + + default: + break; + } + } + break; + + default: + break; + } +} + +// This callback will provide us with event driven notifications of button presses from the stack +void handle_button_event(const isobus::VirtualTerminalClient::VTKeyEvent &event) +{ + switch (event.keyEvent) + { + case isobus::VirtualTerminalClient::KeyActivationCode::ButtonUnlatchedOrReleased: + case isobus::VirtualTerminalClient::KeyActivationCode::ButtonStillHeld: + { + switch (event.objectID) + { + case Plus_Button: + { + virtualTerminalUpdateHelper->increase_numeric_value(ButtonExampleNumber_VarNum); + } + break; + + case Minus_Button: + { + virtualTerminalUpdateHelper->decrease_numeric_value(ButtonExampleNumber_VarNum); + } + break; + + default: + break; + } + } + break; + + default: + break; + } +} + +int main() +{ + std::signal(SIGINT, signal_handler); + + // Automatically load the desired CAN driver based on the available drivers + std::shared_ptr canDriver = nullptr; +#if defined(ISOBUS_SOCKETCAN_AVAILABLE) + canDriver = std::make_shared("can0"); +#elif defined(ISOBUS_WINDOWSPCANBASIC_AVAILABLE) + canDriver = std::make_shared(PCAN_USBBUS1); +#elif defined(ISOBUS_WINDOWSINNOMAKERUSB2CAN_AVAILABLE) + canDriver = std::make_shared(0); // CAN0 +#elif defined(ISOBUS_MACCANPCAN_AVAILABLE) + canDriver = std::make_shared(PCAN_USBBUS1); +#elif defined(ISOBUS_SYS_TEC_AVAILABLE) + canDriver = std::make_shared(); +#endif + if (nullptr == canDriver) + { + std::cout << "Unable to find a CAN driver. Please make sure you have one of the above drivers installed with the library." << std::endl; + std::cout << "If you want to use a different driver, please add it to the list above." << std::endl; + return -1; + } + + isobus::CANStackLogger::set_can_stack_logger_sink(&logger); + isobus::CANStackLogger::set_log_level(isobus::CANStackLogger::LoggingLevel::Info); // Change this to Debug to see more information + isobus::CANHardwareInterface::set_number_of_can_channels(1); + isobus::CANHardwareInterface::assign_can_channel_frame_handler(0, canDriver); + + if ((!isobus::CANHardwareInterface::start()) || (!canDriver->get_is_valid())) + { + std::cout << "Failed to start hardware interface. The CAN driver might be invalid." << std::endl; + return -2; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + isobus::NAME TestDeviceNAME(0); + + //! Make sure you change these for your device!!!! + TestDeviceNAME.set_arbitrary_address_capable(true); + TestDeviceNAME.set_industry_group(1); + TestDeviceNAME.set_device_class(0); + TestDeviceNAME.set_function_code(static_cast(isobus::NAME::Function::SteeringControl)); + TestDeviceNAME.set_identity_number(2); + TestDeviceNAME.set_ecu_instance(0); + TestDeviceNAME.set_function_instance(0); + TestDeviceNAME.set_device_class_instance(0); + TestDeviceNAME.set_manufacturer_code(1407); + + std::vector version3pool = isobus::IOPFileInterface::read_iop_file("VT3TestPool.iop"); + std::vector version4pool = isobus::IOPFileInterface::read_iop_file("window_masks.iop"); + + if (version3pool.empty()) + { + std::cout << "Failed to load object pool from VT3TestPool.iop" << std::endl; + return -3; + } + std::cout << "Loaded object pool from VT3TestPool.iop" << std::endl; + + if (version4pool.empty()) + { + std::cout << "Failed to load object pool from window_masks.iop" << std::endl; + return -4; + } + std::cout << "Loaded object pool from window_masks.iop" << std::endl; + + // Generate a unique version string for this object pool (this is optional, and is entirely application specific behavior) + std::string objectPoolHash = ""; + + const isobus::NAMEFilter filterVirtualTerminal(isobus::NAME::NAMEParameters::FunctionCode, static_cast(isobus::NAME::Function::VirtualTerminal)); + const std::vector vtNameFilters = { filterVirtualTerminal }; + auto TestInternalECU = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(TestDeviceNAME, 0); + auto TestPartnerVT = isobus::CANNetworkManager::CANNetwork.create_partnered_control_function(0, vtNameFilters); + + virtualTerminalClient = std::make_shared(TestPartnerVT, TestInternalECU); + virtualTerminalClient->get_vt_soft_key_event_dispatcher().add_listener(handle_softkey_event); + virtualTerminalClient->get_vt_button_event_dispatcher().add_listener(handle_button_event); + virtualTerminalClient->set_on_ready_for_object_pool_callback([&version3pool, &version4pool, objectPoolHash](isobus::VirtualTerminalClient::VTVersion version) { + // You can check the connected VT version if you need to know what features are available, and select which object pool(s) to use based on that. + // This is optional though. If you want, you can just call set_object_pool() blindly exactly one time at any point if you want to try to use the same object pool for all VT versions. + switch (virtualTerminalClient->get_connected_vt_version()) + { + case isobus::VirtualTerminalClient::VTVersion::Version3: + { + // For version 3, we upload a base pool with only VT version 3 complaint objects + virtualTerminalClient->set_object_pool(0, version3pool.data(), version3pool.size(), objectPoolHash); + } + break; + + case isobus::VirtualTerminalClient::VTVersion::Version4: + case isobus::VirtualTerminalClient::VTVersion::Version5: + case isobus::VirtualTerminalClient::VTVersion::Version6: + { + // For version 4, 5, and 6, we upload the same base pool as version 3, but also upload a second pool with version 4 objects + virtualTerminalClient->set_object_pool(0, version3pool.data(), version3pool.size(), objectPoolHash); + virtualTerminalClient->set_object_pool(1, version4pool.data(), version4pool.size(), objectPoolHash); + } + break; + + default: + { + // Either we're not ready yet, or we don't have an object pool for this version + } + break; + } + }); + virtualTerminalClient->initialize(true); + + virtualTerminalUpdateHelper = std::make_shared(virtualTerminalClient); + virtualTerminalUpdateHelper->add_tracked_numeric_value(ButtonExampleNumber_VarNum, 214748364); // In the object pool the output number has an offset of -214748364 so we use this to represent 0. + virtualTerminalUpdateHelper->initialize(); + + while (running) + { + // CAN stack runs in other threads. Do nothing forever. + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + + virtualTerminalClient->terminate(); + isobus::CANHardwareInterface::stop(); + return 0; +} diff --git a/examples/virtual_terminal/multiple_object_pools/objectPoolObjects.h b/examples/virtual_terminal/multiple_object_pools/objectPoolObjects.h new file mode 100644 index 00000000..bd502572 --- /dev/null +++ b/examples/virtual_terminal/multiple_object_pools/objectPoolObjects.h @@ -0,0 +1,36 @@ +// This is a file that will be auto-generated by your object pool designer application. +// These are the object IDs for the objects in the object pool +#define UNDEFINED 65535 //0xFFFF +#define example_WorkingSet 0 //0x0000 +#define mainRunscreen_DataMask 1000 //0x03E8 +#define example_AlarmMask 2000 //0x07D0 +#define exampleNumberInc_Container 3000 //0x0BB8 +#define mainRunscreen_SoftKeyMask 4000 //0x0FA0 +#define alarm_SKeyMask 4001 //0x0FA1 +#define alarm_SoftKey 5000 //0x1388 +#define acknowledgeAlarm_SoftKey 5001 //0x1389 +#define Plus_Button 6000 //0x1770 +#define Minus_Button 6001 //0x1771 +#define Title_OutStr 11000 //0x2AF8 +#define MainDescription_OutStr 11001 //0x2AF9 +#define temp_OutStr_ID_11002 11002 //0x2AFA +#define temp_OutStr_ID_11003 11003 //0x2AFB +#define AlarmMaskTitle_OutStr 11004 //0x2AFC +#define exampleOutput_OutNum 12000 //0x2EE0 +#define Title_OutRect 14000 //0x36B0 +#define MainRunscreenBackground_OutRect 14001 //0x36B1 +#define avatar_OutPict 20000 //0x4E20 +#define warningIcon_OutPict 20001 //0x4E21 +#define redAlert_OutPict 20002 //0x4E22 +#define greenCheck_OutPict 20003 //0x4E23 +#define ButtonExampleNumber_VarNum 21000 //0x5208 +#define title_OutStr 22000 //0x55F0 +#define MainDescription_VarStr 22001 //0x55F1 +#define alarmMaskTitle_OutStr 22002 //0x55F2 +#define ExampleAlarmMask_VarStr 22003 //0x55F3 +#define temp_FontAttr_ID_23000 23000 //0x59D8 +#define temp_FontAttr_ID_23001 23001 //0x59D9 +#define black48x64_FontAttr 23002 //0x59DA +#define solidBlack_LineAttr 24000 //0x5DC0 +#define solidWhite_FillAttr 25000 //0x61A8 +#define exampleNumberInc_ObjPtr 27000 //0x6978 diff --git a/examples/virtual_terminal/multiple_object_pools/window_masks.iop b/examples/virtual_terminal/multiple_object_pools/window_masks.iop new file mode 100644 index 0000000000000000000000000000000000000000..b0a601e26a116b91b7d2eb090efafb0534691c27 GIT binary patch literal 55 zcmZQ%P-0|cU}yNJ^&bcrm>3Gg8GxWrd;yUBr^Ow@Ai%(QBa#^?$H@?qT3n*wn^>I9 F005G+4