diff --git a/include/app.hpp b/include/app.hpp index 6ceb977..b72cab2 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: @@ -32,6 +56,13 @@ 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); + 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 +75,18 @@ 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; + 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/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..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 c25892c..8ab70e3 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; @@ -66,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) @@ -89,13 +97,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); @@ -171,13 +229,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()) { @@ -329,6 +460,8 @@ bool Application::update() tcServer->request_measurement_commands(); tcServer->update(); + if (tcFunctionalities) + tcFunctionalities->update(); if (tecuFunctionalities) tecuFunctionalities->update(); if (speedMessagesInterface) @@ -401,16 +534,22 @@ 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()) + // 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()) { - 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; - } + send_tc_status_burst(); + } + + // 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; @@ -453,19 +592,301 @@ 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. 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(); + 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() +{ + // 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()) + { + 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::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/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..cda8d37 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,10 +379,74 @@ 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 &) { - 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 = 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; }