feat: use AprilTag instead of QR code
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
@moduledoc """
|
||||
LiveView for managing storage locations and QR codes.
|
||||
LiveView for managing storage locations and AprilTags.
|
||||
"""
|
||||
use ComponentsElixirWeb, :live_view
|
||||
|
||||
alias ComponentsElixir.{Inventory, Auth}
|
||||
alias ComponentsElixir.Inventory.StorageLocation
|
||||
alias ComponentsElixir.QRCode
|
||||
alias ComponentsElixir.AprilTag
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
@@ -24,8 +24,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
|> assign(:show_edit_form, false)
|
||||
|> assign(:editing_location, nil)
|
||||
|> assign(:form, nil)
|
||||
|> assign(:qr_scanner_open, false)
|
||||
|> assign(:scanned_codes, [])
|
||||
|> assign(:apriltag_scanner_open, false)
|
||||
|> assign(:scanned_tags, [])
|
||||
|> assign(:page_title, "Storage Location Management")}
|
||||
end
|
||||
end
|
||||
@@ -38,7 +38,9 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:show_add_form, true)
|
||||
|> assign(:form, form)}
|
||||
|> assign(:form, form)
|
||||
|> assign(:available_apriltag_ids, AprilTag.available_apriltag_ids())
|
||||
|> assign(:apriltag_mode, "auto")}
|
||||
end
|
||||
|
||||
def handle_event("hide_add_form", _params, socket) do
|
||||
@@ -64,7 +66,9 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
socket
|
||||
|> assign(:show_edit_form, true)
|
||||
|> assign(:editing_location, location)
|
||||
|> assign(:form, form)}
|
||||
|> assign(:form, form)
|
||||
|> assign(:available_apriltag_ids, AprilTag.available_apriltag_ids())
|
||||
|> assign(:edit_apriltag_mode, "keep")}
|
||||
end
|
||||
|
||||
def handle_event("hide_edit_form", _params, socket) do
|
||||
@@ -76,7 +80,32 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
end
|
||||
|
||||
def handle_event("save_location", %{"storage_location" => location_params}, socket) do
|
||||
case Inventory.create_storage_location(location_params) do
|
||||
# Process AprilTag assignment based on mode
|
||||
processed_params = case socket.assigns.apriltag_mode do
|
||||
"none" ->
|
||||
# Remove any apriltag_id from params to ensure it's nil
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
|
||||
"auto" ->
|
||||
# Auto-assign next available AprilTag ID
|
||||
case AprilTag.next_available_apriltag_id() do
|
||||
nil ->
|
||||
# No available IDs, proceed without AprilTag
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
apriltag_id ->
|
||||
Map.put(location_params, "apriltag_id", apriltag_id)
|
||||
end
|
||||
|
||||
"manual" ->
|
||||
# Use the manually entered apriltag_id (validation will be handled by changeset)
|
||||
location_params
|
||||
|
||||
_ ->
|
||||
# Fallback: remove apriltag_id
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
end
|
||||
|
||||
case Inventory.create_storage_location(processed_params) do
|
||||
{:ok, _location} ->
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -121,57 +150,86 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("open_qr_scanner", _params, socket) do
|
||||
{:noreply, assign(socket, :qr_scanner_open, true)}
|
||||
def handle_event("open_apriltag_scanner", _params, socket) do
|
||||
{:noreply, assign(socket, :apriltag_scanner_open, true)}
|
||||
end
|
||||
|
||||
def handle_event("close_qr_scanner", _params, socket) do
|
||||
{:noreply, assign(socket, :qr_scanner_open, false)}
|
||||
def handle_event("close_apriltag_scanner", _params, socket) do
|
||||
{:noreply, assign(socket, :apriltag_scanner_open, false)}
|
||||
end
|
||||
|
||||
def handle_event("qr_scanned", %{"code" => code}, socket) do
|
||||
case QRCode.parse_qr_data(code) do
|
||||
{:ok, parsed} ->
|
||||
case Inventory.get_storage_location_by_qr_code(parsed.code) do
|
||||
def handle_event("apriltag_scanned", %{"apriltag_id" => apriltag_id_str}, socket) do
|
||||
case Integer.parse(apriltag_id_str) do
|
||||
{apriltag_id, ""} when apriltag_id >= 0 and apriltag_id <= 586 ->
|
||||
case Inventory.get_storage_location_by_apriltag_id(apriltag_id) do
|
||||
nil ->
|
||||
{:noreply, put_flash(socket, :error, "Storage location not found for QR code: #{code}")}
|
||||
{:noreply, put_flash(socket, :error, "Storage location not found for AprilTag ID: #{apriltag_id}")}
|
||||
|
||||
location ->
|
||||
scanned_codes = [%{code: code, location: location} | socket.assigns.scanned_codes]
|
||||
scanned_tags = [%{apriltag_id: apriltag_id, location: location} | socket.assigns.scanned_tags]
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:scanned_codes, scanned_codes)
|
||||
|> assign(:scanned_tags, scanned_tags)
|
||||
|> put_flash(:info, "Scanned: #{location.path}")}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Invalid QR code: #{reason}")}
|
||||
_ ->
|
||||
{:noreply, put_flash(socket, :error, "Invalid AprilTag ID: #{apriltag_id_str}")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("clear_scanned", _params, socket) do
|
||||
{:noreply, assign(socket, :scanned_codes, [])}
|
||||
{:noreply, assign(socket, :scanned_tags, [])}
|
||||
end
|
||||
|
||||
def handle_event("download_qr", %{"id" => id}, socket) do
|
||||
def handle_event("set_apriltag_mode", %{"mode" => mode}, socket) do
|
||||
{:noreply, assign(socket, :apriltag_mode, mode)}
|
||||
end
|
||||
|
||||
def handle_event("set_edit_apriltag_mode", %{"mode" => mode}, socket) do
|
||||
# Clear the apriltag_id field when switching modes
|
||||
form = case mode do
|
||||
"remove" ->
|
||||
socket.assigns.form
|
||||
|> Phoenix.Component.to_form()
|
||||
|> Map.put(:params, Map.put(socket.assigns.form.params || %{}, "apriltag_id", nil))
|
||||
"keep" ->
|
||||
current_id = socket.assigns.editing_location.apriltag_id
|
||||
socket.assigns.form
|
||||
|> Phoenix.Component.to_form()
|
||||
|> Map.put(:params, Map.put(socket.assigns.form.params || %{}, "apriltag_id", current_id))
|
||||
_ ->
|
||||
socket.assigns.form
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:edit_apriltag_mode, mode)
|
||||
|> assign(:form, form)}
|
||||
end
|
||||
|
||||
def handle_event("download_apriltag", %{"id" => id}, socket) do
|
||||
case Inventory.get_storage_location!(id) do
|
||||
%{apriltag_id: nil} ->
|
||||
{:noreply, put_flash(socket, :error, "No AprilTag assigned to this location")}
|
||||
|
||||
location ->
|
||||
case QRCode.generate_qr_image(location) do
|
||||
{:ok, png_data} ->
|
||||
filename = "#{location.name |> String.replace(" ", "_")}_QR.png"
|
||||
case AprilTag.get_apriltag_url(location) do
|
||||
nil ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to get AprilTag URL")}
|
||||
|
||||
apriltag_url ->
|
||||
filename = "#{location.name |> String.replace(" ", "_")}_AprilTag_#{location.apriltag_id}.svg"
|
||||
|
||||
# Send file download to browser
|
||||
{:noreply,
|
||||
socket
|
||||
|> push_event("download_file", %{
|
||||
|> push_event("download_apriltag", %{
|
||||
filename: filename,
|
||||
data: Base.encode64(png_data),
|
||||
mime_type: "image/png"
|
||||
url: apriltag_url,
|
||||
apriltag_id: location.apriltag_id
|
||||
})}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to generate QR code")}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -230,8 +288,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
Inventory.count_components_in_storage_location(location_id)
|
||||
end
|
||||
|
||||
defp get_qr_image_url(location) do
|
||||
QRCode.get_qr_image_url(location)
|
||||
defp get_apriltag_url(location) do
|
||||
AprilTag.get_apriltag_url(location)
|
||||
end
|
||||
|
||||
# Component for rendering individual storage location items with QR code support
|
||||
@@ -284,21 +342,21 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
<p class="text-xs text-gray-400">
|
||||
{count_components_in_location(@location.id)} components
|
||||
</p>
|
||||
<%= if @location.qr_code do %>
|
||||
<%= if @location.apriltag_id do %>
|
||||
<span class="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded">
|
||||
QR: {@location.qr_code}
|
||||
AprilTag: {@location.apriltag_id}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%= if @location.qr_code do %>
|
||||
<%= if @location.apriltag_id do %>
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= if get_qr_image_url(@location) do %>
|
||||
<div class="qr-code-container flex-shrink-0">
|
||||
<%= if get_apriltag_url(@location) do %>
|
||||
<div class="apriltag-container flex-shrink-0">
|
||||
<img
|
||||
src={get_qr_image_url(@location)}
|
||||
alt={"QR Code for #{@location.name}"}
|
||||
class="w-16 h-16 border border-gray-200 rounded bg-white"
|
||||
src={get_apriltag_url(@location)}
|
||||
alt={"AprilTag for #{@location.name}"}
|
||||
class="w-16 h-auto border border-gray-200 rounded bg-white"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
</div>
|
||||
@@ -308,10 +366,10 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
<% end %>
|
||||
<button
|
||||
phx-click="download_qr"
|
||||
phx-click="download_apriltag"
|
||||
phx-value-id={@location.id}
|
||||
class="inline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 flex-shrink-0"
|
||||
title="Download QR Code"
|
||||
title="Download AprilTag"
|
||||
>
|
||||
<.icon name="hero-arrow-down-tray" class="w-4 h-4 mr-1.5" />
|
||||
Download
|
||||
@@ -427,6 +485,76 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
<.input field={@form[:description]} type="textarea" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">AprilTag ID (Optional)</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="apriltag_assignment"
|
||||
value="none"
|
||||
id="apriltag_none"
|
||||
checked={@apriltag_mode == "none"}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
phx-click="set_apriltag_mode"
|
||||
phx-value-mode="none"
|
||||
/>
|
||||
<label for="apriltag_none" class="text-sm text-gray-700">
|
||||
No AprilTag assignment
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="apriltag_assignment"
|
||||
value="auto"
|
||||
id="apriltag_auto"
|
||||
checked={@apriltag_mode == "auto"}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
phx-click="set_apriltag_mode"
|
||||
phx-value-mode="auto"
|
||||
/>
|
||||
<label for="apriltag_auto" class="text-sm text-gray-700">
|
||||
Auto-assign next available ID
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="apriltag_assignment"
|
||||
value="manual"
|
||||
id="apriltag_manual"
|
||||
checked={@apriltag_mode == "manual"}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
phx-click="set_apriltag_mode"
|
||||
phx-value-mode="manual"
|
||||
/>
|
||||
<label for="apriltag_manual" class="text-sm text-gray-700">
|
||||
Choose specific ID
|
||||
</label>
|
||||
</div>
|
||||
<%= if @apriltag_mode == "manual" do %>
|
||||
<div class="ml-6 space-y-2">
|
||||
<.input
|
||||
field={@form[:apriltag_id]}
|
||||
type="number"
|
||||
min="0"
|
||||
max="586"
|
||||
placeholder="Enter ID (0-586)"
|
||||
class="w-32"
|
||||
/>
|
||||
<div class="text-xs text-gray-500">
|
||||
Available IDs: <%= length(@available_apriltag_ids) %> of 587
|
||||
<%= if length(@available_apriltag_ids) < 20 do %>
|
||||
<br/>Next available: <%= @available_apriltag_ids |> Enum.take(10) |> Enum.join(", ") %>
|
||||
<%= if length(@available_apriltag_ids) > 10, do: "..." %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -483,6 +611,75 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
<.input field={@form[:description]} type="textarea" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">AprilTag ID</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="edit_apriltag_assignment"
|
||||
value="keep"
|
||||
id="edit_apriltag_keep"
|
||||
checked={@edit_apriltag_mode == "keep"}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
phx-click="set_edit_apriltag_mode"
|
||||
phx-value-mode="keep"
|
||||
/>
|
||||
<label for="edit_apriltag_keep" class="text-sm text-gray-700">
|
||||
Keep current assignment
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="edit_apriltag_assignment"
|
||||
value="change"
|
||||
id="edit_apriltag_change"
|
||||
checked={@edit_apriltag_mode == "change"}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
phx-click="set_edit_apriltag_mode"
|
||||
phx-value-mode="change"
|
||||
/>
|
||||
<label for="edit_apriltag_change" class="text-sm text-gray-700">
|
||||
Change to different ID
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="edit_apriltag_assignment"
|
||||
value="remove"
|
||||
id="edit_apriltag_remove"
|
||||
checked={@edit_apriltag_mode == "remove"}
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
phx-click="set_edit_apriltag_mode"
|
||||
phx-value-mode="remove"
|
||||
/>
|
||||
<label for="edit_apriltag_remove" class="text-sm text-gray-700">
|
||||
Remove AprilTag assignment
|
||||
</label>
|
||||
</div>
|
||||
<%= if @edit_apriltag_mode == "change" do %>
|
||||
<div class="ml-6 space-y-2">
|
||||
<.input
|
||||
field={@form[:apriltag_id]}
|
||||
type="number"
|
||||
min="0"
|
||||
max="586"
|
||||
placeholder="Enter new ID (0-586)"
|
||||
class="w-32"
|
||||
/>
|
||||
<div class="text-xs text-gray-500">
|
||||
Available IDs: <%= length(@available_apriltag_ids) %> of 587
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Current: <%= if @editing_location.apriltag_id, do: "ID #{@editing_location.apriltag_id}", else: "None" %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -504,43 +701,43 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- QR Scanner Modal -->
|
||||
<%= if @qr_scanner_open do %>
|
||||
<!-- AprilTag Scanner Modal -->
|
||||
<%= if @apriltag_scanner_open do %>
|
||||
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-10 mx-auto p-5 border w-11/12 md:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">QR Code Scanner</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900">AprilTag Scanner</h3>
|
||||
<button
|
||||
phx-click="close_qr_scanner"
|
||||
phx-click="close_apriltag_scanner"
|
||||
class="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<.icon name="hero-x-mark" class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- QR Scanner Interface -->
|
||||
<!-- AprilTag Scanner Interface -->
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
|
||||
<.icon name="hero-qr-code" class="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p class="mt-2 text-sm text-gray-600">Camera QR scanner would go here</p>
|
||||
<p class="text-xs text-gray-500 mt-1">In a real implementation, this would use JavaScript QR scanning</p>
|
||||
<p class="mt-2 text-sm text-gray-600">Camera AprilTag scanner would go here</p>
|
||||
<p class="text-xs text-gray-500 mt-1">In a real implementation, this would use JavaScript AprilTag detection</p>
|
||||
|
||||
<!-- Test buttons for demo -->
|
||||
<div class="mt-4 space-y-2">
|
||||
<p class="text-sm font-medium text-gray-700">Test with sample codes:</p>
|
||||
<p class="text-sm font-medium text-gray-700">Test with sample AprilTag IDs:</p>
|
||||
<button
|
||||
phx-click="qr_scanned"
|
||||
phx-value-code="SL:0:1MTKDM:ROOT"
|
||||
phx-click="apriltag_scanned"
|
||||
phx-value-apriltag_id="0"
|
||||
class="block w-full px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded"
|
||||
>
|
||||
Scan "Shelf A"
|
||||
Scan AprilTag ID 0
|
||||
</button>
|
||||
<button
|
||||
phx-click="qr_scanned"
|
||||
phx-value-code="SL:1:VDI701:1MTKDM"
|
||||
phx-click="apriltag_scanned"
|
||||
phx-value-apriltag_id="1"
|
||||
class="block w-full px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded"
|
||||
>
|
||||
Scan "Drawer 1"
|
||||
Scan AprilTag ID 1
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -549,8 +746,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Scanned Codes Display -->
|
||||
<%= if length(@scanned_codes) > 0 do %>
|
||||
<!-- Scanned Tags Display -->
|
||||
<%= if length(@scanned_tags) > 0 do %>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
@@ -563,10 +760,10 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div :for={scan <- @scanned_codes} class="flex items-center justify-between bg-white p-2 rounded border">
|
||||
<div :for={scan <- @scanned_tags} class="flex items-center justify-between bg-white p-2 rounded border">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900">{location_display_name(scan.location)}</span>
|
||||
<span class="text-sm text-gray-600 ml-2">({scan.code})</span>
|
||||
<span class="text-sm text-gray-600 ml-2">(AprilTag ID {scan.apriltag_id})</span>
|
||||
</div>
|
||||
<span class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded">
|
||||
Level {scan.location.level}
|
||||
@@ -582,7 +779,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">Storage Location Hierarchy</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">Manage your physical storage locations and QR codes</p>
|
||||
<p class="text-sm text-gray-500 mt-1">Manage your physical storage locations and AprilTags</p>
|
||||
</div>
|
||||
|
||||
<%= if Enum.empty?(@storage_locations) do %>
|
||||
|
||||
Reference in New Issue
Block a user