Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,4 +1,5 @@
_todo.md
.vscode/*

## Claude stuff
.claude*
Expand Down
42 changes: 35 additions & 7 deletions Devices/MBoosterDeviceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ public sealed class MBoosterDeviceController : IDisposable
// (mbooster-brake-threshold); -1 until it answers. Live only, never
// persisted and never copied into MBoosterDeviceSettings.MaxThresholdKg:
// that field's -1 means "user set no override", and seeding it would make
// the plugin start writing the value back on every connect. Used purely as
// ApplyDeadzoneAndMaxForce's fullScaleKg reference in place of the old
// hardcoded 200kg guess. Volatile: written on the serial read thread, read
// by the HID thread and the UI.
// the plugin start writing the value back on every connect. Surfaced in
// Diagnostics (see DiagnosticsTextBuilder) as a read-back sanity check.
// Volatile: written on the serial read thread, read by the HID thread
// and the UI.
private volatile float _deviceReportedMaxThresholdKg = -1;
public float DeviceReportedMaxThresholdKg
{
Expand Down Expand Up @@ -1188,9 +1188,16 @@ public bool SendFloatWrite(string commandName, float value, byte? device = null)
/// omitting it is what made Travel Start/End silently no-op on
/// hardware despite the raw register write reading back fine — see
/// MozaCommandDatabase.cs's mbooster-brake-curve7-* comment. Callers
/// use this after any of Travel/Endstop/Ratio/Threshold's own writes
/// on the theory that the same firmware requirement applies to all of
/// them, not just Travel — unconfirmed for the others.
/// use this after Direction/Min/Max/CurveY/Endstop/Friction/
/// SegmentedDamping/Ratio's own writes too, on the theory that the
/// same firmware requirement applies to all of them, not just
/// Travel — unconfirmed for those. CONFIRMED NOT required for Max
/// Threshold or Deadzone/Max Force specifically: isolated captures
/// for both (max-threshold-4-41-105-153-200.pcapng,
/// max-force-24-75-128-166-200.pcapng, deadzone-0-5-11-14.pcapng)
/// show zero curve7-1..6 traffic alongside their real writes — see
/// MozaPlugin.ApplyMBoosterToHardware's needsCurve7Resync and
/// MBoosterDeviceController.PushFeelCurveResync.
/// </summary>
public void PushCurve7Resync(float[]? curveX, float[]? curveY, byte device)
{
Expand All @@ -1199,6 +1206,27 @@ public void PushCurve7Resync(float[]? curveX, float[]? curveY, byte device)
SendIntWrite($"mbooster-brake-curve7-{i + 1}", MozaMBoosterProtocol.EncodeCurve7Point(curve7[i]), device);
}

/// <summary>
/// Write Deadzone, Max Force, and the 6 interpolated points between
/// them (cmdId 0xAB selectors 0x07-0x0E) as one atomic burst — CONFIRMED
/// real hardware calibration, reverse-engineered from
/// max-force-24-75-128-166-200.pcapng and deadzone-0-5-11-14.pcapng
/// (bug bundle 5VR5AQ8Y): every Deadzone or Max Force change in both
/// captures resent the whole 8-value family together, not just the
/// field that moved — same "no partial update" shape as Segmented
/// Damping. Both values use the identical kg encoding as Max
/// Threshold (<see cref="MozaMBoosterProtocol.EncodeThresholdKg"/>).
/// See <see cref="MozaMBoosterRegistry.ComputeFeelCurve"/>.
/// </summary>
public void PushFeelCurveResync(double deadzoneKg, double maxForceKg, byte device)
{
SendIntWrite("mbooster-brake-deadzone", MozaMBoosterProtocol.EncodeThresholdKg(deadzoneKg), device);
var mid = MozaMBoosterRegistry.ComputeFeelCurve(deadzoneKg, maxForceKg);
for (int i = 0; i < mid.Length; i++)
SendIntWrite($"mbooster-brake-feelcurve-{i + 1}", MozaMBoosterProtocol.EncodeThresholdKg(mid[i]), device);
SendIntWrite("mbooster-brake-maxforce", MozaMBoosterProtocol.EncodeThresholdKg(maxForceKg), device);
}

// ── Coalescing gate for UI-driven calibration writes ──
// A slider raises ValueChanged per tick, and every one of these commands
// is a flash-backed calibration register that additionally drags a
Expand Down
66 changes: 37 additions & 29 deletions Devices/MBoosterTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,10 +463,12 @@ public sealed class MBoosterPedalSettings : IMBoosterPedalConfig
public float SensorOutputRatioPct { get; set; } = -1;
public float MaxThresholdKg { get; set; } = -1;

// Pedal Feel (host-side shaping + brake-only wire calibration).
// Pedal Feel (InputCurveY is host-side shaping; Deadzone/MaxForce are
// real brake-only wire calibration — see MBoosterDeviceSettings for
// the field semantics).
public float[]? InputCurveY { get; set; } = null;
public float DeadzoneKg { get; set; } = 0;
public float MaxForceKg { get; set; } = 200;
public float DeadzoneKg { get; set; } = -1;
public float MaxForceKg { get; set; } = -1;
public float TravelStartMm { get; set; } = -1;
public float TravelEndMm { get; set; } = -1;
public float EndstopFrontStiffness { get; set; } = -1;
Expand Down Expand Up @@ -621,8 +623,13 @@ public sealed class MBoosterDeviceSettings : IMBoosterPedalConfig
// load-cell force to reach 100%, capped at
// MBoosterUiConstants.BrakeFadeMaxThresholdKg — this is what
// actually makes the pedal feel "softer" (more effort needed for
// the same signal), unlike the host-side-only MaxForceKg, which
// has no wire command and wouldn't affect what the game receives.
// the same signal). MaxForceKg is now also a real wire calibration
// (see docs/protocol/devices/mbooster.md "Pedal Feel"), but Brake
// Fade deliberately still ramps MaxThresholdKg, not MaxForceKg:
// Threshold rescales the sensor's own 0-100% span, while Max Force
// only lowers the effort needed below whatever that span already
// is — ramping it couldn't demand MORE force than Threshold
// already caps at, so it can't reproduce "harder to press."
// Both restore to their configured values as brake temp cools. If
// the user has never configured a given base value, that ONE
// calibration stays fully inert (the other can still ramp
Expand Down Expand Up @@ -694,28 +701,29 @@ public sealed class MBoosterDeviceSettings : IMBoosterPedalConfig
// docs/protocol/devices/mbooster.md "Pedal Feel".
public float[]? InputCurveY { get; set; } = null;

// Deadzone at the start of pedal travel, in kg of force (0..40).
// Host-side only, applied before InputCurveY (a physical/sensor
// characteristic — the resting force before the load cell means
// anything — should shape the signal before the user's "feel"
// curve does). See MozaMBoosterRegistry.ApplyDeadzoneAndMaxForce.
// 0 = off (default).
public float DeadzoneKg { get; set; } = 0;

// Force (kg, 0..200) at which the Pedal Feel input curve's X-axis
// reaches 100%. Host-side only. Raw 0-100% pedal travel isn't a
// fixed 0-200kg scale — 100% raw is whatever MaxThresholdKg (Sim
// Input Mapping) currently calibrates the device itself to reach
// 100% at (200kg is only a fallback guess when MaxThresholdKg is
// still -1/unset — see MozaMBoosterRegistry.ApplyDeadzoneAndMaxForce).
// 200 = off IF the device's real threshold is also 200kg; if it's
// lower (real Pit House captures commonly show ~100-125kg), 200
// has no additional effect beyond whatever the device already
// saturates at, since there's no headroom above the device's own
// calibrated max for software to require more force. Lower it if
// you never press hard enough to reach the curve's right edge
// otherwise.
public float MaxForceKg { get; set; } = 200;
// Deadzone at the start of pedal travel, in kg of force (0..40) —
// REAL hardware calibration (wire command mbooster-brake-deadzone,
// cmdId 0xAB selector 0x07), reverse-engineered from
// deadzone-0-5-11-14.pcapng (bug bundle 5VR5AQ8Y). Same kg encoding
// as MaxThresholdKg — see MozaMBoosterProtocol.EncodeThresholdKg and
// MBoosterDeviceController.PushFeelCurveResync. -1 = "not yet set /
// no override", same sentinel convention as every other real
// calibration field, so a fresh profile never overwrites whatever
// the device already has. Previously host-side-only (0 = off
// default); see docs/protocol/devices/mbooster.md "Pedal Feel".
public float DeadzoneKg { get; set; } = -1;

// Force (kg, 0..200) at which the pedal's raw HID axis reaches
// 100% travel — REAL hardware calibration (wire command
// mbooster-brake-maxforce, cmdId 0xAB selector 0x0E), reverse-
// engineered from max-force-24-75-128-166-200.pcapng (bug bundle
// 5VR5AQ8Y). Same kg encoding as MaxThresholdKg. Confirmed NOT
// clamped to MaxThresholdKg on the wire (128/166kg sent while
// Threshold read back 125kg) — it's an independent parameter, not
// a rescale of Threshold's own ceiling. -1 = "not yet set / no
// override". Previously host-side-only (200 = off default); see
// docs/protocol/devices/mbooster.md "Pedal Feel".
public float MaxForceKg { get; set; } = -1;

// Start/End of pedal travel, in mm (Pit House's own calibration
// control, not a host-side shim). Reverse-engineered from two real
Expand Down Expand Up @@ -745,8 +753,8 @@ public sealed class MBoosterDeviceSettings : IMBoosterPedalConfig
public float EndstopEndStiffness { get; set; } = -1;

// Natural Friction (Pit House-style), 0-100%. Real hardware write
// (not host-side-only like Deadzone/MaxForce) — reverse-engineered
// from two real Pit House USB captures (a toggle on/off, and a
// (like Deadzone/MaxForce above) — reverse-engineered from two
// real Pit House USB captures (a toggle on/off, and a
// 0/25/50/75/100% slider sweep): wire commands
// mbooster-brake-friction-0/-1 (cmdId 0xAE with a selector byte,
// always written together with the same value), 2-byte int, fixed
Expand Down
Loading