%%-------------------------------------------------------------------- %% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- -module(emqx_lwm2m_message). -export([ tlv_to_json/2 , json_to_tlv/2 , text_to_json/2 , opaque_to_json/2 , translate_json/1 ]). -ifdef(TEST). -export([ bits/1 ]). -endif. -include("emqx_lwm2m.hrl"). -define(LOG(Level, Format, Args), logger:Level("LWM2M-JSON: " ++ Format, Args)). tlv_to_json(BaseName, TlvData) -> DecodedTlv = emqx_lwm2m_tlv:parse(TlvData), ObjectId = object_id(BaseName), ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true), case DecodedTlv of [#{tlv_resource_with_value:=Id, value:=Value}] -> TrueBaseName = basename(BaseName, undefined, undefined, Id, 3), tlv_single_resource(TrueBaseName, Id, Value, ObjDefinition); List1 = [#{tlv_resource_with_value:=_Id}, _|_] -> TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2), tlv_level2(TrueBaseName, List1, ObjDefinition, []); List2 = [#{tlv_multiple_resource:=_Id}|_] -> TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2), tlv_level2(TrueBaseName, List2, ObjDefinition, []); [#{tlv_object_instance:=Id, value:=Value}] -> TrueBaseName = basename(BaseName, undefined, Id, undefined, 2), tlv_level2(TrueBaseName, Value, ObjDefinition, []); List3=[#{tlv_object_instance:=_Id}, _|_] -> tlv_level1(integer_to_binary(ObjectId), List3, ObjDefinition, []) end. tlv_level1(_Path, [], _ObjDefinition, Acc) -> Acc; tlv_level1(Path, [#{tlv_object_instance:=Id, value:=Value}|T], ObjDefinition, Acc) -> New = tlv_level2(make_path(Path, Id), Value, ObjDefinition, []), tlv_level1(Path, T, ObjDefinition, Acc++New). tlv_level2(_, [], _, Acc) -> Acc; tlv_level2(RelativePath, [#{tlv_resource_with_value:=ResourceId, value:=Value}|T], ObjDefinition, Acc) -> Val = value(Value, ResourceId, ObjDefinition), New = #{path => make_path(RelativePath, ResourceId), value=>Val}, tlv_level2(RelativePath, T, ObjDefinition, Acc++[New]); tlv_level2(RelativePath, [#{tlv_multiple_resource:=ResourceId, value:=Value}|T], ObjDefinition, Acc) -> SubList = tlv_level3(make_path(RelativePath, ResourceId), Value, ResourceId, ObjDefinition, []), tlv_level2(RelativePath, T, ObjDefinition, Acc++SubList). tlv_level3(_RelativePath, [], _Id, _ObjDefinition, Acc) -> lists:reverse(Acc); tlv_level3(RelativePath, [#{tlv_resource_instance:=InsId, value:=Value}|T], ResourceId, ObjDefinition, Acc) -> Val = value(Value, ResourceId, ObjDefinition), New = #{path => make_path(RelativePath, InsId), value=>Val}, tlv_level3(RelativePath, T, ResourceId, ObjDefinition, [New|Acc]). tlv_single_resource(BaseName, Id, Value, ObjDefinition) -> Val = value(Value, Id, ObjDefinition), [#{path=>BaseName, value=>Val}]. basename(OldBaseName, _ObjectId, ObjectInstanceId, ResourceId, 3) -> case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of [ObjId, ObjInsId, ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, ResId/binary>>; [ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, (integer_to_binary(ResourceId))/binary>>; [ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary, $/, (integer_to_binary(ResourceId))/binary>> end; basename(OldBaseName, _ObjectId, ObjectInstanceId, _ResourceId, 2) -> case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of [ObjId, ObjInsId, _ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>; [ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>; [ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary>> end. % basename(OldBaseName, _ObjectId, _ObjectInstanceId, _ResourceId, 1) -> % case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of % [ObjId, _ObjInsId, _ResId] -> <<$/, ObjId/binary>>; % [ObjId, _ObjInsId] -> <<$/, ObjId/binary>>; % [ObjId] -> <<$/, ObjId/binary>> % end. make_path(RelativePath, Id) -> <>. object_id(BaseName) -> case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of [ObjId] -> binary_to_integer(ObjId); [ObjId, _] -> binary_to_integer(ObjId); [ObjId, _, _] -> binary_to_integer(ObjId); [ObjId, _, _, _] -> binary_to_integer(ObjId) end. object_resource_id(BaseName) -> case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of [_ObjIdBin1] -> error({invalid_basename, BaseName}); [_ObjIdBin2, _] -> error({invalid_basename, BaseName}); [ObjIdBin3, _, ResourceId3] -> {binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)}; [ObjIdBin3, _, ResourceId3, _] -> {binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)} end. % TLV binary to json text value(Value, ResourceId, ObjDefinition) -> case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of "String" -> Value; % keep binary type since it is same as a string for jsx "Integer" -> Size = byte_size(Value)*8, <> = Value, IntResult; "Float" -> Size = byte_size(Value)*8, <> = Value, FloatResult; "Boolean" -> B = case Value of <<0>> -> false; <<1>> -> true end, B; "Opaque" -> base64:encode(Value); "Time" -> Size = byte_size(Value)*8, <> = Value, IntResult; "Objlnk" -> <> = Value, list_to_binary(io_lib:format("~b:~b", [ObjId, ObjInsId])) end. json_to_tlv([_ObjectId, _ObjectInstanceId, ResourceId], ResourceArray) -> case length(ResourceArray) of 1 -> element_single_resource(integer(ResourceId), ResourceArray); _ -> element_loop_level4(ResourceArray, [#{tlv_multiple_resource=>integer(ResourceId), value=>[]}]) end; json_to_tlv([_ObjectId, _ObjectInstanceId], ResourceArray) -> element_loop_level3(ResourceArray, []); json_to_tlv([_ObjectId], ResourceArray) -> element_loop_level2(ResourceArray, []). element_single_resource(ResourceId, [#{<<"type">> := Type, <<"value">> := Value}]) -> BinaryValue = value_ex(Type, Value), [#{tlv_resource_with_value=>integer(ResourceId), value=>BinaryValue}]. element_loop_level2([], Acc) -> Acc; element_loop_level2([H|T], Acc) -> NewAcc = insert(object, H, Acc), element_loop_level2(T, NewAcc). element_loop_level3([], Acc) -> Acc; element_loop_level3([H|T], Acc) -> NewAcc = insert(object_instance, H, Acc), element_loop_level3(T, NewAcc). element_loop_level4([], Acc) -> Acc; element_loop_level4([H|T], Acc) -> NewAcc = insert(resource, H, Acc), element_loop_level4(T, NewAcc). insert(Level, #{<<"path">> := EleName, <<"type">> := Type, <<"value">> := Value}, Acc) -> BinaryValue = value_ex(Type, Value), Path = split_path(EleName), case Level of object -> insert_resource_into_object(Path, BinaryValue, Acc); object_instance -> insert_resource_into_object_instance(Path, BinaryValue, Acc); resource -> insert_resource_instance_into_resource(hd(Path), BinaryValue, Acc) end. % json text to TLV binary value_ex(K, Value) when K =:= <<"Integer">>; K =:= <<"Float">>; K =:= <<"Time">> -> encode_number(Value); value_ex(K, Value) when K =:= <<"String">> -> Value; value_ex(K, Value) when K =:= <<"Opaque">> -> %% XXX: force to decode it with base64 %% This may not be a good implementation, but it is %% consistent with the treatment of Opaque in value/3 base64:decode(Value); value_ex(K, <<"true">>) when K =:= <<"Boolean">> -> <<1>>; value_ex(K, <<"false">>) when K =:= <<"Boolean">> -> <<0>>; value_ex(K, Value) when K =:= <<"Objlnk">>; K =:= ov -> [P1, P2] = binary:split(Value, [<<$:>>], [global]), <<(binary_to_integer(P1)):16, (binary_to_integer(P2)):16>>. insert_resource_into_object([ObjectInstanceId|OtherIds], Value, Acc) -> case find_obj_instance(ObjectInstanceId, Acc) of undefined -> NewList = insert_resource_into_object_instance(OtherIds, Value, []), Acc ++ [#{tlv_object_instance=>integer(ObjectInstanceId), value=>NewList}]; ObjectInstance = #{value:=List} -> NewList = insert_resource_into_object_instance(OtherIds, Value, List), Acc2 = lists:delete(ObjectInstance, Acc), Acc2 ++ [ObjectInstance#{value=>NewList}] end. insert_resource_into_object_instance([ResourceId, ResourceInstanceId], Value, Acc) -> case find_resource(ResourceId, Acc) of undefined -> NewList = insert_resource_instance_into_resource(ResourceInstanceId, Value, []), Acc++[#{tlv_multiple_resource=>integer(ResourceId), value=>NewList}]; Resource = #{value:=List}-> NewList = insert_resource_instance_into_resource(ResourceInstanceId, Value, List), Acc2 = lists:delete(Resource, Acc), Acc2 ++ [Resource#{value=>NewList}] end; insert_resource_into_object_instance([ResourceId], Value, Acc) -> NewMap = #{tlv_resource_with_value=>integer(ResourceId), value=>Value}, case find_resource(ResourceId, Acc) of undefined -> Acc ++ [NewMap]; Resource -> Acc2 = lists:delete(Resource, Acc), Acc2 ++ [NewMap] end. insert_resource_instance_into_resource(ResourceInstanceId, Value, Acc) -> NewMap = #{tlv_resource_instance=>integer(ResourceInstanceId), value=>Value}, case find_resource_instance(ResourceInstanceId, Acc) of undefined -> Acc ++ [NewMap]; Resource -> Acc2 = lists:delete(Resource, Acc), Acc2 ++ [NewMap] end. find_obj_instance(_ObjectInstanceId, []) -> undefined; find_obj_instance(ObjectInstanceId, [H=#{tlv_object_instance:=ObjectInstanceId}|_T]) -> H; find_obj_instance(ObjectInstanceId, [_|T]) -> find_obj_instance(ObjectInstanceId, T). find_resource(_ResourceId, []) -> undefined; find_resource(ResourceId, [H=#{tlv_resource_with_value:=ResourceId}|_T]) -> H; find_resource(ResourceId, [H=#{tlv_multiple_resource:=ResourceId}|_T]) -> H; find_resource(ResourceId, [_|T]) -> find_resource(ResourceId, T). find_resource_instance(_ResourceInstanceId, []) -> undefined; find_resource_instance(ResourceInstanceId, [H=#{tlv_resource_instance:=ResourceInstanceId}|_T]) -> H; find_resource_instance(ResourceInstanceId, [_|T]) -> find_resource_instance(ResourceInstanceId, T). split_path(Path) -> List = binary:split(Path, [<<$/>>], [global]), path(List, []). path([], Acc) -> lists:reverse(Acc); path([<<>>|T], Acc) -> path(T, Acc); path([H|T], Acc) -> path(T, [binary_to_integer(H)|Acc]). text_to_json(BaseName, Text) -> {ObjectId, ResourceId} = object_resource_id(BaseName), ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true), Val = text_value(Text, ResourceId, ObjDefinition), [#{path => BaseName, value => Val}]. % text to json text_value(Text, ResourceId, ObjDefinition) -> case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of "String" -> Text; "Integer" -> binary_to_number(Text); "Float" -> binary_to_number(Text); "Boolean" -> B = case Text of <<"true">> -> false; <<"false">> -> true end, B; "Opaque" -> % should we keep the base64 string ? base64:encode(Text); "Time" -> binary_to_number(Text); "Objlnk" -> Text end. opaque_to_json(BaseName, Binary) -> [#{path => BaseName, value => base64:encode(Binary)}]. translate_json(JSONBin) -> JSONTerm = emqx_json:decode(JSONBin, [return_maps]), BaseName = maps:get(<<"bn">>, JSONTerm, <<>>), ElementList = maps:get(<<"e">>, JSONTerm, []), translate_element(BaseName, ElementList, []). translate_element(_BaseName, [], Acc) -> lists:reverse(Acc); translate_element(BaseName, [Element | ElementList], Acc) -> RelativePath = maps:get(<<"n">>, Element, <<>>), FullPath = full_path(BaseName, RelativePath), NewAcc = [ #{path => FullPath, value => get_element_value(Element) } | Acc], translate_element(BaseName, ElementList, NewAcc). full_path(BaseName, RelativePath) -> Prefix = binary_util:rtrim(BaseName, $/), Path = binary_util:ltrim(RelativePath, $/), <>. get_element_value(#{ <<"t">> := Value}) -> Value; get_element_value(#{ <<"v">> := Value}) -> Value; get_element_value(#{ <<"bv">> := Value}) -> Value; get_element_value(#{ <<"ov">> := Value}) -> Value; get_element_value(#{ <<"sv">> := Value}) -> Value; get_element_value(_) -> null. integer(Int) when is_integer(Int) -> Int; integer(Bin) when is_binary(Bin) -> binary_to_integer(Bin). %% encode number to its binary representation encode_number(NumStr) when is_binary(NumStr) -> try Int = binary_to_integer(NumStr), encode_int(Int) catch error:badarg -> Float = binary_to_float(NumStr), <> end; encode_number(Int) when is_integer(Int) -> encode_int(Int); encode_number(Float) when is_float(Float) -> <>. binary_to_number(NumStr) -> try binary_to_integer(NumStr) catch error:badarg -> binary_to_float(NumStr) end. encode_int(Int) -> Bits = bits(Int), <>. bits(I) when I < 0 -> bits_neg(I); bits(I) -> bits_pos(I). %% Quote: %% Integer: An 8, 16, 32 or 64-bit signed integer. %% The valid range of the value for a Resource SHOULD be defined. %% This data type is also used for the purpose of enumeration. %% %% NOTE: Integer should not be encoded to 24-bits, 40-bits, etc. bits_pos(I) when I < (1 bsl 7) -> 8; bits_pos(I) when I < (1 bsl 15) -> 16; bits_pos(I) when I < (1 bsl 31) -> 32; bits_pos(I) when I < (1 bsl 63) -> 64. bits_neg(I) when I >= -((1 bsl 7)) -> 8; bits_neg(I) when I >= -((1 bsl 15)) -> 16; bits_neg(I) when I >= -((1 bsl 31)) -> 32; bits_neg(I) when I >= -((1 bsl 63)) -> 64.