Basic display of info

This commit is contained in:
Christoph Hagen
2023-06-03 08:15:00 +02:00
parent 0f97bfc316
commit 6e0910e47f
16 changed files with 1417 additions and 12 deletions

View 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
}
}
}

View 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)
}
}