167 lines
4.6 KiB
Elixir
167 lines
4.6 KiB
Elixir
defmodule ExSpeedGame.Game.ButtonInput do
|
|
use GenServer
|
|
import ExSpeedGame.Utils.TypedStruct
|
|
require Logger
|
|
|
|
alias ExSpeedGame.Game.Types
|
|
|
|
defmodule Options do
|
|
deftypedstruct(%{
|
|
pins: Types.pins(),
|
|
debounce_delay: pos_integer(),
|
|
repeat_delay: pos_integer(),
|
|
name: GenServer.name()
|
|
})
|
|
end
|
|
|
|
defmodule State do
|
|
deftypedstruct(%{
|
|
pins: Types.pins(),
|
|
debounce_delay: pos_integer(),
|
|
repeat_delay: pos_integer(),
|
|
listener: {pid() | nil, nil},
|
|
pin_refs: {[reference()], []},
|
|
pin_timers: {%{optional(Types.pin()) => reference()}, %{}},
|
|
active: {boolean(), false},
|
|
last_input: {%{optional(Types.pin()) => integer()}, %{}}
|
|
})
|
|
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
|
|
)
|
|
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
|
|
}}
|
|
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}}
|
|
end
|
|
|
|
def handle_call(:acquire, _from, %State{active: true, listener: l}) do
|
|
raise InputError, message: "ButtonInput already acquired to #{inspect(l)}"
|
|
end
|
|
|
|
# Release
|
|
|
|
def handle_call(:release, _from, %State{active: true} = state) do
|
|
{:reply, :ok, %State{state | listener: nil, active: false}}
|
|
end
|
|
|
|
def handle_call(:release, _from, %State{active: false} = state) do
|
|
{:reply, :ok, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(msg, state)
|
|
|
|
def handle_info({:circuits_gpio, pin, _time, value}, %State{active: true} = state) do
|
|
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 ->
|
|
Map.delete(state.pin_timers, pin)
|
|
|
|
0 ->
|
|
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
|
|
|
|
pin_timers = Map.delete(state.pin_timers, pin)
|
|
{:noreply, %State{state | pin_timers: pin_timers, last_input: updated_last_input}}
|
|
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)
|
|
: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
|
|
|
|
@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
|
|
end
|