From 636070fd613e56e339316ea00cda2643e2435705 Mon Sep 17 00:00:00 2001 From: jens Date: Tue, 24 Nov 2020 18:34:47 +0100 Subject: [PATCH] initial import --- brewpi.py | 345 +++++++++++++++++++++++++++++ components/__init__.py | 0 components/actor/heater.py | 39 ++++ components/actor/stirrer.py | 0 components/aheater.py | 34 +++ components/aplant.py | 30 +++ components/astirrer.py | 62 ++++++ components/atemperatureSensor.py | 18 ++ components/pid/temp_controller.py | 0 components/plant/pot.py | 59 +++++ components/sensor/__init__.py | 0 components/sensor/tempSensorSim.py | 21 ++ 12 files changed, 608 insertions(+) create mode 100644 brewpi.py create mode 100644 components/__init__.py create mode 100644 components/actor/heater.py create mode 100644 components/actor/stirrer.py create mode 100644 components/aheater.py create mode 100644 components/aplant.py create mode 100644 components/astirrer.py create mode 100644 components/atemperatureSensor.py create mode 100644 components/pid/temp_controller.py create mode 100644 components/plant/pot.py create mode 100644 components/sensor/__init__.py create mode 100644 components/sensor/tempSensorSim.py diff --git a/brewpi.py b/brewpi.py new file mode 100644 index 0000000..6d3ebbe --- /dev/null +++ b/brewpi.py @@ -0,0 +1,345 @@ +import asyncio +from ws_server import WsServer +from user import User, UserSet, update +from time import sleep +import abc +import json +from connection import IConnection + +from components.sensor.tempSensorSim import TempSensorSim, ATemperatureSensor +from components.plant.pot import Pot, APlant +from components.actor.heater import Heater, AHeater + + +class IWsServer: + @abc.abstractmethod + def on_recv(self, data, loop): + pass + + @abc.abstractmethod + def on_send(self, loop): + pass + + +class WsServerMultiUser(WsServer): + def __init__(self, loop=None, listener: IConnection = None): + WsServer.__init__(self, loop) + self.listener = listener + self.USERS = UserSet() + self.global_state = {} + + async def notify_state(self, data): + if self.USERS: # asyncio.wait doesn't accept an empty list + await asyncio.wait([user.send(data) for user in self.USERS]) + + async def notify_users(self): + if self.USERS: # asyncio.wait doesn't accept an empty list + message = {"info": {"type": "users", "count": len(self.USERS)}} + await asyncio.wait([user.send(message) for user in self.USERS]) + + async def register(self, websocket): + usr = User(websocket) + usr.path_add("info") + self.USERS.add(websocket, usr) + await self.notify_users() + + async def unregister(self, websocket): + self.USERS.remove(websocket) + await self.notify_users() + + async def handler_recv(self, websocket, path): + while True: + try: + data = await websocket.recv() + usr = self.USERS.get(websocket) + processed = await usr.process(data) + if processed: + await usr.send(self.global_state) + else: + await self.listener.on_recv(data) + except Exception as e: + print(e) + break + + async def handler_send(self, websocket, path): + while True: + data = await self.listener.on_send() + try: + await self.notify_state(data) + # Update global state + self.global_state = update(self.global_state, data) + except Exception as e: + print(e) + break + + +class MsgIo: + def __init__(self, key, send): + self.key = key + self.sender = send + self.receiver = None + + def set_recv_handler(self, handler): + self.receiver = handler + + async def on_recv(self, data): + print("MessageHandler {}".format(data)) + if self.receiver is not None: + await self.receiver(data[self.key]) + + async def send(self, data): + return await self.sender({self.key: data}) + + def can_key(self, key): + return self.key == key + + def get_key(self): + return self.key + + +class Value: + def __init__(self, initial=None): + self.value = initial + self.has_changed = True + + def set(self, value): + self.has_changed = self.value != value + self.value = value + + def is_changed(self): + return self.has_changed + + def get(self): + self.has_changed = False + return self.value + + +class ATask: + def __init__(self, interval): + self.interval = interval + + @abc.abstractmethod + def on_process(self): + pass + + +class TempSensorTask(ATask): + def __init__(self, sensor: ATemperatureSensor, interval, msg_handler: MsgIo): + ATask.__init__(self, interval) + + self.msg_handler = msg_handler + msg_handler.set_recv_handler(self.recv) + self.sensor = sensor + self.offset = 0 + + async def recv(self, data): + print(data) + self.offset = data['Temp'] + + async def send(self, data): + await self.msg_handler.send(data) + + async def on_process(self): + print("{}: Started with interval {} s".format(self.msg_handler.get_key(), self.interval)) + while True: + temp = self.sensor.temperature() + self.offset + await self.send(temp) + await asyncio.sleep(self.interval) + print(temp) + + +class HeaterTask(ATask): + def __init__(self, heater: AHeater, interval, msg_handler: MsgIo): + ATask.__init__(self, interval) + + self.msg_handler = msg_handler + msg_handler.set_recv_handler(self.recv) + self.heater = heater + + async def recv(self, data): + print(data) + for pair in data.items(): + if 'Power' in pair[0]: + self.heater.setPower(pair[1]) + if 'Activate' in pair[0]: + self.heater.activate(bool(pair[1])) + + async def send(self, data): + await self.msg_handler.send(data) + + async def on_process(self): + print("{}: Started with interval {} s".format(self.msg_handler.get_key(), self.interval)) + while True: + self.heater.process() + power = self.heater.getPower() + await self.send({'Power': power}) + await asyncio.sleep(self.interval) + + +class PotTask(ATask): + def __init__(self, pot: APlant, interval, msg_handler: MsgIo): + ATask.__init__(self, interval) + + self.msg_handler = msg_handler + msg_handler.set_recv_handler(self.recv) + self.plant = pot + + async def recv(self, data): + print(data) + for key in data: + print(key) + + async def send(self, data): + await self.msg_handler.send(data) + + async def on_process(self): + print("{}: Started with interval {} s".format(self.msg_handler.get_key(), self.interval)) + plant_temp = Value(-1) + plant_power = Value() + while True: + self.plant.process() + plant_power.set(round(self.plant.getPower(), 1)) + plant_temp.set(round(self.plant.getTemperature(), 1)) + + if plant_power.is_changed(): + print("Plant Power {}".format(plant_power.get())) + await self.send({'Power': plant_power.get()}) + + if plant_temp.is_changed(): + print("Plant Temp {}".format(plant_temp.get())) + await self.send({'Temp': plant_temp.get()}) + + await asyncio.sleep(self.interval) + + +class CounterTask(ATask): + def __init__(self, interval, msg_handler: MsgIo): + ATask.__init__(self, interval) + + self.msg_handler = msg_handler + msg_handler.set_recv_handler(self.recv) + + async def recv(self, data): + print(data) + + async def send(self, data): + await self.msg_handler.send(data) + + async def on_process(self): + print("{}: Started with interval {} s".format(self.msg_handler.get_key(), self.interval)) + while True: + for count in range(0, 101): + await self.send(count) + await asyncio.sleep(self.interval) + + +class ColorHandler: + def __init__(self, msg_handler: MsgIo): + self.msg_handler = msg_handler + msg_handler.set_recv_handler(self.recv) + print("{}: Constructed".format(self.msg_handler.get_key())) + + async def recv(self, data): + print(data) + for value in data.values(): + await self.send(value) + + async def send(self, data): + await self.msg_handler.send(data) + + +class MessageDispatcher(IConnection): + def __init__(self): + self.msg_handlers: MsgIo = [] + self.state = asyncio.Queue() + + def msgio_get(self, key): + obj = MsgIo(key, self.send) + if key not in self.msg_handlers: + self.msg_handlers.append(obj) + return obj + return None + + def on_connect(self): + pass + + def on_disconnect(self): + pass + + async def on_recv(self, data): + d = json.loads(data) + for key_req in d.keys(): + for h in self.msg_handlers: + if h.can_key(key_req): + await h.on_recv(d) + + async def on_send(self): + return await self.state.get() + + async def send(self, data): + await self.state.put(data) + + +class TaskManager: + def __init__(self): + self.tasks: ATask = [] + + def add(self, task: ATask): + self.tasks.append(task) + + def start(self): + funcs = [] + for task in self.tasks: + funcs.append(task.on_process()) + + return asyncio.gather(*funcs) + + +if __name__ == '__main__': + if 0: + taskmgr = PeriodicUpdaterThreaded() + taskmgr.start("localhost", 8765) + bg_thread = Thread(target=taskmgr.loop.run_forever) + bg_thread.setDaemon(True) + bg_thread.start() + else: + dispatcher = MessageDispatcher() + server = WsServerMultiUser(listener=dispatcher) + taskmgr = TaskManager() + taskmgr.add(CounterTask(1.0, dispatcher.msgio_get("a"))) + taskmgr.add(CounterTask(0.5, dispatcher.msgio_get("b"))) + taskmgr.add(CounterTask(0.1, dispatcher.msgio_get("c"))) + + # Sensor + sensor = TempSensorSim() + taskmgr.add(TempSensorTask(sensor, 1.0, dispatcher.msgio_get("Sensor"))) + pot_params = { + "dt" : 1.0, + "theta_amb" : 15, + "C" : 4190, + "M" : 20, + "L" : 0.1, + "Td" : 12, + "kn" : 0.2 + } + # {"Heater": {"Activate": 1, "Power": 1000}} + # Plant + pot = Pot(pot_params) + # pot.connect_theta_changed(sensor.set_fake_temp) + taskmgr.add(PotTask(pot, 1.0, dispatcher.msgio_get("Pot"))) + + # Heater + heater = Heater() + heater.connect_power_changed(pot.setPower) + taskmgr.add(HeaterTask(heater, 1.0, dispatcher.msgio_get("Heater"))) + + ColorHandler(dispatcher.msgio_get("Color")) + + h_dispatcher = taskmgr.start() + h_server = server.listen("localhost", 8765) + asyncio.gather(h_dispatcher, h_server) + server.loop.run_forever() + + while True: + print("Hallo") + sleep(1) diff --git a/components/__init__.py b/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/actor/heater.py b/components/actor/heater.py new file mode 100644 index 0000000..8e3ccb4 --- /dev/null +++ b/components/actor/heater.py @@ -0,0 +1,39 @@ +from components.aheater import AHeater + + +class Heater(AHeater): + def __init__(self): + self.power = 0 + self.power_last = -1 + self.is_active = False + + self.power_changed_callback = None + + def connect_power_changed(self, func): + self.power_changed_callback = func + + def get_power_min(self): + return 500 + + def get_power_max(self): + return 3500 + + def activate(self, enable): + self.is_active = enable + + def process(self): + if self.getPower() != self.power_last: + if self.power_changed_callback is not None: + self.power_changed_callback(self.getPower()) + self.power_last = self.getPower() + + def setPower(self, power): + self.power = power + + def getPower(self): + if self.is_active: + return self.power + else: + return 0 + + diff --git a/components/actor/stirrer.py b/components/actor/stirrer.py new file mode 100644 index 0000000..e69de29 diff --git a/components/aheater.py b/components/aheater.py new file mode 100644 index 0000000..ce76dc3 --- /dev/null +++ b/components/aheater.py @@ -0,0 +1,34 @@ +import abc + + +class AHeater: + def __init__(self): + pass + + @abc.abstractmethod + def get_power_min(self): + pass + + @abc.abstractmethod + def get_power_max(self): + pass + + @abc.abstractmethod + def activate(self, enable): + return None + + @abc.abstractmethod + def process(self): + pass + + @abc.abstractmethod + def setPower(self, power): + pass + + @abc.abstractmethod + def getPower(self): + return None + + @abc.abstractmethod + def connect_power_changed(self, func): + pass diff --git a/components/aplant.py b/components/aplant.py new file mode 100644 index 0000000..0e1b4dc --- /dev/null +++ b/components/aplant.py @@ -0,0 +1,30 @@ +import abc + + +class APlant: + def __init__(self, params): + pass + + @abc.abstractmethod + def activate(self, enable): + return None + + @abc.abstractmethod + def process(self): + pass + + @abc.abstractmethod + def setPower(self, power_W): + pass + + @abc.abstractmethod + def getPower(self): + return None + + @abc.abstractmethod + def getTemperature(self): + return None + + @abc.abstractmethod + def connect_theta_changed(self, func): + pass diff --git a/components/astirrer.py b/components/astirrer.py new file mode 100644 index 0000000..00c36cd --- /dev/null +++ b/components/astirrer.py @@ -0,0 +1,62 @@ +import abc +import logging + + +class AStirrer: + @abc.abstractmethod + def name(self): + return "" + + def __init__(self, dt): + self.dt = dt + self.speed = 0 + self.cycleTime = 1 + self.dutyCycle = 1 + self.cycleCounter = 0 + self.isOn = 1 + + def log(self, s): + d = {'user': "Stirrer" + "::" + self.name()} + logging.info ("{}".format(s), extra=d) + + def process(self): + isOn = self.isOn + self.cycleCounter += self.dt + if self.cycleCounter >= self.cycleTime: + self.cycleCounter -= self.cycleTime + isOn = 0 + + if self.cycleCounter >= (self.cycleTime * (1-self.dutyCycle)): + if not isOn: + isOn = 1 + + if self.isOn != isOn: + if isOn: + self.onSetSpeed(self.speed) + else: + self.onSetSpeed(0) + + self.isOn = isOn + + def setSpeed(self, speed): + self.speed = speed + if self.isOn or self.dutyCycle == 1: + self.onSetSpeed(speed) + + def setCycleTime(self, time): + self.cycleTime = time + + def setDutyCycle(self, dutyCycle): + self.dutyCycle = dutyCycle + + @abc.abstractmethod + def getSpeed(self): + return None + + @abc.abstractmethod + def activate(self): + pass + + @abc.abstractmethod + def deactivate(self): + pass diff --git a/components/atemperatureSensor.py b/components/atemperatureSensor.py new file mode 100644 index 0000000..7e24bed --- /dev/null +++ b/components/atemperatureSensor.py @@ -0,0 +1,18 @@ +import abc +import logging + + +class ATemperatureSensor: + def log(self, s): + d = {'user': "TemperatureSensor" + "::" + self.name()} + logging.info ("{}".format(s), extra=d) + + @abc.abstractmethod + def name(self): + return "" + + @abc.abstractmethod + def temperature(self): + return None + + diff --git a/components/pid/temp_controller.py b/components/pid/temp_controller.py new file mode 100644 index 0000000..e69de29 diff --git a/components/plant/pot.py b/components/plant/pot.py new file mode 100644 index 0000000..99e72df --- /dev/null +++ b/components/plant/pot.py @@ -0,0 +1,59 @@ +import numpy as np +import json +from components.aplant import APlant + + +class Pot(APlant): + def __init__(self, params): + print(json.dumps({'Pot': params}, indent=4, sort_keys=True)) + self.dt = params['dt'] + self.alpha = 1.0 + + self.e = 0 + self.x = 0 + self.gain = 0.999 + + self.C = params['C'] + self.M = params['M'] + self.L = params['L'] + self.Td = params['Td'] + self.kn = params['kn'] + self.theta_amb = params['theta_amb'] + + self.theta = 0 + + self.alpha = self.dt / 1 + self.alpha_delay = self.dt/self.Td + + self.theta_changed_callback = None + self.power_set = 0 + self.power_actual = 0 + + def connect_theta_changed(self, func): + self.theta_changed_callback = func + + def activate(self, enable): + pass + + def process(self): + # Delay + self.power_actual = (1-self.alpha_delay) * self.power_actual + self.alpha_delay * self.power_set + + self.e = self.e * (1 - ((self.L * self.theta) * self.dt) / (self.M * self.C)) + self.x = (1 - self.alpha) * self.x + self.gain * self.alpha * self.power_actual + self.e += self.x + theta = self.e / (self.M * self.C) + + if self.theta_changed_callback is not None: + self.theta_changed_callback(self.getTemperature()) + + self.theta = theta + + def setPower(self, power): + self.power_set = power + + def getPower(self): + return round(self.power_actual, 1) + + def getTemperature(self): + return round(self.theta + self.theta_amb + self.kn * np.random.normal(0, 1) / np.sqrt(12.0), 1) diff --git a/components/sensor/__init__.py b/components/sensor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/sensor/tempSensorSim.py b/components/sensor/tempSensorSim.py new file mode 100644 index 0000000..5429477 --- /dev/null +++ b/components/sensor/tempSensorSim.py @@ -0,0 +1,21 @@ +from components.atemperatureSensor import ATemperatureSensor +import math + + +class TempSensorSim(ATemperatureSensor): + def name(self): + return "FakeTemp" + + def __init__(self): + self.count = 0 + self.freq = 0.2 + self.log("Created") + self.temp = 22.37 + + def set_fake_temp(self, temp): + self.temp = temp + + def temperature(self): + self.count += 1 + return self.temp + 0.0*math.sin(2*math.pi*self.count*self.freq) +