defmodule Baud do
@moduledoc """
Serial port module.
```elixir
tty = case :os.type() do
{:unix, :darwin} -> "cu.usbserial-FTYHQD9MA"
{:unix, :linux} -> "ttyUSB0"
{:win32, :nt} -> "COM5"
end
#Try this with a loopback
{:ok, pid} = Baud.start_link(device: tty)
Baud.write pid, "01234\\n56789\\n98765\\n43210"
{:ok, "01234\\n"} = Baud.readln pid
{:ok, "56789\\n"} = Baud.readln pid
{:ok, "98765\\n"} = Baud.readln pid
{:to, "43210"} = Baud.readln pid
Baud.write pid, "01234\\r56789\\r98765\\r43210"
{:ok, "01234\\r"} = Baud.readch pid, 0x0d
{:ok, "56789\\r"} = Baud.readch pid, 0x0d
{:ok, "98765\\r"} = Baud.readch pid, 0x0d
{:to, "43210"} = Baud.readch pid, 0x0d
Baud.write pid, "01234\\n56789\\n98765\\n43210"
{:ok, "01234\\n"} = Baud.readn pid, 6
{:ok, "56789\\n"} = Baud.readn pid, 6
{:ok, "98765\\n"} = Baud.readn pid, 6
{:to, "43210"} = Baud.readn pid, 6
Baud.write pid, "01234\\n"
Baud.write pid, "56789\\n"
Baud.write pid, "98765\\n"
Baud.write pid, "43210"
:timer.sleep 100
{:ok, "01234\\n56789\\n98765\\n43210"} = Baud.readall pid
```
"""
@doc """
Starts the serial server.
`params` *must* contain a keyword list to be merged with the following defaults:
```elixir
[
device: nil, #serial port name: "COM1", "ttyUSB0", "cu.usbserial-FTYHQD9MA"
speed: 9600, #either 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200
#win32 adds 14400, 128000, 256000
config: "8N1", #either "8N1", "7E1", "7O1"
]
```
`opts` is optional and is passed verbatim to GenServer.
Returns `{:ok, pid}`.
## Example
```
Baud.start_link(device: "COM8")
```
"""
def start_link(opts) do
device = Keyword.fetch!(opts, :device)
speed = Keyword.get(opts, :speed, 9600)
config = Keyword.get(opts, :config, "8N1")
case Sniff.open(device, speed, config) do
{:ok, nid} -> Agent.start_link(fn -> {nid, <<>>} end, opts)
other -> other
end
end
@sleep 10
@to 400
@doc """
Stops the serial server.
Returns `:ok`.
"""
def stop(pid) do
Agent.get(
pid,
fn {nid, _} ->
:ok = Sniff.close(nid)
end,
@to
)
Agent.stop(pid)
end
@doc """
Writes `data` to the serial port.
Returns `:ok`.
"""
def write(pid, data, timeout \\ @to) do
Agent.get(
pid,
fn {nid, _} ->
:ok = Sniff.write(nid, data)
end,
timeout
)
end
@doc """
Reads all available data.
Returns `{:ok, data}`.
"""
def readall(pid, timeout \\ @to) do
Agent.get_and_update(
pid,
fn {nid, buf} ->
{:ok, data} = Sniff.read(nid)
all = buf <> data
{{:ok, all}, {nid, <<>>}}
end,
timeout
)
end
@doc """
Reads `count` bytes.
Returns `{:ok, data} | {:to, partial}`.
"""
def readn(pid, count, timeout \\ @to) do
Agent.get_and_update(
pid,
fn {nid, buf} ->
now = now()
size = byte_size(buf)
dl = now + timeout
{res, head, tail} = read_n(nid, [buf], size, count, dl)
{{res, head}, {nid, tail}}
end,
2 * timeout
)
end
@doc """
Reads until 'nl' is received.
Returns `{:ok, line} | {:to, partial}`.
"""
def readln(pid, timeout \\ @to) do
Agent.get_and_update(
pid,
fn {nid, buf} ->
now = now()
ch = 10
index = index(buf, ch)
size = byte_size(buf)
dl = now + timeout
{res, head, tail} = read_ch(nid, [buf], index, size, ch, dl)
{{res, head}, {nid, tail}}
end,
2 * timeout
)
end
@doc """
Reads until 'ch' is received.
Returns `{:ok, data} | {:to, partial}`.
"""
def readch(pid, ch, timeout \\ @to) do
Agent.get_and_update(
pid,
fn {nid, buf} ->
now = now()
index = index(buf, ch)
size = byte_size(buf)
dl = now + timeout
{res, head, tail} = read_ch(nid, [buf], index, size, ch, dl)
{{res, head}, {nid, tail}}
end,
2 * timeout
)
end
defp read_ch(nid, iol, index, size, ch, dl) do
case index >= 0 do
true ->
split_i(iol, index)
false ->
{:ok, data} = Sniff.read(nid)
case data do
<<>> ->
:timer.sleep(@sleep)
now = now()
case now > dl do
true -> {:to, all(iol), <<>>}
false -> read_ch(nid, iol, -1, size, ch, dl)
end
_ ->
case index(data, ch) do
-1 -> read_ch(nid, [data | iol], -1, size + byte_size(data), ch, dl)
index -> read_ch(nid, [data | iol], size + index, size + byte_size(data), ch, dl)
end
end
end
end
defp read_n(nid, iol, size, count, dl) do
case size >= count do
true ->
split_c(iol, count)
false ->
{:ok, data} = Sniff.read(nid)
case data do
<<>> ->
:timer.sleep(@sleep)
now = now()
case now > dl do
true -> {:to, all(iol), <<>>}
false -> read_n(nid, iol, size, count, dl)
end
_ ->
read_n(nid, [data | iol], size + byte_size(data), count, dl)
end
end
end
defp now(), do: :erlang.monotonic_time(:milli_seconds)
defp index(bin, ch) do
case :binary.match(bin, <<ch>>) do
:nomatch -> -1
{index, _} -> index
end
end
defp all(bin) when is_binary(bin) do
bin
end
defp all(list) when is_list(list) do
reversed = Enum.reverse(list)
:erlang.iolist_to_binary(reversed)
end
defp split_i(bin, index) when is_binary(bin) do
head = :binary.part(bin, {0, index + 1})
tail = :binary.part(bin, {index + 1, byte_size(bin) - index - 1})
{:ok, head, tail}
end
defp split_i(list, index) when is_list(list) do
reversed = Enum.reverse(list)
bin = :erlang.iolist_to_binary(reversed)
split_i(bin, index)
end
defp split_c(bin, count) when is_binary(bin) do
<<head::bytes-size(count), tail::binary>> = bin
{:ok, head, tail}
end
defp split_c(list, count) when is_list(list) do
reversed = Enum.reverse(list)
bin = :erlang.iolist_to_binary(reversed)
split_c(bin, count)
end
end