few improvments and fixes to consumption/time tracking

This commit is contained in:
Nicolò Balzarotti 2023-02-21 16:34:40 +01:00
parent e94994f4fb
commit e9713731b1
6 changed files with 102 additions and 33 deletions

6
app.py
View File

@ -131,20 +131,20 @@ def run_recipes():
sleep(1) sleep(1)
def read_sensors(): def read_sensors():
periodi_counter = 0 periodic_counter = 0
scantime = 0.5 scantime = 0.5
periodic_state_every_s = 5 periodic_state_every_s = 5
periodic_state_every_loop = int(periodic_state_every_s/scantime) periodic_state_every_loop = int(periodic_state_every_s/scantime)
while True: while True:
sensors.read() sensors.read()
socketio.emit('sensors', (sensors.get(),)) 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: ## peridoic refresh other data, too, like:
# - current phase (time in this phase) # - current phase (time in this phase)
# - actuators (consumption) # - actuators (consumption)
# Issuing a full state update is easiest # Issuing a full state update is easiest
updateState() updateState()
periodi_counter += 1 periodic_counter += 1
sleep(scantime) sleep(scantime)
import threading import threading

View File

@ -9,6 +9,8 @@ class FixedDutyCycle(controllers.Controller):
self.set_duty(kwargs.pop('duty_perc')) self.set_duty(kwargs.pop('duty_perc'))
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.last_time = None self.last_time = None
self.total_failed_read = 0
self.failed_read = 0
def set_duty(self, duty=None): def set_duty(self, duty=None):
# Updates the duty cycle to a point in time where the switch # 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): def one_cycle(self, time):
self.last_time = 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 ( return (self.timepoint() < self.duty) if (
self.input < self.target) else False 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): def fixed_duty_wrapper(args):
name, duty, period = stringify(args.car, quote=False), args.cdr.car, args.cdr.cdr.car name, duty, period = stringify(args.car, quote=False), args.cdr.car, args.cdr.cdr.car
print('name, duty, period', name, duty*100, period) print('name, duty, period', name, duty*100, period)

31
dist/server.js vendored
View File

@ -53,7 +53,7 @@ function applyState(newstate) {
render_actuators(state.actuators); render_actuators(state.actuators);
render_sidebar(state); render_sidebar(state);
manual_modal(state.manual); manual_modal(state.manual);
current_recipe(state.recipe); current_recipe(Object.assign({}, state.state, state.recipe));
render_load_recipe(state); render_load_recipe(state);
render_plot(); render_plot();
localstate.editing.recipe = state.recipes.findIndex( localstate.editing.recipe = state.recipes.findIndex(
@ -85,6 +85,14 @@ function add_plot_data(newpoints, render=true) {
if (render) render_plot(); 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() { function render_plot() {
if (enable_draw && document.getElementById("data-plot") !== null) { if (enable_draw && document.getElementById("data-plot") !== null) {
Plotly.newPlot('data-plot', Object.values(plot_data)); Plotly.newPlot('data-plot', Object.values(plot_data));
@ -344,10 +352,17 @@ On Exit: {{onexit}}<br>
html.innerHTML = Mustache.render(template, data); 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) { function current_recipe(data) {
let html = document.getElementById("current-recipe"); let html = document.getElementById("current-recipe");
if (html === null) return; if (html === null) return;
if (data === null) { if (data === null || data.phases === undefined) {
html.innerHTML = ''; html.innerHTML = '';
return; return;
} }
@ -356,20 +371,26 @@ function current_recipe(data) {
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title"> <p class="card-header-title">
Active Recipe: {{name}} Active Recipe ({{recipe-time}}): {{name}}
</p> </p>
</header> </header>
<div class="card-content"> <div class="card-content">
{{description}} {{description}}
<br/>
{{recipe-consumption}}
<br/>
<div class="tabs is-centered"> <div class="tabs is-centered">
<ul> <ul>
{{#phases}} {{#phases}}
<li class="{{#current}}is-active{{/current}}"><a>{{name}}</a></li> <li class="{{#current}}is-active{{/current}}"><a>{{name}}{{#current}} ({{phase-time}}){{/current}}</a></li>
{{/phases}} {{/phases}}
</ul> </ul>
</div> </div>
{{current_phase.text}} {{current_phase.text}}
<br/> <br/>
{{phase-consumption}}
<br/>
<br/> <br/>
Next Cond: {{current_phase.nextcond}} Next Cond: {{current_phase.nextcond}}
<br/> <br/>
@ -385,6 +406,8 @@ On Exit: {{current_phase.onexit}}
class="card-footer-item">Stop</a> class="card-footer-item">Stop</a>
</footer> </footer>
</div>`; </div>`;
data['recipe-time'] = stodate(data['recipe-time'])
data['phase-time'] = stodate(data['phase-time'])
html.innerHTML = Mustache.render(template, data); html.innerHTML = Mustache.render(template, data);
} }

View File

@ -7,11 +7,12 @@ from persona import ManualIntervention
from sensors import get_sensor_value from sensors import get_sensor_value
from control import fixed_duty_wrapper from control import fixed_duty_wrapper
from actuators import actuators from actuators import actuators
from utils import consumption_diff, consumption_to_string from utils import consumption_diff, consumption_to_string, stohms
MY_GLOBAL_ENV = ( MY_GLOBAL_ENV = (
_('make-duty-controller', 3, fixed_duty_wrapper, _('make-duty-controller', 3, fixed_duty_wrapper,
GLOBAL_ENV)) _('format-time', 1, lambda x: stohms(x.car),
GLOBAL_ENV)))
def safe_eval(data, env): def safe_eval(data, env):
try: try:
@ -95,8 +96,10 @@ class State():
def getState(self): def getState(self):
return { return {
'phase': self.phase, 'phase': self.phase,
'phase-time': self.phase_time(), 'phase-time': int(self.phase_time()),
'phase-consumption': self.consumption() '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): def now(self):
@ -107,18 +110,21 @@ class State():
return return
self.phase = 0 self.phase = 0
self.env = ( self.env = (
_('recipe-name', 0, lambda x: self.recipe.name, _('recipe-name', 0, lambda _: self.recipe.name,
_('recipe-description', 0, lambda x: self.recipe.description, _('recipe-description', 0, lambda _: self.recipe.description,
_('recipe-consumption', 0, lambda x: self.recipe.consumption(), _('recipe-consumption', 0, lambda _: self.recipe_consumption(),
_('time-in-this-phase', 0, lambda x: self.phase_time(), _('recipe-time', 0, lambda _: self.recipe_time(),
_('phase-consumption', 0, lambda x: self.consumption(), _('time-in-this-phase', 0, lambda _: self.phase_time(),
self.baseenv)))))) _('phase-consumption', 0, lambda _: self.phase_consumption(),
_('phase-time', 0, lambda _: self.phase_time(),
self.baseenv))))))))
self.envdata['controllers'] = {} self.envdata['controllers'] = {}
for controller in self.recipe.controllers: for controller in self.recipe.controllers:
name, ctrl = safe_eval(controller, self.env) name, ctrl = safe_eval(controller, self.env)
self.envdata['controllers'][name] = ctrl self.envdata['controllers'][name] = ctrl
self.envdata['sensors'] = {} 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) self.loadphase(self.phase)
def recipeById(self, id): def recipeById(self, id):
@ -190,7 +196,12 @@ class State():
return None return None
def phase_time(self, arg=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): def loadphase(self, phaseidx):
phase = self.recipe.phases[phaseidx] phase = self.recipe.phases[phaseidx]
@ -240,11 +251,17 @@ class State():
return {actuator: actuators[actuator].consumption() for return {actuator: actuators[actuator].consumption() for
actuator in actuators.keys()} actuator in actuators.keys()}
def consumption(self): def phase_consumption(self):
before = self.envdata['current-phase-start-consumption'] before = self.envdata.get('current-phase-start-consumption', {})
after = self.get_total_consumption() after = self.get_total_consumption()
return consumption_diff(before, after) return consumption_diff(before, after)
def recipe_consumption(self):
return consumption_diff(
self.envdata.get('recipe-start-consumption', {}),
self.get_total_consumption())
class Recipe(): class Recipe():
def __init__(self, name, description='', def __init__(self, name, description='',
controllers=(), controllers=(),
@ -278,16 +295,12 @@ class Recipe():
# 'phase': self.phase # '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): def done(self, env, message=None):
message = message or ''' message = message or '''
(concat "Recipe " (recipe-name) " completed at " (now) "\n" (concat "Recipe " (recipe-name) " completed at " (now)
"Consumption: " (recipe-consumption)) " in " (format-time (recipe-time)) ", Consumption: " (recipe-consumption))
''' '''
#
safe_eval(f'(notify {message})', env) safe_eval(f'(notify {message})', env)
class Phase(): class Phase():
@ -305,7 +318,14 @@ class Phase():
return safe_eval(self.nextcond, env) return safe_eval(self.nextcond, env)
def exit(self, 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): def revert(self, env):
# Undo state changes (attuators values) # Undo state changes (attuators values)

View File

@ -90,11 +90,11 @@ class SHT40(Sensor):
print("ERROR: invalid sensor value: ", what) print("ERROR: invalid sensor value: ", what)
return return
self.measure = what self.measure = what
self.heat_every = every
if self.is_humidity():
self.last_heat = perf_counter()
if not enable_sht: if not enable_sht:
return return
self.heat_every = every
if self.measure != "Temperature":
self.last_heat = perf_counter()
# alternative: HIGHHEAT_1S # alternative: HIGHHEAT_1S
self.heatmode = adafruit_sht4x.Mode.LOWHEAT_100MS self.heatmode = adafruit_sht4x.Mode.LOWHEAT_100MS
self.standardmode = SHT40_DEFAULT self.standardmode = SHT40_DEFAULT
@ -114,6 +114,8 @@ class SHT40(Sensor):
def read(self): def read(self):
time = perf_counter() time = perf_counter()
if not enable_sht:
return (time, None)
reset = False reset = False
if self.is_humidity(): if self.is_humidity():
timediff = time - self.last_heat timediff = time - self.last_heat

View File

@ -1,7 +1,9 @@
from datetime import timedelta
def consumption_diff(before, after): def consumption_diff(before, after):
out = {} out = {}
for key in after: 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 return out
def consumption_to_string(dic): 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 += '{0:s}:\t{1:.2f}W\n'.format(key, dic[key])
out += 'Total:\t{0:.2f}W'.format(sum(dic.values())) out += 'Total:\t{0:.2f}W'.format(sum(dic.values()))
return out return out
def stohms(s):
return str(timedelta(seconds = s))