8 Commits
V3.1.0 ... main

Author SHA1 Message Date
f8fb015de3 Improve docs for config 2025-11-19 12:24:08 +01:00
d1ba8d4d0e Fix char 2025-11-19 12:15:46 +01:00
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 266 additions and 132 deletions

View File

@@ -1,5 +1,6 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
from kivy_deps import sdl2, glew from kivy_deps import sdl2, glew
from kivymd import hooks_path as kivymd_hooks_path
block_cipher = None block_cipher = None
@@ -10,7 +11,7 @@ a = Analysis(
binaries=[], binaries=[],
datas=[], datas=[],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[kivymd_hooks_path],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],
excludes=[], 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 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 # 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. 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 | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 3.1.X | ✅ | | 3.1.X | ✅ |
| 3.0.X | | | 3.0.X | |
| 2.3.0 | ❎ | | 2.3.0 | ❎ |
| 2.2.0 | ❎ | | 2.2.0 | ❎ |
| 2.1.0 | ❎ | | 2.1.0 | ❎ |

View File

@@ -4,7 +4,11 @@
# ╰────────────────────────────────────────────────╯ # ╰────────────────────────────────────────────────╯
# #
# So you would like to read the source code? Nice! # 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 # Kivy to run. If you are unsure of what functions do, consider
# checking out the kivy docs at https://kivy.org/doc. # checking out the kivy docs at https://kivy.org/doc.
# It also uses the pyserial library for communication with the micro- # 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 # Load the config file
import time 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 = str_to_bool(read_config("Dev", "verbose", "False", type_to_validate="bool"))
verbose = verbose if verbose != None else False verbose = verbose if verbose != None else False
@@ -50,8 +75,9 @@ if str_to_bool(
import os import os
from typing import override from typing import override
from lib.com import Com, ComSuperClass from util.com import Com
import lib.test.com from util.interface import ControllerConnection
import util.test.com
# Load config and disable kivy log if necessary # Load config and disable kivy log if necessary
@@ -120,19 +146,19 @@ class BiogasControllerApp(MDApp):
def build(self): def build(self):
# Configure com # Configure com
filters = [ filters = [
x x.strip()
for x in read_config( for x in read_config(
"Connection", "Connection",
"filters", "filters",
"USB-Serial Controller, Prolific USB-Serial Controller", "USB-Serial Controller; Prolific USB-Serial Controller",
).split(",") ).split(";")
] ]
baudrate = int( baudrate = int(
read_config("Connection", "baudrate", "19200", type_to_validate="int") read_config("Connection", "baudrate", "19200", type_to_validate="int")
) )
com: ComSuperClass = Com( com: ControllerConnection = Com(
baudrate, baudrate,
filters, filters,
) )
@@ -140,7 +166,7 @@ class BiogasControllerApp(MDApp):
if str_to_bool( if str_to_bool(
read_config("Dev", "use_test_library", "False", type_to_validate="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")), int(read_config("Dev", "fail_sim", "20", type_to_validate="int")),
baudrate, baudrate,
filters, filters,
@@ -163,7 +189,7 @@ class BiogasControllerApp(MDApp):
print("\n", "-" * 20, "\n") print("\n", "-" * 20, "\n")
self.icon = "./BiogasControllerAppLogo.png" 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(HomeScreen(com, name="home"))
self.screen_manager.add_widget(MainScreen(com, name="main")) self.screen_manager.add_widget(MainScreen(com, name="main"))
self.screen_manager.add_widget(ProgramScreen(com, name="program")) self.screen_manager.add_widget(ProgramScreen(com, name="program"))
@@ -178,22 +204,16 @@ class BiogasControllerApp(MDApp):
# Disallow this file to be imported # Disallow this file to be imported
if __name__ == "__main__": if __name__ == "__main__":
print(
"""
┏━━┓━━━━━━━━━━━━━━━━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━┏┓━┏┓━━━━━━━━┏━━━┓━━━━━━━━
┃┏┓┃━━━━━━━━━━━━━━━━━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━┃┃━┃┃━━━━━━━━┃┏━┓┃━━━━━━━━
┃┗┛┗┓┏┓┏━━┓┏━━┓┏━━┓━┏━━┓┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓┃┃━┃┃━┏━━┓┏━┓┃┃━┃┃┏━━┓┏━━┓
┃┏━┓┃┣┫┃┏┓┃┃┏┓┃┗━┓┃━┃━━┫┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┃┏┓┃┃┃━┃┃━┃┏┓┃┃┏┛┃┗━┛┃┃┏┓┃┃┏┓┃
┃┗━┛┃┃┃┃┗┛┃┃┗┛┃┃┗┛┗┓┣━━┃┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┃┃┗┓┃┗┓┃┃━┫┃┃━┃┏━┓┃┃┗┛┃┃┗┛┃
┗━━━┛┗┛┗━━┛┗━┓┃┗━━━┛┗━━┛┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━┛┗━┛┗━┛┗━━┛┗┛━┗┛━┗┛┃┏━┛┃┏━┛
━━━━━━━━━━━┏━┛┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━┃┃━━
━━━━━━━━━━━┗━━┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━┗┛━━
Version 3.1.0
=> Initializing....
"""
)
set_verbosity(verbose) set_verbosity(verbose)
BiogasControllerApp().run()
# 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!") print("\n => Exiting!")

View File

@@ -1,4 +1,15 @@
***CHANGELOG*** ***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 V3.1.0
- Completely redesigned User Interface using KivyMD - Completely redesigned User Interface using KivyMD
- Added config option for themes - Added config option for themes

View File

@@ -2,8 +2,8 @@
port_override = None port_override = None
baudrate = 19200 baudrate = 19200
# List the names as which the adapter cable will show up separated by commas below # List the names as which the adapter cable will show up separated by commas below
# For ENATECH, the below is likely correct. # For ENATECH, the below is likely correct. The name cannot contain a semicolon
filters = USB-Serial Controller, Prolific USB-Serial Controller filters = USB-Serial Controller; Prolific USB-Serial Controller
[UI] [UI]
height = 600 height = 600

View File

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

View File

@@ -4,9 +4,10 @@ from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog from kivymd.uix.dialog import MDDialog
from kivymd.uix.screen import MDScreen from kivymd.uix.screen import MDScreen
from kivy.lang import Builder from kivy.lang import Builder
from lib.com import ComSuperClass
import platform import platform
from util.interface import ControllerConnection
# Information for errors encountered when using pyserial # Information for errors encountered when using pyserial
information = { information = {
@@ -25,7 +26,7 @@ information = {
# This is the launch screen, i.e. what you see when you start up the app # This is the launch screen, i.e. what you see when you start up the app
class HomeScreen(MDScreen): class HomeScreen(MDScreen):
def __init__(self, com: ComSuperClass, **kw): def __init__(self, com: ControllerConnection, **kw):
self._com = com self._com = com
self.connection_error_dialog = MDDialog( self.connection_error_dialog = MDDialog(
title="Connection", title="Connection",

View File

@@ -10,9 +10,9 @@ import queue
import threading import threading
# Load utilities # Load utilities
from lib.instructions import Instructions from util.instructions import Instructions
from lib.com import ComSuperClass from util.interface import ControllerConnection
from lib.decoder import Decoder from util.decoder import Decoder
# TODO: Consider consolidating start and stop button # 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 # Using a Thread to run this in parallel to the UI to improve responsiveness
class ReaderThread(threading.Thread): class ReaderThread(threading.Thread):
_com: ComSuperClass _com: ControllerConnection
_decoder: Decoder _decoder: Decoder
_instructions: Instructions _instructions: Instructions
# This method allows the user to set Com object to be used. # 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 # 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 """Set the Com object to be used in this
Args: Args:
@@ -106,7 +106,7 @@ class MainScreen(MDScreen):
# The constructor if this class takes a Com object to share one between all screens # The constructor if this class takes a Com object to share one between all screens
# to preserve resources and make handling better # to preserve resources and make handling better
def __init__(self, com: ComSuperClass, **kw): def __init__(self, com: ControllerConnection, **kw):
# Set some variables # Set some variables
self._com = com self._com = com
self._event = None self._event = None

View File

@@ -1,11 +1,11 @@
from typing import List from typing import List
from kivymd.uix.screen import MDScreen from kivymd.uix.screen import MDScreen
from kivy.lang import Builder from kivy.lang import Builder
from lib.decoder import Decoder from util.decoder import Decoder
from lib.instructions import Instructions from util.instructions import Instructions
from util.instructions import ControllerConnection
from kivymd.uix.button import MDFlatButton from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog from kivymd.uix.dialog import MDDialog
from lib.com import ComSuperClass
from kivy.clock import Clock from kivy.clock import Clock
@@ -15,7 +15,7 @@ name_map = ["a", "b", "c", "t"]
class ProgramScreen(MDScreen): class ProgramScreen(MDScreen):
def __init__(self, com: ComSuperClass, **kw): def __init__(self, com: ControllerConnection, **kw):
self._com = com self._com = com
self._instructions = Instructions(com) self._instructions = Instructions(com)
self._decoder = Decoder() 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 override
from typing import Optional
import serial import serial
import struct import struct
import serial.tools.list_ports import serial.tools.list_ports
# The below class is abstract to have a consistent, targetable interface from util.interface import ControllerConnection
# 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
# ┌ ┐ # ┌ ┐
@@ -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 # 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 # 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: def _connection_check(self) -> bool:
if self._serial == None: if self._serial == None:
return self._open() return self._open()
@@ -88,6 +31,7 @@ class Com(ComSuperClass):
else: else:
return False return False
@override
def get_comport(self) -> str: def get_comport(self) -> str:
"""Find the comport the microcontroller has attached to""" """Find the comport the microcontroller has attached to"""
if self._port_override != "": if self._port_override != "":
@@ -109,7 +53,7 @@ class Com(ComSuperClass):
return "" return ""
def _open(self) -> bool: 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: Returns:
Boolean indicates if connection was successful or not Boolean indicates if connection was successful or not
@@ -118,7 +62,7 @@ class Com(ComSuperClass):
comport = self.get_comport() comport = self.get_comport()
# Comport search returns empty string if search unsuccessful # 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 # Try to generate a new Serial object with the configuration of this class
# self._baudrate contains the baud rate and defaults to 19200 # self._baudrate contains the baud rate and defaults to 19200
try: try:
@@ -135,10 +79,12 @@ class Com(ComSuperClass):
# Haven't found a comport # Haven't found a comport
return False return False
@override
def connect(self) -> bool: def connect(self) -> bool:
"""Try to find a comport and connect to the microcontroller. Returns the success as a boolean""" """Try to find a comport and connect to the microcontroller. Returns the success as a boolean"""
return self._connection_check() return self._connection_check()
@override
def close(self) -> None: def close(self) -> None:
"""Close the serial connection, if possible""" """Close the serial connection, if possible"""
if self._serial != None: if self._serial != None:
@@ -147,6 +93,7 @@ class Com(ComSuperClass):
except: except:
pass pass
@override
def receive(self, byte_count: int) -> bytes: def receive(self, byte_count: int) -> bytes:
"""Receive bytes from microcontroller over serial. Returns bytes. Might want to decode using functions from lib.decoder""" """Receive bytes from microcontroller over serial. Returns bytes. Might want to decode using functions from lib.decoder"""
# Check connection # Check connection
@@ -160,6 +107,7 @@ class Com(ComSuperClass):
else: else:
raise Exception("ERR_CONNECTING") raise Exception("ERR_CONNECTING")
@override
def send(self, msg: str) -> None: def send(self, msg: str) -> None:
"""Send a string over serial connection. Will open a connection if none is available""" """Send a string over serial connection. Will open a connection if none is available"""
# Check connection # Check connection
@@ -173,6 +121,7 @@ class Com(ComSuperClass):
else: else:
raise Exception("ERR_CONNECTING") raise Exception("ERR_CONNECTING")
@override
def send_float(self, msg: float) -> None: def send_float(self, msg: float) -> None:
"""Send a float number over serial connection""" """Send a float number over serial connection"""
# Check connection # Check connection

View File

@@ -1,3 +1,5 @@
# This library is used to validate the config file
import configparser import configparser
from typing import List from typing import List
@@ -12,6 +14,7 @@ global is_verbose
is_verbose = True is_verbose = True
# Set the verbosity if needed
def set_verbosity(verbose: bool): def set_verbosity(verbose: bool):
global is_verbose global is_verbose
is_verbose = verbose is_verbose = verbose
@@ -44,11 +47,12 @@ def read_config(
key_0: The first key (top level) key_0: The first key (top level)
key_1: The second key (where the actual key-value pair is) key_1: The second key (where the actual key-value pair is)
default: The default value to return if the check fails default: The default value to return if the check fails
valid_entries: [Optiona] The entries that are valid ones to check against valid_entries: [Optional] The entries that are valid ones to check against
type_to_validate: [Optional] Data type to validate type_to_validate: [Optional] Data type to validate
Returns: Returns:
[TODO:return] The read config option as a string. You can cast this to the type you specified with type_to_validate safely.
When converting to a boolean though, use the str_to_bool function provided by this library
""" """
# Try loading the keys # Try loading the keys
tmp = {} tmp = {}

View File

@@ -1,14 +1,15 @@
from lib.com import ComSuperClass import util.decoder
import lib.decoder
import time import time
decoder = lib.decoder.Decoder() from util.interface import ControllerConnection
decoder = util.decoder.Decoder()
# Class that supports sending instructions to the microcontroller, # Class that supports sending instructions to the microcontroller,
# as well as hooking to data stream according to protocol # as well as hooking to data stream according to protocol
class Instructions: class Instructions:
def __init__(self, com: ComSuperClass) -> None: def __init__(self, com: ControllerConnection) -> None:
self._com = com self._com = com
# Helper method to hook to the data stream according to protocol. # 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 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 │ # │ Testing Module For Com │
# └ ┘ # └ ┘
@@ -18,9 +12,50 @@ from lib.com import ComSuperClass
# even without a microcontroller. It is not documented in a particularly # even without a microcontroller. It is not documented in a particularly
# beginner-friendly way, nor is the code written with beginner-friendliness # 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 # 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 # All double __ prefixed properties and methods are not available in the actual impl
instruction_lut: dict[str, list[str]] = { instruction_lut: dict[str, list[str]] = {
@@ -53,7 +88,7 @@ class SensorConfig:
self.t = t self.t = t
class Com(ComSuperClass): class Com(ControllerConnection):
def __init__( def __init__(
self, fail_sim: int, baudrate: int = 19200, filters: Optional[list[str]] = None self, fail_sim: int, baudrate: int = 19200, filters: Optional[list[str]] = None
) -> None: ) -> None:
@@ -79,13 +114,11 @@ class Com(ComSuperClass):
# Initially, we are in normal mode (which leads to slower data intervals) # Initially, we are in normal mode (which leads to slower data intervals)
self.__mode = "NM" self.__mode = "NM"
def set_port_override(self, override: str) -> None: @override
"""Set the port override, to disable port search"""
self._port_override = override
def get_comport(self) -> str: def get_comport(self) -> str:
return "Sim" if self._port_override == "" else self._port_override return "Sim" if self._port_override == "" else self._port_override
@override
def connect(self) -> bool: def connect(self) -> bool:
# Randomly return false in 1 in fail_sim ish cases # Randomly return false in 1 in fail_sim ish cases
if random.randint(0, self.__fail_sim) == 0: if random.randint(0, self.__fail_sim) == 0:
@@ -93,9 +126,11 @@ class Com(ComSuperClass):
return False return False
return True return True
@override
def close(self) -> None: def close(self) -> None:
pass pass
@override
def receive(self, byte_count: int) -> bytes: def receive(self, byte_count: int) -> bytes:
data = [] data = []
# If queue is too short, refill it # If queue is too short, refill it
@@ -116,6 +151,7 @@ class Com(ComSuperClass):
) )
return b"".join(data) return b"".join(data)
@override
def send(self, msg: str) -> None: def send(self, msg: str) -> None:
# Using LUT to reference # Using LUT to reference
readback = instruction_lut.get(msg) readback = instruction_lut.get(msg)
@@ -143,6 +179,7 @@ class Com(ComSuperClass):
self.__add_float_as_hex(self.__config[i].t) self.__add_float_as_hex(self.__config[i].t)
self.__add_ascii_char("\n") self.__add_ascii_char("\n")
@override
def send_float(self, msg: float) -> None: def send_float(self, msg: float) -> None:
if self.__reconf_step == 0: if self.__reconf_step == 0:
self.__config[self.__reconf_sensor].a = msg self.__config[self.__reconf_sensor].a = msg
@@ -212,7 +249,7 @@ class Com(ComSuperClass):
def __add_integer_as_hex(self, c: int): 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.""" """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): if not (0 <= c <= 0xFFFF):
raise ValueError("Input must be a 16-bit integer (065535)") raise ValueError("Input must be a 16-bit integer (0-65535)")
# Get high byte (most significant byte) # Get high byte (most significant byte)
hi_byte = (c >> 8) & 0xFF hi_byte = (c >> 8) & 0xFF