diff --git a/lib/bacnet.ex b/lib/bacnet.ex index 7acb96a..c105233 100644 --- a/lib/bacnet.ex +++ b/lib/bacnet.ex @@ -1,5 +1,20 @@ -defmodule Bacnet do +defmodule BACNet do @moduledoc """ BACNet client. """ + + require Logger + + defmodule Device do + @typedoc "Placeholder" + @type t :: term + end + + @spec add_device(device :: Device.t) :: :ok | {:error, term} + def add_device(device) do + GenServer.call({:global, :bacnetd}, {:add_device, device}) + end + + @doc false + defdelegate ei_log(level, term), to: Logger, as: :log end diff --git a/lib/bacnet/application.ex b/lib/bacnet/application.ex index e6625a6..9af3622 100644 --- a/lib/bacnet/application.ex +++ b/lib/bacnet/application.ex @@ -1,15 +1,24 @@ -defmodule Bacnet.Application do +defmodule BACNet.Application do @moduledoc false use Application @impl true def start(_type, _args) do - children = children() - opts = [strategy: :one_for_one, name: Bacnet.Supervisor] + bacnetd_exe = "#{:code.priv_dir(:bacnet)}/bacnetd" + bacnetd_args = [ + "--cookie", Node.get_cookie |> to_string, + "--nodename", Node.self |> to_string, + ] - Supervisor.start_link(children, opts) - end + children = [ + {MuonTrap.Daemon, [bacnetd_exe, bacnetd_args, []]} + ] - defp children(), do: [] + Supervisor.start_link( + children, + strategy: :one_for_one, + name: BACNet.Supervisor + ) + end end diff --git a/mix.exs b/mix.exs index dbd111b..0485980 100644 --- a/mix.exs +++ b/mix.exs @@ -1,4 +1,4 @@ -defmodule Bacnet.MixProject do +defmodule BACNet.MixProject do use Mix.Project def project do @@ -33,7 +33,7 @@ defmodule Bacnet.MixProject do def application do [ extra_applications: [:logger], - mod: {Bacnet.Application, []} + mod: {BACNet.Application, []} ] end @@ -52,6 +52,7 @@ defmodule Bacnet.MixProject do {:elixir_cmake, "~> 0.8.0"}, {:excoveralls, "~> 0.18", only: :test}, {:ex_doc, "~> 0.34", only: :dev, runtime: false}, + {:muontrap, "~> 1.5"}, ] end diff --git a/spec/bacnet_spec.exs b/spec/bacnet_spec.exs index 7479a9b..369a289 100644 --- a/spec/bacnet_spec.exs +++ b/spec/bacnet_spec.exs @@ -1,4 +1,4 @@ -defmodule Bacnet.Spec do +defmodule BACNet.Spec do use ESpec specify do: expect true |> to(eq true) diff --git a/src/arg.c b/src/arg.c new file mode 100644 index 0000000..eea3286 --- /dev/null +++ b/src/arg.c @@ -0,0 +1,31 @@ +#include +#include + +#include "arg.h" + +static const struct option options[] = { + {"nodename", required_argument, 0, 'n'}, + {"cookie", required_argument, 0, 'c'}, + {0, 0, 0, 0}, +}; + +void args_parse(arg_t *args, int argc, char **argv) +{ + int opt; + int index = 0; + + while ((opt = getopt_long(argc, argv, "n:c:", options, &index)) != -1) { + switch (opt) { + case 'n': + strncpy(args->nodename, optarg, sizeof(args->nodename)); + break; + + case 'c': + strncpy(args->cookie, optarg, sizeof(args->cookie)); + break; + + default: + break; + } + } +} diff --git a/src/arg.h b/src/arg.h new file mode 100644 index 0000000..8d3a556 --- /dev/null +++ b/src/arg.h @@ -0,0 +1,13 @@ +#ifndef ARG_H +#define ARG_H + +#include + +typedef struct { + char nodename[MAXNODELEN + 1]; + char cookie[MAXATOMLEN + 1]; +} arg_t; + +void args_parse(arg_t *args, int argc, char **argv); + +#endif /* ARG_H */ diff --git a/src/ei_client.c b/src/ei_client.c new file mode 100644 index 0000000..ae09245 --- /dev/null +++ b/src/ei_client.c @@ -0,0 +1,120 @@ +#include +#include +#include +#include + +#include "ei_client.h" + +#define OTP_COMPAT_VER 24 +#define CNODE_NAME "bacnetd" + +struct ei_client { + bool ready; + pthread_mutex_t lock; + struct ei_cnode_s cnode; + uint32_t creation; + int port; + int fd; + char nodename[MAXNODELEN + 1]; + char cookie[MAXATOMLEN + 1]; +}; + +static struct ei_client client = { 0 }; +static void ei_free(); + +bool ei_client_config(const char *nodename, const char *cookie) +{ + if (client.ready) + return true; + + if (atexit(ei_free) != 0) + return false; + + strncpy(client.nodename, nodename, sizeof(client.nodename)); + strncpy(client.cookie, cookie, sizeof(client.cookie)); + + ei_init(); + ei_set_compat_rel(OTP_COMPAT_VER); + + pthread_mutex_init(&client.lock, NULL); + client.creation = 0; + + if (ei_connect_init(&client.cnode, CNODE_NAME, cookie, client.creation) < 0) + return false; + + client.creation++; + + client.fd = ei_connect(&client.cnode, client.nodename); + if (client.fd < 0) + return false; + + erlang_pid *pid = ei_self(&client.cnode); + if (ei_global_register(client.fd, CNODE_NAME, pid) == -1) + return false; + + client.ready = true; + + return true; +} + +bool ei_client_send(char *process_name, ei_x_buff *message) +{ + if (!client.ready) + return false; + + pthread_mutex_lock(&client.lock); + int ret = ei_reg_send( + &client.cnode, + client.fd, + process_name, + message->buff, + message->index + ); + pthread_mutex_unlock(&client.lock); + + return ret == 0 ? true : false; +} + +bool ei_client_send_to(erlang_pid *pid, ei_x_buff *message) +{ + if (!client.ready) + return false; + + pthread_mutex_lock(&client.lock); + int ret = ei_send(client.fd, pid, message->buff, message->index); + pthread_mutex_unlock(&client.lock); + + return ret == 0 ? true : false; +} + +bool ei_client_call(char *module, char *func, ei_x_buff *args, ei_x_buff *result) +{ + pthread_mutex_lock(&client.lock); + int ret = ei_rpc( + &client.cnode, + client.fd, + module, + func, + args->buff, + args->index, + result + ); + pthread_mutex_unlock(&client.lock); + + return ret == -1 ? false : true; +} + +bool ei_client_recv(erlang_msg *meta, ei_x_buff *message) +{ + pthread_mutex_lock(&client.lock); + int ret = ei_xreceive_msg(client.fd, meta, message); + pthread_mutex_unlock(&client.lock); + + return ret == ERL_ERROR ? false : true; +} + +static void ei_free() +{ + if (client.fd > 0) + ei_close_connection(client.fd); +} diff --git a/src/ei_client.h b/src/ei_client.h new file mode 100644 index 0000000..54fca47 --- /dev/null +++ b/src/ei_client.h @@ -0,0 +1,12 @@ +#ifndef EI_CLIENT_H +#define EI_CLIENT_H + +#include + +bool ei_client_config(const char *nodename, const char *cookie); +bool ei_client_send(char *process_name, ei_x_buff *message); +bool ei_client_send_to(erlang_pid *pid, ei_x_buff *messageg); +bool ei_client_call(char *module, char *func, ei_x_buff *message, ei_x_buff *out); +bool ei_client_recv(erlang_msg *meta, ei_x_buff *message); + +#endif /* EI_CLIENT_H */ diff --git a/src/ei_log.c b/src/ei_log.c new file mode 100644 index 0000000..9709cc0 --- /dev/null +++ b/src/ei_log.c @@ -0,0 +1,52 @@ +#include +#include + +#include "ei_client.h" +#include "ei_log.h" + +#define MAX_LOG_LENGTH 1024 + +static const char *level_to_str(log_level_t level); + +void ei_log(log_level_t level, const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + + char log_buffer[MAX_LOG_LENGTH]; + int log_length = vsnprintf(log_buffer, sizeof(log_buffer), format, vargs); + + ei_x_buff out; + ei_x_new(&out); + + ei_x_buff args; + ei_x_new(&args); + ei_x_encode_list_header(&args, 2); + ei_x_encode_atom(&args, level_to_str(level)); + ei_x_encode_binary(&args, log_buffer, log_length); + ei_x_encode_empty_list(&args); + + if (!ei_client_call("Elixir.BACNet", "ei_log", &args, &out)) { + char new_format[MAX_LOG_LENGTH]; + snprintf(new_format, sizeof(new_format), "%s\n", format); + vprintf(new_format, vargs); + } + + ei_x_free(&out); + ei_x_free(&args); + va_end(vargs); +} + +static const char *level_to_str(log_level_t level) +{ + switch (level) { + case EMERGENCY: return "emergency"; + case ALERT: return "alert"; + case CRITICAL: return "critical"; + case ERROR: return "error"; + case WARNING: return "warning"; + case NOTICE: return "notice"; + case INFO: return "info"; + case DEBUG: return "debug"; + } +} diff --git a/src/ei_log.h b/src/ei_log.h new file mode 100644 index 0000000..a3b81f3 --- /dev/null +++ b/src/ei_log.h @@ -0,0 +1,26 @@ +#ifndef EI_LOG_H +#define EI_LOG_H + +#define LOG_EMERGENCY(format, ...) ei_log(EMERGENCY, format, ##__VA_ARGS__) +#define LOG_ALERT(format, ...) ei_log(ALERT, format, ##__VA_ARGS__) +#define LOG_CRITICAL(format, ...) ei_log(CRITICAL, format, ##__VA_ARGS__) +#define LOG_ERROR(format, ...) ei_log(ERROR, format, ##__VA_ARGS__) +#define LOG_WARNING(format, ...) ei_log(WARNING, format, ##__VA_ARGS__) +#define LOG_NOTICE(format, ...) ei_log(NOTICE, format, ##__VA_ARGS__) +#define LOG_INFO(format, ...) ei_log(INFO, format, ##__VA_ARGS__) +#define LOG_DEBUG(format, ...) ei_log(DEBUG, format, ##__VA_ARGS__) + +typedef enum { + EMERGENCY, + ALERT, + CRITICAL, + ERROR, + WARNING, + NOTICE, + INFO, + DEBUG, +} log_level_t; + +void ei_log(log_level_t level, const char *format, ...); + +#endif /* EI_LOG_H */ diff --git a/src/main.c b/src/main.c index 257ee70..5f08867 100644 --- a/src/main.c +++ b/src/main.c @@ -1,7 +1,128 @@ +#include + +#include "arg.h" #include "bacnet/datalink/dlenv.h" +#include "ei_client.h" +#include "ei_log.h" + +#define STRINGIFY(x) #x +#define TOSTR(x) STRINGIFY(x) +#define ERL_TUPLE TOSTR(ERL_SMALL_TUPLE_EXT) TOSTR(ERL_SMALL_TUPLE_EXT) -int main(int argc, char **argv) { +static arg_t args; +static void handle_call(char *buffer, int *index, ei_x_buff *reply); + +int main(int argc, char **argv) +{ dlenv_init(); + args_parse(&args, argc, argv); + + if (!ei_client_config(args.nodename, args.cookie)) { + LOG_ERROR("bacnetd: unable to establish connection"); + return -1; + } + + LOG_DEBUG("bacnetd: process started"); + + while (1) { + erlang_msg meta; + ei_x_buff message; + ei_x_new(&message); + + if (!ei_client_recv(&meta, &message)) + goto cleanup; + + int index = 0; + int version = 0; + ei_term term = { 0 }; + char message_type[MAXATOMLEN] = { 0 }; + + bool is_bad_message = + ei_decode_version(message.buff, &index, &version) + || ei_decode_tuple_header(message.buff, &index, &term.size) + || (term.size < 2) + || ei_decode_atom(message.buff, &index, message_type); + + if (is_bad_message) + goto cleanup; + + if (strcmp(message_type, "$gen_call") == 0) { + erlang_pid from_pid; + erlang_ref from_ref; + + // request {:"$gen_call", {PID, [:alias | REF]}, TUPLE}} + bool is_bad_message = + ei_decode_tuple_header(message.buff, &index, &term.size) + || (term.size != 2) + || ei_decode_pid(message.buff, &index, &from_pid) + || ei_decode_list_header(message.buff, &index, &term.size) + || (term.size != 1) + || ei_decode_atom(message.buff, &index, term.value.atom_name) + || strcmp(term.value.atom_name, "alias") + || ei_decode_ref(message.buff, &index, &from_ref) + || ei_get_type(message.buff, &index, (int *)&term.ei_type, &term.size) + || memchr(ERL_TUPLE, term.ei_type, sizeof(ERL_TUPLE)) == NULL; + + if (is_bad_message) { + LOG_ERROR("Failed decoding message"); + goto cleanup; + } + + // reply {[:alias | REF], REPLY} + ei_x_buff reply; + ei_x_new_with_version(&reply); + ei_x_encode_tuple_header(&reply, 2); + ei_x_encode_list_header(&reply, 1); + ei_x_encode_atom(&reply, "alias"); + ei_x_encode_ref(&reply, &from_ref); + + handle_call(message.buff, &index, &reply); + + if (!ei_client_send_to(&from_pid, &reply)) + LOG_ERROR("Unable to send reply"); + + ei_x_free(&reply); + } + else { + LOG_WARNING("bacnetd: unknown message type %s", message_type); + } + + cleanup: + ei_x_free(&message); + } return 0; } + +static void add_device(char *buffer, int *index, ei_x_buff *reply) +{ + ei_x_encode_tuple_header(reply, 2); + ei_x_encode_atom(reply, "error"); + ei_x_encode_atom(reply, "unimplemented"); +} + +static void handle_call(char *buffer, int *index, ei_x_buff *reply) +{ + int size = 0; + char call_type[MAXATOMLEN] = { 0 }; + + bool is_bad_message = + ei_decode_tuple_header(buffer, index, &size) + || ei_decode_atom(buffer, index, call_type); + + if (is_bad_message) { + ei_x_encode_tuple_header(reply, 2); + ei_x_encode_atom(reply, "error"); + ei_x_encode_atom(reply, "bad_request"); + return; + } + + if (strcmp(call_type, "add_device") == 0) { + add_device(buffer, index, reply); + } + else { + ei_x_encode_tuple_header(reply, 2); + ei_x_encode_atom(reply, "error"); + ei_x_encode_atom(reply, "unimplemented"); + } +}