Fix transfer errors, save raw data
This commit is contained in:
@@ -1,304 +0,0 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
/*
|
||||
final class BluetoothClient: ObservableObject {
|
||||
|
||||
private let updateInterval = 3.0
|
||||
|
||||
private let minimumOffsetToUpdateDeviceClock = 5.0
|
||||
|
||||
private let connection = DeviceManager()
|
||||
|
||||
private let storage: PersistentStorage
|
||||
|
||||
var hasInfo: Bool {
|
||||
deviceInfo != nil
|
||||
}
|
||||
|
||||
var isConnected: Bool {
|
||||
if case .configured = deviceState {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var isUpdatingFlag = false
|
||||
|
||||
@Published
|
||||
var shouldConnect: Bool {
|
||||
didSet {
|
||||
isUpdatingFlag = true
|
||||
connection.shouldConnectIfPossible = shouldConnect
|
||||
log.info("Should connect: \(shouldConnect)")
|
||||
isUpdatingFlag = false
|
||||
}
|
||||
}
|
||||
|
||||
init(storage: PersistentStorage, shouldConnect: Bool = false, deviceInfo: DeviceInfo? = nil) {
|
||||
self.storage = storage
|
||||
self.deviceInfo = deviceInfo
|
||||
self.shouldConnect = shouldConnect
|
||||
connection.shouldConnectIfPossible = shouldConnect
|
||||
connection.delegate = self
|
||||
}
|
||||
|
||||
func connect() -> Bool {
|
||||
connection.connect()
|
||||
}
|
||||
|
||||
@Published
|
||||
private(set) var deviceState: DeviceState = .disconnected {
|
||||
didSet {
|
||||
log.info("State: \(deviceState)")
|
||||
if case .configured = deviceState {
|
||||
startRegularUpdates()
|
||||
} else {
|
||||
endRegularUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published
|
||||
private(set) var deviceInfo: DeviceInfo? {
|
||||
didSet {
|
||||
// collectRecordedData()
|
||||
if let deviceInfo, let runningTransfer {
|
||||
runningTransfer.update(info: deviceInfo)
|
||||
let next = runningTransfer.nextRequest()
|
||||
addRequest(next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var openRequests: [BluetoothRequest] = []
|
||||
|
||||
private var runningRequest: BluetoothRequest?
|
||||
|
||||
private var runningTransfer: TemperatureDataTransfer?
|
||||
|
||||
// MARK: Regular updates
|
||||
|
||||
func updateDeviceInfo() {
|
||||
guard case .configured = deviceState else {
|
||||
return
|
||||
}
|
||||
addRequest(.getInfo)
|
||||
|
||||
}
|
||||
|
||||
private var dataUpdateTimer: Timer?
|
||||
|
||||
private func startRegularUpdates() {
|
||||
guard dataUpdateTimer == nil else {
|
||||
return
|
||||
}
|
||||
log.info("Starting updates")
|
||||
dataUpdateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] timer in
|
||||
guard let self = self else {
|
||||
timer.invalidate()
|
||||
return
|
||||
}
|
||||
self.updateDeviceInfo()
|
||||
}
|
||||
|
||||
dataUpdateTimer?.fire()
|
||||
}
|
||||
|
||||
private func endRegularUpdates() {
|
||||
guard let dataUpdateTimer else {
|
||||
return
|
||||
}
|
||||
dataUpdateTimer.invalidate()
|
||||
runningRequest = nil
|
||||
self.dataUpdateTimer = nil
|
||||
log.info("Ending updates")
|
||||
}
|
||||
|
||||
// MARK: Requests
|
||||
|
||||
func clearDeviceStorage() {
|
||||
guard let count = deviceInfo?.numberOfRecordedBytes else {
|
||||
log.info("Can't clear device data without device info")
|
||||
return
|
||||
}
|
||||
addRequest(.clearRecordingBuffer(byteCount: count))
|
||||
}
|
||||
|
||||
private func performNextRequest() {
|
||||
guard runningRequest == nil else {
|
||||
return
|
||||
}
|
||||
guard !openRequests.isEmpty else {
|
||||
return
|
||||
}
|
||||
let next = openRequests.removeFirst()
|
||||
|
||||
guard connection.send(next.serialized) else {
|
||||
log.warning("Failed to start request \(next)")
|
||||
performNextRequest()
|
||||
return
|
||||
}
|
||||
runningRequest = next
|
||||
}
|
||||
|
||||
func addRequest(_ request: BluetoothRequest) {
|
||||
defer {
|
||||
performNextRequest()
|
||||
}
|
||||
let type = request.byte
|
||||
if let runningRequest, runningRequest.byte == type {
|
||||
log.info("Skipping duplicate request \(request)")
|
||||
return
|
||||
}
|
||||
guard !openRequests.contains(where: { $0.byte == type }) else {
|
||||
log.info("Skipping duplicate request \(request)")
|
||||
return
|
||||
}
|
||||
openRequests.append(request)
|
||||
}
|
||||
|
||||
// MARK: Data transfer
|
||||
|
||||
@discardableResult
|
||||
func collectRecordedData() -> Bool {
|
||||
guard runningTransfer == nil else {
|
||||
log.info("Transfer already running")
|
||||
return false
|
||||
}
|
||||
guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else {
|
||||
log.info("Transfer already scheduled")
|
||||
return false
|
||||
}
|
||||
guard let info = deviceInfo else {
|
||||
log.warning("No device info to start transfer")
|
||||
return false
|
||||
}
|
||||
guard info.numberOfStoredMeasurements > 0 else {
|
||||
return false
|
||||
}
|
||||
let transfer = TemperatureDataTransfer(info: info, previous: storage.lastDeviceTime)
|
||||
runningTransfer = transfer
|
||||
let next = transfer.nextRequest()
|
||||
log.info("Starting transfer")
|
||||
addRequest(next)
|
||||
return true
|
||||
}
|
||||
|
||||
private func didReceive(data: Data, offset: Int, count: Int) {
|
||||
guard let runningTransfer else {
|
||||
log.warning("No running transfer to process device data")
|
||||
self.runningRequest = nil
|
||||
return // TODO: Start new transfer?
|
||||
}
|
||||
guard runningTransfer.add(data: data, offset: offset, count: count) else {
|
||||
self.runningRequest = nil
|
||||
return // TODO: Start new transfer
|
||||
}
|
||||
let next = runningTransfer.nextRequest()
|
||||
addRequest(next)
|
||||
}
|
||||
|
||||
private func decode(info: Data) {
|
||||
guard let newInfo = try? DeviceInfo(info: info) else {
|
||||
log.error("Failed to decode device info")
|
||||
return
|
||||
}
|
||||
self.deviceInfo = newInfo
|
||||
}
|
||||
}
|
||||
|
||||
extension BluetoothClient: DeviceManagerDelegate {
|
||||
|
||||
func deviceManager(shouldConnectToDevice: Bool) {
|
||||
guard !isUpdatingFlag else {
|
||||
return
|
||||
}
|
||||
self.shouldConnect = shouldConnectToDevice
|
||||
}
|
||||
|
||||
func deviceManager(didReceive data: Data) {
|
||||
defer {
|
||||
performNextRequest()
|
||||
}
|
||||
guard let runningRequest else {
|
||||
log.warning("No request active, but \(data) received")
|
||||
return
|
||||
}
|
||||
self.runningRequest = nil
|
||||
|
||||
guard data.count > 0 else {
|
||||
log.error("No response data for request \(runningRequest)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let type = BluetoothResponseType(rawValue: data[0]) else {
|
||||
log.error("Unknown response \(data[0]) for request \(runningRequest)")
|
||||
return
|
||||
}
|
||||
switch type {
|
||||
case .success:
|
||||
break
|
||||
case .responseInProgress:
|
||||
log.info("Device is busy for \(runningRequest)")
|
||||
// Retry the request
|
||||
addRequest(runningRequest)
|
||||
return
|
||||
case .invalidNumberOfBytesToDelete:
|
||||
guard case .clearRecordingBuffer = runningRequest else {
|
||||
log.error("Request \(runningRequest) received non-matching response about number of bytes to delete")
|
||||
return
|
||||
}
|
||||
// If clearing the recording buffer fails due to byte mismatch,
|
||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
||||
addRequest(.getInfo)
|
||||
|
||||
case .responseTooLarge:
|
||||
guard case .getRecordingData = runningRequest else {
|
||||
log.error("Unexpectedly exceeded payload size for request \(runningRequest)")
|
||||
return
|
||||
}
|
||||
// If requesting bytes fails due to the response size,
|
||||
// then requesting new info will update the response size, and the transfer will be resumed
|
||||
addRequest(.getInfo)
|
||||
default:
|
||||
log.error("Unknown response \(data[0]) for request \(runningRequest)")
|
||||
// If clearing the recording buffer fails due to byte mismatch,
|
||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
||||
|
||||
addRequest(.getInfo)
|
||||
return
|
||||
|
||||
}
|
||||
let payload = data.dropFirst()
|
||||
|
||||
switch runningRequest {
|
||||
case .getInfo:
|
||||
decode(info: payload)
|
||||
case .getRecordingData(let offset, let count):
|
||||
didReceive(data: payload, offset: offset, count: count)
|
||||
case .clearRecordingBuffer:
|
||||
didClearDeviceStorage()
|
||||
}
|
||||
}
|
||||
|
||||
private func didClearDeviceStorage() {
|
||||
guard let runningTransfer else {
|
||||
log.warning("No running transfer after clearing device storage")
|
||||
return
|
||||
}
|
||||
defer { self.runningTransfer = nil }
|
||||
guard runningTransfer.completeTransfer() else {
|
||||
return
|
||||
}
|
||||
storage.add(runningTransfer.measurements)
|
||||
storage.lastDeviceTime = runningTransfer.time
|
||||
}
|
||||
|
||||
func deviceManager(didChangeState state: DeviceState) {
|
||||
DispatchQueue.main.async {
|
||||
self.deviceState = state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
@@ -1,67 +0,0 @@
|
||||
import Foundation
|
||||
/*
|
||||
enum BluetoothRequest {
|
||||
/**
|
||||
* Request the number of bytes already recorded
|
||||
*
|
||||
* Request:
|
||||
* - No additional bytes expected
|
||||
*
|
||||
* Response:
|
||||
* - `BluetoothResponseType.success`
|
||||
* - the number of recorded bytes as a `Uint16` (2 bytes)
|
||||
* - the number of seconds until the next measurement as a `Uint16` (2 bytes)
|
||||
* - the number of seconds between measurements as a `Uint16` (2 bytes)
|
||||
* - the number of measurements as a `Uint16` (2 bytes)
|
||||
* - the maximum number of bytes to request as a `Uint16` (2 bytes)
|
||||
* - the number of seconds since power on as a `Uint32` (4 bytes)
|
||||
*/
|
||||
case getInfo
|
||||
|
||||
/**
|
||||
* Request recording data
|
||||
*
|
||||
* Request:
|
||||
* - Bytes 1-2: Memory offset (`UInt16`)
|
||||
* - Bytes 3-4: Number of bytes (`UInt16`)
|
||||
*
|
||||
* Response:
|
||||
* - `BluetoothResponseType.success`, plus the requested bytes
|
||||
* - `BluetoothResponseType.responseTooLarge` if too many bytes are requested
|
||||
*/
|
||||
case getRecordingData(offset: Int, count: Int)
|
||||
|
||||
/**
|
||||
* Request deletion of recordings
|
||||
*
|
||||
* Request:
|
||||
* - Bytes 1-2: Number of bytes to clear (uint16_t)
|
||||
*
|
||||
* Response:
|
||||
* - `BluetoothResponseType.success`
|
||||
* - `BluetoothResponseType.invalidNumberOfBytesToDelete`, if the number of bytes does not match.
|
||||
* This may happen when a new temperature recording is performed in between calls
|
||||
*/
|
||||
case clearRecordingBuffer(byteCount: Int)
|
||||
|
||||
var serialized: Data {
|
||||
let firstByte = Data([byte])
|
||||
switch self {
|
||||
case .getInfo:
|
||||
return firstByte
|
||||
case .getRecordingData(let offset, let count):
|
||||
return firstByte + count.twoByteData + offset.twoByteData
|
||||
case .clearRecordingBuffer(let byteCount):
|
||||
return firstByte + byteCount.twoByteData
|
||||
}
|
||||
}
|
||||
|
||||
var byte: UInt8 {
|
||||
switch self {
|
||||
case .getInfo: return 0
|
||||
case .getRecordingData: return 1
|
||||
case .clearRecordingBuffer: return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
@@ -2,15 +2,15 @@ import Foundation
|
||||
|
||||
struct DeviceInfo {
|
||||
|
||||
/**
|
||||
The maximum factor by which the device clock can run
|
||||
*/
|
||||
private let maximumTimeDilationFactor: Double = 0.01
|
||||
|
||||
/// The unique ID generated by the device to distinguish between power cycles
|
||||
let uniqueIdOfPowerCycle: Int
|
||||
|
||||
/// The number of bytes recorded by the tracker
|
||||
let numberOfRecordedBytes: Int
|
||||
|
||||
|
||||
/// The sum of all recorded bytes
|
||||
let dataChecksum: UInt16
|
||||
|
||||
/// The number of measurements already performed
|
||||
let numberOfStoredMeasurements: Int
|
||||
|
||||
@@ -46,54 +46,15 @@ struct DeviceInfo {
|
||||
time.nextMeasurement.addingTimeInterval(-Double(numberOfStoredMeasurements * measurementInterval))
|
||||
}
|
||||
|
||||
func estimatedTimeDilation(to previous: DeviceTime?) -> (start: Date, dilation: Double) {
|
||||
let trivialResult = (start: currentMeasurementStartTime, dilation: 1.0)
|
||||
guard let previous else {
|
||||
log.info("No previous device time to compare")
|
||||
return trivialResult
|
||||
}
|
||||
// Check if device was restarted in between
|
||||
guard time.secondsSincePowerOn >= previous.secondsSincePowerOn else {
|
||||
log.info("Device restarted (runtime decreased from \(previous.secondsSincePowerOn) to \(time.secondsSincePowerOn))")
|
||||
return trivialResult
|
||||
}
|
||||
let newMeasurementCount = time.totalNumberOfMeasurements - previous.totalNumberOfMeasurements
|
||||
guard newMeasurementCount >= 0 else {
|
||||
log.info("Device restarted (measurements decreased from \(previous.totalNumberOfMeasurements) to \(time.totalNumberOfMeasurements))")
|
||||
return trivialResult
|
||||
}
|
||||
guard newMeasurementCount > 0 else {
|
||||
log.warning("No new measurements to calculate time difference")
|
||||
return trivialResult
|
||||
}
|
||||
|
||||
// Check that no measurements are missing
|
||||
|
||||
// Calculate the difference between the expected time for the next measurement and the device time
|
||||
let deviceTimeDifference = Double(newMeasurementCount * measurementInterval)
|
||||
let expectedNextMeasurement = previous.nextMeasurement.addingTimeInterval(deviceTimeDifference)
|
||||
let timeDifference = time.nextMeasurement.timeIntervalSince(expectedNextMeasurement)
|
||||
|
||||
let realTimeDifference = time.nextMeasurement.timeIntervalSince(previous.nextMeasurement)
|
||||
let timeDilation = realTimeDifference / deviceTimeDifference
|
||||
|
||||
log.info("Device time dilation \(timeDilation) (difference \(timeDifference))")
|
||||
|
||||
guard abs(timeDilation - 1.0) < maximumTimeDilationFactor else {
|
||||
log.warning("Device time too different from expected value (difference \(timeDifference) s)")
|
||||
return (currentMeasurementStartTime, 1.0)
|
||||
}
|
||||
return (previous.nextMeasurement, timeDilation)
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceInfo {
|
||||
|
||||
init(info: Data) throws {
|
||||
let date = Date()
|
||||
|
||||
var data = info
|
||||
|
||||
|
||||
self.uniqueIdOfPowerCycle = try data.decodeFourByteInteger()
|
||||
self.numberOfRecordedBytes = try data.decodeTwoByteInteger()
|
||||
let secondsUntilNextMeasurement = try data.decodeTwoByteInteger()
|
||||
self.measurementInterval = try data.decodeTwoByteInteger()
|
||||
@@ -102,16 +63,24 @@ extension DeviceInfo {
|
||||
self.transferBlockSize = try data.decodeTwoByteInteger()
|
||||
self.storageSize = try data.decodeTwoByteInteger()
|
||||
let secondsSincePowerOn = try data.decodeFourByteInteger()
|
||||
let startSecondsOfCurrentRecording = try data.decodeFourByteInteger()
|
||||
self.dataChecksum = try data.decodeUInt16()
|
||||
self.wakeupReason = .init(rawValue: try data.getByte()) ?? .WAKEUP_UNDEFINED
|
||||
|
||||
self.sensor0 = try data.decodeSensor()
|
||||
self.sensor1 = try data.decodeSensor()
|
||||
|
||||
guard data.isEmpty else {
|
||||
log.error("\(data.count) bytes remaining in device info buffer")
|
||||
throw DeviceInfoError.missingData
|
||||
}
|
||||
|
||||
self.time = .init(
|
||||
date: date,
|
||||
secondsSincePowerOn: secondsSincePowerOn,
|
||||
totalNumberOfMeasurements: totalNumberOfMeasurements,
|
||||
secondsUntilNextMeasurement: secondsUntilNextMeasurement)
|
||||
let _ = try data.decodeFourByteInteger()
|
||||
self.sensor0 = try data.decodeSensor()
|
||||
self.sensor1 = try data.decodeSensor()
|
||||
self.wakeupReason = .init(rawValue: try data.getByte()) ?? .WAKEUP_UNDEFINED
|
||||
secondsUntilNextMeasurement: secondsUntilNextMeasurement,
|
||||
secondsOfFirstMeasurement: startSecondsOfCurrentRecording)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +88,9 @@ extension DeviceInfo {
|
||||
|
||||
static var mock: DeviceInfo {
|
||||
.init(
|
||||
uniqueIdOfPowerCycle: .random(in: 0...Int(UInt32.max)),
|
||||
numberOfRecordedBytes: 123,
|
||||
dataChecksum: .random(in: .min...UInt16.max),
|
||||
numberOfStoredMeasurements: 234,
|
||||
measurementInterval: 60,
|
||||
sensor0: .init(address: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], value: .value(21.0), date: .now.addingTimeInterval(-2)),
|
||||
@@ -130,3 +101,7 @@ extension DeviceInfo {
|
||||
transferBlockSize: 180)
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceInfo: Codable {
|
||||
|
||||
}
|
||||
|
@@ -1,258 +0,0 @@
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
||||
|
||||
static let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")
|
||||
|
||||
static let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002")
|
||||
|
||||
private var manager: CBCentralManager! = nil
|
||||
|
||||
private(set) var lastRSSI: Int = 0
|
||||
|
||||
weak var delegate: DeviceManagerDelegate?
|
||||
|
||||
var state: DeviceState = .disconnected {
|
||||
didSet {
|
||||
delegate?.deviceManager(didChangeState: state)
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
self.manager = CBCentralManager(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func connect() -> Bool {
|
||||
switch state {
|
||||
case .bluetoothDisabled:
|
||||
log.info("Can't connect, bluetooth disabled")
|
||||
return false
|
||||
case .disconnected:
|
||||
break
|
||||
default:
|
||||
return true
|
||||
}
|
||||
guard !manager.isScanning else {
|
||||
state = .scanning
|
||||
return true
|
||||
}
|
||||
if !shouldConnectIfPossible {
|
||||
shouldConnectIfPossible = true
|
||||
}
|
||||
state = .scanning
|
||||
manager.scanForPeripherals(withServices: [DeviceManager.serviceUUID])
|
||||
return true
|
||||
}
|
||||
|
||||
var shouldConnectIfPossible = true {
|
||||
didSet {
|
||||
updateConnectionOnChange()
|
||||
delegate?.deviceManager(shouldConnectToDevice: shouldConnectIfPossible)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateConnectionOnChange() {
|
||||
if shouldConnectIfPossible {
|
||||
ensureConnection()
|
||||
} else {
|
||||
disconnectIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureConnection() {
|
||||
switch state {
|
||||
case .disconnected:
|
||||
connect()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func disconnectIfNeeded() {
|
||||
switch state {
|
||||
case .bluetoothDisabled, .disconnected:
|
||||
return
|
||||
default:
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
if shouldConnectIfPossible {
|
||||
shouldConnectIfPossible = false
|
||||
}
|
||||
switch state {
|
||||
case .bluetoothDisabled, .disconnected:
|
||||
return
|
||||
case .scanning:
|
||||
manager.stopScan()
|
||||
state = .disconnected
|
||||
return
|
||||
case .connecting(let device),
|
||||
.discoveringCharacteristic(let device),
|
||||
.discoveringServices(device: let device),
|
||||
.configured(let device, _):
|
||||
manager.cancelPeripheralConnection(device)
|
||||
manager.stopScan()
|
||||
state = .disconnected
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func send(_ data: Data) -> Bool {
|
||||
guard case .configured(let device, let characteristic) = state else {
|
||||
return false
|
||||
}
|
||||
device.writeValue(data, for: characteristic, type: .withResponse)
|
||||
return self.read()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func read() -> Bool {
|
||||
guard case .configured(let device, let characteristic) = state else {
|
||||
return false
|
||||
}
|
||||
device.readValue(for: characteristic)
|
||||
return true
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||
guard shouldConnectIfPossible else {
|
||||
return
|
||||
}
|
||||
peripheral.delegate = self
|
||||
manager.connect(peripheral)
|
||||
manager.stopScan()
|
||||
state = .connecting(device: peripheral)
|
||||
}
|
||||
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
switch central.state {
|
||||
case .poweredOff:
|
||||
state = .bluetoothDisabled
|
||||
case .poweredOn:
|
||||
state = .disconnected
|
||||
connect()
|
||||
case .unsupported:
|
||||
state = .bluetoothDisabled
|
||||
log.info("Bluetooth is not supported")
|
||||
case .unknown:
|
||||
state = .bluetoothDisabled
|
||||
log.info("Bluetooth state is unknown")
|
||||
case .resetting:
|
||||
state = .bluetoothDisabled
|
||||
log.info("Bluetooth is resetting")
|
||||
case .unauthorized:
|
||||
state = .bluetoothDisabled
|
||||
log.info("Bluetooth is not authorized")
|
||||
@unknown default:
|
||||
state = .bluetoothDisabled
|
||||
log.warning("Unknown state \(central.state)")
|
||||
}
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
log.info("Connected to " + peripheral.name!)
|
||||
peripheral.discoverServices([DeviceManager.serviceUUID])
|
||||
state = .discoveringServices(device: peripheral)
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||
log.info("Disconnected from " + peripheral.name!)
|
||||
state = .disconnected
|
||||
// Attempt to reconnect
|
||||
if shouldConnectIfPossible {
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||
log.warning("Failed to connect device '\(peripheral.name ?? "NO_NAME")'")
|
||||
if let error = error {
|
||||
log.warning(error.localizedDescription)
|
||||
}
|
||||
state = manager.isScanning ? .scanning : .disconnected
|
||||
// Attempt to reconnect
|
||||
if shouldConnectIfPossible {
|
||||
connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceManager: CBPeripheralDelegate {
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
guard let services = peripheral.services, !services.isEmpty else {
|
||||
log.error("No services found for device '\(peripheral.name ?? "NO_NAME")'")
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
return
|
||||
}
|
||||
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else {
|
||||
log.error("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})")
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
return
|
||||
}
|
||||
peripheral.discoverCharacteristics([DeviceManager.characteristicUUID], for: service)
|
||||
state = .discoveringCharacteristic(device: peripheral)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
if let error = error {
|
||||
log.error("Failed to discover characteristics: \(error)")
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
return
|
||||
}
|
||||
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
|
||||
log.error("No characteristics found for device")
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
return
|
||||
}
|
||||
for characteristic in characteristics {
|
||||
guard characteristic.uuid == DeviceManager.characteristicUUID else {
|
||||
log.warning("Unused characteristic \(characteristic.uuid.uuidString)")
|
||||
continue
|
||||
}
|
||||
state = .configured(device: peripheral, characteristic: characteristic)
|
||||
peripheral.setNotifyValue(true, for: characteristic)
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
if let error = error {
|
||||
log.error("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
||||
if let error = error {
|
||||
log.warning("Failed to get RSSI: \(error)")
|
||||
return
|
||||
}
|
||||
lastRSSI = RSSI.intValue
|
||||
log.info("RSSI: \(lastRSSI)")
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
if let error = error {
|
||||
log.error("Failed to read value update: \(error)")
|
||||
return
|
||||
}
|
||||
guard case .configured(device: _, characteristic: let storedCharacteristic) = state else {
|
||||
log.warning("Received data while not properly configured")
|
||||
return
|
||||
}
|
||||
guard characteristic.uuid == storedCharacteristic.uuid else {
|
||||
log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)")
|
||||
return
|
||||
}
|
||||
guard let data = characteristic.value else {
|
||||
log.warning("No data")
|
||||
return
|
||||
}
|
||||
delegate?.deviceManager(didReceive: data)
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
protocol DeviceManagerDelegate: AnyObject {
|
||||
|
||||
func deviceManager(shouldConnectToDevice: Bool)
|
||||
|
||||
func deviceManager(didReceive data: Data)
|
||||
|
||||
func deviceManager(didChangeState state: DeviceState)
|
||||
}
|
@@ -1,82 +0,0 @@
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
enum DeviceState {
|
||||
|
||||
case bluetoothDisabled
|
||||
|
||||
case scanning
|
||||
|
||||
case connecting(device: CBPeripheral)
|
||||
|
||||
case discoveringServices(device: CBPeripheral)
|
||||
|
||||
case discoveringCharacteristic(device: CBPeripheral)
|
||||
|
||||
case configured(device: CBPeripheral, characteristic: CBCharacteristic)
|
||||
|
||||
case disconnected
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .bluetoothDisabled:
|
||||
return "Bluetooth is disabled"
|
||||
case .scanning:
|
||||
return "Scanning..."
|
||||
case .connecting(let device):
|
||||
guard let name = device.name else {
|
||||
return "Connecting..."
|
||||
}
|
||||
return "Connecting to \(name)..."
|
||||
case .discoveringServices:
|
||||
return "Discovering service..."
|
||||
case .discoveringCharacteristic:
|
||||
return "Discovering characteristic..."
|
||||
case .configured(let device, _):
|
||||
guard let name = device.name else {
|
||||
return "Connected"
|
||||
}
|
||||
return name
|
||||
case .disconnected:
|
||||
return "Not connected"
|
||||
}
|
||||
}
|
||||
|
||||
var device: CBPeripheral? {
|
||||
switch self {
|
||||
case .bluetoothDisabled, .disconnected, .scanning:
|
||||
return nil
|
||||
case .connecting(let device),
|
||||
.discoveringCharacteristic(let device),
|
||||
.discoveringServices(device: let device),
|
||||
.configured(let device, _):
|
||||
return device
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceState: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .bluetoothDisabled:
|
||||
return "Bluetooth disabled"
|
||||
case .scanning:
|
||||
return "Searching for device"
|
||||
case .connecting:
|
||||
return "Connecting to device"
|
||||
case .discoveringServices:
|
||||
return "Discovering services"
|
||||
case .discoveringCharacteristic:
|
||||
return "Discovering characteristics"
|
||||
case .configured:
|
||||
return "Connected"
|
||||
case .disconnected:
|
||||
return "Disconnected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceState: Equatable {
|
||||
|
||||
}
|
@@ -10,6 +10,8 @@ struct DeviceTime {
|
||||
|
||||
let secondsUntilNextMeasurement: Int
|
||||
|
||||
let secondsOfFirstMeasurement: Int
|
||||
|
||||
var nextMeasurement: Date {
|
||||
date.adding(seconds: secondsUntilNextMeasurement)
|
||||
}
|
||||
@@ -47,6 +49,7 @@ extension DeviceTime: Codable {
|
||||
self.secondsSincePowerOn = try container.decode(Int.self)
|
||||
self.totalNumberOfMeasurements = try container.decode(Int.self)
|
||||
self.secondsUntilNextMeasurement = try container.decode(Int.self)
|
||||
self.secondsOfFirstMeasurement = try container.decode(Int.self)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
@@ -55,6 +58,7 @@ extension DeviceTime: Codable {
|
||||
try container.encode(secondsSincePowerOn)
|
||||
try container.encode(totalNumberOfMeasurements)
|
||||
try container.encode(secondsUntilNextMeasurement)
|
||||
try container.encode(secondsOfFirstMeasurement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +69,7 @@ extension DeviceTime {
|
||||
date: .now,
|
||||
secondsSincePowerOn: 125,
|
||||
totalNumberOfMeasurements: 3,
|
||||
secondsUntilNextMeasurement: 55)
|
||||
secondsUntilNextMeasurement: 55,
|
||||
secondsOfFirstMeasurement: 1)
|
||||
}
|
||||
}
|
||||
|
@@ -75,3 +75,7 @@ extension DeviceWakeCause {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceWakeCause: Codable {
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user