feat: wait for enrolled sensors to announce, factory reset them on deletion
This commit is contained in:
@@ -18,6 +18,8 @@ config :localiserd, Localiser.Web.Endpoint,
|
|||||||
config :localiserd, :jwt_secret,
|
config :localiserd, :jwt_secret,
|
||||||
System.get_env("JWT_SECRET") || "localiser_dev_jwt_secret_change_in_prod!!"
|
System.get_env("JWT_SECRET") || "localiser_dev_jwt_secret_change_in_prod!!"
|
||||||
|
|
||||||
|
config :localiser, enrollment_timeout_ms: 10_000
|
||||||
|
|
||||||
config :mdns_lite,
|
config :mdns_lite,
|
||||||
hosts: ["localiser"],
|
hosts: ["localiser"],
|
||||||
ttl: 120,
|
ttl: 120,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ defmodule Localiser.Domain.Schema.Sensor do
|
|||||||
|
|
||||||
schema "sensors" do
|
schema "sensors" do
|
||||||
field :sensor_id, :string
|
field :sensor_id, :string
|
||||||
|
field :confirmed, :boolean, default: false
|
||||||
field :x, :float
|
field :x, :float
|
||||||
field :y, :float
|
field :y, :float
|
||||||
field :floor_x, :float, virtual: true
|
field :floor_x, :float, virtual: true
|
||||||
@@ -21,7 +22,7 @@ defmodule Localiser.Domain.Schema.Sensor do
|
|||||||
@doc false
|
@doc false
|
||||||
def changeset(sensor, attrs) do
|
def changeset(sensor, attrs) do
|
||||||
sensor
|
sensor
|
||||||
|> cast(attrs, [:sensor_id, :room_id, :x, :y])
|
|> cast(attrs, [:sensor_id, :confirmed, :room_id, :x, :y])
|
||||||
|> validate_required([:sensor_id])
|
|> validate_required([:sensor_id])
|
||||||
|> unique_constraint(:sensor_id)
|
|> unique_constraint(:sensor_id)
|
||||||
|> assoc_constraint(:room)
|
|> assoc_constraint(:room)
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ defmodule Localiser.Domain.Sensors do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete_sensor(%Sensor{} = sensor) do
|
def delete_sensor(%Sensor{} = sensor) do
|
||||||
|
topic = "localiser/sensor/#{sensor.sensor_id}/cmd"
|
||||||
|
Localiser.MQTT.Connection.publish(topic, Jason.encode!(%{command: "factory_reset"}))
|
||||||
Repo.delete(sensor)
|
Repo.delete(sensor)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -89,14 +91,45 @@ defmodule Localiser.Domain.Sensors do
|
|||||||
|
|
||||||
# Sensor lifecycle / enrollment helpers
|
# Sensor lifecycle / enrollment helpers
|
||||||
|
|
||||||
|
@enrollment_timeout_ms Application.compile_env(:localiser, :enrollment_timeout_ms, 10_000)
|
||||||
|
|
||||||
|
# Called by the phone app to indicate a sensor is about to come online.
|
||||||
|
# Creates an unconfirmed sensor record and starts a timeout Task.
|
||||||
|
def expect_sensor(sensor_id) do
|
||||||
|
result =
|
||||||
|
%Sensor{}
|
||||||
|
|> Sensor.changeset(%{sensor_id: sensor_id, confirmed: false})
|
||||||
|
|> Repo.insert(
|
||||||
|
on_conflict: [set: [confirmed: false, updated_at: DateTime.utc_now()]],
|
||||||
|
conflict_target: :sensor_id,
|
||||||
|
returning: true
|
||||||
|
)
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:ok, sensor} ->
|
||||||
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "sensors", {:sensor_expected, sensor})
|
||||||
|
Task.start(fn ->
|
||||||
|
Process.sleep(@enrollment_timeout_ms)
|
||||||
|
fresh = Repo.get_by(Sensor, sensor_id: sensor_id)
|
||||||
|
if fresh && !fresh.confirmed do
|
||||||
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "sensors", {:sensor_enrollment_timeout, sensor_id})
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
{:ok, sensor}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Called when an ESP board self-announces on MQTT. Inserts a new unplaced sensor
|
# Called when an ESP board self-announces on MQTT. Inserts a new unplaced sensor
|
||||||
# record, or bumps updated_at if the sensor_id already exists.
|
# record, or marks it confirmed if the sensor_id already exists.
|
||||||
def upsert_announced(sensor_id) do
|
def upsert_announced(sensor_id) do
|
||||||
result =
|
result =
|
||||||
%Sensor{}
|
%Sensor{}
|
||||||
|> Sensor.changeset(%{sensor_id: sensor_id})
|
|> Sensor.changeset(%{sensor_id: sensor_id, confirmed: true})
|
||||||
|> Repo.insert(
|
|> Repo.insert(
|
||||||
on_conflict: [set: [updated_at: DateTime.utc_now()]],
|
on_conflict: [set: [confirmed: true, updated_at: DateTime.utc_now()]],
|
||||||
conflict_target: :sensor_id,
|
conflict_target: :sensor_id,
|
||||||
returning: true
|
returning: true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ defmodule Localiser.Web.Channels.SensorsChannel do
|
|||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({:sensor_expected, sensor}, socket) do
|
||||||
|
push(socket, "sensor_expected", render_sensor(sensor))
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:sensor_enrollment_timeout, sensor_id}, socket) do
|
||||||
|
push(socket, "sensor_enrollment_timeout", %{sensor_id: sensor_id})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info(_msg, socket), do: {:noreply, socket}
|
def handle_info(_msg, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
defp render_sensor(sensor) do
|
defp render_sensor(sensor) do
|
||||||
|
|||||||
@@ -68,6 +68,15 @@ defmodule Localiser.Web.Controllers.SensorController do
|
|||||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
operation :enroll,
|
||||||
|
summary: "Notify server a sensor is about to be enrolled",
|
||||||
|
request_body: {"Enroll params", "application/json", Schemas.SensorEnrollParams, required: true},
|
||||||
|
responses: [
|
||||||
|
ok: {"Sensor (unconfirmed)", "application/json", Schemas.Sensor},
|
||||||
|
bad_request: {"Missing sensor_id", "application/json", Schemas.Error},
|
||||||
|
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||||
|
]
|
||||||
|
|
||||||
operation :calibration_start,
|
operation :calibration_start,
|
||||||
summary: "Begin RSSI calibration",
|
summary: "Begin RSSI calibration",
|
||||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||||
@@ -86,6 +95,24 @@ defmodule Localiser.Web.Controllers.SensorController do
|
|||||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def enroll(conn, %{"sensor_id" => sensor_id}) do
|
||||||
|
case Sensors.expect_sensor(sensor_id) do
|
||||||
|
{:ok, sensor} ->
|
||||||
|
json(conn, render_sensor(sensor))
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> json(%{errors: format_errors(changeset)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enroll(conn, _params) do
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "sensor_id is required"})
|
||||||
|
end
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
json(conn, Enum.map(Sensors.list_sensors(), &render_sensor/1))
|
json(conn, Enum.map(Sensors.list_sensors(), &render_sensor/1))
|
||||||
end
|
end
|
||||||
@@ -176,6 +203,7 @@ defmodule Localiser.Web.Controllers.SensorController do
|
|||||||
%{
|
%{
|
||||||
id: sensor.id,
|
id: sensor.id,
|
||||||
sensor_id: sensor.sensor_id,
|
sensor_id: sensor.sensor_id,
|
||||||
|
confirmed: sensor.confirmed,
|
||||||
room_id: sensor.room_id,
|
room_id: sensor.room_id,
|
||||||
floor_x: sensor.floor_x,
|
floor_x: sensor.floor_x,
|
||||||
floor_y: sensor.floor_y,
|
floor_y: sensor.floor_y,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ defmodule Localiser.Web.Router do
|
|||||||
resources "/tags", TagController, except: [:new, :edit]
|
resources "/tags", TagController, except: [:new, :edit]
|
||||||
|
|
||||||
# Sensors
|
# Sensors
|
||||||
|
post "/sensors", SensorController, :enroll
|
||||||
get "/sensors", SensorController, :index
|
get "/sensors", SensorController, :index
|
||||||
get "/sensors/unplaced", SensorController, :unplaced
|
get "/sensors/unplaced", SensorController, :unplaced
|
||||||
get "/sensors/:id", SensorController, :show
|
get "/sensors/:id", SensorController, :show
|
||||||
|
|||||||
@@ -52,12 +52,13 @@ defmodule Localiser.Web.Schemas do
|
|||||||
properties: %{
|
properties: %{
|
||||||
id: %Schema{type: :integer},
|
id: %Schema{type: :integer},
|
||||||
sensor_id: %Schema{type: :string},
|
sensor_id: %Schema{type: :string},
|
||||||
|
confirmed: %Schema{type: :boolean},
|
||||||
room_id: %Schema{type: :integer, nullable: true},
|
room_id: %Schema{type: :integer, nullable: true},
|
||||||
floor_x: %Schema{type: :number, format: :float, nullable: true},
|
floor_x: %Schema{type: :number, format: :float, nullable: true},
|
||||||
floor_y: %Schema{type: :number, format: :float, nullable: true},
|
floor_y: %Schema{type: :number, format: :float, nullable: true},
|
||||||
rssi_ref: %Schema{type: :number, format: :float, nullable: true}
|
rssi_ref: %Schema{type: :number, format: :float, nullable: true}
|
||||||
},
|
},
|
||||||
required: [:id, :sensor_id]
|
required: [:id, :sensor_id, :confirmed]
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -236,6 +237,15 @@ defmodule Localiser.Web.Schemas do
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defmodule SensorEnrollParams do
|
||||||
|
require OpenApiSpex
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "SensorEnrollParams", type: :object,
|
||||||
|
properties: %{sensor_id: %Schema{type: :string}},
|
||||||
|
required: [:sensor_id]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
defmodule SensorUpdateParams do
|
defmodule SensorUpdateParams do
|
||||||
require OpenApiSpex
|
require OpenApiSpex
|
||||||
OpenApiSpex.schema(%{
|
OpenApiSpex.schema(%{
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
defmodule Localiser.Repo.Migrations.AddConfirmedToSensors do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:sensors) do
|
||||||
|
add :confirmed, :boolean, default: false, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user