Integrate live phone and watch remote prototype with guarded motion control

This commit is contained in:
2026-09-18 19:11:52 +04:00
parent 3e787543c0
commit 8131f4f873
54 changed files with 4355 additions and 329 deletions
@@ -0,0 +1,70 @@
import Foundation
import Testing
import RemoteCore
@testable import GestureCore
private func sample(_ t: Double, _ x: Double = 0, _ y: Double = 0, _ q: Double = 1) -> NormalizedMotionSample {
NormalizedMotionSample(timestamp: t, x: x, y: y, quality: q)
}
@Test func requiresStableNeutralAndEmitsOneDirectionalEvent() {
var configuration = GestureRecognitionConfiguration(); configuration.maximumSampleGap = 0.3
var recognizer = CalibratedGestureRecognizer(configuration: configuration)
let id = UUID(); recognizer.arm(sessionID: id, at: 0)
#expect(recognizer.process(sample(0)) == nil)
#expect(recognizer.process(sample(0.21)) == nil)
#expect(recognizer.process(sample(0.30, 0.65, 0)) == nil)
#expect(recognizer.process(sample(0.41, 0.8, 0)) == nil)
let result = recognizer.process(sample(0.50, 0.9, 0))
#expect(result?.command == .right)
#expect(result?.sessionID == id)
#expect(recognizer.process(sample(0.55, 0.9, 0)) == nil)
}
@Test func diagonalLowConfidenceGapAndOutOfOrderAreRejected() {
var recognizer = CalibratedGestureRecognizer()
recognizer.arm(sessionID: UUID(), at: 0)
_ = recognizer.process(sample(0)); _ = recognizer.process(sample(0.21))
_ = recognizer.process(sample(0.35, 0.8, 0.75))
#expect(recognizer.process(sample(0.50, 0.8, 0.75)) == nil) // ambiguous
#expect(recognizer.process(sample(0.49, 0.9, 0)) == nil) // stale
_ = recognizer.process(sample(0.70))
#expect(recognizer.process(sample(0.90, 0.9, 0, 0.2)) == nil)
}
@Test func cooldownAndReturnToNeutralSuppressReverseMotion() {
var configuration = GestureRecognitionConfiguration(); configuration.maximumSampleGap = 0.3
var recognizer = CalibratedGestureRecognizer(configuration: configuration)
recognizer.arm(sessionID: UUID(), at: 0)
_ = recognizer.process(sample(0)); _ = recognizer.process(sample(0.21))
#expect(recognizer.process(sample(0.30, 0.9, 0)) == nil)
#expect(recognizer.process(sample(0.41, 0.9, 0)) == nil)
#expect(recognizer.process(sample(0.50, 0.9, 0))?.command == .right)
_ = recognizer.process(sample(0.60)); _ = recognizer.process(sample(0.70)); _ = recognizer.process(sample(0.80))
_ = recognizer.process(sample(0.90)); _ = recognizer.process(sample(0.95))
#expect(recognizer.process(sample(1.05, -0.9, 0)) == nil)
#expect(recognizer.process(sample(1.15, -0.9, 0)) == nil)
#expect(recognizer.process(sample(1.25, -0.9, 0))?.command == .left)
}
@Test func disarmClearsSessionAndNoReplayAfterRearm() {
var recognizer = CalibratedGestureRecognizer()
recognizer.arm(sessionID: UUID(), at: 0); _ = recognizer.process(sample(0)); _ = recognizer.process(sample(0.21))
recognizer.disarm()
#expect(recognizer.process(sample(1, 0.9, 0)) == nil)
recognizer.arm(sessionID: UUID(), at: 2)
#expect(recognizer.process(sample(2.1, 0.9, 0)) == nil)
}
@Test func invalidSampleDiscardsCandidateUntilFreshNeutral() {
var recognizer = CalibratedGestureRecognizer()
recognizer.arm(sessionID: UUID(), at: 0)
for index in 0...20 {
_ = recognizer.process(.init(timestamp: Double(index) / 50, x: 0, y: 0))
}
_ = recognizer.process(.init(timestamp: 0.42, x: 0.8, y: 0))
_ = recognizer.process(.init(timestamp: 0.44, x: .nan, y: 0))
for index in 23...40 {
#expect(recognizer.process(.init(timestamp: Double(index) / 50, x: 0.8, y: 0)) == nil)
}
}
@@ -0,0 +1,38 @@
import Foundation
import Testing
@testable import SamsungProtocol
private let parser = SamsungEventParser(maximumMessageBytes: 256)
@Test func parsesAllowlistedPairingEvents() throws {
#expect(try parser.parse(Data(#"{"event":"ms.channel.connect","data":{"token":"synthetic-token"}}"#.utf8)) == .connected(token: "synthetic-token"))
#expect(try parser.parse(Data(#"{"event":"ms.channel.connect","data":{}}"#.utf8)) == .connected(token: nil))
#expect(try parser.parse(Data(#"{"event":"ms.channel.unauthorized"}"#.utf8)) == .unauthorized)
#expect(try parser.parse(Data(#"{"event":"ms.channel.timeOut"}"#.utf8)) == .timedOut)
#expect(try parser.parse(Data(#"{"event":"ms.error","data":{"message":"redacted"}}"#.utf8)) == .error)
}
@Test func unknownEventsAreInert() throws {
let fixture = Data(#"{"event":"synthetic.unknown","data":{"token":"not-consumed"}}"#.utf8)
#expect(try parser.parse(fixture) == .unknown)
}
@Test func rejectsMalformedEmptyAndOversizedMessages() {
#expect(throws: SamsungEventParserError.empty) { try parser.parse(Data()) }
#expect(throws: SamsungEventParserError.malformed) { try parser.parse(Data(#"{"event":42}"#.utf8)) }
#expect(throws: SamsungEventParserError.oversized) { try parser.parse(Data(repeating: 0x41, count: 257)) }
}
@Test func boundsEventNamesAndTokens() {
let longEvent = String(repeating: "e", count: SamsungEventParser.maximumEventNameBytes + 1)
let eventFixture = Data(#"{"event":"\#(longEvent)"}"#.utf8)
#expect(throws: SamsungEventParserError.invalidEventName) { try parser.parse(eventFixture) }
let tokenParser = SamsungEventParser(maximumMessageBytes: 8 * 1024)
let longToken = String(repeating: "t", count: SamsungEventParser.maximumTokenBytes + 1)
let tokenFixture = Data(#"{"event":"ms.channel.connect","data":{"token":"\#(longToken)"}}"#.utf8)
#expect(throws: SamsungEventParserError.invalidToken) { try tokenParser.parse(tokenFixture) }
#expect(throws: SamsungEventParserError.invalidToken) {
try tokenParser.parse(Data(#"{"event":"ms.channel.connect","data":{"token":""}}"#.utf8))
}
}