Compare commits

...
10 Commits
Author SHA1 Message Date
caliskanbi 556736ccf5 Class Diagram Updated 2023-09-26 21:57:42 +02:00
duelgerke82357 410e27adc3 Final final Status 2023-09-20 18:39:43 +02:00
duelgerke82357 f6f6421620 Final status 2023-09-20 18:26:22 +02:00
waldluis 5e14633f42 Robot UML update 2023-08-31 10:23:02 +02:00
waldluis 1778a211bb Raspy comment update, Robot UML update 2023-08-17 10:37:00 +02:00
caliskan 22bcf7310f backend code fully commented 2023-08-15 23:22:26 +02:00
caliskan 90235a73f1 [add] fixed backend data 2023-08-14 20:52:07 +02:00
waldluis 2c065573a5 Robot docu software description added 2023-07-24 15:45:34 +02:00
waldluis 2a3136503a Robot docu extended 2023-06-28 15:05:47 +02:00
waldluis d9ad3bce0c Robot comments extended, Docu updated 2023-06-09 10:08:27 +02:00
28 changed files with 4844 additions and 3057 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
<component name="WebServers">
<option name="servers">
<webServer id="1b3e9e3f-7394-4cbc-a32d-b95ef2abda52" name="backend">
<fileTransfer accessType="SFTP" host="192.168.137.197" port="22" sshConfigId="8ca8aafc-5b14-4074-b370-599a5b56368a" sshConfig="lego@192.168.137.197:22 password">
<fileTransfer accessType="SFTP" host="lego-K53SV" port="22" sshConfigId="40359a92-2c87-41df-943b-cfac73c7ea3d" sshConfig="lego@lego-K53SV:22 password">
<advancedOptions>
<advancedOptions dataProtectionLevel="Private" keepAliveTimeout="0" passiveMode="true" shareSSLContext="true" />
</advancedOptions>
Binary file not shown.
File diff suppressed because it is too large. Load diff
@@ -24,8 +24,8 @@
</div>
</div>
<div class="item-IMG">
<img class="IMG-Plants" src="assets/images/gruppe1.jpg" alt="Plant is not available">
<img class="IMG-Plants" src="assets/images/gruppe2.jpg" alt="Plant is not available">
<img class="IMG-Plants" src="assets/images/Pflanze.png" alt="Plant is not available">
<img class="IMG-Plants" src="assets/images/Pflanze.png" alt="Plant is not available">
</div>
<div class="item-delete">
<button class="item-Configure" mat-icon-button color="primary" (click)="onConfigurePlant()">
@@ -19,7 +19,6 @@ export class PlantsComponent {
filteredOptions: Observable<string[]>;
plants$: Observable<Plant[]> = this.storeService.currentPlants;
constructor(public dialog: MatDialog, public storeService: StoreService, private mqttRequestService: MqttRequestService) {
// sobald es eine änderung bei den pflanzenNamen gibt, wird bei der suchleiste ein neuer filter eingesetzt
this.options$.subscribe(response => {
@@ -58,12 +57,10 @@ export class PlantsComponent {
this.selectedPlant = value;
}
onOptionDefault() {
this.selectedPlant = 'undefined';
}
private _filter(value: string, options: string[]): string[] {
const filterValue = value.toLowerCase();
@@ -1,7 +1,7 @@
<div class="container"
*ngIf="robotBatteryResponse$ !== undefined && (robotBatteryResponse$ |async) !== undefined; else errorLoadingData">
<div class="robotImage">
<img src="assets/images/gruppe1.jpg" alt="Plant is not available">
<img src="assets/images/Robot.png" alt="Plant is not available">
</div>
<div class="robotAkku">
<h1 class="akkuMsg">Robot Akku:</h1>
@@ -66,19 +66,6 @@ export class MqttRequestService {
console.error('Error:', err);
});
// this.mqttService.subscribeToTopic("BACKEND/DATA/POSITION").subscribe(data => {
// if (typeof data !== "object") {
// const payload = JSON.parse(data) as Position;
// this.mqttSetDataService.setRobotPosition(payload);
// }
// }, err => {
// this.dialog.open(ErrorDialogComponent, {
// data: err,
// width: '400px',
// });
// console.error('Error:', err);
// });
this.mqttService.subscribeToTopic("BACKEND/DATA/BATTERY").subscribe(data => {
if (typeof data !== "object") {
const payload = JSON.parse(data) as RobotBattery;
@@ -119,22 +106,6 @@ export class MqttRequestService {
console.error('Error:', err);
});
// this.mqttService.subscribeToTopic("BACKEND/DATA/PICTURE").subscribe(data => {
// if (typeof data !== "object") {
// const payload = JSON.parse(data) as Plant;
// this.mqttSetDataService.setDataPlant(payload);
// }
// }, err => {
// console.error('Error:', err);
// });
//Request to get ALL DATA every 10sec!!
// this.timerSubscription = timer(0, 10000).pipe(
// map(() => {
// this.mqttService.publishToTopic('BACKEND/ACTION/GETALLDATA');
// })
// ).subscribe();
//Publish to the Topic to recieve the first Data
this.publishToPLANTCOUNT();
this.publishToGETBATTERY();
@@ -145,18 +116,14 @@ export class MqttRequestService {
publishToGETALLDATA() {
this.mqttService.publishToTopic('BACKEND/ACTION/GETALLDATA', JSON.stringify(this.storeService.getAllPlantNames()));
}
publishToDRIVE() {
this.mqttService.publishToTopic('BACKEND/ACTION/DRIVE');
}
publishToGETBATTERY() {
this.mqttService.publishToTopic('BACKEND/ACTION/GETBATTERY');
}
publishToROBOTREADY() {
this.mqttService.publishToTopic('BACKEND/ACTION/ROBOTREADY');
}
publishToGETPOSITION() {
this.mqttService.publishToTopic('BACKEND/ACTION/GETPOSITION');
}
publishToNEWPLANT(plant: Plant) {
this.mqttService.publishToTopic('BACKEND/ACTION/NEWPLANT', JSON.stringify(plant));
@@ -173,6 +140,7 @@ export class MqttRequestService {
publishToPLANTCOUNT() {
this.mqttService.publishToTopic('BACKEND/ACTION/PLANTCOUNT');
}
/**
* publishToINPUT wird die Spracheingabe übermittelt, die dann mit
* mehreren if else anweisungen einen Request an das Backend sendet und
@@ -32,5 +32,4 @@ export class MqttSetDataService {
setPlantCount(data: PlantCount): void {
this.storeService.setPlantCount(data);
}
}
@@ -10,8 +10,8 @@ export class MqttService {
// Connection zu dem MQTT Broker
constructor() {
// this.client = connect('wss://mqtt.eclipseprojects.io:443/mqtt'); //Je nachdem welchen Link der Broker hat
this.client = connect('mqtt://192.168.137.197:1883', { clientId: 'kemal' });
this.client = connect('wss://mqtt.eclipseprojects.io:443/mqtt'); //Online Broker
// this.client = connect('mqtt://192.168.137.197:1883', { clientId: 'kemal' }); //lokaler Broker
}
public subscribeToTopic(topic: string): Observable<any> {
@@ -28,7 +28,6 @@
<img class="img-item" src="assets/images/logo.png" alt="not available">
</header>
<body>
<router-outlet></router-outlet>
</body>
@@ -1,6 +1,6 @@
<div class="container">
<p class="item-header" mat-dialog-title>Map</p>
<img class="item-img" src="assets/images/gruppe1.jpg" alt="not available">
<img class="item-img" src="assets/images/Map.png" alt="not available">
<div class="item-button" mat-dialog-actions>
<button mat-button mat-dialog-close>Close</button>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.
+250 -15
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,105 +20,249 @@ 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'],
"PlantID": [payload['PlantID']],
"ActionID": payload['ActionID']
}
try:
# 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):
"""
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"])
drive_data = {
"PlantID": plant_ids,
"ActionID": payload['ActionID']
}
try:
# 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'])
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()))
client.publish(Topics['BACKEND_DATA_ROBOTREADY'], message.payload.decode("UTF-8"))
# Send new robot-ready status to frontend channel
client.publish(Topics['BACKEND_DATA_ROBOTREADY'], str(robot.get_robot_status()))
# FrontEnd Channel Reactions
def action_drive(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase,
robot: Robot):
plant_id = mydatabase.get_plant_id(plant_name=json.loads(message.payload.decode("UTF-8"))["PlantName"])
"""
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"))))
# 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(drive_data)
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)
_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(plant_ids))
client.publish(Topics['ROBOT_ACTION_DRIVEALL'], json.dumps(drive_data))
logging.info("BACKEND_ACTION_DRIVEALL Drive Command published: " + json.dumps(drive_data))
else:
if robot.get_order_number() < 6:
@@ -129,63 +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()
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))
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
+3 -2
View File
@@ -1,10 +1,11 @@
"""
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.137.197"
MQTT_BROKER_LOCAL = "192.168.0.102"
MQTT_BROKER_GLOBAL = "mqtt.eclipseprojects.io"
RASPI_CLIENT_ID = "smart_farming_raspi"
BACKEND_CLIENT_ID = "smart_farming_server"
@@ -0,0 +1,12 @@
test_dict = {
"PlantData": [ {"PlantID":1, "Test": 0}, {"PlantID": 2, "Test": 0}],
"ActionID": "SUIIII"
}
plant_ids = []
for i in test_dict["PlantData"]:
plant_ids.append(i["PlantID"])
print(plant_ids)
+16 -4
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")
@@ -39,6 +39,9 @@ def on_connect(_client: mqtt.Client, _userdata, _flags, _rc, _mydatabase, _robot
_client.message_callback_add(Topics['ROBOT_DATA_SENSORDATA'], lambda client, userdata, message: data_functions.
data_sensordata(client, userdata, message, _mydatabase, _robot))
_client.subscribe(Topics['ROBOT_DATA_ALL'])
_client.message_callback_add(Topics['ROBOT_DATA_ALL'], lambda client, userdata, message: data_functions.
data_sensordataall(client, userdata, message, _mydatabase, _robot))
_client.subscribe(Topics['ROBOT_DATA_POSITION'])
_client.message_callback_add(Topics['ROBOT_DATA_POSITION'], data_functions.data_position)
@@ -97,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)
# 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,
@@ -108,10 +116,14 @@ def main():
_mydatabase=my_database,
_robot=robot)
mqttclient.connect(MQTT_BROKER_GLOBAL)
# 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()
+70 -9
View File
@@ -2,30 +2,47 @@
import sqlite3
from typing import Optional
import logging
from itertools import chain
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'))," \
@@ -43,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()
@@ -52,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))
@@ -62,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()
@@ -75,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 "
@@ -88,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:
@@ -119,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()
@@ -129,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")
@@ -138,13 +187,22 @@ 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 [row[0] for row in self.cur.fetchall()]
return self.cur.fetchall()
except Exception as e:
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]
@@ -152,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
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
-2
View File
@@ -65,7 +65,6 @@ ALLSENSORDATA = {
"ActionID": ""
}
# TODO When sensor is available
POSITION = {
"Position": ""
}
@@ -115,7 +114,6 @@ ALLPLANTDATA = [
PLANTDATA
]
# TODO When sensor is available
POSITION = {
"Position": "",
"Timestamp": ""
+31 -21
View File
@@ -18,6 +18,8 @@ def measure_send_data(plantID, actionID, client: mqtt.Client):
"""
Reads all sensors and publishes data via MQTT in form of SENSORDATA
***Function is only neccessary for driving without color codes***
Args:
plantID (_type_): plant to measure
actionID (_type_): current ID of driving action
@@ -48,7 +50,7 @@ def drive_plant_thread(plantID, actionID, client: mqtt.Client):
Meassure and publish data via MQTT
Drive home to starting point
***If color codes are working properly this function is not needed anymore***
***Function is only neccessary for driving without color codes***
Args:
plantID (_type_): plant to measure
@@ -58,35 +60,38 @@ def drive_plant_thread(plantID, actionID, client: mqtt.Client):
errorCode = os.system(f'sshpass -p maker ssh robot@ev3dev.local python3 /home/robot/Programme/plant_{plantID}.py')
# Error handling for drive operation
# Abort when drive operation at start when error occurs
if errorCode != 0:
if errorCode == 65280:
errorMessage = "EV3 not connected"
else:
errorMessage = "Motor or Sensor unplugged"
logging.info(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
logging.error(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
client.publish(Topics["ROBOT_DATA_ERROR"], f"{errorMessage}, Drive Plant aborted, Robot at starting position", qos=1)
client.publish(Topics["ROBOT_DATA_ROBOTREADY"], "True", qos=1)
return
logging.info("Measuring Sensors")
measure_send_data(plantID, actionID, client)
# TODO How to Handle Pictures and PlantID
logging.info("Taking Picture")
sendPicture(client)
# Taking Picture not fully implemented
# logging.info("Taking Picture")
# sendPicture(client)
logging.info("Robot driving home")
errorCode = os.system(f'sshpass -p maker ssh robot@ev3dev.local python3 /home/robot/Programme/drive_back.py {plantID}')
# Error handling for drive operation
# Abort when drive operation at start when error occurs
if errorCode != 0:
if errorCode == 65280:
errorMessage = "EV3 not connected"
else:
errorMessage = "Motor or Sensor unplugged"
logging.info(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
logging.error(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
client.publish(Topics["ROBOT_DATA_ERROR"], f"{errorMessage}, Drive Plant aborted, Robot at plant {plantID}", qos=1)
client.publish(Topics["ROBOT_DATA_ROBOTREADY"], "True", qos=1)
return
@@ -111,7 +116,7 @@ def drive_plant_all_thread(plantIDs: list, actionID, client: mqtt.Client):
client (mqtt.Client): current MQTT client
"""
allPlantData = {
allPlantData = { # Sensordata for all plants
"SensorData": [],
"ActionID": ""
}
@@ -119,17 +124,18 @@ def drive_plant_all_thread(plantIDs: list, actionID, client: mqtt.Client):
allPlantData["ActionID"] = actionID
for plant in plantIDs:
# FIXME Only possible with color codes
logging.info(f"Robot Driving to plant {plant}")
errorCode = os.system(f'sshpass -p maker ssh robot@ev3dev.local python3 /home/robot/Programme/drive_plant.py {plant}')
# Error handling for drive operation
# Abort when drive operation at start when error occurs
if errorCode != 0:
if errorCode == 65280:
errorMessage = "EV3 not connected"
else:
errorMessage = "Motor or Sensor unplugged"
logging.info(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
logging.error(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
client.publish(Topics["ROBOT_DATA_ERROR"], f"{errorMessage}, Drive Plant aborted, Robot at starting position", qos=1)
client.publish(Topics["ROBOT_DATA_ROBOTREADY"], "True", qos=1)
return
@@ -149,27 +155,32 @@ def drive_plant_all_thread(plantIDs: list, actionID, client: mqtt.Client):
allPlantData["SensorData"].append(sensordata)
# TODO How to Handle Pictures and PlantID
logging.info("Taking Picture")
sendPicture(client)
# Taking Picture not fully implemented
# logging.info("Taking Picture")
# sendPicture(client)
errorCode = os.system(f'sshpass -p maker ssh robot@ev3dev.local python3 /home/robot/Programme/drive_arm.py {plant}')
# Error handling for drive operation
# Abort when drive operation at start when error occurs
if errorCode != 0:
if errorCode == 65280:
errorMessage = "EV3 not connected"
else:
errorMessage = "Motor or Sensor unplugged"
logging.info(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
logging.error(f"{errorMessage}, Drive Plant aborted, Robot at plant {plant}")
client.publish(Topics["ROBOT_DATA_ERROR"], f"{errorMessage}, Drive Plant aborted, Robot at plant {plant}", qos=1)
client.publish(Topics["ROBOT_DATA_ROBOTREADY"], "True", qos=1)
return
# End of drive and measure operation
logging.info("All Plants measured, sending data")
client.publish(Topics["ROBOT_DATA_ALL"], json.dumps(allPlantData, indent=4), qos=1)
logging.info("Robot driving home")
# Error handling for drive operation
# Abort when drive operation at start when error occurs
errorCode = os.system(f'sshpass -p maker ssh robot@ev3dev.local python3 /home/robot/Programme/drive_back_straight.py')
if errorCode != 0:
if errorCode == 65280:
@@ -177,7 +188,7 @@ def drive_plant_all_thread(plantIDs: list, actionID, client: mqtt.Client):
else:
errorMessage = "Motor or Sensor unplugged"
logging.info(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
logging.error(f"{errorMessage}, Drive Plant aborted, Robot at starting position")
client.publish(Topics["ROBOT_DATA_ERROR"], f"{errorMessage}, Drive Plant aborted, Robot at plant {plant}", qos=1)
client.publish(Topics["ROBOT_DATA_ROBOTREADY"], "True", qos=1)
return
@@ -196,7 +207,7 @@ def drive_plant(client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
Function to drive to plant according to request
Starting Drive in Thread to not block main programm
***If color codes are working properly this function is not needed anymore***
***Function is only neccessary for driving without color codes***
Args:
client (mqtt.Client): current mqtt client
@@ -215,7 +226,6 @@ def drive_plant(client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
thread.start()
# FIXME Only possible with color codes
def drive_plant_all(client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
"""
Function to drive to plants according to request
@@ -240,7 +250,7 @@ def drive_plant_all(client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
thread.start()
def get_position(clients: mqtt.Client, userdata, message: mqtt.MQTTMessage):
def get_position(client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
"""
Callback function for GPS position request
Function to send actual GPS position via MQTT in form of POSITION
@@ -254,18 +264,18 @@ def get_position(clients: mqtt.Client, userdata, message: mqtt.MQTTMessage):
"""
logging.info("Received position request")
# TODO Write GPS Sensor Function
position = {
"Position": ""
}
clients.publish(Topics["ROBOT_DATA_POSITION"], json.dumps(position, indent=4), qos=1)
client.publish(Topics["ROBOT_DATA_POSITION"], json.dumps(position, indent=4), qos=1)
def get_batteryStatus(client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
"""
Callback function for battery status request
Function to read battery status from EV3 and send via MQTT in form of BATTERY
Current battery level is stored in "voltage_now" file
Reading battery level via SSH script execution
Battery level shown in Volts:
8,5V -> 100%
@@ -282,7 +292,7 @@ def get_batteryStatus(client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
batteryLevel = int(os.popen('sshpass -p maker ssh robot@ev3dev.local cat /sys/devices/platform/battery/power_supply/lego-ev3-battery/voltage_now').read())
except:
logging.error("EV3 not connected")
client.publish(Topics["ROBOT_DATA_ERROR"], "Robot not connected", qos=1)
client.publish(Topics["ROBOT_DATA_ERROR"], "EV3 not connected", qos=1)
return
batteryLevel = round(batteryLevel / 1000000, 2) # Conversion to Volt
+1 -1
View File
@@ -45,7 +45,7 @@ def on_connect(client: mqtt.Client, userdata, flags, rc):
def main():
"""
Initialises MQTT and Sensors
Initialises MQTT
Runs forever and controlls all robot functions
"""
logging.basicConfig(filename="robot.log", filemode="a", encoding="utf-8", level=logging.DEBUG, format='%(asctime)s %(name)s %(levelname)s %(message)s',
+8 -9
View File
@@ -42,7 +42,7 @@ def readDHT22():
def readTSL2561():
"""
Reads TSL2561 brightness in Lux and returns integer value
Reads TSL2561 brightness via I2C in Lux and returns integer value
Raises:
Exception: If TSL2561 not connected properly
@@ -67,7 +67,7 @@ def readTSL2561():
def readMCP3008():
"""
Reads YL-69 via MCP3008 ADC soil moisture in percent and returns float value
Reads YL-69 via MCP3008 ADC and SPI soil moisture in percent and returns float value
Raises:
Exception: If YL-69 not connected properly
@@ -99,6 +99,7 @@ def readSensors(sensorData):
"""
Read DHT22, TSL2561 and Humidity Sensor
Dictionary is passed to ensure that values are available when errors occur
When error occurs during reading of sensor, affected values are set to 0
Args:
sensorData (dictionary): Dictionary of type SENSORDATA
@@ -114,7 +115,7 @@ def readSensors(sensorData):
try:
sensorData["AirTemperature"], sensorData["AirHumidity"] = readDHT22()
except Exception as e:
sensorData["AirHumidity"] = 0 # No value returend if error occurs -> setting dummy value
sensorData["AirHumidity"] = 0 # No value returend if error occurs -> setting safe values
sensorData["AirTemperature"] = 0
errorMessage = str(e) + "\n" # Appending received error message to later forward all occured errors
@@ -122,17 +123,17 @@ def readSensors(sensorData):
try:
sensorData["Brightness"] = readTSL2561()
except Exception as e:
sensorData["Brightness"] = 0 # No value returend if error occurs -> setting dummy value
sensorData["Brightness"] = 0 # No value returend if error occurs -> setting safe value
errorMessage = errorMessage + str(e) + "\n" # Appending received error message to later forward all occured errors
# read YL-69
try:
sensorData["SoilMoisture"] = readMCP3008()
except Exception as e:
sensorData["SoilMoisture"] = 0 # No value returend if error occurs -> setting dummy value
sensorData["SoilMoisture"] = 0 # No value returend if error occurs -> setting safe value
errorMessage = errorMessage + str(e) + "\n" # Appending received error message to later forward all occured errors
# raise combined error message
# raise combined error message, successfull values still available
if errorMessage != "":
raise Exception(errorMessage)
@@ -146,15 +147,13 @@ def takePicture():
try:
camera = PiCamera()
except:
# raise Exception("Camera not connected")
pass
raise Exception("Camera not connected")
camera.start_preview()
camera.capture("picture.png")
camera.stop_preview()
# TODO - read position with sensor
def readPosition():
"""
Read and return Position