diff --git a/README.md b/README.md index 880ca73..8c4aa54 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/biogascontrollerapp.py b/biogascontrollerapp.py index 6d857ed..d701d37 100644 --- a/biogascontrollerapp.py +++ b/biogascontrollerapp.py @@ -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,27 @@ # # ──────────────────────────────────────────────────────────────────── +# Print a welcome message +print( + """ +┏━━┓━━━━━━━━━━━━━━━━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━┏┓━┏┓━━━━━━━━┏━━━┓━━━━━━━━ +┃┏┓┃━━━━━━━━━━━━━━━━━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━┃┃━┃┃━━━━━━━━┃┏━┓┃━━━━━━━━ +┃┗┛┗┓┏┓┏━━┓┏━━┓┏━━┓━┏━━┓┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓┃┃━┃┃━┏━━┓┏━┓┃┃━┃┃┏━━┓┏━━┓ +┃┏━┓┃┣┫┃┏┓┃┃┏┓┃┗━┓┃━┃━━┫┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┃┏┓┃┃┃━┃┃━┃┏┓┃┃┏┛┃┗━┛┃┃┏┓┃┃┏┓┃ +┃┗━┛┃┃┃┃┗┛┃┃┗┛┃┃┗┛┗┓┣━━┃┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┃┃┗┓┃┗┓┃┃━┫┃┃━┃┏━┓┃┃┗┛┃┃┗┛┃ +┗━━━┛┗┛┗━━┛┗━┓┃┗━━━┛┗━━┛┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━┛┗━┛┗━┛┗━━┛┗┛━┗┛━┗┛┃┏━┛┃┏━┛ +━━━━━━━━━━━┏━┛┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━┃┃━━ +━━━━━━━━━━━┗━━┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━┗┛━━ + + Version 3.2.0 + + => Initializing.... + """ +) + # Load the config file import time -from lib.config import read_config, set_verbosity, str_to_bool +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 +72,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 @@ -132,7 +155,7 @@ class BiogasControllerApp(MDApp): read_config("Connection", "baudrate", "19200", type_to_validate="int") ) - com: ComSuperClass = Com( + com: ControllerConnection = Com( baudrate, filters, ) @@ -140,7 +163,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 +186,7 @@ class BiogasControllerApp(MDApp): print("\n", "-" * 20, "\n") self.icon = "./BiogasControllerAppLogo.png" - self.title = "BiogasControllerApp-V3.1.1" + self.title = "BiogasControllerApp-V3.2.0" self.screen_manager.add_widget(HomeScreen(com, name="home")) self.screen_manager.add_widget(MainScreen(com, name="main")) self.screen_manager.add_widget(ProgramScreen(com, name="program")) @@ -178,22 +201,8 @@ class BiogasControllerApp(MDApp): # Disallow this file to be imported if __name__ == "__main__": - print( - """ -┏━━┓━━━━━━━━━━━━━━━━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━┏┓━┏┓━━━━━━━━┏━━━┓━━━━━━━━ -┃┏┓┃━━━━━━━━━━━━━━━━━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━┃┃━┃┃━━━━━━━━┃┏━┓┃━━━━━━━━ -┃┗┛┗┓┏┓┏━━┓┏━━┓┏━━┓━┏━━┓┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓┃┃━┃┃━┏━━┓┏━┓┃┃━┃┃┏━━┓┏━━┓ -┃┏━┓┃┣┫┃┏┓┃┃┏┓┃┗━┓┃━┃━━┫┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┃┏┓┃┃┃━┃┃━┃┏┓┃┃┏┛┃┗━┛┃┃┏┓┃┃┏┓┃ -┃┗━┛┃┃┃┃┗┛┃┃┗┛┃┃┗┛┗┓┣━━┃┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┃┃┗┓┃┗┓┃┃━┫┃┃━┃┏━┓┃┃┗┛┃┃┗┛┃ -┗━━━┛┗┛┗━━┛┗━┓┃┗━━━┛┗━━┛┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━┛┗━┛┗━┛┗━━┛┗┛━┗┛━┗┛┃┏━┛┃┏━┛ -━━━━━━━━━━━┏━┛┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━┃┃━━ -━━━━━━━━━━━┗━━┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━┗┛━━ - - Version 3.1.1 - - => Initializing.... - """ - ) set_verbosity(verbose) + + # Start the application BiogasControllerApp().run() print("\n => Exiting!") diff --git a/changelog b/changelog index 3880c27..acf8f74 100644 --- a/changelog +++ b/changelog @@ -1,4 +1,9 @@ ***CHANGELOG*** +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 diff --git a/gui/home/home.kv b/gui/home/home.kv index 06ae68e..e3c55a8 100644 --- a/gui/home/home.kv +++ b/gui/home/home.kv @@ -40,7 +40,7 @@ on_release: root.quit() MDLabel: - text: "You are running version V3.1.1" + text: "You are running version V3.2.0" font_size: 13 pos_hint: {"y": -0.45, "x":0} halign: 'center' diff --git a/gui/home/home.py b/gui/home/home.py index c4620cb..4c24948 100644 --- a/gui/home/home.py +++ b/gui/home/home.py @@ -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", diff --git a/gui/main/main.py b/gui/main/main.py index bafbae4..787dc20 100644 --- a/gui/main/main.py +++ b/gui/main/main.py @@ -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 diff --git a/gui/program/program.py b/gui/program/program.py index c2fd360..edd33b6 100644 --- a/gui/program/program.py +++ b/gui/program/program.py @@ -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() diff --git a/package.sh b/package.sh index b71f56b..f231351 100755 --- a/package.sh +++ b/package.sh @@ -14,7 +14,7 @@ fi # Build successful cp -r ./gui ./dist -cp -r ./lib ./dist +cp -r ./util ./dist cp ./biogascontrollerapp.py ./dist/ cp ./BiogasControllerAppLogo.png ./dist/ cp ./changelog ./dist/ diff --git a/lib/com.py b/util/com.py similarity index 69% rename from lib/com.py rename to util/com.py index f3f1d71..bd38661 100644 --- a/lib/com.py +++ b/util/com.py @@ -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 diff --git a/lib/config.py b/util/config.py similarity index 98% rename from lib/config.py rename to util/config.py index 1c4b431..146800d 100644 --- a/lib/config.py +++ b/util/config.py @@ -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 diff --git a/lib/decoder.py b/util/decoder.py similarity index 100% rename from lib/decoder.py rename to util/decoder.py diff --git a/lib/instructions.py b/util/instructions.py similarity index 96% rename from lib/instructions.py rename to util/instructions.py index f9f094e..44293de 100644 --- a/lib/instructions.py +++ b/util/instructions.py @@ -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. diff --git a/util/interface.py b/util/interface.py new file mode 100644 index 0000000..f51837f --- /dev/null +++ b/util/interface.py @@ -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 diff --git a/lib/test/com.py b/util/test/com.py similarity index 91% rename from lib/test/com.py rename to util/test/com.py index 057dc70..1297fa8 100644 --- a/lib/test/com.py +++ b/util/test/com.py @@ -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