Packages
macula
0.20.12
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
src/macula_protocol_encoder.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% Protocol message encoder for Macula mesh.
%%% Encodes message maps to binary wire format.
%%%
%%% Frame Format (8-byte header + payload):
%%% - Version (1 byte): Protocol version (currently 0x01)
%%% - Type (1 byte): Message type ID
%%% - Flags (1 byte): Reserved for future use (0x00)
%%% - Reserved (1 byte): Must be 0x00
%%% - Payload Length (4 bytes): Big-endian uint32
%%% - Payload (N bytes): MessagePack-encoded message data
%%% @end
%%%-------------------------------------------------------------------
-module(macula_protocol_encoder).
-export([encode/2]).
%%%===================================================================
%%% Constants
%%%===================================================================
-define(PROTOCOL_VERSION, 1).
-define(FLAGS_NONE, 0).
-define(RESERVED, 0).
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Encode a message to binary format.
%% Returns a binary with 8-byte header + MessagePack payload.
%% @end
-spec encode(macula_protocol_types:message_type(), map()) -> binary().
encode(Type, Msg) when is_atom(Type), is_map(Msg) ->
%% Validate message has required fields
validate_message(Type, Msg),
%% Get message type ID
TypeId = macula_protocol_types:message_type_id(Type),
%% Encode payload with MessagePack
Payload = msgpack:pack(Msg, [{map_format, map}]),
%% Build frame: 8-byte header + payload
PayloadLen = byte_size(Payload),
<<?PROTOCOL_VERSION:8,
TypeId:8,
?FLAGS_NONE:8,
?RESERVED:8,
PayloadLen:32/big-unsigned,
Payload/binary>>.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @doc Validate message contains required fields for its type.
%% Crashes with badarg if validation fails.
%% @end
-spec validate_message(atom(), map()) -> ok.
validate_message(connect, Msg) ->
#{version := _, node_id := _, realm_id := _, capabilities := _} = Msg,
ok;
validate_message(disconnect, Msg) ->
#{reason := _, message := _} = Msg,
ok;
validate_message(ping, Msg) ->
#{timestamp := _} = Msg,
ok;
validate_message(pong, Msg) ->
#{timestamp := _, server_time := _} = Msg,
ok;
validate_message(publish, Msg) ->
%% Accept both atom and binary keys (from MessagePack decoding)
case {maps:is_key(topic, Msg), maps:is_key(<<"topic">>, Msg)} of
{true, _} -> #{topic := _, payload := _, qos := _, retain := _, message_id := _} = Msg;
{_, true} -> #{<<"topic">> := _, <<"payload">> := _, <<"qos">> := _, <<"retain">> := _, <<"message_id">> := _} = Msg
end,
ok;
validate_message(subscribe, Msg) ->
%% Accept both atom and binary keys
case {maps:is_key(topics, Msg), maps:is_key(<<"topics">>, Msg)} of
{true, _} -> #{topics := _, qos := _} = Msg;
{_, true} -> #{<<"topics">> := _, <<"qos">> := _} = Msg
end,
ok;
validate_message(unsubscribe, Msg) ->
%% Accept both atom and binary keys
case {maps:is_key(topics, Msg), maps:is_key(<<"topics">>, Msg)} of
{true, _} -> #{topics := _} = Msg;
{_, true} -> #{<<"topics">> := _} = Msg
end,
ok;
validate_message(rpc_route, Msg) ->
%% Accept both atom and binary keys
case {maps:is_key(destination_node_id, Msg), maps:is_key(<<"destination_node_id">>, Msg)} of
{true, _} -> #{destination_node_id := _, source_node_id := _, hop_count := _,
max_hops := _, payload_type := _, payload := _} = Msg;
{_, true} -> #{<<"destination_node_id">> := _, <<"source_node_id">> := _,
<<"hop_count">> := _, <<"max_hops">> := _,
<<"payload_type">> := _, <<"payload">> := _} = Msg
end,
ok;
validate_message(pubsub_route, Msg) ->
%% Accept both atom and binary keys
case {maps:is_key(destination_node_id, Msg), maps:is_key(<<"destination_node_id">>, Msg)} of
{true, _} -> #{destination_node_id := _, source_node_id := _, hop_count := _,
max_hops := _, topic := _, payload := _} = Msg;
{_, true} -> #{<<"destination_node_id">> := _, <<"source_node_id">> := _,
<<"hop_count">> := _, <<"max_hops">> := _,
<<"topic">> := _, <<"payload">> := _} = Msg
end,
ok;
validate_message(nat_probe, Msg) ->
%% NAT probe: node_id, local_port, observer_id
case {maps:is_key(node_id, Msg), maps:is_key(<<"node_id">>, Msg)} of
{true, _} -> #{node_id := _, local_port := _} = Msg;
{_, true} -> #{<<"node_id">> := _, <<"local_port">> := _} = Msg
end,
ok;
validate_message(nat_probe_reply, Msg) ->
%% NAT probe reply: node_id, reflexive_ip, reflexive_port, server_time
case {maps:is_key(node_id, Msg), maps:is_key(<<"node_id">>, Msg)} of
{true, _} -> #{node_id := _, reflexive_ip := _, reflexive_port := _, server_time := _} = Msg;
{_, true} -> #{<<"node_id">> := _, <<"reflexive_ip">> := _, <<"reflexive_port">> := _, <<"server_time">> := _} = Msg
end,
ok;
validate_message(punch_request, Msg) ->
%% Punch request: session_id, requester_id, target_id
case {maps:is_key(session_id, Msg), maps:is_key(<<"session_id">>, Msg)} of
{true, _} -> #{session_id := _, requester_id := _, target_id := _} = Msg;
{_, true} -> #{<<"session_id">> := _, <<"requester_id">> := _, <<"target_id">> := _} = Msg
end,
ok;
validate_message(punch_coordinate, Msg) ->
%% Punch coordinate: session_id, peer_id, peer_host, peer_ports, punch_time, role
case {maps:is_key(session_id, Msg), maps:is_key(<<"session_id">>, Msg)} of
{true, _} -> #{session_id := _, peer_id := _, peer_host := _, peer_ports := _, punch_time := _, role := _} = Msg;
{_, true} -> #{<<"session_id">> := _, <<"peer_id">> := _, <<"peer_host">> := _, <<"peer_ports">> := _, <<"punch_time">> := _, <<"role">> := _} = Msg
end,
ok;
validate_message(punch_result, Msg) ->
%% Punch result: session_id, success (connected_port is optional)
case {maps:is_key(session_id, Msg), maps:is_key(<<"session_id">>, Msg)} of
{true, _} -> #{session_id := _, success := _} = Msg;
{_, true} -> #{<<"session_id">> := _, <<"success">> := _} = Msg
end,
ok;
validate_message(relay_request, Msg) ->
%% Relay request: session_id, target_id (reason is optional)
case {maps:is_key(session_id, Msg), maps:is_key(<<"session_id">>, Msg)} of
{true, _} -> #{session_id := _, target_id := _} = Msg;
{_, true} -> #{<<"session_id">> := _, <<"target_id">> := _} = Msg
end,
ok;
validate_message(relay_data, Msg) ->
%% Relay data: session_id, payload
case {maps:is_key(session_id, Msg), maps:is_key(<<"session_id">>, Msg)} of
{true, _} -> #{session_id := _, payload := _} = Msg;
{_, true} -> #{<<"session_id">> := _, <<"payload">> := _} = Msg
end,
ok;
validate_message(rpc_request, Msg) ->
%% NATS-style async RPC request
case {maps:is_key(request_id, Msg), maps:is_key(<<"request_id">>, Msg)} of
{true, _} -> #{request_id := _, procedure := _, args := _, from_node := _} = Msg;
{_, true} -> #{<<"request_id">> := _, <<"procedure">> := _, <<"args">> := _, <<"from_node">> := _} = Msg
end,
ok;
validate_message(rpc_reply, Msg) ->
%% NATS-style async RPC reply
case {maps:is_key(request_id, Msg), maps:is_key(<<"request_id">>, Msg)} of
{true, _} -> #{request_id := _, from_node := _} = Msg;
{_, true} -> #{<<"request_id">> := _, <<"from_node">> := _} = Msg
end,
ok;
validate_message(_Type, _Msg) ->
%% For message types not yet validated, allow anything
ok.