Skip to content

fix(position): convert fixed altitude between feet and meters#1267

Open
dkxmercury wants to merge 4 commits into
meshtastic:mainfrom
dkxmercury:fix/position-altitude-units
Open

fix(position): convert fixed altitude between feet and meters#1267
dkxmercury wants to merge 4 commits into
meshtastic:mainfrom
dkxmercury:fix/position-altitude-units

Conversation

@dkxmercury

@dkxmercury dkxmercury commented Jul 11, 2026

Copy link
Copy Markdown

Description

The fixed-position altitude input is labelled "Feet" when the display units are set to Imperial, but the entered value is stored (and the node's value is loaded) as raw meters with no conversion. So a user who wants 1025 ft has to type 312, and the field shows the wrong unit for the stored value. The altitude protobuf field is an integer number of meters (mesh.proto: optional int32 altitude = 3).

This converts altitude between feet and meters at the edges of the Position form when the display units are Imperial, so the number the user types matches the label.

Related Issues

Fixes #1051

Changes Made

  • Add pure feetToMeters / metersToFeet helpers (and METERS_PER_FOOT) in apps/web/src/core/utils/geo.ts.
  • Convert altitude at the three edges of Position.tsx when units are Imperial: form load, submit (into the setFixedPosition admin message), and the "use browser location" fill.
  • Round the stored altitude to integer meters to match the int32 protobuf field, and guard against a cleared/NaN field.
  • Add apps/web/src/core/utils/geo.test.ts covering the conversion and the exact [Bug]: Fixed altitude entry (no GPS) prompts for feet but treats value as meters. #1051 scenario (1025 ft to 312 m to ~1024 ft).

Note: the report also mentioned decimal values being rejected. That is not reproducible on current main (the input parses with Number.parseFloat and validation is z.coerce.number() with no .int()), so this PR only addresses the units mismatch. Metric users are unaffected, since the default units are Metric.

Testing Done

  • Added geo.test.ts (Vitest), 5 cases. Verified it fails on unmodified code (feetToMeters is not a function) and passes with the fix.
  • oxlint clean on the changed files.
  • vitest run src/core/utils/geo.test.ts passes (5/5).

Checklist

  • Code follows project style guidelines
  • Documentation has been updated or added
  • Tests have been added or updated
  • All i18n translation labels have been added (no new i18n keys; the existing {{unit}} interpolation is reused)

Summary by CodeRabbit

  • New Features
    • Added consistent altitude unit handling across the Position settings UI, including correct “Meters” vs “Feet” labeling.
    • Browser geolocation altitude is now converted to the effective display unit for the altitude field.
  • Bug Fixes
    • Fixed saving altitude to reliably convert imperial feet back into stored meters, with safeguards for cleared/invalid inputs.
  • Tests
    • Added regression tests covering imperial/metric conversion, rounding, geolocation conversion, and correct message queuing behavior.

…stic#1051)

The fixed-altitude field is labelled Feet when the display units are set
to Imperial, but the value was stored and loaded as raw meters with no
conversion, so entering 1025 stored 1025 m instead of ~312 m.

Add feetToMeters/metersToFeet helpers in core/utils/geo.ts and convert at
the form load, submit, and geolocation-fill edges when units are Imperial.
Altitude is rounded to integer meters to match the int32 protobuf field.

Fixes meshtastic#1051
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

@dkxmercury is attempting to deploy a commit to the Meshtastic Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant

CLAassistant commented Jul 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b12bf278-0660-407c-9a8c-4c19f10fa510

📥 Commits

Reviewing files that changed from the base of the PR and between 4526023 and 4a99400.

📒 Files selected for processing (3)
  • apps/web/public/i18n/locales/en/common.json
  • apps/web/src/components/PageComponents/Settings/Position.test.tsx
  • apps/web/src/components/PageComponents/Settings/Position.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/components/PageComponents/Settings/Position.tsx
  • apps/web/src/components/PageComponents/Settings/Position.test.tsx

📝 Walkthrough

Walkthrough

Altitude conversion helpers were added and integrated into Position settings so imperial and metric altitude values are converted consistently for display, browser geolocation, fixed-position submission, and protobuf storage. Regression and component tests cover both unit systems.

Changes

Altitude unit handling

Layer / File(s) Summary
Altitude conversion contract
apps/web/src/core/utils/geo.ts, apps/web/src/core/utils/geo.test.ts
Adds feet-to-meters and meters-to-feet helpers using integer-meter storage, with conversion, rounding, and round-trip regression tests.
Position settings integration
apps/web/src/components/PageComponents/Settings/Position.tsx, apps/web/public/i18n/locales/en/common.json
Applies effective display units to browser-location input, rendered altitude values, fixed-position protobuf submission, and altitude field labels.
Position behavior validation
apps/web/src/components/PageComponents/Settings/Position.test.tsx
Verifies imperial and metric display, submission conversion, geolocation conversion, clearing behavior, and test isolation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Position
  participant UseBrowserLocationButton
  participant BrowserGeolocation
  participant GeoHelpers
  participant ProtobufPayload

  Position->>UseBrowserLocationButton: Pass isImperial
  UseBrowserLocationButton->>BrowserGeolocation: Request location
  BrowserGeolocation-->>UseBrowserLocationButton: Return altitude in meters
  UseBrowserLocationButton->>GeoHelpers: Convert meters to feet when imperial
  GeoHelpers-->>Position: Set displayed altitude
  Position->>GeoHelpers: Convert entered feet to meters when imperial
  GeoHelpers-->>ProtobufPayload: Provide rounded integer meters
Loading

Suggested reviewers: hunter275

Poem

I’m a small rabbit hopping light,
Feet to meters, meters right.
GPS brings numbers through,
Forms now show the proper view.
Protobuf gets whole meters neat—
Conversion carrots are a treat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix: converting fixed-position altitude between feet and meters.
Description check ✅ Passed The PR description covers the problem, related issue, key changes, testing, and checklist in the required template.
Linked Issues check ✅ Passed The changes directly address issue #1051 by converting fixed altitude input/output between Imperial feet and stored meters.
Out of Scope Changes check ✅ Passed The added i18n key and tests are supporting changes for the altitude conversion fix, with no unrelated scope evident.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/components/PageComponents/Settings/Position.tsx (1)

61-68: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add integration tests for the Position conversion paths.

The current tests do not exercise Imperial form loading, browser geolocation, or the queued setFixedPosition protobuf message. Add coverage proving that 1025 ft is displayed/submitted as 312 meters and that metric behavior remains unchanged.

Also applies to: 122-141, 164-180

🤖 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 `@apps/web/src/components/PageComponents/Settings/Position.tsx` around lines 61
- 68, Add integration tests for the Position component covering Imperial form
loading and browser geolocation conversion, asserting that 1025 feet is
displayed/submitted as 312 meters while metric values remain unchanged. Also
exercise the queued setFixedPosition protobuf message path, including the
converted altitude, using the existing Position test setup and geolocation
mocks.
🤖 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.

Nitpick comments:
In `@apps/web/src/components/PageComponents/Settings/Position.tsx`:
- Around line 61-68: Add integration tests for the Position component covering
Imperial form loading and browser geolocation conversion, asserting that 1025
feet is displayed/submitted as 312 meters while metric values remain unchanged.
Also exercise the queued setFixedPosition protobuf message path, including the
converted altitude, using the existing Position test setup and geolocation
mocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a186f92f-d5d2-4dc7-9c87-499ca2894aa6

📥 Commits

Reviewing files that changed from the base of the PR and between 38c7569 and 3666342.

📒 Files selected for processing (3)
  • apps/web/src/components/PageComponents/Settings/Position.tsx
  • apps/web/src/core/utils/geo.test.ts
  • apps/web/src/core/utils/geo.ts

meshtastic#1051)

Render the real Position form and assert the conversion at each edge:
Imperial load shows meters as feet, submitting feet queues integer meters
into the setFixedPosition message, metric values pass through unchanged,
and the browser-geolocation fill converts meters to feet.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/components/PageComponents/Settings/Position.test.tsx (1)

151-162: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add regression coverage for cleared/NaN altitude.

The production path explicitly maps non-finite altitude input to 0 meters, but this suite only submits finite values. Add a cleared-field case (and NaN if validation permits it) asserting the queued altitude is 0.

🤖 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 `@apps/web/src/components/PageComponents/Settings/Position.test.tsx` around
lines 151 - 162, Add regression coverage in the Position test suite for cleared
altitude input, using the existing setup, altitudeInput, and lastQueuedPosition
helpers; submit an empty value and assert the queued position altitude is 0. If
the validation path permits NaN input, add a corresponding case that also
verifies altitude 0, without altering the existing finite-value tests.
🤖 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 `@apps/web/src/components/PageComponents/Settings/Position.test.tsx`:
- Around line 21-74: Move the shared mutable state and mock functions currently
declared as top-level let/const bindings into a single vi.hoisted state object.
Update the factories for useDevice, useConfigEditor, useMyNodeAsProto, and
useToast, along with mockGetEffectiveConfig and mockEditor, to reference the
corresponding mocks.* properties while preserving their existing behavior.

---

Nitpick comments:
In `@apps/web/src/components/PageComponents/Settings/Position.test.tsx`:
- Around line 151-162: Add regression coverage in the Position test suite for
cleared altitude input, using the existing setup, altitudeInput, and
lastQueuedPosition helpers; submit an empty value and assert the queued position
altitude is 0. If the validation path permits NaN input, add a corresponding
case that also verifies altitude 0, without altering the existing finite-value
tests.
🪄 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: 84ef096f-ac08-4f99-baa5-c67343163ba7

📥 Commits

Reviewing files that changed from the base of the PR and between 3666342 and 4526023.

📒 Files selected for processing (1)
  • apps/web/src/components/PageComponents/Settings/Position.test.tsx

Comment on lines +21 to +74
// --- Mutable per-test state (read lazily by the mock factories below) --------
// Names are prefixed with `mock` so vitest's vi.mock hoisting whitelist allows
// the factories to reference them.
let mockDisplayUnits: Protobuf.Config.Config_DisplayConfig_DisplayUnits;
let mockPositionConfig: Protobuf.Config.Config_PositionConfig;
let mockMyNode: Protobuf.Mesh.NodeInfo | undefined;

const mockSetRadioSection = vi.fn();
const mockQueueAdminMessage = vi.fn();
const mockToast = vi.fn();
const mockGetCurrentPosition = vi.fn();

// A minimal signal-shaped object; the mocked useSignal just returns `.value`.
const mockEditor = {
radio: { value: {}, peek: () => ({}), subscribe: () => () => {} },
setRadioSection: mockSetRadioSection,
queueAdminMessage: mockQueueAdminMessage,
};

const mockGetEffectiveConfig = (configCase: string) => {
if (configCase === "display") {
return { units: mockDisplayUnits };
}
if (configCase === "position") {
return mockPositionConfig;
}
return undefined;
};

// useWaitForConfig throws a suspense promise until config is present; the render
// path here supplies config directly, so it is a no-op in tests.
vi.mock("@app/core/hooks/useWaitForConfig", () => ({
useWaitForConfig: () => {},
}));

vi.mock("@core/stores", () => ({
useDevice: () => ({
config: { position: mockPositionConfig },
getEffectiveConfig: mockGetEffectiveConfig,
}),
}));

vi.mock("@meshtastic/sdk-react", () => ({
useConfigEditor: () => mockEditor,
useSignal: (signal: { value?: unknown }) => signal?.value ?? {},
}));

vi.mock("@core/hooks/useNodesAsProto.ts", () => ({
useMyNodeAsProto: () => mockMyNode,
}));

vi.mock("@core/hooks/useToast.ts", () => ({
useToast: () => ({ toast: mockToast }),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -n '"vitest"|vi\.mock|vi\.hoisted|mockDisplayUnits|mockEditor' \
  --glob 'package.json' --glob '*lock*' \
  apps/web/src/components/PageComponents/Settings/Position.test.tsx

Repository: meshtastic/web

Length of output: 763


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\n== vitest version ==\n'
rg -n '"vitest"' apps/web/package.json package.json pnpm-lock.yaml yarn.lock package-lock.json --max-count 5 || true

printf '\n== test file context ==\n'
sed -n '1,130p' apps/web/src/components/PageComponents/Settings/Position.test.tsx

Repository: meshtastic/web

Length of output: 5096


🌐 Web query:

Vitest vi.mock hoisted mock prefix documentation vi.hoisted factory variables

💡 Result:

In Vitest, the vi.mock function is hoisted to the top of the file, meaning it is executed before all other imports and code [1][2]. Because of this behavior, you cannot directly reference variables declared in the outer scope within the vi.mock factory function [1][3]. To work around this limitation and pass variables into a mocked module, you should use the vi.hoisted function [1][4]. vi.hoisted: This function allows you to define variables that are hoisted along with your vi.mock calls, making them available within the factory function [1][4]. - It must be defined before the vi.mock call [1]. - Both vi.hoisted and vi.mock will be hoisted in the order they are defined in the file [4]. Example usage: const { someVariable } = vi.hoisted( => { return { someVariable: 'initial-value' } }) vi.mock('./module-path', => { return { getValue: => someVariable } }) Unlike Jest, Vitest does not automatically allow variables to be used inside vi.mock based on specific naming prefixes (like starting with 'mock'). You must explicitly use vi.hoisted to share state between your test logic and the factory. If you do not require hoisting and need access to variables in the local scope, you can use vi.doMock, which is not hoisted, though it will only affect subsequent imports [1][3].

Citations:


Move the shared mock state into vi.hoisted.
vi.mock is hoisted, so these factories shouldn’t close over normal top-level let/const bindings. Use one vi.hoisted(() => ({ ... })) state object and reference mocks.* inside each factory.

🤖 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 `@apps/web/src/components/PageComponents/Settings/Position.test.tsx` around
lines 21 - 74, Move the shared mutable state and mock functions currently
declared as top-level let/const bindings into a single vi.hoisted state object.
Update the factories for useDevice, useConfigEditor, useMyNodeAsProto, and
useToast, along with mockGetEffectiveConfig and mockEditor, to reference the
corresponding mocks.* properties while preserving their existing behavior.

Source: MCP tools

@dkxmercury

Copy link
Copy Markdown
Author

Thanks for the review. On the cleared / NaN altitude nitpick: I looked into it, and clearing the altitude field produces NaN, which fails the altitude: z.coerce.number().optional() rule in PositionValidationSchema. Since DynamicForm only submits when the form is valid, a cleared field never reaches onSubmit, so no setFixedPosition message is queued (I verified that nothing is queued in that case). The Number.isFinite(data.altitude) ? ... : 0 branch is therefore a defensive fallback that the form's submit path cannot reach with a non-finite value, so there is no queued-altitude regression to assert for a cleared field.

Happy to add a test asserting that a cleared field does not queue a message, if you would like that behavior documented.

@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
web-test Ready Ready Preview, Comment Jul 14, 2026 1:48am

Request Review

@Hunter275

Copy link
Copy Markdown
Member

@dkxmercury This looks really good. Look into the code change that CodeRabbit is suggesting.

@Hunter275

Copy link
Copy Markdown
Member

Also @dkxmercury can you look into setting up these values to work with i18n?

Read CONTRIBUTING_I18N_DEVELOPER_GUIDE.md for guidelines.

@danditomaso

Copy link
Copy Markdown
Collaborator

Also, did you investigate the remainder of the codebase for any other usages of meters or feet that should be updated?

@danditomaso danditomaso requested a review from Hunter275 July 12, 2026 02:31
…ng (meshtastic#1051)

Add a `unit.foot` entry to the English common.json (mirroring `unit.meter`)
and use `t("unit.foot.plural")` / `t("unit.meter.plural")` for the altitude
field's unit label and suffix. Other locales are handled by Crowdin, which
uploads only the en sources.
…it (meshtastic#1051)

A cleared number field becomes NaN, which fails the altitude zod validation,
so the form never submits. Document that no setFixedPosition message is queued.
@dkxmercury

Copy link
Copy Markdown
Author

Thanks for the review! All three points addressed:

i18n (@Hunter275): the altitude field now uses translation keys instead of hardcoded strings. I added a unit.foot entry to en/common.json (mirroring the existing unit.meter) and wired the field's label and suffix to t("unit.foot.plural") / t("unit.meter.plural"). Per CONTRIBUTING_I18N_DEVELOPER_GUIDE.md I only touched the English source; Crowdin handles the other locales.

Cleared / NaN altitude (@Hunter275 / CodeRabbit): I looked into CodeRabbit's suggestion. Clearing the field produces NaN, which fails the altitude: z.coerce.number().optional() validation, so DynamicForm never submits and nothing is queued (the Number.isFinite(...) ? ... : 0 branch is a defensive fallback the form can't reach with a non-finite value). I added a test documenting exactly that: a cleared field does not queue a setFixedPosition.

Other feet/meters usages (@danditomaso): good call, I swept the codebase. This PR fixes the write side (the fixed-position config form). There's no shared display-units helper, and I found three read-side displays that render altitude in raw meters and ignore the Imperial/Metric setting (same bug class):

  • NodeDetail.tsx (map popup, ~line 183)
  • NodeDetailsDialog.tsx (~line 423)
  • LocationResponseDialog.tsx (~line 75)

Two of the three are presentational components with no config access, so doing this right is best done as a small shared display-units helper applied consistently, which felt like scope creep on this form fix. I'd suggest a follow-up issue for the read-side altitude displays; happy to open it and take it on if you agree.

All tests pass and lint is clean.

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.

[Bug]: Fixed altitude entry (no GPS) prompts for feet but treats value as meters.

4 participants