Repository navigation
ADR 0061 Mowing Angle Rotation
Accepted (2026-10-28)
A mower that drives the same lines on every run presses permanent tracks into the lawn. The owner asked for the mowing direction to turn on every run ("Mähmuster-Wechsel: Winkel bei jedem Lauf drehen, damit keine Fahrspuren entstehen").
What exists: a schedule can carry a list of angles that its runs take round robin (rotation_angles with rotation_index, ADR 0024). The index belongs to the schedule, so two schedules for the same lawn, a "run now" from another schedule or a manual start from the start sheet do not know about each other, and the user has to type the whole list.
What the mower supports (PyMammotion 0.10.9, the version the adapter pins): the route request NavReqCoverPath (mctrl_nav.proto) carries toward (int32, the path angle in degrees), toward_mode (PathAngleSetting: relative_angle 0, absolute_angle 1, random_angle 2) and toward_included_angle (the second pass of a double grid). GenerateRouteInformation.toward is documented as "is just angle"; MammotionCommand.generate_route_information copies it into NavReqCoverPath.toward. The adapter already maps StartMow.pattern.angle_deg to toward = round(angle) % 180 with toward_mode = absolute_angle and toward_included_angle = 90 (mowdeck_control_mammotion.mapping.route_plan; diagonal adds 45 degrees). So every start can already choose its angle; only the choice per run is missing. The vendor's own "Auto-reverse Mowing Direction" (NavReqCoverPath.auto_change_direction) only flips the direction, needs firmware 2.3.28.1 or later and is excluded for the Luba 1, the LUBA 2 and the original Yuka (DeviceType.supports_auto_change_direction), so it is no answer for the owner's LUBA 2 AWD X; MowDeck does not use it.
Step rotation, stored per mower and area. A schedule gets rotation_step_deg (1 to 179 degrees, null: off). Its next run for one mower takes the last angle that mower mowed the run's areas with and turns it by the step, modulo 180 (a stripe at 190 degrees is the stripe at 10, and the adapter sends toward modulo 180 anyway). Without any history the run uses the base angle: the schedule's pattern.angle_deg, else the mower's mow defaults, else 0. Step rotation and the list exclude each other (422); the list keeps working as before.
The last angle lives in the Scheduler's new table mow_angles (mower, area, angle, time, source schedule or manual, schedule id), written by every started run, whatever its rotation, with the angle it sent. A job mows several areas with one angle: the run continues from the area mowed most recently (the newest direction on that lawn) and stores its angle for every area of the job. A group schedule works this out per mower. Patterns without a direction (edge_only, random_angle) never rotate and store nothing. The Scheduler owns this because it is the only place that issues starts on its own; Fleet stays a pass-through for commands.
Manual starts. A start from the start sheet does not pass the Scheduler. The sheet offers "Mähwinkel drehen" with the mower's step (mower_rotation.manual_step_deg, chips 30/45/60/90): it reads GET /scheduler/mowers/{id}/angles, sends the rotated angle in StartMow.pattern.angle_deg and, after the start went out, reports it with POST /scheduler/mowers/{id}/angles; a changed step is saved with PUT .../angles/settings. A manual start with rotation off (an explicit angle) is not reported, so it does not advance the rotation. The rotated angle never becomes the mow defaults; they keep the sheet's own angle. Reading needs can_view, reporting and the step need can_control on the mower.
Where the angle shows. RunOut.angle_deg and ScheduleRunRecorded.angle_deg (contracts, optional, 0 up to below 180) carry the angle of a started run; the run history prints it. PreviewItemOut.angle_deg gives the angle of each upcoming firing for the target's first mower (angle_mower_id), assuming every earlier firing starts; a skipped firing has none and does not advance. The schedule editor offers off / step / list with a dial of the next angles; the preview shows the angles beside the times.
Migration 0004 of the Scheduler adds schedules.rotation_step_deg and schedule_runs.angle_deg (both null for existing rows, so every schedule runs as before) and the tables mow_angles and mower_rotation.
Considered: a rotation index per mower instead of an angle (cannot continue across schedules with different steps); a new pattern key in MowPattern (would reach the controller and the adapter, which only need the final angle); rotating inside the Mammotion adapter (hidden from the preview and history, and the adapter would need state across starts); the vendor's auto-reverse (not offered on the LUBA 2 family).
- Rotation needs no adapter change: every adapter that honours
angle_degrotates. The Planner path (ADR 0030) gets the angle the same way. - Not verified on hardware: whether the LUBA 2 AWD X firmware honours
towardwithtoward_mode = absolute_angle(PyMammotion markstoward_mode"luba 2 yuka only"), and which reference 0 degrees has on the mower (the dial in the UI is schematic). The owner should start two jobs with 0 and 90 degrees and compare the planned paths in the Mammotion app or the path preview. - Starts outside MowDeck (the Mammotion app, the vendor's own schedules, Home Assistant through another integration) are not seen and do not advance the rotation.
-
mow_angleskeeps rows of areas that were removed from the map; they are never read again for a job without that area.
Mirrored from docs/adr/0061-mowing-angle-rotation.md at commit f35a29d9ffffa8faa57b2ccdb3554c9a5c35e2a5. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit f35a29d9ffffa8faa57b2ccdb3554c9a5c35e2a5. Edit the files in the repository, not this wiki.
MowDeck documentation
- Home
- Phases
- Architecture
-
Decisions (ADR)
- Architecture decisions
- ADR 0001: OpenFGA instead of self-built authorization
- ADR 0002: Self-built Identity service in FastAPI instead of Keycloak or Zitadel
- ADR 0003: SvelteKit for the web app
- ADR 0004: Monorepo with a uv workspace
- ADR 0005: AGPL-3.0 for services and apps, Apache-2.0 for libraries
- ADR 0006: PostgreSQL for all persistence, no SQLite
- ADR 0007: NATS JetStream as event bus
- ADR 0008: Traefik as gateway
- ADR 0009: Command and event envelopes, subject naming and de-duplication
- ADR 0010: Python 3.13 for all services and libraries
- ADR 0011: Separate GPL-3.0 package for the PyMammotion adapter
- ADR 0012: Mower control interface and safety rules
- ADR 0013: Device-Gateway registry, sharding and per-mower concurrency
- ADR 0014: Authorization model, tuple ownership and the mowdeck-authz client
- ADR 0015: Tokens, sessions and key rotation
- ADR 0016: Fleet commands and authorization per command kind
- ADR 0017: Telemetry storage and retention
- ADR 0018: Realtime WebSocket protocol
- ADR 0019: Mammotion adapter transports, manual drive and secrets
- ADR 0020: Web app architecture
- ADR 0021: Browser authentication flow
- ADR 0022: Notifications: rules, recipients and delivery
- ADR 0023: Map rendering and projection
- ADR 0024: Scheduler: recurrence, rain pause and command issuing
- ADR 0025: Home Assistant integration as a pure API client
- ADR 0026: Manual drive channel over the Realtime WebSocket
- ADR 0027: Health history and theft alarm
- ADR 0028: Entitlements: plans as data and enforcement
- ADR 0029: Helm chart and release pipeline
- ADR 0030: Mowing patterns as plug-ins and the Planner
- ADR 0031: Billing with a Stripe adapter
- ADR 0032: Live camera over Agora, joined directly by the browser
- ADR 0033: Vision-assisted recovery with human approval
- ADR 0034: Linked cloud accounts and device discovery
- ADR 0035: Server-side camera frames and automatic AI analysis on errors
- ADR 0036: Native app shells with Capacitor, loading the web app from the server
- ADR 0037: Camera panorama calibration in the browser, with the AI supplying points only
- ADR 0038: Dual link per mower and firmware updates
- ADR 0039: Spot mowing by MowDeck-driven paths
- ADR 0040: Link heartbeat for online status
- ADR 0041: Spot runs from the dock: undock, transit and return
- ADR 0042: Drive calibration
- ADR 0043: Turn styles
- ADR 0044: Drive view with camera
- ADR 0045: Camera relay
- ADR 0046: Device settings as capabilities
- ADR 0047: The RTK base station as a Fleet device
- ADR 0048: Maintenance commands
- ADR 0049: Manual driving over the Mammotion cloud
- ADR 0050: Passkeys and the app login hand-off
- ADR 0051: Sign in with GitHub
- ADR 0052: Sign in with Apple
- ADR 0053: Retention and downsampling of the position track
- ADR 0054: Aerial photo under the mower map
- ADR 0055: Deploy maintenance window
- ADR 0056: Camera photo in push notifications for errors and theft alarms
- ADR 0057: Step-up re-authentication for critical actions
- ADR 0058: Theft tracking mode
- ADR 0059: iOS widgets and the mowing Live Activity
- ADR 0060: Apple Watch app
- ADR 0061: Mowing angle rotation
- ADR 0062: Setup tour for new users
- ADR 0063: Matter server (mowers in Apple Home and Google Home)
- ADR 0064: Personal access tokens and the public API documentation
- ADR 0065: Security center and new-login alerts
- ADR 0066: Cloud provider plugins and adapters for more vendors
- ADR 0067: Error code lexicon, provider independent
- ADR 0068: Public demo mode
-
Development
- Getting started
- Service template
- Shared libraries
- Cloud providers: adding a vendor
- Adding an error lexicon for a vendor
- Design: identity and authorization
- Design: fleet, telemetry, realtime
- Design: web app
- Web app
- Home Assistant integration
- Camera source bridge protocol
- Fuzzing and property-based tests
- GitHub wiki mirror
-
Operations
- Operating the Compose stack
- Operating MowDeck on Kubernetes (Helm)
- Building the Bluetooth bridge (ESPHome on a XIAO ESP32-S3)
- Releases
- Billing operations
- Telemetry: retention and position downsampling
- iPhone and Android apps
- Passkeys
- Sign in with GitHub
- Aerial photo under the map
- Sign in with Apple
- Theft tracking mode
- Matter server
- Source code offer (AGPL-3.0 section 13)
- Public demo
- Spikes
-
Guides
- Guides
- ESP32 bridge: purchase and setup
- Signing in with a passkey
- Signing in with GitHub
- Error codes in plain words
- Aerial photo under the map
- Signing in with Apple
- Confirming critical actions
- Your security page
- iPhone widgets and the mowing Live Activity
- Apple Watch
- The setup tour
- Apple Home and Google Home (Matter)
- Scripting MowDeck
- API reference
- Original plan (German)