Blah blah blah the leds work, shows IP, speed game shows lights

This commit is contained in:
Mikko Ahlroth 2020-03-23 21:52:52 +02:00
parent 94675c3dcc
commit 8abff10929
10 changed files with 491 additions and 55 deletions

View file

@ -5,6 +5,8 @@
# is restricted to this project.
import Config
button_pins = {24, 25, 5, 6}
config :ex_speed_game,
target: Mix.target(),
@ -16,20 +18,40 @@ config :ex_speed_game,
# 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],
button_pins: button_pins,
# Pins of the LED lights of the buttons.
button_light_pins: [],
# Pins of the LED lights of the buttons mapped to the button pins.
button_light_pins: {
{17, elem(button_pins, 0)},
{27, elem(button_pins, 1)},
{22, elem(button_pins, 2)},
{23, elem(button_pins, 3)}
},
# 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 at start of game between ticks, in milliseconds.
delay_start: 570,
# Maximum amount of beeps you can be "behind" before the game is stopped.
max_waiting: 20
# Delay at start of pro game
delay_pro: 321,
# Score at start of pro game
score_pro: 100,
# Maximum amount of ticks you can be "behind" before the game is stopped.
max_waiting: 20,
# Interface to show IP for when using ShowIP
iface: "usb0",
# LED pattern to show when there is a crash
crash_pattern: {false, true, true, false},
# Time to show crash pattern for
crash_pattern_delay: 3_000
# Customize non-Elixir parts of the firmware. See
# https://hexdocs.pm/nerves/advanced-configuration.html for details.

View file

@ -5,6 +5,14 @@ defmodule ExSpeedGame.Application do
use Application
alias ExSpeedGame.Game.{
ButtonInput,
Lights,
Menu
}
alias ExSpeedGame.Game.Supervisor, as: GameSupervisor
def start(_type, _args) do
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
@ -31,14 +39,17 @@ defmodule ExSpeedGame.Application do
def children(_target) do
pins = Application.get_env(:ex_speed_game, :button_pins)
led_pins = Application.get_env(:ex_speed_game, :button_light_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, []}
{ButtonInput,
%ButtonInput.Options{pins: pins, debounce_delay: debounce_delay, name: ButtonInput}},
{Lights, %Lights.Options{light_pins: led_pins, name: Lights}},
{Menu, %Menu.Options{name: Menu}},
{DynamicSupervisor, strategy: :one_for_one, name: GameSupervisor}
]
end

View file

@ -3,6 +3,16 @@ defmodule ExSpeedGame.Game.ButtonInput do
alias ExSpeedGame.Game.Types
require Logger
defmodule Options do
@type t :: %__MODULE__{
pins: Types.pins(),
debounce_delay: integer(),
name: GenServer.name()
}
@enforce_keys [:pins, :debounce_delay, :name]
defstruct [:pins, :debounce_delay, :name]
end
defmodule State do
@type t :: %__MODULE__{
pins: Types.pins(),
@ -23,16 +33,20 @@ defmodule ExSpeedGame.Game.ButtonInput do
### 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)
@spec start_link(Options.t()) :: :ignore | {:error, any} | {:ok, pid}
def start_link(%Options{} = opts) do
GenServer.start_link(
__MODULE__,
%{pins: opts.pins, debounce_delay: opts.debounce_delay},
name: opts.name
)
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}}
@spec init(map()) :: {:ok, State.t()}
def init(%{pins: pins, debounce_delay: debounce_delay}) do
pin_refs = for pin <- Tuple.to_list(pins), do: init_pin(pin)
{:ok, %State{pins: pins, debounce_delay: debounce_delay, pin_refs: pin_refs}}
end
@impl true
@ -42,9 +56,7 @@ defmodule ExSpeedGame.Game.ButtonInput do
@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}}
{:reply, :ok, %State{state | listener: from, active: true}}
end
@spec handle_call(:acquire, any, %State{active: true}) :: no_return()
@ -56,13 +68,12 @@ defmodule ExSpeedGame.Game.ButtonInput do
@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: %{}}}
{:reply, :ok, %State{state | listener: nil, active: false}}
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"
@spec handle_call(:release, any, %State{active: false}) :: {:reply, :ok, State.t()}
def handle_call(:release, _from, %State{active: false} = state) do
{:reply, :ok, state}
end
@impl true
@ -81,10 +92,11 @@ defmodule ExSpeedGame.Game.ButtonInput do
pin_timers =
case value do
0 ->
# Note that due to pullup, the values here are reversed
1 ->
Map.delete(state.pin_timers, pin)
1 ->
0 ->
ref = Process.send_after(self(), {:debounce, pin}, state.debounce_delay)
Map.put(state.pin_timers, pin, ref)
end
@ -108,7 +120,7 @@ defmodule ExSpeedGame.Game.ButtonInput do
@spec init_pin(Types.pin()) :: reference()
defp init_pin(pin) do
{:ok, ref} = Circuits.GPIO.open(pin, :input, pull_mode: :pulldown)
{:ok, ref} = Circuits.GPIO.open(pin, :input, pull_mode: :pullup)
:ok = Circuits.GPIO.set_interrupts(ref, :both)
ref
end

115
lib/game/lights.ex Normal file
View file

@ -0,0 +1,115 @@
defmodule ExSpeedGame.Game.Lights do
use GenServer
use Bitwise, only_operators: true
alias ExSpeedGame.Game.Types
@type values :: {boolean(), boolean(), boolean(), boolean()}
defmodule Options do
@type t :: %__MODULE__{
light_pins: Types.led_pins(),
name: GenServer.name()
}
@enforce_keys [:light_pins, :name]
defstruct [:light_pins, :name]
end
defmodule State do
@type t :: %__MODULE__{
lights: {Types.pin(), Types.pin(), Types.pin(), Types.pin()},
references: [reference()]
}
defstruct [:lights, :references]
end
### SERVER INTERFACE
@spec start_link(Options.t()) :: :ignore | {:error, any} | {:ok, pid}
def start_link(%Options{} = opts) do
{{pin1, _}, {pin2, _}, {pin3, _}, {pin4, _}} = opts.light_pins
GenServer.start_link(
__MODULE__,
%{pins: {pin1, pin2, pin3, pin4}},
name: opts.name
)
end
@impl true
@spec init(%{pins: {Types.pin(), Types.pin(), Types.pin(), Types.pin()}}) :: {:ok, State.t()}
def init(%{pins: pins}) do
references =
for pin <- Tuple.to_list(pins) do
{:ok, ref} = Circuits.GPIO.open(pin, :output, initial_value: 0)
ref
end
{:ok, %State{lights: pins, references: references}}
end
@impl true
@spec handle_call(
{:set_lights, values()},
GenServer.from(),
State.t()
) :: {:reply, :ok, State.t()}
def handle_call({:set_lights, values}, _from, %State{references: references} = state) do
references
|> Enum.with_index()
|> Enum.each(fn {ref, i} ->
Circuits.GPIO.write(ref, elem(values, i) |> bool2val())
end)
{:reply, :ok, state}
end
@spec bool2val(boolean()) :: Circuits.GPIO.value()
defp bool2val(bool)
defp bool2val(true), do: 1
defp bool2val(false), do: 0
### CLIENT INTERFACE
@doc """
Set the lights on/off according to the given value.
"""
@spec set(GenServer.name(), values()) :: :ok | no_return()
def set(server, values) do
:ok = GenServer.call(server, {:set_lights, values})
end
@doc """
Turn off all lights.
"""
@spec clear(GenServer.name()) :: :ok | no_return()
def clear(server) do
set(server, {false, false, false, false})
end
@doc """
Show the given number on the lights as a binary pattern.
"""
@spec show_binary(GenServer.name(), integer()) :: :ok | no_return()
def show_binary(server, number) do
values = number2binary(number)
set(server, values)
end
@doc """
Set the given index on and the others off.
"""
@spec set_index(GenServer.name(), integer()) :: :ok | no_return()
def set_index(server, index) do
set(server, {index == 1, index == 2, index == 3, index == 4})
end
@spec number2binary(integer()) :: values()
defp number2binary(number) do
{
(number &&& 0b1000) != 0,
(number &&& 0b0100) != 0,
(number &&& 0b0010) != 0,
(number &&& 0b0001) != 0
}
end
end

130
lib/game/menu.ex Normal file
View file

@ -0,0 +1,130 @@
defmodule ExSpeedGame.Game.Menu do
use GenServer
require Logger
alias ExSpeedGame.Game.{Types, ButtonInput, ShowIP, Lights}
alias ExSpeedGame.Game.Modes.{
Speed
}
@menu {
{"SpeedGame", Speed,
%Speed.Options{
initial_score: 0,
initial_delay: Application.get_env(:ex_speed_game, :delay_start)
}},
{"SpeedGame Pro", Speed,
%Speed.Options{
initial_score: Application.get_env(:ex_speed_game, :score_pro),
initial_delay: Application.get_env(:ex_speed_game, :delay_pro)
}},
{"MemoryGame", Speed, %{}},
{"Show IP", ShowIP, %{}}
}
@menu_size :erlang.tuple_size(@menu)
defmodule Options do
@type t :: %__MODULE__{
name: GenServer.name()
}
@enforce_keys [:name]
defstruct [:name]
end
defmodule State do
@type mode :: :menu | :ingame
@type t :: %__MODULE__{
index: integer(),
button_pins: Types.pins(),
mode: mode(),
game: pid(),
game_ref: reference()
}
@enforce_keys [:button_pins]
defstruct [:button_pins, :game, :game_ref, index: 0, mode: :menu]
end
@spec start_link(Options.t()) :: :ignore | {:error, any} | {:ok, pid}
def start_link(%Options{} = opts) do
GenServer.start_link(
__MODULE__,
%{},
name: opts.name
)
end
@impl true
@spec init(any) :: {:ok, State.t()}
def init(_) do
state = %State{button_pins: Application.get_env(:ex_speed_game, :button_pins)}
ButtonInput.acquire(ButtonInput)
display_index(state.index)
{:ok, state}
end
@impl true
def handle_info(msg, state)
@spec handle_info({:input, Types.pin()}, State.t()) :: {:noreply, State.t()}
def handle_info(
{:input, btn_pin},
%State{index: index, button_pins: {pin1, _, _, pin4}, mode: :menu} = state
)
when btn_pin in [pin1, pin4] do
is_first_button = btn_pin == pin1
is_last_button = btn_pin == pin4
new_index =
cond do
is_first_button and index > 0 -> index - 1
is_last_button and index < @menu_size - 1 -> index + 1
true -> index
end
display_index(new_index)
{:noreply, %State{state | index: new_index}}
end
@spec handle_info({:input, Types.pin()}, State.t()) :: {:noreply, State.t()}
def handle_info({:input, _pin}, %State{index: index, mode: :menu} = state) do
ButtonInput.release(ButtonInput)
{_, module, init_arg} = elem(@menu, index)
{:ok, pid} =
DynamicSupervisor.start_child(ExSpeedGame.Game.Supervisor, module.child_spec(init_arg))
ref = Process.monitor(pid)
{:noreply, %State{state | mode: :ingame, game: pid, game_ref: ref}}
end
@spec handle_info({:DOWN, reference(), :process, any, any}, State.t()) :: {:noreply, State.t()}
def handle_info(
{:DOWN, ref, :process, _, reason},
%State{
index: index,
mode: :ingame,
game_ref: game_ref
} = state
)
when ref == game_ref do
# Ensure input is released in case process crashed violently
ButtonInput.release(ButtonInput)
# If crashed, show crash pattern for a moment
if reason != :normal do
Lights.set(Lights, Application.get_env(:ex_speed_game, :crash_pattern))
Process.sleep(Application.get_env(:ex_speed_game, :crash_pattern_delay))
end
ButtonInput.acquire(ButtonInput)
display_index(index)
{:noreply, %State{state | mode: :menu, game: nil, game_ref: nil}}
end
defp display_index(index) do
Logger.debug("Menu viewing: #{@menu |> elem(index) |> elem(0)}")
Lights.show_binary(Lights, index + 1)
end
end

81
lib/game/modes/speed.ex Normal file
View file

@ -0,0 +1,81 @@
defmodule ExSpeedGame.Game.Modes.Speed do
use GenServer, restart: :temporary
require Logger
alias ExSpeedGame.Game.{Types, Lights, ButtonInput, Randomiser}
defmodule Options do
@type t :: %__MODULE__{
initial_score: integer(),
initial_delay: integer()
}
@enforce_keys [:initial_score, :initial_delay]
defstruct [:initial_score, :initial_delay]
end
defmodule State do
@type t :: %__MODULE__{
score: integer(),
delay: float(),
queue: [Types.choice()],
previous: Types.choice()
}
@enforce_keys [:score, :delay]
defstruct [:score, :delay, queue: [], previous: nil]
end
@spec start_link(Options.t()) :: :ignore | {:error, any} | {:ok, pid}
def start_link(%Options{} = opts) do
GenServer.start_link(__MODULE__, %{
initial_score: opts.initial_score,
initial_delay: opts.initial_delay
})
end
@impl true
@spec init(map()) :: {:ok, State.t()}
def init(%{initial_score: score, initial_delay: delay}) do
ButtonInput.acquire(ButtonInput)
Lights.clear(Lights)
Logger.debug("Init game with score #{score} and delay #{delay}.")
next_delay = schedule_tick(delay)
{:ok, %State{score: score, delay: next_delay}}
end
@impl true
def handle_info(:tick, %State{delay: delay, previous: previous} = state) do
Logger.debug("Tick #{delay}")
choice = Randomiser.get(previous)
Lights.set_index(Lights, choice)
next_delay = schedule_tick(delay)
{:noreply, %State{state | delay: next_delay, previous: choice}}
end
@impl true
def handle_info({:input, _}, state) do
{:stop, :normal, state}
end
@impl true
@spec terminate(any(), any()) :: term()
def terminate(_reason, _state) do
ButtonInput.release(ButtonInput)
end
@spec schedule_tick(float()) :: float()
defp schedule_tick(delay) do
Process.send_after(self(), :tick, trunc(delay))
get_next_delay(delay)
end
@spec get_next_delay(float()) :: float()
defp get_next_delay(delay)
defp get_next_delay(delay) when delay > 399, do: delay * 0.993
defp get_next_delay(delay) when delay > 326, do: delay * 0.996
defp get_next_delay(delay) when delay > 192, do: delay * 0.9985
defp get_next_delay(delay) when delay > 1, do: delay - 1
defp get_next_delay(delay), do: delay
end

15
lib/game/randomiser.ex Normal file
View file

@ -0,0 +1,15 @@
defmodule ExSpeedGame.Game.Randomiser do
alias ExSpeedGame.Game.Types
@choices 1..4
@doc """
Get a random button choice that is not the same as the given previous choice.
If previous given is nil, any choice can be returned.
"""
@spec get(Types.choice()) :: Types.choice()
def get(previous) do
@choices |> Enum.filter(&(&1 != previous)) |> Enum.random()
end
end

70
lib/game/show_ip.ex Normal file
View file

@ -0,0 +1,70 @@
defmodule ExSpeedGame.Game.ShowIP do
use GenServer, restart: :temporary
require Logger
defmodule State do
@type t :: %__MODULE__{
ip: String.t(),
index: integer()
}
@enforce_keys [:ip]
defstruct [:ip, index: 0]
end
@spec start_link(any) :: :ignore | {:error, any} | {:ok, pid}
def start_link(_) do
GenServer.start_link(__MODULE__, nil)
end
@impl true
@spec init(any) :: {:ok, State.t()}
def init(_) do
ExSpeedGame.Game.ButtonInput.acquire(ExSpeedGame.Game.ButtonInput)
{:ok, ifaddrs} = :inet.getifaddrs()
{_, ifaddr} =
List.keyfind(
ifaddrs,
String.to_charlist(Application.get_env(:ex_speed_game, :iface)),
0,
[]
)
ip = Keyword.get(ifaddr, :addr, {0, 0, 0, 0}) |> :inet.ntoa() |> to_string() |> Kernel.<>(".")
Logger.debug("Showing IP: #{inspect(ip)}")
state = %State{ip: ip}
display_number(state.ip, state.index)
{:ok, state}
end
@impl true
def handle_info({:input, _pin}, %State{ip: ip, index: index} = state) do
if index == String.length(ip) - 1 do
{:stop, :normal, state}
else
index = index + 1
display_number(ip, index)
{:noreply, %State{state | index: index}}
end
end
@impl true
def terminate(_reason, _state) do
ExSpeedGame.Game.ButtonInput.release(ExSpeedGame.Game.ButtonInput)
end
defp display_number(ip, index) do
server = ExSpeedGame.Game.Lights
case String.at(ip, index) do
"." ->
ExSpeedGame.Game.Lights.clear(server)
int ->
{num, ""} = Integer.parse(int)
ExSpeedGame.Game.Lights.show_binary(server, num)
end
end
end

View file

@ -1,5 +1,11 @@
defmodule ExSpeedGame.Game.Types do
@type pin :: Circuits.GPIO.pin_number()
@type pins :: [pin]
@type led_pins :: [pin]
@type pins :: {pin(), pin(), pin(), pin()}
@type led_pins :: {
{pin(), pin()},
{pin(), pin()},
{pin(), pin()},
{pin(), pin()}
}
@type choice :: 1..4
end

View file

@ -1,26 +0,0 @@
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, ref} = Circuits.GPIO.open(16, :output, initial_value: 0)
{:ok, {ref, false}}
end
@impl true
def handle_info(msg, {ref, state}) do
Logger.debug(inspect(msg))
new_state = not state
Circuits.GPIO.write(ref, if(new_state, do: 1, else: 0))
{:noreply, {ref, new_state}}
end
end