Go Down

Topic: Toggle Arduino LED/relay from external webserver (Read 102 times) previous topic - next topic

fehlfarbe

Hi,

I want to control an LED/relay from an external webserver with an Arduino Nano and W5500 Ethernet chip in realtime (latency for button click on website ---> LED toggle should be lower than 1sec). The task is simple with UDP but my Arduino is behind a NAT/router with dynamic IP and port forwarding is not possible so I can't send UDP packets from my external webserver to Arduino.

Arduino <---> NAT/router <---> webserver

To solve this, I establish a TCP connection from Arduino to the webserver. It's a Python webserver and I handle the connection in its own thread. When someone presses the button to toggle the LED, the webserver just appends a command ('0' or '1') to my TCP handler thread that sends the command via established TCP connection to my Arduino.
Problem: it works sometimes for many minutes/hours then the TCP connection stucks. Arduino thinks the connection is established and I also don't get a timeout Exception or something on my Python thread but no data is received/transmitted.

Has someone an idea how I can solve this problem?


This is my Arduino code:

Code: [Select]
#include <SPI.h>
#include <Ethernet2.h>
#include "utility/w5500.h"

// Enter a MAC address for your controller below.
// Newer Ethernet shields have a MAC address printed on a sticker on the shield
byte mac[] = {
        0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02
};

unsigned int TCPPort = 8080;
IPAddress server(192, 168, 100, 28);

EthernetClient client;

// status LED
const int pin_led_tcp = 4;

// relais pins
const int pin_relais1 = 5;

void setup() {
    // Open serial communications and wait for port to open:
    Serial.begin(9600);

    // init and turn off relais
    pinMode(pin_relais1, OUTPUT);
    digitalWrite(pin_relais1, HIGH);

    // init and turn off status LED
    pinMode(pin_led_tcp, OUTPUT);
    digitalWrite(pin_led_tcp, LOW);

    // start the Ethernet connection:
    if (Ethernet.begin(mac) == 0) {
        Serial.println("Failed to configure Ethernet using DHCP");
        // no point in carrying on, so do nothing forevermore:
        for (;;);
    }

    // connect TCP
    connectTCP();
}

void loop() {
    uint8_t status = client.status();
    if(status != SnSR::ESTABLISHED){
        Serial.println("Connection lost...");
        digitalWrite(pin_led_tcp, LOW);
        Serial.println(status, HEX);
        for(size_t i=0; i<MAX_SOCK_NUM; i++){
            Serial.print("state ");
            Serial.print(i);
            Serial.print(": ");
            Serial.println(Ethernet._state[i]);
        }

        client.stop();
        //reconnect
        connectTCP();
    } else {
        // turn LED on if Arduino is connected
        digitalWrite(pin_led_tcp, HIGH);
    }

    char cmd = -1;

    if (client.available()) {
        cmd = client.read();
    }

    // execute command
    switch (cmd) {
        case '1':
            Serial.println("Turn lights on...");
            digitalWrite(pin_relais1, LOW);
            break;
        case '0':
            Serial.println("Turn lights off...");
            digitalWrite(pin_relais1, HIGH);
            break;
        case 'p': // ping
            Serial.println("got ping");
            client.write('p');
            break;
        case -1: // no command
            break;
        default:
            Serial.println("unknown command");
            Serial.println(cmd);
            break;

    }
}

bool connectTCP(){
    // if you get a connection, report back via serial:
    if (client.connect(server, TCPPort)) {
        Serial.println("connected");
        return true;
    } else {
        // if you didn't get a connection to the server:
        Serial.println("connection failed");
    }

    return false;
}



And this is my Python TCP handler:

Code: [Select]

class ArduinoSocket(object):

    def __init__(self, port, logger):
        self.port = port
        self.logger = logger
        self.sock = self.__create_socket()
        self.running = True
        self.commands = []
        self.send_timeout = 5.0

        # start loop
        self.thread = Thread(target=self.loop)
        self.thread.start()

    def __create_socket(self):
        self.logger.debug("Create new socket")
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(2.0)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        """Set TCP keepalive on an open socket.
        It activates after 1 second (after_idle_sec) of idleness,
        then sends a keepalive ping once every 3 seconds (interval_sec),
        and closes the connection after 5 failed ping (max_fails), or 15 seconds
        """
        #sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        #sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
        #sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
        #sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
        sock.bind(("0.0.0.0", self.port))
        sock.listen(8)
        return sock

    def loop(self):
        clientsocket = None
        last_ping = time.time()

        while self.running:
            if clientsocket is None:
                try:
                    (clientsocket, address) = self.sock.accept()
                    self.logger.debug("Got connection to {}".format(address))
                except IOError, e:
                    self.logger.error("IOError {}".format(e))
                    continue
                except socket.timeout:
                    self.logger.error("Timeout {}".format(e))
                    continue
                except Exception as e:
                    self.logger.error("Unknown error {}".format(e))
           
            if len(self.commands) > 0 and clientsocket is not None:
                try:
                    t, cmd = self.commands[0]
                    if time.time() - t < self.send_timeout:
                        self.logger.debug("send command {}".format(cmd))
                        clientsocket.send(str(cmd))
                    self.commands.pop(0)
                except IOError as e:
                    self.logger.error("Clientsocket.send() IOError {}".format(e))
                    if clientsocket is not None:
                        clientsocket.close()
                        clientsocket = None
                except Exception as e:
                    self.logger.error("Clientsocket.send() Unknown error {}".format(e))
                    if clientsocket is not None:
                        clientsocket.close()
                        clientsocket = None
            elif time.time() - last_ping > 5:
                try:
                    if clientsocket is not None:
                        self.logger.debug("send ping")
                        clientsocket.send("p")
                        last_ping = time.time()
                    else:
                        self.logger.debug("no clientsocket to send ping")
                except IOError as e:
                    self.logger.error("Clientsocket.send() IOError {}".format(e))
                    if clientsocket is not None:
                        clientsocket.close()
                        clientsocket = None
                except Exception as e:
                    self.logger.error("Clientsocket.send() Unknown error {}".format(e))
                    if clientsocket is not None:
                        clientsocket.close()
                        clientsocket = None             

    def send(self, cmd):
        self.commands.append((time.time(), cmd))

pylon

I guess that your NAT device simply forgets the connection when no data was transmitted for some time. You might have to insert some keep-alive signal into your protocol.

fehlfarbe

Hi,

the webserver sends a ping every 5 seconds. Maybe I should decrease the interval to 1s? However, the sockets should detect the broken connection and reconnect but both sides think the connection is established!?

pylon

Quote
However, the sockets should detect the broken connection and reconnect but both sides think the connection is established!?
They should detect it after some timeout (I think it's about 180s) but not immediately as they don't get a signal if the NAT router closes the entry in it's table. Did you check that the ping signal is really sent? Wireshark or tcpdump may help you detecting that.

Please post the output of such a session.

fehlfarbe

I captured some data with thsark on my webserver (see attachment, IPs are replaced). There are errors from package 47 to 56 (Reassembly error, protocol TCP: New fragment overlaps old data (retransmission?). The webserver sends the ping but I don't receive it on my Arduino and there is no timeout detected?! I also detached the Ethernet cable from the Arduino and the webserver didn't get a timeout.

pylon

We need a bit more information about your network to interpret that output. I expected an already filtered output where just the Arduino IP is included (as source or target). I won't check over 5000 lines of output manually. And use a tool that does not only log the header information but the content too, otherwise we cannot find out if the ping signal was sent.

fehlfarbe

This is the tshark capture file: https://www.dropbox.com/s/mw7tehecyiz8gi7/tshark3.cap?dl=0
Captured on my vserver/webserver with IP 193... and the NAT's IP is the 178.0.253.37. I only captured port 8080 where the TCP between webserver and Arduino is established. The webserver sends an 'p' for ping and '0' or '1' to toggle the LED.

pylon

Take a look at the end of the communication. The Arduino send a packet with FIN and ACK set which is usually an answer to a FIN packet. So I guess that the NAT router sent a FIN packet to Arduino to terminate a long standing connection (which several devices from network providers are doing). It's interesting that your server is acknowledging that but a few moments later it tries to send another packet into the terminated connection.

I would expect the Arduino to reconnect after some time in this case. What output do you get on the Arduino side? It might be interesting to sniff on the network where the Arduino is connected. Either the arduino freezes for some memory problem or the like, or the NAT router does some really nasty things.

Go Up