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='
Nenhum dispositivo pareado ainda.
'; + return; + } + el.innerHTML=devices.map(d=>{ + const conn=_bleConnections.get(d.id); + const isConnected=conn?.device?.gatt?.connected; + const bat=d.lastBattery; + const batColor=bat==null?'var(--m-text-soft)':bat<20?'var(--m-danger,#ef4444)':bat<50?'var(--m-warn,#f59e0b)':'var(--m-ok,#10b981)'; + const batIcon=bat==null?'❓':bat<20?'🪫':bat<50?'🔋':'🔋'; + return `
+
${batIcon}
+
+
${escapeHtml(d.name)}
+
+ ${bat!=null?`${bat}% · `:''}${isConnected?'conectado':'offline'}${d.manufacturer?` · ${escapeHtml(d.manufacturer)}`:''}${d.model?` ${escapeHtml(d.model)}`:''} +
+
+ ${!isConnected?``:''} + +
`; + }).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='
Nenhum dispositivo pareado ainda.
'; + return; + } + el.innerHTML=devices.map(d=>{ + const conn=_bleConnections.get(d.id); + const isConnected=conn?.device?.gatt?.connected; + const bat=d.lastBattery; + const batColor=bat==null?'var(--m-text-soft)':bat<20?'var(--m-danger,#ef4444)':bat<50?'var(--m-warn,#f59e0b)':'var(--m-ok,#10b981)'; + const batIcon=bat==null?'❓':bat<20?'🪫':bat<50?'🔋':'🔋'; + return `
+
${batIcon}
+
+
${escapeHtml(d.name)}
+
+ ${bat!=null?`${bat}% · `:''}${isConnected?'conectado':'offline'}${d.manufacturer?` · ${escapeHtml(d.manufacturer)}`:''}${d.model?` ${escapeHtml(d.model)}`:''} +
+
+ ${!isConnected?``:''} + +
`; + }).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/server/src/index.js b/server/src/index.js index 2c9a4d3..eba4ec0 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.8.0/Shivao-v1.8.0.apk'; +const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.9.0/Shivao-v1.9.0.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)