ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
cw2017.cpp
Go to the documentation of this file.
1#include "cw2017.h"
2
3#include <cstring>
4
5#include "esphome/core/hal.h"
6#include "esphome/core/log.h"
7
8#ifdef USE_DEEP_SLEEP
10#endif
11
12namespace esphome::cw2017 {
13
14ESPHOME_LOG_TAG(TAG, "cw2017");
15
16static constexpr uint8_t REG_VERSION = 0x00;
17static constexpr uint8_t REG_VCELL = 0x02;
18static constexpr uint8_t REG_SOC = 0x04;
19static constexpr uint8_t REG_MODE = 0x08;
20static constexpr uint8_t REG_SOC_ALERT = 0x0B;
21static constexpr uint8_t REG_BATINFO = 0x10;
22
23static constexpr uint8_t MODE_NORMAL = 0x00;
24static constexpr uint8_t MODE_SLEEP = 0x30;
25static constexpr uint8_t UPDATE_FLAG = 0x80;
26static constexpr uint8_t RESTART_MODES[] PROGMEM = {0xF0, MODE_SLEEP, MODE_NORMAL};
27static constexpr uint8_t RESTART_STEPS = sizeof(RESTART_MODES);
28
29static constexpr uint16_t VCELL_MASK = 0x3FFF;
30static constexpr float VCELL_VOLTS_PER_LSB = 312.5e-6f;
31
32static constexpr uint32_t STEP_DELAY_MS = 20;
33static constexpr uint32_t READY_POLL_MS = 100;
34static constexpr uint8_t READY_ATTEMPTS = 40;
35static constexpr uint32_t INIT_RETRY_MS = 1000;
36static constexpr uint8_t INIT_ATTEMPTS = 3;
37
38static bool version_is_running(uint8_t version) { return (version & 0xFD) == 0x0D; }
39
40// The configured profile only changes with new firmware, and flashing never ends in a deep-sleep wake.
41static bool woke_from_deep_sleep() {
42#ifdef USE_DEEP_SLEEP
44#else
45 return false;
46#endif
47}
48
49bool CW2017::read_profile_matches_(bool &matches) {
50 uint8_t stored[PROFILE_SIZE];
51 uint8_t expected[PROFILE_SIZE];
52 if (!this->read_bytes(REG_BATINFO, stored, PROFILE_SIZE))
53 return false;
54 progmem_memcpy(expected, this->profile_, PROFILE_SIZE);
55 matches = std::memcmp(stored, expected, PROFILE_SIZE) == 0;
56 return true;
57}
58
60 // Built here with the register first, so the bus layer does not heap-allocate a buffer this size.
61 uint8_t buffer[1 + PROFILE_SIZE] = {REG_BATINFO};
62 progmem_memcpy(buffer + 1, this->profile_, PROFILE_SIZE);
63 return this->write(buffer, sizeof(buffer)) == i2c::ERROR_OK && this->write_byte(REG_SOC_ALERT, UPDATE_FLAG);
64}
65
66void CW2017::setup() { this->init_(); }
67
68// Retried because another device resetting on the same bus can disturb an early transfer.
69void CW2017::init_failed_(const LogString *reason) {
70 if (++this->init_attempts_ >= INIT_ATTEMPTS) {
71 this->mark_failed(reason);
72 return;
73 }
74 this->set_timeout(INIT_RETRY_MS, [this]() { this->init_(); });
75}
76
78 // A profile upload only takes effect after a restart, so a failed restart is retried, not skipped.
79 if (this->restart_pending_) {
80 this->restart_();
81 return;
82 }
83
84 uint8_t mode = 0;
85 uint8_t version = 0;
86 uint8_t config = 0;
87 if (!this->read_byte(REG_MODE, &mode) || !this->read_byte(REG_VERSION, &version) ||
88 !this->read_byte(REG_SOC_ALERT, &config)) {
89 this->init_failed_(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
90 return;
91 }
92 const bool has_profile = (config & UPDATE_FLAG) != 0;
93 bool matches = has_profile && woke_from_deep_sleep();
94 if (has_profile && !matches && !this->read_profile_matches_(matches)) {
95 this->init_failed_(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
96 return;
97 }
98
99 if (!matches) {
100 ESP_LOGI(TAG, "Uploading battery profile");
101 if (!this->upload_profile_()) {
102 this->init_failed_(LOG_STR("Battery profile upload failed"));
103 return;
104 }
105 this->restart_();
106 return;
107 }
108
109 if (mode == MODE_SLEEP) {
110 // The gauge keeps its profile and state through its own sleep, so a wake is a mode write, not a restart.
111 if (!this->write_byte(REG_MODE, MODE_NORMAL)) {
112 this->init_failed_(LOG_STR("Gauge wake failed"));
113 return;
114 }
115 this->set_timeout(STEP_DELAY_MS, [this]() { this->wait_ready_(0); });
116 return;
117 }
118
119 if (mode != MODE_NORMAL || !version_is_running(version)) {
120 this->restart_();
121 return;
122 }
123
124 this->wait_ready_(0);
125}
126
128 this->restart_pending_ = true;
129 this->set_timeout(STEP_DELAY_MS, [this]() { this->restart_step_(0); });
130}
131
132void CW2017::restart_step_(uint8_t step) {
133 if (!this->write_byte(REG_MODE, progmem_read_byte(&RESTART_MODES[step]))) {
134 this->init_failed_(LOG_STR("Restart failed"));
135 return;
136 }
137 if (step + 1 < RESTART_STEPS) {
138 this->set_timeout(STEP_DELAY_MS, [this, step]() { this->restart_step_(step + 1); });
139 } else {
140 this->restart_pending_ = false;
141 this->set_timeout(STEP_DELAY_MS, [this]() { this->wait_ready_(0); });
142 }
143}
144
145void CW2017::wait_ready_(uint8_t attempt) {
146 uint8_t version = 0;
147 uint8_t soc = 0;
148 if (this->read_byte(REG_VERSION, &version) && version_is_running(version) && this->read_byte(REG_SOC, &soc) &&
149 soc <= 100) {
150 ESP_LOGD(TAG, "Gauge ready %u ms after boot", (unsigned) millis());
151 this->ready_ = true;
152 this->update();
153 return;
154 }
155 if (attempt + 1 >= READY_ATTEMPTS) {
156 this->init_failed_(LOG_STR("Gauge did not become ready"));
157 return;
158 }
159 this->set_timeout(READY_POLL_MS, [this, attempt]() { this->wait_ready_(attempt + 1); });
160}
161
163 if (!this->ready_)
164 return;
165
166 if (this->battery_level_sensor_ != nullptr) {
167 uint8_t soc = 0;
168 if (this->read_byte(REG_SOC, &soc) && soc <= 100) {
170 } else {
171 this->status_set_warning();
172 return;
173 }
174 }
175
176 if (this->battery_voltage_sensor_ != nullptr) {
177 uint16_t raw = 0;
178 if (this->read_byte_16(REG_VCELL, &raw)) {
179 this->battery_voltage_sensor_->publish_state((raw & VCELL_MASK) * VCELL_VOLTS_PER_LSB);
180 } else {
181 this->status_set_warning();
182 return;
183 }
184 }
185 this->status_clear_warning();
186}
187
189 ESP_LOGCONFIG(TAG, "CW2017:");
190 LOG_I2C_DEVICE(this);
191 LOG_UPDATE_INTERVAL(this);
192 LOG_SENSOR(" ", "Battery Level", this->battery_level_sensor_);
193 LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_sensor_);
194}
195
196} // namespace esphome::cw2017
BedjetMode mode
BedJet operating mode.
uint8_t raw[35]
Definition bl0939.h:0
void mark_failed()
Mark this component as failed.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void status_clear_warning()
Definition component.h:296
void init_failed_(const LogString *reason)
Definition cw2017.cpp:69
sensor::Sensor * battery_level_sensor_
Definition cw2017.h:32
void restart_step_(uint8_t step)
Definition cw2017.cpp:132
sensor::Sensor * battery_voltage_sensor_
Definition cw2017.h:33
const uint8_t * profile_
Definition cw2017.h:31
void wait_ready_(uint8_t attempt)
Definition cw2017.cpp:145
void update() override
Definition cw2017.cpp:162
void setup() override
Definition cw2017.cpp:66
void dump_config() override
Definition cw2017.cpp:188
bool read_profile_matches_(bool &matches)
Definition cw2017.cpp:49
ErrorCode write(const uint8_t *data, size_t len) const
writes an array of bytes to a device using an I2CBus
Definition i2c.h:195
bool write_byte(uint8_t a_register, uint8_t data) const
Definition i2c.h:312
bool read_byte(uint8_t a_register, uint8_t *data)
Definition i2c.h:272
bool read_byte_16(uint8_t a_register, uint16_t *data)
Definition i2c.h:281
bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len)
Compat APIs All methods below have been added for compatibility reasons.
Definition i2c.h:249
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
uint32_t buffer
Definition gif.cpp:50
ESPHOME_LOG_TAG(TAG, "cw2017")
bool woke_from_deep_sleep()
Return true when this boot is a wake from deep sleep.
@ ERROR_OK
No error found during execution of method.
Definition i2c_bus.h:15
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
uint8_t progmem_read_byte(const uint8_t *addr)
Definition hal.h:51
static void uint32_t