feat(ble): diagnóstico verboso pra debugar pareamento BLE v1.9.2
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run

Karlão reportou: "localiza mas não pareia" (picker abre, seleciona
device, mas conexão falha silenciosa). Sem ver onde trava, impossível fix.

Adicionado:
- setBleDiag() exibe cada step com timestamp + cor (info/ok/warn/err)
- Painel <details> expansível "📋 Diagnóstico" no card BLE
- Logs em cada operação: backend, init, picker, connect, getServices,
  battery read, notifications, device info
- Timeout do connect aumentado: 15s → 30s (BMS podem demorar)
- getServices() lista UUIDs descobertos no device — descobre se BMS
  expõe Battery Service padrão ou só protocolo proprietário
- Mensagens explícitas de erro em cada catch (e.message ou errorMessage)

Próximo passo: Karlão testa, abre painel diagnóstico, me passa screenshot
ou copia o log. Daí descubro exatamente onde trava (timeout, sem service,
permissão negada, etc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PontualTech / Karlão 2026-04-28 16:39:41 -03:00
parent 52ee668879
commit 5dd3362469
5 changed files with 116 additions and 34 deletions

View file

@ -1977,7 +1977,11 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
<div id="bt-support" style="font-family:var(--f-mono);font-size:10.5px;letter-spacing:.04em;margin-bottom:10px">Verificando suporte...</div>
<button class="btn btn-block btn-primary" onclick="pairBluetoothDevice()">+ Parear novo dispositivo Bluetooth</button>
<div id="bt-list" style="margin-top:14px"></div>
<div class="field-hint" style="margin-top:8px">Limitações: <strong>iOS Safari não suporta Web Bluetooth</strong> (use no Chrome PC ou Android). Reconexão automática varia por device.</div>
<details style="margin-top:10px">
<summary style="cursor:pointer;font-family:var(--f-mono);font-size:11px;color:var(--m-text-soft,#7d97ad);letter-spacing:.06em">📋 Diagnóstico (logs do pareamento)</summary>
<div id="bt-diag" style="background:var(--m-bg-2,#0f2a40);border:1px solid var(--m-border,rgba(255,255,255,.08));border-radius:6px;padding:10px;margin-top:6px;max-height:200px;overflow-y:auto"></div>
</details>
<div class="field-hint" style="margin-top:8px">Limitações: <strong>iOS Safari não suporta Web Bluetooth</strong>. APK Android usa plugin nativo. BMS proprietários (Victron, JBD) podem aparecer mas não expor Battery Service padrão.</div>
</div>
<!-- Raymarine Gateway -->
@ -5771,26 +5775,42 @@ async function ensureBleNativeReady(){
_bleNativeInitialized=true;
}
// Diagnóstico visível: mostra cada passo no card BLE
function setBleDiag(msg,type){
const el=document.getElementById('bt-diag');
if(!el)return;
const colors={info:'var(--m-text-mid,#b3c5d6)',ok:'var(--m-ok,#10b981)',err:'var(--m-danger,#ef4444)',warn:'var(--m-warn,#f59e0b)'};
const time=new Date().toLocaleTimeString('pt-BR',{hour12:false});
const line=`<div style="color:${colors[type||'info']};font-family:var(--f-mono);font-size:11px;line-height:1.5">${time} · ${escapeHtml(msg)}</div>`;
el.innerHTML=line+el.innerHTML;
// Mantém só últimas 12 linhas
const lines=el.children;
while(lines.length>12)el.removeChild(lines[lines.length-1]);
console.log('[ble]',msg);
}
async function pairBluetoothDevice(){
const backend=bleBackend();
if(!backend){
toast('Bluetooth indisponível neste dispositivo.');
return;
}
setBleDiag('Backend: '+(backend||'NENHUM'),backend?'info':'err');
if(!backend){toast('Bluetooth indisponível');return}
try{
let deviceId,deviceName;
if(backend==='capacitor'){
setBleDiag('Inicializando plugin nativo...');
await ensureBleNativeReady();
setBleDiag('Plugin OK · abrindo picker...');
const ble=window.Capacitor.Plugins.BluetoothLe;
const result=await ble.requestDevice({
services:[],
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
allowDuplicates:false,
});
if(!result?.deviceId){return}
if(!result?.deviceId){setBleDiag('Picker cancelado','warn');return}
deviceId=result.deviceId;
deviceName=result.name||'Dispositivo BLE';
setBleDiag('Selecionado: '+deviceName+' ('+deviceId+')','ok');
}else{
setBleDiag('Abrindo picker do navegador...');
const device=await navigator.bluetooth.requestDevice({
acceptAllDevices:true,
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
@ -5802,11 +5822,13 @@ async function pairBluetoothDevice(){
device.addEventListener('gattserverdisconnected',()=>{
const c=_bleConnections.get(deviceId);if(c)c.connected=false;
renderBluetoothCard();
setBleDiag('Disconnected: '+deviceName,'warn');
});
setBleDiag('Selecionado: '+deviceName,'ok');
}
// Conecta + lê info inicial
setBleDiag('Conectando GATT...');
const info=await connectAndRead(deviceId,deviceName);
// Salva no state
setBleDiag('Connect OK · battery='+(info.battery??'N/A')+' · mfr='+(info.manufacturer||'N/A'),info.battery!=null?'ok':'warn');
if(!state.btDevices)state.btDevices=[];
const existing=state.btDevices.find(d=>d.id===deviceId);
if(existing){
@ -5821,28 +5843,44 @@ async function pairBluetoothDevice(){
}
saveState();
renderBluetoothCard();
toast('✓ '+deviceName+' pareado'+(info.battery!=null?' · '+info.battery+'%':''));
toast('✓ '+deviceName+(info.battery!=null?' · '+info.battery+'%':' (sem leitura de bateria)'));
}catch(e){
if(e.name==='NotFoundError'||/cancel/i.test(e.message||''))return;
if(e.name==='NotFoundError'||/cancel/i.test(e.message||'')){setBleDiag('Cancelado','warn');return}
const msg=e.message||e.errorMessage||JSON.stringify(e).slice(0,100)||'erro desconhecido';
setBleDiag('ERRO: '+msg,'err');
console.warn('[ble] pair failed',e);
toast('Erro: '+(e.message||e.errorMessage||'pareamento falhou'));
toast('Falhou: '+msg);
}
}
async function connectAndRead(deviceId,deviceName){
const info={battery:null,manufacturer:null,model:null};
const info={battery:null,manufacturer:null,model:null,services:[]};
const backend=bleBackend();
try{
if(backend==='capacitor'){
const ble=window.Capacitor.Plugins.BluetoothLe;
await ble.connect({deviceId,timeout:15000});
try{
await ble.connect({deviceId,timeout:30000});
setBleDiag('GATT conectado','ok');
}catch(e){
setBleDiag('connect() falhou: '+(e.message||e.errorMessage||'?'),'err');
throw e;
}
const conn=_bleConnections.get(deviceId)||{};
conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true;
_bleConnections.set(deviceId,conn);
// Discover all services pra diagnóstico
try{
const r=await ble.getServices({deviceId});
const svcs=(r.services||r||[]).map(s=>s.uuid||s).slice(0,8);
setBleDiag('Serviços encontrados: '+svcs.length+' ('+svcs.map(u=>u.slice(4,8)).join(', ')+')','info');
info.services=svcs;
}catch(e){setBleDiag('getServices falhou: '+e.message,'warn')}
// Battery
try{
const r=await ble.read({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR});
info.battery=parseDataView(r.value).getUint8(0);
setBleDiag('Battery Service OK: '+info.battery+'%','ok');
try{
await ble.startNotifications({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR});
ble.addListener('notification|'+deviceId+'|'+BLE_BATTERY_SERVICE+'|'+BLE_BATTERY_CHAR,(ev)=>{
@ -5850,16 +5888,19 @@ async function connectAndRead(deviceId,deviceName){
const dev=state.btDevices?.find(d=>d.id===deviceId);
if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()}
});
}catch(e){}
}catch(e){}
setBleDiag('Notificações ativas','ok');
}catch(e){setBleDiag('startNotifications falhou: '+e.message,'warn')}
}catch(e){setBleDiag('Sem Battery Service padrão (BMS pode usar protocolo proprietário)','warn')}
// Device info
try{
const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MANUFACTURER_CHAR});
info.manufacturer=new TextDecoder().decode(parseDataView(r.value));
setBleDiag('Fabricante: '+info.manufacturer,'info');
}catch(e){}
try{
const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MODEL_CHAR});
info.model=new TextDecoder().decode(parseDataView(r.value));
setBleDiag('Modelo: '+info.model,'info');
}catch(e){}
}else{
const conn=_bleConnections.get(deviceId);

View file

@ -7,8 +7,8 @@ android {
applicationId "br.com.pontualtech.shivao"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 14
versionName "1.9.1"
versionCode 15
versionName "1.9.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View file

@ -1,6 +1,6 @@
{
"name": "shivao-mobile",
"version": "1.9.1",
"version": "1.9.2",
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
"main": "index.js",
"type": "module",

View file

@ -1977,7 +1977,11 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
<div id="bt-support" style="font-family:var(--f-mono);font-size:10.5px;letter-spacing:.04em;margin-bottom:10px">Verificando suporte...</div>
<button class="btn btn-block btn-primary" onclick="pairBluetoothDevice()">+ Parear novo dispositivo Bluetooth</button>
<div id="bt-list" style="margin-top:14px"></div>
<div class="field-hint" style="margin-top:8px">Limitações: <strong>iOS Safari não suporta Web Bluetooth</strong> (use no Chrome PC ou Android). Reconexão automática varia por device.</div>
<details style="margin-top:10px">
<summary style="cursor:pointer;font-family:var(--f-mono);font-size:11px;color:var(--m-text-soft,#7d97ad);letter-spacing:.06em">📋 Diagnóstico (logs do pareamento)</summary>
<div id="bt-diag" style="background:var(--m-bg-2,#0f2a40);border:1px solid var(--m-border,rgba(255,255,255,.08));border-radius:6px;padding:10px;margin-top:6px;max-height:200px;overflow-y:auto"></div>
</details>
<div class="field-hint" style="margin-top:8px">Limitações: <strong>iOS Safari não suporta Web Bluetooth</strong>. APK Android usa plugin nativo. BMS proprietários (Victron, JBD) podem aparecer mas não expor Battery Service padrão.</div>
</div>
<!-- Raymarine Gateway -->
@ -5771,26 +5775,42 @@ async function ensureBleNativeReady(){
_bleNativeInitialized=true;
}
// Diagnóstico visível: mostra cada passo no card BLE
function setBleDiag(msg,type){
const el=document.getElementById('bt-diag');
if(!el)return;
const colors={info:'var(--m-text-mid,#b3c5d6)',ok:'var(--m-ok,#10b981)',err:'var(--m-danger,#ef4444)',warn:'var(--m-warn,#f59e0b)'};
const time=new Date().toLocaleTimeString('pt-BR',{hour12:false});
const line=`<div style="color:${colors[type||'info']};font-family:var(--f-mono);font-size:11px;line-height:1.5">${time} · ${escapeHtml(msg)}</div>`;
el.innerHTML=line+el.innerHTML;
// Mantém só últimas 12 linhas
const lines=el.children;
while(lines.length>12)el.removeChild(lines[lines.length-1]);
console.log('[ble]',msg);
}
async function pairBluetoothDevice(){
const backend=bleBackend();
if(!backend){
toast('Bluetooth indisponível neste dispositivo.');
return;
}
setBleDiag('Backend: '+(backend||'NENHUM'),backend?'info':'err');
if(!backend){toast('Bluetooth indisponível');return}
try{
let deviceId,deviceName;
if(backend==='capacitor'){
setBleDiag('Inicializando plugin nativo...');
await ensureBleNativeReady();
setBleDiag('Plugin OK · abrindo picker...');
const ble=window.Capacitor.Plugins.BluetoothLe;
const result=await ble.requestDevice({
services:[],
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
allowDuplicates:false,
});
if(!result?.deviceId){return}
if(!result?.deviceId){setBleDiag('Picker cancelado','warn');return}
deviceId=result.deviceId;
deviceName=result.name||'Dispositivo BLE';
setBleDiag('Selecionado: '+deviceName+' ('+deviceId+')','ok');
}else{
setBleDiag('Abrindo picker do navegador...');
const device=await navigator.bluetooth.requestDevice({
acceptAllDevices:true,
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
@ -5802,11 +5822,13 @@ async function pairBluetoothDevice(){
device.addEventListener('gattserverdisconnected',()=>{
const c=_bleConnections.get(deviceId);if(c)c.connected=false;
renderBluetoothCard();
setBleDiag('Disconnected: '+deviceName,'warn');
});
setBleDiag('Selecionado: '+deviceName,'ok');
}
// Conecta + lê info inicial
setBleDiag('Conectando GATT...');
const info=await connectAndRead(deviceId,deviceName);
// Salva no state
setBleDiag('Connect OK · battery='+(info.battery??'N/A')+' · mfr='+(info.manufacturer||'N/A'),info.battery!=null?'ok':'warn');
if(!state.btDevices)state.btDevices=[];
const existing=state.btDevices.find(d=>d.id===deviceId);
if(existing){
@ -5821,28 +5843,44 @@ async function pairBluetoothDevice(){
}
saveState();
renderBluetoothCard();
toast('✓ '+deviceName+' pareado'+(info.battery!=null?' · '+info.battery+'%':''));
toast('✓ '+deviceName+(info.battery!=null?' · '+info.battery+'%':' (sem leitura de bateria)'));
}catch(e){
if(e.name==='NotFoundError'||/cancel/i.test(e.message||''))return;
if(e.name==='NotFoundError'||/cancel/i.test(e.message||'')){setBleDiag('Cancelado','warn');return}
const msg=e.message||e.errorMessage||JSON.stringify(e).slice(0,100)||'erro desconhecido';
setBleDiag('ERRO: '+msg,'err');
console.warn('[ble] pair failed',e);
toast('Erro: '+(e.message||e.errorMessage||'pareamento falhou'));
toast('Falhou: '+msg);
}
}
async function connectAndRead(deviceId,deviceName){
const info={battery:null,manufacturer:null,model:null};
const info={battery:null,manufacturer:null,model:null,services:[]};
const backend=bleBackend();
try{
if(backend==='capacitor'){
const ble=window.Capacitor.Plugins.BluetoothLe;
await ble.connect({deviceId,timeout:15000});
try{
await ble.connect({deviceId,timeout:30000});
setBleDiag('GATT conectado','ok');
}catch(e){
setBleDiag('connect() falhou: '+(e.message||e.errorMessage||'?'),'err');
throw e;
}
const conn=_bleConnections.get(deviceId)||{};
conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true;
_bleConnections.set(deviceId,conn);
// Discover all services pra diagnóstico
try{
const r=await ble.getServices({deviceId});
const svcs=(r.services||r||[]).map(s=>s.uuid||s).slice(0,8);
setBleDiag('Serviços encontrados: '+svcs.length+' ('+svcs.map(u=>u.slice(4,8)).join(', ')+')','info');
info.services=svcs;
}catch(e){setBleDiag('getServices falhou: '+e.message,'warn')}
// Battery
try{
const r=await ble.read({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR});
info.battery=parseDataView(r.value).getUint8(0);
setBleDiag('Battery Service OK: '+info.battery+'%','ok');
try{
await ble.startNotifications({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR});
ble.addListener('notification|'+deviceId+'|'+BLE_BATTERY_SERVICE+'|'+BLE_BATTERY_CHAR,(ev)=>{
@ -5850,16 +5888,19 @@ async function connectAndRead(deviceId,deviceName){
const dev=state.btDevices?.find(d=>d.id===deviceId);
if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()}
});
}catch(e){}
}catch(e){}
setBleDiag('Notificações ativas','ok');
}catch(e){setBleDiag('startNotifications falhou: '+e.message,'warn')}
}catch(e){setBleDiag('Sem Battery Service padrão (BMS pode usar protocolo proprietário)','warn')}
// Device info
try{
const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MANUFACTURER_CHAR});
info.manufacturer=new TextDecoder().decode(parseDataView(r.value));
setBleDiag('Fabricante: '+info.manufacturer,'info');
}catch(e){}
try{
const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MODEL_CHAR});
info.model=new TextDecoder().decode(parseDataView(r.value));
setBleDiag('Modelo: '+info.model,'info');
}catch(e){}
}else{
const conn=_bleConnections.get(deviceId);

View file

@ -347,7 +347,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => {
});
// Atalho: /apk redireciona pra última APK release no Forgejo
const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.9.1/Shivao-v1.9.1.apk';
const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.9.2/Shivao-v1.9.2.apk';
app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL));
// Página A4 imprimível com QR Code + instruções (cola no barco/marina)