Files
haptika/ios/HaptikaWatch/Core/RegulationProtocol.swift
T
2026-07-31 16:20:10 +02:00

182 lines
6.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
public enum RegulationFamily: String, Codable, Sendable {
case breathe
case ground
case recenter
}
public enum RegulationMode: String, Codable, Sendable {
case haptic
case liveHeartRate
}
public enum RegulationPhaseKind: String, Codable, Sendable {
case inhale
case hold
case exhale
case anchor
}
public struct RegulationPhase: Codable, Equatable, Sendable, Identifiable {
public var id: String { "\(kind.rawValue)-\(durationSeconds)-\(label)" }
public let kind: RegulationPhaseKind
public let durationSeconds: TimeInterval
public let label: String
public init(kind: RegulationPhaseKind, durationSeconds: TimeInterval, label: String) {
self.kind = kind
self.durationSeconds = durationSeconds
self.label = label
}
}
public struct WatchRegulationProtocol: Codable, Equatable, Sendable, Identifiable {
public let id: String
public let version: Int
public let family: RegulationFamily
public let mode: RegulationMode
public let eyebrow: String
public let title: String
public let summary: String
public let durationSeconds: TimeInterval
public let phases: [RegulationPhase]
public init(
id: String,
version: Int = 1,
family: RegulationFamily,
mode: RegulationMode,
eyebrow: String,
title: String,
summary: String,
durationSeconds: TimeInterval,
phases: [RegulationPhase]
) {
self.id = id
self.version = version
self.family = family
self.mode = mode
self.eyebrow = eyebrow
self.title = title
self.summary = summary
self.durationSeconds = durationSeconds
self.phases = phases
}
}
public enum WatchProtocolCatalog {
private struct Manifest: Decodable {
let schemaVersion: Int
let protocols: [ManifestProtocol]
}
private struct ManifestProtocol: Decodable {
let kind: String
let family: RegulationFamily
let mode: String
let eyebrow: String
let title: String
let description: String
let durationSeconds: TimeInterval
let phases: [RegulationPhase]
var watchProtocol: WatchRegulationProtocol {
WatchRegulationProtocol(
id: kind,
version: 1,
family: family,
mode: mode == "haptic" ? .haptic : .liveHeartRate,
eyebrow: eyebrow,
title: title,
summary: description,
durationSeconds: durationSeconds,
phases: phases
)
}
}
public static let protocols: [WatchRegulationProtocol] = loadBundledManifest() ?? fallbackProtocols
private static let fallbackProtocols: [WatchRegulationProtocol] = [
.init(id: "myCoherence", family: .breathe, mode: .liveHeartRate, eyebrow: "BIOFEEDBACK", title: "Ma Cohérence", summary: "Respiration visuelle 5–5 avec tendance cardiaque.", durationSeconds: 180, phases: [
.init(kind: .inhale, durationSeconds: 5, label: "Inspirez"),
.init(kind: .exhale, durationSeconds: 5, label: "Expirez"),
]),
.init(id: "coherence55", family: .breathe, mode: .haptic, eyebrow: "ÉQUILIBRE", title: "Cohérence 5–5", summary: "Une respiration régulière pour retrouver un rythme calme.", durationSeconds: 300, phases: [
.init(kind: .inhale, durationSeconds: 5, label: "Inspirez"),
.init(kind: .exhale, durationSeconds: 5, label: "Expirez lentement"),
]),
.init(id: "box4444", family: .breathe, mode: .haptic, eyebrow: "STABILITÉ", title: "Respiration carrée", summary: "Quatre temps égaux pour structurer l’attention.", durationSeconds: 240, phases: [
.init(kind: .inhale, durationSeconds: 4, label: "Inspirez"),
.init(kind: .hold, durationSeconds: 4, label: "Gardez l’air"),
.init(kind: .exhale, durationSeconds: 4, label: "Expirez"),
.init(kind: .hold, durationSeconds: 4, label: "Restez vide"),
]),
.init(id: "breathing478", family: .breathe, mode: .haptic, eyebrow: "RALENTISSEMENT", title: "Respiration 4–7–8", summary: "Un cycle court avec une expiration longue.", durationSeconds: 76, phases: [
.init(kind: .inhale, durationSeconds: 4, label: "Inspirez"),
.init(kind: .hold, durationSeconds: 7, label: "Gardez l’air"),
.init(kind: .exhale, durationSeconds: 8, label: "Expirez lentement"),
]),
.init(id: "relax46", family: .breathe, mode: .haptic, eyebrow: "RELÂCHEMENT", title: "Expiration 4–6", summary: "Une expiration plus longue pour accompagner la détente.", durationSeconds: 180, phases: [
.init(kind: .inhale, durationSeconds: 4, label: "Inspirez"),
.init(kind: .exhale, durationSeconds: 6, label: "Expirez lentement"),
]),
.init(id: "sosStress", family: .recenter, mode: .haptic, eyebrow: "RETOUR AU CALME", title: "SOS tension", summary: "Trois impulsions puis un repère toutes les dix secondes.", durationSeconds: 62, phases: [
.init(kind: .anchor, durationSeconds: 62, label: "Restez avec la sensation"),
]),
.init(id: "microAnchor", family: .ground, mode: .haptic, eyebrow: "FORMAT COURT", title: "Micro-ancrage", summary: "Un repère tactile discret pour revenir aux sensations.", durationSeconds: 37, phases: [
.init(kind: .anchor, durationSeconds: 37, label: "Sentez les points de contact"),
]),
]
public static func protocolWithID(_ id: String) -> WatchRegulationProtocol? {
protocols.first { $0.id == id }
}
private static func loadBundledManifest(bundle: Bundle = .main) -> [WatchRegulationProtocol]? {
guard let url = bundle.url(forResource: "regulation-protocols.v1", withExtension: "json"),
let data = try? Data(contentsOf: url),
let manifest = try? JSONDecoder().decode(Manifest.self, from: data),
manifest.schemaVersion == 1,
manifest.protocols.count == 7 else {
return nil
}
return manifest.protocols.map(\.watchProtocol)
}
}
public struct RegulationPhasePosition: Equatable, Sendable {
public let phaseIndex: Int
public let phase: RegulationPhase
public let progress: Double
public let cycleIndex: Int
}
public enum RegulationPhaseEngine {
public static func position(
in regulation: WatchRegulationProtocol,
elapsedSeconds: TimeInterval
) -> RegulationPhasePosition? {
guard !regulation.phases.isEmpty else { return nil }
let cycleDuration = regulation.phases.reduce(0) { $0 + $1.durationSeconds }
guard cycleDuration > 0 else { return nil }
let clamped = max(0, min(elapsedSeconds, max(0, regulation.durationSeconds - 0.001)))
let cycleElapsed = clamped.truncatingRemainder(dividingBy: cycleDuration)
var cursor: TimeInterval = 0
for (index, phase) in regulation.phases.enumerated() {
let end = cursor + phase.durationSeconds
if cycleElapsed < end || index == regulation.phases.count - 1 {
return .init(
phaseIndex: index,
phase: phase,
progress: min(1, max(0, (cycleElapsed - cursor) / phase.durationSeconds)),
cycleIndex: Int(clamped / cycleDuration)
)
}
cursor = end
}
return nil
}
}