Skip to content

Consolidate brownfield chassis and BLE safety packets #1 + #8 - #722

Closed
9thLevelSoftware wants to merge 5 commits into
mainfrom
consolidate/brownfield-8951e31b-01-08-chassis-ble
Closed

Consolidate brownfield chassis and BLE safety packets #1 + #8#722
9thLevelSoftware wants to merge 5 commits into
mainfrom
consolidate/brownfield-8951e31b-01-08-chassis-ble

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Summary

Consolidates brownfield packets #1 and #8 onto current main at f4695a90ce9467852308675c5b00ed0bbde12337.

Applied in source-chain order:

  • bfbd18d7819f67fe01286dd0d7df82fe24189c1b
  • b1486e8fab8ed613f965217841e5ffea661542a1
  • a92214fe925e3156dc78c57ae0e4c1ca78a7f435

The branch also contains a small post-cherry-pick review fix (755524b) for stored-advertisement identity handling and the Vitruvian classifier edge case.

Behavior

  • Enforces per-cable chassis ceilings: Trainer+ 110 kg, V-Form/Unknown 100 kg.
  • Keeps unrecognized/disconnected model resolution fail-closed.
  • Requires the resolved model for legacy and 0x04 CONFIG validation.
  • Caps CONFIG forceMax to min(selected weight + 10, chassis ceiling).
  • Clamps finite-rep progression to remaining chassis headroom; leaves negative regression and unlimited Just Lift/AMRAP progression behavior unchanged.
  • Threads chassis limits through ASE, routine/session managers, rack adjustment, recommendations, drop-set validation, and UI weight controls.
  • Scanner lists named Vee_/VIT devices, excludes named Phoenix/Vitruvian/generic devices, and shows unnamed NUS/FEF3 candidates as visible-only.
  • scanAndConnect excludes unnamed candidates; connect() rechecks both the scanned label and stored advertisement before Peripheral/GATT creation.
  • Last successful identifier is recorded only after ready-up; it can opt in only to unnamed/generated placeholders. Rejected identities produce no CONFIG/RESET traffic.
  • Echo 0x4E packet behavior is unchanged.

Preservation

Verification

Fresh final-tree results:

  • Focused chassis/BLE/rack/recommendation/drop-set suite: 218 tests, 0 failures, 0 errors.
  • Current lifecycle and portal 1RM regression set: 324 tests, 0 failures, 0 errors.
  • Full :shared:testAndroidHostTest: 3,757 tests, 0 failures, 0 errors.
  • :shared:compileKotlinIosArm64 :shared:compileTestKotlinIosArm64: passed.
  • :androidApp:testDebugUnitTest: passed.
  • :androidApp:assembleDebug: passed.
  • :androidApp:lintDebug: passed.
  • git diff --check: passed.

Release boundary

Real V-Form and Trainer+ hardware validation remains a documented release boundary; no physical-device result is being claimed here.

Refs brownfield scan 8951e31b packets #1 and #8

codex and others added 4 commits August 30, 2026 20:15
Unknown and V-Form hardware now cap at 100 kg/cable; Trainer+ stays 110.
Validators, packet factory, ASE send site, and weight sliders require
PhoenixModel. forceMax is min(weight+10, chassisMax). Echo 0x4E is
unchanged and still does not carry kg.
Thread PhoenixModel into rack calculate, set-ready weight, and
weight-increase recommendations. Clamp finite-rep CONFIG progression
to remaining chassis headroom. Wire HardwareDetection.getCapabilities
to per-cable ChassisLimits. Expand host goldens and fail-close test helpers.
Extract BleAdvertisementFilter so startScanning matches scanAndConnect:
connectable iff Vee_ or VIT. connect() re-checks identity before GATT
and does not send CONFIG or RESET. Unnamed NUS/FEF3 may be listed, not
auto-bound. Last-successful identifier remains an opt-in.
Copilot AI lite review requested due to automatic review settings August 31, 2026 00:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T01:12:46.545266Z a450817 New commits
🔒 Security Review Completed 2026-08-31T00:54:17.615335Z 755524b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 755524bece

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

val encodedProgression = ChassisLimits.finiteRepProgressionKg(
requestedKg = params.progressionRegressionKg,
weightPerCableKg = targetWeightPerCable,
reps = params.reps,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include firmware warm-up reps in the progression cap

For finite workouts with positive progression and warmupReps > 0—the normal cable path forces three firmware warm-up reps—the packet serializes reps + warmupReps, and the firmware applies the increment to every rep, but this calculation caps against only params.reps. For example, 90 kg with 8 working reps and 3 warm-up reps encodes a 10/7 kg increment even though there are 10 transitions, producing an implied 104.3 kg trajectory that hits forceMax early instead of progressing to the ceiling on the final rep; pass the total serialized rep count to the cap calculation.

Useful? React with 👍 / 👎.

Comment on lines +554 to +558
BleAdvertisementFilter.mayConnectWithAdvertisementIdentity(
scannedName = device.name,
advertisedName = advertisedName,
identifier = device.address,
lastSuccessfulIdentifier = lastSuccessfulIdentifier,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the stored advertisement to select the chassis model

When a stale ScannedDevice has a VIT... label but the stored advertisement for the same identifier now has a Vee_... label, this predicate accepts both independently. The connection is then published with HardwareDetection.detectModel(device.name) at line 686, so the stale label classifies the V-Form as Trainer+ and permits the new validators and packet builder to use the unsafe 110 kg ceiling. Derive the connected model from advertisedName, or reject identities whose chassis families disagree.

Useful? React with 👍 / 👎.


hasPhoenixServiceData
}
.filter { advertisement -> shouldListAdvertisement(advertisement) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invalidate cached devices on rejected advertisements

If an identifier first advertises an accepted Vee_/VIT name and later advertises a rejected generic name such as Phoenix or Vitruvian, this filter discards the later observation before either discoveredAdvertisements or currentScannedDevices is updated. The stale trainer row therefore remains selectable, and connect() rechecks the previously cached accepted advertisement rather than the latest rejected identity, allowing GATT creation despite the intended fail-closed rule. Rejected observations for known identifiers need to remove or invalidate the cached entry.

Useful? React with 👍 / 👎.

Comment on lines +183 to +185
val preview by remember(currentMode, exercises) {
derivedStateOf {
currentMode?.let { mode -> applyBulkAdjust(exercises, mode) }
currentMode?.let { mode -> applyBulkAdjust(exercises, mode, hardwareModel) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute the bulk preview when the chassis changes

When the connection changes while this dialog is open, hardwareModel can switch between Unknown/V-Form and Trainer+, but it is absent from the remember keys, so the retained derivedStateOf continues calling applyBulkAdjust with the old model. Connecting a Trainer+ leaves the preview capped at 100 kg, while disconnecting or switching to V-Form can leave a preview up to 110 kg that is subsequently applied; include hardwareModel in the remembered inputs.

Useful? React with 👍 / 👎.

name = scannedName,
identifier = identifier,
lastSuccessfulIdentifier = lastSuccessfulIdentifier,
) && mayConnect(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The mayConnect call on line 70 hands advertisedName = null into the opt-in branch. A nameless stored advertisement that was never a successful connect passes purely because its BLE address once matched lastSuccessfulIdentifier. That is not fail-closed — that is fail-warm with a free pass for whatever address your last Trainer had. The whole point of this function is that the stored advertisement is the authoritative identity; treating null as "it's an unnamed placeholder, OK" defeats it.

🩹 The Fix: Require the stored advertisement name to be connectable independently, OR at minimum disallow the lastSuccessfulIdentifier opt-in when advertisedName == null inside mayConnectWithAdvertisementIdentity. For example:

fun mayConnectWithAdvertisementIdentity(
    scannedName: String?,
    advertisedName: String?,
    identifier: String?,
    lastSuccessfulIdentifier: String? = null,
): Boolean {
    val scannedOk = mayConnect(
        name = scannedName,
        identifier = identifier,
        lastSuccessfulIdentifier = lastSuccessfulIdentifier,
    )
    // Stored advertisement is authoritative: must be connectable on its own.
    // Opt-in is only for the scanned UI label path.
    return isConnectableName(advertisedName) && scannedOk
}

📏 Severity: critical


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@@ -178,7 +182,7 @@ fun BulkWeightAdjustDialog(
// Compute preview
val preview by remember(currentMode, exercises) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: remember(currentMode, exercises) does not list hardwareModel as a key, even though the closure on line 185 calls applyBulkAdjust(exercises, mode, hardwareModel). If a user opens the dialog on a V-Form, swaps to a Trainer+ mid-dialog, and then changes nothing else, the preview silently keeps the V-Form 100 kg ceiling. Compose is forgiving until it isn't, and this is the "isn't" day.

🩹 The Fix: Add hardwareModel to the key list:

Suggested change
val preview by remember(currentMode, exercises) {
val preview by remember(currentMode, exercises, hardwareModel) {
derivedStateOf {
currentMode?.let { mode -> applyBulkAdjust(exercises, mode, hardwareModel) }
}
}

📏 Severity: important


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

model: PhoenixModel,
unlimitedReps: Boolean,
): Float {
if (!requestedKg.isFinite()) return 0f

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: if (!requestedKg.isFinite()) return 0f silently turns NaN/Infinity into 0.0 and goes on to write that as the encoded progression byte. BlePacketFactory.createProgramParams calls this before validateProgramParams runs on the packet, so a NaN slipping in here never hits the validator and you get a 0 kg/rep progression encoded on the wire. The contract for "silent rewrite to 0" should be reserved for documented business logic, not "well, validator usually catches it."

🩹 The Fix: Either throw (let the validator fail the packet cleanly), or coerceIn to a finite range and add an explicit Logger.w so field debugging sees the clamp. Do not return a value that silently diverges from what the caller requested.

📏 Severity: important


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


// Trainer+ hardware ceiling — used by UI sliders to enforce absolute maximum
const val MAX_WEIGHT_PER_CABLE_KG = 110f
const val MAX_WEIGHT_KG = ChassisLimits.V_FORM_KG_PER_CABLE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: MAX_WEIGHT_KG = ChassisLimits.V_FORM_KG_PER_CABLE and MAX_WEIGHT_PER_CABLE_KG = ChassisLimits.TRAINER_PLUS_KG_PER_CABLE are now lying about themselves. MAX_WEIGHT_KG reads as "the system-wide max" but equals 100; MAX_WEIGHT_PER_CABLE_KG reads as "the per-cable max" but is hard-wired to Trainer+ 110. AssessmentEngine.kt:51 and MonitorDataProcessor.kt:307-310 (unchanged) still consume these as if they were authoritative, so a V-Form user with a 100 kg program is now correctly capped but the variable name has become a lie that future maintainers will trust. That is how you get a 220 kg regression in six months.

🩹 The Fix: Either delete these aliases and migrate callers to ChassisLimits.maxKgPerCable(model) (preferred — there is exactly one absolute-physical-ceiling concept, and it belongs in ChassisLimits), or rename them to V_FORM_FAIL_CLOSED_KG / TRAINER_PLUS_ABSOLUTE_CEILING_KG so the names match the values.

📏 Severity: minor


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

* [maxResistanceKg] is the per-cable chassis ceiling from [ChassisLimits].
*/
data class HardwareCapabilities(val supportsEccentricMode: Boolean, val supportsEchoMode: Boolean, val maxResistanceKg: Float) {
companion object {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: HardwareCapabilities.DEFAULT is the lonely walrus of this file — defined, fully populated, and never read by anything in the repo. getCapabilities constructs fresh instances per call. Dead config is dead config, regardless of how thoughtfully it was written.

🩹 The Fix: Delete lines 54-60 (the companion object { val DEFAULT = ... } block). If a default is ever needed, getCapabilities("") already returns the correct fail-closed shape via detectModel.

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

object ChassisLimits {
const val V_FORM_KG_PER_CABLE = 100f
const val TRAINER_PLUS_KG_PER_CABLE = 110f
const val UNKNOWN_FAIL_CLOSED_KG_PER_CABLE = V_FORM_KG_PER_CABLE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: UNKNOWN_FAIL_CLOSED_KG_PER_CABLE = V_FORM_KG_PER_CABLE is a synonym with a hat on. There is one constant (V_FORM_KG_PER_CABLE = 100f), one alias of that constant, and exactly one call site (maxKgPerCable, line 30). The alias adds a hop in the reader's head and zero semantics. Naming the value "unknown fail-closed" when it is literally just "V-Form" is the kind of indirection that makes future maintainers think there is a policy where there isn't.

🩹 The Fix: Inline at the single call site and delete the alias:

Suggested change
const val UNKNOWN_FAIL_CLOSED_KG_PER_CABLE = V_FORM_KG_PER_CABLE
fun maxKgPerCable(model: PhoenixModel): Float = when (model) {
PhoenixModel.TrainerPlus -> TRAINER_PLUS_KG_PER_CABLE
PhoenixModel.VFormTrainer,
PhoenixModel.Unknown,
-> V_FORM_KG_PER_CABLE
}

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


fun maxKgPerCable(model: PhoenixModel): Float = when (model) {
PhoenixModel.TrainerPlus -> TRAINER_PLUS_KG_PER_CABLE
PhoenixModel.VFormTrainer,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: Trailing comma followed by -> on its own line is a Kotlin formatter's nightmare. kotlinx.serialization and ktfmt will both normalize this on the next format run, producing a noisy diff churn for zero readability gain. If you wanted a comma-first list you wrote Kotlin; if you wanted ktlint to leave you alone, put it on one line.

🩹 The Fix:

Suggested change
PhoenixModel.VFormTrainer,
fun maxKgPerCable(model: PhoenixModel): Float = when (model) {
PhoenixModel.TrainerPlus -> TRAINER_PLUS_KG_PER_CABLE
PhoenixModel.VFormTrainer, PhoenixModel.Unknown -> V_FORM_KG_PER_CABLE
}

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this incremental commit actually closes the critical/important findings from the previous review. I need to sit down. I had my flamethrower warmed up and everything, and the author fixed the exact three things I asked them to fix.

Fixes verified against a4508172

  • BleAdvertisementFilter.mayConnectWithAdvertisementIdentity now requires storedAdvertisementIsVisibleOnly = true before the last-successful-identifier opt-in can authorize a null-name stored ad. The previous critical seam defect — a null-name ad sliding through on identifier alone — is closed, with BleAdvertisementFilterTest covering both branches (line 142 rejects without the flag, line 151 passes with it).
  • KableBleConnectionManager.connect computes the visible-only flag from the actual stored advertisement (UUIDs + FEF3 service data) and threads it through — no chance of a caller wiring it incorrectly.
  • BulkWeightAdjustDialog remember(currentMode, exercises, hardwareModel) — preview no longer goes stale on a mid-dialog chassis swap.
  • ChassisLimits.finiteRepProgressionKg now throws IllegalArgumentException on non-finite input instead of silently rewriting NaN/Inf to 0; WorkoutCommandValidatorTest.finite progression rejects non-finite input covers NaN, +∞, -∞.

🏆 Best part: The author didn't just rubber-stamp a one-line band-aid. The BLE fix is asymmetric on purpose — scannedAllowed stays permissive (a stale scan label shouldn't block a valid stored ad) while advertisedAllowed gates the stored identity behind visible-only evidence. That's exactly the seam the original review said was broken.

💀 Worst part: Nothing fatal. There's still a UNUSED-flavored constant in the same area and a ktfmt-comma-style when arm that the formatter will churn next CI run, but those are unchanged lines from the previous review and out of scope for this incremental pass.

Overall: Like a sequel that actually listened to the critics — shorter, tighter, and the third act doesn't accidentally re-open the door the second act closed.

Files Reviewed (6 files, incremental diff vs 755524b)
  • shared/.../BleAdvertisementFilter.kt — 1 fix verified
  • shared/.../KableBleConnectionManager.kt — 1 fix verified
  • shared/.../BulkWeightAdjustDialog.kt — 1 fix verified
  • shared/.../ChassisLimits.kt — 1 fix verified
  • shared/.../BleAdvertisementFilterTest.kt — coverage added
  • shared/.../WorkoutCommandValidatorTest.kt — coverage added
Previous Review Summary (commit 755524b)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 755524b)

Verdict: 7 Issues Found | Recommendation: Address the critical finding before merge

Overview

Severity Count
🚨 critical 1
⚠️ important 2
💡 suggestion 0
🤏 nitpick 3
Issue Details (click to expand)
File Line Roast
shared/.../BleAdvertisementFilter.kt 70 mayConnectWithAdvertisementIdentity lets a null-name stored ad pass via lastSuccessfulIdentifier opt-in — defeats the authoritative-identity guarantee.
shared/.../BulkWeightAdjustDialog.kt 183 remember(currentMode, exercises) omits hardwareModel; preview goes stale on a mid-dialog chassis swap.
shared/.../ChassisLimits.kt 49 !requestedKg.isFinite() → 0f silently rewrites NaN/Infinity to 0 before the validator can reject the packet.
shared/.../Constants.kt 18 MAX_WEIGHT_KG/MAX_WEIGHT_PER_CABLE_KG now alias specific chassis ceilings — name lies about the value.
shared/.../HardwareDetection.kt 54 HardwareCapabilities.DEFAULT is dead — no callers. Delete.
shared/.../ChassisLimits.kt 20 UNKNOWN_FAIL_CLOSED_KG_PER_CABLE is a synonym for V_FORM_KG_PER_CABLE. Inline.
shared/.../ChassisLimits.kt 28 Trailing-comma-then-->-on-newline is non-idiomatic Kotlin and will churn on the next ktfmt run.

🏆 Best part: The fail-closed pattern (Unknown → V-Form 100 kg) is consistently threaded through WorkoutCommandValidator, BlePacketFactory.forceMaxKg, ApplyEquipmentRackLoadUseCase, recommendations, rack load, drop-set resolver, UI steppers, and ASE. The chassis abstraction lives in exactly one place (ChassisLimits) and every screen reads from it. Whoever designed the seam deserves a coffee.

💀 Worst part: BleAdvertisementFilter.mayConnectWithAdvertisementIdentity is the gate between user-tapped label and a CONFIG packet — and it lets advertisedName == null slide on a lastSuccessfulIdentifier match alone. That's the one finding here that can write bad CONFIG bytes to a cable.

📊 Overall: A consolidation this big almost always carries one subtle seam defect, and this PR's seam defect is in BLE identity. Fix line 70 of BleAdvertisementFilter.kt and the rest is gravy.

Ponytail Pass (mandatory)
  • shared/.../ChassisLimits.kt:20 — ponytail-shrink: UNKNOWN_FAIL_CLOSED_KG_PER_CABLE = V_FORM_KG_PER_CABLE is a single-use alias. Replace with V_FORM_KG_PER_CABLE directly.
  • shared/.../HardwareDetection.kt:54-60 — ponytail-delete: HardwareCapabilities.DEFAULT is unreferenced. Delete the companion.
  • shared/.../ChassisLimits.kt:28-30 — ponytail-shrink: reformat when to one line per Kotlin idiom; ktfmt will churn otherwise.
  • shared/.../BulkWeightAdjustDialog.kt:183 — already covered above as correctness.

No correctness-required validations, security checks, tests, or logging were touched by Ponytail.

Ponytail net: -8 lines (3 constants, 1 dead companion, 1 minor reformat).

Files Reviewed (44 files)
  • BLE: BleAdvertisementFilter.kt, KableBleConnectionManager.kt, BleConstants.kt, HardwareDetection.kt
  • Core limits: ChassisLimits.kt, Constants.kt, WorkoutCommandValidator.kt, BlePacketFactory.kt, WeightRecommendation.kt
  • Use cases: ApplyEquipmentRackLoadUseCase.kt, DropSetCandidateResolver.kt, DropSetEligibilityPolicy.kt, RecommendWeightAdjustmentUseCase.kt
  • Managers: ActiveSessionEngine.kt, DefaultWorkoutSessionManager.kt, RoutineFlowManager.kt, ActiveSessionEngineChassisLimitHostTest.kt
  • UI: BulkWeightAdjustDialog.kt, ExerciseConfigModal.kt, WeightStepper.kt, WorkoutTabPreviews.kt
  • Screens: ExerciseEditBottomSheet.kt, JustLiftScreen.kt, ModeConfirmationScreen.kt, RestTimerCard.kt, RoutineEditorScreen.kt, RoutineOverviewScreen.kt, SetReadyScreen.kt, SingleExerciseScreen.kt, TrainingCyclesScreen.kt, WorkoutTab.kt
  • Tests: 9 test files (BLE, factory, validator, use-case, dialog, host)

Fix these issues in Kilo Cloud

Suggested Minimal Patch

  1. BleAdvertisementFilter.kt:70 — require isConnectableName(advertisedName) independently of the opt-in, or remove the opt-in from the mayConnect(advertisedName, …) call. The "stored advertisement is authoritative" contract is the whole reason this function exists.
  2. BulkWeightAdjustDialog.kt:183 — add hardwareModel to remember(currentMode, exercises, hardwareModel).
  3. ChassisLimits.kt:49 — replace if (!requestedKg.isFinite()) return 0f with throw IllegalArgumentException(...) or a logged coerceIn; do not silently rewrite to 0.
  4. ChassisLimits.kt:20, HardwareDetection.kt:54-60, ChassisLimits.kt:28-30 — Ponytail cleanups, optional.

Final Merge Guidance

Do not merge until the BleAdvertisementFilter.mayConnectWithAdvertisementIdentity null-name path is closed. The other two important findings should be addressed in the same patch. Ponytail items are optional cleanup.


Reviewed by minimax-m3 · Input: 33.2K · Output: 8.2K · Cached: 874.1K

Review guidance: REVIEW.md from base branch main

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a450817207

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

) {
val weightSuffix = if (weightUnit == WeightUnit.LB) "lbs" else "kg"
val maxWeight = if (weightUnit == WeightUnit.LB) 242f else 110f
val maxWeight = ChassisLimits.maxDisplay(hardwareModel, weightUnit)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp the retained Just Lift weight when the chassis changes

When a profile has a saved Trainer+ weight above 100 kg and this screen loads while disconnected or later connects to a V-Form, this only narrows the picker's range; weightPerCable remains, for example, 105 kg and the parameter-publishing effect is not keyed on hardwareModel. The picker renders its closest in-range value (100 kg), but ActiveSessionEngine subsequently rejects the retained 105 kg direct command, so starting fails despite the UI showing a valid weight until the user manually moves the picker. Clamp/synchronize the backing weight whenever the chassis maximum changes.

Useful? React with 👍 / 👎.

@9thLevelSoftware
9thLevelSoftware deleted the consolidate/brownfield-8951e31b-01-08-chassis-ble branch September 4, 2026 20:59
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.

3 participants