Skip to content

Commit

Permalink
experimental backport of PidCtrl
Browse files Browse the repository at this point in the history
  • Loading branch information
schwabix-1311 committed Nov 7, 2024
1 parent bdbe551 commit 9388338
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 35 deletions.
5 changes: 3 additions & 2 deletions aquaPi/machineroom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import atexit

from .msg_bus import MsgBus, BusRole
from .ctrl_nodes import MinimumCtrl, MaximumCtrl, SunCtrl, FadeCtrl
from .ctrl_nodes import MinimumCtrl, MaximumCtrl, PidCtrl, SunCtrl, FadeCtrl
from .in_nodes import AnalogInput, ScheduleInput
from .out_nodes import SwitchDevice, AnalogDevice
from .aux_nodes import ScaleAux, MinAux, MaxAux, AvgAux
Expand Down Expand Up @@ -143,7 +143,8 @@ def create_default_nodes(self):
# single water temp sensor, switched relay
wasser_i = AnalogInput('Wasser', 'DS1820 xA2E9C', 25.0, '°C',
avg=3, interval=30)
wasser = MinimumCtrl('Temperatur', wasser_i.id, 25.0)
#wasser = MinimumCtrl('Temperatur', wasser_i.id, 25.0)
wasser = PidCtrl('PID Temperatur', wasser_i.id, 25.0)
wasser_o = SwitchDevice('Heizstab', wasser.id,
'GPIO 12 out', inverted=1)
wasser_i.plugin(self.bus)
Expand Down
6 changes: 3 additions & 3 deletions aquaPi/machineroom/aux_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def listen(self, msg):
for k in self.values:
val += self.values[k] / len(self.values)

if (self.data != val):
if (self.data != val) or True:
self.data = val
log.info('AvgAux %s: output %f', self.id, self.data)
self.post(MsgData(self.id, round(self.data, 4)))
Expand Down Expand Up @@ -205,7 +205,7 @@ def listen(self, msg):
for k in self.values:
val = min(val, self.values[k])
val = round(val, 4)
if self.data != val:
if self.data != val or True:
self.data = val
## log.info('MinAux %s: output %f', self.id, self.data)
self.post(MsgData(self.id, self.data))
Expand Down Expand Up @@ -233,7 +233,7 @@ def listen(self, msg):
for k in self.values:
val = max(val, self.values[k])
val = round(val, 4)
if self.data != val:
if self.data != val or True:
self.data = val
log.info('MaxAux %s: output %f', self.id, self.data)
self.post(MsgData(self.id, self.data))
Expand Down
114 changes: 111 additions & 3 deletions aquaPi/machineroom/ctrl_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def listen(self, msg):
elif float(msg.data) >= (self.threshold + self.hysteresis / 2):
new_val = 0.0

if (self.data != new_val) or True: # WAR a startup problem
if (self.data != new_val) or True: #FIXME WAR a startup problem
log.debug('MinimumCtrl: %d -> %d', self.data, new_val)
self.data = new_val

Expand Down Expand Up @@ -182,7 +182,7 @@ def listen(self, msg):
elif float(msg.data) <= (self.threshold - self.hysteresis / 2):
new_val = 0.0

if (self.data != new_val) or True: # WAR a startup problem
if (self.data != new_val) or True: #FIXME WAR a startup problem
log.debug('MaximumCtrl: %d -> %d', self.data, new_val)
self.data = new_val

Expand All @@ -198,7 +198,7 @@ def listen(self, msg):
self.post(MsgData(self.id, self.data))
return super().listen(msg)

def get_settings(self):
def get_settings(self) -> list[tuple]:
limits = get_unit_limits(self.unit)

settings = super().get_settings()
Expand All @@ -209,6 +209,113 @@ def get_settings(self):
return settings


class PidCtrl(ControllerNode):
""" An experimental PID controller producing a slow PWM
Options:
name - unique name of this controller node in UI
receives - id of a single (!) input to receive measurements from
setpoint - the target value
p_fact/i_fact,d_fact - the PID factors
sample - the ratio of sensor reads to 1 PID output
Output:
posts a series of PWM pulses
"""
data_range = DataRange.PERCENT
#data_range = DataRange.BINARY

def __init__(self, name: str, inputs: str, setpoint: float,
p_fact: float = 1.0, i_fact: float = .1, d_fact: float = 0.1,
sample: int = 10,
_cont: bool = False):
super().__init__(name, inputs, _cont=_cont)
self.setpoint: float = setpoint
self.p_fact: float = p_fact
self.i_fact: float = i_fact
self.d_fact: float = d_fact
self.sample: int = sample
self._err_sum: float = 0
self._err_pre: float = 0
self._ta_pre: float = 0
self._t_pre: float = 0
self._cnt: int = 0

def __getstate__(self):
state = super().__getstate__()
state.update(setpoint=self.setpoint)
state.update(p_fact=self.p_fact)
state.update(i_fact=self.i_fact)
state.update(d_fact=self.d_fact)
state.update(sample=self.sample)
return state

def __setstate__(self, state):
log.debug('__SETstate__ %r', state)
self.data = state['data']
PidCtrl.__init__(self, state['name'], state['inputs'], state['setpoint'],
p_fact=state['p_fact'], i_fact=state['i_fact'], d_fact=state['d_fact'],
sample=state['sample'],
_cont=True)

def _pulse(self, dur: float, perc: float):
log.info(' PID ON: %d %% -> %f s', perc, round(dur * perc / 100, 1))
if perc:
self.post(MsgData(self.id, 100))
time.sleep(dur * perc / 100)
log.info(' PID off')
if perc < 100:
self.post(MsgData(self.id, 0))
return

def listen(self, msg):
if isinstance(msg, MsgData):
log.debug('PID got %s', msg)
now = time.time()
ta = now - self._t_pre
err = float(msg.data) - self.setpoint
if self._ta_pre:
err_sum = self._err_sum + err
val = self.p_fact * err \
+ self.i_fact * ta * err_sum \
+ self.d_fact / ta * (err - self._err_pre)

log.debug('PID err %f, e-sum %f, p %f / i %f / d %f, ',
err, self._err_sum,
self.p_fact * err,
self.i_fact * ta * err_sum,
self.d_fact / ta * (err - self._err_pre))
self.data = min(max(0., 50. - val*10), 100.)
if self.data > 0.0 and self.data < 100.:
self._err_sum = err_sum
else:
log.debug('clipped')

self._cnt += 1
if self._cnt % self.sample == 0:
log.info('PID -> %f (%f)', self.data, val)
if self.data_range == DataRange.PERCENT:
self.post(MsgData(self.id, round(self.data, 4)))
#self.post(MsgData(self.id, 100 if self.data >= 50 else 0))
else:
Thread(name='PIDpulse', target=self._pulse, args=[9 * ta, self.data], daemon=True).start()
self._err_pre = err
self._ta_pre = ta
self._t_pre = now

return super().listen(msg)

def get_settings(self) -> list[tuple]:
settings = super().get_settings()
settings.append(('setpoint', 'Sollwert [%s]' % self.unit,
self.setpoint, 'type="number"'))
settings.append(('p_fact', 'P Faktor', self.p_fact, 'type="number" min="0" max="10" step="0.1"'))
settings.append(('i_fact', 'I Faktor', self.i_fact, 'type="number" min="0" max="10" step="0.1"'))
settings.append(('d_fact', 'D Faktor', self.d_fact, 'type="number" min="0" max="10" step="0.1"'))
settings.append(('sample', 'Samples', self.sample, 'type="number" min="1" max="25" step="1"'))
return settings


class FadeCtrl(ControllerNode):
""" Single channel linear fading controller, usable for light (dusk/dawn).
A change of input value will start a ramp from current to new
Expand Down Expand Up @@ -278,6 +385,7 @@ def listen(self, msg):
log.debug('_fader %f -> %f', self.data, self.target)
self._fader_thread = Thread(name=self.id, target=self._fader, daemon=True)
self._fader_thread.start()
return super().listen(msg)

def _fader(self):
""" This fader uses constant steps of 0.1% unless this would be >10 steps/sec
Expand Down
2 changes: 1 addition & 1 deletion aquaPi/machineroom/in_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def _reader(self):
try:
val = self.read()
self.alert = None
if self.data != val:
if self.data != val or True:
self.data = val
log.brief('%s: read %f', self.id, self.data)
self.post(MsgData(self.id, self.data))
Expand Down
18 changes: 10 additions & 8 deletions aquaPi/machineroom/out_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
class DeviceNode(BusListener):
""" Base class for OUT_ENDP such as relay, PWM, GPIO pins.
Receives float input from listened sender.
The interpretation is device specific, recommendation is
to follow pythonic truth testing to avoid surprises.
Binary devices should use a threashold of 50 or pythonic
truth testing, whatever is more intuitive for each dev.
"""
ROLE = BusRole.OUT_ENDP

Expand Down Expand Up @@ -50,8 +50,8 @@ def __init__(self, name, inputs, port, inverted=0, _cont=False):
self._inverted = int(inverted)
##self.unit = '%' if self.data_range != DataRange.BINARY else '⏻'
self.port = port
self.switch(self.data if _cont else 0)
log.info('%s init to %r|%f|%f', self.name, _cont, self.data, inverted)
self.switch(self.data if _cont else False)
log.info('%s init to %r|%f|%r', self.name, _cont, self.data, inverted)

def __getstate__(self):
state = super().__getstate__()
Expand Down Expand Up @@ -86,12 +86,14 @@ def inverted(self, inverted):

def listen(self, msg):
if isinstance(msg, MsgData):
if self.data != bool(msg.data):
self.switch(msg.data)
#if self.data != bool(msg.data):
data = (msg.data > 50.)
if self.data != data:
self.switch(data)
return super().listen(msg)

def switch(self, on):
self.data = 100 if bool(on) else 0
def switch(self, state: bool) -> None:
self.data: bool = state

log.info('SwitchDevice %s: turns %s', self.id, 'ON' if self.data else 'OFF')
if not self.inverted:
Expand Down
1 change: 1 addition & 0 deletions aquaPi/static/spa/components/dashboard/comps.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ const MaximumCtrl = {
},
}
Vue.component('MaximumCtrl', MaximumCtrl)
Vue.component('PidCtrl', MaximumCtrl)


const SunCtrl = {
Expand Down
1 change: 1 addition & 0 deletions aquaPi/static/spa/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default
history: 'Diagramm',
in_endp: 'Eingang',
out_endp: 'Ausgang',
alerts: 'Störung',
},
dataRange: {
default: {
Expand Down
2 changes: 1 addition & 1 deletion aquaPi/static/spa/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ export default
}
}
}

},

misc: {
Expand All @@ -100,6 +99,7 @@ export default
history: 'Diagram',
in_endp: 'Input',
out_endp: 'Output',
alerts: 'Alert',
},
dataRange: {
default: {
Expand Down
18 changes: 1 addition & 17 deletions aquaPi/static/spa/pages/Config.vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,7 @@ const Config = {
<template v-if="node.inputs">
<v-sheet outlined class="ba-1 ml-7">
<h5>INPUTS:</h5>
<!-- {{ node.inputs }}-->
<ul>
<li v-for="item in node.inputs.sender">
<h3>
{{ nodeItem(item).name }}
<span class="font-weight-light">
[{{ nodeItem(item).id }}]
{{ nodeItem(item).identifier }}
| {{ nodeItem(item).type }}
| {{ nodeItem(item).role }}
| data: {{ nodeItem(item).data}} {{ nodeItem(item).unit}} {{ nodeItem(item).data_range }}
</span>
</h3>
<!-- {{ $store.getters['dashboard/node'](item) }} -->
</li>
</ul>
INPUTS: {{ node.inputs }}
</v-sheet>
</template>
</div>
Expand Down

0 comments on commit 9388338

Please sign in to comment.