Skip to content

Commit

Permalink
Merge pull request #5 from FarmBot-Labs/jmashon/working-branch
Browse files Browse the repository at this point in the history
Jmashon/working branch
  • Loading branch information
roryaronson authored Jul 25, 2024
2 parents db6b2f5 + 448ad08 commit eedce48
Show file tree
Hide file tree
Showing 11 changed files with 1,168 additions and 1,179 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.coverage
.DS_Store
__pycache__
test_commands.py
84 changes: 79 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
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.
94 changes: 45 additions & 49 deletions farmbot_api.py → api_connect.py
Original file line number Diff line number Diff line change
@@ -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.",
Expand All @@ -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:
Expand All @@ -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)
94 changes: 94 additions & 0 deletions api_functions.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit eedce48

Please sign in to comment.