From c3fd8b950c56d36157a51a990db5676779460d32 Mon Sep 17 00:00:00 2001 From: dvdrw Date: Thu, 16 Apr 2026 16:57:45 +0200 Subject: [PATCH] feat: dynamically manage floors/rooms/tags --- lib/localiser/application.ex | 2 + lib/localiser/domain/floors.ex | 31 +++++++++++--- lib/localiser/domain/rooms.ex | 38 ++++++++++++++--- lib/localiser/domain/tags.ex | 24 ++++++++--- lib/localiser/localisation/floor/manager.ex | 43 +++++++++++++++++++ lib/localiser/localisation/floor/server.ex | 2 + lib/localiser/localisation/room/manager.ex | 47 +++++++++++++++++++++ lib/localiser/localisation/room/server.ex | 15 +++++++ lib/localiser/localisation/tag/filter.ex | 14 ++++++ lib/localiser/localisation/tag/manager.ex | 43 +++++++++++++++++++ lib/localiser/rssi_buffer.ex | 1 - 11 files changed, 238 insertions(+), 22 deletions(-) create mode 100644 lib/localiser/localisation/floor/manager.ex create mode 100644 lib/localiser/localisation/room/manager.ex create mode 100644 lib/localiser/localisation/tag/manager.ex diff --git a/lib/localiser/application.ex b/lib/localiser/application.ex index 62ba46f..a5ea111 100644 --- a/lib/localiser/application.ex +++ b/lib/localiser/application.ex @@ -9,6 +9,8 @@ defmodule Localiser.Application do {Phoenix.PubSub, name: Localiser.PubSub}, Localiser.MQTT.Supervisor, Localiser.RSSI.Buffer, + Localiser.Localisation.Floor.Manager, + Localiser.Localisation.Tag.Manager, Localiser.Localisation.Filter.Supervisor, Localiser.Localisation.Floor.Supervisor ] diff --git a/lib/localiser/domain/floors.ex b/lib/localiser/domain/floors.ex index 5d3ff3d..ede7060 100644 --- a/lib/localiser/domain/floors.ex +++ b/lib/localiser/domain/floors.ex @@ -11,19 +11,36 @@ defmodule Localiser.Domain.Floors do def get_floor!(id), do: Repo.get!(Floor, id) def create_floor(attrs) do - %Floor{} - |> Floor.changeset(attrs) - |> Repo.insert() + case %Floor{} |> Floor.changeset(attrs) |> Repo.insert() do + {:ok, floor} -> + Phoenix.PubSub.broadcast(Localiser.PubSub, "floors", {:floor_created, floor}) + {:ok, floor} + + error -> + error + end end def update_floor(%Floor{} = floor, attrs) do - floor - |> Floor.changeset(attrs) - |> Repo.update() + case floor |> Floor.changeset(attrs) |> Repo.update() do + {:ok, floor} -> + Phoenix.PubSub.broadcast(Localiser.PubSub, "floors", {:floor_updated, floor}) + {:ok, floor} + + error -> + error + end end 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 def list_floors_with_rooms do diff --git a/lib/localiser/domain/rooms.ex b/lib/localiser/domain/rooms.ex index f3941e2..35cff87 100644 --- a/lib/localiser/domain/rooms.ex +++ b/lib/localiser/domain/rooms.ex @@ -3,6 +3,7 @@ defmodule Localiser.Domain.Rooms do alias Localiser.Repo alias Localiser.Domain.Schema.Room + alias Localiser.Domain.Schema.Sensor def list_rooms do Repo.all(Room) @@ -17,18 +18,41 @@ defmodule Localiser.Domain.Rooms do def get_room!(id), do: Repo.get!(Room, id) def create_room(attrs) do - %Room{} - |> Room.changeset(attrs) - |> Repo.insert() + case %Room{} |> Room.changeset(attrs) |> Repo.insert() do + {:ok, room} -> + Phoenix.PubSub.broadcast(Localiser.PubSub, "rooms", {:room_created, room}) + {:ok, room} + + error -> + error + end end def update_room(%Room{} = room, attrs) do - room - |> Room.changeset(attrs) - |> Repo.update() + case room |> Room.changeset(attrs) |> Repo.update() do + {:ok, room} -> + Phoenix.PubSub.broadcast(Localiser.PubSub, "rooms", {:room_updated, room}) + {:ok, room} + + error -> + error + end end 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 diff --git a/lib/localiser/domain/tags.ex b/lib/localiser/domain/tags.ex index 3b1955f..38bb82a 100644 --- a/lib/localiser/domain/tags.ex +++ b/lib/localiser/domain/tags.ex @@ -13,18 +13,28 @@ defmodule Localiser.Domain.Tags do end def create_tag(attrs) do - %Tag{} - |> Tag.changeset(attrs) - |> Repo.insert() + case %Tag{} |> Tag.changeset(attrs) |> Repo.insert() do + {:ok, tag} -> + Phoenix.PubSub.broadcast(Localiser.PubSub, "tags", {:tag_enrolled, tag}) + {:ok, tag} + + error -> + error + end end def update_tag(%Tag{} = tag, attrs) do - tag - |> Tag.changeset(attrs) - |> Repo.update() + tag |> Tag.changeset(attrs) |> Repo.update() end 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 diff --git a/lib/localiser/localisation/floor/manager.ex b/lib/localiser/localisation/floor/manager.ex new file mode 100644 index 0000000..85a0b32 --- /dev/null +++ b/lib/localiser/localisation/floor/manager.ex @@ -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 diff --git a/lib/localiser/localisation/floor/server.ex b/lib/localiser/localisation/floor/server.ex index 46b1490..52fb1b0 100644 --- a/lib/localiser/localisation/floor/server.ex +++ b/lib/localiser/localisation/floor/server.ex @@ -3,6 +3,7 @@ defmodule Localiser.Localisation.Floor.Server do alias Localiser.Localisation.Room alias Localiser.Localisation.Sensor + def start_link(floor) do Supervisor.start_link(__MODULE__, floor, name: via(floor.id)) end @@ -15,6 +16,7 @@ defmodule Localiser.Localisation.Floor.Server do def init(floor) do children = [ {Room.Supervisor, floor}, + {Room.Manager, floor}, {Sensor.Supervisor, floor}, {Sensor.Manager, floor} ] diff --git a/lib/localiser/localisation/room/manager.ex b/lib/localiser/localisation/room/manager.ex new file mode 100644 index 0000000..d56572a --- /dev/null +++ b/lib/localiser/localisation/room/manager.ex @@ -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 diff --git a/lib/localiser/localisation/room/server.ex b/lib/localiser/localisation/room/server.ex index 5ad836a..3a99cf6 100644 --- a/lib/localiser/localisation/room/server.ex +++ b/lib/localiser/localisation/room/server.ex @@ -27,6 +27,8 @@ defmodule Localiser.Localisation.Room.Server do @impl true def init(room) do + Phoenix.PubSub.subscribe(@pubsub, "rooms") + state = %__MODULE__{ id: room.id, name: room.name, @@ -65,6 +67,19 @@ defmodule Localiser.Localisation.Room.Server do {:reply, state.occupants, state} 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 Phoenix.PubSub.broadcast( @pubsub, diff --git a/lib/localiser/localisation/tag/filter.ex b/lib/localiser/localisation/tag/filter.ex index ab1efb1..fc09a5a 100644 --- a/lib/localiser/localisation/tag/filter.ex +++ b/lib/localiser/localisation/tag/filter.ex @@ -39,6 +39,8 @@ defmodule Localiser.Localisation.Tag.Filter do rooms = load_rooms() {:ok, filter_state} = filter_module.init([], []) + Phoenix.PubSub.subscribe(Localiser.PubSub, "rooms") + state = %__MODULE__{ tag_id: tag.tag_id, filter_module: filter_module, @@ -77,6 +79,18 @@ defmodule Localiser.Localisation.Tag.Filter do 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 Floors.list_floors_with_rooms() |> Enum.flat_map(& &1.rooms) diff --git a/lib/localiser/localisation/tag/manager.ex b/lib/localiser/localisation/tag/manager.ex new file mode 100644 index 0000000..3f20bde --- /dev/null +++ b/lib/localiser/localisation/tag/manager.ex @@ -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 diff --git a/lib/localiser/rssi_buffer.ex b/lib/localiser/rssi_buffer.ex index f207727..1bb0b85 100644 --- a/lib/localiser/rssi_buffer.ex +++ b/lib/localiser/rssi_buffer.ex @@ -1,7 +1,6 @@ defmodule Localiser.RSSI.Buffer do use GenServer - alias Localiser.Localisation alias Localiser.Localisation.Tag.Filter, as: TagFilter alias Localiser.Localisation.Sensor.Server, as: SensorServer