⚙️ Configuration

The properties reconcile loop is a digital twin pattern for persistent device configuration.

Characteristics

  • Desired vs Reported: Separate intended and actual states
  • Eventually consistent: Changes apply when device is ready
  • Persistent: Survives reboots and disconnections
  • Versioned: Track all configuration changes
  • Bulk capable: Update many devices at once

How It Works

Updated by operators

Cloud                     Mir                     Device
  │                        │                         │
  ├── Update Desired ─────▶│                         │
  │                        ├── Store in Database     │
  │                        ├── Send Desired ────────▶│
  │                        │                         │
  │                        │                         ├── Update local database
  │                        │                         ├── Call config handlers
  │                        │                         │
  │                        │◀── Send Reported Props ─┤
  │                        ├── Update Digital Twin   │
  │◀── Confirm Sync ───────┤                         │

Device bootup

Device                       Mir
  │   (online)                │
  ├── 1a. Fetch Desired ─────▶│
  │                           │
  │◀── Send Desired Props ────┤
  │                           │
  ├── Update local database   │
  │                           │
  │   (offline)               │
  ├── 1b. Fetch from local    │
  │                           │
  ├── 2. Call config handlers │

Implementation

Schema Definition

// Desired property
message SampleRateConfig {
  option (mir.device.v1.message_type) = MESSAGE_TYPE_TELECONFIG;

    int32 interval_seconds = 1;
}

// Reported property
message SampleRateStatus {
    int32 interval_seconds = 1;
    google.protobuf.Timestamp last_update = 2;
}

// Reported property
message BatteryStatus {
    int32 percentage = 1;
    float voltage = 2;
    bool charging = 3;
}

Device Side:

// Handle a desired property and report
m.HandleProperties(&schemav1.SampleRateConfig{}, func(msg proto.Message) {
	cmd := msg.(*schemav1.SimpleRateConfig)
  if desired.IntervalSeconds < 10 {
    return fmt.Errorf("interval too short: %d", desired.IntervalSeconds)
  }

  err := sensor.SetSampleRate(desired.IntervalSeconds)
  if err != nil {
    return err
  }

  if err := m.SendProperties(&schemav1.SampleRateStatus{
      LastUpdate: time.Now(),
      IntervalSeconds: desired.IntervalSeconds,
  }); err != nil {
			m.Logger().Error().Err(err).Msg("error sending data rate status")
	}

  return nil
}

// Report a property directly
if err := m.SendProperties(&schemav1.BatteryStatus{
    Percentage: 87,
    Voltage: 3.2,
    Charging: false,
}); err != nil {
	m.Logger().Error().Err(err).Msg("error sending battery status")
}

Use Cases

  • Device settings: Sample rates, thresholds, modes
  • Network configuration: WiFi credentials, server URLs
  • Feature flags: Enable/disable functionality
  • Calibration data: Sensor offsets, scaling factors
  • Schedules: Operating hours, maintenance windows