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
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:
parent
a6a35c6d6f
commit
52ee668879
7 changed files with 369 additions and 156 deletions
|
|
@ -5742,120 +5742,213 @@ function testChartCfg(){
|
||||||
toast('Cartas salvas — abra um mapa pra ver');
|
toast('Cartas salvas — abra um mapa pra ver');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ BLUETOOTH (Web Bluetooth API — Battery Service genérico) ============
|
// ============ BLUETOOTH (Web Bluetooth + Capacitor native plugin) ============
|
||||||
// Funciona em Chrome PC + Chrome Android. NÃO funciona em Safari (iOS).
|
// 2 backends: navigator.bluetooth (Chrome PC/Android web) + Capacitor BluetoothLe (APK Android/iOS)
|
||||||
// Pra suporte iOS: precisa plugin nativo @capacitor/community/bluetooth-le (próxima versão).
|
// UUIDs em formato 128-bit (compatível com ambos backends)
|
||||||
|
|
||||||
const BLE_BATTERY_SERVICE='battery_service'; // 0x180F
|
const BLE_BATTERY_SERVICE='0000180f-0000-1000-8000-00805f9b34fb'; // 0x180F
|
||||||
const BLE_BATTERY_CHAR='battery_level'; // 0x2A19
|
const BLE_BATTERY_CHAR='00002a19-0000-1000-8000-00805f9b34fb'; // 0x2A19
|
||||||
const BLE_DEVICE_INFO='device_information'; // 0x180A
|
const BLE_DEVICE_INFO='0000180a-0000-1000-8000-00805f9b34fb'; // 0x180A
|
||||||
const BLE_OPTIONAL_SERVICES=['device_information'];
|
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}
|
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(){
|
async function pairBluetoothDevice(){
|
||||||
if(!bleSupported()){
|
const backend=bleBackend();
|
||||||
toast('Web Bluetooth não suportado neste navegador. Use Chrome no PC ou Android.');
|
if(!backend){
|
||||||
|
toast('Bluetooth indisponível neste dispositivo.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try{
|
try{
|
||||||
const device=await navigator.bluetooth.requestDevice({
|
let deviceId,deviceName;
|
||||||
// Filtro permissivo: aceita qualquer device com Battery Service OU qualquer device que o usuário escolher
|
if(backend==='capacitor'){
|
||||||
acceptAllDevices:true,
|
await ensureBleNativeReady();
|
||||||
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
|
const ble=window.Capacitor.Plugins.BluetoothLe;
|
||||||
});
|
const result=await ble.requestDevice({
|
||||||
if(!device){return}
|
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
|
// Conecta + lê info inicial
|
||||||
const info=await connectAndRead(device);
|
const info=await connectAndRead(deviceId,deviceName);
|
||||||
// Salva no state
|
// Salva no state
|
||||||
if(!state.btDevices)state.btDevices=[];
|
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){
|
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{
|
}else{
|
||||||
state.btDevices.push({
|
state.btDevices.push({
|
||||||
id:device.id,
|
id:deviceId,name:deviceName,
|
||||||
name:device.name||'Dispositivo BLE',
|
lastBattery:info.battery,lastSeen:Date.now(),
|
||||||
lastBattery:info.battery,
|
manufacturer:info.manufacturer,model:info.model,
|
||||||
lastSeen:Date.now(),
|
addedAt:Date.now(),backend,
|
||||||
manufacturer:info.manufacturer,
|
|
||||||
model:info.model,
|
|
||||||
addedAt:Date.now(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
saveState();
|
saveState();
|
||||||
renderBluetoothCard();
|
renderBluetoothCard();
|
||||||
toast('✓ '+(device.name||'Dispositivo')+' pareado'+(info.battery!=null?' · '+info.battery+'%':''));
|
toast('✓ '+deviceName+' pareado'+(info.battery!=null?' · '+info.battery+'%':''));
|
||||||
// Listener pra desconexão
|
|
||||||
device.addEventListener('gattserverdisconnected',()=>{
|
|
||||||
_bleConnections.delete(device.id);
|
|
||||||
renderBluetoothCard();
|
|
||||||
});
|
|
||||||
}catch(e){
|
}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);
|
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 info={battery:null,manufacturer:null,model:null};
|
||||||
|
const backend=bleBackend();
|
||||||
try{
|
try{
|
||||||
const server=await device.gatt.connect();
|
if(backend==='capacitor'){
|
||||||
_bleConnections.set(device.id,{device,server});
|
const ble=window.Capacitor.Plugins.BluetoothLe;
|
||||||
// Battery Service (opcional)
|
await ble.connect({deviceId,timeout:15000});
|
||||||
try{
|
const conn=_bleConnections.get(deviceId)||{};
|
||||||
const svc=await server.getPrimaryService(BLE_BATTERY_SERVICE);
|
conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true;
|
||||||
const batChar=await svc.getCharacteristic(BLE_BATTERY_CHAR);
|
_bleConnections.set(deviceId,conn);
|
||||||
const val=await batChar.readValue();
|
// Battery
|
||||||
info.battery=val.getUint8(0);
|
|
||||||
// Subscribe pra notificações em tempo real
|
|
||||||
try{
|
try{
|
||||||
await batChar.startNotifications();
|
const r=await ble.read({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR});
|
||||||
batChar.addEventListener('characteristicvaluechanged',(ev)=>{
|
info.battery=parseDataView(r.value).getUint8(0);
|
||||||
const newVal=ev.target.value.getUint8(0);
|
try{
|
||||||
const dev=state.btDevices?.find(d=>d.id===device.id);
|
await ble.startNotifications({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR});
|
||||||
if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()}
|
ble.addListener('notification|'+deviceId+'|'+BLE_BATTERY_SERVICE+'|'+BLE_BATTERY_CHAR,(ev)=>{
|
||||||
});
|
const newVal=parseDataView(ev.value).getUint8(0);
|
||||||
}catch(e){/* notification not supported */}
|
const dev=state.btDevices?.find(d=>d.id===deviceId);
|
||||||
_bleConnections.get(device.id).batteryChar=batChar;
|
if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()}
|
||||||
}catch(e){/* sem battery service */}
|
});
|
||||||
// Device Info (opcional)
|
}catch(e){}
|
||||||
try{
|
}catch(e){}
|
||||||
const svc=await server.getPrimaryService(BLE_DEVICE_INFO);
|
// Device info
|
||||||
try{const c=await svc.getCharacteristic('manufacturer_name_string');info.manufacturer=new TextDecoder().decode(await c.readValue())}catch{}
|
try{
|
||||||
try{const c=await svc.getCharacteristic('model_number_string');info.model=new TextDecoder().decode(await c.readValue())}catch{}
|
const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MANUFACTURER_CHAR});
|
||||||
}catch(e){/* sem device info */}
|
info.manufacturer=new TextDecoder().decode(parseDataView(r.value));
|
||||||
}catch(e){console.warn('[ble] connect failed',device.name,e.message)}
|
}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;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reconnectBluetoothDevice(id){
|
// Helper: o plugin Capacitor envia value como string base64 ou DataView; converte pra DataView
|
||||||
// Web Bluetooth não persiste. Tenta getDevices() (suportado em alguns Chrome ≥85)
|
function parseDataView(v){
|
||||||
if(!bleSupported())return;
|
if(v instanceof DataView)return v;
|
||||||
if(!navigator.bluetooth.getDevices){
|
if(typeof v==='string'){
|
||||||
toast('Reconexão automática não suportada — toque "Parear" pra escolher de novo');
|
// base64 → Uint8Array → DataView
|
||||||
return;
|
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{
|
try{
|
||||||
const devices=await navigator.bluetooth.getDevices();
|
if(backend==='capacitor'){
|
||||||
const dev=devices.find(d=>d.id===id);
|
await ensureBleNativeReady();
|
||||||
if(!dev){toast('Device não encontrado nas permissões — repare manualmente');return}
|
await connectAndRead(id,dev.name);
|
||||||
toast('Reconectando '+(dev.name||'dispositivo')+'...');
|
renderBluetoothCard();
|
||||||
await connectAndRead(dev);
|
}else{
|
||||||
renderBluetoothCard();
|
// 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){
|
}catch(e){
|
||||||
console.warn('[ble] reconnect failed',e.message);
|
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;
|
if(!confirm('Remover dispositivo da lista?'))return;
|
||||||
const conn=_bleConnections.get(id);
|
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);
|
_bleConnections.delete(id);
|
||||||
state.btDevices=(state.btDevices||[]).filter(d=>d.id!==id);
|
state.btDevices=(state.btDevices||[]).filter(d=>d.id!==id);
|
||||||
saveState();
|
saveState();
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ android {
|
||||||
applicationId "br.com.pontualtech.shivao"
|
applicationId "br.com.pontualtech.shivao"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 13
|
versionCode 14
|
||||||
versionName "1.9.0"
|
versionName "1.9.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,11 @@
|
||||||
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
<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.barometer" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.sensor.compass" 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>
|
</manifest>
|
||||||
|
|
|
||||||
23
mobile/package-lock.json
generated
23
mobile/package-lock.json
generated
|
|
@ -1,13 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "shivao-mobile",
|
"name": "shivao-mobile",
|
||||||
"version": "1.2.0",
|
"version": "1.9.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "shivao-mobile",
|
"name": "shivao-mobile",
|
||||||
"version": "1.2.0",
|
"version": "1.9.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor-community/bluetooth-le": "^6.1.0",
|
||||||
"@capacitor/android": "^6.1.2",
|
"@capacitor/android": "^6.1.2",
|
||||||
"@capacitor/app": "^6.0.1",
|
"@capacitor/app": "^6.0.1",
|
||||||
"@capacitor/core": "^6.1.2",
|
"@capacitor/core": "^6.1.2",
|
||||||
|
|
@ -21,6 +22,18 @@
|
||||||
"@capacitor/cli": "^6.1.2"
|
"@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": {
|
"node_modules/@capacitor/android": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-6.2.1.tgz",
|
||||||
|
|
@ -343,6 +356,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.9.10",
|
"version": "0.9.10",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "shivao-mobile",
|
"name": "shivao-mobile",
|
||||||
"version": "1.9.0",
|
"version": "1.9.1",
|
||||||
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
|
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"ios:open": "npx cap open ios"
|
"ios:open": "npx cap open ios"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor-community/bluetooth-le": "^6.1.0",
|
||||||
"@capacitor/android": "^6.1.2",
|
"@capacitor/android": "^6.1.2",
|
||||||
"@capacitor/app": "^6.0.1",
|
"@capacitor/app": "^6.0.1",
|
||||||
"@capacitor/core": "^6.1.2",
|
"@capacitor/core": "^6.1.2",
|
||||||
|
|
|
||||||
|
|
@ -5742,120 +5742,213 @@ function testChartCfg(){
|
||||||
toast('Cartas salvas — abra um mapa pra ver');
|
toast('Cartas salvas — abra um mapa pra ver');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ BLUETOOTH (Web Bluetooth API — Battery Service genérico) ============
|
// ============ BLUETOOTH (Web Bluetooth + Capacitor native plugin) ============
|
||||||
// Funciona em Chrome PC + Chrome Android. NÃO funciona em Safari (iOS).
|
// 2 backends: navigator.bluetooth (Chrome PC/Android web) + Capacitor BluetoothLe (APK Android/iOS)
|
||||||
// Pra suporte iOS: precisa plugin nativo @capacitor/community/bluetooth-le (próxima versão).
|
// UUIDs em formato 128-bit (compatível com ambos backends)
|
||||||
|
|
||||||
const BLE_BATTERY_SERVICE='battery_service'; // 0x180F
|
const BLE_BATTERY_SERVICE='0000180f-0000-1000-8000-00805f9b34fb'; // 0x180F
|
||||||
const BLE_BATTERY_CHAR='battery_level'; // 0x2A19
|
const BLE_BATTERY_CHAR='00002a19-0000-1000-8000-00805f9b34fb'; // 0x2A19
|
||||||
const BLE_DEVICE_INFO='device_information'; // 0x180A
|
const BLE_DEVICE_INFO='0000180a-0000-1000-8000-00805f9b34fb'; // 0x180A
|
||||||
const BLE_OPTIONAL_SERVICES=['device_information'];
|
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}
|
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(){
|
async function pairBluetoothDevice(){
|
||||||
if(!bleSupported()){
|
const backend=bleBackend();
|
||||||
toast('Web Bluetooth não suportado neste navegador. Use Chrome no PC ou Android.');
|
if(!backend){
|
||||||
|
toast('Bluetooth indisponível neste dispositivo.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try{
|
try{
|
||||||
const device=await navigator.bluetooth.requestDevice({
|
let deviceId,deviceName;
|
||||||
// Filtro permissivo: aceita qualquer device com Battery Service OU qualquer device que o usuário escolher
|
if(backend==='capacitor'){
|
||||||
acceptAllDevices:true,
|
await ensureBleNativeReady();
|
||||||
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
|
const ble=window.Capacitor.Plugins.BluetoothLe;
|
||||||
});
|
const result=await ble.requestDevice({
|
||||||
if(!device){return}
|
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
|
// Conecta + lê info inicial
|
||||||
const info=await connectAndRead(device);
|
const info=await connectAndRead(deviceId,deviceName);
|
||||||
// Salva no state
|
// Salva no state
|
||||||
if(!state.btDevices)state.btDevices=[];
|
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){
|
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{
|
}else{
|
||||||
state.btDevices.push({
|
state.btDevices.push({
|
||||||
id:device.id,
|
id:deviceId,name:deviceName,
|
||||||
name:device.name||'Dispositivo BLE',
|
lastBattery:info.battery,lastSeen:Date.now(),
|
||||||
lastBattery:info.battery,
|
manufacturer:info.manufacturer,model:info.model,
|
||||||
lastSeen:Date.now(),
|
addedAt:Date.now(),backend,
|
||||||
manufacturer:info.manufacturer,
|
|
||||||
model:info.model,
|
|
||||||
addedAt:Date.now(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
saveState();
|
saveState();
|
||||||
renderBluetoothCard();
|
renderBluetoothCard();
|
||||||
toast('✓ '+(device.name||'Dispositivo')+' pareado'+(info.battery!=null?' · '+info.battery+'%':''));
|
toast('✓ '+deviceName+' pareado'+(info.battery!=null?' · '+info.battery+'%':''));
|
||||||
// Listener pra desconexão
|
|
||||||
device.addEventListener('gattserverdisconnected',()=>{
|
|
||||||
_bleConnections.delete(device.id);
|
|
||||||
renderBluetoothCard();
|
|
||||||
});
|
|
||||||
}catch(e){
|
}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);
|
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 info={battery:null,manufacturer:null,model:null};
|
||||||
|
const backend=bleBackend();
|
||||||
try{
|
try{
|
||||||
const server=await device.gatt.connect();
|
if(backend==='capacitor'){
|
||||||
_bleConnections.set(device.id,{device,server});
|
const ble=window.Capacitor.Plugins.BluetoothLe;
|
||||||
// Battery Service (opcional)
|
await ble.connect({deviceId,timeout:15000});
|
||||||
try{
|
const conn=_bleConnections.get(deviceId)||{};
|
||||||
const svc=await server.getPrimaryService(BLE_BATTERY_SERVICE);
|
conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true;
|
||||||
const batChar=await svc.getCharacteristic(BLE_BATTERY_CHAR);
|
_bleConnections.set(deviceId,conn);
|
||||||
const val=await batChar.readValue();
|
// Battery
|
||||||
info.battery=val.getUint8(0);
|
|
||||||
// Subscribe pra notificações em tempo real
|
|
||||||
try{
|
try{
|
||||||
await batChar.startNotifications();
|
const r=await ble.read({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR});
|
||||||
batChar.addEventListener('characteristicvaluechanged',(ev)=>{
|
info.battery=parseDataView(r.value).getUint8(0);
|
||||||
const newVal=ev.target.value.getUint8(0);
|
try{
|
||||||
const dev=state.btDevices?.find(d=>d.id===device.id);
|
await ble.startNotifications({deviceId,service:BLE_BATTERY_SERVICE,characteristic:BLE_BATTERY_CHAR});
|
||||||
if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()}
|
ble.addListener('notification|'+deviceId+'|'+BLE_BATTERY_SERVICE+'|'+BLE_BATTERY_CHAR,(ev)=>{
|
||||||
});
|
const newVal=parseDataView(ev.value).getUint8(0);
|
||||||
}catch(e){/* notification not supported */}
|
const dev=state.btDevices?.find(d=>d.id===deviceId);
|
||||||
_bleConnections.get(device.id).batteryChar=batChar;
|
if(dev){dev.lastBattery=newVal;dev.lastSeen=Date.now();saveState();renderBluetoothCard()}
|
||||||
}catch(e){/* sem battery service */}
|
});
|
||||||
// Device Info (opcional)
|
}catch(e){}
|
||||||
try{
|
}catch(e){}
|
||||||
const svc=await server.getPrimaryService(BLE_DEVICE_INFO);
|
// Device info
|
||||||
try{const c=await svc.getCharacteristic('manufacturer_name_string');info.manufacturer=new TextDecoder().decode(await c.readValue())}catch{}
|
try{
|
||||||
try{const c=await svc.getCharacteristic('model_number_string');info.model=new TextDecoder().decode(await c.readValue())}catch{}
|
const r=await ble.read({deviceId,service:BLE_DEVICE_INFO,characteristic:BLE_MANUFACTURER_CHAR});
|
||||||
}catch(e){/* sem device info */}
|
info.manufacturer=new TextDecoder().decode(parseDataView(r.value));
|
||||||
}catch(e){console.warn('[ble] connect failed',device.name,e.message)}
|
}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;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reconnectBluetoothDevice(id){
|
// Helper: o plugin Capacitor envia value como string base64 ou DataView; converte pra DataView
|
||||||
// Web Bluetooth não persiste. Tenta getDevices() (suportado em alguns Chrome ≥85)
|
function parseDataView(v){
|
||||||
if(!bleSupported())return;
|
if(v instanceof DataView)return v;
|
||||||
if(!navigator.bluetooth.getDevices){
|
if(typeof v==='string'){
|
||||||
toast('Reconexão automática não suportada — toque "Parear" pra escolher de novo');
|
// base64 → Uint8Array → DataView
|
||||||
return;
|
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{
|
try{
|
||||||
const devices=await navigator.bluetooth.getDevices();
|
if(backend==='capacitor'){
|
||||||
const dev=devices.find(d=>d.id===id);
|
await ensureBleNativeReady();
|
||||||
if(!dev){toast('Device não encontrado nas permissões — repare manualmente');return}
|
await connectAndRead(id,dev.name);
|
||||||
toast('Reconectando '+(dev.name||'dispositivo')+'...');
|
renderBluetoothCard();
|
||||||
await connectAndRead(dev);
|
}else{
|
||||||
renderBluetoothCard();
|
// 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){
|
}catch(e){
|
||||||
console.warn('[ble] reconnect failed',e.message);
|
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;
|
if(!confirm('Remover dispositivo da lista?'))return;
|
||||||
const conn=_bleConnections.get(id);
|
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);
|
_bleConnections.delete(id);
|
||||||
state.btDevices=(state.btDevices||[]).filter(d=>d.id!==id);
|
state.btDevices=(state.btDevices||[]).filter(d=>d.id!==id);
|
||||||
saveState();
|
saveState();
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Atalho: /apk redireciona pra última APK release no Forgejo
|
// 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));
|
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)
|
// Página A4 imprimível com QR Code + instruções (cola no barco/marina)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue