mikutel: initial commit

This commit is contained in:
olock 2025-08-26 10:56:03 +02:00
commit 6d902412d0
No known key found for this signature in database
8 changed files with 349 additions and 0 deletions

7
rebar.config Normal file
View file

@ -0,0 +1,7 @@
{erl_opts, [debug_info]}.
{deps, []}.
{shell, [
%% {config, "config/sys.config"},
{apps, [mikuphone]}
]}.

1
rebar.lock Normal file
View file

@ -0,0 +1 @@
[].

101
src/mikuivr.erl Normal file
View file

@ -0,0 +1,101 @@
-module(mikuivr).
-behaviour(gen_server).
-export([start_link/1]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3,
terminate/2]).
-record(state, {socket, status, number, callerid}).
start_link(Args) ->
gen_server:start_link(?MODULE, Args, []).
init([Socket]) ->
gen_server:cast(self(), accept),
{ok, #state{socket=Socket, status=ok, number=""}}.
handle_cast(accept, State = #state{socket=ListenSocket}) ->
{ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
mikuvisor:start_socket(),
%% send(AcceptSocket, "S,sranie", []),
%% send(AcceptSocket, "H,nasrane", []),
send(AcceptSocket, "G,${CALLERID(num)}", []),
{noreply, State#state{socket=AcceptSocket}};
handle_cast(_, State) ->
{noreply, State}.
handle_info({tcp, Socket, Message}, State) ->
handle_message(Message, State),
{noreply, State};
handle_info({tcp_closed, _Socket}, State) ->
{stop, normal, State};
handle_info({tcp_error, _Socket}, State) ->
{stop, normal, State};
handle_info(E, State) ->
io:fwrite("unexpected: ~p~n", [E]),
{noreply, State}.
handle_call(_E, _From, State) ->
{noreply, State}.
terminate(_Reason, _Tab) ->
ok.
code_change(_OldVersion, Tab, _Extra) ->
{ok, Tab}.
send(Socket, Str, Args) ->
gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)).
handle_message(Message, State) ->
case Message of
"G"++Rest -> handle_variable_receive(Rest, State);
"*"++_ -> enable_number_mode(Message, State);
"0"++_ -> handle_digit(Message, State);
"1"++_ -> handle_digit(Message, State);
"2"++_ -> handle_digit(Message, State);
"3"++_ -> handle_digit(Message, State);
"4"++_ -> handle_digit(Message, State);
"5"++_ -> handle_digit(Message, State);
"6"++_ -> handle_digit(Message, State);
"7"++_ -> handle_digit(Message, State);
"8"++_ -> handle_digit(Message, State);
"9"++_ -> handle_digit(Message, State);
"#"++_ -> disable_number_mode(Message, State);
_ -> {noreply, State}
end.
handle_variable_receive(Variable, State) ->
["${CALLERID(num)}", CallerID] = strings:lexemes(Variable, "="),
{noreply, State#state{callerid = CallerID}}.
handle_digit(Message, State = #state{status=get_more_digits}) ->
Digit = strings:slice(Message, 1, 1),
Number = State#state.number,
case length(Number) of
4 -> disable_number_mode(Message, State);
_ -> {noreply, State#state{number = Number ++ Digit}}
end;
handle_digit(Message, State = #state{status=ok}) ->
{noreply, State}.
enable_number_mode(_, State) ->
{noreply, State#state{status=get_more_digits, number=""}}.
disable_number_mode(_, State) ->
register(State#state.callerid, State#state.number),
{noreply, State#state{status=ok, number=""}}.
register(CallerID, Number) ->
[Supervisor | _] = mikuvisor:get_ancestors(self()),
{ok, {_, Mikutel, _, _}} = supervisor:which_child(Supervisor, mikutel),
gen_server:cast(Mikutel, {register, CallerID, Number}).

16
src/mikuphone.app.src Normal file
View file

@ -0,0 +1,16 @@
{application, mikuphone, [
{description, "The Mikuphone"},
{vsn, "1.0.0"},
{registered, []},
{mod, {mikuphone, [
[2949, "ommhost", 12622, "user", "password", []]
]}},
{applications, [
kernel,
stdlib
]},
{env, []},
{modules, []},
{licenses, ["MIT"]},
{links, []}
]}.

16
src/mikuphone.erl Normal file
View file

@ -0,0 +1,16 @@
-module(mikuphone).
-behaviour(application).
-export([start/2,stop/1]).
%% 2949 is port for ivr
%% 12622 port for omm
start(_StartType, StartArgs) ->
%% mikuvisor:start_link(IvrPort),
ssl:start(),
mikuvisor:start_link(StartArgs).
%% spawn(mikutel, connect, [OMMAddress, OMMPort, Username, Password]).
%% mikutel:start(OMMAddress, OMMPort, OMMTLS, Username, Password).
stop(_) ->
ok.

123
src/mikutel.erl Normal file
View file

@ -0,0 +1,123 @@
-module(mikutel).
-behaviour(gen_server).
-export([init/1,
handle_cast/2,
handle_call/3,
handle_info/2,
code_change/3,
terminate/2,
start_link/1]).
-record(state, {socket, modulus, exponent}).
%% TODO: add fallback TCP Connection
start_link(Args) ->
gen_server:start_link(?MODULE, Args, []).
init([Host, Port, Username, Password, _]) ->
Ciphers = ssl:prepend_cipher_suites([ssl:str_to_suite("AES256-GCM-SHA384")],
ssl:cipher_suites(default, 'tlsv1.2')),
{ok, Socket} = ssl:connect(Host, Port, [{verify, verify_none},
{versions, ['tlsv1.2']},
{ciphers, Ciphers},
{active, true}]),
%% {ok, Socket} = gen_tcp:connect(Host, Port, []),
%% Arbitrary sleeps because something drops my Erlang messages??
timer:send_interval(60000, ping),
login(Socket, Username, Password),
timer:sleep(100),
set_dect_subscription_mode(Socket, 'Configured'),
timer:sleep(100),
subscribe(Socket, [{'PPUserCnf', [{uid, -1}]},
{'PPDevCnf', [{ppn, -1}]}]),
{ok, #state{socket=Socket}}.
handle_cast(_, State) ->
{noreply, State}.
handle_call(_E, _From, State) ->
{noreply, State}.
handle_cast({register, CallerID, Number}, State) ->
%% TODO
handle_info({subscribe, Events}, State) ->
subscribe(State#state.socket, Events);
handle_info({ssl, Socket, Message}, State) ->
handle_response(Message, State);
handle_info({ssl_closed, Socket}, State) ->
ssl:close(Socket),
{stop, normal, State};
%% handle_info({tcp, Socket, Message}, State) ->
%% handle_response(Message, State);
%% handle_info({tcp_closed, Socket}, State) ->
%% gen_tcp:close(Socket),
%% {stop, normal, State};
handle_info(ping, State = #state{socket=Socket}) ->
Data = {'Ping', [], []},
send(Socket, Data),
{noreply, State}.
terminate(_Reason, _Tab) ->
ok.
code_change(_OldVersion, Tab, _Extra) ->
{ok, Tab}.
login(Socket, Username, Password) ->
Data = {'Open',[{username, Username}, {password, Password}], []},
send(Socket, Data).
set_dect_subscription_mode(Socket, Mode) ->
Data = {'SetDECTSubscriptionMode', [{mode, Mode}], []},
send(Socket, Data).
transform_event({Event, Args}) ->
{e, [{cmd, 'On'}, {eventType, Event}] ++ Args, []}.
subscribe(Socket, Args) ->
Payload = lists:map(fun transform_event/1, Args),
Data = {'Subscribe', [], Payload},
%% send(Socket, Data).
send(Socket, Data).
send(Socket, Data) ->
RawPayload = lists:flatten(xmerl:export_simple([Data], xmerl_xml, [{prolog, ""}])),
BinPayload = list_to_binary(RawPayload),
Payload = binary_to_list(<<BinPayload/binary, <<00>>/binary>>),
ssl:send(Socket, Payload).
%% gen_tcp:send(Socket, Payload).
handle_response(Message, State) ->
{Element, _} = xmerl_scan:string(Message),
{Response, Parameters, Body} = xmerl_lib:simplify_element(Element),
io:format("Received: ~p~n", [Response]),
io:format("Params: ~p~n", [Parameters]),
io:format("Body: ~p~n", [Body]),
case Response of
'OpenResp' -> handle_login(Body, Parameters, State);
'EventPPDevCnf' -> handle_device_configuration_event(Body, Parameters, State);
_ -> {noreply, State}
end.
handle_login(_, [{publicKey, [{modulus, Modulus}, {exponent, Exponent}], []} | _], State) ->
{noreply, State#state{modulus=Modulus, exponent=Exponent}};
handle_login(Body, [_ | Rest], State) ->
handle_login(Body, Rest, State).
handle_device_configuration_event(_, [{pp, Args, _}], State) ->
handle_device_configuration_event(Args, State).
handle_device_configuration_event([{ipei, Ipei} | _], State) ->
ok;
%% create_device().
handle_device_configuration_event([_ | Rest], State) ->
handle_device_configuration_event(Rest, State).

84
src/mikuvisor.erl Normal file
View file

@ -0,0 +1,84 @@
-module(mikuvisor).
-behaviour(supervisor).
-export([init/1, start_link/1, start_child/0]).
start_link([IvrPort, OMMAddress, OMMPort, OMMUsername, OMMPassword, OMMTLS]) ->
supervisor:start_link({local, ?MODULE}, ?MODULE, [IvrPort, OMMAddress, OMMPort, OMMUsername, OMMPassword, OMMTLS]).
init([IvrPort, OMMAddress, OMMPort, OMMUsername, OMMPassword, OMMTLS]) ->
spawn_link(fun empty_listeners/0),
{ok, IVRSocket} = gen_tcp:listen(IvrPort, []),
%% Flags = #{strategy => one_for_one,
%% intensity => 60,
%% period => 3600},
%% Mikutel = case OMMTLS of
%% true -> #{id => mikutel,
%% start => {mikutel_ssl, start_link, [[OMMAddress,
%% OMMPort,
%% OMMUsername,
%% OMMPassword,
%% OMMTLS]]},
%% type => worker,
%% modules => [mikutel_ssl]};
%% _ -> #{id => mikutel,
%% start => {mikutel_plain, start_link, [[OMMAddress,
%% OMMPort,
%% OMMUsername,
%% OMMPassword,
%% OMMTLS]]},
%% type => worker,
%% modules => [mikutel_plain]}
%% end,
%% Specs = [#{id => mikuivr,
%% start => {mikuivr, start_link, [[IVRSocket]]},
%% type => worker,
%% modules => [mikuivr]},
%% Mikutel],
%% {ok, {Flags, Specs}}.
{ok, {{one_for_one, 60, 3600},
[
{
mikuivr,
{mikuivr, start_link, [[IVRSocket]]},
temporary,
1000,
worker,
[mikuivr]
},
{
mikutel,
{mikutel, start_link, [[OMMAddress,
OMMPort,
OMMUsername,
OMMPassword,
OMMTLS]]},
temporary,
1000,
worker,
[mikuivr]
}
]
}}.
start_child() ->
supervisor:start_child(?MODULE, []).
empty_listeners() ->
[start_child() || _ <- lists:seq(1,20)],
ok.
get_ancestors(PID) when is_pid(PID) ->
case erlang:process_info(PID) of
{dictionary, Dict} -> ancestors_from_dict(Dict);
_ -> []
end.
ancestors_from_dict([]) ->
[];
ancestors_from_dict([{'$ancestors', Ancestors} | _]) ->
Ancestors;
ancestors_from_dict([_ | Rest]) ->
ancestors_from_dict(Rest).

1
src/rebar.lock Normal file
View file

@ -0,0 +1 @@
[].