diff --git a/mini_bdx_runtime/mini_bdx_runtime/xbox_controller.py b/mini_bdx_runtime/mini_bdx_runtime/xbox_controller.py index cd74b548..0b778a2d 100644 --- a/mini_bdx_runtime/mini_bdx_runtime/xbox_controller.py +++ b/mini_bdx_runtime/mini_bdx_runtime/xbox_controller.py @@ -6,6 +6,8 @@ from mini_bdx_runtime.buttons import Buttons +AXIS_DEAD_ZONE = 0.1 + X_RANGE = [-0.15, 0.15] Y_RANGE = [-0.2, 0.2] YAW_RANGE = [-1.0, 1.0] @@ -54,17 +56,22 @@ def get_commands(self): right_trigger = self.last_right_trigger l_x = -1 * self.p1.get_axis(0) + l_x = l_x if abs(l_x) > AXIS_DEAD_ZONE else 0 + l_y = -1 * self.p1.get_axis(1) + l_y = l_y if abs(l_y) > AXIS_DEAD_ZONE else 0 + r_x = -1 * self.p1.get_axis(2) + r_x = r_x if abs(r_x) > AXIS_DEAD_ZONE else 0 + r_y = -1 * self.p1.get_axis(3) + r_y = r_y if abs(r_y) > AXIS_DEAD_ZONE else 0 right_trigger = np.around((self.p1.get_axis(4) + 1) / 2, 3) - left_trigger = np.around((self.p1.get_axis(5) + 1) / 2, 3) + right_trigger = right_trigger if right_trigger > AXIS_DEAD_ZONE else 0 - if left_trigger < 0.1: - left_trigger = 0 - if right_trigger < 0.1: - right_trigger = 0 + left_trigger = np.around((self.p1.get_axis(5) + 1) / 2, 3) + left_trigger = left_trigger if left_trigger > AXIS_DEAD_ZONE else 0 if not self.head_control_mode: lin_vel_y = l_x diff --git a/scripts/start_walk_after_controller.py b/scripts/start_walk_after_controller.py new file mode 100644 index 00000000..891e2727 --- /dev/null +++ b/scripts/start_walk_after_controller.py @@ -0,0 +1,133 @@ +import argparse +import os +import subprocess +import sys +import time + +import pygame +from mini_bdx_runtime.sounds import Sounds + + +HOME_DIR = os.path.expanduser("~") +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def wait_for_controller(poll_interval: float = 1.0): + pygame.init() + pygame.joystick.init() + + print("Waiting for bluetooth controller to connect...") + while True: + pygame.event.pump() + if pygame.joystick.get_count() > 0: + joystick = pygame.joystick.Joystick(0) + joystick.init() + print(f"Controller connected: {joystick.get_name()}") + return + + time.sleep(poll_interval) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--onnx_model_path", type=str, required=True) + parser.add_argument( + "--duck_config_path", + type=str, + required=False, + default=f"{HOME_DIR}/duck_config.json", + ) + parser.add_argument("--serial_port", type=str, default="/dev/ttyACM0") + parser.add_argument("-a", "--action_scale", type=float, default=0.25) + parser.add_argument("-p", type=int, default=30) + parser.add_argument("-i", type=int, default=0) + parser.add_argument("-d", type=int, default=0) + parser.add_argument("-c", "--control_freq", type=int, default=50) + parser.add_argument("--pitch_bias", type=float, default=0, help="deg") + parser.add_argument( + "--save_obs", + type=str, + required=False, + default=False, + help="save the run's observations", + ) + parser.add_argument( + "--replay_obs", + type=str, + required=False, + default=None, + help="replay the observations from a previous run (can be from the robot or from mujoco)", + ) + parser.add_argument("--cutoff_frequency", type=float, default=None) + parser.add_argument( + "--sound_path", + type=str, + default=os.path.join( + os.path.dirname(SCRIPT_DIR), "mini_bdx_runtime/assets/happy1.wav" + ), + ) + args = parser.parse_args() + + os.chdir(SCRIPT_DIR) + + turn_on_script = os.path.join(SCRIPT_DIR, "turn_on.py") + walk_script = os.path.join(SCRIPT_DIR, "v2_rl_walk_mujoco.py") + + # Call the original turn_on script + subprocess.run( + [ + sys.executable, + turn_on_script, + ], + check=True, + cwd=SCRIPT_DIR, + ) + + # Play happy sound after turn on + if args.sound_path and os.path.exists(args.sound_path): + try: + pygame.mixer.init() + sound = pygame.mixer.Sound(args.sound_path) + sound.play() + except Exception as e: + print(f"Failed to play sound: {e}") + + wait_for_controller() + + # Build the walk command + walk_cmd = [ + sys.executable, + walk_script, + "--onnx_model_path", + args.onnx_model_path, + "--duck_config_path", + args.duck_config_path, + "-a", + str(args.action_scale), + "-p", + str(args.p), + "-i", + str(args.i), + "-d", + str(args.d), + "-c", + str(args.control_freq), + "--pitch_bias", + str(args.pitch_bias), + ] + + if args.save_obs is not False: + walk_cmd.extend(["--save_obs", args.save_obs]) + + if args.replay_obs is not None: + walk_cmd.extend(["--replay_obs", args.replay_obs]) + + if args.cutoff_frequency is not None: + walk_cmd.extend(["--cutoff_frequency", str(args.cutoff_frequency)]) + + # Hand off execution to the walking script + os.execv(sys.executable, walk_cmd) + + +if __name__ == "__main__": + main() diff --git a/scripts/start_walk_after_controller.sh b/scripts/start_walk_after_controller.sh new file mode 100644 index 00000000..7c729fe4 --- /dev/null +++ b/scripts/start_walk_after_controller.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Setup Instructions for Raspberry Pi OS Lite (Auto-start on boot): +# 1. Make this script executable: +# chmod +x start_walk_after_controller.sh +# 2. Add to crontab: +# crontab -e +# Add the following line at the end: +# @reboot /bin/bash /home/pi/Open_Duck_Mini_Runtime/scripts/start_walk_after_controller.sh >> /home/pi/startup.log 2>&1 +# 3. Ensure your ONNX model and duck_config.json are in the home directory as specified below. + +set -euo pipefail + +# Ensure HOME is defined (crontab sometimes has a limited environment) +export HOME="${HOME:-/home/duck0}" + +# Initialize Conda for the current shell session +CONDA_PROFILE="$HOME/miniconda3/etc/profile.d/conda.sh" +if [ -f "$CONDA_PROFILE" ]; then + source "$CONDA_PROFILE" + conda activate base +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$SCRIPT_DIR" + +exec python3 "$SCRIPT_DIR/start_walk_after_controller.py" \ + --duck_config_path "$HOME/duck_config.json" \ + --onnx_model_path "$HOME/BEST_WALK_ONNX_2.onnx" \ No newline at end of file