MEITT · DOCUMENTACIÓN TÉCNICA

Arquitectura de Componentes

Referencia completa de pantallas, widgets y motores.

3
Pantallas
14
Componentes
3
Motores IA
100%
Local · Sin cloud
KMP
Android + iOS
01 — Sistema
Arquitectura General

Fuente de Datos

Pulsera Meitt SDK 2208 vía BLE 5.0. Todos los datos se obtienen exclusivamente del hardware. Sin cloud, sin APIs externas.


BleRepositoryAndroid.kt

Persistencia

SQLite local (Room). Historial de FC, sueño, SpO2, pasos, sesiones de actividad. Todo en el dispositivo del usuario.


shared/data/local/

ViewModel / MVI

Patrón MVI: Intent → State → Effect. DashboardViewModel centraliza el estado de salud. Coroutines para streaming y sync.


DashboardViewModel.kt

UI — Compose

Kotlin Multiplatform + Compose Multiplatform. Android e iOS desde el mismo código. Tema oscuro OLED (#121416).


composeApp/ui/
Estado global — DashboardState
data class DashboardState(
  selectedDate:     LocalDate,
  latestVitals:     VitalReading?,      // streaming live
  hrHistory:        List<HeartRateRecord>,
  sleepHistory:     List<SleepRecord>,
  spo2History:      List<Spo2Record>,
  selectedDaySteps: StepsRecord?,
  physiologyInsight: PhysiologyInsight?,  // HRV engine
  trueAgeInsight:    TrueAgeInsight?,     // TrueAge™ engine
  trueAgeYears:     Int?,
  activityScore:    Int,
  isSyncing:        Boolean,
  isStreaming:      Boolean,
)
Ciclo de sincronización
// Pull-to-refresh (HealthScreen)
SyncHistoricalData → syncData()

// Si pulsera conectada → sync real:
syncHeartRate() + syncSteps()
+ syncSleepHistory() + syncSpo2()

// Si desconectada → solo recarga local:
fetchDataForSelectedDate()

// Stream en tiempo real (Dashboard):
StartVitalsStream → bleRepository
  .startVitalsStream()
  .collect { vitals → updateState }
Regla clave: pull-to-refresh comprueba connectionState.value is Connected de forma inmediata. Si no hay conexión, recarga desde SQLite sin intentar reconectar.
02 — Navegación
Mapa de Pantallas

Dashboard

DashboardScreen.kt
  • TrueAge™ Widget (hito central)
  • Índice de Resiliencia (Radar)
  • HealthSectionContent (compartido)
  • Actividades Recientes (últimas 3)
  • Coach IA (coming soon)
  • Botones Sync / Disconnect
ViewModels: DashboardViewModel + ActivityViewModel

Salud

HealthScreen.kt
  • Header "Salud"
  • Pull-to-refresh (icono top bar)
  • HealthSectionContent (compartido)
  • DateCarousel para navegar días
ViewModel: DashboardViewModel
Auto-reload en onResume con AppLifecycleEvents

Actividad

ActivityScreen.kt
  • Historial completo de sesiones
  • Detalle por sesión con GPS
  • Métricas: duración, FC, calorías
  • Mapa de ruta (RouteCanvas)
ViewModel: ActivityViewModel

HealthSectionContent

HealthScreen.kt · internal
  • Componente compartido
  • Usado en Dashboard Y Health
  • Recibe DashboardState
  • Renderiza las 7 cards de salud
Para modificar cualquier card de salud, editar en HealthScreen.kt. El cambio se refleja automáticamente en ambas pantallas.
03 — Hito Central
TrueAge™ Widget
TrueAgeWidget
ui/component/TrueAgeWidget.kt · Visible en: DashboardScreen
Canvas Animación 3 actos TrueAge™ Engine
⟳ · 🔋82%
TRUE AGE™  ·  EDAD BIOLÓGICA
34.2
años
2.3 años más joven · 9 días de datos
Tu resiliencia cardiovascular te sostiene — HRV de 48ms absorbiendo el déficit de sueño.
Estados de revelación
CALCULATING · Órbita celular + scanner
REVEALING · Flash radial
REVEALED · Número contando
Animaciones activas (REVEALED)
  • displayAge — count-up 0→trueAge en 1 800 ms
  • ringAlpha — fade-in del anillo en 800 ms
  • ringPulse — respiración suave 1.0→1.04 / 3 000 ms (∞)
  • flashAlpha/Scale — destello de 700 ms en transición
Parámetros de entrada
  • trueAge Double? — edad biológica calculada
  • daysRecorded Int — días con datos (barra de progreso)
  • requiredDays Int = 30 — días para cálculo completo
  • insight TrueAgeInsight? — resultado del motor 10 días
Contenido del insight (si disponible)
  • trend10d — flecha ▼ verde / ▲ rojo / ─ estable (umbral ±0.3)
  • offset — "<X> años más joven/mayor"
  • daysWithData — badge de datos disponibles
  • insightKey — texto humanizado según 8 claves
8 Claves de insight (TrueAgeInsightKey)
Clave Condición
HABITS_HOLDING_BACK habitsDriver es el factor dominante negativo
TREND_DECLINING trend10d > 0.5
TREND_IMPROVING trend10d < −0.5
HRV_SUSTAINS_COMPENSATES hrv alto compensa sueño bajo
SLEEP_DRIVES_RECOVERY sleepDriver dominante positivo
ACTIVITY_BOOSTS_VITALITY activityDriver dominante positivo
BALANCED todos los factores equilibrados
NEUTRAL_INSUFFICIENT_DATA daysWithData < 3
Referencia para modificar: editar TrueAgeWidget.kt. El anillo, el tamaño de número y los colores están en RevealedView. El estado de cálculo en CalculatingView.
04 — Motores de IA
Motores de Cálculo Local
T
TrueAgeEngine · calculateWindowed()
shared/domain/engine/TrueAgeEngine.kt
Motor IA 100% Local
Algoritmo — ventana deslizante 10 días
offset_final = recentOffset × 0.60 + historicalOffset × 0.40 // recentOffset = media ponderada días 1-3 (últimos 3) // historicalOffset = media ponderada días 4-10 biologicalAge = chronologicalAge + offset_final trend10d = avg(segunda_mitad_offsets) - avg(primera_mitad_offsets) // negativo = mejorando (edad bajando)
4 factores de driver
sleepDriver = (avgSleepScore / 100 - 0.5) × 2 hrvDriver = (avgHrvMs / 60 - 0.5) × 2 activityDriver = (avgActivityScore / 100 - 0.5) × 2 habitsDriver = -(bmi_offset + sedentary_penalty)
Entrada requerida
  • profile UserProfile — birthYear, weightKg, heightCm, sex
  • habits UserHabits — smoker, alcoholUnits, exerciseDays
  • tenDayMetrics List<DailyMetrics> — últimos 10 días con datos
  • currentYear Int — para calcular edad cronológica
Salida — TrueAgeInsight
data class TrueAgeInsight(
  biologicalAge:   Double,  // edad calculada
  chronologicalAge: Int,
  offset:          Double,  // + = mayor, - = menor
  trend10d:        Double,  // + = empeorando
  dominantFactor:  TrueAgeFactor,
  insightKey:      TrueAgeInsightKey,
  sleepDriver:     Double,
  hrvDriver:       Double,
  activityDriver:  Double,
  habitsDriver:    Double,
  daysWithData:    Int,
  avgHrvMs:        Double,
  avgSleepScore:   Double,
  avgActivityScore: Double,
)
¿Cuándo se calcula? Tras cada sync y al cargar el Dashboard. El ViewModel hace un bucle de 0..9 días atrás, obtiene DailyMetrics por día y llama a calculateWindowed().
P
HeartRateIntelligenceEngine
shared/domain/engine/ · Salida: PhysiologyInsight
Motor IA 100% Local
Métricas calculadas
rhr = percentil_10(hrHistory_duringSleep) rmssd ≈ sqrt(mean(diff(bpm_sucesivos)²)) // estimación recoveryScore = sleepScore × 0.65 + cardiacEfficiency × 0.35 × hrvMultiplier cardiacEfficiency = 100 - (rhr - 40) × 1.6 sedentaryStressAlert = true si hay picos FC sin variación de movimiento detectados
Salida — PhysiologyInsight
  • rhr — FC mínima estable (reposo)
  • rmssd — variabilidad entre latidos (ms)
  • recoveryScore — 0-100 recuperación
  • recoveryStatus — OPTIMAL / GOOD / MODERATE / LOW
  • rhrTrend — hoy vs media 7 días
  • sedentaryStressAlert — booleano estrés
05 — Dashboard
Índice de Resiliencia (Radar)
R
ResilienceRadarChart
ui/component/ResilienceRadarChart.kt · Visible en: DashboardScreen
Canvas Animación 1200ms
ÍNDICE DE RESILIENCIA BUENO
HRV SpO₂ SUEÑO REC
Parámetros de entrada
  • hrvMs Double — desde physiologyInsight.rmssd
  • spo2Pct Int — desde spo2History.lastOrNull()?.percent
  • sleepScore Int — desde trueAgeInsight.avgSleepScore
  • recoveryScore Int — desde physiologyInsight.recoveryScore
Normalización (0.0 → 1.0)
HRV: min(hrvMs / 80.0, 1.0) SpO₂: (spo2Pct - 90) / 10 → [0.1, 1.0] Sleep: sleepScore / 100 Recovery: recoveryScore / 100
Badge de estado (recoveryScore)
Score Label Color
≥ 85 ÓPTIMO Cyan
65-84 BUENO Verde
40-64 BAJO Amber
< 40 CRÍTICO Rojo
Para modificar: ResilienceRadarChart.kt. Los 4 ejes están fijos (N/E/S/O = HRV/SpO2/Sueño/Recovery). Para añadir ejes, cambiar el loop de 0..3 y los ángulos a i * 360/n.
06 — Sección de Salud
HealthSectionContent · 7 Tarjetas
Componente compartido: internal fun HealthSectionContent(state: DashboardState, modifier: Modifier) — vive en HealthScreen.kt y se usa en DashboardScreen y HealthScreen sin duplicación.
1
ActivityRingsWidget
HealthScreen.kt · private · posición: tarjeta 1
Canvas Spring Bounce
Visual
7.842 PASOS
Origen de datos
steps = selectedDaySteps.totalSteps ?: latestVitals.steps // si es hoy calories = selectedDaySteps.totalCalories distKm = distanceMeters / 1000
3 Anillos — objetivos
Anillo Color Objetivo Fuente
Pasos Cyan 10 000 pasos StepsRecord
Calorías Naranja 500 kcal StepsRecord
Distancia Violeta 5 km StepsRecord
Animación

Cada anillo usa animateFloatAsState con spring(DampingRatioMediumBouncy, StiffnessLow). El bounce es suave y da sensación de física real al cargar.

Para modificar objetivos: constantes en la parte superior de HealthScreen.kt: STEPS_GOAL_DEFAULT = 10_000, CALS_GOAL = 500, DIST_GOAL_KM = 5f.
2
BioAgeCard + LoadRecoveryCard
HealthScreen.kt · Row de 2 tarjetas · posición: tarjeta 2
State Spring
BioAgeCard
  • trueAgeYears — de state.trueAgeYears (Int, redondeado)
  • profile — para calcular edad cronológica
diff = chronologicalAge - trueAgeYears // diff > 0 → más joven de lo esperado ✓ // diff < 0 → por encima de su edad ✗
diff Color Mensaje
≥ 5 Cyan Muy joven para tu edad
1-4 Teal Más joven de lo esperado
0 Teal Acorde a tu edad
−1 a −4 Amber Ligeramente por encima
≤ −5 Rojo Por encima de tu edad
LoadRecoveryCard
  • loadstate.activityScore (0-100)
  • recoveryphysiologyInsight.recoveryScore o estimado local
  • insightPhysiologyInsight?
Subcomponentes internos
  • ScoreGauge — arco de progreso para Carga
  • RecoveryWidget — semicírculo WHOOP-style para Recuperación (componente externo RecoveryWidget.kt)
Para modificar: LoadRecoveryCard en HealthScreen.kt. El RecoveryWidget vive en ui/component/RecoveryWidget.kt.
07 — Frecuencia Cardíaca
HeartRateCard + HeartRateVitalityMap
3
HeartRateCard → HeartRateVitalityMap
HealthScreen.kt (card) · Charts.kt (mapa) · posición: tarjeta 3
Canvas multicapa Draw-in 1400ms Fases circadianas
FRECUENCIA CARDÍACA
72 bpm
00h06h12h18h
■ Sueño
■ Activo
● Estrés
52
Mín
72
Prom
118
Máx
ESTADO DE FORMA
Óptimo
HRV 48ms · RHR 54
RECUPERACIÓN
87
5 capas del Canvas (HeartRateVitalityMap)
# Capa Color Fuente
1 Fases sueño #1A237E α22% SleepRecord (excluye AWAKE)
2 Fases actividad #E65100 α14% FC ≥ 90 bpm × ≥3 muestras consecutivas
3 Etiquetas tiempo Blanco α28% Calculadas sobre ventana 24h (cada 6h)
4 Línea HR #26C6DA / #E65100 HeartRateRecord.bpm
5 Puntos estrés #B71C1C (blink) sedentaryStressAlert=true + máximos locales 75-100 bpm
Animación de dibujo (draw-in)
visibleCount = (drawProgress × records.size) drawProgress: 0.0 → 1.0 en 1 400ms // La línea se dibuja de izquierda a derecha // Se anima cuando cambia records.size
Widgets inferiores — VitalityStatChip × 2
  • Estado de FormaPhysiologyInsight.RecoveryStatus.label + subtítulo HRV/RHR
  • Recuperación — score 0-100 + barra animada spring
Parámetros de HeartRateCard
  • currentHrlatestVitals.heartRate o último bpm
  • hrHistorystate.hrHistory (lista completa 24h)
  • sleepRecordsstate.sleepHistory para overlays
  • insightstate.physiologyInsight
  • recoveryScore — estimado local si no hay insight
Para modificar el mapa HR: fun HeartRateVitalityMap en ui/component/Charts.kt.
Para modificar los chips: HrVitalityInsights y VitalityStatChip en HealthScreen.kt.
Para cambiar el umbral de actividad (actualmente 90 bpm): buscar rec.bpm >= 90 en ambas ubicaciones.
4
SpO2Card + MetabolismCard
HealthScreen.kt · Row de 2 tarjetas · posición: tarjeta 4
StateBMR Mifflin-St Jeor
SpO2Card
  • latestspo2History.lastOrNull()?.percent
  • avg — media del historial del día
Valor Estado Color
≥ 96% Normal Cyan
90-95% Aceptable Amber
< 90% Bajo — Consulta médico Rojo
MetabolismCard — Fórmula BMR
BMR = 10×peso + 6.25×altura − 5×edad + 5 // si MALE − 161 // si FEMALE // Mifflin-St Jeor (commonMain-safe) activeRatio = calories / BMR estado: activo ≥ 25% · moderado ≥ 10% sedentario < 10%
Requiere perfil completo (peso, altura, año de nacimiento). Sin datos → "Perfil incompleto".
08 — Sueño
SleepCard
5
SleepCard
HealthScreen.kt · posición: tarjeta 5
SpringSleepRecord
Visual — estado con datos
SUEÑO
7h 24m
/ 8h objetivo
1h38m
Profundo
1h29m
REM
4h17m
Ligero
Origen de datos
  • sleepMin — suma durationMin donde level != AWAKE
  • deepMin — suma donde level == DEEP
  • remMin — suma donde level == REM
  • lightMin — suma donde level == LIGHT
  • goalMinuserProfile.sleepGoalMin ?: 480 (8h por defecto)
Colores de fases
Fase Color Hex
Profundo #7986CB
REM #4DD0E1
Ligero #80CBC4
Color de la barra de progreso
% del objetivo Color
≥ 85% Cyan (bueno)
60-84% Teal (aceptable)
< 60% Amber (insuficiente)
Para modificar: SleepCard, SleepStagesBar, SleepStageItem en HealthScreen.kt. El estado vacío usa SleepEmptyState (ui/component/SleepEmptyState.kt).
09 — HRV, Estrés y Presión
HrvCard + StressCard + BloodPressureCard
6
HrvCard + StressCard
HealthScreen.kt · Row de 2 tarjetas · posición: tarjeta 6
Canvas GaugeEstimación local
HrvCard
  • hrvlatestVitals.hrv o estimateHrv(hrHistory)
// Estimación local (sin sensor dedicado): diffs = hrHistory.zipWithNext { a,b → b.bpm - a.bpm } rmssd_approx = sqrt(mean(diffs²)) // ⚠ Aproximación BPM-level, no RR-interval
HRV (ms) Estado Color
≥ 60 Excelente Cyan
35-59 Normal Teal
20-34 Bajo Amber
< 20 Muy bajo Rojo
StressCard — Gauge circular
// Si HRV disponible: stressScore = 100 − (hrv / 80 × 100) // Si solo historial FC: avgDiff = mean(|bpm[i] - bpm[i-1]|) stressScore = 100 − (avgDiff / 10 × 50)
Score Estado Color
0-25 Relajado Cyan
26-50 Moderado Teal
51-75 Elevado Amber
76-100 Alto Rojo
Nota: el gauge es un arco circular animado con spring(DampingRatioMediumBouncy).
7
BloodPressureCard
HealthScreen.kt · posición: tarjeta 7 (última)
Estimación
Entrada
  • restingHr — mínimo bpm del día (hrBpms.minOrNull())
  • currentHr — bpm en tiempo real
No es una medición real de presión arterial. Es una perspectiva inferida de la FC en reposo. Se muestra con disclaimer explícito en la UI.
Tabla de perspectivas
RHR Perspectiva Color
< 50 Presión óptima · Perfil atlético Cyan
50-59 Presión óptima · FC excelente Cyan
60-69 Presión normal Teal
70-79 Presión normal-alta Amber
80-89 Atención — FC elevada Naranja
≥ 90 Consulta médico Rojo
10 — Dashboard
RecentActivitiesWidget + ActivityRow
A
RecentActivitiesWidget
DashboardScreen.kt · private · posición: después de HealthSectionContent
ActivityViewModel
Actividades recientes
Ver todas →
🏃
Correr
28 mar
32:14
284 kcal
🚴
Ciclismo
26 mar
58:33
512 kcal
🏋️
Fuerza
25 mar
45:00
380 kcal
Parámetros
  • sessions List<ActivitySession> — de activityState.history
  • isLoading Boolean — de activityState.isLoadingHistory
  • onViewAll — navega a ActivityScreen
  • onSessionClick — navega al detalle
ActivityRow — datos mostrados
  • exerciseType.icon — emoji del tipo de ejercicio
  • exerciseType.label — nombre del ejercicio
  • startMs → fecha — "28 mar" (calculado con Instant.toLocalDateTime)
  • durationSec → "MM:SS" — formato con padStart(2,'0')
  • totalCalories — se muestra si > 0
Máximo visible

Muestra máximo 3 sesiones (sessions.take(3)). La pantalla completa se ve en ActivityScreen.


Resumen de Componentes
# Componente Archivo Pantallas Datos desde
TrueAgeWidget TrueAgeWidget.kt Dashboard state.trueAgeInsight / trueAgeYears
R ResilienceRadarChart ResilienceRadarChart.kt Dashboard physiologyInsight, spo2History, trueAgeInsight
1 ActivityRingsWidget HealthScreen.kt Dashboard + Health selectedDaySteps, latestVitals
2a BioAgeCard HealthScreen.kt Dashboard + Health state.trueAgeYears, userProfile
2b LoadRecoveryCard HealthScreen.kt Dashboard + Health activityScore, physiologyInsight
3 HeartRateCard + HeartRateVitalityMap HealthScreen.kt + Charts.kt Dashboard + Health hrHistory, sleepHistory, physiologyInsight
4a SpO2Card HealthScreen.kt Dashboard + Health spo2History
4b MetabolismCard HealthScreen.kt Dashboard + Health steps, calories, userProfile
5 SleepCard HealthScreen.kt Dashboard + Health sleepHistory, userProfile.sleepGoalMin
6a HrvCard HealthScreen.kt Dashboard + Health latestVitals.hrv o estimado
6b StressCard HealthScreen.kt Dashboard + Health HRV o variación FC
7 BloodPressureCard HealthScreen.kt Dashboard + Health hrHistory.minOrNull() (RHR)
A RecentActivitiesWidget DashboardScreen.kt Dashboard activityState.history