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

Add PunchDialog option for all weeding implements #90

Merged
merged 10 commits into from
Jul 8, 2024
9 changes: 1 addition & 8 deletions field_friend/automations/implements/tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def __init__(self, system: 'System') -> None:
self.tornado_angle: float = 30.0
self.drill_with_open_tornado: bool = False
self.drill_between_crops: bool = False
self.with_punch_check: bool = False
self.field_friend = system.field_friend

async def start_workflow(self) -> bool:
Expand Down Expand Up @@ -78,31 +77,25 @@ def backup(self) -> dict:
return super().backup() | {
'drill_with_open_tornado': self.drill_with_open_tornado,
'drill_between_crops': self.drill_between_crops,
'with_punch_check': self.with_punch_check,
'tornado_angle': self.tornado_angle,
}

def restore(self, data: dict[str, Any]) -> None:
super().restore(data)
self.drill_with_open_tornado = data.get('drill_with_open_tornado', self.drill_with_open_tornado)
self.drill_between_crops = data.get('drill_between_crops', self.drill_between_crops)
self.with_punch_check = data.get('with_punch_check', self.with_punch_check)
self.tornado_angle = data.get('tornado_angle', self.tornado_angle)

def settings_ui(self):
super().settings_ui()

ui.number('Tornado angle', format='%.0f', step=1, min=0, max=180) \
.props('dense outlined suffix=°') \
.classes('w-24') \
.bind_value(self, 'tornado_angle') \
.tooltip('Set the angle for the tornado drill')
ui.label().bind_text_from(self, 'tornado_angle', lambda v: f'Tornado diameters: {self.field_friend.tornado_diameters(v)[0]*100:.1f} cm '
f'- {self.field_friend.tornado_diameters(v)[1]*100:.1f} cm')
ui.checkbox('With punch check') \
.bind_value(self, 'with_punch_check') \
.tooltip('Set the weeding automation to check for punch')
ui.checkbox('Drill 2x with open torando') \
ui.checkbox('Drill 2x with open tornado') \
.bind_value(self, 'drill_with_open_tornado') \
.tooltip('Set the weeding automation to drill a second time with open tornado')
ui.checkbox('Drill between crops') \
Expand Down
6 changes: 6 additions & 0 deletions field_friend/automations/implements/weeding_implement.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(self, name: str, system: 'System', persistence_key: str = 'weeding
self.kpi_provider = system.kpi_provider
self.puncher = system.puncher
self.cultivated_crop: str | None = None
self.with_punch_check: bool = False

# dual mechanism
self.with_drilling: bool = False
Expand Down Expand Up @@ -159,13 +160,15 @@ def backup(self) -> dict:
'with_chopping': self.with_chopping,
'chop_if_no_crops': self.chop_if_no_crops,
'cultivated_crop': self.cultivated_crop,
'with_punch_check': self.with_punch_check,
}

def restore(self, data: dict[str, Any]) -> None:
self.with_drilling = data.get('with_drilling', self.with_drilling)
self.with_chopping = data.get('with_chopping', self.with_chopping)
self.chop_if_no_crops = data.get('chop_if_no_crops', self.chop_if_no_crops)
self.cultivated_crop = data.get('cultivated_crop', self.cultivated_crop)
self.with_punch_check = data.get('with_punch_check', self.with_punch_check)

def clear(self) -> None:
self.crops_to_handle = {}
Expand Down Expand Up @@ -195,3 +198,6 @@ def settings_ui(self):
ui.select(self.system.crop_category_names, label='cultivated crop', on_change=self.request_backup) \
.bind_value(self, 'cultivated_crop').props('clearable') \
.classes('w-40').tooltip('Set the cultivated crop which should be kept safe')
ui.checkbox('With punch check', value=True) \
.bind_value(self, 'with_punch_check') \
.tooltip('Set the weeding automation to check for punch')
1 change: 1 addition & 0 deletions field_friend/automations/implements/weeding_screw.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ async def start_workflow(self) -> bool:
x=next_weed_position.x,
y=next_weed_position.y,
depth=self.weed_screw_depth,
with_punch_check=self.with_punch_check,
backwards_allowed=False)
punched_weeds = [weed_id for weed_id, position in weeds_in_range.items()
if position.distance(next_weed_position) <= self.system.field_friend.DRILL_RADIUS
Expand Down
5 changes: 4 additions & 1 deletion field_friend/automations/kpi_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ def invalidate(self) -> None:

def increment_weeding_kpi(self, indicator: str) -> None:
self.increment(indicator)
new_value = getattr(self.current_weeding_kpis, indicator)+1
if getattr(self.current_weeding_kpis, indicator) is None:
new_value = 1
else:
new_value = getattr(self.current_weeding_kpis, indicator)+1
setattr(self.current_weeding_kpis, indicator, new_value)
self.WEEDING_KPIS_UPDATED.emit()
self.invalidate()
Expand Down
19 changes: 10 additions & 9 deletions field_friend/automations/puncher.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,19 @@ async def punch(self,
if not self.field_friend.y_axis.min_position <= y <= self.field_friend.y_axis.max_position:
rosys.notify('y position out of range', type='negative')
raise PuncherException('y position out of range')

if with_punch_check and plant_id is not None:
self.punch_allowed = 'waiting'
self.POSSIBLE_PUNCH.emit(plant_id)
while self.punch_allowed == 'waiting':
await rosys.sleep(0.1)
if self.punch_allowed == 'not_allowed':
self.log.warning('punch was not allowed')
return
self.log.info('punching was allowed')

if isinstance(self.field_friend.z_axis, Tornado):
await self.field_friend.y_axis.move_to(y)
if with_punch_check and plant_id is not None:
self.punch_allowed = 'waiting'
self.POSSIBLE_PUNCH.emit(plant_id)
while self.punch_allowed == 'waiting':
await rosys.sleep(0.1)
if self.punch_allowed == 'not_allowed':
self.log.warning('punch was not allowed')
return
self.log.info('punching was allowed')
await self.tornado_drill(angle=angle, turns=turns, with_open_drill=with_open_tornado)

elif isinstance(self.field_friend.z_axis, ZAxis):
Expand Down
4 changes: 1 addition & 3 deletions field_friend/interface/components/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ def __init__(self, system: 'System') -> None:
self.implement_selection.value = self.system.current_implement.name

self.system.puncher.POSSIBLE_PUNCH.register_ui(self.can_punch)
self.punch_dialog = PunchDialog(self.system.usb_camera_provider,
self.system.plant_locator,
self.system.odometer)
self.punch_dialog = PunchDialog(self.system)

async def can_punch(self, plant_id: str) -> None:
self.punch_dialog.target_plant = self.system.plant_provider.get_plant_by_id(plant_id)
Expand Down
40 changes: 21 additions & 19 deletions field_friend/interface/components/punch_dialog.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
from typing import Optional
from typing import TYPE_CHECKING

import rosys
from icecream import ic
from nicegui import ui
from rosys.driving import Odometer
from rosys.geometry import Point3d
from rosys.vision import Image
from rosys.automation import Automator, automation_controls

from ...automations import PlantLocator
from ...automations.plant import Plant
from ...vision import CalibratableUsbCameraProvider, SimulatedCamProvider

if TYPE_CHECKING:
from field_friend.system import System


class PunchDialog(ui.dialog):
def __init__(self, camera_provider: rosys.vision.CameraProvider,
plant_locator: PlantLocator,
odometer: Odometer,
shrink_factor: int = 1,
timeout: float = 20.0,
ui_update_rate: float = 0.2) -> None:
def __init__(self, system: 'System', shrink_factor: int = 1, timeout: float = 20.0, ui_update_rate: float = 0.2) -> None:
super().__init__()
self.camera_provider: CalibratableUsbCameraProvider | SimulatedCamProvider = system.usb_camera_provider
self.plant_locator: PlantLocator = system.plant_locator
self.odometer: Odometer = system.odometer
self.automator: Automator = system.automator
self.shrink_factor: int = shrink_factor
self.timeout: float = timeout
self._duration_left: float = timeout
self.ui_update_rate: float = ui_update_rate

self.camera: Optional[rosys.vision.CalibratableCamera] = None
self.camera_provider = camera_provider
self.plant_locator = plant_locator
self.odometer = odometer
self.shrink_factor = shrink_factor
self.timeout = timeout
self._duration_left = timeout
self.ui_update_rate = ui_update_rate
self.static_image_view: Optional[ui.interactive_image] = None
self.live_image_view: Optional[ui.interactive_image] = None
self.target_plant: Optional[Plant] = None
Expand All @@ -41,13 +43,14 @@ def __init__(self, camera_provider: rosys.vision.CameraProvider,
ui.label('Live').classes('text-lg')
self.live_image_view = ui.interactive_image('')
self.label = ui.label('Do you want to punch at the current position?').classes('text-lg')
with ui.row():
with ui.row().classes('w-full'):
# TODO: doesn't load properly on first open
ui.circular_progress(min=0.0, max=self.timeout,
show_value=False, size='lg').bind_value_from(self, '_duration_left')
ui.button('Yes', on_click=lambda: self.submit('Yes'))
ui.button('No', on_click=lambda: self.submit('No'))
ui.button('Cancel', on_click=lambda: self.submit('Cancel'))
ui.space()
automation_controls(self.automator)

def open(self) -> None:
self._duration_left = self.timeout
Expand Down Expand Up @@ -92,15 +95,14 @@ def update_content(self, image_view: ui.interactive_image, image: Image, draw_ta
relative_point = self.odometer.prediction.relative_point(self.target_plant.position)
target_point = self.camera.calibration.project_to_image(
Point3d(x=relative_point.x, y=relative_point.y, z=0))
ic(f'target plant : {self.target_plant.position} and target point : {target_point}')
image_view.set_content(self.to_svg(image.detections, target_point, confidence))

def update_live_view(self) -> None:
if self.camera is None:
return
self.update_content(self.live_image_view, self.camera.latest_detected_image)

def to_svg(self, detections: rosys.vision.Detections, target_point: Optional[rosys.geometry.Point], confidence: Optional[float], color: str = 'red') -> str:
def to_svg(self, detections: rosys.vision.Detections, target_point: Optional[rosys.geometry.Point], confidence: Optional[float], color: str = 'blue') -> str:
svg = ''
cross_size = 20
for point in detections.points:
Expand Down Expand Up @@ -134,7 +136,7 @@ def to_svg(self, detections: rosys.vision.Detections, target_point: Optional[ros
'''
if target_point and confidence:
svg += f'''<circle cx="{target_point.x / self.shrink_factor}" cy="{target_point.y /
self.shrink_factor}" r="18" stroke-width="8" stroke="{color}" fill="none" />'''
self.shrink_factor}" r="20" stroke-width="8" stroke="{color}" fill="none" />'''
svg += f'''<text x="{target_point.x / self.shrink_factor+20}" y="{target_point.y /
self.shrink_factor-20}" font-size="20" fill="{color}">{confidence}</text>'''
return svg