This file explains how bluetooth connection with CORE Sensor work in NeutroFeverGuard.
Back to README.
Core Body Temperature Sensor a non-invasive, wearable sensor that can monitor skin and core body temperature continuously. This sensor follows BLE and ANT protocols and they have documentation on their specs here. In our code, we used Health Thermometer Service that measures skin temperature, so following code BUUIDs and characteristics might need to be customized for your use case and functionalities.
First, we need to be able to scan for Bluetooth devices, discover our CORE Sensor and connect. As you might guess, at any point of time, there are hundreds of bluetooth devices around; and we don't want to overload user with all of these devices.
We add this snipped to our delegate to initialize the bluetooth module and discover only the "advertised service", so that we return only CORE Sensor.
Bluetooth {
Discover(CoreSensor.self, by: .advertisedService("180A"))
}
Tip
Where did we learn this service ID number? We used LightBlue app and the sensor specs provided above.
We subscribed to the Health Thermometer Service(Skin Temperature Service in the code), to receive data when sensor captured any.
// This is service provided by the CORE Sensor, used spec doc and LightBlue app to learn about IDs below.
struct SkinTemperatureService: BluetoothService {
static let id: BTUUID = "1809"
@Characteristic(id: "2A1C", notify: true) var skinTemperature: Data?
}
// notify means that everytime there is new data, we will receive, we are subscribed.
The data that the sensor sends is not directly in human-readable temperature format. The specs detail that this service uses a 5-byte payload in IEEE 11073 format. So, we need to decode it to convert into temperature value. See our detailed code for decoding here, we do this when we create a Skin Temperature Measurement Type.
final class CoreSensor: BluetoothDevice, @unchecked Sendable, ObservableObject, Identifiable {
@Dependency(Measurements.self) private var measurements
@Dependency(NoMeasurementWarningState.self) private var warningState
@DeviceState(\.id) var id: UUID
@DeviceState(\.name) var name: String?
@DeviceState(\.state) var state: PeripheralState
@Service var skinTemperatureService = SkinTemperatureService()
@DeviceAction(\.connect) var connect
@DeviceAction(\.disconnect) var disconnect
required init() {}
@MainActor
func configure() {
skinTemperatureService.$skinTemperature.onChange { [weak self] skintemperature in
guard let self = self, !skintemperature.isEmpty else {
print("No skin temperature data received.")
return
}
// Debug: Print raw data in hex format
print("Raw Skin Temperature Data (Hex):", skintemperature.map { String(format: "%02X", $0) }.joined(separator: " "))
// Decode temperature from Data
if let measurement = SkinTemperatureMeasurement(from: skintemperature) {
print("Decoded Skin Temperature: \(String(format: "%.2f", measurement.temperature)) \(measurement.unit)")
await self.handleNewMeasurement(measurement)
} else {
print("No valid skin temperature detected. Sensor might be off-body or not initialized.")
await self.handleNoMeasurement()
}
}
}
@MainActor
private func handleNewMeasurement(_ measurement: SkinTemperatureMeasurement) async {
warningState.isActive = false
await measurements.recordNewMeasurement(measurement)
}
@MainActor
private func handleNoMeasurement() async {
print("No temperature detected. Sensor might be off-body or waiting for a valid reading.")
warningState.isActive = true // This will be used to trigger UI warning
}
}
As can be seen above, we also handle the cases where the Sensor is connected but the temperature values are NaN values. To see how we use these functions and update UI, see our code.
After we read and decode, next step is converting the temperature value in HealthKit-acceptable body temperature format and pushing our data to HealthKit. After pushing it to HealthKit, our background checking will read it back and push it to Firebase.
func recordNewMeasurement(_ measurement: SkinTemperatureMeasurement) async {
let timestamp = measurement.timestamp ?? Date() // if we dont get timestamp from the sensor, then we need to generate.
recordedTemperatures.append(measurement)
print("New Temperature Recorded: \(measurement.temperature) \(measurement.unit == .celsius ? "°C" : "°F") at \(timestamp)")
do {
// Request HealthKit authorization (Temporarily placed to make sure data is pushed on iphone, remove while pushing to main)
// try await healthKitService.requestAuthorization()
// Convert measurement into HealthKit-compatible TemperatureEntry
let temperatureEntry = try TemperatureEntry(
date: timestamp,
value: Double(measurement.temperature),
unit: measurement.unit == .celsius ? .celsius : .fahrenheit
)
// Save to HealthKit
try await healthKitService.saveTemperature(temperatureEntry)
print("Core Sensor Skin Temperature successfully saved to HealthKit.")
} catch let error as DataError {
print("HealthKit save error: \(error.errorMessage)")
} catch {
print("Unexpected error saving to HealthKit: \(error)")
}
}
Tip
We built these functions on top of SpeziBluetooth. In addition our code and documentation, check out their documentation.
Now you know how bluetooth connection with CORE Sensor work in NeutroFeverGuard! Welcome back to README.
Before you go here is a:
Note
The technology, "Bluetooth," was named after King Harald "Bluetooth" Gormsson, who united Denmark and Norway in the 10th century, with the name chosen to reflect the technology's goal of uniting diverse communication protocols. The Bluetooth logo is a bind rune that merges the Younger Futhark runes H (ᚼ) and B (ᛒ), which represent Harald's initials.