diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1bc3012..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 FarmBot Labs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index ca899cc..1891847 100644 --- a/README.md +++ b/README.md @@ -1,169 +1,120 @@ # 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: - -(1) Clone the repository. -``` -git clone https://github.com/FarmBot-Labs/sidecar-starter-pack -``` +## :book: Contents -(2) Navigate to the project directory. -``` -cd path/to/sidecar-starter-pack -``` +* [Installation](#computer-installation-mac-os) +* [Getting Started](#seedling-getting-started) + * [Get your authentication token](#get-your-authentication-token) + * [Configure echo and verbosity](#configure-echo-and-verbosity) +* [Functions](#compass-functions) +* [Developer Info](#toolbox-developer-info) + * [Formatting message broker messages](#formatting-message-broker-messages) + * [Formatting API requests](#formatting-api-requests) -(3) Create a virtual environment. -``` -python -m venv path/to/venv/location -``` +## :computer: Installation (Mac OS) -(4) Activate the virtual environment. -``` -source path/to/venv/bin/activate -``` +## :seedling: Getting Started -(5) Install the required libraries within venv: +Import `main.py` and create an instance of the Farmbot class: ``` -python3 -m pip install requests -python3 -m pip install paho-mqtt -``` - -## 🌱 Getting Started -To generate your authorization token and get started: - -(1) Import `main.py` and create an instance. -``` -from farmbot_utilities import Farmbot +from main.py import Farmbot bot = Farmbot() ``` -(2) Generate your authorization token. - The server is https://my.farm.bot by default. -``` -bot.get_token('email', 'password', 'server') -``` +### Get your authentication token -(3.1) To interact with your Farmbot via the API, try getting your device info: -``` -bot.get_info('device') -``` +### Configure echo and verbosity -(3.2) Try editing your device name: -``` -bot.edit_info('device', 'name', 'Carrot Commander') -``` -> [!NOTE] -> To interact with your Farmbot via the message broker, you must first establish a connection. Publishing single messages without establishing a connection may trigger your device rate limit. +## :compass: Functions -(4.1) Connect to the message broker: ``` -bot.connect_broker() +sidecar-starter-pack/ +├── functions/ +│ ├── __init__.py +│ ├── authentication.py +│ ├── basic_commands.py +│ ├── broker.py +│ ├── camera.py +│ ├── imports.py +│ ├── information.py +│ ├── jobs.py +│ ├── messages.py +│ ├── movements.py +│ ├── peripherals.py +│ ├── resources.py +│ └── tools.py +├── tests/ +│ ├── __init__.py +│ └── tests_main.py +├── __init.py__ +├── imports.py +├── main.py +└── README.md ``` -(4.2) Try sending a new log message: -``` -bot.send_message('Hello from the message broker!', 'success') -``` - -(4.3) Try sending a movement command: -``` -bot.move(30,40,10) -``` - -(4.5) After sending messages, don't forget to disconnect from the message broker: -``` -bot.disconnect_broker() -``` - -## 🧭 Functions +> [!TIP] +> Functions marked with [API] communicate with the Farm Designer web app via the [REST API](https://developer.farm.bot/v15/docs/web-app/rest-api.html) and those marked with [BROKER] communicate with the FarmBot via the [message broker](https://developer.farm.bot/v15/docs/message-broker). -### Setup +### authentication.py +class `Authentication()` -`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 +### basic_commands.py +class `BasicCommands()` -### Information +### broker.py +class `BrokerConnect()` -`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() +### camera.py +class `Camera()` -### Messaging +### information.py +class `Information()` -`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 +> [!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. -move() -set_home() -find_home() -axis_length() +> [!NOTE] +> Not sure which endpoint to access? [Find the list here](https://developer.farm.bot/v15/docs/web-app/api-docs). -### Peripherals +### jobs.py +class `JobHandling()` -control_peripheral() -toggle_peripheral() -on() -off() +### messages.py +class `MessageHandling()` -### Broker Commands +### movement.py +class `MovementControls()` -calibrate_camera() -control_servo() -take_photo() -soil_height() -detect_weeds() +### peripherals.py +class `Peripherals()` -## 🧰 Developer Info +### resources.py +class `Resources()` -### api_connect.py -Background: https://developer.farm.bot/v15/docs/web-app/rest-api +### tools.py +class `ToolControls()` -Formatting: functions in `api_functions.py` and `main.py` which interact with the API require an endpoint, which is truncated onto the HTTP request. +## :toolbox: Developer Info -List of endpoints: https://developer.farm.bot/v15/docs/web-app/api-docs +### Formatting message broker messages -> [!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. +> [!NOTE] +> Messages sent via the message broker contain [CeleryScript nodes](https://developer.farm.bot/v15/docs/celery-script/nodes.html) which require special formatting. -### broker_connect.py -Background: https://developer.farm.bot/v15/docs/message-broker +``` +message = { + "kind": "rpc_request", + "args": { + "label": # node, + "priority": # number + }, + "body": [ + { + # instructions + } + ] +} +``` -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 +### Formatting API requests \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..bb3595b --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +""" +Initialization for main module. +""" diff --git a/api_functions.py b/api_functions.py deleted file mode 100644 index 7f0f4e6..0000000 --- a/api_functions.py +++ /dev/null @@ -1,92 +0,0 @@ -import json - -from api_connect import ApiConnect - -class ApiFunctions(): - def __init__(self): - self.api_connect = ApiConnect() - - self.echo = True - self.verbose = True - - def get_token(self, email, password, server='https://my.farm.bot'): - # Generate user authentication token - token_str = self.api_connect.get_token(email, password, server) - # Return token as json object: token[""] - return token_str - - def get_info(self, endpoint, id=None): - # Get endpoint info - endpoint_data = self.api_connect.request('GET', endpoint, id) - # Return endpoint info as json object: endpoint[""] - return endpoint_data - - def set_info(self, endpoint, field, value, id=None): - # Edit endpoint info - new_value = { - field: value - } - self.api_connect.request('PATCH', endpoint, id, new_value) - - # Return endpoint info as json object: endpoint[""] - new_endpoint_data = self.api_connect.request('GET', endpoint, id) - return new_endpoint_data - - def log(self, message, type=None, channel=None): - # Send new log message via API - 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.request('POST', endpoint, id, log_message) - - # No inherent return value - - def safe_z(self): - # Get safe z height via get_info() - config_data = self.get_info('fbos_config') - z_value = config_data["safe_height"] - # Return safe z height as value - return z_value - - def garden_size(self): - # Get garden size parameters via get_info() - 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 garden size parameters as values - return length_x, length_y, area - - def group(self, id=None): - # Get all groups or single by id - if id is None: - group_data = self.get_info("point_groups") - else: - group_data = self.get_info('point_groups', id) - - # Return group as json object: group[""] - return group_data - - def curve(self, id=None): - # Get all curves or single by id - if id is None: - curve_data = self.get_info("curves") - else: - curve_data = self.get_info('curves', id) - - # Return curve as json object: curve[""] - return curve_data diff --git a/broker_functions.py b/broker_functions.py deleted file mode 100644 index 39a94d3..0000000 --- a/broker_functions.py +++ /dev/null @@ -1,712 +0,0 @@ -import json -from datetime import datetime - -from broker_connect import BrokerConnect -from api_functions import ApiFunctions - -RPC_REQUEST = { - "kind": "rpc_request", - "args": { - "label": "" - } -} - -"""MESSAGE TEMPLATE - - message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": # Code here... (600) - }, - "body": [ - { - # Instructions here... - } - ] - } - -""" - -class BrokerFunctions(): - def __init__(self): - self.broker_connect = BrokerConnect() - self.api = ApiFunctions() - - self.client = None - - def read_status(self): - # Get device status tree - message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": 600 - }, - "body": [{ - "kind": "read_status", - "args": {} - }] - } - - self.broker_connect.publish(message) - self.broker_connect.listen(5, "status") - - status_tree = self.broker_connect.last_message - - # Return status as json object: status[""] - return status_tree - - def read_sensor(self, id): - # Get sensor data - peripheral_str = self.api.get_info("peripherals", id) - mode = peripheral_str["mode"] - - message = { - **RPC_REQUEST, - "body": [{ - "kind": "read_pin", - "args": { - "pin_mode": mode, - "label": "---", - "pin_number": { - "kind": "named_pin", - "args": { - "pin_type": "Peripheral", - "pin_id": id - } - } - } - }] - } - - self.broker_connect.publish(message) - # Return sensor as json object: sensor[""] - - def message(self, message, type=None, channel="ticker"): - message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": 600 - }, - "body": [{ - "kind": "send_message", - "args": { - "message": message, - "message_type": type - }, - "body": [{ - "kind": "channel", - "args": { - "channel_name": channel - } - }] - }] - } - - self.broker_connect.publish(message) - # No inherent return value - - def debug(self, message): - # Send "debug" type message - self.message(message, "debug", "ticker") - # No inherent return value - - def toast(self, message): - # Send "toast" type message - self.message(message, "info", "toast") - # No inherent return value - - def wait(self, duration): - # Tell bot to wait for some time - message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": 600 - }, - "body": [{ - "kind": "wait", - "args": { - "milliseconds": duration - } - }] - } - self.broker_connect.publish(message) - - # No inherent return value - return print("Waiting for "+str(duration)+" milliseconds...") - - def e_stop(self): - # Tell bot to emergency stop - new_message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": 9000 - }, - "body": [{ - "kind": "emergency_lock", - "args": {} - }] - } - self.broker_connect.publish(new_message) - - # No inherent return value - return print("Triggered device emergency stop.") - - def unlock(self): - # Tell bot to unlock - message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": 9000 - }, - "body": [{ - "kind": "emergency_unlock", - "args": {} - }] - } - self.broker_connect.publish(message) - - # No inherent return value - return print("Triggered device unlock.") - - def reboot(self): - # Tell bot to reboot - # No inherent return value - 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): - # Tell bot to shutdown - # No inherent return value - shutdown_message = { - **RPC_REQUEST, - "body": [{ - "kind": "power_off", - "args": {} - }] - } - - self.broker_connect.publish(shutdown_message) - return print("Triggered device shutdown.") - - 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 - } - } - } - } - - # Tell bot to move to new xyz coord - move_message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": 600 - }, - "body": [{ - "kind": "move", - "args": {}, - "body": [ - axis_overwrite("x", x), - axis_overwrite("y", y), - axis_overwrite("z", z) - ] - }] - } - - self.broker_connect.publish(move_message) - # Return new xyz position as values - - def set_home(self, axis="all"): - # Set current xyz coord as 0,0,0 - set_home_message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": 600 - }, - "body": [{ - "kind": "zero", - "args": { - "axis": axis - } - }] - } - - self.broker_connect.publish(set_home_message) - # No inherent return value - - def find_home(self, axis="all", speed=100): - # Move to 0,0,0 - if speed > 100 or speed < 1: - return print("ERROR: Speed constrained to 1-100.") - else: - message = { - "kind": "rpc_request", - "args": { - "label": "", - "priority": 600 - }, - "body": [{ - "kind": "find_home", - "args": { - "axis": axis, - "speed": speed - } - }] - } - self.broker_connect.publish(message) - - # Return new xyz position as values - - def axis_length(self, axis="all"): - # Get axis length - # Return axis length as values - axis_length_message = { - **RPC_REQUEST, - "body": [{ - "kind": "calibrate", - "args": { - "axis": axis - } - }] - } - - self.broker_connect.publish(axis_length_message) - - def get_xyz(self): - # Get current xyz coord - tree_data = self.read_status() - - x_val = tree_data["location_data"]["position"]["x"] - y_val = tree_data["location_data"]["position"]["y"] - z_val = tree_data["location_data"]["position"]["z"] - - # Return xyz position as values - return x_val, y_val, z_val - - def check_position(self, user_x, user_y, user_z, tolerance): - user_values = [user_x, user_y, user_z] - - position = self.get_xyz() - actual_vals = list(position) - - for user_value, actual_value in zip(user_values, actual_vals): - if not actual_value - tolerance <= user_value <= actual_value + tolerance: - print(f"Farmbot is NOT at position {position}") - return False - - print(f"Farmbot is at position {position}") - return True - - def control_peripheral(self, id, value, mode=None): - # Change peripheral values - # No inherent return value - 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) - - def toggle_peripheral(self, id): - # Toggle peripheral on or off - # Return status - 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) - - def on(self, id): - # Set peripheral to on - # Return status - 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) - - def off(self, id): - # Set peripheral to off - # Return status - self.control_peripheral(id, 0) - - # TODO: sort_points(points, method) - # TODO: sort(points, method) - - def soil_height(self): - # Execute soil height scripts - # Return soil height as value - soil_height_message = { - **RPC_REQUEST, - "body": [{ - "kind": "execute_script", - "args": { - "label": "Measure Soil Height" - } - }] - } - - self.broker_connect.publish(soil_height_message) - - def detect_weeds(self): - # Execute detect weeds script - # Return array of weeds with xyz coords - detect_weeds_message = { - **RPC_REQUEST, - "body": [{ - "kind": "execute_script", - "args": { - "label": "plant-detection" - } - }] - } - - self.broker_connect.publish(detect_weeds_message) - - def calibrate_camera(self): - # Execute calibrate camera script - # No inherent return value - calibrate_message = { - **RPC_REQUEST, - "body": [{ - "kind": "execute_script", - "args": { - "label": "camera-calibration" - }, - }] - } - - self.broker_connect.publish(calibrate_message) - - # TODO: photo_grid() - - def take_photo(self): - # Take single photo - # No inherent return value - take_photo_message = { - **RPC_REQUEST, - "body": [{ - "kind": "take_photo", - "args": {} - }] - } - - self.broker_connect.publish(take_photo_message) - - def control_servo(self, pin, angle): - # Change servo values - # No inherent return value - 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) - - def mark_coord(self, x, y, z, property, mark_as): # TODO: Fix "label" - # Mark xyz coordinate - # Return new xyz coord value(s) - 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 - } - }] - }] - } - - # TODO: verify_tool() --> get broker message example - # Verify tool exists at xyz coord - # Return xyz coord and info(?) - - def mount_tool(self, tool_str): - # Mount tool at xyz coord - # No inherent return value - lua_code = f""" - mount_tool("{tool_str}") - """ - - self.lua(lua_code) - - def dismount_tool(self): - # Dismount tool (at xyz coord?) - # No inherent return value - lua_code = """ - dismount_tool() - """ - - self.lua(lua_code) - - def water(self, plant_id): - # Water the given plant - # No inherent return value - lua_code = f""" - plant = api({{ - method = "GET", - url = "/api/points/{plant_id}" - }}) - water(plant) - """ - - self.lua(lua_code) - - def dispense(self, mL, tool_str, pin): - # Dispense from source at all or single xyz coords - # No inherent return value - lua_code = f""" - dispense({mL}, {{tool_name = "{tool_str}", pin = {pin}}}) - """ - - self.lua(lua_code) - - # TODO: fix read_status() not working if staging.farm.bot not open/refreshed? - - def get_seed_tray_cell(self, tray_id, tray_cell): - tray_data = self.api.get_info("points", tray_id) - - cell = tray_cell.upper() - - seeder_needle_offset = 17.5 - cell_spacing = 12.5 - - cells = { - "A1": {"x": 0, "y": 0}, - "A2": {"x": 0, "y": 1}, - "A3": {"x": 0, "y": 2}, - "A4": {"x": 0, "y": 3}, - - "B1": {"x": -1, "y": 0}, - "B2": {"x": -1, "y": 1}, - "B3": {"x": -1, "y": 2}, - "B4": {"x": -1, "y": 3}, - - "C1": {"x": -2, "y": 0}, - "C2": {"x": -2, "y": 1}, - "C3": {"x": -2, "y": 2}, - "C4": {"x": -2, "y": 3}, - - "D1": {"x": -3, "y": 0}, - "D2": {"x": -3, "y": 1}, - "D3": {"x": -3, "y": 2}, - "D4": {"x": -3, "y": 3} - } - - if tray_data["pointer_type"] != "ToolSlot": - raise ValueError("Seed Tray variable must be a seed tray in a slot") - if cell not in cells: - raise ValueError("Seed Tray Cell must be one of **A1** through **D4**") - - flip = 1 - if tray_data["pullout_direction"] == 1: - flip = 1 - elif tray_data["pullout_direction"] == 2: - flip = -1 - else: - raise ValueError("Seed Tray **SLOT DIRECTION** must be `Positive X` or `Negative X`") - - A1 = { - "x": tray_data["x"] - seeder_needle_offset + (1.5 * cell_spacing * flip), - "y": tray_data["y"] - (1.5 * cell_spacing * flip), - "z": tray_data["z"] - } - - offset = { - "x": cell_spacing * cells[cell]["x"] * flip, - "y": cell_spacing * cells[cell]["y"] * flip - } - - return {"x": A1["x"] + offset["x"], "y": A1["y"] + offset["y"], "z": A1["z"]} - - def sequence(self, sequence_id): - # Execute sequence by id - # No inherent return value - sequence_message = { - **RPC_REQUEST, - "body": [{ - "kind": "execute", - "args": { - "sequence_id": sequence_id - } - }] - } - - self.broker_connect.publish(sequence_message) - - # https://developer.farm.bot/v15/lua/functions/jobs.html - - def get_job(self, job_str): - # Get all or single job by name - status_data = self.read_status() - - if job_str is None: - jobs = status_data["jobs"] - else: - jobs = status_data["jobs"][job_str] - - # Return job as json object: job[""] - return jobs - - def set_job(self, job_str, status_message, value): - lua_code = f""" - local job_name = "{job_str}" - set_job(job_name) - - -- Update the job\'s status and percent: - set_job(job_name, {{ - status = "{status_message}", - percent = {value} - }}) - """ - - self.lua(lua_code) - - def complete_job(self, job_str): - lua_code = f""" - complete_job("{job_str}") - """ - - self.lua(lua_code) - - def lua(self, code_snippet): - # Send custom code snippet - # No inherent return value - lua_message = { - **RPC_REQUEST, - "body": [{ - "kind": "lua", - "args": { - "lua": code_snippet.strip() - } - }] - } - - self.broker_connect.publish(lua_message) - - def if_statement(self, variable, operator, value, then_id, else_id): # TODO: add "do nothing" functionality - # Execute if statement - # No inherent return value - 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) - - def assertion(self, code, as_type, id=""): # TODO: add "continue" functionality - # Execute assertion - # No inherent return value - 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) diff --git a/functions/__init__.py b/functions/__init__.py new file mode 100644 index 0000000..347216c --- /dev/null +++ b/functions/__init__.py @@ -0,0 +1,3 @@ +""" +Initialization for functions module. +""" diff --git a/api_connect.py b/functions/authentication.py similarity index 88% rename from api_connect.py rename to functions/authentication.py index 92f5bce..57ed9ad 100644 --- a/api_connect.py +++ b/functions/authentication.py @@ -1,17 +1,22 @@ -import sys -import json -import requests +""" +Autentication class. +""" -class ApiConnect(): +# └── functions/authentication.py +# ├── [API] token_handling() +# ├── [API] get_token() +# ├── [API] check_token() +# ├── [API] request_handling() +# └── [API] request() + +from .imports import * + +class Authentication(): def __init__(self): self.token = None self.error = None - ## 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.", @@ -29,11 +34,7 @@ def request_handling(self, response): else: self.error = json.dumps(f"UNEXPECTED ERROR {response.status_code}: {response.text}", indent=2) - ## FUNCTIONS - - def get_token(self, email, password, server): - """Fetch user authentication token via API.""" - + def get_token(self, email, password, server="https://my.farm.bot"): try: headers = {'content-type': 'application/json'} user = {'user': {'email': email, 'password': password}} @@ -41,7 +42,7 @@ def get_token(self, email, password, server): # Handle HTTP status codes if response.status_code == 200: token_data = response.json() - self.token = token_data # TODO + self.token = token_data # TODO: simplify? self.error = None return token_data elif response.status_code == 404: @@ -65,15 +66,11 @@ def get_token(self, email, password, server): 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, database_id, payload=None): - """Send requests to the API using various methods.""" - self.check_token() # use 'GET' method to view endpoint data diff --git a/functions/basic_commands.py b/functions/basic_commands.py new file mode 100644 index 0000000..3c7bd42 --- /dev/null +++ b/functions/basic_commands.py @@ -0,0 +1,104 @@ +""" +BasicCommands class. +""" + +# └── functions/basic_commands.py +# ├── [BROKER] wait() +# ├── [BROKER] e_stop() +# ├── [BROKER] unlock() +# ├── [BROKER] reboot() +# └── [BROKER] shutdown() + +from .imports import * +from .broker import BrokerConnect + +class BasicCommands(): + def __init__(self): + self.token = None + self.broker = BrokerConnect() + + def wait(self, duration): + # Tell bot to wait for some time + wait_message = { + "kind": "rpc_request", + "args": { + "label": "", + "priority": 600 + }, + "body": [{ + "kind": "wait", + "args": { + "milliseconds": duration + } + }] + } + self.broker.publish(wait_message) + + # No inherent return value + return print("Waiting for "+str(duration)+" milliseconds...") + + def e_stop(self): + # Tell bot to emergency stop + stop_message = { + "kind": "rpc_request", + "args": { + "label": "", + "priority": 9000 + }, + "body": [{ + "kind": "emergency_lock", + "args": {} + }] + } + self.broker.publish(stop_message) + + # No inherent return value + return print("Triggered device emergency stop.") + + def unlock(self): + # Tell bot to unlock + unlock_message = { + "kind": "rpc_request", + "args": { + "label": "", + "priority": 9000 + }, + "body": [{ + "kind": "emergency_unlock", + "args": {} + }] + } + self.broker.publish(unlock_message) + + # No inherent return value + return print("Triggered device unlock.") + + def reboot(self): + # Tell bot to reboot + # No inherent return value + reboot_message = { + **RPC_REQUEST, + "body": [{ + "kind": "reboot", + "args": { + "package": "farmbot_os" + } + }] + } + + self.broker.publish(reboot_message) + return print("Triggered device reboot.") + + def shutdown(self): + # Tell bot to shutdown + # No inherent return value + shutdown_message = { + **RPC_REQUEST, + "body": [{ + "kind": "power_off", + "args": {} + }] + } + + self.broker.publish(shutdown_message) + return print("Triggered device shutdown.") diff --git a/broker_connect.py b/functions/broker.py similarity index 78% rename from broker_connect.py rename to functions/broker.py index ef9bcaa..9274144 100644 --- a/broker_connect.py +++ b/functions/broker.py @@ -1,9 +1,17 @@ -import threading -import json -import time +""" +BrokerConnect class. +""" -from datetime import datetime -import paho.mqtt.client as mqtt +# └── functions/broker.py +# ├── [BROKER] connect() +# ├── [BROKER] disconnect() +# ├── [BROKER] publish() +# ├── [BROKER] on_connect() +# ├── [BROKER] on_message +# ├── [BROKER] start_listen() +# └── [BROKER] stop_listen() + +from .imports import * class BrokerConnect(): def __init__(self): @@ -12,7 +20,16 @@ def __init__(self): self.last_message = None - ## FUNCTIONS -- SENDING MESSAGES + # def connect() --> connect to message broker to send messages + # def disconnect() --> disconnect from message broker + + # def publish() --> send message via message broker + + # def on_connect() --> subscribe to messages from specific broker channel + # def on_message() --> update message queue with latest response + + # def start_listen() --> start listening to broker channel (print each message if echo == true) + # def stop_listen() --> stop listening to broker channel def connect(self): """Establish persistent connection with message broker.""" @@ -49,8 +66,6 @@ def publish(self, message): payload=json.dumps(message) ) - ## FUNCTIONS -- RECEIVING MESSAGES - def on_connect(self, _client, _userdata, _flags, _rc, channel): """Subscribe to specified message broker channel.""" self.client.subscribe(f"bot/{self.token['token']['unencoded']['bot']}/{channel}") diff --git a/functions/camera.py b/functions/camera.py new file mode 100644 index 0000000..e785f6d --- /dev/null +++ b/functions/camera.py @@ -0,0 +1,46 @@ +""" +Camera class. +""" + +# └── functions/camera.py +# ├── [BROKER] calibrate_camera() +# ├── [BROKER] take_photo() +# └── [BROKER] photo_grid() + +from .imports import * +from .broker import BrokerConnect + +class Camera(): + def __init__(self): + self.token = None + self.broker = BrokerConnect() + + def calibrate_camera(self): + # Execute calibrate camera script + # No inherent return value + calibrate_message = { + **RPC_REQUEST, + "body": [{ + "kind": "execute_script", + "args": { + "label": "camera-calibration" + }, + }] + } + + self.broker.publish(calibrate_message) + + def take_photo(self): + # Take single photo + # No inherent return value + take_photo_message = { + **RPC_REQUEST, + "body": [{ + "kind": "take_photo", + "args": {} + }] + } + + self.broker.publish(take_photo_message) + + # TODO: photo_grid() diff --git a/functions/imports.py b/functions/imports.py new file mode 100644 index 0000000..3934fb4 --- /dev/null +++ b/functions/imports.py @@ -0,0 +1,23 @@ +""" +Imports and dependencies for functions module. +""" + +# Imports + +import sys +import json +import requests + +import time +from datetime import datetime + +import paho.mqtt.client as mqtt + +# Definitions + +RPC_REQUEST = { + "kind": "rpc_request", + "args": { + "label": "" + } +} diff --git a/functions/information.py b/functions/information.py new file mode 100644 index 0000000..eccaa25 --- /dev/null +++ b/functions/information.py @@ -0,0 +1,149 @@ +""" +Information class. +""" + +# └── functions/information +# ├── [API] get_info() +# ├── [API] set_info() +# ├── [API] safe_z() +# ├── [API] garden_size() +# ├── [API] group() +# ├── [API] curve() +# ├── [BROKER] soil_height() +# ├── [BROKER] read_status() +# └── [BROKER] read_sensor() + +from .imports import * +from .broker import BrokerConnect +from .authentication import Authentication + +class Information(): + def __init__(self): + self.token = None + + self.broker = BrokerConnect() + self.auth = Authentication() + + def get_info(self, endpoint, id=None): + # Get endpoint info + endpoint_data = self.auth.request('GET', endpoint, id) + # Return endpoint info as json object: endpoint[""] + return endpoint_data + + def set_info(self, endpoint, field, value, id=None): + # Edit endpoint info + new_value = { + field: value + } + self.auth.request('PATCH', endpoint, id, new_value) + + # Return endpoint info as json object: endpoint[""] + new_endpoint_data = self.auth.request('GET', endpoint, id) + return new_endpoint_data + + def safe_z(self): + # Get safe z height via get_info() + config_data = self.get_info('fbos_config') + z_value = config_data["safe_height"] + # Return safe z height as value + return z_value + + def garden_size(self): + # Get garden size parameters via get_info() + 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 garden size parameters as values + return length_x, length_y, area + + def group(self, id=None): + # Get all groups or single by id + if id is None: + group_data = self.get_info("point_groups") + else: + group_data = self.get_info('point_groups', id) + + # Return group as json object: group[""] + return group_data + + def curve(self, id=None): + # Get all curves or single by id + if id is None: + curve_data = self.get_info("curves") + else: + curve_data = self.get_info('curves', id) + + # Return curve as json object: curve[""] + return curve_data + + def soil_height(self): + # Execute soil height scripts + # Return soil height as value + soil_height_message = { + **RPC_REQUEST, + "body": [{ + "kind": "execute_script", + "args": { + "label": "Measure Soil Height" + } + }] + } + + self.broker.publish(soil_height_message) + + def read_status(self): + # Get device status tree + status_message = { + "kind": "rpc_request", + "args": { + "label": "", + "priority": 600 + }, + "body": [{ + "kind": "read_status", + "args": {} + }] + } + + self.broker.publish(status_message) + self.broker.listen(5, "status") + + status_tree = self.broker.last_message + + # Return status as json object: status[""] + return status_tree + + def read_sensor(self, id): + # Get sensor data + peripheral_str = self.get_info("peripherals", id) + mode = peripheral_str["mode"] + + 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 + } + } + } + }] + } + + self.broker.publish(sensor_message) + # Return sensor as json object: sensor[""] diff --git a/functions/jobs.py b/functions/jobs.py new file mode 100644 index 0000000..30410bd --- /dev/null +++ b/functions/jobs.py @@ -0,0 +1,55 @@ +""" +JobHandling class. +""" + +# └── functions/jobs.py +# ├── [BROKER] get_job() +# ├── [BROKER] set_job() +# └── [BROKER] complete_job() + +from .imports import * +from .broker import BrokerConnect + +from .information import Information +from .resources import Resources + +class JobHandling(): + def __init__(self): + self.token = None + + self.broker = BrokerConnect() + self.info = Information() + self.resource = Resources() + + def get_job(self, job_str): + # Get all or single job by name + status_data = self.info.read_status() + + if job_str is None: + jobs = status_data["jobs"] + else: + jobs = status_data["jobs"][job_str] + + # Return job as json object: job[""] + return jobs + + def set_job(self, job_str, status_message, value): + lua_code = f""" + local job_name = "{job_str}" + set_job(job_name) + + -- Update the job\'s status and percent: + set_job(job_name, {{ + status = "{status_message}", + percent = {value} + }}) + """ + + self.resource.lua(lua_code) + + def complete_job(self, job_str): + lua_code = f""" + complete_job("{job_str}") + """ + + self.resource.lua(lua_code) diff --git a/functions/messages.py b/functions/messages.py new file mode 100644 index 0000000..65cc141 --- /dev/null +++ b/functions/messages.py @@ -0,0 +1,69 @@ +""" +MessageHandling class. +""" + +# └── functions/messages.py +# ├── [API] log() +# ├── [BROKER] message() +# ├── [BROKER] debug() +# └── [BROKER] toast() + +from .imports import * +from .broker import BrokerConnect +from .authentication import Authentication + +class MessageHandling(): + def __init__(self): + self.token = None + + self.broker = BrokerConnect() + self.auth = Authentication() + + def log(self, message_str, type=None, channel=None): + # Send new log message via API + log_message = { + "message": message_str, + "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.auth.request('POST', endpoint, id, log_message) + # No inherent return value + + def message(self, message_str, type=None, channel="ticker"): + message = { + "kind": "rpc_request", + "args": { + "label": "", + "priority": 600 + }, + "body": [{ + "kind": "send_message", + "args": { + "message": message_str, + "message_type": type + }, + "body": [{ + "kind": "channel", + "args": { + "channel_name": channel + } + }] + }] + } + + self.broker.publish(message) + # No inherent return value + + def debug(self, message_str): + # Send "debug" type message + self.message(message_str, "debug", "ticker") + # No inherent return value + + def toast(self, message_str): + # Send "toast" type message + self.message(message_str, "info", "toast") + # No inherent return value diff --git a/functions/movements.py b/functions/movements.py new file mode 100644 index 0000000..264f2ae --- /dev/null +++ b/functions/movements.py @@ -0,0 +1,140 @@ +""" +MovementControls class. +""" + +# └── functions/movements.py +# ├── [BROKER] move() +# ├── [BROKER] set_home() +# ├── [BROKER] find_home() +# ├── [BROKER] axis_length() +# ├── [BROKER] get_xyz() +# └── [BROKER] check_position() + +from .imports import * +from .broker import BrokerConnect +from .information import Information + +class MovementControls(): + def __init__(self): + self.token = None + + self.broker = BrokerConnect() + self.info = Information() + + 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 + } + } + } + } + + # Tell bot to move to new xyz coord + move_message = { + "kind": "rpc_request", + "args": { + "label": "", + "priority": 600 + }, + "body": [{ + "kind": "move", + "args": {}, + "body": [ + axis_overwrite("x", x), + axis_overwrite("y", y), + axis_overwrite("z", z) + ] + }] + } + + self.broker.publish(move_message) + # Return new xyz position as values + + def set_home(self, axis="all"): + # Set current xyz coord as 0,0,0 + set_home_message = { + "kind": "rpc_request", + "args": { + "label": "", + "priority": 600 + }, + "body": [{ + "kind": "zero", + "args": { + "axis": axis + } + }] + } + + self.broker.publish(set_home_message) + # No inherent return value + + def find_home(self, axis="all", speed=100): + # Move to 0,0,0 + if speed > 100 or speed < 1: + return print("ERROR: Speed constrained to 1-100.") + else: + message = { + "kind": "rpc_request", + "args": { + "label": "", + "priority": 600 + }, + "body": [{ + "kind": "find_home", + "args": { + "axis": axis, + "speed": speed + } + }] + } + self.broker.publish(message) + + # Return new xyz position as values + + def axis_length(self, axis="all"): + # Get axis length + # Return axis length as values + axis_length_message = { + **RPC_REQUEST, + "body": [{ + "kind": "calibrate", + "args": { + "axis": axis + } + }] + } + + self.broker.publish(axis_length_message) + + def get_xyz(self): + # Get current xyz coord + tree_data = self.info.read_status() + + x_val = tree_data["location_data"]["position"]["x"] + y_val = tree_data["location_data"]["position"]["y"] + z_val = tree_data["location_data"]["position"]["z"] + + # Return xyz position as values + return x_val, y_val, z_val + + def check_position(self, user_x, user_y, user_z, tolerance): + user_values = [user_x, user_y, user_z] + + position = self.get_xyz() + actual_vals = list(position) + + for user_value, actual_value in zip(user_values, actual_vals): + if not actual_value - tolerance <= user_value <= actual_value + tolerance: + print(f"Farmbot is NOT at position {position}") + return False + + print(f"Farmbot is at position {position}") + return True diff --git a/functions/peripherals.py b/functions/peripherals.py new file mode 100644 index 0000000..fe67642 --- /dev/null +++ b/functions/peripherals.py @@ -0,0 +1,104 @@ +""" +Peripherals class. +""" + +# └── functions/peripherals.py +# ├── [BROKER] control_servo() +# ├── [BROKER] control_peripheral() +# ├── [BROKER] toggle_peripheral() +# ├── [BROKER] on() +# └── [BROKER] off() + +from .imports import * +from .broker import BrokerConnect +from .information import Information + +class Peripherals(): + def __init__(self): + self.token = None + + self.broker = BrokerConnect() + self.info = Information() + + def control_servo(self, pin, angle): + # Change servo values + # No inherent return value + 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) + + def control_peripheral(self, id, value, mode=None): + # Change peripheral values + # No inherent return value + if mode is None: + peripheral_str = self.info.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.publish(control_peripheral_message) + + def toggle_peripheral(self, id): + # Toggle peripheral on or off + # Return status + 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) + + def on(self, id): + # Set peripheral to on + # Return status + peripheral_str = self.info.get_info("peripherals", id) + mode = peripheral_str["mode"] + + if mode == 1: + self.control_peripheral(id, 255) + elif mode == 0: + self.control_peripheral(id, 1) + + def off(self, id): + # Set peripheral to off + # Return status + self.control_peripheral(id, 0) diff --git a/functions/resources.py b/functions/resources.py new file mode 100644 index 0000000..44430be --- /dev/null +++ b/functions/resources.py @@ -0,0 +1,204 @@ +""" +Resources class. +""" + +# └── functions/resources.py +# ├── [BROKER] mark_point() +# ├── [BROKER] sort_points() +# ├── [BROKER] sequence() +# ├── [BROKER] get_seed_tray_cell() +# ├── [BROKER] detect_weeds() +# ├── [BROKER] lua() +# ├── [BROKER] if_statement() +# └── [BROKER] assertion() + +from .imports import * +from .broker import BrokerConnect +from .information import Information + +class Resources(): + def __init__(self): + self.token = None + + self.broker = BrokerConnect() + self.info = Information() + + def mark_coord(self, x, y, z, property, mark_as): # TODO: Fix "label" and TODO: rename mark_point() + # Mark xyz coordinate + # Return new xyz coord value(s) + 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 + } + }] + }] + } + + # TODO: sort_points(points, method) + # TODO: sort(points, method) + + def sequence(self, sequence_id): + # Execute sequence by id + # No inherent return value + sequence_message = { + **RPC_REQUEST, + "body": [{ + "kind": "execute", + "args": { + "sequence_id": sequence_id + } + }] + } + + self.broker.publish(sequence_message) + + def get_seed_tray_cell(self, tray_id, tray_cell): + tray_data = self.info.get_info("points", tray_id) + + cell = tray_cell.upper() + + seeder_needle_offset = 17.5 + cell_spacing = 12.5 + + cells = { + "A1": {"x": 0, "y": 0}, + "A2": {"x": 0, "y": 1}, + "A3": {"x": 0, "y": 2}, + "A4": {"x": 0, "y": 3}, + + "B1": {"x": -1, "y": 0}, + "B2": {"x": -1, "y": 1}, + "B3": {"x": -1, "y": 2}, + "B4": {"x": -1, "y": 3}, + + "C1": {"x": -2, "y": 0}, + "C2": {"x": -2, "y": 1}, + "C3": {"x": -2, "y": 2}, + "C4": {"x": -2, "y": 3}, + + "D1": {"x": -3, "y": 0}, + "D2": {"x": -3, "y": 1}, + "D3": {"x": -3, "y": 2}, + "D4": {"x": -3, "y": 3} + } + + if tray_data["pointer_type"] != "ToolSlot": + raise ValueError("Seed Tray variable must be a seed tray in a slot") + if cell not in cells: + raise ValueError("Seed Tray Cell must be one of **A1** through **D4**") + + flip = 1 + if tray_data["pullout_direction"] == 1: + flip = 1 + elif tray_data["pullout_direction"] == 2: + flip = -1 + else: + raise ValueError("Seed Tray **SLOT DIRECTION** must be `Positive X` or `Negative X`") + + A1 = { + "x": tray_data["x"] - seeder_needle_offset + (1.5 * cell_spacing * flip), + "y": tray_data["y"] - (1.5 * cell_spacing * flip), + "z": tray_data["z"] + } + + offset = { + "x": cell_spacing * cells[cell]["x"] * flip, + "y": cell_spacing * cells[cell]["y"] * flip + } + + return {"x": A1["x"] + offset["x"], "y": A1["y"] + offset["y"], "z": A1["z"]} + + def detect_weeds(self): + # Execute detect weeds script + # Return array of weeds with xyz coords + detect_weeds_message = { + **RPC_REQUEST, + "body": [{ + "kind": "execute_script", + "args": { + "label": "plant-detection" + } + }] + } + + self.broker.publish(detect_weeds_message) + + def lua(self, code_snippet): + # Send custom code snippet + # No inherent return value + lua_message = { + **RPC_REQUEST, + "body": [{ + "kind": "lua", + "args": { + "lua": code_snippet.strip() + } + }] + } + + self.broker.publish(lua_message) + + def if_statement(self, variable, operator, value, then_id, else_id): # TODO: add "do nothing" functionality + # Execute if statement + # No inherent return value + 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.publish(if_statement_message) + + def assertion(self, code, as_type, id=""): # TODO: add "continue" functionality + # Execute assertion + # No inherent return value + 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.publish(assertion_message) diff --git a/functions/tools.py b/functions/tools.py new file mode 100644 index 0000000..b92b77e --- /dev/null +++ b/functions/tools.py @@ -0,0 +1,65 @@ +""" +ToolControls class. +""" + +# └── functions/tools.py +# ├── [BROKER] verify_tool() +# ├── [BROKER] mount_tool() +# ├── [BROKER] dismount_tool() +# ├── [BROKER] water() +# └── [BROKER] dispense() + +from .imports import * +from .broker import BrokerConnect +from .resources import Resources + +class ToolControls(): + def __init__(self): + self.token = None + + self.broker = BrokerConnect() + self.resource = Resources() + + # TODO: verify_tool() --> get broker message example + # Verify tool exists at xyz coord + # Return xyz coord and info(?) + + def mount_tool(self, tool_str): + # Mount tool at xyz coord + # No inherent return value + lua_code = f""" + mount_tool("{tool_str}") + """ + + self.resource.lua(lua_code) + + def dismount_tool(self): + # Dismount tool (at xyz coord?) + # No inherent return value + lua_code = """ + dismount_tool() + """ + + self.resource.lua(lua_code) + + def water(self, plant_id): + # Water the given plant + # No inherent return value + lua_code = f""" + plant = api({{ + method = "GET", + url = "/api/points/{plant_id}" + }}) + water(plant) + """ + + self.resource.lua(lua_code) + + def dispense(self, mL, tool_str, pin): + # Dispense from source at all or single xyz coords + # No inherent return value + lua_code = f""" + dispense({mL}, {{tool_name = "{tool_str}", pin = {pin}}}) + """ + + self.resource.lua(lua_code) diff --git a/imports.py b/imports.py new file mode 100644 index 0000000..e147522 --- /dev/null +++ b/imports.py @@ -0,0 +1,31 @@ +""" +Imports and dependencies for main module. +""" + +# Imports + +from functions.authentication import Authentication +from functions.basic_commands import BasicCommands +from functions.broker import BrokerConnect +from functions.camera import Camera +from functions.information import Information +from functions.jobs import JobHandling +from functions.messages import MessageHandling +from functions.movements import MovementControls +from functions.peripherals import Peripherals +from functions.resources import Resources +from functions.tools import ToolControls + +# Classes + +auth = Authentication() +basic = BasicCommands() +broker = BrokerConnect() +camera = Camera() +info = Information() +job = JobHandling() +message = MessageHandling() +move = MovementControls() +peripheral = Peripherals() +resource = Resources() +tool = ToolControls() diff --git a/main.py b/main.py index 3694c83..fb8e87d 100644 --- a/main.py +++ b/main.py @@ -1,189 +1,201 @@ -from api_functions import ApiFunctions -from broker_functions import BrokerFunctions +""" +Farmbot class. +""" + +from imports import * class Farmbot(): def __init__(self): - self.api = ApiFunctions() - self.broker = BrokerFunctions() - self.token = None self.error = None - ## SETUP + self.echo = True + + # authentication.py def set_token(self, token): self.token = token - # Set api token (redundant--used for tests) - self.api.api_connect.token = token - - # Set broker tokens - self.broker.broker_connect.token = token - self.broker.api.api_connect.token = token + # Set API token (redundant--used for tests) + auth.token = token + + # Set file tokens + basic.token = token + broker.token = token + camera.token = token + info.token = token + job.token = token + message.token = token + move.token = token + peripheral.token = token + resource.token = token + tool.token = token def get_token(self, email, password, server="https://my.farm.bot"): # Call get_token() source # Set authentication token for all modules - token_data = self.api.get_token(email, password, server) + token_data = auth.get_token(email, password, server) - self.set_token(self.api.api_connect.token) - self.error = self.api.api_connect.error + self.set_token(auth.token) + self.error = auth.error return token_data + # basic_commands.py + + def wait(self, duration): + return basic.wait(duration) + + def e_stop(self): + return basic.e_stop() + + def unlock(self): + return basic.unlock() + + def reboot(self): + return basic.reboot() + + def shutdown(self): + return basic.shutdown() + + # broker.py + def connect_broker(self): - self.broker.broker_connect.connect() + return broker.connect() def disconnect_broker(self): - self.broker.broker_connect.disconnect() + return broker.disconnect() - def listen_broker(self, duration, channel='#'): - self.broker.broker_connect.listen(duration, channel) + # camera.py - ## INFORMATION + def calibrate_camera(self): + return camera.calibrate_camera() + + def take_photo(self): + return camera.take_photo() + + # information.py def get_info(self, endpoint, id=None): - return self.api.get_info(endpoint, id) + return info.get_info(endpoint, id) def set_info(self, endpoint, field, value, id=None): - return self.api.set_info(endpoint, field, value, id) + return info.set_info(endpoint, field, value, id) + + def safe_z(self): + return info.safe_z() - def group(self, id): - return self.api.group(id) + def garden_size(self): + return info.garden_size() + + def group(self, id=None): + return info.group(id) - def curve(self, id): - return self.api.curve(id) + def curve(self, id=None): + return info.curve(id) + + def soil_height(self): + return info.soil_height() def read_status(self): - return self.broker.read_status() + return info.read_status() def read_sensor(self, id): - return self.broker.read_sensor(id) + return info.read_sensor(id) - def safe_z(self): - return self.api.safe_z() - - def garden_size(self): - return self.api.garden_size() + # jobs.py - ## MESSAGING + def get_job(self, job_str): + return job.get_job(job_str) - def log(self, message, type=None, channel=None): - return self.api.log(message, type, channel) + def set_job(self, job_str, status_message, value): + return job.set_job(job_str, status_message, value) - def message(self, message, type=None, channel="ticker"): - return self.broker.message(message, type, channel) + def complete_job(self, job_str): + return job.complete_job(job_str) - def debug(self, message): - return self.broker.debug(message) + # messages.py - def toast(self, message): - return self.broker.toast(message) + def log(self, message_str, type=None, channel=None): + return message.log(message_str, type, channel) - ## BASIC COMMANDS + def message(self, message_str, type=None, channel="ticker"): + return message.message(message_str, type, channel) - def wait(self, time): - return self.broker.wait(time) + def debug(self, message_str): + return message.debug(message_str) - def e_stop(self): - return self.broker.e_stop() + def toast(self, message_str): + return message.toast(message_str) - def unlock(self): - return self.broker.unlock() + # movements.py - def reboot(self): - return self.broker.reboot() + def move(self, x, y, z): + return move.move(x, y, z) - def shutdown(self): - return self.broker.shutdown() + def set_home(self, axis="all"): + return move.set_home(axis) - ## MOVEMENT + def find_home(self, axis="all", speed=100): + return move.find_home(axis, speed) - def move(self, x, y, z): - return self.broker.move(x, y, z) + def axis_length(self, axis="all"): + return move.axis_length(axis) - def set_home(self, axis='all'): - return self.broker.set_home(axis) + def get_xyz(self): + return move.get_xyz() - def find_home(self, axis='all', speed=100): - return self.broker.find_home(axis, speed) + def check_position(self, user_x, user_y, user_z, tolerance): + return move.check_position(user_x, user_y, user_z, tolerance) - def axis_length(self, axis='all'): - return self.broker.axis_length(axis) + # peripherals.py - ## PERIPHERALS + def control_servo(self, pin, angle): + return peripheral.control_servo(pin, angle) def control_peripheral(self, id, value, mode=None): - return self.broker.control_peripheral(id, value, mode) + return peripheral.control_peripheral(id, value, mode) def toggle_peripheral(self, id): - return self.broker.toggle_peripheral(id) + return peripheral.toggle_peripheral(id) def on(self, id): - return self.broker.on(id) + return peripheral.on(id) def off(self, id): - return self.broker.off(id) + return peripheral.off(id) - ## BROKER COMMANDS - - def calibrate_camera(self): - return self.broker.calibrate_camera() + # resources.py - def control_servo(self, pin, angle): - return self.broker.control_servo(pin, angle) + def mark_coord(self, x, y, z, property, mark_as): + return resource.mark_coord(x, y, z, property, mark_as) - def take_photo(self): - return self.broker.take_photo() + def sequence(self, sequence_id): + return resource.sequence(sequence_id) - def soil_height(self): - return self.broker.soil_height() + def get_seed_tray_cell(self, tray_id, tray_cell): + return resource.get_seed_tray_cell(tray_id, tray_cell) def detect_weeds(self): - return self.broker.detect_weeds() - - def assertion(self, code, as_type, id=''): - return self.broker.assertion(code, as_type, id) + return resource.detect_weeds() - def get_xyz(self): - return self.broker.get_xyz() + def lua(self, code_snippet): + return resource.lua(code_snippet) - def check_position(self, user_x, user_y, user_z, tolerance): - return self.broker.check_position(user_x, user_y, user_z, tolerance) + def if_statement(self, variable, operator, value, then_id, else_id): + return resource.if_statement(variable, operator, value, then_id, else_id) - def mark_coord(self, x, y, z, property, mark_as): - return self.broker.mark_coord(x, y, z, property, mark_as) + # tools.py def mount_tool(self, tool_str): - return self.broker.mount_tool(tool_str) + return tool.mount_tool(tool_str) def dismount_tool(self): - return self.broker.dismount_tool() - - def water(self, point_id): - return self.broker.water(point_id) - - def dispense(self, mL, tool_str, pin): - return self.broker.dispense(mL, tool_str, pin) - - def get_seed_tray_cell(self, tray, cell): - return self.broker.get_seed_tray_cell(tray, cell) - - def sequence(self, sequence_id): - return self.broker.sequence(sequence_id) + return tool.dismount_tool() - def get_job(self, job_str): - return self.broker.get_job(job_str) - - def set_job(self, job_str, status_message, value): - return self.broker.set_job(job_str, status_message, value) - - def complete_job(self, job_str): - return self.broker.complete_job(job_str) - - def lua(self, code_snippet): - return self.broker.lua(code_snippet) + def water(self, plant_id): + return tool.water(plant_id) - def if_statement(self, variable, operator, value, then_id, else_id): - return self.broker.if_statement(variable, operator, value, then_id, else_id) + def dispense(self, mL, tool_str, pin): + return tool.dispense(mL, tool_str, pin) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d7b9343 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Initialization for tests module. +""" diff --git a/testing.py b/tests/tests_main.py similarity index 99% rename from testing.py rename to tests/tests_main.py index 8b93495..2c6c0ab 100644 --- a/testing.py +++ b/tests/tests_main.py @@ -1,5 +1,5 @@ """ -Farmbot Unit Tests +Farmbot class unit tests. """ import json @@ -19,7 +19,6 @@ } } - class TestFarmbot(unittest.TestCase): """Farmbot tests"""