feat: dynamically manage floors/rooms/tags
This commit is contained in:
@@ -9,6 +9,8 @@ defmodule Localiser.Application do
|
|||||||
{Phoenix.PubSub, name: Localiser.PubSub},
|
{Phoenix.PubSub, name: Localiser.PubSub},
|
||||||
Localiser.MQTT.Supervisor,
|
Localiser.MQTT.Supervisor,
|
||||||
Localiser.RSSI.Buffer,
|
Localiser.RSSI.Buffer,
|
||||||
|
Localiser.Localisation.Floor.Manager,
|
||||||
|
Localiser.Localisation.Tag.Manager,
|
||||||
Localiser.Localisation.Filter.Supervisor,
|
Localiser.Localisation.Filter.Supervisor,
|
||||||
Localiser.Localisation.Floor.Supervisor
|
Localiser.Localisation.Floor.Supervisor
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,19 +11,36 @@ defmodule Localiser.Domain.Floors do
|
|||||||
def get_floor!(id), do: Repo.get!(Floor, id)
|
def get_floor!(id), do: Repo.get!(Floor, id)
|
||||||
|
|
||||||
def create_floor(attrs) do
|
def create_floor(attrs) do
|
||||||
%Floor{}
|
case %Floor{} |> Floor.changeset(attrs) |> Repo.insert() do
|
||||||
|> Floor.changeset(attrs)
|
{:ok, floor} ->
|
||||||
|> Repo.insert()
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "floors", {:floor_created, floor})
|
||||||
|
{:ok, floor}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_floor(%Floor{} = floor, attrs) do
|
def update_floor(%Floor{} = floor, attrs) do
|
||||||
floor
|
case floor |> Floor.changeset(attrs) |> Repo.update() do
|
||||||
|> Floor.changeset(attrs)
|
{:ok, floor} ->
|
||||||
|> Repo.update()
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "floors", {:floor_updated, floor})
|
||||||
|
{:ok, floor}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_floor(%Floor{} = floor) do
|
def delete_floor(%Floor{} = floor) do
|
||||||
Repo.delete(floor)
|
case Repo.delete(floor) do
|
||||||
|
{:ok, deleted} ->
|
||||||
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "floors", {:floor_deleted, deleted.id})
|
||||||
|
{:ok, deleted}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_floors_with_rooms do
|
def list_floors_with_rooms do
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ defmodule Localiser.Domain.Rooms do
|
|||||||
|
|
||||||
alias Localiser.Repo
|
alias Localiser.Repo
|
||||||
alias Localiser.Domain.Schema.Room
|
alias Localiser.Domain.Schema.Room
|
||||||
|
alias Localiser.Domain.Schema.Sensor
|
||||||
|
|
||||||
def list_rooms do
|
def list_rooms do
|
||||||
Repo.all(Room)
|
Repo.all(Room)
|
||||||
@@ -17,18 +18,41 @@ defmodule Localiser.Domain.Rooms do
|
|||||||
def get_room!(id), do: Repo.get!(Room, id)
|
def get_room!(id), do: Repo.get!(Room, id)
|
||||||
|
|
||||||
def create_room(attrs) do
|
def create_room(attrs) do
|
||||||
%Room{}
|
case %Room{} |> Room.changeset(attrs) |> Repo.insert() do
|
||||||
|> Room.changeset(attrs)
|
{:ok, room} ->
|
||||||
|> Repo.insert()
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "rooms", {:room_created, room})
|
||||||
|
{:ok, room}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_room(%Room{} = room, attrs) do
|
def update_room(%Room{} = room, attrs) do
|
||||||
room
|
case room |> Room.changeset(attrs) |> Repo.update() do
|
||||||
|> Room.changeset(attrs)
|
{:ok, room} ->
|
||||||
|> Repo.update()
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "rooms", {:room_updated, room})
|
||||||
|
{:ok, room}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_room(%Room{} = room) do
|
def delete_room(%Room{} = room) do
|
||||||
Repo.delete(room)
|
sensors = Repo.all(from s in Sensor, where: s.room_id == ^room.id)
|
||||||
|
|
||||||
|
case Repo.delete(room) do
|
||||||
|
{:ok, deleted} ->
|
||||||
|
Enum.each(sensors, fn s ->
|
||||||
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "sensors", {:sensor_unenrolled, s.sensor_id})
|
||||||
|
end)
|
||||||
|
|
||||||
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "rooms", {:room_deleted, deleted.id, deleted.floor_id})
|
||||||
|
{:ok, deleted}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,18 +13,28 @@ defmodule Localiser.Domain.Tags do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_tag(attrs) do
|
def create_tag(attrs) do
|
||||||
%Tag{}
|
case %Tag{} |> Tag.changeset(attrs) |> Repo.insert() do
|
||||||
|> Tag.changeset(attrs)
|
{:ok, tag} ->
|
||||||
|> Repo.insert()
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "tags", {:tag_enrolled, tag})
|
||||||
|
{:ok, tag}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_tag(%Tag{} = tag, attrs) do
|
def update_tag(%Tag{} = tag, attrs) do
|
||||||
tag
|
tag |> Tag.changeset(attrs) |> Repo.update()
|
||||||
|> Tag.changeset(attrs)
|
|
||||||
|> Repo.update()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_tag(%Tag{} = tag) do
|
def delete_tag(%Tag{} = tag) do
|
||||||
Repo.delete(tag)
|
case Repo.delete(tag) do
|
||||||
|
{:ok, deleted} ->
|
||||||
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "tags", {:tag_removed, deleted.tag_id})
|
||||||
|
{:ok, deleted}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
defmodule Localiser.Localisation.Floor.Manager do
|
||||||
|
@moduledoc """
|
||||||
|
Global GenServer that reacts to floor lifecycle events and drives Floor.Supervisor.
|
||||||
|
|
||||||
|
- {:floor_created, floor} → start a Floor.Server for the new floor
|
||||||
|
- {:floor_deleted, floor_id} → terminate the Floor.Server subtree
|
||||||
|
"""
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
alias Localiser.Localisation.Floor
|
||||||
|
|
||||||
|
def start_link(_args) do
|
||||||
|
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(:ok) do
|
||||||
|
Phoenix.PubSub.subscribe(Localiser.PubSub, "floors")
|
||||||
|
{:ok, :ok}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:floor_created, floor}, state) do
|
||||||
|
case Registry.lookup(Localiser.Registry, {:floor_server, floor.id}) do
|
||||||
|
[] -> Floor.Supervisor.start_floor_server(floor)
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:floor_deleted, floor_id}, state) do
|
||||||
|
case Registry.lookup(Localiser.Registry, {:floor_server, floor_id}) do
|
||||||
|
[{pid, _}] -> DynamicSupervisor.terminate_child(Floor.Supervisor, pid)
|
||||||
|
[] -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(_msg, state), do: {:noreply, state}
|
||||||
|
end
|
||||||
@@ -3,6 +3,7 @@ defmodule Localiser.Localisation.Floor.Server do
|
|||||||
|
|
||||||
alias Localiser.Localisation.Room
|
alias Localiser.Localisation.Room
|
||||||
alias Localiser.Localisation.Sensor
|
alias Localiser.Localisation.Sensor
|
||||||
|
|
||||||
def start_link(floor) do
|
def start_link(floor) do
|
||||||
Supervisor.start_link(__MODULE__, floor, name: via(floor.id))
|
Supervisor.start_link(__MODULE__, floor, name: via(floor.id))
|
||||||
end
|
end
|
||||||
@@ -15,6 +16,7 @@ defmodule Localiser.Localisation.Floor.Server do
|
|||||||
def init(floor) do
|
def init(floor) do
|
||||||
children = [
|
children = [
|
||||||
{Room.Supervisor, floor},
|
{Room.Supervisor, floor},
|
||||||
|
{Room.Manager, floor},
|
||||||
{Sensor.Supervisor, floor},
|
{Sensor.Supervisor, floor},
|
||||||
{Sensor.Manager, floor}
|
{Sensor.Manager, floor}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
defmodule Localiser.Localisation.Room.Manager do
|
||||||
|
@moduledoc """
|
||||||
|
Per-floor GenServer that reacts to room lifecycle events and drives Room.Supervisor.
|
||||||
|
|
||||||
|
- {:room_created, room} → start a Room.Server (if room belongs to this floor)
|
||||||
|
- {:room_deleted, room_id, floor_id} → terminate the Room.Server (if on this floor)
|
||||||
|
"""
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
alias Localiser.Localisation.Room
|
||||||
|
|
||||||
|
def start_link(floor) do
|
||||||
|
GenServer.start_link(__MODULE__, floor.id, name: via(floor.id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def via(floor_id) do
|
||||||
|
{:via, Registry, {Localiser.Registry, {:room_manager, floor_id}}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(floor_id) do
|
||||||
|
Phoenix.PubSub.subscribe(Localiser.PubSub, "rooms")
|
||||||
|
{:ok, %{floor_id: floor_id}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:room_created, %{floor_id: floor_id} = room}, %{floor_id: floor_id} = state) do
|
||||||
|
case Registry.lookup(Localiser.Registry, {:room, room.id}) do
|
||||||
|
[] -> Room.Supervisor.start_room_server(floor_id, room)
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:room_deleted, room_id, floor_id}, %{floor_id: floor_id} = state) do
|
||||||
|
case Registry.lookup(Localiser.Registry, {:room, room_id}) do
|
||||||
|
[{pid, _}] -> DynamicSupervisor.terminate_child(Room.Supervisor.via(floor_id), pid)
|
||||||
|
[] -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(_msg, state), do: {:noreply, state}
|
||||||
|
end
|
||||||
@@ -27,6 +27,8 @@ defmodule Localiser.Localisation.Room.Server do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(room) do
|
def init(room) do
|
||||||
|
Phoenix.PubSub.subscribe(@pubsub, "rooms")
|
||||||
|
|
||||||
state = %__MODULE__{
|
state = %__MODULE__{
|
||||||
id: room.id,
|
id: room.id,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
@@ -65,6 +67,19 @@ defmodule Localiser.Localisation.Room.Server do
|
|||||||
{:reply, state.occupants, state}
|
{:reply, state.occupants, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:room_updated, %{id: id} = room}, %{id: id} = state) do
|
||||||
|
{:noreply, %{state |
|
||||||
|
name: room.name,
|
||||||
|
offset_x: room.offset_x || 0.0,
|
||||||
|
offset_y: room.offset_y || 0.0,
|
||||||
|
width: room.width || 0.0,
|
||||||
|
height: room.height || 0.0
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(_msg, state), do: {:noreply, state}
|
||||||
|
|
||||||
defp broadcast(room_id, occupants) do
|
defp broadcast(room_id, occupants) do
|
||||||
Phoenix.PubSub.broadcast(
|
Phoenix.PubSub.broadcast(
|
||||||
@pubsub,
|
@pubsub,
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ defmodule Localiser.Localisation.Tag.Filter do
|
|||||||
rooms = load_rooms()
|
rooms = load_rooms()
|
||||||
{:ok, filter_state} = filter_module.init([], [])
|
{:ok, filter_state} = filter_module.init([], [])
|
||||||
|
|
||||||
|
Phoenix.PubSub.subscribe(Localiser.PubSub, "rooms")
|
||||||
|
|
||||||
state = %__MODULE__{
|
state = %__MODULE__{
|
||||||
tag_id: tag.tag_id,
|
tag_id: tag.tag_id,
|
||||||
filter_module: filter_module,
|
filter_module: filter_module,
|
||||||
@@ -77,6 +79,18 @@ defmodule Localiser.Localisation.Tag.Filter do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Room geometry changed — reload the rooms cache so containment checks stay accurate.
|
||||||
|
@impl true
|
||||||
|
def handle_info({event, _}, state) when event in [:room_created, :room_updated] do
|
||||||
|
{:noreply, %{state | rooms: load_rooms()}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:room_deleted, _room_id, _floor_id}, state) do
|
||||||
|
{:noreply, %{state | rooms: load_rooms()}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(_msg, state), do: {:noreply, state}
|
||||||
|
|
||||||
defp load_rooms do
|
defp load_rooms do
|
||||||
Floors.list_floors_with_rooms()
|
Floors.list_floors_with_rooms()
|
||||||
|> Enum.flat_map(& &1.rooms)
|
|> Enum.flat_map(& &1.rooms)
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
defmodule Localiser.Localisation.Tag.Manager do
|
||||||
|
@moduledoc """
|
||||||
|
Global GenServer that reacts to tag lifecycle events and drives Filter.Supervisor.
|
||||||
|
|
||||||
|
- {:tag_enrolled, tag} → start a Tag.Filter for the new tag
|
||||||
|
- {:tag_removed, tag_id} → terminate the Tag.Filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
alias Localiser.Localisation.Filter
|
||||||
|
|
||||||
|
def start_link(_args) do
|
||||||
|
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(:ok) do
|
||||||
|
Phoenix.PubSub.subscribe(Localiser.PubSub, "tags")
|
||||||
|
{:ok, :ok}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:tag_enrolled, tag}, state) do
|
||||||
|
case Registry.lookup(Localiser.Registry, {:filter, tag.tag_id}) do
|
||||||
|
[] -> Filter.Supervisor.start_tag_filter(tag)
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:tag_removed, tag_id}, state) do
|
||||||
|
case Registry.lookup(Localiser.Registry, {:filter, tag_id}) do
|
||||||
|
[{pid, _}] -> DynamicSupervisor.terminate_child(Filter.Supervisor, pid)
|
||||||
|
[] -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(_msg, state), do: {:noreply, state}
|
||||||
|
end
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
defmodule Localiser.RSSI.Buffer do
|
defmodule Localiser.RSSI.Buffer do
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
alias Localiser.Localisation
|
|
||||||
alias Localiser.Localisation.Tag.Filter, as: TagFilter
|
alias Localiser.Localisation.Tag.Filter, as: TagFilter
|
||||||
alias Localiser.Localisation.Sensor.Server, as: SensorServer
|
alias Localiser.Localisation.Sensor.Server, as: SensorServer
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user