11import Combine
22import Foundation
33
4- /// Publiczna fasada gamepada Triki — BLE → parser → motion → `TrikiGameInput` .
4+ /// Publiczna fasada gamepada Triki — BLE → monitor → parser → adaptacyjny motion .
55@MainActor
66public final class TrikiGameController : ObservableObject {
7- /// Tryb czułości (szybki vs stabilny).
87 public enum InputMode : Sendable , Equatable {
98 case game
109 case smooth
@@ -16,25 +15,39 @@ public final class TrikiGameController: ObservableObject {
1615 didSet { motion. inputMode = mapInputMode ( inputMode) }
1716 }
1817
19- /// Log hex + wartości parsera (DEV).
2018 public var debugParserLogging : Bool {
2119 get { parser. debugLoggingEnabled }
2220 set { parser. debugLoggingEnabled = newValue }
2321 }
2422
23+ public var debugBLEMonitorLogging : Bool {
24+ get { bleMonitor. debugLoggingEnabled }
25+ set { bleMonitor. debugLoggingEnabled = newValue }
26+ }
27+
28+ /// Odstęp publikacji HUD w trybie low power (mniej odświeżeń UI).
29+ public var lowPowerHudPublishInterval : TimeInterval = 0.35
30+
2531 @Published public private( set) var gameInput = TrikiGameInput ( )
32+ @Published public private( set) var bleMode : TrikiBLEMode = . unknown
2633 @Published public private( set) var isConnected = false
2734 @Published public private( set) var isReceiving = false
35+ /// Tekst podpowiedzi UX przy low power.
36+ @Published public private( set) var idleStatusMessage : String ? = nil
2837
2938 private var parser = TrikiParser ( )
3039 private var motion = TrikiMotionEngine ( )
40+ private let bleMonitor = TrikiBLEMonitor ( )
3141 private var cancellables = Set < AnyCancellable > ( )
3242 private var lastPacketAt : TimeInterval = 0
3343 private var lastPollTime : TimeInterval ?
44+ private var lastHudPublishAt : TimeInterval = 0
45+ private var lastWakeUpAttemptAt : TimeInterval = 0
3446
3547 private var moveHandlers : [ ( Float ) -> Void ] = [ ]
3648 private var shakeHandlers : [ ( ) -> Void ] = [ ]
3749 private var actionHandlers : [ ( ) -> Void ] = [ ]
50+ private var modeChangedHandlers : [ ( TrikiBLEMode ) -> Void ] = [ ]
3851
3952 public init ( ) {
4053 motion. inputMode = . game
@@ -43,7 +56,6 @@ public final class TrikiGameController: ObservableObject {
4356
4457 // MARK: - Connection
4558
46- /// Skan + auto-connect do jedynego urządzenia z „triki” w nazwie.
4759 public func connect( ) {
4860 ble. autoConnectWhenSingleLikelyMatch = true
4961 if ble. cachedPeripheralUUID != nil , ble. status == . idle {
@@ -58,21 +70,28 @@ public final class TrikiGameController: ObservableObject {
5870 resetSession ( )
5971 }
6072
61- // MARK: - Manual bytes (własny central)
62-
6373 public func ingest( _ bytes: [ UInt8 ] ) {
6474 guard !bytes. isEmpty else { return }
75+ let now = Date ( ) . timeIntervalSince1970
76+ if let transition = bleMonitor. recordPacket ( at: now) {
77+ applyBLEModeTransition ( transition)
78+ }
6579 parser. append ( bytes)
66- lastPacketAt = Date ( ) . timeIntervalSince1970
80+ lastPacketAt = now
6781 if !isReceiving { isReceiving = true }
82+ updateIdleMessage ( )
6883 }
6984
70- /// Wywołuj w pętli gry (~60 Hz). Zwraca bieżącą ramkę gamepada.
7185 @discardableResult
7286 public func tick( deltaTime: TimeInterval ? = nil ) -> TrikiGameInput {
7387 let now = Date ( ) . timeIntervalSince1970
7488 if now - lastPacketAt > 0.35 , isReceiving { isReceiving = false }
7589
90+ if let transition = bleMonitor. evaluateStale ( now: now) {
91+ applyBLEModeTransition ( transition)
92+ }
93+ attemptWakeUpIfStuck ( now: now)
94+
7695 let dt : TimeInterval
7796 if let deltaTime {
7897 dt = deltaTime
@@ -84,29 +103,38 @@ public final class TrikiGameController: ObservableObject {
84103 let frames = parser. drainParsedFrames ( )
85104 if !frames. isEmpty {
86105 motion. ingest ( parsed: frames, deltaTime: dt)
87- gameInput = motion . output
106+ publishGameInputIfNeeded ( now : now )
88107 dispatchCallbacks ( )
108+ } else if bleMode == . lowPower, now - lastHudPublishAt >= lowPowerHudPublishInterval {
109+ gameInput = motion. output
110+ lastHudPublishAt = now
89111 }
112+
90113 return gameInput
91114 }
92115
93116 public func resetSession( ) {
94117 parser. reset ( )
95118 motion. reset ( )
119+ bleMonitor. reset ( )
96120 gameInput = TrikiGameInput ( )
121+ bleMode = . unknown
122+ idleStatusMessage = nil
97123 isReceiving = false
98124 lastPollTime = nil
125+ lastHudPublishAt = 0
126+ lastWakeUpAttemptAt = 0
99127 }
100128
101- // MARK: - Gamepad API (bez surowych osi)
129+ // MARK: - Public API
130+
131+ public func getBLEMode( ) -> TrikiBLEMode { bleMode }
102132
103133 public func getDirection( ) -> Float { motion. getDirection ( ) }
104134 public func getVelocity( ) -> Float { motion. getVelocity ( ) }
105135 public func isShake( ) -> Bool { motion. isShake ( ) }
106136 public func isMoving( ) -> Bool { motion. isMoving ( ) }
107137
108- // MARK: - Callbacks (DX)
109-
110138 public func onMove( _ handler: @escaping ( Float ) -> Void ) {
111139 moveHandlers. append ( handler)
112140 }
@@ -119,10 +147,15 @@ public final class TrikiGameController: ObservableObject {
119147 actionHandlers. append ( handler)
120148 }
121149
150+ public func onModeChanged( _ handler: @escaping ( TrikiBLEMode ) -> Void ) {
151+ modeChangedHandlers. append ( handler)
152+ }
153+
122154 public func clearHandlers( ) {
123155 moveHandlers. removeAll ( )
124156 shakeHandlers. removeAll ( )
125157 actionHandlers. removeAll ( )
158+ modeChangedHandlers. removeAll ( )
126159 }
127160
128161 // MARK: - Private
@@ -146,10 +179,50 @@ public final class TrikiGameController: ObservableObject {
146179 . store ( in: & cancellables)
147180 }
148181
182+ private func applyBLEModeTransition( _ transition: TrikiBLEModeTransition ) {
183+ bleMode = transition. current
184+ motion. setBLEMode ( transition. current)
185+ for handler in modeChangedHandlers {
186+ handler ( transition. current)
187+ }
188+ updateIdleMessage ( )
189+ }
190+
191+ private func updateIdleMessage( ) {
192+ switch bleMode {
193+ case . lowPower:
194+ idleStatusMessage = " Bezruch — czekam na ruch "
195+ case . fast, . normal:
196+ idleStatusMessage = nil
197+ case . unknown:
198+ idleStatusMessage = nil
199+ }
200+ }
201+
202+ private func publishGameInputIfNeeded( now: TimeInterval ) {
203+ let interval : TimeInterval = bleMode == . lowPower ? lowPowerHudPublishInterval : 0
204+ if interval > 0 , now - lastHudPublishAt < interval, lastHudPublishAt > 0 {
205+ return
206+ }
207+ gameInput = motion. output
208+ lastHudPublishAt = now
209+ }
210+
211+ /// Po długim low power bez pakietów — delikatne „obudzenie” streamu (INIT).
212+ private func attemptWakeUpIfStuck( now: TimeInterval ) {
213+ guard bleMode == . lowPower, isConnected else { return }
214+ guard now - lastPacketAt > 4.0 else { return }
215+ guard now - lastWakeUpAttemptAt > 5.0 else { return }
216+ lastWakeUpAttemptAt = now
217+ ble. sendInitAndStartIfReady ( )
218+ if debugBLEMonitorLogging {
219+ print ( " [TrikiGameController] wake-up INIT (low power stall) " )
220+ }
221+ }
222+
149223 private func dispatchCallbacks( ) {
150- let direction = gameInput. direction
151224 if gameInput. isMoving {
152- for handler in moveHandlers { handler ( direction) }
225+ for handler in moveHandlers { handler ( gameInput . direction) }
153226 }
154227 if gameInput. isShake {
155228 for handler in shakeHandlers { handler ( ) }
0 commit comments