Issue #673 — Drop Sets on Failure (user-prompted) Implementation Plan
Authoritative replacement plan. The earlier automatic deload/cross-set proposal, closed PR #682, and branch enhancement/drop-set-old-school-673 are superseded. For Hermes: implement this plan as three sequential PRs (see “Phasing”). PR 1 and PR 2 are prerequisites for PR 3 but ship independent value. This changes set-advancement behavior, so verify normal routine advancement, superset interleaving, and variable warm-up sets remain unchanged.
GitHub issue: #673
Superseded branch: enhancement/drop-set-old-school-673
PR scope: replace the automatic deload-driven drop set with a user-prompted, rest-screen drop set that restarts the failed set.
Goal
When a set ends because the user failed (stall detection fired), offer them a drop set during the rest screen:
- Prompt: “Retry this set with a drop set?”
- If yes, offer negative percentage options (e.g. −10% / −20% / −30%).
- Restart the set that failed at the reduced weight — not the next set, not the next exercise.
- Remaining sets of that same exercise inherit the reduced weight.
- Original programming resumes at the next exercise. The saved routine is never modified.
Background: why the previous approach was abandoned
The first implementation (commits cb93489f..4e606cbe) made drop-set mode replace stall detection and reduce weight automatically on DELOAD_OCCURRED. This is not feasible, for a reason now confirmed from three directions:
- The machine will not accept new set parameters mid-set.
ActiveSessionEngine.kt:4847-4855 documents that a CONFIG frame sent mid-movement de-energises the cable (“the user perceives as ‘the set deloads’”). The user has independently observed this failing repeatedly in testing. Load can only change at a stop/start boundary.
- The old design wasn’t a drop set anyway. It stashed
dropSetNextWeightKg and applied it at the next set boundary — that is auto-regression between sets, not a drop set.
- Firmware progression/regression cannot express it. The
increment field is applied per rep, linearly, open-loop from rep 1. It is blind to failure and produces a continuously decaying ramp, whereas a drop set is a fixed lower weight for a block of reps. There is no cadence field in the protocol to change this (see Appendix A).
The new design accepts the stop/start constraint rather than fighting it. Restarting a set is the only way to express a real drop set on this hardware.
Decisions (settled with maintainer)
| # |
Decision |
Resolution |
| 1 |
Set numbering |
Replay — re-enter the same (exerciseIndex, setIndex). Add attemptNumber to CompletedSet. |
| 2 |
Supersets |
Non-issue by construction — coordinates never move, so getNextStep resumes correctly. |
| 3 |
Echo mode |
Excluded — no prompt, no override. |
| 4 |
Re-failure |
Re-prompt, capped at 2 drops per set; dropSetMinWeightKg is a hard floor. |
| 5 |
% of PR |
Store the override as a multiplier, never a resolved kg — composes with all weight sources. |
| 6 |
Process death |
WorkoutServiceSnapshot carries the failure point and override. |
| 7 |
Remaining-set weight after multiple drops |
Final attempted weight (cumulative multiplier), not the first drop level. |
Current state verified in repo
Set completion / advancement
presentation/manager/ActiveSessionEngine.kt
handleSetCompletion() (:3975) — takes no reason parameter; there is currently no way to know why a set ended.
showBodyweightRepEntry branch (:4026-4035) — existing precedent for pausing completion awaiting user input (clears setCompletionInProgress, returns early).
startRestTimer() (:4420+) — captures completedSetIndex (:4426), pre-loads next-set params (:4487-4499). Does not mutate the set/exercise indices.
startNextSetOrExercise() (:4821+) — mutates indices at :4866-4867; weight-precedence ladder at :4877-4891.
advanceToNextSetInSingleExercise() — parallel ladder at :4741-4764.
startWorkoutOrSetReady() (:4784) — respects the autoplay preference; correct entry point for a restart.
presentation/manager/RoutineFlowManager.kt
getNextStep() (:350) — superset interleaving (A1→B1→A2→B2).
- Index mutation sites during rest:
enterSetReady* (:1051-1052, :1141-1142), skip-to-exercise (:1415-1416).
presentation/manager/WorkoutCoordinator.kt
_userAdjustedWeightDuringRest (:294) — the closest existing analogue: an ephemeral, session-scoped weight override.
- Drop-set scratch state (:433-439) — to be removed.
- Stall constants (:63-85).
Stall detection
data/ble/MonitorDataProcessor.kt (:257-272) — parses and logs DELOAD_WARN / SPOTTER_ACTIVE, but only DELOAD_OCCURRED is surfaced.
data/repository/BleRepository.kt:143 — deloadOccurredEvents: Flow<Unit>; the status word is discarded at this boundary.
domain/model/SampleStatus.kt — all 8 firmware flags parsed; bit definitions confirmed correct against the official app.
docs/audits/2026-07-14-stall-detection-e2e-audit.md — F5 (hysteresis dead band) remains open.
To be removed / corrected
ActiveSessionEngine.kt:456-473 — deload→auto-reduce block.
RoutineFlowManager.kt:107-114 — progressionForSetReady bypasses the ±3kg clamp for drop-set mode.
util/BlePacketFactory.kt:257 — zeroes negative progression; the sole guard preventing an out-of-range increment reaching the wire.
util/BleConstants.kt:96-99 — offsets 0x58/0x5C are misnamed (see Appendix A).
Architecture
1. Set end reason (PR 1)
enum class SetEndReason {
TARGET_REPS_REACHED, STALL_FAILURE, CABLE_RELEASED,
USER_STOPPED, VBT_AUTO_END, TIMER_EXPIRED,
}
Thread through handleSetCompletion(reason: SetEndReason), park on the coordinator for the rest screen, and persist on CompletedSet. Valuable for analytics independent of drop sets (“which sets did I fail”).
2. Failure point snapshot (PR 3)
Captured at handleSetCompletion time — not read live during rest, because skip/navigation paths can move the indices.
data class SetFailurePoint(
val exerciseIndex: Int,
val setIndex: Int,
val routineExerciseId: String, // assert on re-entry; index alone is fragile
val originalWeightPerCableKg: Float,
val attemptNumber: Int,
val warmupSetIndex: Int, // >= 0 means warm-up set — do not prompt
)
3. Exercise load override (PR 3)
data class ExerciseLoadOverride(
val exerciseIndex: Int,
val multiplier: Float, // cumulative across drops, e.g. 0.8 then 0.64
val dropCount: Int, // cap at 2
)
Held on WorkoutCoordinator. Multiplier, never resolved kg — composes with flat weights, per-set weights, and %-of-PR resolution alike.
Requires no eager clearing: it is only consulted when resolving weight for exerciseIndex, so a superset partner never matches it. Clear on routine end / workout reset only.
Refactor first: four near-duplicate weight-precedence ladders exist (startRestTimer :4487, startNextSetOrExercise :4877, advanceToNextSetInSingleExercise :4741, enterSetReadyWithAdjustments). Extract a single resolveNextSetWeight() before adding a fourth precedence tier — audit finding F7 documents how divergent hand-maintained copies of the same logic have already caused bugs here.
4. Restart is a re-entry, not an advance (PR 3)
The critical architectural point. The retry must not route through getNextStep:
restartSetWithDrop(percent):
restore indices from SetFailurePoint (assert routineExerciseId matches)
override = ExerciseLoadOverride(exerciseIndex, cumulativeMultiplier, dropCount + 1)
weight = originalWeightPerCableKg * cumulativeMultiplier, floored at dropSetMinWeightKg
repCounter.resetCountsOnly(); resetAutoStopState()
startWorkoutOrSetReady() // respects autoplay
Because the coordinates never move, when the retried set completes, getNextStep(exIdx, setIdx) resumes exactly as if the failure had not happened — A1 fails → retry A1 → completes → getNextStep returns B1. Superset interleaving never learns a retry occurred.
progressionRegressionKg is untouched by this feature. A restarted set applies whatever increment the exercise has configured, ramping from the lower base.
5. Prompt (PR 3)
Shown during WorkoutState.Resting so the failed set still saves and the rest timer still runs. Gated on:
reason == STALL_FAILURE && exercise.dropSetEnabled && programMode is OldSchool && !isEchoMode && warmupSetIndex < 0 && dropCount < 2 && weight > dropSetMinWeightKg
Autoplay must be suspended while the prompt is open, or the rest timer will advance out from under the user mid-decision.
6. Schema (PR 3)
- Add
attemptNumber to CompletedSet (default 1). Without it, replay produces two indistinguishable rows at the same (exerciseIndex, setIndex) and set-count math reads 4 for a 3-set exercise.
- Keep and repurpose the already-shipped
dropSetEnabled / dropSetMinWeightKg columns (migration 43, SchemaManifest, sync repo, backup, DTOs): dropSetEnabled becomes “offer the drop-set prompt on failure”, dropSetMinWeightKg stays the floor. This preserves the migration and portal parity work.
WorkoutServiceSnapshot (DefaultWorkoutSessionManager.kt:564) carries SetFailurePoint + ExerciseLoadOverride. Silently losing the override mid-exercise would resume remaining sets at full weight — a surprise heavy set, the worst failure mode.
7. Stall detection rework (PR 2)
The prompt is only as good as the failure signal, and the current detector is unreliable. Two changes:
a. Widen the event to carry the status word. deloadOccurredEvents: Flow<Unit> → Flow<SampleStatus> (or a MachineStatusEvent). The flags are already parsed; only the plumbing discards them.
b. Add the geometric ROM-fraction signal. The official app derived rep position from cable position as a fraction of calibrated ROM, not velocity (Appendix A). Phoenix uses velocity thresholds with position only as a crude > 10mm gate. A user grinding to failure is stuck at a characteristic fraction of their ROM — structurally different from a completed rep at the top or racked handles at the bottom. This addresses audit F5 (the 2.5–10 mm/s dead band where a countdown can neither arm nor cancel) at the root: position answers where they are stuck when velocity cannot answer whether they are stuck.
DELOAD_WARN may be useful as a leading indicator but has no reference implementation (Appendix A) — treat any use of it as novel and validate on hardware before relying on it.
8. Cleanup (PR 1)
- Rename
OFFSET_TARGET_WEIGHT → OFFSET_SOFT_MAX, OFFSET_PROGRESSION → OFFSET_INCREMENT (BleConstants.kt:96-99), matching the official field names. “Progression” invited the per-rep/per-set ambiguity that produced the overloaded field.
- Enforce the firmware validation bounds Phoenix currently ignores:
softMax <= 100.0f and increment <= 10.0f. The drop-set path currently bypasses the ±3kg clamp (RoutineFlowManager.kt:107-114), so a −20kg config produces an increment of −20 — double the firmware bound, applied per rep. Only BlePacketFactory.kt:257 stops it reaching the wire.
- Delete the deload→auto-reduce block (
ActiveSessionEngine.kt:456-473), dropSetNextWeightKg and its consumption ladders, the coordinator scratch state, and the progressionRegressionKg overload.
Phasing
| PR |
Scope |
Independent value |
| 1 |
SetEndReason plumbing + protocol cleanup + remove old drop-set logic |
Set-end reasons in history; closes the increment overrun |
| 2 |
Status-word flow + geometric ROM-fraction stall detection |
Fixes “sets randomly ending”; closes audit F5 |
| 3 |
Prompt + SetFailurePoint + ExerciseLoadOverride + attemptNumber |
The feature |
Testing
- Restart lands on the same
(exerciseIndex, setIndex); getNextStep afterwards returns the same step it would have without the retry.
- Superset A1 fails → retry A1 → next step is B1; B’s weights unmodified.
- Override applies to remaining sets of the overridden exercise and to no other exercise.
- Cumulative multiplier after two drops; third prompt suppressed; floor respected.
- Echo exercises never prompt.
- Warm-up set failure never prompts.
- %-of-PR exercise: override composes multiplicatively.
- Autoplay ON: rest timer does not advance while the prompt is open.
- Process death during rest with a pending prompt, and mid-exercise with an active override.
- Regression: normal advancement, variable warm-up sets, single-exercise mode, bodyweight rep entry.
Non-goals
- Mid-set weight changes. Confirmed impossible; do not attempt a 0x4F force-only frame mid-set.
- Firmware-native drop sets via negative
increment. It is a per-rep open-loop ramp, not a drop set.
- Changing progression/regression semantics or the saved routine.
Appendix A — Protocol findings from the deobfuscated official app
Source: 9thLevelSoftware/deob, cloned at ../deob (sibling of this repo).
A1. The official app parses but never acts on the deload flags. In Yj/p$j.smali — the only consumer of the flag enum in the entire APK — all eight flags are read but only REP_TOP_READY, REP_BOTTOM_READY and ROM_OUTSIDE_LOW reach the return value. ROM_OUTSIDE_HIGH, ROM_UNLOAD_ACTIVE, SPOTTER_ACTIVE, DELOAD_WARN and DELOAD_OCCURRED are invoked with no move-result — their values are discarded at bytecode level, so this is neither a decompiler artifact nor dead-code elimination.
Implication: Phoenix’s use of DELOAD_OCCURRED to drive auto-stop is an invention, not a port. There is no reference implementation for failure detection to validate against — consistent with it being hit-or-miss.
A2. Rep position was derived geometrically. The official app compares max(left.position, right.position) against rangeTop − pct·(rangeTop − rangeBottom) and rangeBottom + pct·(...), with per-exercise configurable fractions. Position, not velocity.
A3. Full activation packet layout (Ek/N.java, Ek/C1505b.java, Ek/O.java, Ek/M.java, Ek/L.java):
ActivationPacket (0x04):
RepConfig(
repCounts: RepCounts(total: byte, baseline: byte = 3, adaptive: byte = 3, pad = 0),
seedRange: float,
top: RepBandConfig(threshold = 5.0, drift = 0.0, inner: RepBand, outer: RepBand),
bottom: RepBandConfig(threshold = 5.0, drift = 0.0, inner: RepBand, outer: RepBand),
safety: RepBand(mmPerM: short, mmMax: short))
ActivationForceConfig(
concentric: Ramp, eccentric: Ramp,
forces: ClosedRange<Float>,
softMax: float, // validated <= 100.0 ("Soft max is too high!")
increment: float) // validated <= 10.0 ("Increment is too high!")
Serialization order of the trailing block is forces.min, forces.max, softMax, increment, which maps onto Phoenix’s offsets as:
| Offset |
Phoenix name |
Official name |
| 0x50 |
forceMin |
forces.min ✅ |
| 0x54 |
forceMax |
forces.max ✅ |
| 0x58 |
targetWeight |
softMax |
| 0x5C |
progression |
increment |
A4. No cadence field exists. The only load-stepping parameter in the whole protocol is the single increment float; RegularPacket (0x4F) likewise carries one progression float. The per-rep firing rate is firmware-internal and not exposed. A fixed lower weight for a block of reps therefore requires a stop/restart with increment = 0.
A5. RepCounts(total, baseline = 3, adaptive = 3) — the 3 firmware calibration reps are a protocol-level constant, retroactively justifying Constants.DEFAULT_WARMUP_REPS = 3 (kept deliberately per Issue #411).
Caveat: BLE_PROTOCOL_DEEP_DIVE.md in that repo is partly AI-generated narrative. Its “When Sent” claims (e.g. that RegularPacket adjusts resistance mid-exercise) are unverified and contradicted by field observation. Only the decompiled sources and smali cited above are treated as evidence here.
Issue #673 — Drop Sets on Failure (user-prompted) Implementation Plan
GitHub issue: #673
Superseded branch:
enhancement/drop-set-old-school-673PR scope: replace the automatic deload-driven drop set with a user-prompted, rest-screen drop set that restarts the failed set.
Goal
When a set ends because the user failed (stall detection fired), offer them a drop set during the rest screen:
Background: why the previous approach was abandoned
The first implementation (commits
cb93489f..4e606cbe) made drop-set mode replace stall detection and reduce weight automatically onDELOAD_OCCURRED. This is not feasible, for a reason now confirmed from three directions:ActiveSessionEngine.kt:4847-4855documents that a CONFIG frame sent mid-movement de-energises the cable (“the user perceives as ‘the set deloads’”). The user has independently observed this failing repeatedly in testing. Load can only change at a stop/start boundary.dropSetNextWeightKgand applied it at the next set boundary — that is auto-regression between sets, not a drop set.incrementfield is applied per rep, linearly, open-loop from rep 1. It is blind to failure and produces a continuously decaying ramp, whereas a drop set is a fixed lower weight for a block of reps. There is no cadence field in the protocol to change this (see Appendix A).The new design accepts the stop/start constraint rather than fighting it. Restarting a set is the only way to express a real drop set on this hardware.
Decisions (settled with maintainer)
(exerciseIndex, setIndex). AddattemptNumbertoCompletedSet.getNextStepresumes correctly.dropSetMinWeightKgis a hard floor.WorkoutServiceSnapshotcarries the failure point and override.Current state verified in repo
Set completion / advancement
presentation/manager/ActiveSessionEngine.kthandleSetCompletion()(:3975) — takes no reason parameter; there is currently no way to know why a set ended.showBodyweightRepEntrybranch (:4026-4035) — existing precedent for pausing completion awaiting user input (clearssetCompletionInProgress, returns early).startRestTimer()(:4420+) — capturescompletedSetIndex(:4426), pre-loads next-set params (:4487-4499). Does not mutate the set/exercise indices.startNextSetOrExercise()(:4821+) — mutates indices at :4866-4867; weight-precedence ladder at :4877-4891.advanceToNextSetInSingleExercise()— parallel ladder at :4741-4764.startWorkoutOrSetReady()(:4784) — respects the autoplay preference; correct entry point for a restart.presentation/manager/RoutineFlowManager.ktgetNextStep()(:350) — superset interleaving (A1→B1→A2→B2).enterSetReady*(:1051-1052, :1141-1142), skip-to-exercise (:1415-1416).presentation/manager/WorkoutCoordinator.kt_userAdjustedWeightDuringRest(:294) — the closest existing analogue: an ephemeral, session-scoped weight override.Stall detection
data/ble/MonitorDataProcessor.kt(:257-272) — parses and logsDELOAD_WARN/SPOTTER_ACTIVE, but onlyDELOAD_OCCURREDis surfaced.data/repository/BleRepository.kt:143—deloadOccurredEvents: Flow<Unit>; the status word is discarded at this boundary.domain/model/SampleStatus.kt— all 8 firmware flags parsed; bit definitions confirmed correct against the official app.docs/audits/2026-07-14-stall-detection-e2e-audit.md— F5 (hysteresis dead band) remains open.To be removed / corrected
ActiveSessionEngine.kt:456-473— deload→auto-reduce block.RoutineFlowManager.kt:107-114—progressionForSetReadybypasses the ±3kg clamp for drop-set mode.util/BlePacketFactory.kt:257— zeroes negative progression; the sole guard preventing an out-of-rangeincrementreaching the wire.util/BleConstants.kt:96-99— offsets 0x58/0x5C are misnamed (see Appendix A).Architecture
1. Set end reason (PR 1)
Thread through
handleSetCompletion(reason: SetEndReason), park on the coordinator for the rest screen, and persist onCompletedSet. Valuable for analytics independent of drop sets (“which sets did I fail”).2. Failure point snapshot (PR 3)
Captured at
handleSetCompletiontime — not read live during rest, because skip/navigation paths can move the indices.3. Exercise load override (PR 3)
Held on
WorkoutCoordinator. Multiplier, never resolved kg — composes with flat weights, per-set weights, and %-of-PR resolution alike.Requires no eager clearing: it is only consulted when resolving weight for
exerciseIndex, so a superset partner never matches it. Clear on routine end / workout reset only.Refactor first: four near-duplicate weight-precedence ladders exist (
startRestTimer:4487,startNextSetOrExercise:4877,advanceToNextSetInSingleExercise:4741,enterSetReadyWithAdjustments). Extract a singleresolveNextSetWeight()before adding a fourth precedence tier — audit finding F7 documents how divergent hand-maintained copies of the same logic have already caused bugs here.4. Restart is a re-entry, not an advance (PR 3)
The critical architectural point. The retry must not route through
getNextStep:Because the coordinates never move, when the retried set completes,
getNextStep(exIdx, setIdx)resumes exactly as if the failure had not happened — A1 fails → retry A1 → completes →getNextStepreturns B1. Superset interleaving never learns a retry occurred.progressionRegressionKgis untouched by this feature. A restarted set applies whatever increment the exercise has configured, ramping from the lower base.5. Prompt (PR 3)
Shown during
WorkoutState.Restingso the failed set still saves and the rest timer still runs. Gated on:reason == STALL_FAILURE && exercise.dropSetEnabled && programMode is OldSchool && !isEchoMode && warmupSetIndex < 0 && dropCount < 2 && weight > dropSetMinWeightKgAutoplay must be suspended while the prompt is open, or the rest timer will advance out from under the user mid-decision.
6. Schema (PR 3)
attemptNumbertoCompletedSet(default 1). Without it, replay produces two indistinguishable rows at the same(exerciseIndex, setIndex)and set-count math reads 4 for a 3-set exercise.dropSetEnabled/dropSetMinWeightKgcolumns (migration 43,SchemaManifest, sync repo, backup, DTOs):dropSetEnabledbecomes “offer the drop-set prompt on failure”,dropSetMinWeightKgstays the floor. This preserves the migration and portal parity work.WorkoutServiceSnapshot(DefaultWorkoutSessionManager.kt:564) carriesSetFailurePoint+ExerciseLoadOverride. Silently losing the override mid-exercise would resume remaining sets at full weight — a surprise heavy set, the worst failure mode.7. Stall detection rework (PR 2)
The prompt is only as good as the failure signal, and the current detector is unreliable. Two changes:
a. Widen the event to carry the status word.
deloadOccurredEvents: Flow<Unit>→Flow<SampleStatus>(or aMachineStatusEvent). The flags are already parsed; only the plumbing discards them.b. Add the geometric ROM-fraction signal. The official app derived rep position from cable position as a fraction of calibrated ROM, not velocity (Appendix A). Phoenix uses velocity thresholds with position only as a crude
> 10mmgate. A user grinding to failure is stuck at a characteristic fraction of their ROM — structurally different from a completed rep at the top or racked handles at the bottom. This addresses audit F5 (the 2.5–10 mm/s dead band where a countdown can neither arm nor cancel) at the root: position answers where they are stuck when velocity cannot answer whether they are stuck.DELOAD_WARNmay be useful as a leading indicator but has no reference implementation (Appendix A) — treat any use of it as novel and validate on hardware before relying on it.8. Cleanup (PR 1)
OFFSET_TARGET_WEIGHT→OFFSET_SOFT_MAX,OFFSET_PROGRESSION→OFFSET_INCREMENT(BleConstants.kt:96-99), matching the official field names. “Progression” invited the per-rep/per-set ambiguity that produced the overloaded field.softMax <= 100.0fandincrement <= 10.0f. The drop-set path currently bypasses the ±3kg clamp (RoutineFlowManager.kt:107-114), so a −20kg config produces anincrementof −20 — double the firmware bound, applied per rep. OnlyBlePacketFactory.kt:257stops it reaching the wire.ActiveSessionEngine.kt:456-473),dropSetNextWeightKgand its consumption ladders, the coordinator scratch state, and theprogressionRegressionKgoverload.Phasing
SetEndReasonplumbing + protocol cleanup + remove old drop-set logicincrementoverrunSetFailurePoint+ExerciseLoadOverride+attemptNumberTesting
(exerciseIndex, setIndex);getNextStepafterwards returns the same step it would have without the retry.Non-goals
increment. It is a per-rep open-loop ramp, not a drop set.Appendix A — Protocol findings from the deobfuscated official app
Source:
9thLevelSoftware/deob, cloned at../deob(sibling of this repo).A1. The official app parses but never acts on the deload flags. In
Yj/p$j.smali— the only consumer of the flag enum in the entire APK — all eight flags are read but onlyREP_TOP_READY,REP_BOTTOM_READYandROM_OUTSIDE_LOWreach the return value.ROM_OUTSIDE_HIGH,ROM_UNLOAD_ACTIVE,SPOTTER_ACTIVE,DELOAD_WARNandDELOAD_OCCURREDare invoked with nomove-result— their values are discarded at bytecode level, so this is neither a decompiler artifact nor dead-code elimination.Implication: Phoenix’s use of
DELOAD_OCCURREDto drive auto-stop is an invention, not a port. There is no reference implementation for failure detection to validate against — consistent with it being hit-or-miss.A2. Rep position was derived geometrically. The official app compares
max(left.position, right.position)againstrangeTop − pct·(rangeTop − rangeBottom)andrangeBottom + pct·(...), with per-exercise configurable fractions. Position, not velocity.A3. Full activation packet layout (
Ek/N.java,Ek/C1505b.java,Ek/O.java,Ek/M.java,Ek/L.java):Serialization order of the trailing block is
forces.min, forces.max, softMax, increment, which maps onto Phoenix’s offsets as:forces.min✅forces.max✅softMaxincrementA4. No cadence field exists. The only load-stepping parameter in the whole protocol is the single
incrementfloat;RegularPacket(0x4F) likewise carries oneprogressionfloat. The per-rep firing rate is firmware-internal and not exposed. A fixed lower weight for a block of reps therefore requires a stop/restart withincrement = 0.A5.
RepCounts(total, baseline = 3, adaptive = 3)— the 3 firmware calibration reps are a protocol-level constant, retroactively justifyingConstants.DEFAULT_WARMUP_REPS = 3(kept deliberately per Issue #411).Caveat:
BLE_PROTOCOL_DEEP_DIVE.mdin that repo is partly AI-generated narrative. Its “When Sent” claims (e.g. thatRegularPacketadjusts resistance mid-exercise) are unverified and contradicted by field observation. Only the decompiled sources and smali cited above are treated as evidence here.