diff --git a/app/diario-bordo.html b/app/diario-bordo.html
index 5f2a762..4f0401f 100644
--- a/app/diario-bordo.html
+++ b/app/diario-bordo.html
@@ -1970,6 +1970,31 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
+
+
+
📡 Bluetooth · Baterias & Acessórios
+
Pareie BMS de bateria de lítio (com BLE), fones, smartwatch, smart shunts. Mostra nível de carga em tempo real no app.
+
Verificando suporte...
+
+
+
Limitações: iOS Safari não suporta Web Bluetooth (use no Chrome PC ou Android). Reconexão automática varia por device.
+
+
+
+
+
⚓ Instrumentos Raymarine (gateway NMEA 2000)
+
Pra ler dados de Raymarine (profundidade, vento, GPS, piloto automático), instale um gateway NMEA 2000→WiFi no barco e conecte ao bus SeaTalkNG. Recomendados: Yacht Devices YDWG-02, Actisense W2K-1.
+
+
+
+
+
+
+
+
+
Status: apenas slot de configuração. Parser NMEA 2000 PGNs (depth, wind, AIS, autopilot) será ativado em v1.10 quando você tiver o gateway físico instalado.
+
+
🗺️ Exportar para OpenCPN
@@ -3262,7 +3287,7 @@ function switchPanel(name){
if(p)p.classList.add('active');
// FAB visível em panels que têm "criar item"
document.getElementById('fab').style.display=['trips','maintenance','pending','zones','overview'].includes(name)?'flex':'none';
- if(name==='export'){updateStorageInfo();bindCloudInputs();renderCloudStatus();renderShareList();bindWeatherInputs();renderAuthBox();refreshGoogleStatus()}
+ if(name==='export'){updateStorageInfo();bindCloudInputs();renderCloudStatus();renderShareList();bindWeatherInputs();renderAuthBox();refreshGoogleStatus();renderBluetoothCard();renderNmeaGatewayCard()}
if(name==='pending'&&_gcalConnected&&Date.now()-_gcalLastPullAt>GCAL_PULL_INTERVAL_MS)googlePullNow();
if(name==='zones')renderZones();
window.scrollTo(0,0);
@@ -5717,6 +5742,189 @@ function testChartCfg(){
toast('Cartas salvas — abra um mapa pra ver');
}
+// ============ BLUETOOTH (Web Bluetooth API — Battery Service genérico) ============
+// Funciona em Chrome PC + Chrome Android. NÃO funciona em Safari (iOS).
+// Pra suporte iOS: precisa plugin nativo @capacitor/community/bluetooth-le (próxima versão).
+
+const BLE_BATTERY_SERVICE='battery_service'; // 0x180F
+const BLE_BATTERY_CHAR='battery_level'; // 0x2A19
+const BLE_DEVICE_INFO='device_information'; // 0x180A
+const BLE_OPTIONAL_SERVICES=['device_information'];
+
+const _bleConnections=new Map(); // id → {device, server, batteryChar}
+
+function bleSupported(){return !!navigator.bluetooth}
+
+async function pairBluetoothDevice(){
+ if(!bleSupported()){
+ toast('Web Bluetooth não suportado neste navegador. Use Chrome no PC ou Android.');
+ return;
+ }
+ try{
+ const device=await navigator.bluetooth.requestDevice({
+ // Filtro permissivo: aceita qualquer device com Battery Service OU qualquer device que o usuário escolher
+ acceptAllDevices:true,
+ optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
+ });
+ if(!device){return}
+ // Conecta + lê info inicial
+ const info=await connectAndRead(device);
+ // Salva no state
+ if(!state.btDevices)state.btDevices=[];
+ const existing=state.btDevices.find(d=>d.id===device.id);
+ if(existing){
+ Object.assign(existing,{name:device.name||existing.name,lastBattery:info.battery,lastSeen:Date.now(),manufacturer:info.manufacturer,model:info.model});
+ }else{
+ state.btDevices.push({
+ id:device.id,
+ name:device.name||'Dispositivo BLE',
+ lastBattery:info.battery,
+ lastSeen:Date.now(),
+ manufacturer:info.manufacturer,
+ model:info.model,
+ addedAt:Date.now(),
+ });
+ }
+ saveState();
+ renderBluetoothCard();
+ toast('✓ '+(device.name||'Dispositivo')+' pareado'+(info.battery!=null?' · '+info.battery+'%':''));
+ // Listener pra desconexão
+ device.addEventListener('gattserverdisconnected',()=>{
+ _bleConnections.delete(device.id);
+ renderBluetoothCard();
+ });
+ }catch(e){
+ if(e.name==='NotFoundError')return; // user cancelou
+ console.warn('[ble] pair failed',e);
+ toast('Erro: '+e.message);
+ }
+}
+
+async function connectAndRead(device){
+ const info={battery:null,manufacturer:null,model:null};
+ try{
+ const server=await device.gatt.connect();
+ _bleConnections.set(device.id,{device,server});
+ // Battery Service (opcional)
+ try{
+ const svc=await server.getPrimaryService(BLE_BATTERY_SERVICE);
+ const batChar=await svc.getCharacteristic(BLE_BATTERY_CHAR);
+ const val=await batChar.readValue();
+ info.battery=val.getUint8(0);
+ // Subscribe pra notificações em tempo real
+ try{
+ await batChar.startNotifications();
+ batChar.addEventListener('characteristicvaluechanged',(ev)=>{
+ const newVal=ev.target.value.getUint8(0);
+ const dev=state.btDevices?.find(d=>d.id===device.id);
+ if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()}
+ });
+ }catch(e){/* notification not supported */}
+ _bleConnections.get(device.id).batteryChar=batChar;
+ }catch(e){/* sem battery service */}
+ // Device Info (opcional)
+ try{
+ const svc=await server.getPrimaryService(BLE_DEVICE_INFO);
+ try{const c=await svc.getCharacteristic('manufacturer_name_string');info.manufacturer=new TextDecoder().decode(await c.readValue())}catch{}
+ try{const c=await svc.getCharacteristic('model_number_string');info.model=new TextDecoder().decode(await c.readValue())}catch{}
+ }catch(e){/* sem device info */}
+ }catch(e){console.warn('[ble] connect failed',device.name,e.message)}
+ return info;
+}
+
+async function reconnectBluetoothDevice(id){
+ // Web Bluetooth não persiste. Tenta getDevices() (suportado em alguns Chrome ≥85)
+ if(!bleSupported())return;
+ if(!navigator.bluetooth.getDevices){
+ toast('Reconexão automática não suportada — toque "Parear" pra escolher de novo');
+ return;
+ }
+ try{
+ const devices=await navigator.bluetooth.getDevices();
+ const dev=devices.find(d=>d.id===id);
+ if(!dev){toast('Device não encontrado nas permissões — repare manualmente');return}
+ toast('Reconectando '+(dev.name||'dispositivo')+'...');
+ await connectAndRead(dev);
+ renderBluetoothCard();
+ }catch(e){
+ console.warn('[ble] reconnect failed',e.message);
+ toast('Falha: '+e.message);
+ }
+}
+
+function removeBluetoothDevice(id){
+ if(!confirm('Remover dispositivo da lista?'))return;
+ const conn=_bleConnections.get(id);
+ if(conn?.device?.gatt?.connected)try{conn.device.gatt.disconnect()}catch{}
+ _bleConnections.delete(id);
+ state.btDevices=(state.btDevices||[]).filter(d=>d.id!==id);
+ saveState();
+ renderBluetoothCard();
+}
+
+function renderBluetoothCard(){
+ const el=document.getElementById('bt-list');
+ const supportEl=document.getElementById('bt-support');
+ if(!el)return;
+ if(supportEl){
+ supportEl.textContent=bleSupported()
+ ? 'Bluetooth disponível neste navegador.'
+ : 'Web Bluetooth indisponível (use Chrome no PC ou Android — iOS Safari não suporta).';
+ supportEl.style.color=bleSupported()?'var(--m-ok,#10b981)':'var(--m-warn,#f59e0b)';
+ }
+ const devices=state.btDevices||[];
+ if(devices.length===0){
+ el.innerHTML='
`;
+ }).join('');
+}
+
+// Re-render quando entra na aba Mais
+function refreshBluetoothCard(){renderBluetoothCard()}
+
+// ============ RAYMARINE / NMEA 2000 GATEWAY ============
+// Slot pra quando Karlão tiver gateway NMEA 2000 → WiFi (Yacht Devices YDWG-02, etc)
+
+function saveNmeaGatewayCfg(){
+ const ip=document.getElementById('nmea-gateway-ip')?.value.trim()||'';
+ const port=document.getElementById('nmea-gateway-port')?.value.trim()||'';
+ state.nmeaGateway={ip,port:port?parseInt(port):0,enabled:!!ip};
+ saveState();
+ toast(ip?'Gateway salvo · será usado em viagens futuras':'Gateway desativado');
+ renderNmeaGatewayCard();
+}
+
+function renderNmeaGatewayCard(){
+ const ipEl=document.getElementById('nmea-gateway-ip');
+ const portEl=document.getElementById('nmea-gateway-port');
+ if(!ipEl||!portEl)return;
+ ipEl.value=state.nmeaGateway?.ip||'';
+ portEl.value=state.nmeaGateway?.port||'';
+ const st=document.getElementById('nmea-gateway-status');
+ if(st){
+ if(state.nmeaGateway?.ip)st.innerHTML='Configurado · '+escapeHtml(state.nmeaGateway.ip)+':'+(state.nmeaGateway.port||'?')+'';
+ else st.innerHTML='Não configurado · sem leitura de instrumentos Raymarine';
+ }
+}
+
// ============ EXPORT GPX para OpenCPN ============
function gpxEscape(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index ca0d956..be104aa 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 12
- versionName "1.8.0"
+ versionCode 13
+ versionName "1.9.0"
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 1ce77e0..d371c02 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "shivao-mobile",
- "version": "1.8.0",
+ "version": "1.9.0",
"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 5f2a762..4f0401f 100644
--- a/server/public/index.html
+++ b/server/public/index.html
@@ -1970,6 +1970,31 @@ Marque zonas de proibição (alarme alto) ou atenção (aviso suave). Detecção
+
+
+
📡 Bluetooth · Baterias & Acessórios
+
Pareie BMS de bateria de lítio (com BLE), fones, smartwatch, smart shunts. Mostra nível de carga em tempo real no app.
+
Verificando suporte...
+
+
+
Limitações: iOS Safari não suporta Web Bluetooth (use no Chrome PC ou Android). Reconexão automática varia por device.
+
+
+
+
+
⚓ Instrumentos Raymarine (gateway NMEA 2000)
+
Pra ler dados de Raymarine (profundidade, vento, GPS, piloto automático), instale um gateway NMEA 2000→WiFi no barco e conecte ao bus SeaTalkNG. Recomendados: Yacht Devices YDWG-02, Actisense W2K-1.
+
+
+
+
+
+
+
+
+
Status: apenas slot de configuração. Parser NMEA 2000 PGNs (depth, wind, AIS, autopilot) será ativado em v1.10 quando você tiver o gateway físico instalado.
+
+
🗺️ Exportar para OpenCPN
@@ -3262,7 +3287,7 @@ function switchPanel(name){
if(p)p.classList.add('active');
// FAB visível em panels que têm "criar item"
document.getElementById('fab').style.display=['trips','maintenance','pending','zones','overview'].includes(name)?'flex':'none';
- if(name==='export'){updateStorageInfo();bindCloudInputs();renderCloudStatus();renderShareList();bindWeatherInputs();renderAuthBox();refreshGoogleStatus()}
+ if(name==='export'){updateStorageInfo();bindCloudInputs();renderCloudStatus();renderShareList();bindWeatherInputs();renderAuthBox();refreshGoogleStatus();renderBluetoothCard();renderNmeaGatewayCard()}
if(name==='pending'&&_gcalConnected&&Date.now()-_gcalLastPullAt>GCAL_PULL_INTERVAL_MS)googlePullNow();
if(name==='zones')renderZones();
window.scrollTo(0,0);
@@ -5717,6 +5742,189 @@ function testChartCfg(){
toast('Cartas salvas — abra um mapa pra ver');
}
+// ============ BLUETOOTH (Web Bluetooth API — Battery Service genérico) ============
+// Funciona em Chrome PC + Chrome Android. NÃO funciona em Safari (iOS).
+// Pra suporte iOS: precisa plugin nativo @capacitor/community/bluetooth-le (próxima versão).
+
+const BLE_BATTERY_SERVICE='battery_service'; // 0x180F
+const BLE_BATTERY_CHAR='battery_level'; // 0x2A19
+const BLE_DEVICE_INFO='device_information'; // 0x180A
+const BLE_OPTIONAL_SERVICES=['device_information'];
+
+const _bleConnections=new Map(); // id → {device, server, batteryChar}
+
+function bleSupported(){return !!navigator.bluetooth}
+
+async function pairBluetoothDevice(){
+ if(!bleSupported()){
+ toast('Web Bluetooth não suportado neste navegador. Use Chrome no PC ou Android.');
+ return;
+ }
+ try{
+ const device=await navigator.bluetooth.requestDevice({
+ // Filtro permissivo: aceita qualquer device com Battery Service OU qualquer device que o usuário escolher
+ acceptAllDevices:true,
+ optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
+ });
+ if(!device){return}
+ // Conecta + lê info inicial
+ const info=await connectAndRead(device);
+ // Salva no state
+ if(!state.btDevices)state.btDevices=[];
+ const existing=state.btDevices.find(d=>d.id===device.id);
+ if(existing){
+ Object.assign(existing,{name:device.name||existing.name,lastBattery:info.battery,lastSeen:Date.now(),manufacturer:info.manufacturer,model:info.model});
+ }else{
+ state.btDevices.push({
+ id:device.id,
+ name:device.name||'Dispositivo BLE',
+ lastBattery:info.battery,
+ lastSeen:Date.now(),
+ manufacturer:info.manufacturer,
+ model:info.model,
+ addedAt:Date.now(),
+ });
+ }
+ saveState();
+ renderBluetoothCard();
+ toast('✓ '+(device.name||'Dispositivo')+' pareado'+(info.battery!=null?' · '+info.battery+'%':''));
+ // Listener pra desconexão
+ device.addEventListener('gattserverdisconnected',()=>{
+ _bleConnections.delete(device.id);
+ renderBluetoothCard();
+ });
+ }catch(e){
+ if(e.name==='NotFoundError')return; // user cancelou
+ console.warn('[ble] pair failed',e);
+ toast('Erro: '+e.message);
+ }
+}
+
+async function connectAndRead(device){
+ const info={battery:null,manufacturer:null,model:null};
+ try{
+ const server=await device.gatt.connect();
+ _bleConnections.set(device.id,{device,server});
+ // Battery Service (opcional)
+ try{
+ const svc=await server.getPrimaryService(BLE_BATTERY_SERVICE);
+ const batChar=await svc.getCharacteristic(BLE_BATTERY_CHAR);
+ const val=await batChar.readValue();
+ info.battery=val.getUint8(0);
+ // Subscribe pra notificações em tempo real
+ try{
+ await batChar.startNotifications();
+ batChar.addEventListener('characteristicvaluechanged',(ev)=>{
+ const newVal=ev.target.value.getUint8(0);
+ const dev=state.btDevices?.find(d=>d.id===device.id);
+ if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()}
+ });
+ }catch(e){/* notification not supported */}
+ _bleConnections.get(device.id).batteryChar=batChar;
+ }catch(e){/* sem battery service */}
+ // Device Info (opcional)
+ try{
+ const svc=await server.getPrimaryService(BLE_DEVICE_INFO);
+ try{const c=await svc.getCharacteristic('manufacturer_name_string');info.manufacturer=new TextDecoder().decode(await c.readValue())}catch{}
+ try{const c=await svc.getCharacteristic('model_number_string');info.model=new TextDecoder().decode(await c.readValue())}catch{}
+ }catch(e){/* sem device info */}
+ }catch(e){console.warn('[ble] connect failed',device.name,e.message)}
+ return info;
+}
+
+async function reconnectBluetoothDevice(id){
+ // Web Bluetooth não persiste. Tenta getDevices() (suportado em alguns Chrome ≥85)
+ if(!bleSupported())return;
+ if(!navigator.bluetooth.getDevices){
+ toast('Reconexão automática não suportada — toque "Parear" pra escolher de novo');
+ return;
+ }
+ try{
+ const devices=await navigator.bluetooth.getDevices();
+ const dev=devices.find(d=>d.id===id);
+ if(!dev){toast('Device não encontrado nas permissões — repare manualmente');return}
+ toast('Reconectando '+(dev.name||'dispositivo')+'...');
+ await connectAndRead(dev);
+ renderBluetoothCard();
+ }catch(e){
+ console.warn('[ble] reconnect failed',e.message);
+ toast('Falha: '+e.message);
+ }
+}
+
+function removeBluetoothDevice(id){
+ if(!confirm('Remover dispositivo da lista?'))return;
+ const conn=_bleConnections.get(id);
+ if(conn?.device?.gatt?.connected)try{conn.device.gatt.disconnect()}catch{}
+ _bleConnections.delete(id);
+ state.btDevices=(state.btDevices||[]).filter(d=>d.id!==id);
+ saveState();
+ renderBluetoothCard();
+}
+
+function renderBluetoothCard(){
+ const el=document.getElementById('bt-list');
+ const supportEl=document.getElementById('bt-support');
+ if(!el)return;
+ if(supportEl){
+ supportEl.textContent=bleSupported()
+ ? 'Bluetooth disponível neste navegador.'
+ : 'Web Bluetooth indisponível (use Chrome no PC ou Android — iOS Safari não suporta).';
+ supportEl.style.color=bleSupported()?'var(--m-ok,#10b981)':'var(--m-warn,#f59e0b)';
+ }
+ const devices=state.btDevices||[];
+ if(devices.length===0){
+ el.innerHTML='