diff --git a/CMakeLists.txt b/CMakeLists.txt index c3d2d3d..7604d83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,8 +61,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) diff --git a/include/app.hpp b/include/app.hpp index 6ceb977..76722d9 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,29 @@ #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 +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: @@ -31,7 +55,12 @@ class Application void stop(); private: - void send_task_controller_status_message(); + 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); + 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(); @@ -44,8 +73,16 @@ 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; std::int32_t lastSpeedValue = 0; + + // Connection tracking for diagnostics + std::map connectionTracker; + std::uint32_t lastConnectionTableDumpMs = 0; + std::uint32_t tcInitializedTimestampMs = 0; + + // CAN message log file + std::ofstream canLogFile; }; diff --git a/include/logging_utils.hpp b/include/logging_utils.hpp index 4131135..75bd303 100644 --- a/include/logging_utils.hpp +++ b/include/logging_utils.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -27,3 +28,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/settings.hpp b/include/settings.hpp index f1e5b13..57e2a7a 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -77,6 +77,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 @@ -84,11 +98,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..bbd87da 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; @@ -93,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 3425304..d64390c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -10,15 +10,18 @@ #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 "logging_utils.hpp" +#include #include #include @@ -69,6 +72,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 +100,63 @@ 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); + 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); @@ -174,13 +232,86 @@ 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 + // 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), + 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); + + // 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; + // Initialize speed and distance messages if (tecuCF && tecuCF->get_address_valid()) { @@ -332,6 +463,8 @@ bool Application::update() tcServer->request_measurement_commands(); tcServer->update(); + if (tcFunctionalities) + tcFunctionalities->update(); if (tecuFunctionalities) tecuFunctionalities->update(); if (speedMessagesInterface) @@ -404,69 +537,250 @@ bool Application::update() } } - // 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()) + // 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; +} + +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) { - static bool firstStatusSent = false; - send_task_controller_status_message(); - if (!firstStatusSent) + 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() +{ + // 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 + for (auto &client : clientsRef) + { + auto nameFull = client.first->get_NAME().get_full_name(); + auto it = connectionTracker.find(nameFull); + if (it != connectionTracker.end()) { - std::cout << "[" << get_timestamp() << "] [TC Status] First TC Status message sent (PGN 0xCB00)" << std::endl; - firstStatusSent = true; + it->second.registeredAsClient = true; + it->second.address = client.first->get_address(); } } - return true; + // 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::send_task_controller_status_message() +void Application::log_can_working_set_master(const isobus::CANMessage &message, void *parent) { - // 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()) + if (nullptr == parent) { - statusByte |= 0x01; // Bit 1: Task totals active + return; } - // 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 - }; + 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; +} - // 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) - const auto transmitAttemptTimestamp = isobus::SystemTiming::get_timestamp_ms(); - if (!isobus::CANNetworkManager::CANNetwork.send_can_message(0xCB00, tcStatusData.data(), tcStatusData.size(), tcCF)) +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()) { - std::cout << "[" << get_timestamp() << "] [TC Status] Failed to send TC Status message!" << std::endl; + return; } - // Update the transmit timestamp for every send attempt so failed sends - // still respect the minimum 2-second transmit period. - lastTCStatusTransmit = transmitAttemptTimestamp; + 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::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() diff --git a/src/logging.cpp b/src/logging.cpp index 4996965..48c8794 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; @@ -76,6 +78,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 642c2aa..ffd8dc1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -138,6 +138,11 @@ class ArgumentProcessor return fileLogging; } + bool has_log_level() const + { + return logLevelSpecified; + } + private: bool parse_option(std::string option) { @@ -205,6 +210,7 @@ class ArgumentProcessor } else if ("--log_level" == key) { + logLevelSpecified = true; if ("debug" == value) { isobus::CANStackLogger::set_log_level(isobus::CANStackLogger::LoggingLevel::Debug); @@ -242,6 +248,7 @@ class ArgumentProcessor CANAdapter canAdapter = CANAdapter::NONE; std::string canChannel; bool fileLogging = false; + bool logLevelSpecified = false; }; static std::shared_ptr create_can_driver(const ArgumentProcessor &args) diff --git a/src/settings.cpp b/src/settings.cpp index a601b9f..0e9be3b 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -21,6 +21,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")); @@ -84,6 +87,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; } @@ -93,6 +156,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()) @@ -104,6 +170,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..c1fbdbf 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -10,6 +10,12 @@ #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" @@ -244,14 +250,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) { } @@ -298,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)) { @@ -376,10 +383,57 @@ bool MyTCServer::deactivate_object_pool(std::shared_ptr return true; } +static bool remove_directory_recursive(const std::string &path) +{ + try + { + std::filesystem::remove_all(path); + return true; + } + catch (const std::filesystem::filesystem_error &e) + { + 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 &) { - clients.erase(partnerCF); - uploadedPools.erase(partnerCF); + 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 = std::filesystem::path(dummyFilePath).parent_path().string(); + auto parentDirPath = std::filesystem::path(currentFolderPath).parent_path().string(); + auto archiveFolderPath = parentDirPath + "/" + folderName + "_archive"; + + // If archive already exists, delete it first + if (std::filesystem::exists(archiveFolderPath)) + { + 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 + try + { + std::filesystem::rename(currentFolderPath, archiveFolderPath); + std::cout << "[" << get_timestamp() << "] [TC Server] Archived DDOP folder for NAME " << nameFull + << " to " << archiveFolderPath << std::endl; + } + catch (const std::filesystem::filesystem_error &e) + { + std::cout << "[" << get_timestamp() << "] [TC Server] Failed to archive DDOP folder for NAME " << nameFull + << " (" << e.what() << ")" << std::endl; + } + + { + clients.erase(partnerCF); + uploadedPools.erase(partnerCF); + } return true; }