From 7f11d4b3aff963d33c61ba19f785674c3b700eb4 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Wed, 5 Mar 2025 10:11:25 +0100 Subject: [PATCH 01/31] Prepare for full rewrite --- .idea/.gitignore | 3 - .idea/BiogasControllerApp.iml | 12 - .../inspectionProfiles/profiles_settings.xml | 6 - .idea/misc.xml | 4 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - BiogasControllerApp-V2.3/.idea/.gitignore | 3 - BiogasControllerApp-V2.3/.idea/.name | 1 - BiogasControllerApp-V2.3/.idea/ENATECH.iml | 10 - .../inspectionProfiles/profiles_settings.xml | 6 - BiogasControllerApp-V2.3/.idea/misc.xml | 4 - BiogasControllerApp-V2.3/.idea/modules.xml | 8 - .../BiogasControllerApp-V2.3-stable.spec | 54 -- .../BiogasControllerAppLogo-V2.3.ico | Bin 120363 -> 0 bytes .../BiogasControllerAppLogo.png | Bin 36750 -> 0 bytes BiogasControllerApp-V2.3/bin/gui/gui.kv | 694 -------------- .../bin/lib/communication.py | 96 -- .../bin/lib/comport_search.py | 22 - .../bin/lib/csv_parsers.py | 122 --- BiogasControllerApp-V2.3/bin/lib/lib.py | 73 -- .../biogascontrollerapp.py | 893 ------------------ BiogasControllerApp-V2.3/config/config.csv | 1 - BiogasControllerApp-V2.3/config/settings.ini | 19 - BiogasControllerApp-V2.3/log/logging.md | 5 - biogascontrollerapp/biogascontrollerapp.py | 0 25 files changed, 2050 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/BiogasControllerApp.iml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 BiogasControllerApp-V2.3/.idea/.gitignore delete mode 100644 BiogasControllerApp-V2.3/.idea/.name delete mode 100644 BiogasControllerApp-V2.3/.idea/ENATECH.iml delete mode 100644 BiogasControllerApp-V2.3/.idea/inspectionProfiles/profiles_settings.xml delete mode 100644 BiogasControllerApp-V2.3/.idea/misc.xml delete mode 100644 BiogasControllerApp-V2.3/.idea/modules.xml delete mode 100644 BiogasControllerApp-V2.3/BiogasControllerApp-V2.3-stable.spec delete mode 100644 BiogasControllerApp-V2.3/BiogasControllerAppLogo-V2.3.ico delete mode 100644 BiogasControllerApp-V2.3/BiogasControllerAppLogo.png delete mode 100644 BiogasControllerApp-V2.3/bin/gui/gui.kv delete mode 100644 BiogasControllerApp-V2.3/bin/lib/communication.py delete mode 100644 BiogasControllerApp-V2.3/bin/lib/comport_search.py delete mode 100644 BiogasControllerApp-V2.3/bin/lib/csv_parsers.py delete mode 100644 BiogasControllerApp-V2.3/bin/lib/lib.py delete mode 100644 BiogasControllerApp-V2.3/biogascontrollerapp.py delete mode 100644 BiogasControllerApp-V2.3/config/config.csv delete mode 100644 BiogasControllerApp-V2.3/config/settings.ini delete mode 100644 BiogasControllerApp-V2.3/log/logging.md create mode 100644 biogascontrollerapp/biogascontrollerapp.py diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/BiogasControllerApp.iml b/.idea/BiogasControllerApp.iml deleted file mode 100644 index 8b8c395..0000000 --- a/.idea/BiogasControllerApp.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index dc9ea49..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index f7486eb..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/BiogasControllerApp-V2.3/.idea/.gitignore b/BiogasControllerApp-V2.3/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/BiogasControllerApp-V2.3/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/BiogasControllerApp-V2.3/.idea/.name b/BiogasControllerApp-V2.3/.idea/.name deleted file mode 100644 index 06ffd37..0000000 --- a/BiogasControllerApp-V2.3/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -biogascontrollerapp.py \ No newline at end of file diff --git a/BiogasControllerApp-V2.3/.idea/ENATECH.iml b/BiogasControllerApp-V2.3/.idea/ENATECH.iml deleted file mode 100644 index 0e4e9fa..0000000 --- a/BiogasControllerApp-V2.3/.idea/ENATECH.iml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/BiogasControllerApp-V2.3/.idea/inspectionProfiles/profiles_settings.xml b/BiogasControllerApp-V2.3/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/BiogasControllerApp-V2.3/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/BiogasControllerApp-V2.3/.idea/misc.xml b/BiogasControllerApp-V2.3/.idea/misc.xml deleted file mode 100644 index d1e22ec..0000000 --- a/BiogasControllerApp-V2.3/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/BiogasControllerApp-V2.3/.idea/modules.xml b/BiogasControllerApp-V2.3/.idea/modules.xml deleted file mode 100644 index 6610bd3..0000000 --- a/BiogasControllerApp-V2.3/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/BiogasControllerApp-V2.3/BiogasControllerApp-V2.3-stable.spec b/BiogasControllerApp-V2.3/BiogasControllerApp-V2.3-stable.spec deleted file mode 100644 index 7ab73cd..0000000 --- a/BiogasControllerApp-V2.3/BiogasControllerApp-V2.3-stable.spec +++ /dev/null @@ -1,54 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- -from kivy_deps import sdl2, glew - -block_cipher = None - - -a = Analysis( - ['biogascontrollerapp.py'], - pathex=[], - binaries=[], - datas=[], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='BiogasControllerApp-V2.3-stable', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, - icon='BiogasControllerAppLogo-V2.3.ico', -) - -coll = COLLECT(exe, Tree('C:\\BiogasControllerApp-V2.3\\'), - a.binaries, - a.zipfiles, - a.datas, - *[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)], - strip=False, - upx=True, - name='touchtracer') diff --git a/BiogasControllerApp-V2.3/BiogasControllerAppLogo-V2.3.ico b/BiogasControllerApp-V2.3/BiogasControllerAppLogo-V2.3.ico deleted file mode 100644 index f42f7297e06358aebe1fa151a987d1dd427192e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120363 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

861vr8EUKp_(4Z<0q(Vq~EIgoU5>rG${O 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 DR*=M! diff --git a/BiogasControllerApp-V2.3/bin/gui/gui.kv b/BiogasControllerApp-V2.3/bin/gui/gui.kv deleted file mode 100644 index c567359..0000000 --- a/BiogasControllerApp-V2.3/bin/gui/gui.kv +++ /dev/null @@ -1,694 +0,0 @@ -RootScreen: - HomeScreen: - ReadoutScreen: - ReadData: - ProgramTemp: - Program: - Credits: - Modify: - -: - title: "NOTICE" - size_hint: 0.7, 0.5 - auto_dismiss: True - GridLayout: - cols:1 - Label: - text: "THIS SOFTWARE IS FREE SOFTWARE LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE V3 (GPL V3) AND AS SUCH COMES WITH ABSOLUTELY NO WARRANTY! \n\nmore info under Settings > Credits" - text_size: self.width, None - GridLayout: - cols: 2 - Button: - text: "Don't show anymore" - on_release: - root.notshowanymore() - Button: - text: "ok" - on_release: - root.dismiss() -: - 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.quitapp() - app.stop() - Button: - text: "No" - font_size: 15 - on_press: - root.dismiss() - -: - title: "WARNING!" - font_size: 50 - size_hint: 0.5, 0.4 - auto_dismiss: False - GridLayout: - cols:1 - Label: - text: "Unable to open Serial Port" - font_size: 20 - GridLayout: - cols:2 - Button: - text: "Details" - on_release: - root.details() - Button: - text:"Ok" - on_release: - root.dismiss() - -: - title: "WARNING!" - font_size: 50 - size_hint: 0.7, 0.6 - auto_dismiss: False - GridLayout: - cols:1 - Label: - text: "Unable to communicate" - font_size: 20 - Label: - text: "Possible ways to resolve this problem:\n- Try again\n- Restart the PIC16F877 or reset the program\n- Check the cable / connect one" - font_size: 14 - Button: - text:"Ok" - on_release: - root.dismiss() - -: - on_open: self.update_details = root.infos() - title: "DETAILS" - font_size: 50 - size_hint: 1, 0.7 - auto_dismiss: False - GridLayout: - cols:1 - Label: - text: "Unable to open Serial Port" - font_size: 20 - Label: - id: errormessage - text: root.infos() - font_size: 13 - Label: - text: root.error_tips() - font_size: 13 - Button: - text:"Ok" - on_release: - root.dismiss() - -: - title: "NOTICE!" - font_size: 50 - size_hint: 0.5, 0.4 - auto_dismiss: False - GridLayout: - cols:1 - Label: - text: "Mode Switched!" - font_size: 30 - Button: - text:"Ok" - on_release: - root.dismiss() - -: - title: "NOTICE!" - font_size: 50 - size_hint: 0.5, 0.4 - auto_dismiss: False - GridLayout: - cols:1 - Label: - text: "SAVED!" - font_size: 30 - Button: - text:"Ok" - on_release: - root.dismiss() - -: - title: "NOTICE!" - font_size: 50 - size_hint: 0.5, 0.4 - auto_dismiss: False - GridLayout: - cols:1 - Label: - text: "Establishing connection with PIC16F877" - font_size: 18 - Label: - text: "This Process may take a while..." - font_size: 15 - Button: - text:"Ok" - on_release: - root.dismiss() - -: - title: "WARNING!" - font_size: 50 - size_hint: 0.5, 0.4 - auto_dismiss: False - GridLayout: - cols:1 - Label: - text: "Missing Information!" - font_size: 18 - Label: - text: "Check your entry" - font_size: 15 - Button: - text:"Ok" - on_release: - root.dismiss() - -: - title: "NOTICE!" - font_size: 50 - size_hint: 0.5, 0.4 - auto_dismiss: False - GridLayout: - cols:1 - Label: - text: "Connection with PIC16F877 terminated" - font_size: 18 - Label: - text: "The connection to the Microcontroller\nhas been terminated successfully" - font_size: 15 - Button: - text:"Ok" - on_release: - root.dismiss() - -###################################### -# SCREENS -###################################### -: - name: "HomeS" - canvas.before: - Color: - rgba: (50,50,50,0.2) - Rectangle: - size: self.size - pos: self.pos - GridLayout: - cols:1 - Label: - text: "BiogasanlageControllerApp" - font_size: 50 - color: (0, 113, 0, 1) - bold:True - italic:True - FloatLayout: - GridLayout: - cols: 2 - size_hint: 0.8, 0.8 - pos_hint: {"x": 0.1, "y": 0.1} - Button: - text: "Start" - background_color: (255, 0, 0, 0.6) - font_size: 30 - on_release: - root.tryconnection() - Button: - text: "Quit" - background_color: (255, 0, 0, 0.6) - font_size: 30 - on_release: - root.exitapp() - Label: - text: root.reset() - id: app_version - font_size: 13 - pos_hint: {"y": -0.45, "x":0.05} - Button: - text: "Settings" - 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: - app.root.current = "Settings" - root.manager.transition.direction = "down" - -: - on_pre_enter: self.reset_screen = root.resscreen() - name: "Readout" - canvas.before: - Color: - rgba: (50,50,50,0.2) - Rectangle: - size: self.size - pos: self.pos - GridLayout: - FloatLayout: - Label: - pos_hint: {"y":0.4} - text: "READOUT" - font_size: 40 - color: (0, 113, 0, 1) - bold: True - GridLayout: - cols:4 - size_hint: 0.8, 0.3 - pos_hint: {"x":0.1, "y":0.4} - Label: - text: "SENSOR 1: " - font_size: 20 - Label: - id: sonde1 - text: "" - Label: - text: "SENSOR 2: " - font_size: 20 - Label: - id: sonde2 - text: "" - Label: - text: "SENSOR 3: " - font_size: 20 - Label: - id: sonde3 - text: "" - Label: - text: "SENSOR 4: " - font_size: 20 - Label: - id: sonde4 - text: "" - Button: - text: "Start communication" - size_hint: 0.2, 0.1 - pos_hint: {"x": 0.5, "y": 0.05} - background_color: (255, 0, 0, 0.6) - on_release: - root.start_com() - Button: - text: "End communication" - size_hint: 0.2, 0.1 - pos_hint: {"x": 0.7, "y": 0.05} - background_color: (255, 0, 0, 0.6) - on_release: - root.end_com() - Button: - text: "Back" - size_hint: 0.3, 0.1 - pos_hint: {"x":0.05, "y":0.05} - background_color: (255, 0, 0, 0.6) - on_release: - root.leave_screen() - app.root.current = "HomeS" - root.manager.transition.direction = "left" - ToggleButton: - id: mode_sel - 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_sel.text) - background_color: (255,0,0,0.6) if self.state == "normal" else (0,0,255,0.6) - Button: - text: "Read Data" - size_hint: 0.15, 0.1 - pos_hint: {"x":0.3, "y":0.2} - background_color: (255, 0, 0, 0.6) - on_release: - root.leave_screen() - app.root.current = "RD" - root.manager.transition.direction = "down" - Button: - text: "Temperature" - size_hint: 0.15, 0.1 - pos_hint: {"x":0.5, "y":0.2} - background_color: (255, 0, 0, 0.6) - on_release: - root.leave_screen() - app.root.current = "PT" - root.manager.transition.direction = "down" - Button: - text: "Change all Data" - size_hint: 0.15, 0.1 - pos_hint: {"x":0.7, "y":0.2} - background_color: (255, 0, 0, 0.6) - on_release: - root.leave_screen() - app.root.current = "PR" - root.manager.transition.direction = "down" - Label: - id: frequency - text: "Frequency will appear here" - font_size: 10 - pos_hint: {"x":0.4, "y": 0.3} - -: - name: "RD" - canvas.before: - Color: - rgba: (50,50,50,0.2) - Rectangle: - size: self.size - pos: self.pos - GridLayout: - FloatLayout: - Label: - text: "Read Data" - font_size: 40 - color: (0, 113, 0, 1) - bold: True - pos_hint: {"y":0.4} - Button: - text: "Start Readout" - size_hint: 0.2, 0.1 - pos_hint: {"x":0.4, "y":0.1} - on_release: - root.read_data() - Button: - text: "Back" - size_hint: 0.2, 0.1 - pos_hint: {"x":0.1, "y":0.1} - background_color: (255, 0, 0, 0.6) - on_release: - app.root.current = "Readout" - root.manager.transition.direction = "up" - GridLayout: - cols:4 - size_hint: 0.8, 0.4 - pos_hint: {"x":0.1, "y":0.3} - Label: - text: "Sonde 1" - font_size: 20 - Label: - id: inf_sonde1 - text: "" - Label: - text: "Sonde 2" - font_size: 20 - Label: - id: inf_sonde2 - text: "" - Label: - text: "Sonde 3" - font_size: 20 - Label: - id: inf_sonde3 - text: "" - Label: - text: "Sonde 4" - font_size: 20 - Label: - id: inf_sonde4 - text: "" - -: - on_pre_enter: self.check_config = root.read_config() - name: "PT" - canvas.before: - Color: - rgba: (50,50,50,0.2) - Rectangle: - size: self.size - pos: self.pos - FloatLayout: - Label: - text: "Change Temperature" - pos_hint: {"y":0.4} - font_size: 40 - color: (0, 113, 0, 1) - bold: True - GridLayout: - size_hint: 0.8, 0.4 - pos_hint: {"x": 0.1, "y":0.3} - cols:2 - Label: - text: "Temperature Sensor 1: " - TextInput: - id: temp_s1 - multiline: False - input_filter: "float" - Label: - text: "Temperature Sensor 2: " - TextInput: - id: temp_s2 - multiline: False - input_filter: "float" - Label: - text: "Temperature Sensor 3: " - TextInput: - id: temp_s3 - multiline: False - input_filter: "float" - Label: - text: "Temperature Sensor 4: " - TextInput: - id: temp_s4 - multiline: False - input_filter: "float" - Button: - 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 = "Readout" - root.manager.transition.direction = "up" - ToggleButton: - id: prsel - size_hint: 0.2, 0.1 - pos_hint: {"x":0.35, "y": 0.1} - text: "Full\nreprogramming" if self.state == "normal" else "Partial\nreprogramming" - on_release: root.change_mode() - background_color: (255,0,0,0.6) if self.state == "normal" else (0,0,255,0.6) - Button: - 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.send_data() - -: - name: "PR" - on_pre_enter: self.check_config = root.read_config() - canvas.before: - Color: - rgba: (50,50,50,0.2) - Rectangle: - size: self.size - pos: self.pos - FloatLayout: - Label: - text: "Change all Data" - font_size: 40 - color: (0, 113, 0, 1) - bold: True - pos_hint: {"y":0.4} - GridLayout: - size_hint: 0.8, 0.5 - pos_hint: {"x":0.1, "y":0.2} - cols: 4 - Label: - text: "Sensor 1, a:" - TextInput: - id: s1_a - multiline: False - input_filter: "float" - Label: - text: "Sensor 1, b:" - TextInput: - id: s1_b - multiline: False - input_filter: "float" - Label: - text: "Sensor 1, c:" - TextInput: - id: s1_c - multiline: False - input_filter: "float" - Label: - text: "Sensor 1, Temp:" - TextInput: - id: s1_t - multiline: False - input_filter: "float" - Label: - text: "Sensor 2, a:" - TextInput: - id: s2_a - multiline: False - input_filter: "float" - Label: - text: "Sensor 2, b:" - TextInput: - id: s2_b - multiline: False - input_filter: "float" - Label: - text: "Sensor 2, c:" - TextInput: - id: s2_c - multiline: False - input_filter: "float" - Label: - text: "Sensor 2, Temp:" - TextInput: - id: s2_t - multiline: False - input_filter: "float" - Label: - text: "Sensor 3, a:" - TextInput: - id: s3_a - multiline: False - input_filter: "float" - Label: - text: "Sensor 3, b:" - TextInput: - id: s3_b - multiline: False - input_filter: "float" - Label: - text: "Sensor 3, c:" - TextInput: - id: s3_c - multiline: False - input_filter: "float" - Label: - text: "Sensor 3, Temp:" - TextInput: - id: s3_t - multiline: False - input_filter: "float" - Label: - text: "Sensor 4, a:" - TextInput: - id: s4_a - multiline: False - input_filter: "float" - Label: - text: "Sensor 4, b:" - TextInput: - id: s4_b - multiline: False - input_filter: "float" - Label: - text: "Sensor 4, c:" - TextInput: - id: s4_c - multiline: False - input_filter: "float" - Label: - text: "Sensor 4, Temp:" - TextInput: - id: s4_t - multiline: False - input_filter: "float" - Button: - 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 = "Readout" - root.manager.transition.direction = "up" - ToggleButton: - id: prsel - size_hint: 0.2, 0.1 - pos_hint: {"x":0.35, "y": 0.1} - text: "Full\nreprogramming" if self.state == "normal" else "Partial\nreprogramming" - on_release: root.change_mode() - background_color: (255,0,0,0.6) if self.state == "normal" else (0,0,255,0.6) - Button: - 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.send_data() - - -: - name: "Credits" - canvas.before: - Color: - rgba: (50,50,50,0.2) - Rectangle: - size: self.size - pos: self.pos - FloatLayout: - Button: - text: "back" - size_hint: 0.4, 0.2 - pos_hint: {"x":0.3, "y":0.1} - on_release: - app.root.current = "Settings" - 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 rework of the BiogasControllerApp V1, that was originally programmed by S. Reichmuth." - 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 - -: - on_pre_enter: self.config = root.read_config() - name: "Settings" - canvas.before: - Color: - rgba: (50,50,50,0.2) - Rectangle: - size: self.size - pos: self.pos - GridLayout: - cols: 1 - Label: - text: "Settings" - font_size: 40 - color: (0, 113, 0, 1) - bold: True - FloatLayout: - GridLayout: - pos_hint: {"x":0.05, "y":0.05} - size_hint: 0.9, 0.9 - cols: 4 - Button: - text: "Back" - background_color: (255,0,0,0.6) - on_release: - app.root.current = "HomeS" - root.manager.transition.direction = "up" - Button: - text: "Report a\nBug" - background_color: (255,0,0,0.6) - on_release: - root.issue_reporting() - ToggleButton: - id: prsel - text: "Full\nreprogramming" if self.state == "normal" else "Partial\nreprogramming" - on_release: root.change_programming() - background_color: (255,0,0,0.6) if self.state == "normal" else (0,0,255,0.6) - Button: - text: "Credits" - background_color: (255,0,0,0.6) - on_release: - app.root.current = "Credits" - root.manager.transition.direction = "left" diff --git a/BiogasControllerApp-V2.3/bin/lib/communication.py b/BiogasControllerApp-V2.3/bin/lib/communication.py deleted file mode 100644 index 6bfce41..0000000 --- a/BiogasControllerApp-V2.3/bin/lib/communication.py +++ /dev/null @@ -1,96 +0,0 @@ -import bin.lib.lib -com = bin.lib.lib.Com() - - -class Communication: - def __init__(self): - self.__x = 0 - self.__data_recieve = 0 - self.__output = "" - - def change_temp(self, data, special_port): - com.connect(19200, special_port) - com.send("PT") - self.go = 0 - while True: - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "P": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "T": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.go = 1 - break - else: - pass - else: - pass - else: - pass - else: - pass - if self.go == 1: - self.data = data - while len(self.data) > 0: - self.__data_recieve = com.receive(3) - if self.__data_recieve != "": - com.send_float(float(self.data.pop(0))) - else: - print("error") - break - else: - print("Error") - com.quitcom() - - def change_all(self, data, special_port): - com.connect(19200, special_port) - com.send("PR") - self.go = 0 - while True: - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "P": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "R": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.go = 1 - break - else: - pass - else: - pass - else: - pass - else: - pass - if self.go == 1: - self.data = data - while len(self.data) > 0: - self.__data_recieve = com.receive(3) - if self.__data_recieve != "": - com.send_float(float(self.data.pop(0))) - else: - print("error") - break - else: - print("Error") - com.quitcom() - - -class SwitchMode: - def __init__(self): - pass - - def enable_fastmode(self, special_port): - com.connect(19200, special_port) - com.send("FM") - com.quitcom() - - def disable_fastmode(self, special_port): - com.connect(19200, special_port) - com.send("NM") - com.quitcom() diff --git a/BiogasControllerApp-V2.3/bin/lib/comport_search.py b/BiogasControllerApp-V2.3/bin/lib/comport_search.py deleted file mode 100644 index cadd619..0000000 --- a/BiogasControllerApp-V2.3/bin/lib/comport_search.py +++ /dev/null @@ -1,22 +0,0 @@ -import serial.tools.list_ports - - -class ComportService: - def __init__(self): - self.__comport = [] - self.__import = [] - self.__working = [] - - def get_comport(self, special_port): - self.__comport = [comport.device for comport in serial.tools.list_ports.comports()] - self.__pos = 0 - if special_port != "": - self.__working = special_port - else: - while self.__working == []: - self.__com_name = serial.tools.list_ports.comports()[self.__pos] - if "USB-Serial Controller" or "Prolific USB-Serial Controller" in self.__com_name: - self.__working = self.__comport.pop(self.__pos) - else: - self.__pos += 1 - return self.__working diff --git a/BiogasControllerApp-V2.3/bin/lib/csv_parsers.py b/BiogasControllerApp-V2.3/bin/lib/csv_parsers.py deleted file mode 100644 index d1f593c..0000000 --- a/BiogasControllerApp-V2.3/bin/lib/csv_parsers.py +++ /dev/null @@ -1,122 +0,0 @@ -"""@package docstring -This is a simplification of the csv module""" - -import csv - - -class CsvRead: - """This is a class that reads csv files and depending on the module selected does do different things with it""" - def __init__(self): - self.__imp = "" - self.__raw = "" - self.__raw_list = "" - - def importing(self, path): - """Returns a list of the imported csv-file, requires path, either direct system path or relative path""" - self.__imp = open(path) - self.__raw = csv.reader(self.__imp, delimiter=',') - self.__raw_list = list(self.__raw) - self.__imp.close() - return self.__raw_list - - -class CsvWrite: - """This is a class that modifies csv files""" - def __init__(self): - self.__impl = [] - self.__strpop = [] - self.__removed = [] - self.__removing = 0 - self.__change = 0 - self.__appending = 0 - self.__imp = [] - self.__raw = [] - - def rem_str(self, path, row): - """Opens the csv-file in write mode which is specified as an argument either as direct or relative path""" - self.__imp = open(path) - self.__raw = csv.reader(self.__imp, delimiter=',') - self.__impl = list(self.__raw) - self.__removed = self.__impl.pop(row + 1) - with open(path, "w") as removedata: - self.__removing = csv.writer(removedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__removing.writerow(self.__impl.pop(0)) - while len(self.__impl) > 0: - with open(path, "a") as removedata: - self.__removing = csv.writer(removedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__removing.writerow(self.__impl.pop(0)) - self.__imp.close() - removedata.close() - - - def chg_str(self, path, row, pos, new_value): - """Opens the csv-file in write mode to change a value, e.g. if a recipes is changed.""" - self.__imp = open(path) - self.__raw = csv.reader(self.__imp, delimiter=',') - self.__impl = list(self.__raw) - self.__strpop = self.__impl.pop(row) - self.__strpop.pop(pos) - self.__strpop.insert(pos, new_value) - self.__impl.insert(row, self.__strpop) - with open(path, "w") as changedata: - self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__change.writerow(self.__impl.pop(0)) - while len(self.__impl) > 0: - with open(path, "a") as changedata: - self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__removing.writerow(self.__impl.pop(0)) - self.__imp.close() - changedata.close() - - def chg_str_rem(self, path, row, pos): - """Opens the csv-file in write mode to change a value, e.g. if a recipes is changed.""" - self.__imp = open(path) - self.__raw = csv.reader(self.__imp, delimiter=',') - self.__impl = list(self.__raw) - self.__strpop = self.__impl.pop(row) - self.__strpop.pop(pos) - self.__strpop.pop(pos) - self.__impl.insert(row, self.__strpop) - with open(path, "w") as changedata: - self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__change.writerow(self.__impl.pop(0)) - while len(self.__impl) > 0: - with open(path, "a") as changedata: - self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__removing.writerow(self.__impl.pop(0)) - self.__imp.close() - changedata.close() - - def chg_str_add(self, path, row, new_value1, new_value2): - """Opens the csv-file in write mode to change a value, e.g. if a recipes is changed.""" - self.__imp = open(path) - self.__raw = csv.reader(self.__imp, delimiter=',') - self.__impl = list(self.__raw) - self.__strpop = self.__impl.pop(row) - self.__strpop.append(new_value1) - self.__strpop.append(new_value2) - self.__impl.insert(row, self.__strpop) - with open(path, "w") as changedata: - self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__change.writerow(self.__impl.pop(0)) - while len(self.__impl) > 0: - with open(path, "a") as changedata: - self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__removing.writerow(self.__impl.pop(0)) - self.__imp.close() - changedata.close() - - def app_str(self, path, value): - """Opens the csv-file in append mode and writes given input. CsvWrite.app_str(path, value). - Path can be specified both as direct or relative. value is a list. Will return an error if type of value is - not a list.""" - with open(path, "a") as appenddata: - self.__appending = csv.writer(appenddata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__appending.writerow(value) - appenddata.close() - - def write_str(self, path, value): - with open(path, "w") as writedata: - self.__change = csv.writer(writedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) - self.__change.writerow(value) - writedata.close() diff --git a/BiogasControllerApp-V2.3/bin/lib/lib.py b/BiogasControllerApp-V2.3/bin/lib/lib.py deleted file mode 100644 index 52aa379..0000000 --- a/BiogasControllerApp-V2.3/bin/lib/lib.py +++ /dev/null @@ -1,73 +0,0 @@ -import serial -import struct -import bin.lib.comport_search -"""@package docstring -This package can communicate with a microcontroller""" - -coms = bin.lib.comport_search.ComportService() - - -class Com: - def __init__(self): - self.xr = "" - self.output = "" - self.str_input = "" - self.str_get_input = "" - self.xs = "" - self.__comport = '/dev/ttyUSB0' - - def connect(self, baudrate, special_port): - try: - self.__comport = coms.get_comport(special_port) - except: - pass - self.ser = serial.Serial(self.__comport, baudrate=baudrate, timeout=5) - - def quitcom(self): - try: - self.ser.close() - except: - pass - - def receive(self, amount_bytes): - self.xr = self.ser.read(amount_bytes) - return self.xr - - def decode_ascii(self, value): - try: - self.output = value.decode() - except: - self.output = "Error" - return self.output - - def check_value(self, value_check, checked_value): - if value_check == checked_value: - return 1 - else: - return 0 - - def decode_int(self, value): - self.i = int(value, base=16) - return self.i - - def decode_float(self, value): - self.fs = str(value, 'ascii') + '00' - self.f = struct.unpack('>f', bytes.fromhex(self.fs)) - return str(self.f[0]) - - def decode_float_2(self, value): - self.fs = str(value, 'ascii') + '0000' - self.f = struct.unpack('>f', bytes.fromhex(self.fs)) - return str(self.f[0]) - - def get_input(self): - self.str_get_input = input("please enter a character to send: ") - return self.str_get_input - - def send(self, str_input): - self.xs = str_input.encode() - self.ser.write(self.xs) - - def send_float(self, float_input): - ba = bytearray(struct.pack('>f', float_input)) - self.ser.write(ba[0:3]) diff --git a/BiogasControllerApp-V2.3/biogascontrollerapp.py b/BiogasControllerApp-V2.3/biogascontrollerapp.py deleted file mode 100644 index 57b2ccf..0000000 --- a/BiogasControllerApp-V2.3/biogascontrollerapp.py +++ /dev/null @@ -1,893 +0,0 @@ -import os -import configparser -import serial - -config = configparser.ConfigParser() -config.read('./config/settings.ini') -co = config['Dev Settings']['verbose'] -if co == "True": - pass -else: - os.environ["KIVY_NO_CONSOLELOG"] = "1" - -import threading -import platform -import webbrowser -from kivy.uix.screenmanager import Screen, ScreenManager -from kivy.core.window import Window -from kivy.uix.popup import Popup -from kivy.app import App -from kivy.lang import Builder -from kivy.clock import mainthread, Clock -import bin.lib.lib -import bin.lib.communication -import bin.lib.comport_search -import bin.lib.csv_parsers -import logging -import datetime -import time - -version_app = f"{config['Info']['version']}{config['Info']['subVersion']}" - -################################################################ -# LOGGER SETUP -################## -logging.basicConfig(level=logging.DEBUG, filename="./log/main_log.log", filemode="w") -logs = f"./log/{datetime.datetime.now()}-log-main.log" -logger = logging.getLogger(__name__) -handler = logging.FileHandler(logs) -formatter = logging.Formatter("%(levelname)s - %(asctime)s - %(name)s: %(message)s -- %(lineno)d") -handler.setFormatter(formatter) -logger.addHandler(handler) - -logger.setLevel(config['Dev Settings']['log_level']) -logger.info(f"Logger initialized, app is running Version: {version_app}") -################################################################# - -if config['Port Settings']['specificPort'] == "None" or "\"\"": - special_port = "" -else: - special_port = config['Port Settings']['specificPort'] -cvr = bin.lib.csv_parsers.CsvRead() -cvw = bin.lib.csv_parsers.CsvWrite() -com = bin.lib.lib.Com() - - -################################################################# -# Settings Handler -######################### -class SettingsHandler: - def __init__(self): - self.ports = None - self.window_sizeh = 600 - self.window_sizew = 800 - - def settingshandler(self): - self.ports = config['Port Settings']['specificPort'] - self.window_sizeh = config['UI Config']['sizeH'] - self.window_sizew = config['UI Config']['sizeW'] - Window.size = (int(self.window_sizew), int(self.window_sizeh)) - - -################################################################# - - -logger.info("Started modules") - - -################################################################## -# Popups -################################################################## - - -class QuitPU(Popup): - def quitapp(self): - com.quitcom() - logger.debug("App stopped") - - -class NoConnection(Popup): - def details(self): - self.detailsinfo = DetailInfo() - self.detailsinfo.open() - - -class DetailInfo(Popup): - update_details = "" - def infos(self): - self.err = "" - try: - com.connect(19200, special_port) - com.quitcom() - except Exception as err: - self.err += "Errormessage:\n" - self.err += str(err) - self.err += "\n-------------------------------------------------------------------------------------------------------------------------------------------------------------\n" - return str(self.err) - - def error_tips(self): - self.err_tip = "" - try: - com.connect(19200, special_port) - com.quitcom() - except Exception as err: - self.err_tip += "Possible way to resolve the issue: \n\n" - if str(err)[0:10] == "[Errno 13]": - if platform.system() == "Linux": - self.err_tip += f"Open a terminal and type in: sudo chmod 777 {bin.lib.comport_search.ComportService().get_comport(special_port)}" - elif platform.system() == "Macintosh": - self.err_tip += "Give permission to access the cable" - elif platform.system() == "Windows": - self.err_tip += "Try a different cable or install another driver" - else: - self.err_tip += "Unknown OS" - elif str(err)[0:10] == "[Errno 2] ": - if platform.system() == "Linux": - self.err_tip += "Connect a cable, open a terminal and type in: sudo chmod 777 /dev/ttyUSB0" - elif platform.system() == "Macintosh": - self.err_tip += "Give permission to access the cable" - elif platform.system() == "Windows": - self.err_tip += "Try a different cable or install another driver" - else: - self.err_tip += "Unknown OS" - elif str(err)[0:34] == "could not open port '/dev/ttyUSB0'": - self.err_tip += "Please connect the PC with the microcontroller!" - elif str(err)[0:26] == f"could not open port '{bin.lib.comport_search.ComportService().get_comport(special_port)}'": - self.err_tip += "Try using a different cable or close all monitoring software (like MSI Afterburner)" - else: - self.err_tip += "Special Error, consult the manual of Serial" - return str(self.err_tip) - - -class Modeswitch(Popup): - pass - - -class Connecting_PU(Popup): - pass - - -class Disconnecting_PU(Popup): - pass - - -class MissingFieldsError(Popup): - pass - - -class ConnectionFail(Popup): - pass - - -class SaveConf(Popup): - pass - - -class InfoPU(Popup): - def notshowanymore(self): - config.set("License", "show", "0") - with open("./config/settings.ini", "w") as configfile: - config.write(configfile) - self.dismiss() - - -#################################################################### -# SCREENS -#################################################################### -class HomeScreen(Screen): - def reset(self): - logger.info("HomeScreen initialised") - SettingsHandler().settingshandler() - self.connected = 1 - self.info = f"You are currently running Version {version_app} - If you encounter a bug, please report it!" - try: - com.connect(19200, special_port) - com.quitcom() - except Exception as e: - self.connected = 0 - logger.error(e) - return self.info - - def openlicensepu(self): - self.licensepu = InfoPU() - self.licensepu.open() - - def tryconnection(self): - if config["License"]["show"] == "1": - self.openlicensepu() - logger.info("Showing License info") - else: - pass - try: - com.connect(19200, special_port) - com.quitcom() - self.connected = 1 - self.manager.current = "Readout" - self.manager.transition.direction = "right" - except Exception as ex: - if config['Dev Settings']['disableConnectionCheck'] == "True": - self.connected = 1 - self.manager.current = "Readout" - self.manager.transition.direction = "right" - else: - self.connected = 0 - logger.error(f"COM_error: {ex}") - self.open_popup() - - def open_popup(self): - self.popups = NoConnection() - self.popups.open() - - def exitapp(self): - self.pup = QuitPU() - self.pup.open() - - -class ReadoutScreen(Screen): - go = 1 - - def start_com(self): - self.comstart(1) - logger.info("Trying to start COM") - - def comstart(self, pu_on): - try: - com.connect(19200, special_port) - self.go = 1 - except Exception as e: - self.go = 0 - logger.error(f"COM_error: {e}") - - if self.go == 1: - logger.debug("COM start success") - self.parent.current = "Readout" - if pu_on == 1: - self.openstartpu() - else: - pass - self.communication = threading.Thread(name="communication", target=self.start_coms) - self.communication.start() - else: - self.openconnectionfailpu() - - def end_com(self): - self.stopcom(1) - - def stopcom(self, pu_on): - self.go = 0 - try: - self.communication.join() - except Exception as e: - logger.warning(f"COM_Close_Error: {e}") - if pu_on == 1: - self.openendpu() - else: - pass - - def start_coms(self): - self.check = 1 - self.__level = 0 - self.__distance = 0 - self.__x = "" - self.__begin = time.time() - self.go = 1 - logger.info("Starting COM_Hook") - while self.__x != "\n": - if time.time() - self.__begin > 5: - self.go = 0 - break - else: - self.__x = com.decode_ascii(com.receive(1)) - - if self.go == 1: - logger.info("COM_Hook 1 success") - while self.__level < 3: - self.__x = com.decode_ascii(com.receive(1)) - if self.__x == " ": - if self.__distance == 4: - self.__level += 1 - else: - pass - self.__distance = 0 - else: - if self.__distance > 4: - self.__level = 0 - self.__distance = 0 - else: - self.__distance += 1 - self.check = 1 - logger.info("COM_Hook successful") - com.receive(5) - else: - self.check = 0 - - while self.go == 1: - self.__starttime = time.time() - self.__output = "" - self.__data_recieve = com.receive(68) - self.__output += "Tadc: " - self.__output += str(com.decode_int(self.__data_recieve[0:4])) - self.__output += "\nTemperatur: " - self.__output += com.decode_float(self.__data_recieve[5:11]) - self.__output += f"\nDuty-Cycle: {(float(com.decode_float_2(self.__data_recieve[48:52])) / 65535) * 100}%" - self.change_screen(1, self.__output) - self.__output = "Tadc: " - self.__output += str(com.decode_int(self.__data_recieve[12:16])) - self.__output += "\nTemperatur: " - self.__output += com.decode_float(self.__data_recieve[17:23]) - self.__output += f"\nDuty-Cycle: {(float(com.decode_float_2(self.__data_recieve[53:57])) / 65535) * 100}%" - self.change_screen(2, self.__output) - self.__output = "Tadc: " - self.__output += str(com.decode_int(self.__data_recieve[24:28])) - self.__output += "\nTemperatur: " - self.__output += com.decode_float(self.__data_recieve[29:35]) - self.__output += f"\nDuty-Cycle: {(float(com.decode_float_2(self.__data_recieve[58:62])) / 65535) * 100}%" - self.change_screen(3, self.__output) - self.__output = "Tadc: " - self.__output += str(com.decode_int(self.__data_recieve[36:40])) - self.__output += "\nTemperatur: " - self.__output += com.decode_float(self.__data_recieve[41:47]) - self.__output += "\nDuty-Cycle: " - self.__output += f"\nDuty-Cycle: {(float(com.decode_float_2(self.__data_recieve[63:67])) / 65535) * 100}%" - self.change_screen(4, self.__output) - self.change_screen(5, f"F={1 / (time.time() - self.__starttime)}") - self.change_screen(6, "") - com.quitcom() - - def switch_mode(self, text): - self.go = 0 - try: - self.communication.join() - com.quitcom() - self.com_ok = 1 - logger.info("Mode_Switch successful") - except Exception as e: - if e == serial.SerialException: - logger.info("No running process found, continuing") - else: - logger.fatal(f"FATAL ERROR OCCURED, APP WILL LEAVE NOW: {e}") - self.com_ok = 0 - - if self.com_ok == 1: - if text == "Normal Mode": - bin.lib.communication.SwitchMode().disable_fastmode(special_port) - else: - bin.lib.communication.SwitchMode().enable_fastmode(special_port) - logger.info("Switched mode, restarting COM") - self.openpupups() - self.comstart(0) - logger.info("COM restarted successfully") - else: - self.check = 1 - self.ids.mode_sel.state = "normal" - self.openconnectionfailpu() - - @mainthread - def change_screen(self, pos, value): - if pos == 1: - self.ids.sonde1.text = value - elif pos == 2: - self.ids.sonde2.text = value - elif pos == 3: - self.ids.sonde3.text = value - elif pos == 4: - self.ids.sonde4.text = value - elif pos == 6: - logger.error("COM_fail") - self.openconnectionfailpu() - else: - self.ids.frequency.text = value - - def openpupups(self): - self.popup = Modeswitch() - self.popup.open() - - def openendpu(self): - self.pu = Disconnecting_PU() - self.pu.open() - - def openstartpu(self): - self.pup = Connecting_PU() - self.pup.open() - - def openconnectionfailpu(self): - if self.check == 0: - self.cfpu = ConnectionFail() - self.cfpu.open() - else: - pass - - def leave_screen(self): - logger.info("Stopping COM") - self.stopcom(0) - - def resscreen(self): - logger.info("Screen reset") - self.ids.sonde1.text = "" - self.ids.sonde2.text = "" - self.ids.sonde3.text = "" - self.ids.sonde4.text = "" - self.ids.frequency.text = "" - - -class Program(Screen): - def read_config(self): - logger.debug("Reading config") - self.config_imp = [] - self.__export = [] - self.config_imp = cvr.importing("./config/config.csv") - self.__export = self.config_imp.pop(0) - self.__extracted = self.__export.pop(0) - logger.debug(f"config {self.__extracted}") - if self.__extracted == "1": - self.ids.prsel.state = "normal" - self.ids.s1_a.text = "" - self.ids.s1_b.text = "" - self.ids.s1_c.text = "" - self.ids.s1_t.text = "" - self.ids.s2_a.text = "" - self.ids.s2_b.text = "" - self.ids.s2_c.text = "" - self.ids.s2_t.text = "" - self.ids.s3_a.text = "" - self.ids.s3_b.text = "" - self.ids.s3_c.text = "" - self.ids.s3_t.text = "" - self.ids.s4_a.text = "" - self.ids.s4_b.text = "" - self.ids.s4_c.text = "" - self.ids.s4_t.text = "" - self.__mode = 1 - else: - self.ids.prsel.state = "down" - Clock.schedule_once(self.read_data, 1) - self.__mode = 2 - - def change_mode(self): - logger.info("Changing mode") - logger.debug(f"mode was: {self.__mode}") - if self.__mode == 1: - logger.debug("Sending instruction to read info") - Clock.schedule_once(self.read_data, 1) - self.__mode = 2 - else: - self.ids.s1_a.text = "" - self.ids.s1_b.text = "" - self.ids.s1_c.text = "" - self.ids.s1_t.text = "" - self.ids.s2_a.text = "" - self.ids.s2_b.text = "" - self.ids.s2_c.text = "" - self.ids.s2_t.text = "" - self.ids.s3_a.text = "" - self.ids.s3_b.text = "" - self.ids.s3_c.text = "" - self.ids.s3_t.text = "" - self.ids.s4_a.text = "" - self.ids.s4_b.text = "" - self.ids.s4_c.text = "" - self.ids.s4_t.text = "" - self.__mode = 1 - - def read_data(self, dt): - logger.debug("Starting to read data from the microcontroller") - try: - com.connect(19200, special_port) - self.go = 1 - except Exception as e: - self.go = 0 - logger.error(f"COM_error: {e}") - - if self.go == 1: - logger.info("Sending instructions") - com.send("RD") - self.__pos = 1 - self.__beginning = time.time() - logger.info("Awaiting confirmation from the microcontroller for hook") - while True: - if time.time() - self.__beginning < 5: - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "R": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "D": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.go = 1 - logger.info("Hook successful") - break - else: - pass - else: - pass - else: - pass - else: - pass - else: - self.go = 0 - logger.error("Microcontroller not available, stopping connection") - break - if self.go == 1: - for i in range(4): - self.__x = com.receive(28) - self.__a = str(com.decode_float(self.__x[0:6])) - self.__b = str(com.decode_float(self.__x[7:13])) - self.__c = str(com.decode_float(self.__x[14:20])) - self.__temp = str(com.decode_float(self.__x[21:27])) - if self.__pos == 1: - self.ids.s1_a.text = self.__a - self.ids.s1_b.text = self.__b - self.ids.s1_c.text = self.__c - self.ids.s1_t.text = self.__temp - elif self.__pos == 2: - self.ids.s2_a.text = self.__a - self.ids.s2_b.text = self.__b - self.ids.s2_c.text = self.__c - self.ids.s2_t.text = self.__temp - elif self.__pos == 3: - self.ids.s3_a.text = self.__a - self.ids.s3_b.text = self.__b - self.ids.s3_c.text = self.__c - self.ids.s3_t.text = self.__temp - elif self.__pos == 4: - self.ids.s4_a.text = self.__a - self.ids.s4_b.text = self.__b - self.ids.s4_c.text = self.__c - self.ids.s4_t.text = self.__temp - self.__pos += 1 - logger.info("Recieved info from microcontroller") - else: - self.open_confail_pu() - com.quitcom() - else: - self.open_confail_pu() - - def create_com(self): - self.coms = bin.lib.communication.Communication() - - def send_data(self): - try: - self.create_com() - self.go = 1 - except Exception as e: - self.go = 0 - logger.critical(f"TRANSMISSION_Error: {e}") - - if self.go == 1: - logger.info("Preparing data to be sent") - self.__transmit = [] - if self.ids.s1_a.text != "" and self.ids.s1_b.text != "" and self.ids.s1_c.text != "" and self.ids.s1_t.text != "" and self.ids.s2_a.text != "" and self.ids.s2_b.text != "" and self.ids.s2_c.text != "" and self.ids.s2_t.text != "" and self.ids.s3_a.text != "" and self.ids.s3_b.text != "" and self.ids.s3_c.text != "" and self.ids.s3_t.text != "" and self.ids.s4_a.text != "" and self.ids.s4_b.text != "" and self.ids.s4_c.text != "" and self.ids.s4_t.text != "": - self.__transmit.append(self.ids.s1_a.text) - self.__transmit.append(self.ids.s1_b.text) - self.__transmit.append(self.ids.s1_c.text) - self.__transmit.append(self.ids.s1_t.text) - self.__transmit.append(self.ids.s2_a.text) - self.__transmit.append(self.ids.s2_b.text) - self.__transmit.append(self.ids.s2_c.text) - self.__transmit.append(self.ids.s2_t.text) - self.__transmit.append(self.ids.s3_a.text) - self.__transmit.append(self.ids.s3_b.text) - self.__transmit.append(self.ids.s3_c.text) - self.__transmit.append(self.ids.s3_t.text) - self.__transmit.append(self.ids.s4_a.text) - self.__transmit.append(self.ids.s4_b.text) - self.__transmit.append(self.ids.s4_c.text) - self.__transmit.append(self.ids.s4_t.text) - logger.debug("trying to send...") - try: - self.coms.change_all(self.__transmit, special_port) - logger.info("Transmission successful") - logger.debug("purging fields...") - self.ids.s1_a.text = "" - self.ids.s1_b.text = "" - self.ids.s1_c.text = "" - self.ids.s1_t.text = "" - self.ids.s2_a.text = "" - self.ids.s2_b.text = "" - self.ids.s2_c.text = "" - self.ids.s2_t.text = "" - self.ids.s3_a.text = "" - self.ids.s3_b.text = "" - self.ids.s3_c.text = "" - self.ids.s3_t.text = "" - self.ids.s4_a.text = "" - self.ids.s4_b.text = "" - self.ids.s4_c.text = "" - self.ids.s4_t.text = "" - self.openconfpu() - except Exception as e: - self.open_confail_pu() - logger.critical(f"TRANSMITION_Error: {e}") - else: - self.openerrorpu() - else: - self.open_confail_pu() - - def openerrorpu(self): - self.pu = MissingFieldsError() - self.pu.open() - - def open_confail_pu(self): - self.cfpu = ConnectionFail() - self.cfpu.open() - - def openconfpu(self): - self.confpus = SaveConf() - self.confpus.open() - - -class ProgramTemp(Screen): - def read_config(self): - logger.debug("Reading config") - self.config_imp = [] - self.__export = [] - self.config_imp = cvr.importing("./config/config.csv") - self.__export = self.config_imp.pop(0) - self.__extracted = self.__export.pop(0) - logger.debug(f"Mode set is: {self.__extracted}") - if self.__extracted == "1": - self.ids.prsel.state = "normal" - self.ids.temp_s1.text = "" - self.ids.temp_s2.text = "" - self.ids.temp_s3.text = "" - self.ids.temp_s4.text = "" - self.__mode = 1 - else: - self.ids.prsel.state = "down" - Clock.schedule_once(self.read_data, 1) - self.__mode = 2 - - def change_mode(self): - logger.info("Changing mode") - logger.debug(f"Mode was: {self.__mode}") - if self.__mode == 1: - logger.info("starting sub-thread") - Clock.schedule_once(self.read_data, 1) - self.__mode = 2 - else: - logger.info("clearing screen") - self.ids.temp_s1.text = "" - self.ids.temp_s2.text = "" - self.ids.temp_s3.text = "" - self.ids.temp_s4.text = "" - self.__mode = 1 - - def read_data(self, dt): - logger.info("Trying to establish connection...") - try: - com.connect(19200, special_port) - self.go = 1 - except Exception as e: - self.go = 0 - logger.error(f"COM_Error: {e}") - - if self.go == 1: - logger.info("Sending instructions to microcontroller...") - com.send("RD") - self.__pos = 1 - self.__beginning = time.time() - self.go = 1 - logger.info("Awaiting confirmation from the microcontroller for hook") - while True: - if time.time() - self.__beginning < 5: - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "R": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "D": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.go = 1 - logger.info("Hook successful") - break - else: - pass - else: - pass - else: - pass - else: - pass - else: - self.go = 0 - logger.error("Microcontroller not available, stopping connection") - break - if self.go == 1: - logger.info("Receiving data...") - for i in range(4): - self.__x = com.receive(28) - self.__output = str(com.decode_float(self.__x[21:27])) - if self.__pos == 1: - self.ids.temp_s1.text = self.__output - elif self.__pos == 2: - self.ids.temp_s2.text = self.__output - elif self.__pos == 3: - self.ids.temp_s3.text = self.__output - elif self.__pos == 4: - self.ids.temp_s4.text = self.__output - self.__pos += 1 - logger.info("Recieved data") - com.quitcom() - else: - self.open_confail_pu() - - def create_com(self): - self.coms = bin.lib.communication.Communication() - - def send_data(self): - try: - self.create_com() - self.go = 1 - except Exception as e: - self.go = 0 - logger.critical(f"COM_Error: Microcontroller unavailable: {e}") - - if self.go == 1: - logger.info("Preparing transmission...") - self.__transmit = [] - if self.ids.temp_s1.text != "" and self.ids.temp_s2.text != "" and self.ids.temp_s3.text != "" and self.ids.temp_s4.text != "": - self.__transmit.append(self.ids.temp_s1.text) - self.__transmit.append(self.ids.temp_s2.text) - self.__transmit.append(self.ids.temp_s3.text) - self.__transmit.append(self.ids.temp_s4.text) - logger.debug("Transmitting...") - self.coms.change_temp(self.__transmit, special_port) - self.ids.temp_s1.text = "" - self.ids.temp_s2.text = "" - self.ids.temp_s3.text = "" - self.ids.temp_s4.text = "" - self.openconfpu() - else: - self.openerrorpu() - logger.debug("Missing fields") - else: - self.open_confail_pu() - - def openerrorpu(self): - self.pu = MissingFieldsError() - self.pu.open() - - def openconfpu(self): - self.confpu = SaveConf() - self.confpu.open() - - def open_confail_pu(self): - self.cfpu = ConnectionFail() - self.cfpu.open() - - -class ReadData(Screen): - def read_data(self): - logger.info("Trying to connect to the microcontroller") - try: - com.connect(19200, special_port) - self.go = 1 - except Exception as e: - self.go = 0 - logger.error(f"COM_Error: {e}") - - if self.go == 1: - logger.info("Sending instructions to the microcontroller...") - com.send("RD") - self.__pos = 1 - self.__beginning = time.time() - self.go = 1 - logger.info("Awaiting confirmation from the microcontroller for hook") - while True: - if time.time() - self.__beginning < 5: - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "R": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "D": - self.__data_recieve = com.decode_ascii(com.receive(1)) - if self.__data_recieve == "\n": - self.go = 1 - logger.info("Hook successful") - break - else: - pass - else: - pass - else: - pass - else: - pass - else: - self.go = 0 - logger.error("Microcontroller not available, stopping connection") - break - if self.go == 1: - logger.info("Receiving data") - for i in range(4): - self.__x = com.receive(28) - self.__output = "a: " - self.__output += str(com.decode_float(self.__x[0:6])) - self.__output += f"\nb: {str(com.decode_float(self.__x[7:13]))}" - self.__output += f"\nc: {str(com.decode_float(self.__x[14:20]))}" - self.__output += f"\nTemp: {str(com.decode_float(self.__x[21:27]))}" - if self.__pos == 1: - self.ids.inf_sonde1.text = self.__output - elif self.__pos == 2: - self.ids.inf_sonde2.text = self.__output - elif self.__pos == 3: - self.ids.inf_sonde3.text = self.__output - elif self.__pos == 4: - self.ids.inf_sonde4.text = self.__output - self.__pos += 1 - logger.info("Received data") - else: - self.open_confail_pu() - com.quitcom() - else: - self.open_confail_pu() - - def open_confail_pu(self): - self.cfpu = ConnectionFail() - self.cfpu.open() - - -class Credits(Screen): - pass - - -class Modify(Screen): - def read_config(self): - logger.debug("Reading config") - self.config_imp = [] - self.__export = [] - self.config_imp = cvr.importing("./config/config.csv") - self.__export = self.config_imp.pop(0) - self.__extracted = self.__export.pop(0) - logger.debug(f"Mode at: {self.__extracted}") - if self.__extracted == "1": - self.ids.prsel.state = "normal" - else: - self.ids.prsel.state = "down" - - def issue_reporting(self): - logger.info("Clicked error reporting button") - webbrowser.open("https://github.com/simplePCBuilding/BiogasControllerApp/issues", new=2) - - def change_programming(self): - logger.info("Switching programming mode") - self.csv_import = [] - self.csv_import = cvr.importing("./config/config.csv") - self.csv_import.pop(0) - if self.ids.prsel.text == "Full\nreprogramming": - self.csv_import.insert(0, 1) - else: - self.csv_import.insert(0, 2) - logger.debug(f"Mode now: {self.csv_import}") - cvw.write_str("./config/config.csv", self.csv_import) - - -######################################################## -# Screenmanager -######################################################## - - -class RootScreen(ScreenManager): - pass - - -class BiogasControllerApp(App): - def build(self): - self.icon = "./BiogasControllerAppLogo.png" - self.title = "BiogasControllerApp" - return Builder.load_file("./bin/gui/gui.kv") - - -logger.info("Init finished, starting UI") - -try: - if __name__ == "__main__": - bga = BiogasControllerApp() - bga.run() - -except Exception as e: - logger.critical(e, exc_info=True) diff --git a/BiogasControllerApp-V2.3/config/config.csv b/BiogasControllerApp-V2.3/config/config.csv deleted file mode 100644 index 0cfbf08..0000000 --- a/BiogasControllerApp-V2.3/config/config.csv +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/BiogasControllerApp-V2.3/config/settings.ini b/BiogasControllerApp-V2.3/config/settings.ini deleted file mode 100644 index 89889cd..0000000 --- a/BiogasControllerApp-V2.3/config/settings.ini +++ /dev/null @@ -1,19 +0,0 @@ -[Port Settings] -specificport = None - -[UI Config] -sizeh = 600 -sizew = 800 - -[Dev Settings] -verbose = False -log_level = DEBUG -disableconnectioncheck = False - -[License] -show = 1 - -[Info] -version = V2.3.0 -subversion = - diff --git a/BiogasControllerApp-V2.3/log/logging.md b/BiogasControllerApp-V2.3/log/logging.md deleted file mode 100644 index ee2d608..0000000 --- a/BiogasControllerApp-V2.3/log/logging.md +++ /dev/null @@ -1,5 +0,0 @@ -What is getting logged? -Generally this app logs how and when you interact with the app and sometimes, which values you enter. -No logs are being sent to anybody automatically, you can choose to attach the log file to the bug report. -This helps the devs a lot, as they can better understand the state of the app as it crashed. The logs are -all found in this folder here. \ No newline at end of file diff --git a/biogascontrollerapp/biogascontrollerapp.py b/biogascontrollerapp/biogascontrollerapp.py new file mode 100644 index 0000000..e69de29 From d26e91db314338a8acd4c368fb27273e5bbd6f40 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Wed, 5 Mar 2025 10:12:02 +0100 Subject: [PATCH 02/31] Add gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ From ffd75d94dc2ef8481915836ae3bad113a1fa7a3d Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Wed, 5 Mar 2025 11:20:01 +0100 Subject: [PATCH 03/31] Begin rewrite --- biogascontrollerapp/gui/credits.kv | 0 biogascontrollerapp/gui/home.kv | 0 biogascontrollerapp/gui/popups.kv | 0 biogascontrollerapp/gui/program.kv | 0 biogascontrollerapp/gui/settings.kv | 0 biogascontrollerapp/lib/com.py | 80 +++++++++++++++++++++++++ biogascontrollerapp/lib/decoder.py | 17 ++++++ biogascontrollerapp/lib/instructions.py | 22 +++++++ biogascontrollerapp/lib/test/com.py | 36 +++++++++++ 9 files changed, 155 insertions(+) create mode 100644 biogascontrollerapp/gui/credits.kv create mode 100644 biogascontrollerapp/gui/home.kv create mode 100644 biogascontrollerapp/gui/popups.kv create mode 100644 biogascontrollerapp/gui/program.kv create mode 100644 biogascontrollerapp/gui/settings.kv create mode 100644 biogascontrollerapp/lib/com.py create mode 100644 biogascontrollerapp/lib/decoder.py create mode 100644 biogascontrollerapp/lib/instructions.py create mode 100644 biogascontrollerapp/lib/test/com.py diff --git a/biogascontrollerapp/gui/credits.kv b/biogascontrollerapp/gui/credits.kv new file mode 100644 index 0000000..e69de29 diff --git a/biogascontrollerapp/gui/home.kv b/biogascontrollerapp/gui/home.kv new file mode 100644 index 0000000..e69de29 diff --git a/biogascontrollerapp/gui/popups.kv b/biogascontrollerapp/gui/popups.kv new file mode 100644 index 0000000..e69de29 diff --git a/biogascontrollerapp/gui/program.kv b/biogascontrollerapp/gui/program.kv new file mode 100644 index 0000000..e69de29 diff --git a/biogascontrollerapp/gui/settings.kv b/biogascontrollerapp/gui/settings.kv new file mode 100644 index 0000000..e69de29 diff --git a/biogascontrollerapp/lib/com.py b/biogascontrollerapp/lib/com.py new file mode 100644 index 0000000..f884688 --- /dev/null +++ b/biogascontrollerapp/lib/com.py @@ -0,0 +1,80 @@ +from typing import Optional +import serial +import struct +import serial.tools.list_ports + + +class Com: + def __init__(self, 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 = '' + + def set_port_override(self, override: str) -> None: + """Set the port override, to disable port search""" + self._port_override = override + + def get_comport(self) -> str: + """Find the comport the microcontroller has attached to""" + if self._port_override != '': + return self._port_override + + # Catch all errors and simply return an empty string if search unsuccessful + try: + # Get an array of all used comports + ports = [comport.device for comport in serial.tools.list_ports.comports()] + + # Filter for specific controller + for comport in ports: + for filter in self._filters: + if ( filter in comport ): + return comport + except: + pass + + return '' + + def connect(self, baud_rate: int, port_override: Optional[str] = None) -> bool: + """Try to find a comport and connect to the microcontroller. Returns the success as a boolean""" + comport = self.get_comport() + + # Comport search returns empty string if search unsuccessful + if comport == '': + try: + self._serial = serial.Serial(comport, baud_rate, timeout=5) + except: + return False + return True + else: + return False + + def close(self) -> None: + """Close the serial connection, if possible""" + if self._serial != None: + try: + self._serial.close() + except: + 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""" + if self._serial == None: + self.connect(19200) + if self._serial != None: + return self._serial.read(byte_count) + else: + raise Exception('ERR_CONNECTION') + + def send(self, msg: str) -> None: + """Send a string over serial connection.""" + if self._serial == None: + self.connect(19200) + if self._serial != None: + self._serial.write(msg.encode()) + + def send_float(self, msg: float) -> None: + """Send a float number over serial connection""" + if self._serial == None: + self.connect(19200) + if self._serial != None: + self._serial.write(bytearray(struct.pack('>f', msg))[0:3]) diff --git a/biogascontrollerapp/lib/decoder.py b/biogascontrollerapp/lib/decoder.py new file mode 100644 index 0000000..b8f1b2f --- /dev/null +++ b/biogascontrollerapp/lib/decoder.py @@ -0,0 +1,17 @@ +import struct + +class Decoder: + def decode_ascii(self, value: bytes) -> str: + try: + return value.decode() + except: + return 'Error' + + def decode_float(self, value: bytes) -> float: + return struct.unpack('>f', bytes.fromhex(str(value, 'ascii') + '00'))[0] + + def decode_float_long(self, value: bytes) -> float: + return struct.unpack('>f', bytes.fromhex(str(value, 'ascii') + '0000'))[0] + + def decode_int(self, value: bytes) -> int: + return int(value, base=16) diff --git a/biogascontrollerapp/lib/instructions.py b/biogascontrollerapp/lib/instructions.py new file mode 100644 index 0000000..182bde2 --- /dev/null +++ b/biogascontrollerapp/lib/instructions.py @@ -0,0 +1,22 @@ +from typing import Optional +import lib.com +import lib.decoder + +# TODO: Load filters (for comport search) +com = lib.com.Com() +decoder = lib.decoder.Decoder() + +class Instructions: + def __init__(self) -> None: + pass + + def _hook(self, instruction: str, sequence: list[str]) -> bool: + return False + + def change_temperature(self, new_temps: list[float]) -> None: + pass + + def change_config(self, new_config: list[float]) -> None: + pass + + diff --git a/biogascontrollerapp/lib/test/com.py b/biogascontrollerapp/lib/test/com.py new file mode 100644 index 0000000..4875b21 --- /dev/null +++ b/biogascontrollerapp/lib/test/com.py @@ -0,0 +1,36 @@ +""" +Library to be used in standalone mode (without microcontroller, for testing functionality) +""" + +from typing import Optional +import queue + + +class Com: + def __init__(self) -> None: + # Initialize queue with values to be sent on call of recieve (add like three or so at a time) + self._port_override = '' + + def set_port_override(self, override: str) -> None: + """Set the port override, to disable port search""" + self._port_override = override + + def get_comport(self) -> str: + return 'test' if self._port_override != '' else self._port_override + + def connect(self, baud_rate: int, port_override: Optional[str] = None) -> bool: + return True # TODO: For testing, make cases where there is no successful connection, i.e. we return false + + def close(self) -> None: + pass + + def receive(self, byte_count: int) -> None: + # TODO: Make it return simulated data + pass + + def send(self, msg: str) -> None: + # TODO: Use LUT to find what should be added to the queue for read + pass + + def send_float(self, msg: float) -> None: + pass From 92fcc4a6e7ba24d25630030caaea237e61891b59 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Wed, 5 Mar 2025 14:07:58 +0100 Subject: [PATCH 04/31] App launching, some porting work complete --- .../BiogasControllerAppLogo.png | Bin 0 -> 36750 bytes biogascontrollerapp/biogascontrollerapp.py | 52 +++++++++ biogascontrollerapp/config.ini | 19 ++++ biogascontrollerapp/gui/PopupManager.py | 3 + biogascontrollerapp/gui/credits.kv | 0 biogascontrollerapp/gui/credits/credits.kv | 27 +++++ biogascontrollerapp/gui/credits/credits.py | 8 ++ biogascontrollerapp/gui/home.kv | 0 biogascontrollerapp/gui/home/home.kv | 46 ++++++++ biogascontrollerapp/gui/home/home.py | 17 +++ biogascontrollerapp/gui/popups.kv | 0 biogascontrollerapp/gui/popups/popups.kv | 105 ++++++++++++++++++ biogascontrollerapp/gui/popups/popups.py | 16 +++ .../gui/{ => program}/program.kv | 0 biogascontrollerapp/gui/program/program.py | 16 +++ biogascontrollerapp/gui/settings.kv | 0 biogascontrollerapp/gui/settings/settings.kv | 37 ++++++ biogascontrollerapp/gui/settings/settings.py | 10 ++ biogascontrollerapp/lib/com.py | 38 +++++-- biogascontrollerapp/lib/instructions.py | 51 ++++++++- 20 files changed, 428 insertions(+), 17 deletions(-) create mode 100644 biogascontrollerapp/BiogasControllerAppLogo.png create mode 100644 biogascontrollerapp/config.ini create mode 100644 biogascontrollerapp/gui/PopupManager.py delete mode 100644 biogascontrollerapp/gui/credits.kv create mode 100644 biogascontrollerapp/gui/credits/credits.kv create mode 100644 biogascontrollerapp/gui/credits/credits.py delete mode 100644 biogascontrollerapp/gui/home.kv create mode 100644 biogascontrollerapp/gui/home/home.kv create mode 100644 biogascontrollerapp/gui/home/home.py delete mode 100644 biogascontrollerapp/gui/popups.kv create mode 100644 biogascontrollerapp/gui/popups/popups.kv create mode 100644 biogascontrollerapp/gui/popups/popups.py rename biogascontrollerapp/gui/{ => program}/program.kv (100%) create mode 100644 biogascontrollerapp/gui/program/program.py delete mode 100644 biogascontrollerapp/gui/settings.kv create mode 100644 biogascontrollerapp/gui/settings/settings.kv create mode 100644 biogascontrollerapp/gui/settings/settings.py diff --git a/biogascontrollerapp/BiogasControllerAppLogo.png b/biogascontrollerapp/BiogasControllerAppLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..2bd6e939c33637bbea4af96808833d8e93af07a3 GIT binary patch literal 36750 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 DR*=M! literal 0 HcmV?d00001 diff --git a/biogascontrollerapp/biogascontrollerapp.py b/biogascontrollerapp/biogascontrollerapp.py index e69de29..eec58f6 100644 --- a/biogascontrollerapp/biogascontrollerapp.py +++ b/biogascontrollerapp/biogascontrollerapp.py @@ -0,0 +1,52 @@ +import os +import configparser +from typing import override + +config = configparser.ConfigParser() +config.read('./config.ini') + +# Load config and disable kivy log if necessary +if config['Dev Settings']['verbose'] == "True": + pass +else: + os.environ["KIVY_NO_CONSOLELOG"] = "1" + + +# Load kivy modules +from kivy.core.window import Window, Config +from kivy.uix.screenmanager import ScreenManager +from kivy.app import App + +# Load other libraries +import threading + +# Store the current app version +app_version = f"{config['Info']['version']}{config['Info']['subVersion']}" + +#---------# +# Screens # +#---------# + +from gui.home.home import HomeScreen +from gui.credits.credits import CreditsScreen +from gui.settings.settings import SettingsScreen + +#----------------# +# Screen Manager # +#----------------# +class BiogasControllerApp(App): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen_manager = ScreenManager() + + @override + def build(self): + self.icon = './BiogasControllerAppLogo.png' + self.title = 'BiogasControllerApp-' + app_version + self.screen_manager.add_widget(HomeScreen(name='home')) + self.screen_manager.add_widget(CreditsScreen(name='credits')) + self.screen_manager.add_widget(SettingsScreen(name='settings')) + return self.screen_manager + +if __name__ == "__main__": + BiogasControllerApp().run() diff --git a/biogascontrollerapp/config.ini b/biogascontrollerapp/config.ini new file mode 100644 index 0000000..467137c --- /dev/null +++ b/biogascontrollerapp/config.ini @@ -0,0 +1,19 @@ +[Port Settings] +specificport = None + +[UI Config] +sizeh = 600 +sizew = 800 + +[Dev Settings] +verbose = True +log_level = DEBUG +disableconnectioncheck = False + +[License] +show = 1 + +[Info] +version = V2.3.0 +subversion = + diff --git a/biogascontrollerapp/gui/PopupManager.py b/biogascontrollerapp/gui/PopupManager.py new file mode 100644 index 0000000..adc546c --- /dev/null +++ b/biogascontrollerapp/gui/PopupManager.py @@ -0,0 +1,3 @@ +from gui.popups import * + + diff --git a/biogascontrollerapp/gui/credits.kv b/biogascontrollerapp/gui/credits.kv deleted file mode 100644 index e69de29..0000000 diff --git a/biogascontrollerapp/gui/credits/credits.kv b/biogascontrollerapp/gui/credits/credits.kv new file mode 100644 index 0000000..5f879ad --- /dev/null +++ b/biogascontrollerapp/gui/credits/credits.kv @@ -0,0 +1,27 @@ +: + name: "credits" + canvas.before: + Color: + rgba: (50,50,50,0.2) + Rectangle: + size: self.size + pos: self.pos + FloatLayout: + Button: + text: "back" + size_hint: 0.4, 0.2 + pos_hint: {"x":0.3, "y":0.1} + on_release: + app.root.current = "settings" + 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 rework of the BiogasControllerApp V1, that was originally programmed by S. Reichmuth." + 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 diff --git a/biogascontrollerapp/gui/credits/credits.py b/biogascontrollerapp/gui/credits/credits.py new file mode 100644 index 0000000..4906c62 --- /dev/null +++ b/biogascontrollerapp/gui/credits/credits.py @@ -0,0 +1,8 @@ +from kivy.uix.screenmanager import Screen +from kivy.lang import Builder + + +class CreditsScreen(Screen): + pass + +Builder.load_file('./gui/credits/credits.kv') diff --git a/biogascontrollerapp/gui/home.kv b/biogascontrollerapp/gui/home.kv deleted file mode 100644 index e69de29..0000000 diff --git a/biogascontrollerapp/gui/home/home.kv b/biogascontrollerapp/gui/home/home.kv new file mode 100644 index 0000000..40a78cb --- /dev/null +++ b/biogascontrollerapp/gui/home/home.kv @@ -0,0 +1,46 @@ +: + name: "home" + canvas.before: + Color: + rgba: (50,50,50,0.2) + Rectangle: + size: self.size + pos: self.pos + GridLayout: + cols:1 + Label: + text: "BiogasanlageControllerApp" + font_size: 50 + color: (0, 113, 0, 1) + bold:True + italic:True + FloatLayout: + GridLayout: + cols: 2 + size_hint: 0.8, 0.8 + pos_hint: {"x": 0.1, "y": 0.1} + Button: + text: "Start" + background_color: (255, 0, 0, 0.6) + font_size: 30 + on_release: + root.start() + Button: + text: "Quit" + background_color: (255, 0, 0, 0.6) + font_size: 30 + on_release: + root.quit() + Label: + text: "App version" + id: app_version + font_size: 13 + pos_hint: {"y": -0.45, "x":0.05} + Button: + text: "Settings" + 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_settings() diff --git a/biogascontrollerapp/gui/home/home.py b/biogascontrollerapp/gui/home/home.py new file mode 100644 index 0000000..4273484 --- /dev/null +++ b/biogascontrollerapp/gui/home/home.py @@ -0,0 +1,17 @@ +from kivy.uix.screenmanager import Screen +from kivy.lang import Builder + + +class HomeScreen(Screen): + def start(self): + pass + + def quit(self): + pass + + def to_settings(self): + self.manager.current = 'settings' + self.manager.transition.direction = 'down' + + +Builder.load_file('./gui/home/home.kv') diff --git a/biogascontrollerapp/gui/popups.kv b/biogascontrollerapp/gui/popups.kv deleted file mode 100644 index e69de29..0000000 diff --git a/biogascontrollerapp/gui/popups/popups.kv b/biogascontrollerapp/gui/popups/popups.kv new file mode 100644 index 0000000..6c6f922 --- /dev/null +++ b/biogascontrollerapp/gui/popups/popups.kv @@ -0,0 +1,105 @@ +: + title: "INFORMATION" + size_hint: 0.7, 0.5 + auto_dismiss: True + GridLayout: + cols: 1 + Label: + id: msg + text_size: self.width, None + GridLayout: + cols: 1 + Button: + text: "Ok" + on_release: + root.dismiss() + +: + 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() + +: + 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 + GridLayout: + cols:2 + Button: + id: btn1 + text: "Details" + on_release: + root.action() + Button: + text:"Ok" + on_release: + root.dismiss() + +: + 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() + +: + title: "DETAILS" + font_size: 50 + size_hint: 1, 0.7 + auto_dismiss: False + GridLayout: + Label: + cols:1 + 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() diff --git a/biogascontrollerapp/gui/popups/popups.py b/biogascontrollerapp/gui/popups/popups.py new file mode 100644 index 0000000..5971a70 --- /dev/null +++ b/biogascontrollerapp/gui/popups/popups.py @@ -0,0 +1,16 @@ +from kivy.uix.screenmanager import Screen +from kivy.lang import Builder + + +class HomeScreen(Screen): + def start(self): + pass + + def quit(self): + pass + + def to_settings(self): + pass + + +Builder.load_file('./gui/home/home.kv') diff --git a/biogascontrollerapp/gui/program.kv b/biogascontrollerapp/gui/program/program.kv similarity index 100% rename from biogascontrollerapp/gui/program.kv rename to biogascontrollerapp/gui/program/program.kv diff --git a/biogascontrollerapp/gui/program/program.py b/biogascontrollerapp/gui/program/program.py new file mode 100644 index 0000000..5971a70 --- /dev/null +++ b/biogascontrollerapp/gui/program/program.py @@ -0,0 +1,16 @@ +from kivy.uix.screenmanager import Screen +from kivy.lang import Builder + + +class HomeScreen(Screen): + def start(self): + pass + + def quit(self): + pass + + def to_settings(self): + pass + + +Builder.load_file('./gui/home/home.kv') diff --git a/biogascontrollerapp/gui/settings.kv b/biogascontrollerapp/gui/settings.kv deleted file mode 100644 index e69de29..0000000 diff --git a/biogascontrollerapp/gui/settings/settings.kv b/biogascontrollerapp/gui/settings/settings.kv new file mode 100644 index 0000000..b1354a7 --- /dev/null +++ b/biogascontrollerapp/gui/settings/settings.kv @@ -0,0 +1,37 @@ +: + name: "settings" + canvas.before: + Color: + rgba: (50,50,50,0.2) + Rectangle: + size: self.size + pos: self.pos + GridLayout: + cols: 1 + Label: + text: "Settings" + font_size: 40 + color: (0, 113, 0, 1) + bold: True + FloatLayout: + GridLayout: + pos_hint: {"x":0.05, "y":0.05} + size_hint: 0.9, 0.9 + cols: 3 + Button: + text: "Back" + background_color: (255,0,0,0.6) + on_release: + app.root.current = "home" + root.manager.transition.direction = "up" + Button: + text: "Report a\nBug" + background_color: (255,0,0,0.6) + on_release: + root.report_issue() + Button: + text: "Credits" + background_color: (255,0,0,0.6) + on_release: + app.root.current = "credits" + root.manager.transition.direction = "left" diff --git a/biogascontrollerapp/gui/settings/settings.py b/biogascontrollerapp/gui/settings/settings.py new file mode 100644 index 0000000..0251b1c --- /dev/null +++ b/biogascontrollerapp/gui/settings/settings.py @@ -0,0 +1,10 @@ +from kivy.uix.screenmanager import Screen +from kivy.lang import Builder +import webbrowser + + +class SettingsScreen(Screen): + def report_issue(self): + webbrowser.open('https://github.com/janishutz/BiogasControllerApp/issues', new=2) + +Builder.load_file('./gui/settings/settings.kv') diff --git a/biogascontrollerapp/lib/com.py b/biogascontrollerapp/lib/com.py index f884688..279e80d 100644 --- a/biogascontrollerapp/lib/com.py +++ b/biogascontrollerapp/lib/com.py @@ -9,11 +9,22 @@ class Com: 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 = 19200 def set_port_override(self, override: str) -> None: """Set the port override, to disable port search""" self._port_override = override + def _connection_check(self) -> bool: + if self._serial == None: + return self._open() + if self._serial != None: + if not self._serial.is_open: + self._serial.open() + return True + else: + return False + def get_comport(self) -> str: """Find the comport the microcontroller has attached to""" if self._port_override != '': @@ -34,20 +45,24 @@ class Com: return '' - def connect(self, baud_rate: int, port_override: Optional[str] = None) -> bool: - """Try to find a comport and connect to the microcontroller. Returns the success as a boolean""" + def _open(self) -> bool: comport = self.get_comport() # Comport search returns empty string if search unsuccessful if comport == '': try: - self._serial = serial.Serial(comport, baud_rate, timeout=5) + self._serial = serial.Serial(comport, self._baudrate, timeout=5) except: return False return True else: return False + def connect(self, baud_rate: int) -> bool: + """Try to find a comport and connect to the microcontroller. Returns the success as a boolean""" + self._baudrate = baud_rate + return self._connection_check() + def close(self) -> None: """Close the serial connection, if possible""" if self._serial != None: @@ -58,23 +73,24 @@ class Com: def receive(self, byte_count: int) -> bytes: """Recieve bytes from microcontroller over serial. Returns bytes. Might want to decode using functions from lib.tools""" - if self._serial == None: - self.connect(19200) + self._connection_check() if self._serial != None: return self._serial.read(byte_count) else: - raise Exception('ERR_CONNECTION') + raise Exception('ERR_CONNECTING') def send(self, msg: str) -> None: - """Send a string over serial connection.""" - if self._serial == None: - self.connect(19200) + """Send a string over serial connection. Will open a connection if none is available""" + self._connection_check() if self._serial != None: self._serial.write(msg.encode()) + else: + raise Exception('ERR_CONNECTING') def send_float(self, msg: float) -> None: """Send a float number over serial connection""" - if self._serial == None: - self.connect(19200) + self._connection_check() if self._serial != None: self._serial.write(bytearray(struct.pack('>f', msg))[0:3]) + else: + raise Exception('ERR_CONNECTING') diff --git a/biogascontrollerapp/lib/instructions.py b/biogascontrollerapp/lib/instructions.py index 182bde2..0db6b8a 100644 --- a/biogascontrollerapp/lib/instructions.py +++ b/biogascontrollerapp/lib/instructions.py @@ -1,22 +1,61 @@ -from typing import Optional import lib.com import lib.decoder +import time # TODO: Load filters (for comport search) com = lib.com.Com() decoder = lib.decoder.Decoder() class Instructions: - def __init__(self) -> None: - pass + def set_port_override(self, override: str) -> None: + com.set_port_override(override) def _hook(self, instruction: str, sequence: list[str]) -> bool: + # Send instruction to microcontroller to start hooking process + com.send(instruction) + + # Record start time to respond to timeout + start = time.time() + + # Check for timeout + pointer = 0 + sequence_max = len(sequence) - 1 + while time.time() - start < 5: + if ( decoder.decode_ascii( com.receive(1) ) ) == sequence[pointer]: + pointer += 1 + else: + pointer = 0 + + if pointer == sequence_max: + return True + return False - def change_temperature(self, new_temps: list[float]) -> None: - pass + def _change_data(self, instruction: str, readback: list[str], data: list[float], readback_length: int) -> None: + # Hook to stream + if self._hook(instruction, readback): + while len(data) > 0: + if com.receive(readback_length) != '': + com.send_float(data.pop(0)) + else: + com.close() + raise Exception('Failed to transmit data. No response from controller') + com.close() + else: + com.close() + raise ConnectionError('Failed to hook to controller data stream. No fitting response received') def change_config(self, new_config: list[float]) -> None: - pass + self._change_data('PR', ['\n', 'P', 'R', '\n'], new_config, 3) + def change_temperature(self, temperatures: list[float]) -> None: + self._change_data('PT', ['\n', 'P', 'T', '\n'], temperatures, 3) + + def enable_fastmode(self) -> None: + com.send('FM') + com.close() + + def disable_fastmode(self) -> None: + com.send('NM') + com.close() From 36a30790403e5d5894576b1bf4a0de6f0fd6ce3c Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Tue, 11 Mar 2025 15:12:36 +0100 Subject: [PATCH 05/31] Some fixes for update --- biogascontrollerapp/gui/PopupManager.py | 2 +- biogascontrollerapp/gui/popups/popups.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/biogascontrollerapp/gui/PopupManager.py b/biogascontrollerapp/gui/PopupManager.py index adc546c..a2fbd42 100644 --- a/biogascontrollerapp/gui/PopupManager.py +++ b/biogascontrollerapp/gui/PopupManager.py @@ -1,3 +1,3 @@ -from gui.popups import * +from gui.popups.popups import * diff --git a/biogascontrollerapp/gui/popups/popups.py b/biogascontrollerapp/gui/popups/popups.py index 5971a70..d2728ce 100644 --- a/biogascontrollerapp/gui/popups/popups.py +++ b/biogascontrollerapp/gui/popups/popups.py @@ -1,8 +1,8 @@ -from kivy.uix.screenmanager import Screen +from kivy.uix.popup import Popup from kivy.lang import Builder -class HomeScreen(Screen): +class ThisPopup(Popup): def start(self): pass From e0a54ac2bd28340e4dc7a29a593402458b078816 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Wed, 9 Apr 2025 17:13:24 +0200 Subject: [PATCH 06/31] Popups done, Readout Screen prepared, Small fixes --- biogascontrollerapp/biogascontrollerapp.py | 15 ++- .../{settings/settings.kv => about/about.kv} | 6 +- .../{settings/settings.py => about/about.py} | 4 +- biogascontrollerapp/gui/credits/credits.kv | 2 +- biogascontrollerapp/gui/home/home.kv | 4 +- biogascontrollerapp/gui/home/home.py | 23 +++- biogascontrollerapp/gui/main/main.kv | 107 ++++++++++++++++++ biogascontrollerapp/gui/main/main.py | 25 ++++ biogascontrollerapp/gui/popups/popups.kv | 46 ++++---- biogascontrollerapp/gui/popups/popups.py | 51 +++++++-- biogascontrollerapp/gui/program/program.py | 4 + biogascontrollerapp/lib/com.py | 7 +- 12 files changed, 248 insertions(+), 46 deletions(-) rename biogascontrollerapp/gui/{settings/settings.kv => about/about.kv} (94%) rename biogascontrollerapp/gui/{settings/settings.py => about/about.py} (73%) create mode 100644 biogascontrollerapp/gui/main/main.kv create mode 100644 biogascontrollerapp/gui/main/main.py diff --git a/biogascontrollerapp/biogascontrollerapp.py b/biogascontrollerapp/biogascontrollerapp.py index eec58f6..c2fc2c4 100644 --- a/biogascontrollerapp/biogascontrollerapp.py +++ b/biogascontrollerapp/biogascontrollerapp.py @@ -2,6 +2,8 @@ import os import configparser from typing import override +from lib.com import Com + config = configparser.ConfigParser() config.read('./config.ini') @@ -13,12 +15,12 @@ else: # Load kivy modules -from kivy.core.window import Window, Config +# from kivy.core.window import Window, Config from kivy.uix.screenmanager import ScreenManager from kivy.app import App # Load other libraries -import threading +# import threading # Store the current app version app_version = f"{config['Info']['version']}{config['Info']['subVersion']}" @@ -29,7 +31,8 @@ app_version = f"{config['Info']['version']}{config['Info']['subVersion']}" from gui.home.home import HomeScreen from gui.credits.credits import CreditsScreen -from gui.settings.settings import SettingsScreen +from gui.about.about import AboutScreen +from gui.main.main import MainScreen #----------------# # Screen Manager # @@ -41,11 +44,13 @@ class BiogasControllerApp(App): @override def build(self): + com = Com(); self.icon = './BiogasControllerAppLogo.png' self.title = 'BiogasControllerApp-' + app_version - self.screen_manager.add_widget(HomeScreen(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(CreditsScreen(name='credits')) - self.screen_manager.add_widget(SettingsScreen(name='settings')) + self.screen_manager.add_widget(AboutScreen(name='about')) return self.screen_manager if __name__ == "__main__": diff --git a/biogascontrollerapp/gui/settings/settings.kv b/biogascontrollerapp/gui/about/about.kv similarity index 94% rename from biogascontrollerapp/gui/settings/settings.kv rename to biogascontrollerapp/gui/about/about.kv index b1354a7..7b7a06a 100644 --- a/biogascontrollerapp/gui/settings/settings.kv +++ b/biogascontrollerapp/gui/about/about.kv @@ -1,5 +1,5 @@ -: - name: "settings" +: + name: "about" canvas.before: Color: rgba: (50,50,50,0.2) @@ -9,7 +9,7 @@ GridLayout: cols: 1 Label: - text: "Settings" + text: "About" font_size: 40 color: (0, 113, 0, 1) bold: True diff --git a/biogascontrollerapp/gui/settings/settings.py b/biogascontrollerapp/gui/about/about.py similarity index 73% rename from biogascontrollerapp/gui/settings/settings.py rename to biogascontrollerapp/gui/about/about.py index 0251b1c..180e7cf 100644 --- a/biogascontrollerapp/gui/settings/settings.py +++ b/biogascontrollerapp/gui/about/about.py @@ -3,8 +3,8 @@ from kivy.lang import Builder import webbrowser -class SettingsScreen(Screen): +class AboutScreen(Screen): def report_issue(self): webbrowser.open('https://github.com/janishutz/BiogasControllerApp/issues', new=2) -Builder.load_file('./gui/settings/settings.kv') +Builder.load_file('./gui/about/about.kv') diff --git a/biogascontrollerapp/gui/credits/credits.kv b/biogascontrollerapp/gui/credits/credits.kv index 5f879ad..3c3a02f 100644 --- a/biogascontrollerapp/gui/credits/credits.kv +++ b/biogascontrollerapp/gui/credits/credits.kv @@ -12,7 +12,7 @@ size_hint: 0.4, 0.2 pos_hint: {"x":0.3, "y":0.1} on_release: - app.root.current = "settings" + app.root.current = "about" root.manager.transition.direction = "right" GridLayout: cols:1 diff --git a/biogascontrollerapp/gui/home/home.kv b/biogascontrollerapp/gui/home/home.kv index 40a78cb..e9a052c 100644 --- a/biogascontrollerapp/gui/home/home.kv +++ b/biogascontrollerapp/gui/home/home.kv @@ -37,10 +37,10 @@ font_size: 13 pos_hint: {"y": -0.45, "x":0.05} Button: - text: "Settings" + 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_settings() + root.to_about() diff --git a/biogascontrollerapp/gui/home/home.py b/biogascontrollerapp/gui/home/home.py index 4273484..87b01f1 100644 --- a/biogascontrollerapp/gui/home/home.py +++ b/biogascontrollerapp/gui/home/home.py @@ -1,16 +1,29 @@ from kivy.uix.screenmanager import Screen from kivy.lang import Builder - +from gui.popups.popups import QuitPopup, SingleRowPopup, TwoActionPopup +from lib.com import Com class HomeScreen(Screen): + def __init__(self, com: Com, **kw): + self._com = com; + super().__init__(**kw) + def start(self): - pass + if self._com.connect(19200): + self.manager.current = 'main' + self.manager.transition.direction = 'right' + else: + TwoActionPopup().open('Failed to connect', 'Details', self.open_details_popup) + print('ERROR connecting') + + def open_details_popup(self): + print( 'Details' ) def quit(self): - pass + QuitPopup(self._com).open() - def to_settings(self): - self.manager.current = 'settings' + def to_about(self): + self.manager.current = 'about' self.manager.transition.direction = 'down' diff --git a/biogascontrollerapp/gui/main/main.kv b/biogascontrollerapp/gui/main/main.kv new file mode 100644 index 0000000..9c1b1a7 --- /dev/null +++ b/biogascontrollerapp/gui/main/main.kv @@ -0,0 +1,107 @@ +: + on_pre_enter: root.reset() + name: "main" + canvas.before: + Color: + rgba: (50,50,50,0.2) + Rectangle: + size: self.size + pos: self.pos + GridLayout: + FloatLayout: + Label: + pos_hint: {"y":0.4} + text: "READOUT" + font_size: 40 + color: (0, 113, 0, 1) + bold: True + GridLayout: + cols:4 + size_hint: 0.8, 0.3 + pos_hint: {"x":0.1, "y":0.4} + Label: + text: "SENSOR 1: " + font_size: 20 + Label: + id: sonde1 + text: "" + Label: + text: "SENSOR 2: " + font_size: 20 + Label: + id: sonde2 + text: "" + Label: + text: "SENSOR 3: " + font_size: 20 + Label: + id: sonde3 + text: "" + Label: + text: "SENSOR 4: " + font_size: 20 + Label: + id: sonde4 + text: "" + Button: + text: "Start communication" + size_hint: 0.2, 0.1 + pos_hint: {"x": 0.5, "y": 0.05} + background_color: (255, 0, 0, 0.6) + on_release: + root.start() + Button: + text: "End communication" + size_hint: 0.2, 0.1 + pos_hint: {"x": 0.7, "y": 0.05} + background_color: (255, 0, 0, 0.6) + on_release: + root.end() + Button: + text: "Back" + size_hint: 0.3, 0.1 + pos_hint: {"x":0.05, "y":0.05} + background_color: (255, 0, 0, 0.6) + on_release: + root.end() + app.root.current = "home" + root.manager.transition.direction = "left" + ToggleButton: + 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: + text: "Read Data" + size_hint: 0.15, 0.1 + pos_hint: {"x":0.3, "y":0.2} + background_color: (255, 0, 0, 0.6) + on_release: + root.end() + app.root.current = "read" + root.manager.transition.direction = "down" + Button: + text: "Temperature" + size_hint: 0.15, 0.1 + pos_hint: {"x":0.5, "y":0.2} + background_color: (255, 0, 0, 0.6) + on_release: + root.end() + app.root.current = "temperature" + root.manager.transition.direction = "down" + Button: + text: "Change all Data" + size_hint: 0.15, 0.1 + pos_hint: {"x":0.7, "y":0.2} + background_color: (255, 0, 0, 0.6) + on_release: + root.end() + app.root.current = "program" + root.manager.transition.direction = "down" + Label: + id: frequency + text: "Frequency will appear here" + font_size: 10 + pos_hint: {"x":0.4, "y": 0.3} diff --git a/biogascontrollerapp/gui/main/main.py b/biogascontrollerapp/gui/main/main.py new file mode 100644 index 0000000..ccb8679 --- /dev/null +++ b/biogascontrollerapp/gui/main/main.py @@ -0,0 +1,25 @@ +from kivy.uix.screenmanager import Screen +from kivy.lang import Builder + +from lib.com import Com + + +class MainScreen(Screen): + def __init__(self, com: Com, **kw): + self._com = com; + super().__init__(**kw) + + def start(self): + pass + + def end(self): + pass + + def reset(self): + pass + + def back(self): + pass + + +Builder.load_file('./gui/main/main.kv') diff --git a/biogascontrollerapp/gui/popups/popups.kv b/biogascontrollerapp/gui/popups/popups.kv index 6c6f922..1d212e1 100644 --- a/biogascontrollerapp/gui/popups/popups.kv +++ b/biogascontrollerapp/gui/popups/popups.kv @@ -1,26 +1,10 @@ -: - title: "INFORMATION" - size_hint: 0.7, 0.5 - auto_dismiss: True - GridLayout: - cols: 1 - Label: - id: msg - text_size: self.width, None - GridLayout: - cols: 1 - Button: - text: "Ok" - on_release: - root.dismiss() - -: +: title: "BiogasControllerApp" font_size: 50 size_hint: 0.5, 0.4 auto_dismiss: False GridLayout: - cols:1 + cols: 1 Label: text: "Are you sure you want to leave?" font_size: 20 @@ -38,6 +22,24 @@ on_press: root.dismiss() +: + 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() + : title: "WARNING!" font_size: 50 @@ -49,16 +51,20 @@ id: msg text: "Message" font_size: 20 + halign: 'center' GridLayout: cols:2 Button: id: btn1 text: "Details" on_release: - root.action() + root.action_one() + root.dismiss() Button: + id: btn2 text:"Ok" on_release: + root.action_two() root.dismiss() : @@ -87,8 +93,8 @@ size_hint: 1, 0.7 auto_dismiss: False GridLayout: + cols: 1 Label: - cols:1 id: msg_title text: "title" font_size: 20 diff --git a/biogascontrollerapp/gui/popups/popups.py b/biogascontrollerapp/gui/popups/popups.py index d2728ce..41d5d16 100644 --- a/biogascontrollerapp/gui/popups/popups.py +++ b/biogascontrollerapp/gui/popups/popups.py @@ -1,16 +1,53 @@ +from typing import Callable from kivy.uix.popup import Popup from kivy.lang import Builder +from lib.com import Com -class ThisPopup(Popup): - def start(self): - pass +def empty_func(): + pass + +class QuitPopup(Popup): + def __init__(self, com: Com, **kw): + self._com = com; + super().__init__(**kw) def quit(self): - pass + self._com.close() - def to_settings(self): - pass +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) -Builder.load_file('./gui/home/home.kv') +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) + +Builder.load_file('./gui/popups/popups.kv') diff --git a/biogascontrollerapp/gui/program/program.py b/biogascontrollerapp/gui/program/program.py index 5971a70..3669d49 100644 --- a/biogascontrollerapp/gui/program/program.py +++ b/biogascontrollerapp/gui/program/program.py @@ -3,6 +3,10 @@ from kivy.lang import Builder class HomeScreen(Screen): + def __init__(self, com: Com, **kw): + self._com = com; + super().__init__(**kw) + def start(self): pass diff --git a/biogascontrollerapp/lib/com.py b/biogascontrollerapp/lib/com.py index 279e80d..747ca10 100644 --- a/biogascontrollerapp/lib/com.py +++ b/biogascontrollerapp/lib/com.py @@ -10,11 +10,15 @@ class Com: self._filters = filters if filters != None else [ 'USB-Serial Controller', 'Prolific USB-Serial Controller' ] self._port_override = '' self._baudrate = 19200 + self._err = None def set_port_override(self, override: str) -> None: """Set the port override, to disable port search""" self._port_override = override + def get_error(self) -> serial.SerialException | None: + return self._err + def _connection_check(self) -> bool: if self._serial == None: return self._open() @@ -52,7 +56,8 @@ class Com: if comport == '': try: self._serial = serial.Serial(comport, self._baudrate, timeout=5) - except: + except serial.SerialException as e: + self._err = e return False return True else: From 92836fe427a274f628ee27d3635f842a24258cbc Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Mon, 14 Apr 2025 16:25:06 +0200 Subject: [PATCH 07/31] Fix some naming --- biogascontrollerapp/gui/main/main.kv | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/biogascontrollerapp/gui/main/main.kv b/biogascontrollerapp/gui/main/main.kv index 9c1b1a7..9e19e1d 100644 --- a/biogascontrollerapp/gui/main/main.kv +++ b/biogascontrollerapp/gui/main/main.kv @@ -23,25 +23,25 @@ text: "SENSOR 1: " font_size: 20 Label: - id: sonde1 + id: sensor1 text: "" Label: text: "SENSOR 2: " font_size: 20 Label: - id: sonde2 + id: sensor2 text: "" Label: text: "SENSOR 3: " font_size: 20 Label: - id: sonde3 + id: sensor3 text: "" Label: text: "SENSOR 4: " font_size: 20 Label: - id: sonde4 + id: sensor4 text: "" Button: text: "Start communication" From e71f9e6d02591b9317f3225f15c5d9d7f6960d89 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Thu, 8 May 2025 18:12:26 +0200 Subject: [PATCH 08/31] Main, Program Screen basically done, UI Tweaks, backend fixes, start writing testing library --- README.md | 8 +- biogascontrollerapp/biogascontrollerapp.py | 58 +++++-- biogascontrollerapp/config.ini | 2 +- biogascontrollerapp/gui/home/home.py | 29 +++- biogascontrollerapp/gui/main/main.kv | 24 +-- biogascontrollerapp/gui/main/main.py | 189 ++++++++++++++++++++- biogascontrollerapp/gui/popups/popups.py | 10 ++ biogascontrollerapp/gui/program/program.kv | 131 ++++++++++++++ biogascontrollerapp/gui/program/program.py | 92 +++++++++- biogascontrollerapp/lib/com.py | 9 +- biogascontrollerapp/lib/instructions.py | 101 +++++++---- biogascontrollerapp/lib/test/com.py | 31 +++- changelog | 13 +- 13 files changed, 592 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 3bcf3cb..a20b32e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,13 @@

- + +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 `biogascontrollerapp/lib` folder. If you want to understand and have a look at all of the application, start with the `biogascontrollerapp.py` file in the `biogascontrollerapp` folder + +# Features + ***LOOKING FOR A MacOS BUILD MAINTAINER! You may follow the official build instructions on the kivy.org website. All other materials should already be included in this repository*** ## FEATURES diff --git a/biogascontrollerapp/biogascontrollerapp.py b/biogascontrollerapp/biogascontrollerapp.py index c2fc2c4..f3e4a2a 100644 --- a/biogascontrollerapp/biogascontrollerapp.py +++ b/biogascontrollerapp/biogascontrollerapp.py @@ -1,42 +1,61 @@ +# ──────────────────────────────────────────────────────────────────── +# ╭────────────────────────────────────────────────╮ +# │ BiogasControllerApp │ +# ╰────────────────────────────────────────────────╯ +# +# So you would like to read the source code? Nice! +# Just be warned, this application uses Thread 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- +# controller with RS232 +# +# ──────────────────────────────────────────────────────────────────── + import os import configparser from typing import override from lib.com import Com + +# Load the config file config = configparser.ConfigParser() -config.read('./config.ini') +config.read("./config.ini") # Load config and disable kivy log if necessary -if config['Dev Settings']['verbose'] == "True": +if config["Dev Settings"]["verbose"] == "True": pass else: os.environ["KIVY_NO_CONSOLELOG"] = "1" -# Load kivy modules +# Load kivy modules. Kivy is the UI framework used. See https://kivy.org # from kivy.core.window import Window, Config from kivy.uix.screenmanager import ScreenManager from kivy.app import App -# Load other libraries -# import threading # Store the current app version app_version = f"{config['Info']['version']}{config['Info']['subVersion']}" -#---------# -# Screens # -#---------# +# ╭────────────────────────────────────────────────╮ +# │ Screens │ +# ╰────────────────────────────────────────────────╯ +# 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 # -#----------------# + + +# ╭────────────────────────────────────────────────╮ +# │ Screen Manager │ +# ╰────────────────────────────────────────────────╯ +# Kivy uses a screen manager to manage pages in the application class BiogasControllerApp(App): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -44,14 +63,17 @@ class BiogasControllerApp(App): @override def build(self): - com = Com(); - self.icon = './BiogasControllerAppLogo.png' - self.title = 'BiogasControllerApp-' + app_version - self.screen_manager.add_widget(HomeScreen(com, name='home')) - self.screen_manager.add_widget(MainScreen(com, name='main')) - self.screen_manager.add_widget(CreditsScreen(name='credits')) - self.screen_manager.add_widget(AboutScreen(name='about')) + com = Com() + self.icon = "./BiogasControllerAppLogo.png" + self.title = "BiogasControllerApp-" + app_version + 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 + +# Disallow this file to be imported if __name__ == "__main__": BiogasControllerApp().run() diff --git a/biogascontrollerapp/config.ini b/biogascontrollerapp/config.ini index 467137c..f92e8a9 100644 --- a/biogascontrollerapp/config.ini +++ b/biogascontrollerapp/config.ini @@ -8,7 +8,7 @@ sizew = 800 [Dev Settings] verbose = True log_level = DEBUG -disableconnectioncheck = False +disableconnectioncheck = True [License] show = 1 diff --git a/biogascontrollerapp/gui/home/home.py b/biogascontrollerapp/gui/home/home.py index 87b01f1..6393e78 100644 --- a/biogascontrollerapp/gui/home/home.py +++ b/biogascontrollerapp/gui/home/home.py @@ -1,30 +1,49 @@ from kivy.uix.screenmanager import Screen from kivy.lang import Builder -from gui.popups.popups import QuitPopup, SingleRowPopup, TwoActionPopup +from gui.popups.popups import QuitPopup, TwoActionPopup from lib.com import Com +import configparser + +config = configparser.ConfigParser() +config.read('./config.ini') + +# This is the launch screen, i.e. what you see when you start up the app class HomeScreen(Screen): def __init__(self, com: Com, **kw): self._com = com; super().__init__(**kw) + # Go to the main screen if we can establish connection or the check was disabled + # in the configs def start(self): - if self._com.connect(19200): + if config[ 'Dev Settings' ][ 'disableconnectioncheck' ] != "True": + if self._com.connect(): + self.manager.current = 'main' + self.manager.transition.direction = 'right' + else: + TwoActionPopup().open('Failed to connect', 'Details', self.open_details_popup) + print('ERROR connecting') + else: self.manager.current = 'main' self.manager.transition.direction = 'right' - else: - TwoActionPopup().open('Failed to connect', 'Details', self.open_details_popup) - print('ERROR connecting') + # Open popup for details as to why the connection failed def open_details_popup(self): + # TODO: Finish print( 'Details' ) + # Helper to open a Popup to ask user whether to quit or not def quit(self): QuitPopup(self._com).open() + # Switch to about screen def to_about(self): 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') diff --git a/biogascontrollerapp/gui/main/main.kv b/biogascontrollerapp/gui/main/main.kv index 9e19e1d..4e714b4 100644 --- a/biogascontrollerapp/gui/main/main.kv +++ b/biogascontrollerapp/gui/main/main.kv @@ -74,25 +74,7 @@ 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: - text: "Read Data" - size_hint: 0.15, 0.1 - pos_hint: {"x":0.3, "y":0.2} - background_color: (255, 0, 0, 0.6) - on_release: - root.end() - app.root.current = "read" - root.manager.transition.direction = "down" - Button: - text: "Temperature" - size_hint: 0.15, 0.1 - pos_hint: {"x":0.5, "y":0.2} - background_color: (255, 0, 0, 0.6) - on_release: - root.end() - app.root.current = "temperature" - root.manager.transition.direction = "down" - Button: - text: "Change all Data" + text: "Configuration" size_hint: 0.15, 0.1 pos_hint: {"x":0.7, "y":0.2} background_color: (255, 0, 0, 0.6) @@ -101,7 +83,7 @@ app.root.current = "program" root.manager.transition.direction = "down" Label: - id: frequency - text: "Frequency will appear here" + id: status + text: "Status will appear here" font_size: 10 pos_hint: {"x":0.4, "y": 0.3} diff --git a/biogascontrollerapp/gui/main/main.py b/biogascontrollerapp/gui/main/main.py index ccb8679..ed25ce9 100644 --- a/biogascontrollerapp/gui/main/main.py +++ b/biogascontrollerapp/gui/main/main.py @@ -1,25 +1,200 @@ +from ctypes import ArgumentError +from time import time +from typing import List, override from kivy.uix.screenmanager import Screen from kivy.lang import Builder +from gui.popups.popups import SingleRowPopup, TwoActionPopup, empty_func +from kivy.clock import Clock, ClockEvent +import queue +import threading +# Load utilities +from lib.instructions import Instructions from lib.com import Com +from lib.decoder import Decoder +# TODO: Consider consolidating start and stop button + + +# Queue with data that is used to synchronize +synced_queue: queue.Queue[List[str]] = queue.Queue() + + +# ╭────────────────────────────────────────────────╮ +# │ Data Reading Thread Helper │ +# ╰────────────────────────────────────────────────╯ +# Using a Thread to run this in parallel to the UI to improve responsiveness +class ReaderThread(threading.Thread): + _com: Com + _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: Com): + """Set the Com object to be used in this + + Args: + com: The com object to be used + """ + self._com = com + self._run = True + self._decoder = Decoder() + + # This method is given by the Thread class and has to be overriden to change + # what is executed when the thread starts + @override + def run(self) -> None: + self._run = True + if self._com == None: + raise ArgumentError("Com object not passed in (do using set_com)") + # Hook to output stream + if self._instructions.hook("", ["\n", " ", " ", " "]): + # We are now hooked to the stream (i.e. data is synced) + synced_queue.put(["HOOK"]) + + # making it exit using the stop function + while self._run: + # Take note of the time before reading the data to deduce frequency of updates + start_time = time() + + # We need to read 68 bytes of data, given by the program running on the controller + received = self._com.receive(68) + + # Store the data in a list of strings + data: List[str] = [] + + # For all sensors connected, execute the same thing + for i in range(4): + # The slicing that happens here uses offsets automatically calculated from the sensor id + # This allows for short code + data.append( + f"Tadc: { + self._decoder.decode_float(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 + }%" + ) + # Calculate the frequency of updates + data.append(str(1 / (time() - start_time))) + else: + # Send error message to the UI updater + synced_queue.put(["ERR_HOOK"]) + return + + def stop(self) -> None: + self._run = False + + +# ╭────────────────────────────────────────────────╮ +# │ Main App Screen │ +# ╰────────────────────────────────────────────────╯ +# This is the main screen, where you can read out data class MainScreen(Screen): + _event: ClockEvent + + # 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: Com, **kw): - self._com = com; + # Set some variables + self._com = com + self._event = None + + # Prepare the reader thread + self._reader = ReaderThread() + self._reader.setDaemon(True) + self._reader.set_com(com) + 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 start(self): - pass + self.ids.status.text = "Connecting..." + if self._com.connect(): + self._has_connected = True + # Start communication + self._reader.start() + 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, + ) + # End connection to micro-controller and set it back to normal mode def end(self): - pass + # Set micro-controller back to Normal Mode when ending communication + # to make sure temperature control will work + if self._has_connected: + if self._event != None: + self._event.cancel() + self._reader.stop() + try: + self._com.send("NM") + except: + pass + self._com.close() + self.ids.status.text = "Connection terminated" + # A helper function to update the screen. Is called on an interval + def _update_screen(self): + update = synced_queue.get() + if len(update) == 1: + if update[0] == "ERR_HOOK": + self.ids.status.text = "Hook failed" + self.end() + elif update[0] == "HOOK": + self.ids.status.text = "Connected to controller" + else: + self.ids.sensor1.text = update[0] + self.ids.sensor2.text = update[1] + self.ids.sensor3.text = update[2] + self.ids.sensor4.text = update[3] + self.ids.status.text = "Connected, f = " + update[4] + + # Reset the screen when the screen is entered def reset(self): - pass + self.ids.sensor1.text = "" + self.ids.sensor2.text = "" + self.ids.sensor3.text = "" + self.ids.sensor4.text = "" + self.ids.status.text = "Status will appear here" - def back(self): - pass + # Switch the mode for the micro-controller + def switch_mode(self, new_mode: str): + # Store if we have been connected to the micro-controller before mode was switched + was_connected = self._reader.is_alive + + # Disconnect from the micro-controller + self.end() + self.ids.status.text = "Setting mode..." + + # Try to set the new mode + try: + if new_mode == "Normal Mode": + self._com.send("NM") + else: + self._com.send("FM") + except: + SingleRowPopup().open("Failed to switch modes") + return + + # If we have been connected, reconnect + if was_connected: + self.start() -Builder.load_file('./gui/main/main.kv') +# 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/main/main.kv") diff --git a/biogascontrollerapp/gui/popups/popups.py b/biogascontrollerapp/gui/popups/popups.py index 41d5d16..350d2de 100644 --- a/biogascontrollerapp/gui/popups/popups.py +++ b/biogascontrollerapp/gui/popups/popups.py @@ -4,9 +4,15 @@ from kivy.lang import Builder from lib.com import Com + +# 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: Com, **kw): self._com = com; @@ -50,4 +56,8 @@ class TwoActionPopup(Popup): 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') diff --git a/biogascontrollerapp/gui/program/program.kv b/biogascontrollerapp/gui/program/program.kv index e69de29..7d61e0d 100644 --- a/biogascontrollerapp/gui/program/program.kv +++ b/biogascontrollerapp/gui/program/program.kv @@ -0,0 +1,131 @@ +: + name: "program" + on_pre_enter: self.config_loader = root.load_config() + canvas.before: + Color: + rgba: (50,50,50,0.2) + Rectangle: + size: self.size + pos: self.pos + FloatLayout: + Label: + text: "Configuration" + font_size: 40 + color: (0, 113, 0, 1) + bold: True + pos_hint: {"y":0.4} + GridLayout: + size_hint: 0.8, 0.5 + pos_hint: {"x":0.1, "y":0.2} + cols: 4 + Label: + text: "Sensor 1, a:" + TextInput: + id: s1_a + multiline: False + input_filter: "float" + Label: + text: "Sensor 1, b:" + TextInput: + id: s1_b + multiline: False + input_filter: "float" + Label: + text: "Sensor 1, c:" + TextInput: + id: s1_c + multiline: False + input_filter: "float" + Label: + text: "Sensor 1, Temp:" + TextInput: + id: s1_t + multiline: False + input_filter: "float" + Label: + text: "Sensor 2, a:" + TextInput: + id: s2_a + multiline: False + input_filter: "float" + Label: + text: "Sensor 2, b:" + TextInput: + id: s2_b + multiline: False + input_filter: "float" + Label: + text: "Sensor 2, c:" + TextInput: + id: s2_c + multiline: False + input_filter: "float" + Label: + text: "Sensor 2, Temp:" + TextInput: + id: s2_t + multiline: False + input_filter: "float" + Label: + text: "Sensor 3, a:" + TextInput: + id: s3_a + multiline: False + input_filter: "float" + Label: + text: "Sensor 3, b:" + TextInput: + id: s3_b + multiline: False + input_filter: "float" + Label: + text: "Sensor 3, c:" + TextInput: + id: s3_c + multiline: False + input_filter: "float" + Label: + text: "Sensor 3, Temp:" + TextInput: + id: s3_t + multiline: False + input_filter: "float" + Label: + text: "Sensor 4, a:" + TextInput: + id: s4_a + multiline: False + input_filter: "float" + Label: + text: "Sensor 4, b:" + TextInput: + id: s4_b + multiline: False + input_filter: "float" + Label: + text: "Sensor 4, c:" + TextInput: + id: s4_c + multiline: False + input_filter: "float" + Label: + text: "Sensor 4, Temp:" + TextInput: + id: s4_t + multiline: False + input_filter: "float" + Button: + 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: + 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() diff --git a/biogascontrollerapp/gui/program/program.py b/biogascontrollerapp/gui/program/program.py index 3669d49..479e25e 100644 --- a/biogascontrollerapp/gui/program/program.py +++ b/biogascontrollerapp/gui/program/program.py @@ -1,20 +1,94 @@ +from typing import List from kivy.uix.screenmanager import Screen 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 lib.com import Com +from kivy.clock import Clock -class HomeScreen(Screen): +# The below list maps 0, 1, 2, 3 to a, b, c and t respectively +# This is used to set and read values of the UI +name_map = [ "a", "b", "c", "t" ] + + +class ProgramScreen(Screen): def __init__(self, com: Com, **kw): - self._com = com; + self._com = com + self._instructions = Instructions(com) + self._decoder = Decoder() super().__init__(**kw) - def start(self): - pass + def load_config(self): + Clock.schedule_once(self._load) - def quit(self): - pass + # Load the current configuration from the micro-controller + def _load(self, dt: float): + if self._instructions.hook("RD", ["\n", "R", "D", "\n"]): + config: List[List[str]] = [] - def to_settings(self): - pass + # Load config for all four sensors + for _ in range(4): + # Receive 28 bytes of data + received = self._com.receive(28) + + # Create a list of strings to store the config for the sensor + # This list has the following elements: a, b, c, temperature + config_sensor_i: List[str] = [] + + # Create the list + for j in range(4): + config_sensor_i.append(str(self._decoder.decode_float(received[7 * j:7 * j + 6]))) + + # Add it to the config + config.append(config_sensor_i) + else: + TwoActionPopup().open( + "Failed to connect to micro-controller, retry?", + "Cancel", + empty_func, + "Retry", + lambda: self._load(0), + ) + + # Set the elements of the UI to the values of the config + def _set_ui(self, config: List[List[str]]): + for sensor_id in range(4): + for property in range(4): + self.ids[f"s{sensor_id + 1}_{name_map[property]}"].text = config[sensor_id][property] + + # Read values from the UI. Returns the values as a list or None if the check was infringed + def _read_ui(self, enforce_none_empty: bool = True) -> List[float] | None: + data: List[float] = [] + + # Iterate over all sensor config input fields and collect the data + for sensor_id in range(4): + for property in range(4): + value = self.ids[f"s{sensor_id + 1}_{name_map[property]}"].text + + # If requested (by setting enforce_none_empty to True, which is the default) + # test if the cells are not empty and if we find an empty cell return None + if enforce_none_empty and value == "": + return + data.append(float(value)) + + return data + + # Transmit the changed data to the micro-controller to reconfigure it + def save(self): + data = self._read_ui() + if data == None: + SingleRowPopup().open("Some fields are missing values!") + else: + try: + self._instructions.change_config(data) + except: + SingleRowPopup().open("Could not save data!") + SingleRowPopup().open("Data saved successfully") -Builder.load_file('./gui/home/home.kv') +# 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/program/program.kv") diff --git a/biogascontrollerapp/lib/com.py b/biogascontrollerapp/lib/com.py index 747ca10..4048b2a 100644 --- a/biogascontrollerapp/lib/com.py +++ b/biogascontrollerapp/lib/com.py @@ -5,11 +5,11 @@ import serial.tools.list_ports class Com: - def __init__(self, filters: Optional[list[str]] = None) -> None: + def __init__(self, baudrate: 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 = 19200 + self._baudrate = baudrate self._err = None def set_port_override(self, override: str) -> None: @@ -47,7 +47,7 @@ class Com: except: pass - return '' + return "" def _open(self) -> bool: comport = self.get_comport() @@ -63,9 +63,8 @@ class Com: else: return False - def connect(self, baud_rate: int) -> bool: + def connect(self) -> bool: """Try to find a comport and connect to the microcontroller. Returns the success as a boolean""" - self._baudrate = baud_rate return self._connection_check() def close(self) -> None: diff --git a/biogascontrollerapp/lib/instructions.py b/biogascontrollerapp/lib/instructions.py index 0db6b8a..892b452 100644 --- a/biogascontrollerapp/lib/instructions.py +++ b/biogascontrollerapp/lib/instructions.py @@ -1,61 +1,100 @@ -import lib.com +from lib.com import Com import lib.decoder import time # TODO: Load filters (for comport search) -com = lib.com.Com() decoder = lib.decoder.Decoder() -class Instructions: - def set_port_override(self, override: str) -> None: - com.set_port_override(override) - def _hook(self, instruction: str, sequence: list[str]) -> bool: +# Class that supports sending instructions to the microcontroller, +# as well as hooking to data stream according to protocol +class Instructions: + def __init__(self, com: Com) -> 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 + # ascii characters + def hook(self, instruction: str, sequence: list[str]) -> bool: + # Add protection: If we cannot establish connection, refuse to run + if not self._com.connect(): + return False + # Send instruction to microcontroller to start hooking process - com.send(instruction) - + # If instruction is an empty string, do not send instruction + + if instruction != "": + self._com.send(instruction) + # Record start time to respond to timeout start = time.time() - # Check for timeout + # The pointer below points to the element in the array which is the next expected character to be received pointer = 0 - sequence_max = len(sequence) - 1 + + # Simply the length of the sequence, since it is both cheaper and cleaner to calculate it once + sequence_max = len(sequence) + + # Only run for a limited amount of time while time.time() - start < 5: - if ( decoder.decode_ascii( com.receive(1) ) ) == sequence[pointer]: + # 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]: pointer += 1 else: pointer = 0 + # If the pointer has reached the end of the sequence, return True, as now the hook was successful if pointer == sequence_max: return True + # If we time out, which is the only way in which this code can be reached, return False return False - def _change_data(self, instruction: str, readback: list[str], data: list[float], readback_length: int) -> None: + # Private helper method to transmit data using the necessary protocols + def _change_data( + self, + instruction: str, + readback: list[str], + data: list[float], + readback_length: int, + ) -> None: # Hook to stream - if self._hook(instruction, readback): + if self.hook(instruction, readback): + # Transmit data while len(data) > 0: - if com.receive(readback_length) != '': - com.send_float(data.pop(0)) + # If we received data back, we can send more data, i.e. from this we know + # the controller has received the data + # If not, we close the connection and create an exception + if self._com.receive(readback_length) != "": + self._com.send_float(data.pop(0)) else: - com.close() - raise Exception('Failed to transmit data. No response from controller') - com.close() + self._com.close() + raise Exception( + "Failed to transmit data. No response from controller" + ) + self._com.close() else: - com.close() - raise ConnectionError('Failed to hook to controller data stream. No fitting response received') + self._com.close() + raise ConnectionError( + "Failed to hook to controller data stream. No fitting response received" + ) + # Abstraction of the _change_data method specifically designed to change the entire config def change_config(self, new_config: list[float]) -> None: - self._change_data('PR', ['\n', 'P', 'R', '\n'], new_config, 3) + try: + self._change_data("PR", ["\n", "P", "R", "\n"], new_config, 3) + except Exception as e: + raise e + # Abstraction of the _change_data method specifically designed to change only the configured temperature def change_temperature(self, temperatures: list[float]) -> None: - self._change_data('PT', ['\n', 'P', 'T', '\n'], temperatures, 3) - - def enable_fastmode(self) -> None: - com.send('FM') - com.close() - - def disable_fastmode(self) -> None: - com.send('NM') - com.close() - + try: + self._change_data("PT", ["\n", "P", "T", "\n"], temperatures, 3) + except Exception as e: + raise e diff --git a/biogascontrollerapp/lib/test/com.py b/biogascontrollerapp/lib/test/com.py index 4875b21..a7cc2f2 100644 --- a/biogascontrollerapp/lib/test/com.py +++ b/biogascontrollerapp/lib/test/com.py @@ -1,36 +1,55 @@ """ Library to be used in standalone mode (without microcontroller, for testing functionality) +It simulates the behviour of an actual microcontroller being connected """ from typing import Optional import queue +import random + +# 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 are not available in the actual one class Com: def __init__(self) -> None: # Initialize queue with values to be sent on call of recieve (add like three or so at a time) - self._port_override = '' + self._port_override = "" + self.__mode = "" + self.__simulated_data = queue.Queue() def set_port_override(self, override: str) -> None: """Set the port override, to disable port search""" self._port_override = override def get_comport(self) -> str: - return 'test' if self._port_override != '' else self._port_override + return "test" if self._port_override != "" else self._port_override - def connect(self, baud_rate: int, port_override: Optional[str] = None) -> bool: - return True # TODO: For testing, make cases where there is no successful connection, i.e. we return false + def connect(self) -> bool: + # TODO: For testing, make cases where there is no successful connection, i.e. we return false + # Randomly return false + if random.randint(0, 20): + return False + return True def close(self) -> None: pass - def receive(self, byte_count: int) -> None: + def receive(self, byte_count: int) -> bytes: # TODO: Make it return simulated data - pass + return bytes("A", "ascii") def send(self, msg: str) -> None: # TODO: Use LUT to find what should be added to the queue for read + # Using LUT to reference pass def send_float(self, msg: float) -> None: pass + + def _generate_random_value(self, precision: int) -> bytes: + return bytes(str(round(random.random() * precision) / precision), "ascii") diff --git a/changelog b/changelog index 4c9d366..d40fc1a 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,15 @@ ***CHANGELOG*** +V3.0-beta +- Redesigned GUI +- Consolidated multiple previously separate screens +- Completely rewritten backend +- Improved stability +- Cleaned, documented code + +OLD VERSIONS +------------ + DEVELOPMENT VERSIONS dev-V2rev1: @@ -103,4 +113,5 @@ V2.3 - ADDS logging (you can include the logs in a bugreport so the devs can pin-point the exact cause and replicate the error) - ADDS some settings through a config file - CHANGED License from NONE to GPL V3 -- BUGFIXES \ No newline at end of file +- BUGFIXES + From a8ad40148f0c995266b52398815115b7ea6a43e0 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Fri, 9 May 2025 11:03:49 +0200 Subject: [PATCH 09/31] Improve Com class, continue writing test --- biogascontrollerapp/biogascontrollerapp.py | 10 +++-- biogascontrollerapp/config.ini | 11 ++--- biogascontrollerapp/gui/home/home.py | 18 +++------ biogascontrollerapp/gui/main/main.py | 23 ++++++++--- biogascontrollerapp/gui/popups/popups.py | 4 +- biogascontrollerapp/gui/program/program.kv | 2 +- biogascontrollerapp/gui/program/program.py | 29 +++++++++---- biogascontrollerapp/lib/com.py | 29 ++++++++++++- biogascontrollerapp/lib/instructions.py | 4 +- biogascontrollerapp/lib/test/com.py | 47 +++++++++++++++++----- 10 files changed, 125 insertions(+), 52 deletions(-) diff --git a/biogascontrollerapp/biogascontrollerapp.py b/biogascontrollerapp/biogascontrollerapp.py index f3e4a2a..4be0a04 100644 --- a/biogascontrollerapp/biogascontrollerapp.py +++ b/biogascontrollerapp/biogascontrollerapp.py @@ -16,7 +16,8 @@ import os import configparser from typing import override -from lib.com import Com +from lib.com import Com, ComSuperClass +import lib.test.com # Load the config file @@ -24,7 +25,7 @@ config = configparser.ConfigParser() config.read("./config.ini") # Load config and disable kivy log if necessary -if config["Dev Settings"]["verbose"] == "True": +if config["Dev"]["verbose"] == "True": pass else: os.environ["KIVY_NO_CONSOLELOG"] = "1" @@ -63,7 +64,10 @@ class BiogasControllerApp(App): @override def build(self): - com = Com() + com: ComSuperClass = Com() + if config["Dev"]["use_test_library"] == "True": + com = lib.test.com.Com() + self.icon = "./BiogasControllerAppLogo.png" self.title = "BiogasControllerApp-" + app_version self.screen_manager.add_widget(HomeScreen(com, name="home")) diff --git a/biogascontrollerapp/config.ini b/biogascontrollerapp/config.ini index f92e8a9..faede0d 100644 --- a/biogascontrollerapp/config.ini +++ b/biogascontrollerapp/config.ini @@ -1,17 +1,14 @@ -[Port Settings] +[Ports] specificport = None -[UI Config] +[UI] sizeh = 600 sizew = 800 -[Dev Settings] +[Dev] verbose = True log_level = DEBUG -disableconnectioncheck = True - -[License] -show = 1 +use_test_library = True [Info] version = V2.3.0 diff --git a/biogascontrollerapp/gui/home/home.py b/biogascontrollerapp/gui/home/home.py index 6393e78..86630e2 100644 --- a/biogascontrollerapp/gui/home/home.py +++ b/biogascontrollerapp/gui/home/home.py @@ -1,32 +1,26 @@ from kivy.uix.screenmanager import Screen from kivy.lang import Builder from gui.popups.popups import QuitPopup, TwoActionPopup -from lib.com import Com +from lib.com import ComSuperClass import configparser -config = configparser.ConfigParser() -config.read('./config.ini') # This is the launch screen, i.e. what you see when you start up the app class HomeScreen(Screen): - def __init__(self, com: Com, **kw): + def __init__(self, com: ComSuperClass, **kw): self._com = com; super().__init__(**kw) # Go to the main screen if we can establish connection or the check was disabled # in the configs def start(self): - if config[ 'Dev Settings' ][ 'disableconnectioncheck' ] != "True": - if self._com.connect(): - self.manager.current = 'main' - self.manager.transition.direction = 'right' - else: - TwoActionPopup().open('Failed to connect', 'Details', self.open_details_popup) - print('ERROR connecting') - else: + if self._com.connect(): self.manager.current = 'main' self.manager.transition.direction = 'right' + else: + TwoActionPopup().open('Failed to connect', 'Details', self.open_details_popup) + print('ERROR connecting') # Open popup for details as to why the connection failed def open_details_popup(self): diff --git a/biogascontrollerapp/gui/main/main.py b/biogascontrollerapp/gui/main/main.py index ed25ce9..e15e816 100644 --- a/biogascontrollerapp/gui/main/main.py +++ b/biogascontrollerapp/gui/main/main.py @@ -10,7 +10,7 @@ import threading # Load utilities from lib.instructions import Instructions -from lib.com import Com +from lib.com import ComSuperClass from lib.decoder import Decoder @@ -26,13 +26,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: Com + _com: ComSuperClass _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: Com): + def set_com(self, com: ComSuperClass): """Set the Com object to be used in this Args: @@ -41,6 +41,7 @@ class ReaderThread(threading.Thread): self._com = com self._run = True self._decoder = Decoder() + self._instructions = Instructions(com) # This method is given by the Thread class and has to be overriden to change # what is executed when the thread starts @@ -98,7 +99,7 @@ class MainScreen(Screen): # 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: Com, **kw): + def __init__(self, com: ComSuperClass, **kw): # Set some variables self._com = com self._event = None @@ -117,9 +118,11 @@ class MainScreen(Screen): def start(self): self.ids.status.text = "Connecting..." if self._com.connect(): + print("Acquired connection") self._has_connected = True # Start communication self._reader.start() + print("Reader has started") Clock.schedule_interval(self._update_screen, 0.5) else: self.ids.status.text = "Connection failed" @@ -145,10 +148,18 @@ class MainScreen(Screen): pass self._com.close() self.ids.status.text = "Connection terminated" + print("Connection terminated") # A helper function to update the screen. Is called on an interval - def _update_screen(self): - update = synced_queue.get() + def _update_screen(self, dt): + 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: if update[0] == "ERR_HOOK": self.ids.status.text = "Hook failed" diff --git a/biogascontrollerapp/gui/popups/popups.py b/biogascontrollerapp/gui/popups/popups.py index 350d2de..f366274 100644 --- a/biogascontrollerapp/gui/popups/popups.py +++ b/biogascontrollerapp/gui/popups/popups.py @@ -2,7 +2,7 @@ from typing import Callable from kivy.uix.popup import Popup from kivy.lang import Builder -from lib.com import Com +from lib.com import ComSuperClass # Just an empty function @@ -14,7 +14,7 @@ def empty_func(): # ╰────────────────────────────────────────────────╯ # Below, you can find various popups with various designs that can be used in the app class QuitPopup(Popup): - def __init__(self, com: Com, **kw): + def __init__(self, com: ComSuperClass, **kw): self._com = com; super().__init__(**kw) diff --git a/biogascontrollerapp/gui/program/program.kv b/biogascontrollerapp/gui/program/program.kv index 7d61e0d..cd50188 100644 --- a/biogascontrollerapp/gui/program/program.kv +++ b/biogascontrollerapp/gui/program/program.kv @@ -1,6 +1,6 @@ : name: "program" - on_pre_enter: self.config_loader = root.load_config() + on_enter: self.config_loader = root.load_config() canvas.before: Color: rgba: (50,50,50,0.2) diff --git a/biogascontrollerapp/gui/program/program.py b/biogascontrollerapp/gui/program/program.py index 479e25e..04d01ae 100644 --- a/biogascontrollerapp/gui/program/program.py +++ b/biogascontrollerapp/gui/program/program.py @@ -4,17 +4,17 @@ 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 lib.com import Com +from lib.com import ComSuperClass from kivy.clock import Clock # The below list maps 0, 1, 2, 3 to a, b, c and t respectively # This is used to set and read values of the UI -name_map = [ "a", "b", "c", "t" ] +name_map = ["a", "b", "c", "t"] class ProgramScreen(Screen): - def __init__(self, com: Com, **kw): + def __init__(self, com: ComSuperClass, **kw): self._com = com self._instructions = Instructions(com) self._decoder = Decoder() @@ -31,7 +31,18 @@ class ProgramScreen(Screen): # Load config for all four sensors for _ in range(4): # Receive 28 bytes of data - received = self._com.receive(28) + received = bytes() + try: + received = self._com.receive(28) + except: + TwoActionPopup().open( + "Failed to connect to micro-controller, retry?", + "Cancel", + empty_func, + "Retry", + lambda: self._load(0), + ) + return # Create a list of strings to store the config for the sensor # This list has the following elements: a, b, c, temperature @@ -39,7 +50,9 @@ class ProgramScreen(Screen): # Create the list for j in range(4): - config_sensor_i.append(str(self._decoder.decode_float(received[7 * j:7 * j + 6]))) + config_sensor_i.append( + str(self._decoder.decode_float(received[7 * j : 7 * j + 6])) + ) # Add it to the config config.append(config_sensor_i) @@ -56,12 +69,14 @@ class ProgramScreen(Screen): def _set_ui(self, config: List[List[str]]): for sensor_id in range(4): for property in range(4): - self.ids[f"s{sensor_id + 1}_{name_map[property]}"].text = config[sensor_id][property] + self.ids[f"s{sensor_id + 1}_{name_map[property]}"].text = config[ + sensor_id + ][property] # Read values from the UI. Returns the values as a list or None if the check was infringed def _read_ui(self, enforce_none_empty: bool = True) -> List[float] | None: data: List[float] = [] - + # Iterate over all sensor config input fields and collect the data for sensor_id in range(4): for property in range(4): diff --git a/biogascontrollerapp/lib/com.py b/biogascontrollerapp/lib/com.py index 4048b2a..da97ba8 100644 --- a/biogascontrollerapp/lib/com.py +++ b/biogascontrollerapp/lib/com.py @@ -1,10 +1,11 @@ +from abc import ABC, abstractmethod from typing import Optional import serial import struct import serial.tools.list_ports -class Com: +class ComSuperClass(ABC): def __init__(self, baudrate: 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' ] @@ -19,6 +20,32 @@ class Com: 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 + + +class Com(ComSuperClass): def _connection_check(self) -> bool: if self._serial == None: return self._open() diff --git a/biogascontrollerapp/lib/instructions.py b/biogascontrollerapp/lib/instructions.py index 892b452..4b2bbc2 100644 --- a/biogascontrollerapp/lib/instructions.py +++ b/biogascontrollerapp/lib/instructions.py @@ -1,4 +1,4 @@ -from lib.com import Com +from lib.com import ComSuperClass import lib.decoder import time @@ -9,7 +9,7 @@ decoder = lib.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: Com) -> None: + def __init__(self, com: ComSuperClass) -> None: self._com = com # Set a port override (to use a specific COM port) diff --git a/biogascontrollerapp/lib/test/com.py b/biogascontrollerapp/lib/test/com.py index a7cc2f2..b6a0d65 100644 --- a/biogascontrollerapp/lib/test/com.py +++ b/biogascontrollerapp/lib/test/com.py @@ -6,33 +6,55 @@ It simulates the behviour of an actual microcontroller being connected from typing import Optional import queue import random +import serial + +from lib.com import ComSuperClass # 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 are not available in the actual one +# All double __ prefixed properties and methods are not available in the actual one + +instruction_lut = { + "PR": "\nPR\n", + "PT": "\nPT\n", + "RD": "\nRD\n", + "NM": "\nNM\n", + "FM": "\nFM\n", +} -class Com: - def __init__(self) -> None: - # Initialize queue with values to be sent on call of recieve (add like three or so at a time) - self._port_override = "" - self.__mode = "" - self.__simulated_data = queue.Queue() +class Com(ComSuperClass): + def __init__(self, baudrate: int = 19200, filters: Optional[list[str]] = None) -> None: + # Calling the constructor of the super class to assign defaults + print("WARNING: Using testing library for communication!") + super().__init__(baudrate, filters); + + # Initialize queue with values to be sent on call of recieve + self.__simulated_data: queue.Queue[int] = queue.Queue() + + # Keep track of the number of bytes sent to fulfil protocol + self.__bytes_sent: int = 0 + + # 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 + def get_error(self) -> serial.SerialException | None: + pass + def get_comport(self) -> str: return "test" if self._port_override != "" else self._port_override def connect(self) -> bool: - # TODO: For testing, make cases where there is no successful connection, i.e. we return false - # Randomly return false - if random.randint(0, 20): + # Randomly return false in 1 in 20 ish cases + if random.randint(0, 20) == 1: + print("Simulating error to connect") return False return True @@ -41,6 +63,9 @@ class Com: def receive(self, byte_count: int) -> bytes: # TODO: Make it return simulated data + data = [] + for i in range(byte_count): + data.append(self.__simulated_data.get_nowait()) return bytes("A", "ascii") def send(self, msg: str) -> None: @@ -51,5 +76,5 @@ class Com: def send_float(self, msg: float) -> None: pass - def _generate_random_value(self, precision: int) -> bytes: + def __generate_random_value(self, precision: int) -> bytes: return bytes(str(round(random.random() * precision) / precision), "ascii") From 0729fed5c2a75ec0dce3ea9ab920385d841b1248 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Mon, 12 May 2025 16:27:48 +0200 Subject: [PATCH 10/31] Improve README, add requirements.txt --- README.md | 60 ++++++++++++++++++++++-------------------------- requirements.txt | 2 ++ 2 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 requirements.txt diff --git a/README.md b/README.md index a20b32e..558f052 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,22 @@
- - - - - + + + + +
- GitHub Repo stars - GitHub watchers - - GitHub forks - GitHub commit activity + GitHub Repo stars + GitHub watchers + + GitHub forks + GitHub commit activity
- GitHub all releases - GitHub release (latest by date) - - + GitHub all releases + GitHub release (latest by date) + +