switch mp3 to tts, add genserver to app supervisor tree that periodically checks for newly connected devices and plays their associated tts message

This commit is contained in:
silentsilas 2024-07-17 18:20:05 -04:00
parent 8668405143
commit a085aee5c8
18 changed files with 101 additions and 200 deletions

164
assets/package-lock.json generated
View File

@ -1,164 +0,0 @@
{
"name": "here_i_am",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "here_i_am",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"daisyui": "^4.12.10"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/css-selector-tokenizer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
"integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"fastparse": "^1.1.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/culori": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/daisyui": {
"version": "4.12.10",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.10.tgz",
"integrity": "sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==",
"dev": true,
"dependencies": {
"css-selector-tokenizer": "^0.8",
"culori": "^3",
"picocolors": "^1",
"postcss-js": "^4"
},
"engines": {
"node": ">=16.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/daisyui"
}
},
"node_modules/fastparse": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"dev": true
},
"node_modules/postcss": {
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-js": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
}
}
}

View File

@ -1,14 +0,0 @@
{
"name": "here_i_am",
"version": "1.0.0",
"main": "tailwind.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "silentsilas",
"license": "MIT",
"description": "",
"devDependencies": {
"daisyui": "^4.12.10"
}
}

View File

@ -19,7 +19,6 @@ module.exports = {
}, },
}, },
plugins: [ plugins: [
require('daisyui'),
require("@tailwindcss/forms"), require("@tailwindcss/forms"),
require("@tailwindcss/typography"), require("@tailwindcss/typography"),
// Allows prefixing tailwind classes with LiveView classes to add rules // Allows prefixing tailwind classes with LiveView classes to add rules

View File

@ -11,7 +11,8 @@ config :here_i_am, HereIAm.Repo,
hostname: "localhost", hostname: "localhost",
database: "here_i_am_test#{System.get_env("MIX_TEST_PARTITION")}", database: "here_i_am_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox, pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2 pool_size: System.schedulers_online() * 2,
port: 5560
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.

View File

@ -16,6 +16,7 @@ defmodule HereIAm.Application do
{Finch, name: HereIAm.Finch}, {Finch, name: HereIAm.Finch},
# Start a worker by calling: HereIAm.Worker.start_link(arg) # Start a worker by calling: HereIAm.Worker.start_link(arg)
# {HereIAm.Worker, arg}, # {HereIAm.Worker, arg},
HereIAm.DeviceMonitor,
# Start to serve requests, typically the last entry # Start to serve requests, typically the last entry
HereIAmWeb.Endpoint HereIAmWeb.Endpoint
] ]

View File

@ -0,0 +1,71 @@
defmodule HereIAm.DeviceMonitor do
use GenServer
alias HereIAm.Devices
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
@impl true
def init(state) do
schedule_check()
{:ok, Map.put(state, :connected_ips, %{})}
end
@impl true
def handle_info(:check_devices, state) do
connected_ips = get_connected_ips()
# Determine new devices and TTS
new_devices = MapSet.difference(MapSet.new(connected_ips), MapSet.new(Map.keys(state.connected_ips)))
Enum.each(new_devices, fn ip ->
case Devices.get_device_by_ip(ip) do
{:ok, device} -> play_message(device)
{:error, _reason} -> :ok
end
end)
# Update the registry
new_registry = Map.new(connected_ips, fn ip -> {ip, :connected} end)
schedule_check()
{:noreply, %{state | connected_ips: new_registry}}
end
defp schedule_check() do
Process.send_after(self(), :check_devices, 10_000) # Check every 60 seconds
end
defp get_connected_ips() do
{output, 0} = System.cmd("sudo", ["arp-scan", "-l", "--localnet"])
parse_ips_from_arp_scan(output)
end
defp parse_ips_from_arp_scan(output) do
output
|> String.split("\n")
|> Enum.filter(&String.contains?(&1, "\t")) # Filter lines containing tabs (IP-MAC pairs)
|> Enum.map(&String.split(&1, "\t"))
|> Enum.map(&List.first(&1))
|> Enum.filter(&valid_ip?/1)
end
defp valid_ip?(ip) do
case :inet.parse_address(to_charlist(ip)) do
{:ok, _} -> true
{:error, _} -> false
end
end
defp play_message(%{tts: tts_message}) do
cond do
not is_nil(tts_message) and tts_message != "" -> play_tts(tts_message)
true -> IO.puts("No audio or TTS message to play.")
end
end
defp play_tts(tts_message) do
IO.puts("Playing TTS message: #{tts_message}")
System.cmd("espeak", [tts_message])
end
end

View File

@ -101,4 +101,11 @@ defmodule HereIAm.Devices do
def change_device(%Device{} = device, attrs \\ %{}) do def change_device(%Device{} = device, attrs \\ %{}) do
Device.changeset(device, attrs) Device.changeset(device, attrs)
end end
def get_device_by_ip(ip_address) do
case Repo.get_by(Device, ip_address: ip_address) do
nil -> {:error, :not_found}
device -> {:ok, device}
end
end
end end

View File

@ -4,7 +4,7 @@ defmodule HereIAm.Devices.Device do
schema "devices" do schema "devices" do
field :ip_address, :string field :ip_address, :string
field :audio, :string field :tts, :string
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end
@ -12,7 +12,7 @@ defmodule HereIAm.Devices.Device do
@doc false @doc false
def changeset(device, attrs) do def changeset(device, attrs) do
device device
|> cast(attrs, [:ip_address, :audio]) |> cast(attrs, [:ip_address, :tts])
|> validate_required([:ip_address, :audio]) |> validate_required([:ip_address, :tts])
end end
end end

View File

@ -3,10 +3,10 @@
<div class="container mx-auto text-center py-10"> <div class="container mx-auto text-center py-10">
<article class="prose mx-auto px-4"> <article class="prose mx-auto px-4">
<p> <p>
This site allows you to set an MP3 to play when your device connects to the network. This site allows you to set a message to play when your device connects to the network.
</p> </p>
<a href="/devices"> <a href="/devices">
<button class="btn btn-primary mt-5">Manage Devices</button> <.button class="mt-5">Manage Devices</.button>
</a> </a>
</article> </article>
</div> </div>

View File

@ -20,7 +20,7 @@ defmodule HereIAmWeb.DeviceLive.FormComponent do
phx-submit="save" phx-submit="save"
> >
<.input field={@form[:ip_address]} type="text" label="IP Address" /> <.input field={@form[:ip_address]} type="text" label="IP Address" />
<.input field={@form[:audio]} type="text" label="Audio" /> <.input field={@form[:tts]} type="text" label="tts" />
<:actions> <:actions>
<.button phx-disable-with="Saving...">Save Device</.button> <.button phx-disable-with="Saving...">Save Device</.button>
</:actions> </:actions>

View File

@ -13,7 +13,7 @@
row_click={fn {_id, device} -> JS.navigate(~p"/devices/#{device}") end} row_click={fn {_id, device} -> JS.navigate(~p"/devices/#{device}") end}
> >
<:col :let={{_id, device}} label="IP Address"><%= device.ip_address %></:col> <:col :let={{_id, device}} label="IP Address"><%= device.ip_address %></:col>
<:col :let={{_id, device}} label="Audio"><%= device.audio %></:col> <:col :let={{_id, device}} label="TTS"><%= device.tts %></:col>
<:action :let={{_id, device}}> <:action :let={{_id, device}}>
<div class="sr-only"> <div class="sr-only">
<.link navigate={~p"/devices/#{device}"}>Show</.link> <.link navigate={~p"/devices/#{device}"}>Show</.link>

View File

@ -10,7 +10,7 @@
<.list> <.list>
<:item title="IP Address"><%= @device.ip_address %></:item> <:item title="IP Address"><%= @device.ip_address %></:item>
<:item title="Audio"><%= @device.audio %></:item> <:item title="TTS"><%= @device.tts %></:item>
</.list> </.list>
<.back navigate={~p"/devices"}>Back to devices</.back> <.back navigate={~p"/devices"}>Back to devices</.back>

View File

@ -4,7 +4,7 @@ defmodule HereIAm.Repo.Migrations.CreateDevices do
def change do def change do
create table(:devices) do create table(:devices) do
add :ip_address, :string add :ip_address, :string
add :audio, :string add :tts, :string
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end

View File

@ -8,7 +8,7 @@ defmodule HereIAm.DevicesTest do
import HereIAm.DevicesFixtures import HereIAm.DevicesFixtures
@invalid_attrs %{ip_address: nil, audio: nil} @invalid_attrs %{ip_address: nil, tts: nil}
test "list_devices/0 returns all devices" do test "list_devices/0 returns all devices" do
device = device_fixture() device = device_fixture()
@ -21,11 +21,11 @@ defmodule HereIAm.DevicesTest do
end end
test "create_device/1 with valid data creates a device" do test "create_device/1 with valid data creates a device" do
valid_attrs = %{ip_address: "some ip_address", audio: "some audio"} valid_attrs = %{ip_address: "some ip_address", tts: "some tts"}
assert {:ok, %Device{} = device} = Devices.create_device(valid_attrs) assert {:ok, %Device{} = device} = Devices.create_device(valid_attrs)
assert device.ip_address == "some ip_address" assert device.ip_address == "some ip_address"
assert device.audio == "some audio" assert device.tts == "some tts"
end end
test "create_device/1 with invalid data returns error changeset" do test "create_device/1 with invalid data returns error changeset" do
@ -34,11 +34,11 @@ defmodule HereIAm.DevicesTest do
test "update_device/2 with valid data updates the device" do test "update_device/2 with valid data updates the device" do
device = device_fixture() device = device_fixture()
update_attrs = %{ip_address: "some updated ip_address", audio: "some updated audio"} update_attrs = %{ip_address: "some updated ip_address", tts: "some updated tts"}
assert {:ok, %Device{} = device} = Devices.update_device(device, update_attrs) assert {:ok, %Device{} = device} = Devices.update_device(device, update_attrs)
assert device.ip_address == "some updated ip_address" assert device.ip_address == "some updated ip_address"
assert device.audio == "some updated audio" assert device.tts == "some updated tts"
end end
test "update_device/2 with invalid data returns error changeset" do test "update_device/2 with invalid data returns error changeset" do

View File

@ -3,6 +3,6 @@ defmodule HereIAmWeb.PageControllerTest do
test "GET /", %{conn: conn} do test "GET /", %{conn: conn} do
conn = get(conn, ~p"/") conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Peace of mind from prototype to production" assert html_response(conn, 200) =~ "This site allows you to set a message to play when your device connects to the network"
end end
end end

View File

@ -4,9 +4,9 @@ defmodule HereIAmWeb.DeviceLiveTest do
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
import HereIAm.DevicesFixtures import HereIAm.DevicesFixtures
@create_attrs %{ip_address: "some ip_address", audio: "some audio"} @create_attrs %{ip_address: "some ip_address", tts: "some tts"}
@update_attrs %{ip_address: "some updated ip_address", audio: "some updated audio"} @update_attrs %{ip_address: "some updated ip_address", tts: "some updated tts"}
@invalid_attrs %{ip_address: nil, audio: nil} @invalid_attrs %{ip_address: nil, tts: nil}
defp create_device(_) do defp create_device(_) do
device = device_fixture() device = device_fixture()

View File

@ -11,7 +11,7 @@ defmodule HereIAm.DevicesFixtures do
{:ok, device} = {:ok, device} =
attrs attrs
|> Enum.into(%{ |> Enum.into(%{
audio: "some audio", tts: "some tts",
ip_address: "some ip_address" ip_address: "some ip_address"
}) })
|> HereIAm.Devices.create_device() |> HereIAm.Devices.create_device()