From 1f50273e92a47f558426b19c1d5a7370cd7ac679 Mon Sep 17 00:00:00 2001
From: Andreas Gattringer <andreas.gattringer@univie.ac.at>
Date: Sat, 20 Apr 2024 23:10:05 +0200
Subject: [PATCH] rhtp and related: improved resilience

---
 programs/umnp-rhtp-local.py                   | 28 +++++++++++++++++--
 programs/umnp-rhtp.py                         | 28 +++++++++++++++++--
 .../communication/udp_communicator.py         | 11 ++++++++
 .../devices/network/ethernet.py               |  7 +++++
 .../devices/network/ethernet_w5500.py         | 16 +++++++++++
 umnp/microcontroller/devices/network/udp.py   | 17 ++++++++++-
 umnp/microcontroller/measurementdevice.py     | 24 ++++++++++++++++
 .../sensors/lps28dfw/__init__.py              | 18 ++++++++++--
 .../microcontroller/sensors/sht45/__init__.py | 24 ++++++++++++++--
 .../microcontroller/umock/machine/__init__.py |  5 +++-
 10 files changed, 167 insertions(+), 11 deletions(-)

diff --git a/programs/umnp-rhtp-local.py b/programs/umnp-rhtp-local.py
index 96d6800..742e8af 100644
--- a/programs/umnp-rhtp-local.py
+++ b/programs/umnp-rhtp-local.py
@@ -48,7 +48,27 @@ async def aggregate_and_send(
     data = "{:2.2f},{:2.2f},{:2.2f},{:2.2f}".format(t, rh, p, p_t)
     print("Sending: '{}'".format(data))
     msg = DataMessage(data, device.identifier_raw, device.device_type)
-    await comm.send_message(msg)
+    if comm.network_error:
+        print("Network error")
+        spi: machine.SPI = device.spi
+        if spi:
+            print("SPI::deinit()")
+            spi.deinit()
+            spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
+            print("SPI::new()")
+            device.add_spi(spi)
+            print("SPI::added()")
+            dev_mac = device.generated_mac_raw()
+            device.network.deactivate()
+            print("ETH:new()")
+            ether = EthernetW5500(spi, ETH_CS, ETH_RST, dev_mac, ETH_USE_DHCP)
+            print("ETH:set()")
+            ether.set_network(ETH_IP, ETH_SUBNET, ETH_GATEWAY, ETH_DNS)
+            print("ETH:added()")
+            device.add_network_adapter(ether)
+            comm.clear_network_error()
+    if not comm.network_error:
+        await comm.send_message(msg)
     msg = None
     gc.collect()
 
@@ -57,13 +77,17 @@ async def main():
     # configure network
     device = MeasurementDevice(device_type=DEVICE_TYPE_RHTP)
     spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
-    i2c = machine.I2C(id=1, scl=I2C_SCL, sda=I2C_SDA)
+    device.add_spi(spi)
+
+    i2c = machine.I2C(id=1, scl=I2C_SCL, sda=I2C_SDA, timeout=10000)
+    device.add_i2c(i2c)
 
     dev_mac = device.generated_mac_raw()
     ether = EthernetW5500(spi, ETH_CS, ETH_RST, dev_mac, ETH_USE_DHCP)
     ether.set_network(ETH_IP, ETH_SUBNET, ETH_GATEWAY, ETH_DNS)
 
     device.add_network_adapter(ether)
+    device.add_i2c(i2c)
     comm = device.create_communicator()
 
     sht45 = SHT45(i2c)
diff --git a/programs/umnp-rhtp.py b/programs/umnp-rhtp.py
index 1fd3e5f..e09c853 100644
--- a/programs/umnp-rhtp.py
+++ b/programs/umnp-rhtp.py
@@ -48,7 +48,27 @@ async def aggregate_and_send(
     data = "{:2.2f},{:2.2f},{:2.2f},{:2.2f}".format(t, rh, p, p_t)
     print("Sending: '{}'".format(data))
     msg = DataMessage(data, device.identifier_raw, device.device_type)
-    await comm.send_message(msg)
+    if comm.network_error:
+        print("Network error")
+        spi: machine.SPI = device.spi
+        if spi:
+            print("SPI::deinit()")
+            spi.deinit()
+            spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
+            print("SPI::new()")
+            device.add_spi(spi)
+            print("SPI::added()")
+            dev_mac = device.generated_mac_raw()
+            device.network.deactivate()
+            print("ETH:new()")
+            ether = EthernetW5500(spi, ETH_CS, ETH_RST, dev_mac, ETH_USE_DHCP)
+            print("ETH:set()")
+            ether.set_network(ETH_IP, ETH_SUBNET, ETH_GATEWAY, ETH_DNS)
+            print("ETH:added()")
+            device.add_network_adapter(ether)
+            comm.clear_network_error()
+    if not comm.network_error:
+        await comm.send_message(msg)
     msg = None
     gc.collect()
 
@@ -57,13 +77,17 @@ async def main():
     # configure network
     device = MeasurementDevice(device_type=DEVICE_TYPE_RHTP)
     spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
-    i2c = machine.I2C(id=1, scl=I2C_SCL, sda=I2C_SDA)
+    device.add_spi(spi)
+
+    i2c = machine.I2C(id=1, scl=I2C_SCL, sda=I2C_SDA, timeout=10000)
+    device.add_i2c(i2c)
 
     dev_mac = device.generated_mac_raw()
     ether = EthernetW5500(spi, ETH_CS, ETH_RST, dev_mac, ETH_USE_DHCP)
     ether.set_network(ETH_IP, ETH_SUBNET, ETH_GATEWAY, ETH_DNS)
 
     device.add_network_adapter(ether)
+    device.add_i2c(i2c)
     comm = device.create_communicator()
 
     sht45 = SHT45(i2c)
diff --git a/umnp/microcontroller/communication/udp_communicator.py b/umnp/microcontroller/communication/udp_communicator.py
index 3350839..c9346cc 100644
--- a/umnp/microcontroller/communication/udp_communicator.py
+++ b/umnp/microcontroller/communication/udp_communicator.py
@@ -38,6 +38,17 @@ class UDPCommunicator:
         self._tasks = {}
         self._device = device
 
+    @property
+    def sender(self):
+        return self._sender
+
+    @property
+    def network_error(self):
+        return self._sender.error
+
+    def clear_network_error(self):
+        self._sender.reset_error()
+
     async def queue_incoming_message(self, msg, source):
         async with self._receive_lock:
             self._messages_received.append((msg, source, time.time()))
diff --git a/umnp/microcontroller/devices/network/ethernet.py b/umnp/microcontroller/devices/network/ethernet.py
index 8282bfd..5f38286 100644
--- a/umnp/microcontroller/devices/network/ethernet.py
+++ b/umnp/microcontroller/devices/network/ethernet.py
@@ -1,6 +1,7 @@
 # micropython code
 # can't use abstract baseclass here
 
+
 class EthernetAdapter:
     def __init__(self):
         pass
@@ -15,3 +16,9 @@ class EthernetAdapter:
 
     def enable_dhcp(self):
         pass
+
+    def reset(self):
+        pass
+
+    def activate(self):
+        pass
diff --git a/umnp/microcontroller/devices/network/ethernet_w5500.py b/umnp/microcontroller/devices/network/ethernet_w5500.py
index 23bddc2..01e6b8c 100644
--- a/umnp/microcontroller/devices/network/ethernet_w5500.py
+++ b/umnp/microcontroller/devices/network/ethernet_w5500.py
@@ -1,4 +1,5 @@
 import sys
+import time
 
 from umnp.microcontroller.devices.network.ethernet import EthernetAdapter
 
@@ -8,8 +9,11 @@ if sys.implementation.name == "micropython":
 
     # noinspection PyUnresolvedReferences
     import network
+    import umnp.protocol.compat.logging as logging
+
 else:
     from umnp.microcontroller.umock import machine, network
+    import logging
 
 
 class EthernetW5500(EthernetAdapter):
@@ -48,6 +52,12 @@ class EthernetW5500(EthernetAdapter):
     def netmask(self):
         return self._nic.ifconfig()[1]
 
+    def activate(self):
+        self._nic.active(True)
+
+    def deactivate(self):
+        self._nic.active(False)
+
     @property
     def ip(self):
         return self._nic.ifconfig()[0]
@@ -55,3 +65,9 @@ class EthernetW5500(EthernetAdapter):
     @property
     def mac(self):
         return self._nic.config("mac")
+
+    def reset(self):
+        logging.info("Resetting NIC")
+        self._nic.active(False)
+        time.sleep(0.1)
+        self._nic.active(True)
diff --git a/umnp/microcontroller/devices/network/udp.py b/umnp/microcontroller/devices/network/udp.py
index 06d5d86..ef95620 100644
--- a/umnp/microcontroller/devices/network/udp.py
+++ b/umnp/microcontroller/devices/network/udp.py
@@ -10,7 +10,10 @@ from umnp.microcontroller.devices.network import (
 if sys.implementation.name == "micropython":
     # noinspection PyUnresolvedReferences
     import uasyncio as asyncio
+    import umnp.protocol.compat.logging as logging
+
 else:
+    import logging
     import asyncio
 DEFAULT_UMNP_DATA_IN_PORT = 7777
 DEFAULT_UMNP_COMMAND_IN_PORT = 7776
@@ -45,14 +48,26 @@ class UDPSender:
         self.ip = ip
         self.netmask = netmask
         self._target_port = send_to_port
+        self._error = False
 
         if ip and netmask:
             self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
             self.broadcast_ip = calculate_broadcast_ip(ip, netmask)
 
+    @property
+    def error(self):
+        return self._error
+
+    def reset_error(self):
+        self._error = False
+
     async def broadcast(self, msg):
         # print("sending %s" % msg)
         if isinstance(msg, str):
             msg = msg.encode("utf-8")
         if self.socket:
-            self.socket.sendto(msg, (self.broadcast_ip, self._target_port))
+            try:
+                self.socket.sendto(msg, (self.broadcast_ip, self._target_port))
+            except OSError as e:
+                self._error = True
+                logging.error(e)
diff --git a/umnp/microcontroller/measurementdevice.py b/umnp/microcontroller/measurementdevice.py
index 9a3aef1..9a1ab8f 100644
--- a/umnp/microcontroller/measurementdevice.py
+++ b/umnp/microcontroller/measurementdevice.py
@@ -37,6 +37,8 @@ class MeasurementDevice:
         self._receiver = None
         self._communicator = None
         self._type = device_type
+        self._i2c = None
+        self._spi = None
 
     @property
     def device_type(self):
@@ -46,9 +48,31 @@ class MeasurementDevice:
     def boot_time(self):
         return self._boot_time
 
+    def add_i2c(self, i2c: machine.I2C):
+        self._i2c = i2c
+
+    @property
+    def spi(self):
+        return self._spi
+
+    def add_spi(self, spi: machine.SPI):
+        self._spi = spi
+
+    @property
+    def i2c(self):
+        return self._i2c
+
     def add_network_adapter(self, adapter):
         self._network = adapter
 
+    def reset_network_adapter(self):
+        if self._network:
+            self._network.reset()
+
+    @property
+    def network(self):
+        return self._network
+
     def generated_mac_string(self) -> str:
         machine_id = self.identifier_raw[:6]
         return ":".join(f"{digit:02x}" for digit in machine_id)
diff --git a/umnp/microcontroller/sensors/lps28dfw/__init__.py b/umnp/microcontroller/sensors/lps28dfw/__init__.py
index 2815a9f..11e55ad 100644
--- a/umnp/microcontroller/sensors/lps28dfw/__init__.py
+++ b/umnp/microcontroller/sensors/lps28dfw/__init__.py
@@ -1,5 +1,6 @@
 import asyncio
 import struct
+import sys
 import time
 
 try:
@@ -7,6 +8,15 @@ try:
 except ImportError:
     from umnp.microcontroller.umock.machine import I2C
 
+if sys.implementation.name == "micropython":
+    import umnp.protocol.compat.logging as logging
+else:
+
+    def const(x):
+        return x
+
+    import logging
+
 LPS28DFW_DEFAULT_ADDRESS = const(0x5D)
 LPS28DFW_READ = const(0xB9)
 LPS28DFW_WRITE = const(0xB8)
@@ -183,8 +193,12 @@ class LPS28DFW:
         return status & avail_mask == avail_mask, status & overrun_mask == overrun_mask
 
     async def measure(self):
-        p = await self.pressure()
-        t = await self.temperature()
+        try:
+            p = await self.pressure()
+            t = await self.temperature()
+        except OSError as e:
+            logging.error(f"Unable to read measurement data: {e}")
+            return self._nan, self._nan
         return p, t
 
     async def pressure(self):
diff --git a/umnp/microcontroller/sensors/sht45/__init__.py b/umnp/microcontroller/sensors/sht45/__init__.py
index 7b82359..40da97f 100644
--- a/umnp/microcontroller/sensors/sht45/__init__.py
+++ b/umnp/microcontroller/sensors/sht45/__init__.py
@@ -1,8 +1,19 @@
+import sys
+
 try:
     from machine import I2C
 except ImportError:
     from umnp.microcontroller.umock.machine import I2C
 
+
+if sys.implementation.name == "micropython":
+    import umnp.protocol.compat.logging as logging
+else:
+
+    def const(x):
+        return x
+
+    import logging
 import asyncio
 import time
 
@@ -83,12 +94,19 @@ class SHT45:
 
         cmd = bytearray(1)
         cmd[0] = SHT45_HIRES
-        n_ack = self._i2c.writeto(self._i2c_address, cmd)
+        try:
+            n_ack = self._i2c.writeto(self._i2c_address, cmd)
+        except OSError as e:
+            logging.error(f"Unable to write to I2C bus: {e}")
+            return self._nan, self._nan
         if n_ack != 1:
             return temperature, rh
         await asyncio.sleep(SHT45_WAIT_TIME_MS / 1000)
-
-        data = self._i2c.readfrom(self._i2c_address, 6)
+        try:
+            data = self._i2c.readfrom(self._i2c_address, 6)
+        except OSError as e:
+            logging.error(f"Unable to read from I2C bus: {e}")
+            return self._nan, self._nan
         temperature = self._translate_temperature(data[0:3])
         rh = self._translate_rh(data[3:6])
 
diff --git a/umnp/microcontroller/umock/machine/__init__.py b/umnp/microcontroller/umock/machine/__init__.py
index 4f6fadb..178916d 100644
--- a/umnp/microcontroller/umock/machine/__init__.py
+++ b/umnp/microcontroller/umock/machine/__init__.py
@@ -38,6 +38,9 @@ class SPI:
     def __init__(self, *args, **kwargs):
         pass
 
+    def deinit(self):
+        pass
+
     def write(self, *args):
         pass
 
@@ -54,7 +57,7 @@ class Pin:
 
 
 class I2C:
-    def __init__(self, id: int, scl: Pin, sda: Pin):
+    def __init__(self, id: int, scl: Pin, sda: Pin, timeout=500000):
         self._id = id
         self._scl = scl
         self._sda = sda
-- 
GitLab