-module(spi).
-export([
open/1,
open/2,
close/1,
transfer/2,
write/2,
write/3,
read/2,
read/3
]).
-export_type([
device/0
]).
-on_load(init/0).
-opaque device() :: reference().
%% @equiv open(Path, #{})
-spec open(Path) -> {ok, device()} | {error, term()} when
Path :: file:filename_all().
open(Path) ->
open(Path, #{}).
%% @doc Open an SPI device given its full path. The path should normally look like
%% "/dev/spidevX.Y".
%%
%% Once a device is open, transfers can be done using {@link transfer/2}.
%% {@link read/2}, {@link read/3}, {@link write/2} and {@link write/3} are
%% convenience helpers implemented on top of the {@link transfer/2} function
%% that ignore the reveived data in case of `write' or write a continuous stream
%% of 0s in the case of `read'.
%%
%% The {@link device(). handle} to the SPI device is automatically closed when the
%% process that opened it terminates, it can also be closed explicitely
%% using {@link close/1}.
%%
%% Returns a {@link device(). handle} to communite with the SPI device in case of success.
-spec open(Path, Options) -> {ok, device()} | {error, term()} when
Path :: file:filename_all(),
Options :: #{
mode => 0 | 1 | 2 | 3,
speed_hz => pos_integer(),
bits_per_word => 8 | 16
}.
open(Path, Options) ->
open_nif(unicode:characters_to_list(Path), maps:merge(#{
mode => 0,
speed_hz => 1000,
bits_per_word => 8
}, Options)).
%% nif
open_nif(_Filename, _Options) ->
erlang:nif_error(not_loaded).
%% @doc Close a previously open SPI device.
-spec close(device()) -> ok | {error, term()}.
close(Device) ->
close_nif(Device).
%% nif
close_nif(_Device) ->
erlang:nif_error(not_loaded).
%% @doc Initiate an SPI transfer.
-spec transfer(device(), [transfer()]) -> {ok, [binary()]} | {error, term()}.
-type transfer() :: binary() | {binary(), transfer_options()}.
-type transfer_options() :: #{
speed_hz => pos_integer(),
bits_per_word => 8 | 16,
delay_usecs => pos_integer(),
cs_change => boolean()
}.
transfer(Device, Transfers) ->
transfer_nif(Device, Transfers).
%% nif
transfer_nif(_Device, _Transfers) ->
erlang:nif_error(not_loaded).
%% @equiv write(Device, Data, #{})
-spec write(device(), binary()) -> ok | {error, term()}.
write(Device, Data) ->
write(Device, Data, #{}).
%% @doc Initiate a simple write transfer.
-spec write(device(), binary(), transfer_options()) -> ok | {error, term()}.
write(Device, Data, Options) ->
case transfer(Device, [{Data, Options}]) of
{ok, [Reply]} when byte_size(Reply) =:= byte_size(Reply) ->
ok;
{error, _} = Error ->
Error
end.
%% @equiv read(Device, Size, #{})
-spec read(device(), pos_integer()) -> {ok, binary()} | {error, term()}.
read(Device, Size) ->
read(Device, Size, #{}).
%% @doc Initiate a simple read transfer.
-spec read(device(), pos_integer(), transfer_options()) -> {ok, binary()} | {error, term()}.
read(Device, Size, Options) ->
case transfer(Device, [{<<0:(Size * 8)>>, Options}]) of
{ok, [Reply]} when byte_size(Reply) =:= Size ->
{ok, Reply};
{error, _} = Error ->
Error
end.
init() ->
case nif_path() of
undefined ->
ok;
Path ->
ok = erlang:load_nif(Path, 0)
end.
-spec nif_path() -> string() | binary() | undefined.
nif_path() ->
Priv = case code:priv_dir(spi) of
{error, bad_name} ->
case code:which(?MODULE) of
File when is_list(File) ->
filename:join([filename:dirname(File), "../priv"]);
_ ->
"../priv"
end;
Dir ->
Dir
end,
nif_path(os:type(), Priv).
nif_path({unix, linux}, Dir) ->
filename:join([Dir, "spidev_nif"]);
nif_path(_, _Dir) ->
undefined.