Skip to content

Can't sync HEM-6232T, py code conversion to java android #58

@red7ideias

Description

@red7ideias

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:

    • Abre transmissão proprietária
    • Lê bloco completo de registros do usuário 1 (100 * 14 bytes)
    • Faz parse bit a bit do último registro válido
    • Fecha transmissão
    • Notifica callback
  • 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;
    }
    }
    `

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions