This Val Town HTTP val converts a GPX route or track into a Garmin-compatible
.fit course file.
Inspired by the Fit Route Converter site as well as the more advanced Tobias Fork (Github) of Fit Route Converter
Deployed endpoint:
https://gwoods22--e235379a275711f086ff569c3dd06744.web.val.run/convert
Send a POST request with either a multipart GPX upload or raw GPX XML.
curl -L \ -F "file=@route.gpx" \ -o route.fit \ "https://gwoods22--e235379a275711f086ff569c3dd06744.web.val.run/convert"
curl -L \ -X POST \ -H "Content-Type: application/gpx+xml" \ --data-binary @route.gpx \ -o route.fit \ "https://gwoods22--e235379a275711f086ff569c3dd06744.web.val.run/convert"
Timing is included in the generated FIT file. By default, the route is timed at
20 kph. Override that by sending X-Average-Speed with a positive numeric kph
value:
curl -L \ -H "X-Average-Speed: 25" \ -F "file=@route.gpx" \ -o route.fit \ "https://gwoods22--e235379a275711f086ff569c3dd06744.web.val.run/convert"
Invalid values, such as 0, negative numbers, or non-numeric strings, return a
400 JSON error.
The HTTP handler reads the GPX file, logs the incoming filename, parses the GPX track or route points, computes cumulative distance with the haversine formula, and assigns timestamps based on distance and average speed.
The converter then writes a minimal FIT course file containing:
- file ID and course metadata
- lap summary with total distance, timer time, and elevation gain/loss when elevation exists
- timed record points with latitude, longitude, distance, and optional altitude
- timer start/stop events
- course-point turn hints derived from bearing changes
The FIT encoder writes the file header, message definitions, data records, and CRC trailer directly in TypeScript.
The conversion logic is adapted from Nick Holloway's fit-route project,
especially the FIT encoder files in:
https://gitlab.com/nwholloway/fit-route/-/tree/main/public/js/fit
This project ports the relevant browser-side ideas into a Val Town HTTP val.