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:
parent
8668405143
commit
a085aee5c8
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
plugins: [
|
||||
require('daisyui'),
|
||||
require("@tailwindcss/forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
// Allows prefixing tailwind classes with LiveView classes to add rules
|
||||
|
|
|
@ -11,7 +11,8 @@ config :here_i_am, HereIAm.Repo,
|
|||
hostname: "localhost",
|
||||
database: "here_i_am_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||
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,
|
||||
# you can enable the server option below.
|
||||
|
|
|
@ -16,6 +16,7 @@ defmodule HereIAm.Application do
|
|||
{Finch, name: HereIAm.Finch},
|
||||
# Start a worker by calling: HereIAm.Worker.start_link(arg)
|
||||
# {HereIAm.Worker, arg},
|
||||
HereIAm.DeviceMonitor,
|
||||
# Start to serve requests, typically the last entry
|
||||
HereIAmWeb.Endpoint
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -101,4 +101,11 @@ defmodule HereIAm.Devices do
|
|||
def change_device(%Device{} = device, attrs \\ %{}) do
|
||||
Device.changeset(device, attrs)
|
||||
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
|
||||
|
|
|
@ -4,7 +4,7 @@ defmodule HereIAm.Devices.Device do
|
|||
|
||||
schema "devices" do
|
||||
field :ip_address, :string
|
||||
field :audio, :string
|
||||
field :tts, :string
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
@ -12,7 +12,7 @@ defmodule HereIAm.Devices.Device do
|
|||
@doc false
|
||||
def changeset(device, attrs) do
|
||||
device
|
||||
|> cast(attrs, [:ip_address, :audio])
|
||||
|> validate_required([:ip_address, :audio])
|
||||
|> cast(attrs, [:ip_address, :tts])
|
||||
|> validate_required([:ip_address, :tts])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<div class="container mx-auto text-center py-10">
|
||||
<article class="prose mx-auto px-4">
|
||||
<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>
|
||||
<a href="/devices">
|
||||
<button class="btn btn-primary mt-5">Manage Devices</button>
|
||||
<.button class="mt-5">Manage Devices</.button>
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,7 @@ defmodule HereIAmWeb.DeviceLive.FormComponent do
|
|||
phx-submit="save"
|
||||
>
|
||||
<.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>
|
||||
<.button phx-disable-with="Saving...">Save Device</.button>
|
||||
</:actions>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
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="Audio"><%= device.audio %></:col>
|
||||
<:col :let={{_id, device}} label="TTS"><%= device.tts %></:col>
|
||||
<:action :let={{_id, device}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/devices/#{device}"}>Show</.link>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<.list>
|
||||
<:item title="IP Address"><%= @device.ip_address %></:item>
|
||||
<:item title="Audio"><%= @device.audio %></:item>
|
||||
<:item title="TTS"><%= @device.tts %></:item>
|
||||
</.list>
|
||||
|
||||
<.back navigate={~p"/devices"}>Back to devices</.back>
|
||||
|
|
|
@ -4,7 +4,7 @@ defmodule HereIAm.Repo.Migrations.CreateDevices do
|
|||
def change do
|
||||
create table(:devices) do
|
||||
add :ip_address, :string
|
||||
add :audio, :string
|
||||
add :tts, :string
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule HereIAm.DevicesTest do
|
|||
|
||||
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
|
||||
device = device_fixture()
|
||||
|
@ -21,11 +21,11 @@ defmodule HereIAm.DevicesTest do
|
|||
end
|
||||
|
||||
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 device.ip_address == "some ip_address"
|
||||
assert device.audio == "some audio"
|
||||
assert device.tts == "some tts"
|
||||
end
|
||||
|
||||
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
|
||||
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 device.ip_address == "some updated ip_address"
|
||||
assert device.audio == "some updated audio"
|
||||
assert device.tts == "some updated tts"
|
||||
end
|
||||
|
||||
test "update_device/2 with invalid data returns error changeset" do
|
||||
|
|
|
@ -3,6 +3,6 @@ defmodule HereIAmWeb.PageControllerTest do
|
|||
|
||||
test "GET /", %{conn: conn} do
|
||||
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
|
||||
|
|
|
@ -4,9 +4,9 @@ defmodule HereIAmWeb.DeviceLiveTest do
|
|||
import Phoenix.LiveViewTest
|
||||
import HereIAm.DevicesFixtures
|
||||
|
||||
@create_attrs %{ip_address: "some ip_address", audio: "some audio"}
|
||||
@update_attrs %{ip_address: "some updated ip_address", audio: "some updated audio"}
|
||||
@invalid_attrs %{ip_address: nil, audio: nil}
|
||||
@create_attrs %{ip_address: "some ip_address", tts: "some tts"}
|
||||
@update_attrs %{ip_address: "some updated ip_address", tts: "some updated tts"}
|
||||
@invalid_attrs %{ip_address: nil, tts: nil}
|
||||
|
||||
defp create_device(_) do
|
||||
device = device_fixture()
|
||||
|
|
|
@ -11,7 +11,7 @@ defmodule HereIAm.DevicesFixtures do
|
|||
{:ok, device} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
audio: "some audio",
|
||||
tts: "some tts",
|
||||
ip_address: "some ip_address"
|
||||
})
|
||||
|> HereIAm.Devices.create_device()
|
||||
|
|
Loading…
Reference in New Issue