src/spi.erl

-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.