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)