7#ifdef USE_SHD_FIRMWARE_DATA
12#include <HardwareSerial.h>
22constexpr char TAG[] =
"shelly_dimmer";
24constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200;
25constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3;
26constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000;
29constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01;
30constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04;
33constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01;
34constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10;
35constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11;
36constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20;
39constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2;
40constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10;
41constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3;
44#ifdef USE_SHD_FIRMWARE_DATA
45constexpr uint8_t STM_FIRMWARE[]
PROGMEM = USE_SHD_FIRMWARE_DATA;
46constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES =
sizeof(STM_FIRMWARE);
50constexpr float POWER_SCALING_FACTOR = 880373;
51constexpr float VOLTAGE_SCALING_FACTOR = 347800;
52constexpr float CURRENT_SCALING_FACTOR = 1448;
55template<
typename T,
size_t N>
constexpr size_t size(
const T (&)[N])
noexcept {
return N; }
60namespace shelly_dimmer {
64 return std::accumulate<decltype(buf), uint16_t>(buf, buf +
len, 0);
75 this->
send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION,
nullptr, 0);
76 ESP_LOGI(TAG,
"STM32 current firmware version: %d.%d, desired version: %d.%d", this->
version_major_,
77 this->
version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION);
80#ifdef USE_SHD_FIRMWARE_DATA
82 ESP_LOGW(TAG,
"Failed to upgrade firmware");
88 this->
send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION,
nullptr, 0);
90 ESP_LOGE(TAG,
"STM32 firmware upgrade already performed, but version is still incorrect");
95 ESP_LOGW(TAG,
"Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
104 ESP_LOGI(TAG,
"Initializing");
110 this->
send_command_(SHELLY_DIMMER_PROTO_CMD_POLL,
nullptr, 0);
118 ESP_LOGCONFIG(TAG,
"ShellyDimmer:");
123 " Leading Edge: %s\n"
124 " Warmup Brightness: %d\n"
125 " Minimum Brightness: %d\n"
126 " Maximum Brightness: %d",
131 LOG_UPDATE_INTERVAL(
this);
134 " STM32 current firmware version: %d.%d \n"
135 " STM32 required firmware version: %d.%d",
137 USE_SHD_FIRMWARE_MINOR_VERSION);
141 ESP_LOGE(TAG,
" Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
151 state->current_values_as_brightness(&brightness);
155 ESP_LOGV(TAG,
"Not sending unchanged value");
158 ESP_LOGD(TAG,
"Brightness update: %d (raw: %f)", brightness_int, brightness);
162#ifdef USE_SHD_FIRMWARE_DATA
164 ESP_LOGW(TAG,
"Starting STM32 firmware upgrade");
171 ESP_LOGW(TAG,
"Failed to initialize STM32");
177 ESP_LOGW(TAG,
"Failed to erase STM32 flash memory");
181 static constexpr uint32_t BUFFER_SIZE = 256;
185 uint8_t buffer[BUFFER_SIZE];
186 const uint8_t *p = STM_FIRMWARE;
188 uint32_t addr = stm32->dev->fl_start;
189 const uint32_t
end = addr + STM_FIRMWARE_SIZE_IN_BYTES;
191 while (addr <
end && offset < STM_FIRMWARE_SIZE_IN_BYTES) {
192 const uint32_t left_of_buffer = std::min(
end - addr, BUFFER_SIZE);
193 const uint32_t
len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset);
199 std::memcpy(buffer, p, BUFFER_SIZE);
203 ESP_LOGW(TAG,
"Failed to write to STM32 flash memory");
211 ESP_LOGI(TAG,
"STM32 firmware upgrade successful");
219 if (brightness == 0.0) {
227 const uint8_t payload[] = {
229 static_cast<uint8_t
>(brightness & 0xff),
230 static_cast<uint8_t
>(brightness >> 8),
232 static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE,
"Invalid payload size");
234 this->
send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE);
240 const uint16_t fade_rate = std::min(uint16_t{100}, this->
fade_rate_);
242 float brightness = 0.0;
243 if (this->
state_ !=
nullptr) {
247 ESP_LOGD(TAG,
"Brightness update: %d (raw: %f)", brightness_int, brightness);
249 const uint8_t payload[] = {
251 static_cast<uint8_t
>(brightness_int & 0xff),
252 static_cast<uint8_t
>(brightness_int >> 8),
257 static_cast<uint8_t
>(fade_rate & 0xff),
258 static_cast<uint8_t
>(fade_rate >> 8),
266 static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE,
"Invalid payload size");
268 this->
send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE);
275 ESP_LOGD(TAG,
"Sending command: 0x%02x (%d bytes) payload 0x%s", cmd,
len,
format_hex(payload,
len).c_str());
278 uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE];
282 int retries = SHELLY_DIMMER_MAX_RETRIES;
287 ESP_LOGD(TAG,
"Command sent, waiting for reply");
288 const uint32_t tx_time =
millis();
289 while (
millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) {
295 ESP_LOGW(TAG,
"Timeout while waiting for reply");
297 ESP_LOGW(TAG,
"Failed to send command");
305 data[0] = SHELLY_DIMMER_PROTO_START_BYTE;
306 data[1] = ++this->
seq_;
311 if (payload !=
nullptr) {
312 std::memcpy(data + 4, payload,
len);
318 data[pos++] =
static_cast<uint8_t
>(csum >> 8);
319 data[pos++] =
static_cast<uint8_t
>(csum & 0xff);
320 data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE;
329 return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1;
330 }
else if (pos < 4) {
336 const uint8_t payload_len = this->
buffer_[3];
337 if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) {
341 if (pos < 4 + payload_len + 1) {
346 if (pos == 4 + payload_len + 1) {
348 const uint16_t csum = (this->
buffer_[pos - 1] << 8 | c);
350 if (csum != csum_verify) {
356 if (pos == 4 + payload_len + 2) {
358 return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1;
365 const uint8_t c = this->
read();
368 ESP_LOGV(TAG,
"Read byte: 0x%02x (pos %d)", c, this->
buffer_pos_);
394 const uint8_t cmd = this->
buffer_[2];
395 const uint8_t payload_len = this->
buffer_[3];
397 ESP_LOGD(TAG,
"Got frame: 0x%02x", cmd);
405 const uint8_t *payload = &this->
buffer_[4];
409 case SHELLY_DIMMER_PROTO_CMD_POLL: {
410 if (payload_len < 16) {
414 const uint8_t hw_version = payload[0];
416 const uint16_t brightness =
encode_uint16(payload[3], payload[2]);
418 const uint32_t power_raw =
encode_uint32(payload[7], payload[6], payload[5], payload[4]);
420 const uint32_t voltage_raw =
encode_uint32(payload[11], payload[10], payload[9], payload[8]);
422 const uint32_t current_raw =
encode_uint32(payload[15], payload[14], payload[13], payload[12]);
424 const uint16_t fade_rate = payload[16];
428 power = POWER_SCALING_FACTOR /
static_cast<float>(power_raw);
432 if (voltage_raw > 0) {
433 voltage = VOLTAGE_SCALING_FACTOR /
static_cast<float>(voltage_raw);
437 if (current_raw > 0) {
438 current = CURRENT_SCALING_FACTOR /
static_cast<float>(current_raw);
441 ESP_LOGI(TAG,
"Got dimmer data:");
442 ESP_LOGI(TAG,
" HW version: %d", hw_version);
443 ESP_LOGI(TAG,
" Brightness: %d", brightness);
444 ESP_LOGI(TAG,
" Fade rate: %d", fade_rate);
445 ESP_LOGI(TAG,
" Power: %f W", power);
446 ESP_LOGI(TAG,
" Voltage: %f V", voltage);
447 ESP_LOGI(TAG,
" Current: %f A", current);
462 case SHELLY_DIMMER_PROTO_CMD_VERSION: {
463 if (payload_len < 2) {
471 case SHELLY_DIMMER_PROTO_CMD_SWITCH:
472 case SHELLY_DIMMER_PROTO_CMD_SETTINGS: {
473 return payload_len >= 1 && payload[0] == 0x01;
482 ESP_LOGD(TAG,
"Reset STM32, boot0=%d", boot0);
499 ESP_LOGD(TAG,
"Reset STM32 done");
507 Serial.begin(115200, SERIAL_8N1);
520 Serial.begin(115200, SERIAL_8E1);
virtual void mark_failed()
Mark this component as failed.
virtual void digital_write(bool value)=0
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
void current_values_as_brightness(float *brightness)
void publish_state(float state)
Publish a new state to the front-end.
sensor::Sensor * power_sensor_
void write_state(light::LightState *state) override
void reset_dfu_boot_()
Reset STM32 to boot into DFU mode to enable firmware upgrades.
bool upgrade_firmware_()
Performs a firmware upgrade.
sensor::Sensor * voltage_sensor_
void reset_normal_boot_()
Reset STM32 to boot the regular firmware.
bool handle_frame_()
Handles a complete frame.
sensor::Sensor * current_sensor_
void send_brightness_(uint16_t brightness)
Sends the given brightness value.
void dump_config() override
void send_settings_()
Sends dimmer configuration.
int handle_byte_(uint8_t c)
Handles a single byte as part of a protocol frame.
std::array< uint8_t, SHELLY_DIMMER_BUFFER_SIZE > buffer_
uint16_t convert_brightness_(float brightness)
Convert relative brightness into a dimmer brightness value.
light::LightState * state_
bool is_running_configured_version() const
size_t frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *payload, size_t len)
Frames a given command payload.
void reset_(bool boot0)
Reset STM32 with the BOOT0 pin set to the given value.
bool read_frame_()
Reads a response frame.
uint16_t warmup_brightness_
bool send_command_(uint8_t cmd, const uint8_t *payload, uint8_t len)
Sends a command and waits for an acknowledgement.
void write_array(const uint8_t *data, size_t len)
uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len)
Computes a crappy checksum as defined by the Shelly Dimmer protocol.
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init)
constexpr auto STREAM_SERIAL
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, const unsigned int len)
constexpr auto STM32_MASS_ERASE
stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages)
Providing packet encoding functions for exchanging data with a remote host.
std::string format_hex(const uint8_t *data, size_t length)
Format the byte array data of length len in lowercased hex.
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
void IRAM_ATTR HOT delay(uint32_t ms)
uint32_t IRAM_ATTR HOT millis()
T remap(U value, U min, U max, T min_out, T max_out)
Remap value from the range (min, max) to (min_out, max_out).
const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM