Repository navigation
ADR 0047 Rtk Base Station As A Fleet Device
Accepted (2026-10-23)
A LUBA 2 (also the owner's LUBA 2 AWD X 4G RTK) positions itself with RTK corrections from its own base station, received over LoRa radio or through the vendor cloud, or from a network RTK service (NRTK). Until now MowDeck knew the base station only as a device of a linked Mammotion account that "cannot be registered" (is_mower false, 422 not_a_mower, ADR 0034). The owner could neither see whether the mower still hears its base station, nor where the base stands, nor update its firmware, although all of that decides whether the mower can mow at all.
What the vendor exposes, as far as PyMammotion 0.10.9 and Mammotion-HA show it:
-
The base station is its own cloud device. Mammotion-HA lists devices without a
Luba/Yukaname and with the categoryTrackeras RTK devices; PyMammotion'sDeviceType.is_rtkmatches the namesRTK…,RBS03A0/1/2…,NB…and the RTK product keys, and its device registry holds them asRTKBaseStationDevice(own IoT id, own MQTT properties: position in radians, Wi-Fi RSSI, LoRa version, firmware). Mammotion-HA polls such a device every five hours (basestation_infosent to the base, the vendor's version check) and offers its firmware update (start_ota_upgradewith the base's IoT id). -
The mower describes its base in every report.
report_data.rtkcarries the correction source (mqtt_rtk_info.rtk_switch: LoRa, internet, NRTK), the base position in degrees (mqtt_rtk_info.latitude/longitude;0, 0unknown,-1, -1no GGA), the LoRa link (lora_info.lora_connection_status > 0, the channel) and whether the base was moved (score_info.base_moved,base_moving);report_data.basestation_infothe base's version (rpt_basestation_info.ver_*). Abasestation_inforequest sent to the mower is answered withbase.to_app(ResponseBasestationInfoT: satellites, solution, Wi-Fi RSSI, LoRa channel), which the next report overwrites in PyMammotion's model. -
Base station commands go through the mower (
basestation_info,reset_base_station,set_base_net_rtk_switch,read_and_set_rtk_pairing_code).
The architecture rules stay: Fleet owns the registrations and never talks to the vendor (ADR 0034); the Device-Gateway is manufacturer free and opens one connection per mower; only mowdeck-control-mammotion talks to PyMammotion.
The base station becomes its own Fleet record, linked to the mowers that use it. It gets no connection of its own: its state comes from the connection of a linked mower, and its firmware is checked and installed through that mower's cloud session with the base station's IoT id.
- A table
base_stations(id,account_id,name,sourcecloudorsimulated,linked_account_id,device_name,iot_idwith an installation-wide unique index,model, timestamps) and the nullable columnmowers.base_station_id(ON DELETE SET NULL), migration0012. - Discovery gains
DiscoveredDevice.device_type(mower,rtk_base,other; derived fromis_mowerfor lists cached before), and the device list of a linked account shows a registered base station (registered,base_station_id) or one registered elsewhere, like a mower (ADR 0034, one physical device in one MowDeck account). - API under
/api/v1/fleet/base-stations: list, register ({linked_account_id, device_name, name?}from the cached device list, or{source: "simulated"}), get, rename, delete (the mowers lose the link),POST /{id}/firmware/checkandPOST /{id}/firmware/update({version, mower_id?});PUT /api/v1/fleet/mowers/{id}/base-station({base_station_id}ornull) links a mower. A cloud base station serves cloud mowers of the same linked account (its firmware is asked for with that account's session), the simulated one simulated mowers (409base-station-not-applicableotherwise). - Permissions without a new OpenFGA type: reading base stations and the firmware check need
can_viewon the account, every change and the firmware updatecan_manage_mowerson the account, linking a mowercan_manage_mowerson the mower. A base station of another account is 404. Changes are audited (base_station.registered,.updated,.removed,mower.base_station_set,mower.base_station_removed) and re-publish the affected registrations asmower.updated. - The registration carries the link:
MowerRegistration.base_station(BaseStationRef: id, name, source, device name, IoT id; no secrets).
- The gateway passes the reference to the adapter as the connection option
base_stationand reopens the connection when the linked base station changes (not for a rename). -
MowerState.base_station(BaseStationState): the correction link (lora,internet,network), whether the mower receives the base (link_up, over LoRa), the LoRa channel, the base's satellites, solution and Wi-Fi signal (details_at), its position,moved, its firmware andfirmware_info. Telemetry stores and serves it with the mower state; no new stream or table. - Firmware reuses ADR 0038:
FirmwareCheckRequest.target(mowerorbase_station) on the existing subject, aBaseStationPortprotocol (check_base_station_firmware,start_base_station_firmware_update) with the capabilitybase_station_firmware_update, and the commandbase_station_firmware_update(version). The controller's rule (safety.base_station_firmware_update_problem): only the offered version, nothing installing, and the relaying mower neither mows, is paused, returns, updates nor drives (every mower of the base loses its corrections while it restarts). Fleet records the command like any other (command record, plan featurecontrol, single mower only) and additionally requirescan_manage_mowerson the account and a linked base station. The gateway checks the base station's firmware with the mower's daily check. - The simulator offers a simulated base station (LoRa while its RTK source is the base station, firmware offer and update), the Mammotion adapter the real one (below), the fake cloud a base station for the browser tests.
From every report: rtk.mqtt_rtk_info, rtk.lora_info, rtk.score_info and the version in basestation_info; from a basestation_info request to the mower (20 s after the connect, then hourly, every five minutes while an update runs) the details, kept by the adapter because the next report resets them; from the vendor's firmware service get_device_ota_firmware([base_iot_id]) and start_ota_upgrade(base_iot_id, version) through the mower's cloud session, without touching the mower's own firmware state. A started update ends idle when the vendor or the mower's report shows the target version, unknown after 45 minutes.
-
The base station as a second cloud device in the gateway (its own
RTKBaseStationDevicehandle, MQTT properties, polling like Mammotion-HA'sMammotionRTKCoordinator): rejected for now. It needs a connection kind without a mower (the gateway, the controller and Telemetry are built around mowers), a second device handle on a shared session that was never verified with several handles (ADR 0034), and more cloud traffic, while every value the owner needs is already in the mower's reports. Kept as the follow-up when a base without a linked mower should be watched (the base's own Wi-Fi details,lora_version). -
A
device_typeon the mower record (mowersrows of typertk_base): rejected. Every mower use case, the registry, the gateway sharding, the commands, Telemetry, the Scheduler and the web app would have to exclude base stations; a separate table with a link keeps them out. -
A new OpenFGA type
base_stationwithparentsite: rejected for now. A base station serves mowers of several sites; account-level relations express that, and the mower state (visible with the mower's owncan_view) already shows a site-scoped user the base of their mower. A type can be added when base stations get sub-user grants of their own. - The firmware update as a request/reply instead of a command: rejected; a command gives the audit record, the plan check, the status URL and the controller's safety rules for free.
- Resetting the base station, switching its correction source and the LoRa pairing (
reset_base_station,set_base_net_rtk_switch,read_and_set_rtk_pairing_code): they change how every mower positions itself and are not verified on hardware. - The base station's own cloud device (see above), its LoRa version and Wi-Fi network details, the base of a mower connected only over Bluetooth (no base station link for
ble_bridgemowers). - Models other than the LUBA 2 family: Yuka, Spino and the RTK-less models (vision only) are not verified; a Yuka uses the same report fields in PyMammotion, but nothing here was tested with one.
- The web app: no base station UI yet (the device list keeps base stations out of the add-mower flow). The API and the state are ready for it.
- The owner sees in the mower's state whether it receives its base station and where the base stands, and can update the base's firmware from MowDeck with rules the vendor app does not enforce.
- Unverified against the real mower and base (the hardware owner script is the follow-up): that the LUBA 2 answers
basestation_info(Mammotion-HA sends it to the base itself, PyMammotion'smower_api.pyto the mower), the meaning ofResponseBasestationInfoT.rtk_status(read with PyMammotion'sRTKStatuslike itsBasestationInfo.fix_status; Mammotion-HA shows it asRTKPositionMode), thatrpt_basestation_info.ver_*is the base's version, that the base station's version check and OTA request work with the mower's account session (the base must be in the same Mammotion account or shared to it), and whetherlink_upshould also be derived for corrections over the internet (it is null then). - Fleet migration
0012is additive; an older Fleet keeps working during the rollout. Base stations are registered after the upgrade from the device list (read the linked account again so the list carriesdevice_type). - A rename re-publishes the registrations but does not reconnect; linking or unlinking a base station reconnects the mower.
Mirrored from docs/adr/0047-rtk-base-station-as-a-fleet-device.md at commit 99e41f2138d66bcc9341c01d4125272b1aa65bc4. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit 99e41f2138d66bcc9341c01d4125272b1aa65bc4. 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
- Development
- Operations
- Spikes
- Guides
- Original plan (German)