Limitações MVP: não lê múltiplos usuários, não reseta unread counter, não sincroniza hora.
*/
public class OmronBPService {
private static final String TAG = "OmronBPService";
// UUIDs proprietários (mesmos do script python)
private static final UUID PARENT_SERVICE = UUID.fromString("ecbe3980-c9a2-11e1-b1bd-0002a5d5c51b");
private static final UUID[] RX_CHANNEL_UUIDS = new UUID[]{
UUID.fromString("49123040-aee8-11e1-a74d-0002a5d5c51b"),
UUID.fromString("4d0bf320-aee8-11e1-a0d9-0002a5d5c51b"),
UUID.fromString("5128ce60-aee8-11e1-b84b-0002a5d5c51b"),
UUID.fromString("560f1420-aee8-11e1-8184-0002a5d5c51b")
};
private static final UUID[] TX_CHANNEL_UUIDS = new UUID[]{
UUID.fromString("db5b55e0-aee7-11e1-965e-0002a5d5c51b"),
UUID.fromString("e0b8a060-aee7-11e1-92f4-0002a5d5c51b"),
UUID.fromString("0ae12b00-aee8-11e1-a192-0002a5d5c51b"),
UUID.fromString("10e1ba60-aee8-11e1-89e5-0002a5d5c51b")
};
// Característica de unlock / pairing (para liberar canal de transmissão)
private static final UUID UNLOCK_UUID = UUID.fromString("b305b680-aee7-11e1-a730-0002a5d5c51b");
// Device Information Service + modelo
private static final UUID DEVICE_INFO_SERVICE = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb");
private static final UUID MODEL_NUMBER_CHAR = UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb");
// Chave de pareamento usada no script python (16 bytes em hex)
private static final String PAIRING_KEY_HEX = "deadbeaf12341234deadbeaf12341234";
// Parâmetros do device (hem-6232t.py)
private static final int USER1_START_ADDRESS = 0x2e8; // início registros user1
private static final int RECORD_SIZE = 0x0e; // 14 bytes
private static final int USER1_RECORD_COUNT = 100; // 100 registros
// Endereços de settings (do driver python hem-6232t)
private static final int SETTINGS_READ_ADDRESS = 0x0260;
private static final int TIME_SYNC_OFFSET = 0x14; // início bloco horário relativo a SETTINGS_READ_ADDRESS
private static final int TIME_SYNC_LENGTH = 0x0A; // 2 header + 6 time + 2 crc
private final Context context;
private final BluetoothAdapter adapter;
private BluetoothGatt gatt;
private Callback callback;
// Buffer de recepção multi-canal
private final byte[][] rxChannelBuffers = new byte[4][];
private boolean waitingResponse = false;
private byte[] lastPacketType; // 2 bytes
private byte[] lastEepromAddr; // 2 bytes
private byte[] lastData; // payload
private final Handler handler = new Handler();
private static final UUID CLIENT_CHAR_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private int pendingCccdWrites = 0;
private Runnable afterNotificationsEnabled;
// Estado para fluxo de unlock
private boolean waitingUnlock = false;
private byte[] unlockResponse;
private boolean sessionStarted = false;
private boolean indicationsFallbackTried = false;
private final List cccdQueue = new ArrayList<>();
private boolean processingCccd = false;
// Dump de serviços / características
private final List readDumpQueue = new ArrayList<>();
private boolean readingCharacteristic = false;
private boolean dumpingServices = false;
private Runnable afterServiceDump;
private String deviceModel;
// Watchdog discovery
private int discoverRetryCount = 0;
private static final int MAX_DISCOVER_RETRIES = 2;
private boolean servicesDiscovered = false;
public interface Callback {
void onConnected();
void onMeasurement(int sys, int dia, int pulse, String dateTimeIso);
void onError(String message);
void onDisconnected();
}
public OmronBPService(Context ctx) {
this.context = ctx.getApplicationContext();
this.adapter = BluetoothAdapter.getDefaultAdapter();
}
public void connect(String deviceAddress, Callback cb) {
this.callback = cb;
if (adapter == null) {
cb.onError("BluetoothAdapter indisponível");
return;
}
BluetoothDevice device = adapter.getRemoteDevice(deviceAddress);
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
callback.onError("Permissões de Bluetooth não concedidas");
return;
}
gatt = device.connectGatt(context, false, gattCallback);
}
public void disconnect() {
if (gatt != null) {
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
callback.onError("Permissões de Bluetooth não concedidas");
return;
}
gatt.disconnect();
gatt.close();
gatt = null;
}
}
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@OverRide
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d(TAG, "Conectado. Descobrindo serviços...");
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
callback.onError("Permissões de Bluetooth não concedidas");
return;
}
// Opcional: solicitar MTU maior para robustez (não crítico mas ajuda)
try { gatt.requestMtu(185); } catch (Exception ignored) {}
startServiceDiscoveryWatchdog();
if (callback != null) callback.onConnected();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "Desconectado");
if (callback != null) callback.onDisconnected();
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
Log.d(TAG, "MTU alterado status=" + status + " mtu=" + mtu + " -> iniciando discoverServices");
if (!servicesDiscovered) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return;
gatt.discoverServices();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (servicesDiscovered) return; // evita duplicar fluxo
Log.d(TAG, "onServicesDiscovered status=" + status + " services=" + (gatt.getServices()==null?0:gatt.getServices().size()));
if (status != BluetoothGatt.GATT_SUCCESS || gatt.getServices()==null || gatt.getServices().isEmpty()) {
if (discoverRetryCount < MAX_DISCOVER_RETRIES) {
discoverRetryCount++;
Log.w(TAG, "Discovery falhou/zero serviços. Retry=" + discoverRetryCount);
retryServiceDiscovery();
return;
} else {
if (callback != null) callback.onError("Falha discoverServices após retries");
return;
}
}
servicesDiscovered = true;
// Primeiro fazemos dump completo dos serviços/characterísticas
dumpAllServicesAndReadCharacteristics(gatt, () -> {
BluetoothGattService service = gatt.getService(PARENT_SERVICE);
if (service == null) {
if (callback != null) callback.onError("Serviço Omron não encontrado após dump");
return;
}
enableAllNotifications(gatt, service);
afterNotificationsEnabled = () -> new Thread(() -> {
try {
Log.d(TAG, "Notificações ativas. Executando unlock antes do startTransmission...");
boolean unlocked = unlockWithKey();
waitForResponse(5000); // espera unlock 0x81 0x04
startTransmission();
// Verifica hora
try { logDeviceTimeSkew(); } catch (Exception te) { Log.w(TAG, "Falha leitura hora dispositivo: " + te.getMessage()); }
byte[] allUserBytes = readContinuousEeprom(USER1_START_ADDRESS, USER1_RECORD_COUNT * RECORD_SIZE, 0x10);
endTransmission();
parseAndEmitLastRecord(allUserBytes);
} catch (Exception e) {
Log.e(TAG, "Falha ciclo Omron", e);
if (callback != null) callback.onError(e.getMessage());
}
}).start();
});
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
UUID cuuid = characteristic.getUuid();
Log.d(TAG, "onCharacteristicChanged uuid=" + cuuid + " len=" + (value==null?0:value.length) + " hex=" + bytesToHex(value));
// 1) Primeiro, alimente o reassemblador para limpar waitingResponse
if (value != null && isRxUuid(cuuid)) {
handleRxChannel(cuuid, value);
}
// 2) Depois, trate os atalhos (unlock/start) para logs/estado
if (value != null && value.length >= 2) {
if (value[0] == (byte)0x81) {
if (value[1] == (byte)0x00) Log.d(TAG, "Unlock aceito (8100)");
else if (value[1] == (byte)0x04) Log.d(TAG, "Unlock aceito variante (8104)");
else Log.w(TAG, "Unlock resp desconhecida: " + bytesToHex(value));
lastPacketType = value;
} else if (value[0] == (byte)0x80 && value[1] == (byte)0x00) {
Log.d(TAG, "StartTransmission OK (8000)");
lastPacketType = value;
}
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
if (CLIENT_CHAR_CONFIG.equals(descriptor.getUuid())) {
Log.d(TAG, "CCCD escrito uuid=" + descriptor.getCharacteristic().getUuid() + " status=" + status);
onCccdWritten();
processingCccd = false; // libera para próxima
processNextCccd(gatt);
}
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
Log.d(TAG, "Write concluído char=" + characteristic.getUuid() + " status=" + status);
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
readingCharacteristic = false;
if (status == BluetoothGatt.GATT_SUCCESS) {
byte[] value = characteristic.getValue();
logDumpValue(characteristic, value);
} else {
Log.w(TAG, "DUMP falha leitura uuid=" + characteristic.getUuid() + " status=" + status);
}
startNextRead(gatt);
}
};
private void dumpAllServicesAndReadCharacteristics(BluetoothGatt gatt, Runnable after) {
if (gatt == null) return;
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "Sem permissão BLUETOOTH_CONNECT para dump");
if (after != null) after.run();
return;
}
dumpingServices = true;
afterServiceDump = after;
readDumpQueue.clear();
List services = gatt.getServices();
Log.d(TAG, "DUMP total serviços=" + services.size());
for (BluetoothGattService s : services) {
Log.d(TAG, "DUMP service=" + s.getUuid());
for (BluetoothGattCharacteristic ch : s.getCharacteristics()) {
StringBuilder props = new StringBuilder();
int p = ch.getProperties();
if ((p & BluetoothGattCharacteristic.PROPERTY_READ) != 0) props.append("READ ");
if ((p & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) props.append("WRITE ");
if ((p & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) props.append("WRITE_NR ");
if ((p & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) props.append("NOTIFY ");
if ((p & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) props.append("INDICATE ");
Log.d(TAG, " DUMP char=" + ch.getUuid() + " props=" + props.toString().trim() + " descs=" + ch.getDescriptors().size());
if ((p & BluetoothGattCharacteristic.PROPERTY_READ) != 0) {
readDumpQueue.add(ch);
}
}
}
if (readDumpQueue.isEmpty()) {
Log.d(TAG, "DUMP nenhuma characteristic legível");
dumpingServices = false;
if (afterServiceDump != null) afterServiceDump.run();
return;
}
startNextRead(gatt);
}
private void startNextRead(BluetoothGatt gatt) {
if (!dumpingServices) return;
if (readingCharacteristic) return;
if (readDumpQueue.isEmpty()) {
dumpingServices = false;
Log.d(TAG, "DUMP finalizado. Modelo=" + (deviceModel==null?"desconhecido":deviceModel));
if (afterServiceDump != null) afterServiceDump.run();
return;
}
BluetoothGattCharacteristic ch = readDumpQueue.remove(0);
readingCharacteristic = true;
// Em alguns níveis de API (compileSdk < 33) readCharacteristic retorna boolean.
// Mantemos caminho simples para compatibilidade; quando compileSdk subir podemos adaptar.
boolean started = gatt.readCharacteristic(ch);
Log.d(TAG, "DUMP lendo uuid=" + ch.getUuid() + " started=" + started);
if (!started) {
readingCharacteristic = false;
// tenta próximo
startNextRead(gatt);
}
}
private void logDumpValue(BluetoothGattCharacteristic characteristic, byte[] value) {
String hex = bytesToHex(value);
String ascii = asciiPrintable(value);
Log.d(TAG, "DUMP valor uuid=" + characteristic.getUuid() + " hex=" + hex + (ascii.isEmpty()?"":" ascii=""+ascii+"""));
if (MODEL_NUMBER_CHAR.equals(characteristic.getUuid())) {
deviceModel = ascii.isEmpty()?hex:ascii;
Log.d(TAG, "Modelo Omron detectado=" + deviceModel);
}
}
private String asciiPrintable(byte[] value) {
if (value == null || value.length == 0) return "";
StringBuilder sb = new StringBuilder();
int printable = 0;
for (byte b : value) {
int v = b & 0xFF;
if (v >= 0x20 && v <= 0x7E) { sb.append((char)v); printable++; }
else { sb.append('.'); }
if (sb.length() > 64) break; // limita
}
// Se menos de 50% imprimível, não retorna ascii (ruído)
if (printable * 2 < value.length) return "";
return sb.toString();
}
private void startServiceDiscoveryWatchdog() {
servicesDiscovered = false;
discoverRetryCount = 0;
if (gatt == null) return;
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return;
boolean started = gatt.discoverServices();
Log.d(TAG, "discoverServices iniciado started=" + started);
handler.postDelayed(() -> {
if (!servicesDiscovered) {
Log.w(TAG, "Watchdog: serviços não descobertos em 4s");
retryServiceDiscovery();
}
}, 4000);
}
private void retryServiceDiscovery() {
if (gatt == null) return;
if (servicesDiscovered) return;
if (discoverRetryCount > MAX_DISCOVER_RETRIES) return;
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return;
boolean started = gatt.discoverServices();
Log.d(TAG, "retry discoverServices attempt=" + discoverRetryCount + " started=" + started);
handler.postDelayed(() -> {
if (!servicesDiscovered && discoverRetryCount >= MAX_DISCOVER_RETRIES) {
if (callback != null) callback.onError("Timeout discovery serviços");
}
}, 4000);
}
private void handleRxChannel(UUID uuid, byte[] rxBytes) {
int channelIndex = -1;
for (int i = 0; i < RX_CHANNEL_UUIDS.length; i++) {
if (RX_CHANNEL_UUIDS[i].equals(uuid)) { channelIndex = i; break; }
}
if (channelIndex == -1) return;
Log.v(TAG, "RX canal=" + channelIndex + " len=" + (rxBytes==null?0:rxBytes.length));
rxChannelBuffers[channelIndex] = rxBytes;
if (!waitingResponse) return;
if (rxChannelBuffers[0] != null) {
int packetSize = rxChannelBuffers[0][0] & 0xFF;
ByteBuffer combined = ByteBuffer.allocate(packetSize);
if (packetSize <= 16) {
// 🔹 Usa só canal 0
combined.put(rxChannelBuffers[0], 0, packetSize);
} else {
int requiredChannels = (packetSize + 15) / 16;
for (int i = 0; i < requiredChannels; i++) {
if (rxChannelBuffers[i] == null) return; // espera mais
int toPut = Math.min(16, packetSize - combined.position());
combined.put(rxChannelBuffers[i], 0, toPut);
}
}
byte[] full = combined.array();
byte crc = 0x00;
for (byte b : full) crc ^= b;
if (crc != 0) {
Log.w(TAG, "CRC inválido pacote Omron");
}
lastPacketType = new byte[]{full[1], full[2]};
lastEepromAddr = new byte[]{full[3], full[4]};
int expectedDataLen = full[5] & 0xFF;
if (expectedDataLen > (full.length - 8)) {
lastData = new byte[expectedDataLen];
for (int i = 0; i < expectedDataLen; i++) lastData[i] = (byte) 0xFF;
} else {
if (lastPacketType[0] == (byte)0x8f && lastPacketType[1] == (byte)0x00) {
lastData = new byte[]{ full[6] };
} else {
lastData = new byte[expectedDataLen];
System.arraycopy(full, 6, lastData, 0, expectedDataLen);
}
}
for (int i = 0; i < rxChannelBuffers.length; i++) rxChannelBuffers[i] = null;
waitingResponse = false;
Log.d(TAG, String.format(Locale.US,
"Pacote completo type=%02x%02x addr=%02x%02x dataLen=%d",
full[1], full[2], full[3], full[4], expectedDataLen));
}
}
private void waitForResponse(long timeoutMs) throws Exception {
long start = System.currentTimeMillis();
while (waitingResponse) {
if (System.currentTimeMillis() - start > timeoutMs) {
waitingResponse = false;
throw new Exception("Timeout resposta Omron (" + timeoutMs + "ms)");
}
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
}
}
private void sendCommand(byte[] command) throws Exception {
int requiredChannels = (command.length + 15) / 16;
waitingResponse = true;
Log.d(TAG, "Enviando comando len=" + command.length + " channels=" + requiredChannels + " hex=" + bytesToHex(command));
writeTxChannel(0, command); // usar só canal 0
waitForResponse(5000);
Log.d(TAG, "Resposta recebida packetType=" +
(lastPacketType==null ? "null" :
String.format(Locale.US, "%02x%02x", lastPacketType[0], lastPacketType[1])));
}
private void writeTxChannel(int channelIndex, byte[] value) throws Exception {
if (gatt == null) throw new Exception("Gatt nulo");
BluetoothGattService service = gatt.getService(PARENT_SERVICE);
if (service == null) throw new Exception("Serviço Omron ausente");
BluetoothGattCharacteristic ch = service.getCharacteristic(TX_CHANNEL_UUIDS[channelIndex]);
if (ch == null) throw new Exception("Canal TX " + channelIndex + " ausente");
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
callback.onError("Permissões de Bluetooth não concedidas");
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
int status = gatt.writeCharacteristic(ch, value, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
if (status != android.bluetooth.BluetoothStatusCodes.SUCCESS) {
throw new Exception("Falha write canal " + channelIndex + " status=" + status);
}
} else {
@SuppressWarnings("deprecation") boolean ignore = ch.setValue(value);
boolean ok = gatt.writeCharacteristic(ch);
if (!ok) throw new Exception("Falha write canal " + channelIndex);
}
}
private byte[] buildStartTransmissionCmd() {
// Comando base: 0x08 0x00 0x00 0x00 0x00 0x10 0x00 (7 bytes fixos)
byte[] cmd = new byte[]{
(byte)0x08, (byte)0x00, (byte)0x00, (byte)0x00,
(byte)0x00, (byte)0x10, (byte)0x00, (byte)0x00 // último é CRC
};
// calcula XOR dos 7 primeiros
byte crc = 0x00;
for (int i = 0; i < cmd.length - 1; i++) {
crc ^= cmd[i];
}
cmd[cmd.length - 1] = crc;
Log.d(TAG, "startTransmission CRC=" + String.format("%02X", crc));
return cmd;
}
private void startTransmission() throws Exception {
byte[] cmd = buildStartTransmissionCmd();
sendCommand(cmd);
if (lastPacketType == null) throw new Exception("Nenhuma resposta do dispositivo");
if (lastPacketType[0] == (byte)0x81 && lastPacketType[1] == (byte)0x04) {
Log.d(TAG, "Recebi 8104 (ACK unlock) no lugar do start; reenviando startTransmission...");
sendCommand(cmd); // tenta de novo
}
if (!(lastPacketType[0] == (byte)0x80 && lastPacketType[1] == (byte)0x00)) {
throw new Exception("Resposta inesperada: " + bytesToHex(lastPacketType));
}
}
private void endTransmission() throws Exception {
byte[] cmd = hexStringToByteArray("080f000000000007");
sendCommand(cmd);
if (lastPacketType == null || !(lastPacketType[0] == (byte)0x8f && lastPacketType[1] == (byte)0x00)) {
throw new Exception("Resposta inválida endTransmission");
}
if (lastData != null && lastData.length >=1 && lastData[0] != 0x00) {
throw new Exception("Status erro endTransmission: " + (lastData[0] & 0xFF));
}
}
private byte[] readBlockEeprom(int address, int size) throws Exception {
ByteBuffer buf = ByteBuffer.allocate(8); // base sem crc extra? script monta dinamicamente
// Construção conforme python: sizeByteTotal(1) + 0x01 0x00 + addr(2) + blockSize(1) + 0x00 + crc
// Aqui montamos manualmente
byte[] base = new byte[8];
// tamanho total = 8 (header + crc) => 0x08
base[0] = 0x08;
base[1] = 0x01; base[2] = 0x00;
base[3] = (byte)((address >> 8) & 0xFF);
base[4] = (byte)(address & 0xFF);
base[5] = (byte)(size & 0xFF);
base[6] = 0x00; // filler
byte crc = 0x00;
for (int i=0;i<7;i++) crc ^= base[i];
base[7] = crc;
sendCommand(base);
if (lastPacketType == null || !(lastPacketType[0] == (byte)0x81 && lastPacketType[1] == (byte)0x00)) {
throw new Exception("PacketType inesperado readBlock");
}
if (lastEepromAddr[0] != base[3] || lastEepromAddr[1] != base[4]) {
throw new Exception("Endereço retorno difere do solicitado");
}
return lastData;
}
private byte[] readContinuousEeprom(int startAddress, int bytesToRead, int btBlockSize) throws Exception {
ByteBuffer all = ByteBuffer.allocate(bytesToRead);
int addr = startAddress;
int remaining = bytesToRead;
while (remaining > 0) {
int next = Math.min(remaining, btBlockSize);
byte[] block = readBlockEeprom(addr, next);
all.put(block);
addr += next;
remaining -= next;
}
return all.array();
}
private void parseAndEmitLastRecord(byte[] allBytes) {
// Percorre de trás pra frente para achar último válido (não 0xFF)
for (int offset = allBytes.length - RECORD_SIZE; offset >=0; offset -= RECORD_SIZE) {
boolean allFF = true;
for (int i=0;i<RECORD_SIZE;i++) { if ((allBytes[offset + i] & 0xFF) != 0xFF) { allFF = false; break; } }
if (allFF) continue;
byte[] rec = new byte[RECORD_SIZE];
System.arraycopy(allBytes, offset, rec, 0, RECORD_SIZE);
try {
Record r = parseRecord(rec);
if (callback != null) {
callback.onMeasurement(r.sys, r.dia, r.bpm, r.dateIso);
}
} catch (Exception e) {
if (callback != null) callback.onError("Falha parse registro: " + e.getMessage());
}
return;
}
if (callback != null) callback.onError("Nenhum registro válido encontrado");
}
private static class Record { int sys; int dia; int bpm; String dateIso; }
private Record parseRecord(byte[] rec) throws Exception {
// Endianness big (script) - vamos interpretar como int grande
ByteBuffer bb = ByteBuffer.wrap(rec).order(ByteOrder.BIG_ENDIAN);
long bigInt = 0;
for (int i=0;i<rec.length;i++) { bigInt = (bigInt << 8) | (rec[i] & 0xFF); }
Record r = new Record();
r.dia = getBits(bigInt, 0,7);
r.sys = getBits(bigInt,8,15) + 25; // ajuste script
int year = getBits(bigInt,18,23) + 2000;
r.bpm = getBits(bigInt,24,31);
// ihb bit 32, mov bit 33 (ignorados nesta MVP)
int month = getBits(bigInt,34,37);
int day = getBits(bigInt,38,42);
int hour = getBits(bigInt,43,47);
int minute = getBits(bigInt,52,57);
int second = getBits(bigInt,58,63);
if (second > 59) second = 59;
try {
Date d = new Date(year-1900, month-1, day, hour, minute, second); // Date legacy
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
r.dateIso = sdf.format(d);
} catch (Exception e) {
r.dateIso = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).format(new Date());
}
return r;
}
private int getBits(long bigInt, int firstBit, int lastBit) {
int numBits = (lastBit - firstBit) + 1;
long shifted = bigInt >> (recBitsLen() - (lastBit + 1));
long mask = (1L << numBits) - 1L;
return (int)(shifted & mask);
}
private int recBitsLen() { return RECORD_SIZE * 8; }
private byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
private void enableAllNotifications(BluetoothGatt gatt, BluetoothGattService service) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
if (callback != null) callback.onError("Permissões de Bluetooth não concedidas");
return;
}
pendingCccdWrites = 0;
cccdQueue.clear();
processingCccd = false;
for (UUID rxUuid : RX_CHANNEL_UUIDS) {
BluetoothGattCharacteristic ch = service.getCharacteristic(rxUuid);
if (ch == null) {
Log.w(TAG, "Característica RX ausente uuid=" + rxUuid);
continue;
}
gatt.setCharacteristicNotification(ch, true);
BluetoothGattDescriptor cccd = ch.getDescriptor(CLIENT_CHAR_CONFIG);
if (cccd != null) {
// 🔹 Agora força NOTIFY sempre
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
cccdQueue.add(cccd);
pendingCccdWrites++;
Log.d(TAG, "CCCD set NOTIFY para uuid=" + ch.getUuid());
} else {
Log.w(TAG, "Sem descriptor CCCD para RX uuid=" + ch.getUuid());
}
}
if (pendingCccdWrites == 0) {
Log.w(TAG, "Nenhum CCCD escrito (talvez não seja necessário) continuando...");
if (!sessionStarted && afterNotificationsEnabled != null) {
sessionStarted = true;
afterNotificationsEnabled.run();
}
return;
}
processNextCccd(gatt);
}
/**
-
Envia a chave de desbloqueio para o Omron HEM-6232T.
-
O unlock é feito com write direto na characteristic b305b680.
-
A resposta (0x81xx) não vem pelo próprio unlockCh, mas sim nos canais de notify/indicate.
*/
private boolean unlockWithKey() {
if (gatt == null) return false;
BluetoothGattService omronService = gatt.getService(UUID.fromString("ecbe3980-c9a2-11e1-b1bd-0002a5d5c51b"));
if (omronService == null) {
Log.w(TAG, "Serviço Omron não encontrado para unlock");
return false;
}
BluetoothGattCharacteristic unlockCh =
omronService.getCharacteristic(UUID.fromString("b305b680-aee7-11e1-a730-0002a5d5c51b"));
if (unlockCh == null) {
Log.w(TAG, "Characteristic unlock não encontrada");
return false;
}
try {
// Apenas habilita notificações locais (sem tentar CCCD, não é suportado nesse char)
//gatt.setCharacteristicNotification(unlockCh, true);
// Monta payload: 0x01 + chave (geralmente 16 bytes fixos)
byte[] key = hexStringToByteArray(PAIRING_KEY_HEX);
byte[] payload = new byte[1 + key.length];
payload[0] = 0x01;
System.arraycopy(key, 0, payload, 1, key.length);
unlockCh.setValue(payload);
boolean ok = gatt.writeCharacteristic(unlockCh);
Log.d(TAG, "Unlock enviado, len=" + payload.length + " hex=" + bytesToHex(payload));
return ok;
} catch (Exception e) {
Log.e(TAG, "Falha no unlockWithKey: " + e.getMessage(), e);
return false;
}
}
// Hook de callback para finalização das escritas de CCCD
// Requer override onDescriptorWrite (adicionar abaixo)
private void onCccdWritten() {
if (pendingCccdWrites > 0) pendingCccdWrites--;
if (pendingCccdWrites == 0 && !sessionStarted) {
Log.d(TAG, "Todas notificações habilitadas");
sessionStarted = true;
if (afterNotificationsEnabled != null) afterNotificationsEnabled.run();
}
}
// Adicionar override
private final BluetoothGattCallback gattCallbackOriginal = null; // placeholder não usado
// Extender callback para descriptor write
// (inserido após declaração original; manteríamos em refatoração futura)
// Para simplicidade, adicionamos método dentro da classe (para quando mover callback separar).
// NOTA: Implementação: modificar callback acima seria ideal, mas para patch incremental usamos reflection do próprio callback? Aqui realmente precisamos editar callback original acima - já feito.
// Precisamos adicionar onDescriptorWrite dentro do callback original - refator não trivial agora.
/**
-
Tenta startTransmission direto; se falhar, tenta unlock e repete.
*/
private void attemptStartTransmissionWithRetry() throws Exception {
int attempts = 0;
Exception last = null;
// Tenta até 3 vezes sem unlock
while (attempts < 3) {
attempts++;
try {
startTransmission();
Log.d(TAG, "startTransmission OK tentativa=" + attempts + " sem unlock");
return;
} catch (Exception e) {
last = e;
Log.w(TAG, "startTransmission tentativa=" + attempts + " falhou: " + e.getMessage());
// 🔹 Patch: tenta de novo imediatamente porque a 1ª vez costuma ser ignorada
if (attempts == 1) {
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
try {
startTransmission();
Log.d(TAG, "startTransmission OK na 2ª chamada rápida");
return;
} catch (Exception e2) {
Log.w(TAG, "2ª chamada rápida falhou também: " + e2.getMessage());
last = e2;
}
}
}
}
Log.w(TAG, "Todas tentativas sem unlock falharam: " +
(last != null ? last.getMessage() : "desconhecido") +
" - tentando unlock...");
boolean unlocked = unlockWithKey();
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
try {
startTransmission();
Log.d(TAG, "startTransmission OK após unlock (unlocked=" + unlocked + ")");
} catch (Exception e2) {
Log.w(TAG, "startTransmission ainda falhou após unlock: " + e2.getMessage());
// 🔹 Patch: não tenta mais INDICATION (o Omron só aceita NOTIFY e estava retornando status=3)
// Então em vez disso aumentamos o timeout + delay entre TX
throw e2;
}
}
/**
- Lê bloco de horário do dispositivo e calcula diferença para hora local.
- Layout (conforme código python comentado):
- bytes[0:2] header desconhecido
- bytes[2] mês (1-12)
- bytes[3] ano (offset 2000)
- bytes[4] hora (0-23)
- bytes[5] dia (1-31)
- bytes[6] segundo (0-59)
- bytes[7] minuto (0-59)
- bytes[8:10] crc / filler
*/
private Date readDeviceTime() throws Exception {
int address = SETTINGS_READ_ADDRESS + TIME_SYNC_OFFSET; // 0x0260 + 0x14 = 0x0274
byte[] timeBlock = readBlockEeprom(address, TIME_SYNC_LENGTH);
if (timeBlock == null || timeBlock.length < TIME_SYNC_LENGTH) throw new Exception("Bloco horário incompleto");
int month = timeBlock[2] & 0xFF;
int year = (timeBlock[3] & 0xFF) + 2000;
int hour = timeBlock[4] & 0xFF;
int day = timeBlock[5] & 0xFF;
int second = timeBlock[6] & 0xFF; if (second > 59) second = 59;
int minute = timeBlock[7] & 0xFF;
try {
return new Date(year - 1900, month - 1, day, hour, minute, second);
} catch (Exception e) {
throw new Exception("Data inválida lida do device");
}
}
private void logDeviceTimeSkew() throws Exception {
Date deviceDate = readDeviceTime();
long skewMs = Math.abs(System.currentTimeMillis() - deviceDate.getTime());
long skewMin = skewMs / 60000L;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
if (skewMin > 5) {
Log.w(TAG, "Diferença de hora device vs sistema >5min (" + skewMin + "m) device=" + sdf.format(deviceDate));
} else {
Log.d(TAG, "Hora device OK (skew=" + skewMin + "m) device=" + sdf.format(deviceDate));
}
}
private void processNextCccd(BluetoothGatt gatt) {
if (processingCccd) return;
if (cccdQueue.isEmpty()) return;
processingCccd = true;
BluetoothGattDescriptor next = cccdQueue.remove(0);
next.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
boolean started = gatt.writeDescriptor(next);
Log.d(TAG, "Fila CCCD -> write " + next.getCharacteristic().getUuid() + " started=" + started + (indicationsFallbackTried?" (INDICATION)":""));
if (!started) {
processingCccd = false;
if (!cccdQueue.isEmpty()) processNextCccd(gatt);
}
}
private void reEnableNotificationsWithIndications() {
if (gatt == null) return;
BluetoothGattService service = gatt.getService(PARENT_SERVICE);
if (service == null) return;
pendingCccdWrites = 0;
cccdQueue.clear();
for (UUID rxUuid : RX_CHANNEL_UUIDS) {
BluetoothGattCharacteristic ch = service.getCharacteristic(rxUuid);
if (ch == null) continue;
gatt.setCharacteristicNotification(ch, true);
BluetoothGattDescriptor cccd = ch.getDescriptor(CLIENT_CHAR_CONFIG);
if (cccd != null) {
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
cccdQueue.add(cccd);
pendingCccdWrites++;
}
}
processNextCccd(gatt);
}
private String bytesToHex(byte[] data) {
if (data == null) return "null";
StringBuilder sb = new StringBuilder();
for (byte b : data) sb.append(String.format(Locale.US, "%02X", b));
return sb.toString();
}
private boolean isRxUuid(UUID u) {
for (UUID rx : RX_CHANNEL_UUIDS) {
if (rx.equals(u)) return true;
}
return false;
}
}
`
I'm trying to convert the code and synchronize the HEM6232T device in an Android application, but without success, this is my converted code with some logs in PT-BR, could you help me solve this problem?
Edit:
After some testing, I discovered that I must first register the device in the Omron Connect app. After the first sync there, my app can read all the data and sync.
How do I avoid having to open the Omron app?
Conectado. Descobrindo serviços...
2025-09-27 12:26:44.535 11118-15628 OmronBPService com.cinam.saude D discoverServices iniciado started=true
2025-09-27 12:26:46.588 11118-15842 OmronBPService com.cinam.saude D MTU alterado status=0 mtu=160 -> iniciando discoverServices
2025-09-27 12:26:46.604 11118-15842 OmronBPService com.cinam.saude D onServicesDiscovered status=0 services=9
2025-09-27 12:26:46.605 11118-15842 OmronBPService com.cinam.saude D DUMP total serviços=9
2025-09-27 12:26:46.605 11118-15842 OmronBPService com.cinam.saude D DUMP service=00001800-0000-1000-8000-00805f9b34fb
2025-09-27 12:26:46.605 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a00-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.605 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a01-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a02-0000-1000-8000-00805f9b34fb props=READ WRITE WRITE_NR descs=0
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a03-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a04-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP service=00001801-0000-1000-8000-00805f9b34fb
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a05-0000-1000-8000-00805f9b34fb props=INDICATE descs=1
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP service=0000180a-0000-1000-8000-00805f9b34fb
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a23-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a24-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.606 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a25-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a26-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a27-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a28-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a29-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a2a-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP service=ecbe3980-c9a2-11e1-b1bd-0002a5d5c51b
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=b305b680-aee7-11e1-a730-0002a5d5c51b props=READ WRITE WRITE_NR NOTIFY descs=1
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=db5b55e0-aee7-11e1-965e-0002a5d5c51b props=WRITE WRITE_NR descs=0
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=e0b8a060-aee7-11e1-92f4-0002a5d5c51b props=WRITE WRITE_NR descs=0
2025-09-27 12:26:46.607 11118-15842 OmronBPService com.cinam.saude D DUMP char=0ae12b00-aee8-11e1-a192-0002a5d5c51b props=WRITE WRITE_NR descs=0
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP char=10e1ba60-aee8-11e1-89e5-0002a5d5c51b props=WRITE WRITE_NR descs=0
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP char=49123040-aee8-11e1-a74d-0002a5d5c51b props=READ NOTIFY descs=1
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP char=4d0bf320-aee8-11e1-a0d9-0002a5d5c51b props=READ NOTIFY descs=1
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP char=5128ce60-aee8-11e1-b84b-0002a5d5c51b props=READ NOTIFY descs=1
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP char=560f1420-aee8-11e1-8184-0002a5d5c51b props=READ NOTIFY descs=1
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP char=8858eb40-aee8-11e1-bb67-0002a5d5c51b props=NOTIFY descs=1
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP service=0000180f-0000-1000-8000-00805f9b34fb
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a19-0000-1000-8000-00805f9b34fb props=READ NOTIFY descs=1
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP service=00001805-0000-1000-8000-00805f9b34fb
2025-09-27 12:26:46.608 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a2b-0000-1000-8000-00805f9b34fb props=READ WRITE NOTIFY descs=1
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP service=5df5e817-a945-4f81-89c0-3d4e9759c07c
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a52-0000-1000-8000-00805f9b34fb props=WRITE INDICATE descs=1
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP char=c195da8a-0e23-4582-acd8-d446c77c45de props=INDICATE descs=1
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP service=0000181c-0000-1000-8000-00805f9b34fb
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a99-0000-1000-8000-00805f9b34fb props=READ WRITE NOTIFY descs=1
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a9a-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a9f-0000-1000-8000-00805f9b34fb props=WRITE INDICATE descs=1
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a85-0000-1000-8000-00805f9b34fb props=READ WRITE descs=0
2025-09-27 12:26:46.609 11118-15842 OmronBPService com.cinam.saude D DUMP service=00001810-0000-1000-8000-00805f9b34fb
2025-09-27 12:26:46.610 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a35-0000-1000-8000-00805f9b34fb props=INDICATE descs=1
2025-09-27 12:26:46.610 11118-15842 OmronBPService com.cinam.saude D DUMP char=00002a49-0000-1000-8000-00805f9b34fb props=READ descs=0
2025-09-27 12:26:46.614 11118-15842 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a00-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:46.666 11118-15842 OmronBPService com.cinam.saude D DUMP valor uuid=00002a00-0000-1000-8000-00805f9b34fb hex=48454D2D3632333254 ascii="HEM-6232T"
2025-09-27 12:26:46.669 11118-15842 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a01-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:46.757 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a01-0000-1000-8000-00805f9b34fb hex=8203
2025-09-27 12:26:46.758 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a02-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:46.984 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a02-0000-1000-8000-00805f9b34fb hex=00
2025-09-27 12:26:46.987 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a03-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.074 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a03-0000-1000-8000-00805f9b34fb hex=005FBF4F48D4 ascii="._.OH."
2025-09-27 12:26:47.078 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a04-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.164 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a04-0000-1000-8000-00805f9b34fb hex=100018000000C800
2025-09-27 12:26:47.168 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a23-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.299 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a23-0000-1000-8000-00805f9b34fb hex=D4484FFEFFBF5F00
2025-09-27 12:26:47.304 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a24-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.389 11118-15842 OmronBPService com.cinam.saude D DUMP valor uuid=00002a24-0000-1000-8000-00805f9b34fb hex=48454D2D3632333254 ascii="HEM-6232T"
2025-09-27 12:26:47.389 11118-15842 OmronBPService com.cinam.saude D Modelo Omron detectado=HEM-6232T
2025-09-27 12:26:47.396 11118-15842 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a25-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.478 11118-15842 OmronBPService com.cinam.saude D DUMP valor uuid=00002a25-0000-1000-8000-00805f9b34fb hex=
2025-09-27 12:26:47.482 11118-15842 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a26-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.568 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a26-0000-1000-8000-00805f9b34fb hex=442E30302E3746422D3132 ascii="D.00.7FB-12"
2025-09-27 12:26:47.571 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a27-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.658 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a27-0000-1000-8000-00805f9b34fb hex=30303030303030303030303030313030 ascii="0000000000000100"
2025-09-27 12:26:47.660 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a28-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.748 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a28-0000-1000-8000-00805f9b34fb hex=30303030303030303030303030313034 ascii="0000000000000104"
2025-09-27 12:26:47.751 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a29-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.840 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a29-0000-1000-8000-00805f9b34fb hex=4F4D524F4E4845414C544843415245 ascii="OMRONHEALTHCARE"
2025-09-27 12:26:47.843 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a2a-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:47.929 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a2a-0000-1000-8000-00805f9b34fb hex=00000000000000000000000000000000
2025-09-27 12:26:47.932 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=b305b680-aee7-11e1-a730-0002a5d5c51b started=true
2025-09-27 12:26:48.020 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=b305b680-aee7-11e1-a730-0002a5d5c51b hex=0000000000000000000000000000000000000000
2025-09-27 12:26:48.024 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=49123040-aee8-11e1-a74d-0002a5d5c51b started=true
2025-09-27 12:26:48.154 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=49123040-aee8-11e1-a74d-0002a5d5c51b hex=00000000000000000000000000000000
2025-09-27 12:26:48.158 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=4d0bf320-aee8-11e1-a0d9-0002a5d5c51b started=true
2025-09-27 12:26:48.379 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=4d0bf320-aee8-11e1-a0d9-0002a5d5c51b hex=00000000000000000000000000000000
2025-09-27 12:26:48.382 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=5128ce60-aee8-11e1-b84b-0002a5d5c51b started=true
2025-09-27 12:26:48.468 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=5128ce60-aee8-11e1-b84b-0002a5d5c51b hex=00000000000000000000000000000000
2025-09-27 12:26:48.474 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=560f1420-aee8-11e1-8184-0002a5d5c51b started=true
2025-09-27 12:26:48.603 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=560f1420-aee8-11e1-8184-0002a5d5c51b hex=00000000000000000000000000000000
2025-09-27 12:26:48.605 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a19-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:48.827 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a19-0000-1000-8000-00805f9b34fb hex=00
2025-09-27 12:26:48.829 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a2b-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:49.009 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a2b-0000-1000-8000-00805f9b34fb hex=00000000000000000000
2025-09-27 12:26:49.011 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a99-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:49.097 11118-15628 OmronBPService com.cinam.saude D DUMP valor uuid=00002a99-0000-1000-8000-00805f9b34fb hex=00000000
2025-09-27 12:26:49.101 11118-15628 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a9a-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:53.102 11118-15625 OmronBPService com.cinam.saude D DUMP valor uuid=00002a9a-0000-1000-8000-00805f9b34fb hex=FF
2025-09-27 12:26:53.104 11118-15625 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a85-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:53.512 11118-15842 OmronBPService com.cinam.saude W DUMP falha leitura uuid=00002a85-0000-1000-8000-00805f9b34fb status=128
2025-09-27 12:26:53.514 11118-15842 OmronBPService com.cinam.saude D DUMP lendo uuid=00002a49-0000-1000-8000-00805f9b34fb started=true
2025-09-27 12:26:53.632 11118-15625 OmronBPService com.cinam.saude D DUMP valor uuid=00002a49-0000-1000-8000-00805f9b34fb hex=1700
2025-09-27 12:26:53.632 11118-15625 OmronBPService com.cinam.saude D DUMP finalizado. Modelo=HEM-6232T
2025-09-27 12:26:53.634 11118-15625 OmronBPService com.cinam.saude D CCCD set NOTIFY para uuid=49123040-aee8-11e1-a74d-0002a5d5c51b
2025-09-27 12:26:53.636 11118-15625 OmronBPService com.cinam.saude D CCCD set NOTIFY para uuid=4d0bf320-aee8-11e1-a0d9-0002a5d5c51b
2025-09-27 12:26:53.637 11118-15625 OmronBPService com.cinam.saude D CCCD set NOTIFY para uuid=5128ce60-aee8-11e1-b84b-0002a5d5c51b
2025-09-27 12:26:53.638 11118-15625 OmronBPService com.cinam.saude D CCCD set NOTIFY para uuid=560f1420-aee8-11e1-8184-0002a5d5c51b
2025-09-27 12:26:53.640 11118-15625 OmronBPService com.cinam.saude D Fila CCCD -> write 49123040-aee8-11e1-a74d-0002a5d5c51b started=true
2025-09-27 12:26:53.811 11118-15625 OmronBPService com.cinam.saude D CCCD escrito uuid=49123040-aee8-11e1-a74d-0002a5d5c51b status=0
2025-09-27 12:26:53.813 11118-15625 OmronBPService com.cinam.saude D Fila CCCD -> write 4d0bf320-aee8-11e1-a0d9-0002a5d5c51b started=true
2025-09-27 12:26:53.991 11118-15625 OmronBPService com.cinam.saude D CCCD escrito uuid=4d0bf320-aee8-11e1-a0d9-0002a5d5c51b status=0
2025-09-27 12:26:53.994 11118-15625 OmronBPService com.cinam.saude D Fila CCCD -> write 5128ce60-aee8-11e1-b84b-0002a5d5c51b started=true
2025-09-27 12:26:54.110 11118-15625 OmronBPService com.cinam.saude D CCCD escrito uuid=5128ce60-aee8-11e1-b84b-0002a5d5c51b status=0
2025-09-27 12:26:54.112 11118-15625 OmronBPService com.cinam.saude D Fila CCCD -> write 560f1420-aee8-11e1-8184-0002a5d5c51b started=true
2025-09-27 12:26:54.172 11118-15625 OmronBPService com.cinam.saude D CCCD escrito uuid=560f1420-aee8-11e1-8184-0002a5d5c51b status=0
2025-09-27 12:26:54.172 11118-15625 OmronBPService com.cinam.saude D Todas notificações habilitadas
2025-09-27 12:26:54.174 11118-15945 OmronBPService com.cinam.saude D Notificações ativas. Executando unlock antes do startTransmission...
2025-09-27 12:26:54.177 11118-15945 OmronBPService com.cinam.saude D Unlock enviado, len=17 hex=01DEADBEEF12341234DEADBEEF12341234
2025-09-27 12:26:54.177 11118-15945 OmronBPService com.cinam.saude D startTransmission CRC=18
2025-09-27 12:26:54.178 11118-15945 OmronBPService com.cinam.saude D Enviando comando len=8 channels=1 hex=0800000000100018
2025-09-27 12:26:54.178 11118-15625 OmronBPService com.cinam.saude D Write concluído char=b305b680-aee7-11e1-a730-0002a5d5c51b status=0
2025-09-27 12:26:54.178 11118-15945 OmronBPService com.cinam.saude E Falha ciclo Omron (Ask Gemini)
java.lang.Exception: Falha write canal 0 status=201
at com.cinam.saude.OmronBPService.writeTxChannel(OmronBPService.java:493)
at com.cinam.saude.OmronBPService.sendCommand(OmronBPService.java:470)
at com.cinam.saude.OmronBPService.startTransmission(OmronBPService.java:522)
at com.cinam.saude.OmronBPService.-$$Nest$mstartTransmission(Unknown Source:0)
at com.cinam.saude.OmronBPService$1.lambda$onServicesDiscovered$0(OmronBPService.java:207)
at com.cinam.saude.OmronBPService$1.$r8$lambda$_RwrGyUPQ9XcARxblHbWzaXHrmw(Unknown Source:0)
at com.cinam.saude.OmronBPService$1$$ExternalSyntheticLambda1.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1119)
`package com.cinam.saude;
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
/**
Serviço mínimo para leitura da última medição armazenada no monitor Omron HEM‑6232T.
Baseado na lógica do script Python (export2garmin) porém de forma simplificada:
Limitações MVP: não lê múltiplos usuários, não reseta unread counter, não sincroniza hora.
*/
public class OmronBPService {
private static final String TAG = "OmronBPService";
// UUIDs proprietários (mesmos do script python)
private static final UUID PARENT_SERVICE = UUID.fromString("ecbe3980-c9a2-11e1-b1bd-0002a5d5c51b");
private static final UUID[] RX_CHANNEL_UUIDS = new UUID[]{
UUID.fromString("49123040-aee8-11e1-a74d-0002a5d5c51b"),
UUID.fromString("4d0bf320-aee8-11e1-a0d9-0002a5d5c51b"),
UUID.fromString("5128ce60-aee8-11e1-b84b-0002a5d5c51b"),
UUID.fromString("560f1420-aee8-11e1-8184-0002a5d5c51b")
};
private static final UUID[] TX_CHANNEL_UUIDS = new UUID[]{
UUID.fromString("db5b55e0-aee7-11e1-965e-0002a5d5c51b"),
UUID.fromString("e0b8a060-aee7-11e1-92f4-0002a5d5c51b"),
UUID.fromString("0ae12b00-aee8-11e1-a192-0002a5d5c51b"),
UUID.fromString("10e1ba60-aee8-11e1-89e5-0002a5d5c51b")
};
// Característica de unlock / pairing (para liberar canal de transmissão)
private static final UUID UNLOCK_UUID = UUID.fromString("b305b680-aee7-11e1-a730-0002a5d5c51b");
// Device Information Service + modelo
private static final UUID DEVICE_INFO_SERVICE = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb");
private static final UUID MODEL_NUMBER_CHAR = UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb");
// Chave de pareamento usada no script python (16 bytes em hex)
private static final String PAIRING_KEY_HEX = "deadbeaf12341234deadbeaf12341234";
// Parâmetros do device (hem-6232t.py)
private static final int USER1_START_ADDRESS = 0x2e8; // início registros user1
private static final int RECORD_SIZE = 0x0e; // 14 bytes
private static final int USER1_RECORD_COUNT = 100; // 100 registros
// Endereços de settings (do driver python hem-6232t)
private static final int SETTINGS_READ_ADDRESS = 0x0260;
private static final int TIME_SYNC_OFFSET = 0x14; // início bloco horário relativo a SETTINGS_READ_ADDRESS
private static final int TIME_SYNC_LENGTH = 0x0A; // 2 header + 6 time + 2 crc
private final Context context;
private final BluetoothAdapter adapter;
private BluetoothGatt gatt;
private Callback callback;
// Buffer de recepção multi-canal
private final byte[][] rxChannelBuffers = new byte[4][];
private boolean waitingResponse = false;
private byte[] lastPacketType; // 2 bytes
private byte[] lastEepromAddr; // 2 bytes
private byte[] lastData; // payload
private final Handler handler = new Handler();
private static final UUID CLIENT_CHAR_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private int pendingCccdWrites = 0;
private Runnable afterNotificationsEnabled;
// Estado para fluxo de unlock
private boolean waitingUnlock = false;
private byte[] unlockResponse;
private boolean sessionStarted = false;
private boolean indicationsFallbackTried = false;
private final List cccdQueue = new ArrayList<>();
private boolean processingCccd = false;
// Dump de serviços / características
private final List readDumpQueue = new ArrayList<>();
private boolean readingCharacteristic = false;
private boolean dumpingServices = false;
private Runnable afterServiceDump;
private String deviceModel;
// Watchdog discovery
private int discoverRetryCount = 0;
private static final int MAX_DISCOVER_RETRIES = 2;
private boolean servicesDiscovered = false;
public interface Callback {
void onConnected();
void onMeasurement(int sys, int dia, int pulse, String dateTimeIso);
void onError(String message);
void onDisconnected();
}
public OmronBPService(Context ctx) {
this.context = ctx.getApplicationContext();
this.adapter = BluetoothAdapter.getDefaultAdapter();
}
public void connect(String deviceAddress, Callback cb) {
this.callback = cb;
if (adapter == null) {
cb.onError("BluetoothAdapter indisponível");
return;
}
BluetoothDevice device = adapter.getRemoteDevice(deviceAddress);
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
callback.onError("Permissões de Bluetooth não concedidas");
return;
}
gatt = device.connectGatt(context, false, gattCallback);
}
public void disconnect() {
if (gatt != null) {
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
callback.onError("Permissões de Bluetooth não concedidas");
return;
}
gatt.disconnect();
gatt.close();
gatt = null;
}
}
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@OverRide
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d(TAG, "Conectado. Descobrindo serviços...");
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
callback.onError("Permissões de Bluetooth não concedidas");
return;
}
// Opcional: solicitar MTU maior para robustez (não crítico mas ajuda)
try { gatt.requestMtu(185); } catch (Exception ignored) {}
startServiceDiscoveryWatchdog();
if (callback != null) callback.onConnected();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "Desconectado");
if (callback != null) callback.onDisconnected();
}
}
};
private void dumpAllServicesAndReadCharacteristics(BluetoothGatt gatt, Runnable after) {
if (gatt == null) return;
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "Sem permissão BLUETOOTH_CONNECT para dump");
if (after != null) after.run();
return;
}
dumpingServices = true;
afterServiceDump = after;
readDumpQueue.clear();
List services = gatt.getServices();
Log.d(TAG, "DUMP total serviços=" + services.size());
for (BluetoothGattService s : services) {
Log.d(TAG, "DUMP service=" + s.getUuid());
for (BluetoothGattCharacteristic ch : s.getCharacteristics()) {
StringBuilder props = new StringBuilder();
int p = ch.getProperties();
if ((p & BluetoothGattCharacteristic.PROPERTY_READ) != 0) props.append("READ ");
if ((p & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) props.append("WRITE ");
if ((p & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) props.append("WRITE_NR ");
if ((p & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) props.append("NOTIFY ");
if ((p & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) props.append("INDICATE ");
Log.d(TAG, " DUMP char=" + ch.getUuid() + " props=" + props.toString().trim() + " descs=" + ch.getDescriptors().size());
if ((p & BluetoothGattCharacteristic.PROPERTY_READ) != 0) {
readDumpQueue.add(ch);
}
}
}
if (readDumpQueue.isEmpty()) {
Log.d(TAG, "DUMP nenhuma characteristic legível");
dumpingServices = false;
if (afterServiceDump != null) afterServiceDump.run();
return;
}
startNextRead(gatt);
}
private void startNextRead(BluetoothGatt gatt) {
if (!dumpingServices) return;
if (readingCharacteristic) return;
if (readDumpQueue.isEmpty()) {
dumpingServices = false;
Log.d(TAG, "DUMP finalizado. Modelo=" + (deviceModel==null?"desconhecido":deviceModel));
if (afterServiceDump != null) afterServiceDump.run();
return;
}
BluetoothGattCharacteristic ch = readDumpQueue.remove(0);
readingCharacteristic = true;
// Em alguns níveis de API (compileSdk < 33) readCharacteristic retorna boolean.
// Mantemos caminho simples para compatibilidade; quando compileSdk subir podemos adaptar.
boolean started = gatt.readCharacteristic(ch);
Log.d(TAG, "DUMP lendo uuid=" + ch.getUuid() + " started=" + started);
if (!started) {
readingCharacteristic = false;
// tenta próximo
startNextRead(gatt);
}
}
private void logDumpValue(BluetoothGattCharacteristic characteristic, byte[] value) {
String hex = bytesToHex(value);
String ascii = asciiPrintable(value);
Log.d(TAG, "DUMP valor uuid=" + characteristic.getUuid() + " hex=" + hex + (ascii.isEmpty()?"":" ascii=""+ascii+"""));
if (MODEL_NUMBER_CHAR.equals(characteristic.getUuid())) {
deviceModel = ascii.isEmpty()?hex:ascii;
Log.d(TAG, "Modelo Omron detectado=" + deviceModel);
}
}
private String asciiPrintable(byte[] value) {
if (value == null || value.length == 0) return "";
StringBuilder sb = new StringBuilder();
int printable = 0;
for (byte b : value) {
int v = b & 0xFF;
if (v >= 0x20 && v <= 0x7E) { sb.append((char)v); printable++; }
else { sb.append('.'); }
if (sb.length() > 64) break; // limita
}
// Se menos de 50% imprimível, não retorna ascii (ruído)
if (printable * 2 < value.length) return "";
return sb.toString();
}
private void startServiceDiscoveryWatchdog() {
servicesDiscovered = false;
discoverRetryCount = 0;
if (gatt == null) return;
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return;
boolean started = gatt.discoverServices();
Log.d(TAG, "discoverServices iniciado started=" + started);
handler.postDelayed(() -> {
if (!servicesDiscovered) {
Log.w(TAG, "Watchdog: serviços não descobertos em 4s");
retryServiceDiscovery();
}
}, 4000);
}
private void retryServiceDiscovery() {
if (gatt == null) return;
if (servicesDiscovered) return;
if (discoverRetryCount > MAX_DISCOVER_RETRIES) return;
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return;
boolean started = gatt.discoverServices();
Log.d(TAG, "retry discoverServices attempt=" + discoverRetryCount + " started=" + started);
handler.postDelayed(() -> {
if (!servicesDiscovered && discoverRetryCount >= MAX_DISCOVER_RETRIES) {
if (callback != null) callback.onError("Timeout discovery serviços");
}
}, 4000);
}
private void handleRxChannel(UUID uuid, byte[] rxBytes) {
int channelIndex = -1;
for (int i = 0; i < RX_CHANNEL_UUIDS.length; i++) {
if (RX_CHANNEL_UUIDS[i].equals(uuid)) { channelIndex = i; break; }
}
if (channelIndex == -1) return;
}
private void waitForResponse(long timeoutMs) throws Exception {
long start = System.currentTimeMillis();
while (waitingResponse) {
if (System.currentTimeMillis() - start > timeoutMs) {
waitingResponse = false;
throw new Exception("Timeout resposta Omron (" + timeoutMs + "ms)");
}
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
}
}
private void sendCommand(byte[] command) throws Exception {
int requiredChannels = (command.length + 15) / 16;
waitingResponse = true;
Log.d(TAG, "Enviando comando len=" + command.length + " channels=" + requiredChannels + " hex=" + bytesToHex(command));
}
private void writeTxChannel(int channelIndex, byte[] value) throws Exception {
if (gatt == null) throw new Exception("Gatt nulo");
BluetoothGattService service = gatt.getService(PARENT_SERVICE);
if (service == null) throw new Exception("Serviço Omron ausente");
BluetoothGattCharacteristic ch = service.getCharacteristic(TX_CHANNEL_UUIDS[channelIndex]);
if (ch == null) throw new Exception("Canal TX " + channelIndex + " ausente");
}
private byte[] buildStartTransmissionCmd() {
// Comando base: 0x08 0x00 0x00 0x00 0x00 0x10 0x00 (7 bytes fixos)
byte[] cmd = new byte[]{
(byte)0x08, (byte)0x00, (byte)0x00, (byte)0x00,
(byte)0x00, (byte)0x10, (byte)0x00, (byte)0x00 // último é CRC
};
}
private void startTransmission() throws Exception {
byte[] cmd = buildStartTransmissionCmd();
sendCommand(cmd);
}
private void endTransmission() throws Exception {
byte[] cmd = hexStringToByteArray("080f000000000007");
sendCommand(cmd);
if (lastPacketType == null || !(lastPacketType[0] == (byte)0x8f && lastPacketType[1] == (byte)0x00)) {
throw new Exception("Resposta inválida endTransmission");
}
if (lastData != null && lastData.length >=1 && lastData[0] != 0x00) {
throw new Exception("Status erro endTransmission: " + (lastData[0] & 0xFF));
}
}
private byte[] readBlockEeprom(int address, int size) throws Exception {
ByteBuffer buf = ByteBuffer.allocate(8); // base sem crc extra? script monta dinamicamente
// Construção conforme python: sizeByteTotal(1) + 0x01 0x00 + addr(2) + blockSize(1) + 0x00 + crc
// Aqui montamos manualmente
byte[] base = new byte[8];
// tamanho total = 8 (header + crc) => 0x08
base[0] = 0x08;
base[1] = 0x01; base[2] = 0x00;
base[3] = (byte)((address >> 8) & 0xFF);
base[4] = (byte)(address & 0xFF);
base[5] = (byte)(size & 0xFF);
base[6] = 0x00; // filler
byte crc = 0x00;
for (int i=0;i<7;i++) crc ^= base[i];
base[7] = crc;
sendCommand(base);
if (lastPacketType == null || !(lastPacketType[0] == (byte)0x81 && lastPacketType[1] == (byte)0x00)) {
throw new Exception("PacketType inesperado readBlock");
}
if (lastEepromAddr[0] != base[3] || lastEepromAddr[1] != base[4]) {
throw new Exception("Endereço retorno difere do solicitado");
}
return lastData;
}
private byte[] readContinuousEeprom(int startAddress, int bytesToRead, int btBlockSize) throws Exception {
ByteBuffer all = ByteBuffer.allocate(bytesToRead);
int addr = startAddress;
int remaining = bytesToRead;
while (remaining > 0) {
int next = Math.min(remaining, btBlockSize);
byte[] block = readBlockEeprom(addr, next);
all.put(block);
addr += next;
remaining -= next;
}
return all.array();
}
private void parseAndEmitLastRecord(byte[] allBytes) {
// Percorre de trás pra frente para achar último válido (não 0xFF)
for (int offset = allBytes.length - RECORD_SIZE; offset >=0; offset -= RECORD_SIZE) {
boolean allFF = true;
for (int i=0;i<RECORD_SIZE;i++) { if ((allBytes[offset + i] & 0xFF) != 0xFF) { allFF = false; break; } }
if (allFF) continue;
byte[] rec = new byte[RECORD_SIZE];
System.arraycopy(allBytes, offset, rec, 0, RECORD_SIZE);
try {
Record r = parseRecord(rec);
if (callback != null) {
callback.onMeasurement(r.sys, r.dia, r.bpm, r.dateIso);
}
} catch (Exception e) {
if (callback != null) callback.onError("Falha parse registro: " + e.getMessage());
}
return;
}
if (callback != null) callback.onError("Nenhum registro válido encontrado");
}
private static class Record { int sys; int dia; int bpm; String dateIso; }
private Record parseRecord(byte[] rec) throws Exception {
// Endianness big (script) - vamos interpretar como int grande
ByteBuffer bb = ByteBuffer.wrap(rec).order(ByteOrder.BIG_ENDIAN);
long bigInt = 0;
for (int i=0;i<rec.length;i++) { bigInt = (bigInt << 8) | (rec[i] & 0xFF); }
Record r = new Record();
r.dia = getBits(bigInt, 0,7);
r.sys = getBits(bigInt,8,15) + 25; // ajuste script
int year = getBits(bigInt,18,23) + 2000;
r.bpm = getBits(bigInt,24,31);
// ihb bit 32, mov bit 33 (ignorados nesta MVP)
int month = getBits(bigInt,34,37);
int day = getBits(bigInt,38,42);
int hour = getBits(bigInt,43,47);
int minute = getBits(bigInt,52,57);
int second = getBits(bigInt,58,63);
if (second > 59) second = 59;
try {
Date d = new Date(year-1900, month-1, day, hour, minute, second); // Date legacy
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
r.dateIso = sdf.format(d);
} catch (Exception e) {
r.dateIso = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).format(new Date());
}
return r;
}
private int getBits(long bigInt, int firstBit, int lastBit) {
int numBits = (lastBit - firstBit) + 1;
long shifted = bigInt >> (recBitsLen() - (lastBit + 1));
long mask = (1L << numBits) - 1L;
return (int)(shifted & mask);
}
private int recBitsLen() { return RECORD_SIZE * 8; }
private byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
private void enableAllNotifications(BluetoothGatt gatt, BluetoothGattService service) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
if (callback != null) callback.onError("Permissões de Bluetooth não concedidas");
return;
}
pendingCccdWrites = 0;
cccdQueue.clear();
processingCccd = false;
for (UUID rxUuid : RX_CHANNEL_UUIDS) {
BluetoothGattCharacteristic ch = service.getCharacteristic(rxUuid);
if (ch == null) {
Log.w(TAG, "Característica RX ausente uuid=" + rxUuid);
continue;
}
gatt.setCharacteristicNotification(ch, true);
BluetoothGattDescriptor cccd = ch.getDescriptor(CLIENT_CHAR_CONFIG);
if (cccd != null) {
// 🔹 Agora força NOTIFY sempre
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
cccdQueue.add(cccd);
pendingCccdWrites++;
Log.d(TAG, "CCCD set NOTIFY para uuid=" + ch.getUuid());
} else {
Log.w(TAG, "Sem descriptor CCCD para RX uuid=" + ch.getUuid());
}
}
if (pendingCccdWrites == 0) {
Log.w(TAG, "Nenhum CCCD escrito (talvez não seja necessário) continuando...");
if (!sessionStarted && afterNotificationsEnabled != null) {
sessionStarted = true;
afterNotificationsEnabled.run();
}
return;
}
processNextCccd(gatt);
}
/**
Envia a chave de desbloqueio para o Omron HEM-6232T.
O unlock é feito com write direto na characteristic b305b680.
A resposta (0x81xx) não vem pelo próprio unlockCh, mas sim nos canais de notify/indicate.
*/
private boolean unlockWithKey() {
if (gatt == null) return false;
BluetoothGattService omronService = gatt.getService(UUID.fromString("ecbe3980-c9a2-11e1-b1bd-0002a5d5c51b"));
if (omronService == null) {
Log.w(TAG, "Serviço Omron não encontrado para unlock");
return false;
}
BluetoothGattCharacteristic unlockCh =
omronService.getCharacteristic(UUID.fromString("b305b680-aee7-11e1-a730-0002a5d5c51b"));
if (unlockCh == null) {
Log.w(TAG, "Characteristic unlock não encontrada");
return false;
}
try {
// Apenas habilita notificações locais (sem tentar CCCD, não é suportado nesse char)
//gatt.setCharacteristicNotification(unlockCh, true);
} catch (Exception e) {
Log.e(TAG, "Falha no unlockWithKey: " + e.getMessage(), e);
return false;
}
}
// Hook de callback para finalização das escritas de CCCD
// Requer override onDescriptorWrite (adicionar abaixo)
private void onCccdWritten() {
if (pendingCccdWrites > 0) pendingCccdWrites--;
if (pendingCccdWrites == 0 && !sessionStarted) {
Log.d(TAG, "Todas notificações habilitadas");
sessionStarted = true;
if (afterNotificationsEnabled != null) afterNotificationsEnabled.run();
}
}
// Adicionar override
private final BluetoothGattCallback gattCallbackOriginal = null; // placeholder não usado
// Extender callback para descriptor write
// (inserido após declaração original; manteríamos em refatoração futura)
// Para simplicidade, adicionamos método dentro da classe (para quando mover callback separar).
// NOTA: Implementação: modificar callback acima seria ideal, mas para patch incremental usamos reflection do próprio callback? Aqui realmente precisamos editar callback original acima - já feito.
// Precisamos adicionar onDescriptorWrite dentro do callback original - refator não trivial agora.
/**
Tenta startTransmission direto; se falhar, tenta unlock e repete.
*/
private void attemptStartTransmissionWithRetry() throws Exception {
int attempts = 0;
Exception last = null;
// Tenta até 3 vezes sem unlock
while (attempts < 3) {
attempts++;
try {
startTransmission();
Log.d(TAG, "startTransmission OK tentativa=" + attempts + " sem unlock");
return;
} catch (Exception e) {
last = e;
Log.w(TAG, "startTransmission tentativa=" + attempts + " falhou: " + e.getMessage());
}
Log.w(TAG, "Todas tentativas sem unlock falharam: " +
(last != null ? last.getMessage() : "desconhecido") +
" - tentando unlock...");
boolean unlocked = unlockWithKey();
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
try {
startTransmission();
Log.d(TAG, "startTransmission OK após unlock (unlocked=" + unlocked + ")");
} catch (Exception e2) {
Log.w(TAG, "startTransmission ainda falhou após unlock: " + e2.getMessage());
}
}
/**
*/
private Date readDeviceTime() throws Exception {
int address = SETTINGS_READ_ADDRESS + TIME_SYNC_OFFSET; // 0x0260 + 0x14 = 0x0274
byte[] timeBlock = readBlockEeprom(address, TIME_SYNC_LENGTH);
if (timeBlock == null || timeBlock.length < TIME_SYNC_LENGTH) throw new Exception("Bloco horário incompleto");
int month = timeBlock[2] & 0xFF;
int year = (timeBlock[3] & 0xFF) + 2000;
int hour = timeBlock[4] & 0xFF;
int day = timeBlock[5] & 0xFF;
int second = timeBlock[6] & 0xFF; if (second > 59) second = 59;
int minute = timeBlock[7] & 0xFF;
try {
return new Date(year - 1900, month - 1, day, hour, minute, second);
} catch (Exception e) {
throw new Exception("Data inválida lida do device");
}
}
private void logDeviceTimeSkew() throws Exception {
Date deviceDate = readDeviceTime();
long skewMs = Math.abs(System.currentTimeMillis() - deviceDate.getTime());
long skewMin = skewMs / 60000L;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
if (skewMin > 5) {
Log.w(TAG, "Diferença de hora device vs sistema >5min (" + skewMin + "m) device=" + sdf.format(deviceDate));
} else {
Log.d(TAG, "Hora device OK (skew=" + skewMin + "m) device=" + sdf.format(deviceDate));
}
}
private void processNextCccd(BluetoothGatt gatt) {
if (processingCccd) return;
if (cccdQueue.isEmpty()) return;
processingCccd = true;
BluetoothGattDescriptor next = cccdQueue.remove(0);
next.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
boolean started = gatt.writeDescriptor(next);
Log.d(TAG, "Fila CCCD -> write " + next.getCharacteristic().getUuid() + " started=" + started + (indicationsFallbackTried?" (INDICATION)":""));
if (!started) {
processingCccd = false;
if (!cccdQueue.isEmpty()) processNextCccd(gatt);
}
}
private void reEnableNotificationsWithIndications() {
if (gatt == null) return;
BluetoothGattService service = gatt.getService(PARENT_SERVICE);
if (service == null) return;
pendingCccdWrites = 0;
cccdQueue.clear();
for (UUID rxUuid : RX_CHANNEL_UUIDS) {
BluetoothGattCharacteristic ch = service.getCharacteristic(rxUuid);
if (ch == null) continue;
gatt.setCharacteristicNotification(ch, true);
BluetoothGattDescriptor cccd = ch.getDescriptor(CLIENT_CHAR_CONFIG);
if (cccd != null) {
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
cccdQueue.add(cccd);
pendingCccdWrites++;
}
}
processNextCccd(gatt);
}
private String bytesToHex(byte[] data) {
if (data == null) return "null";
StringBuilder sb = new StringBuilder();
for (byte b : data) sb.append(String.format(Locale.US, "%02X", b));
return sb.toString();
}
private boolean isRxUuid(UUID u) {
for (UUID rx : RX_CHANNEL_UUIDS) {
if (rx.equals(u)) return true;
}
return false;
}
}
`