diff --git a/.gitignore b/.gitignore index 7a7e68f..d47b677 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .coverage +.DS_Store __pycache__ +test_commands.py diff --git a/README.md b/README.md index 8854d86..ca899cc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ # sidecar-starter-pack Authentication and communication utilities for FarmBot sidecars +## 📖 Contents + +* [Installation](#-installation-mac-os) +* [Getting Started](#-getting-started) +* [Functions](#-functions) + * [Setup](#setup) + * [Information](#information) + * [Messaging](#messaging) + * [Basic Commands](#basic-commands) + * [Movement](#movement) + * [Peripherals](#peripherals) + * [Broker Commands](#broker-commands) +* [Developer Info](#-developer-info) + * [api_connect.py](#api_connectpy) + * [broker_connect.py](#broker_connectpy) + ## 💻 Installation (Mac OS) To set up the project locally, follow these steps: @@ -33,7 +49,7 @@ python3 -m pip install paho-mqtt ## 🌱 Getting Started To generate your authorization token and get started: -(1) Import `farmbot_utilities` and create an instance. +(1) Import `main.py` and create an instance. ``` from farmbot_utilities import Farmbot bot = Farmbot() @@ -77,19 +93,77 @@ bot.move(30,40,10) bot.disconnect_broker() ``` +## 🧭 Functions + +### Setup + +`get_token()` generates user authentication token; call before any other function +`connect_broker()` establishes persistent connect to message broker +`disconnect_broker()` disconnects from the message broker +`listen_broker()` displays messages sent to/from message broker + +### Information + +`get_info()` returns information about a specific endpoint +`set_info()` edits information belonging to preexisting endpoint +env() +group() +curve() +read_status() +read_sensor() +safe_z() +garden_size() + +### Messaging + +`log()` sends a new log message via the API +`message()` sends a new log message via the message broker +`debug()` sends a log message of type 'debug' via the message broker +`toast()` sends a log message of type 'toast' via the message broker + +### Basic Commands + +wait() +e_stop() +unlock() +reboot() +shutdown() + +### Movement + +move() +set_home() +find_home() +axis_length() + +### Peripherals + +control_peripheral() +toggle_peripheral() +on() +off() + +### Broker Commands + +calibrate_camera() +control_servo() +take_photo() +soil_height() +detect_weeds() + ## 🧰 Developer Info -### farmbot_API +### api_connect.py Background: https://developer.farm.bot/v15/docs/web-app/rest-api -Formatting: functions in `farmbot_utilities` which interact with the API require an endpoint, which is truncated onto the HTTP request. +Formatting: functions in `api_functions.py` and `main.py` which interact with the API require an endpoint, which is truncated onto the HTTP request. List of endpoints: https://developer.farm.bot/v15/docs/web-app/api-docs > [!CAUTION] > Making requests other than GET to the API will permanently alter the data in your account. DELETE and POST requests may destroy data that cannot be recovered. Altering data through the API may cause account instability. -### farmbot_BROKER +### broker_connect.py Background: https://developer.farm.bot/v15/docs/message-broker -Formatting: functions in `farmbot_utilities` which interact with the message broker send a message containing CelerScript. The messages require the pre-formatted `RPC_request` included in `farmbot_utilities` as the first line of the message. \ No newline at end of file +Formatting: functions in `broker_functions.py` and `main.py` which interact with the message broker send a message containing CeleryScript. The messages require the pre-formatted `RPC_request` included in `broker_functions.py` as the first line of the message. \ No newline at end of file diff --git a/farmbot_api.py b/api_connect.py similarity index 53% rename from farmbot_api.py rename to api_connect.py index 7120934..23cf9d3 100644 --- a/farmbot_api.py +++ b/api_connect.py @@ -1,52 +1,17 @@ -# farmbot_API.py - import sys import json import requests -class FarmbotAPI(): +class ApiConnect(): def __init__(self): self.token = None self.error = None - # API - # ├── token_handling() - # ├── request_handling() - # │ - # ├── get_token() - # ├── check_token() - # │ - # ├── request() - # │ - # ├── get() - # ├── post() - # ├── patch() - # └── delete() - - def token_handling(self, response): - # Handle HTTP status codes - if response.status_code == 200: - return 200 - elif response.status_code == 404: - self.error = "ERROR: The server address does not exist." - elif response.status_code == 422: - self.error = "ERROR: Incorrect email address or password." - else: - self.error = f"ERROR: Unexpected status code {response.status_code}" - - # Handle DNS resolution errors - if response is None: - self.error = "ERROR: There was a problem with the request." - elif isinstance(response, requests.exceptions.ConnectionError): - self.error = "ERROR: The server address does not exist." - elif isinstance(response, requests.exceptions.Timeout): - self.error = "ERROR: The request timed out." - elif isinstance(response, requests.exceptions.RequestException): - self.error = "ERROR: There was a problem with the request." - - return 0 + ## ERROR HANDLING def request_handling(self, response): + """Handle errors relating to bad endpoints and user requests.""" + error_messages = { 404: "The specified endpoint does not exist.", 400: "The specified ID is invalid or you do not have access to it.", @@ -64,26 +29,51 @@ def request_handling(self, response): else: self.error = json.dumps(f"UNEXPECTED ERROR {response.status_code}: {response.text}", indent=2) - return 0 + ## FUNCTIONS def get_token(self, email, password, server): - headers = {'content-type': 'application/json'} - user = {'user': {'email': email, 'password': password}} - response = requests.post(f'{server}/api/tokens', headers=headers, json=user) + """Fetch user authentication token via API.""" + + try: + headers = {'content-type': 'application/json'} + user = {'user': {'email': email, 'password': password}} + response = requests.post(f'{server}/api/tokens', headers=headers, json=user) + # Handle HTTP status codes + if response.status_code == 200: + token_data = response.json() + self.token = token_data + self.error = None + return token_data + elif response.status_code == 404: + self.error = "HTTP ERROR: The server address does not exist." + elif response.status_code == 422: + self.error = "HTTP ERROR: Incorrect email address or password." + else: + self.error = f"HTTP ERROR: Unexpected status code {response.status_code}" + # Handle DNS resolution errors + except requests.exceptions.RequestException as e: + if isinstance(e, requests.exceptions.ConnectionError): + self.error = "DNS ERROR: The server address does not exist." + elif isinstance(e, requests.exceptions.Timeout): + self.error = "DNS ERROR: The request timed out." + elif isinstance(e, requests.exceptions.RequestException): + self.error = "DNS ERROR: There was a problem with the request." + except Exception as e: + self.error = f"DNS ERROR: An unexpected error occurred: {str(e)}" - if self.token_handling(response) == 200: - self.token = response.json() - self.error = None - return self.token - else: - return self.error + self.token = None + return def check_token(self): + """Ensure user authentication token has been generated and persists.""" + if self.token is None: print("ERROR: You have no token, please call `get_token` using your login credentials and the server you wish to connect to.") sys.exit(1) def request(self, method, endpoint, id, payload): + """Send requests from user-accessible functions via API.""" + self.check_token() if id is None: @@ -101,14 +91,20 @@ def request(self, method, endpoint, id, payload): else: return self.error + ## REQUEST METHODS + def get(self, endpoint, id): + """METHOD: 'get' allows user to view endpoint data.""" return self.request('GET', endpoint, id, payload=None) def post(self, endpoint, id, payload): + """METHOD: 'post' allows user to overwrite/create new endpoint data.""" return self.request('POST', endpoint, id, payload) def patch(self, endpoint, id, payload): + """METHOD: 'patch' allows user to edit endpoint data (used for new logs).""" return self.request('PATCH', endpoint, id, payload) def delete(self, endpoint, id): + """METHOD: 'delete' allows user to delete endpoint data (hidden).""" return self.request('DELETE', endpoint, id, payload=None) diff --git a/api_functions.py b/api_functions.py new file mode 100644 index 0000000..cbcc0cb --- /dev/null +++ b/api_functions.py @@ -0,0 +1,94 @@ +from api_connect import ApiConnect + +class ApiFunctions(): + def __init__(self): + self.api_connect = ApiConnect() + + self.echo = True + self.verbose = True + + def __return_config(self, return_value, json_val=False): # TODO: which functions return json() + """Configure echo and verbosity of function returns.""" + + if self.echo is True and self.verbose is True: + print('-' * 100) + if json_val is True: + print(f'FUNCTION: {return_value}\n') + return print(return_value) + else: + print(f'FUNCTION: {return_value}\n') + return print(return_value) + elif self.echo is True and self.verbose is False: + print('-' * 100) + return print(return_value) + elif self.echo is False and self.verbose is False: + return return_value + else: + print('-' * 100) + return print("ERROR: Incompatible return configuration.") + + def get_token(self, email, password, server='https://my.farm.bot'): + token_str = self.api_connect.get_token(email, password, server) + return token_str + + # data = get_info() and like functions will assign 'data' JSON object + # data["name"] will access the field "name" and return the field value + + def get_info(self, endpoint, id=None): + return self.api_connect.get(endpoint, id) + + def set_info(self, endpoint, field, value, id=None): + new_value = { + field: value + } + + self.api_connect.patch(endpoint, id, new_value) + return self.api_connect.get(endpoint, id) + + def env(self, id=None, field=None, new_val=None): # TODO: Fix + if id is None: + data = self.api_connect.get('farmware_envs', id=None) + else: + data = self.api_connect.get('farmware_envs', id) + # return ... + + def log(self, message, type=None, channel=None): + log_message = { + "message": message, + "type": type, # https://software.farm.bot/v15/app/intro/jobs-and-logs#log-types + "channel": channel # Specifying channel does not do anything + } + + endpoint = 'logs' + id = None + + self.api_connect.post(endpoint, id, log_message) + # return ... + + def safe_z(self): + json_data = self.get_info('fbos_config') + return json_data['safe_height'] + + def garden_size(self): + json_data = self.get_info('firmware_config') + + x_steps = json_data['movement_axis_nr_steps_x'] + x_mm = json_data['movement_step_per_mm_x'] + + y_steps = json_data['movement_axis_nr_steps_y'] + y_mm = json_data['movement_step_per_mm_y'] + + length_x = x_steps / x_mm + length_y = y_steps / y_mm + area = length_x * length_y + + return print(f'Garden size:\n' + f'\tx length = {length_x:.2f}\n' + f'\ty length = {length_y:.2f}\n' + f'\tarea = {area:.2f}') + + def group(self, id): # TODO: make ID optional return full tree w/o ID + return self.get_info('point_groups', id) + + def curve(self, id): # TODO: make ID optional return full tree w/o ID + return self.get_info('curves', id) diff --git a/broker_connect.py b/broker_connect.py new file mode 100644 index 0000000..e862fc1 --- /dev/null +++ b/broker_connect.py @@ -0,0 +1,111 @@ +import json +import time + +from datetime import datetime +import paho.mqtt.client as mqtt + +class BrokerConnect(): + def __init__(self): + self.token = None + self.client = None + + self.last_message = None + + ## ERROR HANDLING + + ## FUNCTIONS -- SENDING MESSAGES + + def connect(self): + """Establish persistent connection with message broker.""" + + self.client = mqtt.Client() + self.client.username_pw_set( + username=self.token['token']['unencoded']['bot'], + password=self.token['token']['encoded'] + ) + + self.client.connect( + self.token['token']['unencoded']['mqtt'], + port=1883, + keepalive=60 + ) + + self.client.loop_start() + + def disconnect(self): + """Disconnect from the message broker.""" + + if self.client is not None: + self.client.loop_stop() + self.client.disconnect() + + def publish(self, message): + """Send Celery Script messages via message broker.""" + + if self.client is None: + self.connect() + + self.client.publish( + f'bot/{self.token["token"]["unencoded"]["bot"]}/from_clients', + payload=json.dumps(message) + ) + + ## FUNCTIONS -- RECEIVING MESSAGES + + def on_connect(self, _client, _userdata, _flags, _rc, channel): + """Subscribe to specified broker response channel.""" + self.client.subscribe(f"bot/{self.token['token']['unencoded']['bot']}/{channel}") + + def on_message(self, _client, _userdata, msg): + """Update message queue with latest broker response.""" + + new_message = json.loads(msg.payload) + self.last_message = new_message + + def listen(self, duration, channel): + """Listen to messages via message broker.""" + + if self.client is None: + self.connect() + + # Wrap on_connect to pass channel argument + self.client.on_connect = lambda client, userdata, flags, rc: self.on_connect(client, userdata, flags, rc, channel) + self.client.on_message = self.on_message + + self.client.loop_start() + + # Listen to messages for duration (seconds) + time.sleep(duration) + + self.client.loop_stop() + self.client.disconnect() + + ## FUNCTIONS -- HIDDEN + + def hidden_on_connect(self, _client, _userdata, _flags, _rc): + # Subscribe to all channels + self.client.subscribe(f"bot/{self.token['token']['unencoded']['bot']}/#") + + def hidden_on_message(self, _client, _userdata, msg): + # print channel + print('-' * 100) + print(f'{msg.topic} ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})\n') + # print message + print(json.dumps(json.loads(msg.payload), indent=4)) + + def hidden_listen(self): + if self.client is None: + self.connect() + + self.client.on_connect = self.hidden_on_connect + self.client.on_message = self.hidden_on_message + + # Start loop in a separate thread + self.client.loop_start() + + # Sleep for five seconds to listen for messages + time.sleep(60) + + # Stop loop and disconnect after five seconds + self.client.loop_stop() + self.client.disconnect() diff --git a/broker_functions.py b/broker_functions.py new file mode 100644 index 0000000..13df73d --- /dev/null +++ b/broker_functions.py @@ -0,0 +1,647 @@ +import json + +from broker_connect import BrokerConnect +from api_functions import ApiFunctions + +RPC_REQUEST = { + "kind": "rpc_request", + "args": { + "label": "" + } +} + +class BrokerFunctions(): + def __init__(self): + self.broker_connect = BrokerConnect() + self.api = ApiFunctions() + + self.client = None + + self.echo = True + self.verbose = True + + ## INFORMATION + + def read_status(self): + """Get Farmbot device status tree via message broker.""" + + status_message = { + **RPC_REQUEST, + "body": { + "kind": "read_status", + "args": {} + } + } + + self.broker_connect.publish(status_message) + self.broker_connect.listen(5, 'status') + + status_tree = self.broker_connect.last_message + + return status_tree + + def read_sensor(self, id): + """Get sensor data via message broker.""" + + peripheral_str = self.api.get_info('peripherals', id) + mode = peripheral_str['mode'] + + read_sensor_message = { + **RPC_REQUEST, + "body": [{ + "kind": "read_pin", + "args": { + "pin_mode": mode, + "label": '---', + "pin_number": { + "kind": "named_pin", + "args": { + "pin_type": "Peripheral", + "pin_id": id + } + } + } + }] + } + + # return ... + + ## MESSAGING + + def message(self, message, type=None, channel=None): + """Send log message via message broker.""" + + message_message = { + **RPC_REQUEST, + "body": { + "kind": "send_message", + "args": { + "message": message, + "message_type": type + }, + "body": { + "kind": "channel", + "args": { + "channel_name": channel + } + } + } + } + + self.broker_connect.publish(message_message) + # return ... + + def debug(self, message): + """Send 'debug' type log message via message broker.""" + self.message(message, 'debug') + # return ... + + def toast(self, message): + """Send 'toast' type log message via message broker.""" + self.message(message, 'toast') + # return ... + + ## BASIC COMMANDS + + def wait(self, duration): + """Send wait command to device via message broker.""" + + wait_message = { + **RPC_REQUEST, + "body": { + "kind": "wait", + "args": { + "milliseconds": duration + } + } + } + + self.broker_connect.publish(wait_message) + return print("Waiting for "+str(duration)+" milliseconds...") + + def e_stop(self): + """Send emergency stop command to device via message broker.""" + + e_stop_message = { + **RPC_REQUEST, + "body": { + "kind": "emergency_lock", + "args": {} + } + } + + self.broker_connect.publish(e_stop_message) + return print("Triggered device emergency stop.") + + def unlock(self): + """Send unlock command to device via message broker.""" + + unlock_message = { + **RPC_REQUEST, + "body": { + "kind": "emergency_unlock", + "args": {} + } + } + + self.broker_connect.publish(unlock_message) + return print("Triggered device unlock.") + + def reboot(self): + """Send reboot command to device via message broker.""" + + reboot_message = { + **RPC_REQUEST, + "body": { + "kind": "reboot", + "args": { + "package": "farmbot_os" + } + } + } + + self.broker_connect.publish(reboot_message) + return print("Triggered device reboot.") + + def shutdown(self): + """Send shutdown command to device via message broker.""" + + shutdown_message = { + **RPC_REQUEST, + "body": { + "kind": "power_off", + "args": {} + } + } + + self.broker_connect.publish(shutdown_message) + return print("Triggered device shutdown.") + + ## MOVEMENT + + def move(self, x, y, z): # TODO: update for coord(x,y,z) + """Move to new x, y, z position via message broker.""" + def axis_overwrite(axis, value): + return { + "kind": "axis_overwrite", + "args": { + "axis": axis, + "axis_operand": { + "kind": "numeric", + "args": { + "number": value + } + } + } + } + + move_message = { + **RPC_REQUEST, + "body": { + "kind": "move", + "args": {}, + "body": [ + axis_overwrite("x", x), + axis_overwrite("y", y), + axis_overwrite("z", z) + ] + } + } + + self.broker_connect.publish(move_message) + # return ... + + def set_home(self, axis='all'): + set_home_message = { + **RPC_REQUEST, + "body": { + "kind": "zero", + "args": { + "axis": axis + } + } + } + + self.broker_connect.publish(set_home_message) + # return ... + + def find_home(self, axis='all', speed=100): + if speed > 100 or speed < 1: + return print("ERROR: Speed constrained to 1-100.") + else: + find_home_message = { + **RPC_REQUEST, + "body": { + "kind": "find_home", + "args": { + "axis": axis, + "speed": speed + } + } + } + + self.broker_connect.publish(find_home_message) + # return ... + + def axis_length(self, axis='all'): + axis_length_message = { + **RPC_REQUEST, + "body": { + "kind": "calibrate", + "args": { + "axis": axis + } + } + } + + self.broker_connect.publish(axis_length_message) + # return ... + + def get_xyz(self): # TODO: update for coord(x,y,z) + """Get current x, y, z coordinate of device via message broker.""" + + tree_data = self.read_status() + + position = tree_data["position"] + + x_val = position['x'] + y_val = position['y'] + z_val = position['z'] + + return {'x': x_val, 'y': y_val, 'z': z_val} + + def check_position(self, user_x, user_y, user_z, tolerance): # TODO: update for coord(x,y,z) + + user_values = [user_x, user_y, user_z] + + position_data = self.get_xyz() + actual_vals = [position_data['x'], position_data['y'], position_data['z']] + + for user_value, actual_value in zip(user_values, actual_vals): + if actual_value - tolerance <= user_value <= actual_value + tolerance: + print("Farmbot is at position.") + else: + print("Farmbot is NOT at position.") + + ## PERIPHERALS + + def control_peripheral(self, id, value, mode=None): + if mode is None: + peripheral_str = self.api.get_info('peripherals', id) + mode = peripheral_str['mode'] + + control_peripheral_message = { + **RPC_REQUEST, + "body": { + "kind": "write_pin", + "args": { + "pin_value": value, # Controls ON/OFF or slider value from 0-255 + "pin_mode": mode, # Controls digital (0) or analog (1) mode + "pin_number": { + "kind": "named_pin", + "args": { + "pin_type": "Peripheral", + "pin_id": id + } + } + } + } + } + + self.broker_connect.publish(control_peripheral_message) + # return ... + + def toggle_peripheral(self, id): + """Toggle peripheral off/on via message broker.""" + + toggle_peripheral_message = { + **RPC_REQUEST, + "body": [{ + "kind": "toggle_pin", + "args": { + "pin_number": { + "kind": "named_pin", + "args": { + "pin_type": "Peripheral", + "pin_id": id + } + } + } + }] + } + + self.broker_connect.publish(toggle_peripheral_message) + # return ... + + def on(self, id): + """Toggle peripheral ON via message broker.""" + + peripheral_str = self.api.get_info('peripherals', id) + mode = peripheral_str['mode'] + + if mode == 1: + self.control_peripheral(id, 255) + elif mode == 0: + self.control_peripheral(id, 1) + + # return ... + + def off(self, id): + """Toggle peripheral OFF via message broker.""" + + self.control_peripheral(id, 0) + # return ... + + ## RESOURCES + + # TODO: sort(points, method) --> API? --> order group of points according to chosen method + + # def sort(tray, tray_cell): + # cell = tray_cell.upper() + # seeder_needle_offset = 17.5 + # cell_spacing = 12.5 + + # cells = { + # "A1": {"label": "A1", "x": 0, "y": 0}, + # "A2": {"label": "A2", "x": 0, "y": 1}, + # "A3": {"label": "A3", "x": 0, "y": 2}, + # "A4": {"label": "A4", "x": 0, "y": 3}, + # "B1": {"label": "B1", "x": -1, "y": 0}, + # "B2": {"label": "B2", "x": -1, "y": 1}, + # "B3": {"label": "B3", "x": -1, "y": 2}, + # "B4": {"label": "B4", "x": -1, "y": 3}, + # "C1": {"label": "C1", "x": -2, "y": 0}, + # "C2": {"label": "C2", "x": -2, "y": 1}, + # "C3": {"label": "C3", "x": -2, "y": 2}, + # "C4": {"label": "C4", "x": -2, "y": 3}, + # "D1": {"label": "D1", "x": -3, "y": 0}, + # "D2": {"label": "D2", "x": -3, "y": 1}, + # "D3": {"label": "D3", "x": -3, "y": 2}, + # "D4": {"label": "D4", "x": -3, "y": 3} + # } + + # # Checks + # if tray["pointer_type"] != "ToolSlot": + # print("Error: Seed Tray variable must be a seed tray in a slot") + # return + # elif cell not in cells: + # print("Error: Seed Tray Cell must be one of **A1** through **D4**") + # return + + # # Flip X offsets depending on pullout direction + # flip = 1 + # if tray["pullout_direction"] == 1: + # flip = 1 + # elif tray["pullout_direction"] == 2: + # flip = -1 + # else: + # print("Error: Seed Tray **SLOT DIRECTION** must be `Positive X` or `Negative X`") + # return + + # # A1 coordinates + # A1 = { + # "x": tray["x"] - seeder_needle_offset + (1.5 * cell_spacing * flip), + # "y": tray["y"] - (1.5 * cell_spacing * flip), + # "z": tray["z"] + # } + + # # Cell offset from A1 + # offset = { + # "x": cell_spacing * cells[cell]["x"] * flip, + # "y": cell_spacing * cells[cell]["y"] * flip + # } + + # # Return cell coordinates + # return { + # "x": A1["x"] + offset["x"], + # "y": A1["y"] + offset["y"], + # "z": A1["z"] + # } + + def soil_height(self): + """Execute script to check soil height via message broker.""" + + soil_height_message = { + **RPC_REQUEST, + "body": { + "kind": "execute_script", + "args": { + "label": "Measure Soil Height" + } + } + } + + self.broker_connect.publish(soil_height_message) + # return ... + + def detect_weeds(self): + """Execute script to detect weeds via message broker.""" + + detect_weeds_message = { + **RPC_REQUEST, + "body": { + "kind": "execute_script", + "args": { + "label": "plant-detection" + } + } + } + + self.broker_connect.publish(detect_weeds_message) + # return ... + + ## OTHER FUNCTIONS + + def calibrate_camera(self): # TODO: fix "sequence_id" + calibrate_message = { + **RPC_REQUEST, + "body": { + "kind": "execute_script", + "args": { + "label": "camera-calibration" + }, + "body": { + "kind": "pair", + "args": { + "label": "CAMERA_CALIBRATION_easy_calibration", + "value": "\"TRUE\"" + } + } + } + } + + self.broker_connect.publish(calibrate_message) + # return ... + + def photo_grid(self): # TODO: fix "sequence_id" + photo_grid_message = { + **RPC_REQUEST, + "body": { + "kind": "execute", + "args": { + "sequence_id": 24372 + } + } + } + + self.broker_connect.publish(photo_grid_message) + # return ... + + def control_servo(self, pin, angle): + if angle < 0 or angle > 180: + return print("ERROR: Servo angle constrained to 0-180 degrees.") + else: + control_servo_message = { + **RPC_REQUEST, + "body": { + "kind": "set_servo_angle", + "args": { + "pin_number": pin, + "pin_value": angle # From 0 to 180 + } + } + } + + self.broker_connect.publish(control_servo_message) + # return ... + + def take_photo(self): + """Send photo command to camera via message broker.""" + + take_photo_message = { + **RPC_REQUEST, + "body": { + "kind": "take_photo", + "args": {} + } + } + + self.broker_connect.publish(take_photo_message) + # return ... + + def mark_coord(self, x, y, z, property, mark_as): # TODO: Fix "label" + mark_coord_message = { + **RPC_REQUEST, + "body": { + "kind": "update_resource", + "args": { + "resource": { + "kind": "identifier", + "args": { + "label": "test_location" # What is happening here?? + } + } + }, + "body": { + "kind": "pair", + "args": { + "label": property, + "value": mark_as + } + } + } + } + + # return ... + + # TODO: verify_tool() --> get broker message example + # TODO: mount_tool() --> get broker message example + # TODO: dismount_tool() --> get broker message example + + # def mount_tool(self, x, y, z): + # mount_tool_message = { + # **RPC_REQUEST, + # "body": { + # "kind": "execute", + # "body": { + # "kind": "parameter_application", + # "args": { + # "label": "Tool", + # "data_value": { + # "kind": "coordinate", + # "args": { + # "x": x, + # "y": y, + # "z": z + # } + # } + # } + # } + # } + # } + + # self.broker_connect.publish(mount_tool_message) + # # return ... + + # TODO: water() --> all or single coords + # TODO: dispense() --> single coords? + + # TODO: sequence() --> execute a sequence by ID + # TODO: get_seed_tray_call(tray, cell) --> get coordinates of cell in seed tray by passing tool object and cell id, eg B3 + + # TODO: get_job() --> access status tree --> fetch all or single by name + # TODO: set_job() --> access status tree --> inject(?) new or edit single by name + # TODO: complete_job() --> access status tree --> edit single by name + + def lua(self, code_snippet): # TODO: verify working + lua_message = { + **RPC_REQUEST, + "body": { + "kind": "lua", + "args": { + "lua": code_snippet + } + } + } + + self.broker_connect.publish(lua_message) + # return ... + + def if_statement(self, variable, operator, value, then_id, else_id): # TODO: add 'do nothing' functionality + if_statement_message = { + **RPC_REQUEST, + "body": { + "kind": "_if", + "args": { + "lhs": variable, + "op": operator, + "rhs": value, + "_then": { + "kind": "execute", + "args": { + "sequence_id": then_id + } + }, + "_else": { + "kind": "execute", + "args": { + "sequence_id": else_id + } + } + } + } + } + + self.broker_connect.publish(if_statement_message) + # return ... + + def assertion(self, code, as_type, id=''): # TODO: add 'continue' functionality + assertion_message = { + **RPC_REQUEST, + "body": { + "kind": "assertion", + "args": { + "lua": code, + "_then": { + "kind": "execute", + "args": { + "sequence_id": id # Recovery sequence ID + } + }, + "assertion_type": as_type # If test fails, do this + } + } + } + + self.broker_connect.publish(assertion_message) + # return ... diff --git a/farmbot_broker.py b/farmbot_broker.py deleted file mode 100644 index 69b8537..0000000 --- a/farmbot_broker.py +++ /dev/null @@ -1,52 +0,0 @@ -# farmbot_BROKER.py - -import sys -import json - -import paho.mqtt.client as mqtt - -from farmbot_api import FarmbotAPI - -class FarmbotBroker(): - def __init__(self): - self.api = FarmbotAPI() - - self.token = None - self.client = None - - # BROKER - # ├── connect() - # ├── disconnect() - # │ - # └── publish() - - def connect(self): - self.api.check_token() - - self.client = mqtt.Client() - self.client.username_pw_set( - username=self.token['token']['unencoded']['bot'], - password=self.token['token']['encoded'] - ) - - self.client.connect( - self.token['token']['unencoded']['mqtt'], - port=1883, - keepalive=60 - ) - - self.client.loop_start() - - def disconnect(self): - if self.client is not None: - self.client.loop_stop() - self.client.disconnect() - - def publish(self, message): - if self.client is None: - self.connect() - - self.client.publish( - f'bot/{self.token["token"]["unencoded"]["bot"]}/from_clients', - payload=json.dumps(message) - ) diff --git a/farmbot_util_PORT.py b/farmbot_util_PORT.py deleted file mode 100644 index 378934b..0000000 --- a/farmbot_util_PORT.py +++ /dev/null @@ -1,453 +0,0 @@ -# farmbot_utilities.py - -import json - -from farmbot_broker import FarmbotBroker -from farmbot_api import FarmbotAPI - -RPC_REQUEST = { - "kind": "rpc_request", - "args": { - "label": "" - } -} - -class Farmbot(): - def __init__(self): - self.broker = FarmbotBroker() - self.api = FarmbotAPI() - - self.token = None - self.error = None - - self.echo = True # Choose whether functions print return statement - self.verbose = True # Choose how much detail in return statement - - def function_return(self, return_value): - if self.echo is True: - return print(return_value) - else: - return return_value - - # CONNECTIVITY - # ├── [✅] get_token() - # │ - # ├── [✅] connect_broker() - # └── [✅] disconnect_broker() - - def get_token(self, email, password, server='https://my.farm.bot'): - token_str = self.api.get_token(email, password, server) - - self.token = self.api.token - self.error = self.api.error - - self.broker.token = self.token - - return self.function_return(token_str) - - def connect_broker(self): - self.broker.connect() - # return ... - - def disconnect_broker(self): - self.broker.disconnect() - return self.function_return("Disconnected from message broker.") - - # INFORMATION - # ├── [✅] get_info() - # ├── [✅] set_info() - # │ - # ├── [✅] read_status() - # ├── [🚫] read_sensor() - # │ - # └── [🚫] env() - - def get_info(self, endpoint, id=None): - return self.function_return(self.api.get(endpoint, id)) - # return self.api.get(endpoint, id)... - - def set_info(self, endpoint, field, value, id=None): - new_value = { - field: value - } - - self.api.patch(endpoint, id, new_value) - return self.function_return(self.api.get(endpoint, id)) - # return self.api.get(endpoint, id)... - - def read_status(self): - status_message = { - **RPC_REQUEST, - "body": { - "kind": "read_status", - "args": {} - } - } - - self.broker.publish(status_message) - # return ... - - # MESSAGES - # ├── [✅] log() - # ├── [✅] message() - # │ - # ├── [✅] debug() - # └── [✅] toast() - - def log(self, message, type=None, channel=None): - log_message = { - "message": message, - "type": type, - "channel": channel # Specifying channel does not do anything - } - - endpoint = 'logs' - id = None - - self.api.post(endpoint, id, log_message) - # return ... - - def message(self, message, type=None, channel=None): - message_message = { - **RPC_REQUEST, - "body": { - "kind": "send_message", - "args": { - "message": message, - "message_type": type - }, - "body": { - "kind": "channel", - "args": { - "channel_name": channel - } - } - } - } - - self.broker.publish(message_message) - # return ... - - def debug(self, message): - self.message(message, 'debug') - # return ... - - def toast(self, message): - self.message(message, 'toast') - # return ... - - # LOGIC - # ├── [✅] wait() - # ├── [✅] e_stop() - # ├── [✅] unlock() - # ├── [✅] reboot() - # └── [✅] shutdown() - - def wait(self, time): - wait_message = { - **RPC_REQUEST, - "body": { - "kind": "wait", - "args": { - "milliseconds": time - } - } - } - - self.broker.publish(wait_message) - return self.function_return("Waiting for "+str(TIME)+" milliseconds...") - - def e_stop(self): - e_stop_message = { - **RPC_REQUEST, - "body": { - "kind": "emergency_lock", - "args": {} - } - } - - self.broker.publish(e_stop_message) - return self.function_return("Triggered device emergency stop.") - - def unlock(self): - unlock_message = { - **RPC_REQUEST, - "body": { - "kind": "emergency_unlock", - "args": {} - } - } - - self.broker.publish(unlock_message) - return self.function_return("Triggered device unlock.") - - def reboot(self): - reboot_message = { - **RPC_REQUEST, - "body": { - "kind": "reboot", - "args": { - "package": "farmbot_os" - } - } - } - - self.broker.publish(reboot_message) - return self.function_return("Triggered device reboot.") - - def shutdown(self): - shutdown_message = { - **RPC_REQUEST, - "body": { - "kind": "power_off", - "args": {} - } - } - - self.broker.publish(shutdown_message) - return self.function_return("Triggered device shutdown.") - - # PERIPHERALS - # ├── [✅] control_servo() - # ├── [🚫] control_peripheral() - # ├── [🚫] toggle_peripheral() - # │ - # ├── [🚫] on() - # ├── [🚫] off() - # │ - # ├── [🚫] calibrate_camera() - # ├── [✅] take_photo() - # ├── [🚫] photo_grid() - # │ - # ├── [✅] soil_height() - # └── [✅] detect_weeds() - - def control_servo(self, pin, angle): - if angle < 0 or angle > 180: - return print("ERROR: Servo angle constrained to 0-180 degrees.") - else: - control_servo_message = { - **RPC_REQUEST, - "body": { - "kind": "set_servo_angle", - "args": { - "pin_number": pin, - "pin_value": angle # From 0 to 180 - } - } - } - - self.broker.publish(control_servo_message) - # return ... - - def take_photo(self): - take_photo_message = { - **RPC_REQUEST, - "body": { - "kind": "take_photo", - "args": {} - } - } - - self.broker.publish(take_photo_message) - # return ... - - def soil_height(self): - soil_height_message = { - **RPC_REQUEST, - "body": { - "kind": "execute_script", - "args": { - "label": "Measure Soil Height" - } - } - } - - self.broker.publish(soil_height_message) - # return ... - - def detect_weeds(self): - detect_weeds_message = { - **RPC_REQUEST, - "body": { - "kind": "execute_script", - "args": { - "label": "plant-detection" - } - } - } - - self.broker.publish(detect_weeds_message) - # return ... - - # MOVEMENT - # ├── [✅] move() - # ├── [🚫] get_xyz() - # ├── [🚫] check_position() - # │ - # ├── [✅] set_home() - # ├── [✅] find_home() - # │ - # ├── [✅] axis_length() - # ├── [✅] safe_z() - # │ - # └── [✅] garden_size() - - def move(self, x, y, z): - def axis_overwrite(axis, value): - return { - "kind": "axis_overwrite", - "args": { - "axis": axis, - "axis_operand": { - "kind": "numeric", - "args": { - "number": value - } - } - } - } - - move_message = { - **RPC_REQUEST, - "body": { - "kind": "move", - "args": {}, - "body": [ - axis_overwrite("x", x), - axis_overwrite("y", y), - axis_overwrite("z", z) - ] - } - } - - self.broker.publish(move_message) - # return ... - - def set_home(self, axis='all'): - set_home_message = { - **RPC_REQUEST, - "body": { - "kind": "zero", - "args": { - "axis": axis - } - } - } - - self.broker.publish(set_home_message) - # return ... - - def find_home(self, axis='all', speed=100): - if speed > 100 or speed < 1: - return print("ERROR: Speed constrained to 1-100.") - else: - find_home_message = { - **RPC_REQUEST, - "body": { - "kind": "find_home", - "args": { - "axis": axis, - "speed": speed - } - } - } - - self.broker.publish(find_home_message) - # return ... - - def axis_length(self, axis='all'): - axis_length_message = { - **RPC_REQUEST, - "body": { - "kind": "calibrate", - "args": { - "axis": axis - } - } - } - - self.broker.publish(axis_length_message) - # return ... - - def safe_z(self): - json_data = self.get_info('fbos_config') - return self.function_return(json_data['safe_height']) - # return json_data['safe_height']... - - def garden_size(self): - json_data = self.get_info('firmware_config') - - # Get x axis length in steps - x_steps = json_data['movement_axis_nr_steps_x'] - - # Get number of steps per millimeter on the x axis - x_mm = json_data['movement_step_per_mm_x'] - - # Get y axis length in steps - y_steps = json_data['movement_axis_nr_steps_y'] - - # Get number of steps per millimeter on the y axis - y_mm = json_data['movement_step_per_mm_y'] - - length_x = x_steps / x_mm - length_y = y_steps / y_mm - area = length_x * length_y - - size_value = {'x': length_x, 'y': length_y, 'area': area} - return self.function_return(size_value) - - # COMMANDS - # ├── [🚫] mark_as() - # │ - # ├── [✅] group() - # ├── [✅] curve() - # │ - # ├── [🚫] verify_tool() - # ├── [🚫] mount_tool() - # ├── [🚫] dismount_tool() - # │ - # ├── [🚫] water() - # ├── [🚫] dispense() - # ├── [🚫] sequence() - # │ - # ├── [🚫] get_seed_tray_call(tray, cell) - # ├── [🚫] sort(points, method) - # │ - # ├── [🚫] get_job() - # ├── [🚫] set_job() - # └── [🚫] complete_job() - - def group(self, id): - return self.function_return(self.get_info('point_groups', id)) - # return self.get_info('point_groups', id)... - - def curve(self, id): - return self.function_return(self.get_info('curves', id)) - # return self.get_info('curves', id)... - - # COMPLEX - # ├── [✅] assertion() - # ├── [🚫] if_statement() - # └── [🚫] lua() - - def assertion(self, code, type, id=''): - assertion_message = { - **RPC_REQUEST, - "body": { - "kind": "assertion", - "args": { - "lua": code, - "_then": { - "kind": "execute", - "args": { - "sequence_id": id # Recovery sequence ID - } - }, - "assertion_type": type # If test fails, do this - } - } - } - - self.broker.publish(assertion_message) - # return ... diff --git a/farmbot_utilities.py b/farmbot_utilities.py deleted file mode 100644 index 6104c70..0000000 --- a/farmbot_utilities.py +++ /dev/null @@ -1,563 +0,0 @@ -# farmbot_utilities.py - -from farmbot_broker import FarmbotBroker -from farmbot_api import FarmbotAPI - -RPC_REQUEST = { - "kind": "rpc_request", - "args": { - "label": "" - } -} - -class Farmbot(): - def __init__(self): - self.token = None - self.error = None - self.broker = FarmbotBroker() - self.api = FarmbotAPI() - - def get_token(self, EMAIL, PASSWORD, SERVER='https://my.farm.bot'): - token_string = self.api.get_token(EMAIL, PASSWORD, SERVER) - - self.token = self.api.token - self.error = self.api.error - self.broker.token = self.token - - return token_string - - # bot.connect_broker() - - def connect_broker(self): - self.broker.connect() - # return print("Connected to message broker.")? - - # bot.disconnect_broker() - - def disconnect_broker(self): - self.broker.disconnect() - # return print("Disconnected from message broker.")? - - # bot.get_info('device') - # bot.get_info('peripherals', '21240') - # bot.get_info('peripherals', '21240', 'label') - - def get_info(self, ENDPOINT, ID=None, FIELD=None): - return self.api.get(ENDPOINT, ID, FIELD) - - # bot.set_info('device', 'name', 'Carrot Commander') - # bot.set_info('peripherals', '21240', 'label', 'Lights') - - def set_info(self, ENDPOINT, FIELD, VALUE, ID=None): - new_value = { - FIELD: VALUE - } - - self.api.patch(ENDPOINT, ID, new_value) - return self.api.get(ENDPOINT, ID, FIELD) - - # bot.new_log('👋 Hello world!') - # bot.send_message('🚨 This is a warning...', 'warning', 'toast') - - def new_log(self, MESSAGE, TYPE='success', CHANNEL='toast'): - ENDPOINT='logs' - ID='' - - new_log_message = { - "message": MESSAGE, - "type": TYPE, - "channel": CHANNEL, # Specifying channel does not do anything - } - - self.api.post(ENDPOINT, ID, new_log_message) - - # bot.send_message('👋 Hello world!') - # bot.send_message('🚨 This is a warning...', 'warning', 'toast') - - def send_message(self, MESSAGE, TYPE='success', CHANNEL='toast'): - send_message_message = { - **RPC_REQUEST, - "body": [{ - "kind": "send_message", - "args": { - "message": MESSAGE, - "message_type": TYPE - }, - "body": [{ - "kind": "channel", - "args": { - "channel_name": CHANNEL - } - }] - }] - } - - self.broker.publish(send_message_message) - - # bot.move(x,y,z) - - def move(self, X, Y, Z): - def axis_overwrite(AXIS, VALUE): - return { - "kind": "axis_overwrite", - "args": { - "axis": AXIS, - "axis_operand": { - "kind": "numeric", - "args": { - "number": VALUE - } - } - } - } - - move_message = { - **RPC_REQUEST, - "body": [{ - "kind": "move", - "args": {}, - "body": [ - axis_overwrite("x", X), - axis_overwrite("y", Y), - axis_overwrite("z", Z) - ] - }] - } - - self.broker.publish(move_message) - - # bot.set_home() - # bot.set_home('x') - - def set_home(self, AXIS='all'): - set_home_message = { - **RPC_REQUEST, - "body": [{ - "kind": "zero", - "args": { - "axis": AXIS - } - }] - } - - self.broker.publish(set_home_message) - - # bot.find_home() - # bot.find_home('x') - # bot.find_home('x',50) with max speed? - - def find_home(self, AXIS='all', SPEED=100): - find_home_message = { - **RPC_REQUEST, - "body": [{ - "kind": "find_home", - "args": { - "axis": AXIS, - "speed": SPEED - } - }] - } - - self.broker.publish(find_home_message) - - # bot.find_axis_length() - # bot.find_axis_length('x') - - def find_axis_length(self, AXIS='all'): - find_axis_length_message = { - **RPC_REQUEST, - "body": [{ - "kind": "calibrate", - "args": { - "axis": AXIS - } - }] - } - - self.broker.publish(find_axis_length_message) - - def control_servo(self, PIN, ANGLE): - if ANGLE < 0 or ANGLE > 180: - return print("Servo angle constrained to 0-180 degrees.") - else: - control_servo_message = { - **RPC_REQUEST, - "body": [{ - "kind": "set_servo_angle", - "args": { - "pin_number": PIN, - "pin_value": ANGLE # From 0 to 180 - } - }] - } - - self.broker.publish(control_servo_message) - - # bot.control_peripheral('21240', 1) - # bot.control_peripheral('21240', 0, digital) - - def control_peripheral(self, ID, VALUE, MODE=None): - if MODE == None: - MODE = self.get_info('peripherals', ID, 'mode') - else: - MODE = MODE - - # VALUE = ON/OFF where ON = 1 and OFF = 0 - # MODE = DIGITAL/ANALOG where DIGITAL = 0 (ON/OFF) and ANALOG = 1 (RANGE) - - # Changes for pin/not pin and LEDs change pin_type --> BoxLedx... - control_peripheral_message = { - **RPC_REQUEST, - "body": [{ - "kind": "write_pin", - "args": { - "pin_value": VALUE, # Controls ON/OFF or slider value from 0-255 - "pin_mode": MODE, # Controls digital or analog mode - "pin_number": { - "kind": "named_pin", - "args": { - "pin_type": "Peripheral", - "pin_id": ID - } - } - } - }] - } - - self.broker.publish(control_peripheral_message) - - # bot.toggle('21240') - - def toggle_peripheral(self, ID): - toggle_peripheral_message = { - **RPC_REQUEST, - "body": [{ - "kind": "toggle_pin", - "args": { - "pin_number": { - "kind": "named_pin", - "args": { - "pin_type": "Peripheral", - "pin_id": ID - } - } - } - }] - } - - self.broker.publish(toggle_peripheral_message) - - # bot.read_sensor('21240') - - def read_sensor(self, ID, MODE, LABEL='---'): - read_sensor_message = { - **RPC_REQUEST, - "body": [{ - "kind": "read_pin", - "args": { - "pin_mode": MODE, - "label": LABEL, - "pin_number": { - "kind": "named_pin", - "args": { - "pin_type": "Peripheral", - "pin_id": ID - } - } - } - }] - } - - self.broker.publish(read_sensor_message) - # "Read" sensor... what gets "read"/output? - - # bot.take_photo() - - def take_photo(self): - take_photo_message = { - **RPC_REQUEST, - "body": [{ - "kind": "take_photo", - "args": {} - }] - } - - self.broker.publish(take_photo_message) - - # bot.detect_weeds() - - def detect_weeds(self): - detect_weeds_message = { - **RPC_REQUEST, - "body": [{ - "kind": "execute_script", - "args": { - "label": "plant-detection" - } - }] - } - - self.broker.publish(detect_weeds_message) - # returns what? array of weeds? - - # bot.soil_height() - - def soil_height(self): - soil_height_message = { - **RPC_REQUEST, - "body": [{ - "kind": "execute_script", - "args": { - "label": "Measure Soil Height" - } - }] - } - - self.broker.publish(soil_height_message) - # returns what? soil height value(s)? - - # bot.wait(30000) - - def wait(self, TIME): - wait_message = { - **RPC_REQUEST, - "body": [{ - "kind": "wait", - "args": { - "milliseconds": TIME - } - }] - } - - self.broker.publish(wait_message) - return print("Waiting for "+str(TIME)+" milliseconds...") - - def if_statement(self, VARIABLE, ID, OPERATOR, VALUE, THEN_ID, ELSE_ID): - # Positions - # peripherals - # pins - if VARIABLE == 'position': - define_args = { - "lhs": ID - } - elif VARIABLE == 'peripheral': - define_args = { - "lhs": { - "kind": "named_pin", - "args": { - "pin_type": "Peripheral", - "pin_id": ID - } - } - } - else: - return print("The specified variable does not exist...") - - if_statement_message = { - **RPC_REQUEST, - "body": [{ - "kind": "_if", - "args": { - **define_args, - "op": OPERATOR, - "rhs": VALUE, - "_then": { - "kind": "execute", - "args": { - "sequence_id": THEN_ID - } - }, - "_else": { - "kind": "execute", - "args": { - "sequence_id": ELSE_ID - } - } - } - }] - } - - self.broker.publish(if_statement_message) - - # bot.e_stop() - - def e_stop(self): - e_stop_message = { - **RPC_REQUEST, - "body": [{ - "kind": "emergency_lock", - "args": {} - }] - } - - self.broker.publish(e_stop_message) - # return print("device emergency stop")? - - # bot.unlock() - - def unlock(self): - unlock_message = { - **RPC_REQUEST, - "body": [{ - "kind": "emergency_unlock", - "args": {} - }] - } - - self.broker.publish(unlock_message) - # return print("device unlocked")? - - # bot.reboot() - - def reboot(self): - reboot_message = { - **RPC_REQUEST, - "body": [{ - "kind": "reboot", - "args": { - "package": "farmbot_os" - } - }] - } - - self.broker.publish(reboot_message) - # return "device rebooting..."? - # return "device successfully rebooted"? - - # bot.shutdown() - - def shutdown(self): - shutdown_message = { - **RPC_REQUEST, - "body": [{ - "kind": "power_off", - "args": {} - }] - } - - self.broker.publish(shutdown_message) - - def assertion(self, CODE, TYPE, ID=''): - assertion_message = { - **RPC_REQUEST, - "body": [{ - "kind": "assertion", - "args": { - "lua": CODE, - "_then": { - "kind": "execute", - "args": { - "sequence_id": ID # Recovery sequence ID - } - }, - "assertion_type": TYPE # If test fails, do this - } - }] - } - - # def lua(self, CODE): - # lua_message = { - # **RPC_REQUEST, - # "body": [{ - # "kind": "lua", - # "args": { - # "lua": {CODE} - # } - # }] - # } - - # self.broker.publish(lua_message) - - # env(key, value?) - # env(DO, KEY, VALUE=None, CHANGE=None) - # Gets and sets api/farmware_envs - # Basically a shortcut to get/set_info(farmware_envs, ID) - - # garden_size() - # Shortcut to get ALL axis lenghths - # Calculated from movement_axis_nr_steps_x and movement_step_per_mm_x from api/firmware_config - def garden_size(self): - x_steps = self.get_info('firmware_config', 'movement_axis_nr_steps_x') - x_mm = self.get_info('firmware_config', 'movement_step_per_mm_x') - - y_steps = self.get_info('firmware_config', 'movement_axis_nr_steps_y') - y_mm = self.get_info('firmware_config', 'movement_step_per_mm_y') - - length_x = (x_steps / x_mm) - length_y = (y_steps / y_mm) - - area = (length_x * length_y) - - return {'x': length_x, 'y': length_y, 'area': area} - - # group(ID) = shortcut to get_info(point_groups, ID) - def group(self, ID): - return self.get_info('point_groups', ID) - - # safe_z() = shortcut to safe_height value from api/fbos_config - def safe_z(self): - return self.get_info('fbos_config', 'safe_height') - - # curve(ID) = shortcut to get_info(curves, ID) - def curve(self, ID): - return self.get_info('curves', ID) - - # set_job() - # get_job() - # complete_job() - - # debug() = shortcut for specific type of send_message - def debug(self, MESSAGE): - self.send_message(MESSAGE, 'debug') - - # toast() = shortcut for specific type of send_message - def toast(self, MESSAGE): - self.send_message(MESSAGE, 'toast') - - # go_to_home() = shortcut for move() - def go_to_home(self, AXIS="all"): - go_to_home_message = { - **RPC_REQUEST, - "body": [{ - "kind": "lua", - "args": { - "lua": { - go_to_home(AXIS) - } - } - }] - } - - self.broker.publish(go_to_home_message) - - # on() and off() = shortcuts for control_peripheral() - def on(self, ID): - mode = self.get_info('peripherals', ID, 'mode') - - if mode == 1: - self.control_peripheral(ID, 255) - elif mode == 0: - self.control_peripheral(ID, 1) - - def off(self, ID): - self.control_peripheral(ID, 0) - - # read_status() = shortcut to get FarmBot state tree via message broker - # def read_status(self): - # status_message = { - # **RPC_REQUEST, - # "body": [{ - # "kind": "read_status", - # "args": {} - # }] - # } - - # BROKER.publish(status_message) - # return output state tree contents - - # check_position(coordinate, tolerance) - # Requires read_status() - - # get_xyz() - # Gets current bot position - # Requires read_status() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..dab9d49 --- /dev/null +++ b/main.py @@ -0,0 +1,146 @@ +from api_functions import ApiFunctions +from broker_functions import BrokerFunctions + +class Farmbot(): + def __init__(self): + self.api = ApiFunctions() + self.broker = BrokerFunctions() + + self.token = None + self.error = None + + ## SETUP + + def get_token(self, email, password, server="https://my.farm.bot"): + """Fetch user authentication token via API.""" + + token_data = self.api.get_token(email, password, server) + + self.token = self.api.api_connect.token + self.error = self.api.api_connect.error + + # Set broker tokens + self.broker.broker_connect.token = self.api.api_connect.token + self.broker.api.api_connect.token = self.api.api_connect.token + + return token_data + + def connect_broker(self): + """Establish persistent connection with message broker.""" + self.broker.broker_connect.connect() + + def disconnect_broker(self): + """Disconnect from the message broker.""" + self.broker.broker_connect.disconnect() + + def listen_broker(self, duration, channel='#'): + """Listen to messages via message broker.""" + self.broker.broker_connect.listen(duration, channel) + + ## INFORMATION + + def get_info(self, endpoint, id=None): + return self.api.get_info(endpoint, id) + + def set_info(self, endpoint, field, value, id=None): + return self.api.set_info(endpoint, field, value, id) + + def env(self, id=None, field=None, new_val=None): + return self.api.env(id, field, new_val) + + def group(self, id): + return self.api.group(id) + + def curve(self, id): + return self.api.curve(id) + + def read_status(self): + return self.broker.read_status() + + def read_sensor(self, id): + return self.broker.read_sensor(id) + + def safe_z(self): + return self.api.safe_z() + + def garden_size(self): + return self.api.garden_size() + + ## MESSAGING + + def log(self, message, type=None, channel=None): + return self.api.log(message, type, channel) + + def message(self, message, type=None, channel=None): + return self.broker.message(message, type, channel) + + def debug(self, message): + return self.broker.debug(message) + + def toast(self, message): + return self.broker.toast(message) + + ## BASIC COMMANDS + + def wait(self, time): + return self.broker.wait(time) + + def e_stop(self): + return self.broker.e_stop() + + def unlock(self): + return self.broker.unlock() + + def reboot(self): + return self.broker.reboot() + + def shutdown(self): + return self.broker.shutdown() + + ## MOVEMENT + + def move(self, x, y, z): + return self.broker.move(x, y, z) + + def set_home(self, axis='all'): + return self.broker.set_home(axis) + + def find_home(self, axis='all', speed=100): + return self.broker.find_home(axis, speed) + + def axis_length(self, axis='all'): + return self.broker.axis_length(axis) + + ## PERIPHERALS + + def control_peripheral(self, id, value, mode=None): + return self.broker.control_peripheral(id, value, mode) + + def toggle_peripheral(self, id): + return self.broker.toggle_peripheral(id) + + def on(self, id): + return self.broker.on(id) + + def off(self, id): + return self.broker.off(id) + + ## BROKER COMMANDS + + def calibrate_camera(self): + return self.broker.calibrate_camera() + + def control_servo(self, pin, angle): + return self.broker.control_servo(pin, angle) + + def take_photo(self): + return self.broker.take_photo() + + def soil_height(self): + return self.broker.soil_height() + + def detect_weeds(self): + return self.broker.detect_weeds() + + def assertion(self, code, as_type, id=''): + return self.broker.assertion(code, as_type, id) diff --git a/testing_farmbot_utilities.py b/testing.py similarity index 68% rename from testing_farmbot_utilities.py rename to testing.py index d6e2fe0..9b565ab 100644 --- a/testing_farmbot_utilities.py +++ b/testing.py @@ -1,101 +1,92 @@ +""" +Farmbot Unit Tests +""" + import unittest -import json -from farmbot_utilities import Farmbot -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch + +from main import Farmbot class TestFarmbot(unittest.TestCase): + """Farmbot tests""" - ## POSITIVE TEST: function called with email, password, and default server @patch('requests.post') def test_get_token_default_server(self, mock_post): + """POSITIVE TEST: function called with email, password, and default server""" mock_response = Mock() expected_token = {'token': 'abc123'} mock_response.json.return_value = expected_token mock_response.status_code = 200 mock_post.return_value = mock_response - fb = Farmbot() - # Call with default server fb.get_token('test_email@gmail.com', 'test_pass_123') - mock_post.assert_called_once_with( 'https://my.farm.bot/api/tokens', headers={'content-type': 'application/json'}, json={'user': {'email': 'test_email@gmail.com', 'password': 'test_pass_123'}} ) - self.assertEqual(fb.token, expected_token) self.assertEqual(mock_post.return_value.status_code, 200) - # POSITIVE TEST: function called with email, password, and custom server @patch('requests.post') def test_get_token_custom_server(self, mock_post): + """POSITIVE TEST: function called with email, password, and custom server""" mock_response = Mock() expected_token = {'token': 'abc123'} mock_response.json.return_value = expected_token mock_response.status_code = 200 mock_post.return_value = mock_response - fb = Farmbot() - # Call with custom server fb.get_token('test_email@gmail.com', 'test_pass_123', 'https://staging.farm.bot') - mock_post.assert_called_once_with( 'https://staging.farm.bot/api/tokens', headers={'content-type': 'application/json'}, json={'user': {'email': 'test_email@gmail.com', 'password': 'test_pass_123'}} ) - self.assertEqual(fb.token, expected_token) self.assertEqual(mock_post.return_value.status_code, 200) - # NEGATIVE TEST: function called with bad email or password (HTTP error) @patch('requests.post') def test_get_token_bad_email(self, mock_post): + """NEGATIVE TEST: function called with incorrect email""" mock_response = Mock() mock_response.status_code = 422 mock_post.return_value = mock_response - fb = Farmbot() - # Call with bad email - fb.get_token('bad_email@gmail.com', 'test_pass_123', 'https://staging.farm.bot') - + fb.get_token('bad_email@gmail.com', 'test_pass_123') mock_post.assert_called_once_with( - 'https://staging.farm.bot/api/tokens', + 'https://my.farm.bot/api/tokens', headers={'content-type': 'application/json'}, json={'user': {'email': 'bad_email@gmail.com', 'password': 'test_pass_123'}} ) - + self.assertEqual(fb.error, 'HTTP ERROR: Incorrect email address or password.') self.assertIsNone(fb.token) self.assertEqual(mock_post.return_value.status_code, 422) - # NEGATIVE TEST: function called with bad server address (HTTP error) @patch('requests.post') - def test_get_token_bad_email(self, mock_post): + def test_get_token_bad_server(self, mock_post): + """NEGATIVE TEST: function called with incorrect server""" mock_response = Mock() mock_response.status_code = 404 mock_post.return_value = mock_response - fb = Farmbot() - - # Call with bad email + # Call with bad server fb.get_token('test_email@gmail.com', 'test_pass_123', 'https://bad.farm.bot') - mock_post.assert_called_once_with( 'https://bad.farm.bot/api/tokens', headers={'content-type': 'application/json'}, json={'user': {'email': 'test_email@gmail.com', 'password': 'test_pass_123'}} ) - + self.assertEqual(fb.error, 'HTTP ERROR: The server address does not exist.') self.assertIsNone(fb.token) self.assertEqual(mock_post.return_value.status_code, 404) - # POSITIVE TEST: function called with endpoint only - @patch('requests.get') - def test_get_info_endpoint_only(self, mock_get): + @patch('requests.request') + def test_get_info_endpoint_only(self, mock_request): + """POSITIVE TEST: function called with endpoint only""" mock_token = { 'token': { 'unencoded': {'iss': '//my.farm.bot'}, @@ -106,28 +97,26 @@ def test_get_info_endpoint_only(self, mock_get): expected_response = {'device': 'info'} mock_response.json.return_value = expected_response mock_response.status_code = 200 - mock_get.return_value = mock_response - + mock_request.return_value = mock_response fb = Farmbot() - fb.token = mock_token - + fb.api.api_connect.token = mock_token # Call with endpoint only response = fb.get_info('device') - - mock_get.assert_called_once_with( - 'https://my.farm.bot/api/device/', + mock_request.assert_called_once_with( + 'GET', + 'https://my.farm.bot/api/device', headers={ 'authorization': 'encoded_token_value', 'content-type': 'application/json' - } + }, + json=None, ) + self.assertEqual(response, expected_response) + self.assertEqual(mock_request.return_value.status_code, 200) - self.assertEqual(response, json.dumps(expected_response, indent=2)) - self.assertEqual(mock_get.return_value.status_code, 200) - - # POSITIVE TEST: function called with endpoint and ID value - @patch('requests.get') - def test_get_info_with_id(self, mock_get): + @patch('requests.request') + def test_get_info_with_id(self, mock_request): + """POSITIVE TEST: function called with valid ID""" mock_token = { 'token': { 'unencoded': {'iss': '//my.farm.bot'}, @@ -138,24 +127,22 @@ def test_get_info_with_id(self, mock_get): expected_response = {'peripheral': 'info'} mock_response.json.return_value = expected_response mock_response.status_code = 200 - mock_get.return_value = mock_response - + mock_request.return_value = mock_response fb = Farmbot() - fb.token = mock_token - + fb.api.api_connect.token = mock_token # Call with specific ID response = fb.get_info('peripherals', '12345') - - mock_get.assert_called_once_with( + mock_request.assert_called_once_with( + 'GET', 'https://my.farm.bot/api/peripherals/12345', - headers={ + headers = { 'authorization': 'encoded_token_value', - 'content-type': 'application/json' - } + 'content-type': 'application/json', + }, + json=None, ) - - self.assertEqual(response, json.dumps(expected_response, indent=2)) - self.assertEqual(mock_get.return_value.status_code, 200) + self.assertEqual(response, expected_response) + self.assertEqual(mock_request.return_value.status_code, 200) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main()