From 22bcf7310f2c14e0c284bcbe3d425042cdd9435f Mon Sep 17 00:00:00 2001 From: caliskan Date: Tue, 15 Aug 2023 23:22:26 +0200 Subject: [PATCH] backend code fully commented --- software/backend/data_functions.py | 241 ++++++++++++++++++++++++++--- software/backend/defines.py | 3 +- software/backend/main.py | 17 +- software/backend/plantdatabase.py | 76 ++++++++- software/backend/robot.py | 2 +- 5 files changed, 304 insertions(+), 35 deletions(-) diff --git a/software/backend/data_functions.py b/software/backend/data_functions.py index e0f451e..01cfe11 100644 --- a/software/backend/data_functions.py +++ b/software/backend/data_functions.py @@ -3,6 +3,7 @@ created by caliskan at 19.04.2023 This file contains all functions, which handle the different cases. Every function should return json format with the wanted data from the database +The functions are called, when data is received on the according channel """ import paho.mqtt.client as mqtt from plantdatabase import PlantDataBase @@ -19,8 +20,21 @@ from robot import Robot def data_sensordata(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase, robot: Robot): + """ + This function is used to store received data from the robot inside the plant_database + USAGE FOR DATA OF ONE PLANT + :param client: mqtt client + :param userdata: + :param message: received data + :param mydatabase: database information + :param robot: robot object + :return: none + """ + + # Load the message and convert to json str_in = str(message.payload.decode("UTF-8")) payload = json.loads(str_in) + logging.info("ROBOT_DATA_SENSORDATA Received data: " + json.dumps(payload)) drive_data = { "PlantID": [payload['PlantID']], @@ -28,86 +42,151 @@ def data_sensordata(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, my } try: - print(drive_data) - print(robot.order_handler) + # Delete order from order list robot.delete_order(drive_data) + # Save data in database mydatabase.insert_measurement_data(plant_id=payload['PlantID'], sensordata_temp=payload['AirTemperature'], sensordata_humidity=payload['AirHumidity'], sensordata_soil_moisture=payload['SoilMoisture'], sensordata_brightness=payload['Brightness']) logging.debug("Inserted to data base: " + json.dumps(payload)) + + # Send received data to frontend action_getalldata(client, userdata, message, mydatabase) except Exception as e: logging.error("Could not delete order: " + str(e)) + def data_sensordataall(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase, - robot: Robot): + robot: Robot): + """ + This function is used to store received data from the robot inside the plant_database + USAGE FOR DATA OF ALL PLANTS + :param client: + :param userdata: + :param message: + :param mydatabase: + :param robot: + :return: none + """ + + # Load the message and convert to json str_in = str(message.payload.decode("UTF-8")) payload = json.loads(str_in) logging.info("ROBOT_DATA_SENSORDATAALL Received data: " + json.dumps(payload)) + # Create list of plant_ids and create dataset plant_ids = [] - for i in payload['SensorData']: plant_ids.append(i["PlantID"]) - print("Plant Names:", str(plant_ids)) drive_data = { "PlantID": plant_ids, "ActionID": payload['ActionID'] } try: - print(robot.order_handler) - print(drive_data) + # Delete order from order list robot.delete_order(drive_data) + + # Insert all received data files in plant_database for i in payload['SensorData']: mydatabase.insert_measurement_data(plant_id=i['PlantID'], - sensordata_temp=i['AirTemperature'], - sensordata_humidity=i['AirHumidity'], - sensordata_soil_moisture=i['SoilMoisture'], - sensordata_brightness=i['Brightness']) + sensordata_temp=i['AirTemperature'], + sensordata_humidity=i['AirHumidity'], + sensordata_soil_moisture=i['SoilMoisture'], + sensordata_brightness=i['Brightness']) logging.debug("Inserted to data base: " + json.dumps(payload)) + + # Send all the plant data to the frontend action_getalldata(client, userdata, message, mydatabase) except Exception as e: logging.error("Could not delete order: " + str(e)) - def data_position(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, robot: Robot): + """ + This function is used to receive the robot position and insert it in the robot object + :param client: mqtt client + :param userdata: + :param message: received data + :param robot: robot object to store position + :return: none + """ + logging.info("ROBOT_DATA_POSITION Received data: " + json.dumps(message.payload.decode("UTF-8"))) + + # Store received position data in robot object robot.store_position(json.loads(message.payload.decode("UTF-8"))["Position"]) position_data = { "Position": robot.get_position(), "Timestamp": str(datetime.now()) } + # Send the position as a json object to the frontend channel client.publish(Topics['BACKEND_DATA_POSITION'], json.dumps(position_data)) def data_battery(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, robot: Robot): + """ + This function is used to receive the robot position and insert it in the robot object + :param client: mqtt client object + :param userdata: + :param message: received data + :param robot: robot object to store the received information + :return: none + """ + logging.info("ROBOT_DATA_BATTERY Received data: " + str(json.dumps(message.payload.decode("UTF-8")))) + # Store battery status in robot object robot.store_battery(json.loads(message.payload.decode("UTF-8"))["Battery"]) battery_data = { "Battery": robot.get_battery(), "Timestamp": str(datetime.now()) } + # Send Battery status and Robot-Ready Status as json objects to frontend client.publish(Topics['BACKEND_DATA_BATTERY'], json.dumps(battery_data)) client.publish(Topics['BACKEND_DATA_ROBOTREADY'], str(robot.get_robot_status())) def data_error(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, robot: Robot): + """ + This function is called when the robot sends an error message and forwards it to the frontend + :param client: mqtt client + :param userdata: + :param message: received error message + :param robot: robot objectg + :return: none + """ + + # Store last error in robot object robot.store_last_error(message.payload.decode("UTF-8")) + # Write error into server log logging.error("ROBOT_DATA_ERROR new error received from Robot: " + robot.get_last_error()) + # Send error data to FrontEnd Channel to display it to the user client.publish(Topics['BACKEND_DATA_ERROR'], message.payload.decode("UTF-8")) def data_robotready(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, robot: Robot): + """ + This function is used to update the Robot-Ready Status of the Robot and inform the FrontEnd about it + :param client: mqtt client + :param userdata: + :param message: received data + :param robot: robot object + :return: none + """ + + # Update the robot status robot.change_robot_status(message.payload.decode("UTF-8") == 'True') + + # If possible send new waiting order to the robot if robot.get_robot_status() is True and robot.get_order_number() >= 1: client.publish(Topics['ROBOT_ACTION_DRIVE'], json.dumps(robot.get_next_order())) logging.info("Waiting Order send to Robot") logging.info("ROBOT_DATA_ROBOTREADY status updated: " + str(robot.get_robot_status())) + + # Send new robot-ready status to frontend channel client.publish(Topics['BACKEND_DATA_ROBOTREADY'], str(robot.get_robot_status())) @@ -115,43 +194,72 @@ def data_robotready(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, ro def action_drive(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase, robot: Robot): + """ + This function is called when a drive command from the FrontEnd is received and forwards the order to the robot or + stores it in the order list. + :param client: mqtt client object + :param userdata: + :param message: information of plant to drive to + :param mydatabase: plant_database + :param robot: robot object + :return: none + """ + # Get PlantID from received PlantName plant_id = mydatabase.get_plant_id(plant_name=json.loads(str(message.payload.decode("UTF-8")))) - print(str(plant_id)) + + # Generate a new ActionID action_id = str(uuid.uuid4()) drive_data = { "PlantID": plant_id, "ActionID": action_id } + # Store order in order list or discard if list already contains 5 orders if robot.get_order_number() < 6 and robot.get_robot_status() is True: robot.add_order({"PlantID": [plant_id], "ActionID": action_id}) + # Send order to robot, if robot is available client.publish(Topics['ROBOT_ACTION_DRIVE'], json.dumps(drive_data)) logging.info("BACKEND_ACTION_DRIVE Drive Command published: " + json.dumps(drive_data)) else: if robot.get_order_number() < 6: + # Add to order list if robot not available robot.add_order(drive_data) logging.info("BACKEND_ACTION_DRIVE New data added to order list: " + str(drive_data)) elif robot.get_order_number() >= 6: + # Discard order if list full logging.error("Could not add Order to list. Order discarded") client.publish(Topics['BACKEND_DATA_ERROR'], "Could not add Order to list. Order discarded") def action_driveall(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase, robot: Robot): + """ + This function is called when the frontend sends a drive_all command which lets the robot drive to all registered plants. + Same as action_drive(), but for all plants. + :param client: + :param userdata: + :param message: + :param mydatabase: + :param robot: + :return: none + """ + + # Get all plantnames from the database and extract the id from them plant_names = mydatabase.get_plant_names() plant_ids = [] - print(plant_names) for names in plant_names: _id = mydatabase.get_plant_id(names[0]) plant_ids.append(_id) + # Create a new order number action_id = str(uuid.uuid4()) drive_data = { "PlantID": plant_ids, "ActionID": action_id } - print(drive_data) + + # Send drive command to Robot if possible (same as action_drive()) if robot.get_order_number() < 6 and robot.get_robot_status() is True: robot.add_order(drive_data) client.publish(Topics['ROBOT_ACTION_DRIVEALL'], json.dumps(drive_data)) @@ -166,64 +274,153 @@ def action_driveall(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, my def action_getposition(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase): + """ + This function is called when the frontend demands the robots position from the backend. It forwards the command to + the robot. + :param client: mqtt client object + :param userdata: + :param message: + :param mydatabase: + :return: none + """ + + # Send command to robot client.publish(Topics['ROBOT_ACTION_GETPOSITION']) logging.info("BACKEND_ACTION_GETPOSITION message forwarded to Robot") def action_getbattery(client: mqtt.Client, userdata, message: mqtt.MQTTMessage): + """ + This function is called when the frontend demands the robots battery status from the backend. It forwards the + command to the robot to get new information. + :param client: mqtt client object + :param userdata: + :param message: + :return: none + """ + + # Send command to robot client.publish(Topics['ROBOT_ACTION_GETBATTERY']) logging.info("BACKEND_ACTION_GETBATTERY message forwarded to Robot") - battery_data = { - "Battery": 66, - "Timestamp": str(datetime.now()) - } - print(battery_data) - client.publish(Topics['BACKEND_DATA_BATTERY'], json.dumps(battery_data)) def action_getalldata(client: mqtt.Client, userdata, message: Union[mqtt.MQTTMessage, list], mydatabase: PlantDataBase): + """ + This function is called when the frontend demands the last data of the registered plants. It gets the last data from + the local database and forwards it to the frontend + :param client: mqtt client object + :param userdata: + :param message: + :param mydatabase: database object, where the plant data is stored + :return: none + """ + + # get the all PlantNames plant_names = mydatabase.get_plant_names() - print("SUIII" + str(plant_names)) alldata = [] + + # Get the latest data from all registered plant names by using the plant names for i in plant_names: alldata.append(mydatabase.get_latest_data(plant_name=i[0])) + + # Send the data as a list to the frontends channel client.publish(Topics['BACKEND_DATA_SENSORDATAALL'], json.dumps(alldata)) logging.info("BACKEND_DATA_SENSORDATAALL got data from database:" + str(alldata)) def action_newplant(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase): + """ + This function is called when the frontend wants to register a new plant. The information of the new plant is + delivered from the frontend and used to register the plant + :param client: mqtt client object + :param userdata: + :param message: data from the frontend + :param mydatabase: local database + :return: none + """ + + # Load the plant data as json plant_data = json.loads(message.payload.decode("UTF-8")) + + # Insert the plant in the database mydatabase.insert_plant(plantname=plant_data["PlantName"], plant_id=plant_data["PlantID"]) + + # Insert a first measurement value in the database mydatabase.insert_measurement_data(plant_id=plant_data["PlantID"], sensordata_temp=plant_data["AirTemperature"], sensordata_humidity=plant_data["AirHumidity"], sensordata_soil_moisture=plant_data["SoilMoisture"], sensordata_brightness=plant_data["Brightness"]) logging.info("BACKEND_ACTION_NEWPLANT new plant data received and inserted: " + str(plant_data)) + + # Send all new plant data to the frontend to update it action_getalldata(client, userdata, message, mydatabase) def action_configureplant(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase): + """ + This function is called when a parameter of a plant is changed by the frontend. It updates the information in the + database and sends the updated data to the frontend + :param client: mqtt client object + :param userdata: + :param message: received data from frontend + :param mydatabase: local database + :return: none + """ + + # Load the received data as json plant_data = json.loads(message.payload.decode("UTF-8")) + + # Update the plant in the database mydatabase.configure_plant(plant_id=plant_data["PlantID"], plantname=plant_data["PlantName"]) + # Insert measurement_data into the database (from frontend) mydatabase.insert_measurement_data(plant_id=plant_data["PlantID"], sensordata_temp=plant_data["AirTemperature"], sensordata_humidity=plant_data["AirHumidity"], sensordata_soil_moisture=plant_data["SoilMoisture"], sensordata_brightness=plant_data["Brightness"]) logging.info("BACKEND_ACTION_CONFIGUREPLANT configure plant data received and inserted: " + str(plant_data)) + + # Update the frontend with the current data action_getalldata(client, userdata, message, mydatabase) def action_deleteplant(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase): + """ + This function is called when the frontend wants to delete a registered plant from the database. + :param client: + :param userdata: + :param message: Received data from the frontend + :param mydatabase: local database object + :return: none + """ + + # Load the Plant-Name from the message delete_plant = json.loads(message.payload.decode("UTF-8")) + + # Delete the plant mydatabase.delete_plant(plant_id=delete_plant) logging.info("BACKEND_ACTION_DELETEPLANT delete plant data received and deleted: " + str(delete_plant)) + + # Send current data to frontend to update it action_getalldata(client, userdata, message, mydatabase) def action_countplants(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase): + """ + This function is called when the frontend requires the count of the currently registerd plants. It sends the number + and maximal possible plant number to the frontend as a json object + :param client: mqtt client object + :param userdata: + :param message: + :param mydatabase: local database + :return: none + """ + + # Count plants count = mydatabase.plant_count() + + # Create Object and send to the FrontEnd count_payload = { "CurrentCount": count, "MaxCount": MAX_PLANT_COUNT diff --git a/software/backend/defines.py b/software/backend/defines.py index 9e47725..1eeebb0 100644 --- a/software/backend/defines.py +++ b/software/backend/defines.py @@ -1,7 +1,8 @@ """ created by caliskan at 19.04.2023 -contains all constants for the backend architecture of the smart garden project +contains all constants for the backend architecture of the smart garden project. This file contains no executable script +and is only for documentation purpose """ MQTT_BROKER_LOCAL = "192.168.0.102" diff --git a/software/backend/main.py b/software/backend/main.py index 238c0bb..360cb9f 100644 --- a/software/backend/main.py +++ b/software/backend/main.py @@ -21,13 +21,13 @@ def on_connect(_client: mqtt.Client, _userdata, _flags, _rc, _mydatabase, _robot """ This method gets called, when it connects to a mqtt broker. It is used to subscribe to the specific topics - :param _robot: - :param _mydatabase: + :param _robot: robot object for + :param _mydatabase: object, that contains the custom plant database and methods for its usage :param _client: mqtt client object :param _userdata: :param _flags: :param _rc: connection flag - :return: + :return: none """ if _rc == 0: print("connected") @@ -100,10 +100,15 @@ def on_connect(_client: mqtt.Client, _userdata, _flags, _rc, _mydatabase, _robot def main(): + # Create Robot Object robot = Robot() + + # Connect to Plant_Database and create tables if database did not exist my_database = PlantDataBase(database_name=DATABASE_NAME) my_database.create_tables() - mqttclient = mqtt.Client(BACKEND_CLIENT_ID, transport="websockets") + + # Create MQTT Client and connect to local broker + mqttclient = mqtt.Client(BACKEND_CLIENT_ID, transport="websockets") # transport websockets required for local broker mqttclient.on_connect = lambda client, userdata, flags, rc: on_connect(_client=client, _userdata=userdata, _flags=flags, @@ -111,10 +116,14 @@ def main(): _mydatabase=my_database, _robot=robot) mqttclient.connect(MQTT_BROKER_LOCAL) + + # Initialize logger and save in server.log file logging.basicConfig(filename="server.log", filemode="a", encoding="utf-8", level=logging.DEBUG, format='%(asctime)s %(name)s %(levelname)s %(message)s', datefmt="%d-%m-%Y %H:%M:%S") logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) + + # Starting mqttclient infinite loop mqttclient.loop_forever() diff --git a/software/backend/plantdatabase.py b/software/backend/plantdatabase.py index 9229cbd..9b66405 100644 --- a/software/backend/plantdatabase.py +++ b/software/backend/plantdatabase.py @@ -6,25 +6,43 @@ import logging class PlantDataBase: """ - Class to create Makeathon database + Class of a PlantDataBase Object. It contains functions specifically for the plantdatabase. + Usage: + - First declare object + - Then use create_tables() to create the tables (NECESSARY !!) + - After that use the methods of this class """ def __init__(self, database_name: str): - self.db_file = database_name # 'backend_database.db' + """ + Only Constructor of the Class. Pass name of database to connect to. If not available it will create a new one. + :param database_name: Name of the SQLITE database file system + """ + self.db_file = database_name # 'backend_database.db' usually used name self.conn = None try: + # connect or create new database if not available self.conn = sqlite3.connect(self.db_file, check_same_thread=False) except sqlite3.Error as e: logging.error("Database init error: " + str(e)) + + # cursor on the database self.cur = self.conn.cursor() def create_tables(self): + """ + Use this method to create the plants and measurement_values tables. Call this function before using the data + handling methods below + :return: True if successfully, False if not + """ try: + # Create plants table if not already existing table_config = "CREATE TABLE IF NOT EXISTS plants " \ "(PlantID INTEGER PRIMARY KEY," \ "PlantName TEXT)" self.cur.execute(table_config) + # Create measurement_values_table if not already existing table_config = "CREATE TABLE IF NOT EXISTS measurement_values " \ "(measurementID INTEGER PRIMARY KEY," \ "Timestamp DATETIME DEFAULT (datetime('now', 'localtime'))," \ @@ -42,6 +60,12 @@ class PlantDataBase: return False def insert_plant(self, plantname: str, plant_id: int) -> bool: + """ + Insert a new plant with an id and a plantname + :param plantname: name of plant + :param plant_id: id of plant (position in the bed, must be unique!!) + :return: True if successfully, False if not + """ try: self.cur.execute("INSERT INTO plants (PlantName, PlantID) VALUES (?,?)", (plantname, plant_id)) self.conn.commit() @@ -51,6 +75,12 @@ class PlantDataBase: return False def configure_plant(self, plant_id: int, plantname: str) -> bool: + """ + Change a plants parameters in the database by using its PlantID as a search criteria + :param plant_id: id of plant + :param plantname: name of plant + :return: True if successfully, False if not + """ try: self.cur.execute("UPDATE plants SET PlantID = ?, PlantName = ? WHERE PlantID= ?", (plant_id, plantname, plant_id)) @@ -61,6 +91,11 @@ class PlantDataBase: return False def delete_plant(self, plant_id): + """ + Delete a plant from the database + :param plant_id: PlantID of plant to delete + :return: True if successfully, False if not + """ try: self.cur.execute('DELETE FROM plants WHERE PlantID = ?', (plant_id,)) self.conn.commit() @@ -74,6 +109,15 @@ class PlantDataBase: sensordata_humidity, sensordata_soil_moisture, sensordata_brightness) -> bool: + """ + Insert a measurement value of plantID + :param plant_id: plantID of plant + :param sensordata_temp: Temperature + :param sensordata_humidity: Air Humidity value + :param sensordata_soil_moisture: Soil Moisture value + :param sensordata_brightness: brightness value + :return: True if successfully, False if not + """ try: self.cur.execute(f"INSERT INTO measurement_values (PlantID, AirTemperature, AirHumidity," f"SoilMoisture, Brightness) VALUES " @@ -87,10 +131,11 @@ class PlantDataBase: def get_latest_data(self, plant_name: Optional[str] = None, plant_id: Optional[int] = None): """ - Gets the newest parameter of specific plant and returns all parameters in json format - :param plant_id: - :param plant_name: - :return: + Gets the newest parameter of specific plant and returns all parameters in json format. + Either pass plant_name OR plant_id, BOTH passed -> ERROR + :param plant_id: PlantID of plant + :param plant_name: Name of Plant + :return: JSON with data if successfully, else none """ try: if plant_name is not None and plant_id is None: @@ -118,6 +163,11 @@ class PlantDataBase: logging.error("Could not get measurement values: " + str(e)) def delete_data(self, table_name): + """ + Delete all data from a specific tabel + :param table_name: tabel you want to delete + :return: True if successfully, False if not + """ try: self.cur.execute(f'DELETE FROM {table_name}') self.conn.commit() @@ -128,7 +178,7 @@ class PlantDataBase: def plant_count(self) -> int: """ returns the number of plants registered in the database - :return: + :return: Count of registered plants as int """ try: self.cur.execute("SELECT COUNT(*) FROM plants") @@ -137,6 +187,10 @@ class PlantDataBase: logging.error("Could not count plants: " + str(e)) def get_plant_names(self) -> list: + """ + Use this method to get a list of the Names of all registered plants + :return: list containing plantNames + """ try: self.cur.execute("SELECT PlantName FROM plants") return self.cur.fetchall() @@ -144,6 +198,11 @@ class PlantDataBase: logging.error("Could not get plant names: " + str(e)) def get_plant_id(self, plant_name: str) -> int: + """ + Use this method to get the PlantID of a registered plant by its name + :param plant_name: name of registered plant + :return: ID of plant as int + """ try: self.cur.execute("SELECT PlantID FROM plants WHERE PlantName=?", (plant_name,)) return self.cur.fetchone()[0] @@ -151,4 +210,7 @@ class PlantDataBase: logging.error("Could not get plant id: " + str(e)) def __del__(self): + """ + Destructor of Class. Disconnects from database. + """ self.conn.close() diff --git a/software/backend/robot.py b/software/backend/robot.py index 96cb5e1..8bf2c3b 100644 --- a/software/backend/robot.py +++ b/software/backend/robot.py @@ -1,7 +1,7 @@ class Robot: """ This class contains the features of the robot. It is used as an interface for the main to avoid global variables and - store them instead in an instance of this robot object + store them instead an instance of this robot object. It saves the orders and some basic information of the robots status """ def __init__(self): self.robot_ready = True