Browse Source

backend code fully commented

master
caliskan 1 year ago
parent
commit
22bcf7310f

+ 219
- 22
software/backend/data_functions.py View File

@@ -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

+ 2
- 1
software/backend/defines.py View File

@@ -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"

+ 13
- 4
software/backend/main.py View File

@@ -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()



+ 69
- 7
software/backend/plantdatabase.py View File

@@ -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()

+ 1
- 1
software/backend/robot.py View File

@@ -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

Loading…
Cancel
Save