|
| 1 | +%% This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +%% License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | +%% |
| 5 | +%% Copyright (c) 2025 VAMPIRE BYTE SRL. All Rights Reserved. |
| 6 | +%% |
| 7 | +-module(mc_ocpp). |
| 8 | +-behaviour(mc). |
| 9 | + |
| 10 | +-export([ |
| 11 | + init/1, |
| 12 | + size/1, |
| 13 | + x_header/2, |
| 14 | + property/2, |
| 15 | + routing_headers/2, |
| 16 | + convert_from/3, |
| 17 | + convert_to/3, |
| 18 | + protocol_state/2, |
| 19 | + prepare/2 |
| 20 | +]). |
| 21 | + |
| 22 | +-include_lib("kernel/include/logger.hrl"). |
| 23 | +-include_lib("rabbit_common/include/rabbit_framing.hrl"). % #content{} |
| 24 | +-include_lib("amqp10_common/include/amqp10_framing.hrl"). |
| 25 | +-include_lib("rabbit_common/include/rabbit.hrl"). % #'P_basic'{} |
| 26 | +-include_lib("rabbit/include/mc.hrl"). |
| 27 | +-include("rabbit_web_ocpp.hrl"). |
| 28 | + |
| 29 | +-define(CONTENT_TYPE_JSON, <<"application/json">>). |
| 30 | + |
| 31 | +%%-------------------------------------------------------------------- |
| 32 | +-spec init(#ocpp_msg{}) -> {#ocpp_msg{}, map()} | error. |
| 33 | +init(Msg = #ocpp_msg{}) -> |
| 34 | + Anns = #{ |
| 35 | + ?ANN_ROUTING_KEYS => [Msg#ocpp_msg.client_id], |
| 36 | + ?ANN_DURABLE => false, |
| 37 | + correlation_id => Msg#ocpp_msg.msg_id, |
| 38 | + reply_to => Msg#ocpp_msg.client_id, |
| 39 | + content_type => ?CONTENT_TYPE_JSON |
| 40 | + }, |
| 41 | + {Msg, Anns}; |
| 42 | +init(Other) -> |
| 43 | + ?LOG_ERROR("mc_ocpp:init badarg: ~p", [Other]), |
| 44 | + error(badarg). |
| 45 | + |
| 46 | +%%-------------------------------------------------------------------- |
| 47 | +-spec size(#ocpp_msg{}) -> {non_neg_integer(), non_neg_integer()}. |
| 48 | +size(#ocpp_msg{payload = P}) when is_binary(P) -> |
| 49 | + {0, byte_size(P)}; |
| 50 | +size(#ocpp_msg{payload = Iol}) -> |
| 51 | + {0, iolist_size(Iol)}. |
| 52 | + |
| 53 | +%%-------------------------------------------------------------------- |
| 54 | +x_header(_Key, #ocpp_msg{}) -> |
| 55 | + undefined. |
| 56 | + |
| 57 | +%%-------------------------------------------------------------------- |
| 58 | +property(correlation_id, #ocpp_msg{msg_id = ID}) when is_binary(ID) -> |
| 59 | + {binary, ID}; |
| 60 | +property(reply_to, #ocpp_msg{client_id = C}) when is_binary(C) -> |
| 61 | + {binary, C}; |
| 62 | +property(_, _) -> |
| 63 | + undefined. |
| 64 | + |
| 65 | +%%-------------------------------------------------------------------- |
| 66 | +routing_headers(#ocpp_msg{action = A}, _) when is_binary(A) -> |
| 67 | + #{<<"ocpp_action">> => A}; |
| 68 | +routing_headers(_, _) -> |
| 69 | + #{}. |
| 70 | + |
| 71 | +%%-------------------------------------------------------------------- |
| 72 | +-spec convert_from(atom(), term(), map()) -> #ocpp_msg{} | not_implemented. |
| 73 | + |
| 74 | +%% AMQP 1.0 |
| 75 | +convert_from(mc_amqp, Sections, _Env) -> |
| 76 | + {Payload, Corr, Act, Rto} = extract_amqp1(Sections), |
| 77 | + build_ocpp(Payload, Corr, Act, Rto); |
| 78 | + |
| 79 | +%% AMQP 0-9-1 |
| 80 | +convert_from(mc_amqpl, #content{payload_fragments_rev = Rev, properties = BP}, _Env) -> |
| 81 | + Payload = iolist_to_binary(lists:reverse(Rev)), |
| 82 | + Corr = case BP#'P_basic'.correlation_id of undefined -> undefined; CorrVal -> CorrVal end, |
| 83 | + Act = case BP#'P_basic'.type of |
| 84 | + undefined -> undefined; |
| 85 | + ActVal -> ActVal |
| 86 | + end, |
| 87 | + Rto = case BP#'P_basic'.reply_to of undefined -> undefined; RtoVal -> RtoVal end, |
| 88 | + build_ocpp(Payload, Corr, Act, Rto); |
| 89 | + |
| 90 | +%% Identity / No conversion |
| 91 | +convert_from(?MODULE, Msg, _) -> |
| 92 | + Msg; |
| 93 | +convert_from(_, _, _) -> |
| 94 | + not_implemented. |
| 95 | + |
| 96 | +%%-------------------------------------------------------------------- |
| 97 | +-spec convert_to(atom(), #ocpp_msg{}, map()) -> term() | not_implemented. |
| 98 | + |
| 99 | +%% AMQP 1.0 |
| 100 | +convert_to(mc_amqp, #ocpp_msg{payload=P, msg_id=ID, action=A, client_id=CID} = _Msg, Env) -> |
| 101 | + Header = #'v1_0.header'{durable = false}, |
| 102 | + Props = #'v1_0.properties'{ |
| 103 | + correlation_id = ID, |
| 104 | + subject = A, |
| 105 | + content_type = {symbol, ?CONTENT_TYPE_JSON}, |
| 106 | + reply_to = CID |
| 107 | + }, |
| 108 | + Data = #'v1_0.data'{content = P}, |
| 109 | + Sections = [Header, Props, Data], |
| 110 | + %% Use convert_from/3 here to turn a section list into |
| 111 | + %% a canonical AMQP message that the server knows how to frame. |
| 112 | + mc_amqp:convert_from(mc_amqp, Sections, Env); |
| 113 | + |
| 114 | +%% AMQP 0-9-1 |
| 115 | +convert_to(mc_amqpl, |
| 116 | + #ocpp_msg{payload = Payload, |
| 117 | + msg_id = MsgId, |
| 118 | + action = Action, |
| 119 | + client_id= ClientId}, |
| 120 | + _Env) -> |
| 121 | + %% 1) Build the basic properties record |
| 122 | + BP = #'P_basic'{ |
| 123 | + delivery_mode = 1, % non-persistent |
| 124 | + correlation_id = MsgId, % your OCPP msg_id |
| 125 | + type = Action, % maps to 'type' header |
| 126 | + content_type = ?CONTENT_TYPE_JSON, |
| 127 | + reply_to = ClientId, % OCPP client_id |
| 128 | + headers = undefined % no extra field-table entries |
| 129 | + }, |
| 130 | + |
| 131 | + %% 2) Wrap the JSON payload as a single binary |
| 132 | + PFR = [ iolist_to_binary(Payload) ], |
| 133 | + |
| 134 | + %% 3) Use the Basic class ID (60) for content frames |
| 135 | + #content{ |
| 136 | + class_id = 60, |
| 137 | + properties = BP, |
| 138 | + properties_bin = none, |
| 139 | + payload_fragments_rev = PFR |
| 140 | + }; |
| 141 | + |
| 142 | +%% Identity / No conversion |
| 143 | +convert_to(?MODULE, Msg, _) -> |
| 144 | + Msg; |
| 145 | +convert_to(_, _, _) -> |
| 146 | + not_implemented. |
| 147 | + |
| 148 | +%%-------------------------------------------------------------------- |
| 149 | +%% Helpers |
| 150 | +%%-------------------------------------------------------------------- |
| 151 | + |
| 152 | +%% @doc No‐op before sending—ensure payload is a binary |
| 153 | +-spec prepare(Atom :: atom(), Msg :: #ocpp_msg{}) -> #ocpp_msg{}. |
| 154 | +prepare(_For, Msg = #ocpp_msg{payload = P}) when is_binary(P) -> |
| 155 | + Msg; |
| 156 | +prepare(_For, Msg = #ocpp_msg{payload = Iolist}) -> |
| 157 | + %% convert any stray iolists to a flat binary |
| 158 | + Msg#ocpp_msg{payload = iolist_to_binary(Iolist)}. |
| 159 | + |
| 160 | +%% @doc No protocol‐specific state changes needed |
| 161 | +-spec protocol_state(Msg :: #ocpp_msg{}, Anns :: map()) -> #ocpp_msg{}. |
| 162 | +protocol_state(Msg, _Anns) -> |
| 163 | + Msg. |
| 164 | + |
| 165 | +-spec extract_amqp1([term()]) -> {binary(), binary()|undefined, binary()|undefined, binary()|undefined}. |
| 166 | +extract_amqp1(Sections) -> |
| 167 | + {Rev, Corr, Act, Rto} = |
| 168 | + lists:foldl(fun |
| 169 | + (#'v1_0.data'{content=C}, {R,C0,A0,R0}) -> {[C|R], C0, A0, R0}; |
| 170 | + (#'v1_0.properties'{correlation_id={binary,ID}}, {R,_,A0,R0}) -> {R, ID, A0, R0}; |
| 171 | + (#'v1_0.properties'{subject={utf8,S}}, {R,C0,_,R0}) -> {R, C0, S, R0}; |
| 172 | + (#'v1_0.properties'{reply_to={binary,RT}}, {R,C0,A0,_}) -> {R, C0, A0, RT}; |
| 173 | + (_, Acc) -> Acc |
| 174 | + end, {[], undefined, undefined, undefined}, Sections), |
| 175 | + { iolist_to_binary(lists:reverse(Rev)) |
| 176 | + , Corr, Act, Rto |
| 177 | + }. |
| 178 | + |
| 179 | +-spec build_ocpp(binary(), binary()|undefined, binary()|undefined, binary()|undefined) -> #ocpp_msg{}. |
| 180 | +build_ocpp(Payload, Corr, Act, Rto) -> |
| 181 | + #ocpp_msg{ |
| 182 | + payload = Payload, |
| 183 | + msg_type = undefined, |
| 184 | + msg_id = Corr, |
| 185 | + action = Act, |
| 186 | + client_id = Rto |
| 187 | + }. |
0 commit comments