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>
256 lines
11 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|