Files
hexapod-robot/2026-current/Hexapod_Simulation_Unity/Assets/HexapodSim/SimRemote.cs
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

256 lines
11 KiB
C#

using System;
using UnityEngine;
namespace HexapodSim
{
// Stand-in for the handheld remote. Produces CmdPackets exactly per the
// protocol contract and sends them over the simulated radio. Toggles and
// interlocks mirror remote/RemoteState.cpp and remote/PacketBuilder.cpp -
// keep them in sync if those change.
//
// Input sources:
// Joystick - the real remote as a USB game controller, read through
// Unity input axes "1".."6" like the old HandleHoystick.
// Keyboard - test without any remote (keys listed in SimTestPanel).
// RobotSerialHarness - no radio at all; typed keys go to the firmware's own
// TEST_SERIAL_INPUT harness, like the Arduino Serial Monitor.
// Toggles, the speed slider and the handset tilt sliders live in SimTestPanel.
public class SimRemote : MonoBehaviour
{
public enum InputSource { Joystick, Keyboard, RobotSerialHarness }
[Serializable]
public class AxisBinding
{
public string axis;
public bool invert;
public AxisBinding(string axis, bool invert) { this.axis = axis; this.invert = invert; }
}
public InputSource source = InputSource.Keyboard;
// Defaults follow the old HandleHoystick: axes 4/6/5 were the walk stick
// (b2/b1/b3), 2/1/3 the rotation stick (a1/a2/a3), with 1 and 2 inverted.
// Which of 4/6 is forward vs sideways wasn't recoverable from the old code:
// check the raw axis readout in the test panel and fix here.
[Header("Joystick axes (Input Manager names)")]
[Tooltip("Stick convention after invert: X + = right, Y + = forward, twist + = counterclockwise.")]
public AxisBinding leftX = new AxisBinding("4", false);
public AxisBinding leftY = new AxisBinding("6", false);
public AxisBinding leftTwist = new AxisBinding("5", false);
public AxisBinding rightX = new AxisBinding("2", true);
public AxisBinding rightY = new AxisBinding("1", true);
public AxisBinding rightTwist = new AxisBinding("3", false);
[Range(0, 0.5f)] public float deadzone = 0.08f;
[Header("Operator state (same toggles as the Nextion screen)")]
public bool armed;
public bool stand;
public bool gaitWave;
public bool modeB;
public bool rstickXlate;
public bool remoteGyro;
public bool robotGyro;
[Range(0, 255)] public int speedTarget = 128;
[Tooltip("Standing height setting, -1000 lowest .. 0 default .. 1000 highest (CmdPacket.body_height).")]
[Range(-1000, 1000)] public int height = 0;
[Header("Simulated handset tilt (the remote's MPU6050)")]
[Range(-30, 30)] public float handsetRollDeg;
[Range(-30, 30)] public float handsetPitchDeg;
public float handsetFullScaleDeg = 30f; // remote Config.h REMOTE_TILT_FULL_SCALE_DEG
[Header("Smoothing, as on the remote")]
public float speedRatePerSec = 255f; // SPEED_SCALE_RATE_PER_SEC
public float walkLingerSec = 0.25f; // WALK_LINGER_MS
public float armCenterTolerance = 0.05f; // ARM_CENTER_TOLERANCE / 1000
public float keyStickRatePerSec = 4f; // how fast a held key moves the virtual stick
// Latest processed sticks, -1..1: [L_X, L_Y, L_TWIST, R_X, R_Y, R_TWIST].
public readonly float[] sticks = new float[6];
public readonly float[] rawAxes = new float[6]; // Input axes "1".."6" as read
public CmdPacket LastPacket { get; private set; }
public string Message { get; private set; } = "";
public bool JoystickAxesMissing { get; private set; }
float speedCurrent_ = 128;
float lastWalkInputTime_ = -999f;
byte seq_;
string serialKeys_ = "";
float messageUntil_;
public void ResetState()
{
armed = stand = false;
speedCurrent_ = speedTarget;
lastWalkInputTime_ = -999f;
serialKeys_ = "";
}
void Awake()
{
if (Input.GetJoystickNames().Length > 0 && !string.IsNullOrEmpty(Input.GetJoystickNames()[0]))
source = InputSource.Joystick;
}
void Update()
{
for (int i = 0; i < 6; i++) rawAxes[i] = ReadAxis((i + 1).ToString());
if (source == InputSource.RobotSerialHarness)
{
serialKeys_ += Input.inputString; // raw keystrokes for the firmware's harness
return;
}
HandleToggleKeys();
if (source == InputSource.Joystick) ReadJoystick();
else ReadKeyboardSticks(Time.deltaTime);
if (Time.time > messageUntil_) Message = "";
}
// ---- Operator commands (also called by SimTestPanel buttons) ----
public void ToggleArm()
{
if (armed) { Disarm(); return; }
foreach (float v in sticks)
if (Mathf.Abs(v) > armCenterTolerance) { Say("Center both sticks to arm"); return; }
armed = true;
}
public void Disarm() { armed = false; stand = false; } // disarm always clears stand
public void ToggleStand()
{
if (!armed) { Say("Arm first"); return; }
stand = !stand;
}
public void ToggleRemoteGyro() => remoteGyro = !remoteGyro;
void Say(string msg) { Message = msg; messageUntil_ = Time.time + 2.5f; }
void HandleToggleKeys()
{
if (Input.GetKeyDown(KeyCode.Alpha1)) ToggleArm();
if (Input.GetKeyDown(KeyCode.Alpha2)) ToggleStand();
if (Input.GetKeyDown(KeyCode.Alpha3)) gaitWave = !gaitWave;
if (Input.GetKeyDown(KeyCode.Alpha4)) modeB = !modeB;
if (Input.GetKeyDown(KeyCode.Alpha5)) rstickXlate = !rstickXlate;
if (Input.GetKeyDown(KeyCode.Alpha6)) ToggleRemoteGyro();
if (Input.GetKeyDown(KeyCode.Alpha7)) robotGyro = !robotGyro;
if (Input.GetKeyDown(KeyCode.Backspace)) Disarm();
if (Input.GetKeyDown(KeyCode.Equals) || Input.GetKeyDown(KeyCode.KeypadPlus)) speedTarget = Mathf.Min(255, speedTarget + 25);
if (Input.GetKeyDown(KeyCode.Minus) || Input.GetKeyDown(KeyCode.KeypadMinus)) speedTarget = Mathf.Max(0, speedTarget - 25);
if (Input.GetKeyDown(KeyCode.PageUp)) height = Mathf.Min(1000, height + 100);
if (Input.GetKeyDown(KeyCode.PageDown)) height = Mathf.Max(-1000, height - 100);
}
// ---- Sticks ----
float ReadAxis(string name)
{
try { return Input.GetAxis(name); }
catch (ArgumentException) { JoystickAxesMissing = true; return 0; } // axis not defined in Input Manager
}
float Read(AxisBinding b)
{
float v = ReadAxis(b.axis) * (b.invert ? -1 : 1);
float mag = Mathf.Abs(v);
if (mag <= deadzone) return 0;
return Mathf.Sign(v) * Mathf.Clamp01((mag - deadzone) / (1 - deadzone));
}
void ReadJoystick()
{
sticks[0] = Read(leftX);
sticks[1] = Read(leftY);
sticks[2] = Read(leftTwist);
sticks[3] = Read(rightX);
sticks[4] = Read(rightY);
sticks[5] = Read(rightTwist);
}
// WASD + Q/E = left stick (walk, turn), IJKL + U/O = right stick.
void ReadKeyboardSticks(float dt)
{
float Key(KeyCode pos, KeyCode neg) => (Input.GetKey(pos) ? 1 : 0) - (Input.GetKey(neg) ? 1 : 0);
float[] target =
{
Key(KeyCode.D, KeyCode.A), Key(KeyCode.W, KeyCode.S), Key(KeyCode.Q, KeyCode.E),
Key(KeyCode.L, KeyCode.J), Key(KeyCode.I, KeyCode.K), Key(KeyCode.U, KeyCode.O),
};
for (int i = 0; i < 6; i++) sticks[i] = Mathf.MoveTowards(sticks[i], target[i], keyStickRatePerSec * dt);
}
// ---- Transmit ----
// Called by HexapodSimulation once per remote TX period.
public void Transmit(HexapodSimNative native, float dt)
{
if (source == InputSource.RobotSerialHarness)
{
native.SerialInput(serialKeys_); // no radio: the firmware's harness injects its own packets
serialKeys_ = "";
return;
}
speedCurrent_ = Mathf.MoveTowards(speedCurrent_, speedTarget, speedRatePerSec * dt);
LastPacket = BuildPacket();
native.RadioSend(LastPacket);
}
static short Wire(float v) => (short)Mathf.RoundToInt(Mathf.Clamp(v, -1f, 1f) * 1000f);
// Same mapping as remote/PacketBuilder.cpp.
CmdPacket BuildPacket()
{
var p = new CmdPacket
{
seq = seq_++,
speed_scale = (byte)Mathf.RoundToInt(speedCurrent_),
body_height = (short)Mathf.Clamp(height, -1000, 1000), // a setting: sent even while disarmed
};
byte f = 0;
if (armed) f |= CmdPacket.FLAG_ARMED;
if (stand) f |= CmdPacket.FLAG_STAND;
if (modeB) f |= CmdPacket.FLAG_MODE_B;
if (rstickXlate) f |= CmdPacket.FLAG_RSTICK_XLATE;
if (gaitWave) f |= CmdPacket.FLAG_GAIT_WAVE;
if (robotGyro) f |= CmdPacket.FLAG_ROBOT_GYRO;
if (!armed) { p.flags = f; return p; } // disarmed: neutral command
// Left stick: forward -> +vx, right -> -vy (vy + = left), twist CCW -> +yaw_rate.
p.vx = Wire(sticks[1]);
p.vy = Wire(-sticks[0]);
p.yaw_rate = Wire(sticks[2]);
if (rstickXlate)
{
p.rx = Wire(sticks[4]); // X offset: forward
p.ry = Wire(-sticks[3]); // Y offset: right -> -Y
p.rz = Wire(sticks[5]); // Z offset: twist CCW -> up
}
else
{
p.rx = Wire(sticks[3]); // roll: right side down
p.ry = Wire(sticks[4]); // pitch: nose down
p.rz = Wire(sticks[5]); // yaw: CCW
}
bool walkInput = p.vx != 0 || p.vy != 0 || (!modeB && p.yaw_rate != 0);
if (walkInput) lastWalkInputTime_ = Time.time;
if (stand && (walkInput || Time.time - lastWalkInputTime_ < walkLingerSec)) f |= CmdPacket.FLAG_WALK;
if (remoteGyro)
{
f |= CmdPacket.FLAG_REMOTE_GYRO;
p.remote_roll = Wire(handsetRollDeg / handsetFullScaleDeg);
p.remote_pitch = Wire(handsetPitchDeg / handsetFullScaleDeg);
}
p.flags = f;
return p;
}
}
}