From 3ddf4c535df5851554446477d27e61e5eea290d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Mon, 20 Apr 2026 15:20:56 +0200 Subject: [PATCH 1/8] Add extra logging to understand which ECU-s are on the bus --- src/app.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/app.cpp b/src/app.cpp index c25892c..fbc9d9b 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -97,6 +97,27 @@ bool Application::initialize() auto tcAddressClaimedTime = isobus::SystemTiming::get_timestamp_ms(); std::cout << "[" << get_timestamp() << "] [Init] TC claimed address " << static_cast(tcCF->get_address()) << std::endl; + // Print existing ECUs on the bus with their NAMEs + auto existingControlFunctions = isobus::CANNetworkManager::CANNetwork.get_control_functions(false); + if (!existingControlFunctions.empty()) + { + std::cout << "[" << get_timestamp() << "] [Init] Existing ECUs on the bus:" << std::endl; + for (const auto &cf : existingControlFunctions) + { + if (cf && cf->get_address_valid()) + { + const auto &name = cf->get_NAME(); + std::cout << "[" << get_timestamp() << "] [Init] Address " << static_cast(cf->get_address()) + << " - NAME 0x" << std::hex << name.get_full_name() << std::dec + << " [Fn:" << static_cast(name.get_function_code()) + << "/IG:" << static_cast(name.get_industry_group()) + << "/Cls:" << static_cast(name.get_device_class()) << "]" + << " - Mfg:" << name.get_manufacturer_code() + << " (" << cf->get_type_string() << ")" << std::endl; + } + } + } + // Ensure minimum 250ms delay after address claim per J1939-81 auto tcClaimElapsedMs = isobus::SystemTiming::get_time_elapsed_ms(tcAddressClaimedTime); if (tcClaimElapsedMs < MINIMUM_ADDRESS_CLAIM_DELAY_MS) From 61973b49a0009ce15c49a40170effb5919ff1237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Sat, 9 May 2026 16:45:39 +0200 Subject: [PATCH 2/8] Add extra logging + implement some of the TC V4 features. Hopefully this will address the problem when we join the bus late. --- include/app.hpp | 34 +++++ include/settings.hpp | 48 ++++++ include/task_controller.hpp | 3 +- src/app.cpp | 284 +++++++++++++++++++++++++++++++++++- src/settings.cpp | 119 +++++++++++++++ src/task_controller.cpp | 69 ++++++++- 6 files changed, 551 insertions(+), 6 deletions(-) diff --git a/include/app.hpp b/include/app.hpp index 6ceb977..192c96d 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -12,6 +12,7 @@ #include #include "isobus/hardware_integration/can_hardware_plugin.hpp" +#include "isobus/isobus/can_message.hpp" #include "isobus/isobus/isobus_functionalities.hpp" #include "isobus/isobus/isobus_speed_distance_messages.hpp" #include "isobus/isobus/nmea2000_message_interface.hpp" @@ -21,6 +22,27 @@ #include "task_controller.hpp" #include "udp_connections.hpp" +#include +#include + +/// @brief Tracks the connection state of a potential TC client seen on the bus +struct ClientConnectionInfo +{ + std::uint64_t nameFull = 0; + std::uint8_t address = 0xFF; + std::string typeString; + bool workingSetMasterReceived = false; + bool requestVersionReceived = false; + bool versionResponseSent = false; + bool requestVersionSent = false; + bool clientTaskReceived = false; + bool registeredAsClient = false; + std::uint32_t lastWorkingSetMasterMs = 0; + std::uint32_t lastRequestVersionMs = 0; + std::uint32_t lastClientTaskMs = 0; + std::uint32_t firstSeenMs = 0; +}; + class Application { public: @@ -32,6 +54,12 @@ class Application private: void send_task_controller_status_message(); + void send_tc_status_burst(); + void dump_connection_table(); + void update_connection_tracker(); + + static void log_can_working_set_master(const isobus::CANMessage &message, void *parent); + static void log_can_process_data(const isobus::CANMessage &message, void *parent); std::shared_ptr settings = std::make_shared(); boost::asio::io_context ioContext = boost::asio::io_context(); @@ -48,4 +76,10 @@ class Application std::uint32_t lastJ1939SpeedTransmit = 0; std::uint32_t lastTCStatusTransmit = 0; std::int32_t lastSpeedValue = 0; + + // Connection tracking for diagnostics + std::map connectionTracker; + std::uint32_t lastConnectionTableDumpMs = 0; + std::uint32_t tcInitializedTimestampMs = 0; + bool tcStatusBurstSent = false; }; diff --git a/include/settings.hpp b/include/settings.hpp index 28b8784..a734c44 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -76,6 +76,20 @@ class Settings */ bool set_aog_heartbeat_enabled(bool enabled, bool save = true); + /** + * @brief Get the configured TC ISO 11783-10 version + * @return The configured version (0-4, default 4) + */ + std::uint8_t get_tc_version() const; + + /** + * @brief Set the TC ISO 11783-10 version + * @param version The version to set (0=DIS, 1=FDIS.1, 2=FirstEdition, 3=SecondEditionDraft, 4=SecondPublishedEdition) + * @param save Whether or not to save the settings to file + * @return True if the version was set successfully, false otherwise + */ + bool set_tc_version(std::uint8_t version, bool save = true); + /** * @brief Get the absolute path to the settings file * @param filename The filename to get the path for @@ -83,11 +97,45 @@ class Settings */ static std::string get_filename_path(std::string); + /** + * @brief Get the configured language code (ISO 639-1) + * @return The language code (default "en") + */ + std::string get_language_code() const; + + /** + * @brief Set the language code + * @param code The ISO 639-1 language code + * @param save Whether or not to save the settings to file + * @return True if the setting was set successfully, false otherwise + */ + bool set_language_code(std::string code, bool save = true); + + /** + * @brief Get the configured country code (ISO 3166-1 alpha-2) + * @return The country code (default "US") + */ + std::string get_country_code() const; + + /** + * @brief Set the country code + * @param code The ISO 3166-1 alpha-2 country code + * @param save Whether or not to save the settings to file + * @return True if the setting was set successfully, false otherwise + */ + bool set_country_code(std::string code, bool save = true); + private: constexpr static std::array DEFAULT_SUBNET = { 192, 168, 5 }; constexpr static bool DEFAULT_TECU_ENABLED = true; constexpr static bool DEFAULT_AOG_HEARTBEAT_ENABLED = true; + constexpr static std::uint8_t DEFAULT_TC_VERSION = 3; // SecondEditionDraft (V3 default for maximum implement compatibility) + static const std::string DEFAULT_LANGUAGE_CODE; + static const std::string DEFAULT_COUNTRY_CODE; std::array configuredSubnet = DEFAULT_SUBNET; bool tecuEnabled = DEFAULT_TECU_ENABLED; bool aogHeartbeatEnabled = DEFAULT_AOG_HEARTBEAT_ENABLED; + std::uint8_t tcVersion = DEFAULT_TC_VERSION; + std::string languageCode = DEFAULT_LANGUAGE_CODE; + std::string countryCode = DEFAULT_COUNTRY_CODE; }; diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 9546457..90e6943 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -76,7 +76,8 @@ class ClientState class MyTCServer : public isobus::TaskControllerServer { public: - MyTCServer(std::shared_ptr internalControlFunction); + MyTCServer(std::shared_ptr internalControlFunction, + isobus::TaskControllerServer::TaskControllerVersion version = isobus::TaskControllerServer::TaskControllerVersion::SecondPublishedEdition); bool activate_object_pool(std::shared_ptr partnerCF, ObjectPoolActivationError &, ObjectPoolErrorCodes &, std::uint16_t &, std::uint16_t &) override; bool change_designator(std::shared_ptr, std::uint16_t, const std::vector &) override; bool deactivate_object_pool(std::shared_ptr partnerCF) override; diff --git a/src/app.cpp b/src/app.cpp index fbc9d9b..b45ebca 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -10,13 +10,16 @@ #include "isobus/hardware_integration/available_can_drivers.hpp" #include "isobus/hardware_integration/can_hardware_interface.hpp" +#include "isobus/isobus/can_general_parameter_group_numbers.hpp" #include "isobus/isobus/can_network_manager.hpp" #include "isobus/isobus/isobus_preferred_addresses.hpp" #include "isobus/isobus/isobus_standard_data_description_indices.hpp" +#include "isobus/isobus/isobus_task_controller_server.hpp" #include "isobus/utility/system_timing.hpp" #include "task_controller.hpp" +#include #include using boost::asio::ip::udp; @@ -192,13 +195,50 @@ bool Application::initialize() } } - tcServer = std::make_shared(tcCF); + // Map settings version to TaskControllerVersion enum + isobus::TaskControllerServer::TaskControllerVersion tcVersionEnum; + switch (settings->get_tc_version()) + { + case 0: + tcVersionEnum = isobus::TaskControllerServer::TaskControllerVersion::DraftInternationalStandard; + break; + case 1: + tcVersionEnum = isobus::TaskControllerServer::TaskControllerVersion::FinalDraftInternationalStandardFirstEdition; + break; + case 2: + tcVersionEnum = isobus::TaskControllerServer::TaskControllerVersion::FirstPublishedEdition; + break; + case 3: + tcVersionEnum = isobus::TaskControllerServer::TaskControllerVersion::SecondEditionDraft; + break; + case 4: + default: + tcVersionEnum = isobus::TaskControllerServer::TaskControllerVersion::SecondPublishedEdition; + break; + } + std::cout << "[" << get_timestamp() << "] [Init] TC version set to " << static_cast(settings->get_tc_version()) << " (" + << static_cast(tcVersionEnum) << ")" << std::endl; + + tcServer = std::make_shared(tcCF, tcVersionEnum); auto &languageInterface = tcServer->get_language_command_interface(); - languageInterface.set_language_code("en"); // This is the default, but you can change it if you want - languageInterface.set_country_code("US"); // This is the default, but you can change it if you want + languageInterface.set_language_code(settings->get_language_code()); + languageInterface.set_country_code(settings->get_country_code()); tcServer->initialize(); tcServer->set_task_totals_active(true); // TODO: make this dynamic based on status in AOG + // Register CAN callbacks to log WorkingSetMaster and ProcessData for diagnostics + isobus::CANNetworkManager::CANNetwork.add_any_control_function_parameter_group_number_callback( + static_cast(isobus::CANLibParameterGroupNumber::WorkingSetMaster), + Application::log_can_working_set_master, + this); + isobus::CANNetworkManager::CANNetwork.add_any_control_function_parameter_group_number_callback( + static_cast(isobus::CANLibParameterGroupNumber::ProcessData), + Application::log_can_process_data, + this); + + tcInitializedTimestampMs = isobus::SystemTiming::get_timestamp_ms(); + std::cout << "[" << get_timestamp() << "] [Init] TC server initialized, sending startup status burst..." << std::endl; + // Initialize speed and distance messages if (tecuCF && tecuCF->get_address_valid()) { @@ -422,6 +462,13 @@ bool Application::update() } } + // Send startup TC Status burst to help late-joining implements detect the TC + // ISO 11783-10 allows up to 5 Hz (200ms interval) for TC Status + if (!tcStatusBurstSent && tcCF && tcCF->get_address_valid()) + { + send_tc_status_burst(); + } + // Send Task Controller Status message every 2 seconds (ISO 11783-10 B.8.1) if (isobus::SystemTiming::time_expired_ms(lastTCStatusTransmit, 2000) && tcCF && tcCF->get_address_valid()) { @@ -434,6 +481,16 @@ bool Application::update() } } + // Update connection tracker state + update_connection_tracker(); + + // Dump connection diagnostics table every 30 seconds + if (isobus::SystemTiming::time_expired_ms(lastConnectionTableDumpMs, 30000)) + { + dump_connection_table(); + lastConnectionTableDumpMs = isobus::SystemTiming::get_timestamp_ms(); + } + return true; } @@ -487,6 +544,227 @@ void Application::send_task_controller_status_message() lastTCStatusTransmit = transmitAttemptTimestamp; } +void Application::send_tc_status_burst() +{ + // ISO 11783-10 allows TC Status up to 5 Hz (200ms minimum interval). + // Send a burst of 5 messages at 200ms intervals right after TC initialization + // to maximize the chance that late-joining implements detect the TC. + static std::uint8_t burstCount = 0; + static std::uint32_t lastBurstTransmit = 0; + + if (burstCount == 0) + { + lastBurstTransmit = tcInitializedTimestampMs; + } + + if (isobus::SystemTiming::time_expired_ms(lastBurstTransmit, 200)) + { + send_task_controller_status_message(); + burstCount++; + lastBurstTransmit = isobus::SystemTiming::get_timestamp_ms(); + std::cout << "[" << get_timestamp() << "] [TC Status] Startup burst message " << static_cast(burstCount) << "/5 sent" << std::endl; + + if (burstCount >= 5) + { + tcStatusBurstSent = true; + burstCount = 0; + std::cout << "[" << get_timestamp() << "] [TC Status] Startup burst complete" << std::endl; + } + } +} + +void Application::dump_connection_table() +{ + auto now = isobus::SystemTiming::get_timestamp_ms(); + std::cout << "[" << get_timestamp() << "] === Client Connection Diagnostics Table ===" << std::endl; + std::cout << std::left << std::setw(18) << "NAME" + << std::setw(10) << "Address" + << std::setw(8) << "WSM" + << std::setw(8) << "ReqVer" + << std::setw(8) << "VerSent" + << std::setw(8) << "ReqSent" + << std::setw(8) << "ClTask" + << std::setw(8) << "Reg" + << std::setw(12) << "Type" + << std::setw(10) << "Age(s)" + << std::endl; + std::cout << std::string(98, '-') << std::endl; + + for (const auto &entry : connectionTracker) + { + const auto &info = entry.second; + std::stringstream nameStream; + nameStream << "0x" << std::hex << info.nameFull << std::dec; + std::string nameStr = nameStream.str(); + + std::cout << std::left << std::setw(18) << nameStr.substr(0, 17) + << std::setw(10) << static_cast(info.address) + << std::setw(8) << (info.workingSetMasterReceived ? "Y" : "N") + << std::setw(8) << (info.requestVersionReceived ? "Y" : "N") + << std::setw(8) << (info.versionResponseSent ? "Y" : "N") + << std::setw(8) << (info.requestVersionSent ? "Y" : "N") + << std::setw(8) << (info.clientTaskReceived ? "Y" : "N") + << std::setw(8) << (info.registeredAsClient ? "Y" : "N") + << std::setw(12) << info.typeString.substr(0, 11) + << std::setw(10) << static_cast((now - info.firstSeenMs) / 1000) + << std::endl; + } + + if (connectionTracker.empty()) + { + std::cout << "[" << get_timestamp() << "] No clients seen on the bus yet." << std::endl; + } + std::cout << "[" << get_timestamp() << "] === End Connection Table ===" << std::endl; +} + +void Application::update_connection_tracker() +{ + auto now = isobus::SystemTiming::get_timestamp_ms(); + + // Sync registered clients from tcServer + if (tcServer) + { + for (auto &client : tcServer->get_clients()) + { + auto nameFull = client.first->get_NAME().get_full_name(); + auto it = connectionTracker.find(nameFull); + if (it != connectionTracker.end()) + { + it->second.registeredAsClient = true; + it->second.address = client.first->get_address(); + } + } + } + + // Scan all control functions on the bus and add any we haven't seen yet + auto allCFs = isobus::CANNetworkManager::CANNetwork.get_control_functions(false); + for (const auto &cf : allCFs) + { + if (cf && cf->get_address_valid()) + { + auto nameFull = cf->get_NAME().get_full_name(); + if (connectionTracker.find(nameFull) == connectionTracker.end()) + { + ClientConnectionInfo info; + info.nameFull = nameFull; + info.address = cf->get_address(); + info.typeString = cf->get_type_string(); + info.firstSeenMs = now; + connectionTracker[nameFull] = info; + std::cout << "[" << get_timestamp() << "] [ConnectionTracker] New CF detected: NAME 0x" << std::hex << nameFull << std::dec + << " @ address " << static_cast(cf->get_address()) + << " (" << info.typeString << ")" << std::endl; + } + } + } +} + +void Application::log_can_working_set_master(const isobus::CANMessage &message, void *parent) +{ + if (nullptr == parent) + { + return; + } + auto *app = static_cast(parent); + auto source = message.get_source_control_function(); + if (nullptr == source) + { + return; + } + + auto now = isobus::SystemTiming::get_timestamp_ms(); + auto nameFull = source->get_NAME().get_full_name(); + auto &info = app->connectionTracker[nameFull]; + info.nameFull = nameFull; + info.address = source->get_address(); + info.typeString = source->get_type_string(); + if (info.firstSeenMs == 0) + { + info.firstSeenMs = now; + } + info.workingSetMasterReceived = true; + info.lastWorkingSetMasterMs = now; + + std::uint8_t numberOfMembers = message.get_data().empty() ? 0 : message.get_data()[0]; + std::cout << "[" << get_timestamp() << "] [CAN] WorkingSetMaster from NAME 0x" << std::hex << nameFull << std::dec + << " @ " << static_cast(source->get_address()) + << " - members=" << static_cast(numberOfMembers) << std::endl; +} + +void Application::log_can_process_data(const isobus::CANMessage &message, void *parent) +{ + if (nullptr == parent) + { + return; + } + auto *app = static_cast(parent); + auto source = message.get_source_control_function(); + if (nullptr == source) + { + return; + } + + const auto &data = message.get_data(); + if (data.empty()) + { + return; + } + + std::uint8_t command = data[0] & 0x0F; + std::uint8_t subcommand = data[0] >> 4; + auto now = isobus::SystemTiming::get_timestamp_ms(); + auto nameFull = source->get_NAME().get_full_name(); + auto &info = app->connectionTracker[nameFull]; + info.nameFull = nameFull; + info.address = source->get_address(); + info.typeString = source->get_type_string(); + if (info.firstSeenMs == 0) + { + info.firstSeenMs = now; + } + + // Log RequestVersion (command 0x00, subcommand 0x00) + if (command == 0x00 && subcommand == 0x00) + { + info.requestVersionReceived = true; + info.lastRequestVersionMs = now; + + // Bidirectional version exchange: if a client asks our version, ask theirs too + // This makes a V3-reporting TC behave like V4 for clients that expect it + if (!info.requestVersionSent && app->tcCF && app->tcCF->get_address_valid()) + { + std::array requestVersionPayload = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + }; + if (isobus::CANNetworkManager::CANNetwork.send_can_message( + 0xCB00, requestVersionPayload.data(), requestVersionPayload.size(), app->tcCF, source)) + { + info.requestVersionSent = true; + std::cout << "[" << get_timestamp() << "] [CAN] Sent RequestVersion to NAME 0x" << std::hex << nameFull << std::dec << std::endl; + } + } + + std::cout << "[" << get_timestamp() << "] [CAN] RequestVersion from NAME 0x" << std::hex << nameFull << std::dec + << " @ " << static_cast(source->get_address()) << std::endl; + } + // Log ParameterVersion response (command 0x00, subcommand 0x01) - only if destination is the TC + else if (command == 0x00 && subcommand == 0x01) + { + if (message.get_destination_control_function() == source) + { + // This is actually a Version response FROM the TC TO the client, + // but if we see it going TO a client, it means the TC responded. + info.versionResponseSent = true; + } + } + // Log ClientTask (command 0x0F) + else if (command == 0x0F) + { + info.clientTaskReceived = true; + info.lastClientTaskMs = now; + } +} + void Application::stop() { tcServer->terminate(); diff --git a/src/settings.cpp b/src/settings.cpp index 366b5c4..6996970 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -16,6 +16,9 @@ using json = nlohmann::json; +const std::string Settings::DEFAULT_LANGUAGE_CODE = "en"; +const std::string Settings::DEFAULT_COUNTRY_CODE = "US"; + bool Settings::load() { std::ifstream file(get_filename_path("settings.json")); @@ -79,6 +82,66 @@ bool Settings::load() aogHeartbeatEnabled = DEFAULT_AOG_HEARTBEAT_ENABLED; // Key not found, use default } + if (data.contains("tcVersion")) + { + try + { + int version = data["tcVersion"].get(); + if (version >= 0 && version <= 4) + { + tcVersion = static_cast(version); + } + else + { + std::cout << "[" << get_timestamp() << "] Invalid tcVersion " << version << ", using default " << static_cast(DEFAULT_TC_VERSION) << std::endl; + tcVersion = DEFAULT_TC_VERSION; + } + } + catch (const nlohmann::json::exception &e) + { + std::cout << "[" << get_timestamp() << "] Error parsing 'tcVersion': " << e.what() << std::endl; + tcVersion = DEFAULT_TC_VERSION; // Fallback to default + } + } + else + { + tcVersion = DEFAULT_TC_VERSION; // Key not found, use default + } + + if (data.contains("languageCode")) + { + try + { + languageCode = data["languageCode"].get(); + } + catch (const nlohmann::json::exception &e) + { + std::cout << "[" << get_timestamp() << "] Error parsing 'languageCode': " << e.what() << std::endl; + languageCode = DEFAULT_LANGUAGE_CODE; + } + } + else + { + languageCode = DEFAULT_LANGUAGE_CODE; + } + + if (data.contains("countryCode")) + { + try + { + countryCode = data["countryCode"].get(); + } + catch (const nlohmann::json::exception &e) + { + std::cout << "[" << get_timestamp() << "] Error parsing 'countryCode': " << e.what() << std::endl; + countryCode = DEFAULT_COUNTRY_CODE; + } + } + else + { + countryCode = DEFAULT_COUNTRY_CODE; + } + return true; } @@ -88,6 +151,9 @@ bool Settings::save() const data["subnet"] = configuredSubnet; data["tecuEnabled"] = tecuEnabled; data["aogHeartbeatEnabled"] = aogHeartbeatEnabled; + data["tcVersion"] = tcVersion; + data["languageCode"] = languageCode; + data["countryCode"] = countryCode; std::ofstream file(get_filename_path("settings.json")); if (!file.is_open()) @@ -99,6 +165,59 @@ bool Settings::save() const return true; } +std::uint8_t Settings::get_tc_version() const +{ + return tcVersion; +} + +bool Settings::set_tc_version(std::uint8_t version, bool save) +{ + if (version > 4) + { + std::cout << "[" << get_timestamp() << "] Invalid TC version " << static_cast(version) << ", using default " << static_cast(DEFAULT_TC_VERSION) << std::endl; + tcVersion = DEFAULT_TC_VERSION; + } + else + { + tcVersion = version; + } + if (save) + { + return this->save(); + } + return true; +} + +std::string Settings::get_language_code() const +{ + return languageCode; +} + +bool Settings::set_language_code(std::string code, bool save) +{ + languageCode = code; + if (save) + { + return this->save(); + } + return true; +} + +std::string Settings::get_country_code() const +{ + return countryCode; +} + +bool Settings::set_country_code(std::string code, bool save) +{ + countryCode = code; + if (save) + { + return this->save(); + } + return true; +} + const std::array &Settings::get_subnet() const { return configuredSubnet; diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 6155074..cae37d5 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -10,6 +10,8 @@ #include "logging_utils.hpp" #include "settings.hpp" +#include + #include "isobus/isobus/isobus_device_descriptor_object_pool_helpers.hpp" #include "isobus/isobus/isobus_task_controller_server.hpp" @@ -244,14 +246,15 @@ bool ClientState::try_get_element_work_state(std::uint16_t elementNumber, bool & return false; } -MyTCServer::MyTCServer(std::shared_ptr internalControlFunction) : +MyTCServer::MyTCServer(std::shared_ptr internalControlFunction, + isobus::TaskControllerServer::TaskControllerVersion version) : TaskControllerServer(internalControlFunction, 1, // AOG limits to 1 boom 64, // AOG limits to 16 sections of unique width but can be 64 by using zones 64, // 64 channels for position based control isobus::TaskControllerOptions() .with_implement_section_control(), // We support section control - TaskControllerVersion::SecondEditionDraft) + version) { } @@ -376,8 +379,70 @@ bool MyTCServer::deactivate_object_pool(std::shared_ptr return true; } +static bool remove_directory_recursive(const std::string &path) +{ + WIN32_FIND_DATAA findData; + auto searchPath = path + "\\*"; + auto hFind = FindFirstFileA(searchPath.c_str(), &findData); + if (hFind == INVALID_HANDLE_VALUE) + { + return RemoveDirectoryA(path.c_str()) != 0; + } + + do + { + std::string name = findData.cFileName; + if (name == "." || name == "..") + { + continue; + } + std::string fullPath = path + "\\" + name; + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + remove_directory_recursive(fullPath); + } + else + { + DeleteFileA(fullPath.c_str()); + } + } while (FindNextFileA(hFind, &findData)); + + FindClose(hFind); + return RemoveDirectoryA(path.c_str()) != 0; +} + bool MyTCServer::delete_device_descriptor_object_pool(std::shared_ptr partnerCF, ObjectPoolDeletionErrors &) { + auto nameFull = partnerCF->get_NAME().get_full_name(); + auto folderName = std::to_string(nameFull); + + // Get the directory path where this client's DDOP is stored + auto dummyFilePath = Settings::get_filename_path(folderName + "\\dummy.txt"); + auto currentFolderPath = dummyFilePath.substr(0, dummyFilePath.find_last_of("\\/")); + auto parentDirPath = currentFolderPath.substr(0, currentFolderPath.find_last_of("\\/")); + auto archiveFolderPath = parentDirPath + "\\" + folderName + "_archive"; + + // If archive already exists, delete it first + if (GetFileAttributesA(archiveFolderPath.c_str()) != INVALID_FILE_ATTRIBUTES) + { + if (!remove_directory_recursive(archiveFolderPath)) + { + std::cout << "[" << get_timestamp() << "] [TC Server] Failed to remove old archive folder: " << archiveFolderPath << std::endl; + } + } + + // Rename current folder to archive + if (MoveFileA(currentFolderPath.c_str(), archiveFolderPath.c_str())) + { + std::cout << "[" << get_timestamp() << "] [TC Server] Archived DDOP folder for NAME " << nameFull + << " to " << archiveFolderPath << std::endl; + } + else + { + std::cout << "[" << get_timestamp() << "] [TC Server] Failed to archive DDOP folder for NAME " << nameFull + << " (error " << GetLastError() << ")" << std::endl; + } + clients.erase(partnerCF); uploadedPools.erase(partnerCF); return true; From 2ec2b5252016c93db7a8c0550255ecd1f9432d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Sat, 9 May 2026 16:49:57 +0200 Subject: [PATCH 3/8] clang-format --- src/app.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.cpp b/src/app.cpp index b45ebca..b23c1d4 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -737,7 +737,7 @@ void Application::log_can_process_data(const isobus::CANMessage &message, void * 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; if (isobus::CANNetworkManager::CANNetwork.send_can_message( - 0xCB00, requestVersionPayload.data(), requestVersionPayload.size(), app->tcCF, source)) + 0xCB00, requestVersionPayload.data(), requestVersionPayload.size(), app->tcCF, source)) { info.requestVersionSent = true; std::cout << "[" << get_timestamp() << "] [CAN] Sent RequestVersion to NAME 0x" << std::hex << nameFull << std::dec << std::endl; From 689124f9eab8b20c4eb02c1ae73f305bbb65afb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Tue, 19 May 2026 23:56:50 +0200 Subject: [PATCH 4/8] Added functionality announcement and the display of chsen name --- include/app.hpp | 7 ++ include/logging_utils.hpp | 8 +++ include/task_controller.hpp | 2 +- src/app.cpp | 135 +++++++++++++++++++++++++++++++++--- src/logging.cpp | 3 + src/main.cpp | 7 ++ src/task_controller.cpp | 6 +- 7 files changed, 154 insertions(+), 14 deletions(-) diff --git a/include/app.hpp b/include/app.hpp index 192c96d..b72cab2 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -22,7 +22,9 @@ #include "task_controller.hpp" #include "udp_connections.hpp" +#include #include +#include #include /// @brief Tracks the connection state of a potential TC client seen on the bus @@ -60,6 +62,7 @@ class Application static void log_can_working_set_master(const isobus::CANMessage &message, void *parent); static void log_can_process_data(const isobus::CANMessage &message, void *parent); + static void log_all_can_messages(const isobus::CANMessage &message, void *parent); std::shared_ptr settings = std::make_shared(); boost::asio::io_context ioContext = boost::asio::io_context(); @@ -72,6 +75,7 @@ class Application std::unique_ptr speedMessagesInterface; std::unique_ptr nmea2000MessageInterface; std::unique_ptr tecuFunctionalities; + std::unique_ptr tcFunctionalities; std::uint8_t nmea2000SequenceIdentifier = 0; std::uint32_t lastJ1939SpeedTransmit = 0; std::uint32_t lastTCStatusTransmit = 0; @@ -82,4 +86,7 @@ class Application std::uint32_t lastConnectionTableDumpMs = 0; std::uint32_t tcInitializedTimestampMs = 0; bool tcStatusBurstSent = false; + + // CAN message log file + std::ofstream canLogFile; }; diff --git a/include/logging_utils.hpp b/include/logging_utils.hpp index db064ac..a2dfa20 100644 --- a/include/logging_utils.hpp +++ b/include/logging_utils.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -23,3 +24,10 @@ inline std::string get_timestamp() << std::setfill('0') << std::setw(3) << ms.count(); return oss.str(); } + +// Global mutex to protect all std::cout writes from concurrent access across threads +inline std::mutex &getLoggingMutex() +{ + static std::mutex loggingMutex; + return loggingMutex; +} diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 90e6943..bbd87da 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -94,7 +94,7 @@ class MyTCServer : public isobus::TaskControllerServer std::int32_t processDataValue, std::uint8_t &errorCodes) override; bool store_device_descriptor_object_pool(std::shared_ptr partnerCF, const std::vector &binaryPool, bool appendToPool) override; - std::map, ClientState> &get_clients(); + std::map, ClientState> &get_clients(); ///< Returns a reference to the clients map void request_measurement_commands(); void update_section_states(std::vector §ionStates); void update_section_control_enabled(bool enabled); diff --git a/src/app.cpp b/src/app.cpp index b23c1d4..edf9f87 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -69,6 +69,11 @@ bool Application::initialize() tecuNAME.set_ecu_instance(0); std::cout << "[" << get_timestamp() << "] [Init] Creating Task Controller control function..." << std::endl; + std::cout << "[" << get_timestamp() << "] [Init] TC NAME: 0x" << std::hex << tcNAME.get_full_name() << std::dec + << " [Fn:" << static_cast(tcNAME.get_function_code()) + << "/IG:" << static_cast(tcNAME.get_industry_group()) + << "/Cls:" << static_cast(tcNAME.get_device_class()) << "]" + << " - Preferred address: " << static_cast(isobus::preferred_addresses::IndustryGroup2::TaskController_MappingComputer) << std::endl; tcCF = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(tcNAME, 0, isobus::preferred_addresses::IndustryGroup2::TaskController_MappingComputer); // The preferred address for a TC is defined in ISO 11783 // Wait for TC address claim with bounded wait loop (no async to avoid blocking on destruction) @@ -92,13 +97,42 @@ bool Application::initialize() if (!tcAddressClaimed) { - std::cout << "[" << get_timestamp() << "] Failed to claim address for TC server. The control function might be invalid." << std::endl; + std::cout << "[" << get_timestamp() << "] [ERROR] Failed to claim address for TC server. The control function might be invalid." << std::endl; + + // Dump all visible ECUs to help diagnose address conflicts + std::cout << "[" << get_timestamp() << "] [ERROR] Dumping all visible ECUs before exit:" << std::endl; + auto allCFs = isobus::CANNetworkManager::CANNetwork.get_control_functions(false); + for (const auto &cf : allCFs) + { + if (cf && cf->get_address_valid()) + { + const auto &name = cf->get_NAME(); + std::cout << "[" << get_timestamp() << "] [ERROR] Address " << static_cast(cf->get_address()) + << " - NAME 0x" << std::hex << name.get_full_name() << std::dec + << " [Fn:" << static_cast(name.get_function_code()) + << "/IG:" << static_cast(name.get_industry_group()) + << "/Cls:" << static_cast(name.get_device_class()) << "]" + << " - Mfg:" << name.get_manufacturer_code() + << " (" << cf->get_type_string() << ")" << std::endl; + } + } + if (allCFs.empty()) + { + std::cout << "[" << get_timestamp() << "] [ERROR] No ECUs visible on bus" << std::endl; + } + return false; } // Record when the address was actually claimed for the 250ms delay calculation auto tcAddressClaimedTime = isobus::SystemTiming::get_timestamp_ms(); - std::cout << "[" << get_timestamp() << "] [Init] TC claimed address " << static_cast(tcCF->get_address()) << std::endl; + std::uint8_t tcActualAddress = tcCF->get_address(); + std::cout << "[" << get_timestamp() << "] [Init] TC successfully claimed address " << static_cast(tcActualAddress); + if (tcActualAddress != isobus::preferred_addresses::IndustryGroup2::TaskController_MappingComputer) + { + std::cout << " (DIFFERS from preferred " << static_cast(isobus::preferred_addresses::IndustryGroup2::TaskController_MappingComputer) << ")"; + } + std::cout << std::endl; // Print existing ECUs on the bus with their NAMEs auto existingControlFunctions = isobus::CANNetworkManager::CANNetwork.get_control_functions(false); @@ -226,6 +260,36 @@ bool Application::initialize() tcServer->initialize(); tcServer->set_task_totals_active(true); // TODO: make this dynamic based on status in AOG + // Announce our TC's Control Function Functionalities (PGN 64654, 0xFC8E) per ISO 11783-12. + std::cout << "[" << get_timestamp() << "] [Init] Creating TC Control Function Functionalities..." << std::endl; + tcFunctionalities = std::make_unique(tcCF); + + // TC-BAS (Basic): mandatory baseline for any TC server + tcFunctionalities->set_functionality_is_supported( + isobus::ControlFunctionFunctionalities::Functionalities::TaskControllerBasicServer, + 1, // Generation 1 + true); + + // TC-GEO: DISABLED - we don't support variable rate / prescription maps yet + tcFunctionalities->set_functionality_is_supported( + isobus::ControlFunctionFunctionalities::Functionalities::TaskControllerGeoServer, + 1, // Generation 1 + false); + tcFunctionalities->set_task_controller_geo_server_option_state( + isobus::ControlFunctionFunctionalities::TaskControllerGeoServerOptions::PolygonBasedPrescriptionMapsAreSupported, + false); + + // TC-SC (Section Control): we support up to 1 boom and 64 sections, + // matching the limits configured in the MyTCServer constructor. + tcFunctionalities->set_functionality_is_supported( + isobus::ControlFunctionFunctionalities::Functionalities::TaskControllerSectionControlServer, + 1, // Generation 1 + true); + tcFunctionalities->set_task_controller_section_control_server_option_state( + 1, // numberOfSupportedBooms + 64); // numberOfSupportedSections + std::cout << "[" << get_timestamp() << "] [Init] TC announced TC-BAS and TC-SC (1 boom / 64 sections) via PGN 64654" << std::endl; + // Register CAN callbacks to log WorkingSetMaster and ProcessData for diagnostics isobus::CANNetworkManager::CANNetwork.add_any_control_function_parameter_group_number_callback( static_cast(isobus::CANLibParameterGroupNumber::WorkingSetMaster), @@ -236,6 +300,12 @@ bool Application::initialize() Application::log_can_process_data, this); + // Register raw CAN message logger for debugging + isobus::CANNetworkManager::CANNetwork.add_any_control_function_parameter_group_number_callback( + 0, // 0 = all PGNs + Application::log_all_can_messages, + this); + tcInitializedTimestampMs = isobus::SystemTiming::get_timestamp_ms(); std::cout << "[" << get_timestamp() << "] [Init] TC server initialized, sending startup status burst..." << std::endl; @@ -390,6 +460,8 @@ bool Application::update() tcServer->request_measurement_commands(); tcServer->update(); + if (tcFunctionalities) + tcFunctionalities->update(); if (tecuFunctionalities) tecuFunctionalities->update(); if (speedMessagesInterface) @@ -619,20 +691,20 @@ void Application::dump_connection_table() void Application::update_connection_tracker() { + // Get a reference to clients + auto &clientsRef = tcServer ? tcServer->get_clients() : *new std::map, ClientState>(); + auto now = isobus::SystemTiming::get_timestamp_ms(); // Sync registered clients from tcServer - if (tcServer) + for (auto &client : clientsRef) { - for (auto &client : tcServer->get_clients()) + auto nameFull = client.first->get_NAME().get_full_name(); + auto it = connectionTracker.find(nameFull); + if (it != connectionTracker.end()) { - auto nameFull = client.first->get_NAME().get_full_name(); - auto it = connectionTracker.find(nameFull); - if (it != connectionTracker.end()) - { - it->second.registeredAsClient = true; - it->second.address = client.first->get_address(); - } + it->second.registeredAsClient = true; + it->second.address = client.first->get_address(); } } @@ -765,6 +837,47 @@ void Application::log_can_process_data(const isobus::CANMessage &message, void * } } +void Application::log_all_can_messages(const isobus::CANMessage &message, void *parent) +{ + if (nullptr == parent) + { + return; + } + auto *app = static_cast(parent); + + // Get message details + auto sourceCF = message.get_source_control_function(); + auto destCF = message.get_destination_control_function(); + const auto &data = message.get_data(); + + std::uint8_t sourceAddr = sourceCF ? sourceCF->get_address() : 0xFF; + std::uint8_t destAddr = destCF ? destCF->get_address() : 0xFF; + std::uint32_t pgn = message.get_identifier().get_parameter_group_number(); + std::uint8_t priority = static_cast(message.get_identifier().get_priority()); + + // Log to CSV file if open + if (app->canLogFile.is_open()) + { + auto timestamp = get_timestamp(); + app->canLogFile << timestamp << "," + << "RX," // All messages captured here are received + << "0x" << std::hex << pgn << "," + << std::dec << static_cast(priority) << "," + << "0x" << std::hex << static_cast(sourceAddr) << "," + << "0x" << std::hex << static_cast(destAddr) << "," + << std::dec << data.size() << ","; + + // Write data bytes as hex + for (size_t i = 0; i < data.size(); i++) + { + app->canLogFile << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << static_cast(data[i]); + if (i < data.size() - 1) + app->canLogFile << " "; + } + app->canLogFile << std::dec << std::endl; + } +} + void Application::stop() { tcServer->terminate(); diff --git a/src/logging.cpp b/src/logging.cpp index 354b4f6..7f447ad 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -27,6 +27,7 @@ class TeeStreambuf : public std::streambuf protected: int overflow(int c) override { + std::lock_guard lock(getLoggingMutex()); if (c != EOF) { consoleBuffer->sputc(c); // Write to console @@ -37,6 +38,7 @@ class TeeStreambuf : public std::streambuf int sync() override { + std::lock_guard lock(getLoggingMutex()); consoleBuffer->pubsync(); fileStream.flush(); return 0; @@ -71,6 +73,7 @@ class CustomLogger : public isobus::CANStackLogger void sink_CAN_stack_log(CANStackLogger::LoggingLevel level, const std::string &text) override { + std::lock_guard lock(getLoggingMutex()); std::cout << "[" << get_timestamp() << "] "; switch (level) { diff --git a/src/main.cpp b/src/main.cpp index 6ec6b6c..6892c4f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -107,6 +107,11 @@ class ArgumentProcessor return fileLogging; } + bool has_log_level() const + { + return logLevelSpecified; + } + private: bool parse_option(std::string option) { @@ -173,6 +178,7 @@ class ArgumentProcessor } else if ("--log_level" == key) { + logLevelSpecified = true; if ("debug" == value) { isobus::CANStackLogger::set_log_level(isobus::CANStackLogger::LoggingLevel::Debug); @@ -210,6 +216,7 @@ class ArgumentProcessor CANAdapter canAdapter = CANAdapter::NONE; std::string canChannel; bool fileLogging = false; + bool logLevelSpecified = false; }; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) diff --git a/src/task_controller.cpp b/src/task_controller.cpp index cae37d5..cda8d37 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -443,8 +443,10 @@ bool MyTCServer::delete_device_descriptor_object_pool(std::shared_ptr Date: Mon, 25 May 2026 18:49:44 +0200 Subject: [PATCH 5/8] Remove duplicate Status messages --- src/app.cpp | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index b23c1d4..04e0878 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -464,23 +464,12 @@ bool Application::update() // Send startup TC Status burst to help late-joining implements detect the TC // ISO 11783-10 allows up to 5 Hz (200ms interval) for TC Status + // After the burst, AgIsoStack++ handles the periodic 2-second TC Status internally. if (!tcStatusBurstSent && tcCF && tcCF->get_address_valid()) { send_tc_status_burst(); } - // Send Task Controller Status message every 2 seconds (ISO 11783-10 B.8.1) - if (isobus::SystemTiming::time_expired_ms(lastTCStatusTransmit, 2000) && tcCF && tcCF->get_address_valid()) - { - static bool firstStatusSent = false; - send_task_controller_status_message(); - if (!firstStatusSent) - { - std::cout << "[" << get_timestamp() << "] [TC Status] First TC Status message sent (PGN 0xCB00)" << std::endl; - firstStatusSent = true; - } - } - // Update connection tracker state update_connection_tracker(); @@ -531,13 +520,32 @@ void Application::send_task_controller_status_message() 0xFF // Byte 8: Reserved }; - // Send to global destination (0xFF) - broadcast to all nodes - // Using 4-arg version: PGN, data, length, source CF (destination is implicit in PGN for broadcast) + // Send to global destination (0xFF) with priority 3 (ISO 11783-10 requirement) const auto transmitAttemptTimestamp = isobus::SystemTiming::get_timestamp_ms(); - if (!isobus::CANNetworkManager::CANNetwork.send_can_message(0xCB00, tcStatusData.data(), tcStatusData.size(), tcCF)) + bool sendSuccess = isobus::CANNetworkManager::CANNetwork.send_can_message( + 0xCB00, tcStatusData.data(), tcStatusData.size(), tcCF, nullptr, + isobus::CANIdentifier::CANPriority::Priority3); + + if (!sendSuccess) { std::cout << "[" << get_timestamp() << "] [TC Status] Failed to send TC Status message!" << std::endl; } + else + { + std::cout << "[" << get_timestamp() << "] [TC Status] Sent: ID=0x0CBFFF" << std::hex + << static_cast(tcCF->get_address()) << std::dec + << " Payload=" << std::hex << std::uppercase + << std::setfill('0') << std::setw(2) << static_cast(tcStatusData[0]) << " " + << std::setw(2) << static_cast(tcStatusData[1]) << " " + << std::setw(2) << static_cast(tcStatusData[2]) << " " + << std::setw(2) << static_cast(tcStatusData[3]) << " " + << std::setw(2) << static_cast(tcStatusData[4]) << " " + << std::setw(2) << static_cast(tcStatusData[5]) << " " + << std::setw(2) << static_cast(tcStatusData[6]) << " " + << std::setw(2) << static_cast(tcStatusData[7]) << std::dec + << " [task_totals=" << (statusByte & 0x01) << "]" + << std::endl; + } // Update the transmit timestamp for every send attempt so failed sends // still respect the minimum 2-second transmit period. @@ -562,6 +570,7 @@ void Application::send_tc_status_burst() send_task_controller_status_message(); burstCount++; lastBurstTransmit = isobus::SystemTiming::get_timestamp_ms(); + lastTCStatusTransmit = lastBurstTransmit; // Prevent duplicate 2s timer std::cout << "[" << get_timestamp() << "] [TC Status] Startup burst message " << static_cast(burstCount) << "/5 sent" << std::endl; if (burstCount >= 5) From 11b55277e276491b12d3cf8c0f451826342d1ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Mon, 25 May 2026 18:55:14 +0200 Subject: [PATCH 6/8] Bump stack to a new version: * TC Status message handling is different * CF eviction in case of conflicts * Working Width parsing fixed --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 13c2d04..87b6ea7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,8 +53,8 @@ FetchContent_MakeAvailable(Boost) FetchContent_Declare( isobus - GIT_REPOSITORY https://github.com/Open-Agriculture/AgIsoStack-plus-plus.git - GIT_TAG 0c5e3d2f264270f0a750cc3fdedaaa5861d71a59 + GIT_REPOSITORY https://github.com/gunicsba/AgIsoStack-plus-plus.git + GIT_TAG 355a964b0359ce854be96e02bbe9568f036507fd DOWNLOAD_EXTRACT_TIMESTAMP TRUE) FetchContent_MakeAvailable(isobus) From 489d7e383eaa92b7268cc07395da701e3f0688f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Mon, 25 May 2026 19:17:12 +0200 Subject: [PATCH 7/8] Remove the TC Status message sending (it should never be part of the app...) --- include/app.hpp | 4 -- src/app.cpp | 107 ------------------------------------------------ 2 files changed, 111 deletions(-) diff --git a/include/app.hpp b/include/app.hpp index b72cab2..76722d9 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -55,8 +55,6 @@ class Application void stop(); private: - void send_task_controller_status_message(); - void send_tc_status_burst(); void dump_connection_table(); void update_connection_tracker(); @@ -78,14 +76,12 @@ class Application std::unique_ptr tcFunctionalities; std::uint8_t nmea2000SequenceIdentifier = 0; std::uint32_t lastJ1939SpeedTransmit = 0; - std::uint32_t lastTCStatusTransmit = 0; std::int32_t lastSpeedValue = 0; // Connection tracking for diagnostics std::map connectionTracker; std::uint32_t lastConnectionTableDumpMs = 0; std::uint32_t tcInitializedTimestampMs = 0; - bool tcStatusBurstSent = false; // CAN message log file std::ofstream canLogFile; diff --git a/src/app.cpp b/src/app.cpp index 8ab70e3..e0c0e0d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -534,14 +534,6 @@ bool Application::update() } } - // Send startup TC Status burst to help late-joining implements detect the TC - // ISO 11783-10 allows up to 5 Hz (200ms interval) for TC Status - // After the burst, AgIsoStack++ handles the periodic 2-second TC Status internally. - if (!tcStatusBurstSent && tcCF && tcCF->get_address_valid()) - { - send_tc_status_burst(); - } - // Update connection tracker state update_connection_tracker(); @@ -555,105 +547,6 @@ bool Application::update() return true; } -void Application::send_task_controller_status_message() -{ - // ISO 11783-10 B.8.1 Task Controller Status message - // PGN: 0xCB00 (Process Data), Command: 0x0E (Task Controller Status) - // Transmission rate: 2 seconds, Global destination - // - // Byte 1: Bits 4-1 = 0x0E (Command), Bits 8-5 = 0x0F (Element nibble NA) - // Byte 2: 0xFF (Element number MSB - not available) - // Bytes 3-4: 0xFFFF (DDI - not available) - // Byte 5: Status bits - // Bit 1 = Task totals active (1 = active, 0 = not active) - // Bit 2 = TC busy saving data - // Bit 3 = TC busy reading data - // Bit 4 = TC busy executing B.6 command - // Bit 8 = TC out of memory - // Byte 6: Source address of client for B.6 command (0 if not applicable) - // Byte 7: B.6 command being executed (0 if not applicable) - // Byte 8: Reserved - - std::uint8_t statusByte = 0x00; - if (tcServer && tcServer->get_task_totals_active()) - { - statusByte |= 0x01; // Bit 1: Task totals active - } - // Bits 2-4 and 8 are always 0 for now (not busy, not out of memory) - - std::array tcStatusData = { - 0xFE, // Byte 1: Command 0x0E + Element nibble 0xF - 0xFF, // Byte 2: Element number MSB (not available) - 0xFF, // Byte 3: DDI LSB (not available) - 0xFF, // Byte 4: DDI MSB (not available) - statusByte, // Byte 5: TC Status - 0x00, // Byte 6: Client SA for B.6 command (not applicable) - 0x00, // Byte 7: B.6 command being executed (not applicable) - 0xFF // Byte 8: Reserved - }; - - // Send to global destination (0xFF) with priority 3 (ISO 11783-10 requirement) - const auto transmitAttemptTimestamp = isobus::SystemTiming::get_timestamp_ms(); - bool sendSuccess = isobus::CANNetworkManager::CANNetwork.send_can_message( - 0xCB00, tcStatusData.data(), tcStatusData.size(), tcCF, nullptr, - isobus::CANIdentifier::CANPriority::Priority3); - - if (!sendSuccess) - { - std::cout << "[" << get_timestamp() << "] [TC Status] Failed to send TC Status message!" << std::endl; - } - else - { - std::cout << "[" << get_timestamp() << "] [TC Status] Sent: ID=0x0CBFFF" << std::hex - << static_cast(tcCF->get_address()) << std::dec - << " Payload=" << std::hex << std::uppercase - << std::setfill('0') << std::setw(2) << static_cast(tcStatusData[0]) << " " - << std::setw(2) << static_cast(tcStatusData[1]) << " " - << std::setw(2) << static_cast(tcStatusData[2]) << " " - << std::setw(2) << static_cast(tcStatusData[3]) << " " - << std::setw(2) << static_cast(tcStatusData[4]) << " " - << std::setw(2) << static_cast(tcStatusData[5]) << " " - << std::setw(2) << static_cast(tcStatusData[6]) << " " - << std::setw(2) << static_cast(tcStatusData[7]) << std::dec - << " [task_totals=" << (statusByte & 0x01) << "]" - << std::endl; - } - - // Update the transmit timestamp for every send attempt so failed sends - // still respect the minimum 2-second transmit period. - lastTCStatusTransmit = transmitAttemptTimestamp; -} - -void Application::send_tc_status_burst() -{ - // ISO 11783-10 allows TC Status up to 5 Hz (200ms minimum interval). - // Send a burst of 5 messages at 200ms intervals right after TC initialization - // to maximize the chance that late-joining implements detect the TC. - static std::uint8_t burstCount = 0; - static std::uint32_t lastBurstTransmit = 0; - - if (burstCount == 0) - { - lastBurstTransmit = tcInitializedTimestampMs; - } - - if (isobus::SystemTiming::time_expired_ms(lastBurstTransmit, 200)) - { - send_task_controller_status_message(); - burstCount++; - lastBurstTransmit = isobus::SystemTiming::get_timestamp_ms(); - lastTCStatusTransmit = lastBurstTransmit; // Prevent duplicate 2s timer - std::cout << "[" << get_timestamp() << "] [TC Status] Startup burst message " << static_cast(burstCount) << "/5 sent" << std::endl; - - if (burstCount >= 5) - { - tcStatusBurstSent = true; - burstCount = 0; - std::cout << "[" << get_timestamp() << "] [TC Status] Startup burst complete" << std::endl; - } - } -} - void Application::dump_connection_table() { auto now = isobus::SystemTiming::get_timestamp_ms(); From c7aefa7ce4fb266050a2b64dc3b45dcc9c6d8720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Mon, 25 May 2026 20:28:35 +0200 Subject: [PATCH 8/8] Fix linux build --- src/task_controller.cpp | 55 ++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/src/task_controller.cpp b/src/task_controller.cpp index cda8d37..c1fbdbf 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -10,7 +10,11 @@ #include "logging_utils.hpp" #include "settings.hpp" +#ifdef _WIN32 #include +#endif + +#include #include "isobus/isobus/isobus_device_descriptor_object_pool_helpers.hpp" #include "isobus/isobus/isobus_task_controller_server.hpp" @@ -301,7 +305,7 @@ bool MyTCServer::activate_object_pool(std::shared_ptr p auto it = std::find_if(label.begin(), label.end(), [](char c) { return c == '\0' || static_cast(c) == 0x03; }); label.erase(it, label.end()); - auto fileName = std::to_string(partnerCF->get_NAME().get_full_name()) + "\\" + label + ".ddop"; + auto fileName = std::to_string(partnerCF->get_NAME().get_full_name()) + "/" + label + ".ddop"; std::vector binaryPool; if (state.get_pool().generate_binary_object_pool(binaryPool)) { @@ -381,34 +385,16 @@ bool MyTCServer::deactivate_object_pool(std::shared_ptr static bool remove_directory_recursive(const std::string &path) { - WIN32_FIND_DATAA findData; - auto searchPath = path + "\\*"; - auto hFind = FindFirstFileA(searchPath.c_str(), &findData); - if (hFind == INVALID_HANDLE_VALUE) + try { - return RemoveDirectoryA(path.c_str()) != 0; + std::filesystem::remove_all(path); + return true; } - - do + catch (const std::filesystem::filesystem_error &e) { - std::string name = findData.cFileName; - if (name == "." || name == "..") - { - continue; - } - std::string fullPath = path + "\\" + name; - if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - remove_directory_recursive(fullPath); - } - else - { - DeleteFileA(fullPath.c_str()); - } - } while (FindNextFileA(hFind, &findData)); - - FindClose(hFind); - return RemoveDirectoryA(path.c_str()) != 0; + std::cerr << "[" << get_timestamp() << "] [Error] Failed to remove directory: " << e.what() << std::endl; + return false; + } } bool MyTCServer::delete_device_descriptor_object_pool(std::shared_ptr partnerCF, ObjectPoolDeletionErrors &) @@ -417,13 +403,13 @@ bool MyTCServer::delete_device_descriptor_object_pool(std::shared_ptr