From cff47be336b0bfe12047a616ffcfb2873ab7a330 Mon Sep 17 00:00:00 2001 From: IciaC Date: Mon, 9 Sep 2024 15:34:56 +0200 Subject: [PATCH 01/18] WIP --- src/grisp_connect_api.erl | 29 +++++++++++ src/grisp_connect_updater_progress.erl | 70 ++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/grisp_connect_updater_progress.erl diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 33dcb69..3101567 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -39,6 +39,20 @@ handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> handle_rpc_messages(Batch, [grisp_connect_jsonrpc:format_error(E)| Replies]). +handle_request(<<"post">>, #{type := <<"start_update">>} = Params, ID) -> + URL = maps:get(url, Params, 1), + case start_update(URL) of + % TODO: start_update error cast + {error, update_unavailable} -> {error, + -564095940, + <<"grisp_updater unavailable">>, + undefined, + ID}; + {error, Reason} -> {error, + -32603, + iolist_to_binary(io_lib:format("~p", [Reason])), ID}; + ok -> {result, undefined, ID} + end; handle_request(<<"post">>, #{type := <<"flash">>} = Params, ID) -> Led = maps:get(led, Params, 1), Color = maps:get(color, Params, red), @@ -64,6 +78,21 @@ flash(Led, Color) -> end), ok. +start_update(URL) -> + case is_running(grisp_updater) of + true -> grisp_updater:start_update(URL, + grisp_conntect_updater_progress, + #{client => self()}, #{}); + false -> {error, update_unavailable} + end. + +is_running(AppName) -> + Apps = application:which_applications(), + case [App || {App, _Desc, _VSN} <- Apps, App =:= AppName] of + [] -> false; + [_] -> true + end. + error_atom(-1) -> device_not_linked; error_atom(-2) -> token_expired; error_atom(-3) -> device_already_linked; diff --git a/src/grisp_connect_updater_progress.erl b/src/grisp_connect_updater_progress.erl new file mode 100644 index 0000000..4ff0f65 --- /dev/null +++ b/src/grisp_connect_updater_progress.erl @@ -0,0 +1,70 @@ +-module(grisp_connect_updater_progress). +-behaviour(grisp_updater_progress). + +%--- Includes ------------------------------------------------------------------ + +-include_lib("kernel/include/logger.hrl"). + + +%--- Types --------------------------------------------------------------------- + + +%--- Exports ------------------------------------------------------------------- + + +% Behaviour grisp_updater_progress callbacks +-export([progress_init/1]). +-export([progress_update/2]). +-export([progress_warning/3]). +-export([progress_error/3]). +-export([progress_done/2]). + + +%--- records ------------------------------------------------------------------- + +-record(state, { + client :: pid(), + last_notification :: undefined | integer() +}). + + +%--- API Functions ------------------------------------------------------------- + + +%--- Behavior grisp_updater_progress Callback ---------------------------------- + +progress_init(#{client := PID} = _Opts) -> + {ok, #state{ + last_notification = erlang:system_time(millisecond), + client = PID + }}. + +progress_update(#state{last_notification = LastLog} = State, Stats) -> + case (erlang:system_time(millisecond) - LastLog) > 1000 of + false -> {ok, State}; + true -> + % Recheck log level when there is another way to check the progress update + ?LOG_NOTICE("Update progress: ~b%", [progress_percent(Stats)]), + {ok, State#state{last_notification = erlang:system_time(millisecond)}} + end. + +progress_warning(State, Msg, Reason) -> + ?LOG_WARNING("Update warning; ~s: ~p", [Msg, Reason]), + {ok, State}. + +progress_error(#state{}, Stats, Reason) -> + ?LOG_ERROR("Update failed after ~b% : ~p", + [progress_percent(Stats), Reason]), + ok. + +progress_done(#state{}, _Stats) -> + ?LOG_NOTICE("Update done", []), + ok. + + +%--- Internal Functions -------------------------------------------------------- + +progress_percent(Stats) -> + #{data_total := Total, data_checked := Checked, + data_skipped := Skipped, data_written := Written} = Stats, + (Checked + Skipped + Written) * 100 div (Total * 2). From c50d52abc8346c4a30b6e08a9ffc660443e85081 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Mon, 23 Sep 2024 18:19:47 +0200 Subject: [PATCH 02/18] Send the REason to the peer in inteernal_error error types --- src/grisp_connect_jsonrpc.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/grisp_connect_jsonrpc.erl b/src/grisp_connect_jsonrpc.erl index 8060bd7..3e83b09 100644 --- a/src/grisp_connect_jsonrpc.erl +++ b/src/grisp_connect_jsonrpc.erl @@ -47,8 +47,8 @@ format_error({internal_error, method_not_found, ID}) -> {error, -32601, <<"Method not found">>, undefined, ID}; format_error({internal_error, invalid_params, ID}) -> {error, -32602, <<"Invalid params">>, undefined, ID}; -format_error({internal_error, internal_error, ID}) -> - {error, -32603, <<"Internal error">>, undefined, ID}. +format_error({internal_error, Reason, ID}) -> + {error, -32603, <<"Internal error">>, Reason, ID}. %--- Internal ----------------------------------------------------------------- From dc3cf2f1c36daa65f20e5ff19ba557d3a11c0ef1 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Mon, 23 Sep 2024 18:21:49 +0200 Subject: [PATCH 03/18] Cleanup doc and spec, fix communication errors in jsonrpc handling --- src/grisp_connect_api.erl | 41 ++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 3101567..622d3e0 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -8,12 +8,23 @@ %--- API ----------------------------------------------------------------------- +% #doc Assembles a jsonrpc request and its uuid +-spec request(Method :: atom() | binary(), + Type :: atom() | binary(), + Params :: map()) -> {ID :: binary(), Encoded :: binary()}. request(Method, Type, Params) -> ID = id(), Rpc = {request, Method, maps:put(type, Type, Params), ID}, Encoded = grisp_connect_jsonrpc:encode(Rpc), {ID, Encoded}. +% @doc Indentifies if the message is a request or a reply to a previous request. +% In case it was a request, returns the reply to be sent to the peer. +% In case it was a response, returns the parsed ID and content to be handled by +% the caller. +-spec handle_msg(JSON :: binary()) -> + {request, Response :: binary()} | + {response, ID :: binary(), {ok, Result :: map()} | {error, atom()}}. handle_msg(JSON) -> JSON_RPC = grisp_connect_jsonrpc:decode(JSON), handle_jsonrpc(JSON_RPC). @@ -41,24 +52,26 @@ handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> handle_request(<<"post">>, #{type := <<"start_update">>} = Params, ID) -> URL = maps:get(url, Params, 1), - case start_update(URL) of + Reply = case start_update(URL) of % TODO: start_update error cast - {error, update_unavailable} -> {error, - -564095940, - <<"grisp_updater unavailable">>, - undefined, - ID}; - {error, Reason} -> {error, - -32603, - iolist_to_binary(io_lib:format("~p", [Reason])), ID}; - ok -> {result, undefined, ID} - end; + {error, update_unavailable} -> + {error, -564095940, <<"grisp_updater unavailable">>, undefined, ID}; + {error, Reason} -> + ReasonText = iolist_to_binary(io_lib:format("~p", [Reason])), + grisp_connect_jsonrpc:format_error({internal_error, Reason, ID}); + ok -> + {result, ok, ID} + end, + {request, grisp_connect_jsonrpc:encode(Reply)}; handle_request(<<"post">>, #{type := <<"flash">>} = Params, ID) -> Led = maps:get(led, Params, 1), Color = maps:get(color, Params, red), - {result, flash(Led, Color), ID}; + Reply = {result, flash(Led, Color), ID}, + {request, grisp_connect_jsonrpc:encode(Reply)}; handle_request(_, _, ID) -> - grisp_connect_jsonrpc:format_error({internal_error, method_not_found, ID}). + Error = {internal_error, method_not_found, ID}, + FormattedError = grisp_connect_jsonrpc:format_error(), + grisp_connect_jsonrpc:encode(FormattedError). handle_response(Response) -> {ID, Reply} = case Response of @@ -80,7 +93,7 @@ flash(Led, Color) -> start_update(URL) -> case is_running(grisp_updater) of - true -> grisp_updater:start_update(URL, + true -> grisp_updater:start_update(URL, grisp_conntect_updater_progress, #{client => self()}, #{}); false -> {error, update_unavailable} From 995fc0b76a03b1e65451e3d3d3c4334e98e49777 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Tue, 24 Sep 2024 15:54:47 +0200 Subject: [PATCH 04/18] Improve code readability in handle_msg return atoms --- src/grisp_connect_api.erl | 10 +++++----- src/grisp_connect_client.erl | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 622d3e0..0bf795d 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -23,8 +23,8 @@ request(Method, Type, Params) -> % In case it was a response, returns the parsed ID and content to be handled by % the caller. -spec handle_msg(JSON :: binary()) -> - {request, Response :: binary()} | - {response, ID :: binary(), {ok, Result :: map()} | {error, atom()}}. + {send_response, Response :: binary()} | + {handle_response, ID :: binary(), {ok, Result :: map()} | {error, atom()}}. handle_msg(JSON) -> JSON_RPC = grisp_connect_jsonrpc:decode(JSON), handle_jsonrpc(JSON_RPC). @@ -62,12 +62,12 @@ handle_request(<<"post">>, #{type := <<"start_update">>} = Params, ID) -> ok -> {result, ok, ID} end, - {request, grisp_connect_jsonrpc:encode(Reply)}; + {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(<<"post">>, #{type := <<"flash">>} = Params, ID) -> Led = maps:get(led, Params, 1), Color = maps:get(color, Params, red), Reply = {result, flash(Led, Color), ID}, - {request, grisp_connect_jsonrpc:encode(Reply)}; + {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(_, _, ID) -> Error = {internal_error, method_not_found, ID}, FormattedError = grisp_connect_jsonrpc:format_error(), @@ -80,7 +80,7 @@ handle_response(Response) -> {error, Code, _Message, _Data, ID0} -> {ID0, {error, error_atom(Code)}} end, - {response, ID, Reply}. + {handle_response, ID, Reply}. flash(Led, Color) -> spawn(fun() -> diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl index 2fc299d..40deedb 100644 --- a/src/grisp_connect_client.erl +++ b/src/grisp_connect_client.erl @@ -121,15 +121,15 @@ connected(cast, disconnected, Data) -> grisp_connect_log_server:stop(), {next_state, waiting_ip, Data}; connected(cast, {handle_message, Payload}, #data{requests = Requests} = Data) -> - Replies = grisp_connect_api:handle_msg(Payload), + Responses = grisp_connect_api:handle_msg(Payload), % A reduce operation is needed to support jsonrpc batch comunications - case Replies of + case Responses of [] -> keep_state_and_data; - [{request, Response}] -> % Response for a GRiSP.io request + [{send_response, Response}] -> % Response for a GRiSP.io request grisp_connect_ws:send(Response), keep_state_and_data; - [{response, ID, Response}] -> % GRiSP.io response + [{handle_response, ID, Response}] -> % handle a GRiSP.io response {OtherRequests, Actions} = dispatch_response(ID, Response, Requests), {keep_state, Data#data{requests = OtherRequests}, Actions} end; From 8c7e01842391a879bbf9e65dbd00cc86a92d3c34 Mon Sep 17 00:00:00 2001 From: IciaC Date: Wed, 25 Sep 2024 15:43:45 +0200 Subject: [PATCH 05/18] WIP --- docs/grisp_connect_api.md | 72 +++++++++++++++++++++++++++++++++++++++ src/grisp_connect_api.erl | 52 +++++++++++++++------------- 2 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 docs/grisp_connect_api.md diff --git a/docs/grisp_connect_api.md b/docs/grisp_connect_api.md new file mode 100644 index 0000000..a00bddc --- /dev/null +++ b/docs/grisp_connect_api.md @@ -0,0 +1,72 @@ +# UI Websocket API + +**Table Of Contents** +- [UI Websocket API](#ui-websocket-api) + - [Backend API](#backend-api) + - [Requests](#requests) + - [Error Codes](#error-codes) + - [Default error codes](#default-error-codes) + - [Custom error codes](#custom-error-codes) + +We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend. + +## Backend API + +### Requests + +
Post - Start an update +

+ +**`params`:** +| key (required *) | value | description | +| ----------------- | -------- | -------------------------- | +| `"type"` * | string | `"start_update"` | +| `"url"` * | [string] | URL to the code repository | + +**`result`**: `"ok"` + +**`error`**: + +| Error Content | When it Happens | +| -------------------------------------------------------- | ------------------------------------------------- | +| `{code: -32001, message: "grisp_updater unavailable"}` | When the grisp_updater application is not running | + +

+
+ +
Post - Flash +

+ +**`params`:** +| key (required *) | value | description | +| ------------------- | -------- | --------------------------------- | +| `"led"` * | integer | Number that identifies the LED, to obtain more information about the options, you can visit [grisp_led:color/2](https://hexdocs.pm/grisp/) | +| `"color"` | string | Color of the LED, by default: red. To obtain more information about the options, you can visit [grisp_led:color/2](https://hexdocs.pm/grisp/) | + +**`result`**: `"ok"` + +

+
+ +## Error Codes + +### Default error codes + +| code | message | meaning | +|---------|------------------|--------------------------------------------------| +|-32700 | Parse error | Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. | +|-32600 | Invalid Request | The JSON sent is not a valid Request object. | +|-32601 | Method not found | The method does not exist / is not available.| +|-32602 | Invalid params | Invalid method parameter(s). | +|-32603 | Internal error | Internal JSON-RPC error. | + +### Custom error codes + +Additionally to the default jsonrpc error codes the following codes will be returned. + +|code | message | meaning | +|---|---|---| +| -1 | `"device not linked"` | device can't be used without being linked to a registered user | +| -2 | `"token expired"` | token is expired | +| -3 | `"device already linked"` | device needs to be unlinked first via UI | +| -4 | `"invalid token"` | token is e.g. not orderly encoded | diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 0bf795d..5f0222c 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -1,4 +1,4 @@ -%% @doc Librabry module containing the jsonrpc API logic +%% @doc Library module containing the jsonrpc API logic -module(grisp_connect_api). -export([request/3]). @@ -51,18 +51,23 @@ handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> [grisp_connect_jsonrpc:format_error(E)| Replies]). handle_request(<<"post">>, #{type := <<"start_update">>} = Params, ID) -> - URL = maps:get(url, Params, 1), - Reply = case start_update(URL) of - % TODO: start_update error cast - {error, update_unavailable} -> - {error, -564095940, <<"grisp_updater unavailable">>, undefined, ID}; - {error, Reason} -> - ReasonText = iolist_to_binary(io_lib:format("~p", [Reason])), - grisp_connect_jsonrpc:format_error({internal_error, Reason, ID}); - ok -> - {result, ok, ID} - end, - {send_response, grisp_connect_jsonrpc:encode(Reply)}; + try + URL = maps:get(url, Params, undefined), + Reply = case start_update(URL) of + {error, grisp_updater_unavailable} -> + {error, -10, grisp_updater_unavailable, undefined, ID}; + {error, Reason} -> + ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), + grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}); + ok -> + {result, ok, ID} + end, + {request, grisp_connect_jsonrpc:encode(Reply)} + catch + throw:invalid_params -> + {request, + grisp_connect_jsonrpc:format_error({internal_error, invalid_params, ID})} + end; handle_request(<<"post">>, #{type := <<"flash">>} = Params, ID) -> Led = maps:get(led, Params, 1), Color = maps:get(color, Params, red), @@ -70,7 +75,7 @@ handle_request(<<"post">>, #{type := <<"flash">>} = Params, ID) -> {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(_, _, ID) -> Error = {internal_error, method_not_found, ID}, - FormattedError = grisp_connect_jsonrpc:format_error(), + FormattedError = grisp_connect_jsonrpc:format_error(Error), grisp_connect_jsonrpc:encode(FormattedError). handle_response(Response) -> @@ -93,10 +98,10 @@ flash(Led, Color) -> start_update(URL) -> case is_running(grisp_updater) of - true -> grisp_updater:start_update(URL, - grisp_conntect_updater_progress, - #{client => self()}, #{}); - false -> {error, update_unavailable} + true -> grisp_updater:start(URL, + grisp_conntect_updater_progress, + #{client => self()}, #{}); + false -> {error, grisp_updater_unavailable} end. is_running(AppName) -> @@ -106,11 +111,12 @@ is_running(AppName) -> [_] -> true end. -error_atom(-1) -> device_not_linked; -error_atom(-2) -> token_expired; -error_atom(-3) -> device_already_linked; -error_atom(-4) -> invalid_token; -error_atom(_) -> jsonrpc_error. +error_atom(-1) -> device_not_linked; +error_atom(-2) -> token_expired; +error_atom(-3) -> device_already_linked; +error_atom(-4) -> invalid_token; +error_atom(-10) -> grisp_updater_unavailable; +error_atom(_) -> jsonrpc_error. id() -> list_to_binary(integer_to_list(erlang:unique_integer())). From 8a0bda4678557f89c6755335a2f4a5f0ab0ee7f5 Mon Sep 17 00:00:00 2001 From: IciaC Date: Thu, 26 Sep 2024 17:20:00 +0200 Subject: [PATCH 06/18] doc & fix grisp_updater call --- docs/grisp_connect_api.md | 3 ++- src/grisp_connect_api.erl | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/grisp_connect_api.md b/docs/grisp_connect_api.md index a00bddc..bb248c1 100644 --- a/docs/grisp_connect_api.md +++ b/docs/grisp_connect_api.md @@ -29,7 +29,8 @@ We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend. | Error Content | When it Happens | | -------------------------------------------------------- | ------------------------------------------------- | -| `{code: -32001, message: "grisp_updater unavailable"}` | When the grisp_updater application is not running | +| `{code: -10, message: "grisp_updater unavailable"}` | Grisp updater app is not running | +| `{code: -11, message: "already updating "}` | An update is already happening |

diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 5f0222c..8ca1081 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -6,6 +6,12 @@ -include_lib("kernel/include/logger.hrl"). +%--- Macros -------------------------------------------------------------------- +-define(method_get, <<"get">>). +-define(method_post, <<"post">>). +-define(method_patch, <<"patch">>). +-define(method_delete, <<"delete">>). + %--- API ----------------------------------------------------------------------- % #doc Assembles a jsonrpc request and its uuid @@ -38,7 +44,7 @@ handle_jsonrpc({single, Rpc}) -> handle_rpc_messages([], Replies) -> lists:reverse(Replies); handle_rpc_messages([{request, M, Params, ID} | Batch], Replies) -when M == <<"post">> -> + when M == ?method_post -> handle_rpc_messages(Batch, [handle_request(M, Params, ID) | Replies]); handle_rpc_messages([{result, _, _} = Res| Batch], Replies) -> handle_rpc_messages(Batch, [handle_response(Res)| Replies]); @@ -50,12 +56,14 @@ handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> handle_rpc_messages(Batch, [grisp_connect_jsonrpc:format_error(E)| Replies]). -handle_request(<<"post">>, #{type := <<"start_update">>} = Params, ID) -> +handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> try - URL = maps:get(url, Params, undefined), + URL = maps:get(url, Params), Reply = case start_update(URL) of {error, grisp_updater_unavailable} -> {error, -10, grisp_updater_unavailable, undefined, ID}; + {error, already_updating} -> + {error, -11, already_updating, undefined, ID}; {error, Reason} -> ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}); @@ -64,11 +72,12 @@ handle_request(<<"post">>, #{type := <<"start_update">>} = Params, ID) -> end, {request, grisp_connect_jsonrpc:encode(Reply)} catch - throw:invalid_params -> + throw:bad_key -> {request, - grisp_connect_jsonrpc:format_error({internal_error, invalid_params, ID})} + grisp_connect_jsonrpc:format_error( + {internal_error, invalid_params, ID})} end; -handle_request(<<"post">>, #{type := <<"flash">>} = Params, ID) -> +handle_request(?method_post, #{type := <<"flash">>} = Params, ID) -> Led = maps:get(led, Params, 1), Color = maps:get(color, Params, red), Reply = {result, flash(Led, Color), ID}, @@ -116,6 +125,7 @@ error_atom(-2) -> token_expired; error_atom(-3) -> device_already_linked; error_atom(-4) -> invalid_token; error_atom(-10) -> grisp_updater_unavailable; +error_atom(-11) -> already_updating; error_atom(_) -> jsonrpc_error. id() -> From 52eaf211018eceba87874cd93e3bd11315879bcf Mon Sep 17 00:00:00 2001 From: IciaC Date: Fri, 27 Sep 2024 16:55:19 +0200 Subject: [PATCH 07/18] Typo in callback module --- src/grisp_connect_api.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 8ca1081..b90a6f8 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -108,7 +108,7 @@ flash(Led, Color) -> start_update(URL) -> case is_running(grisp_updater) of true -> grisp_updater:start(URL, - grisp_conntect_updater_progress, + grisp_connect_updater_progress, #{client => self()}, #{}); false -> {error, grisp_updater_unavailable} end. From 411080358129942f5ffcc33dd71160c75b82b7ae Mon Sep 17 00:00:00 2001 From: IciaC Date: Fri, 27 Sep 2024 17:05:31 +0200 Subject: [PATCH 08/18] update readme --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3428e81..e4495e2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # grisp_connect -GRiSP.io Client Library for GRiSP +GRiSP.io Client Library for GRiSP. This library enables secure communication + between your GRiSP2 board and the [GRiSP.io](https://grisp.io) services using + Mutual TLS (mTLS). To get started, add this application as a dependency in your + GRiSP2 project. + +⚠️ **Note:** If you plan to use the API calls related to `grisp_updater`, make + sure to add `grisp_updater` as a dependency in your project as well. ## Table of content @@ -28,10 +34,6 @@ GRiSP.io Client Library for GRiSP - [Development on GRiSP Hardware](#development-on-grisp-hardware) - [Production on GRiSP Hardware](#production-on-grisp-hardware) - -Add this application as a dependency in your GRiSP2 project. -Your board will connect securely using mTLS to the [GRiSP.io](https://grisp.io) services. - ## Usage ### Option 1. Use the `rebar3_grisp` plugin From e7575006f1a574bc6d70f4c9177487f283335200 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Wed, 2 Oct 2024 10:08:32 +0200 Subject: [PATCH 09/18] Fix api atoms --- src/grisp_connect_api.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index b90a6f8..c6e75f9 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -57,7 +57,7 @@ handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> [grisp_connect_jsonrpc:format_error(E)| Replies]). handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> - try + try URL = maps:get(url, Params), Reply = case start_update(URL) of {error, grisp_updater_unavailable} -> @@ -70,10 +70,10 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> ok -> {result, ok, ID} end, - {request, grisp_connect_jsonrpc:encode(Reply)} + {send_response, grisp_connect_jsonrpc:encode(Reply)} catch throw:bad_key -> - {request, + {send_response, grisp_connect_jsonrpc:format_error( {internal_error, invalid_params, ID})} end; @@ -85,7 +85,7 @@ handle_request(?method_post, #{type := <<"flash">>} = Params, ID) -> handle_request(_, _, ID) -> Error = {internal_error, method_not_found, ID}, FormattedError = grisp_connect_jsonrpc:format_error(Error), - grisp_connect_jsonrpc:encode(FormattedError). + {send_response, grisp_connect_jsonrpc:encode(FormattedError)}. handle_response(Response) -> {ID, Reply} = case Response of From da340378b59c510e81e18efbd88851a1403e8406 Mon Sep 17 00:00:00 2001 From: IciaC Date: Fri, 4 Oct 2024 01:25:51 +0200 Subject: [PATCH 10/18] Notify grisp_manager of update progress --- docs/grisp_connect_api.md | 12 ++++++++++++ src/grisp_connect_api.erl | 20 ++++++++++++++++---- src/grisp_connect_updater_progress.erl | 12 +++++++++--- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/grisp_connect_api.md b/docs/grisp_connect_api.md index bb248c1..484d3ad 100644 --- a/docs/grisp_connect_api.md +++ b/docs/grisp_connect_api.md @@ -7,6 +7,7 @@ - [Error Codes](#error-codes) - [Default error codes](#default-error-codes) - [Custom error codes](#custom-error-codes) + - [Notifications](#notifications) We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend. @@ -71,3 +72,14 @@ Additionally to the default jsonrpc error codes the following codes will be retu | -2 | `"token expired"` | token is expired | | -3 | `"device already linked"` | device needs to be unlinked first via UI | | -4 | `"invalid token"` | token is e.g. not orderly encoded | + +## Notifications + +
Notify - Status Update + +**`result`**: `JSON Object` + +| key | value | description | +| -------------- | ----------------- | --------------------------------- | +| `"type"` | `"status_update"` | Type of notification | +| `"percentage"` | integer | Progress percentage of the update | diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index c6e75f9..e6664ec 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -11,6 +11,7 @@ -define(method_post, <<"post">>). -define(method_patch, <<"patch">>). -define(method_delete, <<"delete">>). +-define(method_notify, <<"notify">>). %--- API ----------------------------------------------------------------------- @@ -56,6 +57,11 @@ handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> handle_rpc_messages(Batch, [grisp_connect_jsonrpc:format_error(E)| Replies]). +handle_request(?method_post, #{type := <<"flash">>} = Params, ID) -> + Led = maps:get(led, Params, 1), + Color = maps:get(color, Params, red), + Reply = {result, flash(Led, Color), ID}, + {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> try URL = maps:get(url, Params), @@ -77,10 +83,16 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> grisp_connect_jsonrpc:format_error( {internal_error, invalid_params, ID})} end; -handle_request(?method_post, #{type := <<"flash">>} = Params, ID) -> - Led = maps:get(led, Params, 1), - Color = maps:get(color, Params, red), - Reply = {result, flash(Led, Color), ID}, +handle_request(?method_notify, #{type := <<"status_update">>} = Params, ID) -> + Percentage = maps:get(percentage, Params, undefined), + Reply = case Percentage of + Per when is_integer(Per) -> + {result, #{percentage => Percentage}, ID}; + undefined -> + Reason = update_percentage_not_retrieved, + ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), + grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}) + end, {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(_, _, ID) -> Error = {internal_error, method_not_found, ID}, diff --git a/src/grisp_connect_updater_progress.erl b/src/grisp_connect_updater_progress.erl index 4ff0f65..9d16144 100644 --- a/src/grisp_connect_updater_progress.erl +++ b/src/grisp_connect_updater_progress.erl @@ -41,10 +41,16 @@ progress_init(#{client := PID} = _Opts) -> progress_update(#state{last_notification = LastLog} = State, Stats) -> case (erlang:system_time(millisecond) - LastLog) > 1000 of - false -> {ok, State}; - true -> + false -> + {ok, State}; + true -> + UpdatePercentage = progress_percent(Stats), % Recheck log level when there is another way to check the progress update - ?LOG_NOTICE("Update progress: ~b%", [progress_percent(Stats)]), + ?LOG_NOTICE("Update progress: ~b%", [UpdatePercentage]), + grisp_connect_client:request( + <<"notify">>, + <<"status_update">>, + #{percentage => UpdatePercentage}), {ok, State#state{last_notification = erlang:system_time(millisecond)}} end. From 7f7a24c40dc4277431bb08ac519d918d14b079ef Mon Sep 17 00:00:00 2001 From: IciaC Date: Fri, 4 Oct 2024 02:10:42 +0200 Subject: [PATCH 11/18] Fix PR comments --- README.md | 4 ++-- docs/grisp_connect_api.md | 22 ++++------------------ src/grisp_connect_api.erl | 14 -------------- 3 files changed, 6 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e4495e2..461fd9c 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ GRiSP.io Client Library for GRiSP. This library enables secure communication Mutual TLS (mTLS). To get started, add this application as a dependency in your GRiSP2 project. -⚠️ **Note:** If you plan to use the API calls related to `grisp_updater`, make - sure to add `grisp_updater` as a dependency in your project as well. +⚠️ **Note:** If you plan to use the API calls related to `grisp_updater_grisp2`, +make sure to add `grisp_updater_grisp2` as a dependency in your project as well. ## Table of content diff --git a/docs/grisp_connect_api.md b/docs/grisp_connect_api.md index 484d3ad..9c0e103 100644 --- a/docs/grisp_connect_api.md +++ b/docs/grisp_connect_api.md @@ -28,24 +28,10 @@ We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend. **`error`**: -| Error Content | When it Happens | -| -------------------------------------------------------- | ------------------------------------------------- | -| `{code: -10, message: "grisp_updater unavailable"}` | Grisp updater app is not running | -| `{code: -11, message: "already updating "}` | An update is already happening | - -

-
- -
Post - Flash -

- -**`params`:** -| key (required *) | value | description | -| ------------------- | -------- | --------------------------------- | -| `"led"` * | integer | Number that identifies the LED, to obtain more information about the options, you can visit [grisp_led:color/2](https://hexdocs.pm/grisp/) | -| `"color"` | string | Color of the LED, by default: red. To obtain more information about the options, you can visit [grisp_led:color/2](https://hexdocs.pm/grisp/) | - -**`result`**: `"ok"` +| Error Content | When it Happens | +| ----------------------------------------------------| -------------------------------- | +| `{code: -10, message: "grisp_updater unavailable"}` | Grisp updater app is not running | +| `{code: -11, message: "already updating "}` | An update is already happening |

diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index e6664ec..46011d4 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -57,11 +57,6 @@ handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> handle_rpc_messages(Batch, [grisp_connect_jsonrpc:format_error(E)| Replies]). -handle_request(?method_post, #{type := <<"flash">>} = Params, ID) -> - Led = maps:get(led, Params, 1), - Color = maps:get(color, Params, red), - Reply = {result, flash(Led, Color), ID}, - {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> try URL = maps:get(url, Params), @@ -108,15 +103,6 @@ handle_response(Response) -> end, {handle_response, ID, Reply}. -flash(Led, Color) -> - spawn(fun() -> - ?LOG_NOTICE("Flash from Seawater!~n"), - grisp_led:color(Led, Color), - timer:sleep(100), - grisp_led:off(Led) - end), - ok. - start_update(URL) -> case is_running(grisp_updater) of true -> grisp_updater:start(URL, From c476dcd9323817258d46e533618797aa96be5168 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Fri, 4 Oct 2024 12:31:23 +0000 Subject: [PATCH 12/18] Send progress notifications to the backend (#47) * Do not loop on sync calls to WS while waiting for a connection This allows to instantly detect a connection * Support sending of jsonrpc notifications * Use new notification to report download progress --- src/grisp_connect_api.erl | 13 +++++++++++-- src/grisp_connect_client.erl | 25 +++++++++++++++---------- src/grisp_connect_updater_progress.erl | 17 +++++++++++------ src/grisp_connect_ws.erl | 9 +++------ 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 46011d4..8d56580 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -2,6 +2,7 @@ -module(grisp_connect_api). -export([request/3]). +-export([notify/3]). -export([handle_msg/1]). -include_lib("kernel/include/logger.hrl"). @@ -25,6 +26,14 @@ request(Method, Type, Params) -> Encoded = grisp_connect_jsonrpc:encode(Rpc), {ID, Encoded}. +% #doc Assembles a jsonrpc notification +-spec notify(Method :: atom() | binary(), + Type :: atom() | binary(), + Params :: map()) -> Encoded :: binary(). +notify(Method, Type, Params) -> + Rpc = {notification, Method, maps:put(type, Type, Params)}, + grisp_connect_jsonrpc:encode(Rpc). + % @doc Indentifies if the message is a request or a reply to a previous request. % In case it was a request, returns the reply to be sent to the peer. % In case it was a response, returns the parsed ID and content to be handled by @@ -81,9 +90,9 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> handle_request(?method_notify, #{type := <<"status_update">>} = Params, ID) -> Percentage = maps:get(percentage, Params, undefined), Reply = case Percentage of - Per when is_integer(Per) -> + Per when is_integer(Per) -> {result, #{percentage => Percentage}, ID}; - undefined -> + undefined -> Reason = update_percentage_not_retrieved, ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}) diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl index 40deedb..9a258ef 100644 --- a/src/grisp_connect_client.erl +++ b/src/grisp_connect_client.erl @@ -10,8 +10,10 @@ -export([connect/0]). -export([is_connected/0]). -export([request/3]). +-export([notify/3]). % Internal API +-export([connected/0]). -export([disconnected/0]). -export([handle_message/1]). @@ -49,6 +51,12 @@ is_connected() -> request(Method, Type, Params) -> gen_statem:call(?MODULE, {?FUNCTION_NAME, Method, Type, Params}). +notify(Method, Type, Params) -> + gen_statem:cast(?MODULE, {?FUNCTION_NAME, Method, Type, Params}). + +connected() -> + gen_statem:cast(?MODULE, ?FUNCTION_NAME). + disconnected() -> gen_statem:cast(?MODULE, ?FUNCTION_NAME). @@ -97,16 +105,9 @@ connecting(enter, _OldState, _Data) -> {ok, Port} = application:get_env(grisp_connect, port), ?LOG_NOTICE(#{event => connecting, domain => Domain, port => Port}), grisp_connect_ws:connect(Domain, Port), - {keep_state_and_data, [{state_timeout, 0, wait}]}; -connecting(state_timeout, wait, Data) -> - case grisp_connect_ws:is_connected() of - true -> - ?LOG_NOTICE(#{event => connected}), - {next_state, connected, Data}; - false -> - ?LOG_DEBUG(#{event => waiting_ws_connection}), - {keep_state_and_data, [{state_timeout, ?STD_TIMEOUT, wait}]} - end; + keep_state_and_data; +connecting(cast, connected, Data) -> + {next_state, connected, Data}; connecting(cast, disconnected, _Data) -> repeat_state_and_data; ?HANDLE_COMMON. @@ -141,6 +142,10 @@ connected({call, From}, {request, Method, Type, Params}, {keep_state, Data#data{requests = NewRequests}, [{{timeout, ID}, request_timeout(), request}]}; +connected(cast, {notify, Method, Type, Params}, _Data) -> + Payload = grisp_connect_api:notify(Method, Type, Params), + grisp_connect_ws:send(Payload), + keep_state_and_data; ?HANDLE_COMMON. % Common event handling appended as last match case to each state_function diff --git a/src/grisp_connect_updater_progress.erl b/src/grisp_connect_updater_progress.erl index 9d16144..2bb5db1 100644 --- a/src/grisp_connect_updater_progress.erl +++ b/src/grisp_connect_updater_progress.erl @@ -41,16 +41,17 @@ progress_init(#{client := PID} = _Opts) -> progress_update(#state{last_notification = LastLog} = State, Stats) -> case (erlang:system_time(millisecond) - LastLog) > 1000 of - false -> + false -> {ok, State}; - true -> + true -> UpdatePercentage = progress_percent(Stats), % Recheck log level when there is another way to check the progress update ?LOG_NOTICE("Update progress: ~b%", [UpdatePercentage]), - grisp_connect_client:request( - <<"notify">>, - <<"status_update">>, - #{percentage => UpdatePercentage}), + grisp_connect_client:notify( + <<"update">>, + <<"software_update_event">>, + #{event => progress, + percentage => UpdatePercentage}), {ok, State#state{last_notification = erlang:system_time(millisecond)}} end. @@ -65,6 +66,10 @@ progress_error(#state{}, Stats, Reason) -> progress_done(#state{}, _Stats) -> ?LOG_NOTICE("Update done", []), + grisp_connect_client:notify( + <<"update">>, + <<"software_update_event">>, + #{event => done}), ok. diff --git a/src/grisp_connect_ws.erl b/src/grisp_connect_ws.erl index 88a90b5..074ed46 100644 --- a/src/grisp_connect_ws.erl +++ b/src/grisp_connect_ws.erl @@ -4,7 +4,6 @@ -export([start_link/0]). -export([connect/0]). -export([connect/2]). --export([is_connected/0]). -export([send/1]). -behaviour(gen_server). @@ -37,9 +36,6 @@ connect() -> connect(Server, Port) -> gen_server:cast(?MODULE, {?FUNCTION_NAME, Server, Port}). -is_connected() -> - gen_server:call(?MODULE, ?FUNCTION_NAME). - send(Payload) -> gen_server:cast(?MODULE, {?FUNCTION_NAME, Payload}). @@ -47,8 +43,8 @@ send(Payload) -> init([]) -> {ok, #state{}}. -handle_call(is_connected, _, #state{ws_up = Up} = S) -> - {reply, Up, S}. +handle_call(Call, _, _) -> + error({unexpected_call, Call}). handle_cast({connect, Server, Port}, #state{gun_pid = undefined} = S) -> GunOpts = #{ @@ -87,6 +83,7 @@ handle_info({gun_up, Pid, http}, #state{gun_pid = GunPid} = S) -> handle_info({gun_upgrade, Pid, Stream, [<<"websocket">>], _}, #state{gun_pid = Pid, ws_stream = Stream} = S) -> ?LOG_INFO(#{event => ws_upgrade}), + grisp_connect_client:connected(), {noreply, S#state{ws_up = true, ping_timer = start_ping_timer()}}; handle_info({gun_response, Pid, Stream, _, Status, _Headers}, #state{gun_pid = Pid, ws_stream = Stream} = S) -> From 6caf750b3ea83f74bc82eb4cb9073bbbad8b4609 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Fri, 4 Oct 2024 15:22:23 +0200 Subject: [PATCH 13/18] Fix test suites timings to adapt to faster reconnection detections --- test/grisp_connect_test_client.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/grisp_connect_test_client.erl b/test/grisp_connect_test_client.erl index 587e26d..b76fefe 100644 --- a/test/grisp_connect_test_client.erl +++ b/test/grisp_connect_test_client.erl @@ -17,7 +17,7 @@ cert_dir() -> filename:join(code:lib_dir(grisp_connect, test), "certs"). serial_number() -> <<"0000">>. wait_connection() -> - wait_connection(20). + wait_connection(2000). wait_connection(0) -> ct:pal("grisp_connect_ws state:~n~p~n", [sys:get_state(grisp_connect_ws)]), @@ -26,12 +26,12 @@ wait_connection(N) -> case grisp_connect:is_connected() of true -> ok; false -> - ct:sleep(100), + ct:sleep(1), wait_connection(N - 1) end. wait_disconnection() -> - wait_disconnection(20). + wait_disconnection(2000). wait_disconnection(0) -> ct:pal("grisp_connect_ws state:~n~p~n", [sys:get_state(grisp_connect_ws)]), @@ -39,7 +39,7 @@ wait_disconnection(0) -> wait_disconnection(N) -> case grisp_connect:is_connected() of true -> - ct:sleep(100), + ct:sleep(1), wait_disconnection(N - 1); false -> ok end. From b0c9435583e0972d4268446b5983b173ff1c23be Mon Sep 17 00:00:00 2001 From: IciaC Date: Mon, 7 Oct 2024 15:32:20 +0200 Subject: [PATCH 14/18] Remove API's notify request and add warning/error software_update_event notifications. --- docs/grisp_connect_api.md | 33 ++++++++++++++++---------- src/grisp_connect_api.erl | 12 ---------- src/grisp_connect_updater_progress.erl | 18 +++++++++++--- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/docs/grisp_connect_api.md b/docs/grisp_connect_api.md index 9c0e103..3db5a7d 100644 --- a/docs/grisp_connect_api.md +++ b/docs/grisp_connect_api.md @@ -4,13 +4,16 @@ - [UI Websocket API](#ui-websocket-api) - [Backend API](#backend-api) - [Requests](#requests) + - [Notifications](#notifications) - [Error Codes](#error-codes) - [Default error codes](#default-error-codes) - [Custom error codes](#custom-error-codes) - - [Notifications](#notifications) We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend. +⚠️ **Note:** If you plan to use the API calls related to `grisp_updater`, make + sure to add `grisp_updater_grisp2` as a dependency in your project as well. + ## Backend API ### Requests @@ -36,6 +39,23 @@ We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend.

+### Notifications + +
update {"type":"software_update_event"} - notify the current progess of grisp_updater +

+ +**`params`:** +| key | value | type | description | +|---------------|---------------------------------------------|----------|--------------------------------------| +|`"type"` | `"software_update_event"` | required | | +|`"event_type"` | `"progress"` `"warning"` `"error"` `"done"` | required | | +|`"message"` | integer | optional | expected in case of warning | +|`"reason"` | integer | optional | expected in case of warning or error | +|`"percentage"` | integer | optional | expected in case of progress or error| + +

+
+ ## Error Codes ### Default error codes @@ -58,14 +78,3 @@ Additionally to the default jsonrpc error codes the following codes will be retu | -2 | `"token expired"` | token is expired | | -3 | `"device already linked"` | device needs to be unlinked first via UI | | -4 | `"invalid token"` | token is e.g. not orderly encoded | - -## Notifications - -
Notify - Status Update - -**`result`**: `JSON Object` - -| key | value | description | -| -------------- | ----------------- | --------------------------------- | -| `"type"` | `"status_update"` | Type of notification | -| `"percentage"` | integer | Progress percentage of the update | diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 8d56580..9d820eb 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -12,7 +12,6 @@ -define(method_post, <<"post">>). -define(method_patch, <<"patch">>). -define(method_delete, <<"delete">>). --define(method_notify, <<"notify">>). %--- API ----------------------------------------------------------------------- @@ -87,17 +86,6 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> grisp_connect_jsonrpc:format_error( {internal_error, invalid_params, ID})} end; -handle_request(?method_notify, #{type := <<"status_update">>} = Params, ID) -> - Percentage = maps:get(percentage, Params, undefined), - Reply = case Percentage of - Per when is_integer(Per) -> - {result, #{percentage => Percentage}, ID}; - undefined -> - Reason = update_percentage_not_retrieved, - ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), - grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}) - end, - {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(_, _, ID) -> Error = {internal_error, method_not_found, ID}, FormattedError = grisp_connect_jsonrpc:format_error(Error), diff --git a/src/grisp_connect_updater_progress.erl b/src/grisp_connect_updater_progress.erl index 2bb5db1..ca7b564 100644 --- a/src/grisp_connect_updater_progress.erl +++ b/src/grisp_connect_updater_progress.erl @@ -50,18 +50,30 @@ progress_update(#state{last_notification = LastLog} = State, Stats) -> grisp_connect_client:notify( <<"update">>, <<"software_update_event">>, - #{event => progress, + #{event => progress, percentage => UpdatePercentage}), {ok, State#state{last_notification = erlang:system_time(millisecond)}} end. progress_warning(State, Msg, Reason) -> ?LOG_WARNING("Update warning; ~s: ~p", [Msg, Reason]), + grisp_connect_client:notify( + <<"update">>, + <<"software_update_event">>, + #{event => warning, + reason => Reason, + message => Msg}), {ok, State}. progress_error(#state{}, Stats, Reason) -> - ?LOG_ERROR("Update failed after ~b% : ~p", - [progress_percent(Stats), Reason]), + UpdatePercentage = progress_percent(Stats), + ?LOG_ERROR("Update failed after ~b% : ~p", [UpdatePercentage, Reason]), + grisp_connect_client:notify( + <<"update">>, + <<"software_update_event">>, + #{event => error, + reason => Reason, + percentage => UpdatePercentage}), ok. progress_done(#state{}, _Stats) -> From 41ed68481b8dd72e14a22b9f9b77697c593a2884 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Mon, 7 Oct 2024 16:24:59 +0000 Subject: [PATCH 15/18] Add validate method (#48) * Handle boot_system_not_validated * Add validate method --- docs/grisp_connect_api.md | 31 +++++++++++++++++++++++++++++-- src/grisp_connect_api.erl | 13 +++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/grisp_connect_api.md b/docs/grisp_connect_api.md index 3db5a7d..05c84fa 100644 --- a/docs/grisp_connect_api.md +++ b/docs/grisp_connect_api.md @@ -21,6 +21,8 @@ We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend.
Post - Start an update

+Triggers grisp_updater to install an update from the given URL. + **`params`:** | key (required *) | value | description | | ----------------- | -------- | -------------------------- | @@ -33,8 +35,33 @@ We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend. | Error Content | When it Happens | | ----------------------------------------------------| -------------------------------- | -| `{code: -10, message: "grisp_updater unavailable"}` | Grisp updater app is not running | -| `{code: -11, message: "already updating "}` | An update is already happening | +| `{code: -10, message: "grisp_updater_unavailable"}` | Grisp updater app is not running | +| `{code: -11, message: "already_updating"}` | An update is already happening | +| `{code: -12, message: "boot_system_not_validated"}` | The board rebooted after an update and needs validation | + +

+
+ +
Post - Validate an update +

+ +Validates the current booted partition. This can only be done after an update was installed and a reboot occurred. +This request sets the current partition as permanent in the bootloader if it is not. +If the new partition is not validated, from the next reboot, the bootloader will load the previous one. +This should only be called if the new software is functioning as expected. + +**`params`:** +| key (required *) | value | description | +| ----------------- | -------- | -------------------------- | +| `"type"` * | string | `"validate"` | + +**`result`**: `"ok"` + +**`error`**: + +| Error Content | When it Happens | +| ----------------------------------------------------| -------------------------------- | +| `{code: -13, message: "validate_from_unbooted", data: 0}` | The current partition N cannot be validated |

diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 9d820eb..9b9520c 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -73,6 +73,8 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> {error, -10, grisp_updater_unavailable, undefined, ID}; {error, already_updating} -> {error, -11, already_updating, undefined, ID}; + {error, boot_system_not_validated} -> + {error, -12, boot_system_not_validated, undefined, ID}; {error, Reason} -> ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}); @@ -86,6 +88,17 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> grisp_connect_jsonrpc:format_error( {internal_error, invalid_params, ID})} end; +handle_request(?method_post, #{type := <<"validate">>}, ID) -> + Reply = case grisp_updater:validate() of + {error, {validate_from_unbooted, PartitionIndex}} -> + {error, -13, validate_from_unbooted, PartitionIndex, ID}; + {error, Reason} -> + ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), + grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}); + ok -> + {result, ok, ID} + end, + {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(_, _, ID) -> Error = {internal_error, method_not_found, ID}, FormattedError = grisp_connect_jsonrpc:format_error(Error), From 432af67cc29b5f94265dd97df1ff4adbfb7bcc59 Mon Sep 17 00:00:00 2001 From: IciaC Date: Tue, 8 Oct 2024 14:49:25 +0200 Subject: [PATCH 16/18] Fix callback module errors --- docs/grisp_connect_api.md | 4 ++-- src/grisp_connect_updater_progress.erl | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/grisp_connect_api.md b/docs/grisp_connect_api.md index 05c84fa..695f4c2 100644 --- a/docs/grisp_connect_api.md +++ b/docs/grisp_connect_api.md @@ -36,7 +36,7 @@ Triggers grisp_updater to install an update from the given URL. | Error Content | When it Happens | | ----------------------------------------------------| -------------------------------- | | `{code: -10, message: "grisp_updater_unavailable"}` | Grisp updater app is not running | -| `{code: -11, message: "already_updating"}` | An update is already happening | +| `{code: -11, message: "already_updating"}` | An update is already happening | | `{code: -12, message: "boot_system_not_validated"}` | The board rebooted after an update and needs validation |

@@ -76,7 +76,7 @@ This should only be called if the new software is functioning as expected. |---------------|---------------------------------------------|----------|--------------------------------------| |`"type"` | `"software_update_event"` | required | | |`"event_type"` | `"progress"` `"warning"` `"error"` `"done"` | required | | -|`"message"` | integer | optional | expected in case of warning | +|`"message"` | integer | optional | expected in case of warning or error | |`"reason"` | integer | optional | expected in case of warning or error | |`"percentage"` | integer | optional | expected in case of progress or error| diff --git a/src/grisp_connect_updater_progress.erl b/src/grisp_connect_updater_progress.erl index ca7b564..6c68274 100644 --- a/src/grisp_connect_updater_progress.erl +++ b/src/grisp_connect_updater_progress.erl @@ -16,7 +16,7 @@ -export([progress_init/1]). -export([progress_update/2]). -export([progress_warning/3]). --export([progress_error/3]). +-export([progress_error/4]). -export([progress_done/2]). @@ -55,7 +55,7 @@ progress_update(#state{last_notification = LastLog} = State, Stats) -> {ok, State#state{last_notification = erlang:system_time(millisecond)}} end. -progress_warning(State, Msg, Reason) -> +progress_warning(State, Reason, Msg) -> ?LOG_WARNING("Update warning; ~s: ~p", [Msg, Reason]), grisp_connect_client:notify( <<"update">>, @@ -65,7 +65,7 @@ progress_warning(State, Msg, Reason) -> message => Msg}), {ok, State}. -progress_error(#state{}, Stats, Reason) -> +progress_error(#state{}, Stats, Reason, Msg) -> UpdatePercentage = progress_percent(Stats), ?LOG_ERROR("Update failed after ~b% : ~p", [UpdatePercentage, Reason]), grisp_connect_client:notify( @@ -73,6 +73,7 @@ progress_error(#state{}, Stats, Reason) -> <<"software_update_event">>, #{event => error, reason => Reason, + message => Msg, percentage => UpdatePercentage}), ok. From f1edf28e43f15d5668cf20c2f1c2c1d5a5962641 Mon Sep 17 00:00:00 2001 From: IciaC Date: Wed, 9 Oct 2024 17:46:38 +0200 Subject: [PATCH 17/18] get partition_state endpoint --- docs/grisp_connect_api.md | 38 ++++++++++++++++++++ src/grisp_connect_api.erl | 75 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/docs/grisp_connect_api.md b/docs/grisp_connect_api.md index 695f4c2..2ab9506 100644 --- a/docs/grisp_connect_api.md +++ b/docs/grisp_connect_api.md @@ -18,6 +18,44 @@ We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend. ### Requests +

+
+
Get - partition_state +

+ +Retrieves the current state of the system’s partition, indicating whether the +system requires a reboot, needs validation, or is running an old partition +with no updates pending. This can be used to check if the system is running a + valid system, or has an update pending. + +**`params`:** +| key (required *) | value | description | +| ----------------- | -------- | ------------------- | +| `"type"` * | string | `"partition_state"` | + +**`result`**: JSON Object + +| key | value | type | description | +|-----------------|-----------|----------|----------------------------------------------------| +| state | string | required | `"old"`, `"old_no_update"`, `"new"`, `"unknown"` | +| message | string | required | Message describing the current state of the system | +| action_required | boolean | required | Indicates whether any action is required (e.g., reboot, validation). | + +Meaning of the state: + +| key | description | +|-------------------|--------------------------------------------------------------------------------------------| +| `"new"` | The system has booted into a new partition. Validation is required to finalize the update. | +| `"old"` | Current partition is old. A reboot is required to load the new partition. | +| `"old_no_update"` | There is no update pending. The system is running the old partition. | +| `"unknown"` | The current partition state does not match any of the previous described states. | + +**`error`**: + +| Error Content | When it Happens | +| ----------------------------------------------------| -------------------------------------- | +| `{code: -10, message: "grisp_updater_unavailable"}` | Grisp updater app is not running | +

Post - Start an update

diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 9b9520c..7442aa4 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -53,7 +53,8 @@ handle_jsonrpc({single, Rpc}) -> handle_rpc_messages([], Replies) -> lists:reverse(Replies); handle_rpc_messages([{request, M, Params, ID} | Batch], Replies) - when M == ?method_post -> + when M == ?method_post; + M == ?method_get -> handle_rpc_messages(Batch, [handle_request(M, Params, ID) | Replies]); handle_rpc_messages([{result, _, _} = Res| Batch], Replies) -> handle_rpc_messages(Batch, [handle_response(Res)| Replies]); @@ -65,6 +66,18 @@ handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> handle_rpc_messages(Batch, [grisp_connect_jsonrpc:format_error(E)| Replies]). +handle_request(?method_get, #{type := <<"partition_state">>} = _Params, ID) -> + Info = get_partition_info(), + Reply = case Info of + #{state := _State, + message := _Msg, + action_required := _ActionRequired} = Response -> + {result, Response, ID}; + {error, Reason} -> + ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), + grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}) + end, + {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> try URL = maps:get(url, Params), @@ -121,6 +134,64 @@ start_update(URL) -> false -> {error, grisp_updater_unavailable} end. +get_partition_info() -> + case is_running(grisp_updater) of + true -> + Info = grisp_updater:info(), + #{boot := Boot, valid := Valid, next := Next} = Info, + ActionRequired = maps:get(action_required, Info, false), + case evaluate_partition_state(Boot, Valid, Next) of + new_boot -> + #{state => <<"new">>, + message => <<"New partition booted, validation required">>, + action_required => ActionRequired}; + update_pending -> + #{state => <<"old">>, + message => <<"Reboot required to load new partition">>, + action_required => ActionRequired}; + no_update_pending -> + #{state => <<"old_no_update">>, + message => <<"No update pending, running old partition">>, + action_required => ActionRequired}; + _ -> + #{state => <<"unknown">>, + message => <<"Unknown partition state">>, + action_required => ActionRequired} + end; + false -> {error, grisp_updater_unavailable} + end. + +evaluate_partition_state(BootPartition, ValidPartition, NextPartition) -> + case {BootPartition, ValidPartition, NextPartition} of + % Case 1: Booting from removable media, but system has a pending update + {#{type := removable}, + #{type := system, id := ValidId}, + #{type := system, id := NextId}} + when ValidId =/= NextId -> update_pending; + % Case 2: Booted from system partition, but a different system partition is pending update + {#{type := system, id := BootId}, + #{type := system, id := ValidId}, + #{type := system, id := NextId}} + when BootId == ValidId, ValidId =/= NextId -> update_pending; + % Case 3: Booted from a new partition, validation required + {#{type := system, id := BootId}, + #{type := system, id := ValidId}, + _} + when BootId =/= ValidId -> new_boot; + % Case 4: Booted from removable media, no update pending + {#{type := removable}, + #{type := system, id := ValidId}, + #{type := system, id := NextId}} + when ValidId == NextId -> no_update_pending; + % Case 5: Booted from system partition, no update pending + {#{type := system, id := BootId}, + _, + #{type := system, id := NextId}} + when NextId == BootId -> no_update_pending; + % Default case: Unknown partition state + _ -> unknown_state + end. + is_running(AppName) -> Apps = application:which_applications(), case [App || {App, _Desc, _VSN} <- Apps, App =:= AppName] of @@ -134,6 +205,8 @@ error_atom(-3) -> device_already_linked; error_atom(-4) -> invalid_token; error_atom(-10) -> grisp_updater_unavailable; error_atom(-11) -> already_updating; +error_atom(-12) -> boot_system_not_validated; +error_atom(-13) -> validate_from_unbooted; error_atom(_) -> jsonrpc_error. id() -> From 279942a5505e25491086f4c81c22b06f54c1998c Mon Sep 17 00:00:00 2001 From: IciaC Date: Wed, 9 Oct 2024 19:04:19 +0200 Subject: [PATCH 18/18] Delete error_atom not coming from grisp_manager --- src/grisp_connect_api.erl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 7442aa4..ecb5f4c 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -203,10 +203,6 @@ error_atom(-1) -> device_not_linked; error_atom(-2) -> token_expired; error_atom(-3) -> device_already_linked; error_atom(-4) -> invalid_token; -error_atom(-10) -> grisp_updater_unavailable; -error_atom(-11) -> already_updating; -error_atom(-12) -> boot_system_not_validated; -error_atom(-13) -> validate_from_unbooted; error_atom(_) -> jsonrpc_error. id() ->