2022-01-29 18:59:42 +01:00
|
|
|
import Foundation
|
|
|
|
import SwiftUI
|
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
enum ConnectionError {
|
|
|
|
case serverNotReached
|
|
|
|
case deviceDisconnected
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ConnectionError: CustomStringConvertible {
|
|
|
|
|
|
|
|
var description: String {
|
|
|
|
switch self {
|
|
|
|
case .serverNotReached:
|
|
|
|
return "Server unavailable"
|
|
|
|
case .deviceDisconnected:
|
|
|
|
return "Device disconnected"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum RejectionCause {
|
|
|
|
case invalidCounter
|
|
|
|
case invalidTime
|
|
|
|
case invalidAuthentication
|
|
|
|
case timeout
|
2022-05-01 18:30:30 +02:00
|
|
|
case missingKey
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
extension RejectionCause: CustomStringConvertible {
|
|
|
|
|
|
|
|
var description: String {
|
|
|
|
switch self {
|
|
|
|
case .invalidCounter:
|
|
|
|
return "Invalid counter"
|
|
|
|
case .invalidTime:
|
|
|
|
return "Invalid time"
|
|
|
|
case .invalidAuthentication:
|
|
|
|
return "Invalid authentication"
|
|
|
|
case .timeout:
|
|
|
|
return "Device not responding"
|
2022-05-01 18:30:30 +02:00
|
|
|
case .missingKey:
|
|
|
|
return "No key to verify message"
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-29 18:59:42 +01:00
|
|
|
enum ClientState {
|
2022-04-09 17:43:33 +02:00
|
|
|
|
|
|
|
/// There is no key stored locally on the client. A new key must be generated before use.
|
|
|
|
case noKeyAvailable
|
|
|
|
|
|
|
|
/// The device status is being requested
|
|
|
|
case requestingStatus
|
|
|
|
|
2022-01-29 18:59:42 +01:00
|
|
|
/// The remote device is not connected (no socket opened)
|
2022-04-09 17:43:33 +02:00
|
|
|
case deviceNotAvailable(ConnectionError)
|
|
|
|
|
|
|
|
/// The device is connected and ready to receive a message
|
|
|
|
case ready
|
2022-01-29 18:59:42 +01:00
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
/// The message is being transmitted and a response is awaited
|
2022-01-29 18:59:42 +01:00
|
|
|
case waitingForResponse
|
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
/// The transmitted message was rejected (multiple possible reasons)
|
|
|
|
case messageRejected(RejectionCause)
|
2022-01-29 18:59:42 +01:00
|
|
|
|
2022-05-01 18:30:30 +02:00
|
|
|
case responseRejected(RejectionCause)
|
|
|
|
|
2022-01-29 18:59:42 +01:00
|
|
|
/// The device responded that the opening action was started
|
|
|
|
case openSesame
|
2022-04-09 17:43:33 +02:00
|
|
|
|
|
|
|
case internalError(String)
|
2022-01-29 18:59:42 +01:00
|
|
|
|
|
|
|
var canSendKey: Bool {
|
|
|
|
switch self {
|
2022-04-09 17:43:33 +02:00
|
|
|
case .ready, .openSesame, .messageRejected:
|
2022-01-29 18:59:42 +01:00
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
init(keyResult: MessageResult) {
|
2022-01-29 18:59:42 +01:00
|
|
|
switch keyResult {
|
2022-04-09 17:43:33 +02:00
|
|
|
case .messageAuthenticationFailed:
|
|
|
|
self = .messageRejected(.invalidAuthentication)
|
|
|
|
case .messageTimeMismatch:
|
|
|
|
self = .messageRejected(.invalidTime)
|
|
|
|
case .messageCounterInvalid:
|
|
|
|
self = .messageRejected(.invalidCounter)
|
|
|
|
case .deviceTimedOut:
|
|
|
|
self = .messageRejected(.timeout)
|
|
|
|
case .messageAccepted:
|
2022-01-29 18:59:42 +01:00
|
|
|
self = .openSesame
|
2022-04-09 17:43:33 +02:00
|
|
|
case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
|
|
|
|
self = .internalError(keyResult.description)
|
|
|
|
case .deviceNotConnected:
|
|
|
|
self = .deviceNotAvailable(.deviceDisconnected)
|
|
|
|
case .operationInProgress:
|
|
|
|
self = .waitingForResponse
|
2022-05-01 14:07:43 +02:00
|
|
|
case .deviceConnected:
|
|
|
|
self = .ready
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
|
2022-04-13 14:55:47 +02:00
|
|
|
var actionText: String {
|
2022-05-01 14:07:43 +02:00
|
|
|
"Unlock"
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2022-04-13 14:55:47 +02:00
|
|
|
|
|
|
|
var requiresDescription: Bool {
|
|
|
|
switch self {
|
2022-05-01 18:30:30 +02:00
|
|
|
case .deviceNotAvailable, .messageRejected, .internalError, .responseRejected:
|
2022-04-13 14:55:47 +02:00
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2022-01-29 18:59:42 +01:00
|
|
|
|
2022-04-13 14:55:47 +02:00
|
|
|
var color: Color {
|
2022-01-29 18:59:42 +01:00
|
|
|
switch self {
|
2022-04-13 14:55:47 +02:00
|
|
|
case .noKeyAvailable:
|
2022-05-01 18:30:30 +02:00
|
|
|
return Color(red: 50/255, green: 50/255, blue: 50/255)
|
2022-04-13 14:55:47 +02:00
|
|
|
case .deviceNotAvailable:
|
2022-05-01 18:30:30 +02:00
|
|
|
return Color(red: 150/255, green: 90/255, blue: 90/255)
|
|
|
|
case .messageRejected, .responseRejected:
|
|
|
|
return Color(red: 160/255, green: 30/255, blue: 30/255)
|
2022-04-13 14:55:47 +02:00
|
|
|
case .internalError:
|
2022-05-01 18:30:30 +02:00
|
|
|
return Color(red: 100/255, green: 0/255, blue: 0/255)
|
2022-04-13 14:55:47 +02:00
|
|
|
case .ready:
|
2022-05-01 18:30:30 +02:00
|
|
|
return Color(red: 115/255, green: 140/255, blue: 90/255)
|
|
|
|
case .requestingStatus, .waitingForResponse:
|
|
|
|
return Color(red: 160/255, green: 170/255, blue: 110/255)
|
2022-04-13 14:55:47 +02:00
|
|
|
case .openSesame:
|
2022-05-01 18:30:30 +02:00
|
|
|
return Color(red: 65/255, green: 110/255, blue: 60/255)
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-13 14:55:47 +02:00
|
|
|
var allowsAction: Bool {
|
2022-01-29 18:59:42 +01:00
|
|
|
switch self {
|
2022-05-01 14:07:43 +02:00
|
|
|
case .requestingStatus, .deviceNotAvailable, .waitingForResponse, .noKeyAvailable:
|
2022-01-29 18:59:42 +01:00
|
|
|
return false
|
|
|
|
default:
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
extension ClientState: Equatable {
|
2022-01-29 18:59:42 +01:00
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
extension ClientState: CustomStringConvertible {
|
|
|
|
|
|
|
|
var description: String {
|
|
|
|
switch self {
|
|
|
|
case .noKeyAvailable:
|
|
|
|
return "No key set."
|
|
|
|
case .requestingStatus:
|
|
|
|
return "Checking device status"
|
|
|
|
case .deviceNotAvailable(let status):
|
|
|
|
return status.description
|
|
|
|
case .ready:
|
|
|
|
return "Ready"
|
|
|
|
case .waitingForResponse:
|
|
|
|
return "Unlocking..."
|
|
|
|
case .messageRejected(let cause):
|
|
|
|
return cause.description
|
|
|
|
case .openSesame:
|
|
|
|
return "Unlocked"
|
|
|
|
case .internalError(let e):
|
|
|
|
return "Error: \(e)"
|
2022-05-01 18:30:30 +02:00
|
|
|
case .responseRejected(let cause):
|
|
|
|
switch cause {
|
|
|
|
case .invalidAuthentication:
|
|
|
|
return "Device message not authenticated"
|
|
|
|
default:
|
|
|
|
return cause.description
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Coding
|
|
|
|
|
|
|
|
extension ClientState {
|
|
|
|
|
|
|
|
var encoded: Data {
|
|
|
|
Data([code])
|
|
|
|
}
|
|
|
|
|
|
|
|
private var code: UInt8 {
|
|
|
|
switch self {
|
|
|
|
case .noKeyAvailable:
|
|
|
|
return 1
|
|
|
|
case .requestingStatus:
|
|
|
|
return 2
|
|
|
|
case .deviceNotAvailable(let connectionError):
|
|
|
|
switch connectionError {
|
|
|
|
case .serverNotReached:
|
|
|
|
return 3
|
|
|
|
case .deviceDisconnected:
|
|
|
|
return 4
|
|
|
|
}
|
|
|
|
case .ready:
|
|
|
|
return 5
|
|
|
|
case .waitingForResponse:
|
|
|
|
return 6
|
|
|
|
case .messageRejected(let rejectionCause):
|
|
|
|
switch rejectionCause {
|
|
|
|
case .invalidCounter:
|
|
|
|
return 7
|
|
|
|
case .invalidTime:
|
|
|
|
return 8
|
|
|
|
case .invalidAuthentication:
|
|
|
|
return 9
|
|
|
|
case .timeout:
|
|
|
|
return 10
|
|
|
|
case .missingKey:
|
|
|
|
return 11
|
|
|
|
}
|
|
|
|
case .responseRejected(let rejectionCause):
|
|
|
|
switch rejectionCause {
|
|
|
|
case .invalidCounter:
|
|
|
|
return 12
|
|
|
|
case .invalidTime:
|
|
|
|
return 13
|
|
|
|
case .invalidAuthentication:
|
|
|
|
return 14
|
|
|
|
case .timeout:
|
|
|
|
return 15
|
|
|
|
case .missingKey:
|
|
|
|
return 16
|
|
|
|
}
|
|
|
|
case .openSesame:
|
|
|
|
return 17
|
|
|
|
case .internalError(_):
|
|
|
|
return 18
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
init(code: UInt8) {
|
|
|
|
switch code {
|
|
|
|
case 1:
|
|
|
|
self = .noKeyAvailable
|
|
|
|
case 2:
|
|
|
|
self = .requestingStatus
|
|
|
|
case 3:
|
|
|
|
self = .deviceNotAvailable(.serverNotReached)
|
|
|
|
case 4:
|
|
|
|
self = .deviceNotAvailable(.deviceDisconnected)
|
|
|
|
case 5:
|
|
|
|
self = .ready
|
|
|
|
case 6:
|
|
|
|
self = .waitingForResponse
|
|
|
|
case 7:
|
|
|
|
self = .messageRejected(.invalidCounter)
|
|
|
|
case 8:
|
|
|
|
self = .messageRejected(.invalidTime)
|
|
|
|
case 9:
|
|
|
|
self = .messageRejected(.invalidAuthentication)
|
|
|
|
case 10:
|
|
|
|
self = .messageRejected(.timeout)
|
|
|
|
case 11:
|
|
|
|
self = .messageRejected(.missingKey)
|
|
|
|
case 12:
|
|
|
|
self = .responseRejected(.invalidCounter)
|
|
|
|
case 13:
|
|
|
|
self = .responseRejected(.invalidTime)
|
|
|
|
case 14:
|
|
|
|
self = .responseRejected(.invalidAuthentication)
|
|
|
|
case 15:
|
|
|
|
self = .responseRejected(.timeout)
|
|
|
|
case 16:
|
|
|
|
self = .responseRejected(.missingKey)
|
|
|
|
case 17:
|
|
|
|
self = .openSesame
|
|
|
|
case 18:
|
|
|
|
self = .internalError("")
|
|
|
|
default:
|
|
|
|
self = .internalError("Unknown code \(code)")
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
}
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|