start to remove context from serial handler

This commit is contained in:
connor rigby 2017-08-21 11:37:14 -07:00
parent 3ce586ff5f
commit 5349b93efb
5 changed files with 224 additions and 332 deletions

138
lib/farmbot/context.ex Normal file
View file

@ -0,0 +1,138 @@
defmodule Farmbot.Context do
@moduledoc """
Context serves as an execution sandbox for all CeleryScript
"""
alias Farmbot.CeleryScript.Ast
modules = [
:auth,
:database,
:network,
:serial,
:hardware,
:monitor,
:configuration,
:http,
:transport,
:farmware_manager,
:regimen_supervisor
]
@enforce_keys modules
keys = [{:data_stack, []}, :ref]
defstruct Enum.concat(keys, modules)
defimpl Inspect, for: __MODULE__ do
def inspect(%{ref: ref}, _) when is_reference(ref) do
"#Reference<" <> rest = inspect ref
info = String.trim(rest, ">")
"#Context<#{info}>"
end
def inspect(_, _) do
"#Context<:invalid>"
end
end
@behaviour Access
@doc false
def fetch(%__MODULE__{} = ctx, key), do: Map.fetch(ctx, key)
@doc false
def get(%__MODULE__{} = ctx, key, _default), do: Map.fetch(ctx, key)
@doc false
def get_and_update(%__MODULE__{}, _, _), do: raise "Cant update #{__MODULE__} struct!"
@doc false
def pop(%__MODULE__{}, _), do: raise "Cant pop #{__MODULE__} struct!"
@typedoc false
@type database :: Farmbot.Behaviour.Database.server
@typedoc false
@type auth :: Farmbot.Behaviour.Auth.otp_server
@typedoc false
@type network :: Farmbot.Behaviour.Network.server
@typedoc false
@type serial :: Farmbot.Behaviour.Serial.server
@typedoc false
@type hardware :: Farmbot.Behaviour.Hardware.server
@typedoc false
@type monitor :: Farmbot.Behaviour.Monitor.server
@typedoc false
@type configuration :: Farmbot.Behaviour.Configuration.server
@typedoc false
@type http :: Farmbot.Behaviour.HTTP.server
@typedoc false
@type transport :: Farmbot.Behaviour.Transport.server
@typedoc false
@type farmware_manager :: Farmbot.Behaviour.FarmwareManager.server
@typedoc false
@type regimen_supervisor :: Farmbot.Behaviour.RegimenSupervisor.server
@typedoc """
List of usable modules
"""
@type modules :: Farmbot.Database |
Farmbot.Auth |
Farmbot.System.Network |
Farmbot.Serial.Handler |
Farmbot.BotState.Hardware |
Farmbot.BotState.Monitor |
Farmbot.BotState.Configuration |
Farmbot.HTTP |
Farmbot.Transport |
Farmbot.Farmware.Manager |
Farmbot.Regimen.Supervisor
@typedoc """
Stuff to be passed from one CS Node to another
"""
@type t :: %__MODULE__{
database: database,
auth: auth,
network: network,
serial: serial,
configuration: configuration,
monitor: monitor,
hardware: hardware,
http: http,
transport: transport,
farmware_manager: farmware_manager,
regimen_supervisor: regimen_supervisor,
ref: reference,
data_stack: [Ast.t]
}
@spec push_data(t, Ast.t) :: t
def push_data(%__MODULE__{} = context, %Ast{} = data) do
new_ds = [data | context.data_stack]
%{context | data_stack: new_ds}
end
@spec pop_data(t) :: {Ast.t, t}
def pop_data(%__MODULE__{} = context) do
[result | rest] = context.data_stack
{result, %{context | data_stack: rest}}
end
@doc """
Returns an empty context object for those times you don't care about
side effects or execution.
"""
@spec new :: Context.t
def new do
%__MODULE__{ data_stack: [],
ref: make_ref(),
regimen_supervisor: Farmbot.Regimen.Supervisor,
farmware_manager: Farmbot.Farmware.Manager,
configuration: Farmbot.BotState.Configuration,
transport: Farmbot.Transport,
hardware: Farmbot.BotState.Hardware,
database: Farmbot.Database,
monitor: Farmbot.BotState.Monitor,
network: Farmbot.System.Network,
serial: Farmbot.Serial.Handler,
auth: Farmbot.Auth,
http: Farmbot.HTTP
}
end
end

View file

@ -4,36 +4,33 @@ defmodule Farmbot.Serial.Handler do
"""
alias Farmbot.BotState
alias Farmbot.Context
alias Farmbot.Lib.Maths
alias Farmbot.Serial.Gcode.Parser
alias Nerves.UART
require Logger
use GenServer
# use Farmbot.DebugLog, enable: false
use Farmbot.DebugLog
@typedoc """
Handler pid or name
Handler pid or name
"""
@type handler :: pid | atom
@type handler :: GenServer.server
@typedoc """
Nerves.UART pid or name
Nerves.UART pid or name
"""
@type nerves :: handler
@type nerves :: GenServer.server
@typedoc """
Status of the arduino
Status of the arduino
"""
@type status :: :busy | :done
@typedoc """
State for this GenServer
State for this GenServer
"""
@type state :: %{
context: Context.t,
nerves: nerves,
tty: binary,
current: current,
@ -43,7 +40,7 @@ defmodule Farmbot.Serial.Handler do
}
@typedoc """
The current message being handled
The current message being handled
"""
@type current :: %{
timer: reference,
@ -56,128 +53,71 @@ defmodule Farmbot.Serial.Handler do
@default_timeout_ms 15_000
@max_timeouts 10
@doc """
Starts a UART GenServer
"""
def start_link(%Context{} = ctx, nerves, tty, opts)
when is_pid(nerves) and is_binary(tty) do
GenServer.start_link(__MODULE__, {ctx, nerves, tty}, opts)
end
def start_link(%Context{} = ctx, tty, opts) when is_binary(tty) do
GenServer.start_link(__MODULE__, {ctx, tty}, opts)
@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.
"""
@spec wait_for_available(Context.t) :: :ok | {:error, term}
def wait_for_available(%Context{serial: handler} = context)
when is_atom(handler)
do
pid = Process.whereis(handler)
if is_pid(pid) and Process.alive?(pid) do
new = %{context | serial: pid}
wait_for_available(new)
else
{:error, :noproc}
end
end
def wait_for_available(%Context{serial: handler}) do
def wait_for_available(handler) do
GenServer.call(handler, :wait_for_available, 12_000)
end
@doc """
Checks if we have a handler available
"""
@spec available?(Context.t) :: boolean
def available?(context)
# If handler is a pid
def available?(%Context{serial: handler}) when is_pid(handler) do
if Process.alive?(handler) do
GenServer.call(handler, :available?)
else
false
end
end
# if its a name, look it up
def available?(%Context{serial: handler} = ctx) when is_atom(handler) do
uh = Process.whereis(ctx.serial)
if uh do
available?(%{ctx | serial: uh})
else
false
end
def available?(handler) do
GenServer.call(handler, :available?)
end
@doc """
Writes a string to the uart line
Writes a string to the uart line
"""
@spec write(Context.t, binary, integer) :: binary | {:error, atom}
def write(context, string, timeout \\ @default_timeout_ms)
def write(%Context{} = ctx, str, timeout)
when is_binary(str) and is_number(timeout) do
if available?(ctx) do
# try do
# fn() -> debug_log("write begin from: #{inspect Process.info(self())}") end.()
# rescue
# _ -> :ok
# end
GenServer.call(ctx.serial, {:write, str, timeout}, :infinity)
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.
Send the E stop command to the arduino.
"""
@spec emergency_lock(Context.t) :: :ok | no_return
def emergency_lock(%Context{} = ctx) do
if available?(ctx) do
GenServer.call(ctx.serial, :emergency_lock)
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.
Tell the arduino its fine now.
"""
@spec emergency_unlock(Context.t) :: :ok | no_return
def emergency_unlock(%Context{} = ctx) do
def emergency_unlock(handler) do
# We check for aliveness here, not availableness.
if is_alive?(ctx) do
GenServer.call(ctx.serial, :emergency_unlock)
if is_alive?(handler) do
GenServer.call(handler, :emergency_unlock)
else
{:error, :unavailable}
end
end
defp is_alive?(%Context{serial: serial}) when is_pid(serial) do
Process.alive?(serial)
end
defp is_alive?(%Context{serial: serial} = ctx) when is_atom(serial) do
pid = Process.whereis(serial)
if pid do
is_alive?(%{ctx | serial: pid})
else
false
end
defp is_alive?(handler) do
Process.alive?(handler)
end
## Private
def init({ctx, nerves, tty}) when is_pid(nerves) and is_binary(tty) do
def init(tty) do
{:ok, nerves} = UART.start_link()
Process.link(nerves)
case open_tty(nerves, tty) do
:ok ->
state = %{
context: ctx,
nerves: nerves,
tty: tty,
current: nil,
@ -189,15 +129,10 @@ defmodule Farmbot.Serial.Handler do
{:ok, state}
err ->
debug_log "could not open tty: #{inspect err}"
{:stop, :normal, :no_state}
{:stop, err, :no_state}
end
end
def init({ctx, tty}) when is_binary(tty) do
{:ok, nerves} = UART.start_link()
init({ctx, nerves, tty})
end
@spec open_tty(nerves, binary) :: :ok
defp open_tty(nerves, tty) do
# Open the tty
@ -231,7 +166,7 @@ defmodule Farmbot.Serial.Handler do
def handle_call(:emergency_lock, _, state) do
UART.write(state.nerves, "E")
status = handle_locked(state.current, state.context)
status = handle_locked(state.current)
next = %{state |
current: nil,
timeouts: 0,
@ -261,7 +196,6 @@ defmodule Farmbot.Serial.Handler do
echo_ok = recieve_echo(state.nerves, writeme, "")
# :ok = configure_uart(state.nerves, true)
case echo_ok do
:ok ->
debug_log "timing this out in #{timeout} ms."
@ -276,7 +210,7 @@ defmodule Farmbot.Serial.Handler do
}
{:noreply, %{state | current: current}}
{:error, reason} ->
Farmbot.BotState.set_busy(state.context, false)
Logger.error "Farmbot.BotState.set_busy(false)"
{:reply, {:error, reason}, %{state | current: nil}}
end
@ -287,19 +221,6 @@ defmodule Farmbot.Serial.Handler do
{:reply, {:error, :bad_status}, state}
end
def handle_cast({:update_fw, file, pid}, state) do
UART.close(state.nerves)
Process.sleep(1000)
if String.contains?(state.tty, "tnt") do
Logger.warn "Not a real arduino!"
send(pid, :done)
{:noreply, state}
else
flash_firmware(state.context, state.tty, file, pid)
{:stop, :update, state}
end
end
def handle_info(:timeout, state) do
current = state.current
if current do
@ -338,8 +259,7 @@ defmodule Farmbot.Serial.Handler do
if String.contains?(str, "R00") do
Logger.info "Initializing Firmware!"
fn ->
Process.sleep(2000)
GenServer.cast(state.context.hardware, {:serial_ready, state.context})
Logger.error "GenServer.cast(state.context.hardware, {:serial_ready, state.context})"
end.()
{:noreply, %{state | initialized: true, status: :idle}}
else
@ -350,7 +270,7 @@ defmodule Farmbot.Serial.Handler do
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, state.context) do
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}"
@ -396,7 +316,7 @@ defmodule Farmbot.Serial.Handler do
<< "R09", _ :: binary >> -> {:error, :invalid}
# R87 is E stop
<< "R87", _ :: binary >> -> {:error, :emergency_lock}
other ->
other ->
debug_log "Got an unhandled echo. Expecting: #{writeme} but got: #{echo}"
{:error, "unhandled echo: #{other}"}
end
@ -410,19 +330,16 @@ defmodule Farmbot.Serial.Handler do
List.delete(list, from)
end
@spec do_handle({binary, any}, current | nil, Context.t)
:: current | nil | :locked
defp do_handle({_qcode, parsed}, current, %Context{} = ctx)
when is_map(current) do
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, ctx)
results = handle_gcode(parsed)
debug_log "Handling results: #{inspect results}"
case results do
{:status, :done} -> handle_done(current, ctx)
{:status, :busy} -> handle_busy(current, ctx)
{:status, :locked} -> handle_locked(current, ctx)
{: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}
@ -430,22 +347,20 @@ defmodule Farmbot.Serial.Handler do
end
end
defp do_handle({_qcode, parsed}, nil, %Context{} = ctx) do
handle_gcode(parsed, ctx)
defp do_handle({_qcode, parsed}, nil) do
handle_gcode(parsed)
nil
end
@spec handle_locked(current, Context.t) :: :locked
defp handle_locked(_current, ctx) do
# Side effects.
Farmbot.BotState.lock_bot(ctx)
defp handle_locked(_current) do
Logger.error "Farmbot.BotState.lock_bot(ctx)"
:locked
end
defp handle_busy(current, context) do
defp handle_busy(current) do
debug_log "refreshing timer."
Process.cancel_timer(current.timer)
Farmbot.BotState.set_busy(context, true)
Logger.error "Farmbot.BotState.set_busy(context, true)"
timer = Process.send_after(self(), :timeout, @default_timeout_ms)
%{current | status: :busy, timer: timer}
end
@ -457,9 +372,9 @@ defmodule Farmbot.Serial.Handler do
current
end
defp handle_done(current, context) do
defp handle_done(current) do
debug_log "replying to #{inspect current.from} with: #{inspect current.reply}"
:ok = Farmbot.BotState.set_busy(context, false)
Logger.error "Farmbot.BotState.set_busy(context, false)"
if current.callback do
Process.cancel_timer(current.timer)
handshake = generate_handshake()
@ -480,93 +395,92 @@ defmodule Farmbot.Serial.Handler do
end
end
@spec generate_handshake :: binary
defp generate_handshake, do: "Q#{:rand.uniform(99)}"
@spec handle_gcode(any, Context.t) :: {:status, any} | {:reply, any} | nil
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(:report_emergency_lock, _), do: {:status, :locked}
defp handle_gcode(:busy, %Context{} = _ctx), do: {:status, :busy}
defp handle_gcode(:done, %Context{} = _ctx), do: {:status, :done}
defp handle_gcode(:received, %Context{} = _ctx), do: {:status, :received}
defp handle_gcode(:error, %Context{} = _ctx), do: {:reply, :error}
defp handle_gcode(:report_params_complete, _), do: {:reply, :report_params_complete}
defp handle_gcode(:noop, %Context{} = _ctx), do: nil
defp handle_gcode(:idle, %Context{} = ctx) do
:ok = Farmbot.BotState.set_busy(ctx, false)
defp handle_gcode(:idle) do
Logger.error ":ok = Farmbot.BotState.set_busy(ctx, false)"
{:status, :idle}
end
defp handle_gcode({:debug_message, message}, %Context{} = _ctx) do
defp handle_gcode({:debug_message, message}) do
debug_log "R99 #{message}"
nil
end
defp handle_gcode({:report_pin_value, pin, value} = reply, %Context{} = ctx)
defp handle_gcode({:report_pin_value, pin, value} = reply)
when is_integer(pin) and is_integer(value) do
BotState.set_pin_value(ctx, pin, value)
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, %Context{} = ctx) 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))
debug_log "Position report reply: #{inspect r}"
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, %Context{} = ctx)
defp handle_gcode({:report_parameter_value, param, value} = reply)
when is_atom(param) and is_integer(value) do
unless value == -1 do
BotState.set_param(ctx, param, value)
Logger.error "BotState.set_param(ctx, param, value)"
end
{:reply, reply}
end
defp handle_gcode({:report_axis_calibration, param, value}, ctx) do
defp handle_gcode({:report_axis_calibration, param, value}) do
p = Parser.parse_param(param)
BotState.set_param(ctx, param, value)
Logger.error "BotState.set_param(ctx, param, value)"
{:callback, "F22 P#{p} V#{value}"}
end
defp handle_gcode({:report_calibration, axis, status} = reply, _ctx) do
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, %Context{} = ctx)
{:report_end_stops, x1, x2, y1, y2, z1, z2} = reply)
do
BotState.set_end_stops(ctx, {x1, x2, y1, y2, z1, z2})
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, %Context{} = ctx) do
BotState.set_scaled_encoders(ctx, x, y, z)
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, %Context{} = ctx) do
BotState.set_raw_encoders(ctx, x, y, z)
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, %Context{} = ctx) do
BotState.set_fw_version(ctx, version)
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}, %Context{} = _ctx) do
defp handle_gcode({:unhandled_gcode, code}) do
Logger.warn ">> got an misc gcode #{code}"
{:reply, code}
end
defp handle_gcode(parsed, %Context{} = _ctx) do
defp handle_gcode(parsed) do
Logger.warn "Unhandled message: #{inspect parsed}"
{:reply, parsed}
end
@ -581,63 +495,4 @@ defmodule Farmbot.Serial.Handler do
{:noreply, %{state | current: nil, timeouts: state.timeouts + 1}}
end
end
def flash_firmware(%Context{} = _ctx, tty, hex_file, pid) do
Logger.info ">> Starting arduino firmware flash", type: :busy
args =
["-patmega2560",
"-cwiring",
"-P/dev/#{tty}",
"-b115200",
"-D", "-q", "-V",
"-Uflash:w:#{hex_file}:i"]
avrdude = System.find_executable("avrdude")
port_args = [
:stream,
:binary,
:exit_status,
:hide,
:use_stdio,
:stderr_to_stdout,
args: args
]
port = Port.open({:spawn_executable, avrdude}, port_args)
timer = Process.send_after(self(), :flash_timeout, 20_000)
r = handle_port(port, timer)
send(pid, r)
end
defp handle_port(port, timer) do
receive do
{^port, {:data, contents}} ->
debug_log(contents)
handle_port(port, timer)
{^port, {:exit_status, 0}} ->
Logger.info(">> Flashed new arduino firmware", type: :success)
Process.cancel_timer(timer)
:done
{^port, {:exit_status, error_code}} ->
Logger.error ">> Could not flash firmware (#{error_code})"
Process.cancel_timer(timer)
{:error, error_code}
:flash_timeout ->
Logger.error ">> Timed out flashing firmware!"
# info = Port.info(port)
# if info do
# send port, {self(), :close}
# "kill" |> System.cmd(["15", "#{info.os_pid}"])
# end
{:error, :flash_timeout}
after 21_000 ->
Logger.error ">> Timed out flashing firmware!"
{:error, :flash_timeout}
end
end
@spec spm(atom, Context.t) :: integer
defp spm(xyz, %Context{} = ctx) do
spm = "steps_per_mm_#{xyz}" |> String.to_atom
Farmbot.BotState.get_config(ctx, spm)
end
end

View file

@ -1,32 +0,0 @@
defmodule Farmbot.Serial.Handler.OpenTTY do
@moduledoc false
import Supervisor.Spec
alias Farmbot.{Context, Serial}
alias Serial.Handler
use Farmbot.DebugLog
import __MODULE__.EnvTargetDetector
@doc """
Opens a serial device and ensures it is supervised.
"""
def open_ttys(%Context{} = ctx, supervisor) do
debug_log "Detecting ttys."
tty = try_detect_tty()
if tty do
debug_log "Trying to open tty."
worker_spec = worker(Handler,
[ctx, tty, [name: Handler]],
[restart: :permanent])
{:ok, _} = Supervisor.start_child(supervisor, worker_spec)
else
debug_log "Not opening tty."
nil
end
end
case {Mix.env(), Mix.Project.config[:target]} do
{:dev, "host"} -> dev_host()
{:test, "host"} -> test_host()
{env_, target_} -> target(unquote(env_), unquote(target_))
end
end

View file

@ -1,54 +0,0 @@
defmodule Farmbot.Serial.Handler.OpenTTY.EnvTargetDetector do
@moduledoc false
@doc false
defmacro test_host do
quote do
@doc false
def try_detect_tty do
debug_log "using test/host tty opener."
nil
end
end
end
@doc false
defmacro dev_host do
quote do
@doc false
def try_detect_tty do
debug_log "using dev/host tty opener."
System.get_env("ARDUINO_TTY")
end
end
end
@doc false
defmacro target(_, _target) do
quote do
require Logger
@doc false
def try_detect_tty do
debug_log "using rpi3 tty opener."
Nerves.UART.enumerate()
|> Map.keys()
|> Kernel.--(["ttyAMA0", "ttyS0"])
|> check_and_return()
end
defp check_and_return([tty]), do: tty
defp check_and_return([]) do
debug_log "didnt detect any serial devices."
Logger.info ">> Could not find an Arduino!", type: :error
nil
end
defp check_and_return(_to_many) do
debug_log "too many serial devices."
Logger.info ">> Found too many serial devices!", type: :error
nil
end
end
end
end

View file

@ -1,15 +0,0 @@
defmodule Farmbot.Serial.Supervisor do
@moduledoc "Supervisor for serial services."
alias Farmbot.Context
alias Farmbot.Serial.Handler.OpenTTY
use Context.Supervisor
def init(%Context{} = ctx) do
children = [
worker(Task,
[OpenTTY, :open_ttys, [ctx, __MODULE__]], restart: :transient),
]
opts = [strategy: :one_for_one]
supervise(children, opts)
end
end