mirror of
https://github.com/janishutz/BiogasControllerApp.git
synced 2025-11-25 13:54:24 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4af20a9a91 | |||
| b0bd5f446f | |||
| 265288106e | |||
|
|
1c7b758a11 | ||
|
|
44822e1cc4 | ||
| 4588caf974 | |||
| 223ab40bf8 | |||
| 7905cb851a | |||
| 3a6cd6af3d | |||
| d6a5e90b3c | |||
| 2b8f3c8aad | |||
| d875119071 | |||
| b01232b552 | |||
| b00466c5dd |
10
.github/ISSUE_TEMPLATE/custom.md
vendored
10
.github/ISSUE_TEMPLATE/custom.md
vendored
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
53
LICENSE
53
LICENSE
@@ -619,56 +619,3 @@ Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
13
README.md
13
README.md
@@ -37,6 +37,11 @@ That means:
|
||||
|
||||
Compared to older versions, the new BiogasControllerApp doesn't install itself as an app and only resides in a folder where you can launch it using the executable or the `launch.sh` script.
|
||||
|
||||
## Troubleshooting
|
||||
If you get a warning from Windows, the reason for this is that this app bundle is unsigned (since a signing certificate is about USD 350/year), so it might warn you about that. You can safely click "Run anyway" or the like to bypass that problem.
|
||||
|
||||
If this makes you uncomfortable, you may simply install python and install the necessary dependencies (see below) and run the app using Python.
|
||||
|
||||
# Features
|
||||
- Read data the microcontroller in ENATECH sends
|
||||
- Configure the microcontroller (Coefficients & Temperature). Old settings will be pre-loaded
|
||||
@@ -47,7 +52,7 @@ Compared to older versions, the new BiogasControllerApp doesn't install itself a
|
||||
- Documented code so you can more easily understand what is happening
|
||||
|
||||
# Issues
|
||||
If you encounter any bugs or other weird behaviour, please open an issue on this GitHub repository.
|
||||
If you encounter any bugs or other weird behaviour, please open an issue on this GitHub repository, contact me on my [support page](https://support.janishutz.com) or send me an [email](mailto:development@janishutz.com)
|
||||
|
||||
# Documentation
|
||||
You may find documentation for this project in its wiki here on GitHub. The code is also documented with explanations what it does
|
||||
@@ -55,15 +60,15 @@ You may find documentation for this project in its wiki here on GitHub. The code
|
||||
# Officially Supported OS
|
||||
- Microsoft Windows 10, 11 (through the provided compiled package, might work on older versions as well)
|
||||
- Microsoft Windows XP, Vista, 7, 8, 10, 11 (through running the package with Python yourself)
|
||||
- MacOS 10.9 (Mavericks) or later (required by Python)
|
||||
- GNU/Linux: All distros that support Python 3.8 or later (use `install-linux.sh` to install and `launch.sh` to launch for convenience)
|
||||
- FreeBSD: If you have Pyhton 3.8 or later installed
|
||||
|
||||
## Dependencies
|
||||
Only needed if you run with python directly
|
||||
- Python 3.10 - latest (only tested on this version, but should work down to at least 3.8)
|
||||
- kivy[base]
|
||||
- pyserial
|
||||
- kivy[base]==2.3.1
|
||||
- kivymd==1.1.1
|
||||
- pyserial==3.5
|
||||
|
||||
To install them, run `pip install -r requirements.txt`
|
||||
|
||||
|
||||
@@ -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 | ❎ |
|
||||
|
||||
@@ -13,27 +13,38 @@
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Load the config file
|
||||
import configparser
|
||||
import time
|
||||
config = configparser.ConfigParser()
|
||||
config.read("./config.ini")
|
||||
from lib.config import read_config, set_verbosity, str_to_bool
|
||||
|
||||
verbose = str_to_bool(read_config("Dev", "verbose", "False", type_to_validate="bool"))
|
||||
verbose = verbose if verbose != None else False
|
||||
|
||||
|
||||
# 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.
|
||||
# It is disabled by default
|
||||
if config["Tariffs"]["impose_tariffs"] == "True":
|
||||
if str_to_bool(
|
||||
read_config("Tariffs", "impose_tariffs", "False", type_to_validate="bool")
|
||||
):
|
||||
try:
|
||||
import tariff
|
||||
|
||||
tariff.set({
|
||||
"kivy": int(config["Tariffs"]["kivy_rate"]),
|
||||
"serial": int(config["Tariffs"]["pyserial_rate"]),
|
||||
})
|
||||
tariff.set(
|
||||
{
|
||||
"kivy": int(
|
||||
read_config("Tariffs", "kivy_rate", "0", type_to_validate="int")
|
||||
),
|
||||
"serial": int(
|
||||
read_config("Tariffs", "pyserial_rate", "0", type_to_validate="int")
|
||||
),
|
||||
}
|
||||
)
|
||||
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,22 +54,24 @@ from lib.com import Com, ComSuperClass
|
||||
import lib.test.com
|
||||
|
||||
|
||||
|
||||
# Load config and disable kivy log if necessary
|
||||
if config["Dev"]["verbose"] == "True":
|
||||
if verbose:
|
||||
pass
|
||||
else:
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
|
||||
|
||||
# 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 kivy.app import App
|
||||
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(int(read_config("UI", "width", "800", type_to_validate="int"))),
|
||||
int(int(read_config("UI", "height", "600", type_to_validate="int"))),
|
||||
)
|
||||
|
||||
|
||||
# ╭────────────────────────────────────────────────╮
|
||||
@@ -66,41 +79,107 @@ app_version = f"{config['Info']['version']}{config['Info']['subVersion']}"
|
||||
# ╰────────────────────────────────────────────────╯
|
||||
# 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 │
|
||||
# ╰────────────────────────────────────────────────╯
|
||||
# Kivy uses a screen manager to manage pages in the application
|
||||
class BiogasControllerApp(App):
|
||||
colors = [
|
||||
"Red",
|
||||
"Pink",
|
||||
"Purple",
|
||||
"DeepPurple",
|
||||
"Indigo",
|
||||
"Blue",
|
||||
"LightBlue",
|
||||
"Cyan",
|
||||
"Teal",
|
||||
"Green",
|
||||
"LightGreen",
|
||||
"Lime",
|
||||
"Yellow",
|
||||
"Amber",
|
||||
"Orange",
|
||||
"DeepOrange",
|
||||
"Brown",
|
||||
"Gray",
|
||||
"BlueGray",
|
||||
]
|
||||
|
||||
|
||||
class BiogasControllerApp(MDApp):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.screen_manager = ScreenManager()
|
||||
|
||||
@override
|
||||
def build(self):
|
||||
com: ComSuperClass = Com()
|
||||
if config["Dev"]["use_test_library"] == "True":
|
||||
com = lib.test.com.Com()
|
||||
# Configure com
|
||||
filters = [
|
||||
x.strip()
|
||||
for x in read_config(
|
||||
"Connection",
|
||||
"filters",
|
||||
"USB-Serial Controller; Prolific USB-Serial Controller",
|
||||
).split(";")
|
||||
]
|
||||
|
||||
baudrate = int(
|
||||
read_config("Connection", "baudrate", "19200", type_to_validate="int")
|
||||
)
|
||||
|
||||
com: ComSuperClass = Com(
|
||||
baudrate,
|
||||
filters,
|
||||
)
|
||||
|
||||
if str_to_bool(
|
||||
read_config("Dev", "use_test_library", "False", type_to_validate="bool")
|
||||
):
|
||||
com = lib.test.com.Com(
|
||||
int(read_config("Dev", "fail_sim", "20", type_to_validate="int")),
|
||||
baudrate,
|
||||
filters,
|
||||
)
|
||||
com.set_port_override(read_config("Connection", "port_override", "None"))
|
||||
|
||||
self.theme_cls.theme_style = read_config(
|
||||
"UI", "theme", "Dark", ["Dark", "Light"]
|
||||
)
|
||||
self.theme_cls.material_style = "M3"
|
||||
self.theme_cls.primary_palette = read_config(
|
||||
"UI", "primary_color", "Green", colors
|
||||
)
|
||||
self.theme_cls.accent_palette = read_config(
|
||||
"UI", "accent_color", "Lime", colors
|
||||
)
|
||||
self.theme_cls.theme_style_switch_animation = False
|
||||
|
||||
if verbose:
|
||||
print("\n", "-" * 20, "\n")
|
||||
|
||||
self.icon = "./BiogasControllerAppLogo.png"
|
||||
self.title = "BiogasControllerApp-" + app_version
|
||||
self.title = "BiogasControllerApp-V3.1.1"
|
||||
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(
|
||||
"""
|
||||
┏━━┓━━━━━━━━━━━━━━━━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━┏┓━┏┓━━━━━━━━┏━━━┓━━━━━━━━
|
||||
┃┏┓┃━━━━━━━━━━━━━━━━━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━┃┃━┃┃━━━━━━━━┃┏━┓┃━━━━━━━━
|
||||
┃┗┛┗┓┏┓┏━━┓┏━━┓┏━━┓━┏━━┓┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓┃┃━┃┃━┏━━┓┏━┓┃┃━┃┃┏━━┓┏━━┓
|
||||
@@ -110,9 +189,11 @@ if __name__ == "__main__":
|
||||
━━━━━━━━━━━┏━┛┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━┃┃━━
|
||||
━━━━━━━━━━━┗━━┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━┗┛━━
|
||||
|
||||
Version 3.0.0
|
||||
Version 3.1.1
|
||||
|
||||
=> Initializing....
|
||||
""")
|
||||
"""
|
||||
)
|
||||
set_verbosity(verbose)
|
||||
BiogasControllerApp().run()
|
||||
print("\n => Exiting!")
|
||||
|
||||
20
changelog
20
changelog
@@ -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
|
||||
------------
|
||||
|
||||
30
config.ini
30
config.ini
@@ -1,21 +1,25 @@
|
||||
[Ports]
|
||||
specificport = None
|
||||
[Connection]
|
||||
port_override = 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. The name cannot contain a semicolon
|
||||
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
|
||||
verbose = False
|
||||
use_test_library = False
|
||||
# One time out of how many (plus one) it should fail
|
||||
fail_sim = 20
|
||||
|
||||
[Tariffs]
|
||||
impose_tariffs = False
|
||||
kivy_rate = 50
|
||||
pyserial_rate = 500
|
||||
|
||||
[Info]
|
||||
version = V2.3.0
|
||||
subversion =
|
||||
|
||||
kivy_rate = 0
|
||||
pyserial_rate = 0
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from gui.popups.popups import *
|
||||
|
||||
|
||||
5
gui/README.md
Normal file
5
gui/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# GUI
|
||||
This folder contains all files that are used for the GUI of the app.
|
||||
|
||||
It is written in KivyMD, so if you don't know what that is and you don't want to learn it,
|
||||
there isn't much of use in here for you! - Just so you're warned
|
||||
@@ -1,37 +1,52 @@
|
||||
<AboutScreen>:
|
||||
name: "about"
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
GridLayout:
|
||||
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
|
||||
Label:
|
||||
MDLabel:
|
||||
text: "About"
|
||||
font_size: 40
|
||||
color: (0, 113, 0, 1)
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
bold: True
|
||||
FloatLayout:
|
||||
GridLayout:
|
||||
pos_hint: {"x":0.05, "y":0.05}
|
||||
size_hint: 0.9, 0.9
|
||||
cols: 3
|
||||
Button:
|
||||
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"
|
||||
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)
|
||||
|
||||
MDFillRoundFlatButton:
|
||||
pos_hint: {'right': 0.9, 'y': 0.05}
|
||||
text: "Report a Bug"
|
||||
on_release:
|
||||
root.report_issue()
|
||||
Button:
|
||||
text: "Credits"
|
||||
background_color: (255,0,0,0.6)
|
||||
root.goto("issues")
|
||||
|
||||
MDFillRoundFlatButton:
|
||||
pos_hint: {'right': 0.48, 'y': 0.05}
|
||||
text: "Wiki"
|
||||
on_release:
|
||||
app.root.current = "credits"
|
||||
root.manager.transition.direction = "left"
|
||||
root.goto("wiki")
|
||||
|
||||
MDFillRoundFlatButton:
|
||||
pos_hint: {'x': 0.52, 'y': 0.05}
|
||||
text: "Repo"
|
||||
on_release:
|
||||
root.goto("repo")
|
||||
Label:
|
||||
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,38 @@
|
||||
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
|
||||
|
||||
|
||||
# Simple about screen
|
||||
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):
|
||||
# Prepare dialog
|
||||
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)
|
||||
|
||||
Builder.load_file('./gui/about/about.kv')
|
||||
def goto(self, loc: str):
|
||||
# Open web browser with links
|
||||
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: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
FloatLayout:
|
||||
Button:
|
||||
text: "back"
|
||||
size_hint: 0.4, 0.2
|
||||
pos_hint: {"x":0.3, "y":0.1}
|
||||
on_release:
|
||||
app.root.current = "about"
|
||||
root.manager.transition.direction = "right"
|
||||
GridLayout:
|
||||
cols:1
|
||||
pos_hint:{"x":0.05, "y":0.35}
|
||||
size_hint: 0.9, 0.5
|
||||
Label:
|
||||
text: "This is a controller sofware that helps you reprogram and monitor the micro-controller used in ENATECH at KSWO"
|
||||
Label:
|
||||
text: "Written by: Janis Hutz\nDesigned by: Janis Hutz\nDesign language: Kivy"
|
||||
Label:
|
||||
text: "This software is free Software licensed under the GPL V3 (GNU General Public License) and as such comes with absolutely no warranty. In return, you can use, modify, distribute or use any of the code of this software in your own project, if you reuse the same license. For more infos, you can find a copy of this license in the project folder."
|
||||
text_size: self.width, None
|
||||
@@ -1,8 +0,0 @@
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from kivy.lang import Builder
|
||||
|
||||
|
||||
class CreditsScreen(Screen):
|
||||
pass
|
||||
|
||||
Builder.load_file('./gui/credits/credits.kv')
|
||||
@@ -1,45 +1,62 @@
|
||||
<HomeScreen>:
|
||||
name: "home"
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
GridLayout:
|
||||
cols:1
|
||||
Label:
|
||||
text: "BiogasanlageControllerApp"
|
||||
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: "BiogasControllerApp"
|
||||
font_size: 50
|
||||
color: (0, 113, 0, 1)
|
||||
bold:True
|
||||
italic:True
|
||||
FloatLayout:
|
||||
GridLayout:
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
bold: True
|
||||
italic: True
|
||||
theme_text_color: 'Secondary'
|
||||
pos_hint: {'center_x': 0, 'center_y': 0}
|
||||
|
||||
|
||||
MDGridLayout:
|
||||
spacing: 20
|
||||
size_hint: None, None
|
||||
size: self.minimum_size
|
||||
cols: 2
|
||||
size_hint: 0.8, 0.8
|
||||
pos_hint: {"x": 0.1, "y": 0.1}
|
||||
Button:
|
||||
pos_hint: {'center_x': 0.5, 'center_y': 0.3 }
|
||||
MDFillRoundFlatButton:
|
||||
font_size: 30
|
||||
text: "Start"
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
font_size: 30
|
||||
on_release:
|
||||
root.start()
|
||||
Button:
|
||||
on_release: root.start()
|
||||
|
||||
MDFillRoundFlatButton:
|
||||
text: "Quit"
|
||||
background_color: (255, 0, 0, 0.6)
|
||||
font_size: 30
|
||||
on_release:
|
||||
root.quit()
|
||||
Label:
|
||||
text: "You are running version V3.0.0"
|
||||
pos_hint: {"x": 0.7, "center_y": 0}
|
||||
on_release: root.quit()
|
||||
|
||||
MDLabel:
|
||||
text: "You are running version V3.1.1"
|
||||
font_size: 13
|
||||
pos_hint: {"y": -0.45, "x":0.05}
|
||||
Button:
|
||||
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}
|
||||
background_color: (50, 0, 0, 0.2)
|
||||
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,9 @@
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from kivy.base import Clock
|
||||
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 +13,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(Screen):
|
||||
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)
|
||||
|
||||
def _quit(self):
|
||||
self._com.close()
|
||||
MDApp.get_running_app().stop()
|
||||
|
||||
def start(self):
|
||||
Clock.schedule_once(lambda _: self._start())
|
||||
|
||||
# Go to the main screen if we can establish connection or the check was disabled
|
||||
# in the configs
|
||||
def start(self):
|
||||
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
|
||||
# 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,89 +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}
|
||||
MDFloatLayout:
|
||||
MDGridLayout:
|
||||
cols: 1
|
||||
pos_hint: {'x': 0, 'y': 0.4}
|
||||
MDLabel:
|
||||
text: "READOUT"
|
||||
font_size: 40
|
||||
color: (0, 113, 0, 1)
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {'center_x': 0, 'center_y': 0}
|
||||
bold: True
|
||||
GridLayout:
|
||||
|
||||
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: ""
|
||||
Label:
|
||||
size_hint: 1, 1
|
||||
halign: 'left'
|
||||
text_size: self.size
|
||||
MDLabel:
|
||||
text: "Sensor 2: "
|
||||
font_size: 20
|
||||
Label:
|
||||
MDLabel:
|
||||
id: sensor2
|
||||
text: ""
|
||||
Label:
|
||||
size_hint: 1, 1
|
||||
halign: 'left'
|
||||
text_size: self.size
|
||||
MDLabel:
|
||||
text: "Sensor 3: "
|
||||
font_size: 20
|
||||
Label:
|
||||
MDLabel:
|
||||
id: sensor3
|
||||
text: ""
|
||||
Label:
|
||||
size_hint: 1, 1
|
||||
halign: 'left'
|
||||
text_size: self.size
|
||||
MDLabel:
|
||||
text: "Sensor 4: "
|
||||
font_size: 20
|
||||
Label:
|
||||
MDLabel:
|
||||
id: sensor4
|
||||
text: ""
|
||||
Button:
|
||||
size_hint: 1, 1
|
||||
halign: 'left'
|
||||
text_size: self.size
|
||||
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:
|
||||
|
||||
MDGridLayout:
|
||||
cols: 2
|
||||
size_hint: 0.15, 0.1
|
||||
pos_hint: {"x":0.1, "y":0.15}
|
||||
MDLabel:
|
||||
text: "Fast Mode"
|
||||
valign: "center"
|
||||
MDSwitch:
|
||||
id: mode_selector
|
||||
size_hint: 0.15, 0.1
|
||||
pos_hint: {"x":0.1, "y":0.2}
|
||||
text: "Normal Mode" if self.state == "normal" else "Fast Mode"
|
||||
on_text: root.switch_mode(mode_selector.text)
|
||||
background_color: (255,0,0,0.6) if self.state == "normal" else (0,0,255,0.6)
|
||||
Button:
|
||||
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:
|
||||
|
||||
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
|
||||
pos_hint: {"x":0.4, "y": 0.3}
|
||||
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'
|
||||
|
||||
131
gui/main/main.py
131
gui/main/main.py
@@ -1,10 +1,11 @@
|
||||
from ctypes import ArgumentError
|
||||
from time import time
|
||||
from typing import List, override
|
||||
from kivy.uix.screenmanager import Screen
|
||||
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,23 @@ 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
|
||||
try:
|
||||
data.append(
|
||||
f"Tadc: {
|
||||
self._decoder.decode_int(received[12 * i:12 * i + 4])
|
||||
}\nTemperature: {
|
||||
self._decoder.decode_float(received[12 * i + 5:12 * i + 11])
|
||||
}\nDuty-Cycle: {
|
||||
self._decoder.decode_float_long(received[48 + 5 * i: 52 + 5 * i]) / 65535.0 * 100
|
||||
}\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(1 / (time() - start_time)))
|
||||
data.append(
|
||||
str(round((1 / (time() - start_time)) * 1000) / 1000) + " Hz"
|
||||
)
|
||||
synced_queue.put(data)
|
||||
else:
|
||||
# Send error message to the UI updater
|
||||
synced_queue.put(["ERR_HOOK"])
|
||||
@@ -94,7 +101,7 @@ class ReaderThread(threading.Thread):
|
||||
# │ Main App Screen │
|
||||
# ╰────────────────────────────────────────────────╯
|
||||
# This is the main screen, where you can read out data
|
||||
class MainScreen(Screen):
|
||||
class MainScreen(MDScreen):
|
||||
_event: ClockEvent
|
||||
|
||||
# The constructor if this class takes a Com object to share one between all screens
|
||||
@@ -103,36 +110,78 @@ class MainScreen(Screen):
|
||||
# Set some variables
|
||||
self._com = com
|
||||
self._event = None
|
||||
self._fast_mode = False
|
||||
|
||||
# Set up Dialog for erros
|
||||
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._reader = ReaderThread()
|
||||
self._reader.setDaemon(True)
|
||||
self._reader.set_com(com)
|
||||
self._prepare_reader()
|
||||
self._has_run = False
|
||||
self._has_connected = False
|
||||
|
||||
# Call the constructor for the Screen class
|
||||
super().__init__(**kw)
|
||||
|
||||
# Start the connection to the micro-controller to read data from it.
|
||||
# This also now starts the reader thread to continuously read out data
|
||||
def _prepare_reader(self):
|
||||
# Prepares the reader thread
|
||||
self._reader = ReaderThread()
|
||||
self._reader.daemon = True
|
||||
self._reader.set_com(self._com)
|
||||
|
||||
# Small helper function that makes the UI not freeze by offloading
|
||||
def start(self):
|
||||
Clock.schedule_once(lambda _: self._start())
|
||||
|
||||
# Start the connection to the micro-controller to read data from it.
|
||||
# This also 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
|
||||
|
||||
# Some UI config
|
||||
self.ids.status.text = "Connecting..."
|
||||
if self._com.connect():
|
||||
print("Acquired connection")
|
||||
print("[ COM ] Connection Acquired")
|
||||
|
||||
# Prevent multiple connections
|
||||
self._has_connected = True
|
||||
self._has_run = True
|
||||
if self._has_run:
|
||||
self._prepare_reader()
|
||||
|
||||
# Start communication
|
||||
self._reader.start()
|
||||
print("Reader has started")
|
||||
Clock.schedule_interval(self._update_screen, 0.5)
|
||||
print("[ COM ] Reader has started")
|
||||
|
||||
# Schedule UI updates
|
||||
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):
|
||||
@@ -142,32 +191,52 @@ class MainScreen(Screen):
|
||||
if self._event != None:
|
||||
self._event.cancel()
|
||||
self._reader.stop()
|
||||
|
||||
# Join the thread to end it safely
|
||||
try:
|
||||
self._reader.join()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Go back to Normal Mode on the Controller
|
||||
# This is so you don't accidentally forget!
|
||||
try:
|
||||
self._com.send("NM")
|
||||
except:
|
||||
pass
|
||||
|
||||
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()
|
||||
except:
|
||||
pass
|
||||
|
||||
if len(update) == 0:
|
||||
# There are no updates to process, don't block and simply try again next time
|
||||
return
|
||||
|
||||
if len(update) == 1:
|
||||
# Sync errors
|
||||
if update[0] == "ERR_HOOK":
|
||||
self.ids.status.text = "Hook failed"
|
||||
self.end(False)
|
||||
elif update[0] == "HOOK":
|
||||
|
||||
if len(update) == 2:
|
||||
# Connection successful
|
||||
if update[0] == "HOOK":
|
||||
self.ids.status.text = "Connected to controller"
|
||||
self.ids.port.text = "Port: " + update[1]
|
||||
else:
|
||||
# Update the UI
|
||||
self.ids.sensor1.text = update[0]
|
||||
self.ids.sensor2.text = update[1]
|
||||
self.ids.sensor3.text = update[2]
|
||||
@@ -181,11 +250,12 @@ class MainScreen(Screen):
|
||||
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._reader.is_alive
|
||||
was_connected = self._has_connected
|
||||
|
||||
# Disconnect from the micro-controller
|
||||
self.end()
|
||||
@@ -193,14 +263,15 @@ class MainScreen(Screen):
|
||||
|
||||
# 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"
|
||||
# If we have been connected, reconnect
|
||||
if was_connected:
|
||||
self.start()
|
||||
|
||||
@@ -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,131 +1,123 @@
|
||||
<ProgramScreen>:
|
||||
name: "program"
|
||||
on_enter: self.config_loader = root.load_config()
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (10,10,10,0.1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
FloatLayout:
|
||||
Label:
|
||||
MDGridLayout:
|
||||
cols: 1
|
||||
pos_hint: {'x': 0, 'y': 0.4}
|
||||
MDLabel:
|
||||
text: "Configuration"
|
||||
font_size: 40
|
||||
color: (0, 113, 0, 1)
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {'center_x': 0, 'center_y': 0}
|
||||
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.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()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from typing import List
|
||||
from kivy.uix.screenmanager import Screen
|
||||
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
|
||||
|
||||
@@ -13,18 +14,67 @@ from kivy.clock import Clock
|
||||
name_map = ["a", "b", "c", "t"]
|
||||
|
||||
|
||||
class ProgramScreen(Screen):
|
||||
class ProgramScreen(MDScreen):
|
||||
def __init__(self, com: ComSuperClass, **kw):
|
||||
self._com = com
|
||||
self._instructions = Instructions(com)
|
||||
self._decoder = Decoder()
|
||||
|
||||
# Configure Dialog
|
||||
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.load_config()),
|
||||
],
|
||||
)
|
||||
|
||||
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 _: 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 _: self.save_error_dialog.dismiss(),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
self.save_success_dialog = MDDialog(
|
||||
title="Save",
|
||||
text="Data saved successfully!",
|
||||
buttons=[
|
||||
MDFlatButton(
|
||||
text="Ok",
|
||||
on_release=lambda _: self.save_success_dialog.dismiss(),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
super().__init__(**kw)
|
||||
|
||||
# Load the config (async to not freeze the UI)
|
||||
def load_config(self):
|
||||
Clock.schedule_once(self._load)
|
||||
Clock.schedule_once(lambda _: 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 +87,7 @@ class ProgramScreen(Screen):
|
||||
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,14 +102,11 @@ class ProgramScreen(Screen):
|
||||
|
||||
# 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]]):
|
||||
@@ -92,17 +133,42 @@ class ProgramScreen(Screen):
|
||||
|
||||
return data
|
||||
|
||||
# Transmit the changed data to the micro-controller to reconfigure it
|
||||
def save(self):
|
||||
Clock.schedule_once(lambda _: self._save())
|
||||
|
||||
# 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:
|
||||
SingleRowPopup().open("Could not save data!")
|
||||
SingleRowPopup().open("Data saved successfully")
|
||||
self.save_error_dialog.open()
|
||||
return
|
||||
self.save_success_dialog.open()
|
||||
self.ids.status.text = "Saved!"
|
||||
Clock.schedule_once(self.reset_update, 5)
|
||||
|
||||
def reset_update(self, _):
|
||||
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)
|
||||
|
||||
@@ -31,17 +31,19 @@ use_venv=""
|
||||
read -p "Install dependencies in a virtual environment? (Y/n) " use_venv
|
||||
use_venv=$(echo "$use_venv" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
echo "\n => Checking for repo..."
|
||||
echo "
|
||||
=> Checking for repo...
|
||||
"
|
||||
|
||||
if [[ -f ./biogascontrollerapp.py ]]; then
|
||||
echo "\n -> Data found, not downloading"
|
||||
echo " -> Data found, not downloading"
|
||||
else
|
||||
do_download=""
|
||||
read -p " -> Data not found, okay to download? (Y/n) " do_download
|
||||
do_download=$(echo "$do_download" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "$do_download" == "y" || "$do_download" == "" ]]; then
|
||||
# Check if wget is installed
|
||||
if [[ !command -v wget >/dev/null 2>&1 ]]; then
|
||||
if !command -v wget >/dev/null 2>&1; then
|
||||
echo "wget unavailable. Please install using your distribution's package manager or manually download the repo from GitHub releases"
|
||||
echo 1
|
||||
fi
|
||||
@@ -53,9 +55,10 @@ else
|
||||
tar -xf ./biogascontrollerapp-linux.tar.gz
|
||||
|
||||
# Remove tarball (to keep it clean)
|
||||
rm ./biogascontrollerapp.tar.gz
|
||||
rm ./biogascontrollerapp-linux.tar.gz
|
||||
mv dist biogascontrollerapp-linux
|
||||
|
||||
cd biogascontrollerapp/
|
||||
cd biogascontrollerapp-linux/
|
||||
else
|
||||
echo "Please download the repo manually and execute the script inside the downloaded repo from GitHub releases"
|
||||
exit 1
|
||||
@@ -74,7 +77,7 @@ if [[ "$use_venv" == "y" || "$use_venv" == "" ]]; then
|
||||
source ./.venv/bin/activate
|
||||
fi
|
||||
|
||||
if [[ !command -v deactivate >/dev/null 2>&1 ]]; then
|
||||
if !command -v deactivate >/dev/null 2>&1; then
|
||||
echo "Virtual environment could not be activated.
|
||||
You may install the dependencies by changing to the biogascontrollerapp directory and running
|
||||
pip install -r requirements.txt"
|
||||
|
||||
@@ -10,7 +10,7 @@ if [[ -f ./.venv/bin/activate ]]; then
|
||||
source ./.venv/bin/activate
|
||||
fi
|
||||
|
||||
if [[ !command -v deactivate >/dev/null 2>&1 ]]; then
|
||||
if !command -v deactivate >/dev/null 2>&1; then
|
||||
echo "Virtual environment could not be activated. Trying to run anyway"
|
||||
fi
|
||||
fi
|
||||
|
||||
80
lib/com.py
80
lib/com.py
@@ -4,17 +4,40 @@ import serial
|
||||
import struct
|
||||
import serial.tools.list_ports
|
||||
|
||||
# The below class is abstract to have a consistent, targetable interface
|
||||
# for both the real connection module and the simulation module
|
||||
#
|
||||
# If you are unaware of what classes are, you can mostly ignore the ComSuperClass
|
||||
#
|
||||
# For the interested, a quick rundown of what the benefits of doing it this way is:
|
||||
# This class provides a way to have two wholly different implementations that have
|
||||
# the same function interface (i.e. all functions take the same arguments)
|
||||
#
|
||||
# Another benefit of having classes is that we can pass a single instance around to
|
||||
# various components and have one shared instance that all can modify, reducing some
|
||||
# overhead.
|
||||
#
|
||||
# The actual implementation of most functions (called methods in OOP) are implemented
|
||||
# in the Com class below.
|
||||
|
||||
|
||||
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"""
|
||||
if override != "" and override != "None":
|
||||
self._port_override = override
|
||||
|
||||
def get_error(self) -> serial.SerialException | None:
|
||||
@@ -45,6 +68,15 @@ class ComSuperClass(ABC):
|
||||
pass
|
||||
|
||||
|
||||
# ┌ ┐
|
||||
# │ Main Com Class Implementation │
|
||||
# └ ┘
|
||||
# Below you can find what you were most likely looking for. This is the implementation of the communication with the microcontroller.
|
||||
# You may also be interested in the decoder.py and instructions.py file, as the decoding and the hooking / syncing process are
|
||||
# implemented there. It is recommended that you do NOT read the test/com.py file, as that one is only there for simulation purposes
|
||||
# and is much more complicated than this here, if you are not well versed with Python or are struggling with the basics
|
||||
|
||||
|
||||
class Com(ComSuperClass):
|
||||
def _connection_check(self) -> bool:
|
||||
if self._serial == None:
|
||||
@@ -58,7 +90,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
|
||||
@@ -77,17 +109,30 @@ class Com(ComSuperClass):
|
||||
return ""
|
||||
|
||||
def _open(self) -> bool:
|
||||
"""Open the connection. Internal function, not to be called directly
|
||||
|
||||
Returns:
|
||||
Boolean indicates if connection was successful or not
|
||||
"""
|
||||
# Get the com port the controller has connected to
|
||||
comport = self.get_comport()
|
||||
|
||||
# Comport search returns empty string if search unsuccessful
|
||||
if comport == '':
|
||||
if comport == "":
|
||||
# Try to generate a new Serial object with the configuration of this class
|
||||
# self._baudrate contains the baud rate and defaults to 19200
|
||||
try:
|
||||
self._serial = serial.Serial(comport, self._baudrate, timeout=5)
|
||||
except serial.SerialException as e:
|
||||
# If an error occurs, catch it, handle it and store the error
|
||||
# for the UI and return False to indicate failed connection
|
||||
self._err = e
|
||||
return False
|
||||
|
||||
# Connection succeeded, return True
|
||||
return True
|
||||
else:
|
||||
# Haven't found a comport
|
||||
return False
|
||||
|
||||
def connect(self) -> bool:
|
||||
@@ -103,25 +148,40 @@ class Com(ComSuperClass):
|
||||
pass
|
||||
|
||||
def receive(self, byte_count: int) -> bytes:
|
||||
"""Recieve bytes from microcontroller over serial. Returns bytes. Might want to decode using functions from lib.tools"""
|
||||
"""Receive bytes from microcontroller over serial. Returns bytes. Might want to decode using functions from lib.decoder"""
|
||||
# Check connection
|
||||
self._connection_check()
|
||||
|
||||
# Ignore this boilerplate (extra code), the body of the if is the only thing important.
|
||||
# The reason for the boilerplate is that the type checker will notice that self._serial can be
|
||||
# None, thus showing errors.
|
||||
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"""
|
||||
# Check connection
|
||||
self._connection_check()
|
||||
|
||||
# Ignore this boilerplate (extra code), the body of the if is the only thing important.
|
||||
# The reason for the boilerplate is that the type checker will notice that self._serial can be
|
||||
# None, thus showing errors.
|
||||
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"""
|
||||
# Check connection
|
||||
self._connection_check()
|
||||
|
||||
# Ignore this boilerplate (extra code), the body of the if is the only thing important.
|
||||
# The reason for the boilerplate is that the type checker will notice that self._serial can be
|
||||
# None, thus showing errors.
|
||||
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")
|
||||
|
||||
144
lib/config.py
Normal file
144
lib/config.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import configparser
|
||||
from typing import List
|
||||
|
||||
# Load the config
|
||||
config = configparser.ConfigParser()
|
||||
config.read("./config.ini")
|
||||
|
||||
global first_error
|
||||
first_error = True
|
||||
|
||||
global is_verbose
|
||||
is_verbose = True
|
||||
|
||||
|
||||
def set_verbosity(verbose: bool):
|
||||
global is_verbose
|
||||
is_verbose = verbose
|
||||
|
||||
print("\n", "-" * 20, "\nValidating configuration...\n")
|
||||
|
||||
|
||||
def str_to_bool(val: str) -> bool | None:
|
||||
"""Convert a string to boolean, converting "True" and "true" to True, same for False
|
||||
|
||||
Args:
|
||||
val: The value to try to convert
|
||||
|
||||
Returns:
|
||||
Returns either a boolean if conversion was successful, or None if not a boolean
|
||||
"""
|
||||
return {"True": True, "true": True, "False": False, "false": False}.get(val, None)
|
||||
|
||||
|
||||
def read_config(
|
||||
key_0: str,
|
||||
key_1: str,
|
||||
default: str,
|
||||
valid_entries: List[str] = [],
|
||||
type_to_validate: str = "",
|
||||
) -> str:
|
||||
"""Read the configuration, report potential configuration issues and validate each entry
|
||||
|
||||
Args:
|
||||
key_0: The first key (top level)
|
||||
key_1: The second key (where the actual key-value pair is)
|
||||
default: The default value to return if the check fails
|
||||
valid_entries: [Optiona] The entries that are valid ones to check against
|
||||
type_to_validate: [Optional] Data type to validate
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
# Try loading the keys
|
||||
tmp = {}
|
||||
try:
|
||||
tmp = config[key_0]
|
||||
except KeyError:
|
||||
print_config_error(key_0, key_1, "", default, "unknown", index=1)
|
||||
return default
|
||||
|
||||
value = ""
|
||||
try:
|
||||
value = tmp[key_1]
|
||||
except KeyError:
|
||||
print_config_error(key_0, key_1, "", default, "unknown")
|
||||
return default
|
||||
|
||||
if len(value) == 0:
|
||||
print_config_error(key_0, key_1, value, default, "not_empty")
|
||||
|
||||
# Validate input
|
||||
if type_to_validate != "":
|
||||
# Need to validate
|
||||
if type_to_validate == "int":
|
||||
try:
|
||||
int(value)
|
||||
except ValueError:
|
||||
print_config_error(key_0, key_1, value, default, "int")
|
||||
return default
|
||||
if type_to_validate == "float":
|
||||
try:
|
||||
float(value)
|
||||
except ValueError:
|
||||
print_config_error(key_0, key_1, value, default, "float")
|
||||
return default
|
||||
|
||||
if type_to_validate == "bool":
|
||||
if str_to_bool(value) == None:
|
||||
print_config_error(key_0, key_1, value, default, "bool")
|
||||
return default
|
||||
|
||||
if len(valid_entries) > 0:
|
||||
# Need to validate the names
|
||||
try:
|
||||
valid_entries.index(value)
|
||||
except ValueError:
|
||||
print_config_error(
|
||||
key_0, key_1, value, default, "oneof", valid_entries=valid_entries
|
||||
)
|
||||
return default
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def print_config_error(
|
||||
key_0: str,
|
||||
key_1: str,
|
||||
value: str,
|
||||
default: str,
|
||||
expected: str,
|
||||
valid_entries: List[str] = [],
|
||||
msg: str = "",
|
||||
index: int = 1,
|
||||
):
|
||||
"""Print configuration errors to the shell
|
||||
|
||||
Args:
|
||||
key_0: The first key (top level)
|
||||
key_1: The second key (where the actual value is to be found)
|
||||
expected: The data type expected. If unknown key, set to "unknown" and set index; If should be one of, use "oneof" and set valid_entries list
|
||||
msg: The message to print
|
||||
index: The index in the chain (i.e. if key_0 or key_1)
|
||||
"""
|
||||
if not is_verbose:
|
||||
return
|
||||
|
||||
print(f" ==> Using default setting ({default}) for {key_0}.{key_1}")
|
||||
|
||||
if expected == "unknown":
|
||||
# The field was unknown
|
||||
print(f' -> Unknown field "{key_0 if index == 0 else key_1}"')
|
||||
elif expected == "oneof":
|
||||
print(
|
||||
f' -> Invalid name "{value}". Has to be one of', ", ".join(valid_entries)
|
||||
)
|
||||
elif expected == "not_empty":
|
||||
print(" -> Property is unexpectedly None")
|
||||
elif expected == "bool":
|
||||
print(f' -> Boolean property expected, but instead found "{value}".')
|
||||
else:
|
||||
print(f" -> Expected a config option of type {expected}.")
|
||||
|
||||
if msg != "":
|
||||
print(msg)
|
||||
@@ -1,18 +1,24 @@
|
||||
import struct
|
||||
|
||||
|
||||
# Decoder to decode various sent values from the microcontroller
|
||||
class Decoder:
|
||||
# Decode an ascii character
|
||||
def decode_ascii(self, value: bytes) -> str:
|
||||
try:
|
||||
return value.decode()
|
||||
except:
|
||||
return 'Error'
|
||||
return "Error"
|
||||
|
||||
# Decode a float (6 bits)
|
||||
def decode_float(self, value: bytes) -> float:
|
||||
return struct.unpack('>f', bytes.fromhex(str(value, 'ascii') + '00'))[0]
|
||||
return struct.unpack(">f", bytes.fromhex(str(value, "ascii") + "00"))[0]
|
||||
|
||||
# Decode a float, but with additional offsets
|
||||
def decode_float_long(self, value: bytes) -> float:
|
||||
return struct.unpack('>f', bytes.fromhex(str(value, 'ascii') + '0000'))[0]
|
||||
return struct.unpack(">f", bytes.fromhex(str(value, "ascii") + "0000"))[0]
|
||||
|
||||
# Decode an int
|
||||
def decode_int(self, value: bytes) -> int:
|
||||
# return int.from_bytes(value, 'big')
|
||||
return int(value, base=16)
|
||||
|
||||
@@ -2,7 +2,6 @@ from lib.com import ComSuperClass
|
||||
import lib.decoder
|
||||
import time
|
||||
|
||||
# TODO: Load filters (for comport search)
|
||||
decoder = lib.decoder.Decoder()
|
||||
|
||||
|
||||
@@ -12,10 +11,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
|
||||
@@ -39,11 +34,13 @@ class Instructions:
|
||||
|
||||
# Only run for a limited amount of time
|
||||
while time.time() - start < 5:
|
||||
# If the decoded ascii character is equal to the next expected character, move pointer right by one
|
||||
# If not, jump back to start
|
||||
if decoder.decode_ascii(self._com.receive(1)) == sequence[pointer]:
|
||||
# Receive and decode a single byte and decode as ASCII
|
||||
data = decoder.decode_ascii(self._com.receive(1))
|
||||
if data == sequence[pointer]:
|
||||
# Increment the pointer (move to next element in the List)
|
||||
pointer += 1
|
||||
else:
|
||||
# Jump back to start
|
||||
pointer = 0
|
||||
|
||||
# If the pointer has reached the end of the sequence, return True, as now the hook was successful
|
||||
@@ -53,7 +50,7 @@ class Instructions:
|
||||
# If we time out, which is the only way in which this code can be reached, return False
|
||||
return False
|
||||
|
||||
# Used to hook to the main data stream, as that hooking mechanism is differen
|
||||
# Used to hook to the main data stream, as that hooking mechanism is different
|
||||
def hook_main(self) -> bool:
|
||||
# Record start time to respond to timeout
|
||||
start = time.time()
|
||||
@@ -61,16 +58,26 @@ class Instructions:
|
||||
# Wait to find a CR character (enter)
|
||||
char = decoder.decode_ascii(self._com.receive(1))
|
||||
while char != "\n":
|
||||
# Check for timeout
|
||||
if time.time() - start > 3:
|
||||
return False
|
||||
|
||||
# Set the next character by receiving and decoding it as ASCII
|
||||
char = decoder.decode_ascii(self._com.receive(1))
|
||||
|
||||
# Store the position in the hooking process
|
||||
state = 0
|
||||
distance = 0
|
||||
|
||||
# While we haven't timed out and have not reached the last state execute
|
||||
# The last state indicates that the sync was successful
|
||||
while time.time() - start < 5 and state < 3:
|
||||
# Receive the next char and decode it as ASCII
|
||||
char = decoder.decode_ascii(self._com.receive(1))
|
||||
|
||||
# The character we look for when syncing is Space (ASCII char 32 (decimal))
|
||||
# It is sent every 4 bits. If we have received 3 with the correct distance from
|
||||
# the previous in a row, we are synced
|
||||
if char == " ":
|
||||
if distance == 4:
|
||||
state += 1
|
||||
@@ -82,6 +89,7 @@ class Instructions:
|
||||
else:
|
||||
distance += 1
|
||||
|
||||
# Read 5 more bits to correctly sync up
|
||||
self._com.receive(5)
|
||||
|
||||
return state == 3
|
||||
|
||||
169
lib/test/com.py
169
lib/test/com.py
@@ -3,7 +3,7 @@ Library to be used in standalone mode (without microcontroller, for testing func
|
||||
It simulates the behviour of an actual microcontroller being connected
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
import queue
|
||||
import random
|
||||
import time
|
||||
@@ -11,12 +11,17 @@ import struct
|
||||
|
||||
from lib.com import ComSuperClass
|
||||
|
||||
# ┌ ┐
|
||||
# │ Testing Module For Com │
|
||||
# └ ┘
|
||||
# This file contains a Com class that can be used to test the functionality
|
||||
# even without a microcontroller. It is not documented in a particularly
|
||||
# beginner-friendly way, nor is the code written with beginner-friendliness
|
||||
# in mind. It is the most complicated piece of code of the entire application
|
||||
|
||||
# All double __ prefixed properties and methods are not available in the actual one
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
|
||||
# All double __ prefixed properties and methods are not available in the actual impl
|
||||
|
||||
instruction_lut: dict[str, list[str]] = {
|
||||
"PR": ["\n", "P", "R", "\n"],
|
||||
@@ -26,14 +31,31 @@ instruction_lut: dict[str, list[str]] = {
|
||||
"FM": ["\n", "F", "M", "\n"],
|
||||
}
|
||||
|
||||
reconfig = ["a", "b", "c", "t"]
|
||||
|
||||
|
||||
class SimulationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SensorConfig:
|
||||
a: float
|
||||
b: float
|
||||
c: float
|
||||
t: float
|
||||
|
||||
def __init__(
|
||||
self, a: float = 20, b: float = 30, c: float = 10, t: float = 55
|
||||
) -> None:
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.t = t
|
||||
|
||||
|
||||
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")
|
||||
@@ -43,6 +65,17 @@ class Com(ComSuperClass):
|
||||
self.__simulated_data: queue.Queue[bytes] = queue.Queue()
|
||||
self.__simulated_data_remaining = 0
|
||||
|
||||
self.__reconf_sensor = 0
|
||||
self.__reconf_step = 0
|
||||
self.__fail_sim = fail_sim
|
||||
|
||||
self.__config: List[SensorConfig] = [
|
||||
SensorConfig(),
|
||||
SensorConfig(),
|
||||
SensorConfig(),
|
||||
SensorConfig(),
|
||||
]
|
||||
|
||||
# Initially, we are in normal mode (which leads to slower data intervals)
|
||||
self.__mode = "NM"
|
||||
|
||||
@@ -51,11 +84,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
|
||||
@@ -71,7 +104,7 @@ class Com(ComSuperClass):
|
||||
|
||||
for _ in range(byte_count):
|
||||
if self.__mode == "NM":
|
||||
time.sleep(0.001)
|
||||
time.sleep(0.005)
|
||||
try:
|
||||
data.append(self.__simulated_data.get_nowait())
|
||||
self.__simulated_data_remaining -= 1
|
||||
@@ -81,43 +114,60 @@ class Com(ComSuperClass):
|
||||
"Simulation encountered an error with the simulation queue. The error encountered: \n"
|
||||
+ str(e)
|
||||
)
|
||||
return b''.join(data)
|
||||
return b"".join(data)
|
||||
|
||||
def send(self, msg: str) -> None:
|
||||
# Using LUT to reference
|
||||
readback = instruction_lut.get(msg)
|
||||
if readback != None:
|
||||
for i in range(len(readback)):
|
||||
self.__simulated_data.put(bytes(readback[i], "ascii"))
|
||||
self.__add_ascii_char(readback[i])
|
||||
if msg == "RD":
|
||||
# Handle ReadData readback
|
||||
# self.__simulated_data.put(ord(""))
|
||||
pass
|
||||
self.__set_read_data_data()
|
||||
elif msg == "PR":
|
||||
self.__reconf_sensor = 0
|
||||
self.__reconf_step = 0
|
||||
self.__add_ascii_char("a")
|
||||
self.__add_ascii_char("0")
|
||||
self.__add_ascii_char("\n")
|
||||
|
||||
def __set_read_data_data(self) -> None:
|
||||
# Send data for all four sensors
|
||||
for i in range(4):
|
||||
self.__add_float_as_hex(self.__config[i].a)
|
||||
self.__add_ascii_char(" ")
|
||||
self.__add_float_as_hex(self.__config[i].b)
|
||||
self.__add_ascii_char(" ")
|
||||
self.__add_float_as_hex(self.__config[i].c)
|
||||
self.__add_ascii_char(" ")
|
||||
self.__add_float_as_hex(self.__config[i].t)
|
||||
self.__add_ascii_char("\n")
|
||||
|
||||
def send_float(self, msg: float) -> None:
|
||||
# Encode float as 8 bytes (64 bit)
|
||||
ba = struct.pack("d", msg)
|
||||
for byte in ba:
|
||||
self.__simulated_data.put(byte.to_bytes())
|
||||
if self.__reconf_step == 0:
|
||||
self.__config[self.__reconf_sensor].a = msg
|
||||
elif self.__reconf_step == 1:
|
||||
self.__config[self.__reconf_sensor].b = msg
|
||||
elif self.__reconf_step == 2:
|
||||
self.__config[self.__reconf_sensor].c = msg
|
||||
elif self.__reconf_step == 3:
|
||||
self.__config[self.__reconf_sensor].t = msg
|
||||
|
||||
def __fill_queue_alternative(self):
|
||||
for _ in range(4):
|
||||
for _ in range(4):
|
||||
self.__simulated_data.put(random.randbytes(1))
|
||||
self.__simulated_data.put(bytes(" ", "ascii"))
|
||||
for _ in range(6):
|
||||
self.__simulated_data.put(random.randbytes(1))
|
||||
self.__simulated_data.put(bytes(" ", "ascii"))
|
||||
for _ in range(3):
|
||||
for _ in range(4):
|
||||
self.__simulated_data.put(random.randbytes(1))
|
||||
self.__simulated_data.put(bytes(" ", "ascii"))
|
||||
for _ in range(4):
|
||||
self.__simulated_data.put(random.randbytes(1))
|
||||
self.__simulated_data.put(bytes("\n", "ascii"))
|
||||
self.__simulated_data_remaining = 68
|
||||
if self.__reconf_step == 3:
|
||||
self.__reconf_step = 0
|
||||
self.__reconf_sensor += 1
|
||||
else:
|
||||
self.__reconf_step += 1
|
||||
|
||||
if self.__reconf_sensor == 4:
|
||||
return
|
||||
|
||||
self.__add_ascii_char(reconfig[self.__reconf_step])
|
||||
self.__add_ascii_char(str(self.__reconf_sensor))
|
||||
self.__add_ascii_char("\n")
|
||||
|
||||
def __fill_queue(self):
|
||||
# Simulate a full cycle
|
||||
for _ in range(4):
|
||||
self.__add_integer_as_hex(self.__generate_random_int(200))
|
||||
self.__simulated_data.put(bytes(" ", "ascii"))
|
||||
@@ -127,10 +177,10 @@ class Com(ComSuperClass):
|
||||
for _ in range(3):
|
||||
self.__add_integer_as_hex(self.__generate_random_int(65535))
|
||||
self.__simulated_data.put(bytes(" ", "ascii"))
|
||||
self.__simulated_data_remaining += 1
|
||||
self.__add_integer_as_hex(self.__generate_random_int(65535))
|
||||
self.__simulated_data.put(bytes("\n", "ascii"))
|
||||
self.__simulated_data_remaining += 4
|
||||
print("Length:", self.__simulated_data_remaining)
|
||||
self.__simulated_data_remaining += 1
|
||||
|
||||
def __generate_random_int(self, max: int) -> int:
|
||||
return random.randint(0, max)
|
||||
@@ -138,11 +188,50 @@ class Com(ComSuperClass):
|
||||
def __generate_random_float(self, max: int) -> float:
|
||||
return random.random() * max
|
||||
|
||||
def __add_character_as_hex(self, data: str):
|
||||
pass
|
||||
def __add_ascii_char(self, ascii_string: str):
|
||||
self.__simulated_data.put(ord(ascii_string).to_bytes(1))
|
||||
self.__simulated_data_remaining += 1
|
||||
|
||||
def __add_integer_as_hex(self, data: int):
|
||||
pass
|
||||
def __add_two_byte_value(self, c: int):
|
||||
"""putchhex
|
||||
|
||||
def __add_float_as_hex(self, data: float):
|
||||
pass
|
||||
Args:
|
||||
c: The char (as integer)
|
||||
"""
|
||||
# First nibble (high)
|
||||
high_nibble = (c >> 4) & 0x0F
|
||||
high_char = chr(high_nibble + 48 if high_nibble < 10 else high_nibble + 55)
|
||||
self.__simulated_data.put(high_char.encode())
|
||||
|
||||
# Second nibble (low)
|
||||
low_nibble = c & 0x0F
|
||||
low_char = chr(low_nibble + 48 if low_nibble < 10 else low_nibble + 55)
|
||||
self.__simulated_data.put(low_char.encode())
|
||||
self.__simulated_data_remaining += 2
|
||||
|
||||
def __add_integer_as_hex(self, c: int):
|
||||
"""Writes the hexadecimal representation of the high and low bytes of integer `c` (16-bit) to the simulated serial port."""
|
||||
if not (0 <= c <= 0xFFFF):
|
||||
raise ValueError("Input must be a 16-bit integer (0–65535)")
|
||||
|
||||
# Get high byte (most significant byte)
|
||||
hi_byte = (c >> 8) & 0xFF
|
||||
# Get low byte (least significant byte)
|
||||
lo_byte = c & 0xFF
|
||||
|
||||
# Call putchhex for the high byte and low byte
|
||||
self.__add_two_byte_value(hi_byte)
|
||||
self.__add_two_byte_value(lo_byte)
|
||||
|
||||
def __add_float_as_hex(self, f: float):
|
||||
"""Converts a float to its byte representation and sends the bytes using putchhex."""
|
||||
# Pack the float into bytes (IEEE 754 format)
|
||||
packed = struct.pack(">f", f) # Big-endian format (network byte order)
|
||||
|
||||
# Unpack the bytes into 3 bytes: high, mid, low
|
||||
high, mid, low = packed[0], packed[1], packed[2]
|
||||
|
||||
# Send each byte as hex
|
||||
self.__add_two_byte_value(high)
|
||||
self.__add_two_byte_value(mid)
|
||||
self.__add_two_byte_value(low)
|
||||
|
||||
44
package.sh
Executable file
44
package.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
|
||||
# packaging script
|
||||
rm -rf ./build/
|
||||
rm -rf ./dist/
|
||||
|
||||
# build windows package
|
||||
wine pyinstaller BiogasControllerApp.spec
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "\nBuild unsuccessful, aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build successful
|
||||
cp -r ./gui ./dist
|
||||
cp -r ./lib ./dist
|
||||
cp ./biogascontrollerapp.py ./dist/
|
||||
cp ./BiogasControllerAppLogo.png ./dist/
|
||||
cp ./changelog ./dist/
|
||||
cp ./config.ini ./dist/
|
||||
cp ./README.md ./dist/
|
||||
cp ./requirements.txt ./dist/
|
||||
cp ./SECURITY.md ./dist/
|
||||
|
||||
# Remove build directories
|
||||
rm -rf ./build/
|
||||
|
||||
rm -rf ./dist/biogascontrollerapp/
|
||||
|
||||
|
||||
# Create Windows archive (zip)
|
||||
zip -9r BiogasControllerApp-Windows.zip ./dist
|
||||
|
||||
# Create Linux archive (tar)
|
||||
rm ./dist/BiogasControllerApp.exe
|
||||
cp ./install-linux.sh ./dist/
|
||||
cp ./launch.sh ./dist/
|
||||
|
||||
ouch compress -y ./dist/ biogascontrollerapp-linux.tar.gz
|
||||
|
||||
rm -rf ./dist
|
||||
|
||||
echo "Done!"
|
||||
@@ -6,53 +6,85 @@ n = int(input("Sensor number to be printed: "))
|
||||
|
||||
file = ""
|
||||
|
||||
|
||||
def generate_plot():
|
||||
reader = csv.reader(file, delimiter=',')
|
||||
# Read data using the CSV library
|
||||
reader = csv.reader(file, delimiter=",")
|
||||
|
||||
# Create a list from the data
|
||||
data = list(reader)
|
||||
data.sort(key=lambda imp: float(imp[2]))
|
||||
lenght = len(data)
|
||||
|
||||
# Sort the list using a lambda sort descriptor
|
||||
# A lambda function is an anonymous function (= an unnamed function),
|
||||
# which makes it convenient. A sort descriptor is a function that
|
||||
# (usually, but not here) returns a value indicating which of two values
|
||||
# come before or after in the ordering.
|
||||
# Here, instead we simply return a floating point value for each data point
|
||||
data.sort(key=lambda data_point: float(data_point[2]))
|
||||
|
||||
# Store the x and y coordinates in two arrays
|
||||
x = []
|
||||
y = []
|
||||
|
||||
for _ in range(lenght):
|
||||
extract = data.pop(0)
|
||||
sensor = int(extract.pop(0))
|
||||
for _ in range(len(data)):
|
||||
# Extract the data point
|
||||
data_point = data.pop(0)
|
||||
sensor = int(data_point.pop(0))
|
||||
if sensor == n:
|
||||
ye = extract.pop(0)
|
||||
xe = extract.pop(0)
|
||||
y.append(float(ye))
|
||||
x.append(float(xe))
|
||||
y.append(float(data_point.pop(0)))
|
||||
x.append(float(data_point.pop(0)))
|
||||
|
||||
# Use Numpy's polyfit function to fit a 2nd degree polynomial to the points using quadratic regression
|
||||
# This function returns an array with the coefficients
|
||||
fit = np.polyfit(x, y, 2)
|
||||
|
||||
# The formula to output to the plot
|
||||
formula = f"F(U) = {round(float(fit[0]), 4)}U^2+{round(float(fit[1]), 4)}U+{round(float(fit[2]), 4)}"
|
||||
|
||||
fit_fn = np.poly1d(fit)
|
||||
# Create a fit function from the previously determined coefficients
|
||||
fit_fn = np.poly1d(fit) # Returns a function that takes a list of x-coordinate as argument
|
||||
|
||||
# Plot the line on the graph
|
||||
plt.plot(x, fit_fn(x), color="BLUE", label="T(U)")
|
||||
|
||||
# Scatter Plot the data points that we have
|
||||
plt.scatter(x, y, color="MAGENTA", marker="o", label="Data")
|
||||
|
||||
# Label the graph
|
||||
plt.ylabel("Temperature")
|
||||
plt.xlabel("Voltage")
|
||||
title = 'Sensor MCP9701A #{}'.format(n)
|
||||
plt.title(title)
|
||||
plt.title("Sensor MCP9701A #{}".format(n))
|
||||
|
||||
# Scale the axis appropriately
|
||||
plt.axis((0.6, 2.0, 15.0, 70.0))
|
||||
|
||||
# Print a legend and set the graph to be annotated
|
||||
plt.legend(loc="lower right")
|
||||
plt.annotate(formula, xy=(0.85, 60))
|
||||
|
||||
# Enable the background grid
|
||||
plt.grid(True)
|
||||
|
||||
# Finally, show the graph
|
||||
plt.show()
|
||||
|
||||
# Get user input whether to save the plot or not
|
||||
saveit = input("Do you wish to save the plot? (y/N) ").lower()
|
||||
|
||||
if saveit == "y":
|
||||
plt.savefig("Sensor"+str(n)+".png")
|
||||
plt.savefig("Sensor"+str(n)+".pdf", format="pdf")
|
||||
plt.savefig("Sensor"+str(n)+".svg", format="svg")
|
||||
# Save the plot as Sensor[Number] (e.g. Sensor9) as png, pdf and svg
|
||||
plt.savefig("Sensor" + str(n) + ".png")
|
||||
plt.savefig("Sensor" + str(n) + ".pdf", format="pdf")
|
||||
plt.savefig("Sensor" + str(n) + ".svg", format="svg")
|
||||
print("==> Images saved")
|
||||
else:
|
||||
print("==> Images discarded")
|
||||
|
||||
|
||||
# Since we have defined a function above as a function, this here is executed first
|
||||
filename = input("Please enter a file path to the csv file to be plotted: ")
|
||||
|
||||
# Try to open the file
|
||||
try:
|
||||
file = open(filename, "r")
|
||||
generate_plot()
|
||||
|
||||
@@ -4,29 +4,36 @@ import matplotlib.pyplot as plt
|
||||
import csv
|
||||
import os
|
||||
|
||||
# Get user input for various data
|
||||
path = input("Path to csv-file to be plotted: ")
|
||||
print("For the below, it is recommended to enter data in this format: yyyy-mm-dd-hh-mm")
|
||||
date = input("Date & time at which the measurement was taken (approx.): ")
|
||||
group = input("Group-name: ")
|
||||
saveit = input("Should the graph be saved? (y/n) ").lower()
|
||||
|
||||
imp = open(path, "r")
|
||||
reader = csv.reader(imp, delimiter=',')
|
||||
rohdaten = list(reader)
|
||||
lenght = len(rohdaten)
|
||||
reader = csv.reader(imp, delimiter=",")
|
||||
data = list(reader)
|
||||
x = []
|
||||
y = []
|
||||
for i in range(lenght):
|
||||
extract = rohdaten.pop(0)
|
||||
for i in range(len(data)):
|
||||
# Extract the data
|
||||
extract = data.pop(0)
|
||||
x.append(float(extract.pop(0)))
|
||||
y.append(float(extract.pop(0)))
|
||||
|
||||
# Set up plot
|
||||
plt.plot(x, y, color="MAGENTA")
|
||||
plt.xlabel("Time")
|
||||
plt.ylabel("Voltage")
|
||||
title = f"GC - Biogasanlage {date}"
|
||||
plt.title(title)
|
||||
|
||||
plt.title(f"GC - Biogasanlage {date}")
|
||||
plt.grid(True)
|
||||
if saveit == "y":
|
||||
|
||||
# Check if user wants to save the image
|
||||
if saveit == "n":
|
||||
print("didn't save images")
|
||||
else:
|
||||
pos = 0
|
||||
for letter in path[::-1]:
|
||||
if letter == "/":
|
||||
@@ -40,11 +47,7 @@ if saveit == "y":
|
||||
os.mkdir(save_path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
plt.savefig(save_path)
|
||||
os.rename(f"{save_path}/.png", f"{save_path}/GC-{date}-{group}.png")
|
||||
print(f"saved images to {save_path}")
|
||||
else:
|
||||
print("didn't save images")
|
||||
plt.savefig(f"{save_path}/GC-{date}-{group}.png")
|
||||
|
||||
print(f"Saved images to {save_path}")
|
||||
plt.show()
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
kivy[base]
|
||||
pyserial
|
||||
kivy[base]==2.3.1
|
||||
kivymd==1.1.1
|
||||
pyserial==3.5
|
||||
|
||||
Reference in New Issue
Block a user