ex_speed_game/lib/game/button_input.ex

168 lines
4.6 KiB
Elixir
Raw Permalink Normal View History

2020-03-08 20:14:51 +00:00
defmodule ExSpeedGame.Game.ButtonInput do
use GenServer
2021-10-19 16:51:24 +00:00
import ExSpeedGame.Utils.TypedStruct
require Logger
2021-10-19 16:51:24 +00:00
2020-03-08 20:14:51 +00:00
alias ExSpeedGame.Game.Types
defmodule Options do
2021-10-19 16:51:24 +00:00
deftypedstruct(%{
pins: Types.pins(),
debounce_delay: pos_integer(),
repeat_delay: pos_integer(),
2021-10-19 16:51:24 +00:00
name: GenServer.name()
})
end
2020-03-08 20:14:51 +00:00
defmodule State do
2021-10-19 16:51:24 +00:00
deftypedstruct(%{
pins: Types.pins(),
debounce_delay: pos_integer(),
repeat_delay: pos_integer(),
2021-10-19 16:51:24 +00:00
listener: {pid() | nil, nil},
pin_refs: {[reference()], []},
pin_timers: {%{optional(Types.pin()) => reference()}, %{}},
active: {boolean(), false},
last_input: {%{optional(Types.pin()) => integer()}, %{}}
2021-10-19 16:51:24 +00:00
})
2020-03-08 20:14:51 +00:00
end
defmodule InputError do
defexception [:message]
end
### SERVER INTERFACE
@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, repeat_delay: opts.repeat_delay},
name: opts.name
)
2020-03-08 20:14:51 +00:00
end
@impl true
@spec init(map()) :: {:ok, State.t()}
def init(%{pins: pins, debounce_delay: debounce_delay, repeat_delay: repeat_delay}) do
pin_refs = for pin <- Tuple.to_list(pins), do: init_pin(pin)
{:ok,
%State{
pins: pins,
debounce_delay: debounce_delay,
repeat_delay: repeat_delay,
pin_refs: pin_refs
}}
2020-03-08 20:14:51 +00:00
end
@impl true
def handle_call(msg, from, state)
# Acquire
def handle_call(:acquire, {from, _}, %State{active: false} = state) do
{:reply, :ok, %State{state | listener: from, active: true}}
2020-03-08 20:14:51 +00:00
end
def handle_call(:acquire, _from, %State{active: true, listener: l}) do
raise InputError, message: "ButtonInput already acquired to #{inspect(l)}"
2020-03-08 20:14:51 +00:00
end
# Release
def handle_call(:release, _from, %State{active: true} = state) do
{:reply, :ok, %State{state | listener: nil, active: false}}
2020-03-08 20:14:51 +00:00
end
def handle_call(:release, _from, %State{active: false} = state) do
{:reply, :ok, state}
2020-03-08 20:14:51 +00:00
end
@impl true
def handle_info(msg, state)
2020-03-24 18:36:06 +00:00
def handle_info({:circuits_gpio, pin, _time, value}, %State{active: true} = state) do
2020-03-08 20:14:51 +00:00
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
# Note that due to pullup, the values here are reversed
1 ->
2020-03-08 20:14:51 +00:00
Map.delete(state.pin_timers, pin)
0 ->
2020-03-08 20:14:51 +00:00
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
def handle_info({:debounce, pin}, %State{active: true} = state) do
last_input = Map.get(state.last_input, pin)
now = System.monotonic_time(:millisecond)
Logger.debug("#{inspect({now, last_input})} input pin #{inspect(pin)}")
updated_last_input =
if is_nil(last_input) or last_input + state.repeat_delay < now do
Logger.debug("Input accepted")
send(state.listener, {:input, pin})
Map.put(state.last_input, pin, now)
else
Logger.debug("Input suppressed")
state.last_input
end
2020-03-08 20:14:51 +00:00
pin_timers = Map.delete(state.pin_timers, pin)
{:noreply, %State{state | pin_timers: pin_timers, last_input: updated_last_input}}
2020-03-08 20:14:51 +00:00
end
# Discard all other messages
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: :pullup)
2020-03-08 20:14:51 +00:00
: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
2020-03-24 18:36:06 +00:00
@doc """
Convert given button pin into choice value using the given pins tuple.
"""
@spec get_choice(Types.pin(), Types.pins()) :: Types.choice()
def get_choice(pin, pins)
def get_choice(pin, pins) when pin == elem(pins, 0), do: 1
def get_choice(pin, pins) when pin == elem(pins, 1), do: 2
def get_choice(pin, pins) when pin == elem(pins, 2), do: 3
def get_choice(pin, pins) when pin == elem(pins, 3), do: 4
2020-03-08 20:14:51 +00:00
end