Files
hexapod-robot/2026-current/robot/Limits.cpp
T
mangavalkandClaude Opus 5.5 935526eab5 Firmware simulation, Unity project, and sim-verified robot fixes
Simulation
- sim/: the unchanged robot firmware compiled into hexapod_sim.dll
  against PC stand-ins (Arduino, Servo, Wire + register-level MPU6050,
  RF24 with matching-config radio), a kinematic body model (support
  plane + planted-foot odometry) that also drives the simulated IMU,
  and a headless smoke test (sim_smoke). build.ps1 builds and tests.
- Hexapod_Simulation_Unity/ folded in (history kept in a bundle
  outside the repo). Assets/HexapodSim loads the DLL, drives the
  existing leg rig, and offers joystick / keyboard / robot-serial
  input plus a test panel.

Robot (all found or confirmed in the simulation)
- Config.h geometry taken from the Unity model.
- IK femur elevation sign, gait stance direction, body pose
  rotation direction, yaw lever arm.
- Stance feet integrated from the current command; gait restarts at
  mid-stance; heading integrates the applied turn rate, only while
  walking.
- Leg clearance and reach checks (Limits::feetFeasible); walk command
  and body pose scaled as a whole, pose scale rate-limited, planted
  feet guarded so the body never snaps or legs collide.
- Robot-gyro self-leveling as an integrator.
- Heading mode: steering relative to the body; S walks backward.
- Standing height from the remote (STANCE_HEIGHT_RANGE_MM).

Protocol v3 (reflash both boards)
- CmdPacket gains body_height (22 bytes).

Remote
- Height slider (Nextion hHeight/nHeight, serial [ ]).
- Fixed unspecified I2C read order in RemoteIMU.

LEGACY/: owner's old Nextion HMI and remote hardware assets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:37:12 +02:00

113 lines
4.1 KiB
C++

#include "Limits.h"
#include "Config.h"
#include <Arduino.h>
#include <math.h>
static constexpr float MAX_HORIZ_MM = 70.0f; // tune to your real leg reach, with margin
static constexpr float MAX_VERT_MM = 60.0f;
bool Limits::inWorkspace(int i, const FootTarget& target) {
FootTarget neutral = Kinematics::neutralStance(i);
float dx = target.x - neutral.x;
float dy = target.y - neutral.y;
float dz = target.z - neutral.z;
return dx * dx + dy * dy <= MAX_HORIZ_MM * MAX_HORIZ_MM && fabs(dz) <= MAX_VERT_MM;
}
FootTarget Limits::clampWorkspace(int i, const FootTarget& target) {
FootTarget neutral = Kinematics::neutralStance(i);
float dx = target.x - neutral.x;
float dy = target.y - neutral.y;
float dz = target.z - neutral.z;
float horiz = sqrt(dx * dx + dy * dy);
if (horiz > MAX_HORIZ_MM) {
float scale = MAX_HORIZ_MM / horiz;
dx *= scale;
dy *= scale;
}
dz = constrain(dz, -MAX_VERT_MM, MAX_VERT_MM);
return { neutral.x + dx, neutral.y + dy, neutral.z + dz };
}
// Neighbouring legs: along each side, plus the front and rear pairs.
static const uint8_t NEIGHBOURS[][2] = { {0, 1}, {1, 2}, {3, 4}, {4, 5}, {0, 5}, {2, 3} };
static FootTarget sub(const FootTarget& a, const FootTarget& b) { return { a.x - b.x, a.y - b.y, a.z - b.z }; }
static float dot(const FootTarget& a, const FootTarget& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
// Shortest distance between segments p1-q1 and p2-q2 (Ericson, Real-Time
// Collision Detection, 5.1.9).
static float segmentDistance(const FootTarget& p1, const FootTarget& q1,
const FootTarget& p2, const FootTarget& q2) {
FootTarget d1 = sub(q1, p1), d2 = sub(q2, p2), r = sub(p1, p2);
float a = dot(d1, d1), e = dot(d2, d2), f = dot(d2, r);
float s = 0, t = 0;
constexpr float EPS = 1e-6f;
if (a > EPS || e > EPS) {
if (a <= EPS) {
t = constrain(f / e, 0.0f, 1.0f);
} else {
float c = dot(d1, r);
if (e <= EPS) {
s = constrain(-c / a, 0.0f, 1.0f);
} else {
float b = dot(d1, d2);
float denom = a * e - b * b;
s = denom > EPS ? constrain((b * f - c * e) / denom, 0.0f, 1.0f) : 0.0f;
t = (b * s + f) / e;
if (t < 0) { t = 0; s = constrain(-c / a, 0.0f, 1.0f); }
else if (t > 1) { t = 1; s = constrain((b - c) / a, 0.0f, 1.0f); }
}
}
}
FootTarget c1 = { p1.x + d1.x * s, p1.y + d1.y * s, p1.z + d1.z * s };
FootTarget c2 = { p2.x + d2.x * t, p2.y + d2.y * t, p2.z + d2.z * t };
FootTarget d = sub(c1, c2);
return sqrt(dot(d, d));
}
// Joint positions of all legs for these feet; false if any leg can't reach.
static bool solveAll(const FootTarget feet[6], FootTarget joints[6][4], bool check_workspace) {
for (int i = 0; i < 6; i++) {
if (check_workspace && !Limits::inWorkspace(i, feet[i])) return false;
JointAngles a = Kinematics::solve(i, feet[i]);
if (!a.valid) return false;
Kinematics::jointPositions(i, a, joints[i]);
}
return true;
}
static float minClearance(const FootTarget joints[6][4]) {
float best = 1e9f;
for (const auto& n : NEIGHBOURS)
for (int s1 = 0; s1 < 3; s1++) // segments: coxa, femur, tibia
for (int s2 = 0; s2 < 3; s2++)
best = min(best, segmentDistance(joints[n[0]][s1], joints[n[0]][s1 + 1],
joints[n[1]][s2], joints[n[1]][s2 + 1]));
return best;
}
bool Limits::feetFeasible(const FootTarget feet[6]) {
FootTarget joints[6][4];
return solveAll(feet, joints, true) && minClearance(joints) >= LEG_CLEARANCE_MM;
}
float Limits::legClearance(const FootTarget feet[6]) {
FootTarget joints[6][4];
return solveAll(feet, joints, false) ? minClearance(joints) : -1e9f;
}
float Limits::clampJointRate(float current_deg, float target_deg, float max_deg_per_sec, float dt) {
float max_step = max_deg_per_sec * dt;
float delta = constrain(target_deg - current_deg, -max_step, max_step);
return current_deg + delta;
}
bool Limits::stancePolygonOk(const bool in_stance[6]) {
int count = 0;
for (int i = 0; i < 6; i++) if (in_stance[i]) count++;
return count >= 3;
}