Creating a SpaceX Crew Dragon simulator autopilot in Clojure

Daniils Petrovs
Level Up Coding
Published in
5 min readMay 20, 2020

--

SpaceX Crew Dragon, NASA

In preparation for the SpaceX Crew Dragon’s first crewed mission Demo-2 in late May, SpaceX released a pretty cool little WebGL docking simulator, featuring the Dragon spacecraft. After getting the docking done successfully a few times, I realised this is a perfect opportunity to write a docking autopilot! How does one even approach a problem like this?

An orbital spacecraft is a bit different from what you would consider a normal vehicle, like a car or an airplane. In space, you have all 6 Degrees of Freedom (DoF), meaning you can go up-down, left-right, roll clockwise-anti-clockwise etc.

That is great, but how do we actually dock?

In the context of the simulator, a successful docking means that:

  • We are perfectly aligned with the docking port on the ISS
  • Our final approach rate on contact is below a certain value
  • We don’t want to crash our multi-million dollar spaceship into anything along the way

If you break it down, the problem simply consists of aligning the Dragon how it should be (aligned with the ISS docking port), and flying it to where it should be (at the docking port) slowly.

For the language, I am using Clojure, a Lisp-like functional programming language that runs on the JVM. It is highly extensible, simple, with lots of libraries and powerful concurrency abstractions, which we will absolutely need later.

The first step is to obviously figure out how to control the simulator. The best way to drive the browser these days is using WebDriver, and some API or client library to send commands to and from it. On our side, all we have to do is create a control interface in our code. In the entire project I am using Etaoin, which is a fantastic pure Clojure implementation of the WebDriver protocol.

SpaceX ISS simulator GUI on launch

While the simulator has clickable on-screen controls, it also has key bindings, which we can take advantage of:

Next, an automated spacecraft is hopeless if it doesn’t have telemetry. In short, we have to know where we are looking and how, where we should be looking, as well as where we are in space. Luckily, the good engineers at SpaceX made easily locatable elements for the information in the HUD that our driver will get info from.

After writing a bunch of locators and parsers, we have a nice way of getting all the data we need from the HUD:

Now here lies the first problem: the HUD updates the information all the time as we are moving the spacecraft, so we need to continuously fetch new telemetry and make it available for all future control systems, without blocking the main thread with some loop. This is where Clojure’s concurrency really shines, because we can simply create a recursive poll function, and use a future to put it in a separate thread and move the main thread forward.

We also need to somehow keep our telemetry data in some state , that is also thread-safe. Luckily, atoms come to the rescue, and we can use swap! to simply update the telemetry with new data:

You might have noticed that we are also doing some calculations using x, y and z coordinates of the ship. Unfortunately, the HUD does not give us our velocity vector components, so we have to calculate those ourselves by using a distance / time differential.

The next step is writing the alignment functions for each rotation control axis — roll, pitch and yaw. All it boils down to is this simple logic:

However, here we have the next problem: the alignment needs to keep going all the time, and for each axis simultaneously. This means that if we try to do this sequentially, we will encounter a locking problem: the roll alignment function might be blocking the yaw from getting aligned and killing its rotation etc.

This is where future ‘s come to the rescue again!

;; concurrent futures for each control axis
(println "Rotation alignment enabled")
(future (dragon/align-roll-rot chr))
(future (dragon/align-pitch-rot chr))
(future (dragon/align-yaw-rot chr))

Now that the rotation is sorted out, the next step is to write logic for the approach.

We need to align ourselves on the z and y axes, as well as steadily move on the x axis. We already wrote the velocity component calculations, so all we have to do is dereference our telem telemetry atom whenever we need to.

The tricky part is writing all the right logic for the RCS firings during this phase — start pushing too much in one direction all the time, and it becomes and uncontrollable swaying motion, burning lots of fuel! I came up with the approach of implementing a “deadzone” — we don’t fire our thrusters the wrong way or too much when we are basically nearly on the spot. Once again, we use future s for the translation alignment.

With everything tied together, this is roughly what our threads look like, and how our control functions use the telemetry:

In conclusion, spaceships are hard, and the people of SpaceX and NASA obviously know what they are doing much better than me. However, this is a fun exercise to think about how these automatic docking systems work in real life, as well as to demonstrate where functional programming and concurrency really shines.

You can go check out the code in Github: https://github.com/DaniruKun/spacex-iss-docking-sim-autopilot

Small demo video showing off the autopilot:

--

--