Packages
simple_mqtt
0.1.0
A basic, single node pub-sub implementation where publishers and subscribers use topics and topics filters compatible with MQTT.
Current section
Files
Jump to
Current section
Files
lib/simple_mqtt.ex
defmodule SimpleMqtt do
alias SimpleMqtt.Subscriptions
use GenServer
require Logger
@type package_identifier() :: 0x0001..0xFFFF | nil
@type topic() :: String.t()
@type topic_filter() :: String.t()
@type payload() :: binary() | nil
@moduledoc """
The SimpleMqtt is a basic, single node pub-sub implementation where publishers and subscribers use topics and topics filters
compatible with MQTT.
It cannot replace a real MQTT broker, but can be used in a simple IoT device, with multiple local sensors and actuators that have to communicate with each other.
"""
@doc """
Starts new Simple MQTT server and links it to the current process
"""
def start_link() do
GenServer.start_link(__MODULE__, nil)
end
@doc """
Subscribes the current process to the given list of topics. Each item in the list must be a valid MQTT filter.
## Examples
In the following example, the current process subscribes to two topic filters:
```
{:ok, pid} = SimpleMqtt.start_link()
:ok = SimpleMqtt.subscribe(pid, ["things/sensor_1/+", "things/sensor_2/+"])
```
If the process needs to monitor one more topic filter, it can call `subscribe` again. After this call, the current process
will be subscribed to three topic filters.
```
:ok = SimpleMqtt.subscribe(pid, ["things/sensor_3/+"])
```
"""
@spec subscribe(pid(), [topic_filter()]) :: :ok
def subscribe(pid, topics) do
Logger.debug("Subscribed process #{inspect(pid)} to topics #{inspect(topics)}")
GenServer.call(pid, {:subscribe, topics})
end
@doc """
Unsubscribes the current process from the given list of topics.
## Examples
In the following example, the current process starts the Simple MQTT server, subscribes to two topic filters,
and then unsubscribes from the second one. It will still receive messages published to a topic that matches the first
filter.
```
{:ok, pid} = SimpleMqtt.start_link()
:ok = SimpleMqtt.subscribe(pid, ["things/sensor_1/+", "things/sensor_2/+"])
:ok = SimpleMqtt.unsubscribe(pid, ["things/sensor_2/+"])
```
In the second example, the current process unsubscribes from all topics. It will no longer receive any messages.
```
{:ok, pid} = SimpleMqtt.start_link()
:ok = SimpleMqtt.subscribe(pid, ["things/sensor_1/+", "things/sensor_2/+"])
:ok = SimpleMqtt.unsubscribe(pid, :all)
```
"""
@spec unsubscribe(pid(), [topic_filter()] | :all) :: :ok
def unsubscribe(pid, topics) do
Logger.debug("Unsubscribed process #{inspect(pid)} from topics #{inspect(topics)}")
GenServer.call(pid, {:unsubscribe, topics})
end
@doc """
Publishes message to the given topic.
## Examples
```
{:ok, pid} = SimpleMqtt.start_link()
:ok = SimpleMqtt.publish(pid, "things/sensor_1/temperature", "34.5")
```
"""
@spec publish(pid(), topic(), payload()) :: :ok
def publish(pid, topic, payload) do
Logger.info("Publishing message to topic #{topic}")
GenServer.cast(pid, {:publish, topic, payload})
end
@impl true
def init(_) do
{:ok, {Subscriptions.new(), %{}}}
end
@impl true
def handle_call({:subscribe, topics}, {from, _}, {subscriptions, monitors}) do
case Subscriptions.subscribe(subscriptions, from, topics) do
:error -> {:reply, :error, subscriptions}
new_subscriptions ->
reference = Process.monitor(from)
new_monitors = Map.put(monitors, from, reference)
{:reply, :ok, {new_subscriptions, new_monitors}}
end
end
@impl true
def handle_call({:unsubscribe, topics}, {from, _}, {subscriptions, monitors} = state) do
case Subscriptions.unsubscribe(subscriptions, from, topics) do
:error -> {:reply, :error, state}
{:empty, new_subscriptions} ->
new_monitors = case Map.fetch(monitors, from) do
{:ok, monitor_ref} ->
Process.demonitor(monitor_ref)
Map.delete(monitors, from)
_ -> monitors
end
{:reply, :ok, {new_subscriptions, new_monitors}}
{:not_empty, new_subscriptions} ->
{:reply, :ok, {new_subscriptions, monitors}}
end
end
@impl true
def handle_cast({:publish, topic, payload}, {subscriptions, _} = state) do
case Subscriptions.list_matched(subscriptions, topic) do
:error -> {:noreply, state}
pids ->
for pid <- pids do
Logger.debug("Sending message published to topic #{topic} to subscriber #{inspect(pid)}")
send(pid, {:simple_mqtt, topic, payload})
end
{:noreply, state}
end
end
@impl true
def handle_info({:DOWN, _ref, :process, pid, _reason}, {subscriptions, monitors}) do
Logger.info("Subscriber #{inspect(pid)} exited. Removing its subscriptions")
new_monitors = Map.delete(monitors, pid)
case Subscriptions.unsubscribe(subscriptions, pid, :all) do
:error -> {:noreply, {subscriptions, new_monitors}}
{_, new_subscriptions} -> {:noreply, {new_subscriptions, new_monitors}}
end
end
end