from typing import List from kivymd.uix.screen import MDScreen from kivy.lang import Builder from util.decoder import Decoder from util.instructions import Instructions from util.instructions import ControllerConnection from kivymd.uix.button import MDFlatButton from kivymd.uix.dialog import MDDialog from 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"] class ProgramScreen(MDScreen): def __init__(self, com: ControllerConnection, **kw): self._com = com self._instructions = Instructions(com) self._decoder = Decoder() # Configure Dialog self.connection_error_dialog = MDDialog( title="Connection", text="Failed to connect. Do you wish to retry?", buttons=[ MDFlatButton( text="Cancel", on_release=lambda _: self.connection_error_dialog.dismiss(), ), MDFlatButton(text="Retry", on_release=lambda _: self.load_config()), ], ) self.missing_fields_error_dialog = MDDialog( title="Save", text="Some fields are missing entries. Please fill them out and try again", buttons=[ MDFlatButton( text="Ok", on_release=lambda _: self.missing_fields_error_dialog.dismiss(), ), ], ) self.save_error_dialog = MDDialog( title="Save", text="Failed to save data. Please try again", buttons=[ MDFlatButton( text="Ok", on_release=lambda _: self.save_error_dialog.dismiss(), ), ], ) self.save_success_dialog = MDDialog( title="Save", text="Data saved successfully!", buttons=[ MDFlatButton( text="Ok", on_release=lambda _: self.save_success_dialog.dismiss(), ), ], ) super().__init__(**kw) # Load the config (async to not freeze the UI) def load_config(self): Clock.schedule_once(lambda _: self._load()) # Load the current configuration from the micro-controller def _load(self): self.ids.status.text = "Loading..." # Hook to the microcontroller's data stream (i.e. sync up with it) if self._instructions.hook("RD", ["\n", "R", "D", "\n"]): config: List[List[str]] = [] # Load config for all four sensors for _ in range(4): # Receive 28 bytes of data received = bytes() try: received = self._com.receive(28) except: # Open error popup self.connection_error_dialog.open() return # 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) self.ids.status.text = "" self._set_ui(config) else: self.connection_error_dialog.open() # Set the elements of the UI to the values of the config def _set_ui(self, config: List[List[str]]): 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 def save(self): Clock.schedule_once(lambda _: self._save()) # Transmit the changed data to the micro-controller to reconfigure it def _save(self): self.ids.status.text = "Saving..." data = self._read_ui() if data == None: self.missing_fields_error_dialog() else: try: self._instructions.change_config(data) except: self.save_error_dialog.open() return self.save_success_dialog.open() self.ids.status.text = "Saved!" Clock.schedule_once(self.reset_update, 5) def reset_update(self, _): self.ids.status.text = "" def validate_float(self, instance): text = instance.text # Allow only digits and one dot if text.count(".") > 1 or any(c not in "0123456789." for c in text): # Remove invalid characters clean_text = "".join(c for c in text if c in "0123456789.") # Remove extra dots if clean_text.count(".") > 1: first_dot = clean_text.find(".") clean_text = clean_text[: first_dot + 1] + clean_text[ first_dot + 1 : ].replace(".", "") instance.text = clean_text # Load the design file for this screen (.kv files) # 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")