diff --git a/broker_functions.py b/broker_functions.py index 612b671..39a94d3 100644 --- a/broker_functions.py +++ b/broker_functions.py @@ -50,7 +50,7 @@ def read_status(self): } self.broker_connect.publish(message) - self.broker_connect.listen(5, 'status') + self.broker_connect.listen(5, "status") status_tree = self.broker_connect.last_message @@ -59,8 +59,8 @@ def read_status(self): def read_sensor(self, id): # Get sensor data - peripheral_str = self.api.get_info('peripherals', id) - mode = peripheral_str['mode'] + peripheral_str = self.api.get_info("peripherals", id) + mode = peripheral_str["mode"] message = { **RPC_REQUEST, @@ -68,7 +68,7 @@ def read_sensor(self, id): "kind": "read_pin", "args": { "pin_mode": mode, - "label": '---', + "label": "---", "pin_number": { "kind": "named_pin", "args": { @@ -109,12 +109,12 @@ def message(self, message, type=None, channel="ticker"): # No inherent return value def debug(self, message): - # Send 'debug' type message + # Send "debug" type message self.message(message, "debug", "ticker") # No inherent return value def toast(self, message): - # Send 'toast' type message + # Send "toast" type message self.message(message, "info", "toast") # No inherent return value @@ -240,7 +240,7 @@ def axis_overwrite(axis, value): self.broker_connect.publish(move_message) # Return new xyz position as values - def set_home(self, axis='all'): + def set_home(self, axis="all"): # Set current xyz coord as 0,0,0 set_home_message = { "kind": "rpc_request", @@ -259,7 +259,7 @@ def set_home(self, axis='all'): self.broker_connect.publish(set_home_message) # No inherent return value - def find_home(self, axis='all', speed=100): + 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.") @@ -282,7 +282,7 @@ def find_home(self, axis='all', speed=100): # Return new xyz position as values - def axis_length(self, axis='all'): + def axis_length(self, axis="all"): # Get axis length # Return axis length as values axis_length_message = { @@ -301,35 +301,33 @@ def get_xyz(self): # Get current xyz coord tree_data = self.read_status() - position = tree_data["position"] - - x_val = position['x'] - y_val = position['y'] - z_val = position['z'] + 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): - # Check current xyz coord = user xyz coord within tolerance - # Return in or out of tolerance range 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 actual_value - tolerance <= user_value <= actual_value + tolerance: - print("Farmbot is at position.") - else: - print("Farmbot is NOT at position.") + 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'] + peripheral_str = self.api.get_info("peripherals", id) + mode = peripheral_str["mode"] control_peripheral_message = { **RPC_REQUEST, @@ -517,14 +515,15 @@ def dismount_tool(self): self.lua(lua_code) - def water(self, point_id): - # Dispense water at all or single xyz coords + def water(self, plant_id): + # Water the given plant # No inherent return value - plants = self.api.get_info("points", point_id) - plant_name = plants["name"] - lua_code = f""" - water({plant_name}) + plant = api({{ + method = "GET", + url = "/api/points/{plant_id}" + }}) + water(plant) """ self.lua(lua_code) @@ -538,25 +537,63 @@ def dispense(self, mL, tool_str, pin): self.lua(lua_code) - def get_seed_tray_cell(self, tray, cell): - lua_code = f""" - tray = variable("Seed Tray") - cell_label = variable("Seed Tray Cell") - cell = get_seed_tray_cell({tray}, cell_label) - cell_depth = 5 - - local cell_coordinates = " (" .. cell.x .. ", " .. cell.y .. ", " .. cell.z - cell_depth .. ")" - toast("Picking up seed from cell " .. cell_label .. cell_coordinates) - - move_absolute({{ - x = cell.x, - y = cell.y, - z = cell.z + 25, - safe_z = true - }}) - """ + # TODO: fix read_status() not working if staging.farm.bot not open/refreshed? - self.lua(lua_code) + 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 @@ -592,7 +629,7 @@ def set_job(self, job_str, status_message, value): local job_name = "{job_str}" set_job(job_name) - -- Update the job's status and percent: + -- Update the job\'s status and percent: set_job(job_name, {{ status = "{status_message}", percent = {value} @@ -616,14 +653,14 @@ def lua(self, code_snippet): "body": [{ "kind": "lua", "args": { - "lua": code_snippet + "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 + 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 = { @@ -652,7 +689,7 @@ def if_statement(self, variable, operator, value, then_id, else_id): # TODO: add self.broker_connect.publish(if_statement_message) - def assertion(self, code, as_type, id=''): # TODO: add 'continue' functionality + def assertion(self, code, as_type, id=""): # TODO: add "continue" functionality # Execute assertion # No inherent return value assertion_message = { diff --git a/main.py b/main.py index d5f7465..3694c83 100644 --- a/main.py +++ b/main.py @@ -145,3 +145,45 @@ def detect_weeds(self): def assertion(self, code, as_type, id=''): return self.broker.assertion(code, as_type, id) + + def get_xyz(self): + return self.broker.get_xyz() + + def check_position(self, user_x, user_y, user_z, tolerance): + return self.broker.check_position(user_x, user_y, user_z, tolerance) + + def mark_coord(self, x, y, z, property, mark_as): + return self.broker.mark_coord(x, y, z, property, mark_as) + + def mount_tool(self, tool_str): + return self.broker.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) + + 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 if_statement(self, variable, operator, value, then_id, else_id): + return self.broker.if_statement(variable, operator, value, then_id, else_id) diff --git a/testing.py b/testing.py index ce74d44..8b93495 100644 --- a/testing.py +++ b/testing.py @@ -354,11 +354,11 @@ def send_command_test_helper(self, *args, **kwargs): mock_request = args[2] expected_command = kwargs.get('expected_command') extra_rpc_args = kwargs.get('extra_rpc_args') - expected_api_response = kwargs.get('expected_api_response') + mock_api_response = kwargs.get('mock_api_response') mock_client = Mock() mock_mqtt.return_value = mock_client mock_response = Mock() - mock_response.json.return_value = expected_api_response + mock_response.json.return_value = mock_api_response mock_response.status_code = 200 mock_request.return_value = mock_response fb = Farmbot() @@ -393,7 +393,7 @@ def exec_command(fb): 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}], }, extra_rpc_args={'priority': 600}, - expected_api_response={}) + mock_api_response={}) def test_debug(self): '''Test debug command''' @@ -407,7 +407,7 @@ def exec_command(fb): 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}], }, extra_rpc_args={'priority': 600}, - expected_api_response={}) + mock_api_response={}) def test_toast(self): '''Test toast command''' @@ -421,7 +421,7 @@ def exec_command(fb): 'body': [{'kind': 'channel', 'args': {'channel_name': 'toast'}}], }, extra_rpc_args={'priority': 600}, - expected_api_response={}) + mock_api_response={}) def test_read_status(self): '''Test read_status command''' @@ -434,7 +434,7 @@ def exec_command(fb): 'args': {}, }, extra_rpc_args={'priority': 600}, - expected_api_response={}) + mock_api_response={}) def test_read_sensor(self): '''Test read_sensor command''' @@ -454,7 +454,7 @@ def exec_command(fb): }, }, extra_rpc_args={}, - expected_api_response={'mode': 0}) + mock_api_response={'mode': 0}) def test_assertion(self): '''Test assertion command''' @@ -471,7 +471,7 @@ def exec_command(fb): } }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_wait(self): '''Test wait command''' @@ -484,7 +484,7 @@ def exec_command(fb): 'args': {'milliseconds': 123}, }, extra_rpc_args={'priority': 600}, - expected_api_response={}) + mock_api_response={}) def test_unlock(self): '''Test unlock command''' @@ -497,7 +497,7 @@ def exec_command(fb): 'args': {}, }, extra_rpc_args={'priority': 9000}, - expected_api_response={}) + mock_api_response={}) def test_e_stop(self): '''Test e_stop command''' @@ -510,7 +510,7 @@ def exec_command(fb): 'args': {}, }, extra_rpc_args={'priority': 9000}, - expected_api_response={}) + mock_api_response={}) def test_find_home(self): '''Test find_home command''' @@ -523,7 +523,7 @@ def exec_command(fb): 'args': {'axis': 'all', 'speed': 100}, }, extra_rpc_args={'priority': 600}, - expected_api_response={}) + mock_api_response={}) def test_set_home(self): '''Test set_home command''' @@ -536,7 +536,7 @@ def exec_command(fb): 'args': {'axis': 'all'}, }, extra_rpc_args={'priority': 600}, - expected_api_response={}) + mock_api_response={}) def test_toggle_peripheral(self): '''Test toggle_peripheral command''' @@ -554,7 +554,7 @@ def exec_command(fb): }, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_on(self): '''Test on command''' @@ -574,7 +574,7 @@ def exec_command(fb): }, }, extra_rpc_args={}, - expected_api_response={'mode': 0}) + mock_api_response={'mode': 0}) def test_off(self): '''Test off command''' @@ -594,7 +594,7 @@ def exec_command(fb): }, }, extra_rpc_args={}, - expected_api_response={'mode': 1}) + mock_api_response={'mode': 1}) def test_move(self): '''Test move command''' @@ -618,7 +618,7 @@ def exec_command(fb): ], }, extra_rpc_args={'priority': 600}, - expected_api_response={}) + mock_api_response={}) def test_reboot(self): '''Test reboot command''' @@ -631,7 +631,7 @@ def exec_command(fb): 'args': {'package': 'farmbot_os'}, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_shutdown(self): '''Test shutdown command''' @@ -644,7 +644,7 @@ def exec_command(fb): 'args': {}, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_axis_length(self): '''Test axis_length command''' @@ -657,7 +657,7 @@ def exec_command(fb): 'args': {'axis': 'all'}, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_control_peripheral(self): '''Test control_peripheral command''' @@ -677,7 +677,7 @@ def exec_command(fb): }, }, extra_rpc_args={}, - expected_api_response={'mode': 0}) + mock_api_response={'mode': 0}) def test_soil_height(self): '''Test soil_height command''' @@ -690,7 +690,7 @@ def exec_command(fb): 'args': {'label': 'Measure Soil Height'}, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_detect_weeds(self): '''Test detect_weeds command''' @@ -703,7 +703,7 @@ def exec_command(fb): 'args': {'label': 'plant-detection'}, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_calibrate_camera(self): '''Test calibrate_camera command''' @@ -716,9 +716,9 @@ def exec_command(fb): 'args': {'label': 'camera-calibration'}, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) - def text_sequence(self): + def test_sequence(self): '''Test sequence command''' def exec_command(fb): fb.sequence(123) @@ -729,7 +729,7 @@ def exec_command(fb): 'args': {'sequence_id': 123}, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_take_photo(self): '''Test take_photo command''' @@ -742,7 +742,7 @@ def exec_command(fb): 'args': {}, }, extra_rpc_args={}, - expected_api_response={}) + mock_api_response={}) def test_control_servo(self): '''Test control_servo command''' @@ -758,7 +758,229 @@ def exec_command(fb): }, }, extra_rpc_args={}, - expected_api_response={'mode': 0}) + mock_api_response={'mode': 0}) + + def test_get_xyz(self): + '''Test get_xyz command''' + def exec_command(fb): + fb.broker.broker_connect.last_message = { + 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}}, + } + position = fb.get_xyz() + self.assertEqual(position, (1, 2, 3)) + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'read_status', + 'args': {}, + }, + extra_rpc_args={'priority': 600}, + mock_api_response={}) + + def test_check_position(self): + '''Test check_position command: at position''' + def exec_command(fb): + fb.broker.broker_connect.last_message = { + 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}}, + } + at_position = fb.check_position(1, 2, 3, 0) + self.assertTrue(at_position) + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'read_status', + 'args': {}, + }, + extra_rpc_args={'priority': 600}, + mock_api_response={}) + + def test_check_position_false(self): + '''Test check_position command: not at position''' + def exec_command(fb): + fb.broker.broker_connect.last_message = { + 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}}, + } + at_position = fb.check_position(0, 0, 0, 2) + self.assertFalse(at_position) + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'read_status', + 'args': {}, + }, + extra_rpc_args={'priority': 600}, + mock_api_response={}) + + def test_mount_tool(self): + '''Test mount_tool command''' + def exec_command(fb): + fb.mount_tool('Weeder') + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'lua', + 'args': {'lua': 'mount_tool("Weeder")'}, + }, + extra_rpc_args={}, + mock_api_response={}) + + def test_dismount_tool(self): + '''Test dismount_tool command''' + def exec_command(fb): + fb.dismount_tool() + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'lua', + 'args': {'lua': 'dismount_tool()'}, + }, + extra_rpc_args={}, + mock_api_response={}) + + def test_water(self): + '''Test water command''' + def exec_command(fb): + fb.water(123) + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'lua', + 'args': {'lua': """plant = api({ + method = "GET", + url = "/api/points/123" + }) + water(plant)"""}, + }, + extra_rpc_args={}, + mock_api_response={'name': 'Mint'}) + + def test_dispense(self): + '''Test dispense command''' + def exec_command(fb): + fb.dispense(100, 'Weeder', 4) + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'lua', + 'args': { + 'lua': 'dispense(100, {tool_name = "Weeder", pin = 4})', + }, + }, + extra_rpc_args={}, + mock_api_response={}) + + @patch('paho.mqtt.client.Client') # temporary + @patch('requests.request') + def test_get_seed_tray_cell(self, mock_request, _mock_client): + '''Test get_seed_tray_cell command''' + mock_response = Mock() + mock_api_response = { + 'pointer_type': 'ToolSlot', + 'pullout_direction': 1, + 'x': 0, + 'y': 0, + 'z': 0, + } + mock_response.json.return_value = mock_api_response + mock_response.status_code = 200 + mock_request.return_value = mock_response + fb = Farmbot() + fb.set_token(MOCK_TOKEN) + cell = fb.get_seed_tray_cell(123, 'd4') + mock_request.assert_called_once_with( + 'GET', + 'https://my.farm.bot/api/points/123', + headers={ + 'authorization': 'encoded_token_value', + 'content-type': 'application/json', + }, + json=None, + ) + self.assertEqual(cell, {'x': -36.25, 'y': 18.75, 'z': 0}) + + def test_get_job(self): + '''Test get_job command''' + def exec_command(fb): + fb.broker.broker_connect.last_message = { + 'jobs': { + 'job name': {'status': 'working'}, + }, + } + job = fb.get_job('job name') + self.assertEqual(job, {'status': 'working'}) + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'read_status', + 'args': {}, + }, + extra_rpc_args={'priority': 600}, + mock_api_response={}) + + def test_set_job(self): + '''Test set_job command''' + def exec_command(fb): + fb.set_job('job name', 'working', 50) + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'lua', + 'args': {'lua': """local job_name = "job name" + set_job(job_name) + + -- Update the job's status and percent: + set_job(job_name, { + status = "working", + percent = 50 + })"""}, + }, + extra_rpc_args={}, + mock_api_response={}) + + def test_complete_job(self): + '''Test complete_job command''' + def exec_command(fb): + fb.complete_job('job name') + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'lua', + 'args': {'lua': 'complete_job("job name")'}, + }, + extra_rpc_args={}, + mock_api_response={}) + + def test_lua(self): + '''Test lua command''' + def exec_command(fb): + fb.lua('return true') + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': 'lua', + 'args': {'lua': 'return true'}, + }, + extra_rpc_args={}, + mock_api_response={}) + + def test_if_statement(self): + '''Test if_statement command''' + def exec_command(fb): + fb.if_statement('pin10', '<', 0, 123, 456) + self.send_command_test_helper( + exec_command, + expected_command={ + 'kind': '_if', + 'args': { + 'lhs': 'pin10', + 'op': '<', + 'rhs': 0, + '_then': {'kind': 'execute', 'args': {'sequence_id': 123}}, + '_else': {'kind': 'execute', 'args': {'sequence_id': 456}}, + } + }, + extra_rpc_args={}, + mock_api_response={}) if __name__ == '__main__':