Nerves.UART allows you to access UARTs, serial ports, Bluetooth virtual serial port connections and more in Elixir. Feature highlights:
- Mac, Windows, and desktop and embedded Linux
- Enumerate serial ports
- Receive input via messages or by polling (active and passive modes)
- Add and remove framing on serial data - line-based framing included for use with GPS, cellular, satellite and other modules
- Unit tests (uses the tty0tty virtual null modem on Travis)
Something doesn't work for you? Check out below and the docs. Chat with other users on the nerves channel on the elixir-lang slack, or file an issue or PR.
Discover what serial ports are attached:
iex> Nerves.UART.enumerate
%{"COM14" => %{description: "USB Serial Port", manufacturer: "FTDI", product_id: 24577,
vendor_id: 1027},
"COM5" => %{description: "Prolific USB-to-Serial Comm Port",
manufacturer: "Prolific", product_id: 8963, vendor_id: 1659},
"COM16" => %{description: "Arduino Uno",
manufacturer: "Arduino LLC (www.arduino.cc)", product_id: 67, vendor_id: 9025}}
Start the UART GenServer:
iex> {:ok, pid} = Nerves.UART.start_link
{:ok, #PID<0.132.0>}
The GenServer doesn't open a port automatically, so open up a serial port or
UART now. See the results from your call to Nerves.UART.enumerate/0
for what's
available on your system.
iex> Nerves.UART.open(pid, "COM14", speed: 115200, active: false)
:ok
This opens the serial port up at 115200 baud and turns off active mode. This
means that you'll have to manually call Nerves.UART.read
to receive input. In
active mode, input from the serial port will be sent as messages. See the docs
for all options.
Write something to the serial port:
iex> Nerves.UART.write(pid, "Hello there\r\n")
:ok
See if anyone responds in the next 60 seconds:
iex> Nerves.UART.read(pid, 60000)
{:ok, "Hi"}
Input is reported as soon as it is received, so you may need multiple calls to
read/2
to get everything you want. If you have flow control enabled and stop
calling read/2
, the port will push back to the sender when its buffers fill
up.
Enough with passive mode, let's switch to active mode:
iex> Nerves.UART.configure(pid, active: true)
:ok
iex> flush
{:nerves_uart, "COM14", "a"}
{:nerves_uart, "COM14", "b"}
{:nerves_uart, "COM14", "c"}
{:nerves_uart, "COM14", "\r"}
{:nerves_uart, "COM14", "\n"}
:ok
It turns out that COM14
is a USB to serial port. Let's unplug it and see what
happens:
iex> flush
{:nerves_uart, "COM14", {:error, :eio}}
Oops. Well, when it appears again, it can be reopened. In passive mode, errors
get reported on the calls to Nerves.UART.read/2
and Nerves.UART.write/3
Back to receiving data, it's a little annoying that characters arrive one by
one. That's because our computer is really fast compared to the serial port,
but if something slows it down, we could receive two or more characters at a
time. Rather than reassemble the characters into lines, we can ask nerves_uart
to do it for us:
iex> Nerves.UART.configure(pid, framing: {Nerves.UART.Framing.Line, separator: "\r\n"})
:ok
This tells nerves_uart
to append a \r\n
to each call to write/2
and to
report each line separately in active and passive mode. You can set this
configuration in the call to open/3
as well. Here's what we get now:
iex> flush
{:nerves_uart, "COM14", "abc"} # Note that the "\r\n" is trimmed
:ok
If your serial data is framed differently, check out the Nerves.UART.Framing
behaviour and implement your own. Nerves.UART.Framing.FourByte
is a
particularly simple example of a framer.
You can also set a timeout so that a partial line doesn't hang around in the receive buffer forever:
iex> Nerves.UART.configure(pid, rx_framing_timeout: 500)
:ok
# Assume that the sender sent the letter "A" without sending anything else
# for 500 ms.
iex> flush
{:nerves_uart, "COM14", {:partial, "A"}}
To install nerves_uart
:
- Add
nerves_uart
to your list of dependencies inmix.exs
:
def deps do
[{:nerves_uart, "~> 1.0.0"}]
end
-
Check that the C compiler dependencies are satisified (see below)
-
Run
mix deps.get
andmix compile
Since this library includes C code, make
, gcc
, and Erlang header and
development libraries are required.
On Linux systems, this usually requires you to install the build-essential
and
erlang-dev
packages. For example:
sudo apt-get install build-essential erlang-dev
On Macs, run gcc --version
or make --version
. If they're not installed, you
will be given instructions.
On Windows, if you're obtaining nerves_uart
from hex.pm
, you'll need MinGW
to compile the C code. I use Chocolatey and install
MinGW by running the following in an administrative command prompt:
choco install mingw
On Nerves, you're set - just add nerves_uart
to your mix.exs
. Nerves
contains everything needed by default. If you do use Nerves, though, keep in
mind that the C code is crosscompiled for your target hardware and will not work
on your host (the port will crash when you call start_link
or enumerate
. If
you want to try out nerves_uart
on your host machine, the easiest way is to
either clone the source or add nerves_uart
as a dependency to a regular
(non-Nerves) Elixir project.
The standard Elixir build process applies. Clone nerves_uart
or download a
source release and run:
mix deps.get
mix compile
The unit tests require two serial ports connected via a NULL modem cable to run. Define the names of the serial ports in the environment before running the tests. For example,
export NERVES_UART_PORT1=ttyS0
export NERVES_UART_PORT2=ttyS1
If you're on Windows or Linux, you don't need real serial ports. For linux,
download and install tty0tty. Load the
kernel module and specify tnt0
and tnt1
for the serial ports. Check the
tty0tty
README.md, but this should looks something like:
cd tty0tty/module
make
sudo cp tty0tty.ko /lib/modules/$(uname -r)/kernel/drivers/misc/
sudo depmod
sudo modprobe tty0tty
sudo chmod 666 /dev/tnt*
export NERVES_UART_PORT1=tnt0
export NERVES_UART_PORT2=tnt1
On Windows, download and install
com0com (Look for version 2.2.2.0
if the latest hasn't been signed). The ports on Windows are CNCA0
and CNCB0
.
Then run:
mix test
If you're using tty0tty
, the tests will run at full speed. Real serial ports
seem to take a fraction of a second to close and re-open. I added a gratuitous
delay to each test to work around this. It likely can be much shorter.
No, this project doesn't have any dependencies on any Nerves components. The desire for some serial port library features on Nerves drove us to create it, but we also have host-based use cases. To be useful for us, the library must remain crossplatform and have few dependencies. We're just developing it under the Nerves umbrella.
Serial port files are almost always owned by the dialout
group. Add yourself
to the dialout
group by running sudo adduser yourusername dialout
. Then log
out and back in again, and you should be able to access the serial port.
If you're having trouble and suspect the C code, edit the Makefile
to enable
debug logging. See the Makefile
for instructions on how to do this. Debug
logging is appended to a file by default, but can be sent to stderr
or another
location by editing src/nerves_uart.c
.
If you're on Linux, the tty0tty
emulated null modem removes the flakiness of
real serial port drivers if that's the problem. The serial port monitor
jpnevulator is useful for monitoring the
hardware signals and dumping data as hex byte values.
On OSX and Windows, I've found that PL2303-based serial ports can be flakey. First, make sure that you don't have a counterfeit PL2303. On Windows, they show up in device manager with a warning symbol. On OSX, they seem to hang when closing the port. Non-counterfeit PL2303-based serial ports can pass the unit tests on Windows 10, but I have not been able to get them to pass on OSX. FTDI-based serial ports appear to work better on both operating systesm.
You may have noticed Erlang's erl_interface
code copy/pasted into
src/ei_copy
. This is only used on Windows to work around issues linking to
the distributed version of erl_interface
. That was compiled with Visual
Studio. This project uses MinGW, and even though the C ABIs are the same between
the compilers, Visual Studio adds stack protection calls that I couldn't figure
out how to work around.
Nerves.UART uses a Port and C code. Elixir/Erlang ports have nothing to do with the serial ports of the operating system. They share the same name but are different concepts.
When building this library, node-serialport and QtSerialPort where incredibly helpful in helping to define APIs and point out subtleties with platform-specific serial port code. Sadly, I couldn't reuse their code, but I feel indebted to the authors and maintainers of these libraries, since they undoubtedly saved me hours of time debugging corner cases. I have tried to acknowledge them in the comments where I have used strategies that I learned from them.