src/stk500.erl

%%% @copyright 2012-2022, Michael Santos <michael.santos@gmail.com>

%%% Permission to use, copy, modify, and/or distribute this software for any
%%% purpose with or without fee is hereby granted, provided that the above
%%% copyright notice and this permission notice appear in all copies.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(stk500).
-include_lib("srly/include/serctl.hrl").
-include("stk500.hrl").

-export([
    open/0, open/1, open/2,

    reset/1,
    dtrrts/2,
    version/1,

    sync/1,

    hex_file/1, hex_file/2,
    chunk/2,
    load/2, load/3,
    cmd/2, cmd/3,

    serial_device/0
]).

-define(DEV, "/dev/ttyUSB0").

-define(TIOCMGET, serctl:constant(tiocmget)).
-define(TIOCMSET, serctl:constant(tiocmset)).

-define(TIOCM_DTR, serctl:constant(tiocm_dtr)).
-define(TIOCM_RTS, serctl:constant(tiocm_rts)).

-define(STK500_OK, <<?Resp_STK_INSYNC, ?Resp_STK_OK>>).

%% @doc Connect to the Arduino using the default serial device at
%% 19200 baud
-spec open() -> {ok, serctl:fd()} | {error, file:posix()}.
open() ->
    Dev = serial_device(),
    open(Dev, [{speed, b19200}]).

%% @doc Connect to the Arduino using the specified serial device at
%% 19200 baud
-spec open(serctl:dev()) -> {ok, serctl:fd()} | {error, file:posix()}.
open(Dev) ->
    open(Dev, [{speed, b19200}]).

%% @doc Connect to the Arduino using the specified serial device and settings
%%
%% ```
%% % The Diecimila uses 19200
%% {ok,FD} = stk500:open("/dev/ttyUSB0", [{speed, 19200}]).
%% '''
-spec open(serctl:dev(), proplists:proplist()) -> {ok, serctl:fd()} | {error, file:posix()}.
open(Dev, Opt) ->
    Speed = proplists:get_value(speed, Opt, b19200),

    % 19200, 8 data bits, 1 stop bit, no parity
    Mode = #termios{
        iflag = serctl:constant(ignbrk),
        oflag = 0,
        lflag = 0,
        cflag =
            serctl:constant(cs8) bor
                serctl:constant(clocal) bor
                serctl:constant(cread),

        cc = lists:foldl(
            fun({Offset, Val}, Bin) ->
                serctl:offset(Bin, {Offset, Val})
            end,
            % zero'ed bytes
            <<0:(serctl:constant(nccs) * 8)>>,
            [
                % Minimum number of characters
                {serctl:constant(vmin), 1},
                % Timeout in deciseconds
                {serctl:constant(vtime), 0}
            ]
        )
    },

    Termios = lists:foldl(
        fun(Fun, Acc) -> Fun(Acc) end,
        Mode,
        [
            fun(N) -> serctl:ispeed(N, Speed) end,
            fun(N) -> serctl:ospeed(N, Speed) end,

            fun(N) -> serctl:flow(N, false) end
        ]
    ),

    case serctl:open(Dev) of
        {ok, FD} ->
            case serctl:tcsetattr(FD, tcsanow, Termios) of
                ok ->
                    {ok, FD};
                Error ->
                    Error
            end;
        Error ->
            Error
    end.

%% @doc Reset the Arduino
-spec reset(serctl:fd()) -> ok.
reset(FD) ->
    [
        begin
            dtrrts(FD, Status),
            timer:sleep(50)
        end
     || Status <- [off, on]
    ],

    [
        begin
            serctl:readx(FD, 2, 250),
            sync(FD)
        end
     || _ <- lists:seq(1, 3)
    ],

    serctl:readx(FD, 2, 250),

    ok.

%% @doc Get version
-spec version(serctl:fd()) -> {byte(), byte()}.
version(FD) ->
    reset(FD),

    Size = [{size, 3}],

    {ok, <<?Resp_STK_INSYNC, Major, ?Resp_STK_OK>>} = cmd(
        FD,
        <<?Cmnd_STK_GET_PARAMETER, ?Parm_STK_SW_MAJOR, ?Sync_CRC_EOP>>,
        Size
    ),

    {ok, <<?Resp_STK_INSYNC, Minor, ?Resp_STK_OK>>} = cmd(
        FD,
        <<?Cmnd_STK_GET_PARAMETER, ?Parm_STK_SW_MINOR, ?Sync_CRC_EOP>>,
        Size
    ),

    {Major, Minor}.

-spec dtrrts(serctl:fd(), 0 | 1 | boolean()) -> ok.
dtrrts(FD, Status) when is_integer(Status) ->
    dtrrts(FD, bool(Status));
dtrrts(FD, Status) when is_atom(Status) ->
    {ok, <<Ctl:4/native-unsigned-integer-unit:8>>} = serctl:ioctl(
        FD,
        ?TIOCMGET,
        <<0:32>>
    ),

    Ctl1 =
        case Status of
            on -> Ctl bor (?TIOCM_DTR bor ?TIOCM_RTS);
            off -> Ctl band bnot (?TIOCM_DTR bor ?TIOCM_RTS)
        end,

    {ok, <<_Ctl2:4/native-unsigned-integer-unit:8>>} = serctl:ioctl(
        FD,
        ?TIOCMSET,
        <<Ctl1:4/native-unsigned-integer-unit:8>>
    ),

    ok.

% <<16#30, 16#20>>
-spec sync(serctl:fd()) -> ok | {error, file:posix()}.
sync(FD) ->
    cmd(FD, <<?Cmnd_STK_GET_SYNC, ?Sync_CRC_EOP>>).

%% @doc Read a hex file generated by the Arduino IDE
%%
%% hex_file/1 returns a list of binaries as read from the
%% % file (16 bytes). It's faster to use 128 bytes chunks.
-spec hex_file(file:name_all()) -> [byte()].
hex_file(File) ->
    hex_file(intel, File).

-spec hex_file(intel, file:name_all()) -> ok.
hex_file(intel, File) ->
    {ok, Bin0} = file:read_file(File),
    Bin = binary:replace(Bin0, <<"\r\n">>, <<"\n">>, [global]),
    Hex = binary:split(Bin, <<"\n">>, [global, trim]),
    rec(Hex).

%% @doc Load the hex file
-spec load(serctl:fd(), [byte()]) -> ok.
load(FD, Bytes) ->
    load(FD, Bytes, []).

-spec load(serctl:fd(), [byte()], proplists:proplist()) -> ok.
load(FD, Bytes, Opt) when is_list(Opt) ->
    reset(FD),

    {ok, ?STK500_OK} = cmd(FD, <<?Cmnd_STK_ENTER_PROGMODE, ?Sync_CRC_EOP>>, Opt),

    lists:foldl(
        fun(Buf, Address) ->
            {ok, ?STK500_OK} = cmd(
                FD,
                <<?Cmnd_STK_LOAD_ADDRESS, Address:2/little-unsigned-integer-unit:8, ?Sync_CRC_EOP>>,
                Opt
            ),

            {ok, ?STK500_OK} = cmd(
                FD,
                <<?Cmnd_STK_PROG_PAGE, (byte_size(Buf)):2/big-unsigned-integer-unit:8, $F,
                    Buf/bytes, ?Sync_CRC_EOP>>,
                Opt
            ),
            Address + byte_size(Buf) div 2
        end,
        0,
        Bytes
    ),

    {ok, ?STK500_OK} = cmd(FD, <<?Cmnd_STK_LEAVE_PROGMODE, ?Sync_CRC_EOP>>, Opt),

    ok.

cmd(FD, Cmd) ->
    cmd(FD, Cmd, []).

cmd(FD, Cmd, Opt) ->
    Verbose = proplists:get_value(verbose, Opt, false),

    verbose(Verbose, [{cmd, Cmd}]),

    case serctl:write(FD, Cmd) of
        ok ->
            cmd_1(FD, Cmd, Opt);
        {error, Error} ->
            {error, Error}
    end.

cmd_1(FD, _Cmd, Opt) ->
    Size = proplists:get_value(size, Opt, 2),

    case serctl:readx(FD, Size) of
        {ok, Resp} ->
            {ok, Resp};
        {error, Error} ->
            {error, Error}
    end.

chunk(Bytes, Size) when is_list(Bytes) ->
    chunk(list_to_binary(Bytes), Size);
chunk(Bytes, Size) when is_binary(Bytes) ->
    chunk(Bytes, Size, []).

chunk(Bytes, Size, Acc) when Size > 0, Size rem 2 == 0, Size =< 256 ->
    case Bytes of
        <<Chunk:Size/bytes, Rest/binary>> ->
            chunk(Rest, Size, [Chunk | Acc]);
        _ ->
            lists:reverse([Bytes | Acc])
    end.

%% @doc Try to discover the serial device
%%
%% Lookup the serial device in an environment variable: STK500_SERIAL_PORT
%%
%% Defaults to: /dev/ttyUSB0
serial_device() ->
    case os:getenv("STK500_SERIAL_PORT") of
        false ->
            ?DEV;
        Serial ->
            Serial
    end.

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
bin_to_int(N, Base) ->
    list_to_integer(binary_to_list(N), Base).

bool(0) -> off;
bool(1) -> on.

rec(Hex) ->
    rec(Hex, []).

rec([], Acc) ->
    lists:reverse(Acc);
% Type 0
% XXX Should compute and verify checksum
rec([<<":", LenX:2/bytes, _LoadX:4/bytes, "00", DataSum/binary>> | T], Acc) ->
    Len = bin_to_int(LenX, 16),

    % Each byte is encoded as 2 bytes hex
    HexLen = Len * 2,
    <<Data:HexLen/bytes, _Sum:2/bytes>> = DataSum,

    Bytes = rec_data(Data),

    rec(T, [list_to_binary(Bytes) | Acc]);
% Ignore other types
rec([_ | T], Acc) ->
    rec(T, Acc).

rec_data(Data) ->
    rec_data(Data, []).

rec_data(<<>>, Acc) ->
    lists:reverse(Acc);
rec_data(<<Byte:2/bytes, Rest/binary>>, Acc) ->
    rec_data(Rest, [bin_to_int(Byte, 16) | Acc]).

verbose(false, _) ->
    ok;
verbose(true, Data) ->
    error_logger:info_report(Data).