feat: build NODE.DC control station shell

This commit is contained in:
DCCONSTRUCTIONS
2026-07-16 01:20:09 +03:00
parent 8b5b90d8af
commit 6402601d6d
30 changed files with 5296 additions and 1792 deletions
+11 -7
View File
@@ -45,7 +45,7 @@ class VisualizationRuntime:
self._thread: threading.Thread | None = None
self._stop_event = threading.Event()
self._phase: RuntimePhase = "idle"
self._message = "Готово. Включите K1 и начните с поиска по Bluetooth."
self._message = "Готово. Включите устройство и начните с поиска по Bluetooth."
self._source_mode: SourceMode = "idle"
self._foxglove_ws_url: str | None = None
self._foxglove_viewer_url: str | None = None
@@ -65,7 +65,7 @@ class VisualizationRuntime:
def start_replay(self, path: Path, *, speed: float = 1.0, loop: bool = False) -> None:
resolved = path.expanduser().resolve()
if not resolved.is_file():
raise ValueError("файл записи не найден на этом MacBook")
raise ValueError("файл записи не найден на этом компьютере")
if not math.isfinite(speed) or speed < 0:
raise ValueError("скорость повтора должна быть неотрицательным числом")
# Validate the reviewed shape before changing runtime state.
@@ -96,7 +96,7 @@ class VisualizationRuntime:
self._start(
source_mode="live",
phase="starting_live",
message="Запускаем приём MQTT и мост Foxglove.",
message="Запускаем приём MQTT и локальный мост визуализации.",
target=lambda: self._run_live(
host,
out_dir.expanduser().resolve(),
@@ -199,7 +199,8 @@ class VisualizationRuntime:
out_dir / "captures" / "mqtt_live",
duration_seconds=duration_seconds,
on_ready=lambda: self._set_running(
"live", "Приём запущен. Теперь дважды нажмите физическую кнопку K1."
"live",
"Приём запущен. Теперь дважды нажмите физическую кнопку устройства.",
),
on_message_recorded=on_message,
should_stop=self._stop_event.is_set,
@@ -248,7 +249,9 @@ class VisualizationRuntime:
self._foxglove_viewer_url = bridge.viewer_url
if self._phase != "stopping":
self._phase = running_phase
self._message = "Мост Foxglove готов; источник данных запущен."
self._message = (
"Локальный мост визуализации готов; источник данных запущен."
)
publisher_ready.set()
self._notify()
while not source_done.is_set() or not messages.empty():
@@ -275,13 +278,14 @@ class VisualizationRuntime:
publisher = threading.Thread(target=publish, name="k1-foxglove-publisher", daemon=True)
publisher.start()
if not publisher_ready.wait(timeout=15.0):
self._finish_error("Мост Foxglove не запустился за 15 секунд.")
self._finish_error("Локальный мост визуализации не запустился за 15 секунд.")
source_done.set()
self._stop_event.set()
return
if publisher_error:
self._finish_error(
f"Ошибка моста Foxglove: {type(publisher_error[0]).__name__}: {publisher_error[0]}"
"Ошибка локального моста визуализации: "
f"{type(publisher_error[0]).__name__}: {publisher_error[0]}"
)
source_done.set()
return
+10 -8
View File
@@ -80,13 +80,13 @@ class ConsoleService:
message = runtime["message"]
elif selected_device_id is not None:
phase = "device_selected"
message = "K1 выбран. Теперь введите название и пароль Wi-Fi."
message = "Устройство выбрано. Теперь введите название и пароль 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}. Выберите нужное устройство."
f"Совместимых профилей: {likely_count}. Выберите нужное устройство."
)
else:
phase = "idle"
@@ -144,7 +144,7 @@ class ConsoleService:
raise ValueError("сначала найдите и выберите устройство через Bluetooth")
self._set_operation(
"provisioning",
"Передаём в K1 настройки Wi-Fi одним подтверждённым запросом.",
"Передаём устройству настройки Wi-Fi одним подтверждённым запросом.",
)
session_dir = _new_operation_session_dir(
self.repository_root,
@@ -177,12 +177,14 @@ class ConsoleService:
)
if ipv4 is None:
raise RuntimeError(
"K1 не сообщил адрес в локальной сети; автоматического повтора не было"
"Устройство не сообщило адрес в локальной сети; автоматического повтора не было"
)
with self._lock:
self._selected_device_id = request.device_id
self._k1_ip = ipv4
self._operation_message = "K1 подключён к Wi-Fi и сообщил локальный адрес."
self._operation_message = (
"Устройство подключено к Wi-Fi и сообщило локальный адрес."
)
finally:
password = ""
with self._lock:
@@ -193,7 +195,7 @@ class ConsoleService:
target = host or self.state()["k1_ip"]
if not isinstance(target, str) or not target:
raise ValueError(
"сначала подключите K1 к Wi-Fi или укажите его локальный адрес"
"сначала подключите устройство к Wi-Fi или укажите его локальный адрес"
)
target = validate_private_ipv4(target)
out_dir = new_live_session_dir(self.repository_root)
@@ -219,7 +221,7 @@ class ConsoleService:
service = ConsoleService(REPOSITORY_ROOT)
app = FastAPI(
title="NODE.DC K1 Live Console API",
title="NODE.DC Device Control API",
version=__version__,
docs_url="/api/docs",
redoc_url=None,
@@ -257,7 +259,7 @@ async def connect(request: ConnectRequest) -> dict[str, Any]:
except (BleakError, OSError, TimeoutError, RuntimeError, ValueError) as exc:
raise HTTPException(
status_code=502,
detail=f"Ошибка подключения K1 к Wi-Fi: {exc}",
detail=f"Ошибка подключения устройства к Wi-Fi: {exc}",
) from exc