diff --git a/include/hnz.h b/include/hnz.h index 06618cd..2ad4d5a 100644 --- a/include/hnz.h +++ b/include/hnz.h @@ -179,11 +179,13 @@ class HNZ { */ inline void setDaySection(unsigned char daySection) { m_daySection = daySection; } +#ifdef UNIT_TEST protected: /** * Sends a CG request (reset counters if any was already in progress) */ void sendInitialGI(); +#endif private: // Tells if the plugin is currently running @@ -224,7 +226,7 @@ class HNZ { /** * Waits for new messages and processes them */ - void receive(std::shared_ptr hnz_path_in_use); + void receive(HNZPath* hnz_path_in_use); /** * Handle a message: translate the message and send it to Fledge. @@ -288,6 +290,7 @@ class HNZ { unsigned int ts_iv = 0; unsigned int ts_c = 0; unsigned int ts_s = 0; + bool empty_timestamp = false; // TSCE with no timestamp, the module HNZtoPivot will fill it artificially // TS only bool cg = false; // TM only diff --git a/include/hnzconf.h b/include/hnzconf.h index 6dbafe6..a0100db 100644 --- a/include/hnzconf.h +++ b/include/hnzconf.h @@ -15,6 +15,8 @@ #include #include "rapidjson/document.h" +#define MAXPATHS 2 + // Local definition of make_unique as it is only available since C++14 and right now fledge-south-hnz is built with C++11 template std::unique_ptr make_unique(Args&&... args) { @@ -149,32 +151,18 @@ class HNZConf { unsigned int getLastTSAddress() const; /** - * Get the IP address to remote IEC 104 server (A path) + * Get paths ips as an array of string * - * @return string + * @return Array of string containing paths ip */ - string get_ip_address_A() const { return m_ip_A; } + std::array get_paths_ip() const { return m_paths_ip; } /** - * Get the port number to remote IEC 104 server (A path) + * Get paths ports as an array of unsigned int * - * @return unsigned int + * @return Array of int containing paths ip */ - unsigned int get_port_A() const { return m_port_A; } - - /** - * Get the IP address to remote IEC 104 server (B path) - * - * @return string - */ - string get_ip_address_B() const { return m_ip_B; } - - /** - * Get the port number to remote IEC 104 server (B path) - * - * @return unsigned int - */ - unsigned int get_port_B() const { return m_port_B; } + std::array get_paths_port() const { return m_paths_port; } /** * Get the remote server station address @@ -199,18 +187,10 @@ class HNZConf { unsigned int get_max_sarm() const { return m_max_sarm; } /** - * Get the max number of authorized repeats for path A - * - * @return unsigned int - */ - unsigned int get_repeat_path_A() const { return m_repeat_path_A; } - - /** - * Get the max number of authorized repeats for path B - * + * Get the max number of authorized repeats for all paths * @return unsigned int */ - unsigned int get_repeat_path_B() const { return m_repeat_path_B; } + std::array get_paths_repeat() const { return m_paths_repeat; } /** * Get the time in ms allowed for the receiver to acknowledge a frame, after this @@ -323,14 +303,12 @@ class HNZConf { */ bool m_importDatapoint(const Value &msg); - string m_ip_A, m_ip_B = ""; - unsigned int m_port_A = 0; - unsigned int m_port_B = 0; + std::array m_paths_ip = {"", ""}; + std::array m_paths_port = {0, 0}; + std::array m_paths_repeat = {0, 0}; unsigned int m_remote_station_addr = 0; unsigned int m_inacc_timeout = 0; unsigned int m_max_sarm = 0; - unsigned int m_repeat_path_A = 0; - unsigned int m_repeat_path_B = 0; unsigned int m_repeat_timeout = 0; unsigned int m_anticipation_ratio = 0; BulleFormat m_test_msg_send; diff --git a/include/hnzconnection.h b/include/hnzconnection.h index 00790df..7470629 100644 --- a/include/hnzconnection.h +++ b/include/hnzconnection.h @@ -61,39 +61,44 @@ class HNZConnection { * commands). * @return the active path */ - std::shared_ptr getActivePath() { + HNZPath* getActivePath() { std::lock_guard lock(m_path_mutex); return m_active_path; }; /** - * Get the path in stand-by. - * @return the active path + * Get a pointer to the first passive path found, if any. + * @return a passive path or nullptr */ - std::shared_ptr getPassivePath() { + HNZPath* getPassivePath() { std::lock_guard lock(m_path_mutex); - return m_passive_path; + for (HNZPath* path: m_paths) + { + if(path != nullptr && path != m_active_path) return path; + } + return nullptr; }; /** - * Get both active and passive path (with a single lock) - * @return a path pair (active_path, passive_path) + * Get both active and passive path as an array of pointers (with a single lock) + * @return array of existing paths */ - std::pair, std::shared_ptr> getBothPath() { + std::array getPaths() { std::lock_guard lock(m_path_mutex); - return std::make_pair(m_active_path, m_passive_path); + return m_paths; }; /** - * Switch between the active path and passive path. Must be called in case of - * connection problem on the active path. + * Manages the connection state of the different paths. */ - void switchPath(); + void pathConnectionChanged(HNZPath* path, bool isReady); +#ifdef UNIT_TEST /** * Send the initial GI message (reset retry counter if it was in progress) */ void sendInitialGI(); +#endif /** * Called to update the current connection status @@ -124,6 +129,17 @@ class HNZConnection { */ bool isRunning() const { return m_is_running; }; + /** + * A running path can extract messages only if it is active, or has at least its input connected. + * */ + bool canPathExtractMessage(HNZPath* path){ + if(path == nullptr) return false; + if(path == m_active_path){ + return true; + } + return path == m_first_input_connected && m_active_path == nullptr; + } + /** * Returns the name of the Fledge service instanciating this plugin */ @@ -135,20 +151,29 @@ class HNZConnection { */ inline void setDaySection(unsigned char daySection) { m_hnz_fledge->setDaySection(daySection); } + /** + * Allows a path to notify that a GI request has been sent + */ + void notifyGIsent(); + private: - std::shared_ptr m_active_path; - std::shared_ptr m_passive_path; + HNZPath* m_active_path = nullptr; + // First path in protocol state INPUT_CONNECTED, covers edge cases of the protocol + HNZPath* m_first_input_connected = nullptr; + std::array m_paths = {nullptr, nullptr}; std::recursive_mutex m_path_mutex; std::shared_ptr m_messages_thread; // Main thread that monitors messages std::atomic m_is_running{false}; // If false, the connection thread will stop uint64_t m_current = 0; // Store the last time requested uint64_t m_elapsedTimeMs = 0; // Store elapsed time in milliseconds every time m_current is updated uint64_t m_days_since_epoch = 0; + bool m_sendInitNexConnection = true; // The init messages (time/date/GI) have to be sent + int m_gi_repeat = 0; + long m_gi_start_time = 0; // Plugin configuration - int gi_repeat_count_max = 0; // time to wait for GI completion - int gi_time_max = 0; // repeat GI for this number of times in case it is - // incomplete + int m_gi_repeat_count_max = 0; // time to wait for GI completion + int m_gi_time_max = 0; // repeat GI for this number of times in case it is incomplete GIScheduleFormat m_gi_schedule; int m_repeat_timeout = 0; // time allowed for the receiver to acknowledge a frame @@ -169,7 +194,7 @@ class HNZConnection { * If a message is not acknowledged, then a retransmission request is sent. * @param path the related path */ - void m_check_timer(std::shared_ptr path) const; + void m_check_timer(HNZPath* path); /** * Check the state of ongoing GI (General Interrogation) and manage scheduled @@ -181,7 +206,7 @@ class HNZConnection { * Checks that sent command messages have been acknowledged and removes them * from the sent queue. */ - void m_check_command_timer(); + void m_check_command_timer(HNZPath* path); /** * Update the current time and time elapsed since last call to this function diff --git a/include/hnzpath.h b/include/hnzpath.h index ff9d568..c37734e 100644 --- a/include/hnzpath.h +++ b/include/hnzpath.h @@ -16,13 +16,46 @@ #include #include #include +#include #include #include "hnzconnection.h" -#define CONNECTION 0 -#define CONNECTED 1 +// Connection event used to transition between protocol states +// Some event are unused as they do not appear in the protocol state automaton. +// These states correspond to actions performed by the plugin, and they could be included. +enum class ConnectionEvent : unsigned char { + TCP_CNX_ESTABLISHED = 0, // unused + RECEIVED_SARM = 1, + RECEIVED_UA = 2, + TO_RECV = 3, + MAX_SEND = 4, + TCP_CNX_LOST = 5, + TO_SEND = 6, // unused + RECEIVED_INFO = 7, // unused + SEND_TC = 8, // unused + TO_UA = 9, // unused + MAX_SARM_SENT = 10, + TO_LASTCG = 11,// unused + TO_TCACK = 12 +}; + +// HNZ protocol state +enum class ProtocolState : unsigned char { + CONNECTION = 0, // No connection has been established + INPUT_CONNECTED = 1, // SARM received + OUTPUT_CONNECTED = 2, // UA received + CONNECTED = 3 // Fully connected +}; + +// Connection state +enum class ConnectionState : unsigned char { + DISCONNECTED = 0, // No connection has been established + PENDING_HNZ = 1, // TCP OK, waiting for protocolState to update to allow the transit of messages + PASSIVE = 2, // Available for a path switch + ACTIVE = 3 // Path used to receive and send messages (max. 1) +}; /** * @brief Structure containing internal informations about a message @@ -61,11 +94,16 @@ class HNZPath { // Give access to HNZPath private members for HNZConnection friend class HNZConnection; public: - HNZPath(const std::shared_ptr hnz_conf, HNZConnection* hnz_connection, bool secondary); + HNZPath(const std::shared_ptr hnz_conf, HNZConnection* hnz_connection, int repeat_max, std::string ip, unsigned int port, std::string pathLetter); ~HNZPath(); string getName() const { return m_name_log; }; + /** + * Triggers a transition from the protocol state automaton according to a ConnectionEvent. + */ + void protocolStateTransition(const ConnectionEvent event); + /** * Connect (or re-connect) to the HNZ PA (TCP connection and HNZ connection * management if isn't started). @@ -90,17 +128,11 @@ class HNZPath { */ bool isHNZConnected() { std::lock_guard lock(m_protocol_state_mutex); - return (m_protocol_state == CONNECTED) && isConnected(); + return (m_protocol_state == ProtocolState::CONNECTED) && isTCPConnected(); }; /** - * Is the TCP connection with the PA established and still alive? - * @return true if connected, false otherwise - */ - bool isConnected() { return m_connected && isTCPConnected(); }; - - /** - * Is the TCP connection with the PA still alive according to HNZ client? + * Is the TCP connection with the PA still alive ? * @return true if connected, false otherwise */ bool isTCPConnected(); @@ -143,31 +175,36 @@ class HNZPath { void sendGeneralInterrogation(); /** - * Go to connection state. Can be call when there is a problem with the path. - * It will re-synchronize with the PA. + * Set the state of the path. */ - void go_to_connection(); + void setConnectionState(ConnectionState newState); /** - * Set the state of the path. + * Set the current connection state to PENDING_HNZ. + * This method serve in the protocolState automaton, to notify that the connection on protocol level is pending. */ - void setActivePath(bool active); + void setConnectionPending(); /** - * Gets the state of the path - * @return true if active, false if passive + * Get current connection state. */ - bool isActivePath() const { return m_is_active_path; } + ConnectionState getConnectionState() const { + return m_connection_state; + } /** - * Gets the state of the HNZ protocol (CONNECTION, CONNECTED) - * @return CONNECTION if SARM/UA step is not complete, CONNECTED after that + * Gets the state of the HNZ protocol (CONNECTION, INPUT_CONNECTED, OUTPUT_CONNECTED, CONNECTED) */ - int getProtocolState() const { + ProtocolState getProtocolState() const { std::lock_guard lock(m_protocol_state_mutex); return m_protocol_state; } + long long getLastConnected() const { return m_last_connected;} + void resetLastConnected() { m_last_connected = 0;} + + void sendInitMessages(); + private: std::unique_ptr m_hnz_client; // HNZ Client that manage TCP connection // (receives/assembles and sends TCP frame) @@ -180,21 +217,17 @@ class HNZPath { long long last_sent_time = 0; // Timestamp of the last information message sent int repeat_max = 0; // max number of authorized repeats - int gi_repeat = 0; // number of time a GI is repeated - long gi_start_time = 0; // GI start time std::shared_ptr m_connection_thread; // Main thread that maintains the connection std::mutex m_connection_thread_mutex; // mutex to protect changes in m_connection_thread atomic m_is_running{true}; // If false, the connection thread will stop - atomic m_connected{false}; // TCP Connection state with the PA std::mutex m_state_changed_mutex; // mutex to use condition variable below std::condition_variable m_state_changed_cond; // Condition variable used to notify changes in m_manageHNZProtocolConnection thread bool m_state_changed = false; // variable set to true when m_protocol_state changed - // Initializing to CONNECTED ensures that the initial state transition from go_to_connection generates an audit - int m_protocol_state = CONNECTED; // HNZ Protocol connection state + ProtocolState m_protocol_state = ProtocolState::CONNECTION; // HNZ Protocol connection state + ConnectionState m_connection_state = ConnectionState::DISCONNECTED; // Effective connection state mutable std::recursive_mutex m_protocol_state_mutex; // mutex to protect changes in m_protocol_state - bool m_is_active_path = false; // Plugin configuration string m_ip; // IP of the PA @@ -227,9 +260,8 @@ class HNZPath { long long m_last_msg_time = 0; // Timestamp of the last reception in ms long long m_last_msg_sent_time = 0; // Timestamp of the last sent message in ms long long m_last_sarm_sent_time = 0; // Timestamp of the last sent SARM message in ms - bool sarm_PA_received = false; // The SARM sent by the PA was received - bool sarm_ARP_UA = false; // The UA sent by the PA (after receiving our SARM) was - // received + // Timestamp of the last transition to CONNECTED, to prevent a double-SARM reseting connection and tempo init messages + long long m_last_connected = 0; int m_nbr_sarm_sent = 0; // Number of SARM sent int m_repeat = 0; // Number of times the sent message is repeated @@ -240,18 +272,11 @@ class HNZPath { void m_manageHNZProtocolConnection(); /** - * Manage the HNZ protocol when connecting - * @param now epoch time in ms - * @return Number of miliseconds to sleep after this step - */ - std::chrono::milliseconds m_manageHNZProtocolConnecting(long long now); - - /** - * Manage the HNZ protocol when connected - * @param now epoch time in ms - * @return Number of miliseconds to sleep after this step + * Manage the HNZ protocol according to the current ProtocolState m_protocol_state + * @param now epoch time in milliseconds + * @return Number of milliseconds to sleep after this step */ - std::chrono::milliseconds m_manageHNZProtocolConnected(long long now); + std::chrono::milliseconds m_manageHNZProtocolState(long long now); /** * Analyze a HNZ frame. If the frame is an information frame then we extract @@ -333,6 +358,14 @@ class HNZPath { */ bool m_sendInfoImmediately(Message message); + /** + * Process an information frame and update the list "messages" accordingly + * @param data Raw data + * @param int Frame size + * @param messages Messages list to be updated + */ + void m_receivedINFO(unsigned char* data, int size, vector>* messages); + /** * Send a date configuration message */ @@ -343,23 +376,6 @@ class HNZPath { */ void m_send_time_setting(); - /** - * Go to the CONNECTED statue of the HNZ connection - */ - void m_go_to_connected(); - - /** - * Get the other path if any - * @return Second HNZ path, or nullptr if no other path defined - */ - std::shared_ptr m_getOtherPath() const; - - /** - * Tells if the HNZ connection is fully established and active on the other path - * @return True if the connection is established, false if not established or no other path defined - */ - bool m_isOtherPathHNZConnected() const; - /** * Called after sending a Command to store its information until a ACK is received, if the command was actually sent * @param type Type of command: TC or TVC @@ -383,14 +399,6 @@ class HNZPath { */ void m_NRAccepted(int nr); - /** - * Returns mutex used to protect the protocol state from the other path, - * if no other path is defined, returns a static mutex object instead - * so that the return of this function can always be passed to a lock - * @return Mutex protecting m_protocol_state from the other path, or static mutex - */ - std::recursive_mutex& m_getOtherPathProtocolStateMutex() const; - /** * Send a frame through the HNZ client and record the last send time * @param msg Bytes of the frame to send @@ -405,6 +413,108 @@ class HNZPath { * | INFO | Center | */ void m_sendFrame(unsigned char *msg, unsigned long msgSize, bool usePAAddr = false); + + /** + * Allow for the re-emission of SARM messages + */ + void resetSarmCounters(); + + /** + * Performs the different actions necessary on entry in the protocol state CONNECTED + */ + void resolveProtocolStateConnected(); + + /** + * Performs the different actions necessary on entry in the protocol state CONNECTION + */ + void resolveProtocolStateConnection(); + + /* Discard unacknowledged messages and messages waiting to be sent + */ + void resetInputVariables(); + + /* Discard unacknowledged messages and messages waiting to be sent + */ + void resetOutputVariables(); + + /** + * Calls HNZClient to stop the TCP connection + */ + void stopTCP(); + + /** + * Send audit for path connection status : CONNECTED + */ + void sendAuditSuccess(); + + /** + * Send audit for path connection status : CONNECTION + */ + void sendAuditFail(); + + /** + * Discard unacknowledged messages and messages waiting to be sent + */ + void discardMessages(); + + /** + * Helper function to evaluate if a message is a BULLE. BULLE can be sent in OUTPUT_CONNECTED and CONNECT states. + */ + bool isBULLE(const unsigned char* msg, unsigned long size) const; + + /*! \brief Protocol state automaton + * + * Each entry of this map represents a transition between protocol states, triggered by a ConnectionEvent and resolved by an ordered list of actions. + */ + std::map, std::pair>> protocolStateTransitionMap = { + {{ProtocolState::CONNECTION, ConnectionEvent::RECEIVED_SARM }, {ProtocolState::INPUT_CONNECTED, {&HNZPath::setConnectionPending, &HNZPath::resetInputVariables} }}, + {{ProtocolState::CONNECTION, ConnectionEvent::RECEIVED_UA }, {ProtocolState::OUTPUT_CONNECTED, {&HNZPath::setConnectionPending, &HNZPath::resetOutputVariables} }}, + {{ProtocolState::CONNECTION, ConnectionEvent::MAX_SARM_SENT }, {ProtocolState::CONNECTION, {&HNZPath::stopTCP, &HNZPath::resolveProtocolStateConnection} }}, + {{ProtocolState::INPUT_CONNECTED, ConnectionEvent::RECEIVED_SARM }, {ProtocolState::INPUT_CONNECTED, {&HNZPath::setConnectionPending, &HNZPath::resetInputVariables} }}, + {{ProtocolState::INPUT_CONNECTED, ConnectionEvent::TO_RECV }, {ProtocolState::CONNECTION, {&HNZPath::resolveProtocolStateConnection} }}, + {{ProtocolState::INPUT_CONNECTED, ConnectionEvent::RECEIVED_UA }, {ProtocolState::CONNECTED, {&HNZPath::resetOutputVariables, &HNZPath::resolveProtocolStateConnected, &HNZPath::sendAuditSuccess} }}, + {{ProtocolState::INPUT_CONNECTED, ConnectionEvent::MAX_SARM_SENT }, {ProtocolState::CONNECTION, {&HNZPath::stopTCP, &HNZPath::resolveProtocolStateConnection} }}, + {{ProtocolState::OUTPUT_CONNECTED, ConnectionEvent::RECEIVED_SARM }, {ProtocolState::CONNECTED, {&HNZPath::resetInputVariables, &HNZPath::sendAuditSuccess, &HNZPath::resolveProtocolStateConnected} }}, + {{ProtocolState::OUTPUT_CONNECTED, ConnectionEvent::MAX_SEND }, {ProtocolState::CONNECTION, {&HNZPath::resetSarmCounters, &HNZPath::discardMessages, &HNZPath::resolveProtocolStateConnection} }}, + {{ProtocolState::CONNECTED, ConnectionEvent::MAX_SEND }, {ProtocolState::INPUT_CONNECTED, {&HNZPath::setConnectionPending, &HNZPath::sendAuditFail, &HNZPath::resetSarmCounters, &HNZPath::discardMessages, &HNZPath::resetInputVariables} }}, + {{ProtocolState::CONNECTED, ConnectionEvent::TO_TCACK }, {ProtocolState::INPUT_CONNECTED, {&HNZPath::setConnectionPending, &HNZPath::sendAuditFail, &HNZPath::resetSarmCounters, &HNZPath::discardMessages, &HNZPath::resetInputVariables} }}, + {{ProtocolState::CONNECTED, ConnectionEvent::RECEIVED_SARM }, {ProtocolState::INPUT_CONNECTED, {&HNZPath::setConnectionPending, &HNZPath::sendAuditFail, &HNZPath::resetSarmCounters, &HNZPath::discardMessages, &HNZPath::resetInputVariables} }}, + {{ProtocolState::CONNECTED, ConnectionEvent::TO_RECV }, {ProtocolState::OUTPUT_CONNECTED, {&HNZPath::setConnectionPending, &HNZPath::sendAuditFail, &HNZPath::discardMessages, &HNZPath::resetOutputVariables} }}, + {{ProtocolState::CONNECTION, ConnectionEvent::TCP_CNX_LOST }, {ProtocolState::CONNECTION, {&HNZPath::resolveProtocolStateConnection} }}, + {{ProtocolState::INPUT_CONNECTED, ConnectionEvent::TCP_CNX_LOST }, {ProtocolState::CONNECTION, {&HNZPath::resolveProtocolStateConnection} }}, + {{ProtocolState::OUTPUT_CONNECTED, ConnectionEvent::TCP_CNX_LOST }, {ProtocolState::CONNECTION, {&HNZPath::discardMessages, &HNZPath::resolveProtocolStateConnection} }}, + {{ProtocolState::CONNECTED, ConnectionEvent::TCP_CNX_LOST }, {ProtocolState::CONNECTION, {&HNZPath::sendAuditFail, &HNZPath::resolveProtocolStateConnection, &HNZPath::discardMessages} }} + }; + + std::map connectionState2str = { + {ConnectionState::DISCONNECTED, "disconnected" }, + {ConnectionState::PENDING_HNZ, "pending-hnz" }, + {ConnectionState::PASSIVE, "passive" }, + {ConnectionState::ACTIVE, "active" } + }; + + std::map protocolState2str = { + {ProtocolState::CONNECTION, "CONNECTION" }, + {ProtocolState::INPUT_CONNECTED, "INPUT_CONNECTED" }, + {ProtocolState::OUTPUT_CONNECTED, "OUTPUT_CONNECTED" }, + {ProtocolState::CONNECTED, "CONNECTED" } + }; + + std::map connectionEvent2str = { + {ConnectionEvent::TCP_CNX_ESTABLISHED, "TCP_CNX_ESTABLISHED" }, + {ConnectionEvent::RECEIVED_SARM, "RECEIVED_SARM" }, + {ConnectionEvent::RECEIVED_UA, "RECEIVED_UA" }, + {ConnectionEvent::TO_RECV, "TO_RECV" }, + {ConnectionEvent::MAX_SEND, "MAX_SEND" }, + {ConnectionEvent::TCP_CNX_LOST, "TCP_CNX_LOST" }, + {ConnectionEvent::TO_SEND, "TO_SEND" }, + {ConnectionEvent::RECEIVED_INFO, "RECEIVED_INFO" }, + {ConnectionEvent::SEND_TC, "SEND_TC" }, + {ConnectionEvent::TO_UA, "TO_UA" }, + {ConnectionEvent::MAX_SARM_SENT, "MAX_SARM_SENT" }, + {ConnectionEvent::TO_LASTCG, "TO_LASTCG" }, + {ConnectionEvent::TO_TCACK, "TO_TCACK" } + }; }; #endif \ No newline at end of file diff --git a/src/hnz.cpp b/src/hnz.cpp index f6b274f..4483dde 100644 --- a/src/hnz.cpp +++ b/src/hnz.cpp @@ -50,13 +50,13 @@ void HNZ::start(bool requestedStart /*= false*/) { m_sendAllTMQualityReadings(true, false); m_sendAllTSQualityReadings(true, false); - auto pathPair = m_hnz_connection->getBothPath(); - m_receiving_thread_A = make_unique(&HNZ::receive, this, pathPair.first); - if (pathPair.second != nullptr) { + auto paths = m_hnz_connection->getPaths(); + m_receiving_thread_A = make_unique(&HNZ::receive, this, paths[0]); + if (paths[1] != nullptr) { // Wait after getting the passive path pointer as connection init of active path may swap path this_thread::sleep_for(std::chrono::milliseconds(1000)); // Path B is defined in the configuration - m_receiving_thread_B = make_unique(&HNZ::receive, this, pathPair.second); + m_receiving_thread_B = make_unique(&HNZ::receive, this, paths[1]); } m_hnz_connection->start(); @@ -87,7 +87,7 @@ void HNZ::stop(bool requestedStop /*= false*/) { m_receiving_thread_B = nullptr; } // Connection must be freed after management threads of both path - // as HNZ::m_hnz_connection, HNZConnection::m_active_path and HNZConnection::m_passive_path + // as HNZ::m_hnz_connection and paths of HNZConnection // are used in HNZ::receive running on the threads if (m_hnz_connection != nullptr) { m_hnz_connection = nullptr; @@ -167,7 +167,11 @@ bool HNZ::setJsonConfig(const string& protocol_conf_json, const string& msg_conf return true; } -void HNZ::receive(std::shared_ptr hnz_path_in_use) { +void HNZ::receive(HNZPath* hnz_path_in_use) { + if(!hnz_path_in_use){ + HnzUtility::log_info(HnzUtility::NamePlugin + " - HNZ::receive - No path to use, exit"); + return; + } { std::lock_guard guard(m_configMutex); if (!m_hnz_conf->is_complete()) { @@ -195,10 +199,8 @@ void HNZ::receive(std::shared_ptr hnz_path_in_use) { // Waiting for data messages = hnz_path_in_use->getData(); - if (messages.empty() && !hnz_path_in_use->isConnected()) { - HnzUtility::log_warn("%s Connection lost, reconnecting active path and switching to other path", beforeLog.c_str()); - // If connection lost, try to switch path - if (hnz_path_in_use->isActivePath()) m_hnz_connection->switchPath(); + if (messages.empty() && !hnz_path_in_use->isTCPConnected()) { + HnzUtility::log_warn("%s Connection lost, reconnecting path.", beforeLog.c_str()); // Try to reconnect, unless thread is stopping if (m_is_running) { hnz_path_in_use->disconnect(); @@ -332,6 +334,7 @@ void HNZ::m_handleTM4(vector& readings, const vector& da } void HNZ::m_handleTSCE(vector& readings, const vector& data) const { + std::string beforeLog = HnzUtility::NamePlugin + " - HNZ::m_handleTSCE - "; string msg_code = "TS"; unsigned int msg_address = stoi(to_string((int)data[1]) + to_string((int)(data[2] >> 5))); // AD0 + ADB @@ -362,6 +365,13 @@ void HNZ::m_handleTSCE(vector& readings, const vector& d params.ts_c = ts_c; params.ts_s = ts_s; params.cg = false; + + // In stateINPUT_CONNECTED, timestamp mod10 might be uninitialized + if(m_hnz_connection->getActivePath() != nullptr && m_hnz_connection->getActivePath()->getProtocolState() == ProtocolState::INPUT_CONNECTED){ + HnzUtility::log_info("%s TSCE discarded in path protocol state INPUT_CONNECTED.", beforeLog.c_str()); + params.empty_timestamp = true; + } + readings.push_back(m_prepare_reading(params)); } @@ -450,7 +460,13 @@ void HNZ::m_handleATVC(vector& readings, const vector& d unsigned int msg_address = data[1] & 0x1F; // AD0 - m_hnz_connection->getActivePath()->receivedCommandACK("TVC", msg_address); + // Acknowledge the TVC on the path from which it was sent + // Unexpected partial disconnection of the active path can generate ill states if the message is not acknowledged + for (auto& path: m_hnz_connection->getPaths()) + { + if(path == nullptr) continue; + path->receivedCommandACK("TVC", msg_address); + } string label = m_hnz_conf->getLabel(msg_code, msg_address); if (label.empty()) { @@ -479,7 +495,13 @@ void HNZ::m_handleATC(vector& readings, const vector& da unsigned int msg_address = stoi(to_string((int)data[1]) + to_string((int)(data[2] >> 5))); // AD0 + ADB - m_hnz_connection->getActivePath()->receivedCommandACK("TC", msg_address); + // Acknowledge the TC on the path from which it was sent + // Unexpected partial disconnection of the active path can generate ill states if the message is not acknowledged + for (auto& path: m_hnz_connection->getPaths()) + { + if(path == nullptr) continue; + path->receivedCommandACK("TC", msg_address); + } string label = m_hnz_conf->getLabel(msg_code, msg_address); if (label.empty()) { @@ -549,7 +571,7 @@ Reading HNZ::m_prepare_reading(const ReadingParameters& params) { } if (isTSCE) { // Casting "unsigned long" into "long" for do_ts in order to match implementation of iec104 plugin - measure_features->push_back(m_createDatapoint("do_ts", static_cast(params.ts))); + if(!params.empty_timestamp) measure_features->push_back(m_createDatapoint("do_ts", static_cast(params.ts))); measure_features->push_back(m_createDatapoint("do_ts_iv", static_cast(params.ts_iv))); measure_features->push_back(m_createDatapoint("do_ts_c", static_cast(params.ts_c))); measure_features->push_back(m_createDatapoint("do_ts_s", static_cast(params.ts_s))); @@ -680,6 +702,10 @@ int HNZ::processCommandOperation(int count, PLUGIN_PARAMETER** params) { return 1; } + if(m_hnz_connection->getActivePath() == nullptr){ + return 2; + } + if (type == "TC") { bool success = m_hnz_connection->getActivePath()->sendTCCommand(address, static_cast(value)); return success ? 0 : 2; @@ -738,6 +764,8 @@ unsigned long HNZ::getEpochMsTimestamp(std::chrono::time_point lock(m_connexionGiMutex); + std::string newStateSTR = newState == ConnectionStatus::NOT_CONNECTED ? "NOT CONNECTED" : "STARTED"; + std::string m_connStatusSTR = m_connStatus == ConnectionStatus::NOT_CONNECTED ? "NOT CONNECTED" : "STARTED"; if (m_connStatus == newState) return; m_connStatus = newState; @@ -875,9 +903,11 @@ void HNZ::GICompleted(bool success) { resetGIQueue(); } +#ifdef UNIT_TEST void HNZ::sendInitialGI() { m_hnz_connection->sendInitialGI(); } +#endif void HNZ::m_sendAllTMQualityReadings(bool invalid, bool outdated, const vector& rejectFilter /*= {}*/) { ReadingParameters paramsTemplate; diff --git a/src/hnzconf.cpp b/src/hnzconf.cpp index 62970d8..299d355 100644 --- a/src/hnzconf.cpp +++ b/src/hnzconf.cpp @@ -71,30 +71,19 @@ bool HNZConf::m_importTransportLayer(const Value &transport) { bool is_complete = true; if (m_check_array(transport, CONNECTIONS)) { const Value &conn = transport[CONNECTIONS]; - if (conn.Size() == 1) { - if (!conn[0].IsObject()) { - HnzUtility::log_error(beforeLog + "Bad connections informations (one array element is not an object)."); - is_complete = false; - } - else { - is_complete &= m_retrieve(conn[0], IP_ADDR, &m_ip_A); - is_complete &= m_retrieve(conn[0], IP_PORT, &m_port_A, DEFAULT_PORT); - } - } else if (conn.Size() == 2) { - if (!conn[0].IsObject() || !conn[1].IsObject()) { - HnzUtility::log_error(beforeLog + "Bad connections informations (one array element is not an object)."); - is_complete = false; - } - else { - is_complete &= m_retrieve(conn[0], IP_ADDR, &m_ip_A); - is_complete &= m_retrieve(conn[0], IP_PORT, &m_port_A, DEFAULT_PORT); - is_complete &= m_retrieve(conn[1], IP_ADDR, &m_ip_B); - is_complete &= m_retrieve(conn[1], IP_PORT, &m_port_B, DEFAULT_PORT); - } - } else { + if(conn.Size() == 0 || conn.Size() > MAXPATHS){ string s = IP_ADDR; HnzUtility::log_error(beforeLog + "Bad connections informations (needed one or two " + s + ")."); - is_complete = false; + return false; + } + for (int i = 0; i < conn.Size(); i++) + { + if (!conn[i].IsObject()) { + HnzUtility::log_error(beforeLog + "Bad connections informations (one array element is not an object)."); + return false; + } + is_complete &= m_retrieve(conn[i], IP_ADDR, &(m_paths_ip[i])); + is_complete &= m_retrieve(conn[i], IP_PORT, &(m_paths_port[i]), DEFAULT_PORT); } } return is_complete; @@ -117,10 +106,10 @@ bool HNZConf::m_importApplicationLayer(const Value &conf) { is_complete &= m_retrieve(conf, MAX_SARM, &m_max_sarm, DEFAULT_MAX_SARM); is_complete &= - m_retrieve(conf, REPEAT_PATH_A, &m_repeat_path_A, DEFAULT_REPEAT_PATH); + m_retrieve(conf, REPEAT_PATH_A, &(m_paths_repeat[0]), DEFAULT_REPEAT_PATH); is_complete &= - m_retrieve(conf, REPEAT_PATH_B, &m_repeat_path_B, DEFAULT_REPEAT_PATH); + m_retrieve(conf, REPEAT_PATH_B, &(m_paths_repeat[1]), DEFAULT_REPEAT_PATH); is_complete &= m_retrieve(conf, REPEAT_TIMEOUT, &m_repeat_timeout, DEFAULT_REPEAT_TIMEOUT); diff --git a/src/hnzconnection.cpp b/src/hnzconnection.cpp index f0e83e2..122fe61 100644 --- a/src/hnzconnection.cpp +++ b/src/hnzconnection.cpp @@ -19,30 +19,44 @@ HNZConnection::HNZConnection(std::shared_ptr hnz_conf, HNZ* hnz_fledge) this->m_hnz_conf = hnz_conf; this->m_hnz_fledge = hnz_fledge; + bool ip_configured = false; + for(auto& ip: m_hnz_conf->get_paths_ip()){ + if(ip != ""){ + ip_configured = true; + break; + } + } + if(!ip_configured) { + HnzUtility::log_fatal("%s Attempted to start HNZ connection with no IP configured, aborting", beforeLog.c_str()); + return; + } + // Create the path needed - if (m_hnz_conf->get_ip_address_A() != "") { - // Parent if is mostly here for scope lock + for (int i = 0; i < MAXPATHS; i++) + { std::lock_guard lock(m_path_mutex); - m_active_path = std::make_shared(m_hnz_conf, this, false); - if (m_hnz_conf->get_ip_address_B() != "") { - m_passive_path = std::make_shared(m_hnz_conf, this, true); + std::string pathLetter = i == 0 ? "A" : "B"; + if (m_hnz_conf->get_paths_ip()[i] != "") { + m_paths[i] = new HNZPath( + m_hnz_conf, + this, + m_hnz_conf->get_paths_repeat()[i], + m_hnz_conf->get_paths_ip()[i], + m_hnz_conf->get_paths_port()[i], + pathLetter); } else { // Send initial path connection status audit - HnzUtility::audit_info("SRVFL", hnz_fledge->getServiceName() + "-B-unused"); + HnzUtility::audit_info("SRVFL", hnz_fledge->getServiceName() + "-" + pathLetter + "-unused"); } } - else { - HnzUtility::log_fatal("%s Attempted to start HNZ connection with no IP configured, aborting", beforeLog.c_str()); - return; - } // Send initial connection status audit HnzUtility::audit_fail("SRVFL", hnz_fledge->getServiceName() + "-disconnected"); // Set settings for GI - this->gi_repeat_count_max = m_hnz_conf->get_gi_repeat_count(); - this->gi_time_max = m_hnz_conf->get_gi_time() * 1000; + this->m_gi_repeat_count_max = m_hnz_conf->get_gi_repeat_count(); + this->m_gi_time_max = m_hnz_conf->get_gi_time() * 1000; this->m_gi_schedule = m_hnz_conf->get_gi_schedule(); this->m_gi_schedule_already_sent = false; @@ -55,6 +69,11 @@ HNZConnection::~HNZConnection() { if (m_is_running) { stop(); } + + for (HNZPath* path: m_paths) + { + delete path; + } } void HNZConnection::start() { @@ -73,8 +92,12 @@ void HNZConnection::stop() { // manage HNZ connections) { std::lock_guard lock(m_path_mutex); - if (m_active_path != nullptr) m_active_path->disconnect(); - if (m_passive_path != nullptr) m_passive_path->disconnect(); + for (HNZPath* path: m_paths) + { + if(path != nullptr){ + path->disconnect(); + } + } } // Wait for the end of the thread that manage the messages @@ -98,22 +121,36 @@ void HNZConnection::checkGICompleted(bool success) { return; } // GI not completed in time or last TS received with other missing TS - if (m_active_path->gi_repeat > gi_repeat_count_max) { + if (m_gi_repeat > m_gi_repeat_count_max) { // GI failed - HnzUtility::log_warn("%s Maximum GI repeat reached (%d)", beforeLog.c_str(), gi_repeat_count_max); + HnzUtility::log_warn("%s Maximum GI repeat reached (%d)", beforeLog.c_str(), m_gi_repeat_count_max); m_hnz_fledge->GICompleted(false); } else { HnzUtility::log_warn("%s General Interrogation Timeout, repeat GI", beforeLog.c_str()); // Clean queue in HNZ class m_hnz_fledge->resetGIQueue(); + + if(!m_active_path){ + HnzUtility::log_warn("%s No active path was found to send a GI.", beforeLog.c_str()); + return; + } // Send a new GI m_active_path->sendGeneralInterrogation(); } } void HNZConnection::onGICompleted() { - std::lock_guard lock(m_path_mutex); - m_active_path->gi_repeat = 0; + m_gi_repeat = 0; +} + +void HNZConnection::notifyGIsent(){ + if ((m_gi_repeat == 0) || (getGiStatus() != GiStatus::IN_PROGRESS)) { + updateGiStatus(GiStatus::STARTED); + } + m_gi_repeat++; + m_gi_start_time = std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()) + .count(); } void HNZConnection::m_manageMessages() { @@ -141,16 +178,20 @@ void HNZConnection::m_manageMessages() { // Manage repeat/timeout for each path { std::lock_guard lock(m_path_mutex); - m_check_timer(m_active_path); - m_check_timer(m_passive_path); + for (HNZPath* path: m_paths) + { + if(path != nullptr){ + m_check_timer(path); + + // Manage command ACK + m_check_command_timer(path); + } + } } // GI support m_check_GI(); - // Manage command ACK - m_check_command_timer(); - // Manage quality update m_update_quality_update_timer(); @@ -158,9 +199,11 @@ void HNZConnection::m_manageMessages() { } while (m_is_running); } -void HNZConnection::m_check_timer(std::shared_ptr path) const { - if ((path != nullptr) && !path->msg_sent.empty() && path->isConnected()) { - std::string beforeLog = HnzUtility::NamePlugin + " - HNZConnection::m_check_timer - " + path->getName(); +void HNZConnection::m_check_timer(HNZPath* path) { + if(path == nullptr) return; + + std::string beforeLog = HnzUtility::NamePlugin + " - HNZConnection::m_check_timer - " + path->getName(); + if (!path->msg_sent.empty() && path->isTCPConnected()) { Message& msg = path->msg_sent.front(); if (path->last_sent_time + m_repeat_timeout < m_current) { HnzUtility::log_debug("%s last_sent_time=%lld, m_repeat_timeout=%d, m_current=%llu", beforeLog.c_str(), path->last_sent_time, m_repeat_timeout, m_current); @@ -168,7 +211,7 @@ void HNZConnection::m_check_timer(std::shared_ptr path) const { // Connection disrupted, back to SARM HnzUtility::log_warn("%s Connection disrupted, back to SARM", beforeLog.c_str()); - path->go_to_connection(); + path->protocolStateTransition(ConnectionEvent::MAX_SEND); } else { // Repeat the message HnzUtility::log_warn("%s Timeout, sending back first unacknowledged message", beforeLog.c_str()); @@ -182,15 +225,30 @@ void HNZConnection::m_check_timer(std::shared_ptr path) const { } } } + + if(path->getProtocolState() == ProtocolState::CONNECTED){ + long long now = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + long long ms_since_connected = now - path->getLastConnected(); + if(path->getLastConnected() > 0 && ms_since_connected >= m_repeat_timeout){ + if(path->getConnectionState() == ConnectionState::ACTIVE && m_sendInitNexConnection){ + HnzUtility::log_debug("%s Sending init messages", beforeLog.c_str()); + path->sendInitMessages(); + m_sendInitNexConnection = false; + } else { + HnzUtility::log_debug("%s Discarding init messages", beforeLog.c_str()); + path->resetLastConnected(); + } + } + } } void HNZConnection::m_check_GI() { std::lock_guard lock(m_path_mutex); std::string beforeLog = HnzUtility::NamePlugin + " - HNZConnection::m_check_GI -"; // Check the status of an ongoing GI - if (m_active_path->gi_repeat != 0) { - if (m_active_path->gi_start_time + gi_time_max < m_current) { - HnzUtility::log_warn("%s GI timeout (%d ms)", beforeLog.c_str(), gi_time_max); + if (m_gi_repeat != 0) { + if (m_gi_start_time + m_gi_time_max < m_current) { + HnzUtility::log_warn("%s GI timeout (%d ms)", beforeLog.c_str(), m_gi_time_max); checkGICompleted(false); } } @@ -209,23 +267,27 @@ void HNZConnection::m_check_GI() { // Scheduled GI if (m_gi_schedule.activate && !m_gi_schedule_already_sent && (m_gi_scheduled_time <= m_current)) { + if(m_active_path == nullptr){ + HnzUtility::log_warn("%s No active path on which to send scheduled GI.", beforeLog.c_str()); + return; + } HnzUtility::log_warn("%s It's %dh%d. Executing scheduled GI.", beforeLog.c_str(), m_gi_schedule.hour, m_gi_schedule.min); m_active_path->sendGeneralInterrogation(); m_gi_schedule_already_sent = true; } } -void HNZConnection::m_check_command_timer() { +void HNZConnection::m_check_command_timer(HNZPath* path) { + if(path == nullptr) return; std::lock_guard lock(m_path_mutex); std::string beforeLog = HnzUtility::NamePlugin + " - HNZConnection::m_check_command_timer -"; - if (!m_active_path->command_sent.empty()) { - list::iterator it = m_active_path->command_sent.begin(); - while (it != m_active_path->command_sent.end()) { + if (!path->command_sent.empty()) { + list::iterator it = path->command_sent.begin(); + while (it != path->command_sent.end()) { if (it->timestamp_max < m_current) { HnzUtility::log_warn("%s A remote control (%s addr=%d) was not acknowledged in time !", beforeLog.c_str(), it->type.c_str(), it->addr); - m_active_path->go_to_connection(); - it = m_active_path->command_sent.erase(it); + it = path->command_sent.erase(it); // DF.GLOB.TC : nothing to do in HNZ } else { ++it; @@ -246,45 +308,68 @@ void HNZConnection::m_update_quality_update_timer() { m_hnz_fledge->updateQualityUpdateTimer(m_elapsedTimeMs); } -void HNZConnection::switchPath() { - std::string beforeLog = HnzUtility::NamePlugin + " - HNZConnection::switchPath -"; - // No path switch during shutdown - if (!m_is_running) { - return; - } - if (m_passive_path != nullptr) { - HnzUtility::log_warn("%s Switching active and passive path.", beforeLog.c_str()); - std::lock_guard lock(m_path_mutex); - // Permute path - std::shared_ptr temp = m_active_path; - m_active_path = m_passive_path; - m_passive_path = temp; - m_active_path->setActivePath(true); - m_passive_path->setActivePath(false); +void HNZConnection::pathConnectionChanged(HNZPath* path, bool isReady) { + if(!path) return; + std::string beforeLog = HnzUtility::NamePlugin + " - HNZConnection::pathConnectionChanged -"; + std::lock_guard lock(m_path_mutex); + HnzUtility::log_debug("%s Path %s changed connection state : %s.", beforeLog.c_str(), path->getName().c_str(), isReady ? "ready" : "not ready"); - HnzUtility::log_info("%s New active path is %s", beforeLog.c_str(), m_active_path->getName().c_str()); + if(m_first_input_connected == nullptr && (path->getProtocolState() == ProtocolState::INPUT_CONNECTED || path->getProtocolState() == ProtocolState::CONNECTED)){ + HnzUtility::log_debug("%s Path %s has INPUT_CONNECTED first.", beforeLog.c_str(), path->getName().c_str()); + m_first_input_connected = path; + } else if (m_first_input_connected == path && (path->getProtocolState() == ProtocolState::CONNECTION || path->getProtocolState() == ProtocolState::OUTPUT_CONNECTED)) { + m_first_input_connected = nullptr; + } - // When switching path, update connection status accordingly - if (m_active_path->isHNZConnected()) { + if(isReady){ + if(!m_active_path){ + m_active_path = path; + HnzUtility::log_info("%s New active path is %s", beforeLog.c_str(), m_active_path->getName().c_str()); + path->setConnectionState(ConnectionState::ACTIVE); updateConnectionStatus(ConnectionStatus::STARTED); } - else { + } else { + if(path == m_active_path){ + // We lost the connection on the active path + m_active_path = nullptr; + m_gi_repeat = 0; + // Check for other available paths + for (HNZPath* otherPath: m_paths) + { + if(otherPath != nullptr && otherPath != path && otherPath->getConnectionState() == ConnectionState::PASSIVE){ + m_active_path = otherPath; + otherPath->setConnectionState(ConnectionState::ACTIVE); + HnzUtility::log_warn("%s Switching active and passive path.", beforeLog.c_str()); + HnzUtility::log_info("%s New active path is %s", beforeLog.c_str(), m_active_path->getName().c_str()); + updateConnectionStatus(ConnectionStatus::STARTED); + return; + } + } + // No suitable path found ! updateConnectionStatus(ConnectionStatus::NOT_CONNECTED); } - } else { - // Redundancy isn't enable, can't switch to the other path - HnzUtility::log_warn("%s Redundancy isn't enabled, can't switch to the other path", beforeLog.c_str()); + // else : we lost the connection on a passive path, do nothing } } +#ifdef UNIT_TEST void HNZConnection::sendInitialGI() { std::lock_guard lock(m_path_mutex); - m_active_path->gi_repeat = 0; + std::string beforeLog = HnzUtility::NamePlugin + " - HNZConnection::sendInitialGI -"; + if(!m_active_path){ + HnzUtility::log_error("%s No active path was found !", beforeLog.c_str()); + return; + } + m_gi_repeat = 0; m_active_path->sendGeneralInterrogation(); } +#endif void HNZConnection::updateConnectionStatus(ConnectionStatus newState) { m_hnz_fledge->updateConnectionStatus(newState); + if(newState == ConnectionStatus::NOT_CONNECTED){ + m_sendInitNexConnection = true; + } } void HNZConnection::updateGiStatus(GiStatus newState) { diff --git a/src/hnzpath.cpp b/src/hnzpath.cpp index 68fc015..f3aae4a 100644 --- a/src/hnzpath.cpp +++ b/src/hnzpath.cpp @@ -15,15 +15,15 @@ #include "hnz.h" #include "hnzpath.h" -HNZPath::HNZPath(const std::shared_ptr hnz_conf, HNZConnection* hnz_connection, bool secondary): +HNZPath::HNZPath(const std::shared_ptr hnz_conf, HNZConnection* hnz_connection, int repeat_max, std::string ip, unsigned int port, std::string pathLetter): // Path settings m_hnz_client(make_unique()), m_hnz_connection(hnz_connection), - repeat_max((secondary ? hnz_conf->get_repeat_path_B() : hnz_conf->get_repeat_path_A())-1), - m_ip(secondary ? hnz_conf->get_ip_address_B() : hnz_conf->get_ip_address_A()), - m_port(secondary ? hnz_conf->get_port_B() : hnz_conf->get_port_A()), + repeat_max(repeat_max-1), // -1 because m_repeat is incremented when a message is re-sent + m_ip(ip), + m_port(port), m_timeoutUs(hnz_conf->get_cmd_recv_timeout()), - m_path_letter(secondary ? "B" : "A"), + m_path_letter(pathLetter), m_path_name(std::string("Path ") + m_path_letter), // Global connection settings m_remote_address(hnz_conf->get_remote_station_addr()), @@ -39,8 +39,10 @@ HNZPath::HNZPath(const std::shared_ptr hnz_conf, HNZConnection* hnz_con // Command settings c_ack_time_max(hnz_conf->get_c_ack_time() * 1000) { - setActivePath(!secondary); - go_to_connection(); + setConnectionState(ConnectionState::DISCONNECTED); + // Send audit at startup + sendAuditFail(); + resolveProtocolStateConnection(); } HNZPath::~HNZPath() { @@ -49,6 +51,58 @@ HNZPath::~HNZPath() { } } +void HNZPath::protocolStateTransition(const ConnectionEvent event){ + std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::protocolStateTransition - " + m_name_log; + if(protocolStateTransitionMap.find({m_protocol_state, event}) == protocolStateTransitionMap.end()){ + HnzUtility::log_warn(beforeLog + " Invalid protocol transition : event %s from %s", connectionEvent2str[event].c_str(), protocolState2str[m_protocol_state].c_str()); + return; + } + std::pair> resolveTransition = protocolStateTransitionMap[{m_protocol_state, event}]; + + HnzUtility::log_info(beforeLog + " Issuing protocol state transition %s : %s -> %s", connectionEvent2str[event].c_str(), + protocolState2str[m_protocol_state].c_str(), protocolState2str[resolveTransition.first].c_str()); + // Here m_path_mutex might be locked within the scope of m_protocol_state_mutex lock, so lock both to avoid deadlocks + // Same can happen if m_protocol_state_mutex from the other path gets locked later withing this function + std::lock(m_protocol_state_mutex, m_hnz_connection->getPathMutex()); // Lock all mutexes simultaneously + std::lock_guard lock(m_protocol_state_mutex, std::adopt_lock); + std::lock_guard lock2(m_hnz_connection->getPathMutex(), std::adopt_lock); + + bool state_changed = (m_protocol_state != resolveTransition.first); + m_protocol_state = resolveTransition.first; + for (auto triggeredAction: resolveTransition.second) + { + (this->*triggeredAction)(); + } + + if (state_changed) { + // Notify m_manageHNZProtocolConnection thread that m_protocol_state changed + std::unique_lock lock4(m_state_changed_mutex); + m_state_changed = true; + m_state_changed_cond.notify_one(); + } +} + +void HNZPath::stopTCP(){ + m_hnz_client->stop(); +} + +void HNZPath::resetSarmCounters(){ + m_nbr_sarm_sent = 0; + // Reset time from last message received to prevent instant timeout + m_last_msg_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); +} + +void HNZPath::resetOutputVariables(){ + m_ns = 0; + m_NRR = 0; + m_repeat = 0; +} + +void HNZPath::resetInputVariables(){ + m_nr = 0; + m_last_msg_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); +} + /** * Helper method to convert payload (unsigned char* with size) into a vector of * unsigned char. @@ -104,6 +158,59 @@ std::string convert_messages_to_str(const deque& messages) { return msgStr; } +void HNZPath::sendAuditSuccess(){ + std::string activePassive = m_connection_state == ConnectionState::ACTIVE ? "active" : "passive"; + HnzUtility::audit_success("SRVFL", m_hnz_connection->getServiceName() + "-" + m_path_letter + "-" + activePassive); +} + +void HNZPath::sendAuditFail(){ + HnzUtility::audit_fail("SRVFL", m_hnz_connection->getServiceName() + "-" + m_path_letter + "-disconnected"); +} + +void HNZPath::resolveProtocolStateConnected(){ + std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::resolveProtocolStateConnected - " + m_name_log; + std::lock_guard lock(m_protocol_state_mutex); + + // Be passive by default, HNZConnection will switch to ACTIVE if needed + setConnectionState(ConnectionState::PASSIVE); + m_hnz_connection->pathConnectionChanged(this, true); + + if (m_connection_state == ConnectionState::ACTIVE) { + m_hnz_connection->updateConnectionStatus(ConnectionStatus::STARTED); + } + HnzUtility::log_debug(beforeLog + " HNZ Connection initialized !!"); + + m_last_connected = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); +} + +void HNZPath::resolveProtocolStateConnection(){ + std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::resolveProtocolStateConnection - " + m_name_log; + HnzUtility::log_info(beforeLog + " Going to HNZ connection state... Waiting for a SARM."); + + setConnectionState(ConnectionState::DISCONNECTED); + m_hnz_connection->pathConnectionChanged(this, false); + + // Initialize internal variable + resetInputVariables(); + resetOutputVariables(); + resetSarmCounters(); + m_last_sarm_sent_time = 0; +} + +void HNZPath::discardMessages(){ + std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::discardMessages - " + m_name_log; + if (!msg_sent.empty()) { + std::string sentMsgStr = convert_messages_to_str(msg_sent); + HnzUtility::log_debug(beforeLog + " Discarded unacknowledged messages sent: " + sentMsgStr); + msg_sent.clear(); + } + if (!msg_waiting.empty()) { + std::string waitingMsgStr = convert_messages_to_str(msg_waiting); + HnzUtility::log_debug(beforeLog + " Discarded messages waiting to be sent: " + waitingMsgStr); + msg_waiting.clear(); + } +} + void HNZPath::connect() { std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::connect - " + m_name_log; // Reinitialize those variables in case of reconnection @@ -115,19 +222,17 @@ void HNZPath::connect() { while (m_is_running && m_hnz_connection->isRunning()) { HnzUtility::log_info(beforeLog + " Connecting to PA on " + m_ip + " (" + to_string(m_port) + ")..."); - // Establish TCP connection with the PA - m_connected = !(m_hnz_client->connect_Server(m_ip.c_str(), m_port, m_timeoutUs)); + m_hnz_client->connect_Server(m_ip.c_str(), m_port, m_timeoutUs); // If shutdown started while waiting for connection, exit if(!m_is_running || !m_hnz_connection->isRunning()) { HnzUtility::log_info(beforeLog + " Connection shutting down, abort connect"); return; } - if (m_connected) { + if (isTCPConnected()) { HnzUtility::log_info(beforeLog + " Connected to " + m_ip + " (" + to_string(m_port) + ")."); - go_to_connection(); std::lock_guard lock(m_connection_thread_mutex); - if (m_connection_thread == nullptr) { + if (!m_connection_thread) { // Start the thread that manage the HNZ connection m_connection_thread = std::make_shared(&HNZPath::m_manageHNZProtocolConnection, this); } @@ -136,11 +241,6 @@ void HNZPath::connect() { } HnzUtility::log_warn(beforeLog + " Error in connection, retrying in " + to_string(RETRY_CONN_DELAY) + "s ..."); - if (m_hnz_connection) { - // If connection failed, try to switch path - std::lock_guard lock(m_hnz_connection->getPathMutex()); - if (m_is_active_path) m_hnz_connection->switchPath(); - } this_thread::sleep_for(std::chrono::seconds(RETRY_CONN_DELAY)); } } @@ -149,10 +249,9 @@ void HNZPath::disconnect() { std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::disconnect - " + m_name_log; HnzUtility::log_debug(beforeLog + " HNZ Path stopping..."); // This ensures that the path is in the correct state for both south_event and audits - go_to_connection(); + protocolStateTransition(ConnectionEvent::TCP_CNX_LOST); m_is_running = false; - m_connected = false; m_hnz_client->stop(); HnzUtility::log_debug(beforeLog + " HNZ client stopped"); @@ -185,14 +284,7 @@ void HNZPath::m_manageHNZProtocolConnection() { std::lock_guard lock(m_protocol_state_mutex, std::adopt_lock); std::lock_guard lock2(m_hnz_connection->getPathMutex(), std::adopt_lock); long long now = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); - switch (m_protocol_state) { - case CONNECTION: - sleep = m_manageHNZProtocolConnecting(now); - break; - case CONNECTED: - sleep = m_manageHNZProtocolConnected(now); - break; - } + sleep = m_manageHNZProtocolState(now); } // lock mutex (preparation to wait in cond. var.) std::unique_lock lock3(m_state_changed_mutex); @@ -207,48 +299,32 @@ void HNZPath::m_manageHNZProtocolConnection() { HnzUtility::log_debug(beforeLog + " HNZ Connection Management thread is shutting down..."); } -std::chrono::milliseconds HNZPath::m_manageHNZProtocolConnecting(long long now) { - std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::m_manageHNZProtocolConnecting - " + m_name_log; +std::chrono::milliseconds HNZPath::m_manageHNZProtocolState(long long now) { + std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::m_manageHNZProtocolState - " + m_name_log; auto sleep = std::chrono::milliseconds(1000); // Must have received a SARM and an UA (in response to our SARM) from // the PA to be connected. - if (sarm_ARP_UA && sarm_PA_received) { - return sleep; - } - - if (now - m_last_msg_time <= (m_inacc_timeout * 1000)) { - long long ms_since_last_sarm = now - m_last_sarm_sent_time; - // Enough time elapsed since last SARM sent, send SARM - if (ms_since_last_sarm >= m_repeat_timeout) { + if (m_protocol_state == ProtocolState::CONNECTION || m_protocol_state == ProtocolState::INPUT_CONNECTED) { + if (now - m_last_msg_time <= (m_inacc_timeout * 1000)) { + long long ms_since_last_sarm = now - m_last_sarm_sent_time; + // Wait the appropriate time + sleep = (ms_since_last_sarm >= m_repeat_timeout) * std::chrono::milliseconds(m_repeat_timeout) + + (ms_since_last_sarm < m_repeat_timeout) * std::chrono::milliseconds(m_repeat_timeout - ms_since_last_sarm); + if (ms_since_last_sarm < m_repeat_timeout) return sleep; + // Enough time elapsed since last SARM sent, send SARM if (m_nbr_sarm_sent == m_max_sarm) { HnzUtility::log_warn(beforeLog + " The maximum number of SARM was reached."); - // If the path is the active one, switch to passive path if available - std::lock_guard lock(m_hnz_connection->getPathMutex()); - if (m_is_active_path) m_hnz_connection->switchPath(); - m_nbr_sarm_sent = 0; + protocolStateTransition(ConnectionEvent::MAX_SARM_SENT); } // Send SARM and wait m_sendSARM(); - sleep = std::chrono::milliseconds(m_repeat_timeout); - } - // Else wait until enough time passed - else { - sleep = std::chrono::milliseconds(m_repeat_timeout - ms_since_last_sarm); + } else { + // Inactivity timer reached + HnzUtility::log_warn(beforeLog + " Inacc timeout! Reconnecting..."); + protocolStateTransition(ConnectionEvent::TO_RECV); } - } else { - // Inactivity timer reached - HnzUtility::log_warn(beforeLog + " Inacc timeout! Reconnecting..."); - m_connected = false; - // Reconnection will be done in HNZ::receive - } - return sleep; -} - -std::chrono::milliseconds HNZPath::m_manageHNZProtocolConnected(long long now) { - std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::m_manageHNZProtocolConnected - " + m_name_log; - auto sleep = std::chrono::milliseconds(1000); - long long ms_since_last_msg = now - m_last_msg_time; - if (ms_since_last_msg <= (m_inacc_timeout * 1000)) { + } else if (m_protocol_state == ProtocolState::CONNECTED || m_protocol_state == ProtocolState::OUTPUT_CONNECTED) { + long long ms_since_last_msg = now - m_last_msg_time; long long ms_since_last_msg_sent = now - m_last_msg_sent_time; long long bulle_time_ms = m_bulle_time * 1000; // Enough time elapsed since last message sent, send BULLE @@ -260,108 +336,36 @@ std::chrono::milliseconds HNZPath::m_manageHNZProtocolConnected(long long now) { else { sleep = std::chrono::milliseconds(bulle_time_ms - ms_since_last_msg_sent); } - } else { - HnzUtility::log_warn(beforeLog + " Inactivity timer reached, a message or a BULLE were not received on time, back to SARM"); - go_to_connection(); - sleep = std::chrono::milliseconds(10); - } - return sleep; -} -void HNZPath::go_to_connection() { - std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::go_to_connection - " + m_name_log; - std::recursive_mutex& m_other_path_protocol_state_mutex = m_getOtherPathProtocolStateMutex(); - // Here m_path_mutex might be locked within the scope of m_protocol_state_mutex lock, so lock both to avoid deadlocks - // Same can happen if m_protocol_state_mutex from the other path gets locked later withing this function - std::lock(m_protocol_state_mutex, m_hnz_connection->getPathMutex(), m_other_path_protocol_state_mutex); // Lock all mutexes simultaneously - std::lock_guard lock(m_protocol_state_mutex, std::adopt_lock); - std::lock_guard lock2(m_hnz_connection->getPathMutex(), std::adopt_lock); - std::lock_guard lock3(m_other_path_protocol_state_mutex, std::adopt_lock); - HnzUtility::log_info(beforeLog + " Going to HNZ connection state... Waiting for a SARM."); - bool state_changed = m_protocol_state != CONNECTION; - if (state_changed) { - m_protocol_state = CONNECTION; - // Send audit for path connection status - HnzUtility::audit_fail("SRVFL", m_hnz_connection->getServiceName() + "-" + m_path_letter + "-disconnected"); - } - - if (!m_isOtherPathHNZConnected()) { - m_hnz_connection->updateConnectionStatus(ConnectionStatus::NOT_CONNECTED); - } - - // Initialize internal variable - sarm_PA_received = false; - sarm_ARP_UA = false; - m_nr = 0; - m_ns = 0; - m_NRR = 0; - m_nbr_sarm_sent = 0; - m_repeat = 0; - m_last_msg_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); - m_last_sarm_sent_time = 0; - gi_repeat = 0; - gi_start_time = 0; - - // Discard unacknowledged messages and messages waiting to be sent - if (!msg_sent.empty()) { - std::string sentMsgStr = convert_messages_to_str(msg_sent); - HnzUtility::log_debug(beforeLog + " Discarded unacknowledged messages sent: " + sentMsgStr); - msg_sent.clear(); - } - if (!msg_waiting.empty()) { - std::string waitingMsgStr = convert_messages_to_str(msg_waiting); - HnzUtility::log_debug(beforeLog + " Discarded messages waiting to be sent: " + waitingMsgStr); - msg_waiting.clear(); - } - - if (state_changed) { - // Notify m_manageHNZProtocolConnection thread that m_protocol_state changed - std::unique_lock lock4(m_state_changed_mutex); - m_state_changed = true; - m_state_changed_cond.notify_one(); + if (ms_since_last_msg > (m_inacc_timeout * 1000) && m_protocol_state == ProtocolState::CONNECTED) { + HnzUtility::log_warn(beforeLog + " Inactivity timer reached, a message or a BULLE were not received on time."); + protocolStateTransition(ConnectionEvent::TO_RECV); + sleep = std::chrono::milliseconds(10); + } } + return sleep; } -void HNZPath::setActivePath(bool active) { - m_is_active_path = active; - std::string activePassive = m_is_active_path ? "active" : "passive"; - m_name_log = "[" + m_path_name + " - " + activePassive + "]"; - - if (isHNZConnected()) { - // Send audit for path connection status - HnzUtility::audit_success("SRVFL", m_hnz_connection->getServiceName() + "-" + m_path_letter + "-" + activePassive); - } +void HNZPath::sendInitMessages(){ + m_send_date_setting(); + m_send_time_setting(); + sendGeneralInterrogation(); + m_last_connected = 0; } -void HNZPath::m_go_to_connected() { - std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::m_go_to_connected - " + m_name_log; - std::lock(m_protocol_state_mutex, m_hnz_connection->getPathMutex()); // Lock both mutexes simultaneously - std::lock_guard lock(m_protocol_state_mutex, std::adopt_lock); - std::lock_guard lock2(m_hnz_connection->getPathMutex(), std::adopt_lock); - bool state_changed = m_protocol_state != CONNECTED; - if (state_changed) { - m_protocol_state = CONNECTED; - // Send audit for path connection status - std::string activePassive = m_is_active_path ? "active" : "passive"; - HnzUtility::audit_success("SRVFL", m_hnz_connection->getServiceName() + "-" + m_path_letter + "-" + activePassive); - } - if (m_is_active_path) { - m_hnz_connection->updateConnectionStatus(ConnectionStatus::STARTED); - } - HnzUtility::log_debug(beforeLog + " HNZ Connection initialized !!"); +void HNZPath::setConnectionState(ConnectionState newState) { + bool sendAudit = (isHNZConnected() && + (m_connection_state == ConnectionState::PASSIVE && newState == ConnectionState::ACTIVE) || + (m_connection_state == ConnectionState::ACTIVE && newState == ConnectionState::PASSIVE)); - if (m_is_active_path) { - m_send_date_setting(); - m_send_time_setting(); - sendGeneralInterrogation(); - } + m_connection_state = newState; + m_name_log = "[" + m_path_name + " - " + connectionState2str[m_connection_state] + "]"; + if(sendAudit) sendAuditSuccess(); // A new audit is required in case of a transition active/passive +} - if (state_changed) { - // Notify m_manageHNZProtocolConnection thread that m_protocol_state changed - std::unique_lock lock3(m_state_changed_mutex); - m_state_changed = true; - m_state_changed_cond.notify_one(); - } +void HNZPath::setConnectionPending(){ + setConnectionState(ConnectionState::PENDING_HNZ); + m_hnz_connection->pathConnectionChanged(this, false); } vector> HNZPath::getData() { @@ -416,40 +420,18 @@ vector> HNZPath::m_analyze_frame(MSG_TRAME* frReceived) { m_receivedSARM(); break; default: - // Here m_path_mutex might be locked within the scope of m_protocol_state_mutex lock, so lock both to avoid deadlocks - std::lock(m_protocol_state_mutex, m_hnz_connection->getPathMutex()); // Lock both mutexes simultaneously - std::lock_guard lock(m_protocol_state_mutex, std::adopt_lock); - std::lock_guard lock2(m_hnz_connection->getPathMutex(), std::adopt_lock); - if (m_protocol_state != CONNECTION) { - // Get NR, P/F ans NS field - int ns = (type >> 1) & 0x07; - int pf = (type >> 4) & 0x01; - int nr = (type >> 5) & 0x07; - if ((type & 0x01) == 0) { - // Information frame - HnzUtility::log_info(beforeLog + " Received an information frame (ns = " + to_string(ns) + - ", p = " + to_string(pf) + ", nr = " + to_string(nr) + ")"); - std::lock_guard lock3(m_hnz_connection->getPathMutex()); - if (m_is_active_path) { - // Only the messages on the active path are extracted. The - // passive path does not need them. - int payloadSize = - size - 4; // Remove address, type, CRC (2 bytes) - messages = m_extract_messages(data + 2, payloadSize); - } - - // Computing the frame number & sending RR - if (!m_sendRR(pf == 1, ns, nr)) { - // If NR was invalid, skip message processing - messages.clear(); - } - } else { - // Supervision frame - HnzUtility::log_info(beforeLog + " RR received (f = " + to_string(pf) + ", nr = " + to_string(nr) + ")"); - m_receivedRR(nr, pf == 1); - } + if(m_protocol_state == ProtocolState::CONNECTION) break; + // Get NR, P/F ans NS field + int ns = (type >> 1) & 0x07; + int pf = (type >> 4) & 0x01; + int nr = (type >> 5) & 0x07; + if ((type & 0x01) == 0) { + m_receivedINFO(data, size, &messages); + } else { + // Supervision frame + HnzUtility::log_info(beforeLog + " RR received (f = " + to_string(pf) + ", nr = " + to_string(nr) + ")"); + m_receivedRR(nr, pf == 1); } - break; } } else { @@ -459,6 +441,40 @@ vector> HNZPath::m_analyze_frame(MSG_TRAME* frReceived) { return messages; } +void HNZPath::m_receivedINFO(unsigned char* data, int size, vector>* messages){ + if(messages == nullptr) return; + std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::m_receivedINFO - " + m_name_log; + // Here m_path_mutex might be locked within the scope of m_protocol_state_mutex lock, so lock both to avoid deadlocks + std::lock(m_protocol_state_mutex, m_hnz_connection->getPathMutex()); // Lock both mutexes simultaneously + std::lock_guard lock(m_protocol_state_mutex, std::adopt_lock); + std::lock_guard lock2(m_hnz_connection->getPathMutex(), std::adopt_lock); + unsigned char type = data[1]; // Message type (INFO) + // Get NR, P/F ans NS field + int ns = (type >> 1) & 0x07; + int pf = (type >> 4) & 0x01; + int nr = (type >> 5) & 0x07; + if(m_protocol_state == ProtocolState::OUTPUT_CONNECTED){ + HnzUtility::log_warn(beforeLog + " Unexpected information frame received in partial connection state : OUTPUT_CONNECTED"); + } else { + // Information frame + HnzUtility::log_info(beforeLog + " Received an information frame (ns = " + to_string(ns) + + ", p = " + to_string(pf) + ", nr = " + to_string(nr) + ")"); + std::lock_guard lock3(m_hnz_connection->getPathMutex()); + if (m_hnz_connection->canPathExtractMessage(this)) { + // Only the messages on the active path are extracted. The + // passive path does not need them. + int payloadSize = size - 4; // Remove address, type, CRC (2 bytes) + *messages = m_extract_messages(data + 2, payloadSize); + } + + // Computing the frame number & sending RR + if (!m_sendRR(pf == 1, ns, nr)) { + // If NR was invalid, skip message processing + messages->clear(); + } + } +} + vector> HNZPath::m_extract_messages(unsigned char* data, int payloadSize) { std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::m_extract_messages - " + m_name_log; vector> messages; @@ -527,26 +543,19 @@ vector> HNZPath::m_extract_messages(unsigned char* data, i } void HNZPath::m_receivedSARM() { - std::lock_guard lock(m_protocol_state_mutex); - if (m_protocol_state == CONNECTED) { - // Reset HNZ protocol variables - go_to_connection(); - } - sarm_PA_received = true; + std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::m_receivedSARM - " + m_name_log; m_sendUA(); - if (sarm_ARP_UA) { - m_go_to_connected(); + + long long now = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + if(now - m_last_connected > m_repeat_timeout){ + protocolStateTransition(ConnectionEvent::RECEIVED_SARM); + } else { + HnzUtility::log_info(beforeLog + " Protocol state transition from CONNECTED ignored, a SARM was received too recently."); } } void HNZPath::m_receivedUA() { - std::lock_guard lock(m_protocol_state_mutex); - if (m_protocol_state == CONNECTION) { - sarm_ARP_UA = true; - if (sarm_PA_received) { - m_go_to_connected(); - } - } + protocolStateTransition(ConnectionEvent::RECEIVED_UA); } void HNZPath::m_receivedBULLE() { @@ -692,7 +701,7 @@ bool HNZPath::m_sendRR(bool repetition, int ns, int nr) { bool HNZPath::m_sendInfo(unsigned char* msg, unsigned long size) { std::string beforeLog = HnzUtility::NamePlugin + " - HNZPath::m_sendInfo - " + m_name_log; std::lock_guard lock(m_protocol_state_mutex); - if (m_protocol_state != CONNECTED) { + if (m_protocol_state != ProtocolState::CONNECTED && !(m_protocol_state == ProtocolState::OUTPUT_CONNECTED && isBULLE(msg, size))) { HnzUtility::log_debug(beforeLog + " Connection is not yet fully established, discarding message [" + convert_data_to_str(msg, static_cast(size)) + "]"); return false; @@ -717,7 +726,7 @@ bool HNZPath::m_sendInfoImmediately(Message message) { unsigned char* msg = &message.payload[0]; int size = message.payload.size(); std::lock_guard lock(m_protocol_state_mutex); - if (m_protocol_state != CONNECTED) { + if (m_protocol_state != ProtocolState::CONNECTED && !(m_protocol_state == ProtocolState::OUTPUT_CONNECTED && isBULLE(msg, size))) { HnzUtility::log_debug(beforeLog + " Connection is not yet fully established, discarding message [" + convert_data_to_str(msg, size) + "]"); return false; @@ -808,13 +817,7 @@ void HNZPath::sendGeneralInterrogation() { unsigned char msg[2]{0x13, 0x01}; bool sent = m_sendInfo(msg, sizeof(msg)); HnzUtility::log_info(beforeLog + " GI (General Interrogation) request " + (sent?"sent":"discarded")); - if ((gi_repeat == 0) || (m_hnz_connection->getGiStatus() != GiStatus::IN_PROGRESS)) { - m_hnz_connection->updateGiStatus(GiStatus::STARTED); - } - gi_repeat++; - gi_start_time = std::chrono::duration_cast( - std::chrono::high_resolution_clock::now().time_since_epoch()) - .count(); + if(sent) m_hnz_connection->notifyGIsent(); } bool HNZPath::sendTVCCommand(unsigned char address, int value) { @@ -859,26 +862,6 @@ void HNZPath::receivedCommandACK(string type, int addr) { } -std::shared_ptr HNZPath::m_getOtherPath() const { - std::lock_guard lock(m_hnz_connection->getPathMutex()); - if (m_is_active_path) { - return m_hnz_connection->getPassivePath(); - } - else { - return m_hnz_connection->getActivePath(); - } -} - -bool HNZPath::m_isOtherPathHNZConnected() const { - std::lock_guard lock(m_hnz_connection->getPathMutex()); - auto otherPath = m_getOtherPath(); - if (otherPath == nullptr) { - return false; - } - return otherPath->isHNZConnected(); -} - - void HNZPath::m_registerCommandIfSent(const std::string& type, bool sent, int address, int value, const std::string& beforeLog) { HnzUtility::log_info(beforeLog + " " + type + " " + (sent?"sent":"discarded") + " (address = " + to_string(address) + ", value = " + to_string(value) + ")"); if (!sent) { @@ -896,17 +879,11 @@ void HNZPath::m_registerCommandIfSent(const std::string& type, bool sent, int ad command_sent.push_front(cmd); } -std::recursive_mutex& HNZPath::m_getOtherPathProtocolStateMutex() const { - std::lock_guard lock(m_hnz_connection->getPathMutex()); - auto otherPath = m_getOtherPath(); - if (otherPath == nullptr) { - static std::recursive_mutex dummyMutex; - return dummyMutex; - } - return otherPath->m_protocol_state_mutex; -} - void HNZPath::m_sendFrame(unsigned char *msg, unsigned long msgSize, bool usePAAddr /*= false*/) { m_hnz_client->createAndSendFr(usePAAddr ? m_address_PA : m_address_ARP, msg, static_cast(msgSize)); m_last_msg_sent_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); +} + +bool HNZPath::isBULLE(const unsigned char* msg, unsigned long size) const{ + return (size == 2) && (msg[0] == m_test_msg_send.first) && (msg[1] == m_test_msg_send.second); } \ No newline at end of file diff --git a/tests/server/basic_hnz_server.cpp b/tests/server/basic_hnz_server.cpp index 411cde7..3db6b62 100644 --- a/tests/server/basic_hnz_server.cpp +++ b/tests/server/basic_hnz_server.cpp @@ -316,6 +316,27 @@ bool BasicHNZServer::HNZServerIsReady(int timeout_s /*= 16*/, bool sendSarm /*= return true; } +bool BasicHNZServer::HNZServerForceReady(int timeout_s /* = 16 */){ + // Lock to prevent multiple calls to this function in parallel + std::lock_guard guard(m_init_mutex); + if (receiving_thread) { + printf("[HNZ Server][%d] Server already connected\n", m_port); fflush(stdout); + return true; + } + is_running = true; + printf("[HNZ Server][%d] Waiting for initial connection...\n", m_port); fflush(stdout); + // Wait for the server to finish starting + if (!waitForTCPConnection(timeout_s)) { + return false; + } + if (!is_running) { + printf("[HNZ Server][%d] Not running after initial connection, exit\n", m_port); fflush(stdout); + return false; + } + receiving_thread = new thread(&BasicHNZServer::receiving_loop, this); + return true; +} + void BasicHNZServer::sendSARM() { printf("[HNZ Server][%d] Sending SARM...\n", m_port); fflush(stdout); unsigned char message[1]; diff --git a/tests/server/basic_hnz_server.h b/tests/server/basic_hnz_server.h index d4354df..0ade622 100644 --- a/tests/server/basic_hnz_server.h +++ b/tests/server/basic_hnz_server.h @@ -25,6 +25,9 @@ class BasicHNZServer { // Timeout = 16 = (5 * 3) + 1 sec = (SARM retries * SARM delay) + 1 bool HNZServerIsReady(int timeout_s = 16, bool sendSarm = true, bool delaySarm = false); + // Start server without performing the SARM/UA loop + bool HNZServerForceReady(int timeout_s = 16); + void sendSARM(); struct FrameError { diff --git a/tests/test_hnz.cpp b/tests/test_hnz.cpp index e3d24e9..d7ba5b7 100644 --- a/tests/test_hnz.cpp +++ b/tests/test_hnz.cpp @@ -11,6 +11,7 @@ #include #include "hnz.h" +#include "hnzpath.h" #include "server/basic_hnz_server.h" using namespace std; @@ -162,7 +163,7 @@ string protocol_stack_generator(int port, int port2) { to_string(port2) + "}" : "") + " ] } , \"application_layer\" : { \"repeat_timeout\" : 3000, \"repeat_path_A\" : 3," - "\"remote_station_addr\" : 1, \"max_sarm\" : 5, \"gi_time\" : 1, \"gi_repeat_count\" : 2," + "\"remote_station_addr\" : 1, \"max_sarm\" : 5, \"gi_time\" : 1, \"gi_schedule\": \"00:00\", \"gi_repeat_count\" : 2," "\"anticipation_ratio\" : 5, \"inacc_timeout\" : 180, \"bulle_time\" : 10 }, \"south_monitoring\" : { \"asset\" : \"TEST_STATUS\" } } }"; } @@ -486,7 +487,8 @@ class HNZTest : public testing::Test { // We only expect invalid messages at init, and during init we will also receive 3 extra messages for the failed CG request int waitCG = invalid && !noCG; int expectedMessages = waitCG ? 10 : 7; - int maxWaitTimeMs = waitCG ? 3000 : 0; // Max time necessary for initial CG to fail due to timeout (gi_time * (gi_repeat_count+1) * 1000) + // Max time necessary for initial CG to fail due to timeout (gi_time * (gi_repeat_count+1) * 1000) + repeat_timeout (initial messages tempo, 3s) + int maxWaitTimeMs = waitCG ? 6000 : 0; std::string validStr(invalid ? "1" : "0"); std::string ourdatedStr(outdated ? "1" : "0"); debug_print("[HNZ Server] Waiting for quality update..."); @@ -719,6 +721,209 @@ class ServersWrapper { int m_port2 = 0; }; +class ProtocolStateHelper{ + public: + ProtocolStateHelper(std::shared_ptr server) : _server(server) {} + + bool isInState(ProtocolState state){ + _server->popLastFramesReceived(); + HNZTest::resetCounters(); + // Clear readings + std::shared_ptr currentReading = nullptr; + currentReading = HNZTest::popFrontReading(); + while(currentReading != nullptr){ + HNZTest::debug_print("Clearing reading : " + HNZTest::readingToJson(*currentReading.get())); + currentReading = HNZTest::popFrontReading(); + } + std::array measuredState = { + cnxStarted(), transitTC(), transitTVC(), transitTM(), + transitBULLE(), transitTCACK(), transitTVCACK(), receiveBULLE() + }; + HNZTest::debug_print("[HNZ south plugin] Measured protocol state: %d %d %d %d %d %d %d %d", measuredState[0], measuredState[1], measuredState[2], measuredState[3], measuredState[4], measuredState[5], measuredState[6], measuredState[7]); + switch(state){ + case ProtocolState::CONNECTION: + return measuredState == std::array({ false, false, false, false, false, false, false, false }); + + case ProtocolState::INPUT_CONNECTED: + return measuredState == std::array({ false, false, false, true, true, true, true, false }); + + case ProtocolState::OUTPUT_CONNECTED: + return measuredState == std::array({ false, false, false, false, false, false, false, true }); + + case ProtocolState::CONNECTED: + return measuredState == std::array({ true, true, true, true, true, true, true, true }); + } + return false; + } + + bool transitTC(){ + std::string operationTC("HNZCommand"); + int nbParamsTC = 3; + PLUGIN_PARAMETER paramTC1 = {"co_type", "TC"}; + PLUGIN_PARAMETER paramTC2 = {"co_addr", "142"}; + PLUGIN_PARAMETER paramTC3 = {"co_value", "1"}; + PLUGIN_PARAMETER* paramsTC[nbParamsTC] = {¶mTC1, ¶mTC2, ¶mTC3}; + if(!HNZTest::hnz->operation(operationTC, nbParamsTC, paramsTC)) return false; + HNZTest::debug_print("[HNZ south plugin] TC sent"); + this_thread::sleep_for(chrono::milliseconds(2000)); + // Find the TC frame in the list of frames received by server and validate it + HNZTest::validateFrame(_server->popLastFramesReceived(), {0x19, 0x0e, 0x48}); + if(HNZTest::HasFatalFailure()) return false; + return true; + } + + bool transitTVC(){ + std::string operationTVC("HNZCommand"); + int nbParamsTVC = 3; + PLUGIN_PARAMETER paramTVC1 = {"co_type", "TVC"}; + PLUGIN_PARAMETER paramTVC2 = {"co_addr", "31"}; + PLUGIN_PARAMETER paramTVC3 = {"co_value", "42"}; + PLUGIN_PARAMETER* paramsTVC[nbParamsTVC] = {¶mTVC1, ¶mTVC2, ¶mTVC3}; + if(!HNZTest::hnz->operation(operationTVC, nbParamsTVC, paramsTVC)) return false; + HNZTest::debug_print("[HNZ south plugin] TVC sent"); + this_thread::sleep_for(chrono::milliseconds(2000)); + // Find the TVC frame in the list of frames received by server and validate it + HNZTest::validateFrame(_server->popLastFramesReceived(), {0x1a, 0x1f, 0x2a, 0x00}); + if(HNZTest::HasFatalFailure()) return false; + return true; + } + + bool transitTM(){ + int values[] = {-127, -1, 1, 127}; + unsigned char val0 = static_cast((-values[0]) ^ 0xFF); // Ones' complement + unsigned char val1 = static_cast((-values[1]) ^ 0xFF); // Ones' complement + unsigned char val2 = static_cast(values[2]); + unsigned char val3 = static_cast(values[3]); + _server->sendFrame({0x02, 0x14, val0, val1, val2, val3}, false); + HNZTest::debug_print("[HNZ Server] TMA sent"); + HNZTest::waitUntil(HNZTest::dataObjectsReceived, 4, 1000); + + // Check that ingestCallback had been called 4x times + if(HNZTest::dataObjectsReceived != 4) return false; + HNZTest::resetCounters(); + std::shared_ptr currentReading = nullptr; + for (int i = 0; i < 4; i++) { + std::string label("TM" + to_string(i + 1)); + currentReading = HNZTest::popFrontReadingsUntil(label); + HNZTest::validateReading(currentReading, label, { + {"do_type", {"string", "TM"}}, + {"do_station", {"int64_t", "1"}}, + {"do_addr", {"int64_t", std::to_string(20 + i)}}, + {"do_value", {"int64_t", std::to_string(values[i])}}, + {"do_valid", {"int64_t", "0"}}, + {"do_an", {"string", "TMA"}}, + {"do_outdated", {"int64_t", "0"}}, + }); + if(HNZTest::HasFatalFailure()) return false; + HNZTest::debug_print(HNZTest::readingToJson(*currentReading.get())); + } + return true; + } + + bool transitBULLE(){ + _server->sendFrame({0x13, 0x04}, false); + HNZTest::debug_print("[HNZ Server] BULLE sent"); + this_thread::sleep_for(chrono::milliseconds(1000)); + // Check that RR frame was received + std::shared_ptr RRframe = HNZTest::findRR(_server->popLastFramesReceived()); + if(RRframe == nullptr) return false; + return true; + } + + bool transitTCACK(){ + _server->sendFrame({0x09, 0x0e, 0x49}, false); + HNZTest::debug_print("[HNZ Server] TC ACK sent"); + HNZTest::waitUntil(HNZTest::dataObjectsReceived, 1, 1000); + // Check that ingestCallback had been called once + if(HNZTest::dataObjectsReceived != 1) return false; + HNZTest::resetCounters(); + std::shared_ptr currentReading = HNZTest::popFrontReadingsUntil("TC1"); + HNZTest::validateReading(currentReading, "TC1", { + {"do_type", {"string", "TC"}}, + {"do_station", {"int64_t", "1"}}, + {"do_addr", {"int64_t", "142"}}, + {"do_value", {"int64_t", "1"}}, + {"do_valid", {"int64_t", "0"}}, + }); + if(HNZTest::HasFatalFailure()) return false; + return true; + } + + bool transitTVCACK(){ + // Send TVC ACK from server + _server->sendFrame({0x0a, 0x9f, 0x2a, 0x00}, false); + HNZTest::debug_print("[HNZ Server] TVC ACK sent"); + HNZTest::waitUntil(HNZTest::dataObjectsReceived, 1, 1000); + // Check that ingestCallback had been called once + if(HNZTest::dataObjectsReceived != 1) return false; + HNZTest::resetCounters(); + std::shared_ptr currentReading = HNZTest::popFrontReadingsUntil("TVC1"); + HNZTest::validateReading(currentReading, "TVC1", { + {"do_type", {"string", "TVC"}}, + {"do_station", {"int64_t", "1"}}, + {"do_addr", {"int64_t", "31"}}, + {"do_value", {"int64_t", "42"}}, + {"do_valid", {"int64_t", "0"}}, + }); + if(HNZTest::HasFatalFailure()) return false; + return true; + } + + bool receiveBULLE(){ + HNZTest::debug_print("[HNZ Server] Waiting for a BULLE ..."); + HNZTest::resetCounters(); + _server->popLastFramesReceived(); + this_thread::sleep_for(chrono::milliseconds(10000)); // Default BULLE delay + std::vector> frames = _server->popLastFramesReceived(); + if(frames.size() == 0) return false; + for(auto& frame : frames){ + if(frame->usLgBuffer < 4) continue; + if(frame->aubTrame[2] == 0x13 && frame->aubTrame[3] == 0x04) return true; + } + return false; + } + + bool cnxStarted(){ + HNZTest::debug_print("[HNZ Server] Validate connection status"); + HNZTest::waitUntil(HNZTest::southEventsReceived, 3, 7000); + // Check that ingestCallback had been called the expected number of times + if(HNZTest::southEventsReceived != 3) return false; + HNZTest::resetCounters(); + // Validate new connection state + std::shared_ptr currentReading = HNZTest::popFrontReadingsUntil("TEST_STATUS"); + HNZTest::validateSouthEvent(currentReading, "TEST_STATUS", { + {"connx_status", "started"}, + }); + if(HNZTest::HasFatalFailure()) return false; + // Validate new GI state + currentReading = HNZTest::popFrontReadingsUntil("TEST_STATUS"); + HNZTest::validateSouthEvent(currentReading, "TEST_STATUS", { + {"gi_status", "started"}, + }); + if(HNZTest::HasFatalFailure()) return false; + // Validate new GI state + currentReading = HNZTest::popFrontReadingsUntil("TEST_STATUS"); + HNZTest::validateSouthEvent(currentReading, "TEST_STATUS", { + {"gi_status", "failed"}, + }); + if(HNZTest::HasFatalFailure()) return false; + return true; + } + + bool restartServer(){ + HNZTest::debug_print("[HNZ server] Request server restart ..."); + if(!_server->stopHNZServer()) return false; + _server->startHNZServer(); + if(!_server->HNZServerForceReady()) return false; + _server->resetProtocol(); + this_thread::sleep_for(chrono::milliseconds(1000)); + return true; + } + + private: + std::shared_ptr _server; +}; + TEST_F(HNZTest, TCPConnectionOnePathOK) { ServersWrapper wrapper(0x05, getNextPort()); BasicHNZServer* server = wrapper.server1().get(); @@ -1714,6 +1919,7 @@ TEST_F(HNZTest, ReceivingMessagesTwoPath) { server->popLastFramesReceived(); server2->popLastFramesReceived(); + this_thread::sleep_for(chrono::seconds(3)); // Send a SARM on both path to send them back to SARM loop and make sure no deadlock is happening // by checking that SARM are received and the connection can be established on both path again debug_print("[HNZ Server] Send SARM on Path A and B"); @@ -1841,8 +2047,8 @@ TEST_F(HNZTest, ConnectionLossAndGIStatus) { debug_print("[HNZ south plugin] waiting for connection established..."); BasicHNZServer* server = wrapper.server1().get(); ASSERT_NE(server, nullptr) << "Something went wrong. Connection is not established in 10s..."; - // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) - waitUntil(southEventsReceived, 3, 3000); + // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) + repeat_timeout (initial messages tempo, 3s) + waitUntil(southEventsReceived, 3, 6000); // Check that ingestCallback had been called the expected number of times ASSERT_EQ(southEventsReceived, 3); resetCounters(); @@ -1893,9 +2099,9 @@ TEST_F(HNZTest, ConnectionLossAndGIStatus) { }); if(HasFatalFailure()) return; - // Wait for all CG attempts to expire (gi_time * (gi_repeat_count + initial CG + 1) * 1000) + // Wait for all CG attempts to expire (gi_time * (gi_repeat_count + initial CG + 1) * 1000) + repeat_timeout (initial messages tempo, 3s) debug_print("[HNZ south plugin] waiting for full CG timeout..."); - waitUntil(southEventsReceived, 1, 4000); + waitUntil(southEventsReceived, 1, 7000); // Check that ingestCallback had been called only one time ASSERT_EQ(southEventsReceived, 1); resetCounters(); @@ -1963,8 +2169,8 @@ TEST_F(HNZTest, ConnectionLossAndGIStatus) { debug_print("[HNZ south plugin] waiting for connection 2 established..."); server = wrapper.server1().get(); ASSERT_NE(server, nullptr) << "Something went wrong. Connection 2 is not established in 10s..."; - // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) - waitUntil(southEventsReceived, 3, 3000); + // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) + repeat_timeout (initial messages tempo, 3s) + waitUntil(southEventsReceived, 3, 6000); // Check that ingestCallback had been called the expected number of times ASSERT_EQ(southEventsReceived, 3); resetCounters(); @@ -2050,8 +2256,12 @@ TEST_F(HNZTest, ConnectionLossTwoPath) { BasicHNZServer* server2 = wrapper.server2().get(); ASSERT_NE(server, nullptr) << "Something went wrong. Connection is not established in 10s..."; ASSERT_NE(server2, nullptr) << "Something went wrong. Connection is not established in 10s..."; - // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) - waitUntil(southEventsReceived, 3, 3000); + // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) + repeat_timeout (initial messages tempo, 3s) + waitUntil(southEventsReceived, 3, 6000); + + // Wait one additionnal second in order not to disconnect the path too quickly after init : + // the other path may not have time to realize init messages have already been sent + this_thread::sleep_for(chrono::milliseconds(1000)); // Check that ingestCallback had been called the expected number of times ASSERT_EQ(southEventsReceived, 3); resetCounters(); @@ -2074,6 +2284,9 @@ TEST_F(HNZTest, ConnectionLossTwoPath) { }); if(HasFatalFailure()) return; + // Prevent Path B to send its init messages as well, as its timer is the same as Path A + this_thread::sleep_for(chrono::milliseconds(1000)); + // Disconnect server 1 ASSERT_TRUE(server->stopHNZServer()); debug_print("[HNZ Server] Server 1 disconnected"); @@ -2103,8 +2316,8 @@ TEST_F(HNZTest, ConnectionLossTwoPath) { server2 = wrapper.server2().get(); ASSERT_NE(server, nullptr) << "Something went wrong. Connection 2 is not established in 10s..."; ASSERT_NE(server2, nullptr) << "Something went wrong. Connection 2 is not established in 10s..."; - // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) - waitUntil(southEventsReceived, 3, 3000); + // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) + repeat_timeout (initial messages tempo, 3s) + waitUntil(southEventsReceived, 3, 6000); // Check that ingestCallback had been called the expected number of times ASSERT_EQ(southEventsReceived, 3); resetCounters(); @@ -2151,8 +2364,8 @@ TEST_F(HNZTest, ConnectionLossTwoPath) { server2 = wrapper.server2().get(); ASSERT_NE(server, nullptr) << "Something went wrong. Connection 3 is not established in 10s..."; ASSERT_NE(server2, nullptr) << "Something went wrong. Connection 3 is not established in 10s..."; - // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) - waitUntil(southEventsReceived, 3, 3000); + // Also wait for initial CG request to expire (gi_time * (gi_repeat_count+1) * 1000) + repeat_timeout (initial messages tempo, 3s) + waitUntil(southEventsReceived, 3, 6000); // Check that ingestCallback had been called the expected number of times ASSERT_EQ(southEventsReceived, 3); resetCounters(); @@ -2191,8 +2404,11 @@ TEST_F(HNZTest, ConnectionLossTwoPath) { if(HasFatalFailure()) return; // Complete CG request by sending all expected TS + // Send on both path since we do not know which path is active server->sendFrame({0x16, 0x33, 0x00, 0x00, 0x00, 0x00}, false); server->sendFrame({0x16, 0x39, 0x00, 0x02, 0x00, 0x00}, false); + server2->sendFrame({0x16, 0x33, 0x00, 0x00, 0x00, 0x00}, false); + server2->sendFrame({0x16, 0x39, 0x00, 0x02, 0x00, 0x00}, false); debug_print("[HNZ Server] TSCG sent"); waitUntil(southEventsReceived, 2, 1000); // Check that ingestCallback had been called only for two GI status updates @@ -2652,16 +2868,18 @@ TEST_F(HNZTest, FrameToStr) { ASSERT_STREQ(hnz->frameToStr({0x00, 0xab, 0xcd, 0xff}).c_str(), "\n[0x00, 0xab, 0xcd, 0xff]"); } -TEST_F(HNZTest, BackToSARM) { +TEST_F(HNZTest, BackToPreviousState) { int port = getNextPort(); ServersWrapper wrapper(0x05, port); - BasicHNZServer* server = wrapper.server1().get(); + std::shared_ptr server = wrapper.server1(); ASSERT_NE(server, nullptr) << "Something went wrong. Connection is not established in 10s..."; validateAllTIQualityUpdate(true, false); if(HasFatalFailure()) return; + ProtocolStateHelper psHelper = ProtocolStateHelper(server); + ///////////////////////////// - // Back to SARM after (repeat_timeout * repeat_path_A) due to missing RR + // Back to INPUT_CONNECTED after (repeat_timeout * repeat_path_A) due to missing RR ///////////////////////////// // Stop sending automatic ack (RR) in response to messages from south plugin @@ -2683,14 +2901,11 @@ TEST_F(HNZTest, BackToSARM) { server->popLastFramesReceived(); // Wait (repeat_timeout * repeat_path_A) + m_repeat_timeout = (3 * 3) + 3 = 12s this_thread::sleep_for(chrono::seconds(12)); - - // Find the SARM frame in the list of frames received by server - std::vector> frames = server->popLastFramesReceived(); - std::shared_ptr SARMframe = findProtocolFrameWithId(frames, 0x0f); - ASSERT_NE(SARMframe.get(), nullptr) << "Could not find SARM in frames received: " << BasicHNZServer::framesToStr(frames); + + ASSERT_TRUE(psHelper.isInState(ProtocolState::INPUT_CONNECTED)) << "Expected protocol state INPUT_CONNECTED was not detected or did not match requirements."; ///////////////////////////// - // Back to SARM after inacc_timeout while connected + // Back to OUTPUT_CONNECTED after inacc_timeout while connected ///////////////////////////// // Enable acks again @@ -2709,7 +2924,7 @@ TEST_F(HNZTest, BackToSARM) { server->startHNZServer(); // Check that the server is reconnected after reconfigure - server = wrapper.server1().get(); + server = wrapper.server1(); ASSERT_NE(server, nullptr) << "Something went wrong. Connection 2 is not established in 10s..."; // Clear messages received from south plugin @@ -2718,10 +2933,7 @@ TEST_F(HNZTest, BackToSARM) { debug_print("[HNZ server] Waiting for inacc timeout 2..."); this_thread::sleep_for(chrono::seconds(10)); - // Find the SARM frame in the list of frames received by server - frames = server->popLastFramesReceived(); - SARMframe = findProtocolFrameWithId(frames, 0x0f); - ASSERT_NE(SARMframe.get(), nullptr) << "Could not find SARM 2 in frames received: " << BasicHNZServer::framesToStr(frames); + ASSERT_TRUE(psHelper.isInState(ProtocolState::OUTPUT_CONNECTED)) << "Expected protocol state OUTPUT_CONNECTED was not detected or did not match requirements."; ///////////////////////////// // Connection reset after inacc_timeout while connecting @@ -2750,7 +2962,7 @@ TEST_F(HNZTest, BackToSARM) { server->startHNZServer(); // Check that the server is reconnected after reconfigure - server = wrapper.server1().get(); + server = wrapper.server1(); ASSERT_NE(server, nullptr) << "Something went wrong. Connection 4 is not established in 10s..."; } @@ -3222,4 +3434,128 @@ TEST_F(HNZTest, SendInvalidDirectionBit) { frames = server->popLastFramesReceived(); receivedFrame = findRR(frames); ASSERT_NE(receivedFrame, nullptr) << "Valid INFO (TSCE) was not acknowledged by RR."; +} + +TEST_F(HNZTest, ProtocolStateValidation) { + // Validates the behavior of the different protocol states + // 1) Send SARM and no UA to go into INPUT_CONNECTED + // * No south event received + // * No BULLE received south + // * TC / TVC do not transit + // * BULLE / TM / ACK TC / ACK TVC transit + // 2) Send UA and no SARM to go into OUTPUT_CONNECTED + // * No south event received + // * BULLE received south + // * TC / TVC do not transit + // * BULLE / TM / ACK TC / ACK TVC do not transit + // 3) Send SARM then UA to go into CONNECTED + // * south event received ("connx_status", "started") + // * BULLE received south + // * TC / TVC transit + // * BULLE / TM / ACK TC / ACK TVC transit + // 4) Send SARM then UA to go into CONNECTED + + int customServerPort = getNextPort(); + std::shared_ptr customServer = std::make_shared(customServerPort, 0x05); + std::string protocol_stack = protocol_stack_generator(customServerPort, 0); + // Changing max_sarm to prevent the TCP connection reset + std::string customProtocolStack = std::regex_replace(protocol_stack, std::regex("\"max_sarm\" : 5"), "\"max_sarm\" : 50"); + customServer->startHNZServer(); + this_thread::sleep_for(chrono::milliseconds(1000)); + HNZTest::initConfig(customServerPort, 0, customProtocolStack, ""); + HNZTest::startHNZ(customServerPort, 0, customProtocolStack, ""); + + ProtocolStateHelper psHelper = ProtocolStateHelper(customServer); + ASSERT_TRUE(customServer->HNZServerForceReady()); + + std::shared_ptr currentReading; + unsigned char messageUA[1]; + messageUA[0] = 0x63; + unsigned char messageSARM[1]; + messageSARM[0] = 0x0F; + + debug_print("[HNZ Server] Sending SARM to go in INPUT_CONNECTED ..."); + customServer->createAndSendFrame(0x05, messageSARM, sizeof(messageSARM)); + this_thread::sleep_for(chrono::milliseconds(1000)); + ASSERT_TRUE(psHelper.isInState(ProtocolState::INPUT_CONNECTED)) << "Expected protocol state INPUT_CONNECTED was not detected or did not match requirements."; + + customServer->sendFrame({0x0B, 0x33, 0x28, 0x36, 0xF2}, false); + this_thread::sleep_for(chrono::milliseconds(1000)); + + ASSERT_TRUE(psHelper.restartServer()); + ASSERT_TRUE(psHelper.isInState(ProtocolState::CONNECTION)) << "Expected protocol state CONNECTION was not detected or did not match requirements."; + + customServer->resetProtocol(); + debug_print("[HNZ Server] Sending UA to go in OUTPUT_CONNECTED ..."); + customServer->createAndSendFrame(0x07, messageUA, sizeof(messageUA)); + this_thread::sleep_for(chrono::milliseconds(1000)); + ASSERT_TRUE(psHelper.isInState(ProtocolState::OUTPUT_CONNECTED)) << "Expected protocol state OUTPUT_CONNECTED was not detected or did not match requirements."; + + ASSERT_TRUE(psHelper.restartServer()); + ASSERT_TRUE(psHelper.isInState(ProtocolState::CONNECTION)) << "Expected protocol state CONNECTION was not detected or did not match requirements."; + + customServer->resetProtocol(); + debug_print("[HNZ Server] Sending SARM/UA ..."); + this_thread::sleep_for(chrono::milliseconds(1000)); + customServer->createAndSendFrame(0x07, messageUA, sizeof(messageUA)); + this_thread::sleep_for(chrono::milliseconds(1000)); + customServer->createAndSendFrame(0x05, messageSARM, sizeof(messageSARM)); + + ASSERT_TRUE(psHelper.isInState(ProtocolState::CONNECTED)) << "Expected protocol state CONNECTED was not detected or did not match requirements."; + + ASSERT_TRUE(psHelper.restartServer()); + ASSERT_TRUE(psHelper.isInState(ProtocolState::CONNECTION)) << "Expected protocol state CONNECTION was not detected or did not match requirements."; + + customServer->resetProtocol(); + debug_print("[HNZ Server] Sending UA/SARM ..."); + this_thread::sleep_for(chrono::milliseconds(1000)); + customServer->createAndSendFrame(0x05, messageSARM, sizeof(messageSARM)); + this_thread::sleep_for(chrono::milliseconds(1000)); + customServer->createAndSendFrame(0x07, messageUA, sizeof(messageUA)); + + ASSERT_TRUE(psHelper.isInState(ProtocolState::CONNECTED)) << "Expected protocol state CONNECTED was not detected or did not match requirements."; +} + +TEST_F(HNZTest, GIScheduleActiveFuture) { + int port = getNextPort(); + ServersWrapper wrapper(0x05, port); + BasicHNZServer* server = wrapper.server1().get(); + ASSERT_NE(server, nullptr) << "Something went wrong. Connection is not established in 10s..."; + validateAllTIQualityUpdate(true, false); + if(HasFatalFailure()) return; + + unsigned long epochMs = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + unsigned long totalMinutes = epochMs / 60000; + unsigned long totalHours = totalMinutes / 60; + unsigned long totalDays = totalHours / 24; + int hours = static_cast(totalHours - (totalDays * 24)); + int minutes = static_cast(totalMinutes - (totalHours * 60)); + int delayMin = 2; // Program GI 2 minutes in the future, in case we are close to the end of current minute + + // If we are too close to midnight, wait long enough for the test to pass + if ((hours == 23) && (minutes >= (60 - delayMin))) { + this_thread::sleep_for(chrono::minutes(delayMin)); + minutes += delayMin; + } + + minutes += delayMin; + if (minutes >= 60) { + hours = (hours + 1) % 24; + minutes = minutes % 60; + } + auto formatTime = [](int time) + { + std::stringstream ss; + ss << std::setw(2) << std::setfill('0') << time; + return ss.str(); + }; + std::string giSchedule = formatTime(hours) + ":" + formatTime(minutes); + std::string protocol_stack = protocol_stack_generator(port, 0); + std::string protocol_stack_custom = std::regex_replace(protocol_stack, std::regex("00:00"), giSchedule); + wrapper.initHNZPlugin(protocol_stack_custom); + this_thread::sleep_for(chrono::milliseconds(3000)); + + // Wait for scheduled GI + this_thread::sleep_for(chrono::minutes(delayMin)); + this_thread::sleep_for(chrono::milliseconds(3000)); } \ No newline at end of file diff --git a/tests/test_hnzconf.cpp b/tests/test_hnzconf.cpp index 61c26a9..78ed866 100644 --- a/tests/test_hnzconf.cpp +++ b/tests/test_hnzconf.cpp @@ -105,13 +105,13 @@ TEST(HNZCONF, ConstructorWithParam) { TEST_F(HNZConfTest, ConfComplete) { EXPECT_TRUE(hnz_conf->is_complete()); } TEST_F(HNZConfTest, GetIPAdress) { - ASSERT_STREQ(hnz_conf->get_ip_address_A().c_str(), "192.168.0.10"); - ASSERT_STREQ(hnz_conf->get_ip_address_B().c_str(), "192.168.0.12"); + ASSERT_STREQ(hnz_conf->get_paths_ip()[0].c_str(), "192.168.0.10"); + ASSERT_STREQ(hnz_conf->get_paths_ip()[1].c_str(), "192.168.0.12"); } TEST_F(HNZConfTest, GetPort) { - ASSERT_EQ(hnz_conf->get_port_A(), 6001); - ASSERT_EQ(hnz_conf->get_port_B(), 6002); + ASSERT_EQ(hnz_conf->get_paths_port()[0], 6001); + ASSERT_EQ(hnz_conf->get_paths_port()[1], 6002); } TEST_F(HNZConfTest, GetRemoteStationAddr) { @@ -125,8 +125,8 @@ TEST_F(HNZConfTest, GetInaccTimeout) { TEST_F(HNZConfTest, GetMaxSARM) { ASSERT_EQ(hnz_conf->get_max_sarm(), 40); } TEST_F(HNZConfTest, GetRepeatPathAB) { - ASSERT_EQ(hnz_conf->get_repeat_path_A(), 5); - ASSERT_EQ(hnz_conf->get_repeat_path_B(), 2); + ASSERT_EQ(hnz_conf->get_paths_repeat()[0], 5); + ASSERT_EQ(hnz_conf->get_paths_repeat()[1], 2); } TEST_F(HNZConfTest, GetRepeatTimeout) { @@ -218,11 +218,11 @@ TEST(HNZCONF, MinimumConf) { ASSERT_TRUE(hnz_conf->is_complete()); - ASSERT_STREQ(hnz_conf->get_ip_address_A().c_str(), "0.0.0.0"); + ASSERT_STREQ(hnz_conf->get_paths_ip()[0].c_str(), "0.0.0.0"); - ASSERT_STREQ(hnz_conf->get_ip_address_B().c_str(), ""); + ASSERT_STREQ(hnz_conf->get_paths_ip()[1].c_str(), ""); - ASSERT_EQ(hnz_conf->get_port_A(), 6001); + ASSERT_EQ(hnz_conf->get_paths_port()[0], 6001); ASSERT_EQ(hnz_conf->get_remote_station_addr(), 18); @@ -230,8 +230,8 @@ TEST(HNZCONF, MinimumConf) { ASSERT_EQ(hnz_conf->get_max_sarm(), 30); - ASSERT_EQ(hnz_conf->get_repeat_path_A(), 3); - ASSERT_EQ(hnz_conf->get_repeat_path_B(), 3); + ASSERT_EQ(hnz_conf->get_paths_repeat()[0], 3); + ASSERT_EQ(hnz_conf->get_paths_repeat()[1], 3); ASSERT_EQ(hnz_conf->get_repeat_timeout(), 3000); diff --git a/tests/test_hnzconnection.cpp b/tests/test_hnzconnection.cpp index 95c195f..b3e37d0 100644 --- a/tests/test_hnzconnection.cpp +++ b/tests/test_hnzconnection.cpp @@ -97,8 +97,8 @@ TEST(HNZConnection, OnlyOnePathConfigured) { std::unique_ptr hnz = make_unique(); std::unique_ptr hnz_connection = make_unique(conf, hnz.get()); - ASSERT_NE(nullptr, hnz_connection->getActivePath().get()); - ASSERT_EQ(nullptr, hnz_connection->getPassivePath().get()); + ASSERT_NE(nullptr, hnz_connection->getPaths()[0]); + ASSERT_EQ(nullptr, hnz_connection->getPaths()[1]); } TEST(HNZConnection, TwoPathConfigured) { @@ -122,8 +122,8 @@ TEST(HNZConnection, TwoPathConfigured) { std::unique_ptr hnz = make_unique(); std::unique_ptr hnz_connection = make_unique(conf, hnz.get()); - ASSERT_NE(nullptr, hnz_connection->getActivePath().get()); - ASSERT_NE(nullptr, hnz_connection->getPassivePath().get()); + ASSERT_NE(nullptr, hnz_connection->getPaths()[0]); + ASSERT_NE(nullptr, hnz_connection->getPaths()[1]); } TEST(HNZConnection, NoPathConfigured) { @@ -132,8 +132,8 @@ TEST(HNZConnection, NoPathConfigured) { std::unique_ptr hnz = make_unique(); std::unique_ptr hnz_connection = make_unique(conf, hnz.get()); - ASSERT_EQ(nullptr, hnz_connection->getActivePath().get()); - ASSERT_EQ(nullptr, hnz_connection->getPassivePath().get()); + ASSERT_EQ(nullptr, hnz_connection->getPaths()[0]); + ASSERT_EQ(nullptr, hnz_connection->getPaths()[1]); } TEST(HNZConnection, GIScheduleInactive) { @@ -147,8 +147,8 @@ TEST(HNZConnection, GIScheduleInactive) { std::unique_ptr hnz = make_unique(); std::unique_ptr hnz_connection = make_unique(conf, hnz.get()); - ASSERT_NE(nullptr, hnz_connection->getActivePath().get()); - ASSERT_NE(nullptr, hnz_connection->getPassivePath().get()); + ASSERT_NE(nullptr, hnz_connection->getPaths()[0]); + ASSERT_NE(nullptr, hnz_connection->getPaths()[1]); hnz_connection->start(); // Wait for thread HNZConnection::m_manageMessages() to start @@ -174,8 +174,8 @@ TEST(HNZConnection, GIScheduleActivePassed) { std::unique_ptr hnz = make_unique(); std::unique_ptr hnz_connection = make_unique(conf, hnz.get()); - ASSERT_NE(nullptr, hnz_connection->getActivePath().get()); - ASSERT_NE(nullptr, hnz_connection->getPassivePath().get()); + ASSERT_NE(nullptr, hnz_connection->getPaths()[0]); + ASSERT_NE(nullptr, hnz_connection->getPaths()[1]); hnz_connection->start(); // Wait for thread HNZConnection::m_manageMessages() to start @@ -183,55 +183,6 @@ TEST(HNZConnection, GIScheduleActivePassed) { ASSERT_TRUE(hnz_connection->isRunning()); } -TEST(HNZConnection, GIScheduleActiveFuture) { - // Get current hours and minutes to set a GI schedule in the near future - auto hmPair = getCurrentHoursMinutes(); - int hours = hmPair.first; - int minutes = hmPair.second; - int delayMin = 2; // Program GI 2 minutes in the future, in case we are close to the end of current minute - // If we are too close to midnight, wait long enough for the test to pass - if ((hours == 23) && (minutes >= (60 - delayMin))) { - this_thread::sleep_for(chrono::minutes(delayMin)); - hmPair = getCurrentHoursMinutes(); - hours = hmPair.first; - minutes = hmPair.second; - } - - minutes += delayMin; - if (minutes >= 60) { - hours = (hours + 1) % 24; - minutes = minutes % 60; - } - auto formatTime = [](int time) - { - std::stringstream ss; - ss << std::setw(2) << std::setfill('0') << time; - return ss.str(); - }; - std::shared_ptr conf = std::make_shared(); - std::string giSchedule = formatTime(hours) + ":" + formatTime(minutes); - std::string protocol_stack_custom = std::regex_replace(protocol_stack_def, std::regex("00:00"), giSchedule); - conf->importConfigJson(protocol_stack_custom); - conf->importExchangedDataJson(exchanged_data_def); - ASSERT_TRUE(conf->get_gi_schedule().activate); - ASSERT_TRUE(conf->is_complete()); - - std::unique_ptr hnz = make_unique(); - std::unique_ptr hnz_connection = make_unique(conf, hnz.get()); - - ASSERT_NE(nullptr, hnz_connection->getActivePath().get()); - ASSERT_NE(nullptr, hnz_connection->getPassivePath().get()); - - hnz_connection->start(); - // Wait for thread HNZConnection::m_manageMessages() to start - this_thread::sleep_for(chrono::milliseconds(1100)); - ASSERT_TRUE(hnz_connection->isRunning()); - - // Wait for scheduled GI - this_thread::sleep_for(chrono::minutes(delayMin)); - ASSERT_EQ(hnz_connection->getGiStatus(), GiStatus::STARTED); -} - TEST(HNZConnection, DisconnectPathInDestructor) { std::shared_ptr conf = std::make_shared(); conf->importConfigJson(protocol_stack_def); @@ -240,7 +191,13 @@ TEST(HNZConnection, DisconnectPathInDestructor) { std::unique_ptr hnz = make_unique(); std::unique_ptr hnz_connection = make_unique(conf, hnz.get()); - std::shared_ptr hnz_path = std::make_shared(conf, hnz_connection.get(), false); + std::shared_ptr hnz_path = std::make_shared( + conf, + hnz_connection.get(), + conf->get_paths_repeat()[0], + conf->get_paths_ip()[0], + conf->get_paths_port()[0], + "A"); ASSERT_NE(nullptr, hnz_path.get()); // Start connecting on a thread and wait a little to let it enter the main connection loop