Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
836ab1e
feat: provision and identify the factory demo ride
timohueser Sep 28, 2026
8fddc56
docs: describe factory demo ride provisioning
timohueser Sep 28, 2026
885649d
fix: resolve the factory generator from the workspace root
timohueser Sep 28, 2026
b3a5867
feat(ios): guide first-use pairing and device naming
timohueser Sep 28, 2026
594e284
test(ios): exercise onboarding device and update branches
timohueser Sep 28, 2026
9175102
feat(ios): finish optional onboarding through the Library
timohueser Sep 28, 2026
d79b7e1
Merge remote-tracking branch 'origin/claude/zu-skills-install-x0qn6t'…
timohueser Sep 28, 2026
14176d7
fix(ios): clarify onboarding completion actions
timohueser Sep 28, 2026
3699b1c
Merge remote-tracking branch 'origin/develop' into codex/onboarding-ios
timohueser Sep 28, 2026
1797ff4
test(ios): exercise onboarding at accessibility text sizes
timohueser Sep 28, 2026
e8e32c4
fix: follow the bundled demo route in the fixture guard
timohueser Sep 28, 2026
5e3306f
fix(web): preserve the demo flag in ride objects
timohueser Sep 28, 2026
66f2aa2
docs: refresh the bundled route import capture
timohueser Sep 28, 2026
1aa33e1
fix(ios): use the signpost in device illustrations
timohueser Sep 29, 2026
a8f8d69
fix(ios): open the Library while reconnecting
timohueser Sep 29, 2026
82738c1
fix(ios): preserve ride archives and harden transfer recovery
timohueser Sep 30, 2026
8b35956
fix(firmware): clear personal card data on factory reset
timohueser Sep 30, 2026
666f8ab
fix(firmware): keep factory demo provisioning alive after warm reset
timohueser Sep 30, 2026
7178c2f
docs: define reset deletion scope and demo archive fields
timohueser Sep 30, 2026
f372cef
Merge develop into codex/onboarding-ios
timohueser Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ user_config.json
!specs/vectors/*.gpx
# ...and the iOS companion's bundled GPX import fixture (three OBCKit tests load it).
!companion-ios/Packages/OBCKit/Sources/OBCMock/Fixtures/sample-import.gpx
!companion-ios/Packages/OBCKit/Sources/OBCFormats/Resources/grimsel-pass.gpx
# ...and the captured route-import provider exports, the canonical copy of the same bytes.
!fixtures/sources/route-import/*.gpx
!fixtures/sources/kandel-ride/*.gpx
Expand Down
1 change: 1 addition & 0 deletions builder/app/dev-harness/ride-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function loopRide(
const distanceM = Math.round(radiusDeg * 111_000 * 2 * Math.PI * 1.2);
return {
version: 6,
isDemo: false,
effortLimits: { maxHrBpm: null, ftpW: null },
name,
startTime,
Expand Down
1 change: 1 addition & 0 deletions builder/app/dev-harness/simulated-device.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ function syntheticRide(name: string, startTime: number, points: number, sensors:
}
return {
version: 6,
isDemo: false,
effortLimits: { maxHrBpm: null, ftpW: null },
name,
startTime,
Expand Down
1 change: 1 addition & 0 deletions builder/app/src/lib/device/library.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ function rideObject(name: string, startTime: number, points = 24): RideObject {
}
return {
version: 6,
isDemo: false,
effortLimits: { maxHrBpm: null, ftpW: null },
name,
startTime,
Expand Down
14 changes: 14 additions & 0 deletions builder/app/src/lib/device/rides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function rideFromTrackLog(log: Uint8Array, name: string, startTime: number): Rid
}
return {
version: 6,
isDemo: false,
effortLimits: { maxHrBpm: null, ftpW: null },
name,
startTime,
Expand Down Expand Up @@ -132,6 +133,7 @@ function longRide(points: number): RideObject {
}
return {
version: 6,
isDemo: false,
effortLimits: { maxHrBpm: null, ftpW: null },
name: "Long Way Round",
startTime: 1_783_598_400,
Expand Down Expand Up @@ -231,6 +233,7 @@ describe("the ride object", () => {
const ride = decodeRideObject(bytes);
expect(ride).toMatchObject({
version: 6,
isDemo: true,
name: "Sensor Ride",
startTime: 1_751_460_000,
distanceM: 12_345,
Expand Down Expand Up @@ -262,6 +265,17 @@ describe("the ride object", () => {
expect(encodeRideObject(ride)).toEqual(bytes);
});

it("keeps ordinary rides unmarked and rejects reserved footer flags", () => {
const bytes = encodeRideObject(longRide(1));
const flags = bytes.length - 154 + 33;
expect(bytes[flags]).toBe(0);
expect(decodeRideObject(bytes).isDemo).toBe(false);
for (const reserved of [2, 0x80, 0xff]) {
bytes[flags] = reserved;
expect(() => decodeRideObject(bytes)).toThrow("non-canonical summary footer");
}
});

it("keeps a zero elevation as the device's 'no barometer yet' value", () => {
const ride: RideObject = {
...longRide(1),
Expand Down
7 changes: 6 additions & 1 deletion builder/app/src/lib/usb/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export interface RideEffortLimits {
/** A downloaded v6 ride: the recorded sample bytes followed by one fixed summary footer. */
export interface RideObject {
version: 6;
/** Synthetic onboarding ride; preserved with the recorded payload. */
isDemo: boolean;
name: string;
startTime: number;
distanceM: number;
Expand Down Expand Up @@ -110,7 +112,8 @@ export function decodeRideObject(data: Uint8Array): RideObject {
const version = data[footer + 4];
if (version !== 6) throw new ObjectDecodeError(`ride object version ${version}; this client decodes 6.`);
const limits = footer + RIDE_LIMITS_AT;
if (view.getUint16(footer + 6, true) !== RIDE_FOOTER_LEN || data[footer + 33] !== 0 || data[limits + 1] !== 0) {
const flags = data[footer + 33];
if (view.getUint16(footer + 6, true) !== RIDE_FOOTER_LEN || (flags & ~1) !== 0 || data[limits + 1] !== 0) {
throw new ObjectDecodeError("ride object has a non-canonical summary footer.");
}
const name = footerName(data, footer + RIDE_NAME_AT, data[footer + 5]);
Expand Down Expand Up @@ -149,6 +152,7 @@ export function decodeRideObject(data: Uint8Array): RideObject {

return {
version: 6,
isDemo: (flags & 1) !== 0,
points,
name,
startTime: view.getUint32(footer + 8, true),
Expand Down Expand Up @@ -210,6 +214,7 @@ export function encodeRideObject(r: RideObject): Uint8Array {
out[footer + 30] = r.avgHr ?? NO_U8;
out[footer + 31] = r.maxHr ?? NO_U8;
out[footer + 32] = r.avgCadence ?? NO_U8;
out[footer + 33] = r.isDemo ? 1 : 0;
view.setUint16(footer + 34, r.avgPower ?? NO_U16, true);
view.setUint16(footer + 36, r.maxPower ?? NO_U16, true);
view.setUint32(footer + 38, r.energyKj ?? NO_U32, true);
Expand Down
10 changes: 10 additions & 0 deletions companion-ios/OBCCompanion/OBCCompanion.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:openbikecomputer.com</string>
</array>
</dict>
</plist>
16 changes: 16 additions & 0 deletions companion-ios/OBCCompanion/OBCCompanionApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ struct OBCCompanionApp: App {
RootView(
transport: Self.makeTransport(),
bondStore: Self.makeBondStore(),
onboarding: Self.makeOnboardingProgress(),
updateChecker: Self.makeSetupUpdateChecker(),
library: Self.makeLibraryStore(),
photoLibrary: Self.makePhotoLibrary(),
lastBikeType: Self.makeLastBikeTypeStore(),
Expand Down Expand Up @@ -257,6 +259,20 @@ struct OBCCompanionApp: App {
return PathMonitorReachability()
}

static func makeOnboardingProgress() -> OnboardingProgress {
#if DEBUG
if mockControl != nil { return OnboardingProgress() }
#endif
return OnboardingProgress(defaults: .standard)
}

static func makeSetupUpdateChecker() -> UpdateChecker? {
#if DEBUG
if let mockControl { return OnboardingFixtures.updateChecker(for: mockControl.scenario) }
#endif
return UpdateChecker()
}

/// The bond record behind the launch branch. Mock runs read it from the scenario, so the dev
/// panel can replay first-run pairing; the real path persists it in `UserDefaults`.
static func makeBondStore() -> any BondStore {
Expand Down
Loading
Loading