Skip to content

Make the TrainingPeaks Hub app connect in seconds: pasta networking + LAN re-advert - #8

Merged
BenA-SA merged 6 commits into
masterfrom
feat/hub-readvert
Sep 20, 2026
Merged

BenA-SA merged 6 commits into
masterfrom
feat/hub-readvert

Conversation

@BenA-SA

@BenA-SA BenA-SA commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

Makes the TrainingPeaks Hub companion app connect to TPV in the container within seconds, for every user and with no manual steps. Hub's In Game remote has Gear Up/Down and Difficulty −/+, which covers the handlebar-control goal in #3.

Problem: Hub discovers TPV via mDNS (_tpvirtual._tcp) and connects in on TCP 7779. TPV advertises an address for every interface Wine reports (172.17.0.1 Docker bridge, 100.89.193.106 Tailscale, 192.168.0.146 Wi-Fi). Hub tries only the first and waits out Android's ~127 s TCP connect timeout. Meanwhile it shows "please start a ride".

Fix:

  1. run-tpv.sh runs the container on pasta networking bound to the LAN interface, so TPV only sees that interface.
  2. The entrypoint re-advertises TPV through the host avahi-daemon, pointing at the LAN address. TPV's own advert doesn't reach the phone under pasta, so this is required.

Changes

  • run-tpv.sh: --network=pasta:-i,<LAN if> -p 7779:7779 --hostname <host>.
    • The LAN interface is TPV_LAN_IF, or by default the default-route interface.
    • TPV_NETWORK=host, or qz mode, keeps host networking.
    • Also passes TPV_HOST_NAME / TPV_HUB_ADVERT / TPV_HUB_ADDR / TPV_HUB_NAME.
  • scripts/entrypoint.sh:
    • advertise_hub runs before the TPV launch, and stop_hub_advert after wineserver -w.
    • It publishes <hostname>-tpv-<last octet>.local and <hostname> (LAN)._tpvirtual._tcp port 7779 txtvers=1.
  • Containerfile.full: adds avahi-utils (image rebuild required).
  • verify.sh: check 6 flags each _tpvirtual._tcp IPv4 entry OK/BAD. Under pasta, checks 3 and 5 run inside the container, because QZ's ports live in its network namespace.
  • README: new env vars, the real cause in "Hub app cannot find TPV", and the updated podman flags table.
  • docs/adr/0001-…: the full diagnosis, including a red herring (the advert name) and the measurements below.

Testing

Cold starts of Hub, driven over adb: force-stop, wait 20 s for the Android NSD cache to expire, launch. Connection times are taken from the phone's netstat and TPV's Player.log:

Setup Hub's first attempt Connected
--network=host + re-advert (Docker + Tailscale present) 172.17.0.1:7779 SYN_SENT 132 / 133 / 133 s
--network=host, docker0 deleted 100.89.193.106 (Tailscale, reachable by luck) 3–4 s ×3
pasta on wlp0s20f3 + re-advert (Docker + Tailscale present) 192.168.0.146:7779 3 / 4 / 3 s
  • ✅ Under pasta, Wine's ipconfig shows only wlp0s20f3 and lo.
  • ✅ The rebased branch builds (tpv-full:pr8).
  • ⏳ Still to do before merging (needs the bike):
    • A ride in both mode with the real trainer under pasta. QZ's DIRCON advert goes through host avahi over D-Bus and QZ shares the container's network namespace, so it should work. It's the main risk.
      cd ~/src/tpv-qz-container && git checkout feat/hub-readvert && git pull
      IMAGE=localhost/tpv-full:pr8 ./run-tpv.sh        # expect "==> network: pasta on wlp0s20f3"
      ./verify.sh                                       # checks 2-6 should be OK
      Pair the trainer and ride a few minutes (power/cadence arrive, ERG responds).
    • Hub Live and the In Game remote during that ride (close and reopen Hub: it should connect within a few seconds).
    • Optional: a 60-minute ride watching for "Lost heartbeat" drops.

Known limitation

Trainers that advertise themselves on the LAN (a Wi-Fi KICKR, a remote DIRCON bridge) probably aren't discovered under pasta, because inbound multicast isn't forwarded. The workaround is TPV_NETWORK=host. Tracked in #13.

Notes

  • The manual tpv-hub-addr / tpv-hub-svc workaround units used before this PR are no longer running and aren't needed with it.
  • A report for TrainingPeaks (Hub should try all resolved addresses; TPV should advertise only reachable ones) is drafted separately.

Refs #3

🤖 Generated with Claude Code

@BenA-SA
BenA-SA force-pushed the feat/hub-readvert branch 2 times, most recently from 4744af3 to e4175e0 Compare September 17, 2026 13:31
@BenA-SA BenA-SA changed the title Advertise TPV to the TrainingPeaks Hub app with a routable address Make the TrainingPeaks Hub app connect in seconds: pasta networking + LAN re-advert Sep 17, 2026
@BenA-SA

BenA-SA commented Sep 20, 2026

Copy link
Copy Markdown
Owner Author

@sentry review

Comment thread scripts/entrypoint.sh Outdated
BenA-SA and others added 4 commits September 20, 2026 09:12
Under Wine, TPV's own _tpvirtual._tcp mDNS advert can carry a bridge
address (docker0's 172.17.0.1 on a host running Docker), so the Hub
companion app finds TPV but cannot connect to TCP 7779 and shows no ride.

The entrypoint now publishes a second advert through the host
avahi-daemon while TPV runs, pointing at the default-route IPv4 address.
Hub picks the reachable entry, confirmed on the laptop with a manual
re-advert: Live data and the In Game remote both worked.

- entrypoint: advertise_hub / stop_hub_advert around the TPV launch
- run-tpv.sh: TPV_HOST_NAME, TPV_HUB_ADVERT, TPV_HUB_ADDR, TPV_HUB_NAME
- Containerfile.full: avahi-utils
- verify.sh: check 6 for the _tpvirtual._tcp advert address
- README and ADR-0001

Refs #3

Co-Authored-By: Claude <noreply@anthropic.com>
…s to

End-to-end testing showed Hub ignores 'fedora (LAN)' but connects to
'FEDORA LAN' on the same host record. ADR-0001 records the A/B steps.
TPV advertises an address for every interface Wine reports, and Hub
tries only the first, waiting out Android's ~127 s TCP timeout when it
is a Docker bridge or VPN address. pasta bound to the LAN interface
shows TPV only that interface: cold-start connects went from ~132 s to
3-4 s. TPV_NETWORK=host restores host networking (network trainers, #13).

ADR-0001 records the diagnosis, including the advert-name red herring.
Seer flagged that the advert check leaves an orphaned avahi-publish when
only one of the two publishers dies. The orphan is real, but the stated
mechanism is inverted for this script: bash's kill BUILTIN returns 0 if
ANY listed pid is live, so 'kill -0 "${HUB_ADVERT_PIDS[@]}"' succeeds
with one publisher dead and the failure branch never runs. (/bin/kill
does return non-zero there, which is where the assumption comes from.)

The effect was worse than an orphan: no warning, and TPV reported the
advert as working while publishing half a service to the Hub app.

Check each pid on its own, and reap the rest before returning.
Comment thread scripts/entrypoint.sh
Seer, correctly: lan_address() runs a pipeline under 'set -euo pipefail',
and its value is taken by a plain assignment, so a failing 'ip route get'
propagates and the script exits before the empty-address warning can run.
Reproduced: with the route unavailable the entrypoint dies silently at
exit 2, printing neither the warning nor anything after it.

That is exactly the case this PR introduces -- no route yet while pasta
networking settles -- so it would have bitten on a cold start.

Let the pipeline fail into an empty string instead.
@BenA-SA
BenA-SA merged commit c58e139 into master Sep 20, 2026
4 checks passed
@BenA-SA
BenA-SA deleted the feat/hub-readvert branch September 20, 2026 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant