mirror of
https://github.com/esphome/esphome.git
synced 2026-03-19 09:46:47 +01:00
Add support for Shelly Dimmer 2 (#2954)
Co-authored-by: Niclas Larsson <niclas@edgesystems.se> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Jernej Kos <jernej@kos.mx> Co-authored-by: Richard Nauber <richard@nauber.dev>
This commit is contained in:
parent
047c18eac0
commit
70a35656e4
@ -173,6 +173,7 @@ esphome/components/select/* @esphome/core
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/sgp40/* @SenexCrenshaw
|
||||
esphome/components/shelly_dimmer/* @edge90 @rnauber
|
||||
esphome/components/sht4x/* @sjtrny
|
||||
esphome/components/shutdown/* @esphome/core @jsuanet
|
||||
esphome/components/sim800l/* @glmnet
|
||||
|
||||
2
esphome/components/shelly_dimmer/LICENSE.txt
Normal file
2
esphome/components/shelly_dimmer/LICENSE.txt
Normal file
@ -0,0 +1,2 @@
|
||||
The firmware files for the STM microcontroller (shelly-dimmer-stm32_*.bin) are taken from
|
||||
https://github.com/jamesturton/shelly-dimmer-stm32 and GPLv3 licensed.
|
||||
1
esphome/components/shelly_dimmer/__init__.py
Normal file
1
esphome/components/shelly_dimmer/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@rnauber", "@edge90"]
|
||||
158
esphome/components/shelly_dimmer/dev_table.h
Normal file
158
esphome/components/shelly_dimmer/dev_table.h
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
stm32flash - Open Source ST STM32 flash program for Arduino
|
||||
Copyright (C) 2010 Geoffrey McRae <geoff@spacevs.com>
|
||||
Copyright (C) 2014-2015 Antonio Borneo <borneo.antonio@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
#include "stm32flash.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
constexpr uint32_t SZ_128 = 0x00000080;
|
||||
constexpr uint32_t SZ_256 = 0x00000100;
|
||||
constexpr uint32_t SZ_1K = 0x00000400;
|
||||
constexpr uint32_t SZ_2K = 0x00000800;
|
||||
constexpr uint32_t SZ_16K = 0x00004000;
|
||||
constexpr uint32_t SZ_32K = 0x00008000;
|
||||
constexpr uint32_t SZ_64K = 0x00010000;
|
||||
constexpr uint32_t SZ_128K = 0x00020000;
|
||||
constexpr uint32_t SZ_256K = 0x00040000;
|
||||
|
||||
/*
|
||||
* Page-size for page-by-page flash erase.
|
||||
* Arrays are zero terminated; last non-zero value is automatically repeated
|
||||
*/
|
||||
|
||||
/* fixed size pages */
|
||||
constexpr uint32_t p_128[] = {SZ_128, 0}; // NOLINT
|
||||
constexpr uint32_t p_256[] = {SZ_256, 0}; // NOLINT
|
||||
constexpr uint32_t p_1k[] = {SZ_1K, 0}; // NOLINT
|
||||
constexpr uint32_t p_2k[] = {SZ_2K, 0}; // NOLINT
|
||||
/* F2 and F4 page size */
|
||||
constexpr uint32_t f2f4[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0}; // NOLINT
|
||||
/* F4 dual bank page size */
|
||||
constexpr uint32_t f4db[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, SZ_128K, // NOLINT
|
||||
SZ_128K, SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0};
|
||||
/* F7 page size */
|
||||
constexpr uint32_t f7[] = {SZ_32K, SZ_32K, SZ_32K, SZ_32K, SZ_128K, SZ_256K, 0}; // NOLINT
|
||||
|
||||
/*
|
||||
* Device table, corresponds to the "Bootloader device-dependant parameters"
|
||||
* table in ST document AN2606.
|
||||
* Note that the option bytes upper range is inclusive!
|
||||
*/
|
||||
constexpr stm32_dev_t DEVICES[] = {
|
||||
/* ID "name" SRAM-address-range FLASH-address-range PPS PSize
|
||||
Option-byte-addr-range System-mem-addr-range Flags */
|
||||
/* F0 */
|
||||
{0x440, "STM32F030x8/F05xxx", 0x20000800, 0x20002000, 0x08000000, 0x08010000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFEC00, 0x1FFFF800, 0},
|
||||
{0x442, "STM32F030xC/F09xxx", 0x20001800, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFC800, 0x1FFFF800, F_OBLL},
|
||||
{0x444, "STM32F03xx4/6", 0x20000800, 0x20001000, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFEC00, 0x1FFFF800, 0},
|
||||
{0x445, "STM32F04xxx/F070x6", 0x20001800, 0x20001800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFC400, 0x1FFFF800, 0},
|
||||
{0x448, "STM32F070xB/F071xx/F72xx", 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFC800, 0x1FFFF800, 0},
|
||||
/* F1 */
|
||||
{0x412, "STM32F10xxx Low-density", 0x20000200, 0x20002800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x410, "STM32F10xxx Medium-density", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x414, "STM32F10xxx High-density", 0x20000200, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x420, "STM32F10xxx Medium-density VL", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x428, "STM32F10xxx High-density VL", 0x20000200, 0x20008000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x418, "STM32F105xx/F107xx", 0x20001000, 0x20010000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFB000, 0x1FFFF800, 0},
|
||||
{0x430, "STM32F10xxx XL-density", 0x20000800, 0x20018000, 0x08000000, 0x08100000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFE000, 0x1FFFF800, 0},
|
||||
/* F2 */
|
||||
{0x411, "STM32F2xxxx", 0x20002000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
/* F3 */
|
||||
{0x432, "STM32F373xx/F378xx", 0x20001400, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFD800, 0x1FFFF800, 0},
|
||||
{0x422, "STM32F302xB(C)/F303xB(C)/F358xx", 0x20001400, 0x2000A000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0},
|
||||
{0x439, "STM32F301xx/F302x4(6/8)/F318xx", 0x20001800, 0x20004000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0},
|
||||
{0x438, "STM32F303x4(6/8)/F334xx/F328xx", 0x20001800, 0x20003000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0},
|
||||
{0x446, "STM32F302xD(E)/F303xD(E)/F398xx", 0x20001800, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0},
|
||||
/* F4 */
|
||||
{0x413, "STM32F40xxx/41xxx", 0x20003000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F,
|
||||
0x1FFF0000, 0x1FFF7800, 0},
|
||||
{0x419, "STM32F42xxx/43xxx", 0x20003000, 0x20030000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F,
|
||||
0x1FFF0000, 0x1FFF7800, 0},
|
||||
{0x423, "STM32F401xB(C)", 0x20003000, 0x20010000, 0x08000000, 0x08040000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F,
|
||||
0x1FFF0000, 0x1FFF7800, 0},
|
||||
{0x433, "STM32F401xD(E)", 0x20003000, 0x20018000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F,
|
||||
0x1FFF0000, 0x1FFF7800, 0},
|
||||
{0x458, "STM32F410xx", 0x20003000, 0x20008000, 0x08000000, 0x08020000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
{0x431, "STM32F411xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
{0x421, "STM32F446xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
{0x434, "STM32F469xx", 0x20003000, 0x20060000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
/* F7 */
|
||||
{0x449, "STM32F74xxx/75xxx", 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, f7, 0x1FFF0000, 0x1FFF001F,
|
||||
0x1FF00000, 0x1FF0EDC0, 0},
|
||||
/* L0 */
|
||||
{0x425, "STM32L031xx/041xx", 0x20001000, 0x20002000, 0x08000000, 0x08008000, 32, p_128, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF01000, 0},
|
||||
{0x417, "STM32L05xxx/06xxx", 0x20001000, 0x20002000, 0x08000000, 0x08010000, 32, p_128, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF01000, 0},
|
||||
{0x447, "STM32L07xxx/08xxx", 0x20002000, 0x20005000, 0x08000000, 0x08030000, 32, p_128, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF02000, 0},
|
||||
/* L1 */
|
||||
{0x416, "STM32L1xxx6(8/B)", 0x20000800, 0x20004000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF01000, F_NO_ME},
|
||||
{0x429, "STM32L1xxx6(8/B)A", 0x20001000, 0x20008000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF01000, 0},
|
||||
{0x427, "STM32L1xxxC", 0x20001000, 0x20008000, 0x08000000, 0x08040000, 16, p_256, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF02000, 0},
|
||||
{0x436, "STM32L1xxxD", 0x20001000, 0x2000C000, 0x08000000, 0x08060000, 16, p_256, 0x1FF80000, 0x1FF8009F,
|
||||
0x1FF00000, 0x1FF02000, 0},
|
||||
{0x437, "STM32L1xxxE", 0x20001000, 0x20014000, 0x08000000, 0x08080000, 16, p_256, 0x1FF80000, 0x1FF8009F,
|
||||
0x1FF00000, 0x1FF02000, F_NO_ME},
|
||||
/* L4 */
|
||||
{0x415, "STM32L476xx/486xx", 0x20003100, 0x20018000, 0x08000000, 0x08100000, 1, p_2k, 0x1FFF7800, 0x1FFFF80F,
|
||||
0x1FFF0000, 0x1FFF7000, 0},
|
||||
/* These are not (yet) in AN2606: */
|
||||
{0x641, "Medium_Density PL", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x9a8, "STM32W-128K", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x08040800, 0x0804080F, 0x08040000,
|
||||
0x08040800, 0},
|
||||
{0x9b0, "STM32W-256K", 0x20000200, 0x20004000, 0x08000000, 0x08040000, 4, p_2k, 0x08040800, 0x0804080F, 0x08040000,
|
||||
0x08040800, 0},
|
||||
{0x0, "", 0x0, 0x0, 0x0, 0x0, 0x0, nullptr, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
};
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
#endif
|
||||
219
esphome/components/shelly_dimmer/light.py
Normal file
219
esphome/components/shelly_dimmer/light.py
Normal file
@ -0,0 +1,219 @@
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import re
|
||||
import requests
|
||||
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import light, sensor, uart
|
||||
from esphome.const import (
|
||||
CONF_OUTPUT_ID,
|
||||
CONF_GAMMA_CORRECT,
|
||||
CONF_POWER,
|
||||
CONF_VOLTAGE,
|
||||
CONF_CURRENT,
|
||||
CONF_VERSION,
|
||||
CONF_URL,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
UNIT_VOLT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_WATT,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
)
|
||||
from esphome.core import HexInt, CORE
|
||||
|
||||
DOMAIN = "shelly_dimmer"
|
||||
DEPENDENCIES = ["sensor", "uart"]
|
||||
|
||||
shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer")
|
||||
ShellyDimmer = shelly_dimmer_ns.class_(
|
||||
"ShellyDimmer", light.LightOutput, cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
CONF_FIRMWARE = "firmware"
|
||||
CONF_SHA256 = "sha256"
|
||||
CONF_UPDATE = "update"
|
||||
|
||||
CONF_LEADING_EDGE = "leading_edge"
|
||||
CONF_WARMUP_BRIGHTNESS = "warmup_brightness"
|
||||
# CONF_WARMUP_TIME = "warmup_time"
|
||||
CONF_MIN_BRIGHTNESS = "min_brightness"
|
||||
CONF_MAX_BRIGHTNESS = "max_brightness"
|
||||
|
||||
CONF_NRST_PIN = "nrst_pin"
|
||||
CONF_BOOT0_PIN = "boot0_pin"
|
||||
|
||||
KNOWN_FIRMWARE = {
|
||||
"51.5": (
|
||||
"https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.5/shelly-dimmer-stm32_v51.5.bin",
|
||||
"553fc1d78ed113227af7683eaa9c26189a961c4ea9a48000fb5aa8f8ac5d7b60",
|
||||
),
|
||||
"51.6": (
|
||||
"https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.6/shelly-dimmer-stm32_v51.6.bin",
|
||||
"eda483e111c914723a33f5088f1397d5c0b19333db4a88dc965636b976c16c36",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def parse_firmware_version(value):
|
||||
match = re.match(r"(\d+).(\d+)", value)
|
||||
if match is None:
|
||||
raise ValueError(f"Not a valid version number {value}")
|
||||
major = int(match[1])
|
||||
minor = int(match[2])
|
||||
return major, minor
|
||||
|
||||
|
||||
def get_firmware(value):
|
||||
if not value[CONF_UPDATE]:
|
||||
return None
|
||||
|
||||
def dl(url):
|
||||
try:
|
||||
req = requests.get(url)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(f"Could not download firmware file ({url}): {e}")
|
||||
|
||||
h = hashlib.new("sha256")
|
||||
h.update(req.content)
|
||||
return req.content, h.hexdigest()
|
||||
|
||||
url = value[CONF_URL]
|
||||
|
||||
if CONF_SHA256 in value: # we have a hash, enable caching
|
||||
path = (
|
||||
Path(CORE.config_dir)
|
||||
/ ".esphome"
|
||||
/ DOMAIN
|
||||
/ (value[CONF_SHA256] + "_fw_stm.bin")
|
||||
)
|
||||
|
||||
if not path.is_file():
|
||||
firmware_data, dl_hash = dl(url)
|
||||
|
||||
if dl_hash != value[CONF_SHA256]:
|
||||
raise cv.Invalid(
|
||||
f"Hash mismatch for {url}: {dl_hash} != {value[CONF_SHA256]}"
|
||||
)
|
||||
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
path.write_bytes(firmware_data)
|
||||
|
||||
else:
|
||||
firmware_data = path.read_bytes()
|
||||
else: # no caching, download every time
|
||||
firmware_data, dl_hash = dl(url)
|
||||
|
||||
return [HexInt(x) for x in firmware_data]
|
||||
|
||||
|
||||
def validate_firmware(value):
|
||||
config = value.copy()
|
||||
if CONF_URL not in config:
|
||||
try:
|
||||
config[CONF_URL], config[CONF_SHA256] = KNOWN_FIRMWARE[config[CONF_VERSION]]
|
||||
except KeyError as e:
|
||||
raise cv.Invalid(
|
||||
f"Firmware {config[CONF_VERSION]} is unknown, please specify an '{CONF_URL}' ..."
|
||||
) from e
|
||||
get_firmware(config)
|
||||
return config
|
||||
|
||||
|
||||
def validate_sha256(value):
|
||||
value = cv.string(value)
|
||||
if not value.isalnum() or not len(value) == 64:
|
||||
raise ValueError(f"Not a valid SHA256 hex string: {value}")
|
||||
return value
|
||||
|
||||
|
||||
def validate_version(value):
|
||||
parse_firmware_version(value)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ShellyDimmer),
|
||||
cv.Optional(CONF_FIRMWARE, default="51.6"): cv.maybe_simple_value(
|
||||
{
|
||||
cv.Optional(CONF_URL): cv.url,
|
||||
cv.Optional(CONF_SHA256): validate_sha256,
|
||||
cv.Required(CONF_VERSION): validate_version,
|
||||
cv.Optional(CONF_UPDATE, default=False): cv.boolean,
|
||||
},
|
||||
validate_firmware, # converts a simple version key to generate the full url
|
||||
key=CONF_VERSION,
|
||||
),
|
||||
cv.Optional(CONF_NRST_PIN, default="GPIO5"): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_BOOT0_PIN, default="GPIO4"): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_LEADING_EDGE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_WARMUP_BRIGHTNESS, default=100): cv.uint16_t,
|
||||
# cv.Optional(CONF_WARMUP_TIME, default=20): cv.uint16_t,
|
||||
cv.Optional(CONF_MIN_BRIGHTNESS, default=0): cv.uint16_t,
|
||||
cv.Optional(CONF_MAX_BRIGHTNESS, default=1000): cv.uint16_t,
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
# Change the default gamma_correct setting.
|
||||
cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("10s"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
fw_hex = get_firmware(config[CONF_FIRMWARE])
|
||||
fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION])
|
||||
|
||||
if fw_hex is not None:
|
||||
cg.add_define("USE_SHD_FIRMWARE_DATA", fw_hex)
|
||||
cg.add_define("USE_SHD_FIRMWARE_MAJOR_VERSION", fw_major)
|
||||
cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
yield cg.register_component(var, config)
|
||||
config.pop(
|
||||
CONF_UPDATE_INTERVAL
|
||||
) # drop UPDATE_INTERVAL as it does not apply to the light component
|
||||
|
||||
yield light.register_light(var, config)
|
||||
yield uart.register_uart_device(var, config)
|
||||
|
||||
nrst_pin = yield cg.gpio_pin_expression(config[CONF_NRST_PIN])
|
||||
cg.add(var.set_nrst_pin(nrst_pin))
|
||||
boot0_pin = yield cg.gpio_pin_expression(config[CONF_BOOT0_PIN])
|
||||
cg.add(var.set_boot0_pin(boot0_pin))
|
||||
|
||||
cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE]))
|
||||
cg.add(var.set_warmup_brightness(config[CONF_WARMUP_BRIGHTNESS]))
|
||||
# cg.add(var.set_warmup_time(config[CONF_WARMUP_TIME]))
|
||||
cg.add(var.set_min_brightness(config[CONF_MIN_BRIGHTNESS]))
|
||||
cg.add(var.set_max_brightness(config[CONF_MAX_BRIGHTNESS]))
|
||||
|
||||
for key in [CONF_POWER, CONF_VOLTAGE, CONF_CURRENT]:
|
||||
if key not in config:
|
||||
continue
|
||||
|
||||
conf = config[key]
|
||||
sens = yield sensor.new_sensor(conf)
|
||||
cg.add(getattr(var, f"set_{key}_sensor")(sens))
|
||||
526
esphome/components/shelly_dimmer/shelly_dimmer.cpp
Normal file
526
esphome/components/shelly_dimmer/shelly_dimmer.cpp
Normal file
@ -0,0 +1,526 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "shelly_dimmer.h"
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
#include "stm32flash.h"
|
||||
#endif
|
||||
|
||||
#ifndef USE_ESP_IDF
|
||||
#include <HardwareSerial.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char TAG[] = "shelly_dimmer";
|
||||
|
||||
constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms
|
||||
constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3;
|
||||
constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100%
|
||||
|
||||
// Protocol framing.
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04;
|
||||
|
||||
// Supported commands.
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20;
|
||||
|
||||
// Command payload sizes.
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3;
|
||||
|
||||
// STM Firmware
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA;
|
||||
constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE);
|
||||
#endif
|
||||
|
||||
// Scaling Constants
|
||||
constexpr float POWER_SCALING_FACTOR = 880373;
|
||||
constexpr float VOLTAGE_SCALING_FACTOR = 347800;
|
||||
constexpr float CURRENT_SCALING_FACTOR = 1448;
|
||||
|
||||
// Esentially std::size() for pre c++17
|
||||
template<typename T, size_t N> constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; }
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
/// Computes a crappy checksum as defined by the Shelly Dimmer protocol.
|
||||
uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) {
|
||||
return std::accumulate<decltype(buf), uint16_t>(buf, buf + len, 0);
|
||||
}
|
||||
|
||||
void ShellyDimmer::setup() {
|
||||
this->pin_nrst_->setup();
|
||||
this->pin_boot0_->setup();
|
||||
|
||||
ESP_LOGI(TAG, "Initializing Shelly Dimmer...");
|
||||
|
||||
// Reset the STM32 and check the firmware version.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
this->reset_normal_boot_();
|
||||
this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0);
|
||||
ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_,
|
||||
this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION);
|
||||
if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
|
||||
this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
// Update firmware if needed.
|
||||
ESP_LOGW(TAG, "Unsupported STM32 firmware version, flashing");
|
||||
if (i > 0) {
|
||||
// Upgrade was already performed but the reported version is still not right.
|
||||
ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->upgrade_firmware_()) {
|
||||
ESP_LOGW(TAG, "Failed to upgrade firmware");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Firmware upgrade completed, do the checks again.
|
||||
continue;
|
||||
#else
|
||||
ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this->send_settings_();
|
||||
// Do an immediate poll to refresh current state.
|
||||
this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0);
|
||||
|
||||
this->ready_ = true;
|
||||
}
|
||||
|
||||
void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); }
|
||||
|
||||
void ShellyDimmer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ShellyDimmer:");
|
||||
LOG_PIN(" NRST Pin: ", this->pin_nrst_);
|
||||
LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Leading Edge: %s", YESNO(this->leading_edge_));
|
||||
ESP_LOGCONFIG(TAG, " Warmup Brightness: %d", this->warmup_brightness_);
|
||||
// ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_);
|
||||
// ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_);
|
||||
ESP_LOGCONFIG(TAG, " Minimum Brightness: %d", this->min_brightness_);
|
||||
ESP_LOGCONFIG(TAG, " Maximum Brightness: %d", this->max_brightness_);
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_);
|
||||
ESP_LOGCONFIG(TAG, " STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION,
|
||||
USE_SHD_FIRMWARE_MINOR_VERSION);
|
||||
|
||||
if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
|
||||
this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
|
||||
ESP_LOGE(TAG, " Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
|
||||
}
|
||||
}
|
||||
|
||||
void ShellyDimmer::write_state(light::LightState *state) {
|
||||
if (!this->ready_) {
|
||||
return;
|
||||
}
|
||||
|
||||
float brightness;
|
||||
state->current_values_as_brightness(&brightness);
|
||||
|
||||
const uint16_t brightness_int = this->convert_brightness_(brightness);
|
||||
if (brightness_int == this->brightness_) {
|
||||
ESP_LOGV(TAG, "Not sending unchanged value");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
|
||||
|
||||
this->send_brightness_(brightness_int);
|
||||
}
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
bool ShellyDimmer::upgrade_firmware_() {
|
||||
ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
|
||||
this->reset_dfu_boot_();
|
||||
|
||||
// Could be constexpr in c++17
|
||||
static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); };
|
||||
|
||||
// Cleanup with RAII
|
||||
std::unique_ptr<stm32_t, decltype(CLOSE)> stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE};
|
||||
|
||||
if (!stm32) {
|
||||
ESP_LOGW(TAG, "Failed to initialize STM32");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Erase STM32 flash.
|
||||
if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr uint32_t BUFFER_SIZE = 256;
|
||||
|
||||
// Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored
|
||||
// in flash memory so all accesses need to be 4-byte aligned.
|
||||
uint8_t buffer[BUFFER_SIZE];
|
||||
const uint8_t *p = STM_FIRMWARE;
|
||||
uint32_t offset = 0;
|
||||
uint32_t addr = stm32->dev->fl_start;
|
||||
const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES;
|
||||
|
||||
while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) {
|
||||
const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE);
|
||||
const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset);
|
||||
|
||||
if (len == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::memcpy(buffer, p, BUFFER_SIZE);
|
||||
p += BUFFER_SIZE;
|
||||
|
||||
if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
addr += len;
|
||||
offset += len;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "STM32 firmware upgrade successful");
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t ShellyDimmer::convert_brightness_(float brightness) {
|
||||
// Special case for zero as only zero means turn off completely.
|
||||
if (brightness == 0.0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return remap<uint16_t, float>(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_);
|
||||
}
|
||||
|
||||
void ShellyDimmer::send_brightness_(uint16_t brightness) {
|
||||
const uint8_t payload[] = {
|
||||
// Brightness (%) * 10.
|
||||
static_cast<uint8_t>(brightness & 0xff),
|
||||
static_cast<uint8_t>(brightness >> 8),
|
||||
};
|
||||
static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size");
|
||||
|
||||
this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE);
|
||||
|
||||
this->brightness_ = brightness;
|
||||
}
|
||||
|
||||
void ShellyDimmer::send_settings_() {
|
||||
const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_);
|
||||
|
||||
float brightness = 0.0;
|
||||
if (this->state_ != nullptr) {
|
||||
this->state_->current_values_as_brightness(&brightness);
|
||||
}
|
||||
const uint16_t brightness_int = this->convert_brightness_(brightness);
|
||||
ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
|
||||
|
||||
const uint8_t payload[] = {
|
||||
// Brightness (%) * 10.
|
||||
static_cast<uint8_t>(brightness_int & 0xff),
|
||||
static_cast<uint8_t>(brightness_int >> 8),
|
||||
// Leading / trailing edge [0x01 = leading, 0x02 = trailing].
|
||||
this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02},
|
||||
0x00,
|
||||
// Fade rate.
|
||||
static_cast<uint8_t>(fade_rate & 0xff),
|
||||
static_cast<uint8_t>(fade_rate >> 8),
|
||||
// Warmup brightness.
|
||||
static_cast<uint8_t>(this->warmup_brightness_ & 0xff),
|
||||
static_cast<uint8_t>(this->warmup_brightness_ >> 8),
|
||||
// Warmup time.
|
||||
static_cast<uint8_t>(this->warmup_time_ & 0xff),
|
||||
static_cast<uint8_t>(this->warmup_time_ >> 8),
|
||||
};
|
||||
static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size");
|
||||
|
||||
this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE);
|
||||
|
||||
// Also send brightness separately as it is ignored above.
|
||||
this->send_brightness_(brightness_int);
|
||||
}
|
||||
|
||||
bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) {
|
||||
ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str());
|
||||
|
||||
// Prepare a command frame.
|
||||
uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE];
|
||||
const size_t frame_len = this->frame_command_(frame, cmd, payload, len);
|
||||
|
||||
// Write the frame and wait for acknowledgement.
|
||||
int retries = SHELLY_DIMMER_MAX_RETRIES;
|
||||
while (retries--) {
|
||||
this->write_array(frame, frame_len);
|
||||
this->flush();
|
||||
|
||||
ESP_LOGD(TAG, "Command sent, waiting for reply");
|
||||
const uint32_t tx_time = millis();
|
||||
while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) {
|
||||
if (this->read_frame_()) {
|
||||
return true;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
ESP_LOGW(TAG, "Timeout while waiting for reply");
|
||||
}
|
||||
ESP_LOGW(TAG, "Failed to send command");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) {
|
||||
size_t pos = 0;
|
||||
|
||||
// Generate a frame.
|
||||
data[0] = SHELLY_DIMMER_PROTO_START_BYTE;
|
||||
data[1] = ++this->seq_;
|
||||
data[2] = cmd;
|
||||
data[3] = len;
|
||||
pos += 4;
|
||||
|
||||
if (payload != nullptr) {
|
||||
std::memcpy(data + 4, payload, len);
|
||||
pos += len;
|
||||
}
|
||||
|
||||
// Calculate checksum for the payload.
|
||||
const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len);
|
||||
data[pos++] = static_cast<uint8_t>(csum >> 8);
|
||||
data[pos++] = static_cast<uint8_t>(csum & 0xff);
|
||||
data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE;
|
||||
return pos;
|
||||
}
|
||||
|
||||
int ShellyDimmer::handle_byte_(uint8_t c) {
|
||||
const uint8_t pos = this->buffer_pos_;
|
||||
|
||||
if (pos == 0) {
|
||||
// Must be start byte.
|
||||
return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1;
|
||||
} else if (pos < 4) {
|
||||
// Header.
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Decode payload length from header.
|
||||
const uint8_t payload_len = this->buffer_[3];
|
||||
if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pos < 4 + payload_len + 1) {
|
||||
// Payload.
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (pos == 4 + payload_len + 1) {
|
||||
// Verify checksum.
|
||||
const uint16_t csum = (this->buffer_[pos - 1] << 8 | c);
|
||||
const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len);
|
||||
if (csum != csum_verify) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (pos == 4 + payload_len + 2) {
|
||||
// Must be end byte.
|
||||
return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ShellyDimmer::read_frame_() {
|
||||
while (this->available()) {
|
||||
const uint8_t c = this->read();
|
||||
this->buffer_[this->buffer_pos_] = c;
|
||||
|
||||
ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_);
|
||||
|
||||
switch (this->handle_byte_(c)) {
|
||||
case 0: {
|
||||
// Frame successfully received.
|
||||
this->handle_frame_();
|
||||
this->buffer_pos_ = 0;
|
||||
return true;
|
||||
}
|
||||
case -1: {
|
||||
// Failure.
|
||||
this->buffer_pos_ = 0;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// Need more data.
|
||||
this->buffer_pos_++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShellyDimmer::handle_frame_() {
|
||||
const uint8_t seq = this->buffer_[1];
|
||||
const uint8_t cmd = this->buffer_[2];
|
||||
const uint8_t payload_len = this->buffer_[3];
|
||||
|
||||
ESP_LOGD(TAG, "Got frame: 0x%02x", cmd);
|
||||
|
||||
// Compare with expected identifier as the frame is always a response to
|
||||
// our previously sent command.
|
||||
if (seq != this->seq_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t *payload = &this->buffer_[4];
|
||||
|
||||
// Handle response.
|
||||
switch (cmd) {
|
||||
case SHELLY_DIMMER_PROTO_CMD_POLL: {
|
||||
if (payload_len < 16) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t hw_version = payload[0];
|
||||
// payload[1] is unused.
|
||||
const uint16_t brightness = encode_uint16(payload[3], payload[2]);
|
||||
|
||||
const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]);
|
||||
|
||||
const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]);
|
||||
|
||||
const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]);
|
||||
|
||||
const uint16_t fade_rate = payload[16];
|
||||
|
||||
float power = 0;
|
||||
if (power_raw > 0) {
|
||||
power = POWER_SCALING_FACTOR / static_cast<float>(power_raw);
|
||||
}
|
||||
|
||||
float voltage = 0;
|
||||
if (voltage_raw > 0) {
|
||||
voltage = VOLTAGE_SCALING_FACTOR / static_cast<float>(voltage_raw);
|
||||
}
|
||||
|
||||
float current = 0;
|
||||
if (current_raw > 0) {
|
||||
current = CURRENT_SCALING_FACTOR / static_cast<float>(current_raw);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Got dimmer data:");
|
||||
ESP_LOGI(TAG, " HW version: %d", hw_version);
|
||||
ESP_LOGI(TAG, " Brightness: %d", brightness);
|
||||
ESP_LOGI(TAG, " Fade rate: %d", fade_rate);
|
||||
ESP_LOGI(TAG, " Power: %f W", power);
|
||||
ESP_LOGI(TAG, " Voltage: %f V", voltage);
|
||||
ESP_LOGI(TAG, " Current: %f A", current);
|
||||
|
||||
// Update sensors.
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
}
|
||||
if (this->current_sensor_ != nullptr) {
|
||||
this->current_sensor_->publish_state(current);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case SHELLY_DIMMER_PROTO_CMD_VERSION: {
|
||||
if (payload_len < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->version_minor_ = payload[0];
|
||||
this->version_major_ = payload[1];
|
||||
return true;
|
||||
}
|
||||
case SHELLY_DIMMER_PROTO_CMD_SWITCH:
|
||||
case SHELLY_DIMMER_PROTO_CMD_SETTINGS: {
|
||||
return !(payload_len < 1 || payload[0] != 0x01);
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ShellyDimmer::reset_(bool boot0) {
|
||||
ESP_LOGD(TAG, "Reset STM32, boot0=%d", boot0);
|
||||
|
||||
this->pin_boot0_->digital_write(boot0);
|
||||
this->pin_nrst_->digital_write(false);
|
||||
|
||||
// Wait 50ms for the STM32 to reset.
|
||||
delay(50); // NOLINT
|
||||
|
||||
// Clear receive buffer.
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
|
||||
this->pin_nrst_->digital_write(true);
|
||||
// Wait 50ms for the STM32 to boot.
|
||||
delay(50); // NOLINT
|
||||
|
||||
ESP_LOGD(TAG, "Reset STM32 done");
|
||||
}
|
||||
|
||||
void ShellyDimmer::reset_normal_boot_() {
|
||||
// set NONE parity in normal mode
|
||||
|
||||
#ifndef USE_ESP_IDF // workaround for reconfiguring the uart
|
||||
Serial.end();
|
||||
Serial.begin(115200, SERIAL_8N1);
|
||||
Serial.flush();
|
||||
#endif
|
||||
|
||||
this->flush();
|
||||
this->reset_(false);
|
||||
}
|
||||
|
||||
void ShellyDimmer::reset_dfu_boot_() {
|
||||
// set EVEN parity in bootloader mode
|
||||
|
||||
#ifndef USE_ESP_IDF // workaround for reconfiguring the uart
|
||||
Serial.end();
|
||||
Serial.begin(115200, SERIAL_8E1);
|
||||
Serial.flush();
|
||||
#endif
|
||||
|
||||
this->flush();
|
||||
this->reset_(true);
|
||||
}
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
117
esphome/components/shelly_dimmer/shelly_dimmer.h
Normal file
117
esphome/components/shelly_dimmer/shelly_dimmer.h
Normal file
@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
class ShellyDimmer : public PollingComponent, public light::LightOutput, public uart::UARTDevice {
|
||||
private:
|
||||
static constexpr uint16_t SHELLY_DIMMER_BUFFER_SIZE = 256;
|
||||
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::LATE; }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
|
||||
return traits;
|
||||
}
|
||||
|
||||
void setup_state(light::LightState *state) override { this->state_ = state; }
|
||||
void write_state(light::LightState *state) override;
|
||||
|
||||
void set_nrst_pin(GPIOPin *nrst_pin) { this->pin_nrst_ = nrst_pin; }
|
||||
void set_boot0_pin(GPIOPin *boot0_pin) { this->pin_boot0_ = boot0_pin; }
|
||||
|
||||
void set_leading_edge(bool leading_edge) { this->leading_edge_ = leading_edge; }
|
||||
void set_warmup_brightness(uint16_t warmup_brightness) { this->warmup_brightness_ = warmup_brightness; }
|
||||
void set_warmup_time(uint16_t warmup_time) { this->warmup_time_ = warmup_time; }
|
||||
void set_fade_rate(uint16_t fade_rate) { this->fade_rate_ = fade_rate; }
|
||||
void set_min_brightness(uint16_t min_brightness) { this->min_brightness_ = min_brightness; }
|
||||
void set_max_brightness(uint16_t max_brightness) { this->max_brightness_ = max_brightness; }
|
||||
|
||||
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
|
||||
|
||||
protected:
|
||||
GPIOPin *pin_nrst_;
|
||||
GPIOPin *pin_boot0_;
|
||||
|
||||
// Frame parser state.
|
||||
uint8_t seq_{0};
|
||||
std::array<uint8_t, SHELLY_DIMMER_BUFFER_SIZE> buffer_;
|
||||
uint8_t buffer_pos_{0};
|
||||
|
||||
// Firmware version.
|
||||
uint8_t version_major_;
|
||||
uint8_t version_minor_;
|
||||
|
||||
// Configuration.
|
||||
bool leading_edge_{false};
|
||||
uint16_t warmup_brightness_{100};
|
||||
uint16_t warmup_time_{20};
|
||||
uint16_t fade_rate_{0};
|
||||
uint16_t min_brightness_{0};
|
||||
uint16_t max_brightness_{1000};
|
||||
|
||||
light::LightState *state_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
|
||||
bool ready_{false};
|
||||
uint16_t brightness_;
|
||||
|
||||
/// Convert relative brightness into a dimmer brightness value.
|
||||
uint16_t convert_brightness_(float brightness);
|
||||
|
||||
/// Sends the given brightness value.
|
||||
void send_brightness_(uint16_t brightness);
|
||||
|
||||
/// Sends dimmer configuration.
|
||||
void send_settings_();
|
||||
|
||||
/// Performs a firmware upgrade.
|
||||
bool upgrade_firmware_();
|
||||
|
||||
/// Sends a command and waits for an acknowledgement.
|
||||
bool send_command_(uint8_t cmd, const uint8_t *payload, uint8_t len);
|
||||
|
||||
/// Frames a given command payload.
|
||||
size_t frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *payload, size_t len);
|
||||
|
||||
/// Handles a single byte as part of a protocol frame.
|
||||
///
|
||||
/// Returns -1 on failure, 0 when finished and 1 when more bytes needed.
|
||||
int handle_byte_(uint8_t c);
|
||||
|
||||
/// Reads a response frame.
|
||||
bool read_frame_();
|
||||
|
||||
/// Handles a complete frame.
|
||||
bool handle_frame_();
|
||||
|
||||
/// Reset STM32 with the BOOT0 pin set to the given value.
|
||||
void reset_(bool boot0);
|
||||
|
||||
/// Reset STM32 to boot the regular firmware.
|
||||
void reset_normal_boot_();
|
||||
|
||||
/// Reset STM32 to boot into DFU mode to enable firmware upgrades.
|
||||
void reset_dfu_boot_();
|
||||
};
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
1061
esphome/components/shelly_dimmer/stm32flash.cpp
Normal file
1061
esphome/components/shelly_dimmer/stm32flash.cpp
Normal file
@ -0,0 +1,1061 @@
|
||||
/*
|
||||
stm32flash - Open Source ST STM32 flash program for Arduino
|
||||
Copyright 2010 Geoffrey McRae <geoff@spacevs.com>
|
||||
Copyright 2012-2014 Tormod Volden <debian.tormod@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "stm32flash.h"
|
||||
#include "debug.h"
|
||||
|
||||
#include "dev_table.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t STM32_ACK = 0x79;
|
||||
constexpr uint8_t STM32_NACK = 0x1F;
|
||||
constexpr uint8_t STM32_BUSY = 0x76;
|
||||
|
||||
constexpr uint8_t STM32_CMD_INIT = 0x7F;
|
||||
constexpr uint8_t STM32_CMD_GET = 0x00; /* get the version and command supported */
|
||||
constexpr uint8_t STM32_CMD_GVR = 0x01; /* get version and read protection status */
|
||||
constexpr uint8_t STM32_CMD_GID = 0x02; /* get ID */
|
||||
constexpr uint8_t STM32_CMD_RM = 0x11; /* read memory */
|
||||
constexpr uint8_t STM32_CMD_GO = 0x21; /* go */
|
||||
constexpr uint8_t STM32_CMD_WM = 0x31; /* write memory */
|
||||
constexpr uint8_t STM32_CMD_WM_NS = 0x32; /* no-stretch write memory */
|
||||
constexpr uint8_t STM32_CMD_ER = 0x43; /* erase */
|
||||
constexpr uint8_t STM32_CMD_EE = 0x44; /* extended erase */
|
||||
constexpr uint8_t STM32_CMD_EE_NS = 0x45; /* extended erase no-stretch */
|
||||
constexpr uint8_t STM32_CMD_WP = 0x63; /* write protect */
|
||||
constexpr uint8_t STM32_CMD_WP_NS = 0x64; /* write protect no-stretch */
|
||||
constexpr uint8_t STM32_CMD_UW = 0x73; /* write unprotect */
|
||||
constexpr uint8_t STM32_CMD_UW_NS = 0x74; /* write unprotect no-stretch */
|
||||
constexpr uint8_t STM32_CMD_RP = 0x82; /* readout protect */
|
||||
constexpr uint8_t STM32_CMD_RP_NS = 0x83; /* readout protect no-stretch */
|
||||
constexpr uint8_t STM32_CMD_UR = 0x92; /* readout unprotect */
|
||||
constexpr uint8_t STM32_CMD_UR_NS = 0x93; /* readout unprotect no-stretch */
|
||||
constexpr uint8_t STM32_CMD_CRC = 0xA1; /* compute CRC */
|
||||
constexpr uint8_t STM32_CMD_ERR = 0xFF; /* not a valid command */
|
||||
|
||||
constexpr uint32_t STM32_RESYNC_TIMEOUT = 35 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_MASSERASE_TIMEOUT = 35 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_PAGEERASE_TIMEOUT = 5 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_BLKWRITE_TIMEOUT = 1 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_WUNPROT_TIMEOUT = 1 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_WPROT_TIMEOUT = 1 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_RPROT_TIMEOUT = 1 * 1000; /* milliseconds */
|
||||
constexpr uint32_t DEFAULT_TIMEOUT = 5 * 1000; /* milliseconds */
|
||||
|
||||
constexpr uint8_t STM32_CMD_GET_LENGTH = 17; /* bytes in the reply */
|
||||
|
||||
/* Reset code for ARMv7-M (Cortex-M3) and ARMv6-M (Cortex-M0)
|
||||
* see ARMv7-M or ARMv6-M Architecture Reference Manual (table B3-8)
|
||||
* or "The definitive guide to the ARM Cortex-M3", section 14.4.
|
||||
*/
|
||||
constexpr uint8_t STM_RESET_CODE[] = {
|
||||
0x01, 0x49, // ldr r1, [pc, #4] ; (<AIRCR_OFFSET>)
|
||||
0x02, 0x4A, // ldr r2, [pc, #8] ; (<AIRCR_RESET_VALUE>)
|
||||
0x0A, 0x60, // str r2, [r1, #0]
|
||||
0xfe, 0xe7, // endless: b endless
|
||||
0x0c, 0xed, 0x00, 0xe0, // .word 0xe000ed0c <AIRCR_OFFSET> = NVIC AIRCR register address
|
||||
0x04, 0x00, 0xfa, 0x05 // .word 0x05fa0004 <AIRCR_RESET_VALUE> = VECTKEY | SYSRESETREQ
|
||||
};
|
||||
|
||||
constexpr uint32_t STM_RESET_CODE_SIZE = sizeof(STM_RESET_CODE);
|
||||
|
||||
/* RM0360, Empty check
|
||||
* On STM32F070x6 and STM32F030xC devices only, internal empty check flag is
|
||||
* implemented to allow easy programming of the virgin devices by the boot loader. This flag is
|
||||
* used when BOOT0 pin is defining Main Flash memory as the target boot space. When the
|
||||
* flag is set, the device is considered as empty and System memory (boot loader) is selected
|
||||
* instead of the Main Flash as a boot space to allow user to program the Flash memory.
|
||||
* This flag is updated only during Option bytes loading: it is set when the content of the
|
||||
* address 0x08000 0000 is read as 0xFFFF FFFF, otherwise it is cleared. It means a power
|
||||
* on or setting of OBL_LAUNCH bit in FLASH_CR register is needed to clear this flag after
|
||||
* programming of a virgin device to execute user code after System reset.
|
||||
*/
|
||||
constexpr uint8_t STM_OBL_LAUNCH_CODE[] = {
|
||||
0x01, 0x49, // ldr r1, [pc, #4] ; (<FLASH_CR>)
|
||||
0x02, 0x4A, // ldr r2, [pc, #8] ; (<OBL_LAUNCH>)
|
||||
0x0A, 0x60, // str r2, [r1, #0]
|
||||
0xfe, 0xe7, // endless: b endless
|
||||
0x10, 0x20, 0x02, 0x40, // address: FLASH_CR = 40022010
|
||||
0x00, 0x20, 0x00, 0x00 // value: OBL_LAUNCH = 00002000
|
||||
};
|
||||
|
||||
constexpr uint32_t STM_OBL_LAUNCH_CODE_SIZE = sizeof(STM_OBL_LAUNCH_CODE);
|
||||
|
||||
constexpr char TAG[] = "stm32flash";
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
namespace {
|
||||
|
||||
int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) {
|
||||
if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end))
|
||||
return 0;
|
||||
|
||||
int page = 0;
|
||||
addr -= stm->dev->fl_start;
|
||||
const auto *psize = stm->dev->fl_ps;
|
||||
|
||||
while (addr >= psize[0]) {
|
||||
addr -= psize[0];
|
||||
page++;
|
||||
if (psize[1])
|
||||
psize++;
|
||||
}
|
||||
|
||||
return addr ? page + 1 : page;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) {
|
||||
auto *stream = stm->stream;
|
||||
uint8_t rxbyte;
|
||||
|
||||
if (!(stm->flags & STREAM_OPT_RETRY))
|
||||
timeout = 0;
|
||||
|
||||
if (timeout == 0)
|
||||
timeout = DEFAULT_TIMEOUT;
|
||||
|
||||
const uint32_t start_time = millis();
|
||||
do {
|
||||
yield();
|
||||
if (!stream->available()) {
|
||||
if (millis() < start_time + timeout)
|
||||
continue;
|
||||
ESP_LOGD(TAG, "Failed to read ACK timeout=%i", timeout);
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
stream->read_byte(&rxbyte);
|
||||
|
||||
if (rxbyte == STM32_ACK)
|
||||
return STM32_ERR_OK;
|
||||
if (rxbyte == STM32_NACK)
|
||||
return STM32_ERR_NACK;
|
||||
if (rxbyte != STM32_BUSY) {
|
||||
ESP_LOGD(TAG, "Got byte 0x%02x instead of ACK", rxbyte);
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); }
|
||||
|
||||
stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 2;
|
||||
const uint8_t buf[] = {
|
||||
cmd,
|
||||
static_cast<uint8_t>(cmd ^ 0xFF),
|
||||
};
|
||||
static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes");
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
|
||||
stm32_err_t s_err = stm32_get_ack_timeout(stm, timeout);
|
||||
if (s_err == STM32_ERR_OK)
|
||||
return STM32_ERR_OK;
|
||||
if (s_err == STM32_ERR_NACK) {
|
||||
ESP_LOGD(TAG, "Got NACK from device on command 0x%02x", cmd);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Unexpected reply from device on command 0x%02x", cmd);
|
||||
}
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) {
|
||||
return stm32_send_command_timeout(stm, cmd, 0);
|
||||
}
|
||||
|
||||
/* if we have lost sync, send a wrong command and expect a NACK */
|
||||
stm32_err_t stm32_resync(const stm32_t *stm) {
|
||||
auto *const stream = stm->stream;
|
||||
uint32_t t0 = millis();
|
||||
auto t1 = t0;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 2;
|
||||
const uint8_t buf[] = {
|
||||
STM32_CMD_ERR,
|
||||
static_cast<uint8_t>(STM32_CMD_ERR ^ 0xFF),
|
||||
};
|
||||
static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes");
|
||||
|
||||
uint8_t ack;
|
||||
while (t1 < t0 + STM32_RESYNC_TIMEOUT) {
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
if (!stream->read_array(&ack, 1)) {
|
||||
t1 = millis();
|
||||
continue;
|
||||
}
|
||||
if (ack == STM32_NACK)
|
||||
return STM32_ERR_OK;
|
||||
t1 = millis();
|
||||
}
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/*
|
||||
* some command receive reply frame with variable length, and length is
|
||||
* embedded in reply frame itself.
|
||||
* We can guess the length, but if we guess wrong the protocol gets out
|
||||
* of sync.
|
||||
* Use resync for frame oriented interfaces (e.g. I2C) and byte-by-byte
|
||||
* read for byte oriented interfaces (e.g. UART).
|
||||
*
|
||||
* to run safely, data buffer should be allocated for 256+1 bytes
|
||||
*
|
||||
* len is value of the first byte in the frame.
|
||||
*/
|
||||
stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
if (stm->flags & STREAM_OPT_BYTE) {
|
||||
/* interface is UART-like */
|
||||
if (!stream->read_array(data, 1))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
len = data[0];
|
||||
if (!stream->read_array(data + 1, len + 1))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
const auto ret = stream->read_array(data, len + 2);
|
||||
if (ret && len == data[0])
|
||||
return STM32_ERR_OK;
|
||||
if (!ret) {
|
||||
/* restart with only one byte */
|
||||
if (stm32_resync(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
if (!stream->read_array(data, 1))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Re sync (len = %d)", data[0]);
|
||||
if (stm32_resync(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
len = data[0];
|
||||
if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (!stream->read_array(data, len + 2))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Some interface, e.g. UART, requires a specific init sequence to let STM32
|
||||
* autodetect the interface speed.
|
||||
* The sequence is only required one time after reset.
|
||||
* This function sends the init sequence and, in case of timeout, recovers
|
||||
* the interface.
|
||||
*/
|
||||
stm32_err_t stm32_send_init_seq(const stm32_t *stm) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
stream->write_array(&STM32_CMD_INIT, 1);
|
||||
stream->flush();
|
||||
|
||||
uint8_t byte;
|
||||
bool ret = stream->read_array(&byte, 1);
|
||||
if (ret && byte == STM32_ACK)
|
||||
return STM32_ERR_OK;
|
||||
if (ret && byte == STM32_NACK) {
|
||||
/* We could get error later, but let's continue, for now. */
|
||||
ESP_LOGD(TAG, "Warning: the interface was not closed properly.");
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
if (!ret) {
|
||||
ESP_LOGD(TAG, "Failed to init device.");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if previous STM32_CMD_INIT was taken as first byte
|
||||
* of a command. Send a new byte, we should get back a NACK.
|
||||
*/
|
||||
stream->write_array(&STM32_CMD_INIT, 1);
|
||||
stream->flush();
|
||||
|
||||
ret = stream->read_array(&byte, 1);
|
||||
if (ret && byte == STM32_NACK)
|
||||
return STM32_ERR_OK;
|
||||
ESP_LOGD(TAG, "Failed to init device.");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_mass_erase(const stm32_t *stm) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Can't initiate chip mass erase!");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/* regular erase (0x43) */
|
||||
if (stm->cmd->er == STM32_CMD_ER) {
|
||||
const auto s_err = stm32_send_command_timeout(stm, 0xFF, STM32_MASSERASE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
/* extended erase */
|
||||
static constexpr auto BUFFER_SIZE = 3;
|
||||
const uint8_t buf[] = {
|
||||
0xFF, /* 0xFFFF the magic number for mass erase */
|
||||
0xFF, 0x00, /* checksum */
|
||||
};
|
||||
static_assert(sizeof(buf) == BUFFER_SIZE, "Expected the buffer to be 3 bytes");
|
||||
stream->write_array(buf, 3);
|
||||
stream->flush();
|
||||
|
||||
const auto s_err = stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Mass erase failed. Try specifying the number of pages to be erased.");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
template<typename T> std::unique_ptr<T[], void (*)(T *memory)> malloc_array_raii(size_t size) {
|
||||
// Could be constexpr in c++17
|
||||
static const auto DELETOR = [](T *memory) {
|
||||
free(memory); // NOLINT
|
||||
};
|
||||
return std::unique_ptr<T[], decltype(DELETOR)>{static_cast<T *>(malloc(size)), // NOLINT
|
||||
DELETOR};
|
||||
}
|
||||
|
||||
stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) {
|
||||
auto *const stream = stm->stream;
|
||||
uint8_t cs = 0;
|
||||
int i = 0;
|
||||
|
||||
/* The erase command reported by the bootloader is either 0x43, 0x44 or 0x45 */
|
||||
/* 0x44 is Extended Erase, a 2 byte based protocol and needs to be handled differently. */
|
||||
/* 0x45 is clock no-stretching version of Extended Erase for I2C port. */
|
||||
if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Can't initiate chip mass erase!");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/* regular erase (0x43) */
|
||||
if (stm->cmd->er == STM32_CMD_ER) {
|
||||
// Free memory with RAII
|
||||
auto buf = malloc_array_raii<uint8_t>(1 + pages + 1);
|
||||
|
||||
if (!buf)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
buf[i++] = pages - 1;
|
||||
cs ^= (pages - 1);
|
||||
for (auto pg_num = spage; pg_num < (pages + spage); pg_num++) {
|
||||
buf[i++] = pg_num;
|
||||
cs ^= pg_num;
|
||||
}
|
||||
buf[i++] = cs;
|
||||
stream->write_array(&buf[0], i);
|
||||
stream->flush();
|
||||
|
||||
const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
/* extended erase */
|
||||
|
||||
// Free memory with RAII
|
||||
auto buf = malloc_array_raii<uint8_t>(2 + 2 * pages + 1);
|
||||
|
||||
if (!buf)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
/* Number of pages to be erased - 1, two bytes, MSB first */
|
||||
uint8_t pg_byte = (pages - 1) >> 8;
|
||||
buf[i++] = pg_byte;
|
||||
cs ^= pg_byte;
|
||||
pg_byte = (pages - 1) & 0xFF;
|
||||
buf[i++] = pg_byte;
|
||||
cs ^= pg_byte;
|
||||
|
||||
for (auto pg_num = spage; pg_num < spage + pages; pg_num++) {
|
||||
pg_byte = pg_num >> 8;
|
||||
cs ^= pg_byte;
|
||||
buf[i++] = pg_byte;
|
||||
pg_byte = pg_num & 0xFF;
|
||||
cs ^= pg_byte;
|
||||
buf[i++] = pg_byte;
|
||||
}
|
||||
buf[i++] = cs;
|
||||
stream->write_array(&buf[0], i);
|
||||
stream->flush();
|
||||
|
||||
const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Page-by-page erase failed. Check the maximum pages your device supports.");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
template<typename T> stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err, const T &&log) {
|
||||
switch (s_err) {
|
||||
case STM32_ERR_OK:
|
||||
return STM32_ERR_OK;
|
||||
case STM32_ERR_NACK:
|
||||
log();
|
||||
// TODO: c++17 [[fallthrough]]
|
||||
/* fallthrough */
|
||||
default:
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/* detect CPU endian */
|
||||
bool cpu_le() {
|
||||
static constexpr int N = 1;
|
||||
|
||||
// returns true if little endian
|
||||
return *reinterpret_cast<const char *>(&N) == 1;
|
||||
}
|
||||
|
||||
uint32_t le_u32(const uint32_t v) {
|
||||
if (!cpu_le())
|
||||
return ((v & 0xFF000000) >> 24) | ((v & 0x00FF0000) >> 8) | ((v & 0x0000FF00) << 8) | ((v & 0x000000FF) << 24);
|
||||
return v;
|
||||
}
|
||||
|
||||
template<size_t N> void populate_buffer_with_address(uint8_t (&buffer)[N], uint32_t address) {
|
||||
buffer[0] = static_cast<uint8_t>(address >> 24);
|
||||
buffer[1] = static_cast<uint8_t>((address >> 16) & 0xFF);
|
||||
buffer[2] = static_cast<uint8_t>((address >> 8) & 0xFF);
|
||||
buffer[3] = static_cast<uint8_t>(address & 0xFF);
|
||||
buffer[4] = static_cast<uint8_t>(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]);
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
/* find newer command by higher code */
|
||||
#define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a)))
|
||||
|
||||
stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) {
|
||||
uint8_t buf[257];
|
||||
|
||||
// Could be constexpr in c++17
|
||||
static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); };
|
||||
|
||||
// Cleanup with RAII
|
||||
std::unique_ptr<stm32_t, decltype(CLOSE)> stm{static_cast<stm32_t *>(calloc(sizeof(stm32_t), 1)), // NOLINT
|
||||
CLOSE};
|
||||
|
||||
if (!stm) {
|
||||
return nullptr;
|
||||
}
|
||||
stm->stream = stream;
|
||||
stm->flags = flags;
|
||||
|
||||
stm->cmd = static_cast<stm32_cmd_t *>(malloc(sizeof(stm32_cmd_t))); // NOLINT
|
||||
if (!stm->cmd) {
|
||||
return nullptr;
|
||||
}
|
||||
memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t));
|
||||
|
||||
if ((stm->flags & STREAM_OPT_CMD_INIT) && init) {
|
||||
if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK)
|
||||
return nullptr; // NOLINT
|
||||
}
|
||||
|
||||
/* get the version and read protection status */
|
||||
if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) {
|
||||
return nullptr; // NOLINT
|
||||
}
|
||||
|
||||
/* From AN, only UART bootloader returns 3 bytes */
|
||||
{
|
||||
const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1;
|
||||
if (!stream->read_array(buf, len))
|
||||
return nullptr; // NOLINT
|
||||
stm->version = buf[0];
|
||||
stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0;
|
||||
stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0;
|
||||
if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const auto len = ([&]() {
|
||||
/* get the bootloader information */
|
||||
if (stm->cmd_get_reply) {
|
||||
for (auto i = 0; stm->cmd_get_reply[i].length; ++i) {
|
||||
if (stm->version == stm->cmd_get_reply[i].version) {
|
||||
return stm->cmd_get_reply[i].length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return STM32_CMD_GET_LENGTH;
|
||||
})();
|
||||
|
||||
if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto stop = buf[0] + 1;
|
||||
stm->bl_version = buf[1];
|
||||
int new_cmds = 0;
|
||||
for (auto i = 1; i < stop; ++i) {
|
||||
const auto val = buf[i + 1];
|
||||
switch (val) {
|
||||
case STM32_CMD_GET:
|
||||
stm->cmd->get = val;
|
||||
break;
|
||||
case STM32_CMD_GVR:
|
||||
stm->cmd->gvr = val;
|
||||
break;
|
||||
case STM32_CMD_GID:
|
||||
stm->cmd->gid = val;
|
||||
break;
|
||||
case STM32_CMD_RM:
|
||||
stm->cmd->rm = val;
|
||||
break;
|
||||
case STM32_CMD_GO:
|
||||
stm->cmd->go = val;
|
||||
break;
|
||||
case STM32_CMD_WM:
|
||||
case STM32_CMD_WM_NS:
|
||||
stm->cmd->wm = newer(stm->cmd->wm, val);
|
||||
break;
|
||||
case STM32_CMD_ER:
|
||||
case STM32_CMD_EE:
|
||||
case STM32_CMD_EE_NS:
|
||||
stm->cmd->er = newer(stm->cmd->er, val);
|
||||
break;
|
||||
case STM32_CMD_WP:
|
||||
case STM32_CMD_WP_NS:
|
||||
stm->cmd->wp = newer(stm->cmd->wp, val);
|
||||
break;
|
||||
case STM32_CMD_UW:
|
||||
case STM32_CMD_UW_NS:
|
||||
stm->cmd->uw = newer(stm->cmd->uw, val);
|
||||
break;
|
||||
case STM32_CMD_RP:
|
||||
case STM32_CMD_RP_NS:
|
||||
stm->cmd->rp = newer(stm->cmd->rp, val);
|
||||
break;
|
||||
case STM32_CMD_UR:
|
||||
case STM32_CMD_UR_NS:
|
||||
stm->cmd->ur = newer(stm->cmd->ur, val);
|
||||
break;
|
||||
case STM32_CMD_CRC:
|
||||
stm->cmd->crc = newer(stm->cmd->crc, val);
|
||||
break;
|
||||
default:
|
||||
if (new_cmds++ == 0) {
|
||||
ESP_LOGD(TAG, "GET returns unknown commands (0x%2x", val);
|
||||
} else {
|
||||
ESP_LOGD(TAG, ", 0x%2x", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (new_cmds)
|
||||
ESP_LOGD(TAG, ")");
|
||||
if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* get the device ID */
|
||||
if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto returned = buf[0] + 1;
|
||||
if (returned < 2) {
|
||||
ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned);
|
||||
return nullptr;
|
||||
}
|
||||
stm->pid = (buf[1] << 8) | buf[2];
|
||||
if (returned > 2) {
|
||||
ESP_LOGD(TAG, "This bootloader returns %d extra bytes in PID:", returned);
|
||||
for (auto i = 2; i <= returned; i++)
|
||||
ESP_LOGD(TAG, " %02x", buf[i]);
|
||||
}
|
||||
if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
stm->dev = DEVICES;
|
||||
while (stm->dev->id != 0x00 && stm->dev->id != stm->pid)
|
||||
++stm->dev;
|
||||
|
||||
if (!stm->dev->id) {
|
||||
ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// TODO: Would be much better if the unique_ptr was returned from this function
|
||||
// Release ownership of unique_ptr
|
||||
return stm.release(); // NOLINT
|
||||
}
|
||||
|
||||
void stm32_close(stm32_t *stm) {
|
||||
if (stm)
|
||||
free(stm->cmd); // NOLINT
|
||||
free(stm); // NOLINT
|
||||
}
|
||||
|
||||
stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (!len)
|
||||
return STM32_ERR_OK;
|
||||
|
||||
if (len > 256) {
|
||||
ESP_LOGD(TAG, "Error: READ length limit at 256 bytes");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stm->cmd->rm == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: READ command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->rm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf, address);
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (stm32_send_command(stm, len - 1) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (!stream->read_array(data, len))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (!len)
|
||||
return STM32_ERR_OK;
|
||||
|
||||
if (len > 256) {
|
||||
ESP_LOGD(TAG, "Error: READ length limit at 256 bytes");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/* must be 32bit aligned */
|
||||
if (address & 0x3) {
|
||||
ESP_LOGD(TAG, "Error: WRITE address must be 4 byte aligned");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stm->cmd->wm == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: WRITE command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
/* send the address and checksum */
|
||||
if (stm32_send_command(stm, stm->cmd->wm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf1[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf1, address);
|
||||
|
||||
stream->write_array(buf1, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
const unsigned int aligned_len = (len + 3) & ~3;
|
||||
uint8_t cs = aligned_len - 1;
|
||||
uint8_t buf[256 + 2];
|
||||
|
||||
buf[0] = aligned_len - 1;
|
||||
for (auto i = 0; i < len; i++) {
|
||||
cs ^= data[i];
|
||||
buf[i + 1] = data[i];
|
||||
}
|
||||
/* padding data */
|
||||
for (auto i = len; i < aligned_len; i++) {
|
||||
cs ^= 0xFF;
|
||||
buf[i + 1] = 0xFF;
|
||||
}
|
||||
buf[aligned_len + 1] = cs;
|
||||
stream->write_array(buf, aligned_len + 2);
|
||||
stream->flush();
|
||||
|
||||
const auto s_err = stm32_get_ack_timeout(stm, STM32_BLKWRITE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_wunprot_memory(const stm32_t *stm) {
|
||||
if (stm->cmd->uw == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->uw) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WUNPROT_TIMEOUT),
|
||||
[]() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); });
|
||||
}
|
||||
|
||||
stm32_err_t stm32_wprot_memory(const stm32_t *stm) {
|
||||
if (stm->cmd->wp == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->wp) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WPROT_TIMEOUT),
|
||||
[]() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); });
|
||||
}
|
||||
|
||||
stm32_err_t stm32_runprot_memory(const stm32_t *stm) {
|
||||
if (stm->cmd->ur == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->ur) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT),
|
||||
[]() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); });
|
||||
}
|
||||
|
||||
stm32_err_t stm32_readprot_memory(const stm32_t *stm) {
|
||||
if (stm->cmd->rp == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->rp) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_RPROT_TIMEOUT),
|
||||
[]() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); });
|
||||
}
|
||||
|
||||
stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) {
|
||||
if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES)))
|
||||
return STM32_ERR_OK;
|
||||
|
||||
if (stm->cmd->er == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: ERASE command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (pages == STM32_MASS_ERASE) {
|
||||
/*
|
||||
* Not all chips support mass erase.
|
||||
* Mass erase can be obtained executing a "readout protect"
|
||||
* followed by "readout un-protect". This method is not
|
||||
* suggested because can hang the target if a debug SWD/JTAG
|
||||
* is connected. When the target enters in "readout
|
||||
* protection" mode it will consider the debug connection as
|
||||
* a tentative of intrusion and will hang.
|
||||
* Erasing the flash page-by-page is the safer way to go.
|
||||
*/
|
||||
if (!(stm->dev->flags & F_NO_ME))
|
||||
return stm32_mass_erase(stm);
|
||||
|
||||
pages = flash_addr_to_page_ceil(stm, stm->dev->fl_end);
|
||||
}
|
||||
|
||||
/*
|
||||
* Some device, like STM32L152, cannot erase more than 512 pages in
|
||||
* one command. Split the call.
|
||||
*/
|
||||
static constexpr uint32_t MAX_PAGE_SIZE = 512;
|
||||
while (pages) {
|
||||
const auto n = std::min(pages, MAX_PAGE_SIZE);
|
||||
const auto s_err = stm32_pages_erase(stm, spage, n);
|
||||
if (s_err != STM32_ERR_OK)
|
||||
return s_err;
|
||||
spage += n;
|
||||
pages -= n;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code,
|
||||
uint32_t code_size) {
|
||||
static constexpr uint32_t BUFFER_SIZE = 256;
|
||||
|
||||
const auto stack_le = le_u32(0x20002000);
|
||||
const auto code_address_le = le_u32(target_address + 8 + 1); // thumb mode address (!)
|
||||
uint32_t length = code_size + 8;
|
||||
|
||||
/* Must be 32-bit aligned */
|
||||
if (target_address & 0x3) {
|
||||
ESP_LOGD(TAG, "Error: code address must be 4 byte aligned");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
// Could be constexpr in c++17
|
||||
static const auto DELETOR = [](uint8_t *memory) {
|
||||
free(memory); // NOLINT
|
||||
};
|
||||
|
||||
// Free memory with RAII
|
||||
std::unique_ptr<uint8_t, decltype(DELETOR)> mem{static_cast<uint8_t *>(malloc(length)), // NOLINT
|
||||
DELETOR};
|
||||
|
||||
if (!mem)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
memcpy(mem.get(), &stack_le, sizeof(stack_le));
|
||||
memcpy(mem.get() + 4, &code_address_le, sizeof(code_address_le));
|
||||
memcpy(mem.get() + 8, code, code_size);
|
||||
|
||||
auto *pos = mem.get();
|
||||
auto address = target_address;
|
||||
while (length > 0) {
|
||||
const auto w = std::min(length, BUFFER_SIZE);
|
||||
if (stm32_write_memory(stm, address, pos, w) != STM32_ERR_OK) {
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
address += w;
|
||||
pos += w;
|
||||
length -= w;
|
||||
}
|
||||
|
||||
return stm32_go(stm, target_address);
|
||||
}
|
||||
|
||||
stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (stm->cmd->go == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: GO command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->go) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf, address);
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_reset_device(const stm32_t *stm) {
|
||||
const auto target_address = stm->dev->ram_start;
|
||||
|
||||
if (stm->dev->flags & F_OBLL) {
|
||||
/* set the OBL_LAUNCH bit to reset device (see RM0360, 2.5) */
|
||||
return stm32_run_raw_code(stm, target_address, STM_OBL_LAUNCH_CODE, STM_OBL_LAUNCH_CODE_SIZE);
|
||||
} else {
|
||||
return stm32_run_raw_code(stm, target_address, STM_RESET_CODE, STM_RESET_CODE_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) {
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (address & 0x3 || length & 0x3) {
|
||||
ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stm->cmd->crc == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: CRC command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->crc) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
{
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf, address);
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
}
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
{
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf, address);
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
}
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
{
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
if (!stream->read_array(buf, BUFFER_SIZE))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (buf[4] != (buf[0] ^ buf[1] ^ buf[2] ^ buf[3]))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
*crc = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
|
||||
}
|
||||
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* CRC computed by STM32 is similar to the standard crc32_be()
|
||||
* implemented, for example, in Linux kernel in ./lib/crc32.c
|
||||
* But STM32 computes it on units of 32 bits word and swaps the
|
||||
* bytes of the word before the computation.
|
||||
* Due to byte swap, I cannot use any CRC available in existing
|
||||
* libraries, so here is a simple not optimized implementation.
|
||||
*/
|
||||
uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) {
|
||||
static constexpr uint32_t CRCPOLY_BE = 0x04c11db7;
|
||||
static constexpr uint32_t CRC_MSBMASK = 0x80000000;
|
||||
|
||||
if (len & 0x3) {
|
||||
ESP_LOGD(TAG, "Buffer length must be multiple of 4 bytes");
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (len) {
|
||||
uint32_t data = *buf++;
|
||||
data |= *buf++ << 8;
|
||||
data |= *buf++ << 16;
|
||||
data |= *buf++ << 24;
|
||||
len -= 4;
|
||||
|
||||
crc ^= data;
|
||||
|
||||
for (size_t i = 0; i < 32; ++i) {
|
||||
if (crc & CRC_MSBMASK) {
|
||||
crc = (crc << 1) ^ CRCPOLY_BE;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) {
|
||||
static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF;
|
||||
static constexpr uint32_t BUFFER_SIZE = 256;
|
||||
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
|
||||
if (address & 0x3 || length & 0x3) {
|
||||
ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stm->cmd->crc != STM32_CMD_ERR)
|
||||
return stm32_crc_memory(stm, address, length, crc);
|
||||
|
||||
const auto start = address;
|
||||
const auto total_len = length;
|
||||
uint32_t current_crc = CRC_INIT_VALUE;
|
||||
while (length) {
|
||||
const auto len = std::min(BUFFER_SIZE, length);
|
||||
if (stm32_read_memory(stm, address, buf, len) != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Failed to read memory at address 0x%08x, target write-protected?", address);
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
current_crc = stm32_sw_crc(current_crc, buf, len);
|
||||
length -= len;
|
||||
address += len;
|
||||
|
||||
ESP_LOGD(TAG, "\rCRC address 0x%08x (%.2f%%) ", address, (100.0f / (float) total_len) * (float) (address - start));
|
||||
}
|
||||
ESP_LOGD(TAG, "Done.");
|
||||
*crc = current_crc;
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
#endif
|
||||
129
esphome/components/shelly_dimmer/stm32flash.h
Normal file
129
esphome/components/shelly_dimmer/stm32flash.h
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
stm32flash - Open Source ST STM32 flash program for Arduino
|
||||
Copyright (C) 2010 Geoffrey McRae <geoff@spacevs.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
|
||||
#include <cstdint>
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
/* flags */
|
||||
constexpr auto STREAM_OPT_BYTE = (1 << 0); /* byte (not frame) oriented */
|
||||
constexpr auto STREAM_OPT_GVR_ETX = (1 << 1); /* cmd GVR returns protection status */
|
||||
constexpr auto STREAM_OPT_CMD_INIT = (1 << 2); /* use INIT cmd to autodetect speed */
|
||||
constexpr auto STREAM_OPT_RETRY = (1 << 3); /* allowed read() retry after timeout */
|
||||
constexpr auto STREAM_OPT_I2C = (1 << 4); /* i2c */
|
||||
constexpr auto STREAM_OPT_STRETCH_W = (1 << 5); /* warning for no-stretching commands */
|
||||
|
||||
constexpr auto STREAM_SERIAL = (STREAM_OPT_BYTE | STREAM_OPT_GVR_ETX | STREAM_OPT_CMD_INIT | STREAM_OPT_RETRY);
|
||||
constexpr auto STREAM_I2C = (STREAM_OPT_I2C | STREAM_OPT_STRETCH_W);
|
||||
|
||||
constexpr auto STM32_MAX_RX_FRAME = 256; /* cmd read memory */
|
||||
constexpr auto STM32_MAX_TX_FRAME = (1 + 256 + 1); /* cmd write memory */
|
||||
|
||||
constexpr auto STM32_MAX_PAGES = 0x0000ffff;
|
||||
constexpr auto STM32_MASS_ERASE = 0x00100000; /* > 2 x max_pages */
|
||||
|
||||
using stm32_err_t = enum Stm32Err {
|
||||
STM32_ERR_OK = 0,
|
||||
STM32_ERR_UNKNOWN, /* Generic error */
|
||||
STM32_ERR_NACK,
|
||||
STM32_ERR_NO_CMD, /* Command not available in bootloader */
|
||||
};
|
||||
|
||||
using flags_t = enum Flags {
|
||||
F_NO_ME = 1 << 0, /* Mass-Erase not supported */
|
||||
F_OBLL = 1 << 1, /* OBL_LAUNCH required */
|
||||
};
|
||||
|
||||
using stm32_cmd_t = struct Stm32Cmd {
|
||||
uint8_t get;
|
||||
uint8_t gvr;
|
||||
uint8_t gid;
|
||||
uint8_t rm;
|
||||
uint8_t go;
|
||||
uint8_t wm;
|
||||
uint8_t er; /* this may be extended erase */
|
||||
uint8_t wp;
|
||||
uint8_t uw;
|
||||
uint8_t rp;
|
||||
uint8_t ur;
|
||||
uint8_t crc;
|
||||
};
|
||||
|
||||
using stm32_dev_t = struct Stm32Dev { // NOLINT
|
||||
const uint16_t id;
|
||||
const char *name;
|
||||
const uint32_t ram_start, ram_end;
|
||||
const uint32_t fl_start, fl_end;
|
||||
const uint16_t fl_pps; // pages per sector
|
||||
const uint32_t *fl_ps; // page size
|
||||
const uint32_t opt_start, opt_end;
|
||||
const uint32_t mem_start, mem_end;
|
||||
const uint32_t flags;
|
||||
};
|
||||
|
||||
using stm32_t = struct Stm32 {
|
||||
uart::UARTDevice *stream;
|
||||
uint8_t flags;
|
||||
struct VarlenCmd *cmd_get_reply;
|
||||
uint8_t bl_version;
|
||||
uint8_t version;
|
||||
uint8_t option1, option2;
|
||||
uint16_t pid;
|
||||
stm32_cmd_t *cmd;
|
||||
const stm32_dev_t *dev;
|
||||
};
|
||||
|
||||
/*
|
||||
* Specify the length of reply for command GET
|
||||
* This is helpful for frame-oriented protocols, e.g. i2c, to avoid time
|
||||
* consuming try-fail-timeout-retry operation.
|
||||
* On byte-oriented protocols, i.e. UART, this information would be skipped
|
||||
* after read the first byte, so not needed.
|
||||
*/
|
||||
struct VarlenCmd {
|
||||
uint8_t version;
|
||||
uint8_t length;
|
||||
};
|
||||
|
||||
stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init);
|
||||
void stm32_close(stm32_t *stm);
|
||||
stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len);
|
||||
stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len);
|
||||
stm32_err_t stm32_wunprot_memory(const stm32_t *stm);
|
||||
stm32_err_t stm32_wprot_memory(const stm32_t *stm);
|
||||
stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages);
|
||||
stm32_err_t stm32_go(const stm32_t *stm, uint32_t address);
|
||||
stm32_err_t stm32_reset_device(const stm32_t *stm);
|
||||
stm32_err_t stm32_readprot_memory(const stm32_t *stm);
|
||||
stm32_err_t stm32_runprot_memory(const stm32_t *stm);
|
||||
stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc);
|
||||
stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc);
|
||||
uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len);
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_SHD_FIRMWARE_DATA
|
||||
@ -93,3 +93,9 @@
|
||||
//#define USE_BSEC // Requires a library with proprietary license.
|
||||
|
||||
#define USE_DASHBOARD_IMPORT
|
||||
|
||||
// Dummy firmware payload for shelly_dimmer
|
||||
#define USE_SHD_FIRMWARE_MAJOR_VERSION 56
|
||||
#define USE_SHD_FIRMWARE_MINOR_VERSION 5
|
||||
#define USE_SHD_FIRMWARE_DATA \
|
||||
{}
|
||||
|
||||
@ -1751,6 +1751,18 @@ light:
|
||||
to: 25
|
||||
- single_light_id: ${roomname}_lights
|
||||
|
||||
- platform: shelly_dimmer
|
||||
name: "Shelly Dimmer Light"
|
||||
power:
|
||||
name: "Shelly Dimmer Power"
|
||||
voltage:
|
||||
name: "Shelly Dimmer Voltage"
|
||||
current:
|
||||
name: "Shelly Dimmer Current"
|
||||
max_brightness: 500
|
||||
firmware: "51.6"
|
||||
uart_id: uart0
|
||||
|
||||
remote_transmitter:
|
||||
- pin: 32
|
||||
carrier_duty_percent: 100%
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user