Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ This is most important when you are trying to add support for a new device.
| [HEM-7322T](deviceSpecific/hem-7322t.py) | M700 Intelli IT | ✔️ | ✔️ | ✔️ | ✔️ | userx14 |
| [HEM-7342T](deviceSpecific/hem-7342t.py) | BP7450 | ✔️ | ✔️ | ❓ | ❓ | Toei79, userx14 |
| [HEM-7361T](deviceSpecific/hem-7361t.py) | M500 Intelli IT / M7 Intelli IT | ✔️ | ✔️ | ✔️ | ✔️ | LazyT, userx14, zivanfi, RobertWojtowicz |
| [HEM-7380T1](deviceSpecific/hem-7380t1.py) | X7 Smart AFib / M7 Intelli IT AFib / EOSL / EBK | n/a | ✔️ | ❌ | ❌ | thiagoko |
| [HEM-7530T](deviceSpecific/hem-7530t.py) | Omron Complete | ✔️ | ✔️ (no EKG) | ❌ | ❌ | Toei79, userx14 |
| [HEM-7600T](deviceSpecific/hem-7600t.py) | Omron Evolv | ✔️ | ✔️ | ✔️ | ✔️ | vulcainman |
| [HEM-6232T](deviceSpecific/hem-6232T.py) | RS7 Intelli IT | ✔️ | ✔️ | ❓ | ❓ | invertedburger |
Expand Down
64 changes: 64 additions & 0 deletions deviceSpecific/hem-7380t1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import datetime
import logging
import sys

logger = logging.getLogger("omblepy")

sys.path.append('..')
from sharedDriver import sharedDeviceDriverCode


class deviceSpecificDriver(sharedDeviceDriverCode):
parentService_UUID = "0000fe4a-0000-1000-8000-00805f9b34fb"
deviceRxChannelUUIDs = ["49123040-aee8-11e1-a74d-0002a5d5c51b"]
deviceTxChannelUUIDs = ["db5b55e0-aee7-11e1-965e-0002a5d5c51b"]
requiresUnlock = False
supportsPairing = False
supportsOsBondingOnly = True

deviceEndianess = "little"
userStartAdressesList = [0x01C4, 0x0804]
perUserRecordsCountList = [100, 100]
recordByteSize = 0x10
transmissionBlockSize = 0x38

settingsReadAddress = None
settingsWriteAddress = None
settingsUnreadRecordsBytes = None
settingsTimeSyncBytes = None

def deviceSpecific_ParseRecordFormat(self, singleRecordAsByteArray):
rawSys = singleRecordAsByteArray[0]
if rawSys > 0xE1:
raise ValueError("record slot is empty")

recordDict = dict()
recordDict["sys"] = rawSys + 25
recordDict["dia"] = singleRecordAsByteArray[1]
recordDict["bpm"] = singleRecordAsByteArray[2]

year = 2000 + (singleRecordAsByteArray[3] & 0x3F)
flags1 = singleRecordAsByteArray[4] | (singleRecordAsByteArray[5] << 8)
flags2 = singleRecordAsByteArray[6] | (singleRecordAsByteArray[7] << 8)

recordDict["hour"] = flags1 & 0x1F
day = (flags1 >> 5) & 0x1F
month = (flags1 >> 10) & 0x0F
recordDict["ihb"] = (flags1 >> 14) & 0x01
recordDict["mov"] = (flags1 >> 15) & 0x01
second = min(flags2 & 0x3F, 59)
minute = (flags2 >> 6) & 0x3F

recordDict["datetime"] = datetime.datetime(
year,
month,
day,
recordDict["hour"],
minute,
second,
)
del recordDict["hour"]
return recordDict

def deviceSpecific_syncWithSystemTime(self):
raise ValueError("not supported")
134 changes: 90 additions & 44 deletions omblepy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,56 @@
import csv
import json

#global constants
parentService_UUID = "ecbe3980-c9a2-11e1-b1bd-0002a5d5c51b"

#global variables
bleClient = None
pairingKey = bytearray.fromhex("deadbeaf12341234deadbeaf12341234") #arbitrary choice
deviceSpecific = None #imported module for each device
logger = logging.getLogger("omblepy")

LEGACY_PARENT_SERVICE_UUID = "ecbe3980-c9a2-11e1-b1bd-0002a5d5c51b"
LEGACY_DEVICE_RX_CHANNEL_UUIDS = [
"49123040-aee8-11e1-a74d-0002a5d5c51b",
"4d0bf320-aee8-11e1-a0d9-0002a5d5c51b",
"5128ce60-aee8-11e1-b84b-0002a5d5c51b",
"560f1420-aee8-11e1-8184-0002a5d5c51b",
]
LEGACY_DEVICE_TX_CHANNEL_UUIDS = [
"db5b55e0-aee7-11e1-965e-0002a5d5c51b",
"e0b8a060-aee7-11e1-92f4-0002a5d5c51b",
"0ae12b00-aee8-11e1-a192-0002a5d5c51b",
"10e1ba60-aee8-11e1-89e5-0002a5d5c51b",
]
LEGACY_DEVICE_UNLOCK_UUID = "b305b680-aee7-11e1-a730-0002a5d5c51b"

def convertByteArrayToHexString(array):
return (bytes(array).hex())


class bluetoothTxRxHandler:
#BTLE Characteristic IDs
deviceRxChannelUUIDs = [
"49123040-aee8-11e1-a74d-0002a5d5c51b",
"4d0bf320-aee8-11e1-a0d9-0002a5d5c51b",
"5128ce60-aee8-11e1-b84b-0002a5d5c51b",
"560f1420-aee8-11e1-8184-0002a5d5c51b"
]
deviceTxChannelUUIDs = [
"db5b55e0-aee7-11e1-965e-0002a5d5c51b",
"e0b8a060-aee7-11e1-92f4-0002a5d5c51b",
"0ae12b00-aee8-11e1-a192-0002a5d5c51b",
"10e1ba60-aee8-11e1-89e5-0002a5d5c51b"
]
deviceDataRxChannelIntHandles = [0x360, 0x370, 0x380, 0x390]
deviceUnlock_UUID = "b305b680-aee7-11e1-a730-0002a5d5c51b"

def __init__(self, pairing = False):
def __init__(self, deviceDriver):
self.deviceRxChannelUUIDs = getattr(deviceDriver, "deviceRxChannelUUIDs", LEGACY_DEVICE_RX_CHANNEL_UUIDS)
self.deviceTxChannelUUIDs = getattr(deviceDriver, "deviceTxChannelUUIDs", LEGACY_DEVICE_TX_CHANNEL_UUIDS)
self.deviceUnlock_UUID = getattr(deviceDriver, "deviceUnlock_UUID", LEGACY_DEVICE_UNLOCK_UUID)
self.requiresUnlock = getattr(deviceDriver, "requiresUnlock", True)
self.supportsPairing = getattr(deviceDriver, "supportsPairing", True)
self.currentRxNotifyStateFlag = False
self.rxPacketType = None
self.rxEepromAddress = None
self.rxDataBytes = None
self.rxFinishedFlag = False
self.rxRawChannelBuffer = [None] * 4 #a buffer for each channel
self.rxHandleToChannelId = dict()

def _buildRxHandleMap(self):
self.rxHandleToChannelId = dict()
for channelIdx, rxChannelUUID in enumerate(self.deviceRxChannelUUIDs):
characteristic = bleClient.services.get_characteristic(rxChannelUUID)
if characteristic is not None:
self.rxHandleToChannelId[characteristic.handle] = channelIdx

async def _enableRxChannelNotifyAndCallback(self):
if(self.currentRxNotifyStateFlag != True):
self._buildRxHandleMap()
for rxChannelUUID in self.deviceRxChannelUUIDs:
await bleClient.start_notify(rxChannelUUID, self._callbackForRxChannels)
self.currentRxNotifyStateFlag = True
Expand All @@ -61,26 +71,33 @@ async def _disableRxChannelNotifyAndCallback(self):
self.currentRxNotifyStateFlag = False

def _callbackForRxChannels(self, BleakGATTChar, rxBytes):
if type(BleakGATTChar) is int:
rxChannelId = self.deviceDataRxChannelIntHandles.index(BleakGATTChar)
if len(self.deviceRxChannelUUIDs) == 1:
rxChannelId = 0
elif type(BleakGATTChar) is int:
rxChannelId = self.rxHandleToChannelId[BleakGATTChar]
else:
rxChannelId = self.deviceDataRxChannelIntHandles.index(BleakGATTChar.handle)
rxChannelId = self.rxHandleToChannelId[BleakGATTChar.handle]
self.rxRawChannelBuffer[rxChannelId] = rxBytes

logger.debug(f"rx ch{rxChannelId} < {convertByteArrayToHexString(rxBytes)}")
if self.rxRawChannelBuffer[0]: #if there is data present in the first rx buffer
packetSize = self.rxRawChannelBuffer[0][0]
requiredChannels = range((packetSize + 15) // 16)
#are all required channels already recieved
for channelIdx in requiredChannels:
if self.rxRawChannelBuffer[channelIdx] is None: #if one of the required channels is empty wait for more packets to arrive
return

#check crc
combinedRawRx = bytearray()
for channelIdx in requiredChannels:
combinedRawRx += self.rxRawChannelBuffer[channelIdx]
combinedRawRx = combinedRawRx[:packetSize] #cut extra bytes from the end
if len(self.deviceRxChannelUUIDs) == 1:
combinedRawRx = bytearray(self.rxRawChannelBuffer[0])
self.rxRawChannelBuffer = [None] * 4
else:
packetSize = self.rxRawChannelBuffer[0][0]
requiredChannels = range((packetSize + 15) // 16)
#are all required channels already recieved
for channelIdx in requiredChannels:
if self.rxRawChannelBuffer[channelIdx] is None: #if one of the required channels is empty wait for more packets to arrive
return

#check crc
combinedRawRx = bytearray()
for channelIdx in requiredChannels:
combinedRawRx += self.rxRawChannelBuffer[channelIdx]
combinedRawRx = combinedRawRx[:packetSize] #cut extra bytes from the end
self.rxRawChannelBuffer = [None] * 4 #clear channel buffers
xorCrc = 0
for byte in combinedRawRx:
xorCrc ^= byte
Expand All @@ -97,7 +114,6 @@ def _callbackForRxChannels(self, BleakGATTChar, rxBytes):
self.rxDataBytes = combinedRawRx[6:7]
else:
self.rxDataBytes = combinedRawRx[6: 6 + expectedNumDataBytes]
self.rxRawChannelBuffer = [None] * 4 #clear channel buffers
self.rxFinishedFlag = True
return
return
Expand All @@ -107,11 +123,18 @@ async def _waitForRxOrRetry(self, command, timeoutS = 1.0):
retries = 0
while True:
commandCopy = command
requiredTxChannels = range((len(command) + 15) // 16)
channelWidth = 16
if len(self.deviceTxChannelUUIDs) == 1:
channelWidth = max(channelWidth, len(command))
requiredTxChannels = range((len(command) + channelWidth - 1) // channelWidth)
for channelIdx in requiredTxChannels:
logger.debug(f"tx ch{channelIdx} > {convertByteArrayToHexString(commandCopy[:16])}")
await bleClient.write_gatt_char(self.deviceTxChannelUUIDs[channelIdx], commandCopy[:16])
commandCopy = commandCopy[16:]
txChunk = commandCopy[:channelWidth]
logger.debug(f"tx ch{channelIdx} > {convertByteArrayToHexString(txChunk)}")
if len(self.deviceTxChannelUUIDs) == 1:
await bleClient.write_gatt_char(self.deviceTxChannelUUIDs[channelIdx], txChunk, response=False)
else:
await bleClient.write_gatt_char(self.deviceTxChannelUUIDs[channelIdx], txChunk)
commandCopy = commandCopy[channelWidth:]

currentTimeout = timeoutS
while(self.rxFinishedFlag == False):
Expand Down Expand Up @@ -204,6 +227,8 @@ def _callbackForUnlockChannel(self, UUID_or_intHandle, rxBytes):
return

async def writeNewUnlockKey(self, newKeyByteArray = pairingKey):
if not self.supportsPairing:
raise ValueError("Pairing mode is not supported for this device in omblepy.")
if(len(newKeyByteArray) != 16):
raise ValueError(f"key has to be 16 bytes long, is {len(newKeyByteArray)}")

Expand Down Expand Up @@ -245,6 +270,8 @@ async def writeNewUnlockKey(self, newKeyByteArray = pairingKey):
return

async def unlockWithUnlockKey(self, keyByteArray = pairingKey):
if not self.requiresUnlock:
return
await bleClient.start_notify(self.deviceUnlock_UUID, self._callbackForUnlockChannel)
self.rxFinishedFlag = False
await bleClient.write_gatt_char(self.deviceUnlock_UUID, b'\x01' + keyByteArray, response=True)
Expand Down Expand Up @@ -317,6 +344,7 @@ async def selectBLEdevices():
async def main():
global bleClient
global deviceSpecific
devSpecificDriver = None
parser = argparse.ArgumentParser(description="python tool to read the records of omron blood pressure instruments")
parser.add_argument('-d', "--device", required="true", type=ascii, help="Device name (e.g. hem-7322t, see deviceSpecific folder)")
parser.add_argument("--loggerDebug", action="store_true", help="Enable verbose logger output")
Expand Down Expand Up @@ -353,8 +381,13 @@ async def main():
try:
logger.info(f"Attempt to import module for device {deviceName.lower()}")
deviceSpecific = __import__(deviceName.lower())
devSpecificDriver = deviceSpecific.deviceSpecificDriver()
except ImportError:
raise ValueError("the device is no supported yet, you can help by contributing :)")
supportsPairing = getattr(devSpecificDriver, "supportsPairing", True)
supportsOsBondingOnly = getattr(devSpecificDriver, "supportsOsBondingOnly", False)
if(args.pair and not supportsPairing and not supportsOsBondingOnly):
raise ValueError(f"{deviceName} does not support pairing in omblepy.")

#select device mac address
validMacRegex = re.compile(r"^([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})$")
Expand All @@ -375,21 +408,34 @@ async def main():
try:
logger.info(f"Attempt connecting to {bleAddr}.")
await bleClient.connect()
await asyncio.sleep(0.5)
if(args.pair and getattr(devSpecificDriver, "supportsOsBondingOnly", False)):
logger.info("Requesting OS-level BLE bonding for this device.")
try:
await bleClient.pair()
except TypeError:
await bleClient.pair(protection_level=2)
logger.info("OS-level BLE bonding request completed.")
return
servicesResolved = False
parentServiceUUID = getattr(devSpecificDriver, "parentService_UUID", LEGACY_PARENT_SERVICE_UUID)
for _ in range(20):
await asyncio.sleep(0.25)
if parentServiceUUID in [service.uuid for service in bleClient.services]:
servicesResolved = True
break
#verify that the device is an omron device by checking presence of certain bluetooth services
if parentService_UUID not in [service.uuid for service in bleClient.services]:
if not servicesResolved:
raise OSError("""Some required bluetooth attributes not found on this ble device.
This means that either, you connected to a wrong device,
or that your OS has a bug when reading BT LE device attributes (certain linux versions).""")
bluetoothTxRxObj = bluetoothTxRxHandler()
bluetoothTxRxObj = bluetoothTxRxHandler(devSpecificDriver)
if(args.pair):
await bluetoothTxRxObj.writeNewUnlockKey()
#this seems to be necessary when the device has not been paired to any device
await bluetoothTxRxObj.startTransmission()
await bluetoothTxRxObj.endTransmission()
else:
logger.info("communication started")
devSpecificDriver = deviceSpecific.deviceSpecificDriver()
allRecs = await devSpecificDriver.getRecords(btobj = bluetoothTxRxObj, useUnreadCounter = args.newRecOnly, syncTime = args.timeSync)
logger.info("communication finished")
appendCsv(allRecs)
Expand Down
4 changes: 2 additions & 2 deletions sharedDriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ def resetUnreadRecordsCounter(self):
async def getRecords(self, btobj, useUnreadCounter, syncTime):
await btobj.unlockWithUnlockKey()
await btobj.startTransmission()

#cache settings for time sync and for unread record counter

if(syncTime or useUnreadCounter):
#initialize cached settings bytes with zeros and use bytearray so that the values are mutable
self.cachedSettingsBytes = bytearray(b'\0' * (self.settingsWriteAddress - self.settingsReadAddress))
Expand Down