Skip to content

Support custom source fields from GPX extensions and FIT developer fields - #42

Merged
walkersutton merged 1 commit into
mainfrom
claude/cyclemetry-issue-38-magj7k
Sep 16, 2026
Merged

walkersutton merged 1 commit into
mainfrom
claude/cyclemetry-issue-38-magj7k

Conversation

@walkersutton

Copy link
Copy Markdown
Owner

Summary

Adds support for capturing and rendering numeric fields from GPX <extensions> and FIT developer fields that don't map to built-in attributes. These custom fields are discovered per-activity, surfaced as custom:<field_name> metrics, and can be used in value readouts, plots, meters, and color-by bands just like built-in metrics.

Key Changes

Activity parsing and storage:

  • Added custom: BTreeMap<String, Vec<f64>> field to Activity to store per-sample custom field series
  • Added custom: BTreeMap<String, f64> field to TrackPoint to accumulate readings during parsing
  • Implemented insert_custom() and insert_custom_text() helpers to validate and insert custom field readings with safeguards:
    • Rejects reserved names (built-in attributes and source spellings already consumed by parsers)
    • Rejects non-finite values, empty/oversized names
    • Caps per-point fields at MAX_CUSTOM_FIELDS (32) and per-name length at MAX_CUSTOM_NAME_LEN (48)
  • Updated GPX parser to capture unmodelled numeric leaves inside <extensions> as custom fields
  • Updated FIT parser to capture unmodelled record fields (including developer fields) as custom fields

Metric access and rendering:

  • Added custom_metric_key() and is_custom_metric() helpers to identify and extract field names from custom: tokens
  • Refactored wall_clock_sample() → wall_clock_pos() + sample_at_pos() to resolve position once per frame, allowing both fixed attributes and open-ended custom series to read the same instant
  • Introduced SamplePos enum to represent position on the sample grid (exact or interpolated) with value(), step(), and course() methods for reading different series types
  • Updated get_scalar() to resolve custom metrics via custom_metric_key()
  • Updated plot_data() to resolve custom metrics via custom_metric_key()
  • Custom fields interpolate as continuous values (safe default for sensor channels)

Scene resampling:

  • Custom series survive resample() via linear interpolation, consistent with other continuous attributes

Frontend integration:

  • Added CUSTOM_PREFIX constant and isCustomMetric() helper in ElementProperties.svelte
  • Implemented customMetricLabel() to humanize device-written field names (split snake/kebab/camelCase, sentence-case)
  • Custom metrics appear in value, plot, and meter dropdowns under a "Custom" group
  • Custom fields work with all element types that accept live metrics (value readouts, plots, meters, color-by bands)

Testing:

  • Added comprehensive test suite covering:
    • GPX extension capture and metric availability
    • Reserved name filtering (no shadowing of built-in attributes)
    • Gap-filling behavior (hold-last semantics)
    • Non-numeric and container element rejection
    • Per-activity field count capping
    • FIT developer field capture
    • Scene resampling survival
    • End-to-end rendering in frame.rs

Implementation Details

  • Custom fields are keyed by source name (e.g., "g_force_lat", "Lean Angle") rather than a constant, so they sit outside the fixed attribute match in get_scalar() and plot_data()
  • Reserved name list includes both built-in attribute tokens and source spellings already consumed by parsers (e.g., "watts", "hr", "ele") to prevent the same channel appearing twice under different names
  • Per-point cap (32 fields) prevents pathological files from creating hundreds of series; union cap at build_from_points() bounds what actually becomes metrics
  • Custom series use the same hold-last behavior as gears and lap counts when a sensor samples slower than the GPS track
  • No unit table exists for custom fields, so they render raw values with manual suffixes in templates

https://claude.ai/code/session_01BkMQneMjCeBXkpdCpTucTE

Cyclemetry models a fixed set of telemetry attributes, so any other numeric
channel a rider's device records — lean angle, g-force, core temperature,
a Connect IQ developer field — is dropped at parse time no matter that the
file carries it. Adding one has meant editing ~16 hardcoded sites across
the parsers, the resampler, the accessors and the editor's metric lists,
which is why the list has stayed short.

Capture them generically instead. Each numeric GPX/TCX <extensions> leaf and
each FIT record field outside the modelled profile (developer fields included)
becomes a `custom:<field name>` metric backed by a series in Activity::custom,
discovered per activity rather than declared in code. They flow through
valid_attributes like any built-in, so they reach the editor's value, plot,
meter and color-by pickers with no per-metric wiring, and render with the
identity unit conversion — what the file recorded is what shows, with the
element's manual suffix.

Guards on what becomes a metric: names already feeding a built-in attribute
are skipped so a channel can't appear twice, non-numeric and container
elements are rejected, and the field count is capped (per point, and again
over the union across points) so an open-ended namespace can't turn into
hundreds of series and an unusable dropdown. A field missing from individual
points — a sensor sampling slower than the GPS track — holds its last reading
rather than dropping to zero.

Resampling previously duplicated the same series list across sample_at_index
and sample_between. Both collapse into SamplePos, which resolves a frame's
grid position once and then reads any series from it, so the open-ended custom
map rides the existing interpolate/trim path without a per-frame allocation.

Addresses the generalisation half of #38; no built-in metric changes behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkMQneMjCeBXkpdCpTucTE
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
cyclemetry Ready Ready Preview Sep 8, 2026 11:39am UTC

Request Review

@walkersutton
walkersutton marked this pull request as ready for review September 16, 2026 19:37
@walkersutton
walkersutton merged commit 9115988 into main Sep 16, 2026
3 checks passed
@walkersutton
walkersutton deleted the claude/cyclemetry-issue-38-magj7k branch September 30, 2026 03:07

This branch was successfully deployed

1 active deployment
Preview — fc675f9c Deployed Sep 8, 2026 by vercel[bot]
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.

2 participants