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)