Skip to content

Add calculated depth variable - #2738

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

Add calculated depth variable#2738
rafaellehmkuhl wants to merge 6 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.

@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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — renamed to Failing on the backoff maps.

Comment thread src/libs/data-sources/altitude.ts Outdated
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Shortened the JSDoc. Kept the field itself: #2725 already landed without shipping SCALED_PRESSURE* as a preset on master, so this only helps anyone who persisted settings from an earlier draft of this work. Cheap enough to keep.

Comment thread src/libs/vehicle/mavlink/vehicle.ts Outdated
* @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.

@rafaellehmkuhl rafaellehmkuhl Jul 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Dropped the leading slash — path is now vehicle/{systemId}/parameters/{paramName}, and the compound-variable expressions match. Keeping the dedicated vehicle-parameter namespace for now since Cockpit only requests params from the autopilot.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done — set to false, with a TODO for eventually writing selected parameters back.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. I believe a more broadly work on that front is the needed solution.


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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Multi-pass only runs when a listened dependency changes (or when discovering IDs for subscription), capped at 10 and typically 1–2 passes on these short expressions. syncDependencies already listens to both the inner IDs (autopilotSystemId) and the outer resolved paths.

But let me think about this one performance-wise.

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.
@rafaellehmkuhl
rafaellehmkuhl force-pushed the add-calculated-depth-variable branch from 1e9fc44 to 3e26fba Compare July 22, 2026 21:52
Compound-variable altitude sources (e.g. `baro2.pressure_alt`) return
`NaN` until every dependency has been received. Without this guard the
widgets display "NaN m" and DepthHUD's history buffer gets poisoned.
Treat non-finite values as missing so consumers keep their existing
"--" / blank fallbacks.
@rafaellehmkuhl
rafaellehmkuhl force-pushed the add-calculated-depth-variable branch from 3e26fba to 799a015 Compare July 22, 2026 22:14

@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.

  • 🛑 values don't update
    • I suspect the templating isn't properly accounting for internal variables when determining which variables should trigger updates of a compound variable, so because the system ID doesn't tend to update the value only gets evaluated once and then stays constant
    • If I edit the baro2.pressure_alt definition to use hardcoded 1s for the system ID then it updates fine
      • The preferred solution here is obviously to fix the triggering issue, but if desired this PR could change to hardcoded system IDs and the templating stuff can get fixed in a later PR

Side notes:

  • Searching the data-lake seems to cause low-performance spikes, though this apparently also happens in master
  • I don't remember how data-lake search indexing works, but nothing I search seems to help me find the pressure_alt variables - typing "baro2" or "pressure_alt" results in no filtering
    • Possibly we need to be splitting IDs by slashes and dots as part of the searching process?
  • In the compound variable definition, only the internal variables get the hover treatment
    • The outer variable (using the inner one as part of its ID) has no hover helper to show the name and value
  • Various incoming data lake variables show up (in the table) as "User defined", including CPU temperature, as well as apparently MAVLink Camera Tilt and Network Latency

@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

@ES-Alexander my bad I'm still updating it. Will ping when done.

@ES-Alexander

ES-Alexander commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Did this get lost, or are you hoping #2872 / #2873 makes it unnecessary or something?

@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

Did this get lost, or are you hoping #2872 / #2873 makes it unnecessary or something?

I was hopping #2872 made it unnecessary. What do you guys think?

Pinging @Williangalvani as well on the matter.

@Williangalvani

Copy link
Copy Markdown
Member

I do think it is unnecessary, I'm also adding warnings on Sub when home gets set in not-great frames

@ES-Alexander

Copy link
Copy Markdown
Contributor

I'd want confirmation from the people running into the issue that it's actually resolved before we close down this approach. I suspect #2873 and/or #2874 is needed, to avoid the issue triggering during mission planning, with the latter also preventing users from accidentally triggering it by manually setting the home position from the map.


Independently, commits 1, 3, 4, and 6 are for incidental fixes and improvements that are useful in general, regardless of whether we want the dedicated depth variable. 1 in particular would be nice to get merged somewhere, since it's useful for a lot of other applications. 4 would allow us to use dynamic MAVLink System IDs, but IIRC that functionality wasn't working at last review.

As a side note, 1 has an issue linked in the commit message, which will cause notification spam if merged.

@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

I'd want confirmation from the people running into the issue that it's actually resolved before we close down this approach. I suspect #2873 and/or #2874 is needed, to avoid the issue triggering during mission planning, with the latter also preventing users from accidentally triggering it by manually setting the home position from the map.

I do plan to bring #2873 back. Was rebasing it last Friday. Will finish today and put again for review.

Independently, commits 1, 3, 4, and 6 are for incidental fixes and improvements that are useful in general, regardless of whether we want the dedicated depth variable. 1 in particular would be nice to get merged somewhere, since it's useful for a lot of other applications. 4 would allow us to use dynamic MAVLink System IDs, but IIRC that functionality wasn't working at last review.

Totally agree. Merging this PR or not my plan is to move those dedicated commits to other PRs for review.

As a side note, 1 has an issue linked in the commit message, which will cause notification spam if merged.

Good catch. Will remove it.

@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

I'd want confirmation from the people running into the issue that it's actually resolved before we close down this approach.

Any news on this one?
I'm removing this PR from the 1.19 stable project by the meantime, but we can add back if we found out its needed.

@ES-Alexander

Copy link
Copy Markdown
Contributor

As I understand it we know that planning a mission still triggers it - I don't think it's worth asking for confirmation that it doesn't occur anymore until one of those fix PRs has been merged, right?

It seems #2873 has already been approved - is there something holding it up?

@rafaellehmkuhl

Copy link
Copy Markdown
Member Author

As I understand it we know that planning a mission still triggers it - I don't think it's worth asking for confirmation that it doesn't occur anymore until one of those fix PRs has been merged, right?

It seems #2873 has already been approved - is there something holding it up?

Just time to properly rebase it 😅

As I understand with #2873 merged you plan to ask people to test if they still see the bug. That's it?

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

3 participants