Implementing Streaming Data from Medical IoT Devices
Medical IoT devices—portable ECGs (AliveCor KardiaMobile, Holter monitors), pulse oximeters (Nonin, Masimo), BLE glucometers (Abbott Libre, Dexcom G7), blood pressure monitors (Omron, Withings)—operate on standard Bluetooth LE profiles from Bluetooth SIG or proprietary protocols. Developing a mobile client for such devices sits at the intersection of real-time raw signal streaming, clinical-grade processing, and regulatory compliance (FDA 21 CFR Part 11, EU MDR, Russian Roszdravnadzor).
We are a team with 5 years of experience in medical IoT development. During this time we have implemented streaming for 15+ device types, including multi-channel ECGs and CGM sensors. Our solutions are certified and have passed vendor audits (Abbott, Masimo). We guarantee compliance with standards and data confidentiality (GDPR, 152-FZ). We evaluate your project for free—contact us.
Why Streaming from BLE Medical Devices Is Technically Challenging
BLE is not TCP/IP. Narrow MTU (default 23 bytes), frequent connection interruptions, binary protocols with non-standard number formats (IEEE-11073 SFLOAT, 24-bit signed). Moreover, medical data demands high reliability: a single lost packet can distort the clinical picture. Our experience addresses these issues at the design stage—we use circular buffers, retransmission mechanisms, and proprietary profiles with CRC.
Standard Medical GATT Profiles
For compatible devices, Bluetooth SIG defines the following profiles:
| Profile | UUID | Device |
|---|---|---|
| Health Thermometer (HTP) | 0x1809 |
Thermometers |
| Blood Pressure (BLP) | 0x1810 |
Blood pressure monitors |
| Pulse Oximeter (PLX) | 0x1822 |
Pulse oximeters |
| Glucose Profile (GLP) | 0x1808 |
Glucometers |
| Continuous Glucose (CGP) | 0x181F |
CGM sensors (Libre, Dexcom) |
| ECG Profile | 0x1843 |
ECG devices |
Example parsing of Blood Pressure Measurement (UUID 0x2A35):
func parseBloodPressure(_ data: Data) -> BloodPressureReading {
var offset = 0
let flags = data[offset]; offset += 1
let isMMHg = (flags & 0x01) == 0
let timestampPresent = (flags & 0x02) != 0
let pulseRatePresent = (flags & 0x04) != 0
let systolic = parseSFloat(high: data[offset + 1], low: data[offset])
offset += 2
let diastolic = parseSFloat(high: data[offset + 1], low: data[offset])
offset += 2
let map = parseSFloat(high: data[offset + 1], low: data[offset])
offset += 2
return BloodPressureReading(systolic: systolic, diastolic: diastolic,
meanArterialPressure: map, inMMHg: isMMHg)
}
func parseSFloat(high: UInt8, low: UInt8) -> Double {
let rawValue = Int16(high) << 8 | Int16(low)
let exponent = Int(rawValue >> 12)
let mantissa = Int(rawValue & 0x0FFF)
let signedMantissa = mantissa > 0x07FF ? mantissa - 0x1000 : mantissa
return Double(signedMantissa) * pow(10.0, Double(exponent))
}
IEEE-11073 SFLOAT is not a regular float32 or int16 in 0.1 units. Confusion here leads to systolic pressure readings like "1270 mmHg" on screen—a classic mistake we prevent at the architecture stage.
ECG Streaming: Buffer and MTU
Portable ECGs are the most demanding. AliveCor KardiaMobile 6L outputs 12-lead ECG at 300 sps. Through standard BLE Notify (MTU 23 bytes = 20 bytes payload), throughput barely handles 1-lead ECG at 250 sps. For multi-lead, you must negotiate MTU 247+ bytes:
gatt.requestMtu(247)
// One ECG packet: timestamp(4) + 12 channels * 3 bytes = 40 bytes
// At MTU 247: ~5 frames per notification = 250 sps * 12 channels = 3000 values/sec
data class EcgPacket(
val timestamp: Long,
val samples: Array<IntArray>, // [channel][sample], signed 24-bit
)
fun parseEcgNotification(data: ByteArray): EcgPacket {
var offset = 0
val timestamp = ByteBuffer.wrap(data, offset, 4).int.also { offset += 4 }
val samples = Array(12) { IntArray(data.size / 36) } // 12 channels
var sampleIdx = 0
while (offset + 36 <= data.size) {
for (ch in 0..11) {
// 24-bit signed little-endian
val raw = (data[offset].toInt() and 0xFF) or
((data[offset + 1].toInt() and 0xFF) shl 8) or
((data[offset + 2].toInt()) shl 16)
samples[ch][sampleIdx] = raw
offset += 3
}
sampleIdx++
}
return EcgPacket(timestamp, samples)
}
24-bit signed is used because 16-bit is insufficient for clinical ECG amplitude (range ±5 mV at 1 µV resolution = 10,000 levels, need at least 14 bits; 24 bits are clinical standard).
Buffer and Signal Rendering
Streaming ECG on screen demands strict memory and FPS requirements. A circular buffer for 10 seconds at 300 sps = 3000 points per channel = 36,000 values for 12 channels. We use FloatArray to avoid object allocation in the rendering thread.
On Android—custom View with Canvas, draw using Paint.setPathEffect(null) and accumulate Path. On iOS—CALayer + Core Graphics or Metal for high load. ChartsUI and MPAndroidChart are unsuitable for ECG as they don't handle continuous append in hot path.
class EcgView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: View(context, attrs) {
private val buffer = CircularFloatBuffer(capacity = 3000)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.GREEN
strokeWidth = 1.5f
style = Paint.Style.STROKE
}
fun appendSamples(samples: FloatArray) {
buffer.append(samples)
invalidate()
}
override fun onDraw(canvas: Canvas) {
val data = buffer.snapshot()
val path = Path()
val scaleX = width.toFloat() / data.size
val scaleY = height / 2f
data.forEachIndexed { i, value ->
val x = i * scaleX
val y = scaleY - value * scaleY / MAX_AMPLITUDE
if (i == 0) path.moveTo(x, y) else path.lineTo(x, y)
}
canvas.drawPath(path, paint)
}
}
invalidate() without postInvalidateOnAnimation()—for minimal latency. Vsync naturally caps at 60/120 FPS.
Storage and Transmission: FHIR and GDPR
Medical data is high-sensitivity personal data. On-device encryption via Android Keystore / iOS Data Protection (class NSFileProtectionComplete). Server transmission over TLS 1.2+, preferably mTLS.
For integration with medical information systems (MIS, HL7)—format data in FHIR R4: Observation resource for measurements, DiagnosticReport for ECG reports. Apple HealthKit stores data in FHIR-compatible format since iOS 12.
func saveBloodPressure(_ reading: BloodPressureReading) async throws {
let systolicType = HKQuantityType(.bloodPressureSystolic)
let diastolicType = HKQuantityType(.bloodPressureDiastolic)
let mmHg = HKUnit.millimeterOfMercury()
let systolicSample = HKQuantitySample(type: systolicType,
quantity: HKQuantity(unit: mmHg, doubleValue: reading.systolic),
start: reading.timestamp, end: reading.timestamp)
let diastolicSample = HKQuantitySample(type: diastolicType,
quantity: HKQuantity(unit: mmHg, doubleValue: reading.diastolic),
start: reading.timestamp, end: reading.timestamp)
try await healthStore.save([systolicSample, diastolicSample])
}
Regulatory Requirements and Constraints
If the app qualifies as a medical device (provides diagnosis, recommends treatment)—requires registration with Roszdravnadzor (Russia) or CE MDR (Europe). A "viewer" app without clinical decisions typically falls outside regulation—but this should be decided with medical law experts before development begins.
What We Deliver in a Turnkey Development
We provide a complete package: device protocol documentation, BLE communication module source code, test bench, HealthKit/Google Fit integration, user manual, and client team training. We also assist with certification if needed.
Timelines and Costs
Development of a mobile client for a medical IoT device with streaming, clinically accurate parsing, and HealthKit integration: 12–16 weeks. Complexity increases significantly with proprietary protocols or FHIR integration. Cost is calculated individually after analyzing the device specification and regulatory context. Savings from reusable modules can reach 40% of the budget.
How We Guarantee Clinical Accuracy
We use reference parsers approved by device manufacturers. Each module undergoes stress testing (24-hour streaming without loss) and comparison with reference equipment. The result is data suitable for clinical use.
Contact Us for a Project Assessment
Submit a request via our website or write to Telegram—we will analyze your IoT device specification, propose the optimal architecture, and provide accurate timelines. Get a free consultation today.







