Skip to content
Closed
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
4 changes: 4 additions & 0 deletions lib/circuits_uart.ex
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ defmodule Circuits.UART do
of the Circuits.UART server pid as returned by `start_link/1`. The default
value is `:name`.

A pseudo-terminal, such as the ones `socat` makes, can't do parity or any
other than 8 data bits. Opening one on Linux with other settings fails with
`{:error, :einval}`, so use `parity: :none` and `data_bits: 8` for those.

The following options are supported on Linux only:

* `:rs485_enabled` - (`true` or `false`) enable RS485 mode.
Expand Down
32 changes: 32 additions & 0 deletions src/uart_comm_unix.c
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,35 @@ static int clear_custom_speed(int fd)
}


/**
* @brief Check that the port can do the data bits and parity asked for
*
* A pseudo-terminal, such as the ones socat makes, can't do parity or any
* other than 8 data bits: the kernel drops the request. tcsetattr() still
* succeeds the first time, since it changes other settings, and then fails
* with EINVAL each time after, since it changes nothing. Fail them all, so
* that opening a pty that worked once doesn't stop working.
*
* @param fd
* @param config
* @return <0 on error, with errno set to EINVAL
*/
static int uart_check_pty(int fd, const struct uart_config *config)
{
char name[64];

if (ttyname_r(fd, name, sizeof(name)) != 0 || strncmp(name, "/dev/pts/", 9) != 0)
return 0;

if (config->data_bits != 8 ||
(config->parity != UART_PARITY_NONE && config->parity != UART_PARITY_IGNORE)) {
errno = EINVAL;
return -1;
}

return 0;
}

/**
* @brief Configure the speed, data bits, stop bits and parity for the port
*
Expand All @@ -239,6 +268,9 @@ static int clear_custom_speed(int fd)
*/
static int uart_config_line(int fd, const struct uart_config *config)
{
if (uart_check_pty(fd, config) < 0)
return -1;

struct termios options;
tcgetattr(fd, &options);

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

@moduledoc """
A pseudo-terminal can't do parity or characters of other than 8 bits. These tests
make one with socat, so they are skipped where it isn't installed.
"""

@moduletag skip: if(System.find_executable("socat") == nil, do: "socat is not installed")

setup do
socat = System.find_executable("socat")

port =
Port.open({:spawn_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)

{:ok, uart} = UART.start_link()
%{pty: pty_name(port, ""), uart: uart}
end

# socat prints "N PTY is /dev/pts/12" for each 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 "a pty opens again and again with 8 data bits and no parity", %{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.close(uart)
end
end

test "a pty refuses parity and 7 data bits every time, the first as well", %{
pty: pty,
uart: uart
} do
for options <- [[parity: :even], [parity: :odd], [parity: :mark], [data_bits: 7]],
_ <- 1..2 do
assert {:error, :einval} = UART.open(uart, pty, [speed: 19200] ++ options)
end

assert :ok = UART.open(uart, pty, speed: 19200, parity: :none)
end
end
Loading