Skip to content

Commit dc75e8b

Browse files
authored
Development Page, Key Performance Indicators and Interface File Structure (#19)
* development box and drawer minimalization * battery status in header bar * manual steerer dialog styling * dev_page and header_bar styling * KPI-logging basics * robot info dialog * logging kpis of mowing * kpi in status_drawer preparation * kpi for automations * main as router, restructuring interface folder, added pages files, kpi logic: kpi_generator, kpi_provider and kpi_page * page routing, page_wrapper and dev_page * conditional rendering depending on automation type * fixing import error * sorting rows error handled, join Y and Z axis info in status * renamed UsbCamProvider to CalibratableCameraProvider * added error kpis to dataclass, page route comment, fixed redundant check
1 parent 56a54f7 commit dc75e8b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1066
-550
lines changed

field_friend/automations/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .coin_collecting import CoinCollecting
55
from .coverage_planer import CoveragePlanner
66
from .field_provider import Field, FieldObstacle, FieldProvider, Row
7+
from .kpi_provider import KpiProvider
78
from .mowing import Mowing
89
from .path_provider import Path, PathProvider
910
from .path_recorder import PathRecorder

field_friend/automations/coin_collecting.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def __init__(self, system: 'System') -> None:
2121
super().__init__()
2222
self.log = logging.getLogger('field_friend.coin_collecting')
2323
self.system = system
24+
self.kpi_provider = system.kpi_provider
2425
self.work_x: float = 0.0
2526
self.front_x: float = 0.18
2627

@@ -97,6 +98,7 @@ async def _weeding(self) -> None:
9798
continue
9899
except Exception as e:
99100
self.log.exception(f'error while advancing on crop: {e}')
101+
self.kpi_provider.increment('automation_stopped')
100102
break
101103
if self.system.odometer.prediction.point.distance(Point(x=0, y=0)) > 0.1:
102104
self.log.info('returning to start position')
@@ -107,6 +109,7 @@ async def _weeding(self) -> None:
107109
await self.system.driver.drive_to(target)
108110
already_explored = True # avoid infinite loop if there are no crops
109111
finally:
112+
self.kpi_provider.increment('coin_collecting_completed')
110113
await rosys.sleep(0.1)
111114
await self.system.field_friend.stop()
112115

field_friend/automations/field_provider.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
from dataclasses import dataclass, field
2-
from functools import lru_cache
32
from statistics import mean
4-
from typing import Any, List, Literal, Optional, TypedDict, Union
3+
from typing import Any, Literal, Optional, TypedDict, Union
54

6-
import geopandas as gpd
75
import rosys
86
from geographiclib.geodesic import Geodesic
97
from rosys.geometry import Point
10-
from shapely.geometry import LineString
8+
from shapely.geometry import LineString, Polygon
119

1210
from field_friend.navigation.point_transformation import wgs84_to_cartesian
1311

@@ -198,6 +196,15 @@ def is_polygon(self, field: Field) -> bool:
198196
# the function need to be extended for more special cases
199197

200198
def sort_rows(self, field: Field) -> None:
199+
if len(field.rows) <= 1:
200+
rosys.notify(f'There are not enough rows that can be sorted.', type='warning')
201+
return
202+
203+
for row in field.rows:
204+
if len(row.points_wgs84) < 1:
205+
rosys.notify(f'Row {row.name} has to few points. Sorting not possible.', type='warning')
206+
return
207+
201208
def get_centroid(row: Row) -> Point:
202209
polyline = LineString(row.points_wgs84)
203210
return polyline.centroid
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from dataclasses import dataclass, field
2+
from typing import Any
3+
4+
import rosys
5+
from rosys.analysis import KpiLogger
6+
7+
from .plant_provider import PlantProvider
8+
9+
10+
@dataclass(slots=True, kw_only=True)
11+
class KPIs:
12+
distance: int = 0
13+
time: int = 0
14+
bumps: int = 0
15+
low_battery: int = 0
16+
can_failure: int = 0
17+
automation_stopped: int = 0
18+
e_stop_triggered: int = 0
19+
soft_e_stop_triggered: int = 0
20+
imu_rolling_detected: int = 0
21+
gnss_connection_lost: int = 0
22+
23+
24+
@dataclass(slots=True, kw_only=True)
25+
class Weeding_KPIs(KPIs):
26+
rows_weeded: int = 0
27+
crops_detected: int = 0
28+
weeds_detected: int = 0
29+
punches: int = 0
30+
weeding_completed: bool = False
31+
32+
33+
@dataclass(slots=True, kw_only=True)
34+
class Mowing_KPIs(KPIs):
35+
mowing_completed: bool = False
36+
37+
38+
class KpiProvider(KpiLogger):
39+
def __init__(self, plant_provider: PlantProvider) -> None:
40+
super().__init__()
41+
self.plant_provider = plant_provider
42+
43+
self.plant_provider.ADDED_NEW_CROP.register(lambda: self.increment_weeding_kpi('crops_detected'))
44+
self.plant_provider.ADDED_NEW_WEED.register(lambda: self.increment_weeding_kpi('weeds_detected'))
45+
46+
self.current_weeding_kpis: Weeding_KPIs = Weeding_KPIs()
47+
self.WEEDING_KPIS_UPDATED = rosys.event.Event()
48+
"""one of the KPIs of the running weeding automation has been updated."""
49+
50+
self.current_mowing_kpis: Mowing_KPIs = Mowing_KPIs()
51+
self.MOWING_KPIS_UPDATED = rosys.event.Event()
52+
"""one of the KPIs of the running mowing automation has been updated."""
53+
54+
self.needs_backup: bool = False
55+
56+
def backup(self) -> dict:
57+
logger_backup = super().backup()
58+
return {'current_weeding_kpis': rosys.persistence.to_dict(self.current_weeding_kpis),
59+
'current_mowing_kpis': rosys.persistence.to_dict(self.current_mowing_kpis),
60+
'days': logger_backup['days'],
61+
'months': logger_backup['months']}
62+
63+
def restore(self, data: dict[str, Any]) -> None:
64+
super().restore(data)
65+
rosys.persistence.replace_dataclass(self.current_weeding_kpis, data.get('current_weeding_kpis', Weeding_KPIs()))
66+
rosys.persistence.replace_dataclass(self.current_mowing_kpis, data.get('current_mowing_kpis', Mowing_KPIs()))
67+
68+
def invalidate(self) -> None:
69+
self.request_backup()
70+
self.WEEDING_KPIS_UPDATED.emit()
71+
self.MOWING_KPIS_UPDATED.emit()
72+
73+
def increment_weeding_kpi(self, indicator: str) -> None:
74+
self.increment(indicator)
75+
new_value = getattr(self.current_weeding_kpis, indicator)+1
76+
setattr(self.current_weeding_kpis, indicator, new_value)
77+
self.WEEDING_KPIS_UPDATED.emit()
78+
self.invalidate()
79+
return
80+
81+
def increment_mowing_kpi(self, indicator: str) -> None:
82+
self.increment(indicator)
83+
new_value = getattr(self.current_mowing_kpis, indicator)+1
84+
setattr(self.current_mowing_kpis, indicator, new_value)
85+
self.MOWING_KPIS_UPDATED.emit()
86+
self.invalidate()
87+
88+
def clear_weeding_kpis(self) -> None:
89+
self.current_weeding_kpis = Weeding_KPIs()
90+
self.WEEDING_KPIS_UPDATED.emit()
91+
self.invalidate()
92+
93+
def clear_mowing_kpis(self) -> None:
94+
self.current_mowing_kpis = Mowing_KPIs()
95+
self.MOWING_KPIS_UPDATED.emit()
96+
self.invalidate()

field_friend/automations/mowing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, system: 'System', *, robot_width: float) -> None:
2828
self.gnss = system.gnss
2929
self.system = system
3030
self.coverage_planner = CoveragePlanner(self)
31+
self.kpi_provider = system.kpi_provider
3132

3233
self.padding: float = 1.0
3334
self.lane_distance: float = 0.5
@@ -112,10 +113,12 @@ async def _mowing(self) -> None:
112113
raise Exception('No paths to drive')
113114
self.MOWING_STARTED.emit([path_segment for path in self.paths for path_segment in path])
114115
await self._drive_mowing_paths(self.paths)
116+
self.kpi_provider.increment_mowing_kpi('mowing_completed')
115117
rosys.notify('Mowing finished', 'positive')
116118
# break TODO: only for demo
117119
except Exception as e:
118120
self.log.exception(e)
121+
self.kpi_provider.increment('automation_stopped')
119122
rosys.notify(f'Mowing failed because of {e}', 'negative')
120123
break
121124

field_friend/automations/plant_locator.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import logging
33

44
import rosys
5-
65
from .plant_provider import Plant, PlantProvider
76

87
WEED_CATEGORY_NAME = ['coin', 'weed']

field_friend/automations/plant_provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ def __init__(self) -> None:
3333
self.ADDED_NEW_WEED = rosys.event.Event()
3434
"""A new weed has been added."""
3535

36-
self.ADDED_NEW_BEET = rosys.event.Event()
37-
"""A new beet has been added."""
36+
self.ADDED_NEW_CROP = rosys.event.Event()
37+
"""A new crop has been added."""
3838

3939
rosys.on_repeat(self.prune, 10.0)
4040

@@ -73,7 +73,7 @@ def add_crop(self, crop: Plant) -> None:
7373
return
7474
self.crops.append(crop)
7575
self.PLANTS_CHANGED.emit()
76-
self.ADDED_NEW_BEET.emit()
76+
self.ADDED_NEW_CROP.emit()
7777

7878
def remove_crop(self, crop: Plant) -> None:
7979
self.crops[:] = [c for c in self.crops if c.id != crop.id]

field_friend/automations/puncher.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import rosys
44
from rosys.driving import Driver
55
from rosys.geometry import Point
6-
6+
from .kpi_provider import KpiProvider
77
from ..hardware import ChainAxis, FieldFriend, Tornado, YAxis, YAxisTornado, YAxisTornadoV2
88

99

@@ -12,9 +12,10 @@ class PuncherException(Exception):
1212

1313

1414
class Puncher:
15-
def __init__(self, field_friend: FieldFriend, driver: Driver) -> None:
15+
def __init__(self, field_friend: FieldFriend, driver: Driver, kpi_provider: KpiProvider) -> None:
1616
self.field_friend = field_friend
1717
self.driver = driver
18+
self.kpi_provider = kpi_provider
1819
self.log = logging.getLogger('field_friend.puncher')
1920

2021
async def try_home(self) -> bool:
@@ -85,6 +86,7 @@ async def punch(self, y: float, *, depth: float = 0.01, angle: float = 180) -> N
8586
await self.field_friend.z_axis.move_to(depth)
8687
await self.field_friend.z_axis.return_to_reference()
8788
self.log.info(f'punched successfully at {y:.2f} with depth {depth}')
89+
self.kpi_provider.increment_weeding_kpi('punches')
8890
except Exception as e:
8991
raise PuncherException(f'punching failed because: {e}') from e
9092
finally:

field_friend/automations/weeding.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, system: 'System') -> None:
2828

2929
self.log = logging.getLogger('field_friend.weeding')
3030
self.system = system
31+
self.kpi_provider = system.kpi_provider
3132

3233
self.use_field_planning = True
3334
self.field: Optional[Field] = None
@@ -296,8 +297,10 @@ async def _weeding(self):
296297
self.log.info('Planless weeding completed')
297298

298299
except WorkflowException as e:
300+
self.kpi_provider.increment('automation_stopped')
299301
self.log.error(f'WorkflowException: {e}')
300302
finally:
303+
self.kpi_provider.increment_weeding_kpi('weeding_completed')
301304
await self.system.field_friend.stop()
302305
self.system.plant_locator.pause()
303306

field_friend/interface/__init__.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1 @@
1-
from .automation_controls import automation_controls
2-
from .camera_card import camera_card
3-
from .development import development
4-
from .field_friend_object import field_friend_object
5-
from .field_object import field_object
6-
from .field_planner import field_planner
7-
from .hardware_control import hardware_control
8-
from .header_bar import header_bar
9-
from .key_controls import KeyControls
10-
from .leaflet_map import leaflet_map
11-
from .monitoring import monitoring
12-
from .operation import operation
13-
from .path_planner import path_planner
14-
from .plant_object import plant_objects
15-
from .robot_scene import robot_scene
16-
from .status_dev_page import status_dev_page
17-
from .status_drawer import status_drawer
18-
from .system_bar import system_bar
19-
from .test import test
20-
from .visualizer_object import visualizer_object
1+
from . import components, pages

0 commit comments

Comments
 (0)