neiam
/lambent_ex
Lambent Aether 5
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
Ask the devices what they run, without waiting for the sweep
a1ddc30 · Gabriel Morell · 2026-09-17 01:06
Message
{commit_body(@commit)}
Files changed
modified
android/app/src/main/java/org/neiam/lambent/app/data/LambentSocket.kt
+3
−0
@@ -284,6 +284,9 @@ object LambentSocket {
| 284 | 284 | fun poke(mac: String, mode: String = "auto") = push("devices", "poke", mapOf("id" to mac, "mode" to mode)) |
| 285 | 285 | fun probe(ip: String) = push("devices", "probe", mapOf("ip" to ip)) |
| 286 | 286 | |
| 287 | + /** ask every known device what it is running, rather than waiting for the sweep */ | |
| 288 | + fun refreshDevices() = push("devices", "refresh", emptyMap()) | |
| 289 | + | |
| 287 | 290 | fun sweepInterface(name: String, enabled: Boolean) = |
| 288 | 291 | push("devices", "interface", mapOf("name" to name, "enabled" to enabled)) |
| 289 | 292 |
modified
android/app/src/main/java/org/neiam/lambent/app/ui/DevicesScreen.kt
+8
−1
@@ -16,6 +16,7 @@ import androidx.compose.material.icons.Icons
| 16 | 16 | import androidx.compose.material.icons.filled.Bolt |
| 17 | 17 | import androidx.compose.material.icons.filled.ExpandLess |
| 18 | 18 | import androidx.compose.material.icons.filled.ExpandMore |
| 19 | +import androidx.compose.material.icons.filled.Refresh | |
| 19 | 20 | import androidx.compose.material.icons.filled.Search |
| 20 | 21 | import androidx.compose.material.icons.filled.WbIridescent |
| 21 | 22 | import androidx.compose.material3.AssistChip |
@@ -76,6 +77,12 @@ fun DevicesScreen() {
| 76 | 77 | ) { |
| 77 | 78 | Icon(Icons.Filled.Search, contentDescription = "Probe") |
| 78 | 79 | } |
| 80 | + Spacer(Modifier.width(8.dp)) | |
| 81 | + // versions only change when something is reflashed, and the | |
| 82 | + // sweep is minutes away | |
| 83 | + FilledTonalIconButton(onClick = { LambentSocket.refreshDevices() }) { | |
| 84 | + Icon(Icons.Filled.Refresh, contentDescription = "Re-probe everything") | |
| 85 | + } | |
| 79 | 86 | } |
| 80 | 87 | } |
| 81 | 88 |
@@ -179,7 +186,7 @@ private fun DeviceCard(device: Device) {
| 179 | 186 | Text( |
| 180 | 187 | listOfNotNull( |
| 181 | 188 | "${device.type ?: "?"} · ${device.channels}ch", |
| 182 | − device.version, | |
| 189 | + device.version?.let { "v$it" }, | |
| 183 | 190 | ).joinToString(" · "), |
| 184 | 191 | style = MaterialTheme.typography.labelSmall, |
| 185 | 192 | ) |
modified
lib/lambent_ex/scan/8266-7777.ex
+51
−0
@@ -102,6 +102,16 @@ defmodule LambentEx.Scan.ESP8266x7777 do
| 102 | 102 | via_tuple("scan_8266") |> GenServer.cast({:manual, ip}) |
| 103 | 103 | end |
| 104 | 104 | |
| 105 | + @doc """ | |
| 106 | + Ask every device we already know about what it is running now. | |
| 107 | + | |
| 108 | + The sweep only comes round every few minutes, and a device that has just | |
| 109 | + been reflashed is the one you most want to see the version of. | |
| 110 | + """ | |
| 111 | + def refresh() do | |
| 112 | + via_tuple("scan_8266") |> GenServer.cast(:refresh) | |
| 113 | + end | |
| 114 | + | |
| 105 | 115 | # impls |
| 106 | 116 | |
| 107 | 117 | @impl true |
@@ -443,6 +453,47 @@ defmodule LambentEx.Scan.ESP8266x7777 do
| 443 | 453 | {:reply, :ok, state} |
| 444 | 454 | end |
| 445 | 455 | |
| 456 | + def handle_cast(:refresh, state) do | |
| 457 | + targets = | |
| 458 | + state[:devices] | |
| 459 | + |> Enum.filter(fn {_mac, device} -> is_tuple(device["ip"]) end) | |
| 460 | + | |
| 461 | + Logger.notice("#{@p} Re-probing #{length(targets)} known devices") | |
| 462 | + | |
| 463 | + for {mac, device} <- targets do | |
| 464 | + ip = device["ip"] |> Tuple.to_list() |> Enum.join(".") | |
| 465 | + | |
| 466 | + Task.Supervisor.async_nolink(LambentEx.Scan.Connectivity.Supervisor, fn -> | |
| 467 | + case query_http(ip) do | |
| 468 | + {:lambent, _ip, cnt, ind, ver} -> | |
| 469 | + via_tuple("scan_8266") |> GenServer.cast({:refreshed, mac, cnt, ind, ver}) | |
| 470 | + | |
| 471 | + _otherwise -> | |
| 472 | + :ok | |
| 473 | + end | |
| 474 | + end) | |
| 475 | + end | |
| 476 | + | |
| 477 | + {:noreply, state} | |
| 478 | + end | |
| 479 | + | |
| 480 | + # we already know the mac, so this skips the arp dance a discovery needs | |
| 481 | + def handle_cast({:refreshed, mac, count, ind, ver}, state) do | |
| 482 | + case state[:devices] |> Map.get(mac) do | |
| 483 | + nil -> | |
| 484 | + {:noreply, state} | |
| 485 | + | |
| 486 | + device -> | |
| 487 | + device = | |
| 488 | + device | |
| 489 | + |> Map.put("sox", count) | |
| 490 | + |> Map.put("ind", ind) | |
| 491 | + |> Map.put("ver", ver) | |
| 492 | + | |
| 493 | + {:noreply, %{state | devices: Map.put(state[:devices], mac, device)}} | |
| 494 | + end | |
| 495 | + end | |
| 496 | + | |
| 446 | 497 | def handle_cast({:manual, ip}, state) do |
| 447 | 498 | case resolve_iface(ip, state) do |
| 448 | 499 | nil -> |
modified
lib/lambent_ex_web/channels/devices_channel.ex
+8
−0
@@ -118,6 +118,14 @@ defmodule LambentExWeb.DevicesChannel do
| 118 | 118 | {:reply, :ok, socket} |
| 119 | 119 | end |
| 120 | 120 | |
| 121 | + # ask everything we know about what it is running now, rather than waiting | |
| 122 | + # for the sweep to come round | |
| 123 | + @impl true | |
| 124 | + def handle_in("refresh", _params, socket) do | |
| 125 | + S.refresh() | |
| 126 | + {:reply, :ok, socket} | |
| 127 | + end | |
| 128 | + | |
| 121 | 129 | @impl true |
| 122 | 130 | def handle_in("probe", %{"ip" => ip}, socket) do |
| 123 | 131 | ip = String.trim(ip) |
modified
lib/lambent_ex_web/live/devices/versions.ex
+11
−0
@@ -15,6 +15,17 @@ defmodule LambentExWeb.DevicesLive.Versions do
| 15 | 15 | {:noreply, apply_action(socket, socket.assigns.live_action, params)} |
| 16 | 16 | end |
| 17 | 17 | |
| 18 | + # the sweep is every few minutes, which is a long time to wait to see what a | |
| 19 | + # device you have just reflashed is running | |
| 20 | + @impl true | |
| 21 | + def handle_event("refresh", _params, socket) do | |
| 22 | + LambentEx.Scan.ESP8266x7777.refresh() | |
| 23 | + | |
| 24 | + {:noreply, | |
| 25 | + socket | |
| 26 | + |> put_flash(:info, "Asking #{map_size(socket.assigns.devices)} devices what they run...")} | |
| 27 | + end | |
| 28 | + | |
| 18 | 29 | @impl true |
| 19 | 30 | def handle_info({:devices_pub, devices}, socket) do |
| 20 | 31 | n = Map.merge(socket.assigns.devices, devices) |
modified
lib/lambent_ex_web/live/devices/versions.html.heex
+5
−1
@@ -3,8 +3,12 @@
| 3 | 3 | <.link patch={~p"/cfg/devices"} class="btn btn-ghost normal-case"> |
| 4 | 4 | <i class="fa-solid fa-chevron-left"></i> All devices |
| 5 | 5 | </.link> |
| 6 | + <button class="btn btn-primary normal-case" phx-click="refresh"> | |
| 7 | + <i class="fa-solid fa-arrows-rotate"></i> Re-probe | |
| 8 | + </button> | |
| 6 | 9 | <span class="text-sm opacity-60"> |
| 7 | − Firmware versions as reported by the 7777-flex discovery endpoint. | |
| 10 | + Firmware versions as reported by the 7777-flex discovery endpoint. The sweep | |
| 11 | + picks up a change on its own every few minutes; this asks right now. | |
| 8 | 12 | </span> |
| 9 | 13 | </div> |
| 10 | 14 | <table class="table table-zebra w-full"> |
modified
test/lambent_ex_web/channels/channels_test.exs
+10
−0
@@ -51,6 +51,16 @@ defmodule LambentExWeb.ChannelsTest do
| 51 | 51 | end |
| 52 | 52 | end |
| 53 | 53 | |
| 54 | + describe "re-probing" do | |
| 55 | + test "a client can ask every device what it runs" do | |
| 56 | + socket = connect_to("devices") | |
| 57 | + assert_push "devices", %{devices: _snapshot} | |
| 58 | + | |
| 59 | + ref = push(socket, "refresh", %{}) | |
| 60 | + assert_reply ref, :ok | |
| 61 | + end | |
| 62 | + end | |
| 63 | + | |
| 54 | 64 | describe "choosing what gets swept" do |
| 55 | 65 | setup do |
| 56 | 66 | on_exit(fn -> LambentEx.Meta.put_skipped_ifaces([]) end) |
added
test/lambent_ex_web/live/versions_test.exs
+54
−0
@@ -0,0 +1,54 @@
| 1 | +defmodule LambentExWeb.VersionsTest do | |
| 2 | + use LambentExWeb.ConnCase | |
| 3 | + | |
| 4 | + import Phoenix.LiveViewTest | |
| 5 | + | |
| 6 | + @pubsub LambentEx.PubSub | |
| 7 | + | |
| 8 | + defp publish(devices) do | |
| 9 | + Phoenix.PubSub.broadcast(@pubsub, "scan-82667777", {:devices_pub, devices}) | |
| 10 | + end | |
| 11 | + | |
| 12 | + test "versions are listed for the devices that report one", %{conn: conn} do | |
| 13 | + {:ok, live, _html} = live(conn, "/cfg/devices/versions") | |
| 14 | + | |
| 15 | + publish(%{ | |
| 16 | + "cc:cc:cc:cc:cc:01" => %{ | |
| 17 | + "ip" => {10, 0, 0, 11}, | |
| 18 | + "sox" => 2, | |
| 19 | + "type" => "7777-flex", | |
| 20 | + "ver" => "0.1.0+ci20260916.gb81a582" | |
| 21 | + }, | |
| 22 | + # a legacy device has no version to report and is not listed | |
| 23 | + "cc:cc:cc:cc:cc:02" => %{"ip" => {10, 0, 0, 12}, "sox" => 0, "type" => "7777-8266"} | |
| 24 | + }) | |
| 25 | + | |
| 26 | + html = render(live) | |
| 27 | + assert html =~ "0.1.0+ci20260916.gb81a582" | |
| 28 | + assert html =~ "10.0.0.11" | |
| 29 | + refute html =~ "cc:cc:cc:cc:cc:02" | |
| 30 | + end | |
| 31 | + | |
| 32 | + test "a flex device with no version yet shows as unknown", %{conn: conn} do | |
| 33 | + {:ok, live, _html} = live(conn, "/cfg/devices/versions") | |
| 34 | + | |
| 35 | + publish(%{ | |
| 36 | + "cc:cc:cc:cc:cc:03" => %{"ip" => {10, 0, 0, 13}, "sox" => 2, "type" => "7777-flex"} | |
| 37 | + }) | |
| 38 | + | |
| 39 | + assert render(live) =~ "unknown" | |
| 40 | + end | |
| 41 | + | |
| 42 | + test "the page can ask every device what it runs", %{conn: conn} do | |
| 43 | + {:ok, live, html} = live(conn, "/cfg/devices/versions") | |
| 44 | + | |
| 45 | + assert html =~ "Re-probe" | |
| 46 | + | |
| 47 | + publish(%{ | |
| 48 | + "cc:cc:cc:cc:cc:04" => %{"ip" => {10, 0, 0, 14}, "sox" => 2, "type" => "7777-flex"} | |
| 49 | + }) | |
| 50 | + | |
| 51 | + html = live |> element(~s(button[phx-click="refresh"])) |> render_click() | |
| 52 | + assert html =~ "what they run" | |
| 53 | + end | |
| 54 | +end |
Parents: 95c72a1