Skip to content
Snippets Groups Projects
SilecsConnection.cpp 17 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;
const unsigned int Connection::numberConn_ = 3;

//------------------------------------------------------------------------------------------------------------------------------------------------
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
    timeConn_ = NULL;
    delayConn_ = UrgentConnection; //initial reconnection delay
    remainConn_ = numberConn_; //initial number of attempt
}

//------------------------------------------------------------------------------------------------------------------------------------------------
Connection::~Connection()
{
    //Connection has been closed before from the concrete connection object
    if (timeConn_ != NULL)
        delete timeConn_;

    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)
{
    //LOG((COMM|DIAG)) << "Start attempt to open connection ..."; .. commented, seem like this creates to many log-entries for KIBANA
    bool isReachable = false;
    bool isOpen = false;
    bool justConnected = false;

    {
        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())
        {
            isReachable = (ping((char *)thePLC->getName().c_str(), NULL) == 0);
            if (isReachable)
            {
                LOG((COMM|DIAG)) << "It's time to reconnect";

                // 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)
                        LOG((COMM|DIAG)) << "Connection opened successfully";
                        break;
                    usleep(100000); // wait 100ms
            if (!isOpen)
            {
                logError(thePLC, isReachable);
                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();
            }

            isAlive_ = true;
            isConnected_ = true;
            justConnected = true; //retentive registers synchronization is required!

            //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.
     */
    if (justConnected)
    {
        LOG((COMM|DIAG)) << "First connection - performing registers synchronization";
        thePLC->updateLocalData();
    }
    LOG((COMM|DIAG)) << "isConnected_:" << isConnected_;
    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();

    //Reset reconnection mecanisme in case of connection succeed
    if (isConnected_)
    {
        delayConn_ = UrgentConnection; //initial reconnection delay
        remainConn_ = numberConn_; //initial number of attempt
    }
}

//------------------------------------------------------------------------------------------------------------------------------------------------
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 (delayConn_ == LongTermConnection)
    {
        if (remainConn_ > 0)
        {
            std::ostringstream os;
            os << errorMsg << "Periodic attempt to reconnect, delay " << delayConn_ << "s (tracing off).";
            if (thePLC->theHeader_ != NULL)
                TRACE("error") << os.str();
            LOG(COMM) << os.str();
            remainConn_ = 1; //Try to reconnect again and again (=1 means disable tracing).
        }
        /*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 " << delayConn_ << "s if requested. Remains: " << remainConn_;
        if (thePLC->theHeader_ != NULL)
            TRACE("error") << os.str();
        LOG(COMM) << os.str();
    }
}

//------------------------------------------------------------------------------------------------------------------------------------------------
bool Connection::isTimeToReconnect()
{
    bool toBeReconnected = false;
    if (timeConn_ != NULL)
    { //how many time from the last connect attempt
        double delay = timeConn_->getDelay(S_UNIT);
        if (delay >= double(delayConn_))
        {
            timeConn_->getValue(S_UNIT); //restart delay counting from now
            toBeReconnected = true;

            if (remainConn_ > 0)
                --remainConn_;
            if (remainConn_ == 0)
            {
                if (delayConn_ != LongTermConnection)
                    if (delayConn_ == UrgentConnection)
                        delayConn_ = ShortTermConnection;
                    else
                        //(delayConn_ == ShortTermConnection)
                        delayConn_ = LongTermConnection;
                    remainConn_ = numberConn_;
        //This is the first connection attempt, just start the Time counter.
        timeConn_ = new TsCounter(true); //using hardware clock
        timeConn_->getValue(S_UNIT); //start delay counting from now
        toBeReconnected = true;
    return toBeReconnected;
}
/* 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(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