The post below is in Portuguese. We will translate critical announcements as the project gains an English-speaking audience.
Launch control without wheelie protection is incomplete. Here's how Pistonix does it.
On a high-torque V-twin, releasing the 2-step into 1st gear at WOT lifts the front wheel. Wheelie protection is part of launch control, not a follow-up. The controller landed in our closed-source firmware and there's an interactive demo at /playground.
There’s an essential difference between car and motorcycle when it comes to launch control. On a car, the worst that happens is the engine cuts and the ET gets worse. On a high-torque V-twin — Twin Cam 96+, Milwaukee-Eight, any custom S&S 124+ — releasing the 2-step into 1st gear at WOT lifts the front wheel. Without protection, launch control that’s supposed to improve the ET becomes a liability.
That’s why, on Pistonix, launch control and wheelie protection are the same feature. There’s no “pure” launch control on the roadmap waiting for a later phase to gain protection. They ship together in the firmware.
What landed
This round shipped three related things, all in the closed-source firmware:
1. WheelieProtectionConfig on the LaunchController. Declarative
configuration: pitch threshold (where intervention starts), pitch max
(where it’s saturated), front/rear wheel-speed ratio threshold (proxy
when IMU isn’t trusted alone), and a response mode — Off,
SparkRetard, FuelCut, or Progressive (ramps retard up to 80%
severity, then fuel cut at saturation). Constants ready: STREET
(intervention early, soft), RACE (drag accepts more front in the
air before cutting), OFF (validation / drag-only with reinforced
front).
2. Severity comes from two sources, OR’d. Pitch IMU is the
direct signal (“front is rising”). Front/rear wheel-speed ratio is
the indirect signal (“front stopped spinning while rear keeps going”
= front airborne OR slipping). The controller uses
max(severity_pitch, severity_ratio) — either one fires. At standstill
/ initial hookup (below min_speed_for_ratio_kph), the ratio check
is suppressed to avoid false positives while the bike hasn’t even
gained speed yet.
3. Backwards compat preserved. The legacy
LaunchController.update() is now a wrapper for
update_with_dynamics(.., RideDynamics::SAFE). Anything in the
firmware today without IMU/wheel-speed sensors (Phase 0-3 hardware)
sees zero severity by construction — protection sleeps until there’s
data to act on.
let mut lc = LaunchController::new(LaunchControlConfig::TWIN_CAM_STREET);
let dynamics = RideDynamics {
pitch_deg: 8.0, // 43% of street range (5..12°)
front_wheel_speed_kph: 30.0,
rear_wheel_speed_kph: 30.0,
};
let out = lc.update_with_dynamics(95.0, 4500.0, 1, false, false, dynamics);
match out.wheelie {
WheelieIntervention::SparkRetardDeg(deg) => /* ~8° retard */,
WheelieIntervention::FuelCut => /* saturated severity */,
WheelieIntervention::None => /* dormant */,
}
15 tests cover the controller — including “legacy never fires”, “below threshold inert”, “ratio suppressed at standstill”, “Off never acts”, “safety override silences”, and the ramping + escalation paths to fuel cut.
Why this logic and not “spark cut, done”
Binary spark cut is the most common path in aftermarket because it’s easy — when pitch crosses N°, kill the spark. But it has two problems:
- Drivetrain shock. Cutting everything at once under WOT in gear rocks the powertrain. On a V-twin with belt primary, that shows up as transmission vibration and final chain stress.
- Controlled wheelies exist. A drag racer who knows what they’re doing wants the bike rising at a controlled angle, not the front upside down on the line. Cutting at 5° kills the launch.
The solution is progressive: spark retard ramps from 0° up to
spark_max_deg linearly with severity until 80%. Then, in the final
80..100% range, it escalates to fuel cut. Spark retard reduces power
without cutting abruptly — the bike decelerates the angle, the rider
feels the chassis pushback, adjusts body weight. Fuel cut only kicks
in when spark retard alone won’t stabilize.
These numbers (5°, 12°, 80%) aren’t guesses: they came from reading papers + comparing with systems that exist (Ducati DTC, BMW DBC). They’re starting points; they’ll be refined on track when Phase 4 hardware is installed and we have real data.
What’s missing — IMU + wheel-speed
The controller is ready. But it won’t fire on its own without the physical sensors. Phase 0 hardware (Proteus F7 standalone) doesn’t have an onboard IMU or wheel-speed sensor inputs. Phase 4 is where they enter:
- 6-axis IMU on the chassis — real-time pitch reading.
- Front/rear wheel-speed sensors — independent proxy from the IMU (defense in depth: if IMU lies, ratio catches it).
Until then, launch control ships with a track-only disclaimer in
all race builds. It’s not a street product until protection is
complete. The /compatibility page reflects this, and inside
/playground?build=race there’s an orange banner explaining the
boundary.
Interactive demo
And since the controller landed, it’s exposed in /playground. Pick
build = “race”, click Trigger wheelie test — a 3-second animation
ramps the IMU pitch from 0° to 18° and back. The severity bar fills
green → orange → red. The intervention text changes from — to
Spark retard 7.3° ramping, and when saturated, Fuel cut.
If you want to take the trace home via “Download CSV”, you’ll get
three new columns: wheelie_severity, wheelie_spark_retard_deg,
wheelie_fuel_cut. Same data structure the ECU will send to the
datalogger, just computed in your browser.
Why this matters for the product
The constraint that motivated this round came from the owner in
conversation: “It’s a motorcycle — launch control can’t let it
wheelie.” That’s the kind of detail that separates “product that
says it’s safety-conscious” from “product that proves it.” Inside
the code, I left a SAFETY block at the top of
ecu-firmware/core/src/launch/mod.rs pointing to the project memory,
so anyone touching the module in the future understands wheelie
protection isn’t optional.
Confidence on a motorcycle is built in thousands of small decisions like this. This is one of them, recorded in the code where it needs to be.