From 6cdc1af6951bf2d574fbd471d6e56afe3af3f694 Mon Sep 17 00:00:00 2001 From: Jesse Johnston Date: Mon, 18 Apr 2022 19:52:59 -0700 Subject: [PATCH 01/10] Session Info UART messages --- Sources/CombustionBLE/BleManager.swift | 13 ++++- Sources/CombustionBLE/UART/MessageType.swift | 1 + Sources/CombustionBLE/UART/Response.swift | 3 +- Sources/CombustionBLE/UART/SessionInfo.swift | 55 +++++++++++++++++++ .../{SetColorRequest.swift => SetColor.swift} | 2 +- .../UART/{SetIDRequest.swift => SetID.swift} | 2 +- 6 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 Sources/CombustionBLE/UART/SessionInfo.swift rename Sources/CombustionBLE/UART/{SetColorRequest.swift => SetColor.swift} (98%) rename Sources/CombustionBLE/UART/{SetIDRequest.swift => SetID.swift} (98%) diff --git a/Sources/CombustionBLE/BleManager.swift b/Sources/CombustionBLE/BleManager.swift index f363c31..12d4dee 100644 --- a/Sources/CombustionBLE/BleManager.swift +++ b/Sources/CombustionBLE/BleManager.swift @@ -221,13 +221,15 @@ extension BleManager: CBPeripheralDelegate { } public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { - - // Always enable notifications for Device status characteristic if(characteristic.uuid == Constants.UART_TX_CHAR), let statusChar = deviceStatusCharacteristics[peripheral.identifier.uuidString] { + // After enabling UART notification + // Enable notifications for Device status characteristic peripheral.setNotifyValue(true, for: statusChar) + + // Send request the session ID from device + sendRequest(identifier: peripheral.identifier.uuidString, request: SessionInfoRequest()) } - } public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { @@ -245,6 +247,11 @@ extension BleManager: CBPeripheralDelegate { else if let setColorResponse = Response.fromData(data) as? SetColorResponse { delegate?.handleSetColorResponse(identifier: peripheral.identifier, success: setColorResponse.success) } + else if let sessionResponse = Response.fromData(data) as? SessionInfoResponse { + // TODO JDJ do something with this response + print("JDJ session ID : \(sessionResponse.sessionID)") + print("JDJ sample rate : \(sessionResponse.samplePeriod)") + } } else if characteristic.uuid == Constants.DEVICE_STATUS_CHAR { if let status = DeviceStatus(fromData: data) { diff --git a/Sources/CombustionBLE/UART/MessageType.swift b/Sources/CombustionBLE/UART/MessageType.swift index c11eae4..5622069 100644 --- a/Sources/CombustionBLE/UART/MessageType.swift +++ b/Sources/CombustionBLE/UART/MessageType.swift @@ -29,5 +29,6 @@ import Foundation enum MessageType: UInt8 { case SetID = 1 case SetColor = 2 + case SessionInfo = 3 case Log = 4 } diff --git a/Sources/CombustionBLE/UART/Response.swift b/Sources/CombustionBLE/UART/Response.swift index 54210ca..82bd822 100644 --- a/Sources/CombustionBLE/UART/Response.swift +++ b/Sources/CombustionBLE/UART/Response.swift @@ -69,7 +69,6 @@ extension Response { // Payload Length let lengthByte = data.subdata(in: 6..<7) - _ = lengthByte // Suppress 'unused variable' warning let payloadLength = lengthByte.withUnsafeBytes { $0.load(as: UInt8.self) } @@ -84,6 +83,8 @@ extension Response { return SetIDResponse(success: success) case .SetColor: return SetColorResponse(success: success) + case .SessionInfo: + return SessionInfoResponse(data: data, success: success) } } } diff --git a/Sources/CombustionBLE/UART/SessionInfo.swift b/Sources/CombustionBLE/UART/SessionInfo.swift new file mode 100644 index 0000000..64856e4 --- /dev/null +++ b/Sources/CombustionBLE/UART/SessionInfo.swift @@ -0,0 +1,55 @@ +// SessionInfo.swift + +/*-- +MIT License + +Copyright (c) 2022 Combustion Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--*/ + +import Foundation + +class SessionInfoRequest: Request { + static let PAYLOAD_LENGTH: UInt8 = 0 + + init() { + super.init(payloadLength: LogRequest.PAYLOAD_LENGTH, type: .SessionInfo) + } +} + +class SessionInfoResponse: Response { + let sessionID: UInt16 + let samplePeriod: UInt16 + + init(data: Data, success: Bool) { + let sequenceByteIndex = Response.HEADER_LENGTH + let sessionIDRaw = data.subdata(in: sequenceByteIndex..<(sequenceByteIndex + 2)) + sessionID = sessionIDRaw.withUnsafeBytes { + $0.load(as: UInt16.self) + } + + let samplePeriodRaw = data.subdata(in: (sequenceByteIndex + 2)..<(sequenceByteIndex + 4)) + samplePeriod = samplePeriodRaw.withUnsafeBytes { + $0.load(as: UInt16.self) + } + + super.init(success: success) + } +} diff --git a/Sources/CombustionBLE/UART/SetColorRequest.swift b/Sources/CombustionBLE/UART/SetColor.swift similarity index 98% rename from Sources/CombustionBLE/UART/SetColorRequest.swift rename to Sources/CombustionBLE/UART/SetColor.swift index b5fc49c..00b5dfd 100644 --- a/Sources/CombustionBLE/UART/SetColorRequest.swift +++ b/Sources/CombustionBLE/UART/SetColor.swift @@ -1,4 +1,4 @@ -// SetIDRequest.swift +// SetColor.swift /*-- MIT License diff --git a/Sources/CombustionBLE/UART/SetIDRequest.swift b/Sources/CombustionBLE/UART/SetID.swift similarity index 98% rename from Sources/CombustionBLE/UART/SetIDRequest.swift rename to Sources/CombustionBLE/UART/SetID.swift index 640d583..e5676a4 100644 --- a/Sources/CombustionBLE/UART/SetIDRequest.swift +++ b/Sources/CombustionBLE/UART/SetID.swift @@ -1,4 +1,4 @@ -// SetIDRequest.swift +// SetID.swift /*-- MIT License From ff0b5e6ff183e6b7b459a41535c1cac87b8a8c68 Mon Sep 17 00:00:00 2001 From: Jesse Johnston Date: Mon, 25 Apr 2022 16:52:37 -0700 Subject: [PATCH 02/10] Storing data per session --- Sources/CombustionBLE/BleManager.swift | 10 ++- Sources/CombustionBLE/Device.swift | 18 ++--- Sources/CombustionBLE/DeviceManager.swift | 6 ++ Sources/CombustionBLE/Probe.swift | 79 +++++++++++++------ .../CombustionBLE/ProbeTemperatureLog.swift | 12 ++- Sources/CombustionBLE/SimulatedProbe.swift | 6 +- Sources/CombustionBLE/UART/SessionInfo.swift | 14 +++- 7 files changed, 100 insertions(+), 45 deletions(-) diff --git a/Sources/CombustionBLE/BleManager.swift b/Sources/CombustionBLE/BleManager.swift index 12d4dee..c5ba9dd 100644 --- a/Sources/CombustionBLE/BleManager.swift +++ b/Sources/CombustionBLE/BleManager.swift @@ -34,9 +34,10 @@ protocol BleManagerDelegate: AnyObject { 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 updateDeviceWithSessionInformation(identifier: UUID, sessionInformation: SessionInformation) + func updateDeviceWithStatus(identifier: UUID, status: DeviceStatus) func updateDeviceFwVersion(identifier: UUID, fwVersion: String) func updateDeviceHwRevision(identifier: UUID, hwRevision: String) } @@ -248,9 +249,10 @@ extension BleManager: CBPeripheralDelegate { delegate?.handleSetColorResponse(identifier: peripheral.identifier, success: setColorResponse.success) } else if let sessionResponse = Response.fromData(data) as? SessionInfoResponse { - // TODO JDJ do something with this response - print("JDJ session ID : \(sessionResponse.sessionID)") - print("JDJ sample rate : \(sessionResponse.samplePeriod)") + if(sessionResponse.success) { + delegate?.updateDeviceWithSessionInformation(identifier: peripheral.identifier, + sessionInformation: sessionResponse.info) + } } } else if characteristic.uuid == Constants.DEVICE_STATUS_CHAR { diff --git a/Sources/CombustionBLE/Device.swift b/Sources/CombustionBLE/Device.swift index 1717005..dd271cd 100644 --- a/Sources/CombustionBLE/Device.swift +++ b/Sources/CombustionBLE/Device.swift @@ -69,6 +69,15 @@ public class Device : ObservableObject { public init(identifier: UUID) { self.identifier = identifier.uuidString } + + func updateConnectionState(_ state: ConnectionState) { + connectionState = state + + // If we were disconnected and we should be maintaining a connection, attempt to reconnect. + if(maintainingConnection && (connectionState == .disconnected || connectionState == .failed)) { + DeviceManager.shared.connectToDevice(self) + } + } } extension Device { @@ -98,15 +107,6 @@ extension Device { DeviceManager.shared.disconnectFromDevice(self) } - func updateConnectionState(_ state: ConnectionState) { - connectionState = state - - // If we were disconnected and we should be maintaining a connection, attempt to reconnect. - if(maintainingConnection && (connectionState == .disconnected || connectionState == .failed)) { - DeviceManager.shared.connectToDevice(self) - } - } - func updateDeviceStale() { stale = Date().timeIntervalSince(lastUpdateTime) > Constants.STALE_TIMEOUT } diff --git a/Sources/CombustionBLE/DeviceManager.swift b/Sources/CombustionBLE/DeviceManager.swift index cab5821..b39ee96 100644 --- a/Sources/CombustionBLE/DeviceManager.swift +++ b/Sources/CombustionBLE/DeviceManager.swift @@ -235,6 +235,12 @@ extension DeviceManager : BleManagerDelegate { } } + func updateDeviceWithSessionInformation(identifier: UUID, sessionInformation: SessionInformation) { + if let probe = devices[identifier.uuidString] as? Probe { + probe.updateWithSessionInformation(sessionInformation) + } + } + func updateDeviceFwVersion(identifier: UUID, fwVersion: String) { if let device = devices[identifier.uuidString] { device.firmareVersion = fwVersion diff --git a/Sources/CombustionBLE/Probe.swift b/Sources/CombustionBLE/Probe.swift index 9f6e375..402f325 100644 --- a/Sources/CombustionBLE/Probe.swift +++ b/Sources/CombustionBLE/Probe.swift @@ -34,14 +34,20 @@ public class Probe : Device { @Published public private(set) var serialNumber: UInt32 @Published public private(set) var currentTemperatures: ProbeTemperatures + @Published public private(set) var minSequenceNumber: UInt32? @Published public private(set) var maxSequenceNumber: UInt32? + /// Tracks whether all logs on probe have been synced to the app + @Published public private(set) var logsUpToDate = false + @Published public private(set) var id: ProbeID @Published public private(set) var color: ProbeColor - /// Tracks whether all logs on probe have been synced to the app - @Published public private(set) var logsUpToDate = false + private var sessionInformation: SessionInformation? + + /// Stores historical values of probe temperatures + public private(set) var temperatureLogs: [ProbeTemperatureLog] = [] /// Pretty-formatted device name public var name: String { @@ -57,9 +63,6 @@ public class Probe : Device { public var macAddressString: String { return String(format: "%012llX", macAddress) } - - /// Stores historical values of probe temperatures - public private(set) var temperatureLog : ProbeTemperatureLog = ProbeTemperatureLog() public init(_ advertising: AdvertisingData, RSSI: NSNumber, identifier: UUID) { serialNumber = advertising.serialNumber @@ -71,6 +74,15 @@ public class Probe : Device { updateWithAdvertising(advertising, RSSI: RSSI) } + + override func updateConnectionState(_ state: ConnectionState) { + // Clear session information on disconnect, since probe may have reset + if (state == .disconnected) { + sessionInformation = nil + } + + super.updateConnectionState(state) + } } extension Probe { @@ -91,33 +103,54 @@ extension Probe { color = deviceStatus.color // Log the temperature data point - temperatureLog.appendDataPoint(dataPoint: - LoggedProbeDataPoint.fromDeviceStatus(deviceStatus: - deviceStatus)) + addDataToLog(LoggedProbeDataPoint.fromDeviceStatus(deviceStatus: deviceStatus)) // Check for missing records - if let missingSequence = temperatureLog.firstMissingIndex(sequenceRangeStart: deviceStatus.minSequenceNumber, - sequenceRangeEnd: deviceStatus.maxSequenceNumber) { - // Track that the app is not up to date with the probe - logsUpToDate = false - - // Request missing records - DeviceManager.shared.requestLogsFrom(self, - minSequence: missingSequence, - maxSequence: deviceStatus.maxSequenceNumber) - } else { - // If there were no gaps, mark that the logs are up to date - logsUpToDate = true + if let current = getCurrentTemperatureLog() { + if let missingSequence = current.firstMissingIndex(sequenceRangeStart: deviceStatus.minSequenceNumber, + sequenceRangeEnd: deviceStatus.maxSequenceNumber) { + // Track that the app is not up to date with the probe + logsUpToDate = false + + // Request missing records + DeviceManager.shared.requestLogsFrom(self, + minSequence: missingSequence, + maxSequence: deviceStatus.maxSequenceNumber) + } else { + // If there were no gaps, mark that the logs are up to date + logsUpToDate = true + } } + lastUpdateTime = Date() } + func updateWithSessionInformation(_ sessionInformation: SessionInformation) { + self.sessionInformation = sessionInformation + } + /// Processes an incoming log response (response to a manual request for prior messages) func processLogResponse(logResponse: LogResponse) { - temperatureLog.insertDataPoint(newDataPoint: - LoggedProbeDataPoint.fromLogResponse(logResponse: - logResponse)) + addDataToLog(LoggedProbeDataPoint.fromLogResponse(logResponse: logResponse)) + } + + private func addDataToLog(_ dataPoint: LoggedProbeDataPoint) { + if let current = getCurrentTemperatureLog() { + // Append data to temperature log for current session + current.appendDataPoint(dataPoint: dataPoint) + } + else if let sessionInformation = sessionInformation { + // Create a new Temperature log for session and append data + let log = ProbeTemperatureLog(sessionInfo: sessionInformation) + log.appendDataPoint(dataPoint: dataPoint) + temperatureLogs.append(log) + } + } + + // Find the ProbeTemperatureLog that matches current session ID + private func getCurrentTemperatureLog() -> ProbeTemperatureLog? { + return temperatureLogs.first(where: { $0.sessionInformation.sessionID == sessionInformation?.sessionID } ) } diff --git a/Sources/CombustionBLE/ProbeTemperatureLog.swift b/Sources/CombustionBLE/ProbeTemperatureLog.swift index 4cd86c1..9307d8c 100644 --- a/Sources/CombustionBLE/ProbeTemperatureLog.swift +++ b/Sources/CombustionBLE/ProbeTemperatureLog.swift @@ -29,6 +29,8 @@ import OrderedCollections public class ProbeTemperatureLog : ObservableObject { + let sessionInformation: SessionInformation + /// Buffer of logged data points public var dataPointsDict : OrderedDictionary @@ -52,10 +54,11 @@ public class ProbeTemperatureLog : ObservableObject { /// data point dictionary. This prevents unnecesary re-sorting of the overall dictionary. private var dataPointAccumulator : OrderedSet - init() { + init(sessionInfo: SessionInformation) { accumulatorTimer = nil dataPointsDict = OrderedDictionary() dataPointAccumulator = OrderedSet() + sessionInformation = sessionInfo } /// Finds the first missing sequence number in the specified range of sequence numbers. @@ -103,7 +106,7 @@ public class ProbeTemperatureLog : ObservableObject { /// Inserts a new data point. Places it in the accumulator so it can be inserted with additional /// records coming in. /// - parameter newDataPoint: New data points to be added to the buffer - func insertDataPoint(newDataPoint: LoggedProbeDataPoint) { + private func insertDataPoint(newDataPoint: LoggedProbeDataPoint) { // Add the incoming data point to the accumulator let appendResult = dataPointAccumulator.append(newDataPoint) if appendResult.inserted { @@ -129,7 +132,7 @@ public class ProbeTemperatureLog : ObservableObject { /// Appends data point to the logged probe data. func appendDataPoint(dataPoint: LoggedProbeDataPoint) { - // Ensure new point's sequence number belongs at the end + // Check if new point's sequence number belongs at the end if let lastPoint = dataPointsDict.values.last { if(dataPoint.sequenceNum == (lastPoint.sequenceNum + 1)) { // If it does, simply add it and it will appear at the end of the ordered collection @@ -146,3 +149,6 @@ public class ProbeTemperatureLog : ObservableObject { } } +extension ProbeTemperatureLog: Identifiable { + public var id: UInt16 { return sessionInformation.sessionID } +} diff --git a/Sources/CombustionBLE/SimulatedProbe.swift b/Sources/CombustionBLE/SimulatedProbe.swift index 7955424..5070249 100644 --- a/Sources/CombustionBLE/SimulatedProbe.swift +++ b/Sources/CombustionBLE/SimulatedProbe.swift @@ -65,10 +65,12 @@ class SimulatedProbe: Probe { private func updateFakeStatus() { guard connectionState == .connected else { return } - let firstSeq = temperatureLog.dataPoints.first?.sequenceNum ?? 0 + // TODO JDJ + let firstSeq = temperatureLogs.first?.dataPoints.first?.sequenceNum ?? 0 let lastSequence: UInt32 - if let last = temperatureLog.dataPoints.last?.sequenceNum { + // TODO JDJ + if let last = temperatureLogs.first?.dataPoints.last?.sequenceNum { lastSequence = last + 1 } else { diff --git a/Sources/CombustionBLE/UART/SessionInfo.swift b/Sources/CombustionBLE/UART/SessionInfo.swift index 64856e4..70c4e30 100644 --- a/Sources/CombustionBLE/UART/SessionInfo.swift +++ b/Sources/CombustionBLE/UART/SessionInfo.swift @@ -26,6 +26,11 @@ SOFTWARE. import Foundation +struct SessionInformation { + let sessionID: UInt16 + let samplePeriod: UInt16 +} + class SessionInfoRequest: Request { static let PAYLOAD_LENGTH: UInt8 = 0 @@ -35,20 +40,21 @@ class SessionInfoRequest: Request { } class SessionInfoResponse: Response { - let sessionID: UInt16 - let samplePeriod: UInt16 + let info: SessionInformation init(data: Data, success: Bool) { let sequenceByteIndex = Response.HEADER_LENGTH let sessionIDRaw = data.subdata(in: sequenceByteIndex..<(sequenceByteIndex + 2)) - sessionID = sessionIDRaw.withUnsafeBytes { + let sessionID = sessionIDRaw.withUnsafeBytes { $0.load(as: UInt16.self) } let samplePeriodRaw = data.subdata(in: (sequenceByteIndex + 2)..<(sequenceByteIndex + 4)) - samplePeriod = samplePeriodRaw.withUnsafeBytes { + let samplePeriod = samplePeriodRaw.withUnsafeBytes { $0.load(as: UInt16.self) } + + info = SessionInformation(sessionID: sessionID, samplePeriod: samplePeriod) super.init(success: success) } From ef43396e450cc1ec616176815cd78a252912b3c4 Mon Sep 17 00:00:00 2001 From: Jesse Johnston Date: Mon, 2 May 2022 17:05:43 -0700 Subject: [PATCH 03/10] Calculate start time for session --- .../CombustionBLE/ProbeTemperatureLog.swift | 23 ++++++++++++++++--- Sources/CombustionBLE/SimulatedProbe.swift | 10 +++++--- Sources/CombustionBLE/UART/SessionInfo.swift | 4 ++-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Sources/CombustionBLE/ProbeTemperatureLog.swift b/Sources/CombustionBLE/ProbeTemperatureLog.swift index 9307d8c..e71c96a 100644 --- a/Sources/CombustionBLE/ProbeTemperatureLog.swift +++ b/Sources/CombustionBLE/ProbeTemperatureLog.swift @@ -29,7 +29,7 @@ import OrderedCollections public class ProbeTemperatureLog : ObservableObject { - let sessionInformation: SessionInformation + public let sessionInformation: SessionInformation /// Buffer of logged data points public var dataPointsDict : OrderedDictionary @@ -41,6 +41,9 @@ public class ProbeTemperatureLog : ObservableObject { } } + /// Approximate start time of the session + public var startTime: Date? = nil + /// Number of MS to wait for new log data to flow in before inserting it into the data buffer. private let ACCUMULATOR_STABILIIZATION_TIME = 0.2 /// Maximum number of records to allow to build up in the accumulator before forcing an update @@ -55,7 +58,6 @@ public class ProbeTemperatureLog : ObservableObject { private var dataPointAccumulator : OrderedSet init(sessionInfo: SessionInformation) { - accumulatorTimer = nil dataPointsDict = OrderedDictionary() dataPointAccumulator = OrderedSet() sessionInformation = sessionInfo @@ -145,10 +147,25 @@ public class ProbeTemperatureLog : ObservableObject { } else { // If the collection is empty, just add it dataPointsDict[dataPoint.sequenceNum] = dataPoint + + setStartTime(dataPoint: dataPoint) } } + + private func setStartTime(dataPoint: LoggedProbeDataPoint) { + // Do not recalculate start time after it has been set + guard startTime == nil else { return } + + let currentTime = Date() + let secondDiff = Int(dataPoint.sequenceNum) * Int(sessionInformation.samplePeriod) / 1000 + startTime = Calendar.current.date(byAdding: .second, value: -1 * secondDiff, to: currentTime) + } } extension ProbeTemperatureLog: Identifiable { - public var id: UInt16 { return sessionInformation.sessionID } + + // Use the Session ID for `Identifiable` protocol + public var id: UInt16 { + return sessionInformation.sessionID + } } diff --git a/Sources/CombustionBLE/SimulatedProbe.swift b/Sources/CombustionBLE/SimulatedProbe.swift index 5070249..b678f51 100644 --- a/Sources/CombustionBLE/SimulatedProbe.swift +++ b/Sources/CombustionBLE/SimulatedProbe.swift @@ -34,6 +34,7 @@ class SimulatedProbe: Probe { super.init(advertising, RSSI: SimulatedProbe.randomeRSSI(), identifier: UUID()) firmareVersion = "v1.2.3" + hardwareRevision = "v2.3.4" // Create timer to update probe with fake advertising packets Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in @@ -44,6 +45,10 @@ class SimulatedProbe: Probe { Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in self?.updateFakeStatus() } + + // Set fake session information + let fakeSessionInfo = SessionInformation(sessionID: UInt16.random(in: 0.. Date: Tue, 24 May 2022 18:12:12 -0700 Subject: [PATCH 04/10] Adding backwards compatability for probe session ID --- Sources/CombustionBLE/DeviceManager.swift | 9 +++++++++ Sources/CombustionBLE/UART/SessionInfo.swift | 2 +- Sources/CombustionBLE/UART/SetColor.swift | 2 +- Sources/CombustionBLE/UART/SetID.swift | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/CombustionBLE/DeviceManager.swift b/Sources/CombustionBLE/DeviceManager.swift index b39ee96..58ea450 100644 --- a/Sources/CombustionBLE/DeviceManager.swift +++ b/Sources/CombustionBLE/DeviceManager.swift @@ -244,6 +244,15 @@ extension DeviceManager : BleManagerDelegate { func updateDeviceFwVersion(identifier: UUID, fwVersion: String) { if let device = devices[identifier.uuidString] { device.firmareVersion = fwVersion + + // TODO : remove this at some point + // Prior to v0.8.0, the firmware did not support the Session ID command + // Therefore, add a hardcoded session for backwards compatibility + if(fwVersion.compare("v0.8.0", options: String.CompareOptions.numeric, range: nil, locale: nil) == .orderedAscending) { + let fakeSessionInfo = SessionInformation(sessionID: 0, samplePeriod: 1000) + updateDeviceWithSessionInformation(identifier: identifier, sessionInformation: fakeSessionInfo) + } + } } diff --git a/Sources/CombustionBLE/UART/SessionInfo.swift b/Sources/CombustionBLE/UART/SessionInfo.swift index 00ec7ed..d1254a4 100644 --- a/Sources/CombustionBLE/UART/SessionInfo.swift +++ b/Sources/CombustionBLE/UART/SessionInfo.swift @@ -35,7 +35,7 @@ class SessionInfoRequest: Request { static let PAYLOAD_LENGTH: UInt8 = 0 init() { - super.init(payloadLength: LogRequest.PAYLOAD_LENGTH, type: .SessionInfo) + super.init(payloadLength: SessionInfoRequest.PAYLOAD_LENGTH, type: .SessionInfo) } } diff --git a/Sources/CombustionBLE/UART/SetColor.swift b/Sources/CombustionBLE/UART/SetColor.swift index 00b5dfd..8736099 100644 --- a/Sources/CombustionBLE/UART/SetColor.swift +++ b/Sources/CombustionBLE/UART/SetColor.swift @@ -30,7 +30,7 @@ class SetColorRequest: Request { static let PAYLOAD_LENGTH: UInt8 = 1 init(color: ProbeColor) { - super.init(payloadLength: LogRequest.PAYLOAD_LENGTH, type: .SetColor) + super.init(payloadLength: SetColorRequest.PAYLOAD_LENGTH, type: .SetColor) self.data[Request.HEADER_SIZE] = color.rawValue } } diff --git a/Sources/CombustionBLE/UART/SetID.swift b/Sources/CombustionBLE/UART/SetID.swift index e5676a4..be56a89 100644 --- a/Sources/CombustionBLE/UART/SetID.swift +++ b/Sources/CombustionBLE/UART/SetID.swift @@ -30,7 +30,7 @@ class SetIDRequest: Request { static let PAYLOAD_LENGTH: UInt8 = 1 init(id: ProbeID) { - super.init(payloadLength: LogRequest.PAYLOAD_LENGTH, type: .SetID) + super.init(payloadLength: SetIDRequest.PAYLOAD_LENGTH, type: .SetID) self.data[Request.HEADER_SIZE] = id.rawValue } } From bbfab61a921a75d3140e2d099659e2c468d3806e Mon Sep 17 00:00:00 2001 From: Jesse Johnston Date: Wed, 25 May 2022 15:38:44 -0700 Subject: [PATCH 05/10] Update session ID to 4 bytes --- Sources/CombustionBLE/ProbeTemperatureLog.swift | 2 +- Sources/CombustionBLE/SimulatedProbe.swift | 2 +- Sources/CombustionBLE/UART/SessionInfo.swift | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/CombustionBLE/ProbeTemperatureLog.swift b/Sources/CombustionBLE/ProbeTemperatureLog.swift index e71c96a..70c3ae7 100644 --- a/Sources/CombustionBLE/ProbeTemperatureLog.swift +++ b/Sources/CombustionBLE/ProbeTemperatureLog.swift @@ -165,7 +165,7 @@ public class ProbeTemperatureLog : ObservableObject { extension ProbeTemperatureLog: Identifiable { // Use the Session ID for `Identifiable` protocol - public var id: UInt16 { + public var id: UInt32 { return sessionInformation.sessionID } } diff --git a/Sources/CombustionBLE/SimulatedProbe.swift b/Sources/CombustionBLE/SimulatedProbe.swift index b678f51..532326a 100644 --- a/Sources/CombustionBLE/SimulatedProbe.swift +++ b/Sources/CombustionBLE/SimulatedProbe.swift @@ -47,7 +47,7 @@ class SimulatedProbe: Probe { } // Set fake session information - let fakeSessionInfo = SessionInformation(sessionID: UInt16.random(in: 0.. Date: Tue, 31 May 2022 16:30:38 -0700 Subject: [PATCH 06/10] Address code review feedback --- Sources/CombustionBLE/DeviceManager.swift | 6 +++++- Sources/CombustionBLE/SimulatedProbe.swift | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/CombustionBLE/DeviceManager.swift b/Sources/CombustionBLE/DeviceManager.swift index 58ea450..2eb2e89 100644 --- a/Sources/CombustionBLE/DeviceManager.swift +++ b/Sources/CombustionBLE/DeviceManager.swift @@ -248,7 +248,11 @@ extension DeviceManager : BleManagerDelegate { // TODO : remove this at some point // Prior to v0.8.0, the firmware did not support the Session ID command // Therefore, add a hardcoded session for backwards compatibility - if(fwVersion.compare("v0.8.0", options: String.CompareOptions.numeric, range: nil, locale: nil) == .orderedAscending) { + + // Remove git hash from debug versions + let splitVersion = fwVersion.split(separator: "-") + + if(splitVersion.first?.compare("v0.8.0", options: String.CompareOptions.numeric, range: nil, locale: nil) == .orderedAscending) { let fakeSessionInfo = SessionInformation(sessionID: 0, samplePeriod: 1000) updateDeviceWithSessionInformation(identifier: identifier, sessionInformation: fakeSessionInfo) } diff --git a/Sources/CombustionBLE/SimulatedProbe.swift b/Sources/CombustionBLE/SimulatedProbe.swift index 532326a..dd189a2 100644 --- a/Sources/CombustionBLE/SimulatedProbe.swift +++ b/Sources/CombustionBLE/SimulatedProbe.swift @@ -34,7 +34,7 @@ class SimulatedProbe: Probe { super.init(advertising, RSSI: SimulatedProbe.randomeRSSI(), identifier: UUID()) firmareVersion = "v1.2.3" - hardwareRevision = "v2.3.4" + hardwareRevision = "v0.31-A1" // Create timer to update probe with fake advertising packets Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in From 29b538b80518b71e7e0fcba445d3a4e8b2c2a52d Mon Sep 17 00:00:00 2001 From: Jesse Johnston Date: Thu, 2 Jun 2022 16:38:29 -0700 Subject: [PATCH 07/10] Decode multiple UART messages --- Sources/CombustionBLE/BleManager.swift | 39 ++++++++++--------- Sources/CombustionBLE/DeviceManager.swift | 2 + Sources/CombustionBLE/UART/LogResponse.swift | 4 +- Sources/CombustionBLE/UART/Response.swift | 40 ++++++++++++++++---- Sources/CombustionBLE/UART/SessionInfo.swift | 4 +- 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/Sources/CombustionBLE/BleManager.swift b/Sources/CombustionBLE/BleManager.swift index c5ba9dd..0f2ff91 100644 --- a/Sources/CombustionBLE/BleManager.swift +++ b/Sources/CombustionBLE/BleManager.swift @@ -237,23 +237,7 @@ extension BleManager: CBPeripheralDelegate { guard let data = characteristic.value else { return } if characteristic.uuid == Constants.UART_TX_CHAR { - if let logResponse = Response.fromData(data) as? LogResponse { - if(logResponse.success) { - 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 let sessionResponse = Response.fromData(data) as? SessionInfoResponse { - if(sessionResponse.success) { - delegate?.updateDeviceWithSessionInformation(identifier: peripheral.identifier, - sessionInformation: sessionResponse.info) - } - } + handleUartData(data: data, identifier: peripheral.identifier) } else if characteristic.uuid == Constants.DEVICE_STATUS_CHAR { if let status = DeviceStatus(fromData: data) { @@ -284,4 +268,25 @@ extension BleManager: CBPeripheralDelegate { } } + private func handleUartData(data: Data, identifier: UUID) { + let responses = Response.fromData(data) + + for response in responses { + if let logResponse = response as? LogResponse { + delegate?.updateDeviceWithLogResponse(identifier: identifier, logResponse: logResponse) + } + else if let setIDResponse = response as? SetIDResponse { + delegate?.handleSetIDResponse(identifier: identifier, success: setIDResponse.success) + } + else if let setColorResponse = response as? SetColorResponse { + delegate?.handleSetColorResponse(identifier: identifier, success: setColorResponse.success) + } + else if let sessionResponse = response as? SessionInfoResponse { + if(sessionResponse.success) { + delegate?.updateDeviceWithSessionInformation(identifier: identifier, sessionInformation: sessionResponse.info) + } + } + } + } + } diff --git a/Sources/CombustionBLE/DeviceManager.swift b/Sources/CombustionBLE/DeviceManager.swift index 2eb2e89..81e6132 100644 --- a/Sources/CombustionBLE/DeviceManager.swift +++ b/Sources/CombustionBLE/DeviceManager.swift @@ -218,6 +218,8 @@ extension DeviceManager : BleManagerDelegate { } func updateDeviceWithLogResponse(identifier: UUID, logResponse: LogResponse) { + guard logResponse.success else { return } + if let probe = devices[identifier.uuidString] as? Probe { probe.processLogResponse(logResponse: logResponse) } diff --git a/Sources/CombustionBLE/UART/LogResponse.swift b/Sources/CombustionBLE/UART/LogResponse.swift index e18a3ea..f7eea11 100644 --- a/Sources/CombustionBLE/UART/LogResponse.swift +++ b/Sources/CombustionBLE/UART/LogResponse.swift @@ -28,6 +28,8 @@ import Foundation class LogResponse: Response { + static let PAYLOAD_LENGTH = 17 + let sequenceNumber: UInt32 let temperatures: ProbeTemperatures @@ -45,6 +47,6 @@ class LogResponse: Response { // print("******** Received response!") // print("Sequence = \(sequenceNumber) : Temperature = \(temperatures)") - super.init(success: success) + super.init(success: success, payLoadLength: LogResponse.PAYLOAD_LENGTH) } } diff --git a/Sources/CombustionBLE/UART/Response.swift b/Sources/CombustionBLE/UART/Response.swift index 82bd822..1769590 100644 --- a/Sources/CombustionBLE/UART/Response.swift +++ b/Sources/CombustionBLE/UART/Response.swift @@ -30,16 +30,36 @@ class Response { static let HEADER_LENGTH = 7 let success: Bool + let payLoadLength: Int - init(success: Bool) { + init(success: Bool, payLoadLength: Int) { self.success = success + self.payLoadLength = payLoadLength } } extension Response { - static func fromData(_ data : Data) -> Response? { - // print() - // print("*** Parsing UART response") + static func fromData(_ data : Data) -> [Response] { + var responses = [Response]() + + var numberBytesRead = 0 + + while(numberBytesRead < data.count) { + let bytesToDecode = data.subdata(in: numberBytesRead.. Response? { // Sync bytes let syncBytes = data.subdata(in: 0..<2) let syncString = syncBytes.reduce("") {$0 + String(format: "%02x", $1)} @@ -72,7 +92,13 @@ extension Response { let payloadLength = lengthByte.withUnsafeBytes { $0.load(as: UInt8.self) } - _ = payloadLength // Suppress 'unused variable' warning + + let responseLength = Int(payloadLength) + HEADER_LENGTH + + // Invalid number of bytes + if(data.count < responseLength) { + return nil + } // print("Success: \(success), payloadLength: \(payloadLength)") @@ -80,9 +106,9 @@ extension Response { case .Log: return LogResponse(data: data, success: success) case .SetID: - return SetIDResponse(success: success) + return SetIDResponse(success: success, payLoadLength: Int(payloadLength)) case .SetColor: - return SetColorResponse(success: success) + return SetColorResponse(success: success, payLoadLength: Int(payloadLength)) case .SessionInfo: return SessionInfoResponse(data: data, success: success) } diff --git a/Sources/CombustionBLE/UART/SessionInfo.swift b/Sources/CombustionBLE/UART/SessionInfo.swift index 1ef2763..a565c61 100644 --- a/Sources/CombustionBLE/UART/SessionInfo.swift +++ b/Sources/CombustionBLE/UART/SessionInfo.swift @@ -40,6 +40,8 @@ class SessionInfoRequest: Request { } class SessionInfoResponse: Response { + static let PAYLOAD_LENGTH = 6 + let info: SessionInformation init(data: Data, success: Bool) { @@ -56,6 +58,6 @@ class SessionInfoResponse: Response { info = SessionInformation(sessionID: sessionID, samplePeriod: samplePeriod) - super.init(success: success) + super.init(success: success, payLoadLength: SessionInfoResponse.PAYLOAD_LENGTH) } } From f3ae94525f756fb1c7f0420a5d44a3a1cf340936 Mon Sep 17 00:00:00 2001 From: Jesse Johnston Date: Tue, 14 Jun 2022 14:07:59 -0700 Subject: [PATCH 08/10] Move CSV export into framework --- Sources/CombustionBLE/Utilities/CSV.swift | 132 ++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 Sources/CombustionBLE/Utilities/CSV.swift diff --git a/Sources/CombustionBLE/Utilities/CSV.swift b/Sources/CombustionBLE/Utilities/CSV.swift new file mode 100644 index 0000000..b155f73 --- /dev/null +++ b/Sources/CombustionBLE/Utilities/CSV.swift @@ -0,0 +1,132 @@ +// CSV.swift +// CSV export of probe data + +/*-- +MIT License + +Copyright (c) 2021 Combustion Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--*/ + +import Foundation + +public struct CSV { + + /// Helper function that generates a CSV representation of probe data. + private static func probeDataToCsv(probe: Probe, date: Date = Date()) -> String { + var output = [String]() + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let dateString = dateFormatter.string(from: date) + + output.append("Combustion Inc. Probe Data") + output.append("CSV version: 2") + output.append("Probe S/N: \(String(format: "%4X", probe.serialNumber))") + output.append("Probe FW version: \(probe.firmareVersion ?? "??")") + output.append("Probe HW revision: \(probe.hardwareRevision ?? "??")") + output.append("\(dateString)") + output.append("") + + // TODO add app version to this header + + // Header + output.append("Timestamp,SessionID,SequenceNumber,T1,T2,T3,T4,T5,T6,T7,T8") + + // Add temperature data points + if let firstSessionStart = probe.temperatureLogs.first?.startTime?.timeIntervalSince1970 { + for session in probe.temperatureLogs { + for dataPoint in session.dataPoints { + + // Calculate timestamp for current data point + var timeStamp = 0 + if let currentSessionStart = session.startTime?.timeIntervalSince1970 { + // Number of seconds between first session start time and current start time + let sessionStartTimeDiff = Int(currentSessionStart - firstSessionStart) + + // Number of seconds beteen current data point and session start time + let dataPointSeconds = Int(dataPoint.sequenceNum) * Int(session.sessionInformation.samplePeriod) / 1000 + + // Number of seconds between current data point and first session start + timeStamp = dataPointSeconds + sessionStartTimeDiff + } + + + output.append(String(format: "%d,%u,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f", + timeStamp, + session.id, + dataPoint.sequenceNum, + dataPoint.temperatures.values[0], dataPoint.temperatures.values[1], + dataPoint.temperatures.values[2], dataPoint.temperatures.values[3], + dataPoint.temperatures.values[4], dataPoint.temperatures.values[5], + dataPoint.temperatures.values[6], dataPoint.temperatures.values[7])) + } + } + } + + + + return output.joined(separator: "\n") + } + + /// Creates a CSV file for export. + /// - param probe: Probe for which to create the file + /// - returns: URL of file + public static func createCsvFile(probe: Probe) -> URL? { + let date = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH_mm_ss" + let dateString = dateFormatter.string(from: date) + + let filename = "Probe Data - \(String(format: "%4X", probe.serialNumber)) - \(dateString).csv" + + // Generate the CSV + let csv = probeDataToCsv(probe: probe, date: date) + + // Create the temporary file + let filePath = NSTemporaryDirectory() + "/" + filename; + + let csvURL = URL(fileURLWithPath: filePath) + + do { + try csv.write(to: csvURL, atomically: true, encoding: String.Encoding.utf8) + + } catch { + // Failed to write file, return nothing + return nil + } + + return csvURL + } + + + /// Cleans up the temporary CSV file at location + func cleanUpCsvFile(url: URL) { + do { + let fileManager = FileManager.default + if fileManager.fileExists(atPath: url.absoluteString) { + try fileManager.removeItem(atPath: url.absoluteString) + } + } catch { + // Couldn't delete, don't worry about it + } + } + +} From 3ce4d7792fac277063e09844916f0bfe54fda69d Mon Sep 17 00:00:00 2001 From: Jesse Johnston Date: Tue, 14 Jun 2022 16:25:05 -0700 Subject: [PATCH 09/10] Add support for battery status --- .../BleData/AdvertisingData.swift | 15 ++++++- .../CombustionBLE/BleData/BatteryStatus.swift | 43 +++++++++++++++++++ .../CombustionBLE/BleData/DeviceStatus.swift | 12 ++++++ Sources/CombustionBLE/Device.swift | 5 ++- Sources/CombustionBLE/Probe.swift | 10 ++++- Sources/CombustionBLE/SimulatedProbe.swift | 3 +- 6 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 Sources/CombustionBLE/BleData/BatteryStatus.swift diff --git a/Sources/CombustionBLE/BleData/AdvertisingData.swift b/Sources/CombustionBLE/BleData/AdvertisingData.swift index b896f88..e37a97c 100644 --- a/Sources/CombustionBLE/BleData/AdvertisingData.swift +++ b/Sources/CombustionBLE/BleData/AdvertisingData.swift @@ -33,7 +33,6 @@ public enum CombustionProductType: UInt8 { case NODE = 0x02 } - /// Struct containing advertising data received from device. public struct AdvertisingData { /// Type of Combustion product @@ -48,6 +47,8 @@ public struct AdvertisingData { public let color: ProbeColor /// Probe mode public let mode: ProbeMode + /// Battery Status + public let batteryStatus: BatteryStatus private enum Constants { // Locations of data in advertising packets @@ -55,6 +56,7 @@ public struct AdvertisingData { static let SERIAL_RANGE = 3..<7 static let TEMPERATURE_RANGE = 7..<20 static let MODE_COLOR_ID_RANGE = 20..<21 + static let BATTERY_STATUS_RANGE = 21..<22 } } @@ -101,6 +103,15 @@ extension AdvertisingData { color = .COLOR1 mode = .Normal } + + // Decode battery status if its present in the advertising packet + if(data.count >= 22) { + let statusData = data.subdata(in: Constants.BATTERY_STATUS_RANGE) + batteryStatus = BatteryStatus.fromRawData(data: statusData) + } + else { + batteryStatus = .OK + } } } @@ -114,6 +125,7 @@ extension AdvertisingData { id = .ID1 color = .COLOR1 mode = .Normal + batteryStatus = .OK } // Fake data initializer for Simulated Probe @@ -124,5 +136,6 @@ extension AdvertisingData { id = .ID1 color = .COLOR1 mode = .Normal + batteryStatus = .OK } } diff --git a/Sources/CombustionBLE/BleData/BatteryStatus.swift b/Sources/CombustionBLE/BleData/BatteryStatus.swift new file mode 100644 index 0000000..3c8d5f8 --- /dev/null +++ b/Sources/CombustionBLE/BleData/BatteryStatus.swift @@ -0,0 +1,43 @@ +// BatteryStatus.swift + +/*-- +MIT License + +Copyright (c) 2021 Combustion Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--*/ +import Foundation + +/// Enumeration of Battery status +public enum BatteryStatus: UInt8 { + case OK = 0x00 + case LOW = 0x01 + + private enum Constants { + static let BATTERY_MASK: UInt8 = 0x3 + } + + static func fromRawData(data: Data) -> BatteryStatus { + let statusBytes = [UInt8](data) + + let rawStatus = (statusBytes[0] & (Constants.BATTERY_MASK)) + return BatteryStatus(rawValue: rawStatus) ?? .OK + } +} diff --git a/Sources/CombustionBLE/BleData/DeviceStatus.swift b/Sources/CombustionBLE/BleData/DeviceStatus.swift index 73b997a..7d10d31 100644 --- a/Sources/CombustionBLE/BleData/DeviceStatus.swift +++ b/Sources/CombustionBLE/BleData/DeviceStatus.swift @@ -40,6 +40,8 @@ public struct DeviceStatus { public let color: ProbeColor /// Probe mode public let mode: ProbeMode + /// Battery Status + public let batteryStatus: BatteryStatus private enum Constants { // Locations of data in status packet @@ -47,6 +49,7 @@ public struct DeviceStatus { static let MAX_SEQ_RANGE = 4..<8 static let TEMPERATURE_RANGE = 8..<21 static let MODE_COLOR_ID_RANGE = 21..<22 + static let BATTERY_STATUS_RANGE = 22..<23 } } @@ -80,5 +83,14 @@ extension DeviceStatus { color = .COLOR1 mode = .Normal } + + // Decode battery status if its present in the advertising packet + if(data.count >= 23) { + let statusData = data.subdata(in: Constants.BATTERY_STATUS_RANGE) + batteryStatus = BatteryStatus.fromRawData(data: statusData) + } + else { + batteryStatus = .OK + } } } diff --git a/Sources/CombustionBLE/Device.swift b/Sources/CombustionBLE/Device.swift index ba00c8d..4c1d0dd 100644 --- a/Sources/CombustionBLE/Device.swift +++ b/Sources/CombustionBLE/Device.swift @@ -55,7 +55,7 @@ public class Device : ObservableObject { @Published public internal(set) var connectionState: ConnectionState = .disconnected /// Signal strength to device - @Published public internal(set) var rssi : Int = Int.min + @Published public internal(set) var rssi: Int /// Tracks whether the app should attempt to maintain a connection to the device. @Published public internal(set) var maintainingConnection = false @@ -66,8 +66,9 @@ public class Device : ObservableObject { /// Time at which device was last updated internal var lastUpdateTime = Date() - public init(identifier: UUID) { + public init(identifier: UUID, RSSI: NSNumber) { self.identifier = identifier.uuidString + self.rssi = RSSI.intValue } func updateConnectionState(_ state: ConnectionState) { diff --git a/Sources/CombustionBLE/Probe.swift b/Sources/CombustionBLE/Probe.swift index 0b409be..85a0580 100644 --- a/Sources/CombustionBLE/Probe.swift +++ b/Sources/CombustionBLE/Probe.swift @@ -45,6 +45,8 @@ public class Probe : Device { @Published public private(set) var id: ProbeID @Published public private(set) var color: ProbeColor + @Published public private(set) var batteryStatus: BatteryStatus + private var sessionInformation: SessionInformation? /// Stores historical values of probe temperatures @@ -72,8 +74,9 @@ public class Probe : Device { serialNumber = advertising.serialNumber id = advertising.id color = advertising.color + batteryStatus = advertising.batteryStatus - super.init(identifier: identifier) + super.init(identifier: identifier, RSSI: RSSI) updateWithAdvertising(advertising, RSSI: RSSI) } @@ -116,6 +119,10 @@ extension Probe { rssi = RSSI.intValue + id = advertising.id + color = advertising.color + batteryStatus = advertising.batteryStatus + lastUpdateTime = Date() } @@ -125,6 +132,7 @@ extension Probe { maxSequenceNumber = deviceStatus.maxSequenceNumber id = deviceStatus.id color = deviceStatus.color + batteryStatus = deviceStatus.batteryStatus if(deviceStatus.mode == .Normal) { currentTemperatures = deviceStatus.temperatures diff --git a/Sources/CombustionBLE/SimulatedProbe.swift b/Sources/CombustionBLE/SimulatedProbe.swift index 38c574c..f58e012 100644 --- a/Sources/CombustionBLE/SimulatedProbe.swift +++ b/Sources/CombustionBLE/SimulatedProbe.swift @@ -86,7 +86,8 @@ class SimulatedProbe: Probe { temperatures: ProbeTemperatures.withRandomData(), id: .ID1, color: .COLOR1, - mode: .Normal) + mode: .Normal, + batteryStatus: .OK) updateProbeStatus(deviceStatus: deviceStatus) } From 3c4f73ab3fa92047839877731c23c70e466ac60c Mon Sep 17 00:00:00 2001 From: Jesse Johnston Date: Thu, 16 Jun 2022 17:00:57 -0700 Subject: [PATCH 10/10] Ignore battery status for FW version < 0.8.0 --- Sources/CombustionBLE/DeviceManager.swift | 7 +--- Sources/CombustionBLE/Probe.swift | 15 +++++-- Sources/CombustionBLE/Utilities/Version.swift | 42 +++++++++++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 Sources/CombustionBLE/Utilities/Version.swift diff --git a/Sources/CombustionBLE/DeviceManager.swift b/Sources/CombustionBLE/DeviceManager.swift index 81e6132..c14d75d 100644 --- a/Sources/CombustionBLE/DeviceManager.swift +++ b/Sources/CombustionBLE/DeviceManager.swift @@ -250,15 +250,10 @@ extension DeviceManager : BleManagerDelegate { // TODO : remove this at some point // Prior to v0.8.0, the firmware did not support the Session ID command // Therefore, add a hardcoded session for backwards compatibility - - // Remove git hash from debug versions - let splitVersion = fwVersion.split(separator: "-") - - if(splitVersion.first?.compare("v0.8.0", options: String.CompareOptions.numeric, range: nil, locale: nil) == .orderedAscending) { + if(Version.isBefore(deviceFirmware: fwVersion, comparison: "v0.8.0")) { let fakeSessionInfo = SessionInformation(sessionID: 0, samplePeriod: 1000) updateDeviceWithSessionInformation(identifier: identifier, sessionInformation: fakeSessionInfo) } - } } diff --git a/Sources/CombustionBLE/Probe.swift b/Sources/CombustionBLE/Probe.swift index 85a0580..f472704 100644 --- a/Sources/CombustionBLE/Probe.swift +++ b/Sources/CombustionBLE/Probe.swift @@ -45,7 +45,7 @@ public class Probe : Device { @Published public private(set) var id: ProbeID @Published public private(set) var color: ProbeColor - @Published public private(set) var batteryStatus: BatteryStatus + @Published public private(set) var batteryStatus: BatteryStatus = .OK private var sessionInformation: SessionInformation? @@ -74,7 +74,6 @@ public class Probe : Device { serialNumber = advertising.serialNumber id = advertising.id color = advertising.color - batteryStatus = advertising.batteryStatus super.init(identifier: identifier, RSSI: RSSI) @@ -99,6 +98,14 @@ public class Probe : Device { super.updateDeviceStale() } + + private func setBatteryStatus(_ batteryStatus: BatteryStatus) { + // Prior to v0.8.0, the firmware did not support battery status + // Therefore, ignore battery status if version < v0.8.0 + if let fwVersion = firmareVersion, !Version.isBefore(deviceFirmware: fwVersion, comparison: "v0.8.0") { + self.batteryStatus = batteryStatus + } + } } extension Probe { @@ -121,7 +128,7 @@ extension Probe { id = advertising.id color = advertising.color - batteryStatus = advertising.batteryStatus + setBatteryStatus(advertising.batteryStatus) lastUpdateTime = Date() } @@ -132,7 +139,7 @@ extension Probe { maxSequenceNumber = deviceStatus.maxSequenceNumber id = deviceStatus.id color = deviceStatus.color - batteryStatus = deviceStatus.batteryStatus + setBatteryStatus(deviceStatus.batteryStatus) if(deviceStatus.mode == .Normal) { currentTemperatures = deviceStatus.temperatures diff --git a/Sources/CombustionBLE/Utilities/Version.swift b/Sources/CombustionBLE/Utilities/Version.swift new file mode 100644 index 0000000..cf04e1f --- /dev/null +++ b/Sources/CombustionBLE/Utilities/Version.swift @@ -0,0 +1,42 @@ +// Version.swift + +/*-- +MIT License + +Copyright (c) 2021 Combustion Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--*/ +import Foundation + +class Version { + + /// Compare device firmware versions. + /// - param deviceFirmware: Device firmware version + /// - param comparison: Version to compare against + /// - returns: true if deviceFirmware < comparison + static func isBefore(deviceFirmware: String, comparison: String) -> Bool { + // Remove git hash from debug versions + let splitVersion = deviceFirmware.split(separator: "-") + + return splitVersion.first?.compare(comparison, options: String.CompareOptions.numeric, range: nil, locale: nil) == .orderedAscending + } + + +}