6 Commits

Author SHA1 Message Date
00773612c3 Version bump, fix spec file issues 2025-06-30 08:20:32 +02:00
bc39e7a50d Version bump
I had forgotten to install the kivymd dependency in the python install
through wine, so that was missing leading to a non-functional build.
Fixed now
2025-06-30 08:04:51 +02:00
efa6bca56c Refactor for some name changes of libraries 2025-06-22 13:22:14 +02:00
4af20a9a91 Add packaging script 2025-06-20 14:25:15 +02:00
b0bd5f446f Finish V3.1.1 version bump 2025-06-20 14:13:59 +02:00
265288106e Fix issue with config parsing for filtering of cable 2025-06-20 14:07:26 +02:00
17 changed files with 262 additions and 129 deletions

View File

@@ -1,5 +1,6 @@
# -*- mode: python ; coding: utf-8 -*-
from kivy_deps import sdl2, glew
from kivymd import hooks_path as kivymd_hooks_path
block_cipher = None
@@ -10,7 +11,7 @@ a = Analysis(
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hookspath=[kivymd_hooks_path],
hooksconfig={},
runtime_hooks=[],
excludes=[],

View File

@@ -26,7 +26,7 @@
BiogasControllerApp has just received a major rewrite, where I focused on code-readability, documentation and stability. The documentation in the code is aimed at beginners and does contain some unnecessary extra comments
If you are here to read the code, the files you are most likely looking for can be found in the `lib` folder. If you want to understand and have a look at all of the application, start with the `biogascontrollerapp.py` file
***If you are here to read the code, the files you are most likely looking for can be found in the `util` folder. If you want to understand and have a look at all of the application, start with the `biogascontrollerapp.py` file***
# Installation
To install it, navigate to the releases tab on the right hand side. Click the current release, scroll down to assets and select the version appropriate for your operating system.

View File

@@ -9,7 +9,7 @@ Only Version 3.1 and later are supported due to the poor code quality of V2.3.0
| Version | Supported |
| ------- | ------------------ |
| 3.1.X | ✅ |
| 3.0.X | |
| 3.0.X | |
| 2.3.0 | ❎ |
| 2.2.0 | ❎ |
| 2.1.0 | ❎ |

View File

@@ -4,7 +4,11 @@
# ╰────────────────────────────────────────────────╯
#
# So you would like to read the source code? Nice!
# Just be warned, this application uses Thread and a UI Toolkit called
#
# If you simply want to know how the connection stuff works, then head to
# the util/ folder and check out the com.py file!
#
# Just be warned, this application uses Threads and a UI Toolkit called
# Kivy to run. If you are unsure of what functions do, consider
# checking out the kivy docs at https://kivy.org/doc.
# It also uses the pyserial library for communication with the micro-
@@ -12,9 +16,30 @@
#
# ────────────────────────────────────────────────────────────────────
# Print a welcome message
print(
"""
┏━━┓━━━━━━━━━━━━━━━━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━┏┓━┏┓━━━━━━━━┏━━━┓━━━━━━━━
┃┏┓┃━━━━━━━━━━━━━━━━━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━┃┃━┃┃━━━━━━━━┃┏━┓┃━━━━━━━━
┃┗┛┗┓┏┓┏━━┓┏━━┓┏━━┓━┏━━┓┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓┃┃━┃┃━┏━━┓┏━┓┃┃━┃┃┏━━┓┏━━┓
┃┏━┓┃┣┫┃┏┓┃┃┏┓┃┗━┓┃━┃━━┫┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┃┏┓┃┃┃━┃┃━┃┏┓┃┃┏┛┃┗━┛┃┃┏┓┃┃┏┓┃
┃┗━┛┃┃┃┃┗┛┃┃┗┛┃┃┗┛┗┓┣━━┃┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┃┃┗┓┃┗┓┃┃━┫┃┃━┃┏━┓┃┃┗┛┃┃┗┛┃
┗━━━┛┗┛┗━━┛┗━┓┃┗━━━┛┗━━┛┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━┛┗━┛┗━┛┗━━┛┗┛━┗┛━┗┛┃┏━┛┃┏━┛
━━━━━━━━━━━┏━┛┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━┃┃━━
━━━━━━━━━━━┗━━┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━┗┛━━
Version 3.2.2
=> Initializing....
"""
)
# Load the config file
import time
from lib.config import read_config, set_verbosity, str_to_bool
import sys
from kivy.resources import resource_add_path
from util.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
@@ -50,8 +75,9 @@ if str_to_bool(
import os
from typing import override
from lib.com import Com, ComSuperClass
import lib.test.com
from util.com import Com
from util.interface import ControllerConnection
import util.test.com
# Load config and disable kivy log if necessary
@@ -120,19 +146,19 @@ class BiogasControllerApp(MDApp):
def build(self):
# Configure com
filters = [
x
x.strip()
for x in read_config(
"Connection",
"filters",
"USB-Serial Controller, Prolific USB-Serial Controller",
).split(",")
"USB-Serial Controller; Prolific USB-Serial Controller",
).split(";")
]
baudrate = int(
read_config("Connection", "baudrate", "19200", type_to_validate="int")
)
com: ComSuperClass = Com(
com: ControllerConnection = Com(
baudrate,
filters,
)
@@ -140,7 +166,7 @@ class BiogasControllerApp(MDApp):
if str_to_bool(
read_config("Dev", "use_test_library", "False", type_to_validate="bool")
):
com = lib.test.com.Com(
com = util.test.com.Com(
int(read_config("Dev", "fail_sim", "20", type_to_validate="int")),
baudrate,
filters,
@@ -163,7 +189,7 @@ class BiogasControllerApp(MDApp):
print("\n", "-" * 20, "\n")
self.icon = "./BiogasControllerAppLogo.png"
self.title = "BiogasControllerApp-V3.1.0"
self.title = "BiogasControllerApp-V3.2.2"
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"))
@@ -178,22 +204,16 @@ class BiogasControllerApp(MDApp):
# Disallow this file to be imported
if __name__ == "__main__":
print(
"""
┏━━┓━━━━━━━━━━━━━━━━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━┏┓━┏┓━━━━━━━━┏━━━┓━━━━━━━━
┃┏┓┃━━━━━━━━━━━━━━━━━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━┃┃━┃┃━━━━━━━━┃┏━┓┃━━━━━━━━
┃┗┛┗┓┏┓┏━━┓┏━━┓┏━━┓━┏━━┓┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓┃┃━┃┃━┏━━┓┏━┓┃┃━┃┃┏━━┓┏━━┓
┃┏━┓┃┣┫┃┏┓┃┃┏┓┃┗━┓┃━┃━━┫┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┃┏┓┃┃┃━┃┃━┃┏┓┃┃┏┛┃┗━┛┃┃┏┓┃┃┏┓┃
┃┗━┛┃┃┃┃┗┛┃┃┗┛┃┃┗┛┗┓┣━━┃┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┃┃┗┓┃┗┓┃┃━┫┃┃━┃┏━┓┃┃┗┛┃┃┗┛┃
┗━━━┛┗┛┗━━┛┗━┓┃┗━━━┛┗━━┛┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━┛┗━┛┗━┛┗━━┛┗┛━┗┛━┗┛┃┏━┛┃┏━┛
━━━━━━━━━━━┏━┛┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━┃┃━━
━━━━━━━━━━━┗━━┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━┗┛━━
Version 3.1.0
=> Initializing....
"""
)
set_verbosity(verbose)
# Start the application
try:
if hasattr(sys, '_MEIPASS'):
resource_add_path(os.path.join(sys._MEIPASS))
BiogasControllerApp().run()
except Exception as e:
print("Failed to run BiogasControllerApp!")
if verbose:
print(e)
input("Press enter to continue.")
print("\n => Exiting!")

View File

@@ -1,4 +1,15 @@
***CHANGELOG***
V3.2.2
- Fix issues with Windows distributable: kivymd build
V3.2.1
- Fix issue with Windows distributable: kivymd not found
V3.2.0
- Fixed a bug with comport assignment
- Refactored some naming
- Added more comments
V3.1.0
- Completely redesigned User Interface using KivyMD
- Added config option for themes

View File

@@ -2,8 +2,8 @@
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.
filters = USB-Serial Controller, Prolific USB-Serial Controller
# For ENATECH, the below is likely correct. The name cannot contain a semicolon
filters = USB-Serial Controller; Prolific USB-Serial Controller
[UI]
height = 600

View File

@@ -40,7 +40,7 @@
on_release: root.quit()
MDLabel:
text: "You are running version V3.1.0"
text: "You are running version V3.2.2"
font_size: 13
pos_hint: {"y": -0.45, "x":0}
halign: 'center'

View File

@@ -4,9 +4,10 @@ from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.screen import MDScreen
from kivy.lang import Builder
from lib.com import ComSuperClass
import platform
from util.interface import ControllerConnection
# Information for errors encountered when using pyserial
information = {
@@ -25,7 +26,7 @@ information = {
# 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):
def __init__(self, com: ControllerConnection, **kw):
self._com = com
self.connection_error_dialog = MDDialog(
title="Connection",

View File

@@ -10,9 +10,9 @@ import queue
import threading
# Load utilities
from lib.instructions import Instructions
from lib.com import ComSuperClass
from lib.decoder import Decoder
from util.instructions import Instructions
from util.interface import ControllerConnection
from util.decoder import Decoder
# TODO: Consider consolidating start and stop button
@@ -27,13 +27,13 @@ synced_queue: queue.Queue[List[str]] = queue.Queue()
# ╰────────────────────────────────────────────────╯
# Using a Thread to run this in parallel to the UI to improve responsiveness
class ReaderThread(threading.Thread):
_com: ComSuperClass
_com: ControllerConnection
_decoder: Decoder
_instructions: Instructions
# This method allows the user to set Com object to be used.
# The point of this is to allow for the use of a single Com object to not waste resources
def set_com(self, com: ComSuperClass):
def set_com(self, com: ControllerConnection):
"""Set the Com object to be used in this
Args:
@@ -106,7 +106,7 @@ class MainScreen(MDScreen):
# The constructor if this class takes a Com object to share one between all screens
# to preserve resources and make handling better
def __init__(self, com: ComSuperClass, **kw):
def __init__(self, com: ControllerConnection, **kw):
# Set some variables
self._com = com
self._event = None

View File

@@ -1,11 +1,11 @@
from typing import List
from kivymd.uix.screen import MDScreen
from kivy.lang import Builder
from lib.decoder import Decoder
from lib.instructions import Instructions
from util.decoder import Decoder
from util.instructions import Instructions
from util.instructions import ControllerConnection
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from lib.com import ComSuperClass
from kivy.clock import Clock
@@ -15,7 +15,7 @@ name_map = ["a", "b", "c", "t"]
class ProgramScreen(MDScreen):
def __init__(self, com: ComSuperClass, **kw):
def __init__(self, com: ControllerConnection, **kw):
self._com = com
self._instructions = Instructions(com)
self._decoder = Decoder()

44
package.sh Executable file
View 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 ./util ./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!"

View File

@@ -1,71 +1,9 @@
from abc import ABC, abstractmethod
from typing import Optional
from typing import override
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:
self._serial: Optional[serial.Serial] = None
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:
return self._err
@abstractmethod
def get_comport(self) -> str:
pass
@abstractmethod
def connect(self) -> bool:
pass
@abstractmethod
def close(self) -> None:
pass
@abstractmethod
def receive(self, byte_count: int) -> bytes:
pass
@abstractmethod
def send(self, msg: str) -> None:
pass
@abstractmethod
def send_float(self, msg: float) -> None:
pass
from util.interface import ControllerConnection
# ┌ ┐
@@ -76,8 +14,13 @@ class ComSuperClass(ABC):
# 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
# All variables starting in self are bound to the object and can be changed by any consumer of this library. The Com class
# inherits from the ControllerConnection class (found in interface.py), which implements some of the methods (functions)
# this class exposes, namely the constructor, set_port_override and get_error. They are not further relevant for the code below
# though, so you can safely ignore it.
class Com(ComSuperClass):
class Com(ControllerConnection):
def _connection_check(self) -> bool:
if self._serial == None:
return self._open()
@@ -88,6 +31,7 @@ class Com(ComSuperClass):
else:
return False
@override
def get_comport(self) -> str:
"""Find the comport the microcontroller has attached to"""
if self._port_override != "":
@@ -109,7 +53,7 @@ class Com(ComSuperClass):
return ""
def _open(self) -> bool:
"""Open the connection. Internal function, not to be called directly
"""Open the connection. Internal function, not to be called directly, use connect instead
Returns:
Boolean indicates if connection was successful or not
@@ -118,7 +62,7 @@ class Com(ComSuperClass):
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:
@@ -135,10 +79,12 @@ class Com(ComSuperClass):
# Haven't found a comport
return False
@override
def connect(self) -> bool:
"""Try to find a comport and connect to the microcontroller. Returns the success as a boolean"""
return self._connection_check()
@override
def close(self) -> None:
"""Close the serial connection, if possible"""
if self._serial != None:
@@ -147,6 +93,7 @@ class Com(ComSuperClass):
except:
pass
@override
def receive(self, byte_count: int) -> bytes:
"""Receive bytes from microcontroller over serial. Returns bytes. Might want to decode using functions from lib.decoder"""
# Check connection
@@ -160,6 +107,7 @@ class Com(ComSuperClass):
else:
raise Exception("ERR_CONNECTING")
@override
def send(self, msg: str) -> None:
"""Send a string over serial connection. Will open a connection if none is available"""
# Check connection
@@ -173,6 +121,7 @@ class Com(ComSuperClass):
else:
raise Exception("ERR_CONNECTING")
@override
def send_float(self, msg: float) -> None:
"""Send a float number over serial connection"""
# Check connection

View File

@@ -1,3 +1,5 @@
# This library is used to validate the config file
import configparser
from typing import List
@@ -12,6 +14,7 @@ global is_verbose
is_verbose = True
# Set the verbosity if needed
def set_verbosity(verbose: bool):
global is_verbose
is_verbose = verbose

View File

@@ -1,14 +1,15 @@
from lib.com import ComSuperClass
import lib.decoder
import util.decoder
import time
decoder = lib.decoder.Decoder()
from util.interface import ControllerConnection
decoder = util.decoder.Decoder()
# Class that supports sending instructions to the microcontroller,
# as well as hooking to data stream according to protocol
class Instructions:
def __init__(self, com: ComSuperClass) -> None:
def __init__(self, com: ControllerConnection) -> None:
self._com = com
# Helper method to hook to the data stream according to protocol.

66
util/interface.py Normal file
View File

@@ -0,0 +1,66 @@
from abc import ABC, abstractmethod
from typing import Optional
import serial
# If you don't know what OOP is, you can safely ignore this file
#
# The below class is abstract to have a consistent, targetable interface
# for both the real connection module and the simulation module
#
# For the interested, a quick rundown of what the benefits are 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 ControllerConnection(ABC):
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._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:
return self._err
@abstractmethod
def get_comport(self) -> str:
pass
@abstractmethod
def connect(self) -> bool:
pass
@abstractmethod
def close(self) -> None:
pass
@abstractmethod
def receive(self, byte_count: int) -> bytes:
pass
@abstractmethod
def send(self, msg: str) -> None:
pass
@abstractmethod
def send_float(self, msg: float) -> None:
pass

View File

@@ -3,14 +3,8 @@ 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 List, Optional
import queue
import random
import time
import struct
from lib.com import ComSuperClass
# ────────────────────────────────────────────────────────────────────
# ┌ ┐
# │ Testing Module For Com │
# └ ┘
@@ -18,9 +12,50 @@ from lib.com import ComSuperClass
# 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
# ────────────────────────────────────────────────────────────────────
# Just be warned, more OOP concepts and less documentation can be found here.
# Code starts here
# ────────────────────────────────────────────────────────────────────
from typing import List, Optional, override
import queue
import random
import time
import struct
from util.interface import ControllerConnection
# All double __ prefixed properties and methods are not available in the actual impl
instruction_lut: dict[str, list[str]] = {
@@ -53,7 +88,7 @@ class SensorConfig:
self.t = t
class Com(ComSuperClass):
class Com(ControllerConnection):
def __init__(
self, fail_sim: int, baudrate: int = 19200, filters: Optional[list[str]] = None
) -> None:
@@ -79,13 +114,11 @@ class Com(ComSuperClass):
# Initially, we are in normal mode (which leads to slower data intervals)
self.__mode = "NM"
def set_port_override(self, override: str) -> None:
"""Set the port override, to disable port search"""
self._port_override = override
@override
def get_comport(self) -> str:
return "Sim" if self._port_override == "" else self._port_override
@override
def connect(self) -> bool:
# Randomly return false in 1 in fail_sim ish cases
if random.randint(0, self.__fail_sim) == 0:
@@ -93,9 +126,11 @@ class Com(ComSuperClass):
return False
return True
@override
def close(self) -> None:
pass
@override
def receive(self, byte_count: int) -> bytes:
data = []
# If queue is too short, refill it
@@ -116,6 +151,7 @@ class Com(ComSuperClass):
)
return b"".join(data)
@override
def send(self, msg: str) -> None:
# Using LUT to reference
readback = instruction_lut.get(msg)
@@ -143,6 +179,7 @@ class Com(ComSuperClass):
self.__add_float_as_hex(self.__config[i].t)
self.__add_ascii_char("\n")
@override
def send_float(self, msg: float) -> None:
if self.__reconf_step == 0:
self.__config[self.__reconf_sensor].a = msg