Skip to content

Commit

Permalink
Merge commit 'ac9d19e1ce203e0e858bb5ba761a07b22ce44787' into navigati…
Browse files Browse the repository at this point in the history
…on-gnss
  • Loading branch information
rodja committed Jul 17, 2024
2 parents caab61e + ac9d19e commit 93312dd
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 63 deletions.
3 changes: 1 addition & 2 deletions field_friend/automations/automation_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ def __init__(self, system: 'System') -> None:
rosys.on_repeat(self.try_resume, 0.1)
rosys.on_repeat(self.check_field_bounds, 1.0)
if self.field_friend.bumper:
self.field_friend.bumper.BUMPER_TRIGGERED.register(
lambda name: self.pause(f'Bumper {name} was triggered'))
self.field_friend.bumper.BUMPER_TRIGGERED.register(lambda name: self.pause(f'Bumper {name} was triggered'))
self.gnss.GNSS_CONNECTION_LOST.register(lambda: self.pause('GNSS connection lost'))
self.gnss.RTK_FIX_LOST.register(lambda: self.pause('GNSS RTK fix lost'))

Expand Down
1 change: 1 addition & 0 deletions field_friend/automations/implements/tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ async def start_workflow(self) -> None:
self.system.detector.simulated_objects = [obj for obj in self.system.detector.simulated_objects
if not (inner_radius <= obj.position.projection().distance(punch_position) <= outer_radius)]
self.log.info(f'simulated_objects2: {len(self.system.detector.simulated_objects)}')
return True
except PuncherException:
self.log.error('Error in Tornado Workflow')
except Exception as e:
Expand Down
4 changes: 2 additions & 2 deletions field_friend/automations/implements/weeding_screw.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async def start_workflow(self) -> None:
self.system.detector.simulated_objects = [
obj for obj in self.system.detector.simulated_objects
if obj.position.projection().distance(punch_position) > self.system.field_friend.DRILL_RADIUS]
# NOTE no weeds to work on at this position -> advance robot
return True # NOTE no weeds to work on at this position -> advance robot
except Exception as e:
raise ImplementException(f'Error in Weed Screw Workflow: {e}') from e

Expand All @@ -49,7 +49,7 @@ async def get_stretch(self, max_distance: float) -> float:
if not weeds_in_range:
self.log.info('No weeds in range')
return self.WORKING_DISTANCE
# self.log.info(f'Found {len(weeds_in_range)} weeds in range: {weeds_in_range}')
self.log.info(f'Found {len(weeds_in_range)} weeds in range: {weeds_in_range}')
for next_weed_id, next_weed_position in weeds_in_range.items():
# next_weed_position.x += 0.01 # NOTE somehow this helps to mitigate an offset we experienced in the tests
weed_world_position = self.system.odometer.prediction.transform(next_weed_position)
Expand Down
30 changes: 11 additions & 19 deletions field_friend/automations/navigation/follow_crops_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ async def finish(self) -> None:
await self.implement.deactivate()

async def _drive(self, distance: float):
row = self.plant_provider.get_relevant_crops(self.odometer.prediction.point)
if len(row) >= 2:
row = self.plant_provider.get_relevant_crops(self.odometer.prediction.point, max_distance=1.0)
if len(row) >= 3:
points_array = np.array([(p.position.x, p.position.y) for p in row])
# Fit a line using least squares
A = np.vstack([points_array[:, 0], np.ones(len(points_array))]).T
Expand All @@ -50,23 +50,14 @@ async def _drive(self, distance: float):
if np.abs(yaw - self.odometer.prediction.yaw) > math.pi / 2:
yaw = yaw + math.pi

# Calculate a point 0.3 meters in front of the robot along the line
x_front = self.odometer.prediction.point.x + 0.3 * np.cos(yaw)
y_front = m * x_front + c
point_front = np.array([x_front, y_front])

# Calculate the desired yaw angle from the robot's current position to the front point
delta_x = point_front[0] - self.odometer.prediction.point.x
delta_y = point_front[1] - self.odometer.prediction.point.y
yaw_of_row = np.arctan2(delta_y, delta_x)
fitted_line = rosys.geometry.Line(a=m, b=-1, c=c)
closest_point = fitted_line.foot_point(self.odometer.prediction.point)
front_point = closest_point.polar(0.3, yaw)
target_yaw = self.odometer.prediction.point.direction(front_point)
else:
yaw_of_row = self.odometer.prediction.yaw
target_yaw = self.combine_angles(yaw_of_row, self.crop_attraction, self.odometer.prediction.yaw)
# self.log.info(f'following crops with target yaw {target_yaw}')
target = self.odometer.prediction.point.polar(distance, target_yaw)
# self.log.info(f'Current world position: {self.odometer.prediction} Target next crop at {target}')
with self.driver.parameters.set(linear_speed_limit=0.125, angular_speed_limit=0.1):
await self.driver.drive_to(target)
target_yaw = self.odometer.prediction.yaw
target_yaw = self.combine_angles(target_yaw, self.crop_attraction, self.odometer.prediction.yaw)
await self._drive_to_yaw(distance, target_yaw)

def combine_angles(self, angle1: float, influence: float, angle2: float) -> float:
weight1 = influence
Expand All @@ -83,7 +74,8 @@ def combine_angles(self, angle1: float, influence: float, angle2: float) -> floa
def create_simulation(self):
for i in range(100):
x = i/10.0
p = rosys.geometry.Point3d(x=x, y=np.sin(x/2), z=0)
p = rosys.geometry.Point3d(x=x, y=(x/4) ** 3, z=0)
p = self.odometer.prediction.transform3d(p)
self.detector.simulated_objects.append(rosys.vision.SimulatedObject(category_name='maize', position=p))

def _should_finish(self) -> bool:
Expand Down
23 changes: 18 additions & 5 deletions field_friend/automations/navigation/navigation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import abc
import asyncio
import logging
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -31,6 +32,8 @@ def __init__(self, system: 'System', implement: Implement) -> None:
self.detector = system.detector
self.name = 'Unknown'
self.start_position = self.odometer.prediction.point
self.linear_speed_limit = 0.125
self.angular_speed_limit = 0.1

async def start(self) -> None:
try:
Expand Down Expand Up @@ -75,11 +78,21 @@ async def finish(self) -> None:

@abc.abstractmethod
async def _drive(self, distance: float) -> None:
"""Drives the vehicle forward
This should only advance the robot by a small distance, e.g. 2 cm
to allow for adjustments and observations.
"""
"""Drives the vehicle a short distance forward"""

async def _drive_to_yaw(self, distance: float, yaw: float):
deadline = rosys.time() + 2
start_position = self.odometer.prediction.point
yaw -= self.odometer.prediction.yaw # take current yaw into account and only steer the difference
with self.driver.parameters.set(linear_speed_limit=self.linear_speed_limit, angular_speed_limit=self.angular_speed_limit):
await self.driver.wheels.drive(*self.driver._throttle(1, yaw)) # pylint: disable=protected-access
try:
while self.odometer.prediction.point.distance(start_position) < distance:
if rosys.time() >= deadline:
raise TimeoutError('Driving Timeout')
await rosys.sleep(0.01)
finally:
await self.driver.wheels.stop()

@abc.abstractmethod
def _should_finish(self) -> bool:
Expand Down
20 changes: 12 additions & 8 deletions field_friend/automations/navigation/straight_line_navigation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
from random import randint
from typing import TYPE_CHECKING, Any

Expand All @@ -20,12 +21,14 @@ def __init__(self, system: 'System', tool: Implement) -> None:
self.detector = system.detector
self.length = 2.0
self.name = 'Straight Line'
self.linear_speed_limit = 0.125
self.angular_speed_limit = 0.1
self.origin: rosys.geometry.Point
self.target: rosys.geometry.Point

async def prepare(self) -> bool:
await super().prepare()
self.log.info(f'Activating {self.implement.name}...')
self.origin = self.odometer.prediction.point
self.target = self.odometer.prediction.transform(rosys.geometry.Point(x=self.length, y=0))
await self.implement.activate()
return True

Expand All @@ -34,14 +37,15 @@ async def finish(self) -> None:
await self.implement.deactivate()

async def _drive(self, distance: float):
target = self.odometer.prediction.transform(rosys.geometry.Point(x=distance, y=0))
# self.log.info(f'driving to {target}')
with self.driver.parameters.set(linear_speed_limit=self.linear_speed_limit, angular_speed_limit=self.angular_speed_limit):
await self.driver.drive_to(target)
start_position = self.odometer.prediction.point
closest_point = rosys.geometry.Line.from_points(self.origin, self.target).foot_point(start_position)
local_target = rosys.geometry.Pose(x=closest_point.x, y=closest_point.y, yaw=start_position.direction(self.target), time=0) \
.transform(rosys.geometry.Point(x=1, y=0))
await self._drive_to_yaw(distance, start_position.direction(local_target))

def _should_finish(self):
distance = self.odometer.prediction.point.distance(self.start_position)
return abs(distance - self.length) < 0.05
end_pose = rosys.geometry.Pose(x=self.target.x, y=self.target.y, yaw=self.origin.direction(self.target), time=0)
return end_pose.relative_point(self.odometer.prediction.point).x > 0

def create_simulation(self):
crop_distance = 0.2
Expand Down
15 changes: 7 additions & 8 deletions field_friend/automations/plant_locator.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import asyncio
import logging
from typing import TYPE_CHECKING, Any, Optional
import aiohttp
from typing import TYPE_CHECKING, Any

import aiohttp
import rosys
from nicegui import ui
from nicegui.events import ValueChangeEventArguments
from rosys.vision import Autoupload

from ..vision import SimulatedCam
Expand Down Expand Up @@ -38,7 +36,7 @@ def __init__(self, system: 'System') -> None:
self.tags: list[str] = []
self.is_paused = True
self.autoupload: Autoupload = Autoupload.DISABLED
self.upload_images: bool = False
self.upload_images: bool = False
self.weed_category_names: list[str] = WEED_CATEGORY_NAME
self.crop_category_names: list[str] = CROP_CATEGORY_NAME
self.minimum_weed_confidence: float = MINIMUM_WEED_CONFIDENCE
Expand Down Expand Up @@ -140,14 +138,14 @@ def resume(self) -> None:

async def get_outbox_mode(self, port: int) -> bool:
# TODO: not needed right now, but can be used when this code is moved to the DetectorHardware code
url = f'http://localhost:{port}/outbox_mode'
url = f'http://localhost:{port}/outbox_mode'
async with aiohttp.request('GET', url) as response:
if response.status != 200:
self.log.error(f'Could not get outbox mode on port {port} - status code: {response.status}')
return None
response_text = await response.text()
return response_text == 'continuous_upload'

async def set_outbox_mode(self, value: bool, port: int) -> None:
url = f'http://localhost:{port}/outbox_mode'
async with aiohttp.request('PUT', url, data='continuous_upload' if value else 'stopped') as response:
Expand All @@ -172,7 +170,8 @@ def settings_ui(self) -> None:
.bind_value(self, 'autoupload') \
.classes('w-24').tooltip('Set the autoupload for the weeding automation')
if isinstance(self.detector, rosys.vision.DetectorHardware):
ui.checkbox('Upload images', on_change=self.request_backup).bind_value(self, 'upload_images').on('click', lambda: self.set_outbox_mode(value=self.upload_images, port=self.detector.port))
ui.checkbox('Upload images', on_change=self.request_backup).bind_value(self, 'upload_images') \
.on('click', lambda: self.set_outbox_mode(value=self.upload_images, port=self.detector.port))

@ui.refreshable
def chips():
Expand Down
2 changes: 1 addition & 1 deletion field_friend/hardware/field_friend_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self, robot_id) -> None:
self.CHOP_RADIUS = config_params['chop_radius']
else:
raise NotImplementedError(f'Unknown FieldFriend tool: {tool}')
wheels = rosys.hardware.WheelsSimulation()
wheels = rosys.hardware.WheelsSimulation(self.WHEEL_DISTANCE)

y_axis: YAxisSimulation | ChainAxisSimulation | None
if config_hardware['y_axis']['version'] == 'chain_axis':
Expand Down
5 changes: 3 additions & 2 deletions field_friend/localization/gnss_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

class GnssSimulation(Gnss):

def __init__(self, odometer: rosys.driving.Odometer) -> None:
def __init__(self, odometer: rosys.driving.Odometer, wheels: rosys.hardware.WheelsSimulation) -> None:
super().__init__(odometer, 0.0)
self.wheels = wheels
self.allow_connection = True
self.gps_quality = 4

Expand All @@ -18,7 +19,7 @@ async def try_connection(self) -> None:
self.device = 'simulation'

async def _create_new_record(self) -> Optional[GNSSRecord]:
pose = self.odometer.prediction
pose = self.wheels.pose
reference = self.reference if self.reference else GeoPoint(lat=51.983159, long=7.434212)
new_position = reference.shifted(pose.point)
record = GNSSRecord(timestamp=pose.time, location=new_position)
Expand Down
3 changes: 2 additions & 1 deletion field_friend/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def __init__(self) -> None:
assert isinstance(self.field_friend, FieldFriendHardware)
self.gnss = GnssHardware(self.odometer, self.field_friend.ANTENNA_OFFSET)
else:
self.gnss = GnssSimulation(self.odometer)
assert isinstance(self.field_friend.wheels, rosys.hardware.WheelsSimulation)
self.gnss = GnssSimulation(self.odometer, self.field_friend.wheels)
self.gnss.ROBOT_POSE_LOCATED.register(self.odometer.handle_detection)
self.driver = rosys.driving.Driver(self.field_friend.wheels, self.odometer)
self.driver.parameters.linear_speed_limit = 0.3
Expand Down
3 changes: 2 additions & 1 deletion tests/test_field_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@


def test_loading_from_old_persistence():
field_provider = FieldProvider(GnssSimulation(rosys.driving.Odometer(rosys.hardware.WheelsSimulation())))
wheels = rosys.hardware.WheelsSimulation()
field_provider = FieldProvider(GnssSimulation(rosys.driving.Odometer(wheels), wheels))
field_provider.restore(json.loads(Path('tests/old_field_provider_persistence.json').read_text()))
assert len(field_provider.fields) == 3
field = field_provider.fields[1]
Expand Down
3 changes: 2 additions & 1 deletion tests/test_gnss.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import pytest
from copy import deepcopy

import pytest
import rosys
from conftest import ROBOT_GEO_START_POSITION
from rosys.testing import assert_point, forward
Expand Down Expand Up @@ -76,6 +76,7 @@ async def empty():
assert_point(gnss_driving.odometer.prediction.point, rosys.geometry.Point(x=2, y=0))


@pytest.mark.skip('does not work anymore due to gps using wheels.pose instead of odometry.pose')
async def test_changing_reference(gnss_driving: System):
assert gnss_driving.gnss.current is not None
assert gnss_driving.gnss.reference is not None
Expand Down
Loading

0 comments on commit 93312dd

Please sign in to comment.