Merge pull request #12892 from HJianBo/fix-gateway-related-issues

fix(ocpp): avoid an error log in handling downstream messages
This commit is contained in:
JianBo He 2024-04-22 11:25:26 +08:00 committed by GitHub
commit d85df14b85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 279 additions and 45 deletions

View File

@ -1,7 +1,7 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
{application, emqx_gateway, [ {application, emqx_gateway, [
{description, "The Gateway management application"}, {description, "The Gateway management application"},
{vsn, "0.1.31"}, {vsn, "0.1.32"},
{registered, []}, {registered, []},
{mod, {emqx_gateway_app, []}}, {mod, {emqx_gateway_app, []}},
{applications, [kernel, stdlib, emqx, emqx_auth, emqx_ctl]}, {applications, [kernel, stdlib, emqx, emqx_auth, emqx_ctl]},

View File

@ -247,9 +247,10 @@ page_params(Qs) ->
get_cluster_listeners_info(GwName) -> get_cluster_listeners_info(GwName) ->
Listeners = emqx_gateway_conf:listeners(GwName), Listeners = emqx_gateway_conf:listeners(GwName),
ListenOns = lists:map( ListenOns = lists:map(
fun(#{id := Id} = Conf) -> fun(#{id := Id, type := Type0} = Conf) ->
Type = binary_to_existing_atom(Type0),
ListenOn = emqx_gateway_conf:get_bind(Conf), ListenOn = emqx_gateway_conf:get_bind(Conf),
{Id, ListenOn} {Type, Id, ListenOn}
end, end,
Listeners Listeners
), ),
@ -293,17 +294,11 @@ listeners_cluster_status(Listeners) ->
do_listeners_cluster_status(Listeners) -> do_listeners_cluster_status(Listeners) ->
Node = node(), Node = node(),
lists:foldl( lists:foldl(
fun({Id, ListenOn}, Acc) -> fun({Type, Id, ListenOn}, Acc) ->
BinId = erlang:atom_to_binary(Id), {Running, Curr} = current_listener_status(Type, Id, ListenOn),
{ok, #{<<"max_connections">> := Max}} = emqx_gateway_conf:listener(BinId), {ok, #{<<"max_connections">> := Max}} = emqx_gateway_conf:listener(
{Running, Curr} = erlang:atom_to_binary(Id)
try esockd:get_current_connections({Id, ListenOn}) of ),
Int -> {true, Int}
catch
%% not started
error:not_found ->
{false, 0}
end,
Acc#{ Acc#{
Id => #{ Id => #{
node => Node, node => Node,
@ -319,6 +314,24 @@ do_listeners_cluster_status(Listeners) ->
Listeners Listeners
). ).
current_listener_status(Type, Id, _ListenOn) when Type =:= ws; Type =:= wss ->
Info = ranch:info(Id),
Conns = proplists:get_value(all_connections, Info, 0),
Running =
case proplists:get_value(status, Info) of
running -> true;
_ -> false
end,
{Running, Conns};
current_listener_status(_Type, Id, ListenOn) ->
try esockd:get_current_connections({Id, ListenOn}) of
Int -> {true, Int}
catch
%% not started
error:not_found ->
{false, 0}
end.
ensure_integer_or_infinity(infinity) -> ensure_integer_or_infinity(infinity) ->
infinity; infinity;
ensure_integer_or_infinity(<<"infinity">>) -> ensure_integer_or_infinity(<<"infinity">>) ->
@ -762,9 +775,9 @@ examples_listener() ->
<<"tlsv1.1">>, <<"tlsv1.1">>,
<<"tlsv1">> <<"tlsv1">>
], ],
cacertfile => <<"/etc/emqx/certs/cacert.pem">>, cacertfile => <<"${EMQX_ETC_DIR}/certs/cacert.pem">>,
certfile => <<"/etc/emqx/certs/cert.pem">>, certfile => <<"${EMQX_ETC_DIR}/certs/cert.pem">>,
keyfile => <<"/etc/emqx/certs/key.pem">>, keyfile => <<"${EMQX_ETC_DIR}/certs/key.pem">>,
verify => <<"verify_none">>, verify => <<"verify_none">>,
fail_if_no_peer_cert => false fail_if_no_peer_cert => false
}, },
@ -808,9 +821,9 @@ examples_listener() ->
dtls_options => dtls_options =>
#{ #{
versions => [<<"dtlsv1.2">>, <<"dtlsv1">>], versions => [<<"dtlsv1.2">>, <<"dtlsv1">>],
cacertfile => <<"/etc/emqx/certs/cacert.pem">>, cacertfile => <<"${EMQX_ETC_DIR}/certs/cacert.pem">>,
certfile => <<"/etc/emqx/certs/cert.pem">>, certfile => <<"${EMQX_ETC_DIR}/certs/cert.pem">>,
keyfile => <<"/etc/emqx/certs/key.pem">>, keyfile => <<"${EMQX_ETC_DIR}/certs/key.pem">>,
verify => <<"verify_none">>, verify => <<"verify_none">>,
fail_if_no_peer_cert => false fail_if_no_peer_cert => false
}, },
@ -835,9 +848,9 @@ examples_listener() ->
dtls_options => dtls_options =>
#{ #{
versions => [<<"dtlsv1.2">>, <<"dtlsv1">>], versions => [<<"dtlsv1.2">>, <<"dtlsv1">>],
cacertfile => <<"/etc/emqx/certs/cacert.pem">>, cacertfile => <<"${EMQX_ETC_DIR}/certs/cacert.pem">>,
certfile => <<"/etc/emqx/certs/cert.pem">>, certfile => <<"${EMQX_ETC_DIR}/certs/cert.pem">>,
keyfile => <<"/etc/emqx/certs/key.pem">>, keyfile => <<"${EMQX_ETC_DIR}/certs/key.pem">>,
verify => <<"verify_none">>, verify => <<"verify_none">>,
user_lookup_fun => <<"emqx_tls_psk:lookup">>, user_lookup_fun => <<"emqx_tls_psk:lookup">>,
ciphers => ciphers =>
@ -869,5 +882,95 @@ examples_listener() ->
user_id_type => <<"username">> user_id_type => <<"username">>
} }
} }
},
ws_listener =>
#{
summary => <<"A simple WebSocket listener example">>,
value =>
#{
name => <<"ws-def">>,
type => <<"ws">>,
bind => <<"33043">>,
acceptors => 16,
max_connections => 1024000,
max_conn_rate => 1000,
websocket =>
#{
path => <<"/ocpp">>,
fail_if_no_subprotocol => true,
supported_subprotocols => <<"ocpp1.6">>,
check_origin_enable => false,
check_origins =>
<<"http://localhost:18083, http://127.0.0.1:18083">>,
compress => false,
piggyback => <<"single">>
},
tcp_options =>
#{
active_n => 100,
backlog => 1024,
send_timeout => <<"15s">>,
send_timeout_close => true,
recbuf => <<"10KB">>,
sndbuf => <<"10KB">>,
buffer => <<"10KB">>,
high_watermark => <<"1MB">>,
nodelay => false,
reuseaddr => true,
keepalive => "none"
}
}
},
wss_listener =>
#{
summary => <<"A simple WebSocket/TLS listener example">>,
value =>
#{
name => <<"ws-ssl-def">>,
type => <<"wss">>,
bind => <<"33053">>,
acceptors => 16,
max_connections => 1024000,
max_conn_rate => 1000,
websocket =>
#{
path => <<"/ocpp">>,
fail_if_no_subprotocol => true,
supported_subprotocols => <<"ocpp1.6">>,
check_origin_enable => false,
check_origins =>
<<"http://localhost:18083, http://127.0.0.1:18083">>,
compress => false,
piggyback => <<"single">>
},
ssl_options =>
#{
versions => [
<<"tlsv1.3">>,
<<"tlsv1.2">>,
<<"tlsv1.1">>,
<<"tlsv1">>
],
cacertfile => <<"${EMQX_ETC_DIR}/certs/cacert.pem">>,
certfile => <<"${EMQX_ETC_DIR}/certs/cert.pem">>,
keyfile => <<"${EMQX_ETC_DIR}/certs/key.pem">>,
verify => <<"verify_none">>,
fail_if_no_peer_cert => false
},
tcp_options =>
#{
active_n => 100,
backlog => 1024,
send_timeout => <<"15s">>,
send_timeout_close => true,
recbuf => <<"10KB">>,
sndbuf => <<"10KB">>,
buffer => <<"10KB">>,
high_watermark => <<"1MB">>,
nodelay => false,
reuseaddr => true,
keepalive => "none"
}
}
} }
}. }.

View File

@ -86,10 +86,9 @@
-define(IS_ERROR(F), F = #{type := ?OCPP_MSG_TYPE_ID_CALLERROR}). -define(IS_ERROR(F), F = #{type := ?OCPP_MSG_TYPE_ID_CALLERROR}).
-define(IS_ERROR(F, Id), F = #{type := ?OCPP_MSG_TYPE_ID_CALLERROR, id := Id}). -define(IS_ERROR(F, Id), F = #{type := ?OCPP_MSG_TYPE_ID_CALLERROR, id := Id}).
-define(IS_BootNotification_RESP(Payload), #{ -define(IS_BootNotification_RESP(Status, Interval), #{
type := ?OCPP_MSG_TYPE_ID_CALLRESULT, type := ?OCPP_MSG_TYPE_ID_CALLRESULT,
action := ?OCPP_ACT_BootNotification, payload := #{<<"status">> := Status, <<"interval">> := Interval}
payload := Payload
}). }).
-define(ERR_FRAME(Id, Code, Desc), #{ -define(ERR_FRAME(Id, Code, Desc), #{

View File

@ -1,6 +1,6 @@
{application, emqx_gateway_ocpp, [ {application, emqx_gateway_ocpp, [
{description, "OCPP-J 1.6 Gateway for EMQX"}, {description, "OCPP-J 1.6 Gateway for EMQX"},
{vsn, "0.1.3"}, {vsn, "0.1.4"},
{registered, []}, {registered, []},
{applications, [kernel, stdlib, jesse, emqx, emqx_gateway]}, {applications, [kernel, stdlib, jesse, emqx, emqx_gateway]},
{env, []}, {env, []},

View File

@ -527,20 +527,19 @@ apply_frame(Frames, Channel) when is_list(Frames) ->
{Outgoings, NChannel} = lists:foldl(fun do_apply_frame/2, {[], Channel}, Frames), {Outgoings, NChannel} = lists:foldl(fun do_apply_frame/2, {[], Channel}, Frames),
{lists:reverse(Outgoings), NChannel}; {lists:reverse(Outgoings), NChannel};
apply_frame(Frames, Channel) -> apply_frame(Frames, Channel) ->
?SLOG(error, #{msg => "unexpected_frame_list", frames => Frames, channel => Channel}), ?SLOG(error, #{msg => "unexpected_frame_list", frames => Frames}),
Channel. Channel.
do_apply_frame(?IS_BootNotification_RESP(Payload), {Outgoings, Channel}) -> do_apply_frame(?IS_BootNotification_RESP(Status, Interval), {Outgoings, Channel}) ->
case maps:get(<<"status">>, Payload) of case Status of
<<"Accepted">> -> <<"Accepted">> ->
Intv = maps:get(<<"interval">>, Payload), ?SLOG(info, #{msg => "adjust_heartbeat_timer", new_interval_s => Interval}),
?SLOG(info, #{msg => "adjust_heartbeat_timer", new_interval_s => Intv}), {[{event, updated} | Outgoings], reset_keepalive(Interval, Channel)};
{[{event, updated} | Outgoings], reset_keepalive(Intv, Channel)};
_ -> _ ->
{Outgoings, Channel} {Outgoings, Channel}
end; end;
do_apply_frame(Frame, Acc = {_Outgoings, Channel}) -> do_apply_frame(Frame, Acc = {_Outgoings, _Channel}) ->
?SLOG(error, #{msg => "unexpected_frame", frame => Frame, channel => Channel}), ?SLOG(info, #{msg => "skip_to_apply_frame", frame => Frame}),
Acc. Acc.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -762,19 +761,15 @@ payload2frame(#{
action => Action, action => Action,
payload => Payload payload => Payload
}; };
payload2frame( payload2frame(#{
MqttPayload =
#{
<<"MessageTypeId">> := ?OCPP_MSG_TYPE_ID_CALLRESULT, <<"MessageTypeId">> := ?OCPP_MSG_TYPE_ID_CALLRESULT,
<<"UniqueId">> := Id, <<"UniqueId">> := Id,
<<"Payload">> := Payload <<"Payload">> := Payload
} }) ->
) ->
Action = maps:get(<<"Action">>, MqttPayload, undefined),
#{ #{
type => ?OCPP_MSG_TYPE_ID_CALLRESULT, type => ?OCPP_MSG_TYPE_ID_CALLRESULT,
id => Id, id => Id,
action => Action, action => undefined,
payload => Payload payload => Payload
}; };
payload2frame(#{ payload2frame(#{

View File

@ -16,6 +16,7 @@
-module(emqx_ocpp_SUITE). -module(emqx_ocpp_SUITE).
-include("emqx_ocpp.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
@ -145,3 +146,136 @@ t_enable_disable_gw_ocpp(_Config) ->
AssertEnabled(false), AssertEnabled(false),
?assertEqual({204, #{}}, request(put, "/gateways/ocpp/enable/true", <<>>)), ?assertEqual({204, #{}}, request(put, "/gateways/ocpp/enable/true", <<>>)),
AssertEnabled(true). AssertEnabled(true).
t_adjust_keepalive_timer(_Config) ->
{ok, ClientPid} = connect("127.0.0.1", 33033, <<"client1">>),
UniqueId = <<"3335862321">>,
BootNotification = #{
id => UniqueId,
type => ?OCPP_MSG_TYPE_ID_CALL,
action => <<"BootNotification">>,
payload => #{
<<"chargePointVendor">> => <<"vendor1">>,
<<"chargePointModel">> => <<"model1">>
}
},
ok = send_msg(ClientPid, BootNotification),
%% check the default keepalive timer
timer:sleep(1000),
?assertMatch(
#{conninfo := #{keepalive := 60}}, emqx_gateway_cm:get_chan_info(ocpp, <<"client1">>)
),
%% publish the BootNotification.ack
AckPayload = emqx_utils_json:encode(#{
<<"MessageTypeId">> => ?OCPP_MSG_TYPE_ID_CALLRESULT,
<<"UniqueId">> => UniqueId,
<<"Payload">> => #{
<<"currentTime">> => "2023-06-21T14:20:39+00:00",
<<"interval">> => 300,
<<"status">> => <<"Accepted">>
}
}),
_ = emqx:publish(emqx_message:make(<<"ocpp/cs/client1">>, AckPayload)),
{ok, _Resp} = receive_msg(ClientPid),
%% assert: check the keepalive timer is adjusted
?assertMatch(
#{conninfo := #{keepalive := 300}}, emqx_gateway_cm:get_chan_info(ocpp, <<"client1">>)
),
%% close conns
close(ClientPid),
timer:sleep(1000),
%% assert:
?assertEqual(undefined, emqx_gateway_cm:get_chan_info(ocpp, <<"client1">>)),
ok.
t_listeners_status(_Config) ->
{200, [Listener]} = request(get, "/gateways/ocpp/listeners"),
?assertMatch(
#{
status := #{running := true, current_connections := 0}
},
Listener
),
%% add a connection
{ok, ClientPid} = connect("127.0.0.1", 33033, <<"client1">>),
UniqueId = <<"3335862321">>,
BootNotification = #{
id => UniqueId,
type => ?OCPP_MSG_TYPE_ID_CALL,
action => <<"BootNotification">>,
payload => #{
<<"chargePointVendor">> => <<"vendor1">>,
<<"chargePointModel">> => <<"model1">>
}
},
ok = send_msg(ClientPid, BootNotification),
timer:sleep(1000),
%% assert: the current_connections is 1
{200, [Listener1]} = request(get, "/gateways/ocpp/listeners"),
?assertMatch(
#{
status := #{running := true, current_connections := 1}
},
Listener1
),
%% close conns
close(ClientPid),
timer:sleep(1000),
%% assert: the current_connections is 0
{200, [Listener2]} = request(get, "/gateways/ocpp/listeners"),
?assertMatch(
#{
status := #{running := true, current_connections := 0}
},
Listener2
).
%%--------------------------------------------------------------------
%% ocpp simple client
connect(Host, Port, ClientId) ->
Timeout = 5000,
ConnOpts = #{connect_timeout => 5000},
case gun:open(Host, Port, ConnOpts) of
{ok, ConnPid} ->
{ok, _} = gun:await_up(ConnPid, Timeout),
case upgrade(ConnPid, ClientId, Timeout) of
{ok, _Headers} -> {ok, ConnPid};
Error -> Error
end;
Error ->
Error
end.
upgrade(ConnPid, ClientId, Timeout) ->
Path = binary_to_list(<<"/ocpp/", ClientId/binary>>),
WsHeaders = [{<<"cache-control">>, <<"no-cache">>}],
StreamRef = gun:ws_upgrade(ConnPid, Path, WsHeaders, #{protocols => [{<<"ocpp1.6">>, gun_ws_h}]}),
receive
{gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], Headers} ->
{ok, Headers};
{gun_response, ConnPid, _, _, Status, Headers} ->
{error, {ws_upgrade_failed, Status, Headers}};
{gun_error, ConnPid, StreamRef, Reason} ->
{error, {ws_upgrade_failed, Reason}}
after Timeout ->
{error, timeout}
end.
send_msg(ConnPid, Frame) when is_map(Frame) ->
Opts = emqx_ocpp_frame:serialize_opts(),
Msg = emqx_ocpp_frame:serialize_pkt(Frame, Opts),
gun:ws_send(ConnPid, {text, Msg}).
receive_msg(ConnPid) ->
receive
{gun_ws, ConnPid, _Ref, {_Type, Msg}} ->
ParseState = emqx_ocpp_frame:initial_parse_state(#{}),
{ok, Frame, _Rest, _NewParseStaet} = emqx_ocpp_frame:parse(Msg, ParseState),
{ok, Frame}
after 5000 ->
{error, timeout}
end.
close(ConnPid) ->
gun:shutdown(ConnPid).

3
changes/ee/fix-12892.md Normal file
View File

@ -0,0 +1,3 @@
Fix an error in OCPP gateway's handling of downstream BootNotification.
Fix the `gateways/ocpp/listeners` endpoint to return the correct number of current connections.