Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/circuits_uart.ex
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ defmodule Circuits.UART do
of the Circuits.UART server pid as returned by `start_link/1`. The default
value is `:name`.

On Linux and macOS, opening or configuring a port returns `{:error, :einval}`
if the driver fails to set the requested data bits, stop bits, or parity.
Linux pseudo-terminals, such as those created by `socat`, require
`data_bits: 8` and `parity: :none`.

The following options are supported on Linux only:

* `:rs485_enabled` - (`true` or `false`) enable RS485 mode.
Expand Down
26 changes: 24 additions & 2 deletions src/uart_comm_unix.c
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ static int clear_custom_speed(int fd)
static int uart_config_line(int fd, const struct uart_config *config)
{
struct termios options;
tcgetattr(fd, &options);
if (tcgetattr(fd, &options) < 0)
return -1;

// Set up the speed
int baud_constant = to_baudrate_constant(config->speed);
Expand Down Expand Up @@ -317,7 +318,28 @@ static int uart_config_line(int fd, const struct uart_config *config)
options.c_cc[VTIME] = 0;

// Set everything
return tcsetattr(fd, TCSANOW, &options);
if (tcsetattr(fd, TCSANOW, &options) < 0)
return -1;

struct termios actual;
if (tcgetattr(fd, &actual) < 0)
return -1;

// tcsetattr() may succeed even when only some settings were applied.
tcflag_t mask = CSIZE | CSTOPB | PARENB;
if (options.c_cflag & PARENB) {
mask |= PARODD;
#ifdef CMSPAR
mask |= CMSPAR;
#endif
}

if ((actual.c_cflag & mask) != (options.c_cflag & mask)) {
errno = EINVAL;
return -1;
}

return 0;
}

/**
Expand Down
81 changes: 81 additions & 0 deletions test/pty_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# SPDX-FileCopyrightText: 2026 Tallak Tveide
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule PtyTest do
use ExUnit.Case
alias Circuits.UART

@skip_reason (cond do
:os.type() != {:unix, :linux} -> "These tests require Linux pseudo-terminals"
System.find_executable("socat") == nil -> "socat is not installed"
true -> false
end)
@moduletag skip: @skip_reason

setup do
port =
Port.open({:spawn_executable, System.find_executable("socat")}, [
:binary,
:stderr_to_stdout,
args: ["-d", "-d", "pty,raw,echo=0", "pty,raw,echo=0"]
])

{:os_pid, os_pid} = Port.info(port, :os_pid)
on_exit(fn -> System.cmd("kill", [to_string(os_pid)]) end)

uart = start_supervised!(UART)
%{pty: pty_name(port, ""), uart: uart}
end

defp pty_name(port, output) do
case Regex.run(~r/PTY is (\S+)/, output, capture: :all_but_first) do
[name] ->
name

nil ->
receive do
{^port, {:data, data}} -> pty_name(port, output <> data)
after
2000 -> flunk("socat did not open a pty: #{output}")
end
end
end

test "supported settings work on repeated opens and reconfiguration", %{pty: pty, uart: uart} do
for _ <- 1..3 do
assert :ok = UART.open(uart, pty, speed: 19200, data_bits: 8, parity: :none)
assert :ok = UART.configure(uart, stop_bits: 2)
assert :ok = UART.configure(uart, stop_bits: 1)
assert :ok = UART.close(uart)
end
end

for options <- [
[parity: :even],
[parity: :odd],
[parity: :mark],
[parity: :space],
[data_bits: 5],
[data_bits: 6],
[data_bits: 7]
] do
test "unsupported settings #{inspect(options)} fail on every open", %{pty: pty, uart: uart} do
for _ <- 1..2 do
assert {:error, :einval} =
UART.open(uart, pty, [speed: 19200] ++ unquote(options))
end

assert :ok = UART.open(uart, pty, data_bits: 8, parity: :none)
end

test "unsupported settings #{inspect(options)} fail on reconfiguration", %{
pty: pty,
uart: uart
} do
assert :ok = UART.open(uart, pty)
assert {:error, :einval} = UART.configure(uart, unquote(options))
assert :ok = UART.configure(uart, data_bits: 8, parity: :none)
end
end
end
Loading