diff --git a/app/diario-bordo.html b/app/diario-bordo.html index 4f0401f..d6e7c29 100644 --- a/app/diario-bordo.html +++ b/app/diario-bordo.html @@ -5742,120 +5742,213 @@ 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). +// ============ BLUETOOTH (Web Bluetooth + Capacitor native plugin) ============ +// 2 backends: navigator.bluetooth (Chrome PC/Android web) + Capacitor BluetoothLe (APK Android/iOS) +// UUIDs em formato 128-bit (compatível com ambos backends) -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 BLE_BATTERY_SERVICE='0000180f-0000-1000-8000-00805f9b34fb'; // 0x180F +const BLE_BATTERY_CHAR='00002a19-0000-1000-8000-00805f9b34fb'; // 0x2A19 +const BLE_DEVICE_INFO='0000180a-0000-1000-8000-00805f9b34fb'; // 0x180A +const BLE_MANUFACTURER_CHAR='00002a29-0000-1000-8000-00805f9b34fb'; +const BLE_MODEL_CHAR='00002a24-0000-1000-8000-00805f9b34fb'; const _bleConnections=new Map(); // id → {device, server, batteryChar} -function bleSupported(){return !!navigator.bluetooth} +// Detecta backend: Capacitor nativo se disponível, senão Web Bluetooth +function bleBackend(){ + if(window.Capacitor?.Plugins?.BluetoothLe)return 'capacitor'; + if(navigator.bluetooth)return 'web'; + return null; +} +function bleSupported(){return bleBackend()!==null} + +let _bleNativeInitialized=false; +async function ensureBleNativeReady(){ + if(_bleNativeInitialized)return; + const ble=window.Capacitor?.Plugins?.BluetoothLe; + if(!ble)return; + await ble.initialize({androidNeverForLocation:true}); + _bleNativeInitialized=true; +} async function pairBluetoothDevice(){ - if(!bleSupported()){ - toast('Web Bluetooth não suportado neste navegador. Use Chrome no PC ou Android.'); + const backend=bleBackend(); + if(!backend){ + toast('Bluetooth indisponível neste dispositivo.'); 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} + let deviceId,deviceName; + if(backend==='capacitor'){ + await ensureBleNativeReady(); + const ble=window.Capacitor.Plugins.BluetoothLe; + const result=await ble.requestDevice({ + services:[], + optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO], + allowDuplicates:false, + }); + if(!result?.deviceId){return} + deviceId=result.deviceId; + deviceName=result.name||'Dispositivo BLE'; + }else{ + const device=await navigator.bluetooth.requestDevice({ + acceptAllDevices:true, + optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO], + }); + if(!device){return} + deviceId=device.id; + deviceName=device.name||'Dispositivo BLE'; + _bleConnections.set(deviceId,{device,backend:'web'}); + device.addEventListener('gattserverdisconnected',()=>{ + const c=_bleConnections.get(deviceId);if(c)c.connected=false; + renderBluetoothCard(); + }); + } // Conecta + lê info inicial - const info=await connectAndRead(device); + const info=await connectAndRead(deviceId,deviceName); // Salva no state if(!state.btDevices)state.btDevices=[]; - const existing=state.btDevices.find(d=>d.id===device.id); + const existing=state.btDevices.find(d=>d.id===deviceId); if(existing){ - Object.assign(existing,{name:device.name||existing.name,lastBattery:info.battery,lastSeen:Date.now(),manufacturer:info.manufacturer,model:info.model}); + Object.assign(existing,{name:deviceName||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(), + id:deviceId,name:deviceName, + lastBattery:info.battery,lastSeen:Date.now(), + manufacturer:info.manufacturer,model:info.model, + addedAt:Date.now(),backend, }); } saveState(); renderBluetoothCard(); - toast('✓ '+(device.name||'Dispositivo')+' pareado'+(info.battery!=null?' · '+info.battery+'%':'')); - // Listener pra desconexão - device.addEventListener('gattserverdisconnected',()=>{ - _bleConnections.delete(device.id); - renderBluetoothCard(); - }); + toast('✓ '+deviceName+' pareado'+(info.battery!=null?' · '+info.battery+'%':'')); }catch(e){ - if(e.name==='NotFoundError')return; // user cancelou + if(e.name==='NotFoundError'||/cancel/i.test(e.message||''))return; console.warn('[ble] pair failed',e); - toast('Erro: '+e.message); + toast('Erro: '+(e.message||e.errorMessage||'pareamento falhou')); } } -async function connectAndRead(device){ +async function connectAndRead(deviceId,deviceName){ const info={battery:null,manufacturer:null,model:null}; + const backend=bleBackend(); 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 + if(backend==='capacitor'){ + const ble=window.Capacitor.Plugins.BluetoothLe; + await ble.connect({deviceId,timeout:15000}); + const conn=_bleConnections.get(deviceId)||{}; + conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true; + _bleConnections.set(deviceId,conn); + // Battery 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)} + const r=await ble.read({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR}); + info.battery=parseDataView(r.value).getUint8(0); + try{ + await ble.startNotifications({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR}); + ble.addListener('notification|'+deviceId+'|'+BLE_BATTERY_SERVICE+'|'+BLE_BATTERY_CHAR,(ev)=>{ + const newVal=parseDataView(ev.value).getUint8(0); + const dev=state.btDevices?.find(d=>d.id===deviceId); + if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()} + }); + }catch(e){} + }catch(e){} + // Device info + try{ + const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MANUFACTURER_CHAR}); + info.manufacturer=new TextDecoder().decode(parseDataView(r.value)); + }catch(e){} + try{ + const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MODEL_CHAR}); + info.model=new TextDecoder().decode(parseDataView(r.value)); + }catch(e){} + }else{ + const conn=_bleConnections.get(deviceId); + const device=conn?.device; + if(!device)return info; + const server=await device.gatt.connect(); + conn.server=server;conn.connected=true; + 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); + try{ + await batChar.startNotifications(); + batChar.addEventListener('characteristicvaluechanged',(ev)=>{ + const newVal=ev.target.value.getUint8(0); + const dev=state.btDevices?.find(d=>d.id===deviceId); + if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()} + }); + }catch{} + conn.batteryChar=batChar; + }catch{} + try{ + const svc=await server.getPrimaryService(BLE_DEVICE_INFO); + try{const c=await svc.getCharacteristic(BLE_MANUFACTURER_CHAR);info.manufacturer=new TextDecoder().decode(await c.readValue())}catch{} + try{const c=await svc.getCharacteristic(BLE_MODEL_CHAR);info.model=new TextDecoder().decode(await c.readValue())}catch{} + }catch{} + } + }catch(e){console.warn('[ble] connect failed',deviceName,e.message||e.errorMessage)} 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; +// Helper: o plugin Capacitor envia value como string base64 ou DataView; converte pra DataView +function parseDataView(v){ + if(v instanceof DataView)return v; + if(typeof v==='string'){ + // base64 → Uint8Array → DataView + const bin=atob(v); + const bytes=new Uint8Array(bin.length); + for(let i=0;id.id===id); + if(!dev){toast('Dispositivo não encontrado');return} + const backend=bleBackend(); + if(!backend){toast('Bluetooth indisponível');return} + toast('Reconectando '+dev.name+'...'); 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(); + if(backend==='capacitor'){ + await ensureBleNativeReady(); + await connectAndRead(id,dev.name); + renderBluetoothCard(); + }else{ + // Web Bluetooth: tenta getDevices() (Chrome ≥85) + if(!navigator.bluetooth.getDevices){ + toast('Reconexão automática indisponível — pareie de novo');return; + } + const devices=await navigator.bluetooth.getDevices(); + const device=devices.find(d=>d.id===id); + if(!device){toast('Sem permissão p/ esse device — pareie manualmente');return} + _bleConnections.set(id,{device,backend:'web'}); + device.addEventListener('gattserverdisconnected',()=>{ + const c=_bleConnections.get(id);if(c)c.connected=false;renderBluetoothCard(); + }); + await connectAndRead(id,device.name); + renderBluetoothCard(); + } }catch(e){ console.warn('[ble] reconnect failed',e.message); - toast('Falha: '+e.message); + toast('Falha: '+(e.message||e.errorMessage)); } } -function removeBluetoothDevice(id){ +async 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{} + try{ + if(conn?.backend==='capacitor'){ + const ble=window.Capacitor?.Plugins?.BluetoothLe; + if(ble)await ble.disconnect({deviceId:id}).catch(()=>{}); + }else if(conn?.device?.gatt?.connected){ + conn.device.gatt.disconnect(); + } + }catch{} _bleConnections.delete(id); state.btDevices=(state.btDevices||[]).filter(d=>d.id!==id); saveState(); diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index be104aa..de77655 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 13 - versionName "1.9.0" + versionCode 14 + versionName "1.9.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/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 7814995..e035cc0 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -51,4 +51,11 @@ + + + + + + + diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 6baf0bf..529f312 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -1,13 +1,14 @@ { "name": "shivao-mobile", - "version": "1.2.0", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shivao-mobile", - "version": "1.2.0", + "version": "1.9.0", "dependencies": { + "@capacitor-community/bluetooth-le": "^6.1.0", "@capacitor/android": "^6.1.2", "@capacitor/app": "^6.0.1", "@capacitor/core": "^6.1.2", @@ -21,6 +22,18 @@ "@capacitor/cli": "^6.1.2" } }, + "node_modules/@capacitor-community/bluetooth-le": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-6.1.0.tgz", + "integrity": "sha512-hnNChEwV+xNOVqDYI4bfkQtFtvEyzBMlgYs+6xsLYTJVl0v8h6Hn3nCwjW9l6LH0tMzYaRYlFLCiGHKPHt1N0Q==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20" + }, + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/android": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-6.2.1.tgz", @@ -343,6 +356,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, "node_modules/@xmldom/xmldom": { "version": "0.9.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", diff --git a/mobile/package.json b/mobile/package.json index d371c02..450c1ff 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,6 +1,6 @@ { "name": "shivao-mobile", - "version": "1.9.0", + "version": "1.9.1", "description": "Shivao app nativo (Capacitor wrapper Android/iOS)", "main": "index.js", "type": "module", @@ -12,6 +12,7 @@ "ios:open": "npx cap open ios" }, "dependencies": { + "@capacitor-community/bluetooth-le": "^6.1.0", "@capacitor/android": "^6.1.2", "@capacitor/app": "^6.0.1", "@capacitor/core": "^6.1.2", diff --git a/server/public/index.html b/server/public/index.html index 4f0401f..d6e7c29 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -5742,120 +5742,213 @@ 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). +// ============ BLUETOOTH (Web Bluetooth + Capacitor native plugin) ============ +// 2 backends: navigator.bluetooth (Chrome PC/Android web) + Capacitor BluetoothLe (APK Android/iOS) +// UUIDs em formato 128-bit (compatível com ambos backends) -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 BLE_BATTERY_SERVICE='0000180f-0000-1000-8000-00805f9b34fb'; // 0x180F +const BLE_BATTERY_CHAR='00002a19-0000-1000-8000-00805f9b34fb'; // 0x2A19 +const BLE_DEVICE_INFO='0000180a-0000-1000-8000-00805f9b34fb'; // 0x180A +const BLE_MANUFACTURER_CHAR='00002a29-0000-1000-8000-00805f9b34fb'; +const BLE_MODEL_CHAR='00002a24-0000-1000-8000-00805f9b34fb'; const _bleConnections=new Map(); // id → {device, server, batteryChar} -function bleSupported(){return !!navigator.bluetooth} +// Detecta backend: Capacitor nativo se disponível, senão Web Bluetooth +function bleBackend(){ + if(window.Capacitor?.Plugins?.BluetoothLe)return 'capacitor'; + if(navigator.bluetooth)return 'web'; + return null; +} +function bleSupported(){return bleBackend()!==null} + +let _bleNativeInitialized=false; +async function ensureBleNativeReady(){ + if(_bleNativeInitialized)return; + const ble=window.Capacitor?.Plugins?.BluetoothLe; + if(!ble)return; + await ble.initialize({androidNeverForLocation:true}); + _bleNativeInitialized=true; +} async function pairBluetoothDevice(){ - if(!bleSupported()){ - toast('Web Bluetooth não suportado neste navegador. Use Chrome no PC ou Android.'); + const backend=bleBackend(); + if(!backend){ + toast('Bluetooth indisponível neste dispositivo.'); 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} + let deviceId,deviceName; + if(backend==='capacitor'){ + await ensureBleNativeReady(); + const ble=window.Capacitor.Plugins.BluetoothLe; + const result=await ble.requestDevice({ + services:[], + optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO], + allowDuplicates:false, + }); + if(!result?.deviceId){return} + deviceId=result.deviceId; + deviceName=result.name||'Dispositivo BLE'; + }else{ + const device=await navigator.bluetooth.requestDevice({ + acceptAllDevices:true, + optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO], + }); + if(!device){return} + deviceId=device.id; + deviceName=device.name||'Dispositivo BLE'; + _bleConnections.set(deviceId,{device,backend:'web'}); + device.addEventListener('gattserverdisconnected',()=>{ + const c=_bleConnections.get(deviceId);if(c)c.connected=false; + renderBluetoothCard(); + }); + } // Conecta + lê info inicial - const info=await connectAndRead(device); + const info=await connectAndRead(deviceId,deviceName); // Salva no state if(!state.btDevices)state.btDevices=[]; - const existing=state.btDevices.find(d=>d.id===device.id); + const existing=state.btDevices.find(d=>d.id===deviceId); if(existing){ - Object.assign(existing,{name:device.name||existing.name,lastBattery:info.battery,lastSeen:Date.now(),manufacturer:info.manufacturer,model:info.model}); + Object.assign(existing,{name:deviceName||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(), + id:deviceId,name:deviceName, + lastBattery:info.battery,lastSeen:Date.now(), + manufacturer:info.manufacturer,model:info.model, + addedAt:Date.now(),backend, }); } saveState(); renderBluetoothCard(); - toast('✓ '+(device.name||'Dispositivo')+' pareado'+(info.battery!=null?' · '+info.battery+'%':'')); - // Listener pra desconexão - device.addEventListener('gattserverdisconnected',()=>{ - _bleConnections.delete(device.id); - renderBluetoothCard(); - }); + toast('✓ '+deviceName+' pareado'+(info.battery!=null?' · '+info.battery+'%':'')); }catch(e){ - if(e.name==='NotFoundError')return; // user cancelou + if(e.name==='NotFoundError'||/cancel/i.test(e.message||''))return; console.warn('[ble] pair failed',e); - toast('Erro: '+e.message); + toast('Erro: '+(e.message||e.errorMessage||'pareamento falhou')); } } -async function connectAndRead(device){ +async function connectAndRead(deviceId,deviceName){ const info={battery:null,manufacturer:null,model:null}; + const backend=bleBackend(); 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 + if(backend==='capacitor'){ + const ble=window.Capacitor.Plugins.BluetoothLe; + await ble.connect({deviceId,timeout:15000}); + const conn=_bleConnections.get(deviceId)||{}; + conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true; + _bleConnections.set(deviceId,conn); + // Battery 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)} + const r=await ble.read({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR}); + info.battery=parseDataView(r.value).getUint8(0); + try{ + await ble.startNotifications({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR}); + ble.addListener('notification|'+deviceId+'|'+BLE_BATTERY_SERVICE+'|'+BLE_BATTERY_CHAR,(ev)=>{ + const newVal=parseDataView(ev.value).getUint8(0); + const dev=state.btDevices?.find(d=>d.id===deviceId); + if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()} + }); + }catch(e){} + }catch(e){} + // Device info + try{ + const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MANUFACTURER_CHAR}); + info.manufacturer=new TextDecoder().decode(parseDataView(r.value)); + }catch(e){} + try{ + const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MODEL_CHAR}); + info.model=new TextDecoder().decode(parseDataView(r.value)); + }catch(e){} + }else{ + const conn=_bleConnections.get(deviceId); + const device=conn?.device; + if(!device)return info; + const server=await device.gatt.connect(); + conn.server=server;conn.connected=true; + 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); + try{ + await batChar.startNotifications(); + batChar.addEventListener('characteristicvaluechanged',(ev)=>{ + const newVal=ev.target.value.getUint8(0); + const dev=state.btDevices?.find(d=>d.id===deviceId); + if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()} + }); + }catch{} + conn.batteryChar=batChar; + }catch{} + try{ + const svc=await server.getPrimaryService(BLE_DEVICE_INFO); + try{const c=await svc.getCharacteristic(BLE_MANUFACTURER_CHAR);info.manufacturer=new TextDecoder().decode(await c.readValue())}catch{} + try{const c=await svc.getCharacteristic(BLE_MODEL_CHAR);info.model=new TextDecoder().decode(await c.readValue())}catch{} + }catch{} + } + }catch(e){console.warn('[ble] connect failed',deviceName,e.message||e.errorMessage)} 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; +// Helper: o plugin Capacitor envia value como string base64 ou DataView; converte pra DataView +function parseDataView(v){ + if(v instanceof DataView)return v; + if(typeof v==='string'){ + // base64 → Uint8Array → DataView + const bin=atob(v); + const bytes=new Uint8Array(bin.length); + for(let i=0;id.id===id); + if(!dev){toast('Dispositivo não encontrado');return} + const backend=bleBackend(); + if(!backend){toast('Bluetooth indisponível');return} + toast('Reconectando '+dev.name+'...'); 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(); + if(backend==='capacitor'){ + await ensureBleNativeReady(); + await connectAndRead(id,dev.name); + renderBluetoothCard(); + }else{ + // Web Bluetooth: tenta getDevices() (Chrome ≥85) + if(!navigator.bluetooth.getDevices){ + toast('Reconexão automática indisponível — pareie de novo');return; + } + const devices=await navigator.bluetooth.getDevices(); + const device=devices.find(d=>d.id===id); + if(!device){toast('Sem permissão p/ esse device — pareie manualmente');return} + _bleConnections.set(id,{device,backend:'web'}); + device.addEventListener('gattserverdisconnected',()=>{ + const c=_bleConnections.get(id);if(c)c.connected=false;renderBluetoothCard(); + }); + await connectAndRead(id,device.name); + renderBluetoothCard(); + } }catch(e){ console.warn('[ble] reconnect failed',e.message); - toast('Falha: '+e.message); + toast('Falha: '+(e.message||e.errorMessage)); } } -function removeBluetoothDevice(id){ +async 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{} + try{ + if(conn?.backend==='capacitor'){ + const ble=window.Capacitor?.Plugins?.BluetoothLe; + if(ble)await ble.disconnect({deviceId:id}).catch(()=>{}); + }else if(conn?.device?.gatt?.connected){ + conn.device.gatt.disconnect(); + } + }catch{} _bleConnections.delete(id); state.btDevices=(state.btDevices||[]).filter(d=>d.id!==id); saveState(); diff --git a/server/src/index.js b/server/src/index.js index eba4ec0..03071ff 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.9.0/Shivao-v1.9.0.apk'; +const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.9.1/Shivao-v1.9.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)