Add Simulation tab for controlling the simulated mower - #7
Conversation
Adds a "Simulation" tab that drives the mower_simulation node over MQTT RPC and reflects its retained sim/state/json stream. The tab is feature-detected: it only appears when a simulator is connected (hidden on real-hardware production deployments). - openrpc.json: sim.* methods (state.get, emergency/movement/battery/gps set, dock.move, twist.set, displace) + SimState schema; regenerated rpc.ts client. battery.set takes `full` or an exact `volts`. - mowersStore: subscribe sim/state/json, parse into Mower.simState (also the feature-detect flag), updateSimState action. - navigationItems: gate the tab on simAvailable (selected mower has simState), wired from Sidebar and MobileBottomBar. - useSimControl hook: wraps the RPCs with optimistic state updates and per-action pending/error tracking. - Custom widgets: EmergencyCard (latch + reason chips), BatteryGauge (SVG gauge, Crit low/Empty/Full/Crit high presets, click-to-enter exact voltage), ToggleCard (traction/GPS), TwistCard (VirtualJoystick override), DockCard, DisplaceCard.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughThis PR adds simulator control capabilities: new sim.* OpenRPC methods and generated RPC client bindings, Zod schemas for simulator state and ROS parameters, store wiring to subscribe/parse simulator state, a useSimControl hook extended with joy_override, and new UI components (SimulatorButton, BatteryGauge) plus map/teleop wiring for simulator mode. ChangesSimulator control flow
Estimated code review effort: 3 (Moderate) | ~30 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
src/components/map/MowerMap.tsxOops! Something went wrong! :( ESLint: 9.39.4 TypeError: Converting circular structure to JSON src/components/map/SimulatorButton.tsxOops! Something went wrong! :( ESLint: 9.39.4 TypeError: Converting circular structure to JSON src/components/map/teleop/TeleopControls.tsxOops! Something went wrong! :( ESLint: 9.39.4 TypeError: Converting circular structure to JSON
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🧪 Test this PRA multi-arch image (amd64 + arm64) for this PR has been pushed to: OpenMowerOS v2
Plain Dockerdocker run --rm -p 3000:3000 ghcr.io/xtech/openmower-app:pr-7
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/simulation/BatteryGauge.tsx (1)
103-115: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winClickable battery graphic/label are not keyboard operable.
Both interactive regions (
role="button"SVG wrapper at Line 103-115, and the "Tap to set voltage" box at Line 164-177) only handleonClick; neither hastabIndex,onKeyDown(Enter/Space), nor does the second one even declarerole="button". Keyboard-only users cannot open the voltage dialog through these elements.Also applies to: 164-177
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/simulation/BatteryGauge.tsx` around lines 103 - 115, The interactive battery graphic and the “Tap to set voltage” box in BatteryGauge are mouse-only and not keyboard operable. Update both Box wrappers that call openDialog to behave as proper buttons by adding keyboard focusability and keyboard activation handling, and ensure the second box also declares an appropriate button role. Use the existing openDialog handler and the relevant Box components in BatteryGauge so Enter/Space can open the voltage dialog from either region.
🧹 Nitpick comments (1)
src/components/simulation/BatteryGauge.tsx (1)
31-61: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winInconsistent data sourcing:
voltage/chargingare props whilepercentage/paramsare pulled from the store.The component already reads
paramsandbattery_percentagedirectly viauseSelectedMower(Lines 50-51), but still acceptsvoltageandchargingas external props (Lines 32-33) instead of deriving them the same way (m?.simState.battery_voltage,m?.simState.charging— both fields already used inSimulatorButton.tsx). If a caller ever passes stale or differently-sourced values, the percentage and voltage/charging readouts could diverge and show an inconsistent battery state.Deriving all battery fields from the same store selector would remove this single source-of-truth risk.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/simulation/BatteryGauge.tsx` around lines 31 - 61, BatteryGauge is mixing prop-driven and store-driven battery state, which can make the UI inconsistent. Update BatteryGauge to derive voltage and charging from the same useSelectedMower selector used for params and battery_percentage, using the simState fields already referenced in SimulatorButton.tsx, and then remove or stop relying on the voltage and charging props in BatteryGaugeProps and the component body.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/map/MowerMap.tsx`:
- Around line 65-67: Reset the lingering teleop state in MowerMap when the
selected mower changes so `showTeleop` does not stay true for a new mower.
Update the `manualDrive` state in `MowerMap` (near the `useSelectedMower`,
`useState`, and `showTeleop` logic) to clear it whenever `simAvailable` becomes
false or the selected mower identity changes. Use the existing `manualDrive`,
`simAvailable`, and `showTeleop` symbols to locate the state transition and
ensure `TeleopControls` cannot remain mounted after switching to a non-sim
mower.
In `@src/components/map/SimulatorButton.tsx`:
- Around line 253-260: The Popover in SimulatorButton uses a fixed 460px width
that can overflow on mobile or narrow screens. Update the Popover’s paper
styling to keep the existing desktop width behavior while adding a
viewport-aware maxWidth/width constraint so it fits within small screens. Use
the Popover and slotProps.paper styling in SimulatorButton as the place to
adjust this responsive sizing.
- Around line 95-133: The custom SVG slider in SimulatorButton is pointer-only,
so add keyboard accessibility to the same control. Update the SVG in
SimulatorButton to be focusable with an appropriate role like slider, expose
aria-valuemin/aria-valuemax/aria-valuenow, and add onKeyDown handling for
arrow/home/end adjustments that calls the existing onChange/toV path. Keep the
pointer handlers intact, but ensure the same value update logic is reused so
keyboard and pointer interactions stay in sync.
- Around line 95-118: The battery slider is sending an RPC on every pointer move
because `onPointerMove` calls `handlePointer`, which immediately triggers
`onChange` and the `setBatteryVoltage`/`sim.battery.set` path. Update
`SimulatorButton` to keep a local drag value for responsive thumb movement
during pointer tracking, and defer the `onChange` call until `onPointerUp` (or
add throttling inside `handlePointer`). Use the existing `handlePointer`,
`onPointerDown`, `onPointerMove`, and `onPointerUp` handlers as the hook points
for the fix.
- Around line 215-216: The SimulatorButton popover is not surfacing the
per-action loading and failure state returned by useSimControl, so duplicate
clicks can happen and RPC failures are invisible. Update the destructuring in
SimulatorButton to include the hook’s pending and error values, then use pending
to disable the matching Switches/Buttons while each action is in flight and
render error in the popover when present. Apply this consistently to the action
handlers and controls in SimulatorButton so every simulated command has visible
in-flight and failure feedback.
- Around line 66-72: The measuredRef callback in SimulatorButton creates a
ResizeObserver but never cleans it up, so each Popover open/close can leak an
observer. Update the measuredRef logic to keep track of the observer for the
current element and disconnect it when the ref is cleared or the component
unmounts, ensuring the observer tied to setWidth and ResizeObserver is properly
disposed.
In `@src/components/simulation/BatteryGauge.tsx`:
- Around line 70-78: The voltage parsing in BatteryGauge is accepting
comma-decimal input but using parseFloat, which truncates values like “21,5” to
21 without failing. Update the parsing/validation in BatteryGauge’s submit flow
(and the related input handling around the pattern/voltageInput state) to
normalize comma decimals before converting, or otherwise explicitly reject
invalid comma-formatted values, so onSetVoltage receives the correct numeric
value and the dialog shows validation instead of silently submitting a wrong
voltage.
In `@src/stores/mowersStore.ts`:
- Around line 247-252: The params/json branch in the message handler is throwing
on malformed MQTT payloads because it uses rosParamsSchema.parse directly.
Update the mowersStore message callback to use rosParamsSchema.safeParse for
this branch, and only assign mower.params and call mower.getDatumFromParams()
when parsing succeeds, matching the invalid-data handling used by events/json
and sim/state/json.
---
Outside diff comments:
In `@src/components/simulation/BatteryGauge.tsx`:
- Around line 103-115: The interactive battery graphic and the “Tap to set
voltage” box in BatteryGauge are mouse-only and not keyboard operable. Update
both Box wrappers that call openDialog to behave as proper buttons by adding
keyboard focusability and keyboard activation handling, and ensure the second
box also declares an appropriate button role. Use the existing openDialog
handler and the relevant Box components in BatteryGauge so Enter/Space can open
the voltage dialog from either region.
---
Nitpick comments:
In `@src/components/simulation/BatteryGauge.tsx`:
- Around line 31-61: BatteryGauge is mixing prop-driven and store-driven battery
state, which can make the UI inconsistent. Update BatteryGauge to derive voltage
and charging from the same useSelectedMower selector used for params and
battery_percentage, using the simState fields already referenced in
SimulatorButton.tsx, and then remove or stop relying on the voltage and charging
props in BatteryGaugeProps and the component body.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 073bb2fb-794a-4e5a-9bb7-5422613da8d0
📒 Files selected for processing (11)
openrpc.jsonsrc/components/map/MowerMap.tsxsrc/components/map/SimulatorButton.tsxsrc/components/map/teleop/TeleopControls.tsxsrc/components/map/teleop/VirtualJoystick.tsxsrc/components/simulation/BatteryGauge.tsxsrc/hooks/useSimControl.tssrc/hooks/useTeleop.tssrc/lib/rpc.tssrc/stores/mowersStore.tssrc/stores/schemas.ts
| const measuredRef = useCallback((el: HTMLDivElement | null) => { | ||
| if (!el) return; | ||
| const update = () => setWidth(el.clientWidth); | ||
| update(); | ||
| const ro = new ResizeObserver(update); | ||
| ro.observe(el); | ||
| }, []); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
ResizeObserver is created but never disconnected.
measuredRef creates a new ResizeObserver on every mount but the early-return on unmount (if (!el) return;) never calls ro.disconnect(). Since this Box lives inside a Popover that mounts/unmounts on open/close, every open cycle leaks an observer that keeps firing setWidth on a detached node.
🔧 Proposed fix
+ const observerRef = useRef<ResizeObserver | null>(null);
const measuredRef = useCallback((el: HTMLDivElement | null) => {
+ observerRef.current?.disconnect();
+ observerRef.current = null;
if (!el) return;
const update = () => setWidth(el.clientWidth);
update();
const ro = new ResizeObserver(update);
ro.observe(el);
+ observerRef.current = ro;
}, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const measuredRef = useCallback((el: HTMLDivElement | null) => { | |
| if (!el) return; | |
| const update = () => setWidth(el.clientWidth); | |
| update(); | |
| const ro = new ResizeObserver(update); | |
| ro.observe(el); | |
| }, []); | |
| const observerRef = useRef<ResizeObserver | null>(null); | |
| const measuredRef = useCallback((el: HTMLDivElement | null) => { | |
| observerRef.current?.disconnect(); | |
| observerRef.current = null; | |
| if (!el) return; | |
| const update = () => setWidth(el.clientWidth); | |
| update(); | |
| const ro = new ResizeObserver(update); | |
| ro.observe(el); | |
| observerRef.current = ro; | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/map/SimulatorButton.tsx` around lines 66 - 72, The measuredRef
callback in SimulatorButton creates a ResizeObserver but never cleans it up, so
each Popover open/close can leak an observer. Update the measuredRef logic to
keep track of the observer for the current element and disconnect it when the
ref is cleared or the component unmounts, ensuring the observer tied to setWidth
and ResizeObserver is properly disposed.
| const handlePointer = useCallback( | ||
| (e: React.PointerEvent<SVGSVGElement>) => { | ||
| if (!svgRef.current) return; | ||
| const rect = svgRef.current.getBoundingClientRect(); | ||
| // scale from CSS pixels to viewBox units | ||
| const scale = width / rect.width; | ||
| const x = (e.clientX - rect.left) * scale; | ||
| onChange(toV(x)); | ||
| }, | ||
| [onChange, toV, width], | ||
| ); | ||
|
|
||
| const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => { | ||
| (e.currentTarget as SVGSVGElement).setPointerCapture(e.pointerId); | ||
| setDragging(true); | ||
| handlePointer(e); | ||
| }; | ||
| const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => { | ||
| if (dragging) handlePointer(e); | ||
| }; | ||
| const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => { | ||
| setDragging(false); | ||
| (e.currentTarget as SVGSVGElement).releasePointerCapture(e.pointerId); | ||
| }; |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Battery slider dispatches an RPC on every pointer-move event with no throttling.
handlePointer is invoked directly from onPointerMove while dragging is true, and it calls onChange (→ setBatteryVoltage → sim.battery.set RPC) synchronously for every event. Since value is bound to the externally-stored simState.battery_voltage (not local drag state), a single drag gesture can fire dozens of RPC calls per second, and the thumb only moves once each round-trip completes, causing visible jank plus needless load on the RPC/MQTT path.
Track a local "dragging value" for immediate visual feedback and only invoke onChange on onPointerUp (or throttle it during drag).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/map/SimulatorButton.tsx` around lines 95 - 118, The battery
slider is sending an RPC on every pointer move because `onPointerMove` calls
`handlePointer`, which immediately triggers `onChange` and the
`setBatteryVoltage`/`sim.battery.set` path. Update `SimulatorButton` to keep a
local drag value for responsive thumb movement during pointer tracking, and
defer the `onChange` call until `onPointerUp` (or add throttling inside
`handlePointer`). Use the existing `handlePointer`, `onPointerDown`,
`onPointerMove`, and `onPointerUp` handlers as the hook points for the fix.
| const handlePointer = useCallback( | ||
| (e: React.PointerEvent<SVGSVGElement>) => { | ||
| if (!svgRef.current) return; | ||
| const rect = svgRef.current.getBoundingClientRect(); | ||
| // scale from CSS pixels to viewBox units | ||
| const scale = width / rect.width; | ||
| const x = (e.clientX - rect.left) * scale; | ||
| onChange(toV(x)); | ||
| }, | ||
| [onChange, toV, width], | ||
| ); | ||
|
|
||
| const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => { | ||
| (e.currentTarget as SVGSVGElement).setPointerCapture(e.pointerId); | ||
| setDragging(true); | ||
| handlePointer(e); | ||
| }; | ||
| const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => { | ||
| if (dragging) handlePointer(e); | ||
| }; | ||
| const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => { | ||
| setDragging(false); | ||
| (e.currentTarget as SVGSVGElement).releasePointerCapture(e.pointerId); | ||
| }; | ||
|
|
||
| const rx = 3; | ||
|
|
||
| return ( | ||
| <Box ref={measuredRef} sx={{px: 1.5, pb: 1}}> | ||
| <svg | ||
| ref={svgRef} | ||
| width="100%" | ||
| height={SVG_H} | ||
| viewBox={`0 0 ${width} ${SVG_H}`} | ||
| style={{display: 'block', cursor: 'text', userSelect: 'none', overflow: 'visible'}} | ||
| onPointerDown={onPointerDown} | ||
| onPointerMove={onPointerMove} | ||
| onPointerUp={onPointerUp} | ||
| > |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Custom battery slider has no keyboard support.
The SVG only wires onPointerDown/Move/Up; there's no tabIndex, role="slider", aria-valuenow/min/max, or onKeyDown for arrow-key adjustment. Keyboard-only users cannot operate this control at all, unlike the standard MUI Switch/Button/ToggleButtonGroup used elsewhere in the same popover.
Also applies to: 199-200
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/map/SimulatorButton.tsx` around lines 95 - 133, The custom SVG
slider in SimulatorButton is pointer-only, so add keyboard accessibility to the
same control. Update the SVG in SimulatorButton to be focusable with an
appropriate role like slider, expose aria-valuemin/aria-valuemax/aria-valuenow,
and add onKeyDown handling for arrow/home/end adjustments that calls the
existing onChange/toV path. Keep the pointer handlers intact, but ensure the
same value update logic is reused so keyboard and pointer interactions stay in
sync.
| const {simState, available, setEmergency, setMovementAllowed, setBatteryVoltage, setGpsGood, moveToDock, displace} = | ||
| useSimControl(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
pending/error from useSimControl are never surfaced in the UI.
The hook exposes per-action pending and error, but the popover destructures only simState, available, setEmergency, setMovementAllowed, setBatteryVoltage, setGpsGood, moveToDock, displace (Line 215-216). Since there's no optimistic update (state only changes once the retained sim/state/json message round-trips), none of the Switches/Buttons are disabled while a call is in flight, and a failed RPC (error) is silently swallowed with zero user feedback. This allows rapid duplicate clicks on toggles like "Trigger emergency" and leaves users unaware when an action fails.
🔧 Suggested direction
- const {simState, available, setEmergency, setMovementAllowed, setBatteryVoltage, setGpsGood, moveToDock, displace} =
+ const {simState, available, pending, error, setEmergency, setMovementAllowed, setBatteryVoltage, setGpsGood, moveToDock, displace} =
useSimControl();Then pass disabled={pending === 'gps'} etc. to the relevant switches/buttons, and render error (e.g. via an Alert) when non-null.
Also applies to: 271-411
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/map/SimulatorButton.tsx` around lines 215 - 216, The
SimulatorButton popover is not surfacing the per-action loading and failure
state returned by useSimControl, so duplicate clicks can happen and RPC failures
are invisible. Update the destructuring in SimulatorButton to include the hook’s
pending and error values, then use pending to disable the matching
Switches/Buttons while each action is in flight and render error in the popover
when present. Apply this consistently to the action handlers and controls in
SimulatorButton so every simulated command has visible in-flight and failure
feedback.
| <Popover | ||
| open={open} | ||
| anchorEl={buttonRef.current} | ||
| onClose={() => setOpen(false)} | ||
| anchorOrigin={{vertical: 'bottom', horizontal: 'left'}} | ||
| transformOrigin={{vertical: 'bottom', horizontal: 'right'}} | ||
| slotProps={{paper: {sx: {width: 460, py: 1}}}} | ||
| > |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fixed Popover width (460px) may overflow small/mobile viewports.
The map (and this control) is also used on mobile per the nav-gating changes in this PR; a hardcoded width: 460 with no maxWidth constraint tied to viewport size can clip or push the popover off-screen on narrow devices.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/map/SimulatorButton.tsx` around lines 253 - 260, The Popover
in SimulatorButton uses a fixed 460px width that can overflow on mobile or
narrow screens. Update the Popover’s paper styling to keep the existing desktop
width behavior while adding a viewport-aware maxWidth/width constraint so it
fits within small screens. Use the Popover and slotProps.paper styling in
SimulatorButton as the place to adjust this responsive sizing.
| const parsed = parseFloat(voltageInput); | ||
| const valid = Number.isFinite(parsed); | ||
|
|
||
| const submit = () => { | ||
| if (!valid || minVoltage === undefined || maxVoltage === undefined) return; | ||
| const clamped = Math.min(maxVoltage, Math.max(minVoltage, parsed)); | ||
| onSetVoltage(clamped); | ||
| setDialogOpen(false); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Comma decimal input is silently mis-parsed.
The input's pattern (Line 226) explicitly permits a comma decimal separator ('[0-9]*[.,]?[0-9]*'), but parseFloat (Line 70) does not understand commas — parseFloat("21,5") returns 21, not 21.5. Since Number.isFinite(21) is true, valid stays true and no error is shown, so a user typing a comma-decimal value (common in EU locales) silently submits a truncated voltage.
🔧 Proposed fix
- const parsed = parseFloat(voltageInput);
+ const parsed = parseFloat(voltageInput.replace(',', '.'));Also applies to: 216-229
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/simulation/BatteryGauge.tsx` around lines 70 - 78, The voltage
parsing in BatteryGauge is accepting comma-decimal input but using parseFloat,
which truncates values like “21,5” to 21 without failing. Update the
parsing/validation in BatteryGauge’s submit flow (and the related input handling
around the pattern/voltageInput state) to normalize comma decimals before
converting, or otherwise explicitly reject invalid comma-formatted values, so
onSetVoltage receives the correct numeric value and the dialog shows validation
instead of silently submitting a wrong voltage.
| } else if (partialTopic === 'params/json') { | ||
| set((state) => { | ||
| const mower = state.mowers[idx]; | ||
| mower.params = JSON.parse(payload.toString()) as Record<string, unknown>; | ||
| mower.params = rosParamsSchema.parse(JSON.parse(payload.toString())); | ||
| mower.map.datum ??= mower.getDatumFromParams(); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the relevant store and schema definitions.
git ls-files | rg 'src/stores/mowersStore\.ts|rosParamsSchema|simStateSchema|events/json|params/json'
echo '--- mowersStore outline ---'
ast-grep outline src/stores/mowersStore.ts --view expanded || true
echo '--- relevant line ranges ---'
nl -ba src/stores/mowersStore.ts | sed -n '220,290p'
echo '--- rosParamsSchema references ---'
rg -n "rosParamsSchema|params/json|safeParse|parse\(" src/stores/mowersStore.ts src -g '!**/*.map' -g '!**/dist/**' || trueRepository: xtech/openmower-app
Length of output: 2042
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- mowersStore around getDatumFromParams and the message handler ---'
nl -ba src/stores/mowersStore.ts | sed -n '1,120p'
echo
nl -ba src/stores/mowersStore.ts | sed -n '150,290p'Repository: xtech/openmower-app
Length of output: 267
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- handler around params/json ---'
sed -n '220,270p' src/stores/mowersStore.ts
echo
echo '--- getDatumFromParams and nearby methods ---'
sed -n '79,100p' src/stores/mowersStore.tsRepository: xtech/openmower-app
Length of output: 3433
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the Ros params schema definition and any related types.
rg -n "rosParamsSchema|type RosParams|interface RosParams|RosParams =" src
echo
echo '--- schema definition candidates ---'
rg -n "datum_lat|datum_long|datum_height|gps/datum|z\.object|z\.record|z\.number|catchall" srcRepository: xtech/openmower-app
Length of output: 3926
Use safeParse for params/json to avoid throwing on malformed params payloads.
rosParamsSchema requires the seven numeric values here, so a bad retained MQTT message will throw inside the message callback. This branch should skip invalid data the same way events/json and sim/state/json do.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/stores/mowersStore.ts` around lines 247 - 252, The params/json branch in
the message handler is throwing on malformed MQTT payloads because it uses
rosParamsSchema.parse directly. Update the mowersStore message callback to use
rosParamsSchema.safeParse for this branch, and only assign mower.params and call
mower.getDatumFromParams() when parsing succeeds, matching the invalid-data
handling used by events/json and sim/state/json.
Summary
Adds a Simulation tab that drives the
mower_simulationnode over MQTT RPC and reflects its retainedsim/state/jsonstream. The tab is feature-detected — it only appears when a simulator is connected, so it stays hidden on real-hardware production deployments.Changes
openrpc.json—sim.*methods (state.get,emergency/movement/battery/gps.set,dock.move,twist.set,displace) +SimStateschema; regeneratedrpc.ts.sim.battery.setacceptsfullor an exactvolts.mowersStore— subscribesim/state/json, parse intoMower.simState(also the feature-detect flag), updateSimState` action.createNavigationItems(capabilities, simAvailable);simAvailable= selected mower hassimState. Wired from Sidebar + MobileBottomBar.useSimControlhook — wraps the RPCs with optimistic state updates and per-action pending/error tracking.EmergencyCard— latch state + decoded reason chips, Trigger/Clear.BatteryGauge— SVG gauge, Crit low / Empty / Full / Crit high presets, click battery to enter an exact voltage.ToggleCard— traction (stuck simulation) and GPS quality.TwistCard— movement override via the existingVirtualJoystick(throttled RPCs).DockCard,DisplaceCard.Notes
sim.battery.set(honorvolts) — handled separately./simulationon a non-sim connection shows a "Simulator not detected" empty state.Summary by CodeRabbit
New Features
Bug Fixes