-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDeviceState.swift
More file actions
117 lines (103 loc) · 4.06 KB
/
Copy pathDeviceState.swift
File metadata and controls
117 lines (103 loc) · 4.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import Foundation
import Observation
public enum ConnectionState: Equatable, Sendable {
case bluetoothOff
case notConfigured // no saved peripheral identifier
case waiting // pending connection armed, buds unavailable
case connecting
case ready
case failed(String)
public var isReady: Bool { self == .ready }
public var label: String {
switch self {
case .bluetoothOff: "Bluetooth is off"
case .notConfigured: "No earbuds selected"
case .waiting: "Waiting for earbuds"
case .connecting: "Connecting…"
case .ready: "Connected"
case .failed(let reason): reason
}
}
}
/// Mutated only by `DeviceController`, only on the main actor. Single writer.
@Observable
@MainActor
public final class DeviceState {
public var connection: ConnectionState = .notConfigured
/// Last mode the device confirmed.
public var mode: ANCMode?
/// Optimistic target during the device's ~1.4 s apply window.
public var pendingMode: ANCMode?
/// True from the moment a connection lands until the first re-read of the
/// mode comes back.
///
/// Separate from `isBusy`, which means a set of *ours* is in flight. This
/// one means we do not yet know what the device is doing. The read taken as
/// the link comes up is the least trustworthy one we ever take — see
/// `DeviceController.settleMode` — so the UI says it is still reading
/// rather than presenting that read as a confident selection.
public var isResolvingMode = false
public var batteryLeft: Int?
public var batteryRight: Int?
public var firmware: String?
public var lastError: String?
public init() {}
/// What the UI renders as selected.
public var displayMode: ANCMode? { pendingMode ?? mode }
public var isBusy: Bool { pendingMode != nil }
/// Template-rendered menu bar glyph. Encodes mode at a glance; the
/// disconnected case is handled by the view's foreground style, not here.
public var menuBarSymbol: String {
displayMode?.symbol ?? "ear"
}
/// Cheap, Sendable projection for the widget extension.
///
/// Reports the *optimistic* mode, like the menu bar: waiting for the
/// device's ~1.4 s confirmation before repainting made every Control
/// Center tap feel broken. `pending` carries the distinction for callers
/// that need the truth — `SetModeIntent` must not report success off an
/// optimistic value.
public var snapshot: ModeSnapshot {
ModeSnapshot(
mode: displayMode,
connected: connection.isReady,
pending: isBusy,
batteryLeft: batteryLeft,
batteryRight: batteryRight
)
}
}
/// The entire contract between the agent and the control extension.
public struct ModeSnapshot: Codable, Sendable, Equatable {
public var mode: ANCMode?
public var connected: Bool
/// `mode` is a target we have asked for, not one the device has confirmed.
///
/// ponytail: synthesized Decodable ignores this default, so a snapshot
/// written by a pre-`pending` agent fails to decode and reads as
/// disconnected until the agent's next publish — seconds, after an app
/// update only. Write a custom init(from:) if that ever matters.
public var pending: Bool = false
public var batteryLeft: Int?
public var batteryRight: Int?
public init(
mode: ANCMode?,
connected: Bool,
pending: Bool = false,
batteryLeft: Int? = nil,
batteryRight: Int? = nil
) {
self.mode = mode
self.connected = connected
self.pending = pending
self.batteryLeft = batteryLeft
self.batteryRight = batteryRight
}
public var label: String {
guard connected else { return "Disconnected" }
return mode?.label ?? "Unknown"
}
public var symbol: String { mode?.symbol ?? "ear" }
/// Shown in the Control Center gallery before any real state exists.
public static let placeholder = ModeSnapshot(mode: .anc, connected: true)
}