From e9713731b1e16e821bb3e776cce477ccf1f153d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Balzarotti?= Date: Tue, 21 Feb 2023 16:34:40 +0100 Subject: [PATCH] few improvments and fixes to consumption/time tracking --- app.py | 6 ++-- control/fixed_duty.py | 19 +++++++++++++ dist/server.js | 31 ++++++++++++++++++--- phasectrl.py | 64 ++++++++++++++++++++++++++++--------------- sensors.py | 8 ++++-- utils.py | 7 ++++- 6 files changed, 102 insertions(+), 33 deletions(-) diff --git a/app.py b/app.py index c991baa..28da9e0 100644 --- a/app.py +++ b/app.py @@ -131,20 +131,20 @@ def run_recipes(): sleep(1) def read_sensors(): - periodi_counter = 0 + periodic_counter = 0 scantime = 0.5 periodic_state_every_s = 5 periodic_state_every_loop = int(periodic_state_every_s/scantime) while True: sensors.read() socketio.emit('sensors', (sensors.get(),)) - if periodic_counter % periodic_state_every == 0: + if periodic_counter % periodic_state_every_loop == 0: ## peridoic refresh other data, too, like: # - current phase (time in this phase) # - actuators (consumption) # Issuing a full state update is easiest updateState() - periodi_counter += 1 + periodic_counter += 1 sleep(scantime) import threading diff --git a/control/fixed_duty.py b/control/fixed_duty.py index b6e1d17..0b01341 100644 --- a/control/fixed_duty.py +++ b/control/fixed_duty.py @@ -9,6 +9,8 @@ class FixedDutyCycle(controllers.Controller): self.set_duty(kwargs.pop('duty_perc')) super().__init__(*args, **kwargs) self.last_time = None + self.total_failed_read = 0 + self.failed_read = 0 def set_duty(self, duty=None): # Updates the duty cycle to a point in time where the switch @@ -33,10 +35,27 @@ class FixedDutyCycle(controllers.Controller): def one_cycle(self, time): self.last_time = time + if self.input is None: + self.total_failed_read += 1 + self.failed_read += 1 + # TODO: WARN/EXIT IF WE HAVE TOO MANY FAILED READS? + return False + self.failed_read = 0 return (self.timepoint() < self.duty) if ( self.input < self.target) else False +# 0. refractary_period is fixed (e.g. 30s, depends on the hardware) +# 1. period = refractary_period / max(min(duty, 1-duty), 0.01) +# this guarantees that the shortest period is always the refractary period +# Store a) ON/OFF HISTORY and the number of times we inhibit control +# The duty must be adjusted automatically +# a) Take the mean() of the history +# b) if mean << duty: current duty is too high, set it to mean +# c) if mean = duty: current duty is either too low (so we are always on, when we can) or it is fine. +# use the percentage of skipped periods over total number of tests +# HOW LONG MUST BE THE HISTORY? + def fixed_duty_wrapper(args): name, duty, period = stringify(args.car, quote=False), args.cdr.car, args.cdr.cdr.car print('name, duty, period', name, duty*100, period) diff --git a/dist/server.js b/dist/server.js index 848ecee..215ab3b 100644 --- a/dist/server.js +++ b/dist/server.js @@ -53,7 +53,7 @@ function applyState(newstate) { render_actuators(state.actuators); render_sidebar(state); manual_modal(state.manual); - current_recipe(state.recipe); + current_recipe(Object.assign({}, state.state, state.recipe)); render_load_recipe(state); render_plot(); localstate.editing.recipe = state.recipes.findIndex( @@ -85,6 +85,14 @@ function add_plot_data(newpoints, render=true) { if (render) render_plot(); } +// function set_plot_points(maxpoints) { +// let more = localstate.maxpoints > maxpoints; +// localstate.maxpoints = maxpoints; +// if (more) { +// for (key in plot_data.keys()) +// } +// } + function render_plot() { if (enable_draw && document.getElementById("data-plot") !== null) { Plotly.newPlot('data-plot', Object.values(plot_data)); @@ -344,10 +352,17 @@ On Exit: {{onexit}}
html.innerHTML = Mustache.render(template, data); } +function stodate(value) { + var date = new Date(0); + date.setSeconds(value); + var timeString = date.toISOString().substring(11, 19); + return timeString; +} + function current_recipe(data) { let html = document.getElementById("current-recipe"); if (html === null) return; - if (data === null) { + if (data === null || data.phases === undefined) { html.innerHTML = ''; return; } @@ -356,20 +371,26 @@ function current_recipe(data) {

- Active Recipe: {{name}} + Active Recipe ({{recipe-time}}): {{name}}

{{description}} +
+{{recipe-consumption}} +
+ {{current_phase.text}}
+{{phase-consumption}} +

Next Cond: {{current_phase.nextcond}}
@@ -385,6 +406,8 @@ On Exit: {{current_phase.onexit}} class="card-footer-item">Stop
`; + data['recipe-time'] = stodate(data['recipe-time']) + data['phase-time'] = stodate(data['phase-time']) html.innerHTML = Mustache.render(template, data); } diff --git a/phasectrl.py b/phasectrl.py index dde9e85..e06402a 100644 --- a/phasectrl.py +++ b/phasectrl.py @@ -7,11 +7,12 @@ from persona import ManualIntervention from sensors import get_sensor_value from control import fixed_duty_wrapper from actuators import actuators -from utils import consumption_diff, consumption_to_string +from utils import consumption_diff, consumption_to_string, stohms MY_GLOBAL_ENV = ( _('make-duty-controller', 3, fixed_duty_wrapper, - GLOBAL_ENV)) + _('format-time', 1, lambda x: stohms(x.car), + GLOBAL_ENV))) def safe_eval(data, env): try: @@ -95,8 +96,10 @@ class State(): def getState(self): return { 'phase': self.phase, - 'phase-time': self.phase_time(), - 'phase-consumption': self.consumption() + 'phase-time': int(self.phase_time()), + 'phase-consumption': consumption_to_string(self.phase_consumption()), + 'recipe-time': int(self.recipe_time()), + 'recipe-consumption': consumption_to_string(self.recipe_consumption()) } def now(self): @@ -107,18 +110,21 @@ class State(): return self.phase = 0 self.env = ( - _('recipe-name', 0, lambda x: self.recipe.name, - _('recipe-description', 0, lambda x: self.recipe.description, - _('recipe-consumption', 0, lambda x: self.recipe.consumption(), - _('time-in-this-phase', 0, lambda x: self.phase_time(), - _('phase-consumption', 0, lambda x: self.consumption(), - self.baseenv)))))) + _('recipe-name', 0, lambda _: self.recipe.name, + _('recipe-description', 0, lambda _: self.recipe.description, + _('recipe-consumption', 0, lambda _: self.recipe_consumption(), + _('recipe-time', 0, lambda _: self.recipe_time(), + _('time-in-this-phase', 0, lambda _: self.phase_time(), + _('phase-consumption', 0, lambda _: self.phase_consumption(), + _('phase-time', 0, lambda _: self.phase_time(), + self.baseenv)))))))) self.envdata['controllers'] = {} for controller in self.recipe.controllers: name, ctrl = safe_eval(controller, self.env) self.envdata['controllers'][name] = ctrl self.envdata['sensors'] = {} - self.envdata['recipe-start-consumption'] = self.consumption() + self.envdata['recipe-start-consumption'] = self.get_total_consumption() + self.envdata['current-recipe-loadtime'] = self.now() self.loadphase(self.phase) def recipeById(self, id): @@ -190,7 +196,12 @@ class State(): return None def phase_time(self, arg=None): - return (self.now() - self.envdata.get('current-phase-loadtime', 0)) + start = self.envdata.get('current-phase-loadtime', 0) + return start and self.now() - start + + def recipe_time(self, arg=None): + start = self.envdata.get('current-recipe-loadtime', 0) + return start and self.now() - start def loadphase(self, phaseidx): phase = self.recipe.phases[phaseidx] @@ -240,11 +251,17 @@ class State(): return {actuator: actuators[actuator].consumption() for actuator in actuators.keys()} - def consumption(self): - before = self.envdata['current-phase-start-consumption'] + def phase_consumption(self): + before = self.envdata.get('current-phase-start-consumption', {}) after = self.get_total_consumption() return consumption_diff(before, after) + def recipe_consumption(self): + return consumption_diff( + self.envdata.get('recipe-start-consumption', {}), + self.get_total_consumption()) + + class Recipe(): def __init__(self, name, description='', controllers=(), @@ -278,16 +295,12 @@ class Recipe(): # 'phase': self.phase } - def consumption(self): - return consumption_to_string(consumption_diff( - self.envdata['recipe-start-consumption'], - self.envdata['recipe-end-consumption'])) - def done(self, env, message=None): message = message or ''' - (concat "Recipe " (recipe-name) " completed at " (now) "\n" - "Consumption: " (recipe-consumption)) + (concat "Recipe " (recipe-name) " completed at " (now) + " in " (format-time (recipe-time)) ", Consumption: " (recipe-consumption)) ''' + # safe_eval(f'(notify {message})', env) class Phase(): @@ -305,7 +318,14 @@ class Phase(): return safe_eval(self.nextcond, env) def exit(self, env): - return safe_eval(self.onexit, env) + value = safe_eval(self.onexit, env) + message = ''' +(notify + (concat "Phase done in " (format-time (phase-time)) + ", with consumption: " (phase-consumption))) + ''' + safe_eval(message, env) + return value def revert(self, env): # Undo state changes (attuators values) diff --git a/sensors.py b/sensors.py index 2824d98..5699f48 100644 --- a/sensors.py +++ b/sensors.py @@ -90,11 +90,11 @@ class SHT40(Sensor): print("ERROR: invalid sensor value: ", what) return self.measure = what + self.heat_every = every + if self.is_humidity(): + self.last_heat = perf_counter() if not enable_sht: return - self.heat_every = every - if self.measure != "Temperature": - self.last_heat = perf_counter() # alternative: HIGHHEAT_1S self.heatmode = adafruit_sht4x.Mode.LOWHEAT_100MS self.standardmode = SHT40_DEFAULT @@ -114,6 +114,8 @@ class SHT40(Sensor): def read(self): time = perf_counter() + if not enable_sht: + return (time, None) reset = False if self.is_humidity(): timediff = time - self.last_heat diff --git a/utils.py b/utils.py index 40bff80..1a4fe4f 100644 --- a/utils.py +++ b/utils.py @@ -1,7 +1,9 @@ +from datetime import timedelta + def consumption_diff(before, after): out = {} for key in after: - out[key] = before.get(key, 0) - after.get(key, 0) + out[key] = after.get(key, 0) - before.get(key, 0) return out def consumption_to_string(dic): @@ -10,3 +12,6 @@ def consumption_to_string(dic): out += '{0:s}:\t{1:.2f}W\n'.format(key, dic[key]) out += 'Total:\t{0:.2f}W'.format(sum(dic.values())) return out + +def stohms(s): + return str(timedelta(seconds = s))