start implementing fw again

pull/363/head
connor rigby 2017-08-21 13:31:32 -07:00
parent 5349b93efb
commit 194bcbdffb
14 changed files with 223 additions and 546 deletions

1
.gitignore vendored
View File

@ -43,3 +43,4 @@ fwup-key.priv
.env
# secret config stuffs for dev environment.
config/auth_secret.exs
flash_fw.sh

View File

@ -29,14 +29,18 @@ config :fs, path: "/tmp/images"
config :farmbot, :init, [ ]
# Transports.
config :farmbot, :transport, [ ]
config :farmbot, :transport, [ ]
# Configure Farmbot Behaviours.
config :farmbot, :behaviour, [
authorization: Farmbot.Bootstrap.Authorization,
firmware: Farmbot.Firmware.UartHandler
]
config :farmbot, :uart_handler, [
tty: "/dev/ttyACM0"
]
# import config specific to our nerves_target
IO.puts "using #{target} - #{env} configuration."

View File

@ -3,17 +3,11 @@ defmodule Farmbot.BotState.Configuration do
defstruct [
os_auto_update: false,
steps_per_mm_x: 25,
steps_per_mm_y: 25,
steps_per_mm_z: 25
]
@typedoc "Config data"
@type t :: %__MODULE__{
os_auto_update: boolean,
steps_per_mm_x: number,
steps_per_mm_y: number,
steps_per_mm_z: number
}
use Farmbot.BotState.Lib.Partition

View File

@ -11,7 +11,7 @@ defmodule Farmbot.BotState.InformationalSettings do
:sync_now,
:synced,
:syncing,
:unknown
:unknown,
]
@typedoc "Status of the sync bar"
@ -39,7 +39,8 @@ defmodule Farmbot.BotState.InformationalSettings do
firmware_version: :disconnected,
throttled: false,
private_ip: :disconnected,
sync_status: SyncStatus.status(:sync_now)
sync_status: SyncStatus.status(:sync_now),
busy: true
]
@typedoc "Information Settings."
@ -48,8 +49,17 @@ defmodule Farmbot.BotState.InformationalSettings do
firmware_version: :disconnected | Version.version,
throttled: boolean,
private_ip: :disconnected | Version.version,
sync_status: SyncStatus.t
sync_status: SyncStatus.t,
busy: boolean
}
use Farmbot.BotState.Lib.Partition
def set_busy(part, busy) do
GenServer.call(part, {:set_busy, busy})
end
def partition_call({:set_busy, busy}, _, state) do
{:reply, :ok, %{state | busy: busy}}
end
end

View File

@ -25,15 +25,49 @@ defmodule Farmbot.BotState.LocationData do
defstruct [
position: Vec3.new(-1, -1, -1),
scaled_encoders: Vec3.new(-1, -1, -1),
raw_encoders: Vec3.new(-1, -1, -1)
raw_encoders: Vec3.new(-1, -1, -1),
end_stops: "-1-1-1-1-1-1"
]
@typedoc "Data about the bot's position."
@type t :: %__MODULE__{
position: Vec3.t,
scaled_encoders: Vec3.t,
raw_encoders: Vec3.t
raw_encoders: Vec3.t,
end_stops: binary
}
use Farmbot.BotState.Lib.Partition
def report_current_position(part, x, y, z) do
GenServer.call(part, {:report_current_position, Vec3.new(x,y,z)})
end
def report_encoder_position_scaled(part, x, y, z) do
GenServer.call(part, {:report_encoder_position_scaled, Vec3.new(x,y,z)})
end
def report_encoder_position_raw(part, x, y, z) do
GenServer.call(part, {:report_encoder_position_raw, Vec3.new(x,y,z)})
end
def report_end_stops(part, xa, xb, ya, yb, za, zb) do
GenServer.call(part, {:report_end_stops, "#{xa}#{xb}#{ya}#{yb}#{za}#{zb}"})
end
def partition_call({:report_current_position, pos}, _, state) do
{:reply, :ok, %{state | position: pos}}
end
def partition_call({:report_encoder_position_scaled, pos}, _, state) do
{:reply, :ok, %{state | scaled_encoders: pos}}
end
def partition_call({:report_encoder_position_raw, pos}, _, state) do
{:reply, :ok, %{state | raw_encoders: pos}}
end
def partition_call({:report_end_stops, stops}, _, state) do
{:reply, :ok, %{state | end_stops: stops}}
end
end

View File

@ -26,10 +26,10 @@ defmodule Farmbot.BotState.Supervisor do
worker(McuParams, [BotState, [name: McuParams]]),
# Transport part.
supervisor(Transport.Supervisor, [token, BotState, [name: Transport.Supervisor]])
supervisor(Transport.Supervisor, [token, BotState, [name: Transport.Supervisor]]),
# Firmware part.
# supervisor(FW.Supervisor, [name: FW.Supervisor])
supervisor(FW.Supervisor, [BotState, InformationalSettings, Configuration, LocationData, McuParams, [name: FW.Supervisor]])
]
# We set one_for_all here, since all of these link to `BotState`
supervise(children, [strategy: :one_for_all])

View File

@ -60,6 +60,7 @@ defmodule Farmbot.BotState.Transport.GenMqtt do
def handle_info({:bot_state, bs}, state) do
json = Poison.encode!(bs)
GenMQTT.publish(self(), status_topic(state.bot), json, 0, false)
{:noreply, state}
end
def handle_cast(_, %{connected: false} = state), do: {:noreply, state}

View File

@ -0,0 +1,75 @@
defmodule Farmbot.Firmware do
@moduledoc "Allows communication with the firmware."
use GenServer
require Logger
alias Farmbot.BotState
alias Farmbot.BotState.{
InformationalSettings,
LocationData
}
def handle_gcode(firmware, code), do: GenServer.call(firmware, {:handle_gcode, code})
def start_link(bot_state, informational_settings, configuration, location_data, mcu_params, handler_mod, opts) do
GenServer.start_link(__MODULE__, [bot_state, informational_settings, configuration, location_data, mcu_params, handler_mod], opts)
end
defmodule State do
defstruct [
:bot_state,
:informational_settings,
:configuration,
:location_data,
:mcu_params,
:handler_mod,
:handler
]
end
def init([bot_state, informational_settings, configuration, location_data, mcu_params, handler_mod]) do
{:ok, handler} = handler_mod.start_link(self(), name: handler_mod)
Process.link(handler)
s = %State{
bot_state: bot_state,
informational_settings: informational_settings,
configuration: configuration,
location_data: location_data,
mcu_params: mcu_params,
handler_mod: handler_mod,
handler: handler
}
{:ok, s}
end
def handle_call({:handle_gcode, :idle}, _, state) do
reply = InformationalSettings.set_busy(state.informational_settings, false)
{:reply, reply, state}
end
def handle_call({:handle_gcode, {:report_current_position, x, y, z}}, _, state) do
reply = LocationData.report_current_position(state.location_data, x, y, z)
{:reply, reply, state}
end
def handle_call({:handle_gcode, {:report_encoder_position_scaled, x, y, z}}, _, state) do
reply = LocationData.report_encoder_position_scaled(state.location_data, x, y, z)
{:reply, reply, state}
end
def handle_call({:handle_gcode, {:report_encoder_position_raw, x, y, z}}, _, state) do
reply = LocationData.report_encoder_position_raw(state.location_data, x, y, z)
{:reply, reply, state}
end
def handle_call({:handle_gcode, {:report_end_stops, xa, xb, ya, yb, za, zb}}, _, state) do
reply = LocationData.report_end_stops(state.location_data, xa, xb, ya, yb, za, zb)
{:reply, reply, state}
end
def handle_call({:handle_gcode, code}, _, state) do
Logger.warn "Got misc gcode: #{inspect code}"
{:reply, {:error, :unhandled}, state}
end
end

View File

@ -0,0 +1,3 @@
defmodule Farmbot.Firmware.Handler do
@moduledoc "Handler for a firmware."
end

View File

@ -2,15 +2,19 @@ defmodule Farmbot.Firmware.Supervisor do
@moduledoc "Supervises the firmware handler."
use Supervisor
@error_msg "Please configure a Firmware handler."
@doc "Start the Firmware Supervisor."
def start_link(args, opts \\ []) do
Supervisor.start_link(__MODULE__, args, opts)
def start_link(bot_state, informational_settings, configuration, location_data, mcu_params, opts \\ []) do
Supervisor.start_link(__MODULE__, [bot_state, informational_settings, configuration, location_data, mcu_params], opts)
end
def init(args) do
def init([bot_state, informational_settings, configuration, location_data, mcu_params]) do
handler_mod = Application.get_env(:farmbot, :behaviour)[:firmware] || raise @error_msg
children = [
worker(Farmbot.Firmware, [bot_state, informational_settings, configuration, location_data, mcu_params, handler_mod, [name: Farmbot.Firmware]])
]
opts = [strategy: :one_for_one]
supervise(children, opts)
end
end

View File

@ -0,0 +1,79 @@
defmodule Farmbot.Firmware.UartHandler do
@moduledoc """
Handles communication between farmbot and uart devices
"""
use GenServer
use Farmbot.DebugLog
alias Nerves.UART
alias Farmbot.Serial.Gcode.Parser
require Logger
@default_timeout_ms 15_000
@max_timeouts 10
@doc """
Writes a string to the uart line
"""
def write(handler, string) do
GenServer.call(handler, {:write, string, @default_timeout_ms}, :infinity)
end
@doc "Starts a UART GenServer"
def start_link(firmware, opts) do
GenServer.start_link(__MODULE__, firmware, opts)
end
## Private
defmodule State do
@moduledoc false
defstruct [
:firmware,
:nerves
]
end
def init(firmware) do
tty = Application.get_env(:farmbot, :uart_handler)[:tty] || raise "Please configure uart handler!"
{:ok, nerves} = UART.start_link()
Process.link(nerves)
case open_tty(nerves, tty) do
:ok -> {:ok, %State{firmware: firmware, nerves: nerves}}
err -> {:stop, err, :no_state}
end
end
defp open_tty(nerves, tty) do
case UART.open(nerves, tty, speed: 115_200, active: true) do
:ok ->
:ok = configure_uart(nerves, true)
# Flush the buffers so we start fresh
:ok = UART.flush(nerves)
:ok
err -> err
end
end
defp configure_uart(nerves, active) do
UART.configure(nerves,
framing: {UART.Framing.Line, separator: "\r\n"},
active: active,
rx_framing_timeout: 500)
end
def handle_info({:nerves_uart, _, {:error, reason}}, state) do
{:stop, {:error, reason}, state}
end
def handle_info({:nerves_uart, _, bin}, state) do
case Parser.parse_code(bin) do
{:unhandled_gcode, code_str} ->
Logger.warn "Got unhandled code: #{code_str}"
{_q, gcode} ->
_reply = Farmbot.Firmware.handle_gcode(state.firmware, gcode)
end
{:noreply, state}
end
end

View File

@ -1,30 +0,0 @@
defmodule Farmbot.Lib.Maths do
@moduledoc """
Math related functions.
"""
@doc """
Converts millimeters ot steps.
"""
@spec mm_to_steps(integer, integer) :: integer
def mm_to_steps(mm, spm), do: mm * spm
@doc """
Converts steps to mm.
"""
# NOTE(Connor), we will round here. This WILL come up at some point im sure.
# if we never allow sending of steps we should always be able to
# divide back by spm.
@spec steps_to_mm(integer, integer) :: integer
def steps_to_mm(steps, spm) when is_binary(spm) do
case Integer.parse(spm) do
{int, _} -> steps_to_mm(steps, int)
_ ->
raise "Can not converts steps (#{steps}) to millimeters!" <>
" #{inspect spm} must be an integer!"
end
end
def steps_to_mm(steps, spm), do: Kernel.div(steps, spm)
end

View File

@ -1,498 +0,0 @@
defmodule Farmbot.Serial.Handler do
@moduledoc """
Handles communication between farmbot and uart devices
"""
alias Farmbot.BotState
alias Farmbot.Lib.Maths
alias Farmbot.Serial.Gcode.Parser
alias Nerves.UART
require Logger
use GenServer
use Farmbot.DebugLog
@typedoc """
Handler pid or name
"""
@type handler :: GenServer.server
@typedoc """
Nerves.UART pid or name
"""
@type nerves :: GenServer.server
@typedoc """
Status of the arduino
"""
@type status :: :busy | :done
@typedoc """
State for this GenServer
"""
@type state :: %{
nerves: nerves,
tty: binary,
current: current,
timeouts: integer,
status: status,
initialized: boolean
}
@typedoc """
The current message being handled
"""
@type current :: %{
timer: reference,
reply: tuple,
status: status,
from: {pid, reference},
q: binary
} | nil
@default_timeout_ms 15_000
@max_timeouts 10
@doc "Starts a UART GenServer"
def start_link(tty, opts) when is_binary(tty) do
GenServer.start_link(__MODULE__, tty, opts)
end
@doc """
Blocks the calling process until serial is ready.
"""
def wait_for_available(handler) do
GenServer.call(handler, :wait_for_available, 12_000)
end
@doc """
Checks if we have a handler available
"""
def available?(handler) do
GenServer.call(handler, :available?)
end
@doc """
Writes a string to the uart line
"""
def write(handler, string, timeout \\ @default_timeout_ms) do
if available?(handler) do
GenServer.call(handler, {:write, string, timeout}, :infinity)
else
{:error, :unavailable}
end
end
@doc """
Send the E stop command to the arduino.
"""
def emergency_lock(handler) do
if available?(handler) do
GenServer.call(handler, :emergency_lock)
else
{:error, :unavailable}
end
end
@doc """
Tell the arduino its fine now.
"""
def emergency_unlock(handler) do
# We check for aliveness here, not availableness.
if is_alive?(handler) do
GenServer.call(handler, :emergency_unlock)
else
{:error, :unavailable}
end
end
defp is_alive?(handler) do
Process.alive?(handler)
end
## Private
def init(tty) do
{:ok, nerves} = UART.start_link()
Process.link(nerves)
case open_tty(nerves, tty) do
:ok ->
state = %{
nerves: nerves,
tty: tty,
current: nil,
timeouts: 0,
status: :busy,
waiting: [],
initialized: false
}
{:ok, state}
err ->
debug_log "could not open tty: #{inspect err}"
{:stop, err, :no_state}
end
end
@spec open_tty(nerves, binary) :: :ok
defp open_tty(nerves, tty) do
# Open the tty
case UART.open(nerves, tty, speed: 115_200, active: true) do
:ok ->
:ok = configure_uart(nerves, true)
# Flush the buffers so we start fresh
:ok = UART.flush(nerves)
:ok
err -> err
end
end
defp configure_uart(nerves, active) do
debug_log "reconfigureing uart: #{active}"
UART.configure(nerves,
framing: {UART.Framing.Line, separator: "\r\n"},
active: active,
rx_framing_timeout: 500)
end
def handle_call(:wait_for_available, from, state) do
case state.status do
:idle -> {:reply, :ok, state}
:locked -> {:reply, {:error, :locked}, state}
_ ->
Process.send_after(self(), {:waiting_timeout, from}, 10_000)
{:noreply, %{state | waiting: [from | state.waiting]}}
end
end
def handle_call(:emergency_lock, _, state) do
UART.write(state.nerves, "E")
status = handle_locked(state.current)
next = %{state |
current: nil,
timeouts: 0,
status: status,
initialized: false
}
{:reply, :ok, next}
end
def handle_call(:emergency_unlock, _, state) do
UART.write(state.nerves, "F09")
next = %{state | status: :idle, initialized: false}
{:reply, :ok, next}
end
def handle_call(:available?, _, state), do: {:reply, state.initialized, state}
def handle_call({:write, str, timeout}, from, %{status: :idle} = state)
when is_binary(str) do
handshake = generate_handshake()
writeme = "#{str} #{handshake}"
# :ok = configure_uart(state.nerves, false)
debug_log "writing: #{writeme}"
:ok = UART.write(state.nerves, writeme)
echo_ok = recieve_echo(state.nerves, writeme, "")
case echo_ok do
:ok ->
debug_log "timing this out in #{timeout} ms."
timer = Process.send_after(self(), :timeout, timeout)
current = %{
status: nil,
reply: nil,
from: from,
q: handshake,
timer: timer,
callback: nil
}
{:noreply, %{state | current: current}}
{:error, reason} ->
Logger.error "Farmbot.BotState.set_busy(false)"
{:reply, {:error, reason}, %{state | current: nil}}
end
end
def handle_call({:write, _str, _timeout}, _from, %{status: status} = state) do
debug_log "Serial Handler status: #{status}"
{:reply, {:error, :bad_status}, state}
end
def handle_info(:timeout, state) do
current = state.current
if current do
if current.reply do
debug_log "ignoring timeout. This transaction seems fine."
{:noreply, %{state | current: %{current | timer: nil}}}
else
debug_log "timing out current: #{inspect current}"
GenServer.reply(current.from, {:error, :timeout})
check_timeouts(state)
end
else
debug_log "got stray timeout."
{:noreply, %{state | current: nil}}
end
end
def handle_info({:waiting_timeout, from}, state) do
if from in state.waiting do
debug_log("Arduino is taking too long. Timing out waiting process: #{inspect from}")
GenServer.reply(from, {:error, :timeout})
{:noreply, %{state | waiting: List.delete(state.waiting, from)}}
else
{:noreply, state}
end
end
def handle_info({:nerves_uart, tty, {:error, error}}, state) do
Logger.error "#{tty} handler exiting!: #{error}"
{:stop, error, state}
end
def handle_info({:nerves_uart, _tty, {:partial, _}}, s), do: {:noreply, s}
def handle_info({:nerves_uart, _, str}, %{initialized: false} = state) do
if String.contains?(str, "R00") do
Logger.info "Initializing Firmware!"
fn ->
Logger.error "GenServer.cast(state.context.hardware, {:serial_ready, state.context})"
end.()
{:noreply, %{state | initialized: true, status: :idle}}
else
debug_log "Serial not initialized yet: #{str}"
{:noreply, state}
end
end
def handle_info({:nerves_uart, _, str}, state) when is_binary(str) do
debug_log "Reading: #{str}"
case str |> Parser.parse_code |> do_handle(state.current) do
:locked -> {:noreply, %{state | current: nil, status: :locked}}
{:callback, str, current} ->
debug_log "setting callback: #{str}"
:ok = UART.write(state.nerves, str)
{:noreply, %{state | current: current}}
current ->
next = %{state |
current: current, status: current[:status] || :idle, timeouts: 0
}
if next.status == :idle do
waiting = do_resolve_waiting(state.waiting)
{:noreply, %{next | waiting: waiting}}
else
{:noreply, next}
end
end
end
def terminate(_, :no_state), do: :ok
def terminate(_reason, state) do
UART.close(state.nerves)
UART.stop(state.nerves)
end
# This function should be called after every write and makes a couple assumptions.
defp recieve_echo(_nerves, writeme, acc) do
debug_log "Waiting for echo: sent: #{writeme} have: #{acc}"
# this could return {:error, reason}
receive do
{:nerves_uart, _, bin} when is_binary(bin) -> parse_echo(acc <> bin, writeme)
{:nerves_uart, _, {:error, reason}} -> {:error, reason}
end
end
defp parse_echo(echo, writeme) do
debug_log "Parsing echo: #{echo}"
case echo do
# R08 means valid command + whatever you wrote.
"R08 " <> echo ->
if echo |> String.trim() |> String.contains?(writeme), do: :ok, else: {:error, :bad_echo}
# R09 means invalid command.
<< "R09", _ :: binary >> -> {:error, :invalid}
# R87 is E stop
<< "R87", _ :: binary >> -> {:error, :emergency_lock}
other ->
debug_log "Got an unhandled echo. Expecting: #{writeme} but got: #{echo}"
{:error, "unhandled echo: #{other}"}
end
end
defp do_resolve_waiting(list) do
from = List.last(list)
if from do
GenServer.reply(from, :ok)
end
List.delete(list, from)
end
defp do_handle({_qcode, parsed}, current) when is_map(current) do
if current.timer do
Process.cancel_timer(current.timer)
end
results = handle_gcode(parsed)
debug_log "Handling results: #{inspect results}"
case results do
{:status, :done} -> handle_done(current)
{:status, :busy} -> handle_busy(current)
{:status, :locked} -> handle_locked(current)
{:status, status} -> %{current | status: status}
{:callback, str} -> %{current | callback: str}
{:reply, reply} -> %{current | reply: reply}
thing -> handle_other(thing, current)
end
end
defp do_handle({_qcode, parsed}, nil) do
handle_gcode(parsed)
nil
end
defp handle_locked(_current) do
Logger.error "Farmbot.BotState.lock_bot(ctx)"
:locked
end
defp handle_busy(current) do
debug_log "refreshing timer."
Process.cancel_timer(current.timer)
Logger.error "Farmbot.BotState.set_busy(context, true)"
timer = Process.send_after(self(), :timeout, @default_timeout_ms)
%{current | status: :busy, timer: timer}
end
defp handle_other(thing, current) do
unless is_nil(thing) do
debug_log "Unexpected thing: #{inspect thing}"
end
current
end
defp handle_done(current) do
debug_log "replying to #{inspect current.from} with: #{inspect current.reply}"
Logger.error "Farmbot.BotState.set_busy(context, false)"
if current.callback do
Process.cancel_timer(current.timer)
handshake = generate_handshake()
writeme = "#{current.callback} #{handshake}"
timer = Process.send_after(self(), :timeout, @default_timeout_ms)
current = %{
status: nil,
reply: nil,
from: current.from,
q: handshake,
timer: timer,
callback: nil
}
{:callback, writeme, current}
else
GenServer.reply(current.from, current.reply || "R02")
nil
end
end
defp generate_handshake, do: "Q#{:rand.uniform(99)}"
defp handle_gcode(:report_emergency_lock), do: {:status, :locked}
defp handle_gcode(:busy), do: {:status, :busy}
defp handle_gcode(:done), do: {:status, :done}
defp handle_gcode(:received), do: {:status, :received}
defp handle_gcode(:error), do: {:reply, :error}
defp handle_gcode(:report_params_complete), do: {:reply, :report_params_complete}
defp handle_gcode(:noop), do: nil
defp handle_gcode(:idle) do
Logger.error ":ok = Farmbot.BotState.set_busy(ctx, false)"
{:status, :idle}
end
defp handle_gcode({:debug_message, message}) do
debug_log "R99 #{message}"
nil
end
defp handle_gcode({:report_pin_value, pin, value} = reply)
when is_integer(pin) and is_integer(value) do
Logger.error "BotState.set_pin_value(ctx, pin, value)"
{:reply, reply}
end
defp handle_gcode({:report_current_position, x_steps, y_steps, z_steps} = reply) do
# thing_x = spm(:x, ctx)
# thing_y = spm(:y, ctx)
# thing_z = spm(:z, ctx)
# r = BotState.set_pos(ctx,
# Maths.steps_to_mm(x_steps, thing_x),
# Maths.steps_to_mm(y_steps, thing_y),
# Maths.steps_to_mm(z_steps, thing_z))
Logger.error "Report position not implemented"
# debug_log "Position report reply: #{inspect r}"
{:reply, reply}
end
defp handle_gcode({:report_parameter_value, param, value} = reply)
when is_atom(param) and is_integer(value) do
unless value == -1 do
Logger.error "BotState.set_param(ctx, param, value)"
end
{:reply, reply}
end
defp handle_gcode({:report_axis_calibration, param, value}) do
p = Parser.parse_param(param)
Logger.error "BotState.set_param(ctx, param, value)"
{:callback, "F22 P#{p} V#{value}"}
end
defp handle_gcode({:report_calibration, axis, status} = reply) do
Logger.info ">> Calibration message: #{axis}: #{status}"
{:reply, reply}
end
defp handle_gcode(
{:report_end_stops, x1, x2, y1, y2, z1, z2} = reply)
do
Logger.error "BotState.set_end_stops({x1, x2, y1, y2, z1, z2})"
{:reply, reply}
end
defp handle_gcode({:report_encoder_position_scaled, x, y, z} = reply) do
Logger.error "BotState.set_scaled_encoders(ctx, x, y, z)"
{:reply, reply}
end
defp handle_gcode({:report_encoder_position_raw, x, y, z} = reply) do
Logger.error "BotState.set_raw_encoders(ctx, x, y, z)"
{:reply, reply}
end
defp handle_gcode({:report_software_version, version} = reply) do
Logger.error "BotState.set_fw_version(ctx, version)"
{:reply, reply}
end
defp handle_gcode({:unhandled_gcode, code}) do
Logger.warn ">> got an misc gcode #{code}"
{:reply, code}
end
defp handle_gcode(parsed) do
Logger.warn "Unhandled message: #{inspect parsed}"
{:reply, parsed}
end
defp check_timeouts(state) do
if state.timeouts > @max_timeouts do
debug_log "reached max timeouts!"
:ok = UART.close(state.nerves)
Process.sleep(5000)
{:stop, :max_timeouts, state}
else
{:noreply, %{state | current: nil, timeouts: state.timeouts + 1}}
end
end
end