Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixup underflow-detection and busy-state updating #56

Merged
merged 4 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <falkeboc@cs.aau.dk>
maintainer=Falke Carlsen <falkeboc@cs.aau.dk>
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
Loading