Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crop-Type for Beds Handling and Selected Bed Fixing #230

Merged
merged 36 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ec1e247
adding bed crops to field, row and fieldprovider
LukasBaecker Nov 13, 2024
dbd63a0
first changes on field creator and operation - adding and editing bed…
LukasBaecker Nov 13, 2024
ec63547
better structure for field_creator
LukasBaecker Nov 13, 2024
3d39c5e
loading crop_types in plant_locator and interim saving changes
LukasBaecker Nov 20, 2024
2777a38
handling beds crops correctly and make them changeable
LukasBaecker Nov 21, 2024
bd97874
Merge branch 'main' into crops_per_bed
LukasBaecker Nov 21, 2024
58e9130
fixing deleting field missing change from merge
LukasBaecker Nov 21, 2024
8407a81
fixing tests, cleanup
LukasBaecker Nov 21, 2024
d4182e2
using rows crops in field_navigation, test_navigation (not correct yet)
LukasBaecker Nov 22, 2024
49b0918
optimization, styling, error fixing and cleanup
LukasBaecker Nov 22, 2024
af6bc65
adding leaflet map to field_creator and cleanup
LukasBaecker Nov 25, 2024
340073f
cleanup and bed label fix
LukasBaecker Nov 25, 2024
38aaaf7
fixing plant_locator
LukasBaecker Nov 25, 2024
4589897
cleanup
LukasBaecker Nov 25, 2024
171474f
fixing field_creator
LukasBaecker Nov 25, 2024
457cc3b
check for nearest row and do not start if robot is placed wrong
LukasBaecker Nov 26, 2024
cc21bf5
tests with tornado, tests running
LukasBaecker Nov 27, 2024
de7648c
test_field_with_first_row_excluded
LukasBaecker Nov 27, 2024
30ef220
double quote cleanup
LukasBaecker Nov 27, 2024
487d4d8
mypy
LukasBaecker Nov 27, 2024
8264377
ruff
LukasBaecker Nov 27, 2024
d322dc8
Merge branch 'main' into crops_per_bed
LukasBaecker Nov 27, 2024
1fb39ff
ruff
LukasBaecker Nov 27, 2024
314f635
fixing crop updating with new navigation states
LukasBaecker Nov 27, 2024
44f627b
fixing nearest row
LukasBaecker Dec 2, 2024
6a7ad88
Merge branch 'main' into crops_per_bed
LukasBaecker Dec 2, 2024
d11c14a
notify crop change only when crop changed
LukasBaecker Dec 2, 2024
8526cdd
fixing nearest row
LukasBaecker Dec 2, 2024
5005f45
editing notification
LukasBaecker Dec 2, 2024
b19a739
setting crop of None to default button
LukasBaecker Dec 2, 2024
94c57a9
cleaning up get_nearest_row
LukasBaecker Dec 2, 2024
c637a7b
refactor get_buffered_area
pascalzauberzeug Dec 2, 2024
2412644
fix persistence
pascalzauberzeug Dec 2, 2024
620d354
refactor
pascalzauberzeug Dec 2, 2024
f542b16
remove button for default crop for now
pascalzauberzeug Dec 2, 2024
ef15607
make checkers happy
pascalzauberzeug Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion field_friend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .system import System

__all__ = [
'System',
'interface',
'log_configuration',
'System',
]
2 changes: 1 addition & 1 deletion field_friend/automations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
'KpiProvider',
'Path',
'PathProvider',
'Plant',
'PlantLocator',
'PlantProvider',
'Plant',
'Puncher',
'Row',
'RowSupportPoint',
Expand Down
17 changes: 12 additions & 5 deletions field_friend/automations/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
@dataclass(slots=True, kw_only=True)
class Row(GeoPointCollection):
reverse: bool = False
crop: str | None = None

def reversed(self):
return Row(
id=self.id,
name=self.name,
points=list(reversed(self.points)),
crop=self.crop
)

def line_segment(self) -> rosys.geometry.LineSegment:
Expand Down Expand Up @@ -49,7 +51,8 @@ def __init__(self, *,
outline_buffer_width: float = 2,
row_support_points: list[RowSupportPoint] | None = None,
bed_count: int = 1,
bed_spacing: float = 0.5) -> None:
bed_spacing: float = 0.5,
bed_crops: dict[str, str | None] | None = None) -> None:
self.id: str = id
self.name: str = name
self.first_row_start: GeoPoint = first_row_start
Expand All @@ -64,6 +67,7 @@ def __init__(self, *,
self.visualized: bool = False
self.rows: list[Row] = []
self.outline: list[GeoPoint] = []
self.bed_crops: dict[str, str | None] = bed_crops or {str(i): None for i in range(bed_count)}
self.refresh()

@property
Expand Down Expand Up @@ -135,13 +139,14 @@ def _generate_rows(self) -> list[Row]:
offset_row_coordinated = offset_curve(ab_line_cartesian, -offset).coords
row_points: list[GeoPoint] = [localization.reference.shifted(
Point(x=p[0], y=p[1])) for p in offset_row_coordinated]
row = Row(id=f'field_{self.id}_row_{i + 1!s}', name=f'row_{i + 1}', points=row_points)
row = Row(id=f'field_{self.id}_row_{i + 1!s}', name=f'row_{i + 1}',
points=row_points, crop=self.bed_crops[str(bed_index)])
rows.append(row)
return rows

def _generate_outline(self) -> list[GeoPoint]:
assert len(self.rows) > 0
return self.get_buffered_area(self.rows, self.outline_buffer_width)
return self.get_buffered_area()

def to_dict(self) -> dict:
return {
Expand All @@ -155,6 +160,7 @@ def to_dict(self) -> dict:
'row_support_points': [rosys.persistence.to_dict(sp) for sp in self.row_support_points],
'bed_count': self.bed_count,
'bed_spacing': self.bed_spacing,
'bed_crops': self.bed_crops,
}

def shapely_polygon(self) -> shapely.geometry.Polygon:
Expand All @@ -173,7 +179,8 @@ def args_from_dict(cls, data: dict[str, Any]) -> dict:
'outline_buffer_width': 1,
'row_support_points': [],
'bed_count': 1,
'bed_spacing': 1
'bed_spacing': 1,
'bed_crops': {}
}
for key in defaults:
if key in data:
Expand All @@ -194,7 +201,7 @@ def from_dict(cls, data: dict[str, Any]) -> Self:
field_data.id = str(uuid.uuid4())
return field_data

def get_buffered_area(self, rows: list[Row], buffer_width: float) -> list[GeoPoint]:
def get_buffered_area(self) -> list[GeoPoint]:
outline_unbuffered: list[Point] = [
self.first_row_end.cartesian(),
self.first_row_start.cartesian()
Expand Down
39 changes: 30 additions & 9 deletions field_friend/automations/field_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ def __init__(self) -> None:
self.FIELD_SELECTED = rosys.event.Event()
"""A field has been selected."""

self.FIELDS_CHANGED.register(self.refresh_fields)
self.FIELD_SELECTED.register(self.clear_selected_beds)

self._only_specific_beds: bool = False
self._selected_beds: list[int] = []

Expand All @@ -37,7 +34,7 @@ def selected_beds(self, value: list[int]) -> None:
def backup(self) -> dict:
return {
'fields': {f.id: f.to_dict() for f in self.fields},
'selected_field': self.selected_field.id if self.selected_field else None,
'selected_field': self.selected_field.id if self.selected_field else None
}

def restore(self, data: dict[str, Any]) -> None:
Expand All @@ -47,17 +44,18 @@ def restore(self, data: dict[str, Any]) -> None:
self.fields.append(new_field)
selected_field_id: str | None = data.get('selected_field')
if selected_field_id:
self.selected_field = self.get_field(selected_field_id)
self.FIELD_SELECTED.emit()
self.select_field(selected_field_id)
self.refresh_fields()
self.FIELDS_CHANGED.emit()

def invalidate(self) -> None:
self.request_backup()
self.refresh_fields()
self.FIELDS_CHANGED.emit()
if self.selected_field and self.selected_field not in self.fields:
self.selected_field = None
self._only_specific_beds = False
self.selected_beds = []
self.clear_selected_beds()
self.FIELD_SELECTED.emit()

def get_field(self, id_: str | None) -> Field | None:
Expand Down Expand Up @@ -106,7 +104,9 @@ def refresh_fields(self) -> None:

def select_field(self, id_: str | None) -> None:
self.selected_field = self.get_field(id_)
self.clear_selected_beds()
self.FIELD_SELECTED.emit()
self.request_backup()

def update_field_parameters(self, *,
field_id: str,
Expand All @@ -115,7 +115,8 @@ def update_field_parameters(self, *,
row_spacing: float,
outline_buffer_width: float,
bed_count: int,
bed_spacing: float) -> None:
bed_spacing: float,
bed_crops: dict[str, str | None]) -> None:
field = self.get_field(field_id)
if not field:
self.log.warning('Field with id %s not found. Cannot update parameters.', field_id)
Expand All @@ -126,6 +127,17 @@ def update_field_parameters(self, *,
field.bed_count = bed_count
field.bed_spacing = bed_spacing
field.outline_buffer_width = outline_buffer_width
bed_crops = bed_crops.copy()
if len(bed_crops) < bed_count:
for i in range(bed_count - len(bed_crops)):
bed_crops[str(i+len(field.bed_crops))] = None
field.bed_crops = bed_crops
elif len(bed_crops) > bed_count:
for i in range(len(bed_crops) - bed_count):
bed_crops.pop(str(bed_count + i))
field.bed_crops = bed_crops
else:
field.bed_crops = bed_crops
self.log.info('Updated parameters for field %s: row number = %d, row spacing = %f',
field.name, row_count, row_spacing)
self.invalidate()
Expand All @@ -149,4 +161,13 @@ def get_rows_to_work_on(self) -> list[Row]:
for bed in self.selected_beds:
for row_index in range(self.selected_field.row_count):
row_indices.append((bed - 1) * self.selected_field.row_count + row_index)
return [row for i, row in enumerate(self.selected_field.rows) if i in row_indices]
rows_to_work_on = [row for i, row in enumerate(self.selected_field.rows) if i in row_indices]
return rows_to_work_on

def is_row_in_selected_beds(self, row_index: int) -> bool:
if not self._only_specific_beds:
return True
if self.selected_field is None:
return False
bed_index = row_index // self.selected_field.row_count + 1
return bed_index in self.selected_beds
8 changes: 4 additions & 4 deletions field_friend/automations/implements/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
from .weeding_screw import WeedingScrew

__all__ = [
'ExternalMower',
'Implement',
'WeedingImplement',
'ImplementException',
'Recorder',
'WeedingScrew',
'Tornado',
'ExternalMower',
'ImplementException',
'WeedingImplement',
'WeedingScrew',
]
8 changes: 4 additions & 4 deletions field_friend/automations/navigation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from .straight_line_navigation import StraightLineNavigation

__all__ = [
'CrossglideDemoNavigation',
'FieldNavigation',
'FollowCropsNavigation',
'Navigation',
'WorkflowException',
'StraightLineNavigation',
'FollowCropsNavigation',
'FieldNavigation',
'CrossglideDemoNavigation',
'WorkflowException',
]
29 changes: 22 additions & 7 deletions field_friend/automations/navigation/field_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from ..field import Field, Row
from ..implements.implement import Implement
from ..implements.weeding_implement import WeedingImplement
from .straight_line_navigation import StraightLineNavigation

if TYPE_CHECKING:
Expand Down Expand Up @@ -77,12 +78,12 @@ async def prepare(self) -> bool:
if not len(row.points) >= 2:
rosys.notify(f'Row {idx} on field {self.field.name} has not enough points', 'negative')
return False
self.row_index = self.field.rows.index(self.get_nearest_row())
nearest_row = self.get_nearest_row()
if nearest_row is None:
return False
self._state = State.APPROACH_START_ROW
self.plant_provider.clear()

self.automation_watcher.start_field_watch(self.field.outline)

self.log.info(f'Activating {self.implement.name}...')
await self.implement.activate()
return True
Expand All @@ -95,14 +96,17 @@ async def finish(self) -> None:
self.automation_watcher.stop_field_watch()
await self.implement.deactivate()

def get_nearest_row(self) -> Row:
def get_nearest_row(self) -> Row | None:
assert self.field is not None
assert self.gnss.device is not None
row = min(self.field.rows, key=lambda r: r.line_segment().line.foot_point(
self.odometer.prediction.point).distance(self.odometer.prediction.point))
self.log.info(f'Nearest row is {row.name}')
self.row_index = self.field.rows.index(row)
return row
if row not in self.rows_to_work_on:
rosys.notify('Please place the robot in front of a selected bed\'s row', 'negative')
return None
self.row_index = self.rows_to_work_on.index(row)
return self.rows_to_work_on[self.row_index]

def set_start_and_end_points(self):
assert self.field is not None
Expand Down Expand Up @@ -153,6 +157,7 @@ async def _drive(self, distance: float) -> None:

async def _run_approach_start_row(self) -> State:
self.robot_in_working_area = False
rosys.notify(f'Approaching row {self.current_row.name}')
self.set_start_and_end_points()
if self.start_point is None or self.end_point is None:
return State.ERROR
Expand All @@ -177,6 +182,7 @@ async def _run_approach_start_row(self) -> State:
assert self.end_point is not None
driving_yaw = self.odometer.prediction.direction(self.end_point)
await self.turn_in_steps(driving_yaw)
self._set_cultivated_crop()
return State.FOLLOW_ROW

async def _run_change_row(self) -> State:
Expand All @@ -189,7 +195,6 @@ async def _run_change_row(self) -> State:
self.create_simulation()
else:
self.plant_provider.clear()

await self.gnss.ROBOT_POSE_LOCATED.emitted(self._max_gnss_waiting_time)
# turn towards row start
assert self.start_point is not None
Expand All @@ -202,6 +207,7 @@ async def _run_change_row(self) -> State:
assert self.end_point is not None
driving_yaw = self.odometer.prediction.direction(self.end_point)
await self.turn_in_steps(driving_yaw)
self._set_cultivated_crop()
return State.FOLLOW_ROW

async def drive_in_steps(self, target: Pose) -> None:
Expand Down Expand Up @@ -272,6 +278,15 @@ async def _run_row_completed(self) -> State:
next_state = State.FIELD_COMPLETED
return next_state

def _set_cultivated_crop(self) -> None:
if not isinstance(self.implement, WeedingImplement):
return
if self.implement.cultivated_crop == self.current_row.crop:
return
rosys.notify(f'Setting crop {self.current_row.crop} for {self.implement.name}')
self.implement.cultivated_crop = self.current_row.crop
self.implement.request_backup()

def _is_in_working_area(self, start_point: Point, end_point: Point) -> bool:
# TODO: check if in working rectangle, current just checks if between start and stop
relative_start = self.odometer.prediction.relative_point(start_point)
Expand Down
29 changes: 26 additions & 3 deletions field_friend/automations/plant_locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
from .plant import Plant

WEED_CATEGORY_NAME = ['coin', 'weed', 'weedy_area', ]
CROP_CATEGORY_NAME = ['coin_with_hole', 'borrietsch', 'estragon', 'feldsalat', 'garlic', 'jasione', 'kohlrabi', 'liebstoeckel', 'maize', 'minze', 'onion',
'oregano_majoran', 'pastinake', 'petersilie', 'pimpinelle', 'red_beet', 'salatkopf', 'schnittlauch', 'sugar_beet', 'thymian_bohnenkraut', 'zitronenmelisse', ]
CROP_CATEGORY_NAME: dict[str, str] = {}
MINIMUM_CROP_CONFIDENCE = 0.3
MINIMUM_WEED_CONFIDENCE = 0.3

Expand Down Expand Up @@ -39,7 +38,7 @@ def __init__(self, system: 'System') -> None:
self.autoupload: Autoupload = Autoupload.DISABLED
self.upload_images: bool = False
self.weed_category_names: list[str] = WEED_CATEGORY_NAME
self.crop_category_names: list[str] = CROP_CATEGORY_NAME
self.crop_category_names: dict[str, str] = CROP_CATEGORY_NAME
self.minimum_crop_confidence: float = MINIMUM_CROP_CONFIDENCE
self.minimum_weed_confidence: float = MINIMUM_WEED_CONFIDENCE
rosys.on_repeat(self._detect_plants, 0.01) # as fast as possible, function will sleep if necessary
Expand All @@ -50,6 +49,7 @@ def __init__(self, system: 'System') -> None:
self.teltonika_router = system.teltonika_router
self.teltonika_router.CONNECTION_CHANGED.register(self.set_upload_images)
self.teltonika_router.MOBILE_UPLOAD_PERMISSION_CHANGED.register(self.set_upload_images)
rosys.on_startup(self.get_crop_names)

def backup(self) -> dict:
self.log.info(f'backup: autoupload: {self.autoupload}')
Expand Down Expand Up @@ -216,3 +216,26 @@ def set_upload_images(self):
self.upload_images = True
else:
self.upload_images = False

async def get_crop_names(self) -> dict[str, str]:
if isinstance(self.detector, rosys.vision.DetectorSimulation):
simulated_crop_names: list[str] = ['coin_with_hole', 'borrietsch', 'estragon', 'feldsalat', 'garlic', 'jasione', 'kohlrabi', 'liebstoeckel', 'maize', 'minze', 'onion',
'oregano_majoran', 'pastinake', 'petersilie', 'pimpinelle', 'red_beet', 'salatkopf', 'schnittlauch', 'sugar_beet', 'thymian_bohnenkraut', 'zitronenmelisse', ]

CROP_CATEGORY_NAME.update({name: name.replace('_', ' ').title() for name in simulated_crop_names})
self.crop_category_names = CROP_CATEGORY_NAME
return CROP_CATEGORY_NAME
port = self.detector.port
url = f'http://localhost:{port}/about'
async with aiohttp.request('GET', url) as response:
if response.status != 200:
self.log.error(f'Could not get crop names on port {port} - status code: {response.status}')
return {}
response_text = await response.json()
crop_names: list[str] = [category['name'] for category in response_text['model_info']['categories']]
weeds = ['weed', 'weedy_area', 'coin', 'danger', 'big_weed']
for weed in weeds:
crop_names.remove(weed)
CROP_CATEGORY_NAME.update({name: name.replace('_', ' ').title() for name in crop_names})
self.crop_category_names = CROP_CATEGORY_NAME
return CROP_CATEGORY_NAME
6 changes: 3 additions & 3 deletions field_friend/interface/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

__all__ = [
'CameraCard',
'create_development_ui',
'create_header',
'create_status_drawer',
'LeafletMap',
'Monitoring',
'Operation',
'RobotScene',
'create_development_ui',
'create_header',
'create_status_drawer',
]
Loading
Loading