feat: accept calibration readings only from a chosen tag
This commit is contained in:
@@ -14,8 +14,11 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
|
|
||||||
# mode:
|
# mode:
|
||||||
# :ok
|
# :ok
|
||||||
# {:calibration_mode, completed_stages}
|
# {:calibration_mode, tag_id :: String.t() | nil, completed_stages}
|
||||||
# {:calibrating_stage, distance, samples, completed_stages}
|
# nil -> scanning: all incoming (tag_id, rssi) pairs are advertised on the channel
|
||||||
|
# tag -> tag committed, awaiting stage start
|
||||||
|
# {:calibrating_stage, tag_id, distance, samples, completed_stages}
|
||||||
|
# only readings from tag_id are accumulated; others are dropped
|
||||||
#
|
#
|
||||||
# completed_stages :: [%{distance: float, mean_rssi: float, readings: [{rssi, is_outlier}]}]
|
# completed_stages :: [%{distance: float, mean_rssi: float, readings: [{rssi, is_outlier}]}]
|
||||||
defstruct [:sensor_id, :sensor_db_id, :floor_x, :floor_y, :rssi_ref, :path_loss_exp, mode: :ok]
|
defstruct [:sensor_id, :sensor_db_id, :floor_x, :floor_y, :rssi_ref, :path_loss_exp, mode: :ok]
|
||||||
@@ -32,9 +35,9 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
GenServer.call(via(sensor_id), {:measure, rssi, tx_power})
|
GenServer.call(via(sensor_id), {:measure, rssi, tx_power})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true only when a stage is actively collecting (used by RSSI.Buffer).
|
# Returns true when in any calibration state (used by RSSI.Buffer to redirect readings).
|
||||||
def calibrating?(sensor_id) do
|
def in_calibration_mode?(sensor_id) do
|
||||||
GenServer.call(via(sensor_id), :calibrating?)
|
GenServer.call(via(sensor_id), :in_calibration_mode?)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a JSON-serialisable snapshot of the current calibration state for channel join.
|
# Returns a JSON-serialisable snapshot of the current calibration state for channel join.
|
||||||
@@ -42,23 +45,25 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
GenServer.call(via(sensor_id), :calibration_state)
|
GenServer.call(via(sensor_id), :calibration_state)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Feeds a raw RSSI value into the active stage buffer. Ignored between stages.
|
# Puts the sensor into scanning mode. RSSI readings for all tags are advertised on the
|
||||||
def calibration_reading(sensor_id, rssi) do
|
# calibration channel so the user can identify and select a tag.
|
||||||
GenServer.cast(via(sensor_id), {:calibration_reading, rssi})
|
|
||||||
end
|
|
||||||
|
|
||||||
# Puts the sensor into calibration mode (between-stages). Returns :ok.
|
|
||||||
def begin_calibration_mode(sensor_id) do
|
def begin_calibration_mode(sensor_id) do
|
||||||
GenServer.call(via(sensor_id), :begin_calibration_mode)
|
GenServer.call(via(sensor_id), :begin_calibration_mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Starts collecting samples for a given distance. Returns {:ok, samples_needed} or {:error, reason}.
|
# Commits to collecting samples only from tag_id in subsequent stages.
|
||||||
|
# Can be called again to change the tag (resets completed stages).
|
||||||
|
def set_calibration_tag(sensor_id, tag_id) do
|
||||||
|
GenServer.call(via(sensor_id), {:set_calibration_tag, tag_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
# Starts collecting samples for a given distance. Requires a tag to be committed first.
|
||||||
|
# Returns {:ok, samples_needed} or {:error, reason}.
|
||||||
def start_stage(sensor_id, distance) do
|
def start_stage(sensor_id, distance) do
|
||||||
GenServer.call(via(sensor_id), {:start_stage, distance})
|
GenServer.call(via(sensor_id), {:start_stage, distance})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Runs OLS regression over completed stages and saves the result. Requires >= 2 stages.
|
# Runs OLS regression over completed stages and saves the result. Requires >= 2 stages.
|
||||||
# Returns {:ok, %{rssi_ref: integer, path_loss_exp: float}} or {:error, reason}.
|
|
||||||
def finish_calibration(sensor_id) do
|
def finish_calibration(sensor_id) do
|
||||||
GenServer.call(via(sensor_id), :finish_calibration)
|
GenServer.call(via(sensor_id), :finish_calibration)
|
||||||
end
|
end
|
||||||
@@ -68,6 +73,11 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
GenServer.cast(via(sensor_id), :abort_calibration)
|
GenServer.cast(via(sensor_id), :abort_calibration)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Called by RSSI.Buffer for every reading when the sensor is in any calibration state.
|
||||||
|
def calibration_reading(sensor_id, tag_id, rssi) do
|
||||||
|
GenServer.cast(via(sensor_id), {:calibration_reading, tag_id, rssi})
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init({sensor, room}) do
|
def init({sensor, room}) do
|
||||||
Phoenix.PubSub.subscribe(Localiser.PubSub, "sensors")
|
Phoenix.PubSub.subscribe(Localiser.PubSub, "sensors")
|
||||||
@@ -104,8 +114,8 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call(:calibrating?, _from, state) do
|
def handle_call(:in_calibration_mode?, _from, state) do
|
||||||
{:reply, match?({:calibrating_stage, _, _, _}, state.mode), state}
|
{:reply, state.mode != :ok, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@@ -114,19 +124,29 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
:ok ->
|
:ok ->
|
||||||
%{status: "idle"}
|
%{status: "idle"}
|
||||||
|
|
||||||
{:calibration_mode, completed} ->
|
{:calibration_mode, nil, _completed} ->
|
||||||
|
%{
|
||||||
|
status: "scanning",
|
||||||
|
tag_id: nil,
|
||||||
|
samples_needed: samples_needed(),
|
||||||
|
completed_stages: []
|
||||||
|
}
|
||||||
|
|
||||||
|
{:calibration_mode, tag_id, completed} ->
|
||||||
%{
|
%{
|
||||||
status: "calibration_mode",
|
status: "calibration_mode",
|
||||||
|
tag_id: tag_id,
|
||||||
samples_needed: samples_needed(),
|
samples_needed: samples_needed(),
|
||||||
completed_stages: Enum.map(completed, &render_stage/1)
|
completed_stages: Enum.map(completed, &render_stage/1)
|
||||||
}
|
}
|
||||||
|
|
||||||
{:calibrating_stage, distance, samples, completed} ->
|
{:calibrating_stage, tag_id, distance, samples, completed} ->
|
||||||
%{
|
%{
|
||||||
status: "stage_active",
|
status: "stage_active",
|
||||||
|
tag_id: tag_id,
|
||||||
distance: distance,
|
distance: distance,
|
||||||
samples_needed: samples_needed(),
|
samples_needed: samples_needed(),
|
||||||
stage_progress: {length(samples), samples_needed()},
|
stage_progress: %{current: length(samples), total: samples_needed()},
|
||||||
completed_stages: Enum.map(completed, &render_stage/1)
|
completed_stages: Enum.map(completed, &render_stage/1)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -138,26 +158,44 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
def handle_call(:begin_calibration_mode, _from, state) do
|
def handle_call(:begin_calibration_mode, _from, state) do
|
||||||
MQTTConnection.publish("localiser/sensor/#{state.sensor_id}/cmd", ~s({"action":"calibrate_start"}))
|
MQTTConnection.publish("localiser/sensor/#{state.sensor_id}/cmd", ~s({"action":"calibrate_start"}))
|
||||||
broadcast_calibration(state.sensor_id, {:calibration_mode_entered, state.sensor_id, samples_needed()})
|
broadcast_calibration(state.sensor_id, {:calibration_mode_entered, state.sensor_id, samples_needed()})
|
||||||
{:reply, :ok, %{state | mode: {:calibration_mode, []}}}
|
{:reply, :ok, %{state | mode: {:calibration_mode, nil, []}}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call({:start_stage, _distance}, _from, %{mode: {:calibrating_stage, _, _, _}} = state) do
|
def handle_call({:set_calibration_tag, tag_id}, _from, %{mode: {:calibration_mode, _old_tag, _completed}} = state) do
|
||||||
|
broadcast_calibration(state.sensor_id, {:calibration_tag_set, state.sensor_id, tag_id})
|
||||||
|
{:reply, :ok, %{state | mode: {:calibration_mode, tag_id, []}}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:set_calibration_tag, _tag_id}, _from, %{mode: {:calibrating_stage, _, _, _, _}} = state) do
|
||||||
|
{:reply, {:error, :stage_active}, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:set_calibration_tag, _tag_id}, _from, state) do
|
||||||
|
{:reply, {:error, :not_in_calibration_mode}, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:start_stage, _distance}, _from, %{mode: {:calibrating_stage, _, _, _, _}} = state) do
|
||||||
{:reply, {:error, :already_active}, state}
|
{:reply, {:error, :already_active}, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_call({:start_stage, _distance}, _from, %{mode: {:calibration_mode, nil, _}} = state) do
|
||||||
|
{:reply, {:error, :no_tag_selected}, state}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_call({:start_stage, _distance}, _from, %{mode: :ok} = state) do
|
def handle_call({:start_stage, _distance}, _from, %{mode: :ok} = state) do
|
||||||
{:reply, {:error, :not_in_calibration_mode}, state}
|
{:reply, {:error, :not_in_calibration_mode}, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call({:start_stage, distance}, _from, %{mode: {:calibration_mode, completed}} = state) do
|
def handle_call({:start_stage, distance}, _from, %{mode: {:calibration_mode, tag_id, completed}} = state) do
|
||||||
n = samples_needed()
|
n = samples_needed()
|
||||||
broadcast_calibration(state.sensor_id, {:stage_started, state.sensor_id, distance, length(completed)})
|
broadcast_calibration(state.sensor_id, {:stage_started, state.sensor_id, distance, length(completed)})
|
||||||
{:reply, {:ok, n}, %{state | mode: {:calibrating_stage, distance, [], completed}}}
|
{:reply, {:ok, n}, %{state | mode: {:calibrating_stage, tag_id, distance, [], completed}}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call(:finish_calibration, _from, %{mode: {:calibration_mode, completed}} = state)
|
def handle_call(:finish_calibration, _from, %{mode: {:calibration_mode, _tag_id, completed}} = state)
|
||||||
when length(completed) >= 2 do
|
when length(completed) >= 2 do
|
||||||
case Calibration.least_squares(completed) do
|
case Calibration.least_squares(completed) do
|
||||||
{:ok, {rssi_ref, path_loss_exp}} ->
|
{:ok, {rssi_ref, path_loss_exp}} ->
|
||||||
@@ -186,11 +224,11 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call(:finish_calibration, _from, %{mode: {:calibration_mode, _}} = state) do
|
def handle_call(:finish_calibration, _from, %{mode: {:calibration_mode, _, _}} = state) do
|
||||||
{:reply, {:error, :insufficient_stages}, state}
|
{:reply, {:error, :insufficient_stages}, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call(:finish_calibration, _from, %{mode: {:calibrating_stage, _, _, _}} = state) do
|
def handle_call(:finish_calibration, _from, %{mode: {:calibrating_stage, _, _, _, _}} = state) do
|
||||||
{:reply, {:error, :stage_active}, state}
|
{:reply, {:error, :stage_active}, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -199,49 +237,53 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_cast(:abort_calibration, state) do
|
def handle_cast(:abort_calibration, %{mode: :ok} = state), do: {:noreply, state}
|
||||||
case state.mode do
|
|
||||||
:ok ->
|
|
||||||
{:noreply, state}
|
|
||||||
|
|
||||||
_ ->
|
def handle_cast(:abort_calibration, state) do
|
||||||
MQTTConnection.publish("localiser/sensor/#{state.sensor_id}/cmd", ~s({"action":"calibrate_stop"}))
|
MQTTConnection.publish("localiser/sensor/#{state.sensor_id}/cmd", ~s({"action":"calibrate_stop"}))
|
||||||
broadcast_calibration(state.sensor_id, {:calibration_cancelled, state.sensor_id})
|
broadcast_calibration(state.sensor_id, {:calibration_cancelled, state.sensor_id})
|
||||||
{:noreply, %{state | mode: :ok}}
|
{:noreply, %{state | mode: :ok}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Scan mode: tag not yet committed - advertise all (tag_id, rssi) pairs for UI selection.
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:calibration_reading, tag_id, rssi}, %{mode: {:calibration_mode, nil, _}} = state) do
|
||||||
|
broadcast_calibration(state.sensor_id, {:calibration_scan_reading, state.sensor_id, tag_id, rssi})
|
||||||
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
# Tag committed but no stage active - drop readings silently.
|
||||||
def handle_cast({:calibration_reading, rssi}, %{mode: {:calibrating_stage, distance, samples, completed}} = state) do
|
def handle_cast({:calibration_reading, _tag_id, _rssi}, %{mode: {:calibration_mode, _tag, _}} = state) do
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Stage active: only accumulate readings from the committed tag.
|
||||||
|
def handle_cast({:calibration_reading, tag_id, rssi}, %{mode: {:calibrating_stage, tag_id, distance, samples, completed}} = state) do
|
||||||
n = samples_needed()
|
n = samples_needed()
|
||||||
is_outlier = Calibration.outlier?(rssi, samples)
|
is_outlier = Calibration.outlier?(rssi, samples)
|
||||||
new_samples = [rssi | samples]
|
new_samples = [rssi | samples]
|
||||||
progress = {length(new_samples), n}
|
|
||||||
|
|
||||||
broadcast_calibration(state.sensor_id, {:calibration_reading, state.sensor_id, rssi, is_outlier, %{stage: progress}})
|
broadcast_calibration(state.sensor_id, {:calibration_reading, state.sensor_id, rssi, is_outlier, %{stage: {length(new_samples), n}}})
|
||||||
|
|
||||||
if length(new_samples) >= n do
|
if length(new_samples) >= n do
|
||||||
classified = Calibration.classify_outliers(new_samples)
|
classified = Calibration.classify_outliers(new_samples)
|
||||||
clean = for {r, false} <- classified, do: r
|
clean = for {r, false} <- classified, do: r
|
||||||
|
|
||||||
mean_rssi = if clean == [] do
|
mean_rssi = if clean == [], do: mean(new_samples), else: mean(clean)
|
||||||
mean(new_samples)
|
|
||||||
else
|
|
||||||
mean(clean)
|
|
||||||
end
|
|
||||||
|
|
||||||
stage = %{distance: distance, mean_rssi: mean_rssi, readings: classified}
|
stage = %{distance: distance, mean_rssi: mean_rssi, readings: classified}
|
||||||
new_completed = [stage | Enum.reject(completed, &(&1.distance == distance))]
|
new_completed = [stage | Enum.reject(completed, &(&1.distance == distance))]
|
||||||
|
|
||||||
broadcast_calibration(state.sensor_id, {:stage_complete, state.sensor_id, distance, classified, mean_rssi})
|
broadcast_calibration(state.sensor_id, {:stage_complete, state.sensor_id, distance, classified, mean_rssi})
|
||||||
|
|
||||||
{:noreply, %{state | mode: {:calibration_mode, new_completed}}}
|
{:noreply, %{state | mode: {:calibration_mode, tag_id, new_completed}}}
|
||||||
else
|
else
|
||||||
{:noreply, %{state | mode: {:calibrating_stage, distance, new_samples, completed}}}
|
{:noreply, %{state | mode: {:calibrating_stage, tag_id, distance, new_samples, completed}}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_cast({:calibration_reading, _rssi}, state), do: {:noreply, state}
|
# Reading from a different tag while stage active - drop.
|
||||||
|
def handle_cast({:calibration_reading, _other_tag, _rssi}, state), do: {:noreply, state}
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:sensor_enrolled, %Sensor{sensor_id: sid} = sensor, room}, %{sensor_id: sid} = state) do
|
def handle_info({:sensor_enrolled, %Sensor{sensor_id: sid} = sensor, room}, %{sensor_id: sid} = state) do
|
||||||
@@ -266,9 +308,7 @@ defmodule Localiser.Localisation.Sensor.Server do
|
|||||||
Application.get_env(:localiser, :calibration_samples, @default_samples)
|
Application.get_env(:localiser, :calibration_samples, @default_samples)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp mean(list) do
|
defp mean(list), do: Enum.sum(list) / length(list)
|
||||||
Enum.sum(list) / length(list)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp rssi_to_distance(rssi, rssi_ref, path_loss_exp) do
|
defp rssi_to_distance(rssi, rssi_ref, path_loss_exp) do
|
||||||
:math.pow(10.0, (rssi_ref - rssi) / (10.0 * path_loss_exp))
|
:math.pow(10.0, (rssi_ref - rssi) / (10.0 * path_loss_exp))
|
||||||
|
|||||||
@@ -35,26 +35,27 @@ defmodule Localiser.RSSI.Buffer do
|
|||||||
|
|
||||||
defp flush_batches(batches) do
|
defp flush_batches(batches) do
|
||||||
Enum.each(batches, fn {tag_id, readings} ->
|
Enum.each(batches, fn {tag_id, readings} ->
|
||||||
case Registry.lookup(Localiser.Registry, {:filter, tag_id}) do
|
# Calibration routing runs regardless of whether the tag has a filter registered,
|
||||||
[{_pid, _}] ->
|
# so scan-mode readings from unknown tags still reach the sensor server.
|
||||||
measurements = Enum.flat_map(readings, &resolve_measurement/1)
|
normal = Enum.flat_map(readings, &route_reading(tag_id, &1))
|
||||||
if measurements != [], do: TagFilter.ingest(tag_id, measurements)
|
|
||||||
|
|
||||||
[] ->
|
if normal != [] do
|
||||||
:ok
|
case Registry.lookup(Localiser.Registry, {:filter, tag_id}) do
|
||||||
|
[{_pid, _}] -> TagFilter.ingest(tag_id, normal)
|
||||||
|
[] -> :ok
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Resolves a raw RSSI reading to a measurement with sensor location and distance.
|
# Routes one reading. If the sensor is in any calibration state, forwards to Sensor.Server
|
||||||
# If the sensor is in calibration mode, feeds the reading to Sensor.Server instead
|
# (with tag_id so scan-mode can advertise it) and returns [] to exclude from Tag.Filter.
|
||||||
# and returns [] so the sample is excluded from Tag.Filter measurements.
|
# Otherwise returns a normal measurement map.
|
||||||
# If the sensor server isn't running, returns [].
|
defp route_reading(tag_id, %{sensor_id: sensor_id, rssi: rssi, tx_power: tx_power}) do
|
||||||
defp resolve_measurement(%{sensor_id: sensor_id, rssi: rssi, tx_power: tx_power}) do
|
|
||||||
case Registry.lookup(Localiser.Registry, {:sensor, sensor_id}) do
|
case Registry.lookup(Localiser.Registry, {:sensor, sensor_id}) do
|
||||||
[{_pid, _}] ->
|
[{_pid, _}] ->
|
||||||
if SensorServer.calibrating?(sensor_id) do
|
if SensorServer.in_calibration_mode?(sensor_id) do
|
||||||
SensorServer.calibration_reading(sensor_id, rssi)
|
SensorServer.calibration_reading(sensor_id, tag_id, rssi)
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
[SensorServer.measure(sensor_id, rssi, tx_power)]
|
[SensorServer.measure(sensor_id, rssi, tx_power)]
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ defmodule Localiser.Web.Channels.CalibrationChannel do
|
|||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({:calibration_scan_reading, _sensor_id, tag_id, rssi}, socket) do
|
||||||
|
push(socket, "scan_reading", %{tag_id: tag_id, rssi: rssi})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:calibration_tag_set, _sensor_id, tag_id}, socket) do
|
||||||
|
push(socket, "tag_set", %{tag_id: tag_id})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info({:stage_started, _sensor_id, distance, completed_count}, socket) do
|
def handle_info({:stage_started, _sensor_id, distance, completed_count}, socket) do
|
||||||
push(socket, "stage_started", %{distance: distance, completed_stages: completed_count})
|
push(socket, "stage_started", %{distance: distance, completed_stages: completed_count})
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|||||||
@@ -116,6 +116,16 @@ defmodule Localiser.Web.Controllers.SensorController do
|
|||||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
operation :calibration_set_tag,
|
||||||
|
summary: "Commit to a specific tag for calibration stages",
|
||||||
|
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||||
|
request_body: {"Tag params", "application/json", Schemas.CalibrationTagParams, required: true},
|
||||||
|
responses: [
|
||||||
|
ok: {"Tag committed", "application/json", Schemas.CalibrationBeginResponse},
|
||||||
|
unprocessable_entity: {"Stage active or not in calibration mode", "application/json", Schemas.Error},
|
||||||
|
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||||
|
]
|
||||||
|
|
||||||
operation :calibration_stage_start,
|
operation :calibration_stage_start,
|
||||||
summary: "Start collecting samples for a specific distance",
|
summary: "Start collecting samples for a specific distance",
|
||||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||||
@@ -276,6 +286,31 @@ defmodule Localiser.Web.Controllers.SensorController do
|
|||||||
json(conn, %{status: "calibration_mode", samples_needed: calibration_samples_needed()})
|
json(conn, %{status: "calibration_mode", samples_needed: calibration_samples_needed()})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def calibration_set_tag(conn, %{"id" => id, "tag_id" => tag_id}) when is_binary(tag_id) do
|
||||||
|
sensor = Sensors.get_sensor!(id)
|
||||||
|
|
||||||
|
case SensorServer.set_calibration_tag(sensor.sensor_id, tag_id) do
|
||||||
|
:ok ->
|
||||||
|
json(conn, %{status: "calibration_mode", samples_needed: calibration_samples_needed()})
|
||||||
|
|
||||||
|
{:error, :stage_active} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> json(%{error: "cannot change tag while a stage is active"})
|
||||||
|
|
||||||
|
{:error, :not_in_calibration_mode} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> json(%{error: "sensor is not in calibration mode"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def calibration_set_tag(conn, _params) do
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "tag_id is required"})
|
||||||
|
end
|
||||||
|
|
||||||
def calibration_stage_start(conn, %{"id" => id, "distance" => distance})
|
def calibration_stage_start(conn, %{"id" => id, "distance" => distance})
|
||||||
when is_number(distance) and distance > 0 do
|
when is_number(distance) and distance > 0 do
|
||||||
sensor = Sensors.get_sensor!(id)
|
sensor = Sensors.get_sensor!(id)
|
||||||
@@ -293,6 +328,11 @@ defmodule Localiser.Web.Controllers.SensorController do
|
|||||||
conn
|
conn
|
||||||
|> put_status(:unprocessable_entity)
|
|> put_status(:unprocessable_entity)
|
||||||
|> json(%{error: "sensor is not in calibration mode"})
|
|> json(%{error: "sensor is not in calibration mode"})
|
||||||
|
|
||||||
|
{:error, :no_tag_selected} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> json(%{error: "no tag selected — call /calibration/tag first"})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ defmodule Localiser.Web.Router do
|
|||||||
post "/sensors/:id/factory_reset", SensorController, :factory_reset
|
post "/sensors/:id/factory_reset", SensorController, :factory_reset
|
||||||
post "/sensors/:id/reconfigure", SensorController, :reconfigure
|
post "/sensors/:id/reconfigure", SensorController, :reconfigure
|
||||||
post "/sensors/:id/calibration/begin", SensorController, :calibration_begin
|
post "/sensors/:id/calibration/begin", SensorController, :calibration_begin
|
||||||
|
post "/sensors/:id/calibration/tag", SensorController, :calibration_set_tag
|
||||||
post "/sensors/:id/calibration/stage", SensorController, :calibration_stage_start
|
post "/sensors/:id/calibration/stage", SensorController, :calibration_stage_start
|
||||||
post "/sensors/:id/calibration/finish", SensorController, :calibration_finish
|
post "/sensors/:id/calibration/finish", SensorController, :calibration_finish
|
||||||
delete "/sensors/:id/calibration", SensorController, :calibration_cancel
|
delete "/sensors/:id/calibration", SensorController, :calibration_cancel
|
||||||
|
|||||||
@@ -330,6 +330,17 @@ defmodule Localiser.Web.Schemas do
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defmodule CalibrationTagParams do
|
||||||
|
require OpenApiSpex
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "CalibrationTagParams", type: :object,
|
||||||
|
properties: %{
|
||||||
|
tag_id: %Schema{type: :string, description: "Tag ID to lock in for calibration stages"}
|
||||||
|
},
|
||||||
|
required: [:tag_id]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
defmodule CalibrationStageParams do
|
defmodule CalibrationStageParams do
|
||||||
require OpenApiSpex
|
require OpenApiSpex
|
||||||
OpenApiSpex.schema(%{
|
OpenApiSpex.schema(%{
|
||||||
|
|||||||
Reference in New Issue
Block a user