Back to about

I.S.S Docking Autopilot

An autopilot script for the SpaceX International Space Station docking simulator.

MaxRandle/spacex_iss_docking_simulator_script

1

SpaceX Crew Dragon Demo-2

On the 30th of May 2020 SpaceX launched mission Crew Dragon Demo-2, the first ever commercial manned space launch mission to the International Space Station. This mission involved the Dragon-2 capsule being launched from Cape Canaveral Air Force Station and then carried into low-Earth-orbit aboard the SpaceX Falcon-9 rocket. Once the Dragon-2 crew capsule separated from the Falcon-9 upper stage the pilots used the reaction control thruster system to manouver within 20 metres of the I.S.S at which point Dragon-2 takes over and docks autonomously.

ISS docking simulator

To pique public interest in the mission, SpaceX published a free online docking simulator for practicing manouvering and docking the Dragon-2 crew capsule using the reaction control thruster system.

The simulator recreated the actual interface that the pilots used to control Dragon. This quickly became extremely popular and developed a speedrunning community within hours of release with some of the more hardcode speedrunners publishing some very impressive sub 45 second times on YouTube.

simulator interface

After playing around with this for a while, I decided to try to write a script that could autonomously dock. This was quite tricky because it meant I had to open up the website source and figure out which JS file the action was happening in. Then I had to read some minified code and try to figure out which variables were responsible for the numbers I was seeing on the screen such as displacement in 3 axes, pitch, roll, and yaw as well as translational and rotational velocities, the functions that controlled the velocoities, the magnitude of change in the velocity resulting from pressing one of the truster buttons, etc...

Eventually I had reverse engineered the code and came up with a simple algorithm that could safely and accurately dock using only the reaction control thrusters and not cheating by editing any code or variables directly. The algorithm attempted to match Dragon's velocity in all degrees of motion to an amount proportional to the displacement for each axis of motion.

I was pretty happy with what I had created but I wasn't quite done having fun with this project yet. What's the next logical step?

DO. IT. FASTER.

I devised a highly optimised algorithm based on simple harmonic motion, critical damping, and the spring equation that could dock insanely fast and put even the worlds best human docking speedrunners to absolute shame.

Try it out

  1. Go to the SpaceX ISS docking simulator
  2. Click begin to start docking
  3. Open your browsers developer tools
  4. Copy paste the code below into the console
  5. A new button that toggles the autopilot labeled 'A' will appear in the top left.
  6. Click the button!
function htmlToElement(html) {
  var template = document.createElement("template");
  html = html.trim(); // Never return a text node of whitespace as the result
  template.innerHTML = html;
  return template.content.firstChild;
}

// add autopilot button
let autopilotEngaged = false;
const optionsElement = document.getElementById("options");
const autopilotButton = htmlToElement(
  '<div id="autopilot" title="Engage Autopilot" class="icon">A</div>'
);
autopilotButton.addEventListener("click", () => {
  autopilotEngaged = !autopilotEngaged;
});
optionsElement.appendChild(autopilotButton);

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

const shouldMove = (targetVelocity, currentVelocity, pulse) => {
  // will moving get me closer to my target velocity
  const newVelocity = currentVelocity + pulse;
  return (
    Math.abs(targetVelocity - newVelocity) <
    Math.abs(targetVelocity - currentVelocity)
  );
};

const shouldTogglePrecision = (distance) => {
  const precisionThreshold = 5;
  const precisionStatusElement = document.getElementById(
    "precision-translation-status"
  );
  const precisionToggleButton = document.getElementById("toggle-translation");

  if (
    distance > precisionThreshold &&
    precisionStatusElement.className !== "noselect large"
  ) {
    precisionToggleButton.click();
  } else if (
    distance <= precisionThreshold &&
    precisionStatusElement.className === "noselect large"
  ) {
    precisionToggleButton.click();
  }
};

const rotationIncrement = rotationPulseSize * toRAD * 0.01;
// time between each loop
const sleepTime = 20;

// rotation spring constant
const rk = 0.02;
// rotation critical damping constant
const rb = Math.sqrt(4 * rk);

// translation spring constant
const tk = 0.8;
// translation critical damping constant
const tb = Math.sqrt(4 * tk);

const loop = async () => {
  console.log("autopilot engaged");

  while (true) {
    if (autopilotEngaged) {
      // should toggle translation precision

      // calculate rotaion velocities
      const rotationX = camera.rotation.x;
      const rotationY = camera.rotation.y;
      const rotationZ = camera.rotation.z;
      const targetRotationXRate = -rk * rotationX - rb * currentRotationX;
      const targetRotationYRate = -rk * rotationY - rb * currentRotationY;
      const targetRotationZRate = -rk * rotationZ - rb * currentRotationZ;

      // calculate translation velocities
      const xRange = camera.position.x - issObject.position.x;
      const yRange = camera.position.y - issObject.position.y;
      const zRange = camera.position.z - issObject.position.z;
      const currentXVelocity = motionVector.x * 50;
      const currentYVelocity = motionVector.y * 50;
      const currentZVelocity = motionVector.z * 50;
      const targetXVelocity = -tk * xRange - tb * currentXVelocity;
      const targetYVelocity = -tk * yRange - tb * currentYVelocity;
      const targetZVelocity = -tk * zRange - tb * currentZVelocity - 0.01;

      shouldTogglePrecision(zRange);

      // rotation actuators
      if (
        shouldMove(targetRotationXRate, currentRotationX, -rotationIncrement)
      ) {
        pitchDown();
      }
      if (
        shouldMove(targetRotationXRate, currentRotationX, rotationIncrement)
      ) {
        pitchUp();
      }
      if (
        shouldMove(targetRotationYRate, currentRotationY, -rotationIncrement)
      ) {
        yawRight();
      }
      if (
        shouldMove(targetRotationYRate, currentRotationY, rotationIncrement)
      ) {
        yawLeft();
      }
      if (
        shouldMove(targetRotationZRate, currentRotationZ, -rotationIncrement)
      ) {
        rollRight();
      }
      if (
        shouldMove(targetRotationZRate, currentRotationZ, rotationIncrement)
      ) {
        rollLeft();
      }
      // translation actuators
      if (
        shouldMove(targetXVelocity, currentXVelocity, -translationPulseSize)
      ) {
        translateLeft();
      }
      if (shouldMove(targetXVelocity, currentXVelocity, translationPulseSize)) {
        translateRight();
      }
      if (
        shouldMove(targetYVelocity, currentYVelocity, -translationPulseSize)
      ) {
        translateDown();
      }
      if (shouldMove(targetYVelocity, currentYVelocity, translationPulseSize)) {
        translateUp();
      }
      if (
        shouldMove(targetZVelocity, currentZVelocity, -translationPulseSize)
      ) {
        translateForward();
      }
      if (shouldMove(targetZVelocity, currentZVelocity, translationPulseSize)) {
        translateBackward();
      }
    }
    await sleep(sleepTime);
  }
};

loop();