Basic display of info
This commit is contained in:
204
TempTrack/Bluetooth/BluetoothClient.swift
Normal file
204
TempTrack/Bluetooth/BluetoothClient.swift
Normal file
@@ -0,0 +1,204 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum BluetoothResponseType: UInt8 {
|
||||
|
||||
/// The response to the last request is provided
|
||||
case success = 0
|
||||
|
||||
/// Invalid command received
|
||||
case invalidCommand = 1
|
||||
|
||||
case responseTooLarge = 2
|
||||
|
||||
case unknownCommand = 3
|
||||
|
||||
case invalidNumberOfBytesToDelete = 4
|
||||
}
|
||||
|
||||
final class BluetoothClient: ObservableObject {
|
||||
|
||||
private let updateInterval = 3.0
|
||||
|
||||
private let connection = DeviceManager()
|
||||
|
||||
private let recorder = TemperatureStorage()
|
||||
|
||||
init(deviceInfo: DeviceInfo? = nil) {
|
||||
connection.delegate = self
|
||||
self.deviceInfo = deviceInfo
|
||||
}
|
||||
|
||||
func connect() -> Bool {
|
||||
connection.connect()
|
||||
}
|
||||
|
||||
@Published
|
||||
private(set) var deviceState: DeviceState = .disconnected {
|
||||
didSet {
|
||||
print("State: \(deviceState.text)")
|
||||
if case .configured = deviceState {
|
||||
startRegularUpdates()
|
||||
} else {
|
||||
endRegularUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published
|
||||
private(set) var deviceInfo: DeviceInfo?
|
||||
|
||||
private var openRequests: [BluetoothRequest] = []
|
||||
|
||||
private var runningRequest: BluetoothRequest?
|
||||
|
||||
private var runningTransfer: TemperatureDataTransfer?
|
||||
|
||||
func updateDeviceInfo() {
|
||||
addRequest(.getInfo)
|
||||
}
|
||||
|
||||
private var dataUpdateTimer: Timer?
|
||||
|
||||
private func startRegularUpdates() {
|
||||
guard dataUpdateTimer == nil else {
|
||||
return
|
||||
}
|
||||
print("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
|
||||
print("Ending updates")
|
||||
}
|
||||
|
||||
private func performNextRequest() {
|
||||
guard runningRequest == nil else {
|
||||
return
|
||||
}
|
||||
guard !openRequests.isEmpty else {
|
||||
return
|
||||
}
|
||||
let next = openRequests.removeFirst()
|
||||
//print("Starting request \(next)")
|
||||
|
||||
guard connection.send(next.serialized) else {
|
||||
print("Failed to start request \(next)")
|
||||
performNextRequest()
|
||||
return
|
||||
}
|
||||
runningRequest = next
|
||||
}
|
||||
|
||||
func addRequest(_ request: BluetoothRequest) {
|
||||
openRequests.append(request)
|
||||
performNextRequest()
|
||||
}
|
||||
|
||||
|
||||
func collectRecordedData() -> Bool {
|
||||
guard let info = deviceInfo else {
|
||||
return false
|
||||
}
|
||||
let transfer = TemperatureDataTransfer(info: info)
|
||||
runningTransfer = transfer
|
||||
runningTransfer?.delegate = recorder
|
||||
let next = transfer.nextRequest()
|
||||
addRequest(next)
|
||||
return true
|
||||
}
|
||||
|
||||
private func didReceive(data: Data, offset: Int, count: Int) {
|
||||
guard let runningTransfer else {
|
||||
return // TODO: Start new transfer?
|
||||
}
|
||||
runningTransfer.add(data: data, offset: offset, count: count)
|
||||
continueTransfer()
|
||||
}
|
||||
|
||||
private func continueTransfer() {
|
||||
guard let runningTransfer else {
|
||||
return // TODO: Start new transfer?
|
||||
}
|
||||
let next = runningTransfer.nextRequest()
|
||||
addRequest(next)
|
||||
}
|
||||
|
||||
private func decode(info: Data) {
|
||||
guard let newInfo = DeviceInfo(info: info) else {
|
||||
return
|
||||
}
|
||||
self.deviceInfo = newInfo
|
||||
guard let runningTransfer else {
|
||||
return
|
||||
}
|
||||
runningTransfer.update(info: newInfo)
|
||||
let next = runningTransfer.nextRequest()
|
||||
addRequest(next)
|
||||
}
|
||||
}
|
||||
|
||||
extension BluetoothClient: DeviceManagerDelegate {
|
||||
|
||||
func deviceManager(didReceive data: Data) {
|
||||
defer {
|
||||
self.runningRequest = nil
|
||||
performNextRequest()
|
||||
}
|
||||
guard let runningRequest else {
|
||||
print("No request active, but \(data) received")
|
||||
return
|
||||
}
|
||||
|
||||
guard data.count > 0 else {
|
||||
print("No response data for request \(runningRequest)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let type = BluetoothResponseType(rawValue: data[0]) else {
|
||||
print("Unknown response \(data[0]) for request \(runningRequest)")
|
||||
return
|
||||
}
|
||||
guard type == .success else {
|
||||
print("Error 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
|
||||
// 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)
|
||||
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:
|
||||
runningTransfer?.completeTransfer()
|
||||
runningTransfer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func deviceManager(didChangeState state: DeviceState) {
|
||||
DispatchQueue.main.async {
|
||||
self.deviceState = state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
86
TempTrack/Bluetooth/BluetoothRequest.swift
Normal file
86
TempTrack/Bluetooth/BluetoothRequest.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Int {
|
||||
|
||||
var twoByteData: Data {
|
||||
let value = UInt16(clamping: self)
|
||||
return Data([value.low, value.high])
|
||||
}
|
||||
}
|
||||
|
||||
private extension UInt16 {
|
||||
|
||||
var low: UInt8 {
|
||||
UInt8(self & 0xFF)
|
||||
}
|
||||
|
||||
var high: UInt8 {
|
||||
UInt8((self >> 8) & 0xFF)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user