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

@@ -4,11 +4,12 @@
Currently only the newest versions get security updates as security updates are also part of a release.
Only Version 3 is supported due to the poor code quality of V2.3.0 and below.
Only Version 3.1 and later are supported due to the poor code quality of V2.3.0 and different UI before.
| Version | Supported |
| ------- | ------------------ |
| 3.0.0 | ✅ |
| 3.1.X | ✅ |
| 3.0.X | ✅ |
| 2.3.0 | ❎ |
| 2.2.0 | ❎ |
| 2.1.0 | ❎ |

View File

@@ -4,9 +4,9 @@
# ╰────────────────────────────────────────────────╯
#
# So you would like to read the source code? Nice!
# Just be warned, this application uses Thread and a UI Toolkit called
# Kivy to run. If you are unsure of what functions do, consider
# checking out the kivy docs at https://kivy.org/doc.
# Just be warned, this application uses Thread and a UI Toolkit called
# Kivy to run. If you are unsure of what functions do, consider
# checking out the kivy docs at https://kivy.org/doc.
# It also uses the pyserial library for communication with the micro-
# controller with RS232
#
@@ -15,25 +15,30 @@
# Load the config file
import configparser
import time
config = configparser.ConfigParser()
config.read("./config.ini")
# Introducing tariffs to Python imports.
# It was too funny of an idea to miss out on
# You can enable or disable this in the config.
# You can enable or disable this in the config.
# It is disabled by default
if config["Tariffs"]["impose_tariffs"] == "True":
try:
import tariff
tariff.set({
"kivy": int(config["Tariffs"]["kivy_rate"]),
"serial": int(config["Tariffs"]["pyserial_rate"]),
})
tariff.set(
{
"kivy": int(config["Tariffs"]["kivy_rate"]),
"serial": int(config["Tariffs"]["pyserial_rate"]),
}
)
except Exception as e:
print(e)
print("You cannot evade the tariffs. I will impose impose a tariff of 1000000% on the launch of this app!")
print(
"You cannot evade the tariffs. I will impose impose a tariff of 1000000% on the launch of this app!"
)
time.sleep(2000000)
import os
@@ -43,7 +48,6 @@ from lib.com import Com, ComSuperClass
import lib.test.com
# Load config and disable kivy log if necessary
if config["Dev"]["verbose"] == "True":
pass
@@ -52,27 +56,25 @@ else:
# Load kivy modules. Kivy is the UI framework used. See https://kivy.org
# from kivy.core.window import Window, Config
from kivy.core.window import Window
from kivy.uix.screenmanager import ScreenManager
from kivymd.app import MDApp
# Store the current app version
app_version = f"{config['Info']['version']}{config['Info']['subVersion']}"
# Set Window size
Window.size = (int(config["UI"]["width"]), int(config["UI"]["height"]))
# ╭────────────────────────────────────────────────╮
# │ Screens │
# ╰────────────────────────────────────────────────╯
# Import all the screens (= pages) used in the app
# Import all the screens (= pages) used in the app
from gui.home.home import HomeScreen
from gui.credits.credits import CreditsScreen
from gui.program.program import ProgramScreen
from gui.about.about import AboutScreen
from gui.main.main import MainScreen
# ╭────────────────────────────────────────────────╮
# │ Screen Manager │
# ╰────────────────────────────────────────────────╯
@@ -84,29 +86,54 @@ class BiogasControllerApp(MDApp):
@override
def build(self):
com: ComSuperClass = Com()
# Configure com
conn = config["Connection"]
filters = [x for x in conn["filters"].split(",")]
com: ComSuperClass = Com(
int(conn["baudrate"]) if conn["baudrate"] != None else 19200, filters
)
if config["Dev"]["use_test_library"] == "True":
com = lib.test.com.Com()
com = lib.test.com.Com(
int(config["Dev"]["fail_sim"]),
int(conn["baudrate"]) if conn["baudrate"] != None else 19200,
filters,
)
com.set_port_override(conn["baudrate"])
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Green"
self.theme_cls.accent_palette = "Lime"
self.theme_cls.theme_style_switch_animation = True
self.theme_cls.theme_style_switch_animation_duration = 0.8
self.theme_cls.theme_style = (
"Dark" if config["UI"]["theme"] == None else config["UI"]["theme"]
)
self.theme_cls.material_style = "M3"
self.theme_cls.primary_palette = (
"Green"
if config["UI"]["primary_color"] == None
else config["UI"]["primary_color"]
)
self.theme_cls.accent_palette = (
"Lime"
if config["UI"]["accent_color"] == None
else config["UI"]["accent_color"]
)
self.theme_cls.theme_style_switch_animation = False
self.icon = "./BiogasControllerAppLogo.png"
self.title = "BiogasControllerApp-" + app_version
self.title = "BiogasControllerApp-V3.1.0"
self.screen_manager.add_widget(HomeScreen(com, name="home"))
self.screen_manager.add_widget(MainScreen(com, name="main"))
self.screen_manager.add_widget(ProgramScreen(com, name="program"))
self.screen_manager.add_widget(CreditsScreen(name="credits"))
self.screen_manager.add_widget(AboutScreen(name="about"))
return self.screen_manager
def change_theme(self):
self.theme_cls.theme_style = (
"Dark" if self.theme_cls.theme_style == "Light" else "Light"
)
# Disallow this file to be imported
if __name__ == "__main__":
print("""
print(
"""
┏━━┓━━━━━━━━━━━━━━━━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━┏┓━┏┓━━━━━━━━┏━━━┓━━━━━━━━
┃┏┓┃━━━━━━━━━━━━━━━━━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━┃┃━┃┃━━━━━━━━┃┏━┓┃━━━━━━━━
┃┗┛┗┓┏┓┏━━┓┏━━┓┏━━┓━┏━━┓┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓┃┃━┃┃━┏━━┓┏━┓┃┃━┃┃┏━━┓┏━━┓
@@ -116,9 +143,10 @@ if __name__ == "__main__":
━━━━━━━━━━━┏━┛┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━┃┃━━
━━━━━━━━━━━┗━━┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━┗┛━━
Version 3.0.0
Version 3.1.0
=> Initializing....
""")
"""
)
BiogasControllerApp().run()
print("\n => Exiting!")

View File

@@ -1,11 +1,27 @@
***CHANGELOG***
V3.1.0
- Completely redesigned User Interface using KivyMD
- Added config option for themes
V3.0-beta
- Redesigned GUI
V3.0.1
- Install script fixes
- Packaging fixes
V3.0.0
- Small UI fixes
- Consolidated multiple previously separate screens
- Completely rewritten backend
- Improved stability
- Cleaned, documented code
- Reduced overhead of connecting
- Improved hooking reliability
- Removed installer, simpler setup now possible
- Removed official MacOS support as it didn't really work before anyway
- Added additional config options
- Improved linguistics
- Bugfixes
OLD VERSIONS
------------

View File

@@ -1,21 +1,24 @@
[Ports]
specificport = None
[Connection]
override_port = None
baudrate = 19200
# List the names as which the adapter cable will show up separated by commas below
# For ENATECH, the below is likely correct.
filters = USB-Serial Controller, Prolific USB-Serial Controller
[UI]
sizeh = 600
sizew = 800
height = 600
width = 800
# Can be Dark or Light
theme = Dark
primary_color = Green
accent_color = Lime
[Dev]
verbose = True
log_level = DEBUG
use_test_library = True
verbose = False
use_test_library = False
fail_sim = 10
[Tariffs]
impose_tariffs = False
kivy_rate = 50
pyserial_rate = 500
[Info]
version = V2.3.0
subversion =

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)

View File

@@ -6,16 +6,23 @@ import serial.tools.list_ports
class ComSuperClass(ABC):
def __init__(self, baudrate: Optional[int] = 19200, filters: Optional[list[str]] = None) -> None:
def __init__(
self, baudrate: Optional[int] = 19200, filters: Optional[list[str]] = None
) -> None:
self._serial: Optional[serial.Serial] = None
self._filters = filters if filters != None else [ 'USB-Serial Controller', 'Prolific USB-Serial Controller' ]
self._port_override = ''
self._filters = (
filters
if filters != None
else ["USB-Serial Controller", "Prolific USB-Serial Controller"]
)
self._port_override = ""
self._baudrate = baudrate if baudrate != None else 19200
self._err = None
def set_port_override(self, override: str) -> None:
"""Set the port override, to disable port search"""
self._port_override = override
if override != "" and override != "None":
self._port_override = override
def get_error(self) -> serial.SerialException | None:
return self._err
@@ -58,7 +65,7 @@ class Com(ComSuperClass):
def get_comport(self) -> str:
"""Find the comport the microcontroller has attached to"""
if self._port_override != '':
if self._port_override != "":
return self._port_override
# Catch all errors and simply return an empty string if search unsuccessful
@@ -80,7 +87,7 @@ class Com(ComSuperClass):
comport = self.get_comport()
# Comport search returns empty string if search unsuccessful
if comport == '':
if comport == "":
try:
self._serial = serial.Serial(comport, self._baudrate, timeout=5)
except serial.SerialException as e:
@@ -108,7 +115,7 @@ class Com(ComSuperClass):
if self._serial != None:
return self._serial.read(byte_count)
else:
raise Exception('ERR_CONNECTING')
raise Exception("ERR_CONNECTING")
def send(self, msg: str) -> None:
"""Send a string over serial connection. Will open a connection if none is available"""
@@ -116,12 +123,12 @@ class Com(ComSuperClass):
if self._serial != None:
self._serial.write(msg.encode())
else:
raise Exception('ERR_CONNECTING')
raise Exception("ERR_CONNECTING")
def send_float(self, msg: float) -> None:
"""Send a float number over serial connection"""
self._connection_check()
if self._serial != None:
self._serial.write(bytearray(struct.pack('>f', msg))[0:3])
self._serial.write(bytearray(struct.pack(">f", msg))[0:3])
else:
raise Exception('ERR_CONNECTING')
raise Exception("ERR_CONNECTING")

View File

@@ -12,10 +12,6 @@ class Instructions:
def __init__(self, com: ComSuperClass) -> None:
self._com = com
# Set a port override (to use a specific COM port)
def set_port_override(self, override: str) -> None:
self._com.set_port_override(override)
# Helper method to hook to the data stream according to protocol.
# You can specify the sequence that the program listens to to sync up,
# as an array of strings, that should each be of length one and only contain

View File

@@ -50,7 +50,7 @@ class SensorConfig:
class Com(ComSuperClass):
def __init__(
self, baudrate: int = 19200, filters: Optional[list[str]] = None
self, fail_sim: int, baudrate: int = 19200, filters: Optional[list[str]] = None
) -> None:
# Calling the constructor of the super class to assign defaults
print("\n\nWARNING: Using testing library for communication!\n\n")
@@ -62,6 +62,7 @@ class Com(ComSuperClass):
self.__reconf_sensor = 0
self.__reconf_step = 0
self.__fail_sim = fail_sim
self.__config: List[SensorConfig] = [
SensorConfig(),
@@ -78,11 +79,11 @@ class Com(ComSuperClass):
self._port_override = override
def get_comport(self) -> str:
return "test" if self._port_override != "" else self._port_override
return "Sim" if self._port_override == "" else self._port_override
def connect(self) -> bool:
# Randomly return false in 1 in 20 ish cases
if random.randint(0, 20) == 1:
# Randomly return false in 1 in fail_sim ish cases
if random.randint(0, self.__fail_sim) == 0:
print("Simulating error to connect")
return False
return True