mirror of
https://github.com/janishutz/BiogasControllerApp.git
synced 2025-11-25 05:44:23 +00:00
Redesign app, prepare for 3.1.0 release
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
123
gui/main/main.kv
123
gui/main/main.kv
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user