fix(ble): plugin nativo @capacitor-community/bluetooth-le pra APK Android v1.9.1
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run

Bug v1.9.0: APK Android mostrava "Web Bluetooth não suportado" porque
Android System WebView desabilita Web Bluetooth API por padrão (segurança).

Fix: instala plugin @capacitor-community/bluetooth-le@^6.1.0 (compatível
com Capacitor 6) que expõe API nativa Android/iOS. JS detecta backend:
- Capacitor (APK): usa window.Capacitor.Plugins.BluetoothLe
- Browser web: usa navigator.bluetooth (Chrome PC continua funcionando)

Mudanças:
- mobile/package.json: nova dep @capacitor-community/bluetooth-le ^6.1.0
- AndroidManifest.xml: BLUETOOTH_SCAN (neverForLocation), BLUETOOTH_CONNECT,
  BLUETOOTH/BLUETOOTH_ADMIN (Android ≤30), uses-feature bluetooth_le
- bleBackend() detecta runtime, ensureBleNativeReady() inicializa plugin
- pairBluetoothDevice + connectAndRead + reconnect + remove abstraem backend
- UUIDs em formato 128-bit (compatível com ambos)
- parseDataView helper: plugin envia value como base64, web envia DataView

iOS: plugin suporta nativamente — quando build iOS for feito, funciona.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PontualTech / Karlão 2026-04-28 16:24:50 -03:00
parent a6a35c6d6f
commit 52ee668879
7 changed files with 369 additions and 156 deletions

View file

@ -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;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);
return new DataView(bytes.buffer);
}
if(v?.buffer)return new DataView(v.buffer);
return new DataView(new ArrayBuffer(0));
}
async function reconnectBluetoothDevice(id){
const dev=state.btDevices?.find(d=>d.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();

View file

@ -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.

View file

@ -51,4 +51,11 @@
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-feature android:name="android.hardware.sensor.barometer" android:required="false" />
<uses-feature android:name="android.hardware.sensor.compass" android:required="false" />
<!-- Bluetooth LE (BMS de bateria, fones, smart shunts) -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
</manifest>

View file

@ -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",

View file

@ -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",

View file

@ -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;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);
return new DataView(bytes.buffer);
}
if(v?.buffer)return new DataView(v.buffer);
return new DataView(new ArrayBuffer(0));
}
async function reconnectBluetoothDevice(id){
const dev=state.btDevices?.find(d=>d.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();

View file

@ -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)