├── .formatter.exs ├── .gitignore ├── README.md ├── config └── config.exs ├── lib ├── autopilot.ex └── autopilot │ ├── application.ex │ ├── axis_x.ex │ ├── axis_y.ex │ ├── axis_z.ex │ ├── pid_controller.ex │ ├── pitch.ex │ ├── roll.ex │ ├── simulation.ex │ └── yaw.ex ├── mix.exs ├── mix.lock └── test ├── autopilot_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | autopilot-*.tar 24 | 25 | # Temporary files for e.g. tests 26 | /tmp 27 | 28 | # Idea stuff 29 | /.idea/ 30 | /autopilot.iml 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autopilot 2 | 3 | [SpaceX ISS Docking Simulator](https://iss-sim.spacex.com/) autopilot in Elixir. 4 | 5 | ## Usage 6 | Grab the code, run with `mix do deps.get, deps.compile && mix run --no-halt`. 7 | You also need [ChromeDriver](https://chromedriver.chromium.org) running and listening on localhost. 8 | 9 | ## Video sample 10 | 11 | https://user-images.githubusercontent.com/1916877/119236454-f52bd380-bb50-11eb-8b36-9d5f5ed5b781.mp4 12 | 13 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :info 4 | config :hound, driver: "chrome_driver" 5 | -------------------------------------------------------------------------------- /lib/autopilot.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot do 2 | @moduledoc """ 3 | Documentation for `Autopilot`. 4 | """ 5 | end 6 | -------------------------------------------------------------------------------- /lib/autopilot/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | Autopilot.Simulation, 12 | Autopilot.Roll, 13 | Autopilot.Pitch, 14 | Autopilot.Yaw, 15 | Autopilot.AxisY, 16 | Autopilot.AxisZ, 17 | {Autopilot.AxisX, kp: 0.01, bias: 0.1, poll_interval: 500} 18 | # Starts a worker by calling: Autopilot.Worker.start_link(arg) 19 | # {Autopilot.Worker, arg} 20 | ] 21 | 22 | # See https://hexdocs.pm/elixir/Supervisor.html 23 | # for other strategies and supported options 24 | opts = [strategy: :one_for_one, name: Autopilot.Supervisor] 25 | Supervisor.start_link(children, opts) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/autopilot/axis_x.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.AxisX do 2 | @moduledoc false 3 | 4 | use Autopilot.PidController, sensor: {:axis, :x} 5 | 6 | def set_output!(output, current_rate) do 7 | output = Float.round(output, 2) 8 | current_rate = Float.round(current_rate, 2) 9 | 10 | cond do 11 | current_rate == output -> :ok 12 | current_rate < output -> Autopilot.Simulation.translate(:forward) 13 | current_rate > output -> Autopilot.Simulation.translate(:backward) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/autopilot/axis_y.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.AxisY do 2 | @moduledoc false 3 | 4 | use Autopilot.PidController, sensor: {:axis, :y} 5 | 6 | def set_output!(output, current_rate) do 7 | output = Float.round(output, 2) 8 | current_rate = Float.round(current_rate, 2) 9 | 10 | cond do 11 | current_rate == output -> :ok 12 | current_rate < output -> Autopilot.Simulation.translate(:left) 13 | current_rate > output -> Autopilot.Simulation.translate(:right) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/autopilot/axis_z.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.AxisZ do 2 | @moduledoc false 3 | 4 | use Autopilot.PidController, sensor: {:axis, :z} 5 | 6 | def set_output!(output, current_rate) do 7 | output = Float.round(output, 2) 8 | current_rate = Float.round(current_rate, 2) 9 | 10 | cond do 11 | current_rate == output -> :ok 12 | current_rate < output -> Autopilot.Simulation.translate(:down) 13 | current_rate > output -> Autopilot.Simulation.translate(:up) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/autopilot/pid_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.PidController do 2 | @moduledoc false 3 | 4 | defmacro __using__([sensor: sensor] = _opts) do 5 | quote do 6 | use GenServer 7 | require Logger 8 | 9 | @wake_term :work 10 | @sensor_name unquote(sensor) 11 | 12 | # Client 13 | 14 | def start_link(opts) do 15 | GenServer.start_link(__MODULE__, opts) 16 | end 17 | 18 | # Server (callbacks) 19 | 20 | @impl true 21 | def init(opts) do 22 | state = %{ 23 | previous_error: 0.0, 24 | previous_integral: 0.0, 25 | last_update: Time.utc_now(), 26 | poll_interval: Keyword.get(opts, :poll_interval, 250), 27 | kp: Keyword.get(opts, :kp, 0.1), 28 | kd: Keyword.get(opts, :kd, 0.0), 29 | ki: Keyword.get(opts, :ki, 0.0), 30 | bias: Keyword.get(opts, :bias, 0.0) 31 | } 32 | 33 | Process.send_after(self(), @wake_term, state.poll_interval) 34 | 35 | {:ok, state} 36 | end 37 | 38 | @impl true 39 | def handle_info(:work, state) do 40 | new_state = 41 | try do 42 | {error, rate} = read_signal!(@sensor_name, state) 43 | {output, new_state} = calculate_output(error, state) 44 | set_output!(output, rate) 45 | new_state 46 | catch 47 | kind, failure -> 48 | Logger.debug(sensor: @sensor_name, kind: kind, failure: failure) 49 | state 50 | end 51 | 52 | Process.send_after(self(), @wake_term, state.poll_interval) 53 | {:noreply, new_state} 54 | end 55 | 56 | # Implementation 57 | 58 | defp read_signal!(sensor, state) do 59 | Autopilot.Simulation.telemetry(sensor) 60 | |> prepare_signal(state) 61 | end 62 | 63 | @spec calculate_output(float(), any()) :: {float(), any()} 64 | defp calculate_output(error, state) do 65 | time_passed = Time.diff(Time.utc_now(), state.last_update, :millisecond) 66 | integral = state.previous_integral + error * time_passed 67 | derivative = (error - state.previous_error) / time_passed 68 | output = state.kp * error + state.ki * integral + state.kd * derivative + state.bias 69 | 70 | { 71 | output, 72 | %{ 73 | state 74 | | previous_error: error, 75 | previous_integral: integral, 76 | last_update: Time.utc_now() 77 | } 78 | } 79 | end 80 | 81 | defp calculate_rate(error, state) do 82 | space_travel = state.previous_error - error 83 | time_passed = Time.diff(Time.utc_now(), state.last_update, :millisecond) 84 | rate = space_travel / time_passed * 900 85 | 86 | rate 87 | end 88 | 89 | defp prepare_signal([{sensor, :error, error}, {sensor, :rate, rate}], _state) do 90 | {clean_signal(error), clean_signal(rate)} 91 | end 92 | 93 | defp prepare_signal([{:position, _axis, error}], state) do 94 | error = clean_signal(error) 95 | rate = calculate_rate(error, state) 96 | {error, rate} 97 | end 98 | 99 | @spec clean_signal(String.t()) :: float() 100 | defp clean_signal(signal) do 101 | with [first_number | _] <- Regex.run(~r"^[-+]?\d*\.\d+|\d+", signal), 102 | {signal, _} <- Float.parse(first_number) do 103 | signal 104 | end 105 | end 106 | end 107 | end 108 | 109 | @callback set_output!(float(), float()) :: any() 110 | end 111 | -------------------------------------------------------------------------------- /lib/autopilot/pitch.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.Pitch do 2 | @moduledoc false 3 | 4 | use Autopilot.PidController, sensor: :pitch 5 | 6 | def set_output!(output, current_rate) do 7 | output = Float.round(output, 2) 8 | 9 | cond do 10 | current_rate == output -> :ok 11 | current_rate < output -> Autopilot.Simulation.pitch(:down) 12 | current_rate > output -> Autopilot.Simulation.pitch(:up) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/autopilot/roll.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.Roll do 2 | @moduledoc false 3 | 4 | use Autopilot.PidController, sensor: :roll 5 | 6 | def set_output!(output, current_rate) do 7 | output = Float.round(output, 2) 8 | 9 | cond do 10 | current_rate == output -> :ok 11 | current_rate < output -> Autopilot.Simulation.roll(:clockwise) 12 | current_rate > output -> Autopilot.Simulation.roll(:counter_clockwise) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/autopilot/simulation.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.Simulation do 2 | @moduledoc false 3 | 4 | use GenServer 5 | import Hound.Matchers 6 | import Hound.Helpers.{Navigation, Element, Page} 7 | require Logger 8 | 9 | # Client 10 | 11 | def start_link(_opts) do 12 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 13 | end 14 | 15 | @spec telemetry(:roll | :pitch | :yaw | {:axis, :x | :y | :z}) :: list() 16 | def telemetry(sensor) do 17 | GenServer.call(__MODULE__, {:telemetry, sensor}, 5000) 18 | end 19 | 20 | @spec roll(:clockwise | :counter_clockwise) :: :ok 21 | def roll(direction) do 22 | GenServer.cast(__MODULE__, {:pilot, :roll, direction}) 23 | end 24 | 25 | @spec pitch(:up | :down) :: :ok 26 | def pitch(direction) do 27 | GenServer.cast(__MODULE__, {:pilot, :pitch, direction}) 28 | end 29 | 30 | @spec yaw(:left | :right) :: :ok 31 | def yaw(direction) do 32 | GenServer.cast(__MODULE__, {:pilot, :yaw, direction}) 33 | end 34 | 35 | @spec translate(:up | :down | :left | :right | :forward | :backward) :: :ok 36 | def translate(direction) do 37 | GenServer.cast(__MODULE__, {:pilot, :translate, direction}) 38 | end 39 | 40 | # Server (callbacks) 41 | 42 | @impl true 43 | def init(:ok) do 44 | Hound.Helpers.Session.start_session(browser: "chrome") 45 | {:ok, :initializing, {:continue, :initialize_simulation}} 46 | end 47 | 48 | @impl true 49 | def terminate(_reason, _state) do 50 | Hound.Helpers.Session.end_session() 51 | end 52 | 53 | @impl true 54 | def handle_continue(:initialize_simulation, _state) do 55 | navigate_to("https://iss-sim.spacex.com/") 56 | wait_for(fn -> visible_in_page?(~r/begin/iu) end) 57 | click({:id, "begin-button"}) 58 | 59 | # Wait for fancy animation 60 | :timer.sleep(:timer.seconds(8)) 61 | 62 | {:noreply, :ready} 63 | end 64 | 65 | @impl true 66 | def handle_call({:telemetry, :roll}, _from, state) do 67 | currents = [ 68 | sensor_data(:roll, :error), 69 | sensor_data(:roll, :rate) 70 | ] 71 | 72 | {:reply, currents, state} 73 | end 74 | 75 | @impl true 76 | def handle_call({:telemetry, :pitch}, _from, state) do 77 | currents = [ 78 | sensor_data(:pitch, :error), 79 | sensor_data(:pitch, :rate) 80 | ] 81 | 82 | {:reply, currents, state} 83 | end 84 | 85 | @impl true 86 | def handle_call({:telemetry, :yaw}, _from, state) do 87 | currents = [ 88 | sensor_data(:yaw, :error), 89 | sensor_data(:yaw, :rate) 90 | ] 91 | 92 | {:reply, currents, state} 93 | end 94 | 95 | @impl true 96 | def handle_call({:telemetry, {:axis, :x}}, _from, state) do 97 | {:reply, [sensor_data(:position, :x)], state} 98 | end 99 | 100 | @impl true 101 | def handle_call({:telemetry, {:axis, :y}}, _from, state) do 102 | {:reply, [sensor_data(:position, :y)], state} 103 | end 104 | 105 | @impl true 106 | def handle_call({:telemetry, {:axis, :z}}, _from, state) do 107 | {:reply, [sensor_data(:position, :z)], state} 108 | end 109 | 110 | @impl true 111 | def handle_cast({:pilot, device, direction}, state) do 112 | actuator_action(device, direction) 113 | {:noreply, state} 114 | end 115 | 116 | # Implementation 117 | 118 | defp sensor_xpath(:roll, :error), do: ~s(//*[@id="roll"]/div[1]) 119 | defp sensor_xpath(:roll, :rate), do: ~s(//*[@id="roll"]/div[2]) 120 | 121 | defp sensor_xpath(:pitch, :error), do: ~s(//*[@id="pitch"]/div[1]) 122 | defp sensor_xpath(:pitch, :rate), do: ~s(//*[@id="pitch"]/div[2]) 123 | 124 | defp sensor_xpath(:yaw, :error), do: ~s(//*[@id="yaw"]/div[1]) 125 | defp sensor_xpath(:yaw, :rate), do: ~s(//*[@id="yaw"]/div[2]) 126 | 127 | defp sensor_xpath(:position, :x), do: ~s(//*[@id="x-range"]/div) 128 | defp sensor_xpath(:position, :y), do: ~s(//*[@id="y-range"]/div) 129 | defp sensor_xpath(:position, :z), do: ~s(//*[@id="z-range"]/div) 130 | 131 | defp sensor_data(sensor, dimension) do 132 | current_readings = 133 | find_element(:xpath, sensor_xpath(sensor, dimension)) 134 | |> inner_text 135 | 136 | {sensor, dimension, current_readings} 137 | end 138 | 139 | defp actuator_xpath(:roll, :clockwise), do: ~s(//*[@id="roll-right-button"]) 140 | defp actuator_xpath(:roll, :counter_clockwise), do: ~s(//*[@id="roll-left-button"]) 141 | 142 | defp actuator_xpath(:pitch, :up), do: ~s(//*[@id="pitch-up-button"]) 143 | defp actuator_xpath(:pitch, :down), do: ~s(//*[@id="pitch-down-button"]) 144 | 145 | defp actuator_xpath(:yaw, :left), do: ~s(//*[@id="yaw-left-button"]) 146 | defp actuator_xpath(:yaw, :right), do: ~s(//*[@id="yaw-right-button"]) 147 | 148 | defp actuator_xpath(:translate, :up), do: ~s(//*[@id="translate-up-button"]) 149 | defp actuator_xpath(:translate, :down), do: ~s(//*[@id="translate-down-button"]) 150 | defp actuator_xpath(:translate, :left), do: ~s(//*[@id="translate-left-button"]) 151 | defp actuator_xpath(:translate, :right), do: ~s(//*[@id="translate-right-button"]) 152 | defp actuator_xpath(:translate, :forward), do: ~s(//*[@id="translate-forward-button"]) 153 | defp actuator_xpath(:translate, :backward), do: ~s(//*[@id="translate-backward-button"]) 154 | 155 | defp actuator_action(device, direction) do 156 | try do 157 | click({:xpath, actuator_xpath(device, direction)}) 158 | catch 159 | kind, failure -> 160 | Logger.debug(device: device, kind: kind, failure: failure) 161 | :actuator_error 162 | end 163 | end 164 | 165 | defp wait_for(fun) do 166 | if fun.() do 167 | :ok 168 | else 169 | :timer.sleep(200) 170 | wait_for(fun) 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/autopilot/yaw.ex: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.Yaw do 2 | @moduledoc false 3 | 4 | use Autopilot.PidController, sensor: :yaw 5 | 6 | def set_output!(output, current_rate) do 7 | output = Float.round(output, 2) 8 | 9 | cond do 10 | current_rate == output -> :ok 11 | current_rate < output -> Autopilot.Simulation.yaw(:right) 12 | current_rate > output -> Autopilot.Simulation.yaw(:left) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Autopilot.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :autopilot, 7 | version: "0.1.0", 8 | elixir: "~> 1.13", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger], 18 | mod: {Autopilot.Application, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | {:hound, "~> 1.1"}, 28 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 29 | {:dialyxir, "~> 1.1", only: [:dev], runtime: false} 30 | ] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, 4 | "credo": {:hex, :credo, "1.6.3", "0a9f8925dbc8f940031b789f4623fc9a0eea99d3eed600fe831e403eb96c6a83", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1167cde00e6661d740fc54da2ee268e35d3982f027399b64d3e2e83af57a1180"}, 5 | "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, 6 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 7 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 8 | "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, 9 | "hound": {:hex, :hound, "1.1.1", "d3afce4cf0f446331d9d00427e9eb74fa135c296b1d3745d4bbe2096ce259087", [:mix], [{:hackney, "~> 1.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8c6342b49f53bb0e5c51d5ecca18a8ce872c44da05a8ce6f828385ebd744fe2a"}, 10 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 11 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 12 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 13 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 14 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 15 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 16 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 17 | } 18 | -------------------------------------------------------------------------------- /test/autopilot_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AutopilotTest do 2 | use ExUnit.Case 3 | doctest Autopilot 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------