From e5af29f06c81382ee870ff276911856a10e67956 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Sun, 8 Mar 2020 22:14:51 +0200 Subject: [PATCH] Initial commit --- .formatter.exs | 4 ++ .gitignore | 19 +++++ .tool-versions | 2 + README.md | 32 +++++++++ config/config.exs | 52 ++++++++++++++ config/target.exs | 58 +++++++++++++++ lib/application.ex | 48 +++++++++++++ lib/game/button_input.ex | 135 ++++++++++++++++++++++++++++++++++ lib/game/manager.ex | 0 lib/game/types.ex | 5 ++ lib/test.ex | 20 ++++++ mix.exs | 66 +++++++++++++++++ mix.lock | 36 ++++++++++ rel/vm.args.eex | 43 +++++++++++ rootfs_overlay/etc/iex.exs | 18 +++++ test/ex_speed_game_test.exs | 8 +++ test/test_helper.exs | 1 + upload.sh | 139 ++++++++++++++++++++++++++++++++++++ 18 files changed, 686 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 .tool-versions create mode 100644 README.md create mode 100644 config/config.exs create mode 100644 config/target.exs create mode 100644 lib/application.ex create mode 100644 lib/game/button_input.ex create mode 100644 lib/game/manager.ex create mode 100644 lib/game/types.ex create mode 100644 lib/test.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 rel/vm.args.eex create mode 100644 rootfs_overlay/etc/iex.exs create mode 100644 test/ex_speed_game_test.exs create mode 100644 test/test_helper.exs create mode 100755 upload.sh diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c2b50f --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +.elixir_ls diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..e92fe88 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.10.2-otp-22 +erlang 22.2.8 diff --git a/README.md b/README.md new file mode 100644 index 0000000..be898c8 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# ExSpeedGame + +**TODO: Add description** + +## Targets + +Nerves applications produce images for hardware targets based on the +`MIX_TARGET` environment variable. If `MIX_TARGET` is unset, `mix` builds an +image that runs on the host (e.g., your laptop). This is useful for executing +logic tests, running utilities, and debugging. Other targets are represented by +a short name like `rpi3` that maps to a Nerves system image for that platform. +All of this logic is in the generated `mix.exs` and may be customized. For more +information about targets see: + +https://hexdocs.pm/nerves/targets.html#content + +## Getting Started + +To start your Nerves app: + * `export MIX_TARGET=my_target` or prefix every command with + `MIX_TARGET=my_target`. For example, `MIX_TARGET=rpi3` + * Install dependencies with `mix deps.get` + * Create firmware with `mix firmware` + * Burn to an SD card with `mix firmware.burn` + +## Learn more + + * Official docs: https://hexdocs.pm/nerves/getting-started.html + * Official website: https://nerves-project.org/ + * Forum: https://elixirforum.com/c/nerves-forum + * Discussion Slack elixir-lang #nerves ([Invite](https://elixir-slackin.herokuapp.com/)) + * Source: https://github.com/nerves-project/nerves diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..ed2be68 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,52 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. +import Config + +config :ex_speed_game, + target: Mix.target(), + + # Set to true to enable debug prints (via serial console) and false to disable. + debug: true, + + # Amount of buttons in the game. + buttons: 4, + + # Pins of the buttons, in order from left to right in the case. Leftmost will be 0, and number + # will increase to the right. + button_pins: [17, 27, 22, 23], + + # Pins of the LED lights of the buttons. + button_light_pins: [], + + # Debounce delay, i.e. how long a button must be high or low before it is accepted, to reduce + # spurious inputs. In milliseconds. + debounce_delay: 20, + + # Delay at start of game between beeps, in milliseconds. + delay_start: 570, + + # Maximum amount of beeps you can be "behind" before the game is stopped. + max_waiting: 20 + +# Customize non-Elixir parts of the firmware. See +# https://hexdocs.pm/nerves/advanced-configuration.html for details. + +config :nerves, :firmware, rootfs_overlay: "rootfs_overlay" + +# Set the SOURCE_DATE_EPOCH date for reproducible builds. +# See https://reproducible-builds.org/docs/source-date-epoch/ for more information + +config :nerves, source_date_epoch: "1583172807" + +# Use Ringlogger as the logger backend and remove :console. +# See https://hexdocs.pm/ring_logger/readme.html for more information on +# configuring ring_logger. + +config :logger, backends: [RingLogger] + +if Mix.target() != :host do + import_config "target.exs" +end diff --git a/config/target.exs b/config/target.exs new file mode 100644 index 0000000..769c045 --- /dev/null +++ b/config/target.exs @@ -0,0 +1,58 @@ +import Config + +# Use shoehorn to start the main application. See the shoehorn +# docs for separating out critical OTP applications such as those +# involved with firmware updates. + +config :shoehorn, + init: [:nerves_runtime, :nerves_init_gadget], + app: Mix.Project.config()[:app] + +# Nerves Runtime can enumerate hardware devices and send notifications via +# SystemRegistry. This slows down startup and not many programs make use of +# this feature. + +config :nerves_runtime, :kernel, use_system_registry: false + +# Authorize the device to receive firmware using your public key. +# See https://hexdocs.pm/nerves_firmware_ssh/readme.html for more information +# on configuring nerves_firmware_ssh. + +keys = + [ + Path.join([System.user_home!(), ".ssh", "id_rsa.pub"]), + Path.join([System.user_home!(), ".ssh", "id_ecdsa.pub"]), + Path.join([System.user_home!(), ".ssh", "id_ed25519.pub"]) + ] + |> Enum.filter(&File.exists?/1) + +if keys == [], + do: + Mix.raise(""" + No SSH public keys found in ~/.ssh. An ssh authorized key is needed to + log into the Nerves device and update firmware on it using ssh. + See your project's config.exs for this error message. + """) + +config :nerves_firmware_ssh, + authorized_keys: Enum.map(keys, &File.read!/1) + +# Configure nerves_init_gadget. +# See https://hexdocs.pm/nerves_init_gadget/readme.html for more information. + +# Setting the node_name will enable Erlang Distribution. +# Only enable this for prod if you understand the risks. +node_name = if Mix.env() != :prod, do: "ex_speed_game" + +config :nerves_init_gadget, + ifname: "usb0", + address_method: :dhcpd, + mdns_domain: "nerves.local", + node_name: node_name, + node_host: :mdns_domain + +# Import target specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +# Uncomment to use target specific configurations + +# import_config "#{Mix.target()}.exs" diff --git a/lib/application.ex b/lib/application.ex new file mode 100644 index 0000000..044098e --- /dev/null +++ b/lib/application.ex @@ -0,0 +1,48 @@ +defmodule ExSpeedGame.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + def start(_type, _args) do + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: ExSpeedGame.Supervisor] + + children = + [ + # Children for all targets + # Starts a worker by calling: ExSpeedGame.Worker.start_link(arg) + # {ExSpeedGame.Worker, arg}, + ] ++ children(target()) + + Supervisor.start_link(children, opts) + end + + # List all child processes to be supervised + def children(:host) do + [ + # Children that only run on the host + # Starts a worker by calling: ExSpeedGame.Worker.start_link(arg) + # {ExSpeedGame.Worker, arg}, + ] + end + + def children(_target) do + pins = Application.get_env(:ex_speed_game, :button_pins) + debounce_delay = Application.get_env(:ex_speed_game, :debounce_delay) + + [ + # Children for all targets except host + # Starts a worker by calling: ExSpeedGame.Worker.start_link(arg) + {ExSpeedGame.Game.ButtonInput, + {{pins, debounce_delay}, name: ExSpeedGame.Game.ButtonInput}}, + {ExSpeedGame.Test, []} + ] + end + + def target() do + Application.get_env(:ex_speed_game, :target) + end +end diff --git a/lib/game/button_input.ex b/lib/game/button_input.ex new file mode 100644 index 0000000..79d45c0 --- /dev/null +++ b/lib/game/button_input.ex @@ -0,0 +1,135 @@ +defmodule ExSpeedGame.Game.ButtonInput do + use GenServer + alias ExSpeedGame.Game.Types + require Logger + + defmodule State do + @type t :: %__MODULE__{ + pins: Types.pins(), + debounce_delay: integer(), + listener: pid() | nil, + pin_refs: [reference()], + pin_timers: %{optional(Types.pin()) => reference()}, + active: boolean() + } + + @enforce_keys [:pins, :debounce_delay] + defstruct [:pins, :debounce_delay, :listener, pin_refs: [], pin_timers: %{}, active: false] + end + + defmodule InputError do + defexception [:message] + end + + ### SERVER INTERFACE + + @spec start_link({{[Types.pins()], integer()}, keyword()}) :: + :ignore | {:error, any} | {:ok, pid} + def start_link({init, opts}) do + GenServer.start_link(__MODULE__, init, opts) + end + + @impl true + @spec init({Types.pins(), integer()}) :: {:ok, State.t()} + def init({pins, debounce_delay}) do + {:ok, %State{pins: pins, debounce_delay: debounce_delay}} + end + + @impl true + def handle_call(msg, from, state) + + # Acquire + + @spec handle_call(:acquire, GenServer.from(), %State{active: false}) :: {:reply, :ok, State.t()} + def handle_call(:acquire, {from, _}, %State{active: false} = state) do + pin_refs = for pin <- state.pins, do: init_pin(pin) + + {:reply, :ok, %State{state | listener: from, pin_refs: pin_refs, active: true}} + end + + @spec handle_call(:acquire, any, %State{active: true}) :: no_return() + def handle_call(:acquire, _from, %State{active: true, listener: l}) do + raise InputError, message: "ButtonInput already acquired to #{l}" + end + + # Release + + @spec handle_call(:release, any, %State{active: true}) :: {:reply, :ok, State.t()} + def handle_call(:release, _from, %State{active: true} = state) do + # Circuits.GPIO will free resources automatically when the pin refs are GCd + {:reply, :ok, %State{state | listener: nil, pin_refs: %{}}} + end + + @spec handle_call(:release, any, %State{active: false}) :: no_return() + def handle_call(:release, _from, %State{active: false}) do + raise InputError, message: "ButtonInput already released" + end + + @impl true + def handle_info(msg, state) + + @spec handle_info({:circuits_gpio, Types.pin(), integer(), 0 | 1}, %State{active: true}) :: + {:noreply, State.t()} + def handle_info({:circuits_gpio, pin, _time, value} = msg, %State{active: true} = state) do + Logger.debug(inspect(msg)) + + timer_ref = Map.get(state.pin_timers, pin) + + if not is_nil(timer_ref) do + Process.cancel_timer(timer_ref) + end + + pin_timers = + case value do + 0 -> + Map.delete(state.pin_timers, pin) + + 1 -> + ref = Process.send_after(self(), {:debounce, pin}, state.debounce_delay) + Map.put(state.pin_timers, pin, ref) + end + + {:noreply, %State{state | pin_timers: pin_timers}} + end + + @spec handle_info({:debounce, Types.pin()}, %State{active: true}) :: {:noreply, State.t()} + def handle_info({:debounce, pin}, %State{active: true} = state) do + send(state.listener, {:input, pin}) + + pin_timers = Map.delete(state.pin_timers, pin) + {:noreply, %State{state | pin_timers: pin_timers}} + end + + # Discard all other messages + @spec handle_info(any, %State{active: false}) :: {:noreply, State.t()} + def handle_info(_msg, %State{active: false} = state) do + {:noreply, state} + end + + @spec init_pin(Types.pin()) :: reference() + defp init_pin(pin) do + {:ok, ref} = Circuits.GPIO.open(pin, :input, pull_mode: :pulldown) + :ok = Circuits.GPIO.set_interrupts(ref, :both) + ref + end + + ### CLIENT INTERFACE + + @doc """ + Acquire the button input for current process and start monitoring the buttons. + + When this has been called, the ButtonInput process will start sending messages for button events. + """ + @spec acquire(GenServer.server()) :: term + def acquire(server) do + GenServer.call(server, :acquire) + end + + @doc """ + Release the button input and stop monitoring the buttons. + """ + @spec release(GenServer.server()) :: term + def release(server) do + GenServer.call(server, :release) + end +end diff --git a/lib/game/manager.ex b/lib/game/manager.ex new file mode 100644 index 0000000..e69de29 diff --git a/lib/game/types.ex b/lib/game/types.ex new file mode 100644 index 0000000..d1f7760 --- /dev/null +++ b/lib/game/types.ex @@ -0,0 +1,5 @@ +defmodule ExSpeedGame.Game.Types do + @type pin :: Circuits.GPIO.pin_number() + @type pins :: [pin] + @type led_pins :: [pin] +end diff --git a/lib/test.ex b/lib/test.ex new file mode 100644 index 0000000..7c62892 --- /dev/null +++ b/lib/test.ex @@ -0,0 +1,20 @@ +defmodule ExSpeedGame.Test do + use GenServer + require Logger + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts) + end + + @impl true + def init(_) do + ExSpeedGame.Game.ButtonInput.acquire(ExSpeedGame.Game.ButtonInput) + {:ok, :ok} + end + + @impl true + def handle_info(msg, s) do + Logger.debug(inspect(msg)) + {:noreply, s} + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..2b58dff --- /dev/null +++ b/mix.exs @@ -0,0 +1,66 @@ +defmodule ExSpeedGame.MixProject do + use Mix.Project + + @app :ex_speed_game + @version "0.1.0" + @all_targets [:rpi0] + + def project do + [ + app: @app, + version: @version, + elixir: "~> 1.10", + archives: [nerves_bootstrap: "~> 1.7"], + start_permanent: Mix.env() == :prod, + build_embedded: true, + aliases: [loadconfig: [&bootstrap/1]], + deps: deps(), + releases: [{@app, release()}], + preferred_cli_target: [run: :host, test: :host] + ] + end + + # Starting nerves_bootstrap adds the required aliases to Mix.Project.config() + # Aliases are only added if MIX_TARGET is set. + def bootstrap(args) do + Application.start(:nerves_bootstrap) + Mix.Task.run("loadconfig", args) + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + mod: {ExSpeedGame.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # Dependencies for all targets + {:nerves, "~> 1.5.0", runtime: false}, + {:shoehorn, "~> 0.6"}, + {:ring_logger, "~> 0.6"}, + {:toolshed, "~> 0.2"}, + {:circuits_gpio, "~> 0.4"}, + + # Dependencies for all targets except :host + {:nerves_runtime, "~> 0.6", targets: @all_targets}, + {:nerves_init_gadget, "~> 0.4", targets: @all_targets}, + + # Dependencies for specific targets + {:nerves_system_rpi0, "~> 1.10", runtime: false, targets: :rpi0} + ] + end + + def release do + [ + overwrite: true, + cookie: "#{@app}_cookie", + include_erts: &Nerves.Release.erts/0, + steps: [&Nerves.Release.init/1, :assemble], + strip_beams: Mix.env() == :prod + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..a96a4e9 --- /dev/null +++ b/mix.lock @@ -0,0 +1,36 @@ +%{ + "circuits_gpio": {:hex, :circuits_gpio, "0.4.5", "4d5b0f707c425fc56f03086232259f65482a3d1f1cf15335253636d0bb846446", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b42d28d60a6cfdfb6b21b66ab0b8c5de0ea5a32b390b61d2fe86a2ad8edb90ad"}, + "dns": {:hex, :dns, "2.1.2", "81c46d39f7934f0e73368355126e4266762cf227ba61d5889635d83b2d64a493", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm", "6818589d8e59c03a2c73001e5cd7a957f99c30a796021aa32445ea14d0f3356b"}, + "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, + "mdns": {:hex, :mdns, "1.0.3", "f08414daf5636bf5cd364611e838818e9250c91a3282a817ad9174b03e757401", [:mix], [{:dns, "~> 2.0", [hex: :dns, repo: "hexpm", optional: false]}], "hexpm", "6cb44eac9d1d71d0c5b400a383ccdc2b474d2e89a49a1e049e496637a6bab4c1"}, + "muontrap": {:hex, :muontrap, "0.5.1", "98fe96d0e616ee518860803a37a29eb23ffc2ca900047cb1bb7fd37521010093", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3c11b7f151b202148912c73cbdd633b76fa68fabc26cc441c9d6d140e22290dc"}, + "nerves": {:hex, :nerves, "1.5.4", "d5a2a29a642e92277d5680f40d0fadff17796e75faa82de87ba0bc920ffcf818", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "283ce855f369ff209f3358d25e58f1ac941d58aef3ce7e8cc0be9919d25bf4f5"}, + "nerves_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.4.4", "12b0d9c84ec9f79c1b0ac0de1c575372ef972d0c58ce21c36bf354062c6222d9", [:mix], [{:nerves_runtime, "~> 0.6", [hex: :nerves_runtime, repo: "hexpm", optional: false]}], "hexpm", "98c40104d0d2c6e6e8cce22f8c8fd8ad5b4b97f8694e42a9101ca44befac38f0"}, + "nerves_init_gadget": {:hex, :nerves_init_gadget, "0.7.0", "7402b190a7354fc4df53e707e1a8e421352ac6c3dba4262273d1f1a55bbb019b", [:mix], [{:mdns, "~> 1.0", [hex: :mdns, repo: "hexpm", optional: false]}, {:nerves_firmware_ssh, "~> 0.2", [hex: :nerves_firmware_ssh, repo: "hexpm", optional: false]}, {:nerves_network, "~> 0.3", [hex: :nerves_network, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.3", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:nerves_time, "~> 0.2", [hex: :nerves_time, repo: "hexpm", optional: false]}, {:one_dhcpd, "~> 0.1", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:ring_logger, "~> 0.4", [hex: :ring_logger, repo: "hexpm", optional: false]}], "hexpm", "989df1fd939545ae3f1a7185d7447273f7700b0a0bc204fbb1b18e03e6d38ad3"}, + "nerves_network": {:hex, :nerves_network, "0.5.5", "4690c362707f76c4072810bd9639b2ae8eb7dd9c21119656308b462a087230aa", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nerves_network_interface, "~> 0.4.4", [hex: :nerves_network_interface, repo: "hexpm", optional: false]}, {:nerves_wpa_supplicant, "~> 0.5", [hex: :nerves_wpa_supplicant, repo: "hexpm", optional: false]}, {:one_dhcpd, "~> 0.2.0", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:system_registry, "~> 0.7", [hex: :system_registry, repo: "hexpm", optional: false]}], "hexpm", "5e63529c6e128d147f5a6df82bd7daffd211057b8ac0c8ba625939a7fde9ccff"}, + "nerves_network_interface": {:hex, :nerves_network_interface, "0.4.6", "d50e57daca8154f0f780fd98eb5ae94a005579e0d72d69840e80e228375d88ad", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "94eb89db67ceb17a7f3465d55c05b00e8d8cf10aa812556745ce0c06868768d3"}, + "nerves_runtime": {:hex, :nerves_runtime, "0.11.0", "96787c3935ec1a4943c8fcd63ce6ed1f2301f5f28eac41db800f3fafe0043f1f", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:system_registry, "~> 0.8.0", [hex: :system_registry, repo: "hexpm", optional: false]}, {:uboot_env, "~> 0.1.1", [hex: :uboot_env, repo: "hexpm", optional: false]}], "hexpm", "7adc7f7163422bddd569daadcb5d9ceb3010687afcba9f3c147ad17361417d7e"}, + "nerves_system_bbb": {:hex, :nerves_system_bbb, "2.5.2", "9fb8744f61bfcca57e8a2c9273ff7fe484e329b15275fadcbec3a910a6b8ac11", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "210fb7dd964cd6c5d2f9171d5198cf6d2ec52e0355351cabe6f329a7156064b3"}, + "nerves_system_br": {:hex, :nerves_system_br, "1.10.2", "2cbf767e9da2c526c3a00720a3f7212b861735fed41f3abcbc2af4a960731d7c", [:mix], [], "hexpm", "e344a65a27c116aacbd8d165920af54bf436edcdcf7db91aa4a39f491fcffe31"}, + "nerves_system_linter": {:hex, :nerves_system_linter, "0.3.0", "84e0f63c8ac196b16b77608bbe7df66dcf352845c4e4fb394bffd2b572025413", [:mix], [], "hexpm", "bffbdfb116bc72cde6e408c34c0670b199846e9a8f0953cc1c9f1eea693821a1"}, + "nerves_system_rpi": {:hex, :nerves_system_rpi, "1.10.2", "b30bfa5470a2ac97aa200dbceee4261c625c5d18f241c684cb0477bf6acca07d", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", [hex: :nerves_toolchain_armv6_rpi_linux_gnueabi, repo: "hexpm", optional: false]}], "hexpm", "7548bf9a99be7ceef723ca016317aaeed8ff4dc8009f557ae53df8eac651d077"}, + "nerves_system_rpi0": {:hex, :nerves_system_rpi0, "1.10.2", "5d4dbc3163d3a798bb7c2f52555e4b11cb1beab9e4ef325272aa15cd2cc5119a", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", [hex: :nerves_toolchain_armv6_rpi_linux_gnueabi, repo: "hexpm", optional: false]}], "hexpm", "834de3a59448ea756724a70ccf177a948fd1e12f3229ab3f294469b64e20f0ee"}, + "nerves_system_rpi2": {:hex, :nerves_system_rpi2, "1.10.2", "d4ddc6e0fd85f48b2aba5a3c582d3f571bc84fabe9dbcc7e9d48a3624572dc98", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "53dd43843cdf4e98dc7453c981aab69fa84449a09de414a291dd23aecc7d8721"}, + "nerves_system_rpi3": {:hex, :nerves_system_rpi3, "1.10.2", "a5efd36ce80940c427c386383cf0ba1d97b839822639573d3788e7cc25febf81", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "12f73065b61d87d0cda023424cf91e37fd64957f726380994c12a8d0fddca5a3"}, + "nerves_system_rpi3a": {:hex, :nerves_system_rpi3a, "1.10.2", "f084f2c382500179e762e6e8b0750f34b8741d4f451cfbdb9eff99a694b916ee", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "26112289a7064c2a11f942e36ae02c69a54e2a9e97fe0194b885a46241073541"}, + "nerves_system_rpi4": {:hex, :nerves_system_rpi4, "1.10.2", "af0f7d1985d849f571fd300f8e4f9e16dec6e82f0d7f1a67b293fd0f79c4d5d2", [:mix], [{:nerves, "~> 1.5", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "f146d4db7ec7623af2706b5d7fb2f27b5235019fe437d01223d02cfca0404d45"}, + "nerves_system_x86_64": {:hex, :nerves_system_x86_64, "1.10.2", "20f0660c86616a9e0d373ef1e98068a7f05a1ccdd2b4282df2365f70a70ce194", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_x86_64_unknown_linux_musl, "1.2.0", [hex: :nerves_toolchain_x86_64_unknown_linux_musl, repo: "hexpm", optional: false]}], "hexpm", "57359eb25f0d0bfccfd73ff9a590b0c53c8bec328325330804a22af80453ed38"}, + "nerves_time": {:hex, :nerves_time, "0.4.0", "8c2a6be23df71fe331407d1ab0c61334d8159c4acd55f777a0c835a6b6f81483", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm", "51dd7a34fa346647214f94b201cf524b182c62c4f9f44656bc1910823028dd59"}, + "nerves_toolchain_arm_unknown_linux_gnueabihf": {:hex, :nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", "ba48ce7c846ee12dfca8148dc7240988d96a3f2eb9c234bf08bffe4f0f7a3c62", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.6.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "18df425fee48a9088bf941d3615c677b818b537310123c4b4c90b710e4a34180"}, + "nerves_toolchain_armv6_rpi_linux_gnueabi": {:hex, :nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", "007668c7ad1f73bad8fd54ad1a27a3b0fb91bca51b4af6bb3bbdac968ccae0ba", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.6.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "4e843f405d4b8e6137419f94e5b8f491bff5a87b02ac2223e126182e8cec4256"}, + "nerves_toolchain_ctng": {:hex, :nerves_toolchain_ctng, "1.6.0", "452f8589c1a58ac787477caab20a8cfc6671e345837ccc19beefe49ae35ba983", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}], "hexpm", "7ee5744dc606c6debf3e459ef122e77c13d6a1be9e093f7e29af3759896f9dbb"}, + "nerves_toolchain_x86_64_unknown_linux_musl": {:hex, :nerves_toolchain_x86_64_unknown_linux_musl, "1.2.0", "fbe688fa561b03190765e269d4336333c4961a48d2acd3f6cb283443a058e138", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.6.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "9cb676b7fb7a4564b18e1e16f72bc64f06f77346a477033fde6e4c2d1cc2ecff"}, + "nerves_wpa_supplicant": {:hex, :nerves_wpa_supplicant, "0.5.2", "4ec392fc08faf35f50d1070446c2e5019f6b85bd53f5716f904e3f75716d9596", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "c286ed5397fa185ab986226596eeb72d8f5f9f1b1156103418193f85905da713"}, + "one_dhcpd": {:hex, :one_dhcpd, "0.2.4", "2664f2e1ac72cbae0474a88d1a3d55019ccc3ee582ded382589511627a8ed516", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "38f08c228d066153dbe352b150910345e395eacd2db3c19085d54c0baeeebacb"}, + "ring_logger": {:hex, :ring_logger, "0.8.0", "b1baddc269099b2afe2ea3a87b8e2b71e57331c0000038ae55090068aac679db", [:mix], [], "hexpm", "9b2f482e4346c13c11ef555f898202d0ddbfda6e2354e5c6e0559d2b4e0cf781"}, + "shoehorn": {:hex, :shoehorn, "0.6.0", "f9a1b7d6212cf18ba91c4f71c26076059df33cea4db2eb3c098bfa6673349412", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}], "hexpm", "e54a1f58a121caf8f0f3a355686b2661258b1bc0d4fffef8923bd7b11c2f9d79"}, + "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"}, + "system_registry": {:hex, :system_registry, "0.8.2", "df791dc276652fcfb53be4dab823e05f8269b96ac57c26f86a67838dbc0eefe7", [:mix], [], "hexpm", "f7acdede22c73ab0b3735eead7f2095efb2a7a6198366564205274db2ca2a8f8"}, + "toolshed": {:hex, :toolshed, "0.2.11", "0cd5312bd6a48f5b654b6ffa9239a63af9f3d200da414790fe25f066e14842a9", [:mix], [{:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: true]}], "hexpm", "f22ae95d77136f9f7db93cddd40d42bc8252d825f15a772a17d4c7947b6faad5"}, + "uboot_env": {:hex, :uboot_env, "0.1.1", "b01e3ec0973e99473234f27839e29e63b5b81eba6a136a18a78d049d4813d6c5", [:mix], [], "hexpm", "f7b82da0cb40c8db9c9fb1fc977780ab0c28d961ec1f3c7ab265c4352e4141ae"}, +} diff --git a/rel/vm.args.eex b/rel/vm.args.eex new file mode 100644 index 0000000..7731ff5 --- /dev/null +++ b/rel/vm.args.eex @@ -0,0 +1,43 @@ +## Add custom options here + +## Distributed Erlang Options +## The cookie needs to be configured prior to vm boot for +## for read only filesystem. + +-setcookie <%= @release.options[:cookie] %> + +## Use Ctrl-C to interrupt the current shell rather than invoking the emulator's +## break handler and possibly exiting the VM. ++Bc + +# Allow time warps so that the Erlang system time can more closely match the +# OS system time. ++C multi_time_warp + +## Load code at system startup +## See http://erlang.org/doc/system_principles/system_principles.html#code-loading-strategy +-mode embedded + +## Save the shell history between reboots +## See http://erlang.org/doc/man/kernel_app.html for additional options +-kernel shell_history enabled + +## Enable heartbeat monitoring of the Erlang runtime system +-heart -env HEART_BEAT_TIMEOUT 30 + +## Start the Elixir shell + +-noshell +-user Elixir.IEx.CLI + +## Enable colors in the shell +-elixir ansi_enabled true + +## Options added after -extra are interpreted as plain arguments and can be +## retrieved using :init.get_plain_arguments(). Options before the "--" are +## interpreted by Elixir and anything afterwards is left around for other IEx +## and user applications. +-extra --no-halt +-- +--dot-iex /etc/iex.exs + diff --git a/rootfs_overlay/etc/iex.exs b/rootfs_overlay/etc/iex.exs new file mode 100644 index 0000000..6c94f7e --- /dev/null +++ b/rootfs_overlay/etc/iex.exs @@ -0,0 +1,18 @@ +# Add Toolshed helpers to the IEx session +use Toolshed + +if RingLogger in Application.get_env(:logger, :backends, []) do + IO.puts """ + RingLogger is collecting log messages from Elixir and Linux. To see the + messages, either attach the current IEx session to the logger: + + RingLogger.attach + + or print the next messages in the log: + + RingLogger.next + """ +end + +# Be careful when adding to this file. Nearly any error can crash the VM and +# cause a reboot. diff --git a/test/ex_speed_game_test.exs b/test/ex_speed_game_test.exs new file mode 100644 index 0000000..ddf8919 --- /dev/null +++ b/test/ex_speed_game_test.exs @@ -0,0 +1,8 @@ +defmodule ExSpeedGameTest do + use ExUnit.Case + doctest ExSpeedGame + + test "greets the world" do + assert ExSpeedGame.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/upload.sh b/upload.sh new file mode 100755 index 0000000..1291da0 --- /dev/null +++ b/upload.sh @@ -0,0 +1,139 @@ +#!/bin/sh + +# +# Upload new firmware to a target running nerves_firmware_ssh +# +# Usage: +# upload.sh [destination IP] [Path to .fw file] +# +# If unspecifed, the destination is nerves.local and the .fw file is naively +# guessed +# +# You may want to add the following to your `~/.ssh/config` to avoid recording +# the IP addresses of the target: +# +# Host nerves.local +# UserKnownHostsFile /dev/null +# StrictHostKeyChecking no +# +# The firmware update protocol is: +# +# 1. Connect to the nerves_firmware_ssh service running on port 8989 +# 2. Send "fwup:$FILESIZE,reboot\n" where `$FILESIZE` is the size of the file +# being uploaded +# 3. Send the firmware file +# 4. The response from the device is a progress bar from fwup that can either +# be ignored or shown to the user. +# 5. The ssh connection is closed with an exit code to indicate success or +# failure +# +# Feel free to copy this script whereever is convenient. The template is at +# https://github.com/nerves-project/nerves_firmware_ssh/blob/master/priv/templates/script.upload.eex +# + +set -e + +DESTINATION=$1 +FILENAME="$2" + +help() { + echo + echo "upload.sh [destination IP] [Path to .fw file]" + echo + echo "Default destination IP is 'nerves.local'" + echo "Default firmware bundle is the first .fw file in '_build/\${MIX_TARGET}_\${MIX_ENV}/nerves/images'" + echo + echo "MIX_TARGET=$MIX_TARGET" + echo "MIX_ENV=$MIX_ENV" + exit 1 +} + +[ -n "$DESTINATION" ] || DESTINATION=nerves.local +[ -n "$MIX_TARGET" ] || MIX_TARGET=rpi0 +[ -n "$MIX_ENV" ] || MIX_ENV=dev +if [ -z "$FILENAME" ]; then + FIRMWARE_PATH="./_build/${MIX_TARGET}_${MIX_ENV}/nerves/images" + if [ ! -d "$FIRMWARE_PATH" ]; then + # Try the Nerves 1.4 path if the user hasn't upgraded their mix.exs + FIRMWARE_PATH="./_build/${MIX_TARGET}/${MIX_TARGET}_${MIX_ENV}/nerves/images" + if [ ! -d "$FIRMWARE_PATH" ]; then + # Try the pre-Nerves 1.4 path + FIRMWARE_PATH="./_build/${MIX_TARGET}/${MIX_ENV}/nerves/images" + if [ ! -d "$FIRMWARE_PATH" ]; then + echo "Can't find the build products. Specify path to .fw file or try running 'mix firmware'" + exit 1 + fi + fi + fi + + FILENAME=$(ls "$FIRMWARE_PATH/"*.fw 2> /dev/null | head -n 1) +fi + +[ -n "$FILENAME" ] || (echo "Error: error determining firmware bundle."; help) +[ -f "$FILENAME" ] || (echo "Error: can't find '$FILENAME'"; help) + +# Check the flavor of stat for sending the filesize +if stat --version 2>/dev/null | grep GNU >/dev/null; then + # The QNU way + FILESIZE=$(stat -c%s "$FILENAME") +else + # Else default to the BSD way + FILESIZE=$(stat -f %z "$FILENAME") +fi + +FIRMWARE_METADATA=$(fwup -m -i "$FILENAME" || echo "meta-product=Error reading metadata!") +FIRMWARE_PRODUCT=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-product=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"') +FIRMWARE_VERSION=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-version=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"') +FIRMWARE_PLATFORM=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-platform=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"') +FIRMWARE_UUID=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-uuid=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"') + +echo "Path: $FILENAME" +echo "Product: $FIRMWARE_PRODUCT $FIRMWARE_VERSION" +echo "UUID: $FIRMWARE_UUID" +echo "Platform: $FIRMWARE_PLATFORM" +echo +echo "Uploading to $DESTINATION..." + +# Don't fall back to asking for passwords, since that won't work +# and it's easy to misread the message thinking that it's asking +# for the private key password +SSH_OPTIONS="-o PreferredAuthentications=publickey" + +if [ "$(uname -s)" = "Darwin" ]; then + DESTINATION_IP=$(arp -n $DESTINATION | sed 's/.* (\([0-9.]*\).*/\1/' || exit 0) + if [ -z "$DESTINATION_IP" ]; then + echo "Can't resolve $DESTINATION" + exit 1 + fi + TEST_DESTINATION_IP=$(printf "$DESTINATION_IP" | head -n 1) + if [ "$DESTINATION_IP" != "$TEST_DESTINATION_IP" ]; then + echo "Multiple destination IP addresses for $DESTINATION found:" + echo "$DESTINATION_IP" + echo "Guessing the first one..." + DESTINATION_IP=$TEST_DESTINATION_IP + fi + + IS_DEST_LL=$(echo $DESTINATION_IP | grep '^169\.254\.' || exit 0) + if [ -n "$IS_DEST_LL" ]; then + LINK_LOCAL_IP=$(ifconfig | grep 169.254 | sed 's/.*inet \([0-9.]*\) .*/\1/') + if [ -z "$LINK_LOCAL_IP" ]; then + echo "Can't find an interface with a link local address?" + exit 1 + fi + TEST_LINK_LOCAL_IP=$(printf "$LINK_LOCAL_IP" | tail -n 1) + if [ "$LINK_LOCAL_IP" != "$TEST_LINK_LOCAL_IP" ]; then + echo "Multiple interfaces with link local addresses:" + echo "$LINK_LOCAL_IP" + echo "Guessing the last one, but YMMV..." + LINK_LOCAL_IP=$TEST_LINK_LOCAL_IP + fi + + # If a link local address, then force ssh to bind to the link local IP + # when connecting. This fixes an issue where the ssh connection is bound + # to another Ethernet interface. The TCP SYN packet that goes out has no + # chance of working when this happens. + SSH_OPTIONS="$SSH_OPTIONS -b $LINK_LOCAL_IP" + fi +fi + +printf "fwup:$FILESIZE,reboot\n" | cat - $FILENAME | ssh -s -p 8989 $SSH_OPTIONS $DESTINATION nerves_firmware_ssh