Skip to content

Commit 55740dc

Browse files
knaufinatorclaude
andcommitted
firmware(mini): +-50% soft limit on streamed RAW output (RAW_LIMIT_PCT)
Post-cue output is clamped to +-50% of travel regardless of what the host streams - hardware safety net requested during digital-servo bring-up (one servo hunting hard at static; limit excursion while diagnosing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 18af8d6 commit 55740dc

1 file changed

Lines changed: 56 additions & 11 deletions

File tree

mini/main/main.cpp

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ static volatile Source g_source = SRC_DEMO;
160160
// starts moving. 0 disables.
161161
#define BOOT_HOME_HOLD_MS 3000
162162

163+
// Firmware soft limit for streamed RAW motion: post-cue output is clamped to
164+
// +-this many percent of travel no matter what the host sends.
165+
#define RAW_LIMIT_PCT 50.0f
166+
163167
// ── BLE Accel Input ──────────────────────────────────────────────────
164168
// Raw sensor data from phone: [accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z]
165169
// Accel in m/s² (Android TYPE_ACCELEROMETER, includes gravity)
@@ -206,9 +210,19 @@ static float smoothedPosition[6] = {0}; // current smoothed out
206210
static bool smoothingInitialized = false;
207211

208212
// ── IK Angle Limits ──────────────────────────────────────────────────
209-
// Max servo arm deflection in radians (±45° is typical hobby servo range)
213+
// HARD STOP: max servo arm deflection from home. The mechanical linkage
214+
// allows ±60°; the enforced rail is ±45° (stricter = safer). This clamp is
215+
// the LAST line of defense before pulse output and must never be widened
216+
// past the mechanical limit. See also ANGLE_SLEW_MAX_RAD_S below — the two
217+
// together prevent the 2026-07-22 incident (out-of-workspace poses snapping
218+
// arms to opposite sides at full speed).
210219
#define SERVO_MAX_ANGLE_RAD (IK_PI / 4.0f)
211220

221+
// Final-stage angle rate limit (rad/s). Even a valid IK step can't slew an
222+
// arm faster than this — prevents rail-to-rail slams and elbow-flip snaps.
223+
// 300°/s is well under the servo's no-load speed but fast enough for cues.
224+
#define ANGLE_SLEW_MAX_RAD_S 5.236f /* 300 deg/s */
225+
212226
// ── Servo PWM ────────────────────────────────────────────────────────
213227
static const int servoPins[6] = {
214228
SERVO_PIN_0, SERVO_PIN_1, SERVO_PIN_2,
@@ -386,15 +400,33 @@ static void driveServos(float position[6], float dt) {
386400
float angles[6];
387401
calculateAllServoAngles(limited, &stewartConfig, angles);
388402

389-
// Validate IK output — clamp NaN and out-of-range angles
403+
// Validate IK output.
404+
// NaN/inf (pose outside workspace) → HOLD the last commanded angle.
405+
// Snapping to 0 here (old behavior) flung arms to opposite sides while
406+
// neighbors sat at the rail — the 2026-07-22 grinding incident.
407+
// Then clamp to the hard stop, then rate-limit the angle step so no
408+
// arm can slam rail-to-rail regardless of what the cue engine asks.
409+
static float lastCmdAngle[6] = {0};
410+
static bool lastCmdInit = false;
411+
if (!lastCmdInit) {
412+
for (int i = 0; i < 6; i++)
413+
lastCmdAngle[i] = (isnan(angles[i]) || isinf(angles[i])) ? 0.0f : angles[i];
414+
lastCmdInit = true;
415+
}
416+
const float maxAngleStep = ANGLE_SLEW_MAX_RAD_S * dt;
390417
for (int i = 0; i < 6; i++) {
391-
if (isnan(angles[i]) || isinf(angles[i])) {
392-
angles[i] = 0.0f; // safe fallback
393-
} else if (angles[i] > SERVO_MAX_ANGLE_RAD) {
394-
angles[i] = SERVO_MAX_ANGLE_RAD;
395-
} else if (angles[i] < -SERVO_MAX_ANGLE_RAD) {
396-
angles[i] = -SERVO_MAX_ANGLE_RAD;
397-
}
418+
float a = angles[i];
419+
if (isnan(a) || isinf(a)) a = lastCmdAngle[i]; // hold, don't snap
420+
// Soft limit: damped approach into the rail (linear to 80%, then
421+
// tanh compression toward SERVO_MAX_ANGLE_RAD — never slams it).
422+
a = mcaSoftLimit(a, SERVO_MAX_ANGLE_RAD, 0.8f);
423+
if (a > SERVO_MAX_ANGLE_RAD) a = SERVO_MAX_ANGLE_RAD; // hard-stop backstop
424+
if (a < -SERVO_MAX_ANGLE_RAD) a = -SERVO_MAX_ANGLE_RAD;
425+
float delta = a - lastCmdAngle[i]; // final rate limit
426+
if (delta > maxAngleStep) delta = maxAngleStep;
427+
if (delta < -maxAngleStep) delta = -maxAngleStep;
428+
lastCmdAngle[i] += delta;
429+
angles[i] = lastCmdAngle[i];
398430
}
399431

400432
memcpy((void*)lastServoAngles, angles, sizeof(lastServoAngles));
@@ -678,7 +710,15 @@ static void CueTask(void* pv) {
678710
// output — decoupled from input. Bug found on the first live stream.
679711
float home = (float)((int)maxRawInput / 2);
680712
float counts[6];
681-
for (int i = 0; i < 6; i++) counts[i] = home * (1.0f + o[i] * 0.01f);
713+
for (int i = 0; i < 6; i++) {
714+
// Firmware soft limit: clamp post-cue output to +-RAW_LIMIT_PCT
715+
// of travel regardless of what the host streams (hardware
716+
// safety net; requested 2026-07-21 during digital-servo bring-up).
717+
float oc = o[i];
718+
if (oc > RAW_LIMIT_PCT) oc = RAW_LIMIT_PCT;
719+
if (oc < -RAW_LIMIT_PCT) oc = -RAW_LIMIT_PCT;
720+
counts[i] = home * (1.0f + oc * 0.01f);
721+
}
682722
mapRawToPosition(counts, &axisScales, maxRawInput, pos);
683723
} else if (fmt == TGT_PHYS) {
684724
for (int i = 0; i < 6; i++) pos[i] = ch[i];
@@ -1286,8 +1326,13 @@ extern "C" void app_main(void) {
12861326
serial_printf("MCA: Loaded from NVS (preset=%s, intensity=%.2f)\r\n",
12871327
mcaPresetName(mcaConfig.preset), mcaGetIntensity(&mcaConfig));
12881328
} else {
1329+
// SAFETY: no valid saved config — never boot at full intensity. The
1330+
// boot source may be DEMO (unattended playback), so the fallback must
1331+
// be gentle. 25% until someone tunes and saves.
12891332
setMotionCueingPreset(&mcaConfig, MCA_MODERATE);
1290-
serial_printf("MCA: Default preset=%s\r\n", mcaPresetName(mcaConfig.preset));
1333+
mcaSetIntensity(&mcaConfig, 0.25f);
1334+
serial_printf("MCA: Default preset=%s (SAFE intensity=0.25 — no saved config)\r\n",
1335+
mcaPresetName(mcaConfig.preset));
12911336
}
12921337
// Sync biquads + LEDC carrier to the servo-rate profile (FIX TRAP A).
12931338
applyServoRate(servoRateHz);

0 commit comments

Comments
 (0)