diff --git a/.gitignore b/.gitignore index a22f770048..4ef3035042 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,12 @@ firmware/esp32-csi-node/test/*.obj # Claude Flow swarm runtime state .swarm/ +# Local runtime/debug artifacts from the Windows PT-BR contribution workflow +/.dbg/ +/v2/.dbg/ +/v2/crates/wifi-densepose-sensing-server/.dbg/ +/v2/data/config.json + # CSI recordings (local training/capture data — CSI is person data per # CLAUDE.md; never commit). Covers current and legacy layouts. See ADR-299. data/recordings/ diff --git a/README.md b/README.md index 5233dcd778..39b772a398 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,57 @@ **Turn ordinary WiFi into a spatial intelligence / sensing system.** Detect people, measure breathing and heart rate, track movement, and monitor rooms — through walls, in the dark, with no cameras or wearables. Just physics. +## Fork roadmap: Hybrid Wi-Fi intelligence + +This fork is evolving RuView into a **hybrid sensing + network-awareness** platform for home and small-office environments. + +Our goal is to keep the upstream WiFi sensing pipeline intact while adding a practical layer that can: + +- inventory devices visible on the local network, +- heuristically classify likely **smartphones**, **smart TVs**, **computers**, and **IoT/media** devices, +- fuse that inventory with RuView's **presence / motion / estimated-person** outputs, +- help operators distinguish **human movement** from **stationary connected devices**. + +### What this fork is adding + +- `GET /api/v1/hybrid/latest` for a fused hybrid snapshot +- `GET /api/v1/hybrid/overrides`, `POST /api/v1/hybrid/overrides/upsert`, and `DELETE /api/v1/hybrid/overrides/delete` for manual device labeling +- `GET /api/v1/bluetooth/status` for an **experimental Windows-only Bluetooth helper** status +- LAN device discovery from the local machine +- heuristic device classification for common home devices +- manual override of device naming/category for home deployments +- dashboard visibility into: + - estimated humans, + - known devices, + - likely smartphones, + - likely smart TVs, + - Bluetooth helper availability / adapter / service state + +### Honest scope + +This does **not** mean RuView can magically identify every device purely from ambient RF disturbance. The fork uses a **hybrid approach**: + +- **WiFi sensing layer** for presence, motion, and human-related inference +- **network inventory layer** for connected-device hints +- **manual home-device labeling** for practical overrides in non-ideal networks +- **Bluetooth helper layer** as an optional Windows-only context signal +- **fusion layer** to keep devices like TVs and idle phones from being mistaken for people + +In the current notebook/basic-RSSI mode, this fork aims to extract the **maximum useful home-presence signal** without pretending it has CSI-grade spatial precision. Presence and movement can still be useful; stable multi-person separation remains a hardware-limited case until CSI-capable hardware is used. + +### Roadmap + +1. **Phase 1** + LAN discovery + heuristic classification + dashboard fusion +2. **Phase 2** + router/vendor integrations for richer client metadata +3. **Phase 3** + stronger room-level fusion with CSI-capable multi-node deployments +4. **Phase 4** + better semantic automation and publishable case studies + +If this fork proves useful in practice, the plan is to upstream the pieces that are generic and maintain the rest as optional extensions for hybrid deployments. + Works natively with the four major smart-home ecosystems: **[Home Assistant](docs/integrations/home-assistant.md)** via the HA-DISCO MQTT publisher, **[Apple Home & HomePod](docs/user-guide-apple-homepod.md)** as a discoverable HAP-1.1 bridge, **[Google Home](docs/integrations/home-assistant.md)** + **[Amazon Alexa](docs/integrations/home-assistant.md)** via the same HA bridge or a [Matter](docs/adr/ADR-122-bfld-ruview-ha-matter-exposure.md) endpoint. Siri, Google Assistant, and Alexa can voice presence and vitals by room with zero custom skills. [![Works with Home Assistant](https://img.shields.io/badge/Works%20with-Home%20Assistant-blue?logo=home-assistant&logoColor=white&labelColor=41BDF5)](docs/integrations/home-assistant.md) [![Works with Matter](https://img.shields.io/badge/Works%20with-Matter-blue?labelColor=4285F4)](docs/adr/ADR-122-bfld-ruview-ha-matter-exposure.md) [![Works with Apple Home](https://img.shields.io/badge/Works%20with-Apple%20Home-black?logo=apple)](docs/user-guide-apple-homepod.md) [![Works with Google Home](https://img.shields.io/badge/Works%20with-Google%20Home-blue?logo=googlehome)](docs/integrations/home-assistant.md) [![Works with Alexa](https://img.shields.io/badge/Works%20with-Alexa-blue?logo=amazon&logoColor=white&labelColor=00CAFF)](docs/integrations/home-assistant.md) diff --git a/docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md b/docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md new file mode 100644 index 0000000000..30a1eb2217 --- /dev/null +++ b/docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md @@ -0,0 +1,261 @@ +# Comparativo: RuView original (`HEAD`) vs nossa adaptacao BR + +## 1. Baseline usado + +Este comparativo usa como **projeto original** o estado limpo do repositório `RuView` em `HEAD` no branch `main`, porque este clone local não possui remoto `upstream` separado. +Ou seja: + +- **Original** = `HEAD` limpo do repo +- **Adaptado / nosso** = working tree local atual + arquivos novos criados na contribuição + +## 2. Visao executiva + +Nossa adaptacao transforma o RuView original em uma variante mais voltada para: + +- **usuarios brasileiros leigos** +- **instalacao local em Windows** +- **uso com Wi-Fi caseiro / notebook no modo basico** +- **inventario hibrido de dispositivos da rede** +- **interface traduzida e mais explicita** +- **launcher em PT-BR com logs operacionais** + +Em troca, esta adaptacao assume conscientemente um escopo mais pragmatico: + +- nao promete precisao de radar/CSI quando o hardware e apenas notebook + RSSI +- trata Bluetooth como **auxilio experimental** +- usa camadas hibridas e overrides manuais para separar melhor **pessoas x dispositivos** + +## 3. Diferencas principais por area + +### A. Inicializacao e experiencia para leigos + +**Original** +- inicializacao mais tecnica +- fluxo menos orientado a usuario final local Windows +- sem launcher PT-BR dedicado no nosso workspace local + +**Adaptado** +- adicionados: + - `start.bat` + - `start_ruview.ps1` +- o launcher: + - limpa conflitos de porta/processo + - abre a interface automaticamente + - mostra logs em portugues + - mantem o console visivel + - agora tambem informa o estado do **Bluetooth auxiliar experimental** + +### B. Traducao e localizacao PT-BR + +**Original** +- UI orientada ao ingles, com cobertura local limitada para nosso publico alvo + +**Adaptado** +- ampliada traducao em: + - `ui/index.html` + - `ui/components/DashboardTab.js` + - `ui/components/HardwareTab.js` + - `ui/components/LiveDemoTab.js` + - `ui/components/SensingTab.js` + - `ui/pose-fusion.html` + - `ui/pose-fusion/js/main.js` + - `ui/observatory.html` + - `ui/utils/i18n.js` +- persistencia de idioma e textos adicionais para o fluxo BR/leigo + +### C. Protecao contra cache e coerencia local + +**Original** +- sem a camada local especifica que resolvemos aqui para desenvolvimento e cache do navegador no nosso ambiente + +**Adaptado** +- novo arquivo: + - `ui/utils/local-dev-cache-guard.js` +- uso de `boot=` / cache-busting e origem canonica `localhost` +- mitigacao dos problemas reais que apareciam no Chrome com scripts antigos e divergencia `127.0.0.1` vs `localhost` + +### D. Modo basico Wi-Fi caseiro + +**Original** +- o caminho de sensing basico estava mais conservador +- no nosso uso real, isso derrubava demais a leitura multipessoa e fazia o sistema colapsar facil para `0/1` + +**Adaptado** +- ajustado: + - `v2/crates/wifi-densepose-wifiscan/src/adapter/netsh_scanner.rs` + - `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- ganhos aplicados: + - parser `netsh` mais robusto para contexto PT-BR + - melhor aproveitamento do RSSI no modo `wifi:*` + - heuristica pratica menos travada + - pequena retencao temporal para evitar queda imediata em oscilacoes curtas + - preservacao melhor da contagem resolvida antes de montar a pose da UI + +### E. Camada hibrida de dispositivos de rede + +**Original** +- nao havia no baseline limpo do repo local esta camada domestica orientada a inventario de aparelhos da casa + +**Adaptado** +- novo backend: + - `v2/crates/wifi-densepose-sensing-server/src/hybrid_detection.rs` +- integracao no servidor: + - `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- capacidade adicionada: + - varrer dispositivos da LAN + - inferir categoria heuristica + - contar provaveis smartphones / smart TVs / computadores / IoT + - combinar isso com presenca humana inferida pelo Wi-Fi + +### F. Cadastro manual de dispositivos conhecidos + +**Original** +- sem area dedicada para o usuario corrigir nome/categoria de aparelhos detectados + +**Adaptado** +- Dashboard ganhou area para: + - ver dispositivos detectados + - renomear + - corrigir categoria + - cadastrar manualmente + - salvar override persistente +- arquivos centrais: + - `ui/index.html` + - `ui/components/DashboardTab.js` + - `ui/style.css` + - `ui/utils/i18n.js` + - `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- novos endpoints: + - `GET /api/v1/hybrid/overrides` + - `POST /api/v1/hybrid/overrides/upsert` + - `DELETE /api/v1/hybrid/overrides/delete` + +### G. Bluetooth auxiliar experimental + +**Original** +- sem trilha pronta de Bluetooth integrada para o nosso caso de uso + +**Adaptado** +- suporte **experimental e Windows-only** +- sem vender Bluetooth como detector humano principal +- usado como contexto operacional / indicio auxiliar +- pontos alterados: + - `start_ruview.ps1` + - `ui/index.html` + - `ui/components/DashboardTab.js` + - `ui/utils/i18n.js` + - `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- novos endpoints: + - `GET /api/v1/bluetooth/status` + - `POST /api/v1/bluetooth/enabled` + +### H. Observatory mais honesto para leigo + +**Original** +- leitura visual mais ambigua no nosso uso pratico +- aura e destaque poderiam ser interpretados como algo mais preciso do que realmente eram + +**Adaptado** +- alterados: + - `ui/observatory.html` + - `ui/observatory/css/observatory.css` + - `ui/observatory/js/main.js` + - `ui/observatory/js/hud-controller.js` + - `ui/observatory/js/figure-pool.js` + - `ui/utils/i18n.js` +- melhorias: + - modo realista + - destaque por dispositivo condicionado ao contexto hibrido + - explicacao textual do significado da aura + - bloco visivel de fusao **Wi-Fi + Bluetooth auxiliar** + - visual mais discreto e menos enganoso + +### I. README e narrativa do fork + +**Original** +- README sem o recorte completo da nossa adaptacao BR/local-caseira + +**Adaptado** +- `README.md` atualizado para registrar: + - camada hibrida + - overrides manuais + - Bluetooth auxiliar experimental + - honestidade sobre os limites do notebook + RSSI + +## 4. Arquivos alterados no working tree + +### Arquivos rastreados modificados + +- `README.md` +- `ui/app.js` +- `ui/components/DashboardTab.js` +- `ui/components/HardwareTab.js` +- `ui/components/LiveDemoTab.js` +- `ui/components/SensingTab.js` +- `ui/index.html` +- `ui/observatory.html` +- `ui/observatory/css/observatory.css` +- `ui/observatory/js/figure-pool.js` +- `ui/observatory/js/hud-controller.js` +- `ui/observatory/js/main.js` +- `ui/pose-fusion.html` +- `ui/pose-fusion/js/main.js` +- `ui/style.css` +- `ui/utils/i18n.js` +- `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- `v2/crates/wifi-densepose-wifiscan/src/adapter/netsh_scanner.rs` + +### Arquivos novos locais relevantes da contribuicao + +- `start.bat` +- `start_ruview.ps1` +- `ui/utils/local-dev-cache-guard.js` +- `v2/crates/wifi-densepose-sensing-server/src/hybrid_detection.rs` +- `docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md` +- `docs/validation/debug-basic-rssi-coverage-ptbr.md` +- `docs/validation/debug-basic-rssi-invalid-ptbr.md` + +## 5. Diferenca de filosofia entre original e adaptado + +### Original + +- mais proximo da base tecnica/generica do projeto +- maior foco em pipeline e pesquisa +- menos opinativo para o usuario final domestico brasileiro + +### Adaptado + +- assume explicitamente o papel de **versao amigavel para BR** +- prioriza: + - instalacao simples + - feedback visual claro + - textos em portugues + - controle operacional local + - honestidade sobre o que e deteccao humana e o que e apenas contexto de rede + +## 6. Limites que continuam existindo + +Mesmo com a adaptacao, alguns limites continuam reais: + +- notebook + RSSI basico **nao vira CSI** +- multipessoa em comodos separados continua instavel +- Bluetooth entra apenas como apoio experimental +- classificacao automatica de dispositivos ainda e heuristica +- para salto real de qualidade em multi-pessoa/localizacao, o caminho continua sendo **ESP32-S3 / CSI** + +## 7. Valor da contribuicao + +Esta contribuicao entrega algo que o baseline original nao estava focando diretamente no nosso contexto: + +- uma **versao mais utilizavel por brasileiros leigos** +- com **launcher operacional** +- **traducao mais ampla** +- **inventario hibrido** +- **cadastro manual de dispositivos** +- **Observatory mais explicativo** +- e uma abordagem mais honesta para o uso de **Wi-Fi caseiro como sensor de presenca** + +Em resumo: + +> O projeto original continua sendo a base tecnica. +> A nossa adaptacao transforma essa base em uma versao mais acessivel, mais local, mais transparente e mais util para uso domestico no Brasil. diff --git a/docs/validation/debug-basic-rssi-coverage-ptbr.md b/docs/validation/debug-basic-rssi-coverage-ptbr.md new file mode 100644 index 0000000000..779242ce48 --- /dev/null +++ b/docs/validation/debug-basic-rssi-coverage-ptbr.md @@ -0,0 +1,83 @@ +# Debug Session: basic-rssi-coverage +- **Status**: [CONCLUIDO — EVIDENCIA COLETADA E AJUSTE APLICADO] +- **Issue**: No modo basico Windows/RSSI (sem ESP32-S3), com varias pessoas em areas diferentes da casa (PC na mesa, cozinha, quarto), o Observatorio nao mostra sinal de cobertura multi-pessoa e o numero de pessoas nao passa de 1. Queremos confirmar se ainda estamos "deixando passar" sinal ou se isso ja e o limite real desse hardware/source. +- **Debug Server**: http://127.0.0.1:7777/event +- **Log File**: .dbg/trae-debug-log-basic-rssi-coverage.ndjson + +## Pipeline encontrado (leitura estatica de codigo) + +### Cap estrutural de codigo confirmado +- `practical_wifi_person_count_cap()`: havia um freio estrutural agressivo no modo `wifi:*`, principalmente quando a visibilidade caia para poucos BSSIDs. +- `score_to_person_count()`: limiares altos para adicionar pessoas: + - 1→2 exige `smoothed_score > 0.70` (histerese down 0.55) + - 2→3 exige `smoothed_score > 0.92` +- `practical_wifi_quality_override()`: havia um clamp pratico conservador demais no modo basico com baixa visibilidade. +- **Conclusao final da leitura + runtime**: o limite observado vinha de uma combinacao de: + 1. hardware RSSI de notebook oscilando entre fases boas (`obs_count` 4-6) e fases ruins (`obs_count` 1), + 2. heuristicas do software conservadoras demais para o caminho `wifi:*`. + +### Instrumentation points aplicados +Arquivo: `v2/crates/wifi-densepose-sensing-server/src/main.rs` +| Ponto | Local | O que captura | Hipoteses | +|-------|-------|---------------|-----------| +| B | `wifi_task::after_scan` | Lista completa de BSSIDs visiveis (SSID/BSSID/RSSI/sinal/ch/banda, obs_count) | A, B, D | +| AC | `wifi_task::after_cap` | `raw_score`, `smoothed_person_score`, `pure_person_count` (antes do cap via `s.person_count()`), `cap_branch` (qual regra do practical_cap pegou), `estimated_persons_after_cap`, variance, motion, presence, quality | A, C, D | +| AC | `windows_wifi_fallback_tick::after_cap` | Mesmo do AC para o caminho fallback single-RSSI | A, C | +| E | `hybrid_poll_task` | Snapshot hybrid: `total_devices`, `fusion`, `network_devices` list com ip/mac/hostname/categoria/vendor/confidence | E | + +Helpers atualizados para procurar `.dbg/` em multiplos paths relativos (WD até ../../../../). + +## Status das hipoteses apos instrumentacao +| ID | Hypothesis | Likelihood | Effort | Evidence | +|----|------------|------------|--------|----------| +| A | O modo basico netsh esta lendo apenas 1 BSSID valido e o pipeline/baseline esta descartando/limiting para 1 pessoa na mesma heuristica de min_bssids | High | Low | **Parcialmente confirmada**: no inicio do teste houve `obs_count=4` e em outro trecho `obs_count=6`, mas no estado final/atual o scan caiu para `obs_count=1` (`JHON_5G`). | +| B | Netsh wlan mostra outros BSSIDs (rede vizinha, band 2.4G vs 5G) mas eles estao com RSSI qualificado como ruido estavel e nao entram na contagem | High | Low | **Parcialmente confirmada**: o scanner enxergou multiplos BSSIDs no comeco (`2.4G + 5G + radios extras`), depois colapsou para um unico BSSID visivel. | +| C | A pipeline esta capturando RSSI/variance validos, mas a heuristica `estimated_persons` ainda esta clampada para 1 no fallback basico Windows | High | Low | **Confirmada por evidencia**: `pure_person_count_via_score_to_person_count` chegou a `2` e `estimated_persons_after_cap` tambem chegou a `2` quando havia `obs_count>=4` e qualidade `Permit`. No estado final, com `obs_count=1`, o branch passou para `force_1__live_le_2`. | +| D | A qualidade do canal esta ruim e ha 3 pessoas porem o sinal real de todas elas esta abaixo do minimo medivel do RSSI bruto (pessoas longe/obstaculos pesados) | Medium | Low | **Parcialmente confirmada**: no estado final a qualidade pratica caiu para `Warn`, `variance=0`, `motion_band_power=0` e o sistema ficou cego para cobertura multipessoa. | +| E | A camada hybrid detecta varios dispositivos mas nao esta repassando limite de pessoas para a UI live do Observatory | Low | Low | **Confirmada em parte**: havia `3` dispositivos conhecidos, mas `likely_smartphones=0` em todos os eventos registrados; portanto a UI nao tinha base para ligar destaque por smartphone. | + +## Findings principais +- O teste mostrou que o sistema **nao ficou preso em 1 o tempo todo**: + - `obs_count` variou de **1** ate **6** + - `pure_person_count_via_score_to_person_count` chegou a **2** + - `estimated_persons_after_cap` chegou a **2** +- O estado final do print atual corresponde a uma fase pior do radio: + - `obs_count=1` + - `quality_verdict=Warn` + - `variance=0` + - `motion_band_power=0` + - `estimated_persons=1` +- A camada hibrida detectou dispositivos na LAN, mas **nao classificou nenhum como smartphone** (`likely_smartphones=0`), entao qualquer "aura de dispositivo" percebida na UI estava ambigua visualmente e precisava de ajuste no frontend. +- O fork agora recebeu um ajuste pratico adicional no backend: + - cap de pessoas menos travado no modo `wifi:*`, + - quality override mais permissivo quando houver movimento real, + - pequena retencao temporal para evitar colapso imediato de `2 -> 0/1` em oscilacoes curtas, + - `derive_pose_from_sensing()` preservando melhor a contagem resolvida no tick. + +## Reproduction Steps +1. Manter Debug Server rodando em http://127.0.0.1:7777 (ver /health OK) +2. Garantir que portas 3000/3001/8765 LIVRES +3. NDJSON limpo em `.dbg/trae-debug-log-basic-rssi-coverage.ndjson` +4. **CENARIO**: voce no PC (movendo mouse/digitando), esposa na cozinha com celular, filha no quarto com celular/aparelho ligado — manter por **3 MINUTOS** +5. **EXECUCAO**: duplo clique em `RuView\start.bat` (launcher usa `cargo run` → carrega codigo instrumentado) +6. Abrir Observatory em `http://localhost:3000/ui/observatory.html` e conferir: + - Badge LIVE (nao DEMO) + - Contador de pessoas na HUD (canto superior direito?) + - Modo realista e destaque por dispositivo LIGADOS +7. Apos 3 minutos, parar a execucao (fechar janela do launcher) e coletar ndjson +8. Analisar: quantos % dos logs AC tem cap_branch force_1? Qual o max pure_person_count? + +## Conclusao final +- **Nao** era correto dizer que o notebook estava extraindo "tudo" no estado anterior. +- **Sim**, havia freio de software removivel e ele foi afrouxado nesta sessao. +- **Mesmo apos o ajuste**, o modo basico continua limitado por fisica/hardware: + - bom para **presenca geral**, + - util para **movimento**, + - pode sugerir **2 pessoas** em janelas boas, + - **nao** e um caminho confiavel para separar varias pessoas em comodos diferentes com consistencia de radar/CSI. + +## Proximo uso recomendado +- usar o modo basico como **sensor de presenca/movimento em tempo real**; +- usar o inventario hibrido + cadastro manual para separar melhor **pessoas x aparelhos conectados**; +- tratar Bluetooth apenas como **auxilio experimental de contexto** no Windows; +- reservar **ESP32-S3 / CSI** para o salto real de qualidade em multi-pessoa e localizacao. diff --git a/docs/validation/debug-basic-rssi-invalid-ptbr.md b/docs/validation/debug-basic-rssi-invalid-ptbr.md new file mode 100644 index 0000000000..b73eda098d --- /dev/null +++ b/docs/validation/debug-basic-rssi-invalid-ptbr.md @@ -0,0 +1,65 @@ +# Debug Session: basic-rssi-invalid +- **Status**: [OPEN] +- **Issue**: O modo basico via RSSI conecta ao backend e ao Observatory, mas o sinal chega com qualidade invalida ou quase nula (`quality_verdict: Deny`, `signal_quality_score: 0`, `mean_rssi: -100`, `variance: 0`, `motion_band_power: 0`), impedindo deteccao humana confiavel. +- **Debug Server**: http://127.0.0.1:7777/event +- **Log File**: .dbg/trae-debug-log-basic-rssi-invalid.ndjson + +## Reproduction Steps +1. Iniciar o RuView em Windows pelo `start.bat` com `--source wifi`. +2. Abrir `http://127.0.0.1:3000/ui/observatory.html`. +3. Observar o painel de sinal e a presenca no modo basico. +4. Comparar os snapshots de `GET /api/v1/sensing/latest` durante o teste. + +## Hypotheses & Verification +| ID | Hypothesis | Likelihood | Effort | Evidence | +|----|------------|------------|--------|----------| +| A | O coletor RSSI do Windows nao esta conseguindo ler amostras reais do adaptador e cai em valores sentinela/degenerados. | High | Low | Pending | +| B | O pipeline recebe amostras, mas a filtragem/deduplicacao zera a variancia e o motion score antes da classificacao. | High | Medium | Pending | +| C | O adaptador/rede atual entrega dados somente do AP conectado, com atualizacao lenta ou congelada, abaixo do minimo esperado pelo algoritmo. | Medium | Medium | Pending | +| D | O modo `wifi` esta pegando a interface errada ou um estado stale do Windows, produzindo snapshots validos do ponto de vista tecnico, mas sem relacao com o modem/rede testados. | Medium | Medium | Pending | +| E | A classificacao/adaptive classifier do modo basico esta inferindo `presence` de forma otimista mesmo quando o sinal base esta invalido. | Medium | Low | Pending | + +## Log Evidence +Instrumentacao ativa em: +- `main.rs:parse_netsh_interfaces_output` -> hipotese A +- `main.rs:wifi_task` -> hipoteses B/C +- `main.rs:windows_wifi_fallback_tick` -> hipotese D +- `main.rs:derive_pose_from_sensing` -> hipotese E + +- Evidencia pre-fix do parser localizado: + - `.dbg/trae-debug-log-basic-rssi-invalid.ndjson:995-1000` mostrou a transicao de `mean_rssi = -100` para `mean_rssi ~= -57/-58` quando o parser PT-BR passou a entender `Sinal`, `Banda`, `Canal` e `Tipo de Radio`. +- Evidencia de runtime apos reiniciar com o binario atualizado: + - `.dbg/trae-debug-log-basic-rssi-invalid.ndjson:2245-2264` mostra `observations = 1`, `bssid_count = 3`, `mean_rssi ~= -58`, `quality_verdict = Permit` e `source = wifi:JHON_5G`. +- Snapshot HTTP apos o ajuste de agregacao: + - `GET /api/v1/sensing/latest` passou a responder com `features.mean_rssi = -59.0` e `nodes[0].rssi_dbm = -59.0`, em vez de propagar `-100`. +- Snapshot HTTP apos o ajuste pratico para hardware comum: + - `GET /api/v1/sensing/latest` passou a responder com `source = wifi:JHON_5G`, `bssid_count = 1`, `quality_verdict = Warn`, `signal_quality_score ~= 0.25`, `estimated_persons = 1` e `presence = true`. + +Correcao minima aplicada apos evidencia: +- `wifi-densepose-wifiscan/src/adapter/netsh_scanner.rs` + - parser do `netsh wlan show networks mode=bssid` agora aceita rotulos PT-BR. +- `wifi-densepose-sensing-server/src/main.rs` + - o frame multi-BSSID deixou de herdar cegamente o RSSI da primeira observacao bruta; + - o snapshot agora usa um RSSI representativo do frame agregado; + - a expiracao do `BssidRegistry` no modo `netsh` foi reduzida de 30s para 10s para diminuir BSSIDs stale em testes locais. + - o modo Wi-Fi basico agora limita a contagem de pessoas em cenarios de baixa visibilidade (`1-2` observacoes vivas); + - o `quality_verdict` do modo basico pode subir de `Deny` para `Warn` quando houver sinal real suficiente para uso pratico, sem fingir qualidade de CSI. + +## Verification Conclusion +| ID | Hypothesis | Status | Evidence | +|----|------------|--------|----------| +| A | O coletor RSSI do Windows nao esta conseguindo ler amostras reais do adaptador e cai em valores sentinela/degenerados. | Parcialmente confirmada | O adaptador nao era o problema principal; o parser PT-BR fazia parte do caminho cair em `-100`. Depois do fix, o runtime passou a mostrar `-58/-59` em vez de `-100`. | +| B | O pipeline recebe amostras, mas a filtragem/deduplicacao zera a variancia e o motion score antes da classificacao. | Rejeitada como causa principal | Apos o fix, `variance`, `motion_band_power` e `spectral_power` ficaram positivos e estaveis; o pipeline nao estava zerando tudo por si so. | +| C | O adaptador/rede atual entrega dados somente do AP conectado, com atualizacao lenta ou congelada, abaixo do minimo esperado pelo algoritmo. | Parcialmente confirmada | Os logs mais recentes mostram `observations = 1` enquanto o frame ainda mantem `bssid_count = 3` por janela curta de historico/cache. O modo basico continua dependente de scans lentos e pouco ricos. | +| D | O modo `wifi` esta pegando a interface errada ou um estado stale do Windows, produzindo snapshots validos do ponto de vista tecnico, mas sem relacao com o modem/rede testados. | Inconclusiva | Houve sinais de cache/staleness, mas nao ficou provado que a interface esteja errada. O principal problema observado foi localizacao + agregacao de RSSI. | +| E | A classificacao/adaptive classifier do modo basico esta inferindo `presence` de forma otimista mesmo quando o sinal base esta invalido. | Confirmada historicamente, mitigada | Antes, havia presenca com `-100`. Agora os campos principais do snapshot deixam de carregar esse RSSI invalido, e a UI ja havia recebido gating visual para nao exagerar a confianca. | + +Diagnostico atual: +- O bug do `-100 fantasma` foi resolvido em dois niveis: + 1. parser localizado do `netsh`; + 2. agregacao incorreta entre `first_rssi` e frame multi-BSSID. +- O que sobra agora e uma limitacao estrutural do modo basico no Windows: + - scans lentos, + - poucos pontos observaveis, + - historico curto ainda sujeito a staleness/cache do `netsh`. +- A sessao continua `[OPEN]` ate confirmacao sua em uso real do Observatory, porque ainda precisamos validar a sensacao final na interface. diff --git a/start.bat b/start.bat new file mode 100644 index 0000000000..07f288d849 --- /dev/null +++ b/start.bat @@ -0,0 +1,16 @@ +@echo off +setlocal +chcp 65001 >nul + +set "SCRIPT_PATH=%~dp0start_ruview.ps1" +set "RUVIEW_CLOSE_CONFLICTS=1" + +if not exist "%SCRIPT_PATH%" ( + echo [ERRO] Nao encontrei o launcher PowerShell do RuView. + echo [ERRO] Caminho esperado: "%SCRIPT_PATH%" + pause + exit /b 1 +) + +powershell -NoLogo -NoExit -ExecutionPolicy Bypass -File "%SCRIPT_PATH%" -CloseConflicts +exit /b %errorlevel% diff --git a/start_ruview.ps1 b/start_ruview.ps1 new file mode 100644 index 0000000000..6aa1b54ef6 --- /dev/null +++ b/start_ruview.ps1 @@ -0,0 +1,687 @@ +param( + [switch]$CloseConflicts +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) + +$RootDir = $PSScriptRoot +$V2Dir = Join-Path $RootDir "v2" +$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss" +$UiUrl = "http://127.0.0.1:3000/ui/index.html" +$UiLaunchUrl = "{0}?boot={1}" -f $UiUrl, $Timestamp +$ApiUrl = "http://127.0.0.1:3000/api/v1/sensing/latest" +$HttpPort = 3000 +$WsPort = 3001 +$UdpPort = 5005 +$TickMs = 500 +$LogDir = Join-Path $RootDir "data\launcher-logs" +$LogPath = Join-Path $LogDir "ruview-launcher-$Timestamp.log" +$script:BrowserOpened = $false +$script:LastUiProbeError = $null +$script:ServerProcess = $null +$script:ProcessJobHandle = [IntPtr]::Zero + +New-Item -ItemType Directory -Force -Path $LogDir | Out-Null + +if (-not ("RuViewProcessJob" -as [type])) { + Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; + +public static class RuViewProcessJob +{ + public const int JobObjectExtendedLimitInformation = 9; + public const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public UIntPtr Affinity; + public uint PriorityClass; + public uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IO_COUNTERS + { + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetInformationJobObject( + IntPtr hJob, + int JobObjectInfoClass, + IntPtr lpJobObjectInfo, + uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr handle); +} +"@ +} + +function Write-LauncherLog { + param( + [Parameter(Mandatory = $true)][string]$Level, + [Parameter(Mandatory = $true)][string]$Message + ) + + $timestamp = Get-Date -Format "HH:mm:ss" + $line = "$timestamp [$Level] $Message" + + switch ($Level) { + "HEADER" { $emoji = "[RU]"; $color = "Cyan" } + "STEP" { $emoji = "[>>]"; $color = "Cyan" } + "INFO" { $emoji = "[i]"; $color = "Gray" } + "SUCCESS" { $emoji = "[OK]"; $color = "Green" } + "WARN" { $emoji = "[!]"; $color = "Yellow" } + "ERROR" { $emoji = "[X]"; $color = "Red" } + "SERVER" { $emoji = "[~]"; $color = "White" } + default { $emoji = "[ ]"; $color = "White" } + } + + Add-Content -Path $LogPath -Value $line + Write-Host "$emoji $line" -ForegroundColor $color +} + +function Test-RuViewUi { + try { + $response = Invoke-WebRequest -UseBasicParsing -Uri $UiUrl -Method Head -TimeoutSec 2 + $script:LastUiProbeError = $null + return ($response.StatusCode -eq 200) + } + catch { + $script:LastUiProbeError = $_.Exception.Message + return $false + } +} + +function Get-BluetoothDiagnostics { + $service = Get-Service bthserv -ErrorAction SilentlyContinue + $adapter = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | + Where-Object { $_.FriendlyName -and $_.FriendlyName -notmatch 'Enumerator' } | + Select-Object -First 1 + + [PSCustomObject]@{ + AdapterPresent = [bool]$adapter + AdapterName = if ($adapter) { $adapter.FriendlyName } else { $null } + ServiceRunning = [bool]($service -and $service.Status -eq 'Running') + HelperMode = 'experimental' + } +} + +function Get-CargoCommandPath { + $cargo = Get-Command cargo -ErrorAction SilentlyContinue + if (-not $cargo) { + throw "O comando 'cargo' nao foi encontrado no sistema." + } + return $cargo.Source +} + +function Initialize-ProcessLifetimeGuard { + if ($script:ProcessJobHandle -ne [IntPtr]::Zero) { + return + } + + $jobHandle = [RuViewProcessJob]::CreateJobObject([IntPtr]::Zero, "RuViewLauncher-$PID") + if ($jobHandle -eq [IntPtr]::Zero) { + throw "Nao foi possivel criar a protecao de ciclo de vida do processo." + } + + $jobInfo = New-Object RuViewProcessJob+JOBOBJECT_EXTENDED_LIMIT_INFORMATION + $jobInfo.BasicLimitInformation.LimitFlags = [RuViewProcessJob]::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + $jobInfoSize = [System.Runtime.InteropServices.Marshal]::SizeOf([type][RuViewProcessJob+JOBOBJECT_EXTENDED_LIMIT_INFORMATION]) + $jobInfoPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($jobInfoSize) + + try { + [System.Runtime.InteropServices.Marshal]::StructureToPtr($jobInfo, $jobInfoPtr, $false) + $configured = [RuViewProcessJob]::SetInformationJobObject( + $jobHandle, + [RuViewProcessJob]::JobObjectExtendedLimitInformation, + $jobInfoPtr, + [uint32]$jobInfoSize + ) + + if (-not $configured) { + throw "Nao foi possivel configurar o encerramento automatico do processo filho." + } + } + finally { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($jobInfoPtr) + } + + $script:ProcessJobHandle = $jobHandle +} + +function Add-ProcessToLifetimeGuard { + param( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process]$Process + ) + + Initialize-ProcessLifetimeGuard + + $assigned = [RuViewProcessJob]::AssignProcessToJobObject($script:ProcessJobHandle, $Process.Handle) + if (-not $assigned) { + throw "Nao foi possivel vincular o servidor ao ciclo de vida desta janela." + } +} + +function Stop-RuViewRuntime { + param( + [string]$Reason = "Encerrando instancia atual do RuView." + ) + + if ($script:ServerProcess -and -not $script:ServerProcess.HasExited) { + Write-LauncherLog "WARN" $Reason + try { + & taskkill /PID $script:ServerProcess.Id /T /F | Out-Null + } + catch { + try { + Stop-Process -Id $script:ServerProcess.Id -Force -ErrorAction Stop + } + catch { + Write-LauncherLog "WARN" "Nao consegui encerrar o processo principal automaticamente: $($_.Exception.Message)" + } + } + } +} + +function Get-RuViewRelatedProcesses { + $currentPid = $PID + Get-CimInstance Win32_Process | Where-Object { + $_.ProcessId -ne $currentPid -and ( + $_.Name -in @("sensing-server.exe", "cargo.exe", "powershell.exe", "pwsh.exe") -or + $_.CommandLine -match "wifi-densepose-sensing-server|sensing-server\.exe|start_ruview" + ) + } +} + +function Stop-ProcessTree { + param( + [Parameter(Mandatory = $true)] + [int]$ProcessId + ) + + try { + & taskkill /PID $ProcessId /T /F | Out-Null + return $true + } + catch { + try { + Stop-Process -Id $ProcessId -Force -ErrorAction Stop + return $true + } + catch { + return $false + } + } +} + +function Test-PortsFree { + param( + [int[]]$Ports + ) + + $listeners = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | + Where-Object { $Ports -contains $_.LocalPort } + + return ($null -eq $listeners -or $listeners.Count -eq 0) +} + +function Wait-ForPortsFree { + param( + [int[]]$Ports, + [int]$TimeoutSeconds = 15 + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (Test-PortsFree -Ports $Ports) { + return $true + } + + Start-Sleep -Milliseconds 400 + } + + return (Test-PortsFree -Ports $Ports) +} + +function Stop-RuViewConflicts { + param( + [int[]]$Ports + ) + + $killed = @{} + + foreach ($port in $Ports) { + $listeners = Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue + foreach ($listener in $listeners) { + $procId = $listener.OwningProcess + if (-not $procId -or $procId -eq $PID -or $killed.ContainsKey($procId)) { + continue + } + + try { + $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $procId" -ErrorAction Stop + Write-LauncherLog "WARN" "Encerrando processo na porta ${port}: PID $procId ($($proc.Name))" + if (Stop-ProcessTree -ProcessId $procId) { + $killed[$procId] = $true + Start-Sleep -Milliseconds 400 + } + else { + Write-LauncherLog "WARN" "Nao consegui encerrar a arvore do PID $procId preso na porta ${port}." + } + } + catch { + Write-LauncherLog "WARN" "Nao consegui encerrar o PID $procId preso na porta ${port}: $($_.Exception.Message)" + } + } + } + + $related = Get-RuViewRelatedProcesses + foreach ($proc in $related) { + if ($killed.ContainsKey($proc.ProcessId)) { + continue + } + + $commandLine = if ($null -ne $proc.CommandLine) { $proc.CommandLine } else { "" } + $isRuViewProcess = ( + $proc.Name -eq "sensing-server.exe" -or + $commandLine -match "wifi-densepose-sensing-server|sensing-server\.exe|start_ruview" + ) + + if (-not $isRuViewProcess) { + continue + } + + try { + Write-LauncherLog "WARN" "Encerrando instancia antiga: PID $($proc.ProcessId) ($($proc.Name))" + if (Stop-ProcessTree -ProcessId $proc.ProcessId) { + $killed[$proc.ProcessId] = $true + Start-Sleep -Milliseconds 400 + } + else { + Write-LauncherLog "WARN" "Nao consegui encerrar a arvore do PID $($proc.ProcessId)." + } + } + catch { + Write-LauncherLog "WARN" "Nao consegui encerrar o PID $($proc.ProcessId): $($_.Exception.Message)" + } + } + + if ($killed.Count -gt 0) { + Write-LauncherLog "SUCCESS" "Limpeza concluida. Processos encerrados: $($killed.Count)" + } + else { + Write-LauncherLog "INFO" "Nenhum processo antigo do RuView foi encontrado." + } + + if (-not (Wait-ForPortsFree -Ports $Ports -TimeoutSeconds 15)) { + $blocked = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | + Where-Object { $Ports -contains $_.LocalPort } | + Select-Object -ExpandProperty LocalPort -Unique + throw "As portas do RuView ainda estao ocupadas apos a limpeza: $($blocked -join ', ')." + } + + Write-LauncherLog "SUCCESS" "Portas do RuView livres e prontas para uma nova inicializacao." +} + +function Convert-ServerLine { + param([string]$Line) + + $trimmed = $Line.Trim() + if ([string]::IsNullOrWhiteSpace($trimmed)) { + return + } + + if ($trimmed -match "^warning:") { + Write-LauncherLog "WARN" "Cargo: $trimmed" + return + } + + if ($trimmed -match "^error(\[[^\]]+\])?:") { + Write-LauncherLog "ERROR" "Cargo: $trimmed" + return + } + + if ($trimmed -match "^\s*Compiling\s+(.+)$") { + Write-LauncherLog "STEP" "Compilando componente: $($Matches[1])" + return + } + + if ($trimmed -match "^\s*Finished\s+.+target\(s\)\s+in\s+(.+)$") { + Write-LauncherLog "SUCCESS" "Compilacao concluida em $($Matches[1])." + return + } + + if ($trimmed -match "^\s*Running\s+(.+)$") { + Write-LauncherLog "STEP" "Executando: $($Matches[1])" + return + } + + if ($trimmed -like "*INFO sensing_server:*HTTP:*") { + $httpValue = ($trimmed.Substring($trimmed.IndexOf("HTTP:") + 5)).Trim() + Write-LauncherLog "SUCCESS" "Interface HTTP disponivel em $httpValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*WebSocket:*") { + $wsValue = ($trimmed.Substring($trimmed.IndexOf("WebSocket:") + 10)).Trim() + Write-LauncherLog "INFO" "WebSocket ativo em $wsValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*UDP:*") { + $udpValue = ($trimmed.Substring($trimmed.IndexOf("UDP:") + 4)).Trim() + Write-LauncherLog "INFO" "Escuta UDP preparada em $udpValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*UI path:*") { + $uiValue = ($trimmed.Substring($trimmed.IndexOf("UI path:") + 8)).Trim() + Write-LauncherLog "INFO" "Pasta da interface: $uiValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*Source:*") { + $sourceValue = ($trimmed.Substring($trimmed.IndexOf("Source:") + 7)).Trim() + Write-LauncherLog "INFO" "Fonte de dados selecionada: $sourceValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*Data source:*") { + $dataValue = ($trimmed.Substring($trimmed.IndexOf("Data source:") + 12)).Trim() + Write-LauncherLog "INFO" "Motor de captura: $dataValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*Vital sign detector sample rate:*") { + $sampleValue = ($trimmed.Substring($trimmed.IndexOf("Vital sign detector sample rate:") + 32)).Trim() + Write-LauncherLog "INFO" "Taxa do detector de sinais vitais: $sampleValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*WiFi-DensePose Sensing Server*") { + Write-LauncherLog "SUCCESS" "Servidor RuView iniciado." + return + } + + if ($trimmed -match "INFO sensing_server:\s+HTTP:\s+(.+)$") { + Write-LauncherLog "SUCCESS" "Interface HTTP disponivel em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+WebSocket:\s+(.+)$") { + Write-LauncherLog "INFO" "WebSocket ativo em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+UDP:\s+(.+)$") { + Write-LauncherLog "INFO" "Escuta UDP preparada em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+UI path:\s+(.+)$") { + Write-LauncherLog "INFO" "Pasta da interface: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Source:\s+(.+)$") { + Write-LauncherLog "INFO" "Fonte de dados selecionada: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Data source:\s+(.+)$") { + Write-LauncherLog "INFO" "Motor de captura: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Vital sign detector sample rate:\s+(.+)$") { + Write-LauncherLog "INFO" "Taxa do detector de sinais vitais: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Loaded runtime config:\s+(.+)$") { + Write-LauncherLog "INFO" "Configuracao carregada: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Loaded adaptive classifier:\s+(.+)$") { + Write-LauncherLog "INFO" "Classificador adaptativo carregado: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+WiFi RSSI pipeline active\s+\((.+)\)$") { + Write-LauncherLog "SUCCESS" "Pipeline Wi-Fi RSSI ativo ($($Matches[1]))" + return + } + + if ($trimmed -match "INFO sensing_server:\s+HTTP server listening on\s+(.+)$") { + Write-LauncherLog "SUCCESS" "Servidor HTTP ouvindo em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+WebSocket server listening on\s+(.+)$") { + Write-LauncherLog "SUCCESS" "Servidor WebSocket ouvindo em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Open\s+(.+)\s+in your browser$") { + Write-LauncherLog "INFO" "Abra no navegador: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+API auth: OFF") { + Write-LauncherLog "WARN" "Autenticacao da API desativada para ambiente local." + return + } + + if ($trimmed -match "INFO sensing_server:\s+RuView discovery skipped") { + Write-LauncherLog "INFO" "Descoberta local ignorada porque o bind esta apenas em loopback." + return + } + + if ($trimmed -match "INFO wifi_densepose_sensing_server::browser_session:\s+browser session secret: loaded path=(.+)$") { + Write-LauncherLog "INFO" "Chave da sessao do navegador carregada de: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Discovered\s+(\d+)\s+model files,\s+(\d+)\s+recording files$") { + Write-LauncherLog "INFO" "Arquivos encontrados: modelos=$($Matches[1]) | gravacoes=$($Matches[2])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Host-header validation ON") { + Write-LauncherLog "INFO" "Validacao de host-header ativada." + return + } + + if ($trimmed -match "WARN") { + Write-LauncherLog "WARN" $trimmed + return + } + + if ($trimmed -match "ERROR|thread 'main' panicked|panicked at") { + Write-LauncherLog "ERROR" $trimmed + return + } + + Write-LauncherLog "SERVER" $trimmed +} + +function Start-RuViewBrowser { + if (-not $script:BrowserOpened) { + try { + Start-Process -FilePath $UiLaunchUrl -ErrorAction Stop | Out-Null + $script:BrowserOpened = $true + Write-LauncherLog "SUCCESS" "Interface aberta no navegador." + } + catch { + Write-LauncherLog "WARN" "Nao consegui abrir o navegador automaticamente: $($_.Exception.Message)" + Write-LauncherLog "INFO" "Abra manualmente: $UiLaunchUrl" + } + + Write-LauncherLog "INFO" "URL da interface: $UiLaunchUrl" + Write-LauncherLog "INFO" "API local: $ApiUrl" + } +} + +try { + Write-LauncherLog "HEADER" "RuView Launcher PT-BR" + Write-LauncherLog "INFO" "Log desta sessao: $LogPath" + Write-LauncherLog "STEP" "Validando estrutura do projeto..." + + if (-not (Test-Path (Join-Path $V2Dir "Cargo.toml"))) { + throw "Nao encontrei a pasta 'v2' do projeto em '$V2Dir'." + } + + $cargoPath = Get-CargoCommandPath + Write-LauncherLog "SUCCESS" "Cargo localizado em: $cargoPath" + + if ($CloseConflicts -or $env:RUVIEW_CLOSE_CONFLICTS -eq "1") { + Write-LauncherLog "STEP" "Encerrando instancias antigas e portas em conflito..." + Stop-RuViewConflicts -Ports @($HttpPort, $WsPort, 8765) + } + + if (Test-RuViewUi) { + Write-LauncherLog "SUCCESS" "O RuView ja esta respondendo na porta $HttpPort." + Start-RuViewBrowser + Write-LauncherLog "INFO" "Nenhuma nova instancia foi iniciada." + return + } + + Write-LauncherLog "STEP" "Preparando subida do servidor..." + Write-LauncherLog "INFO" "Modo: basico Wi-Fi (RSSI-only)" + Write-LauncherLog "INFO" "Portas esperadas: HTTP=$HttpPort | WS=$WsPort | UDP=$UdpPort" + Write-LauncherLog "INFO" "Intervalo de leitura: ${TickMs}ms" + + $bluetooth = Get-BluetoothDiagnostics + if ($bluetooth.AdapterPresent) { + Write-LauncherLog "INFO" "Bluetooth auxiliar detectado: $($bluetooth.AdapterName)" + } + else { + Write-LauncherLog "WARN" "Nenhum adaptador Bluetooth utilizavel foi detectado para o modo auxiliar." + } + + if ($bluetooth.ServiceRunning) { + Write-LauncherLog "INFO" "Servico Bluetooth ativo. O modo auxiliar experimental pode ser consultado pela interface." + } + else { + Write-LauncherLog "WARN" "Servico Bluetooth inativo. O RuView segue funcionando, mas sem o apoio auxiliar experimental." + } + + $arguments = "run -p wifi-densepose-sensing-server -- --source wifi --http-port $HttpPort --ws-port $WsPort --tick-ms $TickMs" + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $cargoPath + $psi.Arguments = $arguments + $psi.WorkingDirectory = $V2Dir + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.UTF8Encoding]::new($false) + $psi.StandardErrorEncoding = [System.Text.UTF8Encoding]::new($false) + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $psi + $process.EnableRaisingEvents = $true + + $stdoutEvent = Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -Action { + if ($EventArgs.Data) { + Convert-ServerLine -Line $EventArgs.Data + } + } + + $stderrEvent = Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -Action { + if ($EventArgs.Data) { + Convert-ServerLine -Line $EventArgs.Data + } + } + + Write-LauncherLog "STEP" "Iniciando processo do servidor..." + $null = $process.Start() + $script:ServerProcess = $process + Add-ProcessToLifetimeGuard -Process $process + $process.BeginOutputReadLine() + $process.BeginErrorReadLine() + + Write-LauncherLog "SUCCESS" "Processo iniciado com PID $($process.Id)." + Write-LauncherLog "INFO" "Aguarde alguns segundos enquanto o projeto compila/inicializa." + Write-LauncherLog "INFO" "Esta janela permanece visivel durante toda a execucao." + Write-LauncherLog "INFO" "Se voce fechar esta janela, o RuView sera encerrado junto." + + $deadline = (Get-Date).AddSeconds(90) + while (-not $process.HasExited) { + if (-not $script:BrowserOpened -and (Test-RuViewUi)) { + Start-RuViewBrowser + } + + if (-not $script:BrowserOpened -and (Get-Date) -ge $deadline) { + Write-LauncherLog "WARN" "A interface ainda nao respondeu dentro de 90 segundos." + Write-LauncherLog "WARN" "Se a compilacao ainda estiver rolando, aguarde mais um pouco." + $deadline = (Get-Date).AddYears(10) + } + + Start-Sleep -Seconds 2 + } + + Start-Sleep -Milliseconds 500 + + if ($process.ExitCode -eq 0) { + Write-LauncherLog "SUCCESS" "Servidor encerrado normalmente." + } + else { + Write-LauncherLog "ERROR" "Servidor encerrado com falha. Codigo de saida: $($process.ExitCode)" + if ($script:LastUiProbeError) { + Write-LauncherLog "WARN" "Ultimo erro ao testar a interface: $script:LastUiProbeError" + } + } +} +catch { + Write-LauncherLog "ERROR" $_.Exception.Message +} +finally { + Stop-RuViewRuntime -Reason "Fechando launcher: encerrando o RuView junto com esta janela." + + Get-EventSubscriber | Where-Object { + $_.SourceObject -is [System.Diagnostics.Process] + } | Unregister-Event -Force -ErrorAction SilentlyContinue + + if ($script:ProcessJobHandle -ne [IntPtr]::Zero) { + [RuViewProcessJob]::CloseHandle($script:ProcessJobHandle) | Out-Null + $script:ProcessJobHandle = [IntPtr]::Zero + } + + Write-LauncherLog "INFO" "Janela pronta para diagnostico. Feche quando quiser." +} diff --git a/ui/app.js b/ui/app.js index 55e4f741eb..4afb6a3dad 100644 --- a/ui/app.js +++ b/ui/app.js @@ -29,6 +29,10 @@ import { ScreenshotTool } from './utils/screenshot.js'; import { UptimeClock } from './utils/uptime-clock.js'; import { QuickSettings } from './utils/quick-settings.js'; import { dataSourceBanner } from './utils/data-source-banner.js'; +import { + enforceLocalDevFreshness, + shouldBypassPersistentCaching +} from './utils/local-dev-cache-guard.js'; class WiFiDensePoseApp { constructor() { @@ -296,6 +300,11 @@ class WiFiDensePoseApp { // Register service worker for offline capability registerServiceWorker() { + if (shouldBypassPersistentCaching()) { + console.info('Local loopback detected - skipping service worker registration'); + return; + } + if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js').then(reg => { console.info('Service worker registered:', reg.scope); @@ -487,11 +496,27 @@ class WiFiDensePoseApp { } } -// Initialize app when DOM is ready -document.addEventListener('DOMContentLoaded', () => { - window.wifiDensePoseApp = new WiFiDensePoseApp(); - window.wifiDensePoseApp.init(); -}); +async function bootWiFiDensePoseApp() { + if (window.wifiDensePoseApp?.isReady?.()) { + return; + } + + if (!window.wifiDensePoseApp) { + window.wifiDensePoseApp = new WiFiDensePoseApp(); + } + + await enforceLocalDevFreshness(); + await window.wifiDensePoseApp.init(); +} + +// Initialize app reliably even when this module is loaded via dynamic import. +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + bootWiFiDensePoseApp(); + }, { once: true }); +} else { + bootWiFiDensePoseApp(); +} // Export for testing -export { WiFiDensePoseApp }; \ No newline at end of file +export { WiFiDensePoseApp }; diff --git a/ui/components/DashboardTab.js b/ui/components/DashboardTab.js index 5e505fa99f..ae4907582a 100644 --- a/ui/components/DashboardTab.js +++ b/ui/components/DashboardTab.js @@ -3,6 +3,8 @@ import { healthService } from '../services/health.service.js'; import { poseService } from '../services/pose.service.js'; import { sensingService } from '../services/sensing.service.js'; +import { apiService } from '../services/api.service.js'; +import { i18n } from '../utils/i18n.js'; export class DashboardTab { constructor(containerElement) { @@ -15,6 +17,19 @@ export class DashboardTab { // Initialize component async init() { this.cacheElements(); + this.wireManualForm(); + this.wireBluetoothToggle(); + this._localeUnsub = i18n.onLocaleChange(() => { + i18n.applyTranslations(this.container); + this.updateDataSourceIndicator(); + if (this._lastHealth) this.updateHealthStatus(this._lastHealth); + if (this._lastHybrid) this.updateHybridSummary(this._lastHybrid); + if (this._lastBluetoothStatus) this.updateBluetoothStatus(this._lastBluetoothStatus); + this.renderKnownDeviceRegistry( + this._lastHybrid?.network_devices || [], + this._lastHybridOverrides || [] + ); + }); await this.loadInitialData(); this.startMonitoring(); } @@ -50,6 +65,7 @@ export class DashboardTab { // Get current stats const stats = await poseService.getStats(1); this.updateStats(stats); + await this.refreshHybridSummary(); } catch (error) { // DensePose API may not be running (sensing-only mode) — fail silently @@ -92,11 +108,11 @@ export class DashboardTab { const statusText = el.querySelector('.status-text'); const statusMsg = el.querySelector('.status-message'); const config = { - 'live': { text: 'ESP32', status: 'healthy', msg: 'Real hardware connected' }, - 'server-simulated': { text: 'SIMULATED', status: 'warning', msg: 'Server running without hardware' }, - 'reconnecting': { text: 'RECONNECTING', status: 'degraded', msg: 'Attempting to connect...' }, - 'unreachable': { text: 'NO DATA', status: 'unhealthy', msg: 'Server unreachable — readings below are stale' }, - 'simulated': { text: 'INVENTED', status: 'unhealthy', msg: 'Browser-generated data, not measured' }, + 'live': { text: 'ESP32', status: 'healthy', msg: i18n.t('status.realHardwareConnected') }, + 'server-simulated': { text: i18n.t('common.simulated').toUpperCase(), status: 'warning', msg: i18n.t('status.serverRunningWithoutHardware') }, + 'reconnecting': { text: i18n.t('common.reconnecting').toUpperCase(), status: 'degraded', msg: i18n.t('status.attemptingConnect') }, + 'unreachable': { text: 'NO DATA', status: 'unhealthy', msg: i18n.t('status.serverUnreachableStale') }, + 'simulated': { text: 'INVENTED', status: 'unhealthy', msg: i18n.t('status.browserGeneratedData') }, }; const cfg = config[ds] || config['reconnecting']; el.className = `component-status status-${cfg.status}`; @@ -154,12 +170,13 @@ export class DashboardTab { // Update health status updateHealthStatus(health) { if (!health) return; + this._lastHealth = health; // Update overall status const overallStatus = this.container.querySelector('.overall-health'); if (overallStatus) { overallStatus.className = `overall-health status-${health.status}`; - overallStatus.textContent = health.status.toUpperCase(); + overallStatus.textContent = i18n.t(`common.${health.status}`) || health.status.toUpperCase(); } // Update component statuses @@ -210,11 +227,11 @@ export class DashboardTab { const apiStatusMessage = apiElement.querySelector('.status-message'); if (apiStatusText) { - apiStatusText.textContent = 'HEALTHY'; + apiStatusText.textContent = i18n.t('common.healthy').toUpperCase(); } if (apiStatusMessage) { - apiStatusMessage.textContent = 'API server is running normally'; + apiStatusMessage.textContent = i18n.t('status.apiRunningNormally'); } } } @@ -280,6 +297,7 @@ export class DashboardTab { // Get zones summary const zonesSummary = await poseService.getZonesSummary(); this.updateZonesDisplay(zonesSummary); + await this.refreshHybridSummary(); } catch (error) { console.error('Failed to update live stats:', error); @@ -344,7 +362,7 @@ export class DashboardTab { const zoneCountSpan = document.createElement('span'); zoneCountSpan.className = 'zone-count'; - zoneCountSpan.textContent = 'undefined'; + zoneCountSpan.textContent = i18n.t('common.undefined'); zoneElement.appendChild(zoneNameSpan); zoneElement.appendChild(zoneCountSpan); @@ -389,6 +407,280 @@ export class DashboardTab { } } + async refreshHybridSummary() { + try { + const [hybrid, overrides, bluetooth] = await Promise.all([ + apiService.get('/api/v1/hybrid/latest'), + apiService.get('/api/v1/hybrid/overrides').catch(() => ({ items: [] })), + apiService.get('/api/v1/bluetooth/status').catch(() => null) + ]); + this._lastHybridOverrides = overrides?.items || []; + this._lastBluetoothStatus = bluetooth; + this.updateHybridSummary(hybrid); + this.updateBluetoothStatus(bluetooth); + this.renderKnownDeviceRegistry(hybrid?.network_devices || [], this._lastHybridOverrides); + } catch (error) { + console.log('Dashboard: hybrid snapshot not available yet'); + } + } + + updateHybridSummary(hybrid) { + if (!hybrid) return; + this._lastHybrid = hybrid; + + const fusion = hybrid.fusion || {}; + const devices = hybrid.network_devices || []; + + const setText = (selector, value) => { + const el = this.container.querySelector(selector); + if (el) el.textContent = String(value); + }; + + setText('.hybrid-humans', fusion.estimated_humans || 0); + setText('.hybrid-devices', devices.length); + setText('.hybrid-smartphones', fusion.likely_smartphones || 0); + setText('.hybrid-smarttvs', fusion.likely_smart_tvs || 0); + + const list = this.container.querySelector('.hybrid-device-list'); + if (!list) return; + list.innerHTML = ''; + + if (devices.length === 0) { + const empty = document.createElement('div'); + empty.className = 'hybrid-device-item'; + empty.textContent = i18n.t('hybrid.empty'); + list.appendChild(empty); + return; + } + + devices.slice(0, 12).forEach(device => { + const item = document.createElement('div'); + item.className = 'hybrid-device-item'; + + const name = document.createElement('div'); + name.className = 'hybrid-device-name'; + name.textContent = device.display_name || device.hostname || device.ip; + + const category = document.createElement('div'); + category.className = 'hybrid-device-meta'; + const badge = device.manual_override ? i18n.t('hybrid.manualBadge') : i18n.t('hybrid.autoBadge'); + category.textContent = `${i18n.t(`hybrid.category.${device.category}`)} • ${Math.round((device.confidence || 0) * 100)}% • ${badge}`; + + const meta = document.createElement('div'); + meta.className = 'hybrid-device-meta'; + meta.textContent = [device.vendor, device.ip, device.mac].filter(Boolean).join(' • '); + + item.appendChild(name); + item.appendChild(category); + item.appendChild(meta); + list.appendChild(item); + }); + } + + updateBluetoothStatus(status) { + if (!status) return; + + const availability = this.container.querySelector('.hybrid-bluetooth-availability'); + const mode = this.container.querySelector('.hybrid-bluetooth-mode'); + const adapter = this.container.querySelector('.hybrid-bluetooth-adapter'); + const adapterName = this.container.querySelector('.hybrid-bluetooth-adapter-name'); + const service = this.container.querySelector('.hybrid-bluetooth-service'); + const serviceNote = this.container.querySelector('.hybrid-bluetooth-service-note'); + const note = this.container.querySelector('.hybrid-bluetooth-note'); + const toggle = this.container.querySelector('.hybrid-bluetooth-toggle'); + + if (availability) availability.textContent = status.enabled + ? (status.effective ? i18n.t('common.active') : i18n.t('common.warning')) + : i18n.t('common.inactive'); + if (mode) mode.textContent = status.helper_mode || 'experimental'; + if (adapter) adapter.textContent = status.adapter_present ? i18n.t('common.connected') : i18n.t('common.disconnected'); + if (adapterName) adapterName.textContent = status.adapter_name || i18n.t('common.unknown'); + if (service) service.textContent = status.service_running ? i18n.t('common.active') : i18n.t('common.inactive'); + if (serviceNote) serviceNote.textContent = status.service_running + ? i18n.t('hybrid.bluetoothServiceRunning') + : i18n.t('hybrid.bluetoothServiceStopped'); + if (note) note.textContent = status.note || i18n.t('hybrid.bluetoothNote'); + if (toggle) toggle.textContent = status.enabled + ? i18n.t('hybrid.bluetoothDisable') + : i18n.t('hybrid.bluetoothEnable'); + } + + wireBluetoothToggle() { + const button = this.container.querySelector('.hybrid-bluetooth-toggle'); + if (!button) return; + + button.addEventListener('click', async () => { + const nextEnabled = !this._lastBluetoothStatus?.enabled; + button.disabled = true; + try { + const bluetooth = await apiService.post('/api/v1/bluetooth/enabled', { + enabled: nextEnabled + }); + this._lastBluetoothStatus = bluetooth; + this.updateBluetoothStatus(bluetooth); + } catch (error) { + this.showError(error.message || i18n.t('common.error')); + } finally { + button.disabled = false; + } + }); + } + + wireManualForm() { + const form = this.container.querySelector('.hybrid-manual-form'); + if (!form) return; + + form.addEventListener('submit', async (event) => { + event.preventDefault(); + const formData = new FormData(form); + const payload = { + display_name: String(formData.get('display_name') || '').trim() || null, + category: String(formData.get('category') || 'unknown_device'), + match_mac: String(formData.get('match_mac') || '').trim() || null, + match_ip: String(formData.get('match_ip') || '').trim() || null, + match_hostname: String(formData.get('match_hostname') || '').trim() || null, + notes: String(formData.get('notes') || '').trim() || null + }; + + if (!payload.display_name && !payload.match_mac && !payload.match_ip && !payload.match_hostname) { + this.showError(i18n.t('hybrid.form.validation')); + return; + } + + try { + await apiService.post('/api/v1/hybrid/overrides/upsert', payload); + form.reset(); + await this.refreshHybridSummary(); + } catch (error) { + this.showError(error.message || i18n.t('common.error')); + } + }); + } + + renderKnownDeviceRegistry(devices, overrides) { + const container = this.container.querySelector('.hybrid-known-device-list'); + if (!container) return; + container.innerHTML = ''; + + const overrideMap = new Map((overrides || []).map(entry => [entry.id, entry])); + + if (!devices.length && !overrideMap.size) { + const empty = document.createElement('div'); + empty.className = 'hybrid-known-device-card'; + empty.textContent = i18n.t('hybrid.registryEmpty'); + container.appendChild(empty); + return; + } + + const matchedOverrideIds = new Set(); + devices.forEach(device => { + if (device.matched_override_id) matchedOverrideIds.add(device.matched_override_id); + container.appendChild(this.buildKnownDeviceCard(device, overrideMap.get(device.matched_override_id))); + }); + + overrides + .filter(entry => !matchedOverrideIds.has(entry.id)) + .forEach(entry => { + container.appendChild(this.buildKnownDeviceCard(null, entry)); + }); + } + + buildKnownDeviceCard(device, overrideEntry) { + const card = document.createElement('div'); + card.className = 'hybrid-known-device-card'; + + const title = document.createElement('div'); + title.className = 'hybrid-device-name'; + title.textContent = overrideEntry?.display_name || device?.display_name || device?.hostname || device?.ip || i18n.t('hybrid.manualEntry'); + + const identity = document.createElement('div'); + identity.className = 'hybrid-device-meta'; + identity.textContent = [ + device?.mac || overrideEntry?.match_mac, + device?.ip || overrideEntry?.match_ip, + device?.hostname || overrideEntry?.match_hostname + ].filter(Boolean).join(' • ') || i18n.t('hybrid.noIdentity'); + + const controls = document.createElement('div'); + controls.className = 'hybrid-known-device-controls'; + + const nameInput = document.createElement('input'); + nameInput.className = 'hybrid-manual-input'; + nameInput.value = overrideEntry?.display_name || device?.display_name || device?.hostname || ''; + nameInput.placeholder = i18n.t('hybrid.form.namePlaceholder'); + + const categorySelect = document.createElement('select'); + categorySelect.className = 'hybrid-manual-select'; + ['smartphone', 'smart_tv', 'computer', 'iot_or_media', 'unknown_device'].forEach(category => { + const option = document.createElement('option'); + option.value = category; + option.textContent = i18n.t(`hybrid.category.${category}`); + option.selected = (overrideEntry?.category || device?.category || 'unknown_device') === category; + categorySelect.appendChild(option); + }); + + const notesInput = document.createElement('input'); + notesInput.className = 'hybrid-manual-input'; + notesInput.value = overrideEntry?.notes || ''; + notesInput.placeholder = i18n.t('hybrid.form.notesPlaceholder'); + + const actions = document.createElement('div'); + actions.className = 'hybrid-known-device-actions'; + + const saveButton = document.createElement('button'); + saveButton.type = 'button'; + saveButton.className = 'hybrid-inline-btn hybrid-inline-btn--primary'; + saveButton.textContent = i18n.t('hybrid.save'); + saveButton.addEventListener('click', async () => { + const payload = { + id: overrideEntry?.id || device?.matched_override_id || '', + display_name: nameInput.value.trim() || null, + category: categorySelect.value, + match_mac: device?.mac || overrideEntry?.match_mac || null, + match_ip: device?.ip || overrideEntry?.match_ip || null, + match_hostname: device?.hostname || overrideEntry?.match_hostname || null, + notes: notesInput.value.trim() || null + }; + + try { + await apiService.post('/api/v1/hybrid/overrides/upsert', payload); + await this.refreshHybridSummary(); + } catch (error) { + this.showError(error.message || i18n.t('common.error')); + } + }); + + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'hybrid-inline-btn'; + deleteButton.textContent = i18n.t('hybrid.delete'); + deleteButton.disabled = !(overrideEntry?.id || device?.matched_override_id); + deleteButton.addEventListener('click', async () => { + const id = overrideEntry?.id || device?.matched_override_id; + if (!id) return; + try { + await apiService.delete('/api/v1/hybrid/overrides/delete', { + body: JSON.stringify({ id }) + }); + await this.refreshHybridSummary(); + } catch (error) { + this.showError(error.message || i18n.t('common.error')); + } + }); + + controls.appendChild(nameInput); + controls.appendChild(categorySelect); + controls.appendChild(notesInput); + actions.appendChild(saveButton); + actions.appendChild(deleteButton); + + card.appendChild(title); + card.appendChild(identity); + card.appendChild(controls); + card.appendChild(actions); + return card; + } + // Format feature name formatFeatureName(name) { return name.replace(/_/g, ' ') @@ -428,6 +720,7 @@ export class DashboardTab { } if (this._sensingUnsub) this._sensingUnsub(); if (this._sensingDataUnsub) this._sensingDataUnsub(); + if (this._localeUnsub) this._localeUnsub(); if (this.statsInterval) { clearInterval(this.statsInterval); @@ -435,4 +728,4 @@ export class DashboardTab { healthService.stopHealthMonitoring(); } -} \ No newline at end of file +} diff --git a/ui/components/HardwareTab.js b/ui/components/HardwareTab.js index 7f36113a93..6e62244f24 100644 --- a/ui/components/HardwareTab.js +++ b/ui/components/HardwareTab.js @@ -1,5 +1,7 @@ // Hardware Tab Component +import { i18n } from '../utils/i18n.js'; + export class HardwareTab { constructor(containerElement) { this.container = containerElement; @@ -127,9 +129,9 @@ export class HardwareTab { return div; }; - arrayStatus.appendChild(createInfoDiv('Active TX:', `${txActive}/3`)); - arrayStatus.appendChild(createInfoDiv('Active RX:', `${rxActive}/6`)); - arrayStatus.appendChild(createInfoDiv('Signal Quality:', `${this.calculateSignalQuality(txActive, rxActive)}%`)); + arrayStatus.appendChild(createInfoDiv(i18n.t('hardware.activeTx'), `${txActive}/3`)); + arrayStatus.appendChild(createInfoDiv(i18n.t('hardware.activeRx'), `${rxActive}/6`)); + arrayStatus.appendChild(createInfoDiv(i18n.t('hardware.signalQuality'), `${this.calculateSignalQuality(txActive, rxActive)}%`)); } // Calculate signal quality based on active antennas @@ -171,4 +173,4 @@ export class HardwareTab { antenna.removeEventListener('click', this.toggleAntenna); }); } -} \ No newline at end of file +} diff --git a/ui/components/LiveDemoTab.js b/ui/components/LiveDemoTab.js index b4de941c3c..4761be9683 100644 --- a/ui/components/LiveDemoTab.js +++ b/ui/components/LiveDemoTab.js @@ -5,6 +5,7 @@ import { poseService } from '../services/pose.service.js'; import { streamService } from '../services/stream.service.js'; import { wsService } from '../services/websocket.service.js'; import { sensingService } from '../services/sensing.service.js'; +import { i18n } from '../utils/i18n.js'; // Optional services - loaded lazily in init() to avoid blocking module graph let modelService = null; @@ -97,6 +98,12 @@ export class LiveDemoTab { // Create enhanced DOM structure this.createEnhancedStructure(); + this._localeUnsub = i18n.onLocaleChange(() => { + this.refreshStaticTexts(); + this.updateUI(); + this.updateModelUI(); + this.updateTrainingStatus(); + }); // Initialize pose detection canvas this.initializePoseCanvas(); @@ -148,26 +155,26 @@ export class LiveDemoTab {
- Detecting data source... + ${i18n.t('demo.detectingSource')}
-

Live Human Pose Detection

+

${i18n.t('demo.title')}

- Ready + ${i18n.t('common.ready')}
- - - - + + + +
@@ -179,45 +186,45 @@ export class LiveDemoTab {
-

Performance Metrics

+

${i18n.t('demo.performanceMetrics')}

- - Disconnected + + ${i18n.t('common.disconnected')}
- + 0
- + 0s
- + 0
- - Never + + ${i18n.t('common.never')}
-

Estimation Mode

+

${i18n.t('demo.estimationMode')}

- Unknown + ${i18n.t('common.unknown')}

- Waiting for first frame... + ${i18n.t('demo.waitingFrame')}

-

Model Control

+

${i18n.t('demo.modelControl')}

- +
- - + +
-
No model loaded
+
${i18n.t('demo.noModelLoaded')}
- - + +
-

Training

+

${i18n.t('demo.training')}

- Idle + ${i18n.t('demo.idle')}
- - + +
-

Setup Guide

+

${i18n.t('demo.setupGuide')}

1x
- 1 ESP32 + 1 AP -

Presence, breathing, gross motion

+ ${i18n.t('demo.setup1Title')} +

${i18n.t('demo.setup1Desc')}

3x
- 2-3 ESP32s -

Body localization, motion direction

+ ${i18n.t('demo.setup2Title')} +

${i18n.t('demo.setup2Desc')}

4x+
- 4+ ESP32s + trained model -

Individual limb tracking, full pose

+ ${i18n.t('demo.setup3Title')} +

${i18n.t('demo.setup3Desc')}

-

- Signal-Derived mode uses aggregate CSI features. - For per-limb tracking, load a trained .rvf model - with --model path.rvf and use 4+ sensors. -

+

${i18n.t('demo.setupNote')}

-

System Health

+

${i18n.t('demo.systemHealth')}

- - Unknown + + ${i18n.t('common.unknown')}
- - Unknown + + ${i18n.t('common.unknown')}
- - Unknown + + ${i18n.t('common.unknown')}