Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Activity Profile Editor for Garmin Edge

What it does

This toolkit reads and edits Garmin Edge 530 Activity Profile screens directly, on a computer, instead of through the on-device menu — reordering screens, changing which data fields appear and in what layout, showing/hiding or deleting screens outright, cloning a profile under a new name, restoring a profile from backup, and batching several changes into one device write/restart. Cloning duplicates the entire profile file, not just its screens — every other setting you've already tuned (navigation, alerts, sensor pairing, and everything else that isn't a data screen) comes along automatically, so creating a second, similar profile doesn't mean redoing all of that by hand the way it does starting a new profile on-device. It reaches data fields the on-device editor doesn't expose at all (power-meter/ Shimano Di2 metrics, Torque Effectiveness, Balance, TSS/IF, and more).

Two ways to use it, sharing the same validated backend and the same automatic backup-before-every-write safety net: a point-and-click GUI (gui_app.py) that walks through detect → back up → edit → review → deploy, or a set of command-line tools (fit_dump.py, fit_patch.py, and others) for the same operations plus lower-level inspection, useful for scripting or checking exactly what a change will do before it's written.

Who this is for

This toolkit is built for riders who are comfortable with a terminal and a pip install, not (yet) a plug-and-play app for the general public. There's no installer or double-clickable application bundle — setup means cloning this repo, running a setup script (macOS) or a couple of pip install commands by hand (Windows), and running the GUI or CLI tools from a terminal with that environment active. If that's a step you've done before for some other tool, you'll be fine here; if "open a terminal" isn't something you do, this project isn't there yet.

What you get in exchange for that setup: direct editing access to Activity Profile screens and data fields the on-device editor doesn't expose at all (power-meter/Shimano Di2 metrics, Torque Effectiveness, Balance, TSS/IF, and more), the ability to delete a screen outright rather than just hide it, clone a profile under a new name, restore a profile that's no longer on the device, batch several changes into one device restart, and automatic backups before every write.

License

Released under the MIT License — free to use, modify, and redistribute, with no warranty.

Disclaimer

This is an independent, unofficial project. It is not affiliated with, endorsed by, or sponsored by Garmin Ltd. or its subsidiaries. "Garmin" and "Edge" are trademarks of Garmin Ltd. or its subsidiaries; they're used here only to describe which devices this toolkit is compatible with, not to claim any official status.

The data_screen message and related undocumented fields this project relies on were reverse-engineered entirely through black-box observation — making isolated changes on a real device (or via a custom byte-patcher) and diffing the resulting files — never by decompiling, disassembling, or otherwise reverse-engineering Garmin's own software or SDK. The official garmin_fit_sdk Python package is used only as a normal dependency, for its documented decode/encode support of standard FIT messages.

Use at your own risk. This toolkit patches undocumented file structures and writes the result back to your device through an undocumented pathway (NewFiles/). It's been tested carefully against real hardware throughout development, but Garmin could change this behavior in a future firmware update without notice, and no guarantee is made against corrupting a profile, losing data, or other unintended device behavior. Back up your profiles before use (this toolkit does this automatically before every write, but the responsibility is ultimately yours) and don't run it on a device you can't afford to have misbehave.

No warranty of any kind is provided, express or implied — see LICENSE for the full legal text.

Setup

The toolkit itself (CLI tools and GUI) runs on both macOS and Windows — device detection is confirmed working on real hardware on both (see garmin_device.py's changelog). Both platforms have a one-command setup script (install.sh/install_windows.bat); the after-setup launch path differs a bit, covered below for each.

macOS

Run the install script — it checks your python3 version, creates a dedicated .venv (nothing touches your system or Homebrew Python), and installs both dependencies (garmin-fit-sdk and wxPython) into it:

./install.sh

Safe to re-run any time; add --upgrade to update already-installed packages, or --help for details. It's a plain bash script, no network access beyond pip, and no admin/sudo required.

Once that's done, launching the GUI from here on doesn't need Terminal at all — double-click launch_gui.command in Finder. (The very first double-click will likely be refused by Gatekeeper as "from an unidentified developer" — right-click, or Control-click, the file and choose Open instead, just that first time; every launch after that works with a normal double-click. This is standard macOS behavior for any downloaded/unsigned script, not specific to this toolkit.) This is the macOS equivalent of Windows' native double-click-gui_app.py convenience below — macOS has no matching file association to hijack the way Windows' installer does, so a small launcher script is the actual fix here, not a workaround; see PROJECT_NOTES.md "macOS double-click launcher" for the full story of why.

Prefer to do it by hand, or need just the CLI tools without the GUI's wxPython dependency? The manual equivalent:

python3 -m venv .venv
source .venv/bin/activate
pip install garmin-fit-sdk --break-system-packages

Windows

install_windows.bat — download or copy the whole toolkit folder (Doug used a Documents subfolder), then double-click install_windows.bat in File Explorer. It checks for Python 3.10+ (the py launcher, falling back to python.exe), installs garmin-fit-sdk and wxPython directly — deliberately no virtual environment, see below — and verifies both import cleanly. If Python isn't found or is too old, it opens the python.org download page and stops with instructions rather than trying to install Python itself. Not yet confirmed on real hardware — written against Doug's own confirmed-working manual sequence below, but there's no way to dry-run Windows batch syntax in this project's dev environment (no cmd.exe available), so this needs a real run before it can be trusted the way the manual steps already are.

Prefer to install by hand, or the script hits a snag? This is the exact sequence Doug used for the confirmed-working 2026-08-19 test, no venv involved:

  1. Install Python from python.org (the standard Windows installer, downloaded via browser).
  2. Install the two dependencies directly — no virtual environment needed for this toolkit on Windows:
    python -m pip install garmin-fit-sdk wxPython
  3. Copy the whole toolkit folder somewhere (Doug used a Documents subfolder) — not individual .py files. garmin_device.py, for example, imports fit_dump.py at runtime, so a partial copy fails with ModuleNotFoundError.

From there, either run the CLI tools from PowerShell (python garmin_device.py detect, etc.), or — found during testing, and a nicer experience once setup is done — just double-click gui_app.py in File Explorer to launch the GUI directly. That works here (and deliberately doesn't on macOS, see the About section/gui_app.py changelog for that story) because the python.org Windows installer associates .py files with the same Python install pip just used — there's no separate system-vs-project Python split for a file association to point at the wrong one.

A .venv is optional, not required, on this path — reach for one only if you want this toolkit's dependencies kept isolated from other Python projects on the same machine:

python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install garmin-fit-sdk wxPython

(Double-clicking gui_app.py in File Explorer won't use a venv's Python, though — that path only works with the direct, no-venv install above; from inside a venv, launch via python gui_app.py in an activated PowerShell instead.)

Confirmed on Windows 11 (2026-08-19): detect, screens (both CLI tools), and the full GUI workflow (add a screen, deploy, restart, NewFiles round-trip). Also confirmed on Windows 10 32-bit (2026-08-29), including the same detect/edit/deploy round trip — see the note below for genuinely 32-bit-only hardware, which needs a different install path from the one above. Linux is untested.

Genuinely 32-bit-only hardware (no 64-bit CPU at all — confirmed on an Intel Atom N270 netbook-era laptop) needs a different install path, not just the 32-bit Windows installer above. wxPython dropped official 32-bit Windows wheels as of version 4.2.0, and the last release that has one (4.1.1) only has wheels for Python 3.6–3.9 — so this specifically needs 32-bit Python 3.9.13 (the last 3.9.x release with a binary installer at all — later 3.9.x releases are source-only; get it directly from https://www.python.org/ftp/python/3.9.13/python-3.9.13.exe, since python.org's normal download flow now points to an installer that only fetches current releases) plus a pinned wxPython:

py -m pip install garmin-fit-sdk wxPython==4.1.1

install_windows.bat won't work for this case — it hard-checks for Python 3.10+ — so skip it and run the command above directly. If pip tries to build wxPython from source instead of downloading a cp39-cp39-win32 wheel, stop and double check the Python install is really the 32-bit 3.9.13 build; a source build needs a full Visual Studio C++ toolchain and isn't a realistic path here.

Quick start

The commands below assume an active environment with both dependencies installed. On macOS via install.sh, that means running source .venv/bin/activate first (once per Terminal session — launch_gui.command handles this for you for the GUI, but these are CLI commands, run directly in Terminal). On Windows via the no-venv install path, no activation step is needed at all.

Connect your Edge 530 via USB, then:

# Confirm the device is detected (also shows device info: manufacturer/product/serial/software version)
python3 garmin_device.py detect

# See what profiles are on it
python3 garmin_device.py list

# Back everything up before touching anything
python3 garmin_device.py backup ~/path/to/a/working/directory

# See the current screen layout for one profile
python3 garmin_device.py screens CyclingRoadRoadtest.fit

To make a change, stage a backed-up profile, patch it, then deploy:

python3 garmin_device.py stage CyclingRoadRoadtest.fit <backup_path> <working_dir>
python3 fit_patch.py <staged_file> <patched_file> --slot 9 --hide
python3 garmin_device.py deploy <patched_file> CyclingRoadRoadtest.fit

To make several changes before a single device write/restart, use fit_chain.py instead of calling fit_patch.py directly:

python3 fit_chain.py <staged_file> <patched_file> \
    --step '--slot 9 --hide' \
    --step '--swap-order 1,11' \
    --step '--slot 4 --layout 1'
python3 garmin_device.py deploy <patched_file> CyclingRoadRoadtest.fit

Follow the eject prompt. After the device's automatic restart finishes, press the power button once to bring it back into mass-storage mode (it does not remount on its own), then reconnect and verify:

python3 garmin_device.py wait-for-remount
python3 garmin_device.py screens CyclingRoadRoadtest.fit

To view or replace the device's custom boot message (startup.txt, lives at the device root, NOT in Sports/ — a direct overwrite, no NewFiles/eject-import involved; the existing file is backed up first):

python3 garmin_device.py startup-txt ~/path/to/a/working/directory                     # view
python3 garmin_device.py startup-txt ~/path/to/a/working/directory --write new_msg.txt  # overwrite

Eject, then allow one full power cycle (off, then on) — not just eject/remount — for the new message to take effect (confirmed via the file's own on-device comment).

To clone an existing profile under a new name (e.g. building a second bike's profile from an existing one instead of rebuilding every screen by hand):

python3 fit_clone_profile.py <staged_file> <cloned_file> --name "NewBikeName"
python3 garmin_device.py deploy <cloned_file> <NEW_FILENAME>.fit   # filename must NOT match an existing profile

Tools

Tool Version Purpose
install.sh 1.0.3 macOS setup script (see install_windows.bat for the Windows counterpart). v1.0.3 (2026-08-30): new feature, Doug's go-ahead -- builds/confirms the new launch_gui.command (see its own table row below) executable as a defensive last step, in case whatever download method delivered the toolkit didn't preserve the bit; "Next steps" output now mentions it as the no-Terminal alternative. No change to the dependency-install logic itself. SECOND real bug found via real hardware, same test session (2026-08-13): past the CLT/python3 checks (Homebrew python3 3.14, freshly installed), "Installing garmin-fit-sdk..." died with PIP_EXTRA[@]: unbound variable. Cause: bash 3.2 (macOS's stock /bin/bash — confirmed that's really what runs this script, via Doug's bash-3.2$ prompt) has a long-standing bug where expanding an empty array under set -u throws unbound-variable instead of silently expanding to nothing — fixed in bash 4.4+, so it never showed up in the bash 5 dev/test sandbox, only on real hardware. Fixed in v1.0.2 by replacing the PIP_EXTRA array with a small pip_install() wrapper function that branches on $UPGRADE directly — no array left anywhere in the script, so this bug class is now structurally impossible, not just avoided this one instance. Also v1.0.1: on a genuinely fresh Mac with no Xcode Command Line Tools ever installed, v1.0.0 crashed silently right after "Found python3" — invoking python3 for its version check triggered macOS's own xcode-select "requesting install" note to stderr and a non-zero exit, which set -e turned into an unexplained stop with no error message from the script itself. Fixed with an explicit Command Line Tools check as its own step, before python3 is touched at all, plus defense-in-depth error handling around the version-check invocation; new --version flag. Checks the platform, checks Xcode CLT, checks python3 is present and at least 3.9 (warns if older than 3.10, since wxPython only ships pre-built PyPI wheels from 3.10 up), creates/reuses a dedicated .venv so nothing touches system or Homebrew Python, installs garmin-fit-sdk and wxPython into it, then imports both back to confirm the install actually works. Idempotent, --upgrade/--help/--version flags. Windows/Linux support deferred until device detection itself is implemented for those platforms (see garmin_device.py's _find_garmin_root_windows() stub).
launch_gui.command 1.0.0 macOS double-click launcher for gui_app.py, Doug's go-ahead (2026-08-30) -- the macOS equivalent of Windows' native double-click-gui_app.py convenience, which macOS has no matching file association to hijack (see PROJECT_NOTES.md "macOS double-click launcher" for the full mismatch story: macOS's Python Launcher, if it even runs a plain .py, invokes whatever python3 it resolves by default -- NOT this toolkit's own .venv where wxPython/garmin-fit-sdk actually live, since install.sh deliberately uses an isolated venv, unlike install_windows.bat's deliberate no-venv install). Resolves its own folder, confirms .venv exists (points at ./install.sh with a paused, readable message if not), then launches gui_app.py directly via .venv/bin/python3 -- not source .venv/bin/activate in a subshell, simpler and equally correct. Pauses with a readable message on any non-zero exit (missing venv, gui_app.py crash) so Terminal's window doesn't vanish before the message can be read; exits cleanly with no pause on a normal GUI close. install.sh v1.0.3 now chmod +x's it defensively on every run. Headlessly verified (all three branches: missing venv, clean exit, non-zero exit) via a simulated .venv/bin/python3 stand-in, since real Finder/Gatekeeper behavior needs Doug's own run.
install_windows.bat 1.0.1 Windows setup script. CONFIRMED on real Windows 11 hardware (v1.0.1, 2026-08-22) — Doug uninstalled garmin-fit-sdk/wxPython via pip uninstall, confirmed both failed to import, then ran this script fresh: Python detection, version check, install, and post-install import verification all worked cleanly (Python 3.14, prebuilt wxPython 4.3.1 wheel). One benign pip warning seen (wx's bundled demo/dev console scripts installed outside PATH) — confirmed harmless, unrelated to how this toolkit is actually launched. Double-click launch of gui_app.py reconfirmed post-reinstall — Windows showed a one-time "how do you want to open this file" dialog on the very first double-click (normal first-use file-association behavior for any .py file, not caused by this script); picking Python + "Always" made every subsequent double-click launch directly with no dialog. Confirmation-only, no code changed. Original entry (v1.0.0, Doug's go-ahead, 2026-08-20), scoped directly against his own confirmed real Windows 11 install (README.md Doc rev 54): checks for the py launcher (falling back to python.exe) and a version >= 3.10 -- a HARD floor here, stricter than install.sh's soft warn-and-offer-to-continue at the same threshold on macOS, since building wxPython from source below 3.10 needs Visual Studio Build Tools on Windows versus a single-command Xcode CLT install on macOS. If Python's missing or too old, detects and guides (opens the python.org download page, prints instructions, stops) rather than trying to silently download/install Python itself -- Doug's explicit call, matching install.sh's own treatment of missing Xcode CLT, no elevation and nothing modified without the user's own click. Installs garmin-fit-sdk/wxPython directly with NO virtual environment (deliberately different from install.sh) -- a venv would break the double-click-gui_app.py-in-File-Explorer behavior Doug's own real install produced, which only works because the python.org installer associates .py files with the SAME python pip install (no venv) puts packages into. Verifies both packages import after install. --upgrade/--help/--version flags, safe to re-run.
garmin_device.py 0.12.8 Detect (+ device info)/list/backup/stage/write/eject/remount-wait workflow for the device itself. v0.12.8 (2026-08-25): new feature, Doug's go-ahead -- backup retention/pruning. New prune_old_backups(working_dir, older_than_days, dry_run=True) deletes entire working_dir/backups/<timestamp>/ folders older than a cutoff, decided from each folder's OWN NAME (its timestamp), not filesystem mtime. Time-based-only and manual-only were both deliberate choices confirmed with Doug (rejected: keep-latest-N-per-profile, too complex given folders snapshot every profile together; keep-only-latest, cuts against Restore-from-Backup's whole purpose and wasn't justified by Doug's own disk-usage numbers). New prune-backups <working_dir> [--older-than-days N] [--dry-run] [--yes] CLI subcommand, always previews before an interactive confirm unless --dry-run/--yes. Backs gui_app.py v0.19.19's new "Clean Up Old Backups..." dialog. Headlessly verified against a fake multi-age backup tree plus a non-timestamp junk folder (left untouched); CLI exercised end-to-end. v0.12.7 (2026-08-24): real safety fix. write_to_newfiles() gained an optional working_dir parameter -- if given, and a profile currently exists on the device under the target filename, it's backed up to working_dir/backups/<timestamp>/ (same convention as backup_profiles(), immediately browsable via Restore-from-Backup) BEFORE being overwritten. Every GUI write already got this for free via the profile list's own backup pass, but a bare CLI deploy call bypassed it entirely -- closes that gap. Deliberately a silent auto-backup, not a block or interactive prompt, since overwriting the target is deploy's normal intended outcome. CLI deploy gained a --working-dir flag; omitting it now prints a one-line NOTE about the missing safety net rather than staying silent. gui_app.py's DeployPanel now passes frame.working_dir through. Headlessly verified (existing-target backup, working_dir-omitted no-op, no-existing-target skip all behave correctly). Prior entry: Windows support CONFIRMED on real hardware (v0.12.6, 2026-08-19) — Doug ran detect, screens (both CLI tools), and the full GUI workflow (add screen, deploy, restart, NewFiles round-trip) against a real Edge 530 on a real Windows 11 laptop; all worked with zero code changes. D:\Garmin has Sports/NewFiles flat at the drive root, resolving Doc rev 48's open question. Confirmation-only entry, no code changed. See Doc rev 49 above. Prior entry (v0.12.5, Doug's go-ahead): _find_garmin_root_windows() filled in — the single deliberately-stubbed function per this file's own module docstring. Scans drive letters C:-Z: for the same Sports/NewFiles structure check the macOS half uses, at both the drive root and one level of subfolder (mirroring macOS's own two-level nesting check). Plain os.path/os.listdir iteration, no new dependency. Headlessly verified via ntpath-monkeypatched fake drive trees. See Doc rev 48 above. Prior entry (v0.12.4, same-day follow-up to v0.12.3, Doug's report from testing): a second, separate cause of the startup.txt "?" corruption — a leading UTF-8 BOM decoded (via errors="replace") into 3 replacement characters that rode through the preserved header and got re-encoded to 3 literal "?" on every save, self-perpetuating regardless of the v0.12.3 smart-quote fix. read_startup_txt() now strips a leading UTF-8 BOM before decoding. See Doc rev 43 above. Prior entry (v0.12.3, Doug's report from testing): write_startup_txt() now reverses common macOS smart-substitution characters (curly quotes, en/em dash, ellipsis) back to plain ASCII before writing — typed "..." was silently becoming a single "?" on save, since wx.TextCtrl on macOS auto-substitutes as you type and the old ASCII-only encode replaced each result with "?". Any character outside the known-substitution table still degrades honestly to "?", unchanged. Prior entry (v0.12.2): list_backed_up_profile_filenames(working_dir) returns every .fit filename ever backed up (regardless of current on-device presence), scanning all backups/<timestamp>/ folders — filtered to .fit so a startup.txt backup (which lives in the same folder structure) isn't mistaken for a deleted profile. New deleted-profiles CLI subcommand. Backs gui_app.py v0.19.0's new second profile list. Prior entry (v0.12.1): read_startup_txt() now normalizes all line endings to plain \n right after decoding — real reported bug, the GUI's message editor was showing a blank line between every real line, likely from CRLF line endings in the file that BBEdit/vi both silently normalize on open (so it looked fine there) but wx.TextCtrl does not handle cleanly when fed embedded \r\n. Headlessly verified against a simulated CRLF file. Prior entry (v0.12.0): startup.txt (custom boot message) support — read_startup_txt()/parse_startup_txt()/build_startup_txt()/write_startup_txt(), plus a new startup-txt CLI subcommand (view, or --write FILE to overwrite). CONFIRMED via Doug's own real Edge 530 (2026-08-14): the file lives at garmin_root itself (same level as Sports/NewFiles), and a write is a DIRECT overwrite while mounted — NOT a NewFiles import — per the file's own on-device comment ("Allow one full power cycle after editing for your message to be updated"). write_startup_txt() backs up any existing file first, reusing the same working_dir/backups/<timestamp>/ structure backup_profiles() uses. parse_startup_txt()/build_startup_txt() split/rejoin the file at its last comment block, so Garmin's own instructional text is preserved byte-for-byte and only the <display=N> value + message text are ever regenerated — headlessly round-trip-tested byte-identical against Doug's real file. Character/line-count reference constants (STARTUP_TXT_MAX_CHARS/STARTUP_TXT_MAX_LINES) are guidance only, not enforced — actual on-device wrapping is character-width-dependent (Doug's own note). Backs gui_app.py's new StartupTxtPanel. Prior entry (v0.11.0): list_backup_history() lists a single profile's backup history (newest first), de-duplicating consecutive byte-identical backups — a real characteristic of this app, since every visit to the GUI's profile list re-backs-up all profiles, not just on real changes. Backs the GUI's Restore-from-Backup picker; also a new backup-history CLI subcommand.
fit_dump.py 2.4.25 Read and inspect a .fit file (dump, unknown, diff, screens subcommands). v2.4.25: CONFIRMED via GUI, real hardware (2026-08-22) — Doug spot-checked the field picker directly against several of the 169 confirmed entries added across the three 2026-08-20 batches, confirming the labels shown match what's on-device. Closes the loop from "correctly typed in the dict" to "actually wired through and displayed correctly." No dict values changed, confirmation-only. v2.4.24: 13 new confirmed field IDs, 2026-08-20 batch #3, Doug's cross-check of his confirmed-field list against the Garmin Edge 530 Owner's Manual's own data-field appendix — 98 Watts/kg, 439 3s W/kg, 207-213 Power Z1-Z7, 418 Power Z8, 419 Power Z9 (Time in Power Zone 1-9, the Power-Zone analog of the existing 199-203 HR Zone 1-5 (time) fields — 9 zones, not 5), 24 Laps, 41 Max Lap Power (resolves an earlier ambiguous "Laps Max" note — two separate fields Doug had conflated, not one). Fills the last two gaps in the W/kg family (base + 3s variant), extends Power's own Lap/Max/Last-Lap set. No collisions with any existing entry. Closes out the manual-appendix cross-check — no further appendix-listed fields remain unconfirmed. FIELD_ID_NAMES now 169 confirmed entries (was 156). One real gap still open, not part of this batch: "Trainer Resistance" — hypothesized (unverified) to need a paired ANT+ FE-C smart trainer to appear on-device at all, same sensor-gated pattern as eBike Metrics. v2.4.23: 10 new confirmed field IDs, 2026-08-20 batch #2, Doug's continued field census — 265 Lap PCO, 267 Avg Right PP, 268 Lap Right PP, 269 Right PPP, 271 Lap Right PPP, 273 Avg Left PP, 274 Lap Left PP, 275 Left PPP, 277 Lap Left PPP, 440 10s W/kg. Fills out the L/R Power Phase/Peak Power Phase family alongside the existing 263/264/266/270/272/276 entries. No collisions with any existing entry. FIELD_ID_NAMES now 156 confirmed entries (was 146). Same batch, doc-only: Doug's investigation of the last unmapped on-device label he'd been tracking, "Battery Status" (Lights category), confirms it's an alias that navigates straight to field 317 "Light Battery" — not a separate field, no table entry needed. v2.4.22: correction, same day, Doug's own catch — the v2.4.21 batch's "Target" was mistyped as field 512; the real ID is 521 (512 never existed on-device under either name, a simple mistyped digit not a raw-ID/name mismatch). Fixed before this ever shipped in a tagged release. Doug also confirmed, unprompted, that all 9 fields from that batch are verified against the real on-device screen — same confirmation standard as every other batch here. No count change, still 146 entries (corrected key, not new/removed). v2.4.21: 9 new confirmed field IDs, 2026-08-20 batch, Doug's continued field census — 521 Target (see correction above), 523 Step Time, 522 Duration, 511 Workout Comparison, 45 Workout Step, 100 Last Lap Power, 258 Lap Time Standing, 260 Lap Time Seated, 264 Avg PCO. No collisions with any existing entry. Notable: 5 of these 9 (45, 511, 521, 522, 523) read by name as Workout/structured-step fields — directly adjacent to this project's still-open f10=38 "Workout" SCREEN-TYPE question (see PROJECT_NOTES.md Open Items and FIELD_EDIT_UNCERTAIN_TYPES below): whether that screen's field slots are actually meaningful/rendered on-device, since the on-device editor exposes no field options for it at all. That's a separate, still-unconfirmed question from these fields' own identity, which Doug's on-device verification establishes with the same confidence as any other entry. The other 4 extend already-populated families: Power (100, matching the existing 3s/10s/30s/Lap/Avg pattern) and Cycling Dynamics (258/260/264, standing/seated lap time + PCO). FIELD_ID_NAMES now 146 confirmed entries (was 137). v2.4.20: FULLY CONFIRMED via direct raw-byte inspection — CyclingRoadRoadtemp.fit (the original census profile, Screen 3/4 intact at 10 fields each) came through and was dumped directly; raw arrays match all 20 corrected pairs from v2.4.19 position-for-position exactly, including 177 "Torque Effect" under its own ID. No values changed — this closes out the last open question from the fix below. v2.4.19: RESOLVED — the entire 2026-08-17 field-ID batch had raw IDs and names correctly identified but wrongly paired (Doug's census screens 3 and 4 got transposed when the original list was written up, so all 10 IDs from one screen's block were paired with the 10 names from the other's — same 20 raw IDs, only the pairing changed). Caught via real device testing (editing a screen to "Intensity Factor (IF)"/"Pedal Smoothness"/"Torque Effect" actually displayed "Avg W/kg"/"Lap NP"/"Last Lap NP"); Doug re-derived the correct pairing from the census screens and it resolves all three mismatches exactly. v2.4.18 (superseded by the fix above): flagged the whole batch SUSPECT with no value changes, pending re-verification. v2.4.17: real bug fix, Doug's report from checking the device — field 148 was stored as "Torque Effect." (a guessed abbreviated form, by analogy to field 320's "Perf. Conditioning" convention); Doug directly confirmed the real on-device text in a half-width field is "Torque Effect", no trailing period. Corrected; no count change, still 137 entries. v2.4.16: 20 new confirmed field IDs, 2026-08-17 batch — this project's first batch touching power-meter/Di2 metrics: Balance family (42/80/40/441/411/408 — Balance, Avg, Lap, 3s, 10s, 30s), Power/W-kg (150 30s Power, 151 Max Power, 83 Avg W/kg, 159 30s W/kg), training load (149 %FTP, 43 TSS, 437 Intensity Factor (IF)), NP (176 Lap NP, 177 Last Lap NP), pedaling metrics (148 Torque Effect, 147 Pedal Smoothness, 82 Power Zone), Shimano Di2 (161 Di2 Battery, 160 Di2 Shift Mode) — confirms the existing 3s/10s/30s/Lap/Avg Power naming pattern repeats identically for Balance. No collisions. FIELD_ID_NAMES now 137 confirmed entries (was 117). v2.4.15: new FIELD_EDIT_UNCERTAIN_TYPES set (currently {38}, "Workout") — flags screen types whose stored field bytes are real and correctly readable but whose actual on-device meaning/rendering is uncertain, backing a new non-blocking warning in the GUI's Edit Screen panel. Built in response to Doug's question about the Workout screen's edit guard; backed by Garmin's own Edge 530 manual confirming Workouts are a separate, dynamically-rendered subsystem (see Doc rev 42 above and PROJECT_NOTES.md doc rev 54). v2.4.14 (correction, same day): v2.4.13's new keys were wrong (39/59/96, read off this tool's own f10+1 display fallback) — corrected to the real f10 values, 38 "Workout", 58 "eBike Metrics", 95 "STEPS Metrics (Shimano)". Also resolved the f10=38 "Workout" field question: confirmed via a direct raw-byte comparison of CyclingEbike.fit that its fields are byte-for-byte identical to Cycling Dynamics' on the same profile — real data, correctly read, not a bug. See PROJECT_NOTES.md Open Items. v2.4.12 (rename only, Doug's decision): NAMED_SCREEN_TYPES[32] renamed "GroupTrack" → "Reserved" — this Conditional-only record is present on every profile regardless of GroupTrack usage, so its real purpose was never actually confirmed (f10=57 "GroupTrack List" is unaffected, remains correctly GroupTrack-specific). No functional change. v2.4.11: new GRAPH_OR_BARS_FIELD_IDS set (10 fields confirmed needing a full-width screen slot to render as a graph/bar), kept separate from FIELD_ID_NAMES, backing the GUI's new Graph/Bars full-width warning — no FIELD_ID_NAMES entries changed. v2.4.10 (doc-only): the screens subcommand's verbose "Removed screens" note referenced fit_patch.py's now-retired --un-remove flag — updated to point at Restore-from-Backup instead. classify_screens()/active_field_ids()/screen_type_name() are print-free, importable data functions — the seam the GUI reads screens through. FIELD_ID_NAMES has 117 confirmed entries (KNOWN_UNRESOLVED_IDS still empty; v2.4.9: field 320 corrected "Conditioning" → "Perf. Conditioning" — full concept name is "Performance Conditioning," but the actual on-device DATA FIELD display reads "Perf. Conditioning" (abbreviated), matching this toolkit's on-device-display naming convention; v2.4.8: field 49 corrected "Avg Speed (Alt)" → "Avg Speed" — deployed into a full-width slot and visually confirmed as plain text, no graph/bars; flagged as a caution (not a falsification) for the Graph/Bars marker theory below, since there's no record this field's old "(Alt)" label was ever a real on-device marker transcription like 23/348/349 were; v2.4.7, 2026-08-11 batch: 12 new IDs plus 3 corrected placeholder names — 23 "Heart Rate (Alt)" → "HR Zone Graph", 348/349 "Speed /Cadence " → "Speed Bars"/"Cadence Bars" — confirming the ""/"(Alt)" marker denotes a Graph/Bars-style field needing a full-width slot; v2.4.6, doc-only: the long-open "" marker mystery on fields 348/349 is likely resolved — marks a Graph/Bars-style rendering needing a full-width screen slot, else falls back to plain text; v2.4.5: fields 58/87 corrected from "Lap Timer"/"Last Lap Timer" to "Lap Time"/"Last Lap Time" — a mistaken analogy to the separate, correctly-named field 56 "Timer"; 2026-08-10 batch: 18 IDs confirmed via a dedicated two-screen, 10-field-each census on a real profile, cross-referencing on-device field names against their GUI-shown position); NAMED_SCREEN_TYPES has 10 confirmed f10 screen-type codes (Map, Compass, Segment, ClimbPro, etc.) — screens output now shows real screen names.
fit_patch.py 1.14.2 Patch a screen's fields, layout, order, or visibility. v1.14.2 (doc-only, Doug's decision): comments referencing f10=32 as "GroupTrack" updated to "Reserved" — its real purpose was never actually confirmed, this record is present regardless of GroupTrack usage; f10=57 "GroupTrack List" unaffected. No functional change. --remove CONFIRMED via a real on-device round-trip test (v1.14.1, 2026-08-14) — target screen correctly removed from the on-device order, and the removed screen was wiped by NewFiles rather than surviving as recoverable, matching --un-remove's own retirement reasoning. GUI wrapper (ViewScreensPanel) is now unblocked, still unbuilt until asked for. --remove/remove_screen() (v1.14.0) — the backend half of "Delete Screen": permanently transitions a slot to Removed (f1=0, f9/f10 cleared, content preserved), reusing --hide's exact two hard guards (Map/ClimbPro block, last-visible-user-screen floor) with no new guard logic. Remove availability for named types confirmed and clarified (2026-08-13, on-device): Map and ClimbPro are the only common named screen types with Remove disabled — Elevation, "GroupTrack List" (the on-device label — not to be confused with the separate GroupTrack Conditional record, which isn't a selectable row at all), Cycling Dynamics, Lap Summary, Virtual Partner, Compass, and Segment all show it active. That's the complete confirmed set, same boundary NO_SHOW_TOGGLE_TYPES already hard-codes for the Show Screen toggle. Doc-only, directly informs the still-scoped --remove/"Delete Screen" feature — see PROJECT_NOTES.md Open Items. --un-remove RETIRED (2026-08-13, real user decision) — Restore-from-Backup already covers real recovery from an accidental delete (whole-profile undo, confirmed on real hardware), --un-remove had a confirmed historical device-side data-loss hazard never re-verified after the fix below, and Garmin's own editor doesn't offer an un-remove workflow either — see PROJECT_NOTES.md "Product note on --un-remove" for the full history. next_available_field10() auto-computes a collision-free screen identity for --new-slot, replacing the old hardcoded f10=0 default — root cause of the now-RESOLVED "Add New Screen always fails" limitation; CONFIRMED working via live on-device round-trip. check_system_screen_guard() is f10-based and CERTAIN for any Active screen (old content-pattern/field-count heuristics are a fallback only for Removed-state slots) — fixed a real false positive on a confirmed user screen. would_hide_last_visible_screen() is a HARD, non-heuristic guard (no --force) blocking --hide/--disable on a profile's last remaining real USER screen, counted via f10. hide_unsupported_screen_type() is a SECOND hard guard blocking --hide on Map or ClimbPro entirely — confirmed neither has a Show Screen toggle at all, on any profile type.
fit_chain.py 1.0.0 Apply several fit_patch.py operations in sequence before one device write
fit_clone_profile.py 1.0.1 Clone a profile under a new display name (patches sport_mesgs[0].name). v1.0.1: new PROFILE_NAME_MAX_CHARS = 15 reference constant — Doug's real on-device test (2026-08-19) confirmed Garmin's own Activity Profile name editor hard-blocks typing a 16th character. Distinct from, and much stricter than, the existing NAME_FIELD_SIZE byte-capacity check (31 usable bytes) patch_profile_name() already enforces safely. No functional change to patch_profile_name() itself — gui_app.py's ClonePanel (v0.19.9) does the enforcing.
fit_raw_walk.py 1.0.0 Internal support — generic FIT byte-offset walker, not meant to be run directly
fit_crc.py 1.0.0 Internal support — FIT CRC-16, not meant to be run directly
gui_app.py 0.19.19 wxPython GUI — covers steps 1-10 plus Restore-from-Backup, Clone Profile, Startup Message, Favorite Screen, and Import External Profile. v0.19.19 (2026-08-25): new feature, Doug's go-ahead -- backup retention/pruning GUI. New "Clean Up Old Backups..." button on ProfileListPanel (next to "Change...") opens a dialog: type a day count (default 30), see a live preview of how many backup folders / how much space would be freed, then an explicit YES/NO confirm before anything is deleted -- same confirmation posture as Restore/permanent Remove/Favorite overwrite elsewhere in this app. Wraps garmin_device.py v0.12.8's new prune_old_backups() directly. Headlessly verified (day-count validation, preview/cleanup decision logic against the real prune function). v0.19.18 (2026-08-24): two more real bugs found via Doug's hardware testing of Import, same session as v0.19.17's deploy-path fix. (1) The app window expanded to near-screen-width after reaching Deploy -- ViewScreensPanel's title and DeployPanel's status text both embed dynamic paths/filenames in a plain, unwrapped wx.StaticText (the same recurring bug class this codebase has hit before), and the window only ever grows, never shrinks -- fixed with the established textwrap.fill() hard-wrap pattern, via a new _wrap_status_paragraphs() helper that wraps each paragraph separately so blank-line breaks survive. (2) CONFIRMED root cause of the long value: ImportPanel suggested a filename straight from os.path.basename() -- picking an old file already in this toolkit's own staging/ folder as the Import source chained every layer of this toolkit's own internal _staged_/_clone_ timestamp suffixes into the suggestion. Doug's real case produced a 92-character filename that got written to NewFiles/ and sat there unconsumed even after a full power cycle -- the device silently declined to import it. New strip_internal_staging_suffixes() cleans the suggested default only (still free-text editable). The device's actual filename-length tolerance remains unconfirmed -- logged as an open question, not guessed at. v0.19.17 (2026-08-24): real bug fix, Doug's report from actually using Import External Profile right after it shipped — the reviewed-screens page for a freshly imported profile had no way to reach Deploy at all ("Review & Deploy..." never enabled), and going Back silently dropped the staged import with zero warning. Root cause: ImportPanel.on_import() left editing_path at None, correct for the normal Stage-for-Edit flow (staged file already matches the device) but wrong for Import, where the staged file is content the device has never had — it needs to be deployable immediately, before any screen edit. Now creates the scratch working copy right away and sets a new frame.import_pending flag; PreflightPanel's deploy gating (previously pure byte-diff) now also allows deploying a byte-identical, freshly-imported file, with accurate messaging instead of "nothing to deploy"; the Back-button warning now fires correctly for an abandoned import too. Headlessly verified via a standalone simulation of the import/discard/deploy-gating logic; real GUI behavior needs Doug's own run. v0.19.16 (2026-08-24): real bug fix, Doug's report from using Favorite Screen — "Save as Favorite" let a second Save silently overwrite the existing favorite with no warning; now checks first and shows a YES/NO confirm naming the field count and source profile of what's already saved, before reading anything about the newly-selected screen. Still one slot, still overwritten on confirm — a future multi-favorite folder (mirroring GarminBackups's staging//backups/ structure) is a possible later idea if this feature turns out to be popular, not scoped or built. v0.19.15 (2026-08-24): Import an external profile — a .fit never backed up by this toolkit (Garmin Connect download, emailed by another rider, an old manual copy). New "Import Profile..." button on ProfileListPanel (always enabled, no selection needed) opens a wx.FileDialog, hands off to a new ImportPanel that validates the target filename (new shared filename_collision_problem(), extracted from ClonePanel's own check so both panels enforce the identical collision rule) then calls garmin_device.stage_for_edit() directly on the external path — lands it in staging with the normal lineage sidecar, then routes to the regular Screens review step (not straight to Deploy like Clone/Restore, since the source is unverified). v0.19.14 (2026-08-24): Favorite Screen — a single favorite slot (not a list, Doug's design call). "Save as Favorite" on ViewScreensPanel captures the selected screen's fields+layout to a small JSON sidecar; "Load from Favorite..." on AddScreenPanel pre-fills the same, then falls through the existing add-screen path unchanged. Cross-profile mismatch is warn-only, not blocked. v0.19.13 (2026-08-24): defense-in-depthDeployPanel now passes working_dir to write_to_newfiles() (see garmin_device.py v0.12.7). All three headlessly verified (load/save round-trip, overwrite semantics, collision checks); real GUI behavior needs Doug's own run. Prior entry, cosmetic doc-only fix (v0.19.12, 2026-08-20): FieldPickerDialog's docstring said "156 confirmed entries" — stale after fit_dump.py v2.4.24's third 2026-08-20 batch (Doug's Garmin manual appendix cross-check) added 13 new field IDs, bringing FIELD_ID_NAMES to 169. No functional change. Prior entry, cosmetic doc-only fix (v0.19.11, 2026-08-20): FieldPickerDialog's docstring said "146 confirmed entries" — stale after fit_dump.py v2.4.23's second 2026-08-20 batch added 10 new field IDs (L/R Power Phase/Peak Power Phase family + 10s W/kg), bringing FIELD_ID_NAMES to 156. No functional change. Prior entry, cosmetic doc-only fix (v0.19.10, 2026-08-20): FieldPickerDialog's docstring said "137 confirmed entries" — stale after fit_dump.py v2.4.21's 2026-08-20 batch added 9 new field IDs, bringing FIELD_ID_NAMES to 146. No functional change — the field picker reads FIELD_ID_NAMES live, so it was never wrong, only this comment. Prior entry, new hard block, Doug's report + direct on-device confirmation (v0.19.9, 2026-08-19): the Clone Profile "New display name" field had no length check at all. Doug found a note that Garmin limits Activity Profile names to 15 characters and confirmed it directly on his own device — typing a 16th character does nothing, the editor just switches to the checkmark/complete control. ClonePanel now hard-blocks Create Clone past 15 characters (fit_clone_profile.py v1.0.1's new PROFILE_NAME_MAX_CHARS constant), the validation message shows a live "(N/15 characters)" count, and the same check runs as a belt-and-suspenders guard in on_create(). Separate from, and much stricter than, fit_clone_profile.py's existing 31-usable-byte storage cap, which was already safely enforced. Also confirmed while scoping this (no change needed): the "New filename" field already required the .fit suffix and checked for case-insensitive collisions against every profile on the device. Prior entry, real bug fix, Doug's report from actually using the GUI (v0.19.8, 2026-08-19): unchecking "Show Screen" for a named type (Lap Summary, Cycling Dynamics, Elevation) popped a dialog claiming hiding it was "genuinely UNTESTED," even though Doug had already confirmed it worked, repeatedly. That soft confirm ran for any named-type match downstream of two hard blocks (Map/ClimbPro, last-visible-user-screen) that already run first — and since the Map/ClimbPro list was derived as exhaustive (direct on-device inspection, not a sample), anything reaching the soft confirm is by elimination a type with a working toggle. Removed entirely, not just reworded, once Doug confirmed he'd tested every named type with an on-device Show Screen checkbox — hiding one now behaves exactly like hiding a plain user screen. The separate guard on editing a named screen's fields is untouched. Prior entry, real bug fix, Doug's report from actually editing a profile (v0.19.7, 2026-08-19): "Stage Selected for Edit" and "View Screens" were two separate clicks — staging set the frame's staged-profile state and enabled View Screens, but selecting a different profile in the list afterward never reset either one, so View Screens stayed clickable and silently opened whichever profile was staged first, not the one now highlighted. Discussed with Doug before fixing (staging turned out to be a plain local file copy, no device I/O at all) and merged the two into one: stage_btn removed entirely, "View Screens →" now stages the currently-selected profile immediately before navigating, on every click — no second piece of state left to drift out of sync with the list selection. Also brings this button in line with how Restore/Clone (same panel) already behave — one click, no separate staging step. Prior entry, real bug fix, Doug's report from Windows 11 testing (v0.19.6, 2026-08-19): StartupTxtPanel's message box showed only ~2 lines on Windows vs. ~5 on the Mac for the same file — the multiline TextCtrl had no explicit minimum height (just proportion=1/EXPAND in its sizer), so its visible size was whatever leftover space remained after every fixed-size sibling control, and that leftover genuinely differs by platform font metrics/DPI. Fixed with a SetMinSize() floor computed from GetCharHeight() × STARTUP_TXT_MAX_LINES (6) — guarantees at least 6 lines visible with no scrolling on any platform's real font metrics rather than a pixel value tuned to one machine; still grows taller than this floor when more room's available. Prior entry, cosmetic doc-only fix (v0.19.5): field picker's docstring count updated 117 -> 137 to match fit_dump.py v2.4.16's 2026-08-17 batch (20 new field IDs), no functional change. Prior entry (v0.19.4): new field_edit_uncertain_warning_text() warning on the Edit Screen panel, fires only for a "Workout" (f10=38) screen — explains the on-device editor offers no field editing for this type at all, so an edit here may have no visible effect even though it's mechanically safe (same write path as every other screen); backed by fit_dump.py's new FIELD_EDIT_UNCERTAIN_TYPES set and Garmin's own Edge 530 manual confirming Workouts are a separate, dynamically-rendered subsystem. Prior entry, doc-only, no code change (v0.19.3): "Restore a Deleted Profile" CONFIRMED via Doug's own real GUI test — a deliberately-deleted profile correctly appeared in the "Deleted, but available to restore" list, and restoring it worked cleanly end to end. Prior entry, real bug fix (v0.19.2, Doug's report from testing): v0.19.1's redundant-backup fix didn't work in practice — every "‹ Back" click from the profile list routes through the Detect screen, which auto-re-detects every time it's shown, and that was itself resetting the new needs_backup flag on every visit. Now only resets it on a genuine reconnect (the detected device path actually changes), not a same-device re-verification. Prior entry (v0.19.1, Doug's go-ahead, low priority): "Reduce redundant profile backups" — the profile list no longer re-backs-up every profile on every ordinary visit; a new needs_backup flag (set on a fresh Detect or a confirmed post-deploy reconnect) gates the real backup call, and the Refresh button still always forces one regardless. Prior entry (v0.19.0, Doug's go-ahead): "Restore a Deleted Profile" — the profile list gets a second list, "Deleted, but available to restore," populated from garmin_device.list_backed_up_profile_filenames() minus what's currently live; the existing "Restore from Backup..." button now works from either list (no new button, no new panel — per Doug's own 2026-08-11 design decision). The Restore confirmation now says "RECREATING" instead of "REPLACING" for a profile that isn't currently on the device. Prior entry (v0.18.1, doc-only, Doug's decision: the f10=32 "GroupTrack"→"Reserved" rename lives in fit_dump.py; this file's two user-facing display strings referencing the old name updated to match, no functional change; v0.18.0: new StartupTxtPanel, reached via a "Startup Message..." button on the Detect screen — view/edit the device's startup.txt boot message, built on garmin_device.py v0.12.0. Editable fields are the <display=N> seconds value and the free-form message text; Garmin's own comment scaffolding is preserved byte-for-byte. Live char/line-count guidance shown, deliberately NOT a hard block on Save — Doug's own call, since actual on-device wrapping depends on character width in a way this toolkit can't predict; the safety net is the automatic pre-write backup instead. Save flow: confirm -> direct device write (no NewFiles) -> eject/full-power-cycle instructions, reusing DeployPanel's eject-button pattern; deliberately no post-write verification step, since a boot message can't be read back by this app. Back button warns on unsaved edits, same style as ViewScreensPanel's v0.16.17 fix. Prior entry, v0.17.0: "Remove Selected Screen" on the Screens view — the GUI wrapper for --remove, completing "Delete Screen" now that the backend and a real device test are both confirmed; reuses --remove's exact two CLI guards with no override, plus an explicit permanent-deletion confirmation; same change split that view's single 9-button row into two, per real feedback that it ran the full window width; v0.16.17: reworded the Back-button warning per Doug's feedback — the old wording wrongly implied a resumable state, now his own direct wording, no logic change; v0.16.16: real bug fix found using v0.16.15's new warning — it blew out the Edit Screen window's width, the fourth time this codebase has hit that bug class; fixed via textwrap-based hard wrapping rather than wx.StaticText.Wrap() (untestable in the dev sandbox, documented bad behavior with pre-existing newlines); v0.16.15: Graph/Bars full-width warning — new helpers derived from LAYOUT_GRIDS surfaced in the field picker (static note) and Edit/Add Screen panels (context-aware, recomputed on every refresh); v0.16.14: real bug fix — the Screens view's Back button now warns before discarding an in-progress, undeployed edit instead of silently losing it, matching the existing confirm-dialog style used elsewhere; v0.16.13: doc-only, two comments updated now that fit_patch.py's --un-remove is retired entirely rather than just unexposed in the GUI — no functional change; v0.16.12: cosmetic doc-only fix, field picker's docstring count updated 105 -> 117 to match fit_dump.py v2.4.9's current entry count (the picker itself was never wrong — it reads FIELD_ID_NAMES live — only the comment had drifted stale, caught while confirming pre-release state ahead of a possible v1.0.1 tag); v0.16.11: doc-only, the "restore a profile no longer on the device" feature's one real open risk (whether NewFiles can recreate a genuinely deleted profile, not just replace/create-new) is now CONFIRMED via a direct garmin_device.py deploy test against a deliberately-deleted profile — only the GUI entry point itself remains unbuilt; v0.16.10: doc-only, no code change — Clone Profile CONFIRMED via real hardware, reported after the fact (two working clones deployed via NewFiles under brand-new filenames: Clonebox, CloneRoad), correcting a stale "not yet tested through the actual GUI" note and resolving whether NewFiles accepts a genuinely new filename, not just a replacement — see the toolkit table's v0.16.0 entry below for the corrected text; v0.16.9: pre-Windows-support housekeeping — the default backup working directory was hardcoded to a specific Mac path, now ~/GarminBackups (resolves correctly on any OS/user); the working directory is now also persisted across restarts via a small config file (~/.garmin_screen_editor_config.json), so a custom location picked via "Change..." is remembered instead of resetting every launch; v0.16.8: new "About" button on the detect screen opens a short summary dialog — name/version, "not affiliated with Garmin" disclaimer, reverse-engineering method note, MIT mention pointing to LICENSE/README.md for the full text; v0.16.7: window title renamed to "Activity Profile Screen Editor for Garmin Edge" ahead of a possible public release — this is an independent, unofficial project, not a Garmin product; see LICENSE and README_DISCLAIMER_DRAFT.md (pending review) for the rest of that pass; v0.16.6: fixed a real bug where v0.16.3's own fix for the Fields-column width issue was itself wrong — capping the column stopped the window from growing but silently truncated text instead, with no scrollbar; correct fix decouples the frame's size from the column's width via a ScreensListCtrl subclass, letting the column go back to full auto-size and the list's real native horizontal scroll work as intended — see PROJECT_NOTES.md toolkit table row and "Corrections and lessons learned" for the full story; v0.16.5: cosmetic doc-only fix, field picker's docstring count updated 87 -> 105 to match fit_dump.py v2.4.4's new field IDs, no functional change; v0.16.4: bumped the on-device layout diagram's font from 9pt to 13pt for readability, per real feedback with a screenshot — safe change, no width/height risk since that panel is custom-painted at a fixed size, unlike the widgets behind v0.16.2/v0.16.3; v0.16.3: same-day follow-up to v0.16.2 — the identical unresolved-field-ID window-widening bug also hit the Fields column on the main Screens view, not just the Edit Screen panel; fixed with the same terse-label approach plus a width ceiling on that column; v0.16.2: fixed a real bug where editing a screen with an unresolved field ID permanently oversized/off-screened the window — see PROJECT_NOTES.md toolkit table row for the full root-cause writeup; v0.16.1: the "not connected" message and window title are now model-generic/version-visible — see PROJECT_NOTES.md "Model portability") (detect, list/backup, select+stage, view screens with a real Type column and screen-level Move Up/Down reordering, add a brand-new screen, edit one screen's fields/layout/Show-Hide/type, review accumulated changes, deploy to the device, post-write verification, restore any profile from its backup history, and clone a profile under a new name). This closes out the GUI's full feature backlog. NEW (v0.16.0): ClonePanel — "Clone..." on the profile list patches sport_mesgs[0].name via fit_clone_profile.py's patch_profile_name() (CLI-validated full-fidelity on real hardware already), with live filename-collision checking against every profile currently on the device (deploying under an existing filename would silently overwrite it) and an auto-suggested filename from the display name. Hands off straight to Deploy, same as Restore — no staged-vs-editing diff applies to a clone. Headless-verified: filename validation, byte-for-byte-structurally-identical clone output, and zero screen differences between source and clone via describe_screen_changes(). CONFIRMED via real hardware (2026-08-11, reported after the fact): at least two clones deployed and working correctly through NewFiles under brand-new filenames not previously present on the device (Clonebox from Sandbox, CloneRoad from Road) — this also confirms NewFiles correctly accepts a genuinely new filename, not just a replacement of an existing one, a question that had been open until now. Prior entry (v0.15.2): cosmetic doc-only fix (a stale field-count reference in a docstring, no functional change). v0.15.1: fixed a real bug found via testing — backing out of a Restore attempt without completing it left a stale reference to the abandoned backup file in place, so a subsequent normal Stage silently showed that leftover instead of the profile just staged ("View Screens shows the backup I was about to restore, not what I just staged"). A fresh Stage now always clears any prior session's state first. v0.15.0: "Restore from Backup..." on the profile list now goes somewhere — RestorePanel lists the selected profile's backup history with a plain-English screen summary per entry ("8 screen(s): Screen 1, Lap Summary, Map, ..."), and picking one hands off straight to Deploy, skipping the staged-vs-editing review (nothing to review — you already picked a known backup). The backup file is used directly, never copied. "Back" from Deploy now returns to wherever it was actually reached from. v0.14.0: the moment "Check for Reconnected Device" succeeds, the GUI automatically re-pulls the live profile from the device and compares it against what was sent, reusing the same plain-English per-screen summary as "Review & Deploy..." (now shared as a module-level describe_screen_changes()). Compares visible/active screens only — the device's known Removed-list wipe on NewFiles import (a side effect that's always happened, unrelated to anything this GUI does) isn't reported, matching the fact that neither Garmin's own editor nor this GUI offers an un-remove workflow. CONFIRMED live on real hardware (2026-08-06) alongside a full deploy of a new 10-field screen. v0.13.0: "Continue to Deploy" now goes somewhere — DeployPanel writes the working copy to the device's NewFiles/ (with byte-for-byte write-back verification), then a confirm-then-diskutil eject button (plus an "I Ejected It Myself" fallback for non-macOS/Finder), then a manual "Check for Reconnected Device" button. User-confirmed design decision: no background polling for the reconnect wait (this app has never used a background thread, and it's not worth the new failure-mode class for saving a few clicks) — each Check click is one immediate, non-blocking connectivity check. v0.12.0: "Review & Deploy..." now describes changes in plain English per screen (e.g. "Screen 4: added Cadence, removed Grade") instead of a raw fit_dump.py diff-style unified diff — real user feedback that the byte-level diff was too technical for the GUI's actual audience (a rider, not a developer); the CLI tools remain there for anyone who wants that detail. Covers new/removed screens, field changes, layout changes, show/hide changes, and position changes, with a fallback line so real changes are never silently under-reported; whether there's anything to deploy is still decided from the raw bytes directly. v0.11.1: fixed a real reported bug where the Fields column silently clipped (not wrapped) any screen with more than ~3-4 short field names — a 10-field screen only showed 3 fields and part of a 4th; the column now auto-sizes to its actual content on every refresh. v0.11.0: fixed a real reported bug where manually enlarging the window (e.g. to see more of the screens list) snapped back to a smaller size the moment any button triggered a refresh — _relayout() was calling Fit(), which resizes in both directions including shrinking; now only grows the window when content needs more room, never shrinks it. v0.10.0: "Review & Deploy..." is a pre-flight step showing a fit_dump.py diff-style comparison against the untouched staged file plus a real CRC check against the working file's actual bytes — REVISES the original "pending/preview state" plan to match how the GUI actually works (every change is already applied immediately, click by click; there's no separate queue to apply, only a review+verify step). Continue to Deploy is a placeholder until deploy/eject/remount is built. "Change Type..." is Add-New-Screen and EditScreenPanel's "Replace Field" — swaps one field's ID without the Remove+Add+reposition workaround. Add-New-Screen panel replicates --new-slot's exact defaulting logic — auto-assigns f9/f10, enforces the confirmed 10-user-screen cap with a friendly message; CONFIRMED live on real hardware (2026-08-05), including a confirmed field-type change on ClimbPro after overriding the guard. Screen-level reordering is select + Move Up/Down on the main screens list, wired to swap_display_order() — same validated primitive as --swap-order. Show/Hide hard-blocks hiding a profile's last real user screen (f10-based) AND hiding Map/ClimbPro at all (neither has an on-device toggle). Guard dialogs no longer false-positive on confirmed user screens (real GUI testing found and fixed this). Swallows a cosmetic teardown-only wxAssertionError on exit. Field picker offers 169 confirmed IDs.

GUI

gui_app.py is the editor GUI, built incrementally, one step of the agreed flow (see PROJECT_NOTES.md) at a time — each step wired to its already-validated backend function, tested against real hardware before the next step is added. All 10 flow steps plus Restore-from-Backup, Clone Profile, and Startup Message are now built: detect the device and show its info; list profiles (via an automatic backup-all-profiles call); select a profile and stage it for editing; view the staged profile's current screens, read-only, with screen-level reordering (Move Up/Down); a real Add-New-Screen panel (pick fields and layout, the tool auto-assigns everything else — CONFIRMED live on real hardware); drill into a single screen to reorder, add, remove, or change the type of its fields and change its A/B layout, with a live visual diagram of the on-device grid (built from the developer's own text-based Edge 530 layout reference — see PROJECT_NOTES.md / "On-device layout geometry") — the field-type swap ("Change Type...") is CONFIRMED live on real hardware including a guard-overridden edit to ClimbPro; a "Review & Deploy..." pre-flight step showing a plain-English, per-screen change summary plus a real CRC check; "Deploy to Device" (write to NewFiles/, guided eject, manual reconnect check) — CONFIRMED live on real hardware alongside a full 10-field new screen; automatic post-write verification the instant reconnect is confirmed, re-pulling the live profile and comparing it against what was sent; "Restore from Backup..." — pick any of a profile's past backups from a plain-English-summarized list and it goes through the same Deploy/verify pipeline as a normal edit; and "Clone..." — clone the selected profile under a new display name (fit_clone_profile.py, already CLI-validated on real hardware) with live filename-collision checking, going through the same Deploy pipeline as Restore. See PROJECT_NOTES.md / "Clone Profile" for the full writeup. And "Startup Message..." — reached directly from the Detect screen (not from the profile flow, since startup.txt is a device-root file, not a profile) — view/edit the device's custom boot message, with Garmin's own comment scaffolding preserved byte-for-byte and a direct (non-NewFiles) write path of its own. See PROJECT_NOTES.md / "startup.txt" for the full writeup.

Editing architecture: rather than an abstract in-memory list of "pending changes," a scratch working copy of the staged file (which persists across edits to multiple screens in one session) IS the queue — every button click is a real, immediately-applied fit_patch.py operation, and the screen always re-reads the actual resulting bytes afterward instead of trusting an in-memory guess. "Discard Edits" on the screens view resets back to the untouched staged file.

The Show/Hide checkbox is guarded two ways, checked in order: a HARD block (unhideable, no override) if hiding this screen would leave the profile with zero visible Active/Display screens — confirmed via real on-device testing that Garmin's own editor refuses this too — followed by the existing softer heuristic guard for likely system/overlay screens (content-pattern match OR field count ≤2, --force-style confirm dialog).

Editing UX decision (recorded ahead of building it): reordering is select + Move Up/Move Down buttons, not drag-and-drop — it maps directly onto the already-validated --swap-order/--swap-fields primitives with no new backend logic. Field count changes and reassigning which fields appear on a screen go through --fields (replaces the whole list, count follows from its length), fed from a picker over the known field ID catalog rather than free-text entry, and guarded by the same heuristic the CLI uses.

./install.sh              # installs wxPython too -- see Setup above
source .venv/bin/activate
python3 gui_app.py

(Manual equivalent without the script: pip install wxPython --break-system-packages.)

Full CLI reference: FIT_PATCH.md. (A companion FIT_DUMP.md reference is planned but not yet written.)

⚠️ Before you write anything to a real device

  • Always run garmin_device.py backup first.
  • Always verify a patched file with fit_crc.py and fit_dump.py screens before deploying it.
  • Adding a brand-new screen via fit_patch.py --new-slot is now CONFIRMED WORKING (v1.12.0+, live on-device round-trip verified 2026-08-05) — the earlier "must use the on-device Add New menu" guidance is superseded. Still verify with fit_dump.py screens and fit_crc.py before every deploy, and expect the profile's entire Removed-screen list to be purged by the deploy regardless of what it targets. See PROJECT_NOTES.md/FIT_PATCH.md BUGS for the full writeup.
  • fit_patch.py --un-remove uses the same corrected auto-default as --new-slot but has NOT itself been re-tested live since the fix. Also: Garmin's own on-device editor has no un-remove option at all, so this is likely never going to be a first-class GUI feature — kept available for deliberate testing, final call deferred. Back up first, not just recommended, required.
  • fit_patch.py --fields refuses to overwrite a slot whose current content matches a pattern commonly seen on system/overlay screens (empty, Elevation/Grade, Cycling Dynamics) unless you pass --force — a "did you mean to?" pause, not a certain identification. Verify what the screen actually is on-device first.
  • Connect IQ data fields (third-party apps like WindField) can't be touched by this toolkit once they're on a screen — not just "added," ANY edit to that screen's field count, field list, or layout is refused. Confirmed via real-hardware testing: adding, removing, or reordering other, ordinary fields on a screen that already has a working Connect IQ field breaks that field's on-device linkage just as completely as trying to place one fresh does — even though the CIQ field's own ID never changes. Both fit_patch.py (--fields/--swap-fields/--layout) and the GUI (Add/Remove Field, Move Up/Down, Replace Field, Layout A/B) now refuse outright the moment a target slot already has one, rather than writing something that looks right but silently renders as "Timer" on the device. This isn't a licensing check (confirmed with both a paid and a completely free Connect IQ app showing the identical behavior) — the numeric field ID Garmin assigns a Connect IQ app is device-local and gets reassigned to a different app when you install another one, so nothing in the file itself is enough to recreate or preserve a placement through an edit. Cloning a whole profile untouched preserves a working Connect IQ field; only Garmin's own on-device editor can place one for the first time or restructure a screen that already has one. See PROJECT_NOTES.md Doc rev 95-98 for the full investigation.

Changelog

Detailed, chronological doc-revision notes -- every fix, feature, and correction to this project, newest first. Most readers won't need this; it's kept for the full history.

Doc rev 74 — refreshed 2026-09-03. Connect IQ data field guard, real-hardware bug fix -- CONFIRMED bug, CONFIRMED fix. Doug reported a WindField (third-party Connect IQ) data field set via the GUI showing correctly in the file and GUI but rendering as "Timer" on the actual device; extensive real-hardware testing (his own, across two Connect IQ apps -- one paid, one free) traced this to a device-local, install- order-reassigned numeric field ID that nothing in the file carries enough information to reproduce -- not a licensing check, since the free app failed identically. Further testing (also Doug's own, the day after the first fix shipped) found the first guard was too narrow: it only blocked FRESHLY placing one of these fields, but editing OTHER, ordinary fields on a screen that already had a working one broke it just the same, and that write path (the GUI's Add/Remove Field, Move Up/Down, Replace Field, Layout A/B) never went through the first guard's code at all. Fixed properly this time with a second check against the slot's CURRENT content, wired into every real write path in both fit_patch.py and the GUI. Tagged v1.2.2. See PROJECT_NOTES.md Doc rev 95-99 and RELEASE_NOTES_v1.2.2.md for the full investigation and fix writeup. Prior rev (73, 2026-08-31) follows.*

Doc rev 73 — refreshed 2026-08-31. Setup section clarity fix, Doug's question -- doc-only, no code changed. Doug asked whether source .venv/bin/activate is now handled by install.sh itself, given launch_gui.command's addition. It isn't, and can't be -- a script running in its own subshell can't activate a venv in the Terminal window that launched it, a shell limitation, not a gap; install.sh's own "Next steps" output already prints that line as something the USER runs, not something it does silently, unchanged by the launcher's addition. What HAD gone stale: showing source .venv/bin/activate as step 2 of the primary Setup block implied it's required for everyone, when the GUI path via launch_gui.command doesn't need it at all anymore -- only direct CLI use does. Removed it from the Setup block; added a short note to Quick start instead, where the CLI commands actually need it. Prior rev (72, 2026-08-31) follows.*

Doc rev 72 — refreshed 2026-08-31. launch_gui.command CONFIRMED on real hardware. Doug ran ./install.sh fresh, then double-clicked launch_gui.command in Finder -- worked, closing out the one thing Doc rev 71 flagged as unverifiable from the dev sandbox. Tagged v1.2.1. See PROJECT_NOTES.md Doc rev 92. Prior rev (71, 2026-08-30) follows.*

Doc rev 71 — refreshed 2026-08-30. New macOS double-click launcher (launch_gui.command), Doug's go-ahead -- closes the gap first noted back at Doc rev 76's macOS Python Launcher/.venv mismatch story (2026-08-22). New file, launch_gui.command v1.0.0: resolves its own folder, confirms .venv exists (points at ./install.sh with a paused, readable message if not), then launches gui_app.py directly via .venv/bin/python3 -- pauses on any error so Terminal's window doesn't vanish before the message is readable, exits cleanly with no pause on a normal close. install.sh is now v1.0.3 -- chmod +x's the launcher as a defensive last step (covers a download method that didn't preserve the executable bit) and mentions it in its "Next steps" output. Setup section rewritten to document it, including the one-time Gatekeeper right-click-Open note every downloaded/unsigned macOS script needs. Also corrected a stale line in the same section claiming Windows "doesn't have [a setup script] yet" -- install_windows.bat has existed and been confirmed on real hardware since Doc rev 54/62, this was leftover wording from before it was built. See the Tools table above for both files' full entries, and PROJECT_NOTES.md Doc rev 91 for the fuller writeup. Headlessly verified (all three of the launcher's branches -- missing venv, clean exit, non-zero exit -- via a simulated .venv/bin/python3 stand-in); real Finder/Gatekeeper behavior needs Doug's own run. Prior rev (70, 2026-08-30) follows.*

Doc rev 70 — refreshed 2026-08-30. "What it does" now explains WHY Clone Profile matters, not just what it does -- doc-only, no code changed. Doug recalled a complaint from an Edge 530 review's comment section around launch: creating a new profile on-device doesn't carry over any of your other settings (navigation, alerts, sensor pairing, etc.) from an existing one, so every new profile means redoing all of that by hand. Clone Profile already solves this by construction, not by design intent discovered after the fact -- fit_clone_profile.py's patch_profile_name() duplicates the ENTIRE input file and only overwrites the 32-byte name field, so every other byte/setting rides along untouched, always has. The opening "What it does" section now says so directly, since a new reader skimming that section wouldn't otherwise know cloning preserves anything beyond the screens. See PROJECT_NOTES.md Doc rev 90 for the fuller writeup. Prior rev (69, 2026-08-29) follows.*

Doc rev 69 — refreshed 2026-08-29. Confirmed on genuinely 32-bit-only hardware (Windows 10, Intel Atom N270 -- no 64-bit CPU extensions at all), a donated bike-club laptop. Full round trip -- detect, a real data-field edit on the GRAVEL profile, and deploy -- all confirmed working. This needed a different install path from ordinary 32-bit Windows: wxPython dropped official 32-bit wheels as of 4.2.0, and the last version with one (4.1.1) only supports Python 3.6-3.9, so this specifically needs 32-bit Python 3.9.13 (the last 3.9.x release with a binary installer -- python.org's normal download flow no longer surfaces it, direct link in the Setup section above) plus pip install wxPython==4.1.1 (pinned, not latest). install_windows.bat doesn't fit this case since it hard-checks for Python 3.10+ -- install manually instead. One cosmetic-only bug found along the way: on the very first Edit Screen visit each session, the Layout A/B radio controls render blank until the panel is revisited (Back and back in) -- looks like a native-control paint-timing quirk specific to this old wxPython/old-graphics combination, not a logic bug (the same code runs correctly every subsequent visit), and not chased further since there's no way to test a fix without this exact hardware. See PROJECT_NOTES.md Doc rev 88 for the full writeup. Prior rev (68, 2026-08-25) follows.*

Doc rev 68 — refreshed 2026-08-25. Project rename finished -- "Publishing housekeeping cleanup" closed. The GUI window title has read "Activity Profile Screen Editor for Garmin Edge" since v0.16.7, but the rename never went further -- README.md's/PROJECT_NOTES.md's own top-level headings still led with "Garmin Edge 530," exactly the pattern the original rename was meant to avoid (implying this might be a Garmin product), and gui_app.py's module docstring, About dialog text, MVP_SCOPE.md's heading, and FIT_PATCH.md's banner all carried one of three different inconsistent names. Doug clarified the canonical name: "Activity Profile Editor for Garmin Edge" -- no "Screen," no "530" -- matching his GitHub repo name ("Activity-Profile-Editor") and his own Release titles ("Activity Profile Editor for Garmin Edge Devices"). Rationale, his own words: the editor works with any Edge device, not just the 530 specifically (even though the confirmed field-ID/screen-type data so far is 530-only), and "Activity Profile" is the right noun to lead with over "Screen" since backups, restores, and deploys all operate at the whole-profile level, not the individual-screen level. Updated: this file's own H1, PROJECT_NOTES.md's H1, MVP_SCOPE.md's H1, FIT_PATCH.md's man-page banner, and gui_app.py's window title/module docstring/About dialog text (now v0.19.20) -- see that file's own changelog entry for the full list. Also closes the other half of "Publishing housekeeping cleanup": README_DISCLAIMER_DRAFT.md, superseded back on 2026-08-11 when its content was merged into this file's License/Disclaimer section, was found to already be deleted -- nothing left to do there. Deliberately LEFT UNCHANGED: MEMORY_LOG.md (an explicitly archived, dated historical record) and RELEASE_NOTES_v1.1.0.md/ RELEASE_NOTES_v1.1.1.md (already published as GitHub Releases under Doug's own chosen title) -- both are point-in-time snapshots, not live docs, same reasoning this project already applies to never rewriting old changelog/Doc-rev entries.

Doc rev 67 — refreshed 2026-08-25. New feature, Doug's go-ahead: backup retention/pruning. garmin_device.py is now v0.12.8, gui_app.py is now v0.19.19. Backups have accumulated with nothing to clean them up since v0.11.0 -- an Open Item deliberately deferred out of MVP, since Doug's own real usage numbers (~1098 backed-up .fit files, ~4-5GB, over this project's entire prior history) confirmed disk space was never the actual problem. Design chosen from three options put to Doug: time-based folder deletion (delete entire backups/<timestamp>/ folders older than a chosen day count) -- won out over keep-latest-N-per-profile (rejected: each timestamped folder snapshots every profile together, not one folder per profile, so per-profile retention would mean deleting individual files out of a shared folder rather than whole folders) and keep-only-the-latest (rejected: cuts against Restore-from-Backup's whole reason for existing, and wasn't justified by the disk numbers). Manual-only, not automatic -- no silent pruning on launch, matching every other destructive action in this app. New CLI prune-backups subcommand and GUI "Clean Up Old Backups..." dialog (live preview of exactly what would be deleted before an explicit confirm) -- see both files' toolkit-table entries above for the full writeup. Headlessly verified, and CONFIRMED via Doug's own real hardware test (2026-08-25) -- worked as designed.

Doc rev 66 — refreshed 2026-08-24. Two more real bugs found via Doug's hardware testing of Import, same session as Doc rev 65's deploy-path fix. gui_app.py is now v0.19.18. (1) Window width: the app window expanded to near-screen-width by the time Doug reached Deploy. Root cause -- ViewScreensPanel's title and DeployPanel's status text both embed dynamic paths/filenames in a plain, unwrapped wx.StaticText, and the window's _relayout() only ever grows, never shrinks, so a long value on the Screens page carries forward even though the text visibly on screen at Deploy looked short enough to Doug. Fixed with this codebase's established textwrap.fill() hard-wrap pattern. (2) The actual long value, confirmed on real hardware: ImportPanel suggested os.path.basename(source) verbatim as the default filename -- Doug picked an old file already sitting in his own working_dir/staging/ folder (one of this toolkit's own artifacts, not a genuinely external profile) as the Import source, and its basename carried every layer of this toolkit's own internal _staged_<timestamp>/_clone_<timestamp> naming, chained across repeat passes, into the suggestion: a 92-character filename (CyclingRoadTClone_clone_20260823_124234_staged_20260824_182004_staged_20260824_182104.fit). Deployed to NewFiles/, Doug confirmed it sat there unconsumed even after a full power cycle -- the device silently declined to import it. Fixed by stripping this toolkit's own internal suffixes from the suggested default (the field stays free-text editable either way). The device's real filename-length tolerance is still unconfirmed -- logged as an open question in PROJECT_NOTES.md, not guessed at with an invented hard limit.

Doc rev 65 — refreshed 2026-08-24. Real bug fix, Doug's report from actually using Import External Profile right after it shipped: no way to reach Deploy for a freshly imported profile. gui_app.py is now v0.19.17. The Screens review page for an import showed Back/Re-read File/+ Add New Screen but no "Review & Deploy...", and going Back lost the staged import silently. Root cause: importing left editing_path at None, correct for the normal Stage-for-Edit flow (the staged file already matches the device — nothing to deploy until an edit happens) but wrong for Import, where the staged file is content the device has never had at all. Fix creates the scratch working copy immediately on import and adds a frame.import_pending flag so PreflightPanel treats a byte-identical, zero-edit import as valid to deploy instead of "nothing to deploy," and so the Back-button warning fires (with import-accurate wording) instead of staying silent. Discarding edits after an import no longer re-strands the session either — it re-copies from the staged file instead of nulling editing_path outright. Headlessly verified via a standalone simulation of the import/discard/ deploy-gating logic; real GUI behavior needs Doug's own run.

Doc rev 64 — refreshed 2026-08-24. Real bug fix, Doug's report from using Favorite Screen right after it shipped: "Save as Favorite" had no warning before overwriting an existing favorite. gui_app.py is now v0.19.16 — saving a new favorite when one's already saved now shows a confirm naming what's currently there (field count, source profile) before it's replaced; canceling costs nothing. Still a single favorite slot by design, not a list — Doug's own note for later, not acted on now: if this feature turns out to be popular, a GarminBackups-style favorites/ folder (alongside the existing staging//backups/) could hold multiple named favorites, useful for sports/disciplines that share similar screen needs across several profiles. Logged as a future idea in PROJECT_NOTES.md, not scoped.

Doc rev 63 — refreshed 2026-08-24. Three backlog items built in one batch, Doug's go-ahead: a real safety fix, Favorite Screen, and Import an external profile. (1) garmin_device.py v0.12.7 — deploy (CLI and GUI) now auto-backs-up whatever profile currently exists under the target filename before overwriting it, closing a real gap where a bare CLI deploy call had no safety net at all (every GUI-driven write already got this indirectly). (2) gui_app.py v0.19.14 — Favorite Screen, a single save/load slot for a screen's field set + layout, reusable across profiles; "Save as Favorite" on the Screens view, "Load from Favorite..." on Add New Screen. (3) gui_app.py v0.19.15 — Import an external profile: a new "Import Profile..." button opens a file picker for a .fit never backed up by this toolkit, validates the target filename, and stages it for the normal Screens review before any deploy. All three headlessly verified (safety-fix backup/no-op/skip cases, favorite save/load round-trip and overwrite semantics, filename-collision rules) — real on-device GUI testing is still pending Doug's own run, same as every GUI feature in this project. See PROJECT_NOTES.md for the full scoping history behind each of these (all three were previously design-locked Open Items, not new ideas built on the fly).

Doc rev 62 — refreshed 2026-08-22. install_windows.bat CONFIRMED on real Windows 11 hardware — the toolkit's own testing checklist before v1.1.1. Doug uninstalled garmin-fit-sdk/wxPython via pip, confirmed both failed to import, then ran the script fresh: detection, version check, install, and post-install import verification all worked cleanly (Python 3.14). Double-click launch of gui_app.py reconfirmed too, including a normal one-time Windows file-association dialog on the very first double-click (unrelated to this script, harmless, doesn't recur). Also confirmed: the 169-entry FIELD_ID_NAMES field picker shows correct on-device labels for the three 2026-08-20 batches, spot-checked directly in the GUI. install_windows.bat is now v1.0.1, fit_dump.py is now v2.4.25 — both confirmation-only entries, no code changed either place. This closes out the real-world testing Doug wanted done before tagging v1.1.1.

Doc rev 61 — refreshed 2026-08-21. README restructured, Doug's request: the document now opens with a new "What it does" section (covering both the GUI and CLI in one place) leading straight into "Who this is for" (moved up from further down, unchanged text) — readers get the pitch and the experience-level expectation before anything else. "License" and "Disclaimer" follow, then "Setup", "Quick start", "Tools", "GUI", and the pre-write warning, in that same order as before. The entire 46-entry "Doc rev" changelog block (this one included) moved from the top of the document to a new "## Changelog" section at the very end — same content, unchanged, just relocated so a first-time reader hits the toolkit's own explanation before 60+ revisions of edit history. No content removed.

Doc rev 60 — refreshed 2026-08-20. 13 new confirmed field IDs — Doug's cross-check against the Garmin manual's own data-field appendix. 98 Watts/kg, 439 3s W/kg, 207-213 Power Z1-Z7, 418 Power Z8, 419 Power Z9, 24 Laps, 41 Max Lap Power — fit_dump.py's FIELD_ID_NAMES now has 169 confirmed entries (was 156). Fills the last two gaps in the W/kg family (base + 3s variant) and adds the Power-Zone analog of the existing 5 HR-Zone-time fields (9 zones here, not 5). 24/41 resolve an earlier same-day ambiguous "Laps Max" note — turned out to be two separate fields Doug had conflated in his own notes, not one oddly-named field. No collisions with any existing entry. This closes out Doug's manual-appendix cross-check — no further appendix-listed fields remain unconfirmed. One real gap still open, not part of this batch: "Trainer Resistance," hypothesized (unverified) to need a paired ANT+ FE-C smart trainer to even appear on-device, the same sensor-gated pattern already seen for eBike Metrics fields. See FIT_PATCH.md Doc rev 31 for the regenerated field ID table. No code changed beyond the new dict entries and a stale docstring count in gui_app.py. Prior rev (59, 2026-08-20) follows.

Doc rev 59 — refreshed 2026-08-20. 10 new confirmed field IDs. Doug's continued field census: 265 Lap PCO, 267 Avg Right PP, 268 Lap Right PP, 269 Right PPP, 271 Lap Right PPP, 273 Avg Left PP, 274 Lap Left PP, 275 Left PPP, 277 Lap Left PPP, 440 10s W/kg — fit_dump.py's FIELD_ID_NAMES now has 156 confirmed entries (was 146). Fills out the L/R Power Phase/Peak Power Phase family alongside the existing 263/264/266/270/272/276 entries — Doug supplied both the on-device display label (stored, per this dict's established convention) and each field's full concept name for the record (e.g. "Avg Right PP" = "Avg Right Pwr Phase"). No collisions with any existing entry. Same batch, doc-only: Doug's investigation of the last unmapped on-device label he'd been tracking, "Battery Status" (Lights category), confirms it's an alias/duplicate menu entry that navigates straight to field 317 "Light Battery" — not a separate field, no table entry added or needed. See FIT_PATCH.md Doc rev 30 for the regenerated field ID table. No code changed beyond the new dict entries and a stale docstring count in gui_app.py. Prior rev (58, 2026-08-20) follows.

Doc rev 58 — refreshed 2026-08-20. Correction, same day: "Target" is field 521, not 512 — Doug's own catch. A transcription typo in the Doc rev 56 batch writeup, not a raw-ID/name mismatch — 512 never existed on-device under either name. Fixed before this ever shipped in a tagged release. Doug also confirmed, unprompted, that all 9 fields from that batch are verified against the real on-device screen — same confirmation standard as every other batch in fit_dump.py's FIELD_ID_NAMES, not just a naming guess for the 5 that read as Workout-related. fit_dump.py now v2.4.22 (was 146 entries, still 146 — a corrected key, not a new/removed one). FIT_PATCH.md's FIELD ID REFERENCE table and NOTE updated to match (Doc rev 29). Prior rev (57, 2026-08-20) follows.

Doc rev 57 — refreshed 2026-08-20. New install_windows.bat setup script, Doug's go-ahead. Prompted by real feedback — a rider who wanted to try the toolkit found the install burden more than he wanted to take on — plus Doug's own confirmation that double-clicking gui_app.py in File Explorer already works once Python/dependencies are in place. Scoped directly against Doug's own confirmed real Windows 11 install (Doc rev 54 below): checks for Python 3.10+ (py launcher, falling back to python.exe), installs garmin-fit-sdk/ wxPython directly with no virtual environment (a venv would break that double-click behavior), verifies both import, --upgrade/ --help/--version flags. If Python's missing or too old, it detects and guides (opens the python.org download page, stops) rather than silently downloading/installing Python itself — Doug's explicit call, matching install.sh's own treatment of missing Xcode CLT on macOS. One real platform-specific deviation from install.sh: the 3.10 floor is a HARD requirement here, not a soft warn-and-continue, since building wxPython from source below that needs Visual Studio Build Tools on Windows (a much bigger ask than macOS's one-command Xcode CLT). Not yet confirmed on real hardware — there's no cmd.exe in this project's dev sandbox to dry-run Windows batch syntax against at all, so this is less tested going in than any other single-platform code in this toolkit; Doug plans to uninstall and re-test via this script on the Windows laptop before the next release. Windows Setup section above now leads with the script, keeping the manual sequence as a documented fallback. See the new install_windows.bat toolkit table row below for the full writeup. Prior rev (56, 2026-08-20) follows.

Doc rev 56 — refreshed 2026-08-20. 9 new confirmed field IDs. Doug's continued field census: 512 Target, 523 Step Time, 522 Duration, 511 Workout Comparison, 45 Workout Step, 100 Last Lap Power, 258 Lap Time Standing, 260 Lap Time Seated, 264 Avg PCO — fit_dump.py's FIELD_ID_NAMES now has 146 confirmed entries (was 137). No collisions with any existing entry. Worth flagging: 5 of these 9 (45, 511, 512, 522, 523) read by name as Workout/structured- step fields (Workout Step, Workout Comparison, Target, Duration, Step Time) — directly adjacent to this project's still-open f10=38 "Workout" screen-type question (see PROJECT_NOTES.md Open Items): whether that screen's field slots are actually meaningful/rendered on-device, since the on-device editor exposes no field options for it at all. A naming lead worth watching, not new evidence either way — a field's name alone doesn't confirm where or how it renders. The other 4 extend already-populated families (Power; Cycling Dynamics). See FIT_PATCH.md Doc rev 28 for the regenerated field ID table. No code changed beyond the new dict entries and a stale docstring count in gui_app.py. Prior rev (55, 2026-08-19) follows.

Doc rev 55 — refreshed 2026-08-19. New hard block: Clone Profile's display name is now capped at 15 characters. Doug found a note that Garmin limits Activity Profile display names to 15 characters, and confirmed it directly on his own device: typing a 16th character in Garmin's own name editor does nothing — it just switches straight to the checkmark/complete control instead of accepting more input. Since that's a confirmed device fact, not a guess, gui_app.py's Clone Profile panel now hard-blocks Create Clone past 15 characters (new fit_clone_profile.py v1.0.1 constant PROFILE_NAME_MAX_CHARS), the same way Map/ClimbPro's hide-toggle guard is a hard block rather than a warning. Also confirmed (no change needed): the "New filename" field already enforced the required .fit suffix and a case-insensitive collision check against every profile on the device. Prior rev (54, 2026-08-19) follows.

Doc rev 54 — refreshed 2026-08-19. Correction: the Windows Setup section had the wrong install steps. Doug corrected this directly after re-reading it: the venv-based sequence written there was never actually what he ran on the Windows 11 laptop — that was an unconfirmed inference on this project's part (mirroring the macOS pattern), not something Doug had verified. What he actually did: install Python from python.org via browser, python -m pip install garmin-fit-sdk wxPython directly (no venv at all), copy the whole toolkit folder to a Documents subfolder, then run from PowerShell — plus a genuinely nice discovery, double-clicking gui_app.py in File Explorer launches the GUI directly with no wrapper script needed, because the python.org installer associates .py files with the same Python pip installed into, unlike macOS's Python Launcher pointing at a different python3 than the one with wx (see gui_app.py's own ModuleNotFoundError episode this same week for that contrast). The Windows section now leads with this confirmed no-venv path, keeps a venv as an explicitly optional alternative (with a note that double-click launch won't use it), and no longer misattributes a theoretical sequence to Doug's real test. No code changed — README.md only. Prior rev (53, 2026-08-19) follows.

Doc rev 53 — refreshed 2026-08-19. Real bug fix: stale "hide is untested" warning removed for named Garmin screens. Doug's report: unchecking "Show Screen" for Lap Summary, Cycling Dynamics, Elevation, and others he'd tested popped a dialog claiming hiding it via a raw file write was "genuinely UNTESTED" — even though he'd already confirmed it worked, repeatedly. Root cause: that soft confirm ran for ANY named-type match, downstream of two HARD blocks (Map/ClimbPro, last-visible-user-screen) that already run first — and since NO_SHOW_TOGGLE_TYPES (Map, ClimbPro) was derived as an exhaustive list via direct on-device inspection, anything reaching the soft confirm is, by elimination, a type with a working toggle. The "untested" claim was stale the moment real testing caught up with that logic. Removed entirely (not just reworded) after Doug confirmed he's now tested hiding every named type that has an on-device Show Screen checkbox — gui_app.py now v0.19.8. Hiding a named screen now behaves exactly like hiding a plain user screen, no popup. The separate guard on editing a named screen's fields is untouched — a genuinely different, still-cautious case. See PROJECT_NOTES.md for the full writeup. Prior rev (52, 2026-08-19) follows.

Doc rev 52 — refreshed 2026-08-19. Real bug fix: "View Screens" could silently open the wrong profile. Doug's report from actually editing one of his main profiles: stage Profile A, then click a different profile (B) in the list — "View Screens" stayed enabled and advancing to Edit Screens showed A, not the one now highlighted. Root cause: staging and navigation were two separate buttons/clicks, and selecting a different profile never reset the first one's state. Fixed by merging them — "View Screens →" now stages the profile that's actually selected immediately before navigating, on every click, so there's no second piece of state left to go stale. Doug chose this over two smaller alternatives (disabling the button on reselect, or warning before opening a stale selection) once it was clear staging itself is just a local file copy with no device involved — free to redo every time. gui_app.py now v0.19.7. See PROJECT_NOTES.md for the full writeup. Prior rev (51, 2026-08-19) follows.

Doc rev 51 — refreshed 2026-08-19. New "Who this is for" section, between Disclaimer and Setup, ahead of Doug posting this update to GitHub -- prompted by Doug's own reflection after hitting setup friction on both platforms this session (venv/Python Launcher on Mac, PowerShell/module-copying on Windows) and asking whether the toolkit was exceeding the audience it's actually built for. States plainly this is a terminal-comfortable-rider tool right now, not a plug-and-play app, and names what you get in return (direct field access the on-device editor doesn't expose, Delete Screen, Clone, Restore, batch changes, automatic backups) so a visitor self-selects correctly instead of hitting the same friction Doug did. No scope change to the toolkit itself -- packaging into a real installer/app bundle for a non-technical audience was discussed and deliberately deferred, to revisit only if this GitHub post actually draws interest from less-technical riders. Prior rev (50, 2026-08-19) follows.

Doc rev 50 — refreshed 2026-08-19. Two follow-ups from the Windows test pass. (1) Setup docs restructured into explicit macOS/Windows sections — the toolkit itself runs on both now, but only macOS has a one-command install script; Windows setup for now is manual pip install garmin-fit-sdk wxPython in PowerShell (see Setup above), plus a note that the WHOLE toolkit folder needs copying over, not individual files, after Doug's first test hit ModuleNotFoundError: No module named 'fit_dump' from copying just garmin_device.py. (2) Real bug fix, Doug's report: StartupTxtPanel (the GUI's startup.txt editor)'s message box showed only ~2 lines on Windows vs. ~5 on the Mac for the same file — the multiline TextCtrl had no explicit minimum height, just proportion=1/EXPAND in its sizer, so its visible size was whatever leftover space remained after every fixed-size sibling control, and that leftover happened to differ by platform (font metrics/DPI). Fixed in gui_app.py v0.19.6 with a SetMinSize() floor computed from the control's own GetCharHeight() × STARTUP_TXT_MAX_LINES (6) — guarantees at least 6 lines visible with no scrolling on any platform's real font metrics, not a pixel value tuned to one machine; the control still grows taller than this floor when more room is available. Prior rev (49, 2026-08-19) follows.

Doc rev 49 — refreshed 2026-08-19. Windows support CONFIRMED on real hardware. Doug ran the full test pass on a real Windows 11 laptop against his actual Edge 530: garmin_device.py detect printed the same device info the Mac shows, screens worked from both fit_dump.py and garmin_device.py, and the full GUI workflow — add a screen to the Sandbox profile, deploy, restart, NewFiles round-trip — completed cleanly. No code changes were needed; copying the toolkit's .py files to the laptop was enough (one hiccup along the way: an early CLI-only test copied over just garmin_device.py, which failed with ModuleNotFoundError: No module named 'fit_dump' since get_device_info() imports it — resolved by copying the whole toolkit folder). Doug's D:\Garmin has Sports/NewFiles flat at the drive root, resolving Doc rev 48's open question — Level 1 of _find_garmin_root_windows()'s two-level check is what matched; the Level 2 (one-subfolder-deep) branch is still unexercised on real hardware but has no reason to behave differently. garmin_device.py now v0.12.6 (confirmation-only entry, no code changed). install.sh remains macOS-only; Windows setup for now is the manual pip install garmin-fit-sdk wxPython path, which is what Doug used here. No Linux testing has been done. Prior rev (48, 2026-08-17) follows.

Doc rev 48 — refreshed 2026-08-17. Windows device detection is now implemented, pending real-hardware confirmation. Doug has lined up Windows 11 access for testing. _find_garmin_root_windows()garmin_device.py's single deliberately-stubbed function, per its own module docstring — now scans drive letters C: through Z: for the same Sports/NewFiles structure check the macOS half uses, checking both the drive root and one level of subfolder (mirroring the nesting the macOS half already handles, since real Edge 530 hardware puts Sports/NewFiles one folder down under the mounted volume on Doug's Mac — whether Windows does the same is the first open question this testing pass should answer). Uses plain os.path/os.listdir drive iteration rather than a Windows API, so no new dependency beyond what install.sh already installs. garmin_device.py now v0.12.5. Headlessly verified via ntpath-monkeypatched fake drive trees (real Windows drive letters can't be exercised in the dev sandbox) — nested, flat, no-device, and flaky-drive cases all pass; find_garmin_root() dispatch confirmed. install.sh remains macOS-only (it's a bash script; Windows needs its own setup path, not yet built). Prior rev (47, 2026-08-17) follows.

Doc rev 47 — refreshed 2026-08-17. FULLY CONFIRMED via direct raw-byte inspection. Doug's CyclingRoadRoadtemp.fit — the original census profile, Screen 3/Screen 4 still intact at 10 fields each — came through on a second upload attempt. Raw field-ID arrays dumped directly: Screen 3 (slot 6) = [150, 149, 177, 176, 43, 437, 40, 408, 411, 441], Screen 4 (slot 7) = [80, 42, 148, 147, 82, 83, 151, 161, 160, 159]. Every one of the 20 corrected pairs from Doc rev 46 matches these raw arrays position-for-position exactly, including 177 "Torque Effect" under its own ID — closing the last open question from that fix. fit_dump.py now v2.4.20. Same direct byte-level verification standard as every other confirmed batch in this project now, not resting on device-observed inference alone. Prior rev (46, 2026-08-17) follows.

Doc rev 46 — refreshed 2026-08-17. RESOLVED: the 2026-08-17 field-ID batch had raw IDs and names correctly identified but WRONGLY PAIRED. Doug's census screens 3 and 4 got transposed when the original list was written up, so all 10 IDs from one screen's block were paired with the 10 names from the other screen's block — a clean systematic offset (same 20 raw IDs, only the name assignment changed). Caught via real device testing: editing a screen to "Intensity Factor (IF)"/"Pedal Smoothness"/"Torque Effect" actually displayed "Avg W/kg"/"Lap NP"/"Last Lap NP" on the device. Doug re-derived the correct pairing directly from the census screens; it resolves all three mismatches exactly. Corrected pairing: 80 30s Power, 42 %FTP, 148 Last Lap NP, 147 Lap NP, 82 TSS, 83 Intensity Factor (IF), 151 Lap Balance, 161 30s Balance, 160 10s Balance, 159 3s Balance, 150 Avg Balance, 149 Balance, 177 Torque Effect, 176 Pedal Smoothness, 43 Power Zone, 437 Avg W/kg, 40 Max Power, 408 Di2 Battery, 411 Di2 Shift Mode, 441 30s W/kg. fit_dump.py now v2.4.19. Not independently re-confirmed via a raw byte dump (upload sync issue this session) — treated as sufficiently confirmed on the 3-for-3 match against observed device behavior. See PROJECT_NOTES.md for the full writeup. Prior rev (45, 2026-08-17) follows.

Doc rev 45 — refreshed 2026-08-17. Real bug fix: field 148 was stored as "Torque Effect." — a guessed abbreviated form, by analogy to field 320's "Perf. Conditioning" convention. Doug directly confirmed the real on-device text in a half-width (1/2 side-by-side) field: "Torque Effect", no trailing period. Corrected — no count change, still 137 confirmed entries. fit_dump.py now v2.4.17. Prior rev (44, 2026-08-17) follows.

Doc rev 44 — refreshed 2026-08-17. 20 new confirmed field IDs — Doug's continued field census, this project's first batch touching power-meter/Di2-electronic-shifting metrics: Balance family (42 Balance, 80 Avg Balance, 40 Lap Balance, 441 3s Balance, 411 10s Balance, 408 30s Balance), Power/W-kg (150 30s Power, 151 Max Power, 83 Avg W/kg, 159 30s W/kg), training load (149 %FTP, 43 TSS, 437 Intensity Factor (IF)), NP (176 Lap NP, 177 Last Lap NP), pedaling metrics (148 Torque Effect, 147 Pedal Smoothness, 82 Power Zone), and Shimano Di2 (161 Di2 Battery, 160 Di2 Shift Mode). Notably confirms the Power family's 3s/10s/30s/Lap/Avg naming pattern repeats identically for L/R Power Balance, with "Balance" as the base metric mirroring "Power" — a self-consistent family, not one-off guesses. No collisions with any existing entry. fit_dump.py now v2.4.16, FIELD_ID_NAMES now 137 confirmed entries (was 117); the GUI's FieldPickerDialog docstring updated to match (gui_app.py now v0.19.5, doc-only). See PROJECT_NOTES.md for the full writeup. Prior rev (43, 2026-08-16) follows.

Doc rev 43 — refreshed 2026-08-16. Real bug: startup.txt's "?" corruption had a second, separate cause. After the earlier smart-quote fix, Doug still found 3 literal "?" at the very front of the file's preserved header comment line — not anywhere he'd typed, invisible in the GUI's own editor, only found by opening the raw file in BBEdit. Cause: read_startup_txt() decodes with errors="replace", so a leading UTF-8 BOM (3 bytes, each invalid for ASCII) becomes 3 replacement characters that ride through the preserved header and get re-encoded to 3 literal "?" on every save — self-perpetuating regardless of the smart-quote fix, since that only guards freshly-typed text. read_startup_txt() now strips a leading UTF-8 BOM before decoding. Matches Doug's own sequence exactly: he manually cleaned the "?" via BBEdit, and a fresh gui_app edit after that didn't bring them back. garmin_device.py now v0.12.4. See PROJECT_NOTES.md doc rev 55 for the full writeup. Prior rev (42, 2026-08-16) follows.

Doc rev 42 — refreshed 2026-08-16. The f10=38 "Workout" screen's purpose is now backed by Garmin's own Edge 530 Owner's Manual: its Training > Workouts feature is a separate subsystem from Activity Profile screens, and running a structured Workout "displays each step of the workout, the target (if any), and current workout data" — almost certainly what this screen type renders, dynamically, only while a Workout is actively running (synced via Garmin Connect or built on-device under GARMIN/Workouts/Guided or /Scheduled), the same "only meaningful under a specific runtime condition" pattern this project already established for ClimbPro/Segment/GroupTrack List. New FIELD_EDIT_UNCERTAIN_TYPES set (fit_dump.py, now v2.4.15, currently just {38}) backs a new non-blocking warning in the GUI's Edit Screen panel (field_edit_uncertain_warning_text(), gui_app.py now v0.19.4) — explains that editing this screen's fields is mechanically safe (same proven write path as every other screen) but may have no visible on-device effect, since the on-device editor doesn't expose field editing for this type at all. Not independently confirmed via an actual running Workout — inference from official Garmin documentation plus this profile's own byte-level evidence. See PROJECT_NOTES.md doc rev 54 and the "f10=38 'Workout'" Open Item for the full writeup. Prior rev (41, 2026-08-16) follows.

Doc rev 41 — refreshed 2026-08-16. CORRECTION: 3 new confirmed f10 screen types are 38 Workout, 58 eBike Metrics, 95 STEPS Metrics (Shimano) — not 39/59/96 as first added (those were read off this tool's own "Screen N" display label, which is f10 + 1, not the raw byte; corrected once Doug's CyclingEbike.fit was actually inspected directly). fit_dump.py now v2.4.14. The f10=38 "Workout" field question is now RESOLVED: its field bytes are confirmed byte-for-byte identical to Cycling Dynamics' on the same profile — real data, correctly read, not a bug — even though the on-device editor offers no fields/options for this type at all. See PROJECT_NOTES.md doc rev 53 / Open Items for the full writeup. Prior rev (40, 2026-08-16, superseded) follows.

Doc rev 40 — refreshed 2026-08-16. 3 new confirmed f10 screen types (39 Workout, 59 eBike Metrics, 96 STEPS Metrics (Shimano)) — SUPERSEDED, see Doc rev 41 above for the corrected f10 values. CyclingEbike.fit, Doug's first e-bike/third-party-drivetrain profile. fit_dump.py now v2.4.13. f10=39 "Workout" is flagged but not resolved — this toolkit shows it with Cycling Dynamics' field set even though the on-device editor offers no fields/options for it at all. See PROJECT_NOTES.md doc rev 52 / Open Items for the full writeup. Prior rev (39, 2026-08-16) follows.

Doc rev 39 — refreshed 2026-08-16. Real bug fix, Doug's report from actually using the GUI: a routine startup.txt edit came back with "?" characters where none were typed — macOS silently auto-substitutes typed text (e.g. "..." becomes a single ellipsis character), and the old ASCII-only write encoding replaced each result with "?". Fixed by reversing the common substitutions back to plain ASCII before writing. garmin_device.py now v0.12.3. See PROJECT_NOTES.md doc rev 51. Prior rev (38, 2026-08-15) follows.

Doc rev 38 — refreshed 2026-08-15. "Restore a Deleted Profile" CONFIRMED via Doug's own real GUI test — a deliberately-deleted profile correctly appeared in the "Deleted, but available to restore" list, and restoring it worked cleanly end to end. gui_app.py now v0.19.3 (doc-only). Prior rev (37, 2026-08-15) follows.

Doc rev 37 — refreshed 2026-08-15. Real bug fix, Doug's report from actually testing v0.19.1: the "reduce redundant backups" fix didn't actually reduce them -- DetectPanel.on_show() auto-detects every time it becomes active, and Back from the profile list always routes there, so every ordinary Back click was itself resetting the needs_backup flag. Fixed to only reset it on a GENUINE reconnect (the detected root actually changes), not a redundant re-verification of the same already-connected device. gui_app.py now v0.19.2. See PROJECT_NOTES.md doc rev 47. Prior rev (36, 2026-08-15) follows.

Doc rev 36 — refreshed 2026-08-15. "Reduce redundant profile backups," Doug's go-ahead (low priority). ProfileListPanel no longer re-backs-up every profile on every ordinary visit to the profile list -- a new needs_backup flag (set on a fresh Detect or a confirmed post-deploy reconnect) gates the real device backup call; the "Refresh (re-backup + re-list)" button still always forces one. gui_app.py now v0.19.1. See PROJECT_NOTES.md doc rev 46. Prior rev (35, 2026-08-15) follows.

Doc rev 35 — refreshed 2026-08-15. "Restore a profile that's no longer on the device," Doug's go-ahead. ProfileListPanel gets a second list, "Deleted, but available to restore," below the existing "On Device" list -- no new button, no new panel, per Doug's own 2026-08-11 design decision. Populated from a new garmin_device.py helper, list_backed_up_profile_filenames() (now v0.12.2), which scans every profile ever backed up and subtracts what's currently live. The existing "Restore from Backup..." button now works from either list. gui_app.py now v0.19.0. See PROJECT_NOTES.md doc rev 45 for the full writeup. Prior rev (34, 2026-08-15) follows.

Doc rev 34 — refreshed 2026-08-15. f10=32 renamed "GroupTrack" -> "Reserved", Doug's decision. This Conditional-only runtime record is present on every profile examined so far regardless of whether GroupTrack has ever been used, so its real purpose was never actually confirmed. fit_dump.py now v2.4.12, fit_patch.py now v1.14.2, gui_app.py now v0.18.1 — all doc-only, no functional/behavioral change (screen_type_name(32) just returns a different string). f10=57 "GroupTrack List" (the real on-device menu entry) is unaffected. See PROJECT_NOTES.md doc rev 44. Prior rev (33, 2026-08-14) follows.

Doc rev 33 — refreshed 2026-08-14. Real bug fix, Doug's report from actually using the new Startup Message editor: the message text showed a blank line between every real line, even though the same file opened cleanly (no blank lines) in BBEdit and vi. Best-evidenced cause: the file is likely CRLF-terminated (both those editors silently auto-normalize that on open, so it looks identical to LF there), and wx.TextCtrl has documented bad behavior when fed a string containing embedded \r\n. Fixed by normalizing all line endings to plain \n the moment the file is read (garmin_device.py's read_startup_txt(), now v0.12.1) — headlessly verified against a simulated CRLF file (zero blank lines, zero stray \r afterward) with the existing all-LF round-trip test unaffected. See PROJECT_NOTES.md doc rev 43. Prior rev (32, 2026-08-14) follows.

Doc rev 32 — refreshed 2026-08-14. New feature: view/edit the device's startup.txt custom boot message. New "Startup Message..." button on the GUI's Detect screen opens StartupTxtPanel — edits the <display=N> seconds value and the free-form message text, preserving Garmin's own comment scaffolding byte-for-byte; live char/line-count guidance only, no hard block, per Doug's own call that on-device wrapping is character-width-dependent and can't be reliably predicted. Confirmed via Doug's own real Edge 530 (2026-08-14): the file lives at garmin_root itself (same level as Sports/NewFiles), and writes are a DIRECT overwrite — no NewFiles import — needing a full power cycle to take effect, per the file's own on-device comment. garmin_device.py now v0.12.0 (read_startup_txt()/ parse_startup_txt()/build_startup_txt()/write_startup_txt(), plus a new startup-txt CLI subcommand), gui_app.py now v0.18.0. See PROJECT_NOTES.md doc rev 42 for the full writeup. Prior rev (31, 2026-08-14) follows.

Doc rev 31 — refreshed 2026-08-14. "Delete Screen" is now complete end to end. New "Remove Selected Screen" button on the GUI's Screens view, next to Move Up/Down, reusing --remove's exact CLI guards with no override and a plain confirmation that the delete is permanent (Restore-from-Backup only undo). gui_app.py now v0.17.0. Same change: split that view's button row into two, since a single row had grown to 9 buttons and ran the full window width. See PROJECT_NOTES.md doc rev 41 for the full writeup. Prior rev (30, 2026-08-14) follows.

Doc rev 30 — refreshed 2026-08-14. Real hardware feedback on all three items from doc rev 29. --remove is CONFIRMED via a real on-device round-trip test (fit_patch.py v1.14.1) — the GUI wrapper is now unblocked, still unbuilt until asked for. Two real bugs found and fixed from actually using the GUI: the Graph/Bars warning text blew out the Edit Screen window (fixed via textwrap-based hard wrapping, not wx.StaticText.Wrap()), and the Back-button warning's wording wrongly implied a resumable state (reworded to Doug's preference). gui_app.py now v0.16.17. Full details in PROJECT_NOTES.md doc rev 40 and FIT_PATCH.md doc rev 19. Prior rev (29, 2026-08-14) follows.

Doc rev 29 — refreshed 2026-08-14. Three pending items built, Doug's prioritization pass. ViewScreensPanel's Back-button data-loss bug is FIXED (gui_app.py v0.16.14); the Graph/Bars full-width warning is BUILT, surfaced in the field picker and both screen-editing panels (fit_dump.py v2.4.11, gui_app.py v0.16.15); and "Delete Screen"'s backend half — a new --remove flag reusing --hide's exact guards — is BUILT and headless-verified, though NOT YET on real hardware, so its GUI wrapper stays deliberately unbuilt (fit_patch.py v1.14.0). Full details in PROJECT_NOTES.md doc rev 39 and FIT_PATCH.md doc rev 18. Prior rev (28, 2026-08-13) summary follows.

Doc rev 28 — refreshed 2026-08-13. Clarified, not a new finding: Doug's "GroupTrack" in the prior rev's confirmed-active-Remove list meant the on-device editor's actual label "GroupTrack List" (f10=57) — already covered, not a separate untested type. The genuinely different f10=32 GroupTrack Conditional record never appears as a row in the on-device editor at all, so it has no Remove status to check and is structurally out of reach of the future --remove flag regardless. NO_SHOW_TOGGLE_TYPES (Map, ClimbPro) is now the complete confirmed Remove-block set for common named types — no remaining gap. fit_patch.py now v1.13.2 (doc-only). Prior rev (27, 2026-08-13) summary follows.

Doc rev 27 — refreshed 2026-08-13. Remove availability for named screen types confirmed on-device (Doug). Map and ClimbPro are the only common named types with Remove disabled — Elevation, GroupTrack, Cycling Dynamics, Lap Summary, Virtual Partner, Compass, and Segment all show it active, same boundary NO_SHOW_TOGGLE_TYPES already hard-codes for Show/Hide. fit_patch.py now v1.13.1 (doc-only), directly informs the still-scoped --remove/"Delete Screen" feature — see PROJECT_NOTES.md Open Items for the full writeup. GroupTrack List not yet separately confirmed. Prior rev (26, 2026-08-13) summary follows.

Doc rev 26 — refreshed 2026-08-13. fit_patch.py's --un-remove flag RETIRED entirely (Doug's decision, now v1.13.0). Restore-from- Backup already covers real recovery from an accidental delete at the whole-profile level (confirmed on real hardware), --un-remove had a confirmed historical device-side data-loss hazard never re-verified after its fix, and Garmin's own editor doesn't offer an un-remove workflow either — see PROJECT_NOTES.md "Product note on --un-remove" for the full history. fit_dump.py (v2.4.10) and gui_app.py (v0.16.13) comments updated to match, doc-only. Prior rev (25, 2026-08-13) summary follows.

Doc rev 25 — refreshed 2026-08-13. Second real bug found via real Mac hardware, same test session: install.sh v1.0.1 crashed with PIP_EXTRA[@]: unbound variable the moment it tried to install garmin-fit-sdk, on a Mac with a freshly-installed Homebrew python3 3.14. Cause: bash 3.2 (confirmed to be what actually runs this script on real macOS, via Doug's bash-3.2$ prompt) throws unbound-variable when an empty array is expanded under set -u — a real bash bug, fixed in 4.4+, invisible in the bash 5 dev sandbox. Fixed in v1.0.2: replaced the array with a pip_install() wrapper function, no array left in the script at all — eliminates the whole bug class rather than patching around this one instance. Re-verified both the plain-install and --upgrade code paths in the dev sandbox. Prior rev (24, 2026-08-13) summary follows.

Doc rev 24 — refreshed 2026-08-13. Real bug found via real hardware test: install.sh v1.0.0 crashed silently on a fresh Mac with no Xcode Command Line Tools installed — invoking python3 for its version check triggered macOS's own xcode-select "requesting install" note plus a non-zero exit, and set -e turned that into an unexplained stop right after "Found python3," no error message from the script. Fixed in v1.0.1: explicit Command Line Tools check as its own step, before python3 is touched at all, with clear instructions instead of a silent failure; defense-in-depth error handling on the python3 version-check call itself; new --version flag. Verified the fix in the dev sandbox by reproducing both the no-CLT and CLT-present paths. Not yet re-tested on Doug's actual Mac. Prior rev (23, 2026-08-13) summary follows.

Doc rev 23 — refreshed 2026-08-13. Cosmetic fix confirming pre-v1.0.1 release state. gui_app.py's FieldPickerDialog docstring still said "105 confirmed entries," stale since the 2026-08-11 batches grew fit_dump.py to 117 — comment only, the picker itself always read FIELD_ID_NAMES live so it was never actually wrong. Fixed, gui_app.py now v0.16.12. Confirmed for Doug: fit_dump.py v2.4.9 (117 entries, field 320 "Perf. Conditioning" the latest correction) and gui_app.py both reflect everything gathered so far, ahead of testing install.sh on real Mac hardware and a possible v1.0.1 tag. Prior rev (22, 2026-08-13) summary follows.

Doc rev 22 — refreshed 2026-08-13. New install.sh setup script (macOS only, matching this toolkit's current real platform support). Checks python3 presence/version (warns and checks for Xcode Command Line Tools if older than 3.10, since wxPython only has pre-built PyPI wheels from 3.10 up), creates/reuses a dedicated .venv, installs garmin-fit-sdk and wxPython into it, then verifies both import. Idempotent, --upgrade/--help flags. Setup section and GUI section updated to lead with it, manual pip install steps kept as a fallback. Cross-platform support deferred until Windows/Linux device detection itself exists. Prior rev (21, 2026-08-11) summary follows.

Doc rev 21 — refreshed 2026-08-11. Field 320 corrected — "Conditioning" is now "Perf. Conditioning." Full concept name is "Performance Conditioning," but Doug confirmed the actual on-device DATA FIELD display reads "Perf. Conditioning" (abbreviated), matching this toolkit's on-device-display naming convention (fit_dump.py now v2.4.9, still 117 entries, rename only). Prior rev (20, 2026-08-11) summary follows.

Doc rev 20 — refreshed 2026-08-11. Field 49 corrected — "Avg Speed (Alt)" is actually just "Avg Speed," confirmed by deploying it into a full-width screen slot and visually checking on-device: plain text, no graph/bars (fit_dump.py now v2.4.8). Flagged as a caution for the Graph/Bars marker theory, not a falsification — see PROJECT_NOTES.md for the full reasoning. Prior rev (19, 2026-08-11) summary follows.

Doc rev 19 — refreshed 2026-08-11. 12 new field IDs from Doug's continued census (2, 15, 18, 32, 165, 347, 350, 433, 452, 478, 495, 497), plus 3 placeholder names corrected now that the ""/"(Alt)" on-device marker is understood — it denotes a Graph/Bars-style field needing a full-width screen slot (fit_dump.py now v2.4.7, 117 confirmed entries). See PROJECT_NOTES.md for the full writeup. Prior rev (18, 2026-08-11) summary follows.*

Doc rev 18 — refreshed 2026-08-11. Two field names corrected (real user report) — fields 58/87 were "Lap Timer"/"Last Lap Timer," now correctly "Lap Time"/"Last Lap Time" (fit_dump.py v2.4.5, no IDs added/removed, still 105 confirmed entries). See PROJECT_NOTES.md for the full writeup. Prior rev (17, 2026-08-11) summary follows.

Doc rev 17 — refreshed 2026-08-11. Clone Profile CONFIRMED via real hardware — two working clones deployed via NewFiles under brand-new filenames (Clonebox, CloneRoad), reported after the fact and corrected here (this file had been carrying a stale "not yet tested through the actual GUI" note). gui_app.py reached v0.16.10 (doc-only) logging the confirmation. See PROJECT_NOTES.md for the fuller writeup. Prior rev (16, 2026-08-11) summary follows.

Doc rev 16 — refreshed 2026-08-11. Pre-publish housekeeping ahead of a possible GitHub release. Added a License/Disclaimer section (MIT, "not affiliated with Garmin" trademark note, black-box- reverse-engineering method note, device-write risk warning). gui_app.py reached v0.16.9: window title renamed to "Activity Profile Screen Editor for Garmin Edge" (v0.16.7), a new About button/dialog (v0.16.8), and a fix to the default backup working directory — now cross-platform (~/GarminBackups) and persisted across restarts instead of resetting to a hardcoded Mac-specific path every launch (v0.16.9). See PROJECT_NOTES.md for the full pre-publish writeup, including an in-progress Windows-support scoping assessment. Prior rev (15, 2026-08-05) summary follows.

*Doc rev 15 — refreshed 2026-08-05. MAJOR REVERSAL: Add New Screen via NewFiles is now CONFIRMED WORKING. The "always fails" limitation documented since early in this project was root-caused as an f10 IDENTITY COLLISION, not a hard device restriction — --new-slot's old default silently wrote f10=0, colliding with the f10 almost every real profile's existing "Screen 1" already holds. fit_patch.py 1.12.0 adds next_available_field10() and uses it as the new auto-default, replacing the old hardcoded 0. CONFIRMED via a live on-device round-trip (2026-08-05, CyclingRoadSandbox): a new screen with a collision-free f10 survives the NewFiles restart cycle intact, verified independently by both fit_dump.py and garmin_device.py reading the live mounted device afterward. --un-remove uses the same corrected default but hasn't itself been re-tested live yet. See FIT_PATCH.md BUGS and PROJECT_NOTES.md for the full writeup, including the superseded original failure diagnosis.

Prior rev (14, 2026-08-04): field 10 (f10) confirmed as a real, content-independent screen TYPE identifier — named Garmin types (Map, Compass, Elevation, Segment, ClimbPro, etc.) get a fixed global code; plain user screens use a per-profile counter shown on-device as "Screen N". This finally answers "how many real user screens does this profile have" directly from the file. Landed: fit_dump.py 2.4.2 (bug fix for a classify_screens() gate that missed some genuine screens; screens output now shows real type names; 86 confirmed field IDs — 84/87 resolved, closing the last open field-ID mystery), fit_patch.py 1.11.0 (would_hide_last_visible_screen() rewritten to count only real user screens via f10, fixing a confirmed undercounting bug; check_system_screen_guard() also made f10-based, fixing a real reported false positive where a confirmed user screen still triggered the old "possibly a system screen" pause; NEW hide_unsupported_screen_type() hard-blocks hiding Map or ClimbPro at all — confirmed via direct on-device inspection that neither has a Show Screen toggle, on any profile type), and gui_app.py 0.6.3 (screen list shows a Type column with real names, edit screen title shows the real name, guard warning dialogs no longer false-positive on confirmed user screens, and a third HARD block against hiding Map/ClimbPro). Also corrects an earlier claim that Removed-state screen content is preserved indefinitely — it's purged by the next NewFiles-mediated deploy of any kind, confirmed via real testing. See PROJECT_NOTES.md for full detail.*

Command-line toolkit for reading and editing Garmin Edge 530 Activity Profile screen configurations directly, without the on-device menu — screen order, field lists, layout, show/hide, multi-step batched edits, and profile cloning, all from a computer. Built by reverse-engineering an undocumented section of the FIT file format; see PROJECT_NOTES.md for the full technical writeup, MVP_SCOPE.md for what's in/out of scope, and MEMORY_LOG.md for the complete project history and findings log.

Status: CLI toolkit complete and validated end-to-end on real hardware — every core capability (field edits in both directions, layout, show/hide, reorder, multi-step chaining, profile clone-and-retarget, restore-from-backup) has a confirmed real-device round trip. GUI implementation has started: gui_app.py is a skeleton covering step 1 of the agreed flow (detect device + show device info) — see PROJECT_NOTES.md for the full flow and what's built vs. still to come.

About

GUI and CLI toolkit to edit the Activity Profiles on Garmin Edge bicycle computers

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages