Skip to content

Commit

Permalink
Merge pull request #56 from DEIS-Tools/fixup
Browse files Browse the repository at this point in the history
Fixup underflow-detection and busy-state updating
  • Loading branch information
falkecarlsen authored Aug 16, 2024
2 parents 5586676 + cfc8fac commit d45a3c8
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 33 deletions.
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name=CLAIRE
version=0.1.13
version=0.1.14
author=Falke Carlsen <[email protected]>
maintainer=Falke Carlsen <[email protected]>
sentence=API to interface with CLAIRE water management demonstrator at DEIS-AAU.
Expand Down
66 changes: 38 additions & 28 deletions py_driver/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

IMMEDIATE_OUTPUT = True
TAG = "DRIVER:"
CLAIRE_VERSION = "v0.1.13"
CLAIRE_VERSION = "v0.1.14"
CLAIRE_READY_SIGNAL = "CLAIRE-READY"
TUBE_MAX_LEVEL = 900
DEBUG = True
COMMUNICATION_TIMEOUT = 10
Expand Down Expand Up @@ -67,7 +68,7 @@ class ClaireState:
last_update: datetime = datetime.now() - timedelta(hours=1)

def __init__(self, state):
self.dynamic = None
self.dynamic = False # set false initially to allow first update to succeed
self.set_state(state)

def set_state(self, state):
Expand Down Expand Up @@ -128,13 +129,12 @@ class ClaireDevice:
"""
Class that represents the Claire demonstrator setup.
"""
state: ClaireState

def __init__(self, port):
self.e_stop = False
self.state = None
self.device = port
self.busy = True # initially unknown, therefore busy
# read timeout in secs, 1 should be sufficient

# exclusive only available on posix-like systems, assumes mac-env is posix-like
if ["linux", "darwin"].__contains__(sys.platform):
Expand All @@ -153,18 +153,19 @@ def __init__(self, port):
self.read_thread.daemon = True
self.read_thread.start()

self.underflow_thread = threading.Thread(target=self._underflow_check)
self.underflow_thread.daemon = True
self.underflow_thread.start()

print(f'{TAG} Device connected to {port}, waiting for initialization...')
while not self.ready():
sleep(1)
while self.busy:
sleep(0.1)
self.check_version()

print(f'{TAG} Device initialized. Getting initial state...')
self.heartbeat = time() # last time device was alive
self.update_state()
self.update_state(initial=True)

# start underflow check delay
self.underflow_thread = threading.Thread(target=self._underflow_check)
self.underflow_thread.daemon = True
self.underflow_thread.start()

def alive(self):
"""Check if the device is still alive within bound."""
Expand All @@ -173,10 +174,13 @@ def alive(self):
def ready(self):
return not self.busy and self.alive() and not self.e_stop

def update_state(self, tube=None, quick=False):
def outdated(self):
return self.state.last_update < datetime.now() - timedelta(COMMUNICATION_TIMEOUT)

def update_state(self, tube=None, quick=False, initial=False):
"""Get the last state of the device. If cached state is outdated, a new sensor reading is requested."""
# Return cached state if not outdated nor unstable.
if not self.state.dynamic and self.state.last_update >= datetime.now() - timedelta(COMMUNICATION_TIMEOUT):
if not initial and not self.state.dynamic and self.outdated():
return self.state

arg = ""
Expand Down Expand Up @@ -216,36 +220,41 @@ def update_state(self, tube=None, quick=False):
sleep(0.1)
total_wait += 0.1

if total_wait > COMMUNICATION_TIMEOUT and not self.busy:
raise RuntimeError("Waiting too long for state to be communicated.")
if total_wait > COMMUNICATION_TIMEOUT and self.ready():
raise RuntimeError(
f"Waiting too long for state to be communicated. {self.busy=}, {self.ready()=}")

# New state retrieved, parse it.
state = self.get_last_raw_state()
if state:
# Convert distance to water level
state["Tube1_sonar_dist_mm"] = round(self.state.convert_distance_to_level(state["Tube1_sonar_dist_mm"]), 1)
state["Tube2_sonar_dist_mm"] = round(self.state.convert_distance_to_level(state["Tube2_sonar_dist_mm"]), 1)
state["Tube1_sonar_dist_mm"] = round(ClaireState.convert_distance_to_level(state["Tube1_sonar_dist_mm"]), 1)
state["Tube2_sonar_dist_mm"] = round(ClaireState.convert_distance_to_level(state["Tube2_sonar_dist_mm"]), 1)
self.state = ClaireState(state)
return True
return False

def _underflow_check(self):
TAG = "UNDERFLOW_CHECK"
while True:
# sanity check
if not self.alive():
if DEBUG:
print(f'{TAG}: Device is not alive. Waiting {UNDERFLOW_CHECK_INTERVAL} seconds.')
sleep(UNDERFLOW_CHECK_INTERVAL)
continue
if not self.ready():
# do liveness check and update state if device is outdated but was ready on last communication
if self.outdated():
print(f"Device is outdated. {self.state.last_update=}, {datetime.now()=}")
# if last line is OK, then device is still alive, do update of state
if self.read_buffer and self.read_buffer[-1] == CLAIRE_READY_SIGNAL:
print(f"Device is alive. {self.state.last_update=}, {datetime.now()=}")
self.busy = False
self.update_state(quick=True)
else:
if DEBUG:
print(f'{TAG}: Device is busy. Waiting {UNDERFLOW_CHECK_INTERVAL} seconds.')
sleep(UNDERFLOW_CHECK_INTERVAL)
print(f'{TAG}: Device is not ready. Waiting {UNDERFLOW_CHECK_INTERVAL} seconds.')
sleep(UNDERFLOW_CHECK_INTERVAL)
continue

# check if water level is below 0 fixme: errors out in callee during long-running functions due to timeout reached
self.update_state()
# update state if device is dynamic
if self.state.dynamic:
self.update_state(quick=True)

# check underflows
if self.state.Tube1_sonar_dist_mm < TUBE_MAX_LEVEL:
Expand All @@ -265,6 +274,7 @@ def _underflow_check(self):
else:
if DEBUG:
print(f'{TAG}: No underflow detected in watchdog.')
sleep(UNDERFLOW_CHECK_INTERVAL)

def _read_lines(self):
"""Read lines from the serial port and add to the buffer in a thread to not block the main thread."""
Expand All @@ -278,7 +288,7 @@ def _read_lines(self):
self.print_new_lines_buf()
# Check whether the new lines contain the ready signal.
for line in new_lines:
if line == "CLAIRE-READY":
if line == CLAIRE_READY_SIGNAL:
self.busy = False

# Stop reading lines.
Expand Down
6 changes: 3 additions & 3 deletions py_driver/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

# Insert the name of the usb port, which might be different for different devices.
# An easy way to get the port name is to use the Arduino IDE.
PORT = '/dev/ttyUSB1'
PORT = '/dev/ttyUSB0'
#PORT = '/dev/cu.usbserial-1420'


def example_experiment():
claire = driver.ClaireDevice(PORT)
state = claire.update_state() # get current state of device
_ok = claire.update_state() # get current state of device
claire.print_state()
print(f'Current height of TUBE1: {state.Tube1_sonar_dist_mm}')
print(f'Current height of TUBE1: {claire.state.Tube1_sonar_dist_mm} mm')

claire.set_inflow(1, 100)
sleep(3)
Expand Down
2 changes: 1 addition & 1 deletion src/Claire.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include <Arduino.h>
#include <EEPROM.h>

#define VERSION "0.1.13"
#define VERSION "0.1.14"

#define OUTPUT_GPIO_MIN 2
#define OUTPUT_GPIO_MAX 7
Expand Down

0 comments on commit d45a3c8

Please sign in to comment.