Skip to content

Instantly share code, notes, and snippets.

@csrutil
Created May 14, 2026 14:00
Show Gist options
  • Select an option

  • Save csrutil/3555327b45fe72a114f315647f8c1cd5 to your computer and use it in GitHub Desktop.

Select an option

Save csrutil/3555327b45fe72a114f315647f8c1cd5 to your computer and use it in GitHub Desktop.

It is table-driven, not a simple full-range linear fit.

We read the battery voltage in millivolts, then convert it to state-of-charge using an open-circuit-voltage table. Between adjacent table entries, the code does linear interpolation.

For the Wio Tracker L1, the OCV table is:

Charge Voltage
100% 4200 mV
90% 3876 mV
80% 3826 mV
70% 3763 mV
60% 3713 mV
50% 3660 mV
40% 3573 mV
30% 3485 mV
20% 3422 mV
10% 3359 mV
0% 3300 mV

The actual conversion source is:

static int getBatteryPercent(uint16_t batteryMillivolts) {
  if (batteryMillivolts == 0) {
    return -1;
  }

  const uint16_t *ocv = getBatteryProfile();
  uint16_t noBatteryMillivolts = (ocv[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
  if (batteryMillivolts < noBatteryMillivolts) {
    return -1;
  }

  float batterySoc = 0.0f;
  uint16_t cellMillivolts = batteryMillivolts / NUM_CELLS;

  for (int i = 0; i < NUM_OCV_POINTS; i++) {
    if (ocv[i] <= cellMillivolts) {
      if (i == 0) {
        batterySoc = 100.0f;
      } else {
        batterySoc = 100.0f / (NUM_OCV_POINTS - 1.0f) *
                     (NUM_OCV_POINTS - 1.0f - i +
                     ((float)cellMillivolts - ocv[i]) / (ocv[i - 1] - ocv[i]));
      }
      break;
    }
  }

  int result = (int)batterySoc;
  if (result < 0) result = 0;
  if (result > 100) result = 100;
  return result;
}

For Wio Tracker L1 specifically:

#define OCV_ARRAY 4200, 3876, 3826, 3763, 3713, 3660, 3573, 3485, 3422, 3359, 3300

The Wio Tracker L1 voltage reader averages ADC samples, converts the raw ADC value to millivolts with the ADC multiplier, caches readings for 5 seconds, and applies a simple exponential moving average:

uint16_t getBattMilliVolts() override {
  static uint32_t last_read_time_ms = 0;
  static float last_filtered_mv = 0.0f;
  static bool initial_read_done = false;

  uint32_t now = millis();

  if (initial_read_done && (now - last_read_time_ms < 5000)) {
    return (uint16_t)last_filtered_mv;
  }

  last_read_time_ms = now;

  analogReadResolution(12);
  analogReference(AR_INTERNAL);
  delay(1);

  uint32_t sum = 0;
  for (int i = 0; i < 15; i++) {
    sum += analogRead(PIN_VBAT_READ);
  }
  uint16_t raw_avg = sum / 15;

  float scaled_mv = (raw_avg * getAdcMultiplier() * AREF_VOLTAGE * 1000.0f) / 4096.0f;

  if (!initial_read_done) {
    last_filtered_mv = scaled_mv;
    initial_read_done = true;
  } else {
    last_filtered_mv += (scaled_mv - last_filtered_mv) * 0.5f;
  }

  return (uint16_t)last_filtered_mv;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment