diff --git a/Sources/BluetoothServices/Characteristics/RecordAccess/RecordAccessOpCode.swift b/Sources/BluetoothServices/Characteristics/RecordAccess/RecordAccessOpCode.swift index 087bdab5..d908c0e4 100644 --- a/Sources/BluetoothServices/Characteristics/RecordAccess/RecordAccessOpCode.swift +++ b/Sources/BluetoothServices/Characteristics/RecordAccess/RecordAccessOpCode.swift @@ -60,7 +60,7 @@ public struct RecordAccessOpCode { /// The operator is ``RecordAccessOperator/null``. /// The operand should include information similar to ``RecordAccessGeneralResponse`` (specifically containing an error code /// of ``RecordAccessResponseCode``). - public static let responseCode = RecordAccessOpCode(rawValue: 0x06) + public static let responseCode = RecordAccessOpCode(rawValue: 0x06) // TODO: rename "response"? /// Request a combined report. /// /// After record transmission, the control point responds with the ``responseCode`` code. diff --git a/Sources/BluetoothServices/Characteristics/UserData/UserControlPoint.swift b/Sources/BluetoothServices/Characteristics/UserData/UserControlPoint.swift index 1b88e058..0ba32c6c 100644 --- a/Sources/BluetoothServices/Characteristics/UserData/UserControlPoint.swift +++ b/Sources/BluetoothServices/Characteristics/UserData/UserControlPoint.swift @@ -11,47 +11,36 @@ import NIOCore import SpeziBluetooth -public struct UserControlPoint { - public struct OpCode { - public static let reserved = OpCode(rawValue: 0x00) - public static let registerNewUser = OpCode(rawValue: 0x01) - public static let consent = OpCode(rawValue: 0x02) - public static let deleteUserData = OpCode(rawValue: 0x03) - public static let listAllUsers = OpCode(rawValue: 0x04) // TODO: optional - public static let deleteUser = OpCode(rawValue: 0x05) // TODO: optional - public static let responseCode = OpCode(rawValue: 0x20) - - public let rawValue: UInt8 - - public init(rawValue: UInt8) { - self.rawValue = rawValue - } +public struct UserControlPoint { + public var opCode: UserControlPointOpCode { + parameter.opCode } + public let parameter: Parameter - public let opcode: OpCode - public let parameter: Any // TODO: what? + public init(_ parameter: Parameter) { + self.parameter = parameter + } } -extension UserControlPoint.OpCode: RawRepresentable {} +extension UserControlPoint: Hashable, Sendable {} -extension UserControlPoint.OpCode: Hashable, Sendable {} +extension UserControlPoint: ControlPointCharacteristic {} -extension UserControlPoint.OpCode: ByteCodable { +extension UserControlPoint: ByteCodable { public init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - guard let rawValue = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { + guard let opCode = UserControlPointOpCode(from: &byteBuffer, preferredEndianness: endianness), + let parameter = Parameter(from: &byteBuffer, preferredEndianness: endianness, opCode: opCode) else { return nil } - self.init(rawValue: rawValue) - } + self.init(parameter) + } public func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { - rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + opCode.encode(to: &byteBuffer, preferredEndianness: endianness) // TODO: should we move encoding to Parameter? could avoid custom initializer! + parameter.encode(to: &byteBuffer, preferredEndianness: endianness) } } - - -extension UserControlPoint: ControlPointCharacteristic {} diff --git a/Sources/BluetoothServices/Characteristics/UserData/UserControlPointGenericParameter.swift b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointGenericParameter.swift new file mode 100644 index 00000000..7b0ea1ce --- /dev/null +++ b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointGenericParameter.swift @@ -0,0 +1,101 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import ByteCoding +import NIOCore + + +// TODO: CharacteristicAccessor extensions? + + +public enum UserControlPointGenericParameter { + case registerNewUser(consentCode: UInt16) + case consent(userIndex: UInt8, consentCode: UInt16) + case deleteUserData + case listAllUsers + case deleterUser(userIndex: UInt8) + case response( + requestOpCode: UserControlPointOpCode, + response: UserControlPointResponse + ) +} + + +extension UserControlPointGenericParameter: Hashable, Sendable {} + + +extension UserControlPointGenericParameter: UserControlPointParameter { + public var opCode: UserControlPointOpCode { + switch self { + case .registerNewUser: + return .registerNewUser + case .consent: + return .consent + case .deleteUserData: + return .deleteUserData + case .listAllUsers: + return .listAllUsers + case .deleterUser: + return .deleteUser + case .response: + return .response + } + } + + public init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness, opCode: UserControlPointOpCode) { + switch opCode { + case .registerNewUser: + guard let consentCode = UInt16(from: &byteBuffer, preferredEndianness: endianness) else { + return nil + } + self = .registerNewUser(consentCode: consentCode) + case .consent: + guard let userIndex = UInt8(from: &byteBuffer, preferredEndianness: endianness), + let consentCode = UInt16(from: &byteBuffer, preferredEndianness: endianness) else { + return nil + } + self = .consent(userIndex: userIndex, consentCode: consentCode) + case .deleteUserData: + self = .deleteUserData + case .listAllUsers: + self = .listAllUsers + case .deleteUser: + guard let userIndex = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { + return nil + } + self = .deleterUser(userIndex: userIndex) + case .response: + guard let requestOpCode = UserControlPointOpCode(from: &byteBuffer, preferredEndianness: endianness), + let response = UserControlPointResponse(from: &byteBuffer, preferredEndianness: endianness, requestOpCode: requestOpCode) else { + return nil + } + self = .response(requestOpCode: requestOpCode, response: response) + default: + return nil + } + } + + public func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + switch self { + case let .registerNewUser(consentCode): + consentCode.encode(to: &byteBuffer, preferredEndianness: endianness) + case let .consent(userIndex, consentCode): + userIndex.encode(to: &byteBuffer, preferredEndianness: endianness) + consentCode.encode(to: &byteBuffer, preferredEndianness: endianness) + case .deleteUserData: + break + case .listAllUsers: + break + case let .deleterUser(userIndex): + userIndex.encode(to: &byteBuffer, preferredEndianness: endianness) + case let .response(requestOpCode, response): + requestOpCode.encode(to: &byteBuffer, preferredEndianness: endianness) + response.encode(to: &byteBuffer, preferredEndianness: endianness) + } + } +} diff --git a/Sources/BluetoothServices/Characteristics/UserData/UserControlPointOpCode.swift b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointOpCode.swift new file mode 100644 index 00000000..c2049782 --- /dev/null +++ b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointOpCode.swift @@ -0,0 +1,47 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import ByteCoding +import NIOCore + + +public struct UserControlPointOpCode { + public static let reserved = UserControlPointOpCode(rawValue: 0x00) + public static let registerNewUser = UserControlPointOpCode(rawValue: 0x01) + public static let consent = UserControlPointOpCode(rawValue: 0x02) + public static let deleteUserData = UserControlPointOpCode(rawValue: 0x03) + public static let listAllUsers = UserControlPointOpCode(rawValue: 0x04) // TODO: optional + public static let deleteUser = UserControlPointOpCode(rawValue: 0x05) // TODO: optional + public static let response = UserControlPointOpCode(rawValue: 0x20) + + public let rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } +} + + +extension UserControlPointOpCode: RawRepresentable {} + + +extension UserControlPointOpCode: Hashable, Sendable {} + + +extension UserControlPointOpCode: ByteCodable { + public init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + guard let rawValue = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { + return nil + } + self.init(rawValue: rawValue) + } + + public func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + } +} diff --git a/Sources/BluetoothServices/Characteristics/UserData/UserControlPointParameter.swift b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointParameter.swift new file mode 100644 index 00000000..43af45cb --- /dev/null +++ b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointParameter.swift @@ -0,0 +1,18 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import ByteCoding +import NIOCore + + +public protocol UserControlPointParameter: ByteEncodable, Hashable, Sendable { + var opCode: UserControlPointOpCode { get } + + + init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness, opCode: UserControlPointOpCode) +} diff --git a/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponse.swift b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponse.swift new file mode 100644 index 00000000..8905b169 --- /dev/null +++ b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponse.swift @@ -0,0 +1,90 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import ByteCoding +import NIOCore + + +public enum UserControlPointResponse { + case success(UserControlPointResponseParameter? = nil) + case opCodeNotSupported + case invalidParameter + case operationFailed + case userNotAuthorized + + + public var responseValue: UserControlPointResponseValue { + switch self { + case .success: + return .success + case .opCodeNotSupported: + return .opCodeNotSupported + case .invalidParameter: + return .invalidParameter + case .operationFailed: + return .operationFailed + case .userNotAuthorized: + return .userNotAuthorized + } + } +} + + +extension UserControlPointResponse: Hashable, Sendable {} + + +extension UserControlPointResponse: ByteEncodable { + public init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness, requestOpCode: UserControlPointOpCode) { + guard let responseValue = UserControlPointResponseValue(from: &byteBuffer, preferredEndianness: endianness) else { + return nil + } + + switch responseValue { + case .success: + let parameter: UserControlPointResponseParameter? + switch requestOpCode { + case .registerNewUser, .deleteUser: + guard let userIndex = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { + return nil + } + parameter = .userIndex(userIndex) + case .listAllUsers: + guard let numberOfUsers = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { + return nil + } + parameter = .numberOfUsers(numberOfUsers) + default: + parameter = nil + } + + self = .success(parameter) + case .opCodeNotSupported: + self = .opCodeNotSupported + case .invalidParameter: + self = .invalidParameter + case .operationFailed: + self = .operationFailed + case .userNotAuthorized: + self = .userNotAuthorized + default: + return nil + } + } + + public func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + responseValue.encode(to: &byteBuffer, preferredEndianness: endianness) + switch self { + case let .success(parameter): + if let parameter { + parameter.rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + } + default: + break + } + } +} diff --git a/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponseParameter.swift b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponseParameter.swift new file mode 100644 index 00000000..5efb69ef --- /dev/null +++ b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponseParameter.swift @@ -0,0 +1,29 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import ByteCoding +import NIOCore + + +public enum UserControlPointResponseParameter { + case userIndex(UInt8) + case numberOfUsers(UInt8) + + + public var rawValue: UInt8 { + switch self { + case let .userIndex(value): + return value + case let .numberOfUsers(value): + return value + } + } +} + + +extension UserControlPointResponseParameter: Hashable, Sendable {} diff --git a/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponseValue.swift b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponseValue.swift new file mode 100644 index 00000000..fb085d1b --- /dev/null +++ b/Sources/BluetoothServices/Characteristics/UserData/UserControlPointResponseValue.swift @@ -0,0 +1,46 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import ByteCoding +import NIOCore + + +public struct UserControlPointResponseValue { + public static let reserved = UserControlPointResponseValue(rawValue: 0x00) + public static let success = UserControlPointResponseValue(rawValue: 0x01) + public static let opCodeNotSupported = UserControlPointResponseValue(rawValue: 0x02) + public static let invalidParameter = UserControlPointResponseValue(rawValue: 0x03) + public static let operationFailed = UserControlPointResponseValue(rawValue: 0x04) + public static let userNotAuthorized = UserControlPointResponseValue(rawValue: 0x05) + + public let rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } +} + + +extension UserControlPointResponseValue: RawRepresentable {} + + +extension UserControlPointResponseValue: Hashable, Sendable {} + + +extension UserControlPointResponseValue: ByteCodable { + public init?(from byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + guard let rawValue = UInt8(from: &byteBuffer, preferredEndianness: endianness) else { + return nil + } + self.init(rawValue: rawValue) + } + + public func encode(to byteBuffer: inout ByteBuffer, preferredEndianness endianness: Endianness) { + rawValue.encode(to: &byteBuffer, preferredEndianness: endianness) + } +} diff --git a/Sources/BluetoothServices/Services/UserDataService.swift b/Sources/BluetoothServices/Services/UserDataService.swift index 61ecd97e..124bc844 100644 --- a/Sources/BluetoothServices/Services/UserDataService.swift +++ b/Sources/BluetoothServices/Services/UserDataService.swift @@ -7,6 +7,7 @@ // import class CoreBluetooth.CBUUID +// TODO: import Foundation import SpeziBluetooth @@ -16,8 +17,8 @@ public final class UserDataService: BluetoothService { // TODO: UDS characteristics? - @Characteristic(id: "2A85") - public var dateOfBirth: Date? // TODO: date overload? + // TODO: @Characteristic(id: "2A85") + // TODO: public var dateOfBirth: Date? // TODO: date overload? @Characteristic(id: "2A8C") public var gender: Gender? @Characteristic(id: "2A8E") diff --git a/Tests/BluetoothServicesTests/UserControlPointTests.swift b/Tests/BluetoothServicesTests/UserControlPointTests.swift new file mode 100644 index 00000000..cc75056f --- /dev/null +++ b/Tests/BluetoothServicesTests/UserControlPointTests.swift @@ -0,0 +1,42 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +@_spi(TestingSupport) +@testable import BluetoothServices +import CoreBluetooth +import NIO +@_spi(TestingSupport) +@testable import SpeziBluetooth +import XCTByteCoding +import XCTest + + +typealias UCP = UserControlPoint + + +final class UserControlPointTests: XCTestCase { + func testUserControlPoint() throws { + try testIdentity(from: UCP(.registerNewUser(consentCode: 8234))) + try testIdentity(from: UCP(.consent(userIndex: 26, consentCode: 8234))) + try testIdentity(from: UCP(.deleteUserData)) + try testIdentity(from: UCP(.listAllUsers)) + try testIdentity(from: UCP(.deleterUser(userIndex: 82))) + + try testIdentity(from: UCP(.response(requestOpCode: .registerNewUser, response: .success(.userIndex(123))))) + try testIdentity(from: UCP(.response(requestOpCode: .consent, response: .success()))) + try testIdentity(from: UCP(.response(requestOpCode: .listAllUsers, response: .success(.numberOfUsers(5))))) + try testIdentity(from: UCP(.response(requestOpCode: .deleteUser, response: .success(.userIndex(23))))) + + try testIdentity(from: UCP(.response(requestOpCode: .reserved, response: .opCodeNotSupported))) + try testIdentity(from: UCP(.response(requestOpCode: .registerNewUser, response: .invalidParameter))) + try testIdentity(from: UCP(.response(requestOpCode: .registerNewUser, response: .operationFailed))) + try testIdentity(from: UCP(.response(requestOpCode: .consent, response: .userNotAuthorized))) + } + + // TODO: unit test characteristic accessors? +}