diff --git a/app/diario-bordo.html b/app/diario-bordo.html
index f7ec915..cf51dbe 100644
--- a/app/diario-bordo.html
+++ b/app/diario-bordo.html
@@ -2542,6 +2542,21 @@ Hora: {HORA}
+
+
+
+
+
⚡ Monitor da Bateria
+
+
+
+
+
+
+
@@ -5983,21 +5998,32 @@ async function bmsAttachJBD(deviceId,deviceName){
const ble=window.Capacitor.Plugins.BluetoothLe;
try{
setBleDiag('Detectando JBD BMS protocol...','info');
- // Subscribe nas notificações
- await ble.startNotifications({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_NOTIFY});
- ble.addListener('notification|'+deviceId+'|'+BMS_JBD_SERVICE+'|'+BMS_JBD_NOTIFY,(ev)=>{
+ // Subscribe nas notificações com listener registrado ANTES de start
+ const listenerKey='notification|'+deviceId+'|'+BMS_JBD_SERVICE+'|'+BMS_JBD_NOTIFY;
+ 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('Notify ff01 OK · enviando query...','ok');
- // Solicita dados básicos
+ 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);
- // Re-poll a cada 30s
const dev=state.btDevices?.find(d=>d.id===deviceId);
if(dev){
dev.isJBD=true;
if(dev._pollInterval)clearInterval(dev._pollInterval);
}
+ // 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;
}catch(e){
@@ -6006,10 +6032,26 @@ async function bmsAttachJBD(deviceId,deviceName){
}
}
-async function bmsQueryBasic(deviceId){
+async function bmsQueryBasic(deviceId,withoutResponse){
const ble=window.Capacitor?.Plugins?.BluetoothLe;
if(!ble)return;
- await ble.write({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_WRITE,value:bytesToBase64(BMS_CMD_BASIC)});
+ const fn=withoutResponse?'writeWithoutResponse':'write';
+ await ble[fn]({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_WRITE,value:bytesToBase64(BMS_CMD_BASIC)});
+}
+
+// Re-leitura manual a partir do botão UI
+async function bmsManualRead(deviceId){
+ setBleDiag('🔄 Re-leitura manual...','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);
+ }catch(e){setBleDiag('Manual read falhou: '+(e.message||e.errorMessage),'err')}
}
function bmsHandleChunk(deviceId,dv,deviceName){
@@ -6185,7 +6227,9 @@ function renderBluetoothCard(){
${bat!=null?`${bat}% · `:''}${isConnected?'conectado':'offline'}${d.manufacturer?` · ${escapeHtml(d.manufacturer)}`:''}${d.model?` ${escapeHtml(d.model)}`:''}
+ ${d.isJBD?``:''}
${!isConnected?``:''}
+ ${d.isJBD?``:''}
${bmsBlock}
@@ -6196,6 +6240,123 @@ function renderBluetoothCard(){
// Re-render quando entra na aba Mais
function refreshBluetoothCard(){renderBluetoothCard()}
+// ============ BMS DASHBOARD (monitor de bordo full-screen) ============
+let _dashRefreshTimer=null;
+function openBmsDashboard(deviceId){
+ window._currentDashDeviceId=deviceId;
+ const dev=state.btDevices?.find(d=>d.id===deviceId);
+ if(!dev)return;
+ document.getElementById('bms-dash-title').textContent='⚡ '+(dev.name||'Bateria');
+ renderBmsDashboard();
+ openModal('bms-dashboard-modal');
+ // Auto-refresh a cada 5s enquanto modal aberto
+ if(_dashRefreshTimer)clearInterval(_dashRefreshTimer);
+ _dashRefreshTimer=setInterval(()=>{
+ if(document.getElementById('bms-dashboard-modal')?.classList.contains('show')){
+ bmsManualRead(deviceId);
+ setTimeout(renderBmsDashboard,2000);
+ }else{
+ clearInterval(_dashRefreshTimer);_dashRefreshTimer=null;
+ }
+ },10000);
+}
+
+function renderBmsDashboard(){
+ const id=window._currentDashDeviceId;
+ const dev=state.btDevices?.find(d=>d.id===id);
+ const body=document.getElementById('bms-dash-body');
+ if(!dev||!body)return;
+ const b=dev.bms||{};
+ if(!b.voltage){
+ body.innerHTML=`
+
+
⏳
+
Aguardando dados da bateria...
+
O BMS deve responder em alguns segundos. Se demorar, toque 🔄 Re-ler abaixo.
+
`;
+ return;
+ }
+ const isCharging=b.current>0;
+ const isDischarging=b.current<0;
+ const power=Math.abs(b.voltage*b.current).toFixed(0);
+ const flowText=isCharging?'⚡ CARREGANDO':isDischarging?'↓ DESCARGA':'— REPOUSO';
+ const flowColor=isCharging?'#10b981':isDischarging?'#f59e0b':'#7d97ad';
+ const socColor=b.soc<20?'#ef4444':b.soc<50?'#f59e0b':'#10b981';
+ // Tempo restante (descarga) ou tempo até cheia (carga)
+ let timeRemaining='';
+ if(isDischarging&&b.remainCap){
+ const hours=b.remainCap/Math.abs(b.current);
+ timeRemaining=hours>1?`~${hours.toFixed(1)}h restantes`:`~${Math.round(hours*60)}min restantes`;
+ }else if(isCharging&&b.totalCap&&b.remainCap){
+ const hoursToFull=(b.totalCap-b.remainCap)/b.current;
+ timeRemaining=hoursToFull>0?`~${hoursToFull.toFixed(1)}h até cheia`:'Quase cheia';
+ }
+ const cellsBlock=b.cells&&b.cells.length>0?`
+
+
Células (${b.cells.length}S)
+
+ ${b.cells.map((v,i)=>{
+ const c=v<3.0?'#ef4444':v<3.3?'#f59e0b':v>3.6?'#10b981':'#06b6d4';
+ return `
+
C${i+1}
+
${v.toFixed(3)}V
+
`;
+ }).join('')}
+
+
`:'';
+ body.innerHTML=`
+
+
+
+
+
+
${b.soc||'?'}%
+
Estado de Carga
+
+
+
+
+
+ ${flowText}${timeRemaining?' · '+timeRemaining:''}
+
+
+
+
+
Tensão Total
+
${b.voltage.toFixed(2)} V
+
+
+
Corrente
+
${b.current.toFixed(2)} A
+
+
+
Potência
+
${power} W
+
+
+
Capacidade
+
${b.remainCap?b.remainCap.toFixed(1):'?'} /${b.totalCap?b.totalCap.toFixed(0):'?'}Ah
+
+
+
+
+
+ ♻ ${b.cycles||0} ciclos
+ ${b.temps&&b.temps.length>0?'🌡 '+b.temps.map(t=>t+'°C').join(', '):''}
+ v${b.swVersion||'?'}
+
+
+ ${cellsBlock}
+ Última leitura: ${new Date(b.lastRead).toLocaleTimeString('pt-BR')} · auto-refresh 10s
+ `;
+}
+
// ============ RAYMARINE / NMEA 2000 GATEWAY ============
// Slot pra quando Karlão tiver gateway NMEA 2000 → WiFi (Yacht Devices YDWG-02, etc)
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index 2214bc1..5de84f2 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 16
- versionName "1.10.0"
+ versionCode 17
+ versionName "1.10.1"
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 6c500e2..590f518 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "shivao-mobile",
- "version": "1.10.0",
+ "version": "1.10.1",
"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 f7ec915..cf51dbe 100644
--- a/server/public/index.html
+++ b/server/public/index.html
@@ -2542,6 +2542,21 @@ Hora: {HORA}
+
+
+
+
+
⚡ Monitor da Bateria
+
+
+
+
+
+
+
@@ -5983,21 +5998,32 @@ async function bmsAttachJBD(deviceId,deviceName){
const ble=window.Capacitor.Plugins.BluetoothLe;
try{
setBleDiag('Detectando JBD BMS protocol...','info');
- // Subscribe nas notificações
- await ble.startNotifications({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_NOTIFY});
- ble.addListener('notification|'+deviceId+'|'+BMS_JBD_SERVICE+'|'+BMS_JBD_NOTIFY,(ev)=>{
+ // Subscribe nas notificações com listener registrado ANTES de start
+ const listenerKey='notification|'+deviceId+'|'+BMS_JBD_SERVICE+'|'+BMS_JBD_NOTIFY;
+ 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('Notify ff01 OK · enviando query...','ok');
- // Solicita dados básicos
+ 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);
- // Re-poll a cada 30s
const dev=state.btDevices?.find(d=>d.id===deviceId);
if(dev){
dev.isJBD=true;
if(dev._pollInterval)clearInterval(dev._pollInterval);
}
+ // 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;
}catch(e){
@@ -6006,10 +6032,26 @@ async function bmsAttachJBD(deviceId,deviceName){
}
}
-async function bmsQueryBasic(deviceId){
+async function bmsQueryBasic(deviceId,withoutResponse){
const ble=window.Capacitor?.Plugins?.BluetoothLe;
if(!ble)return;
- await ble.write({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_WRITE,value:bytesToBase64(BMS_CMD_BASIC)});
+ const fn=withoutResponse?'writeWithoutResponse':'write';
+ await ble[fn]({deviceId,service:BMS_JBD_SERVICE,characteristic:BMS_JBD_WRITE,value:bytesToBase64(BMS_CMD_BASIC)});
+}
+
+// Re-leitura manual a partir do botão UI
+async function bmsManualRead(deviceId){
+ setBleDiag('🔄 Re-leitura manual...','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);
+ }catch(e){setBleDiag('Manual read falhou: '+(e.message||e.errorMessage),'err')}
}
function bmsHandleChunk(deviceId,dv,deviceName){
@@ -6185,7 +6227,9 @@ function renderBluetoothCard(){
${bat!=null?`${bat}% · `:''}${isConnected?'conectado':'offline'}${d.manufacturer?` · ${escapeHtml(d.manufacturer)}`:''}${d.model?` ${escapeHtml(d.model)}`:''}
+ ${d.isJBD?``:''}
${!isConnected?``:''}
+ ${d.isJBD?``:''}
${bmsBlock}
@@ -6196,6 +6240,123 @@ function renderBluetoothCard(){
// Re-render quando entra na aba Mais
function refreshBluetoothCard(){renderBluetoothCard()}
+// ============ BMS DASHBOARD (monitor de bordo full-screen) ============
+let _dashRefreshTimer=null;
+function openBmsDashboard(deviceId){
+ window._currentDashDeviceId=deviceId;
+ const dev=state.btDevices?.find(d=>d.id===deviceId);
+ if(!dev)return;
+ document.getElementById('bms-dash-title').textContent='⚡ '+(dev.name||'Bateria');
+ renderBmsDashboard();
+ openModal('bms-dashboard-modal');
+ // Auto-refresh a cada 5s enquanto modal aberto
+ if(_dashRefreshTimer)clearInterval(_dashRefreshTimer);
+ _dashRefreshTimer=setInterval(()=>{
+ if(document.getElementById('bms-dashboard-modal')?.classList.contains('show')){
+ bmsManualRead(deviceId);
+ setTimeout(renderBmsDashboard,2000);
+ }else{
+ clearInterval(_dashRefreshTimer);_dashRefreshTimer=null;
+ }
+ },10000);
+}
+
+function renderBmsDashboard(){
+ const id=window._currentDashDeviceId;
+ const dev=state.btDevices?.find(d=>d.id===id);
+ const body=document.getElementById('bms-dash-body');
+ if(!dev||!body)return;
+ const b=dev.bms||{};
+ if(!b.voltage){
+ body.innerHTML=`
+
+
⏳
+
Aguardando dados da bateria...
+
O BMS deve responder em alguns segundos. Se demorar, toque 🔄 Re-ler abaixo.
+
`;
+ return;
+ }
+ const isCharging=b.current>0;
+ const isDischarging=b.current<0;
+ const power=Math.abs(b.voltage*b.current).toFixed(0);
+ const flowText=isCharging?'⚡ CARREGANDO':isDischarging?'↓ DESCARGA':'— REPOUSO';
+ const flowColor=isCharging?'#10b981':isDischarging?'#f59e0b':'#7d97ad';
+ const socColor=b.soc<20?'#ef4444':b.soc<50?'#f59e0b':'#10b981';
+ // Tempo restante (descarga) ou tempo até cheia (carga)
+ let timeRemaining='';
+ if(isDischarging&&b.remainCap){
+ const hours=b.remainCap/Math.abs(b.current);
+ timeRemaining=hours>1?`~${hours.toFixed(1)}h restantes`:`~${Math.round(hours*60)}min restantes`;
+ }else if(isCharging&&b.totalCap&&b.remainCap){
+ const hoursToFull=(b.totalCap-b.remainCap)/b.current;
+ timeRemaining=hoursToFull>0?`~${hoursToFull.toFixed(1)}h até cheia`:'Quase cheia';
+ }
+ const cellsBlock=b.cells&&b.cells.length>0?`
+
+
Células (${b.cells.length}S)
+
+ ${b.cells.map((v,i)=>{
+ const c=v<3.0?'#ef4444':v<3.3?'#f59e0b':v>3.6?'#10b981':'#06b6d4';
+ return `
+
C${i+1}
+
${v.toFixed(3)}V
+
`;
+ }).join('')}
+
+
`:'';
+ body.innerHTML=`
+
+
+
+
+
+
${b.soc||'?'}%
+
Estado de Carga
+
+
+
+
+
+ ${flowText}${timeRemaining?' · '+timeRemaining:''}
+
+
+
+
+
Tensão Total
+
${b.voltage.toFixed(2)} V
+
+
+
Corrente
+
${b.current.toFixed(2)} A
+
+
+
Potência
+
${power} W
+
+
+
Capacidade
+
${b.remainCap?b.remainCap.toFixed(1):'?'} /${b.totalCap?b.totalCap.toFixed(0):'?'}Ah
+
+
+
+
+
+ ♻ ${b.cycles||0} ciclos
+ ${b.temps&&b.temps.length>0?'🌡 '+b.temps.map(t=>t+'°C').join(', '):''}
+ v${b.swVersion||'?'}
+
+
+ ${cellsBlock}
+ Última leitura: ${new Date(b.lastRead).toLocaleTimeString('pt-BR')} · auto-refresh 10s
+ `;
+}
+
// ============ RAYMARINE / NMEA 2000 GATEWAY ============
// Slot pra quando Karlão tiver gateway NMEA 2000 → WiFi (Yacht Devices YDWG-02, etc)
diff --git a/server/src/index.js b/server/src/index.js
index 811ae72..f364012 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.0/Shivao-v1.10.0.apk';
+const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.10.1/Shivao-v1.10.1.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)