feat(ble): probe via Web Bluetooth pro Chrome PC v1.10.12
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run

Karlão sugeriu testar no notebook (Chrome) onde Web Bluetooth API
é mais madura que plugin Capacitor v6. Implementado bmsProbeWebBluetooth
que usa navigator.bluetooth direto:
- getPrimaryService(ff00)
- getCharacteristics() lista chars
- writeValueWithoutResponse / writeValue conforme properties
- characteristicvaluechanged event listener
- Wake sequence + 3 protocolos JBD/JK/Daly

Quando Web descobrir o protocolo certo, copio a lógica pro path
Capacitor (APK Android também vai funcionar).

requestDevice pra browser web agora inclui ff00/fff0/ffe0 nos
optionalServices pra Web Bluetooth permitir acesso.

Sem APK rebuild (web only) — só deploy backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PontualTech / Karlão 2026-04-29 08:34:24 -03:00
parent 330d5aaa62
commit 638ed5e37b
2 changed files with 158 additions and 16 deletions

View file

@ -5892,7 +5892,7 @@ async function pairBluetoothDevice(){
setBleDiag('Abrindo picker do navegador...');
const device=await navigator.bluetooth.requestDevice({
acceptAllDevices:true,
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO,'0000ff00-0000-1000-8000-00805f9b34fb','0000fff0-0000-1000-8000-00805f9b34fb','0000ffe0-0000-1000-8000-00805f9b34fb'],
});
if(!device){return}
deviceId=device.id;
@ -5922,11 +5922,11 @@ async function pairBluetoothDevice(){
}
saveState();
renderBluetoothCard();
// Se detectou JBD BMS, ativa parser proprietário
if(info.isJBD){
// Se detectou JBD BMS OU backend é web (sempre tenta probe — descobre serviço dinâmico), ativa probe
if(info.isJBD||backend==='web'){
const ok=await bmsAttachJBD(deviceId,deviceName);
if(ok)toast('✓ '+deviceName+' · JBD BMS ativo');
else toast('✓ '+deviceName+' (BMS detectado mas falha no parser)');
if(ok)toast('✓ '+deviceName+' · BMS ativo');
else toast('✓ '+deviceName+' (sem BMS detectável)');
}else{
toast('✓ '+deviceName+(info.battery!=null?' · '+info.battery+'%':' (sem leitura de bateria)'));
}
@ -6056,10 +6056,81 @@ function bytesToBase64(arr){
return btoa(bin);
}
// Probe: lista characteristics + identifica notify/write chars + tenta protocolos
// Probe via Web Bluetooth API (Chrome PC)
async function bmsProbeWebBluetooth(deviceId,deviceName){
const conn=_bleConnections.get(deviceId);
const device=conn?.device;
if(!device){setBleDiag('Device sem referência (re-pareie)','err');return false}
try{
setBleDiag(`📦 Shivao v${APP_VERSION} · Probe WEB iniciado`,'info');
const server=device.gatt.connected?device.gatt:await device.gatt.connect();
setBleDiag('GATT web conectado','ok');
let svc=null;
try{svc=await server.getPrimaryService('0000ff00-0000-1000-8000-00805f9b34fb');setBleDiag('Service ff00 OK','info')}
catch(e){setBleDiag('Service ff00 falhou: '+e.message,'err');return false}
const chars=await svc.getCharacteristics();
setBleDiag(`Svc ff00 · ${chars.length} chars`,'info');
let notifyChar=null,writeChar=null;
for(const c of chars){
const p=c.properties;
const propsStr=[p.notify&&'notify',p.indicate&&'indicate',p.write&&'write',p.writeWithoutResponses&&'wnr',p.read&&'read'].filter(Boolean).join(',');
const cu=c.uuid.toLowerCase();
setBleDiag(` ${cu.slice(4,8)} [${propsStr}]`,'info');
if(!notifyChar&&(p.notify||p.indicate))notifyChar=c;
if(!writeChar&&(p.write||p.writeWithoutResponses))writeChar=c;
}
if(!notifyChar||!writeChar){setBleDiag('Sem chars notify+write','err');return false}
setBleDiag(`Notify=${notifyChar.uuid.slice(4,8)} Write=${writeChar.uuid.slice(4,8)}`,'ok');
notifyChar.addEventListener('characteristicvaluechanged',(ev)=>{
const dv=ev.target.value;
const hex=Array.from(new Uint8Array(dv.buffer)).map(b=>b.toString(16).padStart(2,'0')).join(' ');
setBleDiag(`← RX ${dv.byteLength}b: ${hex.slice(0,100)}${hex.length>100?'...':''}`,'ok');
const first=new Uint8Array(dv.buffer)[0];
if(first===0xDD)bmsHandleChunk(deviceId,dv,deviceName);
else if(first===0xAA)bmsHandleJK(deviceId,dv,deviceName);
else if(first===0xA5)bmsHandleDaly(deviceId,dv,deviceName);
});
await notifyChar.startNotifications();
setBleDiag('Notify ativo · iniciando wake...','ok');
await new Promise(r=>setTimeout(r,500));
try{
const fn=writeChar.properties.writeWithoutResponses?'writeValueWithoutResponse':'writeValue';
await writeChar[fn](new Uint8Array([0x5A,0x5A,0x5A,0x5A]));
setBleDiag('Wake 5A x4 enviado','info');
}catch(e){setBleDiag('Wake skip: '+e.message,'info')}
await new Promise(r=>setTimeout(r,1500));
const PROTOCOLS=[
{name:'JBD-0x03',bytes:[0xDD,0xA5,0x03,0x00,0xFF,0xFD,0x77]},
{name:'JK-getInfo',bytes:[0xAA,0x55,0x90,0xEB,0x96,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10]},
{name:'Daly-getInfo',bytes:[0xA5,0x80,0x90,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xBD]},
];
for(const p of PROTOCOLS){
try{
setBleDiag(`→ TX ${p.name}`,'info');
const fn=writeChar.properties.writeWithoutResponses?'writeValueWithoutResponse':'writeValue';
await writeChar[fn](new Uint8Array(p.bytes));
setBleDiag(`✔ write ${p.name} OK`,'info');
await new Promise(r=>setTimeout(r,2500));
const dev=state.btDevices?.find(d=>d.id===deviceId);
if(dev?.bms?.voltage||dev?._lastRxAt){
setBleDiag(`✓ ${p.name} respondeu!`,'ok');
if(dev){dev.bmsProtocol=p.name;dev.isJBD=true;saveState()}
conn.notifyChar=notifyChar;conn.writeChar=writeChar;
return true;
}
setBleDiag(`✗ ${p.name} sem RX`,'info');
}catch(e){setBleDiag(`${p.name} erro: ${e.message}`,'warn')}
}
setBleDiag('⚠ Nenhum protocolo respondeu','err');
return false;
}catch(e){setBleDiag('Probe web falhou: '+e.message,'err');return false}
}
// Probe Capacitor: lista characteristics + identifica notify/write chars + tenta protocolos
async function bmsProbeAndAttach(deviceId,deviceName){
const backend=bleBackend();
if(backend!=='capacitor'){setBleDiag('Probe requer Capacitor (APK)','warn');return false}
if(backend==='web')return bmsProbeWebBluetooth(deviceId,deviceName);
if(backend!=='capacitor'){setBleDiag('Backend desconhecido','warn');return false}
const ble=window.Capacitor.Plugins.BluetoothLe;
try{
setBleDiag(`📦 Shivao v${APP_VERSION} · Probe iniciado`,'info');
@ -6369,7 +6440,7 @@ async function removeBluetoothDevice(id){
renderBluetoothCard();
}
const APP_VERSION='1.10.11';
const APP_VERSION='1.10.12';
function renderBluetoothCard(){
const el=document.getElementById('bt-list');
const supportEl=document.getElementById('bt-support');

View file

@ -5892,7 +5892,7 @@ async function pairBluetoothDevice(){
setBleDiag('Abrindo picker do navegador...');
const device=await navigator.bluetooth.requestDevice({
acceptAllDevices:true,
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO,'0000ff00-0000-1000-8000-00805f9b34fb','0000fff0-0000-1000-8000-00805f9b34fb','0000ffe0-0000-1000-8000-00805f9b34fb'],
});
if(!device){return}
deviceId=device.id;
@ -5922,11 +5922,11 @@ async function pairBluetoothDevice(){
}
saveState();
renderBluetoothCard();
// Se detectou JBD BMS, ativa parser proprietário
if(info.isJBD){
// Se detectou JBD BMS OU backend é web (sempre tenta probe — descobre serviço dinâmico), ativa probe
if(info.isJBD||backend==='web'){
const ok=await bmsAttachJBD(deviceId,deviceName);
if(ok)toast('✓ '+deviceName+' · JBD BMS ativo');
else toast('✓ '+deviceName+' (BMS detectado mas falha no parser)');
if(ok)toast('✓ '+deviceName+' · BMS ativo');
else toast('✓ '+deviceName+' (sem BMS detectável)');
}else{
toast('✓ '+deviceName+(info.battery!=null?' · '+info.battery+'%':' (sem leitura de bateria)'));
}
@ -6056,10 +6056,81 @@ function bytesToBase64(arr){
return btoa(bin);
}
// Probe: lista characteristics + identifica notify/write chars + tenta protocolos
// Probe via Web Bluetooth API (Chrome PC)
async function bmsProbeWebBluetooth(deviceId,deviceName){
const conn=_bleConnections.get(deviceId);
const device=conn?.device;
if(!device){setBleDiag('Device sem referência (re-pareie)','err');return false}
try{
setBleDiag(`📦 Shivao v${APP_VERSION} · Probe WEB iniciado`,'info');
const server=device.gatt.connected?device.gatt:await device.gatt.connect();
setBleDiag('GATT web conectado','ok');
let svc=null;
try{svc=await server.getPrimaryService('0000ff00-0000-1000-8000-00805f9b34fb');setBleDiag('Service ff00 OK','info')}
catch(e){setBleDiag('Service ff00 falhou: '+e.message,'err');return false}
const chars=await svc.getCharacteristics();
setBleDiag(`Svc ff00 · ${chars.length} chars`,'info');
let notifyChar=null,writeChar=null;
for(const c of chars){
const p=c.properties;
const propsStr=[p.notify&&'notify',p.indicate&&'indicate',p.write&&'write',p.writeWithoutResponses&&'wnr',p.read&&'read'].filter(Boolean).join(',');
const cu=c.uuid.toLowerCase();
setBleDiag(` ${cu.slice(4,8)} [${propsStr}]`,'info');
if(!notifyChar&&(p.notify||p.indicate))notifyChar=c;
if(!writeChar&&(p.write||p.writeWithoutResponses))writeChar=c;
}
if(!notifyChar||!writeChar){setBleDiag('Sem chars notify+write','err');return false}
setBleDiag(`Notify=${notifyChar.uuid.slice(4,8)} Write=${writeChar.uuid.slice(4,8)}`,'ok');
notifyChar.addEventListener('characteristicvaluechanged',(ev)=>{
const dv=ev.target.value;
const hex=Array.from(new Uint8Array(dv.buffer)).map(b=>b.toString(16).padStart(2,'0')).join(' ');
setBleDiag(`← RX ${dv.byteLength}b: ${hex.slice(0,100)}${hex.length>100?'...':''}`,'ok');
const first=new Uint8Array(dv.buffer)[0];
if(first===0xDD)bmsHandleChunk(deviceId,dv,deviceName);
else if(first===0xAA)bmsHandleJK(deviceId,dv,deviceName);
else if(first===0xA5)bmsHandleDaly(deviceId,dv,deviceName);
});
await notifyChar.startNotifications();
setBleDiag('Notify ativo · iniciando wake...','ok');
await new Promise(r=>setTimeout(r,500));
try{
const fn=writeChar.properties.writeWithoutResponses?'writeValueWithoutResponse':'writeValue';
await writeChar[fn](new Uint8Array([0x5A,0x5A,0x5A,0x5A]));
setBleDiag('Wake 5A x4 enviado','info');
}catch(e){setBleDiag('Wake skip: '+e.message,'info')}
await new Promise(r=>setTimeout(r,1500));
const PROTOCOLS=[
{name:'JBD-0x03',bytes:[0xDD,0xA5,0x03,0x00,0xFF,0xFD,0x77]},
{name:'JK-getInfo',bytes:[0xAA,0x55,0x90,0xEB,0x96,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10]},
{name:'Daly-getInfo',bytes:[0xA5,0x80,0x90,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xBD]},
];
for(const p of PROTOCOLS){
try{
setBleDiag(`→ TX ${p.name}`,'info');
const fn=writeChar.properties.writeWithoutResponses?'writeValueWithoutResponse':'writeValue';
await writeChar[fn](new Uint8Array(p.bytes));
setBleDiag(`✔ write ${p.name} OK`,'info');
await new Promise(r=>setTimeout(r,2500));
const dev=state.btDevices?.find(d=>d.id===deviceId);
if(dev?.bms?.voltage||dev?._lastRxAt){
setBleDiag(`✓ ${p.name} respondeu!`,'ok');
if(dev){dev.bmsProtocol=p.name;dev.isJBD=true;saveState()}
conn.notifyChar=notifyChar;conn.writeChar=writeChar;
return true;
}
setBleDiag(`✗ ${p.name} sem RX`,'info');
}catch(e){setBleDiag(`${p.name} erro: ${e.message}`,'warn')}
}
setBleDiag('⚠ Nenhum protocolo respondeu','err');
return false;
}catch(e){setBleDiag('Probe web falhou: '+e.message,'err');return false}
}
// Probe Capacitor: lista characteristics + identifica notify/write chars + tenta protocolos
async function bmsProbeAndAttach(deviceId,deviceName){
const backend=bleBackend();
if(backend!=='capacitor'){setBleDiag('Probe requer Capacitor (APK)','warn');return false}
if(backend==='web')return bmsProbeWebBluetooth(deviceId,deviceName);
if(backend!=='capacitor'){setBleDiag('Backend desconhecido','warn');return false}
const ble=window.Capacitor.Plugins.BluetoothLe;
try{
setBleDiag(`📦 Shivao v${APP_VERSION} · Probe iniciado`,'info');
@ -6369,7 +6440,7 @@ async function removeBluetoothDevice(id){
renderBluetoothCard();
}
const APP_VERSION='1.10.11';
const APP_VERSION='1.10.12';
function renderBluetoothCard(){
const el=document.getElementById('bt-list');
const supportEl=document.getElementById('bt-support');