From e5094d042f71efa5c453c0816da2636a2b2ba2ff Mon Sep 17 00:00:00 2001 From: LukasBaecker <65940705+LukasBaecker@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:24:54 +0100 Subject: [PATCH 1/3] No Bumper Handling (#241) When the robot has no bumper the automation_watch will throw an assertion as you can see in Issue #240. There are Fieldfriends that do not have bumpers at all. That is why we need an appropriate handling of it in the automation_watch. We would like to log a warning in case a robot with bumpers has an error or disconnected bumpers but beside that we just deactivate the bumper watcher. --- field_friend/automations/automation_watcher.py | 6 +++--- field_friend/system.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/field_friend/automations/automation_watcher.py b/field_friend/automations/automation_watcher.py index de00fd25..ba6580cd 100644 --- a/field_friend/automations/automation_watcher.py +++ b/field_friend/automations/automation_watcher.py @@ -86,9 +86,9 @@ def stop(self, reason: str) -> None: def try_resume(self) -> None: # Set conditions to True by default, which means they don't block the process if the watch is not active - # TODO: what to do if we don't have bumpers? - assert self.field_friend.bumper is not None - bumper_condition = not bool(self.field_friend.bumper.active_bumpers) if self.bumper_watch_active else True + bumper_condition = True + if self.field_friend.bumper is not None and self.bumper_watch_active: + bumper_condition = not bool(self.field_friend.bumper.active_bumpers) gnss_condition = (self.gnss.current is not None and ('R' in self.gnss.current.mode or self.gnss.current.mode == 'SSSS')) \ if self.gnss_watch_active else True diff --git a/field_friend/system.py b/field_friend/system.py index bf6dd17e..099b0482 100644 --- a/field_friend/system.py +++ b/field_friend/system.py @@ -178,6 +178,8 @@ def watch_robot() -> None: self.current_implement = self.monitoring if self.field_friend.bumper: self.automation_watcher.bumper_watch_active = True + else: + self.log.warning('Bumper is not available, does robot have bumpers?') if self.is_real: assert isinstance(self.field_friend, FieldFriendHardware) From 2e566f2bdd86f82fed62db10e30394fa495f4a05 Mon Sep 17 00:00:00 2001 From: "Johannes T." <119115663+Johannes-Thiel@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:31:07 +0100 Subject: [PATCH 2/3] add config for f16 (#242) add the config for the robot f16. --- config/f16_config_rb43/camera.py | 7 +++ config/f16_config_rb43/hardware.py | 66 ++++++++++++++++++++++++++++ config/f16_config_rb43/params.py | 11 +++++ config/f16_config_rb43/robotbrain.py | 4 ++ 4 files changed, 88 insertions(+) create mode 100644 config/f16_config_rb43/camera.py create mode 100644 config/f16_config_rb43/hardware.py create mode 100644 config/f16_config_rb43/params.py create mode 100644 config/f16_config_rb43/robotbrain.py diff --git a/config/f16_config_rb43/camera.py b/config/f16_config_rb43/camera.py new file mode 100644 index 00000000..b158f6f5 --- /dev/null +++ b/config/f16_config_rb43/camera.py @@ -0,0 +1,7 @@ +configuration = { + 'parameters': { + 'width': 1920, + 'height': 1080, + 'auto_exposure': True, + }, +} diff --git a/config/f16_config_rb43/hardware.py b/config/f16_config_rb43/hardware.py new file mode 100644 index 00000000..8bb7ca5b --- /dev/null +++ b/config/f16_config_rb43/hardware.py @@ -0,0 +1,66 @@ +configuration = { + 'wheels': { + 'version': 'double_wheels', + 'name': 'wheels', + 'left_back_can_address': 0x100, + 'left_front_can_address': 0x000, + 'right_back_can_address': 0x300, + 'right_front_can_address': 0x200, + 'is_left_reversed': True, + 'is_right_reversed': False, + 'odrive_version': 6, + }, + 'y_axis': { + 'version': 'none', + }, + 'flashlight': { + 'version': 'none', + }, + + 'z_axis': { + 'version': 'none', + }, + 'estop': { + 'name': 'estop', + 'pins': {'1': 34, '2': 35}, + }, + 'bms': { + 'name': 'bms', + 'on_expander': True, + 'rx_pin': 26, + 'tx_pin': 27, + 'baud': 9600, + 'num': 2, + }, + 'battery_control': { + 'name': 'battery_control', + 'on_expander': True, + 'reset_pin': 15, + 'status_pin': 13, + }, + 'status_control': { + 'name': 'status_control', + }, + 'bluetooth': { + 'name': 'fieldfriend-f16', + }, + 'serial': { + 'name': 'serial', + 'rx_pin': 26, + 'tx_pin': 27, + 'baud': 115200, + 'num': 1, + }, + 'expander': { + 'name': 'p0', + 'boot': 25, + 'enable': 14, + }, + 'can': { + 'name': 'can', + 'on_expander': False, + 'rx_pin': 32, + 'tx_pin': 33, + 'baud': 1_000_000, + }, +} diff --git a/config/f16_config_rb43/params.py b/config/f16_config_rb43/params.py new file mode 100644 index 00000000..c350d324 --- /dev/null +++ b/config/f16_config_rb43/params.py @@ -0,0 +1,11 @@ +configuration = { + 'motor_gear_ratio': 12.52, + 'thooth_count': 15, + 'pitch': 0.033, + 'wheel_distance': 0.74, + 'antenna_offset': 0.35, + 'work_x': -0.06933333, + 'work_y': 0.0094166667, + 'drill_radius': 0.025, + 'tool': 'none', +} diff --git a/config/f16_config_rb43/robotbrain.py b/config/f16_config_rb43/robotbrain.py new file mode 100644 index 00000000..ae834015 --- /dev/null +++ b/config/f16_config_rb43/robotbrain.py @@ -0,0 +1,4 @@ +configuration = {'robot_brain': { + 'flash_params': ['nand', 'orin', 'v05'] +}, +} From 4dc14c58a2e393920646820a46a872f6d488d07c Mon Sep 17 00:00:00 2001 From: LukasBaecker <65940705+LukasBaecker@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:42:07 +0100 Subject: [PATCH 3/3] Handling Restoring Fields with Errors (#243) Currently fields that have incomplete or faulty data which may happen to updates in the code base, the fields are getting deleted and will not be stored again. We want to prevent this. When data is loaded from the persistence missing values should be replaced to a default value and the user should be getting informed by a notification to recheck the field's data. --- field_friend/automations/field.py | 30 ++++++++++++++++--- tests/old_field_provider_persistence.json | 4 ++- ...ield_provider_persistence_with_errors.json | 19 ++++++++++++ tests/test_field_provider.py | 21 ++++++++++++- 4 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 tests/old_field_provider_persistence_with_errors.json diff --git a/field_friend/automations/field.py b/field_friend/automations/field.py index a4df4704..6026aa0d 100644 --- a/field_friend/automations/field.py +++ b/field_friend/automations/field.py @@ -1,4 +1,5 @@ import math +import uuid from dataclasses import dataclass from typing import Any, Self @@ -161,15 +162,36 @@ def shapely_polygon(self) -> shapely.geometry.Polygon: @classmethod def args_from_dict(cls, data: dict[str, Any]) -> dict: - return data + # Ensure all required fields exist with defaults + defaults: dict[str, Any] = { + 'id': str(uuid.uuid4()), + 'name': 'Field', + 'first_row_start': None, + 'first_row_end': None, + 'row_spacing': 1, + 'row_count': 1, + 'outline_buffer_width': 1, + 'row_support_points': [], + 'bed_count': 1, + 'bed_spacing': 1 + } + for key in defaults: + if key in data: + defaults[key] = data[key] + return defaults @classmethod def from_dict(cls, data: dict[str, Any]) -> Self: - data['first_row_start'] = GeoPoint(lat=data['first_row_start']['lat'], long=data['first_row_start']['long']) - data['first_row_end'] = GeoPoint(lat=data['first_row_end']['lat'], long=data['first_row_end']['long']) + data['first_row_start'] = GeoPoint(lat=data['first_row_start']['lat'], + long=data['first_row_start']['long']) if data.get('first_row_start') else GeoPoint(lat=0, long=0) + data['first_row_end'] = GeoPoint(lat=data['first_row_end']['lat'], + long=data['first_row_end']['long']) if data.get('first_row_end') else GeoPoint(lat=1, long=1) data['row_support_points'] = [rosys.persistence.from_dict( - RowSupportPoint, sp) for sp in data['row_support_points']] if 'row_support_points' in data else [] + RowSupportPoint, sp) for sp in data['row_support_points']] if data.get('row_support_points') else [] field_data = cls(**cls.args_from_dict(data)) + # if id is None, set it to a random uuid + if field_data.id is None: + field_data.id = str(uuid.uuid4()) return field_data def get_buffered_area(self, rows: list[Row], buffer_width: float) -> list[GeoPoint]: diff --git a/tests/old_field_provider_persistence.json b/tests/old_field_provider_persistence.json index a1909a8f..f615649b 100644 --- a/tests/old_field_provider_persistence.json +++ b/tests/old_field_provider_persistence.json @@ -11,7 +11,9 @@ "row_spacing": 0.5, "row_count": 10, "outline_buffer_width": 2, - "row_support_points": [] + "row_support_points": [], + "bed_count": 1, + "bed_spacing": 0.5 } } } diff --git a/tests/old_field_provider_persistence_with_errors.json b/tests/old_field_provider_persistence_with_errors.json new file mode 100644 index 00000000..f9dfac2f --- /dev/null +++ b/tests/old_field_provider_persistence_with_errors.json @@ -0,0 +1,19 @@ +{ + "fields": { + "eb24db0f-d48e-4a88-b23a-4833cda55483": { + "id": "eb24db0f-d48e-4a88-b23a-4833cda55483", + "name": "field_1", + "first_row_start": { + "lat": 51.98333789813455, + "long": 7.434242765994318 + }, + "first_row_end": { "lat": 51.98334192260392, "long": 7.434293309874038 }, + "row_spacing": 0.5, + "row_count": 10, + "not_existing_value": true, + "row_support_points": [], + "bed_count": 1, + "bed_spacing": 0.5 + } + } +} diff --git a/tests/test_field_provider.py b/tests/test_field_provider.py index 292c6a31..6841f7b2 100644 --- a/tests/test_field_provider.py +++ b/tests/test_field_provider.py @@ -11,7 +11,7 @@ from field_friend.localization import GeoPoint -def test_loading_from_old_persistence(system: System): +def test_loading_from_persistence(system: System): system.field_provider.restore(json.loads(Path('tests/old_field_provider_persistence.json').read_text())) assert len(system.field_provider.fields) == 1 field = system.field_provider.fields[0] @@ -27,6 +27,25 @@ def test_loading_from_old_persistence(system: System): assert field.first_row_end == GeoPoint(lat=51.98334192260392, long=7.434293309874038) +def test_loading_from_persistence_with_errors(system: System): + system.field_provider.restore(json.loads(Path('tests/old_field_provider_persistence_with_errors.json').read_text())) + assert len(system.field_provider.fields) == 1 + field = system.field_provider.fields[0] + # should set outline_buffer_width to default value because it is missing in the persistence data + assert field.outline_buffer_width == 1 + # should not write the not_existing_value to the field + assert not hasattr(field, 'not_existing_value') + assert field.row_count == 10 + assert field.row_spacing == 0.5 + assert len(field.outline) == 5 + assert len(field.rows) == 10 + assert len(field.row_support_points) == 0 + for row in field.rows: + assert len(row.points) == 2 + assert field.first_row_start == FIELD_FIRST_ROW_START + assert field.first_row_end == GeoPoint(lat=51.98334192260392, long=7.434293309874038) + + def test_field_outline(system: System, field: Field): field = system.field_provider.fields[0] outline = field.outline