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

External mower #149

Merged
merged 65 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
800dd60
update calibrate hosqvana script
angelom93 Aug 6, 2024
5f56c03
first implementation of external mower class
angelom93 Aug 6, 2024
dac4bab
add external mower to field freind hardware
angelom93 Aug 6, 2024
e1a6ffd
add external mower buttons to hardware
angelom93 Aug 6, 2024
5547652
add config for f14
angelom93 Aug 6, 2024
1dd17e0
rename f14 config hostname
angelom93 Aug 6, 2024
0e961e1
feat: Add odrive version to hardware configuration
angelom93 Aug 6, 2024
1c54656
update f14 config
angelom93 Aug 6, 2024
ac6f82a
add mowing implement
angelom93 Aug 6, 2024
694536b
turn off motor
angelom93 Aug 6, 2024
8e54279
add mower to safety
angelom93 Aug 6, 2024
60d7bc2
add mower control buttons
angelom93 Aug 6, 2024
ae0bf79
update calibration scripts
angelom93 Aug 6, 2024
6ad83af
add MowerSimulation and a mower implement
pascalzauberzeug Aug 6, 2024
9dfab60
remove unused name parameter
pascalzauberzeug Aug 6, 2024
aba0bc1
fix notify
pascalzauberzeug Aug 6, 2024
ada269a
handle stuck motors
pascalzauberzeug Aug 7, 2024
b9d2961
rework mower hardware control
pascalzauberzeug Aug 7, 2024
8f0b8fa
fix class setup
pascalzauberzeug Aug 7, 2024
098361d
make linear speed adjustable
pascalzauberzeug Aug 7, 2024
9baff91
Improve status_dev page layout and add mower status and reset buttons
angelom93 Aug 7, 2024
dd1b419
rework motor reset routine
pascalzauberzeug Aug 7, 2024
1b33d23
wait for motor startup
pascalzauberzeug Aug 7, 2024
a2184e6
add demo mode
pascalzauberzeug Aug 7, 2024
8fbed3b
wip: remove recorder from mower-robot
pascalzauberzeug Aug 7, 2024
2ae5d93
Merge commit '9baff916c303117e2ef9dc74201ab045b241192c' into external…
pascalzauberzeug Aug 7, 2024
6465c13
only pause gnss pose fix not gnss reading
angelom93 Aug 7, 2024
370e63b
Merge branch 'external_mower' of github.com:zauberzeug/field_friend i…
angelom93 Aug 7, 2024
e0362ec
first implementation of a-b_line navigation
angelom93 Aug 7, 2024
5b6ab4e
further implementation of ab-line navigation
angelom93 Aug 7, 2024
133d96d
Remove unnecessary code in ABLineNavigation class
angelom93 Aug 7, 2024
35484fb
add more logging
angelom93 Aug 7, 2024
3395739
add delayed confirm dialog to start the motor
pascalzauberzeug Aug 8, 2024
a4bd782
add STOP_DISTANCE constant
pascalzauberzeug Aug 8, 2024
ba8c0c5
add row not none assertion
pascalzauberzeug Aug 8, 2024
e176e8d
choose nearest row with footpoint
angelom93 Aug 9, 2024
14b9d76
move constants to inside of the navigation class
pascalzauberzeug Aug 9, 2024
ea5c3f3
change mower stretch length
pascalzauberzeug Aug 9, 2024
79621ae
make speed adjustable
pascalzauberzeug Aug 9, 2024
e912fff
_drive_to_yaw approach
pascalzauberzeug Aug 9, 2024
a5e151e
ensure we dont overshot target
angelom93 Aug 9, 2024
bd2d6f5
change mower stretch to 1.0m
angelom93 Aug 9, 2024
ec015ed
Merge branch 'external_mower' of github.com:zauberzeug/field_friend i…
angelom93 Aug 9, 2024
d063faa
cleanup and fix merge
angelom93 Aug 9, 2024
60c3b74
fix mower hardware class to use correct odrive version
angelom93 Aug 9, 2024
3804844
Add gnss.is_paused info to dev page
pascalzauberzeug Aug 9, 2024
cc98f7d
remove ensure_field_reference
angelom93 Aug 9, 2024
d914351
use drive_to function
angelom93 Aug 9, 2024
edab368
Merge branch 'external_mower' of github.com:zauberzeug/field_friend i…
angelom93 Aug 9, 2024
362881b
cleanup
angelom93 Aug 9, 2024
52641a6
request on_change backup for settings
pascalzauberzeug Aug 9, 2024
219aeef
Merge commit '362881b8332f5c3d4946b642582edf2cb1706ab2' into external…
pascalzauberzeug Aug 9, 2024
b4f8619
can start from both sides
pascalzauberzeug Aug 9, 2024
10c4e35
needed gnss poses as variable
pascalzauberzeug Aug 12, 2024
de452cc
wip: set gnss parameters via mower
pascalzauberzeug Aug 12, 2024
dfb2afa
wip: throttle_at_end=False
pascalzauberzeug Aug 12, 2024
c7d3301
dont stop after driving spline
pascalzauberzeug Aug 13, 2024
14db470
remove gnss from mower
pascalzauberzeug Aug 13, 2024
6778a64
remove unused import
pascalzauberzeug Aug 13, 2024
f8269c8
refactor abline navigation
pascalzauberzeug Aug 13, 2024
38e4174
throttle down at last stretch
pascalzauberzeug Aug 13, 2024
041cc2d
make stretch distance configurable for mower
pascalzauberzeug Aug 13, 2024
07f1aca
Merge commit 'a1048ad2c6786df0e869ecebf7bafb0eab3cb954' into external…
pascalzauberzeug Aug 19, 2024
1e2fa96
update to rosys 0.14.0
pascalzauberzeug Aug 19, 2024
90406a0
Merge commit 'ac7f16447c80daafbd2fa2e89c04b8dd379471b7' into external…
pascalzauberzeug Aug 28, 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
13 changes: 13 additions & 0 deletions config/f14_config_f14/camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
configuration = {'parameters': {
'width': 1280,
'height': 720,
'auto_exposure': True,
'fps': 10,
},
'crop': {
'left': 60,
'right': 200,
'up': 20,
'down': 0,
}
}
88 changes: 88 additions & 0 deletions config/f14_config_f14/hardware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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': False,
'is_right_reversed': True,
'odrive_version': 6,
},
# # FIXME: PIN checken
# 'eyes': {
# 'name': 'eyes',
# 'on_expander': True,
# 'eyes_pin': 12,
# },
'y_axis': {
'version': 'none',
},
'z_axis': {
'version': 'none',
},
'external_mower': {
'name': 'mower',
'm0_can_address': 0x400,
'm1_can_address': 0x500,
'm2_can_address': 0x600,
'm_per_tick': 0.01,
'speed': 10.0,
'is_m0_reversed': False,
'is_m1_reversed': False,
'is_m2_reversed': False,
'odrive_version': 6,
},
'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,
},
'flashlight': {
'version': 'none',
},
'bumper': {
'name': 'bumper',
'on_expander': True,
'pins': {'front_top': 18, 'front_bottom': 35, 'back': 21},
},
'status_control': {
'name': 'status_control',
},
'bluetooth': {
'name': 'fieldfriend-f14',
},
'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,
},
}
8 changes: 8 additions & 0 deletions config/f14_config_f14/params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
configuration = {
'motor_gear_ratio': 12.52,
'thooth_count': 15,
'pitch': 0.033,
'wheel_distance': 0.47,
'antenna_offset': 0.205,
'tool': 'mower',
}
3 changes: 3 additions & 0 deletions config/f14_config_f14/robotbrain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
configuration = {'robot_brain': {
'flash_params': ['orin', 'v05', 'nand']
}}
2 changes: 0 additions & 2 deletions field_friend/automations/field_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,6 @@ def add_row_point(self, field: Field, row: Row, point: Optional[GeoPoint] = None
rosys.notify("GNSS position is not accurate enough.")
return
new_point = positioning
if self.gnss.device != 'simulation':
self.ensure_field_reference(field)
if point is not None:
index = row.points.index(point)
row.points[index] = new_point
Expand Down
2 changes: 2 additions & 0 deletions field_friend/automations/implements/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .recorder import Recorder
from .tornado import Tornado
from .weeding_screw import WeedingScrew
from .external_mower import ExternalMower

__all__ = [
'Implement',
Expand All @@ -14,5 +15,6 @@
'Recorder',
'WeedingScrew',
'Tornado',
'ExternalMower',
'ImplementException',
]
75 changes: 75 additions & 0 deletions field_friend/automations/implements/external_mower.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@


import logging
from typing import TYPE_CHECKING, Any

import rosys
from nicegui import ui

from ...hardware import MowerHardware
from ..navigation import WorkflowException
from .weeding_implement import Implement

if TYPE_CHECKING:
from system import System


class ExternalMower(Implement, rosys.persistence.PersistentModule):
STRETCH_DISTANCE: float = 2.0

def __init__(self, system: 'System') -> None:
super().__init__('Mower')
self.log = logging.getLogger('field_friend.mower')
self.mower_hardware: MowerHardware = system.field_friend.mower
self.driver = system.driver
assert self.mower_hardware is not None
assert self.driver is not None
self.is_demo: bool = False
self.stretch_distance: float = self.STRETCH_DISTANCE

async def activate(self):
if not self.is_demo:
await self.mower_hardware.turn_on()
await rosys.sleep(2)
await super().activate()

async def deactivate(self):
await self.mower_hardware.turn_off()
await super().deactivate()

async def get_stretch(self, max_distance: float) -> float:
if not any([self.mower_hardware.m0_error, self.mower_hardware.m1_error, self.mower_hardware.m2_error]):
return min(self.stretch_distance, max_distance)
if all([self.mower_hardware.m0_error, self.mower_hardware.m1_error, self.mower_hardware.m2_error]):
rosys.notify('All motors are stuck', 'negative')
raise WorkflowException('All motors are stuck')
# TODO: implement a better error handling
await rosys.sleep(0.1)
self.log.warning('Stuck motor detected')
await self.driver.wheels.stop()
await self.mower_hardware.reset_motors()
await rosys.sleep(5)
await self.mower_hardware.turn_on()
await rosys.sleep(2)
return 0.0

def backup(self) -> dict:
return {
'is_demo': self.is_demo,
'stretch_distance': self.stretch_distance,
}

def restore(self, data: dict[str, Any]) -> None:
super().restore(data)
self.is_demo = data.get('is_demo', self.is_demo)
self.stretch_distance = data.get('stretch_distance', self.stretch_distance)

def settings_ui(self):
ui.checkbox('Demo Mode', on_change=self.request_backup) \
.bind_value(self, 'is_demo') \
.tooltip('Do not start the mowing motors')
ui.number('Stretch Distance', step=0.05, min=0.05, max=100.0, format='%.2f', on_change=self.request_backup) \
.props('dense outlined') \
.classes('w-24') \
.bind_value(self, 'stretch_distance') \
.tooltip(f'Forward speed limit in m/s (default: {self.STRETCH_DISTANCE:.2f})')
5 changes: 4 additions & 1 deletion field_friend/automations/navigation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@

from .a_b_line_navigation import ABLineNavigation
from .coverage_navigation import CoverageNavigation
from .follow_crops_navigation import FollowCropsNavigation
from .navigation import Navigation
from .navigation import Navigation, WorkflowException
from .row_on_field_navigation import RowsOnFieldNavigation
from .straight_line_navigation import StraightLineNavigation

__all__ = [
'Navigation',
'WorkflowException',
'RowsOnFieldNavigation',
'StraightLineNavigation',
'FollowCropsNavigation',
'CoverageNavigation',
'ABLineNavigation',
]
142 changes: 142 additions & 0 deletions field_friend/automations/navigation/a_b_line_navigation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from typing import TYPE_CHECKING, Any

import rosys
from nicegui import ui
from rosys.geometry import Point

from ..field import Field, Row
from ..implements.implement import Implement
from .navigation import Navigation

if TYPE_CHECKING:
from system import System


class ABLineNavigation(Navigation):

def __init__(self, system: 'System', tool: Implement) -> None:
super().__init__(system, tool)
self.MAX_STRETCH_DISTANCE: float = 2.0
self.STOP_DISTANCE: float = 0.1
self.start_position = self.odometer.prediction.point
self.name = 'A-B line'
self.gnss = system.gnss
self.bms = system.field_friend.bms
self.automation_watcher = system.automation_watcher
self.field_provider = system.field_provider
assert self.gnss is not None
assert self.bms is not None
assert self.automation_watcher is not None
assert self.field_provider is not None
self.field: Field | None = None
self.row: Row | None = None
self.start_point: Point | None = None
self.end_point: Point | None = None

async def prepare(self) -> bool:
await super().prepare()
if self.field is None:
rosys.notify('No field selected', 'negative')
return False
if not self.field.rows:
rosys.notify('No rows available', 'negative')
return False
if self.gnss.device is None:
rosys.notify('GNSS is not available', 'negative')
return False
self.row = self.get_nearest_row()
if self.row is None:
rosys.notify('No row found', 'negative')
return False
if not len(self.row.points) >= 2:
rosys.notify(f'Row {self.row.name} on field {self.field.name} has not enough points', 'negative')
return False
self.gnss.is_paused = False
await rosys.sleep(3) # wait for GNSS to update
self.automation_watcher.start_field_watch(self.field.outline)

# determine start and end point
relative_point_0 = self.odometer.prediction.relative_point(self.row.points[0].cartesian())
relative_point_1 = self.odometer.prediction.relative_point(self.row.points[-1].cartesian())
self.log.info(f'{relative_point_0=} - {relative_point_1=}')
self.start_point = None
self.end_point = None
if relative_point_0.x < 0 or relative_point_0.x < relative_point_1.x:
self.start_point = self.row.points[0].cartesian()
self.end_point = self.row.points[-1].cartesian()
elif relative_point_1.x < 0 or relative_point_1.x < relative_point_0.x:
self.start_point = self.row.points[-1].cartesian()
self.end_point = self.row.points[0].cartesian()
assert self.start_point is not None
assert self.end_point is not None
self.log.info(f'Start point: {self.start_point} End point: {self.end_point}')

self.log.info(f'Activating {self.implement.name}...')
await self.implement.activate()
return True

async def finish(self) -> None:
await super().finish()
self.automation_watcher.stop_field_watch()
await self.implement.deactivate()

async def _drive(self, distance: float):
assert self.field is not None
assert self.row is not None
assert self.start_point is not None
assert self.end_point is not None
current_position = self.odometer.prediction.point
direction = self.start_point.direction(self.end_point)
self.log.info(f'line direction: {direction} robot yaw: {self.odometer.prediction.yaw}')
distance_to_end = current_position.distance(self.end_point)
distance = min(distance, distance_to_end)
near_end = distance > distance_to_end
line = self.row.line_segment().line
foot_point = line.foot_point(self.odometer.prediction.point)
target = foot_point.polar(distance, direction)
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, throttle_at_end=near_end, stop_at_end=near_end)

def _should_finish(self) -> bool:
assert self.row is not None
distance = self.odometer.prediction.point.distance(self.end_point)
if distance < self.STOP_DISTANCE:
self.log.info(f'Row {self.row.name} completed')
return True
if self.bms.is_below_percent(20):
self.log.error('Battery is low')
return True
return False

def get_nearest_row(self) -> Row:
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}')
return row

def _set_field(self, field_id: str) -> None:
field = self.field_provider.get_field(field_id)
if field is not None:
self.field = field

def settings_ui(self) -> None:
super().settings_ui()
field_selection = ui.select(
{f.id: f.name for f in self.field_provider.fields if len(f.rows) >= 1 and len(f.points) >= 3},
on_change=lambda args: self._set_field(args.value),
label='Field')\
.classes('w-32') \
.tooltip('Select the field to work on')
field_selection.bind_value_from(self, 'field', lambda f: f.id if f else None)

def backup(self) -> dict:
return super().backup() | {
'field_id': self.field.id if self.field else None,
}

def restore(self, data: dict[str, Any]) -> None:
super().restore(data)
field_id = data.get('field_id', self.field_provider.fields[0].id if self.field_provider.fields else None)
self.field = self.field_provider.get_field(field_id)
3 changes: 2 additions & 1 deletion field_friend/automations/navigation/coverage_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __init__(self, system: 'System', implement: Implement) -> None:
self.continue_mowing: bool = False

def backup(self) -> dict:
return {
return super().backup() | {
'padding': self.padding,
'lane_distance': self.lane_distance,
'paths': [[rosys.persistence.to_dict(segment) for segment in path] for path in self.paths],
Expand All @@ -55,6 +55,7 @@ def backup(self) -> dict:
}

def restore(self, data: dict[str, Any]) -> None:
super().restore(data)
self.padding = data.get('padding', self.padding)
self.lane_distance = data.get('lane_distance', self.lane_distance)
paths_data = data.get('paths', [])
Expand Down
Loading
Loading