Timeout for BPUP

Suggestion for BPUP: The timeout counter should be reset when each status message is sent. If that were the case, I could listen for with some pre-described timeout, say the suggested 60 seconds, and if the listen timesout, I could simply send the keep-alive and loop. If I get a status message back before the timeout, I could process the status message and loop, knowing that I still had (at least) the full 60 seconds before it would time out.

As it works now, I have to set my timeout to some sub-interval of the keep-alive time (say 20 seconds), and every time the listen comes back, whether with a status message or a timeout, I have to check the time of the last keep-alive message and calculate whether the keep-alive interval has expired, send the new keep-alive, store the time for the latest keep-alive, process any status message, all before I can loop back to the listen. Doable yes, but unnecessarily more complex than the simplicity of just letting the listen timeout be the timer for the need for a new keep-alive message.

Or you could sent the keep-alive totally independent of the receive cycle. Takes another thread for that, but easy enough in Python, which is what Iā€™m using.

1 Like

Again, though, unnecessary complexity and resources when doing it the way I suggested would be so easy.

The transmission of a message from the Bond to the client doesnā€™t provide any information about the reachability of the client. This is why we donā€™t extend the timeout.

If we extended the timeout, and the Bond were sending one update message per minute (which will be the case if a fan is on Breeze mode), then client ā€œconnectionsā€ would never timeout.

ā€œtransmission of a message from the Bond to the client doesnā€™t provide any information about the reachability of the client.ā€ In the case of a direct socket connection, the send should fail if the socket was closed on the client side - I know this is the case in my integration if it is closed on the Bond side. In the event that the client stopped WITHOUT the socket closing AND the Bond is sending messages more frequently than once a minute for an extended period of time, then there would be the possibility that a number of messages could be sent that werenā€™t necessary. Would that be a huge resource loss? Regardless, I can deal with timeout (already have) - just trying to suggest ways to make the API simpler for IoT integrations.

I think youā€™re referring to the ICMP Destination Unreachable responses which some TCP/IP stacks return if you send an unsolicited UDP datagram, or send a datagram to a port where the corresponding socket has been closed.

We cannot rely on the ICMP errors here for several reasons, most significantly that if the host goes offline entirely, thereā€™s nobody to send the ICMP error replies. Secondarily, a number of host stacks have stopped sending these error replies as a countermeasure against port scanners (though I could be wrong on that one). Lastly, this is just a SHOULD not a MUST in the specifications (RFC 1122 - Requirements for Internet Hosts - Communication Layers). So, we just rely on the UDP layer, ignoring the ICMP layer.

and I appreciate that. This kind of feedback is invaluable.


By the way, the timeout is 125 seconds, not 60. Itā€™s designed so you just need to send one datagram each minute.

The client should continue to send the Keep-Alive datagram on the same socket every 60 seconds to keep the connection active. If no Keep-Alive datagram is received after 125 seconds, Bond will stop sending feedback to the client.

I just received your other reply (donā€™t know what happened to it in the display) and you make a good point: just send the keep-alive after every timeout OR status message is received. That would be very simple.

1 Like

Actually, you canā€™t send it after every message received, because the ping itself causes a message. You would need to filter those out or you get a really bad loop. Donā€™t ask how I know. :wink:

1 Like

FWIW, hereā€™s my code.

########################################
# Bond Push UDP Protocol (BPUP)
########################################

def udp_start(self, callback):
    self.callback = callback
    if not self.sock:
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.sock.settimeout(TIMEOUT)
        except:
            raise
        else:
            self.logger.debug(u"udp_start() socket listener started")

    # start up the receiver thread        
    self.receive_thread = Thread(target=self.udp_receive)
    self.receive_thread.daemon = True
    self.receive_thread.start()
    
def udp_receive(self):
    self.next_ping = time.time()

    while True: 
        now = time.time()
        if now > self.next_ping:
            self.sock.sendto('\n', (self.address, 30007))
            self.next_ping = now + TIMEOUT
            
        try:
            json_data, addr = self.sock.recvfrom(2048)
        except socket.timeout as err:
            continue
        except socket.error as err:
            raise
        else:
            try:
                data = json.loads(json_data.decode("utf-8"))
            except Exception as err:
                raise

            # don't send ping acks
            if len(data) == 1:
                continue
                
            # fix up the data

            topic = data['t'].split('/')
            data['id'] = topic[1]

            self.callback(data)

For the Olibra folks lurking - you might notice a couple odd things in that code. First, Iā€™m testing for a dict size of one to determine if the message was just the ping ack. It really would have been better if the message actually had a field that identified what kind of message it is.

Second, Iā€™m adding an ā€˜idā€™ field, because you didnā€™t include one that specifically identified which device this message pertains to. I had to pull it out of the topic field.

If it is helpful: Here is how I did it with asyncio

Similar code for the data processing:

        topic = json_msg["t"].split("/")
        device_id = topic[1]

        for callback in self._callbacks.get(device_id, []):
            callback(json_msg["b"])

Yeah, I canā€™t use asyncio. This code is for Python2.7. :frowning: