Skip to content

Add calculated depth variable#2738

Open
rafaellehmkuhl wants to merge 16 commits into
bluerobotics:masterfrom
rafaellehmkuhl:add-calculated-depth-variable
Open

Add calculated depth variable#2738
rafaellehmkuhl wants to merge 16 commits into
bluerobotics:masterfrom
rafaellehmkuhl:add-calculated-depth-variable

Conversation

@rafaellehmkuhl

@rafaellehmkuhl rafaellehmkuhl commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Adds depth/altitude readings derived from SCALED_PRESSURE2/3 as predefined data-lake variables, with the underlying machinery needed to make them work cleanly across vehicles. Widgets that currently read raw pressure (DepthHUD, DepthIndicator, RelativeAltitudeIndicator) can now subscribe directly to the resolved altitude in meters.

What changed

  • Autopilot parameters in the data-lake. Every PARAM_VALUE received from the autopilot is mirrored at /vehicle/{systemId}/parameters/{paramName}, making parameters reactive to widgets and transforming functions (addresses the readable half of data-lake: provide access to autopilot parameters #1848).
  • Predefined baro2.pressure_alt / baro3.pressure_alt. Registered as transforming functions that compute altitude in meters from SCALED_PRESSURE*.press_abs and the autopilot's BARO*/BARO_SPEC_GRAV/BARO_ALT_OFFSET parameters using ArduSub's underwater depth formula, with the active vehicle resolved through the nested {{autopilotSystemId}} template.
  • Nested template resolution. replaceDataLakeInputsInString / findDataLakeVariablesIdsInString now loop until stable (cap 10 passes), so a placeholder whose value itself contains {{...}} (e.g. {{/vehicle/{{autopilotSystemId}}/parameters/BARO_SPEC_GRAV}}) fully resolves. Non-nested inputs behave exactly as before.
  • Graceful transforming-function evaluation. Missing dependencies now substitute NaN into the expression instead of leaving the literal {{...}} placeholder (which threw SyntaxError and burned the exponential-backoff budget). Listeners are re-discovered after every evaluation so expressions follow inner-template changes — e.g. when autopilotSystemId flips on connect, dependencies switch to the new /vehicle/{N}/... paths automatically.
  • Widget options wired through the new variables. Depth/altitude widgets expose baro2.pressure_alt and baro3.pressure_alt as altitude sources whose value points directly at the compound-variable IDs (toMeters becomes the identity). A new legacyMatchSuffixes field migrates persisted settings that still reference the raw SCALED_PRESSURE*/press_abs paths.
  • NaN guards on altitude consumers. DepthHUD, DepthIndicator, and RelativeAltitudeIndicator now treat non-finite altitude as missing, so the brief window where a compound variable is still resolving doesn't show "NaN m" or poison the depth-history canvas.

Test plan

  • Connect to an ArduSub vehicle and confirm baro2.pressure_alt / baro3.pressure_alt appear in the data lake and resolve to sensible values once parameters arrive.
  • In DepthHUD, DepthIndicator, and RelativeAltitudeIndicator option menus, select the new altitude sources and confirm readings match the previous raw-pressure path.
  • Existing widgets persisted on the old SCALED_PRESSURE*/press_abs source still load and switch to the new compound variable via legacyMatchSuffixes.
  • During the first seconds after connect (before parameters land) widgets show -- / blank instead of NaN m.
  • Reconnecting to a vehicle with a different system ID, the compound variables re-bind to the new /vehicle/{N}/... paths without a refresh.

To be merged after #2725.
Closes #2736.

Replaces the vehicle store altitude watch with useDataLakeVariable
composable. The altitude source variable ID is now configurable via
miniWidget.options.altitudeVariableId (default: /mavlink/1/1/AHRS2/altitude).
Properly accepts MiniWidget props. Removes useMainVehicleStore dependency.
Replace the vehicle store altitude watch with useDataLakeVariable and
store the altitude source in widget.options.altitudeVariableId.
Replaces the vehicle store altitude watch with useDataLakeVariable
composable. The altitude source variable ID is now configurable via
miniWidget.options.altitudeVariableId (default: GLOBAL_POSITION_INT/relative_alt).
Properly accepts MiniWidget props. Removes useMainVehicleStore dependency.
Add configurable altitude source options (ahrs2.alt, vfr_hud.alt,
global_position_int.alt) to the Depth HUD config menu.
Add a config menu with altitude source options and mark the mini-widget as configurable in edit mode.
…ni-widget

Add a config menu with altitude source options and mark the mini-widget as configurable in edit mode.
Default source is global_position_int.alt, not ahrs2.alt.
When the consumer-provided variable ID changes to one that has no data
yet, the previous value would persist until the new source published
something, leaving widgets stuck on stale numbers after a source
change. Reset the value on every resubscribe so consumers fall back to
their undefined-state placeholder immediately.
Each `PARAM_VALUE` received from the autopilot is now stored under
`/vehicle/{systemId}/parameters/{paramName}`, exposing parameters to
widgets and transforming functions reactively (the readable aspect of
bluerobotics#1848).
Registers `baro2.pressure_alt` and `baro3.pressure_alt` as transforming
functions in the predefined data-lake resources, alongside the camera
zoom/focus ones. Each computes altitude in meters from
`SCALED_PRESSURE*.press_abs` and the autopilot
`BARO*/BARO_SPEC_GRAV/BARO_ALT_OFFSET` parameters using ArduSub's
underwater depth formula, with the active autopilot resolved through
the nested `{{autopilotSystemId}}` template.
Two related changes to the transforming-function evaluator so the
predefined compound variables behave well across a connect:

- Expression evaluation now substitutes `NaN` for any missing
  dependency instead of leaving the `{{...}}` placeholder literal. The
  expression evaluates to `NaN` (which is what consumers expect for a
  variable that "isn't ready yet") instead of throwing a SyntaxError
  that burns through the exponential backoff retry budget.

- Listener setup re-discovers dependencies after every evaluation so
  the resolved variable set follows changes to inner-template values.
  When `autopilotSystemId` flips (a vehicle connects, or its system ID
  is changed), expressions that template on it now seamlessly switch
  to listening on the newly-resolved `/vehicle/{N}/...` paths.
The data-lake substitution engine was single-pass, so a placeholder
whose value itself contained `{{...}}` was left half-resolved. That
blocks the `baro2.pressure_alt` / `baro3.pressure_alt` compound
variables (and any future one) from referencing per-vehicle paths via
nested `{{autopilotSystemId}}` substitution, e.g.
`{{/vehicle/{{autopilotSystemId}}/parameters/BARO_SPEC_GRAV}}`.

`replaceDataLakeInputsInString` and `findDataLakeVariablesIdsInString`
now loop the substitution / discovery until the result is stable
(capped at 10 passes). Inputs that don't nest behave exactly as before
since the first pass already produces the final result; nested inputs
fully resolve in one call and their inner-resolved variable IDs are
included in the discovery output.
… variables

Adds `baro2.pressure_alt` and `baro3.pressure_alt` to the altitude
source options that depth/altitude widgets expose in their config
menus. Their `value` points at the predefined compound variable IDs
(so the widget subscribes directly to the resolved altitude in meters
and `toMeters` is the identity), and the new `legacyMatchSuffixes`
field migrates persisted widget settings that still reference the raw
`SCALED_PRESSURE*/press_abs` paths.
Compound-variable altitude sources (e.g. `baro2.pressure_alt`) return
`NaN` until every dependency has been received. Without this guard the
widget displays "NaN m" during that window instead of the existing
"--" fallback.
Compound-variable altitude sources (e.g. `baro2.pressure_alt`) return
`NaN` until every dependency has been received. Without this guard the
widget displays "NaN m" during that window instead of the existing
"--" fallback.
Compound-variable altitude sources (e.g. `baro2.pressure_alt`) return
`NaN` until every dependency has been received. Without this guard the
depth-history buffer gets seeded with `NaN` samples that poison the
canvas rendering.

@ES-Alexander ES-Alexander left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving because code generally looks ok, and the feature does seem to work.

Some comments/suggestions/questions :-)

Comment on lines +32 to +42
// Substitutes `NaN` for any data-lake variable that has not been populated yet,
// so expressions referencing missing dependencies evaluate to `NaN` instead of
// throwing a SyntaxError. Once the missing variable's data arrives, the listener
// fires and the expression is re-evaluated with the real value.
const expressionReplaceFunction = (match: string): string => {
const variableId = match.replace('{{', '').replace('}}', '').trim()
if (!variableId) return 'NaN'
const variableData = getDataLakeVariableData(variableId)
if (variableData === undefined) return 'NaN'
return variableData.toString()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this cause problems for variables that were expected to be strings? NaN makes sense for numerical data, but maybe undefined is more general?

If necessary we could have a very rough proxy where NaN is used if the computed output is expected to be numerical, but that assumes no numerical output is determined using string inputs (which would happen if you were mapping a set of string options to some meaningful numbers, for example).

Alternatively we could extend the specifier syntax to include an optional "value if undefined", but that adds complexity that ideally we would be able to avoid users needing to deal with 🤷‍♂️

Comment on lines +115 to +116
const currentDelay = nextDelayToEvaluateFaillingTransformingFunction[func.id] || 10
const lastTimeTried = lastTimeTriedToEvaluateFaillingTransformingFunction[func.id] || 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const currentDelay = nextDelayToEvaluateFaillingTransformingFunction[func.id] || 10
const lastTimeTried = lastTimeTriedToEvaluateFaillingTransformingFunction[func.id] || 0
const currentDelay = nextDelayToEvaluateFailingTransformingFunction[func.id] || 10
const lastTimeTried = lastTimeTriedToEvaluateFailingTransformingFunction[func.id] || 0

There's an extra l in "Failing".

This is continued usage of an existing variable, so not critical to fix in this PR, though it does add more instances that need fixing if it's just used as-is.

Comment on lines +9 to +14
/**
* Optional MAVLink path suffixes (e.g. `SCALED_PRESSURE2/press_abs`) that
* should be migrated to this option when found in persisted widget settings.
* Used to retire legacy paths in favor of the compound variables defined in
* `predefined-resources.ts`.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely excessive commentary for a migration helper. Also if we merge this PR without merging #2725 we can skip the migration entirely.

* @param {number} paramValue - Parameter value
*/
private setParameterInDataLake(paramName: string, paramValue: number): void {
const dataLakeId = `/vehicle/${this.currentSystemId}/parameters/${paramName}`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const dataLakeId = `/vehicle/${this.currentSystemId}/parameters/${paramName}`
const dataLakeId = `vehicle/${this.currentSystemId}/parameters/${paramName}`

I believe we're generally avoiding root slashes (with MAVLink kept as a legacy exception), because they don't really contribute anything.

More generally, it may be better to expose params through the existing MAVLink path (with param_id as the instancing field), and maybe just make the name nice? Technically the parameter protocol is not limited to vehicles, and can be from any MAVLink component (e.g. a robot arm and autopilot could be independent components in the same system that each have their own parameters).

That being said, I believe Cockpit only requests parameters from the autopilot at this stage, so if we're assuming that won't change then the current approach may be sufficient.

createDataLakeVariable({
id: dataLakeId,
name: `Parameter ${paramName} (MAVLink / System: ${this.currentSystemId})`,
type: 'number',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's likely worth explicitly including allowUserToChangeValue: false,, possibly with a TODO comment about eventually supporting parameter setting (for relevant params) as well.

id: dataLakeId,
name: `Parameter ${paramName} (MAVLink / System: ${this.currentSystemId})`,
type: 'number',
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we would include metadata like the per-firmware parameter descriptions, a breakdown of bitmasks and booleans vs regular numbers, and longer-term the units too. None are required for this PR, since it's already a net improvement as-is.


return input.toString().replace(dataLakeInputRegex, (match) => replaceFunctionToUse(match))
let current = input.toString()
for (let pass = 0; pass < MAX_DATA_LAKE_RESOLUTION_PASSES; pass++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this happen every time a compound or subtitle string variable gets evaluated?

From a performance standpoint it seems valuable to cache the outermost IDs, and set up a second set of listeners for the nested ones so re-resolving only happens when one of the dependencies has actually changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add calculated depth variable to overcome problems on ArduSub

2 participants