Browse Source

Error Handling BackEnd added, logging included, order list and robot class added

master
caliskanbi 1 year ago
parent
commit
4e60cd9ad0

+ 14
- 0
.idea/deployment.xml View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" serverName="backend" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="backend">
<serverdata>
<mappings>
<mapping deploy="/home/lego/SMARTGARDENING" local="$PROJECT_DIR$/software" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

+ 8
- 0
.idea/sshConfigs.xml View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SshConfigs">
<configs>
<sshConfig authType="PASSWORD" host="lego-K53SV" id="40359a92-2c87-41df-943b-cfac73c7ea3d" port="22" nameFormat="DESCRIPTIVE" username="lego" useOpenSSHConfig="true" />
</configs>
</component>
</project>

+ 14
- 0
.idea/webServers.xml View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebServers">
<option name="servers">
<webServer id="1b3e9e3f-7394-4cbc-a32d-b95ef2abda52" name="backend">
<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>
</fileTransfer>
</webServer>
</option>
</component>
</project>

+ 1
- 1
requirements.txt View File

@@ -29,4 +29,4 @@ urllib3==1.26.14
Werkzeug==2.2.3
zipp==3.15.0
python-ev3dev2==2.1.0.post1
pytest
pytest==7.3.1

BIN
software/backend/backend_database.db View File


+ 75
- 30
software/backend/data_functions.py View File

@@ -10,73 +10,103 @@ from software.defines import Topics, MAX_PLANT_COUNT
import json
import uuid
from typing import Union
from datetime import datetime
import logging
from robot import Robot


# Robot Channel Reactions

def data_sensordata(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase,
order_handler: list):
print("message received")
# TODO: Store data in database
robot: Robot):
str_in = str(message.payload.decode("UTF-8"))
payload = json.loads(str_in)
print("Received data: ", json.dumps(payload))
logging.info("ROBOT_DATA_SENSORDATA Received data: " + json.dumps(payload))
drive_data = {
"PlantID": payload['PlantID'],
"ActionID": payload['ActionID']
}

order_handler.remove(payload['ActionID'])
try:
robot.delete_order(drive_data)
except Exception as e:
logging.error("Could not delete order: " + str(e))

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


def data_position(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase):
# TODO: Forward to frontend in json format
client.publish(Topics['BACKEND_DATA_POSITION'], message.payload.decode("utf-8"))
def data_position(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, robot: Robot):
logging.info("ROBOT_DATA_POSITION Received data: " + json.dumps(message.payload.decode("UTF-8")))
robot.store_position(json.loads(message.payload.decode("UTF-8"))["Position"])
position_data = {
"Position": robot.get_position(),
"Timestamp": str(datetime.now())
}
client.publish(Topics['BACKEND_DATA_POSITION'], json.dumps(position_data))


def data_battery(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase):
# TODO: Forward to frontend in json format
client.publish(Topics['BACKEND_DATA_BATTERY'], message.payload.decode("utf-8"))
def data_battery(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, robot: Robot):
logging.info("ROBOT_DATA_BATTERY Received data: " + json.dumps(message.payload.decode("UTF-8")))
robot.store_battery(json.loads(message.payload.decode("UTF-8"))["Battery"])
battery_data = {
"Battery": robot.get_battery(),
"Timestamp": str(datetime.now())
}
client.publish(Topics['BACKEND_DATA_BATTERY'], json.dumps(battery_data))


# FrontEnd Channel Reactions

def action_drive(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase,
order_handler: list):
# TODO: ROBOT READY CHECK
if len(order_handler) < 5:
order_handler.append(uuid.uuid4())
robot: Robot):
plant_id = mydatabase.get_plant_id(plant_name=json.loads(message.payload.decode("UTF-8"))["PlantName"])
action_id = str(uuid.uuid4())
drive_data = {
"PlantID": plant_id,
"ActionID": action_id
}

if robot.get_order_number() < 5 and robot.get_robot_status() is True:
robot.add_order(drive_data)
client.publish(Topics['ROBOT_ACTION_DRIVE'], json.dumps(drive_data))
logging.info("BACKEND_ACTION_DRIVE Drive Command published: " + json.dumps(drive_data))
else:
# TODO: What to do when no place in order_list left
pass
client.publish(Topics['ROBOT_ACTION_DRIVE'], message.payload.decode("utf-8"))
if robot.get_order_number() < 5:
robot.add_order(drive_data)
logging.info("BACKEND_ACTION_DRIVE New data added to order list: " + str(drive_data))
elif robot.get_order_number() >= 5:
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):
def action_driveall(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase,
robot: Robot):
# TODO: Implement here
pass
print("HELLO")


def action_getposition(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase):
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, mydatabase: PlantDataBase):
def action_getbattery(client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
client.publish(Topics['ROBOT_ACTION_GETBATTERY'])
logging.info("BACKEND_ACTION_GETBATTERY message forwarded to Robot")


def action_getalldata(client: mqtt.Client, userdata, message: Union[mqtt.MQTTMessage, list], mydatabase: PlantDataBase):
plant_names = mydatabase.get_plant_names()
print(type(plant_names))
alldata = []
for i in plant_names:
print("I Type: " + str(type(i)))
print("I: " + i[0])
alldata.append(mydatabase.get_latest_data(plant_name=i[0]))
client.publish(Topics['BACKEND_DATA_SENSORDATAALL'], json.dumps(alldata))
print("BACKEND_DATA_SENSORDATAALL SEND DATA:" + str(alldata))
logging.info("BACKEND_DATA_SENSORDATAALL got data from database:" + str(alldata))


def action_newplant(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase):
@@ -87,8 +117,7 @@ def action_newplant(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, my
sensordata_humidity=plant_data["AirHumidity"],
sensordata_soil_moisture=plant_data["SoilMoisture"],
sensordata_brightness=plant_data["Brightness"])
print("BACKEND_ACTION_NEWPLANT RECEIVED DATA: " + str(plant_data))
print(mydatabase.get_plant_names())
logging.info("BACKEND_ACTION_NEWPLANT new plant data received and inserted: " + str(plant_data))
action_getalldata(client, userdata, message, mydatabase)


@@ -100,14 +129,14 @@ def action_configureplant(client: mqtt.Client, userdata, message: mqtt.MQTTMessa
sensordata_humidity=plant_data["AirHumidity"],
sensordata_soil_moisture=plant_data["SoilMoisture"],
sensordata_brightness=plant_data["Brightness"])
print("BACKEND_ACTION_CONFIGUREPLANT RECEIVED DATA: " + str(plant_data))
logging.info("BACKEND_ACTION_CONFIGUREPLANT configure plant data received and inserted: " + str(plant_data))
action_getalldata(client, userdata, message, mydatabase)


def action_deleteplant(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, mydatabase: PlantDataBase):
delete_plant = json.loads(message.payload.decode("UTF-8"))
mydatabase.delete_plant(plant_id=delete_plant)
print("BACKEND_ACTION_DELETEPLANT RECEIVED DATA: " + str(delete_plant))
logging.info("BACKEND_ACTION_DELETEPLANT delete plant data received and deleted: " + str(delete_plant))
action_getalldata(client, userdata, message, mydatabase)


@@ -118,4 +147,20 @@ def action_countplants(client: mqtt.Client, userdata, message: mqtt.MQTTMessage,
"MaxCount": MAX_PLANT_COUNT
}
client.publish(Topics["BACKEND_DATA_PLANTCOUNT"], json.dumps(count_payload))
print("BACKEND_DATA_PLANTCOUNT SEND DATA: " + str(count_payload))
logging.info("BACKEND_DATA_PLANTCOUNT forwarded plant count to FrontEnd: " + str(count_payload))


def data_error(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, robot: Robot):
robot.store_last_error(message.payload.decode("UTF-8"))
logging.error("ROBOT_DATA_ERROR new error received from Robot: " + robot.get_last_error())
client.publish(Topics['BACKEND_DATA_ERROR'], message.payload.decode("UTF-8"))


def data_robotready(client: mqtt.Client, userdata, message: mqtt.MQTTMessage, robot: Robot):
robot.change_robot_status(message.payload.decode("UTF-8") == 'True')
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"))

+ 7
- 12
software/backend/dev_test_examples/mqtt_publisher.py View File

@@ -1,10 +1,12 @@
import paho.mqtt.client as mqtt

import software.defines
from software.defines import MQTT_BROKER_LOCAL
from random import randrange, uniform
import time
import json
from software.defines import Topics, PLANTDATA
mqttBroker = "192.168.178.182"
mqttBroker = software.defines.MQTT_BROKER_GLOBAL


def on_connect(client, userdata, flags, rc):
@@ -20,17 +22,10 @@ client.on_connect = on_connect
client.connect(mqttBroker)

plantdata = {
"AirTemperature": 20.4,
"AirHumidity": 7.0,
"SoilMoisture": 5.0,
"Brightness": 39,
"PlantID": 2,
"Timestamp": "hallo",
"MeasurementID": 187
"PlantName": "Kemal"
}

print(type(PLANTDATA))
while True:
client.publish("TEST", json.dumps(plantdata))
print(json.dumps(plantdata))
time.sleep(2)
client.publish("BACKEND/ACTION/GETBATTERY", json.dumps(plantdata))
print(json.dumps(plantdata))
time.sleep(2)

+ 39
- 20
software/backend/main.py View File

@@ -12,17 +12,17 @@ import paho.mqtt.client as mqtt
from software.defines import MQTT_BROKER_LOCAL, MQTT_BROKER_GLOBAL, Topics, BACKEND_CLIENT_ID, DATABASE_NAME
from plantdatabase import PlantDataBase
import data_functions
import logging
import sys
from robot import Robot

# inits
mydatabase = PlantDataBase(database_name=DATABASE_NAME)
mydatabase.create_tables()
order_handler = [] # will contain UUIDS with Order IDs


def on_connect(_client: mqtt.Client, _userdata, _flags, _rc):
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 _client: mqtt client object
:param _userdata:
:param _flags:
@@ -37,25 +37,25 @@ def on_connect(_client: mqtt.Client, _userdata, _flags, _rc):
# From Robot:
_client.subscribe(Topics['ROBOT_DATA_SENSORDATA'])
_client.message_callback_add(Topics['ROBOT_DATA_SENSORDATA'], lambda client, userdata, message: data_functions.
data_sensordata(client, userdata, message, mydatabase, order_handler))
data_sensordata(client, userdata, message, _mydatabase, _robot))

_client.subscribe(Topics['ROBOT_DATA_POSITION'])
_client.message_callback_add(Topics['ROBOT_DATA_POSITION'], data_functions.data_position)

_client.subscribe(Topics['ROBOT_DATA_BATTERY'])
_client.message_callback_add(Topics['ROBOT_DATA_BATTERY'], lambda client, userdata, message: data_functions.
data_battery(client, userdata, message, mydatabase))
data_battery(client, userdata, message))

# client.subscribe('Robot/Data/Picture')

# From FrontEnd:
_client.subscribe(Topics['BACKEND_ACTION_DRIVE'])
_client.message_callback_add(Topics['BACKEND_ACTION_DRIVE'], lambda client, userdata, message: data_functions.
action_drive(client, userdata, message, mydatabase, order_handler))
action_drive(client, userdata, message, _mydatabase, _robot))

_client.subscribe(Topics['BACKEND_ACTION_DRIVEALL'])
_client.message_callback_add(Topics['BACKEND_ACTION_DRIVE'], lambda client, userdata, message: data_functions.
action_driveall(client, userdata, message, mydatabase))
_client.message_callback_add(Topics['BACKEND_ACTION_DRIVEALL'], lambda client, userdata, message: data_functions.
action_driveall(client, userdata, message, _mydatabase, _robot))

_client.subscribe(Topics['BACKEND_ACTION_GETPOSITION'])
_client.message_callback_add(Topics['BACKEND_ACTION_GETPOSITION'], data_functions.action_getposition)
@@ -66,35 +66,54 @@ def on_connect(_client: mqtt.Client, _userdata, _flags, _rc):
_client.subscribe(Topics['BACKEND_ACTION_GETALLDATA'])
_client.message_callback_add(Topics['BACKEND_ACTION_GETALLDATA'],
lambda client, userdata, message: data_functions.
action_getalldata(client, userdata, message, mydatabase))
action_getalldata(client, userdata, message, _mydatabase))

_client.subscribe(Topics['BACKEND_ACTION_NEWPLANT'])
_client.message_callback_add(Topics['BACKEND_ACTION_NEWPLANT'], lambda client, userdata, message: data_functions.
action_newplant(client, userdata, message, mydatabase))
action_newplant(client, userdata, message, _mydatabase))

_client.subscribe(Topics['BACKEND_ACTION_CONFIGUREPLANT'])
_client.message_callback_add(Topics['BACKEND_ACTION_CONFIGUREPLANT'], lambda client, userdata, message: data_functions.
action_configureplant(client, userdata, message, mydatabase))
action_configureplant(client, userdata, message, _mydatabase))

_client.subscribe(Topics['BACKEND_ACTION_DELETEPLANT'])
_client.message_callback_add(Topics['BACKEND_ACTION_DELETEPLANT'],
lambda client, userdata, message: data_functions.
action_deleteplant(client, userdata, message, mydatabase))
action_deleteplant(client, userdata, message, _mydatabase))

_client.subscribe(Topics['BACKEND_ACTION_PLANTCOUNT'])
_client.message_callback_add(Topics['BACKEND_ACTION_PLANTCOUNT'], lambda client, userdata, message: data_functions.
action_countplants(client, userdata, message, mydatabase))
action_countplants(client, userdata, message, _mydatabase))

_client.subscribe(Topics['ROBOT_DATA_ERROR'])
_client.message_callback_add(Topics['ROBOT_DATA_ERROR'], lambda client, userdata, message: data_functions.
data_error(client, userdata, message, _robot))

_client.subscribe(Topics['ROBOT_DATA_ROBOTREADY'])
_client.message_callback_add(Topics['ROBOT_DATA_ROBOTREADY'], lambda client, userdata, message: data_functions.
data_robotready(client, userdata, message, _robot))
# END TOPIC SUBSCRIPTIONS
else:
print("connection failed")


def main():
client = mqtt.Client(BACKEND_CLIENT_ID)
client.on_connect = on_connect
client.connect(MQTT_BROKER_GLOBAL)
client.loop_forever()
robot = Robot()
my_database = PlantDataBase(database_name=DATABASE_NAME)
my_database.create_tables()
mqttclient = mqtt.Client(BACKEND_CLIENT_ID)
mqttclient.on_connect = lambda client, userdata, flags, rc: on_connect(_client=client,
_userdata=userdata,
_flags=flags,
_rc=rc,
_mydatabase=my_database,
_robot=robot)
mqttclient.connect(MQTT_BROKER_GLOBAL)
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))
mqttclient.loop_forever()


if __name__ == "__main__":

+ 47
- 27
software/backend/plantdatabase.py View File

@@ -1,6 +1,7 @@
# file to create a database via python script
import sqlite3
from typing import Optional
import logging


class PlantDataBase:
@@ -13,9 +14,8 @@ class PlantDataBase:
self.conn = None
try:
self.conn = sqlite3.connect(self.db_file, check_same_thread=False)
print(sqlite3.version)
except sqlite3.Error as e:
print(e)
logging.error("Database init error: " + str(e))
self.cur = self.conn.cursor()

def create_tables(self):
@@ -38,32 +38,36 @@ class PlantDataBase:
self.cur.execute(table_config)
return True
except sqlite3.Warning as e:
return e
logging.error("Could not create tables: " + str(e))
return False

def insert_plant(self, plantname: str, plant_id: int):
def insert_plant(self, plantname: str, plant_id: int) -> bool:
try:
self.cur.execute("INSERT INTO plants (PlantName, PlantID) VALUES (?,?)", (plantname, plant_id))
self.conn.commit()
return True
except (sqlite3.NotSupportedError, sqlite3.Warning) as e:
return e
except Exception as e:
logging.error("Could not insert plant: " + str(e))
return False

def configure_plant(self, plant_id: int, plantname: str):
def configure_plant(self, plant_id: int, plantname: str) -> bool:
try:
self.cur.execute("UPDATE plants SET PlantID = ?, PlantName = ? WHERE PlantID= ?",
(plant_id, plantname, plant_id))
self.conn.commit()
return True
except (sqlite3.NotSupportedError, sqlite3.Warning) as e:
return e
except Exception as e:
logging.error("Could not configure plant: " + str(e))
return False

def delete_plant(self, plant_id):
try:
self.cur.execute('DELETE FROM plants WHERE PlantID = ?', (plant_id,))
self.conn.commit()
return True
except (sqlite3.NotSupportedError, sqlite3.Warning) as e:
return e
except Exception as e:
logging.error("Could not delete plant: " + str(e))
return False

def insert_measurement_data(self, plant_id,
sensordata_temp,
@@ -72,13 +76,14 @@ class PlantDataBase:
sensordata_brightness) -> bool:
try:
self.cur.execute(f"INSERT INTO measurement_values (PlantID, AirTemperature, AirHumidity,"
f"SoilMoisture, Brightness) VALUES "
f"({plant_id}, {sensordata_temp}, {sensordata_humidity}, {sensordata_soil_moisture}"
f", {sensordata_brightness})")
f"SoilMoisture, Brightness) VALUES "
f"({plant_id}, {sensordata_temp}, {sensordata_humidity}, {sensordata_soil_moisture}"
f", {sensordata_brightness})")
self.conn.commit()
return True
except (sqlite3.NotSupportedError, sqlite3.Warning) as e:
return e
except Exception as e:
logging.error("Could not insert measurement data: " + str(e))
return False

def get_latest_data(self, plant_name: Optional[str] = None, plant_id: Optional[int] = None):
"""
@@ -108,27 +113,42 @@ class PlantDataBase:
"PlantName": plant_name
}
return json_file
except (sqlite3.Warning, TypeError) as e:
return e

def delete_data(self, table_name):
self.cur.execute(f'DELETE FROM {table_name}')
self.conn.commit()
return True
except Exception as e:
logging.error("Could not get measurement values: " + str(e))

# TODO: Kemals Scheiß implementieren
def delete_data(self, table_name):
try:
self.cur.execute(f'DELETE FROM {table_name}')
self.conn.commit()
return True
except Exception as e:
logging.error("Could not delete data: " + str(e))

def plant_count(self) -> int:
"""
returns the number of plants registered in the database
:return:
"""
self.cur.execute("SELECT COUNT(*) FROM plants")
return self.cur.fetchone()[0]
try:
self.cur.execute("SELECT COUNT(*) FROM plants")
return self.cur.fetchone()[0]
except Exception as e:
logging.error("Could not count plants: " + str(e))

def get_plant_names(self) -> list:
self.cur.execute("SELECT PlantName FROM plants")
return self.cur.fetchall()
try:
self.cur.execute("SELECT PlantName FROM plants")
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:
try:
self.cur.execute("SELECT PlantID FROM plants WHERE PlantName=?", (plant_name,))
return self.cur.fetchone()[0]
except Exception as e:
logging.error("Could not get plant id: " + str(e))

def __del__(self):
self.conn.close()

+ 47
- 0
software/backend/robot.py View File

@@ -0,0 +1,47 @@
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
"""
def __init__(self):
self.robot_ready = True
self.order_handler = []
self.battery = 0
self.position = ""
self.last_error = ""

def change_robot_status(self, status: bool):
self.robot_ready = status

def add_order(self, drivedata):
self.order_handler.append(drivedata)

def delete_order(self, drivedata):
self.order_handler.remove(drivedata)

def get_next_order(self):
return self.order_handler[0]

def get_order_number(self):
return len(self.order_handler)

def store_battery(self, battery):
self.battery = battery

def store_position(self, position):
self.position = position

def store_last_error(self, error):
self.last_error = error

def get_battery(self):
return self.battery

def get_position(self):
return self.position

def get_last_error(self):
return self.last_error

def get_robot_status(self):
return self.robot_ready

BIN
software/backend/tests/test_database.db View File


+ 4
- 1
software/defines.py View File

@@ -21,6 +21,8 @@ Topics = {
"ROBOT_DATA_BATTERY": "ROBOT/DATA/BATTERY",
"ROBOT_DATA_POSITION": "ROBOT/DATA/POSITION",
"ROBOT_DATA_PICTURE": "ROBOT/DATA/PICTURE",
"ROBOT_DATA_ERROR": "ROBOT/DATA/ERROR",
"ROBOT_DATA_ROBOTREADY": "ROBOT/DATA/ROBOTREADY",

"BACKEND_ACTION_DRIVE": "BACKEND/ACTION/DRIVE",
"BACKEND_ACTION_DRIVEALL": "BACKEND/ACTION/DRIVEALL",
@@ -38,7 +40,8 @@ Topics = {
"BACKEND_DATA_BATTERY": "BACKEND/DATA/BATTERY",
"BACKEND_DATA_PICTURE": "BACKEND/DATA/PICTURE",
"BACKEND_DATA_PLANTCOUNT": "BACKEND/DATA/PLANTCOUNT",

"BACKEND_DATA_ERROR": "BACKEND/DATA/ERROR",
"BACKEND_DATA_ROBOTREADY": "BACKEND/DATA/ROBOTREADY"

}


Loading…
Cancel
Save