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) {
{{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))