Redesign app, prepare for 3.1.0 release

This commit is contained in:
2025-06-16 12:21:45 +02:00
parent d6a5e90b3c
commit 3a6cd6af3d
19 changed files with 599 additions and 562 deletions

View File

@@ -1,37 +1,52 @@
<AboutScreen>:
name: "about"
canvas.before:
Color:
rgba: (10,10,10,0.1)
Rectangle:
size: self.size
pos: self.pos
GridLayout:
cols: 1
MDFloatLayout:
Image:
source: "BiogasControllerAppLogo.png"
pos_hint: {"top": 0.9}
size_hint_y: .3
radius: 36, 36, 0, 0
allow_stretch: True
keep_ratio: True
MDGridLayout:
cols: 1
MDLabel:
text: "About"
font_size: 40
halign: 'center'
valign: 'center'
bold: True
italic: True
theme_text_color: 'Secondary'
pos_hint: {'center_x': 0, 'center_y': 0}
MDFillRoundFlatButton:
pos_hint: {'x': 0.1, 'y': 0.05}
text: "Back"
on_release:
app.root.current = "home"
root.manager.transition.direction = "up"
MDFillRoundFlatButton:
pos_hint: {'right': 0.9, 'y': 0.05}
text: "Report a Bug"
on_release:
root.goto("issues")
MDFillRoundFlatButton:
pos_hint: {'right': 0.48, 'y': 0.05}
text: "Wiki"
on_release:
root.goto("wiki")
MDFillRoundFlatButton:
pos_hint: {'x': 0.52, 'y': 0.05}
text: "Repo"
on_release:
root.goto("repo")
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"
text: "This is a simple controller application that allows you to read data from and configure the microcontroller used in ENATECH at KSWO. It is written in Python using KivyMD as its UI framework.\n\nThis software is free Software licensed under the GNU General Public License Version 3 and as such comes with absolutely no warranty."
pos_hint: {'x': 0.05, 'top': 0.42}
text_size: self.width, None
size_hint: 0.9, None

View File

@@ -1,13 +1,28 @@
from kivy.uix.screenmanager import Screen
from kivymd.uix.dialog import MDDialog
from kivymd.uix.button import MDFlatButton
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)
def __init__(self, **kw):
self.opened_web_browser_dialog = MDDialog(
title="Open Link",
text="Your webbrowser has been opened. Continue there",
buttons=[
MDFlatButton(text="Ok", on_release=lambda _: self.opened_web_browser_dialog.dismiss()),
],
)
super().__init__(**kw)
def goto(self, loc: str):
if loc == "wiki":
webbrowser.open('https://github.com/janishutz/BiogasControllerApp/wiki', new=2)
elif loc == "issues":
webbrowser.open('https://github.com/janishutz/BiogasControllerApp/issues', new=2)
elif loc == "repo":
webbrowser.open('https://github.com/janishutz/BiogasControllerApp', new=2)
self.opened_web_browser_dialog.open()
Builder.load_file('./gui/about/about.kv')

View File

@@ -1,27 +0,0 @@
<CreditsScreen>:
name: "credits"
canvas.before:
Color:
rgba: (20,20,20,0.2)
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

View File

@@ -1,8 +0,0 @@
from kivymd.uix.screen import MDScreen
from kivy.lang import Builder
class CreditsScreen(MDScreen):
pass
Builder.load_file('./gui/credits/credits.kv')

View File

@@ -1,19 +1,13 @@
<HomeScreen>:
name: "home"
MDGridLayout:
cols:1
MDFloatLayout:
MDCard:
radius: 36
pos_hint: {"center_x": .5, "center_y": .5}
size_hint: .6, .8
Image:
source: "BiogasControllerAppLogo.png"
pos_hint: {"top": 1}
radius: 36, 36, 0, 0
allow_stretch: True
keep_ratio: True
MDFloatLayout:
Image:
source: "BiogasControllerAppLogo.png"
pos_hint: {"top": 0.9}
size_hint_y: .3
radius: 36, 36, 0, 0
allow_stretch: True
keep_ratio: True
MDGridLayout:
cols: 1
@@ -28,32 +22,41 @@
pos_hint: {'center_x': 0, 'center_y': 0}
MDFloatLayout:
MDGridLayout:
cols: 2
size_hint: 0.2, 0.2
pos_hint: {"x": 0.4, "y": 0.3}
MDGridLayout:
spacing: 20
size_hint: None, None
size: self.minimum_size
cols: 2
pos_hint: {'center_x': 0.5, 'center_y': 0.3 }
MDFillRoundFlatButton:
font_size: 30
text: "Start"
on_release: root.start()
MDRaisedButton:
on_release: root.start()
font_size: 30
text: "Start"
radius: [25]
MDRaisedButton:
text: "Quit"
font_size: 30
on_release:
root.quit()
MDFillRoundFlatButton:
text: "Quit"
font_size: 30
pos_hint: {"x": 0.7, "center_y": 0}
on_release: root.quit()
MDLabel:
text: "You are running version V3.0.1"
font_size: 13
pos_hint: {"y": -0.45, "x":0}
halign: 'center'
MDFlatButton:
text: "About"
font_size: 13
size_hint: 0.07, 0.06
pos_hint: {"x":0.01, "y":0.01}
on_release:
root.to_about()
MDLabel:
text: "You are running version V3.1.0"
font_size: 13
pos_hint: {"y": -0.45, "x":0}
halign: 'center'
MDFlatButton:
text: "About"
font_size: 13
size_hint: 0.07, 0.06
pos_hint: {"x":0.01, "y":0.01}
on_release:
root.to_about()
# MDFlatButton:
# text: "Change Theme"
# font_size: 13
# size_hint: 0.07, 0.06
# pos_hint: {"right":0.99, "y":0.01}
# on_release:
# app.change_theme()

View File

@@ -1,6 +1,8 @@
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.screen import MDScreen
from kivy.lang import Builder
from gui.popups.popups import DualRowPopup, QuitPopup, TwoActionPopup
from lib.com import ComSuperClass
import platform
@@ -10,62 +12,113 @@ 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"
"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"
}
"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(MDScreen):
def __init__(self, com: ComSuperClass, **kw):
self._com = com;
self._com = com
self.connection_error_dialog = MDDialog(
title="Connection",
text="Failed to connect. See Details for more information and troubleshooting guide",
buttons=[
MDFlatButton(
text="Cancel",
on_release=lambda _: self.connection_error_dialog.dismiss(),
),
MDFlatButton(
text="Details", on_release=lambda _: self.open_details_popup()
),
],
)
self.quit_dialog = MDDialog(
title="Exit BiogasControllerApp",
text="Do you really want to exit BiogasControllerApp?",
buttons=[
MDFlatButton(
text="Cancel",
on_release=lambda _: self.quit_dialog.dismiss(),
),
MDFlatButton(
text="Quit", on_release=lambda _: self._quit()
),
],
)
super().__init__(**kw)
# Go to the main screen if we can establish connection or the check was disabled
def _quit(self):
self._com.close()
MDApp.get_running_app().stop()
# 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'
self.manager.current = "main"
self.manager.transition.direction = "right"
else:
TwoActionPopup().open('Failed to connect', 'Details', self.open_details_popup)
print('ERROR connecting')
self.connection_error_dialog.open()
print("[ COM ] Connection failed!")
# Open popup for details as to why the connection failed
def open_details_popup(self):
DualRowPopup().open("Troubleshooting tips", self._generate_help())
self.connection_error_dialog.dismiss()
self.details_dialog = MDDialog(
title="Troubleshooting",
text=self._generate_help(),
buttons=[
MDFlatButton(
text="Ok", on_release=lambda _: self.details_dialog.dismiss()
)
],
)
self.details_dialog.open()
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}'"
port = self._com.get_comport()
if port == "Sim":
return "Running in simulator, so this error is just simulated"
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"
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()
self.quit_dialog.open()
# Switch to about screen
def to_about(self):
self.manager.current = 'about'
self.manager.transition.direction = 'down'
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
# 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')
Builder.load_file("./gui/home/home.kv")

View File

@@ -1,101 +1,124 @@
<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:
MDFloatLayout:
MDGridLayout:
cols: 1
pos_hint: {'x': 0, 'y': 0.4}
MDLabel:
text: "READOUT"
font_size: 40
halign: 'center'
valign: 'center'
pos_hint: {'center_x': 0, 'center_y': 0}
bold: True
MDGridLayout:
cols:4
size_hint: 0.8, 0.3
pos_hint: {"x":0.1, "y":0.4}
Label:
MDLabel:
text: "Sensor 1: "
font_size: 20
Label:
MDLabel:
id: sensor1
text: ""
size_hint: 1, 1
halign: 'left'
text_size: self.size
Label:
MDLabel:
text: "Sensor 2: "
font_size: 20
Label:
MDLabel:
id: sensor2
text: ""
size_hint: 1, 1
halign: 'left'
text_size: self.size
Label:
MDLabel:
text: "Sensor 3: "
font_size: 20
Label:
MDLabel:
id: sensor3
text: ""
size_hint: 1, 1
halign: 'left'
text_size: self.size
Label:
MDLabel:
text: "Sensor 4: "
font_size: 20
Label:
MDLabel:
id: sensor4
text: ""
size_hint: 1, 1
halign: 'left'
text_size: self.size
Button:
MDFillRoundFlatButton:
text: "Connect"
size_hint: 0.2, 0.1
pos_hint: {"x": 0.5, "y": 0.05}
background_color: (255, 0, 0, 0.6)
size_hint: 0.15, 0.09
pos_hint: {"x": 0.03, "y": 0.05}
on_release:
root.start()
Button:
MDFillRoundFlatButton:
text: "Disconnect"
size_hint: 0.2, 0.1
pos_hint: {"x": 0.7, "y": 0.05}
background_color: (255, 0, 0, 0.6)
size_hint: 0.15, 0.09
pos_hint: {"x": 0.2, "y": 0.05}
on_release:
root.end()
Button:
MDFillRoundFlatButton:
text: "Back"
size_hint: 0.3, 0.1
pos_hint: {"x":0.05, "y":0.05}
background_color: (255, 0, 0, 0.6)
size_hint: 0.15, 0.09
pos_hint: {"right": 0.95, "y":0.05}
md_bg_color: app.theme_cls.primary_dark
on_release:
root.end()
app.root.current = "home"
root.manager.transition.direction = "left"
ToggleButton:
id: mode_selector
MDGridLayout:
cols: 2
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:
pos_hint: {"x":0.1, "y":0.15}
MDLabel:
text: "Fast Mode"
valign: "center"
MDSwitch:
id: mode_selector
on_active: root.switch_mode()
icon_active: "check"
MDFillRoundFlatButton:
text: "Configuration"
size_hint: 0.15, 0.1
pos_hint: {"x":0.7, "y":0.2}
background_color: (255, 0, 0, 0.6)
size_hint: 0.1, 0.07
pos_hint: {"x":0.45, "y":0.06}
md_bg_color: app.theme_cls.accent_dark
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}
MDGridLayout:
size_hint: 0.2, None
spacing: 0
padding: 0
cols: 1
pos_hint: {'right': 0.95, 'top': 0.95}
MDLabel:
id: status
text: "Status will appear here"
font_size: 10
halign: 'right'
MDGridLayout:
size_hint: None, None
spacing: 0
padding: 0
cols: 1
pos_hint: {'right': 0.95, 'top': 0.925}
MDLabel:
id: port
text: "Port: Not connected"
font_size: 10
halign: 'right'

View File

@@ -3,8 +3,9 @@ from time import time
from typing import List, override
from kivymd.uix.screen import MDScreen
from kivy.lang import Builder
from gui.popups.popups import SingleRowPopup, TwoActionPopup, empty_func
from kivy.clock import Clock, ClockEvent
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
import queue
import threading
@@ -53,7 +54,7 @@ class ReaderThread(threading.Thread):
# 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"])
synced_queue.put(["HOOK", self._com.get_comport()])
# making it exit using the stop function
while self._run:
@@ -70,17 +71,22 @@ class ReaderThread(threading.Thread):
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])
}\nTemp: {
round(self._decoder.decode_float(received[12 * i + 5:12 * i + 11]) * 1000) / 1000
}°C\nDC: {
round((self._decoder.decode_float_long(received[48 + 5 * i: 52 + 5 * i]) / 65535.0 * 100) * 1000) / 1000
}%"
)
try:
data.append(
f"Tadc: {
self._decoder.decode_int(received[12 * i:12 * i + 4])
}\nTemp: {
round(self._decoder.decode_float(received[12 * i + 5:12 * i + 11]) * 1000) / 1000
}°C\nDC: {
round((self._decoder.decode_float_long(received[48 + 5 * i: 52 + 5 * i]) / 65535.0 * 100) * 1000) / 1000
}%"
)
except:
data.append("Bad data")
# Calculate the frequency of updates
data.append(str(round((1 / (time() - start_time)) * 1000) / 1000) + " Hz")
data.append(
str(round((1 / (time() - start_time)) * 1000) / 1000) + " Hz"
)
synced_queue.put(data)
else:
# Send error message to the UI updater
@@ -104,6 +110,30 @@ class MainScreen(MDScreen):
# Set some variables
self._com = com
self._event = None
self._fast_mode = False
self.connection_error_dialog = MDDialog(
title="Connection",
text="Failed to connect. Do you wish to retry?",
buttons=[
MDFlatButton(
text="Cancel",
on_release=lambda _: self.connection_error_dialog.dismiss(),
),
MDFlatButton(text="Retry", on_release=lambda _: self.start()),
],
)
self.mode_switch_error_dialog = MDDialog(
title="Mode Switch",
text="Failed to change mode. Please try again",
buttons=[
MDFlatButton(
text="Ok",
on_release=lambda _: self.mode_switch_error_dialog.dismiss(),
),
],
)
# Prepare the reader thread
self._prepare_reader()
@@ -115,36 +145,31 @@ class MainScreen(MDScreen):
def _prepare_reader(self):
self._reader = ReaderThread()
self._reader.setDaemon(True)
self._reader.daemon = True
self._reader.set_com(self._com)
# 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):
# Prevent running multiple times
self.connection_error_dialog.dismiss()
if self._has_connected:
return
self.ids.status.text = "Connecting..."
if self._com.connect():
print("Acquired connection")
print("[ COM ] Connection Acquired")
self._has_connected = True
self._has_run = True
if self._has_run:
self._prepare_reader()
# Start communication
self._reader.start()
print("Reader has started")
print("[ COM ] Reader has started")
self._event = 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,
)
self.connection_error_dialog.open()
# End connection to micro-controller and set it back to normal mode
def end(self, set_msg: bool = True):
@@ -166,10 +191,12 @@ class MainScreen(MDScreen):
self._com.close()
if set_msg:
self.ids.status.text = "Connection terminated"
self.ids.port.text = "Port: Not connected"
self._has_connected = False
print("Connection terminated")
# A helper function to update the screen. Is called on an interval
def _update_screen(self, dt):
def _update_screen(self, _):
update = []
try:
update = synced_queue.get_nowait()
@@ -182,8 +209,10 @@ class MainScreen(MDScreen):
if update[0] == "ERR_HOOK":
self.ids.status.text = "Hook failed"
self.end(False)
elif update[0] == "HOOK":
if len(update) == 2:
if update[0] == "HOOK":
self.ids.status.text = "Connected to controller"
self.ids.port.text = "Port: " + update[1]
else:
self.ids.sensor1.text = update[0]
self.ids.sensor2.text = update[1]
@@ -198,9 +227,10 @@ class MainScreen(MDScreen):
self.ids.sensor3.text = ""
self.ids.sensor4.text = ""
self.ids.status.text = "Status will appear here"
self.ids.port.text = "Port: Not connected"
# Switch the mode for the micro-controller
def switch_mode(self, new_mode: str):
def switch_mode(self):
# Store if we have been connected to the micro-controller before mode was switched
was_connected = self._has_connected
@@ -210,12 +240,12 @@ class MainScreen(MDScreen):
# Try to set the new mode
try:
if new_mode == "Normal Mode":
if self._fast_mode:
self._com.send("NM")
else:
self._com.send("FM")
except:
SingleRowPopup().open("Failed to switch modes")
self.mode_switch_error_dialog.open()
return
self.ids.status.text = "Mode set"

View File

@@ -1,111 +0,0 @@
<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()

View File

@@ -1,63 +0,0 @@
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')

View File

@@ -1,126 +1,123 @@
<ProgramScreen>:
name: "program"
on_enter: self.config_loader = root.load_config()
md_bg_color: app.theme_cls.primary_color
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}
MDGridLayout:
cols: 1
pos_hint: {'x': 0, 'y': 0.4}
MDLabel:
text: "Configuration"
font_size: 40
halign: 'center'
valign: 'center'
pos_hint: {'center_x': 0, 'center_y': 0}
bold: True
MDGridLayout:
cols: 1
pos_hint: {'x': 0, 'y': 0.33}
MDLabel:
text: "Change the configuration of the microcontroller"
font_size: 18
halign: 'center'
valign: 'center'
pos_hint: {'center_x': 0, 'center_y': 0}
italic: True
MDGridLayout:
cols: 1
pos_hint: {'x': 0, 'y': 0.25}
MDLabel:
id: status
text: "Loading..."
font_size: 17
halign: 'center'
bold: True
MDGridLayout:
size_hint: 0.9, 0.5
spacing: 10
pos_hint: {"x":0.05, "y":0.2}
cols: 4
Label:
text: "Sensor 1, a:"
TextInput:
MDTextField:
id: s1_a
multiline: False
input_filter: "float"
Label:
text: "Sensor 1, b:"
TextInput:
hint_text: 'Sensor 1 a'
on_text: root.validate_float(self)
MDTextField:
id: s1_b
multiline: False
input_filter: "float"
Label:
text: "Sensor 1, c:"
TextInput:
hint_text: 'Sensor 1 b'
on_text: root.validate_float(self)
MDTextField:
id: s1_c
multiline: False
input_filter: "float"
Label:
text: "Sensor 1, Temp:"
TextInput:
hint_text: 'Sensor 1 c'
on_text: root.validate_float(self)
MDTextField:
id: s1_t
multiline: False
input_filter: "float"
Label:
text: "Sensor 2, a:"
TextInput:
hint_text: 'Sensor 1 Temperature'
on_text: root.validate_float(self)
MDTextField:
id: s2_a
multiline: False
input_filter: "float"
Label:
text: "Sensor 2, b:"
TextInput:
hint_text: 'Sensor 2 a'
on_text: root.validate_float(self)
MDTextField:
id: s2_b
multiline: False
input_filter: "float"
Label:
text: "Sensor 2, c:"
TextInput:
hint_text: 'Sensor 2 b'
on_text: root.validate_float(self)
MDTextField:
id: s2_c
multiline: False
input_filter: "float"
Label:
text: "Sensor 2, Temp:"
TextInput:
hint_text: 'Sensor 2 c'
on_text: root.validate_float(self)
MDTextField:
id: s2_t
multiline: False
input_filter: "float"
Label:
text: "Sensor 3, a:"
TextInput:
hint_text: 'Sensor 2 Temperature'
on_text: root.validate_float(self)
MDTextField:
id: s3_a
multiline: False
input_filter: "float"
Label:
text: "Sensor 3, b:"
TextInput:
hint_text: 'Sensor 3 a'
on_text: root.validate_float(self)
MDTextField:
id: s3_b
multiline: False
input_filter: "float"
Label:
text: "Sensor 3, c:"
TextInput:
hint_text: 'Sensor 3 b'
on_text: root.validate_float(self)
MDTextField:
id: s3_c
multiline: False
input_filter: "float"
Label:
text: "Sensor 3, Temp:"
TextInput:
hint_text: 'Sensor 3 c'
on_text: root.validate_float(self)
MDTextField:
id: s3_t
multiline: False
input_filter: "float"
Label:
text: "Sensor 4, a:"
TextInput:
hint_text: 'Sensor 3 Temperature'
on_text: root.validate_float(self)
MDTextField:
id: s4_a
multiline: False
input_filter: "float"
Label:
text: "Sensor 4, b:"
TextInput:
hint_text: 'Sensor 4 a'
on_text: root.validate_float(self)
MDTextField:
id: s4_b
multiline: False
input_filter: "float"
Label:
text: "Sensor 4, c:"
TextInput:
hint_text: 'Sensor 4 b'
on_text: root.validate_float(self)
MDTextField:
id: s4_c
multiline: False
input_filter: "float"
Label:
text: "Sensor 4, Temp:"
TextInput:
hint_text: 'Sensor 4 c'
on_text: root.validate_float(self)
MDTextField:
id: s4_t
multiline: False
input_filter: "float"
Button:
hint_text: 'Sensor 4 Temperature'
on_text: root.validate_float(self)
MDFillRoundFlatButton:
size_hint: 0.1, 0.07
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:
MDFillRoundFlatButton:
size_hint: 0.15, 0.09
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()

View File

@@ -3,7 +3,8 @@ from kivymd.uix.screen import MDScreen
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 kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from lib.com import ComSuperClass
from kivy.clock import Clock
@@ -18,13 +19,60 @@ class ProgramScreen(MDScreen):
self._com = com
self._instructions = Instructions(com)
self._decoder = Decoder()
self.connection_error_dialog = MDDialog(
title="Connection",
text="Failed to connect. Do you wish to retry?",
buttons=[
MDFlatButton(
text="Cancel",
on_release=lambda dt: self.connection_error_dialog.dismiss(),
),
MDFlatButton(text="Retry", on_release=lambda dt: self._load()),
],
)
self.missing_fields_error_dialog = MDDialog(
title="Save",
text="Some fields are missing entries. Please fill them out and try again",
buttons=[
MDFlatButton(
text="Ok",
on_release=lambda dt: self.missing_fields_error_dialog.dismiss(),
),
],
)
self.save_error_dialog = MDDialog(
title="Save",
text="Failed to save data. Please try again",
buttons=[
MDFlatButton(
text="Ok",
on_release=lambda dt: self.save_error_dialog.dismiss(),
),
],
)
self.save_success_dialog = MDDialog(
title="Save",
text="Data saved successfully!",
buttons=[
MDFlatButton(
text="Ok",
on_release=lambda dt: self.save_success_dialog.dismiss(),
),
],
)
super().__init__(**kw)
def load_config(self):
Clock.schedule_once(self._load)
Clock.schedule_once(lambda dt: self._load())
# Load the current configuration from the micro-controller
def _load(self, dt: float):
def _load(self):
self.ids.status.text = "Loading..."
# 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]] = []
@@ -37,13 +85,7 @@ class ProgramScreen(MDScreen):
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),
)
self.connection_error_dialog.open()
return
# Create a list of strings to store the config for the sensor
@@ -58,16 +100,11 @@ class ProgramScreen(MDScreen):
# Add it to the config
config.append(config_sensor_i)
self.ids.status.text = ""
self._set_ui(config)
else:
TwoActionPopup().open(
"Failed to connect to micro-controller, retry?",
"Cancel",
empty_func,
"Retry",
lambda: self._load(0),
)
self.connection_error_dialog.open()
# Set the elements of the UI to the values of the config
def _set_ui(self, config: List[List[str]]):
@@ -96,16 +133,37 @@ class ProgramScreen(MDScreen):
# Transmit the changed data to the micro-controller to reconfigure it
def save(self):
self.ids.status.text = "Saving..."
data = self._read_ui()
if data == None:
SingleRowPopup().open("Some fields are missing values!")
self.missing_fields_error_dialog()
else:
try:
self._instructions.change_config(data)
except Exception as e:
SingleRowPopup().open("Could not save data!")
self.save_error_dialog.open()
return
SingleRowPopup().open("Data saved successfully")
self.save_success_dialog.open()
self.ids.status.text = "Saved!"
Clock.schedule_once(self.reset_update, 5)
def reset_update(self, dt):
self.ids.status.text = ""
def validate_float(self, instance):
text = instance.text
# Allow only digits and one dot
if text.count(".") > 1 or any(c not in "0123456789." for c in text):
# Remove invalid characters
clean_text = "".join(c for c in text if c in "0123456789.")
# Remove extra dots
if clean_text.count(".") > 1:
first_dot = clean_text.find(".")
clean_text = clean_text[: first_dot + 1] + clean_text[
first_dot + 1 :
].replace(".", "")
instance.text = clean_text
# Load the design file for this screen (.kv files)