mirror of
https://github.com/janishutz/BiogasControllerApp.git
synced 2025-11-25 13:54:24 +00:00
Restructure for better usability
This commit is contained in:
3
gui/PopupManager.py
Normal file
3
gui/PopupManager.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from gui.popups.popups import *
|
||||
|
||||
|
||||
37
gui/about/about.kv
Normal file
37
gui/about/about.kv
Normal file
@@ -0,0 +1,37 @@
|
||||
<AboutScreen>:
|
||||
name: "about"
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
GridLayout:
|
||||
cols: 1
|
||||
Label:
|
||||
text: "About"
|
||||
font_size: 40
|
||||
color: (0, 113, 0, 1)
|
||||
bold: True
|
||||
FloatLayout:
|
||||
GridLayout:
|
||||
pos_hint: {"x":0.05, "y":0.05}
|
||||
size_hint: 0.9, 0.9
|
||||
cols: 3
|
||||
Button:
|
||||
text: "Back"
|
||||
background_color: (255,0,0,0.6)
|
||||
on_release:
|
||||
app.root.current = "home"
|
||||
root.manager.transition.direction = "up"
|
||||
Button:
|
||||
text: "Report a\nBug"
|
||||
background_color: (255,0,0,0.6)
|
||||
on_release:
|
||||
root.report_issue()
|
||||
Button:
|
||||
text: "Credits"
|
||||
background_color: (255,0,0,0.6)
|
||||
on_release:
|
||||
app.root.current = "credits"
|
||||
root.manager.transition.direction = "left"
|
||||
13
gui/about/about.py
Normal file
13
gui/about/about.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from kivy.lang import Builder
|
||||
import webbrowser
|
||||
|
||||
from gui.popups.popups import SingleRowPopup
|
||||
|
||||
|
||||
class AboutScreen(Screen):
|
||||
def report_issue(self):
|
||||
SingleRowPopup().open("Opened your web-browser")
|
||||
webbrowser.open('https://github.com/janishutz/BiogasControllerApp/issues', new=2)
|
||||
|
||||
Builder.load_file('./gui/about/about.kv')
|
||||
27
gui/credits/credits.kv
Normal file
27
gui/credits/credits.kv
Normal file
@@ -0,0 +1,27 @@
|
||||
<CreditsScreen>:
|
||||
name: "credits"
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
FloatLayout:
|
||||
Button:
|
||||
text: "back"
|
||||
size_hint: 0.4, 0.2
|
||||
pos_hint: {"x":0.3, "y":0.1}
|
||||
on_release:
|
||||
app.root.current = "about"
|
||||
root.manager.transition.direction = "right"
|
||||
GridLayout:
|
||||
cols:1
|
||||
pos_hint:{"x":0.05, "y":0.35}
|
||||
size_hint: 0.9, 0.5
|
||||
Label:
|
||||
text: "This is a controller sofware that helps you reprogram and monitor the micro-controller used in ENATECH at KSWO"
|
||||
Label:
|
||||
text: "Written by: Janis Hutz\nDesigned by: Janis Hutz\nDesign language: Kivy"
|
||||
Label:
|
||||
text: "This software is free Software licensed under the GPL V3 (GNU General Public License) and as such comes with absolutely no warranty. In return, you can use, modify, distribute or use any of the code of this software in your own project, if you reuse the same license. For more infos, you can find a copy of this license in the project folder."
|
||||
text_size: self.width, None
|
||||
8
gui/credits/credits.py
Normal file
8
gui/credits/credits.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from kivy.lang import Builder
|
||||
|
||||
|
||||
class CreditsScreen(Screen):
|
||||
pass
|
||||
|
||||
Builder.load_file('./gui/credits/credits.kv')
|
||||
45
gui/home/home.kv
Normal file
45
gui/home/home.kv
Normal file
@@ -0,0 +1,45 @@
|
||||
<HomeScreen>:
|
||||
name: "home"
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
GridLayout:
|
||||
cols:1
|
||||
Label:
|
||||
text: "BiogasanlageControllerApp"
|
||||
font_size: 50
|
||||
color: (0, 113, 0, 1)
|
||||
bold:True
|
||||
italic:True
|
||||
FloatLayout:
|
||||
GridLayout:
|
||||
cols: 2
|
||||
size_hint: 0.8, 0.8
|
||||
pos_hint: {"x": 0.1, "y": 0.1}
|
||||
Button:
|
||||
text: "Start"
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
font_size: 30
|
||||
on_release:
|
||||
root.start()
|
||||
Button:
|
||||
text: "Quit"
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
font_size: 30
|
||||
on_release:
|
||||
root.quit()
|
||||
Label:
|
||||
text: "You are running version V3.0.0"
|
||||
font_size: 13
|
||||
pos_hint: {"y": -0.45, "x":0.05}
|
||||
Button:
|
||||
text: "About"
|
||||
font_size: 13
|
||||
size_hint: 0.07, 0.06
|
||||
pos_hint: {"x":0.01, "y":0.01}
|
||||
background_color: (50, 0, 0, 0.2)
|
||||
on_release:
|
||||
root.to_about()
|
||||
71
gui/home/home.py
Normal file
71
gui/home/home.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from kivy.lang import Builder
|
||||
from gui.popups.popups import DualRowPopup, QuitPopup, TwoActionPopup
|
||||
from lib.com import ComSuperClass
|
||||
import platform
|
||||
|
||||
|
||||
# Information for errors encountered when using pyserial
|
||||
information = {
|
||||
"Windows": {
|
||||
"2": "Un- and replug the cable and ensure you have the required driver(s) installed",
|
||||
"13": "You are probably missing a required driver or your cable doesn't work. Consult the wiki for more information",
|
||||
"NO_COM": "Could not find a microcontroller. Please ensure you have one connected and the required driver(s) installed"
|
||||
},
|
||||
"Linux": {
|
||||
"2": "Un- and replug the cable, or if you haven't plugged a controller in yet, do that",
|
||||
"13": "Incorrect permissions at /dev/ttyUSB0. Open a terminal and type: sudo chmod 777 /dev/ttyUSB0",
|
||||
"NO_COM": "Could not find a microcontroller. Please ensure you have one connected"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# This is the launch screen, i.e. what you see when you start up the app
|
||||
class HomeScreen(Screen):
|
||||
def __init__(self, com: ComSuperClass, **kw):
|
||||
self._com = com;
|
||||
super().__init__(**kw)
|
||||
|
||||
# Go to the main screen if we can establish connection or the check was disabled
|
||||
# in the configs
|
||||
def start(self):
|
||||
if self._com.connect():
|
||||
self.manager.current = 'main'
|
||||
self.manager.transition.direction = 'right'
|
||||
else:
|
||||
TwoActionPopup().open('Failed to connect', 'Details', self.open_details_popup)
|
||||
print('ERROR connecting')
|
||||
|
||||
# Open popup for details as to why the connection failed
|
||||
def open_details_popup(self):
|
||||
DualRowPopup().open("Troubleshooting tips", self._generate_help())
|
||||
|
||||
def _generate_help(self) -> str:
|
||||
operating_system = platform.system()
|
||||
if operating_system == "Windows" or operating_system == "Linux":
|
||||
port = self._com.get_comport();
|
||||
information["Linux"]["13"] = f"Incorrect permissions at {port}. Resolve by running 'sudo chmod 777 {port}'"
|
||||
if port == "":
|
||||
return information[operating_system]["NO_COM"]
|
||||
err = self._com.get_error()
|
||||
if err != None:
|
||||
return information[operating_system][str(err.errno)]
|
||||
else:
|
||||
return "No error message available"
|
||||
else:
|
||||
return "You are running on an unsupported Operating System. No help available"
|
||||
|
||||
# Helper to open a Popup to ask user whether to quit or not
|
||||
def quit(self):
|
||||
QuitPopup(self._com).open()
|
||||
|
||||
# Switch to about screen
|
||||
def to_about(self):
|
||||
self.manager.current = 'about'
|
||||
self.manager.transition.direction = 'down'
|
||||
|
||||
|
||||
# Load the design file for this screen (.kv files)
|
||||
# The path has to be relative to root of the app, i.e. where the biogascontrollerapp.py
|
||||
# file is located
|
||||
Builder.load_file('./gui/home/home.kv')
|
||||
89
gui/main/main.kv
Normal file
89
gui/main/main.kv
Normal file
@@ -0,0 +1,89 @@
|
||||
<MainScreen>:
|
||||
on_pre_enter: root.reset()
|
||||
name: "main"
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
GridLayout:
|
||||
FloatLayout:
|
||||
Label:
|
||||
pos_hint: {"y":0.4}
|
||||
text: "READOUT"
|
||||
font_size: 40
|
||||
color: (0, 113, 0, 1)
|
||||
bold: True
|
||||
GridLayout:
|
||||
cols:4
|
||||
size_hint: 0.8, 0.3
|
||||
pos_hint: {"x":0.1, "y":0.4}
|
||||
Label:
|
||||
text: "Sensor 1: "
|
||||
font_size: 20
|
||||
Label:
|
||||
id: sensor1
|
||||
text: ""
|
||||
Label:
|
||||
text: "Sensor 2: "
|
||||
font_size: 20
|
||||
Label:
|
||||
id: sensor2
|
||||
text: ""
|
||||
Label:
|
||||
text: "Sensor 3: "
|
||||
font_size: 20
|
||||
Label:
|
||||
id: sensor3
|
||||
text: ""
|
||||
Label:
|
||||
text: "Sensor 4: "
|
||||
font_size: 20
|
||||
Label:
|
||||
id: sensor4
|
||||
text: ""
|
||||
Button:
|
||||
text: "Connect"
|
||||
size_hint: 0.2, 0.1
|
||||
pos_hint: {"x": 0.5, "y": 0.05}
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
on_release:
|
||||
root.start()
|
||||
Button:
|
||||
text: "Disconnect"
|
||||
size_hint: 0.2, 0.1
|
||||
pos_hint: {"x": 0.7, "y": 0.05}
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
on_release:
|
||||
root.end()
|
||||
Button:
|
||||
text: "Back"
|
||||
size_hint: 0.3, 0.1
|
||||
pos_hint: {"x":0.05, "y":0.05}
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
on_release:
|
||||
root.end()
|
||||
app.root.current = "home"
|
||||
root.manager.transition.direction = "left"
|
||||
ToggleButton:
|
||||
id: mode_selector
|
||||
size_hint: 0.15, 0.1
|
||||
pos_hint: {"x":0.1, "y":0.2}
|
||||
text: "Normal Mode" if self.state == "normal" else "Fast Mode"
|
||||
on_text: root.switch_mode(mode_selector.text)
|
||||
background_color: (255,0,0,0.6) if self.state == "normal" else (0,0,255,0.6)
|
||||
Button:
|
||||
text: "Configuration"
|
||||
size_hint: 0.15, 0.1
|
||||
pos_hint: {"x":0.7, "y":0.2}
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
on_release:
|
||||
root.end()
|
||||
app.root.current = "program"
|
||||
root.manager.transition.direction = "down"
|
||||
Label:
|
||||
id: status
|
||||
text: "Status will appear here"
|
||||
font_size: 10
|
||||
pos_hint: {"x":0.4, "y": 0.3}
|
||||
212
gui/main/main.py
Normal file
212
gui/main/main.py
Normal file
@@ -0,0 +1,212 @@
|
||||
from ctypes import ArgumentError
|
||||
from time import time
|
||||
from typing import List, override
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from kivy.lang import Builder
|
||||
from gui.popups.popups import SingleRowPopup, TwoActionPopup, empty_func
|
||||
from kivy.clock import Clock, ClockEvent
|
||||
import queue
|
||||
import threading
|
||||
|
||||
# Load utilities
|
||||
from lib.instructions import Instructions
|
||||
from lib.com import ComSuperClass
|
||||
from lib.decoder import Decoder
|
||||
|
||||
|
||||
# TODO: Consider consolidating start and stop button
|
||||
|
||||
|
||||
# Queue with data that is used to synchronize
|
||||
synced_queue: queue.Queue[List[str]] = queue.Queue()
|
||||
|
||||
|
||||
# ╭────────────────────────────────────────────────╮
|
||||
# │ Data Reading Thread Helper │
|
||||
# ╰────────────────────────────────────────────────╯
|
||||
# Using a Thread to run this in parallel to the UI to improve responsiveness
|
||||
class ReaderThread(threading.Thread):
|
||||
_com: ComSuperClass
|
||||
_decoder: Decoder
|
||||
_instructions: Instructions
|
||||
|
||||
# This method allows the user to set Com object to be used.
|
||||
# The point of this is to allow for the use of a single Com object to not waste resources
|
||||
def set_com(self, com: ComSuperClass):
|
||||
"""Set the Com object to be used in this
|
||||
|
||||
Args:
|
||||
com: The com object to be used
|
||||
"""
|
||||
self._com = com
|
||||
self._run = True
|
||||
self._decoder = Decoder()
|
||||
self._instructions = Instructions(com)
|
||||
|
||||
# This method is given by the Thread class and has to be overriden to change
|
||||
# what is executed when the thread starts
|
||||
@override
|
||||
def run(self) -> None:
|
||||
self._run = True
|
||||
if self._com == None:
|
||||
raise ArgumentError("Com object not passed in (do using set_com)")
|
||||
# Hook to output stream
|
||||
if self._instructions.hook_main():
|
||||
# We are now hooked to the stream (i.e. data is synced)
|
||||
synced_queue.put(["HOOK"])
|
||||
|
||||
# making it exit using the stop function
|
||||
while self._run:
|
||||
# Take note of the time before reading the data to deduce frequency of updates
|
||||
start_time = time()
|
||||
|
||||
# We need to read 68 bytes of data, given by the program running on the controller
|
||||
received = self._com.receive(68)
|
||||
|
||||
# Store the data in a list of strings
|
||||
data: List[str] = []
|
||||
|
||||
# For all sensors connected, execute the same thing
|
||||
for i in range(4):
|
||||
# The slicing that happens here uses offsets automatically calculated from the sensor id
|
||||
# This allows for short code
|
||||
data.append(
|
||||
f"Tadc: {
|
||||
self._decoder.decode_int(received[12 * i:12 * i + 4])
|
||||
}\nTemperature: {
|
||||
self._decoder.decode_float(received[12 * i + 5:12 * i + 11])
|
||||
}\nDuty-Cycle: {
|
||||
self._decoder.decode_float_long(received[48 + 5 * i: 52 + 5 * i]) / 65535.0 * 100
|
||||
}%"
|
||||
)
|
||||
# Calculate the frequency of updates
|
||||
data.append(str(1 / (time() - start_time)))
|
||||
else:
|
||||
# Send error message to the UI updater
|
||||
synced_queue.put(["ERR_HOOK"])
|
||||
return
|
||||
|
||||
def stop(self) -> None:
|
||||
self._run = False
|
||||
|
||||
|
||||
# ╭────────────────────────────────────────────────╮
|
||||
# │ Main App Screen │
|
||||
# ╰────────────────────────────────────────────────╯
|
||||
# This is the main screen, where you can read out data
|
||||
class MainScreen(Screen):
|
||||
_event: ClockEvent
|
||||
|
||||
# The constructor if this class takes a Com object to share one between all screens
|
||||
# to preserve resources and make handling better
|
||||
def __init__(self, com: ComSuperClass, **kw):
|
||||
# Set some variables
|
||||
self._com = com
|
||||
self._event = None
|
||||
|
||||
# Prepare the reader thread
|
||||
self._reader = ReaderThread()
|
||||
self._reader.setDaemon(True)
|
||||
self._reader.set_com(com)
|
||||
self._has_connected = False
|
||||
|
||||
# Call the constructor for the Screen class
|
||||
super().__init__(**kw)
|
||||
|
||||
# Start the connection to the micro-controller to read data from it.
|
||||
# This also now starts the reader thread to continuously read out data
|
||||
def start(self):
|
||||
self.ids.status.text = "Connecting..."
|
||||
if self._com.connect():
|
||||
print("Acquired connection")
|
||||
self._has_connected = True
|
||||
# Start communication
|
||||
self._reader.start()
|
||||
print("Reader has started")
|
||||
Clock.schedule_interval(self._update_screen, 0.5)
|
||||
else:
|
||||
self.ids.status.text = "Connection failed"
|
||||
TwoActionPopup().open(
|
||||
"Failed to connect. Do you want to retry?",
|
||||
"Cancel",
|
||||
empty_func,
|
||||
"Retry",
|
||||
self.start,
|
||||
)
|
||||
|
||||
# End connection to micro-controller and set it back to normal mode
|
||||
def end(self, set_msg: bool = True):
|
||||
# Set micro-controller back to Normal Mode when ending communication
|
||||
# to make sure temperature control will work
|
||||
if self._has_connected:
|
||||
if self._event != None:
|
||||
self._event.cancel()
|
||||
self._reader.stop()
|
||||
try:
|
||||
self._com.send("NM")
|
||||
except:
|
||||
pass
|
||||
self._com.close()
|
||||
if set_msg:
|
||||
self.ids.status.text = "Connection terminated"
|
||||
print("Connection terminated")
|
||||
|
||||
# A helper function to update the screen. Is called on an interval
|
||||
def _update_screen(self, dt):
|
||||
update = []
|
||||
try:
|
||||
update = synced_queue.get_nowait()
|
||||
except:
|
||||
pass
|
||||
if len(update) == 0:
|
||||
# There are no updates to process, don't block and simply try again next time
|
||||
return
|
||||
if len(update) == 1:
|
||||
if update[0] == "ERR_HOOK":
|
||||
self.ids.status.text = "Hook failed"
|
||||
self.end(False)
|
||||
elif update[0] == "HOOK":
|
||||
self.ids.status.text = "Connected to controller"
|
||||
else:
|
||||
self.ids.sensor1.text = update[0]
|
||||
self.ids.sensor2.text = update[1]
|
||||
self.ids.sensor3.text = update[2]
|
||||
self.ids.sensor4.text = update[3]
|
||||
self.ids.status.text = "Connected, f = " + update[4]
|
||||
|
||||
# Reset the screen when the screen is entered
|
||||
def reset(self):
|
||||
self.ids.sensor1.text = ""
|
||||
self.ids.sensor2.text = ""
|
||||
self.ids.sensor3.text = ""
|
||||
self.ids.sensor4.text = ""
|
||||
self.ids.status.text = "Status will appear here"
|
||||
|
||||
# Switch the mode for the micro-controller
|
||||
def switch_mode(self, new_mode: str):
|
||||
# Store if we have been connected to the micro-controller before mode was switched
|
||||
was_connected = self._reader.is_alive
|
||||
|
||||
# Disconnect from the micro-controller
|
||||
self.end()
|
||||
self.ids.status.text = "Setting mode..."
|
||||
|
||||
# Try to set the new mode
|
||||
try:
|
||||
if new_mode == "Normal Mode":
|
||||
self._com.send("NM")
|
||||
else:
|
||||
self._com.send("FM")
|
||||
except:
|
||||
SingleRowPopup().open("Failed to switch modes")
|
||||
return
|
||||
|
||||
# If we have been connected, reconnect
|
||||
if was_connected:
|
||||
self.start()
|
||||
|
||||
|
||||
# Load the design file for this screen (.kv files)
|
||||
# The path has to be relative to root of the app, i.e. where the biogascontrollerapp.py
|
||||
# file is located
|
||||
Builder.load_file("./gui/main/main.kv")
|
||||
111
gui/popups/popups.kv
Normal file
111
gui/popups/popups.kv
Normal file
@@ -0,0 +1,111 @@
|
||||
<QuitPopup>:
|
||||
title: "BiogasControllerApp"
|
||||
font_size: 50
|
||||
size_hint: 0.5, 0.4
|
||||
auto_dismiss: False
|
||||
GridLayout:
|
||||
cols: 1
|
||||
Label:
|
||||
text: "Are you sure you want to leave?"
|
||||
font_size: 20
|
||||
GridLayout:
|
||||
cols:2
|
||||
Button:
|
||||
text: "Yes"
|
||||
font_size: 15
|
||||
on_release:
|
||||
root.quit()
|
||||
app.stop()
|
||||
Button:
|
||||
text: "No"
|
||||
font_size: 15
|
||||
on_press:
|
||||
root.dismiss()
|
||||
|
||||
<SingleRowPopup>:
|
||||
title: "INFORMATION"
|
||||
size_hint: 0.7, 0.5
|
||||
auto_dismiss: True
|
||||
GridLayout:
|
||||
cols: 1
|
||||
Label:
|
||||
id: msg
|
||||
text: "Message"
|
||||
text_size: self.width, None
|
||||
halign: 'center'
|
||||
GridLayout:
|
||||
cols: 1
|
||||
Button:
|
||||
text: "Ok"
|
||||
on_release:
|
||||
root.dismiss()
|
||||
|
||||
<TwoActionPopup>:
|
||||
title: "WARNING!"
|
||||
font_size: 50
|
||||
size_hint: 0.5, 0.4
|
||||
auto_dismiss: False
|
||||
GridLayout:
|
||||
cols:1
|
||||
Label:
|
||||
id: msg
|
||||
text: "Message"
|
||||
font_size: 20
|
||||
halign: 'center'
|
||||
GridLayout:
|
||||
cols:2
|
||||
Button:
|
||||
id: btn1
|
||||
text: "Details"
|
||||
on_release:
|
||||
root.action_one()
|
||||
root.dismiss()
|
||||
Button:
|
||||
id: btn2
|
||||
text:"Ok"
|
||||
on_release:
|
||||
root.action_two()
|
||||
root.dismiss()
|
||||
|
||||
<DualRowPopup>:
|
||||
title: "Details"
|
||||
font_size: 50
|
||||
size_hint: 0.7, 0.6
|
||||
auto_dismiss: False
|
||||
GridLayout:
|
||||
cols:1
|
||||
Label:
|
||||
id: msg_title
|
||||
text: "Message title"
|
||||
font_size: 20
|
||||
Label:
|
||||
id: msg_body
|
||||
text: "Message body"
|
||||
font_size: 14
|
||||
Button:
|
||||
text:"Ok"
|
||||
on_release:
|
||||
root.dismiss()
|
||||
|
||||
<LargeTrippleRowPopUp>:
|
||||
title: "DETAILS"
|
||||
font_size: 50
|
||||
size_hint: 1, 0.7
|
||||
auto_dismiss: False
|
||||
GridLayout:
|
||||
cols: 1
|
||||
Label:
|
||||
id: msg_title
|
||||
text: "title"
|
||||
font_size: 20
|
||||
Label:
|
||||
id: msg_body
|
||||
text: "Message"
|
||||
font_size: 13
|
||||
Label:
|
||||
text: msg_extra
|
||||
font_size: 13
|
||||
Button:
|
||||
text:"Ok"
|
||||
on_release:
|
||||
root.dismiss()
|
||||
63
gui/popups/popups.py
Normal file
63
gui/popups/popups.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from typing import Callable
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.lang import Builder
|
||||
|
||||
from lib.com import ComSuperClass
|
||||
|
||||
|
||||
# Just an empty function
|
||||
def empty_func():
|
||||
pass
|
||||
|
||||
# ╭────────────────────────────────────────────────╮
|
||||
# │ Popups │
|
||||
# ╰────────────────────────────────────────────────╯
|
||||
# Below, you can find various popups with various designs that can be used in the app
|
||||
class QuitPopup(Popup):
|
||||
def __init__(self, com: ComSuperClass, **kw):
|
||||
self._com = com;
|
||||
super().__init__(**kw)
|
||||
|
||||
def quit(self):
|
||||
self._com.close()
|
||||
|
||||
class SingleRowPopup(Popup):
|
||||
def open(self, message, *_args, **kwargs):
|
||||
self.ids.msg.text = message
|
||||
return super().open(*_args, **kwargs)
|
||||
|
||||
class DualRowPopup(Popup):
|
||||
def open(self, title: str, message: str, *_args, **kwargs):
|
||||
self.ids.msg_title.text = title
|
||||
self.ids.msg_body.text = message
|
||||
return super().open(*_args, **kwargs)
|
||||
|
||||
class LargeTrippleRowPopup(Popup):
|
||||
def open(self, title: str, message: str, details: str, *_args, **kwargs):
|
||||
self.ids.msg_title.text = title
|
||||
self.ids.msg_body.text = message
|
||||
self.ids.msg_extra.text = details
|
||||
return super().open(*_args, **kwargs)
|
||||
|
||||
class TwoActionPopup(Popup):
|
||||
def open(self,
|
||||
message: str,
|
||||
button_one: str,
|
||||
action_one: Callable[[], None],
|
||||
button_two: str = 'Ok',
|
||||
action_two: Callable[[], None] = empty_func,
|
||||
*_args,
|
||||
**kwargs
|
||||
):
|
||||
self.ids.msg.text = message
|
||||
self.ids.btn1.text = button_one
|
||||
self.ids.btn2.text = button_two
|
||||
self.action_one = action_one
|
||||
self.action_two = action_two
|
||||
return super().open(*_args, **kwargs)
|
||||
|
||||
|
||||
# Load the design file for this screen (.kv files)
|
||||
# The path has to be relative to root of the app, i.e. where the biogascontrollerapp.py
|
||||
# file is located
|
||||
Builder.load_file('./gui/popups/popups.kv')
|
||||
131
gui/program/program.kv
Normal file
131
gui/program/program.kv
Normal file
@@ -0,0 +1,131 @@
|
||||
<ProgramScreen>:
|
||||
name: "program"
|
||||
on_enter: self.config_loader = root.load_config()
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
FloatLayout:
|
||||
Label:
|
||||
text: "Configuration"
|
||||
font_size: 40
|
||||
color: (0, 113, 0, 1)
|
||||
bold: True
|
||||
pos_hint: {"y":0.4}
|
||||
GridLayout:
|
||||
size_hint: 0.8, 0.5
|
||||
pos_hint: {"x":0.1, "y":0.2}
|
||||
cols: 4
|
||||
Label:
|
||||
text: "Sensor 1, a:"
|
||||
TextInput:
|
||||
id: s1_a
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 1, b:"
|
||||
TextInput:
|
||||
id: s1_b
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 1, c:"
|
||||
TextInput:
|
||||
id: s1_c
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 1, Temp:"
|
||||
TextInput:
|
||||
id: s1_t
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 2, a:"
|
||||
TextInput:
|
||||
id: s2_a
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 2, b:"
|
||||
TextInput:
|
||||
id: s2_b
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 2, c:"
|
||||
TextInput:
|
||||
id: s2_c
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 2, Temp:"
|
||||
TextInput:
|
||||
id: s2_t
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 3, a:"
|
||||
TextInput:
|
||||
id: s3_a
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 3, b:"
|
||||
TextInput:
|
||||
id: s3_b
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 3, c:"
|
||||
TextInput:
|
||||
id: s3_c
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 3, Temp:"
|
||||
TextInput:
|
||||
id: s3_t
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 4, a:"
|
||||
TextInput:
|
||||
id: s4_a
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 4, b:"
|
||||
TextInput:
|
||||
id: s4_b
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 4, c:"
|
||||
TextInput:
|
||||
id: s4_c
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Label:
|
||||
text: "Sensor 4, Temp:"
|
||||
TextInput:
|
||||
id: s4_t
|
||||
multiline: False
|
||||
input_filter: "float"
|
||||
Button:
|
||||
text: "Back"
|
||||
size_hint: 0.1, 0.1
|
||||
pos_hint: {"x":0.1, "y":0.1}
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
on_release:
|
||||
app.root.current = "main"
|
||||
root.manager.transition.direction = "up"
|
||||
Button:
|
||||
text: "Save"
|
||||
size_hint: 0.2, 0.1
|
||||
pos_hint: {"x":0.6, "y":0.1}
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
on_release:
|
||||
root.save()
|
||||
111
gui/program/program.py
Normal file
111
gui/program/program.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from typing import List
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from kivy.lang import Builder
|
||||
from lib.decoder import Decoder
|
||||
from lib.instructions import Instructions
|
||||
from gui.popups.popups import SingleRowPopup, TwoActionPopup, empty_func
|
||||
from lib.com import ComSuperClass
|
||||
from kivy.clock import Clock
|
||||
|
||||
|
||||
# The below list maps 0, 1, 2, 3 to a, b, c and t respectively
|
||||
# This is used to set and read values of the UI
|
||||
name_map = ["a", "b", "c", "t"]
|
||||
|
||||
|
||||
class ProgramScreen(Screen):
|
||||
def __init__(self, com: ComSuperClass, **kw):
|
||||
self._com = com
|
||||
self._instructions = Instructions(com)
|
||||
self._decoder = Decoder()
|
||||
super().__init__(**kw)
|
||||
|
||||
def load_config(self):
|
||||
Clock.schedule_once(self._load)
|
||||
|
||||
# Load the current configuration from the micro-controller
|
||||
def _load(self, dt: float):
|
||||
# Hook to the microcontroller's data stream (i.e. sync up with it)
|
||||
if self._instructions.hook("RD", ["\n", "R", "D", "\n"]):
|
||||
config: List[List[str]] = []
|
||||
|
||||
# Load config for all four sensors
|
||||
for _ in range(4):
|
||||
# Receive 28 bytes of data
|
||||
received = bytes()
|
||||
try:
|
||||
received = self._com.receive(28)
|
||||
except:
|
||||
# Open error popup
|
||||
TwoActionPopup().open(
|
||||
"Failed to connect to micro-controller, retry?",
|
||||
"Cancel",
|
||||
empty_func,
|
||||
"Retry",
|
||||
lambda: self._load(0),
|
||||
)
|
||||
return
|
||||
|
||||
# Create a list of strings to store the config for the sensor
|
||||
# This list has the following elements: a, b, c, temperature
|
||||
config_sensor_i: List[str] = []
|
||||
|
||||
# Create the list
|
||||
for j in range(4):
|
||||
config_sensor_i.append(
|
||||
str(self._decoder.decode_float(received[7 * j : 7 * j + 6]))
|
||||
)
|
||||
|
||||
# Add it to the config
|
||||
config.append(config_sensor_i)
|
||||
else:
|
||||
TwoActionPopup().open(
|
||||
"Failed to connect to micro-controller, retry?",
|
||||
"Cancel",
|
||||
empty_func,
|
||||
"Retry",
|
||||
lambda: self._load(0),
|
||||
)
|
||||
|
||||
# Set the elements of the UI to the values of the config
|
||||
def _set_ui(self, config: List[List[str]]):
|
||||
for sensor_id in range(4):
|
||||
for property in range(4):
|
||||
self.ids[f"s{sensor_id + 1}_{name_map[property]}"].text = config[
|
||||
sensor_id
|
||||
][property]
|
||||
|
||||
# Read values from the UI. Returns the values as a list or None if the check was infringed
|
||||
def _read_ui(self, enforce_none_empty: bool = True) -> List[float] | None:
|
||||
data: List[float] = []
|
||||
|
||||
# Iterate over all sensor config input fields and collect the data
|
||||
for sensor_id in range(4):
|
||||
for property in range(4):
|
||||
value = self.ids[f"s{sensor_id + 1}_{name_map[property]}"].text
|
||||
|
||||
# If requested (by setting enforce_none_empty to True, which is the default)
|
||||
# test if the cells are not empty and if we find an empty cell return None
|
||||
if enforce_none_empty and value == "":
|
||||
return
|
||||
data.append(float(value))
|
||||
|
||||
return data
|
||||
|
||||
# Transmit the changed data to the micro-controller to reconfigure it
|
||||
def save(self):
|
||||
data = self._read_ui()
|
||||
if data == None:
|
||||
SingleRowPopup().open("Some fields are missing values!")
|
||||
else:
|
||||
try:
|
||||
self._instructions.change_config(data)
|
||||
except:
|
||||
SingleRowPopup().open("Could not save data!")
|
||||
SingleRowPopup().open("Data saved successfully")
|
||||
|
||||
|
||||
# Load the design file for this screen (.kv files)
|
||||
# The path has to be relative to root of the app, i.e. where the biogascontrollerapp.py
|
||||
# file is located
|
||||
Builder.load_file("./gui/program/program.kv")
|
||||
Reference in New Issue
Block a user