Restructure into brewpi/client/ws packages and add README
Splits the server, GUI client, and websocket layer into their own packages with dedicated requirements.txt files, and adds a README documenting the project's architecture and usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+3774
File diff suppressed because it is too large
Load Diff
Executable
+174
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
from brewpi_win import Ui_MainWindow
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
import sys
|
||||
from ws.client.ws_client import WsClient
|
||||
from ws.message import MessageDispatcherSync
|
||||
from queue import Queue
|
||||
import asyncio
|
||||
|
||||
|
||||
class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
def __init__(self):
|
||||
QtWidgets.QMainWindow.__init__(self)
|
||||
self.setWindowIcon(QtGui.QIcon('res/beer.png'))
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self.msg_dispatch = MessageDispatcherSync(auto_subscribe=True)
|
||||
self.ws_client = WsClient(listener=self.msg_dispatch, loop=loop)
|
||||
self.msg_pot = self.msg_dispatch.msgio_get('Pot')
|
||||
self.msg_sensor = self.msg_dispatch.msgio_get('Sensor')
|
||||
self.msg_heater = self.msg_dispatch.msgio_get('Heater')
|
||||
self.msg_stirrer = self.msg_dispatch.msgio_get('Stirrer')
|
||||
self.msg_tempctrl = self.msg_dispatch.msgio_get('TempCtrl')
|
||||
|
||||
self.msg_pot.set_recv_handler(self.on_pot_changed)
|
||||
self.msg_sensor.set_recv_handler(self.on_sensor_changed)
|
||||
self.msg_heater.set_recv_handler(self.on_heater_changed)
|
||||
self.msg_stirrer.set_recv_handler(self.on_stirrer_changed)
|
||||
self.msg_tempctrl.set_recv_handler(self.on_tempctrl_changed)
|
||||
|
||||
self.setupUi(self)
|
||||
self.actionStart.triggered.connect(self.connect)
|
||||
self.actionStop.triggered.connect(self.disconnect)
|
||||
self.Slider_pwr_soll.valueChanged.connect(self.on_slider_pwr_soll_changed)
|
||||
self.Slider_temp_soll.valueChanged.connect(self.on_slider_temp_soll_changed)
|
||||
self.Slider_speed_soll.valueChanged.connect(self.on_slider_stirrer_speed_soll_changed)
|
||||
|
||||
self.doubleSpinBox_heatrate_soll.valueChanged.connect(self.on_heatrate_soll_changed)
|
||||
|
||||
self.checkbox_heater_activate.stateChanged.connect(self.on_checkbox_changed)
|
||||
self.checkbox_stirrer_activate.stateChanged.connect(self.on_checkbox_stirrer_activate_changed)
|
||||
|
||||
self.send_data = Queue()
|
||||
self.slider_pwr_initial_update = True
|
||||
self.slider_temp_soll_initial_update = True
|
||||
self.slider_speed_initial_update = True
|
||||
self.checkbox_heater_activate_initial_update = True
|
||||
self.checkbox_stirrer_activate_initial_update = True
|
||||
self.heatrate_soll_initial_update = True
|
||||
|
||||
def connect(self):
|
||||
self.slider_pwr_initial_update = True
|
||||
self.slider_temp_soll_initial_update = True
|
||||
self.slider_speed_initial_update = True
|
||||
self.checkbox_heater_activate_initial_update = True
|
||||
self.checkbox_stirrer_activate_initial_update = True
|
||||
self.heatrate_soll_initial_update = True
|
||||
self.ws_client.connect(uri=self.plainTextUri.toPlainText())
|
||||
|
||||
def disconnect(self):
|
||||
self.ws_client.disconnect()
|
||||
|
||||
def on_slider_temp_soll_changed(self, value):
|
||||
print("on_slider_temp_soll_changed {}".format(value))
|
||||
self.msg_tempctrl.send({'Soll': {'Temp': value}})
|
||||
|
||||
def on_heatrate_soll_changed(self, value):
|
||||
value = round(value, 1)
|
||||
print("on_textbox_heatrate_soll_changed {}".format(value))
|
||||
self.msg_tempctrl.send({'Soll': {'Rate': value}})
|
||||
|
||||
def on_slider_pwr_soll_changed(self, value):
|
||||
print("on_slider_pwr_soll_changed {}".format(value))
|
||||
self.msg_heater.send({'Power': value})
|
||||
|
||||
def on_checkbox_changed(self, value):
|
||||
print("on_checkbox_changed {}".format(value))
|
||||
self.msg_heater.send({'Activate': int(value == 2)})
|
||||
|
||||
def on_slider_stirrer_speed_soll_changed(self, value):
|
||||
print("on_slider_stirrer_speed_soll_changed {}".format(value))
|
||||
self.msg_stirrer.send({'Speed': value})
|
||||
|
||||
def on_checkbox_stirrer_activate_changed(self, value):
|
||||
print("on_checkbox_stirrer_activate_changed {}".format(value))
|
||||
self.msg_stirrer.send({'Activate': int(value == 2)})
|
||||
|
||||
def on_pot_changed(self, msg):
|
||||
print("on_pot_changed {}".format(msg))
|
||||
for key in msg:
|
||||
if "Power" in key:
|
||||
self.lcdNumber_power_pot.display(msg['Power'])
|
||||
|
||||
def on_sensor_changed(self, msg):
|
||||
print("on_sensor_changed {}".format(msg))
|
||||
|
||||
def on_tempctrl_changed(self, msg):
|
||||
print("on_tempctrl_changed {}".format(msg))
|
||||
for key in msg:
|
||||
if 'State' in key:
|
||||
self.label_state.setText(msg['State'])
|
||||
elif "Soll" in key:
|
||||
submsg = msg['Soll']
|
||||
if 'Temp' in submsg:
|
||||
self.lcdNumber_temp_soll.display(str(submsg['Temp']))
|
||||
if self.slider_temp_soll_initial_update:
|
||||
self.Slider_temp_soll.setValue(submsg['Temp'])
|
||||
self.slider_temp_soll_initial_update = False
|
||||
if 'Rate' in submsg:
|
||||
subsubmsg = submsg['Rate']
|
||||
for subkey in subsubmsg:
|
||||
if "Current" in subkey:
|
||||
self.lcdNumber_heatrate_soll.display(str(subsubmsg['Current']))
|
||||
if "Set" in subkey:
|
||||
if self.heatrate_soll_initial_update:
|
||||
self.heatrate_soll_initial_update = False
|
||||
self.doubleSpinBox_heatrate_soll.setValue(subsubmsg['Set'])
|
||||
if "Ist" in key:
|
||||
submsg = msg['Ist']
|
||||
if 'Temp' in submsg:
|
||||
self.lcdNumber_temp_ist.display(str(submsg['Temp']))
|
||||
if 'Rate' in submsg:
|
||||
self.lcdNumber_heatrate_ist.display(str(submsg['Rate']))
|
||||
|
||||
def on_heater_changed(self, msg):
|
||||
print("on_heater_changed {}".format(msg))
|
||||
for key in msg:
|
||||
if "Activate" in key:
|
||||
if self.checkbox_heater_activate_initial_update:
|
||||
self.checkbox_heater_activate.setCheckState(2 if msg['Activate'] == 1 else 0)
|
||||
self.checkbox_heater_activate_initial_update = False
|
||||
elif "Power" in key:
|
||||
self.lcdNumber_power_heater.display(msg['Power'])
|
||||
if self.slider_pwr_initial_update:
|
||||
self.Slider_pwr_soll.setValue(msg['Power'])
|
||||
self.slider_pwr_initial_update = False
|
||||
elif "Capabilities" in key:
|
||||
submsg = msg['Capabilities']
|
||||
if "Power" in submsg:
|
||||
submsg = submsg['Power']
|
||||
self.Slider_pwr_soll.setLowerBound(submsg['Min'])
|
||||
self.Slider_pwr_soll.setUpperBound(submsg['Max'])
|
||||
|
||||
def on_stirrer_changed(self, msg):
|
||||
print("on_stirrer_changed {}".format(msg))
|
||||
for key in msg:
|
||||
if "Activate" in key:
|
||||
if self.checkbox_stirrer_activate_initial_update:
|
||||
self.checkbox_stirrer_activate.setCheckState(2 if msg['Activate'] == 1 else 0)
|
||||
self.checkbox_stirrer_activate_initial_update = False
|
||||
elif "Speed" in key:
|
||||
if self.slider_speed_initial_update:
|
||||
self.Slider_speed_soll.setValue(msg['Speed'])
|
||||
self.slider_speed_initial_update = False
|
||||
elif "Capabilities" in key:
|
||||
submsg = msg['Capabilities']
|
||||
if "Power" in submsg:
|
||||
submsg = submsg['Power']
|
||||
self.Slider_speed_soll.setLowerBound(submsg['Min'])
|
||||
self.Slider_speed_soll.setUpperBound(submsg['Max'])
|
||||
|
||||
def closeEvent(self, event):
|
||||
print("Exitting gracefully!")
|
||||
self.ws_client.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
win = Window()
|
||||
win.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
print ("End of program.")
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
source $WORKON_HOME/brewpi/bin/activate
|
||||
$BREWPI_HOME/brewpi_gui.py
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
pyqt5
|
||||
@@ -0,0 +1,251 @@
|
||||
# This is a sample Python script.
|
||||
import asyncio
|
||||
import sys
|
||||
from PyQt5 import QtGui, QtWidgets, QtCore
|
||||
from ws.client.ws_client import WsClient
|
||||
from ws.connection import IConnection
|
||||
import json
|
||||
from queue import Queue
|
||||
|
||||
|
||||
class Window(QtWidgets.QMainWindow, IConnection):
|
||||
|
||||
# Use signals and slots to avoid crashes
|
||||
# during updating gui elements from websocket context
|
||||
upd_progressbar_signal = QtCore.pyqtSignal(int, int)
|
||||
upd_color_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self):
|
||||
QtWidgets.QMainWindow.__init__(self)
|
||||
self.ws_client = WsClient(self)
|
||||
self.is_exit_gracefully = True
|
||||
self.textEdit = None
|
||||
self.progressbar = []
|
||||
self.styleChoice = None
|
||||
|
||||
self.setGeometry(50, 50, 500, 300)
|
||||
self.setWindowTitle("Hallo Mausi!")
|
||||
self.setWindowIcon(QtGui.QIcon('res/flasche.png'))
|
||||
|
||||
self.menubar_create()
|
||||
self.statusbar_create()
|
||||
self.toolbar_create()
|
||||
self.buttons_create()
|
||||
self.checkbox_create()
|
||||
|
||||
self.progressbar_create()
|
||||
self.style_chooser_create()
|
||||
self.calendar_create()
|
||||
|
||||
self.send_data = Queue()
|
||||
self.show()
|
||||
|
||||
self.upd_progressbar_signal.connect(self.upd_progressbar_slot)
|
||||
self.upd_color_signal.connect(self.upd_color_slot)
|
||||
|
||||
def calendar_create(self):
|
||||
cal = QtWidgets.QCalendarWidget(self)
|
||||
cal.move(500, 200)
|
||||
cal.resize(250, 250)
|
||||
|
||||
def menubar_create(self):
|
||||
mainMenu = self.menuBar()
|
||||
fileMenu = mainMenu.addMenu("&File")
|
||||
|
||||
actionExit = QtWidgets.QAction("&Exit", self)
|
||||
actionExit.setShortcut("Ctrl+Q")
|
||||
actionExit.setStatusTip("Leave the App")
|
||||
actionExit.triggered.connect(self.on_btn_exit_clicked)
|
||||
|
||||
actionEditor = QtWidgets.QAction("&Editor", self)
|
||||
actionEditor.setShortcut("Ctrl+E")
|
||||
actionEditor.setStatusTip("Open Editor")
|
||||
actionEditor.triggered.connect(self.on_action_editor)
|
||||
|
||||
actionFileOpener = QtWidgets.QAction("File &Open", self)
|
||||
actionFileOpener.setShortcut("Ctrl+O")
|
||||
actionFileOpener.setStatusTip("Open a file")
|
||||
actionFileOpener.triggered.connect(self.on_action_file_open)
|
||||
|
||||
actionFileSaver = QtWidgets.QAction("File &Save", self)
|
||||
actionFileSaver.setShortcut("Ctrl+S")
|
||||
actionFileSaver.setStatusTip("Save a file")
|
||||
actionFileSaver.triggered.connect(self.on_action_file_save)
|
||||
|
||||
fileMenu.addAction(actionExit)
|
||||
fileMenu.addAction(actionEditor)
|
||||
fileMenu.addAction(actionFileOpener)
|
||||
fileMenu.addAction(actionFileSaver)
|
||||
|
||||
def statusbar_create(self):
|
||||
statusBar = self.statusBar()
|
||||
|
||||
def toolbar_create(self):
|
||||
def action_caller(a, n):
|
||||
return lambda action: self.on_action_triggered(a, n)
|
||||
|
||||
action1 = QtWidgets.QAction(QtGui.QIcon('res/glass.png'), "Drink a beer!", self)
|
||||
action1.triggered.connect(action_caller(action1, 1))
|
||||
|
||||
action2 = QtWidgets.QAction(QtGui.QIcon('res/glass2.png'), "Drink more beer!", self)
|
||||
action2.triggered.connect(action_caller(action2, 2))
|
||||
|
||||
action3 = QtWidgets.QAction(QtGui.QIcon('res/glass_flasche.png'), "Choose font!", self)
|
||||
action3.triggered.connect(self.on_action_font_triggered)
|
||||
|
||||
action4 = QtWidgets.QAction(QtGui.QIcon('res/dose.png'), "Choose color!", self)
|
||||
action4.triggered.connect(self.on_action_color_triggered)
|
||||
|
||||
toolbar = self.addToolBar("Toolbar")
|
||||
|
||||
toolbar.addAction(action1)
|
||||
toolbar.addAction(action2)
|
||||
toolbar.addAction(action3)
|
||||
toolbar.addAction(action4)
|
||||
|
||||
def buttons_create(self):
|
||||
btn = QtWidgets.QPushButton("Quit", self)
|
||||
btn.clicked.connect(self.on_btn_exit_clicked)
|
||||
btn.resize(btn.minimumSizeHint())
|
||||
btn.move(0, 100)
|
||||
|
||||
def checkbox_create(self):
|
||||
checkbox = QtWidgets.QCheckBox('Exit gracefully!', self)
|
||||
checkbox.stateChanged.connect(self.on_checkbox_changed)
|
||||
checkbox.setCheckState(2 if self.is_exit_gracefully else 0)
|
||||
checkbox.move(0, 150)
|
||||
|
||||
def progressbar_create(self):
|
||||
for i in range(0, 3):
|
||||
progressbar = QtWidgets.QProgressBar(self)
|
||||
progressbar.setGeometry(200, 80+25*i, 250, 20)
|
||||
self.progressbar.append(progressbar)
|
||||
|
||||
btn = QtWidgets.QPushButton("Download", self)
|
||||
btn.clicked.connect(self.on_btn_download_clicked)
|
||||
btn.move(0, 70)
|
||||
|
||||
def style_chooser_create(self):
|
||||
print(self.style().objectName())
|
||||
self.styleChoice = QtWidgets.QLabel("Linux", self)
|
||||
comboxBox = QtWidgets.QComboBox(self)
|
||||
comboxBox.addItem("motif")
|
||||
comboxBox.addItem("Windows")
|
||||
comboxBox.addItem("cde")
|
||||
comboxBox.addItem("Plastique")
|
||||
comboxBox.addItem("Cleanlooks")
|
||||
comboxBox.addItem("windowsvista")
|
||||
comboxBox.addItem("fusion")
|
||||
comboxBox.move(50, 250)
|
||||
self.styleChoice.move(150, 150)
|
||||
comboxBox.activated[str].connect(self.on_combobox_changed)
|
||||
|
||||
def on_btn_exit_clicked(self):
|
||||
print('PyQt5 on_btn_click')
|
||||
self.close()
|
||||
|
||||
def on_action_triggered(self, action, n):
|
||||
print('PyQt5 on_action_triggered {}'.format(n))
|
||||
if n == 1:
|
||||
self.ws_client.connect(uri="ws://localhost:8765")
|
||||
if n == 2:
|
||||
self.ws_client.disconnect()
|
||||
|
||||
def on_action_editor(self):
|
||||
self.textEdit = QtWidgets.QTextEdit()
|
||||
self.setCentralWidget(self.textEdit)
|
||||
|
||||
def on_action_file_open(self):
|
||||
name, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')
|
||||
file = open(name, 'r')
|
||||
|
||||
self.on_action_editor()
|
||||
with file:
|
||||
text = file.read()
|
||||
self.textEdit.setText(text)
|
||||
|
||||
def on_action_file_save(self):
|
||||
|
||||
if self.textEdit is not None:
|
||||
name, _ = QtWidgets.QFileDialog.getSaveFileName(self, 'Save file')
|
||||
file = open(name, 'w')
|
||||
text = self.textEdit.toPlainText()
|
||||
file.write(text)
|
||||
file.close()
|
||||
else:
|
||||
print("No file to save!")
|
||||
|
||||
def on_action_font_triggered(self):
|
||||
font, valid = QtWidgets.QFontDialog.getFont()
|
||||
if valid:
|
||||
self.styleChoice.setFont(font)
|
||||
|
||||
def on_action_color_triggered(self):
|
||||
color = QtWidgets.QColorDialog.getColor()
|
||||
self.send_data.put({'Color': color.name()})
|
||||
|
||||
def on_checkbox_changed(self, state):
|
||||
print('PyQt5 on_checkbox_changed, state={}'.format(state))
|
||||
if state == QtCore.Qt.Checked:
|
||||
self.is_exit_gracefully = True
|
||||
else:
|
||||
self.is_exit_gracefully = False
|
||||
|
||||
def on_btn_download_clicked(self):
|
||||
print("Nothing happens!")
|
||||
|
||||
def on_combobox_changed(self, text):
|
||||
self.styleChoice.setText(text)
|
||||
QtWidgets.QApplication.setStyle(QtWidgets.QStyleFactory.create(text))
|
||||
|
||||
def on_connect(self):
|
||||
self.send_data.put_nowait({'+':'a'})
|
||||
self.send_data.put_nowait({'+':'b'})
|
||||
self.send_data.put_nowait({'+':'c'})
|
||||
self.send_data.put_nowait({'+':'Color'})
|
||||
|
||||
def on_disconnect(self):
|
||||
pass
|
||||
|
||||
@QtCore.pyqtSlot(int, int)
|
||||
def upd_progressbar_slot(self, n, v):
|
||||
self.progressbar[n].setValue(v)
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def upd_color_slot(self, color_str):
|
||||
self.styleChoice.setStyleSheet("QWidget {background-color: %s}" % color_str)
|
||||
|
||||
async def on_recv(self, data):
|
||||
try:
|
||||
d = json.loads(data)
|
||||
keys = {'a': 0, 'b': 1, 'c': 2}
|
||||
color = {'Color': 0}
|
||||
for cmd in d:
|
||||
if cmd in keys.keys():
|
||||
await asyncio.get_event_loop().run_in_executor(None, self.upd_progressbar_signal.emit(keys[cmd], int(d[cmd] + 0.5)))
|
||||
|
||||
if cmd in color.keys():
|
||||
await asyncio.get_event_loop().run_in_executor(None, self.upd_color_signal.emit(d[cmd]))
|
||||
except:
|
||||
pass
|
||||
|
||||
async def on_send(self):
|
||||
try:
|
||||
data = await asyncio.get_event_loop().run_in_executor(None, self.send_data.get, True, 1)
|
||||
return data
|
||||
except:
|
||||
return None
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self.is_exit_gracefully:
|
||||
print("Exitting gracefully!")
|
||||
self.ws_client.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
gui = Window()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
|
||||
Reference in New Issue
Block a user