Files
hexapod-robot/2026-current/sim/HexapodSimApi.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

162 lines
6.1 KiB
C++

#include "HexapodSimApi.h"
#include "SimHardware.h"
#include "SimRobotAccess.h"
#include "SimBody.h"
#include "../robot/Config.h" // geometry and pins as the firmware sees them
#include <HexapodProtocol.h>
static_assert(NUM_LEGS == HXSIM_NUM_LEGS, "HxGeometry arrays are sized for 6 legs");
// The real Teensy spins loop() far faster than this; 250 us keeps the
// control-rate jitter under 2% at 60 Hz while staying cheap.
static constexpr uint32_t HXSIM_LOOP_PERIOD_US = 250;
static bool g_started = false;
static uint32_t g_control_ticks = 0;
static bool g_manual_imu = false; // hxsim_set_imu() called: stop feeding the IMU from the body
static uint64_t g_last_body_us = 0;
// After each control tick: move the kinematic body to the new feet, and feed
// the simulated MPU6050 what it would measure on that body.
static void bodyStep() {
float feet[18];
hxsim_get_feet_mm(feet);
uint64_t now = simhw::nowMicros();
simbody::update(feet, (now - g_last_body_us) / 1e6f);
g_last_body_us = now;
if (!g_manual_imu) {
float a[3], g[3];
simbody::imu(a, g);
simhw::imuSet(a[0], a[1], a[2], g[0], g[1], g[2]);
}
}
HXSIM_API int32_t hxsim_api_version(void) { return HXSIM_API_VERSION; }
HXSIM_API int32_t hxsim_packet_size(void) { return (int32_t)sizeof(CmdPacket); }
HXSIM_API void hxsim_get_geometry(HxGeometry* g) {
g->num_legs = NUM_LEGS;
for (int i = 0; i < NUM_LEGS; i++) {
g->mount_x_mm[i] = LEG_MOUNT_X_MM[i];
g->mount_y_mm[i] = LEG_MOUNT_Y_MM[i];
g->mount_angle_deg[i] = LEG_MOUNT_ANGLE_DEG[i];
}
g->coxa_mm = COXA_LEN_MM;
g->femur_mm = FEMUR_LEN_MM;
g->tibia_mm = TIBIA_LEN_MM;
g->stance_radius_mm = STANCE_RADIUS_MM;
g->stance_height_mm = STANCE_HEIGHT_MM;
g->coxa_min_deg = COXA_MIN_DEG; g->coxa_max_deg = COXA_MAX_DEG;
g->femur_min_deg = FEMUR_MIN_DEG; g->femur_max_deg = FEMUR_MAX_DEG;
g->tibia_min_deg = TIBIA_MIN_DEG; g->tibia_max_deg = TIBIA_MAX_DEG;
g->control_hz = CONTROL_HZ;
g->servo_us_at_0deg = SERVO_US_AT_0DEG;
g->servo_us_per_deg = SERVO_US_PER_DEG;
g->leg_clearance_mm = LEG_CLEARANCE_MM;
g->stance_height_range_mm = STANCE_HEIGHT_RANGE_MM;
}
HXSIM_API void hxsim_set_radio_present(int32_t present) { simhw::radioSetPresent(present != 0); }
HXSIM_API void hxsim_set_imu_present(int32_t present) { simhw::imuSetPresent(present != 0); }
HXSIM_API void hxsim_start(void) {
if (g_started) return;
g_started = true;
simhw::imuSet(0, 0, 1, 0, 0, 0); // level and still until the body model says otherwise
simbody::reset();
simrobot::setupOnce();
bodyStep();
}
HXSIM_API void hxsim_step(uint32_t dt_us) {
if (!g_started) hxsim_start();
while (dt_us > 0) {
uint32_t chunk = dt_us < HXSIM_LOOP_PERIOD_US ? dt_us : HXSIM_LOOP_PERIOD_US;
simhw::advanceMicros(chunk);
dt_us -= chunk;
uint32_t before = simrobot::lastLoopMicros();
simrobot::loopOnce();
if (simrobot::lastLoopMicros() != before) {
g_control_ticks++;
bodyStep();
}
}
}
HXSIM_API void hxsim_radio_send(const uint8_t* bytes, int32_t len) { simhw::radioSend(bytes, len); }
HXSIM_API void hxsim_set_imu(float ax, float ay, float az, float gx, float gy, float gz) {
g_manual_imu = true;
simhw::imuSet(ax, ay, az, gx, gy, gz);
}
HXSIM_API void hxsim_set_ground_tilt(float roll_deg, float pitch_deg) { simbody::setGroundTilt(roll_deg, pitch_deg); }
HXSIM_API void hxsim_get_body(float* pos_mm, float* quat_wxyz, float* yaw_deg, int32_t* contact_mask) {
if (pos_mm) simbody::position(pos_mm);
if (quat_wxyz) simbody::orientation(quat_wxyz);
if (yaw_deg) *yaw_deg = simbody::yawDeg();
if (contact_mask) *contact_mask = simbody::contactMask();
}
HXSIM_API void hxsim_serial_input(const char* bytes, int32_t len) { simhw::serialFeed(bytes, len); }
HXSIM_API int32_t hxsim_serial_output(char* out, int32_t cap) { return simhw::serialDrain(out, cap); }
HXSIM_API void hxsim_get_servos(int32_t* us, int32_t* attached, uint32_t* writes) {
for (int i = 0; i < NUM_LEGS; i++)
for (int j = 0; j < 3; j++) {
const simhw::ServoPin& p = simhw::servoPin(SERVO_PINS[i][j]);
if (us) us[i * 3 + j] = p.us;
if (attached) attached[i * 3 + j] = p.attached ? 1 : 0;
if (writes) writes[i * 3 + j] = p.writes;
}
}
// Inverse of ServoDriver::degToMicros - what angle the firmware meant.
static float pulseToDeg(int us) {
return (us - SERVO_US_AT_0DEG) / (float)SERVO_US_PER_DEG;
}
HXSIM_API void hxsim_get_joints_deg(float* deg) {
for (int i = 0; i < NUM_LEGS; i++)
for (int j = 0; j < 3; j++)
deg[i * 3 + j] = pulseToDeg(simhw::servoPin(SERVO_PINS[i][j]).us);
}
// Forward kinematics, written independently of robot/Kinematics.cpp so it
// can catch IK mistakes. Joint convention (the one Config.h's limits imply):
// coxa - CCW from the leg's mount angle, viewed from above
// femur - elevation above horizontal, + = up
// tibia - relative to the femur line, + = up, so a knee-down leg is negative
HXSIM_API void hxsim_get_feet_mm(float* xyz) {
float deg[NUM_LEGS * 3];
hxsim_get_joints_deg(deg);
for (int i = 0; i < NUM_LEGS; i++) {
float a = radians(LEG_MOUNT_ANGLE_DEG[i] + deg[i * 3 + 0]);
float f = radians(deg[i * 3 + 1]);
float t = radians(deg[i * 3 + 1] + deg[i * 3 + 2]);
float horiz = COXA_LEN_MM + FEMUR_LEN_MM * cosf(f) + TIBIA_LEN_MM * cosf(t);
xyz[i * 3 + 0] = LEG_MOUNT_X_MM[i] + horiz * cosf(a);
xyz[i * 3 + 1] = LEG_MOUNT_Y_MM[i] + horiz * sinf(a);
xyz[i * 3 + 2] = FEMUR_LEN_MM * sinf(f) + TIBIA_LEN_MM * sinf(t);
}
}
HXSIM_API void hxsim_get_status(HxStatus* s) {
s->robot_state = simrobot::state();
s->sit_stand_blend = simrobot::sitStandBlend();
s->link_ok = simrobot::linkOk() ? 1 : 0;
s->heading_deg = simrobot::headingDeg();
s->imu_roll_deg = simrobot::imuRollDeg();
s->imu_pitch_deg = simrobot::imuPitchDeg();
s->imu_enabled = simrobot::imuEnabled() ? 1 : 0;
s->sim_time_ms = (uint32_t)(simhw::nowMicros() / 1000);
s->control_ticks = g_control_ticks;
s->pose_reach_scale = simrobot::poseReachScale();
s->twist_scale = simrobot::twistScale();
s->stance_height_mm = simrobot::stanceHeightMm();
s->stance_mask = 0;
for (int i = 0; i < NUM_LEGS; i++)
if (simrobot::legInStance(i)) s->stance_mask |= 1 << i;
}