feat(ao-vivo): avisa bateria baixa e mostra o ultimo sinal do celular
- aviso no painel quando a bateria do celular chega a 15% e a 5% sem carregador (uma vez cada; volta a valer depois de carregar) - selo do celular fica vermelho com bateria baixa - celular offline mostra ha quanto tempo foi o ultimo sinal e com quanta bateria; abaixo de 10% sugere que a bateria acabou Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,13 @@ def testar(base, server, capturas):
|
|||||||
expect(page.get_by_text('samsung SM-G973F')).to_be_visible()
|
expect(page.get_by_text('samsung SM-G973F')).to_be_visible()
|
||||||
expect(page.get_by_text('192.168.1.16')).to_be_visible()
|
expect(page.get_by_text('192.168.1.16')).to_be_visible()
|
||||||
|
|
||||||
|
# 2b. bateria baixa sem carregador vira aviso na hora
|
||||||
|
page.request.post(base + '/app/status', headers=auth, data={'bateria': {'nivel': 12, 'carregando': False}})
|
||||||
|
expect(page.get_by_text('Bateria do celular em 12%. Ligue o carregador')).to_be_visible(timeout=5000)
|
||||||
|
page.request.post(base + '/app/status', headers=auth, data={'dormindo': False, 'bateria': 0.8,
|
||||||
|
'aparelho': {'ip': '192.168.1.16', 'modelo': 'samsung SM-G973F', 'android': '12',
|
||||||
|
'app': '0.8.1', 'wifi': 3, 'rssi': -58}})
|
||||||
|
|
||||||
# 3. gesto pelo dock
|
# 3. gesto pelo dock
|
||||||
kiosk.FILA.limpar()
|
kiosk.FILA.limpar()
|
||||||
page.get_by_role('button', name='Feliz').first.click()
|
page.get_by_role('button', name='Feliz').first.click()
|
||||||
|
|||||||
+2
-6
@@ -104,11 +104,7 @@
|
|||||||
const conexaoTexto = $derived(
|
const conexaoTexto = $derived(
|
||||||
{ conectando: 'Conectando…', aberta: 'Servidor', caiu: 'Sem servidor', token: 'Token necessário' }[estado.conexao],
|
{ conectando: 'Conectando…', aberta: 'Servidor', caiu: 'Sem servidor', token: 'Token necessário' }[estado.conexao],
|
||||||
);
|
);
|
||||||
const bateria = $derived.by(() => {
|
const bateria = $derived(estado.bateria.nivel);
|
||||||
const b = estado.app?.estado?.bateria;
|
|
||||||
const n = typeof b === 'number' ? b : b?.nivel;
|
|
||||||
return typeof n === 'number' ? Math.round(n <= 1 ? n * 100 : n) : null;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onkeydown={teclado} />
|
<svelte:window onkeydown={teclado} />
|
||||||
@@ -147,7 +143,7 @@
|
|||||||
<Selo tom={estado.conexao === 'aberta' ? 'ok' : estado.conexao === 'conectando' ? 'aviso' : 'perigo'}>
|
<Selo tom={estado.conexao === 'aberta' ? 'ok' : estado.conexao === 'conectando' ? 'aviso' : 'perigo'}>
|
||||||
{conexaoTexto}
|
{conexaoTexto}
|
||||||
</Selo>
|
</Selo>
|
||||||
<Selo tom={estado.celularOnline ? 'ok' : 'neutro'}>
|
<Selo tom={!estado.celularOnline ? 'neutro' : estado.bateriaBaixa ? 'perigo' : 'ok'}>
|
||||||
Celular {estado.celularOnline ? 'online' : 'offline'}{bateria !== null && estado.celularOnline ? ` · ${bateria}%` : ''}
|
Celular {estado.celularOnline ? 'online' : 'offline'}{bateria !== null && estado.celularOnline ? ` · ${bateria}%` : ''}
|
||||||
</Selo>
|
</Selo>
|
||||||
{#if estado.conversa}
|
{#if estado.conversa}
|
||||||
|
|||||||
@@ -5,15 +5,19 @@
|
|||||||
// Cartão do aparelho com o que o próprio app informa (IP, modelo, Android, versão, Wi-Fi, bateria).
|
// Cartão do aparelho com o que o próprio app informa (IP, modelo, Android, versão, Wi-Fi, bateria).
|
||||||
const s = $derived(estado.app?.estado ?? {});
|
const s = $derived(estado.app?.estado ?? {});
|
||||||
const ap = $derived(s.aparelho ?? null);
|
const ap = $derived(s.aparelho ?? null);
|
||||||
const bateria = $derived.by(() => {
|
const bateria = $derived(estado.bateria.nivel);
|
||||||
const b = s.bateria;
|
const carregando = $derived(estado.bateria.carregando);
|
||||||
const n = typeof b === 'number' ? b : b?.nivel;
|
// Tempo desde o último sinal, em texto curto ("há 3 min").
|
||||||
return typeof n === 'number' ? Math.round(n <= 1 ? n * 100 : n) : null;
|
const semSinal = $derived.by(() => {
|
||||||
|
if (!estado.ultimoSinalEm) return '';
|
||||||
|
const seg = Math.max(0, Math.round((estado.agora - estado.ultimoSinalEm) / 1000));
|
||||||
|
if (seg < 60) return `há ${seg} s`;
|
||||||
|
if (seg < 3600) return `há ${Math.round(seg / 60)} min`;
|
||||||
|
if (seg < 86400) return `há ${Math.round(seg / 3600)} h`;
|
||||||
|
return `há ${Math.round(seg / 86400)} dia(s)`;
|
||||||
});
|
});
|
||||||
const carregando = $derived(typeof s.bateria === 'object' && !!s.bateria?.carregando);
|
|
||||||
const IconeBateria = $derived(carregando ? BatteryCharging : bateria === null ? BatteryMedium : bateria < 20 ? BatteryLow : bateria < 70 ? BatteryMedium : BatteryFull);
|
const IconeBateria = $derived(carregando ? BatteryCharging : bateria === null ? BatteryMedium : bateria < 20 ? BatteryLow : bateria < 70 ? BatteryMedium : BatteryFull);
|
||||||
const ip = $derived(ap?.ip || estado.app?.celular_ip || '—');
|
const ip = $derived(ap?.ip || estado.app?.celular_ip || '—');
|
||||||
const visto = $derived(estado.app?.idade ?? null);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="celular" class:offline={!estado.celularOnline}>
|
<div class="celular" class:offline={!estado.celularOnline}>
|
||||||
@@ -25,7 +29,7 @@
|
|||||||
{#if estado.celularOnline}
|
{#if estado.celularOnline}
|
||||||
{[ap?.android && 'Android ' + ap.android, ap?.app && 'app ' + ap.app].filter(Boolean).join(' · ') || 'online'}
|
{[ap?.android && 'Android ' + ap.android, ap?.app && 'app ' + ap.app].filter(Boolean).join(' · ') || 'online'}
|
||||||
{:else}
|
{:else}
|
||||||
offline{visto !== null ? ` · visto há ${Math.round(visto)} s` : ''}
|
offline{semSinal ? ' · último sinal ' + semSinal : ''}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,6 +74,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
{:else}
|
{:else}
|
||||||
|
{#if bateria !== null && estado.ultimoSinalEm}
|
||||||
|
<p class="ultimo" class:baixa={bateria <= 15 && !carregando}>
|
||||||
|
<IconeBateria size={18} /> Último sinal com {bateria}%{carregando ? ', carregando' : ''}.
|
||||||
|
{#if bateria <= 10 && !carregando}Provavelmente a bateria acabou: ligue o carregador e abra o app.{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
<p class="ajuda">Abra o app Carinha no celular e confira se ele aponta para este servidor (toque longo › Mudar o servidor).</p>
|
<p class="ajuda">Abra o app Carinha no celular e confira se ele aponta para este servidor (toque longo › Mudar o servidor).</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -170,6 +180,17 @@
|
|||||||
rect.cheia {
|
rect.cheia {
|
||||||
fill: var(--texto);
|
fill: var(--texto);
|
||||||
}
|
}
|
||||||
|
.ultimo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--e-2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: var(--t-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.ultimo.baixa {
|
||||||
|
color: var(--perigo);
|
||||||
|
}
|
||||||
.ajuda {
|
.ajuda {
|
||||||
font-size: var(--t-sm);
|
font-size: var(--t-sm);
|
||||||
color: var(--texto-3);
|
color: var(--texto-3);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* Nenhuma tela faz polling; todas leem daqui.
|
* Nenhuma tela faz polling; todas leem daqui.
|
||||||
*/
|
*/
|
||||||
import { ErroApi, pedir, quandoNegado, urlEventos } from './api';
|
import { ErroApi, pedir, quandoNegado, urlEventos } from './api';
|
||||||
|
import { avisos } from './avisos.svelte';
|
||||||
import type { AppConfig, Conversa, EstadoServidor, IA, Motores, Reacao, Telemetria } from './tipos';
|
import type { AppConfig, Conversa, EstadoServidor, IA, Motores, Reacao, Telemetria } from './tipos';
|
||||||
|
|
||||||
export type Conexao = 'conectando' | 'aberta' | 'caiu' | 'token';
|
export type Conexao = 'conectando' | 'aberta' | 'caiu' | 'token';
|
||||||
@@ -37,6 +38,18 @@ class Estado {
|
|||||||
agora = $state(Date.now());
|
agora = $state(Date.now());
|
||||||
|
|
||||||
celularOnline = $derived(!!this.app?.online && this.agora - this.appRecebidoEm < 6000);
|
celularOnline = $derived(!!this.app?.online && this.agora - this.appRecebidoEm < 6000);
|
||||||
|
/** Quando o celular mandou o último sinal (0 = nunca, nesta sessão nem no servidor). */
|
||||||
|
ultimoSinalEm = $state(0);
|
||||||
|
/** Bateria do último sinal recebido, mesmo com o celular offline. */
|
||||||
|
bateria = $derived.by(() => {
|
||||||
|
const b = this.app?.estado?.bateria;
|
||||||
|
const n = typeof b === 'number' ? b : b?.nivel;
|
||||||
|
return {
|
||||||
|
nivel: typeof n === 'number' ? Math.round(n <= 1 ? n * 100 : n) : null,
|
||||||
|
carregando: typeof b === 'object' && !!b?.carregando,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
bateriaBaixa = $derived(this.bateria.nivel !== null && this.bateria.nivel <= 15 && !this.bateria.carregando);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const estado = new Estado();
|
export const estado = new Estado();
|
||||||
@@ -51,6 +64,23 @@ export function ouvir(tipo: string, fn: Ouvinte): () => void {
|
|||||||
return () => ouvintes.get(tipo)!.delete(fn);
|
return () => ouvintes.get(tipo)!.delete(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avisa uma vez em 15% e outra em 5% (sem carregador); zera quando volta a carregar.
|
||||||
|
let avisouEm: number | null = null;
|
||||||
|
function avisarBateria() {
|
||||||
|
const { nivel, carregando } = estado.bateria;
|
||||||
|
if (nivel === null) return;
|
||||||
|
if (carregando || nivel > 20) {
|
||||||
|
avisouEm = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const limite = nivel <= 5 ? 5 : nivel <= 15 ? 15 : null;
|
||||||
|
if (limite !== null && avisouEm !== limite) {
|
||||||
|
avisouEm = limite;
|
||||||
|
avisos.mostrar(`Bateria do celular em ${nivel}%. Ligue o carregador ou o Carinha vai desligar.`, 'erro');
|
||||||
|
noFeed('estop', `Bateria do celular em ${nivel}%`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let seqFeed = 0;
|
let seqFeed = 0;
|
||||||
function noFeed(tipo: string, texto: string) {
|
function noFeed(tipo: string, texto: string) {
|
||||||
estado.feed = [{ id: ++seqFeed, quando: Date.now(), tipo, texto }, ...estado.feed].slice(0, 40);
|
estado.feed = [{ id: ++seqFeed, quando: Date.now(), tipo, texto }, ...estado.feed].slice(0, 40);
|
||||||
@@ -67,6 +97,7 @@ function aplicarFoto(f: EstadoServidor) {
|
|||||||
if (f.app) {
|
if (f.app) {
|
||||||
estado.app = f.app;
|
estado.app = f.app;
|
||||||
estado.appRecebidoEm = Date.now();
|
estado.appRecebidoEm = Date.now();
|
||||||
|
estado.ultimoSinalEm = typeof f.app.idade === 'number' ? Date.now() - f.app.idade * 1000 : 0;
|
||||||
}
|
}
|
||||||
if (f.app_config) estado.appConfig = f.app_config;
|
if (f.app_config) estado.appConfig = f.app_config;
|
||||||
if (f.motores) estado.motores = f.motores;
|
if (f.motores) estado.motores = f.motores;
|
||||||
@@ -83,7 +114,8 @@ const tratadores: Record<string, (d: any) => void> = {
|
|||||||
},
|
},
|
||||||
telemetria: (d: Telemetria) => {
|
telemetria: (d: Telemetria) => {
|
||||||
estado.app = d;
|
estado.app = d;
|
||||||
estado.appRecebidoEm = Date.now();
|
estado.appRecebidoEm = estado.ultimoSinalEm = Date.now();
|
||||||
|
avisarBateria();
|
||||||
},
|
},
|
||||||
app_config: (d: AppConfig) => (estado.appConfig = d),
|
app_config: (d: AppConfig) => (estado.appConfig = d),
|
||||||
aparencia: (d) => (estado.aparencia = d),
|
aparencia: (d) => (estado.aparencia = d),
|
||||||
|
|||||||
Reference in New Issue
Block a user