Skip to content
Snippets Groups Projects
SilecsConnection.cpp 15.6 KiB
Newer Older
// Copyright 2016 CERN and GSI
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include <fcntl.h>
#include <signal.h>

#include <silecs-communication/interface/equipment/SilecsCluster.h>
#include <silecs-communication/interface/utility/SilecsLog.h>
#include <silecs-communication/interface/communication/SilecsConnection.h>
#include <silecs-communication/interface/equipment/SilecsPLC.h>
#include <silecs-communication/interface/communication/ietype.h>
// static definition
bool Connection::isAlive_ = false;

//------------------------------------------------------------------------------------------------------------------------------------------------
Connection::Connection(PLC* thePLC)
{
    // Start the PLC Thread
    readMux_ = new Mutex("readMux");
    writeMux_ = new Mutex("writeMux");
    connMux_ = new Mutex("connMux");

    //Not yet allowed, not yet connected!
    isEnabled_ = false;
    isConnected_ = false;

    //Reset the Reconnection mechanism
    lastReconnectionAttempt_ = 0;
    reconnectDelay_ = shortDelay; //initial reconnection delay
    reconnectAttempts_ = 0;
}

//------------------------------------------------------------------------------------------------------------------------------------------------
Connection::~Connection()
{
    delete connMux_;
    delete writeMux_;
    delete readMux_;
}

//------------------------------------------------------------------------------------------------------------------------------------------------
bool Connection::enable(PLC* thePLC, bool connectNow)
{
    {
        Lock lock(connMux_);
        isEnabled_ = true;
    }
    if (connectNow)
        doOpen(thePLC);
    return isConnected_;
}

//------------------------------------------------------------------------------------------------------------------------------------------------
void Connection::disable(PLC* thePLC)
{
    if (!isEnabled_)
    {
        LOG(DEBUG) << "Trying to disconnect a PLC that is not enabled: " << thePLC->getName();
        return;
    }

    Lock lock(connMux_);
    {
        Lock lock(readMux_);
        {
            Lock lock(writeMux_);
            isEnabled_ = false;
            doClose(thePLC, /*withLock =*/false);
//------------------------------------------------------------------------------------------------------------------------------------------------
bool Connection::doOpen(PLC* thePLC)
{
    {
        Lock lock(connMux_);
            if (!isEnabled_)
            {
                LOG((COMM|DIAG)) << "The PLC is connected but the client wants to disable the transactions";
                doClose(thePLC, /*withLock =*/false);
            }
            return isConnected_;
        }

        if (!isEnabled_)
        {
            LOG((COMM|DIAG)) << "The communication currently is not enabled";
            return isConnected_;
        if (!isTimeToReconnect())
        { // Do nothing, just wait a bit to dont pullute the log
          return isConnected_;
        }

        if (ping(thePLC->getName().c_str(), NULL))
          logError(thePLC, false);
          return isConnected_;
        }
        // It's time to open the connection according to the (re)connection timing
        // Let's try several times with limited delay (some ms).
        // It allows wake-up frozen PLC (SIEMENS in particular) after long stop period.
        bool isOpen = false;
        unsigned int nbConn = 2; //for fast low-level iteration
        for (unsigned int i = 0; i < nbConn; i++)
        {
            LOG((COMM|DIAG)) << "Attempt to open PLC connection ....";
            isOpen = open(thePLC);
            if (isOpen)
               //reset reconnection settings
                reconnectDelay_ = shortDelay;
                reconnectAttempts_ = 0;

                isAlive_ = true;
                isConnected_ = true;
                LOG((COMM|DIAG)) << "Connection opened successfully";
                break;
            usleep(100000); // wait 100ms
        }
        if (!isOpen)
        {
            logError(thePLC, true);
            return isConnected_;
        }
        if (thePLC->isSharedConnection())
        {
            std::ostringstream os;
            os << "Shared connection with " << thePLC->getName() << " is established.";
            if (thePLC->theHeader_ != NULL)
                TRACE("info") << os.str();
            LOG(COMM) << os.str();
        }
        else
        {
            std::ostringstream os;
            os << "Connection with " << thePLC->getName() << ":" << thePLC->theCluster_->getClassName() << "/v" << thePLC->theCluster_->getClassVersion() << " is established.";
            if (thePLC->theHeader_ != NULL)
                TRACE("info") << os.str();
            LOG(COMM) << os.str();

        //Connection status has changed: update the diagnostic variables
        LOG((COMM|DIAG)) << "Updating PLC status";
        updateStatus(thePLC);
    } //release lock

    /* Process the Retentive registers synchronization each time the PLC is just (re)connected.
     * This is a recursive call: performs task::execute method from doOpen that is
     * called into execute itself. The recursion is terminated when SilecsHeader connection is closed finally.
     */
    LOG((COMM|DIAG)) << "First connection - performing registers synchronization";
    thePLC->updateLocalData();
    return isConnected_;
}

//------------------------------------------------------------------------------------------------------------------------------------------------
void Connection::doClose(PLC* thePLC, bool withLock)
{
    //This process that can be called inside and outside protected section.
    //withLock argument is used to avoid Recursive mutex that is not supported
    //by LynxOS platform.

    if (withLock)
        Lock lock(connMux_);

    if (isConnected_)
    {
        if (close(thePLC))
        {
            isConnected_ = false;

            //Connection status has changed: update the diagnostic variables
            updateStatus(thePLC);

            if (thePLC->isSharedConnection())
            {
                std::ostringstream os;
                os << "Shared connection with " << thePLC->getName() << " is closed.";
                if (thePLC->theHeader_ != NULL)
                    TRACE("warn") << os.str();
                LOG(COMM) << os.str();
            }
            else
            {
                std::ostringstream os;
                os << "Connection with " << thePLC->getName() << " (" << thePLC->theCluster_->getClassName() << "/v" << thePLC->theCluster_->getClassVersion() << ")" << " is closed.";
                if (thePLC->theHeader_ != NULL)
                    TRACE("warn") << os.str();
                LOG(COMM) << os.str();
            }
        }
        else
        {
            LOG(COMM) << "Close connection with " << thePLC->getName() << " (" << thePLC->theCluster_->getClassName() << "/v" << thePLC->theCluster_->getClassVersion() << ")" << " has failed.";
        }
    }

}

//------------------------------------------------------------------------------------------------------------------------------------------------
bool Connection::reOpen(PLC* thePLC)
{
    if (ping((char *)thePLC->getName().c_str(), 0) == 0)
    {
        doClose(thePLC, /*withLock =*/true);
        return doOpen(thePLC);
    }
    isAlive_ = false;
    return false;
}

bool Connection::isEnabled()
{
    return isEnabled_;
}
bool Connection::isConnected()
{
    return isConnected_;
}

//-------------------------------------------------------------------------------------------------------------------
int Connection::readUnitCode(PLC* thePLC, UnitCodeType& dataStruct)
{
    throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
    return -1;
}

int Connection::readUnitStatus(PLC* thePLC, UnitStatusType& dataStruct)
{
    throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
    return -1;
}

int Connection::readCPUInfo(PLC* thePLC, CPUInfoType& dataStruct)
{
    throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
    return -1;
}

int Connection::readCPInfo(PLC* thePLC, CPInfoType& dataStruct)
{
    throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
    return -1;
}

bool Connection::isRunning(PLC* thePLC)
{
    throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
    return -1;
}

int Connection::coldRestart(PLC* thePLC)
{
    throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
    return -1;
}

//PERFORM COLD RESTART
int Connection::plcStop(PLC* thePLC)
{
    throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
    return -1;
}

//------------------------------------------------------------------------------------------------------------------------------------------------
void Connection::updateStatus(PLC* thePLC)
{
    //Connection status has changed (opened/closed)
    //Update the PLC diagnostic variables
    thePLC->updateStatus();
}

//------------------------------------------------------------------------------------------------------------------------------------------------
void Connection::logError(PLC* thePLC, bool isReachable)
{
    std::string errorMsg = isReachable ? "Connection with " + thePLC->getName() + ":" + thePLC->theCluster_->getClassName() + "/v" + thePLC->theCluster_->getClassVersion() + " has failed.\n" : "Controller " + thePLC->getName() + " does not respond to ping, might be OFF!\n";

    if (reconnectDelay_ == longDelay)
        if (reconnectAttempts_ <  MAX_CONNECTION_ATTEMPTS_PER_DELAY)
            os << errorMsg << "Periodic attempt to reconnect, delay " << RECONNECTION_DELAYS[reconnectDelay_] << " seconds (tracing off).";
            if (thePLC->theHeader_ != NULL)
                TRACE("error") << os.str();
            LOG(COMM) << os.str();
        }
        /*else
         PLC does not respond anymore. It's probably stopped for a long time.
         Do not log error anymore (but still try to reconnect, with long-term delay).
         */
    }
    else
    {
        std::ostringstream os;
        os << errorMsg << "Next attempt to reconnect in " << RECONNECTION_DELAYS[reconnectDelay_] << " seconds.";
        if (thePLC->theHeader_ != NULL)
            TRACE("error") << os.str();
        LOG(COMM) << os.str();
    }
}

//------------------------------------------------------------------------------------------------------------------------------------------------
bool Connection::isTimeToReconnect()
{
  time_t now;
  time(&now);
  double secondsElapsed = difftime(now, lastReconnectionAttempt_);
  if ( secondsElapsed < RECONNECTION_DELAYS[reconnectDelay_])
    return false;

  lastReconnectionAttempt_ = now;
  reconnectAttempts_ ++;

  if( reconnectAttempts_ < MAX_CONNECTION_ATTEMPTS_PER_DELAY)
    return true;

  if( reconnectDelay_ < longDelay)
  {
    reconnectAttempts_ = 0;
    reconnectDelay_ =  static_cast<ReconnectionDelay>(reconnectDelay_+ 1);
  }
  return true;
/* This macro is used to trap the unexpected broken pipe and
 return an error instead of exit process.
 */
static __sighandler_t sigpipeHandler = (__sighandler_t )-1;
#define _DISABLE_SIGPIPE sigpipeHandler = signal(SIGPIPE, SIG_IGN)
#define _ENABLE_SIGPIPE signal(SIGPIPE, sigpipeHandler)

//------------------------------------------------------------------------------------------------------------------------------------------------
int connect_nonb(int sockfd, struct sockaddr *saptr, socklen_t salen, int nsec)
{
    int flags, n, error;
    socklen_t len;
    fd_set rset, wset;
    struct timeval tval;

    flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    error = 0;
    /*v1.1*/
    _DISABLE_SIGPIPE;
    n = connect(sockfd, (struct sockaddr *)saptr, salen);
    _ENABLE_SIGPIPE;
    if (n < 0)
    {
        if (errno != EINPROGRESS)
            return (-1);
    }

    /* Do whatever we want while the connect is taking place. */

    if (n == 0)
        goto done;
    /* connect completed immediately */
    FD_ZERO(&rset);
    FD_SET(sockfd, &rset);
    wset = rset;
    tval.tv_sec = nsec;
    tval.tv_usec = 0;
    if ( (n = select(sockfd + 1, &rset, &wset, NULL, nsec ? &tval : NULL)) == 0)
        close(sockfd); /* timeout */
        errno = ETIMEDOUT;
        return (-1);
    }

    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset))
    {
        len = sizeof (error);
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
        {
            return (-1); /* Solaris pending error */
        }
    done: fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
        /*close(sockfd); just in case */
        errno = error;
        return (-1);
    return (0);
}

/*----------------------------------------------------------*/
int rfcPing(char *ip, long ts)
{
    int s, val = 1;
    struct protoent *pent;
    struct sockaddr_in rsock;

    /* Socket create/connect */
    memset((char *)&rsock, 0, sizeof (rsock));
    if ( (s = socket(AF_INET, SOCK_STREAM, 0)) == -1)
        LOG(ERROR) << "Can't create socket: " << strerror(errno);
    /* Set TCP_NODELAY option to force immediate TCP/IP acknowledgement */
    if ( (pent = getprotobyname("TCP")) == NULL)
        LOG(ERROR) << "Can't configure socket: " << strerror(errno);
        return -1;
    }
    if (setsockopt(s, pent->p_proto, TCP_NODELAY, &val, 4) == -1)
    {
        LOG(ERROR) << "Can't configure socket: " << strerror(errno);
    rsock.sin_addr.s_addr = inet_addr(ip);
    rsock.sin_family = AF_INET;
    /*check any port to detect if the host is OFF*/
    rsock.sin_port = htons(102);

    if (connect_nonb(s, (struct sockaddr *) (&rsock), sizeof (rsock), static_cast<int>(ts)) == -1)
    {
        /*if hostname is OFF, connect() fails on TIMEOUT*/
        if ( (errno == ETIMEDOUT) || (errno == EHOSTDOWN) || (errno == EHOSTUNREACH))
    }

    close(s);
    return 0; //controller was reachable
}
/*..........................................................*/
int Connection::ping(const char *hostName, char *plcIP)
{
    struct in_addr addr;
    struct hostent *hp;
    char *ipstr = plcIP;
    std::string errorMsg;
    std::ostringstream os;

    if ( (hostName == NULL) && (plcIP == NULL))
        os << errorMsg << "Bad parameter(s) value/format";
        LOG(COMM) << os.str();
        return -1;
    }
    // use hostName reference in priority else plcIP
    if (hostName)
    {
        hp = gethostbyname(hostName);
        if (hp)
            addr.s_addr = static_cast<in_addr_t>(* ((unsigned long int *)hp->h_addr));
            ipstr = inet_ntoa(addr);
    }

    if (ipstr == NULL)
    {
        os << errorMsg << "PLC hostname unknown";
        LOG(COMM) << os.str();
        return -1;
    }

    /*trying to connect PLC (1 second max)*/
    return (rfcPing(ipstr, 1));
}

} // end namespace