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>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-09-29 00:37:12 +02:00
1 parent 91dac16fca
commit 935526eab5
145 files changed
+431258 -114

No files matched your search

+3
View File
@@ -93,6 +93,9 @@ constexpr float REMOTE_TILT_FADE_PER_SEC = 2.0f;
constexpr uint8_t SPEED_SCALE_DEFAULT = 128;
constexpr float SPEED_SCALE_RATE_PER_SEC = 255.0f; // slider jumps ramp over <= 1 s (robot doesn't smooth twist)
constexpr uint8_t SPEED_SCALE_STEP = 25; // serial console +/- step
// Standing height slider -> CmdPacket.body_height (-1000..1000, 0 = the
// robot's default). No smoothing here: the robot rate-limits height itself.
constexpr int16_t HEIGHT_STEP = 100; // serial console [ / ] step
// FLAG_WALK stays set this long after the left stick returns to center, so
// hovering at the deadzone edge doesn't chatter STAND <-> WALKING.
constexpr uint32_t WALK_LINGER_MS = 250;
+8
View File
@@ -1,5 +1,10 @@
# Nextion HMI build guide
> The owner's earlier 6-page HMI is in `LEGACY/remote/remote.HMI`. This
> guide describes the minimal single page the current firmware expects.
> To reuse the old HMI instead, `NextionUI` has to be changed to match
> its component names.
The firmware drives the display by **component name**, and the display
talks back with small custom frames sent from each button's touch event.
Build one page in Nextion Editor to match the tables below. Layout, fonts
@@ -40,6 +45,8 @@ the firmware writes. Button bounds here allow some slack.
| `bBGyro` | Button | – | bco green / gray (label it "ROBOT GYRO") |
| `hSpeed` | Slider | – | `val` 0..255 (set **maxval = 255**) |
| `nSpeed` | Number | – | `val` 0..100 (speed %) |
| `hHeight`| Slider | – | `val` 0..200, 100 = default (set **maxval = 200**) |
| `nHeight`| Number | – | `val` -100..100 (height offset %) |
| `bCal` | Button | 9 | `CAL` / `SAVE CAL`, bco orange while active |
| `bCalX` | Button | – | nothing (static "CANCEL CAL") |
| `bZero` | Button | – | nothing (static "LEVEL") |
@@ -63,6 +70,7 @@ uses **Touch Move** as well so the value tracks your finger). Frame format:
| `bRGyro` | `printh 23 01 52` | `R` remote-gyro tilt on/off |
| `bBGyro` | `printh 23 01 42` | `B` robot self-stabilize on/off |
| `hSpeed` | `printh 23 02 56`<br>`prints hSpeed.val,1` | `V` + speed byte |
| `hHeight` | `printh 23 02 48`<br>`prints hHeight.val,1` | `H` + height byte (Touch Move + Release) |
| `bZero` | `printh 23 01 5A` | `Z` zero handset level |
| `bCal` | `printh 23 01 43` | `C` start / save stick calibration |
| `bCalX` | `printh 23 01 51` | `Q` cancel calibration |
+5
View File
@@ -46,6 +46,7 @@ bool NextionUI::decodeCustom(UiEvent& out) {
case 'R': out.cmd = UiCmd::ToggleRemoteGyro; break;
case 'B': out.cmd = UiCmd::ToggleRobotGyro; break;
case 'V': out.cmd = UiCmd::SetSpeed; break;
case 'H': out.cmd = UiCmd::SetHeight; break;
case 'Z': out.cmd = UiCmd::ZeroGyro; break;
case 'C': out.cmd = UiCmd::CalStartSave; break;
case 'Q': out.cmd = UiCmd::CalCancel; break;
@@ -124,6 +125,10 @@ void NextionUI::pushState(const UiView& v, bool full) {
cmd("hSpeed.val=%u", v.speed_target);
cmd("nSpeed.val=%u", (v.speed_target * 100 + 127) / 255);
}
if (full || v.height != s.height) {
cmd("hHeight.val=%d", v.height / 10 + 100); // slider 0..200, 100 = default
cmd("nHeight.val=%d", v.height / 10); // -100..100 %
}
if (full || v.calibrating != s.calibrating) {
cmd("bCal.txt=\"%s\"", v.calibrating ? "SAVE CAL" : "CAL");
cmd("bCal.bco=%u", v.calibrating ? COL_WARN : COL_OFF);
+1
View File
@@ -13,6 +13,7 @@ CmdPacket PacketBuilder::build(const StickFrame& s, const RemoteIMU& imu, const
CmdPacket p;
p.seq = seq_++;
p.speed_scale = st.speedCurrent();
p.body_height = st.height(); // a setting, not motion: sent even while disarmed
uint8_t f = 0;
if (st.armed()) f |= FLAG_ARMED;
+8
View File
@@ -36,6 +36,13 @@ Stick pots are wired 3.3 V – wiper – GND. **Never feed 5 V into a Teensy
| Right stick, **ROTATE** (default) | `rx/ry/rz` | body roll / pitch / yaw |
| Right stick, **MOVE** (`FLAG_RSTICK_XLATE`) | `rx/ry/rz` | body X / Y / Z (height, via twist) offset |
| Handset tilt | `remote_roll/pitch` | extra body roll/pitch while REMOTE GYRO is on |
| Height slider | `body_height` | standing height, default ± `STANCE_HEIGHT_RANGE_MM` (robot rate-limits the change) |
In **HEADING** mode the left stick steers, relative to where the robot
faces at that moment. Forward/back walks forward/back, and sideways
steers: forward+left or back+left walk while turning left, and left alone
turns on the spot for as long as you hold it. Let go and the robot holds
its heading.
Touchscreen: ARM / DISARM, SIT/STAND, TRIPOD/WAVE, DIRECT/HEADING,
R: ROTATE/MOVE, REMOTE GYRO, ROBOT GYRO, speed slider, LEVEL (re-zero the
@@ -91,6 +98,7 @@ display attached.
| `m` | direct ↔ heading-hold | `t` | right stick rotate ↔ translate |
| `r` | remote gyro on/off | `b` | robot gyro on/off |
| `+` / `-` | speed ±25 | `z` | zero handset level |
| `]` / `[` | height up / down 10% | | |
| `c` | start / save calibration | `x` | cancel calibration |
| `p` | print status (raw + mapped sticks, IMU, link, state, cal) | `d` | toggle 10 Hz packet dump |
| `R` | reset stick cal to Config defaults | | |
+6 -2
View File
@@ -55,8 +55,12 @@ bool RemoteIMU::readRaw(float a[3], float g[3]) {
Wire.requestFrom((int)IMU_ADDR, 14);
if (Wire.available() < 14) return false;
int16_t raw[7];
for (int k = 0; k < 7; k++) raw[k] = (Wire.read() << 8) | Wire.read(); // ax ay az temp gx gy gz
int16_t raw[7]; // ax ay az temp gx gy gz, big-endian
for (int k = 0; k < 7; k++) {
uint8_t hi = Wire.read(); // two statements: the order of calls inside one expression is unspecified
uint8_t lo = Wire.read();
raw[k] = (int16_t)((hi << 8) | lo);
}
float chip_a[3] = { raw[0] / 16384.0f, raw[1] / 16384.0f, raw[2] / 16384.0f };
float chip_g[3] = { raw[4] / 131.0f, raw[5] / 131.0f, raw[6] / 131.0f };
+5
View File
@@ -56,6 +56,10 @@ bool RemoteState::handle(const UiEvent& e, Sticks& stick_hw, RemoteIMU& imu, uin
case UiCmd::SpeedUp: speed_target_ = (uint8_t)min(255, speed_target_ + SPEED_SCALE_STEP); return true;
case UiCmd::SpeedDown: speed_target_ = (uint8_t)max(0, speed_target_ - SPEED_SCALE_STEP); return true;
case UiCmd::SetHeight: height_ = (int16_t)constrain(((int)e.value - 100) * 10, -1000, 1000); return true;
case UiCmd::HeightUp: height_ = (int16_t)min(1000, height_ + HEIGHT_STEP); return true;
case UiCmd::HeightDown: height_ = (int16_t)max(-1000, height_ - HEIGHT_STEP); return true;
case UiCmd::ZeroGyro:
imu.zeroLevel();
setMessage("Remote level zeroed", now);
@@ -110,5 +114,6 @@ void RemoteState::fillView(UiView& v, uint32_t now_ms) const {
v.robot_gyro = robot_gyro_;
v.calibrating = calibrating_;
v.speed_target = speed_target_;
v.height = height_;
v.message = message(now_ms);
}
+2
View File
@@ -29,6 +29,7 @@ public:
bool robotGyro() const { return robot_gyro_; }
uint8_t speedTarget() const { return speed_target_; }
int16_t height() const { return height_; } // -1000..1000, 0 = default
uint8_t speedCurrent() const { return (uint8_t)lroundf(speed_.current); }
float remoteTiltBlend() const { return tilt_blend_.current; } // 0..1, fades on the gyro toggle
@@ -48,6 +49,7 @@ private:
bool calibrating_ = false;
uint8_t speed_target_ = SPEED_SCALE_DEFAULT;
int16_t height_ = 0;
SmoothedValue speed_{SPEED_SCALE_RATE_PER_SEC};
SmoothedValue tilt_blend_{REMOTE_TILT_FADE_PER_SEC};
+2
View File
@@ -17,6 +17,8 @@ bool SerialConsole::poll(UiEvent& ui, Local& local) {
case 'b': ui.cmd = UiCmd::ToggleRobotGyro; return true;
case '+': case '=': ui.cmd = UiCmd::SpeedUp; return true;
case '-': case '_': ui.cmd = UiCmd::SpeedDown; return true;
case ']': ui.cmd = UiCmd::HeightUp; return true;
case '[': ui.cmd = UiCmd::HeightDown; return true;
case 'z': ui.cmd = UiCmd::ZeroGyro; return true;
case 'c': ui.cmd = UiCmd::CalStartSave; return true;
case 'x': ui.cmd = UiCmd::CalCancel; return true;
+4
View File
@@ -17,6 +17,9 @@ enum class UiCmd : uint8_t {
SetSpeed, // value = 0..255
SpeedUp, // serial console only
SpeedDown, // serial console only
SetHeight, // value = 0..200, 100 = default standing height
HeightUp, // serial console only
HeightDown, // serial console only
ZeroGyro, // capture the handset's current attitude as "level"
CalStartSave, // start sweep calibration, or save it if running
CalCancel,
@@ -39,6 +42,7 @@ struct UiView {
bool robot_gyro = false;
bool calibrating = false;
uint8_t speed_target = 0;
int16_t height = 0; // -1000..1000, 0 = default
bool link_ok = false;
uint8_t link_pct = 0;
const char* message = ""; // always a string literal - NextionUI diffs it by pointer
+4 -4
View File
@@ -34,17 +34,17 @@ static void printStatus() {
imu.ok() ? "ok" : "MISSING", imu.rollDeg(), imu.pitchDeg());
Serial.printf("radio %s link %s %u%%\n", radio_link.present() ? "ok" : "MISSING",
radio_link.linkOk() ? "OK" : "DOWN", radio_link.qualityPct());
Serial.printf("armed %d stand %d gait %s mode %s rstick %s remote_gyro %d robot_gyro %d speed %u->%u\n",
Serial.printf("armed %d stand %d gait %s mode %s rstick %s remote_gyro %d robot_gyro %d speed %u->%u height %d\n",
state.armed(), state.standRequested(), state.gaitWave() ? "wave" : "tripod",
state.modeB() ? "B" : "A", state.rstickXlate() ? "xlate" : "rot",
state.remoteGyro(), state.robotGyro(), state.speedTarget(), state.speedCurrent());
state.remoteGyro(), state.robotGyro(), state.speedTarget(), state.speedCurrent(), state.height());
sticks.printCalibration(Serial);
}
static void dumpPacket(const CmdPacket& p) {
Serial.printf("#%3u vx %5d vy %5d yaw %5d | r %5d %5d %5d | tilt %5d %5d | spd %3u flags %02X\n",
Serial.printf("#%3u vx %5d vy %5d yaw %5d | r %5d %5d %5d | tilt %5d %5d | h %5d spd %3u flags %02X\n",
p.seq, p.vx, p.vy, p.yaw_rate, p.rx, p.ry, p.rz,
p.remote_roll, p.remote_pitch, p.speed_scale, p.flags);
p.remote_roll, p.remote_pitch, p.body_height, p.speed_scale, p.flags);
}
static void pollInputs(uint32_t now_ms) {