Skip to content

Commit

Permalink
Merge pull request #13 from combustion-inc/develop
Browse files Browse the repository at this point in the history
Release v0.8.0
  • Loading branch information
jjohnstz authored Apr 8, 2022
2 parents 4251d5f + b27f699 commit 680b20f
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 104 deletions.
17 changes: 5 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ An instance of the `Probe` class representes an individual temperature probe tha
- `name` - String format of probe serial number
- `macAddress` - Probe's MAC address
- `macAddressString` - String representation of Probe's MAC address
- `id` - Probe's numeric ID (1-8)
- `color` - Probe's silicone ring color
- `batteryLevel` - Battery level as reported by probe *NOTE: This is not yet implemented in probe firmware and will likely change to a boolean 'battery low' flag in the near future.*
- `currentTemperatures` - `ProbeTemperatures` struct containing the most recent temperatures read by the Probe.
- `currentTemperatures.values` - Array of these temperatures, in celsius, where `values[0]` is temperature sensor T1, and `values[7]` is temperature sensor T8.
Expand All @@ -90,7 +92,7 @@ An instance of the `Probe` class representes an individual temperature probe tha
- T7 - High-temperature thermistor
- T8 - High-temperature thermistor on handle tip measuring ambient

- `id` - iOS-provided UUID for the device
- `identifier` - iOS-provided UUID for the device

- `rssi` - Signal strength between Probe and iOS device

Expand All @@ -102,9 +104,8 @@ An instance of the `Probe` class representes an individual temperature probe tha

- `stale` - `true` if no advertising data or notifications have been received from the Probe within the "staleness timeout" (15 seconds), or `false` if recent data has been received.

- `status` - `DeviceStatus` struct containing device status information.
- `minSequenceNumber` - Minimum sequence number of log records stored on the probe
- `maxSequenceNumber` - Maximum sequence number of log records stored on the probe
- `minSequenceNumber` - Minimum sequence number of log records stored on the probe
- `maxSequenceNumber` - Maximum sequence number of log records stored on the probe

- `logsUpToDate` - Boolean value that indicates whether all log sequence numbers contained in the probe (determined by the `status` sequence number range) have been successfully retrieved and stored in the app's memory.

Expand Down Expand Up @@ -154,14 +155,6 @@ struct EngineeringProbeList: View {

The following features are planned for near-term development but are not yet implemented in this version of the Combustion BLE Framework.

### Set ring color

The framework will provide functions allowing a probe's identifying silicone ring color to be configured by the user (colors TBA).

### Set numeric ID

The framework will provide functions allowing a Probe's numeric ID (1-8) to be configured by the user.

### Firmware update

The framework will provide methods for updating a Probe's firmware with a signed firmware image.
Expand Down
70 changes: 69 additions & 1 deletion Sources/CombustionBLE/AdvertisingData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,54 @@ public enum CombustionProductType: UInt8 {
case NODE = 0x02
}

/// Probe colors
public enum ProbeColor: UInt8, CaseIterable {
case COLOR1 = 0x00
case COLOR2 = 0x01
case COLOR3 = 0x02
case COLOR4 = 0x03
case COLOR5 = 0x04
case COLOR6 = 0x05
case COLOR7 = 0x06
case COLOR8 = 0x07

private enum Constants {
static let PRODE_COLOR_MASK: UInt8 = 0x7
static let PRODE_COLOR_SHIFT: UInt8 = 2
}

static func fromRawData(data: Data) -> ProbeColor {
let modeIdColorBytes = [UInt8](data)

let rawProbeColor = (modeIdColorBytes[0] & (Constants.PRODE_COLOR_MASK << Constants.PRODE_COLOR_SHIFT)) >> Constants.PRODE_COLOR_SHIFT
return ProbeColor(rawValue: rawProbeColor) ?? .COLOR1
}
}

/// Probe IDs
public enum ProbeID: UInt8, CaseIterable {
case ID1 = 0x00
case ID2 = 0x01
case ID3 = 0x02
case ID4 = 0x03
case ID5 = 0x04
case ID6 = 0x05
case ID7 = 0x06
case ID8 = 0x07

private enum Constants {
static let PRODE_ID_MASK: UInt8 = 0x7
static let PRODE_ID_SHIFT: UInt8 = 5
}

static func fromRawData(data: Data) -> ProbeID {
let modeIdColorBytes = [UInt8](data)

let rawProbeID = (modeIdColorBytes[0] & (Constants.PRODE_ID_MASK << Constants.PRODE_ID_SHIFT)) >> Constants.PRODE_ID_SHIFT
return ProbeID(rawValue: rawProbeID) ?? .ID1
}
}


/// Struct containing advertising data received from device.
public struct AdvertisingData {
Expand All @@ -42,19 +90,24 @@ public struct AdvertisingData {
public let serialNumber: UInt32
/// Latest temperatures read by device
public let temperatures: ProbeTemperatures
/// Prode ID
public let id: ProbeID
/// Probe Color
public let color: ProbeColor

private enum Constants {
// Locations of data in advertising packets
static let PRODUCT_TYPE_RANGE = 2..<3
static let SERIAL_RANGE = 3..<7
static let TEMPERATURE_RANGE = 7..<20
static let MODE_COLOR_ID_RANGE = 20..<21
}
}

extension AdvertisingData {
init?(fromData : Data?) {
guard let data = fromData else { return nil }
guard data.count >= 19 else { return nil }
guard data.count >= 20 else { return nil }

// Product type (1 byte)
let rawType = data.subdata(in: Constants.PRODUCT_TYPE_RANGE)
Expand All @@ -81,6 +134,17 @@ extension AdvertisingData {
// Temperatures (8 13-bit) values
let tempData = data.subdata(in: Constants.TEMPERATURE_RANGE)
temperatures = ProbeTemperatures.fromRawData(data: tempData)

// Decode Probe ID and Color if its present in the advertising packet
if(data.count >= 21) {
let modeIdColorData = data.subdata(in: Constants.MODE_COLOR_ID_RANGE)
id = ProbeID.fromRawData(data: modeIdColorData)
color = ProbeColor.fromRawData(data: modeIdColorData)
}
else {
id = .ID1
color = .COLOR1
}
}
}

Expand All @@ -91,12 +155,16 @@ extension AdvertisingData {
type = .PROBE
temperatures = ProbeTemperatures.withFakeData()
serialNumber = fakeSerial
id = .ID1
color = .COLOR1
}

// Fake data initializer for Simulated Probe
public init(fakeSerial: UInt32, fakeTemperatures: ProbeTemperatures) {
type = .PROBE
temperatures = fakeTemperatures
serialNumber = fakeSerial
id = .ID1
color = .COLOR1
}
}
77 changes: 41 additions & 36 deletions Sources/CombustionBLE/BleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ import Foundation
import CoreBluetooth

protocol BleManagerDelegate: AnyObject {
func didConnectTo(id: UUID)
func didFailToConnectTo(id: UUID)
func didDisconnectFrom(id: UUID)
func updateDeviceWithStatus(id: UUID, status: DeviceStatus)
func updateDeviceWithAdvertising(advertising: AdvertisingData, rssi: NSNumber, id: UUID)
func updateDeviceWithLogResponse(id: UUID, logResponse: LogResponse)
func updateDeviceFwVersion(id: UUID, fwVersion: String)
func didConnectTo(identifier: UUID)
func didFailToConnectTo(identifier: UUID)
func didDisconnectFrom(identifier: UUID)
func handleSetIDResponse(identifier: UUID, success: Bool)
func handleSetColorResponse(identifier: UUID, success: Bool)
func updateDeviceWithStatus(identifier: UUID, status: DeviceStatus)
func updateDeviceWithAdvertising(advertising: AdvertisingData, rssi: NSNumber, identifier: UUID)
func updateDeviceWithLogResponse(identifier: UUID, logResponse: LogResponse)
func updateDeviceFwVersion(identifier: UUID, fwVersion: String)
func updateDeviceHwRevision(identifier: UUID, hwRevision: String)
}

/// Manages Core Bluetooth interface with the rest of the app.
Expand All @@ -58,6 +61,7 @@ class BleManager : NSObject {
static let UART_SERVICE = CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")

static let FW_VERSION_CHAR = CBUUID(string: "2a26")
static let HW_REVISION_CHAR = CBUUID(string: "2a27")
static let DEVICE_STATUS_CHAR = CBUUID(string: "00000101-CAAB-3792-3D44-97AE51C1407A")
static let UART_RX_CHAR = CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
static let UART_TX_CHAR = CBUUID(string: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
Expand All @@ -75,14 +79,15 @@ class BleManager : NSObject {
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
}

func sendRequest(id: String, request: Request) {
if let connectionPeripheral = getConnectedPeripheral(id: id), let uartChar = uartCharacteristics[id] {
func sendRequest(identifier: String, request: Request) {
if let connectionPeripheral = getConnectedPeripheral(identifier: identifier),
let uartChar = uartCharacteristics[identifier] {
connectionPeripheral.writeValue(request.data, for: uartChar, type: .withoutResponse)
}
}

private func getConnectedPeripheral(id: String) -> CBPeripheral? {
let uuid = UUID(uuidString: id)
private func getConnectedPeripheral(identifier: String) -> CBPeripheral? {
let uuid = UUID(uuidString: identifier)
let devicePeripherals = peripherals.filter { $0.identifier == uuid }
guard !devicePeripherals.isEmpty else {
// print("Failed to find peripherals")
Expand Down Expand Up @@ -125,16 +130,16 @@ extension BleManager: CBCentralManagerDelegate{
// Store peripheral reference for later use
peripherals.insert(peripheral)

delegate?.updateDeviceWithAdvertising(advertising: advData, rssi: RSSI, id: peripheral.identifier)
delegate?.updateDeviceWithAdvertising(advertising: advData, rssi: RSSI, identifier: peripheral.identifier)
} else {
// print("Ignoring device with type \(advData.type)")
}
}
}

/// Connect to device with the specified name.
public func connect(id: String) {
let uuid = UUID(uuidString: id)
public func connect(identifier: String) {
let uuid = UUID(uuidString: identifier)
let devicePeripherals = peripherals.filter { $0.identifier == uuid }
guard !devicePeripherals.isEmpty else {
print("Failed to find peripheral")
Expand All @@ -148,8 +153,8 @@ extension BleManager: CBCentralManagerDelegate{
}

/// Disconnect from device with the specified name.
public func disconnect(id: String) {
if let connectedPeripheral = getConnectedPeripheral(id: id) {
public func disconnect(identifier: String) {
if let connectedPeripheral = getConnectedPeripheral(identifier: identifier) {
manager.cancelPeripheralConnection(connectedPeripheral)
}
}
Expand All @@ -160,20 +165,20 @@ extension BleManager: CBCentralManagerDelegate{
peripheral.delegate = self
peripheral.discoverServices(nil)

delegate?.didConnectTo(id: peripheral.identifier)
delegate?.didConnectTo(identifier: peripheral.identifier)
}


public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
// print("\(#function)")

delegate?.didFailToConnectTo(id: peripheral.identifier)
delegate?.didFailToConnectTo(identifier: peripheral.identifier)
}

public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
// print("\(#function)")

delegate?.didDisconnectFrom(id: peripheral.identifier)
delegate?.didDisconnectFrom(identifier: peripheral.identifier)
}

}
Expand All @@ -197,13 +202,13 @@ extension BleManager: CBPeripheralDelegate {
guard let characteristics = service.characteristics else { return }

for characteristic in characteristics {
// print("discovered characteristic : \(characteristic.uuid) : \(characteristic.uuid.uuidString) :\(characteristic.descriptors)")

if(characteristic.uuid == Constants.UART_RX_CHAR) {
uartCharacteristics[peripheral.identifier.uuidString] = characteristic
} else if(characteristic.uuid == Constants.DEVICE_STATUS_CHAR) {
deviceStatusCharacteristics[peripheral.identifier.uuidString] = characteristic
} else if(characteristic.uuid == Constants.FW_VERSION_CHAR) {
} else if(characteristic.uuid == Constants.FW_VERSION_CHAR ||
characteristic.uuid == Constants.HW_REVISION_CHAR) {
// Read FW version and HW revision when the characteristics are discovered
peripheral.readValue(for: characteristic)
}

Expand All @@ -212,11 +217,10 @@ extension BleManager: CBPeripheralDelegate {
}

public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
// print("\(#function)")

}

public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
// print("\(#function): \(characteristic)")

// Always enable notifications for Device status characteristic
if(characteristic.uuid == Constants.UART_TX_CHAR),
Expand All @@ -229,26 +233,31 @@ extension BleManager: CBPeripheralDelegate {
public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
guard let data = characteristic.value else { return }

// print("didUpdateValueFor: \(characteristic): data size = \(data.count)")

if characteristic.uuid == Constants.UART_TX_CHAR {
if let logResponse = Response.fromData(data) as? LogResponse {
// print("Got log response: \(logResponse.success), sequence: \(logResponse.sequenceNumber)")
if(logResponse.success) {
delegate?.updateDeviceWithLogResponse(id: peripheral.identifier, logResponse: logResponse)
} else {
// print("Ignoring unsuccessful log response")
delegate?.updateDeviceWithLogResponse(identifier: peripheral.identifier, logResponse: logResponse)
}
}
else if let setIDResponse = Response.fromData(data) as? SetIDResponse {
delegate?.handleSetIDResponse(identifier: peripheral.identifier, success: setIDResponse.success)
}
else if let setColorResponse = Response.fromData(data) as? SetColorResponse {
delegate?.handleSetColorResponse(identifier: peripheral.identifier, success: setColorResponse.success)
}
}
else if characteristic.uuid == Constants.DEVICE_STATUS_CHAR {
if let status = DeviceStatus(fromData: data) {
delegate?.updateDeviceWithStatus(id: peripheral.identifier, status: status)
delegate?.updateDeviceWithStatus(identifier: peripheral.identifier, status: status)
}
}
else if characteristic.uuid == Constants.FW_VERSION_CHAR {
let fwVersion = String(decoding: data, as: UTF8.self)
delegate?.updateDeviceFwVersion(id: peripheral.identifier, fwVersion: fwVersion)
delegate?.updateDeviceFwVersion(identifier: peripheral.identifier, fwVersion: fwVersion)
}
else if characteristic.uuid == Constants.HW_REVISION_CHAR {
let hwRevision = String(decoding: data, as: UTF8.self)
delegate?.updateDeviceHwRevision(identifier: peripheral.identifier, hwRevision: hwRevision)
}
else {
// print("didUpdateValueFor: \(characteristic): unknown service")
Expand All @@ -258,11 +267,7 @@ extension BleManager: CBPeripheralDelegate {
public func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
guard let descriptors = characteristic.descriptors, !descriptors.isEmpty else { return }

// print("didDiscoverDescriptorsFor : \(characteristic.uuid)")

for _ in descriptors {
// print("discovered descriptor : \(descriptor.uuid)")

// Always enable notifications for UART TX characteristic
if(characteristic.uuid == Constants.UART_TX_CHAR) {
peripheral.setNotifyValue(true, for: characteristic)
Expand Down
13 changes: 8 additions & 5 deletions Sources/CombustionBLE/Device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ public class Device : ObservableObject {
}

/// String representation of device identifier (UUID)
public private(set) var id: String
public var identifier: String

/// Device firmware version
public internal(set) var firmareVersion: String?

/// Device hardware revision
public internal(set) var hardwareRevision: String?

/// Current connection state of device
@Published public internal(set) var connectionState: ConnectionState = .disconnected

Expand All @@ -63,8 +66,8 @@ public class Device : ObservableObject {
/// Time at which device was last updated
internal var lastUpdateTime = Date()

public init(id: UUID) {
self.id = id.uuidString
public init(identifier: UUID) {
self.identifier = identifier.uuidString
}
}

Expand Down Expand Up @@ -113,10 +116,10 @@ extension Device {

extension Device: Hashable {
public static func == (lhs: Device, rhs: Device) -> Bool {
return lhs.id == rhs.id
return lhs.identifier == rhs.identifier
}

public func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(identifier)
}
}
Loading

0 comments on commit 680b20f

Please sign in to comment.