diff --git a/app/diario-bordo.html b/app/diario-bordo.html
index cf51dbe..896d975 100644
--- a/app/diario-bordo.html
+++ b/app/diario-bordo.html
@@ -5992,65 +5992,142 @@ function bytesToBase64(arr){
return btoa(bin);
}
-async function bmsAttachJBD(deviceId,deviceName){
+// Probe: lista characteristics + identifica notify/write chars + tenta protocolos
+async function bmsProbeAndAttach(deviceId,deviceName){
const backend=bleBackend();
- if(backend!=='capacitor'){setBleDiag('JBD parser requer Capacitor (APK)','warn');return false}
+ if(backend!=='capacitor'){setBleDiag('Probe requer Capacitor (APK)','warn');return false}
const ble=window.Capacitor.Plugins.BluetoothLe;
try{
- setBleDiag('Detectando JBD BMS protocol...','info');
- // Subscribe nas notificações com listener registrado ANTES de start
- const listenerKey='notification|'+deviceId+'|'+BMS_JBD_SERVICE+'|'+BMS_JBD_NOTIFY;
+ setBleDiag('🔍 Enumerando characteristics...','info');
+ // Tenta serviços vendor: ff00, fff0 (Daly), ffe0 (JK), 0203
+ const VENDOR_SVCS=[
+ '0000ff00-0000-1000-8000-00805f9b34fb',
+ '0000fff0-0000-1000-8000-00805f9b34fb',
+ '0000ffe0-0000-1000-8000-00805f9b34fb',
+ '00000203-0000-1000-8000-00805f9b34fb',
+ ];
+ let notifyChar=null,writeChar=null,foundService=null;
+ for(const svcId of VENDOR_SVCS){
+ try{
+ const r=await ble.getServices({deviceId});
+ const svcs=r.services||r||[];
+ const svc=svcs.find(s=>(s.uuid||'').toLowerCase()===svcId);
+ if(!svc)continue;
+ const chars=svc.characteristics||[];
+ if(chars.length===0)continue;
+ setBleDiag(`Service ${svcId.slice(4,8)} · ${chars.length} chars`,'info');
+ for(const c of chars){
+ const props=c.properties||{};
+ const propsStr=[props.notify&&'notify',props.indicate&&'indicate',props.write&&'write',props.writeWithoutResponse&&'wnr',props.read&&'read'].filter(Boolean).join(',');
+ setBleDiag(` ${(c.uuid||'').slice(4,8)} [${propsStr}]`,'info');
+ if(!notifyChar&&(props.notify||props.indicate)){notifyChar=c.uuid;foundService=svc.uuid}
+ if(!writeChar&&(props.write||props.writeWithoutResponse))writeChar=c.uuid;
+ }
+ if(notifyChar&&writeChar)break;
+ }catch(e){}
+ }
+ if(!notifyChar||!writeChar){
+ setBleDiag('Não achei chars notify+write em services vendor','err');
+ return false;
+ }
+ setBleDiag(`Notify=${notifyChar.slice(4,8)} Write=${writeChar.slice(4,8)} Svc=${foundService.slice(4,8)}`,'ok');
+ // Subscribe + handler
+ const listenerKey='notification|'+deviceId+'|'+foundService+'|'+notifyChar;
ble.addListener(listenerKey,(ev)=>{
const dv=parseDataView(ev.value);
const hex=Array.from(new Uint8Array(dv.buffer)).map(b=>b.toString(16).padStart(2,'0')).join(' ');
- setBleDiag('← RX '+dv.byteLength+' bytes: '+hex.slice(0,80)+(hex.length>80?'...':''),'info');
- bmsHandleChunk(deviceId,dv,deviceName);
+ setBleDiag('← RX '+dv.byteLength+'b: '+hex.slice(0,100)+(hex.length>100?'...':''),'ok');
+ // Detecta protocolo por byte de início
+ const first=new Uint8Array(dv.buffer)[0];
+ if(first===0xDD)bmsHandleChunk(deviceId,dv,deviceName); // JBD
+ else if(first===0xAA)bmsHandleJK(deviceId,dv,deviceName); // JK BMS
+ else if(first===0xA5)bmsHandleDaly(deviceId,dv,deviceName); // Daly
});
- await ble.startNotifications({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_NOTIFY});
- setBleDiag('Notify ff01 ativo · aguardando 500ms...','ok');
- await new Promise(r=>setTimeout(r,500)); // alguns BMS precisam wake-up
- setBleDiag('→ TX comando 0x03 (basic info)','info');
- await bmsQueryBasic(deviceId);
+ await ble.startNotifications({deviceId,service:foundService,characteristic:notifyChar});
+ setBleDiag('Notify ativo · aguardando 800ms...','ok');
+ await new Promise(r=>setTimeout(r,800));
+ // Salva config no device pra reuso
const dev=state.btDevices?.find(d=>d.id===deviceId);
if(dev){
+ dev.bmsService=foundService;
+ dev.bmsNotifyChar=notifyChar;
+ dev.bmsWriteChar=writeChar;
dev.isJBD=true;
- if(dev._pollInterval)clearInterval(dev._pollInterval);
+ saveState();
}
- // Se em 5s não chegou resposta, tenta com writeWithoutResponse
- setTimeout(async()=>{
- const dev2=state.btDevices?.find(d=>d.id===deviceId);
- if(dev2&&!dev2.bms?.voltage){
- setBleDiag('Sem resposta · tentando writeWithoutResponse...','warn');
- await bmsQueryBasic(deviceId,true).catch(e=>setBleDiag('writeWoR falhou: '+e.message,'err'));
- }
- },5000);
- setInterval(()=>bmsQueryBasic(deviceId).catch(()=>{}),30000);
- return true;
+ // Tenta cada protocolo até alguém responder
+ return await bmsTryProtocols(deviceId);
}catch(e){
- setBleDiag('JBD attach falhou: '+(e.message||e.errorMessage),'err');
+ setBleDiag('Probe falhou: '+(e.message||e.errorMessage),'err');
return false;
}
}
-async function bmsQueryBasic(deviceId,withoutResponse){
+async function bmsWriteCmd(deviceId,bytes,withoutResponse){
const ble=window.Capacitor?.Plugins?.BluetoothLe;
- if(!ble)return;
+ const dev=state.btDevices?.find(d=>d.id===deviceId);
+ if(!ble||!dev?.bmsService||!dev?.bmsWriteChar)throw new Error('config BMS ausente');
const fn=withoutResponse?'writeWithoutResponse':'write';
- await ble[fn]({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_WRITE,value:bytesToBase64(BMS_CMD_BASIC)});
+ await ble[fn]({deviceId,service:dev.bmsService,characteristic:dev.bmsWriteChar,value:bytesToBase64(bytes)});
+}
+
+async function bmsTryProtocols(deviceId){
+ 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]},
+ {name:'JBD-write-no-response',bytes:[0xDD,0xA5,0x03,0x00,0xFF,0xFD,0x77],wnr:true},
+ ];
+ for(const p of PROTOCOLS){
+ try{
+ setBleDiag(`→ TX ${p.name}: ${p.bytes.map(b=>b.toString(16).padStart(2,'0')).join(' ').slice(0,40)}`,'info');
+ await bmsWriteCmd(deviceId,p.bytes,p.wnr);
+ // Espera 2s pra ver se gerou RX
+ 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');
+ // Configura poll periódico com este protocolo
+ if(dev)dev.bmsProtocol=p.name;
+ setInterval(async()=>{try{await bmsWriteCmd(deviceId,p.bytes,p.wnr)}catch{}},30000);
+ return true;
+ }
+ }catch(e){setBleDiag(`${p.name} falhou: ${e.message||e.errorMessage}`,'warn')}
+ }
+ setBleDiag('⚠ Nenhum protocolo funcionou. BMS pode usar firmware proprietário não documentado.','err');
+ return false;
+}
+
+// Stubs pra protocolos JK e Daly (parsers básicos)
+function bmsHandleJK(deviceId,dv,deviceName){
+ const dev=state.btDevices?.find(d=>d.id===deviceId);if(!dev)return;
+ dev._lastRxAt=Date.now();
+ // JK protocol: header AA 55 90 EB
+ // Frame info varia muito por modelo — por hora só confirma RX
+ setBleDiag('JK BMS frame recebido (parser específico em desenvolvimento)','info');
+}
+function bmsHandleDaly(deviceId,dv,deviceName){
+ const dev=state.btDevices?.find(d=>d.id===deviceId);if(!dev)return;
+ dev._lastRxAt=Date.now();
+ setBleDiag('Daly BMS frame recebido (parser específico em desenvolvimento)','info');
+}
+
+// Compat alias - chamadas antigas viram probe
+async function bmsAttachJBD(deviceId,deviceName){
+ return bmsProbeAndAttach(deviceId,deviceName);
+}
+
+async function bmsQueryBasic(deviceId,withoutResponse){
+ // Usa config descoberta no probe
+ await bmsWriteCmd(deviceId,BMS_CMD_BASIC,withoutResponse);
}
// Re-leitura manual a partir do botão UI
async function bmsManualRead(deviceId){
- setBleDiag('🔄 Re-leitura manual...','info');
+ setBleDiag('🔄 Re-leitura manual · re-rodando probe completo...','info');
try{
- await bmsQueryBasic(deviceId);
- setTimeout(async()=>{
- const dev=state.btDevices?.find(d=>d.id===deviceId);
- if(dev&&!dev.bms?.voltage){
- setBleDiag('Tentando writeWithoutResponse...','warn');
- await bmsQueryBasic(deviceId,true);
- }
- },3000);
+ // Re-roda probe completo (lista chars de novo + tenta protocolos)
+ await bmsProbeAndAttach(deviceId,state.btDevices?.find(d=>d.id===deviceId)?.name||'BMS');
}catch(e){setBleDiag('Manual read falhou: '+(e.message||e.errorMessage),'err')}
}
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index 5de84f2..4227dcb 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "br.com.pontualtech.shivao"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 17
- versionName "1.10.1"
+ versionCode 18
+ versionName "1.10.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/mobile/package.json b/mobile/package.json
index 590f518..b00efc7 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "shivao-mobile",
- "version": "1.10.1",
+ "version": "1.10.2",
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
"main": "index.js",
"type": "module",
diff --git a/server/public/index.html b/server/public/index.html
index cf51dbe..896d975 100644
--- a/server/public/index.html
+++ b/server/public/index.html
@@ -5992,65 +5992,142 @@ function bytesToBase64(arr){
return btoa(bin);
}
-async function bmsAttachJBD(deviceId,deviceName){
+// Probe: lista characteristics + identifica notify/write chars + tenta protocolos
+async function bmsProbeAndAttach(deviceId,deviceName){
const backend=bleBackend();
- if(backend!=='capacitor'){setBleDiag('JBD parser requer Capacitor (APK)','warn');return false}
+ if(backend!=='capacitor'){setBleDiag('Probe requer Capacitor (APK)','warn');return false}
const ble=window.Capacitor.Plugins.BluetoothLe;
try{
- setBleDiag('Detectando JBD BMS protocol...','info');
- // Subscribe nas notificações com listener registrado ANTES de start
- const listenerKey='notification|'+deviceId+'|'+BMS_JBD_SERVICE+'|'+BMS_JBD_NOTIFY;
+ setBleDiag('🔍 Enumerando characteristics...','info');
+ // Tenta serviços vendor: ff00, fff0 (Daly), ffe0 (JK), 0203
+ const VENDOR_SVCS=[
+ '0000ff00-0000-1000-8000-00805f9b34fb',
+ '0000fff0-0000-1000-8000-00805f9b34fb',
+ '0000ffe0-0000-1000-8000-00805f9b34fb',
+ '00000203-0000-1000-8000-00805f9b34fb',
+ ];
+ let notifyChar=null,writeChar=null,foundService=null;
+ for(const svcId of VENDOR_SVCS){
+ try{
+ const r=await ble.getServices({deviceId});
+ const svcs=r.services||r||[];
+ const svc=svcs.find(s=>(s.uuid||'').toLowerCase()===svcId);
+ if(!svc)continue;
+ const chars=svc.characteristics||[];
+ if(chars.length===0)continue;
+ setBleDiag(`Service ${svcId.slice(4,8)} · ${chars.length} chars`,'info');
+ for(const c of chars){
+ const props=c.properties||{};
+ const propsStr=[props.notify&&'notify',props.indicate&&'indicate',props.write&&'write',props.writeWithoutResponse&&'wnr',props.read&&'read'].filter(Boolean).join(',');
+ setBleDiag(` ${(c.uuid||'').slice(4,8)} [${propsStr}]`,'info');
+ if(!notifyChar&&(props.notify||props.indicate)){notifyChar=c.uuid;foundService=svc.uuid}
+ if(!writeChar&&(props.write||props.writeWithoutResponse))writeChar=c.uuid;
+ }
+ if(notifyChar&&writeChar)break;
+ }catch(e){}
+ }
+ if(!notifyChar||!writeChar){
+ setBleDiag('Não achei chars notify+write em services vendor','err');
+ return false;
+ }
+ setBleDiag(`Notify=${notifyChar.slice(4,8)} Write=${writeChar.slice(4,8)} Svc=${foundService.slice(4,8)}`,'ok');
+ // Subscribe + handler
+ const listenerKey='notification|'+deviceId+'|'+foundService+'|'+notifyChar;
ble.addListener(listenerKey,(ev)=>{
const dv=parseDataView(ev.value);
const hex=Array.from(new Uint8Array(dv.buffer)).map(b=>b.toString(16).padStart(2,'0')).join(' ');
- setBleDiag('← RX '+dv.byteLength+' bytes: '+hex.slice(0,80)+(hex.length>80?'...':''),'info');
- bmsHandleChunk(deviceId,dv,deviceName);
+ setBleDiag('← RX '+dv.byteLength+'b: '+hex.slice(0,100)+(hex.length>100?'...':''),'ok');
+ // Detecta protocolo por byte de início
+ const first=new Uint8Array(dv.buffer)[0];
+ if(first===0xDD)bmsHandleChunk(deviceId,dv,deviceName); // JBD
+ else if(first===0xAA)bmsHandleJK(deviceId,dv,deviceName); // JK BMS
+ else if(first===0xA5)bmsHandleDaly(deviceId,dv,deviceName); // Daly
});
- await ble.startNotifications({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_NOTIFY});
- setBleDiag('Notify ff01 ativo · aguardando 500ms...','ok');
- await new Promise(r=>setTimeout(r,500)); // alguns BMS precisam wake-up
- setBleDiag('→ TX comando 0x03 (basic info)','info');
- await bmsQueryBasic(deviceId);
+ await ble.startNotifications({deviceId,service:foundService,characteristic:notifyChar});
+ setBleDiag('Notify ativo · aguardando 800ms...','ok');
+ await new Promise(r=>setTimeout(r,800));
+ // Salva config no device pra reuso
const dev=state.btDevices?.find(d=>d.id===deviceId);
if(dev){
+ dev.bmsService=foundService;
+ dev.bmsNotifyChar=notifyChar;
+ dev.bmsWriteChar=writeChar;
dev.isJBD=true;
- if(dev._pollInterval)clearInterval(dev._pollInterval);
+ saveState();
}
- // Se em 5s não chegou resposta, tenta com writeWithoutResponse
- setTimeout(async()=>{
- const dev2=state.btDevices?.find(d=>d.id===deviceId);
- if(dev2&&!dev2.bms?.voltage){
- setBleDiag('Sem resposta · tentando writeWithoutResponse...','warn');
- await bmsQueryBasic(deviceId,true).catch(e=>setBleDiag('writeWoR falhou: '+e.message,'err'));
- }
- },5000);
- setInterval(()=>bmsQueryBasic(deviceId).catch(()=>{}),30000);
- return true;
+ // Tenta cada protocolo até alguém responder
+ return await bmsTryProtocols(deviceId);
}catch(e){
- setBleDiag('JBD attach falhou: '+(e.message||e.errorMessage),'err');
+ setBleDiag('Probe falhou: '+(e.message||e.errorMessage),'err');
return false;
}
}
-async function bmsQueryBasic(deviceId,withoutResponse){
+async function bmsWriteCmd(deviceId,bytes,withoutResponse){
const ble=window.Capacitor?.Plugins?.BluetoothLe;
- if(!ble)return;
+ const dev=state.btDevices?.find(d=>d.id===deviceId);
+ if(!ble||!dev?.bmsService||!dev?.bmsWriteChar)throw new Error('config BMS ausente');
const fn=withoutResponse?'writeWithoutResponse':'write';
- await ble[fn]({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_WRITE,value:bytesToBase64(BMS_CMD_BASIC)});
+ await ble[fn]({deviceId,service:dev.bmsService,characteristic:dev.bmsWriteChar,value:bytesToBase64(bytes)});
+}
+
+async function bmsTryProtocols(deviceId){
+ 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]},
+ {name:'JBD-write-no-response',bytes:[0xDD,0xA5,0x03,0x00,0xFF,0xFD,0x77],wnr:true},
+ ];
+ for(const p of PROTOCOLS){
+ try{
+ setBleDiag(`→ TX ${p.name}: ${p.bytes.map(b=>b.toString(16).padStart(2,'0')).join(' ').slice(0,40)}`,'info');
+ await bmsWriteCmd(deviceId,p.bytes,p.wnr);
+ // Espera 2s pra ver se gerou RX
+ 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');
+ // Configura poll periódico com este protocolo
+ if(dev)dev.bmsProtocol=p.name;
+ setInterval(async()=>{try{await bmsWriteCmd(deviceId,p.bytes,p.wnr)}catch{}},30000);
+ return true;
+ }
+ }catch(e){setBleDiag(`${p.name} falhou: ${e.message||e.errorMessage}`,'warn')}
+ }
+ setBleDiag('⚠ Nenhum protocolo funcionou. BMS pode usar firmware proprietário não documentado.','err');
+ return false;
+}
+
+// Stubs pra protocolos JK e Daly (parsers básicos)
+function bmsHandleJK(deviceId,dv,deviceName){
+ const dev=state.btDevices?.find(d=>d.id===deviceId);if(!dev)return;
+ dev._lastRxAt=Date.now();
+ // JK protocol: header AA 55 90 EB
+ // Frame info varia muito por modelo — por hora só confirma RX
+ setBleDiag('JK BMS frame recebido (parser específico em desenvolvimento)','info');
+}
+function bmsHandleDaly(deviceId,dv,deviceName){
+ const dev=state.btDevices?.find(d=>d.id===deviceId);if(!dev)return;
+ dev._lastRxAt=Date.now();
+ setBleDiag('Daly BMS frame recebido (parser específico em desenvolvimento)','info');
+}
+
+// Compat alias - chamadas antigas viram probe
+async function bmsAttachJBD(deviceId,deviceName){
+ return bmsProbeAndAttach(deviceId,deviceName);
+}
+
+async function bmsQueryBasic(deviceId,withoutResponse){
+ // Usa config descoberta no probe
+ await bmsWriteCmd(deviceId,BMS_CMD_BASIC,withoutResponse);
}
// Re-leitura manual a partir do botão UI
async function bmsManualRead(deviceId){
- setBleDiag('🔄 Re-leitura manual...','info');
+ setBleDiag('🔄 Re-leitura manual · re-rodando probe completo...','info');
try{
- await bmsQueryBasic(deviceId);
- setTimeout(async()=>{
- const dev=state.btDevices?.find(d=>d.id===deviceId);
- if(dev&&!dev.bms?.voltage){
- setBleDiag('Tentando writeWithoutResponse...','warn');
- await bmsQueryBasic(deviceId,true);
- }
- },3000);
+ // Re-roda probe completo (lista chars de novo + tenta protocolos)
+ await bmsProbeAndAttach(deviceId,state.btDevices?.find(d=>d.id===deviceId)?.name||'BMS');
}catch(e){setBleDiag('Manual read falhou: '+(e.message||e.errorMessage),'err')}
}
diff --git a/server/src/index.js b/server/src/index.js
index f364012..786aaab 100644
--- a/server/src/index.js
+++ b/server/src/index.js
@@ -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.10.1/Shivao-v1.10.1.apk';
+const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.10.2/Shivao-v1.10.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)