summaryrefslogtreecommitdiff
path: root/src/battery/battery.cpp
blob: debef9e66ab7b534fba22b3b0ff259ae714171d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/*
 * Copyright 2023 jacqueline <me@jacqueline.id.au>
 *
 * SPDX-License-Identifier: GPL-3.0-only
 */

#include "battery.hpp"

#include <cstdint>

#include "adc.hpp"
#include "event_queue.hpp"
#include "freertos/portmacro.h"
#include "samd.hpp"
#include "system_events.hpp"

namespace battery {

static const TickType_t kBatteryCheckPeriod = pdMS_TO_TICKS(60 * 1000);

/*
 * Battery voltage, in millivolts, at which the battery charger IC will stop
 * charging.
 */
static const uint32_t kFullChargeMilliVolts = 4200;

/*
 * Battery voltage, in millivolts, at which *we* will consider the battery to
 * be completely discharged. This is intentionally higher than the charger IC
 * cut-off and the protection on the battery itself; we want to make sure we
 * finish up and have everything unmounted and snoozing before the BMS cuts us
 * off.
 */
static const uint32_t kEmptyChargeMilliVolts = 3200;  // BMS limit is 3100.

using ChargeStatus = drivers::Samd::ChargeStatus;

static void check_voltage_cb(TimerHandle_t timer) {
  Battery* instance = reinterpret_cast<Battery*>(pvTimerGetTimerID(timer));
  instance->Update();
}

Battery::Battery(drivers::Samd& samd, std::unique_ptr<drivers::AdcBattery> adc)
    : samd_(samd), adc_(std::move(adc)) {
  timer_ = xTimerCreate("BATTERY", kBatteryCheckPeriod, true, this,
                        check_voltage_cb);
  xTimerStart(timer_, portMAX_DELAY);
  Update();
}

Battery::~Battery() {
  xTimerStop(timer_, portMAX_DELAY);
  xTimerDelete(timer_, portMAX_DELAY);
}

auto Battery::Update() -> void {
  std::lock_guard<std::mutex> lock{state_mutex_};

  auto charge_state = samd_.GetChargeStatus();

  // FIXME: So what we *should* do here is measure the actual real-life
  // time from full battery -> empty battery, store it in NVS, then rely on
  // that. If someone could please do this, it would be lovely. Thanks!
  uint32_t mV = std::max(adc_->Millivolts(), kEmptyChargeMilliVolts);
  uint_fast8_t percent = static_cast<uint_fast8_t>(std::min<double>(
      std::max<double>(0.0, mV - kEmptyChargeMilliVolts) /
          (kFullChargeMilliVolts - kEmptyChargeMilliVolts) * 100.0,
      100.0));

  bool is_charging;
  if (!charge_state) {
    is_charging = false;
  } else {
    is_charging = *charge_state == ChargeStatus::kChargingRegular ||
                  *charge_state == ChargeStatus::kChargingFast ||
                  *charge_state == ChargeStatus::kFullCharge ||
                  // Treat 'no battery' as charging because, for UI purposes,
                  // we're *kind of* at full charge if u think about it.
                  *charge_state == ChargeStatus::kNoBattery;
  }

  if (state_ && state_->is_charging == is_charging &&
      state_->percent == percent) {
    return;
  }

  state_ = BatteryState{
      .percent = percent,
      .millivolts = mV,
      .is_charging = is_charging,
  };
  EmitEvent();
}

auto Battery::State() -> std::optional<BatteryState> {
  std::lock_guard<std::mutex> lock{state_mutex_};
  return state_;
}

auto Battery::EmitEvent() -> void {
  auto state = state_;
  if (!state) {
    return;
  }
  system_fsm::BatteryStateChanged ev{
      .new_state = *state,
  };
  events::System().Dispatch(ev);
  events::Ui().Dispatch(ev);
}

}  // namespace battery