fix: show all BLE devices in K1 console
This commit is contained in:
parent
ea834e09e8
commit
8b5b90d8af
|
|
@ -185,11 +185,18 @@ function DeviceRow({
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="device-row" data-selected={selected ? "true" : undefined}>
|
<div
|
||||||
|
className="device-row"
|
||||||
|
data-likely-k1={device.likely_k1 ? "true" : undefined}
|
||||||
|
data-selected={selected ? "true" : undefined}
|
||||||
|
>
|
||||||
<div className="device-row__identity">
|
<div className="device-row__identity">
|
||||||
<span className="device-row__signal" aria-hidden="true" />
|
<span className="device-row__signal" aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
<strong>{device.name?.trim() || "Возможный K1"}</strong>
|
<span className="device-row__name">
|
||||||
|
<strong>{device.name?.trim() || "Устройство без имени"}</strong>
|
||||||
|
{device.likely_k1 ? <small>Возможно K1</small> : null}
|
||||||
|
</span>
|
||||||
<code>{device.device_id}</code>
|
<code>{device.device_id}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -293,8 +300,18 @@ export default function App() {
|
||||||
const [replayLoop, setReplayLoop] = useState(false);
|
const [replayLoop, setReplayLoop] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state?.selected_device_id) setSelectedDeviceId(state.selected_device_id);
|
if (state?.selected_device_id) {
|
||||||
}, [state?.selected_device_id]);
|
setSelectedDeviceId(state.selected_device_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
selectedDeviceId &&
|
||||||
|
state?.devices &&
|
||||||
|
!state.devices.some((device) => device.device_id === selectedDeviceId)
|
||||||
|
) {
|
||||||
|
setSelectedDeviceId("");
|
||||||
|
}
|
||||||
|
}, [selectedDeviceId, state?.devices, state?.selected_device_id]);
|
||||||
|
|
||||||
const metrics = state?.metrics;
|
const metrics = state?.metrics;
|
||||||
const latency = pipelineLatency(metrics);
|
const latency = pipelineLatency(metrics);
|
||||||
|
|
@ -508,12 +525,12 @@ export default function App() {
|
||||||
|
|
||||||
<WizardStep
|
<WizardStep
|
||||||
number="02"
|
number="02"
|
||||||
title="Найдите K1 по Bluetooth (BLE)"
|
title="Выберите K1 среди устройств Bluetooth"
|
||||||
status={
|
status={
|
||||||
pendingAction === "scan"
|
pendingAction === "scan"
|
||||||
? "Поиск…"
|
? "Поиск…"
|
||||||
: selectedDeviceId
|
: selectedDeviceId
|
||||||
? "K1 выбран"
|
? "Устройство выбрано"
|
||||||
: `Найдено: ${devices.length}`
|
: `Найдено: ${devices.length}`
|
||||||
}
|
}
|
||||||
tone={
|
tone={
|
||||||
|
|
@ -525,16 +542,22 @@ export default function App() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p className="step-copy">
|
<p className="step-copy">
|
||||||
Поиск занимает 6 секунд и ничего не записывает в настройки сканера.
|
Поиск занимает 6 секунд и показывает все видимые BLE-устройства. Вероятные
|
||||||
|
K1 будут сверху и получат метку, но окончательный выбор делаете вы.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
width="full"
|
width="full"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={<Icon name="search" />}
|
icon={<Icon name="search" />}
|
||||||
disabled={!powerConfirmed || isBusy}
|
disabled={!powerConfirmed || isBusy}
|
||||||
onClick={() => void scan()}
|
onClick={() => {
|
||||||
|
setSelectedDeviceId("");
|
||||||
|
void scan();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{pendingAction === "scan" ? "Ищем K1 — 6 секунд…" : "Найти K1"}
|
{pendingAction === "scan"
|
||||||
|
? "Сканируем Bluetooth — 6 секунд…"
|
||||||
|
: "Показать все BLE-устройства"}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="device-list">
|
<div className="device-list">
|
||||||
{devices.length ? (
|
{devices.length ? (
|
||||||
|
|
@ -548,8 +571,8 @@ export default function App() {
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="empty-device-list">
|
<div className="empty-device-list">
|
||||||
K1 пока не найден. Убедитесь, что он включён и горит ровно зелёным, затем
|
Устройства пока не найдены. Убедитесь, что K1 включён и горит ровно
|
||||||
нажмите «Найти K1».
|
зелёным, затем запустите поиск ещё раз.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export interface BleDevice {
|
||||||
rssi?: number | null;
|
rssi?: number | null;
|
||||||
address?: string | null;
|
address?: string | null;
|
||||||
connectable?: boolean | null;
|
connectable?: boolean | null;
|
||||||
|
likely_k1?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SourceMode = "idle" | "live" | "replay";
|
export type SourceMode = "idle" | "live" | "replay";
|
||||||
|
|
|
||||||
|
|
@ -409,8 +409,11 @@ input {
|
||||||
|
|
||||||
.device-list {
|
.device-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
max-height: 28rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-row {
|
.device-row {
|
||||||
|
|
@ -445,6 +448,25 @@ input {
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-row__name {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row__name small {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--k1-accent-soft);
|
||||||
|
color: rgb(var(--nodedc-accent-rgb));
|
||||||
|
padding: 0.2rem 0.42rem;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.device-row__identity strong {
|
.device-row__identity strong {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 0.74rem;
|
font-size: 0.74rem;
|
||||||
|
|
@ -467,6 +489,11 @@ input {
|
||||||
height: 0.6rem;
|
height: 0.6rem;
|
||||||
flex: 0 0 0.6rem;
|
flex: 0 0 0.6rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
background: var(--nodedc-text-muted);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row[data-likely-k1="true"] .device-row__signal {
|
||||||
background: rgb(var(--nodedc-success-rgb));
|
background: rgb(var(--nodedc-success-rgb));
|
||||||
box-shadow: 0 0 0 0.25rem var(--k1-success-soft);
|
box-shadow: 0 0 0 0.25rem var(--k1-success-soft);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ def advertisement_record(device: BLEDevice, advertisement: AdvertisementData) ->
|
||||||
service_uuid: data.hex()
|
service_uuid: data.hex()
|
||||||
for service_uuid, data in sorted(advertisement.service_data.items())
|
for service_uuid, data in sorted(advertisement.service_data.items())
|
||||||
},
|
},
|
||||||
"k1_name_candidate": any(marker in normalized_name for marker in ("lixel", "xgrids", "k1")),
|
"k1_name_candidate": normalized_name.startswith("xgr-")
|
||||||
|
or any(marker in normalized_name for marker in ("lixel", "xgrids", "k1")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,13 @@ class ConsoleService:
|
||||||
elif selected_device_id is not None:
|
elif selected_device_id is not None:
|
||||||
phase = "device_selected"
|
phase = "device_selected"
|
||||||
message = "K1 выбран. Теперь введите название и пароль Wi-Fi."
|
message = "K1 выбран. Теперь введите название и пароль Wi-Fi."
|
||||||
|
elif devices:
|
||||||
|
likely_count = sum(bool(item.get("likely_k1")) for item in devices)
|
||||||
|
phase = "idle"
|
||||||
|
message = (
|
||||||
|
f"Найдено BLE-устройств: {len(devices)}. "
|
||||||
|
f"Возможных K1: {likely_count}. Выберите нужное устройство."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
phase = "idle"
|
phase = "idle"
|
||||||
message = runtime["message"]
|
message = runtime["message"]
|
||||||
|
|
@ -107,7 +114,7 @@ class ConsoleService:
|
||||||
}
|
}
|
||||||
|
|
||||||
async def scan_ble(self, duration_seconds: float) -> dict[str, Any]:
|
async def scan_ble(self, duration_seconds: float) -> dict[str, Any]:
|
||||||
self._set_operation("scanning", "Ищем K1 по Bluetooth (BLE)…")
|
self._set_operation("scanning", "Сканируем все устройства Bluetooth (BLE)…")
|
||||||
try:
|
try:
|
||||||
result = await scan(duration_seconds)
|
result = await scan(duration_seconds)
|
||||||
devices = [
|
devices = [
|
||||||
|
|
@ -117,16 +124,14 @@ class ConsoleService:
|
||||||
"rssi": item["rssi"],
|
"rssi": item["rssi"],
|
||||||
"address": None,
|
"address": None,
|
||||||
"connectable": True,
|
"connectable": True,
|
||||||
|
"likely_k1": item["k1_name_candidate"],
|
||||||
}
|
}
|
||||||
for item in result["devices"]
|
for item in result["devices"]
|
||||||
if item["k1_name_candidate"]
|
|
||||||
]
|
]
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._devices = devices
|
self._devices = devices
|
||||||
if len(devices) == 1:
|
|
||||||
self._selected_device_id = str(devices[0]["device_id"])
|
|
||||||
self._operation_message = (
|
self._operation_message = (
|
||||||
f"Поиск завершён. Найдено устройств K1: {len(devices)}."
|
f"Поиск завершён. Найдено BLE-устройств: {len(devices)}."
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
|
@ -136,7 +141,7 @@ class ConsoleService:
|
||||||
async def connect(self, request: ConnectRequest) -> dict[str, Any]:
|
async def connect(self, request: ConnectRequest) -> dict[str, Any]:
|
||||||
known_ids = {str(item["device_id"]) for item in self.state()["devices"]}
|
known_ids = {str(item["device_id"]) for item in self.state()["devices"]}
|
||||||
if request.device_id not in known_ids:
|
if request.device_id not in known_ids:
|
||||||
raise ValueError("сначала найдите и выберите K1 через Bluetooth")
|
raise ValueError("сначала найдите и выберите устройство через Bluetooth")
|
||||||
self._set_operation(
|
self._set_operation(
|
||||||
"provisioning",
|
"provisioning",
|
||||||
"Передаём в K1 настройки Wi-Fi одним подтверждённым запросом.",
|
"Передаём в K1 настройки Wi-Fi одним подтверждённым запросом.",
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,19 @@ def test_advertisement_record_marks_k1_candidate() -> None:
|
||||||
assert record["manufacturer_data_hex"] == {"123": "0102"}
|
assert record["manufacturer_data_hex"] == {"123": "0102"}
|
||||||
assert record["service_data_hex"] == {"service": "aa"}
|
assert record["service_data_hex"] == {"service": "aa"}
|
||||||
assert record["service_uuids"] == ["B", "a"]
|
assert record["service_uuids"] == ["B", "a"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_advertisement_record_marks_observed_xgr_serial_name_as_k1_candidate() -> None:
|
||||||
|
device = BLEDevice("F89438FA-55ED-85AD-EED7-734AC84746D8", "XGR-A46BE7", None)
|
||||||
|
advertisement = AdvertisementData(
|
||||||
|
local_name="XGR-A46BE7",
|
||||||
|
manufacturer_data={},
|
||||||
|
service_data={},
|
||||||
|
service_uuids=[],
|
||||||
|
tx_power=0,
|
||||||
|
rssi=-60,
|
||||||
|
platform_data=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
record = advertisement_record(device, advertisement)
|
||||||
|
assert record["k1_name_candidate"] is True
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pytest import MonkeyPatch
|
||||||
|
|
||||||
|
import k1link.web.app as web_app
|
||||||
|
|
||||||
|
|
||||||
|
def test_ble_scan_exposes_every_device_and_only_labels_likely_k1(
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
async def fake_scan(duration_seconds: float) -> dict[str, Any]:
|
||||||
|
assert duration_seconds == 6.0
|
||||||
|
return {
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"macos_uuid": "K1-UUID",
|
||||||
|
"name": "XGR-A46BE7",
|
||||||
|
"local_name": "XGR-A46BE7",
|
||||||
|
"rssi": -51,
|
||||||
|
"k1_name_candidate": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"macos_uuid": "OTHER-UUID",
|
||||||
|
"name": "AirPods Pro",
|
||||||
|
"local_name": "AirPods Pro",
|
||||||
|
"rssi": -62,
|
||||||
|
"k1_name_candidate": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(web_app, "scan", fake_scan)
|
||||||
|
service = web_app.ConsoleService(tmp_path)
|
||||||
|
|
||||||
|
state = asyncio.run(service.scan_ble(6.0))
|
||||||
|
|
||||||
|
assert state["selected_device_id"] is None
|
||||||
|
assert [device["device_id"] for device in state["devices"]] == [
|
||||||
|
"K1-UUID",
|
||||||
|
"OTHER-UUID",
|
||||||
|
]
|
||||||
|
assert [device["likely_k1"] for device in state["devices"]] == [True, False]
|
||||||
|
|
||||||
Loading…
Reference in New Issue