← Engineering Blog · April 2026

Twin Calibration: Bridging Digital and Physical One Sensor at a Time

digital-twin esp32 calibration convergence

Version 16.5 is a convergence milestone. For the first time, physical sensors feed into the simulation's sub-frame generation — not as overrides, not as replacements, but as a calibration layer. The sol keyframes remain sacred. The timeline is untouched. But between keyframes, real-world sensor data nudges the interpolation toward physical truth. Each sensor added is one more harmonic from reality instead of pure mathematics.

The Standing Wave Analogy

Think of the simulation as a standing wave. The fundamental frequency is defined by the sol keyframes — the 1,087 immutable data points that anchor the timeline. These never change. They are the tone.

Sub-frame interpolation adds the first overtone — smooth curves between keyframes derived from diurnal physics models. Temperature follows a sinusoidal curve. Wind modulates with thermal convection patterns. This is still pure simulation.

Physical sensors add higher harmonics. They don't change the fundamental. They don't alter the overtones. They add texture — the micro-variations that make a real environment different from a modeled one.

Fundamental ▏████████████████████████████▕ Sol keyframes (immutable) 1st overtone ▏~~∿∿~~∿∿~~∿∿~~∿∿~~∿∿~~∿∿~~▕ Diurnal physics model 2nd overtone ▏⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓⁓▕ BME280 (temp, humidity, pressure) 3rd overtone ▏·····························▕ TSL2591 (ambient light) 4th overtone ▏‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥▕ SCD40 (CO₂ concentration) Sum = Calibrated sub-frame (converges to keyframe at boundaries)
The fundamental is untouched. Sensors add overtones. The result is a richer waveform that still resolves to the same note at every keyframe boundary.

The Hardware Stack

Three sensor modules feed the calibration layer, all connected to an ESP32 microcontroller running a WebSocket server:

BME280 — Temperature, Humidity, Pressure

TSL2591 — Ambient Light

SCD40 — CO₂ Concentration

The WebSocket Bridge

The ESP32 runs a lightweight WebSocket server on the local network. Sensor readings are published as JSON at 1 Hz:

// ESP32 WebSocket payload (1 Hz)
{
  "timestamp": "2026-04-15T14:23:01Z",
  "bme280": {
    "temp_c": 22.4,
    "humidity_pct": 45.2,
    "pressure_hpa": 1013.25
  },
  "tsl2591": {
    "lux": 340,
    "ir_ratio": 0.32
  },
  "scd40": {
    "co2_ppm": 890,
    "temp_c": 22.1
  }
}

The browser-side simulation connects to this WebSocket and feeds readings into the sub-frame interpolation engine. If the WebSocket disconnects, the simulation falls back to pure mathematical interpolation. No sensor? No problem. The calibration layer is strictly additive.

┌──────────┐ WebSocket ┌──────────────────┐ │ ESP32 │ ──── 1 Hz ──────→ │ Browser Client │ │ │ JSON │ │ │ BME280 │ │ Sub-Frame Engine │ │ TSL2591 │ │ ┌──────────────┐ │ │ SCD40 │ │ │ Keyframe N │ │ └──────────┘ │ │ ↕ blend │ │ │ │ Sensor data │ │ │ │ ↕ decay │ │ │ │ Keyframe N+1 │ │ │ └──────────────┘ │ └──────────────────┘

The Decay Function

This is the critical constraint that keeps the timeline sacred: sensor influence decays to zero at keyframe boundaries.

A sub-frame at the midpoint between two keyframes has maximum sensor influence. As the sub-frame approaches the next keyframe, sensor influence fades and the interpolated value converges exactly to the keyframe value.

function blendWithSensor(keyframeValue, sensorValue, t) {
  // t = 0 at keyframe N, t = 1 at keyframe N+1
  // Influence peaks at t = 0.5, decays to 0 at boundaries
  const influence = Math.sin(t * Math.PI);
  const maxInfluence = 0.3;  // sensor can nudge up to 30% at peak
  const blend = influence * maxInfluence;
  return keyframeValue * (1 - blend) + sensorValue * blend;
}

// At t = 0.0: influence = 0.00 → pure keyframe N
// At t = 0.25: influence = 0.21 → 6.4% sensor blend
// At t = 0.50: influence = 0.30 → 9.0% sensor blend (peak)
// At t = 0.75: influence = 0.21 → 6.4% sensor blend
// At t = 1.0: influence = 0.00 → pure keyframe N+1

The sin(t * π) envelope guarantees smooth entry and exit. No discontinuities. No jumps. The sensor whispers between keyframes and goes silent at the boundaries.

Sensor-to-Sim Mapping

Each sensor channel maps to a specific simulation parameter with its own scaling function:

const sensorMapping = {
  'bme280.temp_c': {
    simParam: 'weather.temp_c',
    // Earth temp → Mars temp: scale to Martian range
    transform: (v) => (v - 20) * 4.5 - 60,
  },
  'bme280.pressure_hpa': {
    simParam: 'weather.pressure_pa',
    // Earth pressure → Mars pressure: scale 1013 hPa → 610 Pa
    transform: (v) => (v / 1013.25) * 610,
  },
  'tsl2591.ir_ratio': {
    simParam: 'weather.dust_tau',
    // Higher IR ratio → more particulates → higher optical depth
    transform: (v) => v * 3.0,
  },
  'scd40.co2_ppm': {
    simParam: 'isru.efficiency',
    // Higher CO₂ → better ISRU extraction
    transform: (v) => Math.min(v / 2000, 1.0),
  }
};

What Convergence Means

Before v16.5, the simulation was a closed system. Frames described a Mars that existed only in mathematics. The interpolation between keyframes was smooth but fictional — based on models, not measurements.

With sensor calibration, the gap between digital and physical narrows. Not all at once. One sensor at a time.

Each sensor is additive. Each one replaces a slice of interpolation with a slice of reality. The simulation doesn't become less accurate — it becomes differently accurate. Grounded in physical measurement rather than mathematical assumption.

The bridge between pure sim and analog habitat isn't a bridge you build once. It's a gradient you walk along, one sensor at a time. Each step replaces interpolation with measurement. Each measurement is one more harmonic from reality.

The Architecture Guarantee

The critical guarantee: removing all sensors returns the simulation to its pre-v16.5 state exactly. No sensor data is persisted into keyframes. No calibration residue accumulates. The decay function ensures clean convergence at every boundary.

This is not a digital twin in the traditional sense — where the physical system is the source of truth. Here, the simulation is the source of truth. The physical sensors are guests. They influence the spaces between truths, but they never alter truth itself. The keyframes are sacred. The timeline is sacred. The sensors just make the silence between notes more musical.


← Back to blog index · GitHub · See it live in the RTS view