Commit Graph
5 Commits
Author SHA1 Message Date
dtourolleandClaude Opus 5 db974dcac1 Pin the versionCode in the config, not in a file the build overwrites
🚴 Build and Test BikeControl / Android compile check (push) Successful in 3m29s
🚴 Build and Test BikeControl / Workspace tests (push) Successful in 13m33s
The Android job died on `tauri.properties not found`. It is not found
because `cargo tauri android init` does not write it: in a clean clone
init leaves gen/android/app/ holding build.gradle.kts, proguard-rules.pro
and src/, nothing else. The generated app/.gitignore names the pattern —
tauri.properties sits with tauri.build.gradle.kts and proguard-tauri.pro,
the tauri.* files that `android build` stamps out on every run.

So the step was wrong twice and the crash was the lucky half. Had the
file existed, the build would have rewritten it from tauri.conf.json and
discarded the sed — a green build shipping the default versionCode, which
is the failure that reaches a rider's phone rather than the log.

Tauri exposes the actual input, so use it:

- tauri.conf.json gains bundle.android.versionCode, committed rather than
  conjured by CI, so the key is greppable and the sed has a fixed target.
- ci-android-version-code.sh edits the config. Formula and the 1000 floor
  are unchanged; the header comment is rewritten, since its premise (init
  writes the file, the default collides) does not hold — Tauri's default
  is major*1000000 + minor*1000 + patch, monotonic, and it puts 0.1.0 at
  exactly 1000, which is where the floor comes from. Missing key or failed
  substitution now exits 1 instead of degrading to a silent no-op.
- The step moves ahead of `android init`, next to ci-set-version.sh, since
  both edit the same config.

Verified against a clean clone: 0.1.0 -> 1100, v0.2.3 -> 1203,
v1.0.0 -> 11000, config still parses at each step. Then a real
`cargo tauri android build`, which wrote versionCode=1100 into
tauri.properties — the value reaches the APK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 17:39:06 +02:00
dtourolleandClaude Opus 5 13458ca180 Teach droidplug to indicate and to ask for a bigger MTU
Two Android-only BLE defects, both of which BlueZ hides and both of which
made a working Click pod look like broken hardware.

droidplug writes ENABLE_NOTIFICATION_VALUE to the CCCD whatever the
characteristic supports. A characteristic that indicates but does not
notify rejects that write, so the Click's Sync TX channel
(00000004-19ca-…, read/indicate) failed to subscribe on every pod with
"Unable to write descriptor". The patch picks the value from the
characteristic's properties.

droidplug also never calls requestMtu, so Android stayed at the 23-byte
default and a notification carried 20 bytes. The pods send up to 106.
Anything longer arrived truncated mid-field and failed to parse, which
looks exactly like a pod that has gone quiet — the frames were being cut
off, not withheld. Requesting 517 settles at 251 against this hardware,
and a 105-byte frame now arrives whole.

Both are applied by the sync script after it lifts the Java out of the
crate, each guarded by a grep that fails the sync loudly if upstream
moves the line rather than silently producing an unpatched build.

Also ignore the .gradle cache that IDE Gradle daemons drop into
src-tauri/android/app, which they mistake for a project root because of
the build.gradle.kts template living there.

Verified on the tablet: no subscribe failures, longest Click frame 105
bytes where the cap was 20, and both paddles shifting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:15:53 +02:00
dtourolleandClaude Opus 5 06b4635470 Build every artifact in a pinned image, not on my laptop
Two builder images, because the jobs genuinely need two distributions:
Ubuntu for the tests, the Linux bundle and the Android APK, and Arch for
the .pkg.tar.zst, since makepkg is Arch-specific. Both are built from
this repo (NFR-12) and pinned by tag in the workflows, so a Dockerfile
change only reaches CI once it has been pushed.

libdbus-1-dev is not incidental in the Ubuntu image: btleplug's Linux
backend is bluez-async over the dbus crate, so without it the workspace
does not build at all.

The per-commit Android job is a cargo check, not an APK. The full signed
build is ~15 minutes and runs only on tags; a one-minute check catches
what actually breaks — the JNI shim, droidplug, and any desktop-only API
that has crept into a shared crate.

Two invariants a compiler cannot see are checked there too, because both
fail silently: the app builds, installs, launches, and finds no trainer.
The JNI symbol in android.rs is matched by the runtime by name, so
renaming the Kotlin package compiles fine and simply never initialises
btleplug; and gen/ must stay untracked or the sync script quietly becomes
optional.

The Android versionCode carries a 1000 floor. `tauri android init` writes
1000 for 0.1.0 today, so anyone holding a locally built APK already has
that number installed, and a bare major/minor/patch code would be 100 —
a downgrade, which Android refuses outright.

The fmt check is advisory for now. The tree predates this workflow and
`cargo fmt --all` currently rewrites ~2000 lines across 28 files; making
it a gate here would mean landing a repo-wide reformat as a side effect
of adding CI. Run fmt in its own commit, then drop the continue-on-error.
The two clippy warnings that stood between the tree and a real
`-D warnings` gate are fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:52:45 +02:00
dtourolleandClaude Opus 5 0679a1f524 Ride on Android: the same BLE stack, over JNI
G-4 said port to Android without rewriting the core, and nothing in
crates/core, crates/ble or crates/fit needed touching (NFR-5) — the
Android work is two files of glue and a Gradle project.

btleplug's Android backend is a hybrid crate: the GATT work happens in
Java and Rust drives it over JNI. `platform::init` has to run once with a
JNIEnv, and it cannot come from Rust's own startup — JNI resolves classes
with the calling thread's class loader, and a thread Rust spawned has
only the bootstrap loader. So MainActivity.onCreate calls into
src/android.rs, before super.onCreate: TauriActivity's super chain
synchronously starts the thread that runs `run()`, which builds AppState
and starts scanning while we are still in onCreate. Lose that race and
droidplug's global_adapter() — an `expect` — panics inside the scan task,
silently, for the life of the process.

Failing soft here is not enough for the same reason, so init sets a READY
flag and devices.rs asks before every call in. Bluetooth switched off at
launch then reads as an ordinary "no adapter", which the connection
screen already knows how to show, and onResume retries so switching it on
and coming back works.

The Java half is not a maven dependency. Upstream tells you to publish a
0.1.1-SNAPSHOT artifact to mavenLocal by hand, which no CI runner can
reproduce and which drifts from the crate silently — the failure is a
NoSuchMethodError at the first scan, not a build error. Instead
sync-android-sources.sh lifts the classes out of the btleplug and
jni-utils crate sources at exactly the versions in Cargo.lock, so a
mismatch is impossible by construction.

gen/ stays generated and untracked, so everything hand-written lives in
src-tauri/android/ and is copied back after each `tauri android init`.
check-android-sources.sh fails the build if a source exists only under
gen/ or differs from its tracked copy: both are files git has never seen
and the next init deletes, and the resulting APK builds, installs, and
behaves as though they were never written.

Permissions are split at API 31, because asking for one the platform does
not know is a permanent denial. neverForLocation on BLUETOOTH_SCAN is a
promise we can keep honestly: every scan filters by service UUID, so no
location permission is needed on Android 12+.

Also: tracing to logcat, since Android has no stdout and the default
writer drops every line into a closed fd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:52:33 +02:00
dtourolleandClaude Opus 5 57eb5e809b Virtual gearing, trainer-speed blend, and cadence decode
Gears are expressed as an offset to the commanded gradient, leaving the
physics on the route's true gradient so shifting changes effort, not speed.
Neutral gear commands exactly the route gradient, so an un-shifted ride is
unchanged.

Cadence is not in FTMS on this trainer but is on its Zwift channel, decoded
against captured frames. The undeclared FTMS trailing bytes were ruled out:
wheel RPM restated at a fixed 73.8x speed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:33:28 +02:00