mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Merge branch 'esp32-pio-pch' into nrf52-pch
This commit is contained in:
@@ -422,6 +422,7 @@ esphome/components/pn7150_i2c/* @jesserockz @kbx81
|
||||
esphome/components/pn7160/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_i2c/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
||||
esphome/components/pn71xx/* @jesserockz @kbx81
|
||||
esphome/components/power_supply/* @esphome/core
|
||||
esphome/components/preferences/* @esphome/core
|
||||
esphome/components/provisioning/* @esphome/core
|
||||
@@ -641,6 +642,7 @@ esphome/components/wts01/* @alepee
|
||||
esphome/components/x9c/* @EtienneMD
|
||||
esphome/components/xdb401/* @RT530
|
||||
esphome/components/xgzp68xx/* @gcormier
|
||||
esphome/components/xiaomi_body_scale/* @dckiller51
|
||||
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
||||
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
||||
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cerrno>
|
||||
#include <sys/select.h>
|
||||
|
||||
namespace esphome::async_tcp {
|
||||
|
||||
@@ -42,7 +41,15 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
socket_->setblocking(false);
|
||||
if (socket_->setblocking(false) != 0) {
|
||||
// Capture before the log and close() clobber errno
|
||||
const int saved_errno = errno;
|
||||
ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, saved_errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
||||
if (err == 0) {
|
||||
@@ -97,45 +104,22 @@ void AsyncClient::loop() {
|
||||
return;
|
||||
|
||||
if (connecting_) {
|
||||
// For connecting, we need to check writability, not readability
|
||||
// The Application's select() only monitors read FDs, so we do our own check here
|
||||
// For ESP platforms lwip_select() might be faster, but this code isn't used
|
||||
// on those platforms anyway. If it was, we'd fix the Application select()
|
||||
// to report writability instead of doing it this way.
|
||||
int fd = socket_->get_fd();
|
||||
if (fd < 0) {
|
||||
ESP_LOGW(TAG, "Invalid socket fd");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
|
||||
struct timeval tv = {0, 0};
|
||||
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
|
||||
if (ret > 0 && FD_ISSET(fd, &writefds)) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
|
||||
int err = 0;
|
||||
switch (socket::poll_connect(*socket_, err)) {
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_PENDING:
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED:
|
||||
connecting_ = false;
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection failed: %d", error);
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_ERROR:
|
||||
ESP_LOGW(TAG, "Connection failed: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
const int err = errno;
|
||||
ESP_LOGE(TAG, "Select error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
error_cb_(error_arg_, this, err);
|
||||
break;
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
|
||||
@@ -23,6 +23,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||
CONF_HOST = "host"
|
||||
CONF_HUMIDITY_SOURCE = "humidity_source"
|
||||
CONF_IAQ = "iaq"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Standard CSS named colors, as 0xRRGGBB values."""
|
||||
|
||||
CSS_COLORS: dict[str, int] = {
|
||||
"aliceblue": 0xF0F8FF,
|
||||
"antiquewhite": 0xFAEBD7,
|
||||
"aqua": 0x00FFFF,
|
||||
"aquamarine": 0x7FFFD4,
|
||||
"azure": 0xF0FFFF,
|
||||
"beige": 0xF5F5DC,
|
||||
"bisque": 0xFFE4C4,
|
||||
"black": 0x000000,
|
||||
"blanchedalmond": 0xFFEBCD,
|
||||
"blue": 0x0000FF,
|
||||
"blueviolet": 0x8A2BE2,
|
||||
"brown": 0xA52A2A,
|
||||
"burlywood": 0xDEB887,
|
||||
"cadetblue": 0x5F9EA0,
|
||||
"chartreuse": 0x7FFF00,
|
||||
"chocolate": 0xD2691E,
|
||||
"coral": 0xFF7F50,
|
||||
"cornflowerblue": 0x6495ED,
|
||||
"cornsilk": 0xFFF8DC,
|
||||
"crimson": 0xDC143C,
|
||||
"cyan": 0x00FFFF,
|
||||
"darkblue": 0x00008B,
|
||||
"darkcyan": 0x008B8B,
|
||||
"darkgoldenrod": 0xB8860B,
|
||||
"darkgray": 0xA9A9A9,
|
||||
"darkgreen": 0x006400,
|
||||
"darkgrey": 0xA9A9A9,
|
||||
"darkkhaki": 0xBDB76B,
|
||||
"darkmagenta": 0x8B008B,
|
||||
"darkolivegreen": 0x556B2F,
|
||||
"darkorange": 0xFF8C00,
|
||||
"darkorchid": 0x9932CC,
|
||||
"darkred": 0x8B0000,
|
||||
"darksalmon": 0xE9967A,
|
||||
"darkseagreen": 0x8FBC8F,
|
||||
"darkslateblue": 0x483D8B,
|
||||
"darkslategray": 0x2F4F4F,
|
||||
"darkslategrey": 0x2F4F4F,
|
||||
"darkturquoise": 0x00CED1,
|
||||
"darkviolet": 0x9400D3,
|
||||
"deeppink": 0xFF1493,
|
||||
"deepskyblue": 0x00BFFF,
|
||||
"dimgray": 0x696969,
|
||||
"dimgrey": 0x696969,
|
||||
"dodgerblue": 0x1E90FF,
|
||||
"firebrick": 0xB22222,
|
||||
"floralwhite": 0xFFFAF0,
|
||||
"forestgreen": 0x228B22,
|
||||
"fuchsia": 0xFF00FF,
|
||||
"gainsboro": 0xDCDCDC,
|
||||
"ghostwhite": 0xF8F8FF,
|
||||
"goldenrod": 0xDAA520,
|
||||
"gold": 0xFFD700,
|
||||
"gray": 0x808080,
|
||||
"green": 0x008000,
|
||||
"greenyellow": 0xADFF2F,
|
||||
"grey": 0x808080,
|
||||
"honeydew": 0xF0FFF0,
|
||||
"hotpink": 0xFF69B4,
|
||||
"indianred": 0xCD5C5C,
|
||||
"indigo": 0x4B0082,
|
||||
"ivory": 0xFFFFF0,
|
||||
"khaki": 0xF0E68C,
|
||||
"lavenderblush": 0xFFF0F5,
|
||||
"lavender": 0xE6E6FA,
|
||||
"lawngreen": 0x7CFC00,
|
||||
"lemonchiffon": 0xFFFACD,
|
||||
"lightblue": 0xADD8E6,
|
||||
"lightcoral": 0xF08080,
|
||||
"lightcyan": 0xE0FFFF,
|
||||
"lightgoldenrodyellow": 0xFAFAD2,
|
||||
"lightgray": 0xD3D3D3,
|
||||
"lightgreen": 0x90EE90,
|
||||
"lightgrey": 0xD3D3D3,
|
||||
"lightpink": 0xFFB6C1,
|
||||
"lightsalmon": 0xFFA07A,
|
||||
"lightseagreen": 0x20B2AA,
|
||||
"lightskyblue": 0x87CEFA,
|
||||
"lightslategray": 0x778899,
|
||||
"lightslategrey": 0x778899,
|
||||
"lightsteelblue": 0xB0C4DE,
|
||||
"lightyellow": 0xFFFFE0,
|
||||
"lime": 0x00FF00,
|
||||
"limegreen": 0x32CD32,
|
||||
"linen": 0xFAF0E6,
|
||||
"magenta": 0xFF00FF,
|
||||
"maroon": 0x800000,
|
||||
"mediumaquamarine": 0x66CDAA,
|
||||
"mediumblue": 0x0000CD,
|
||||
"mediumorchid": 0xBA55D3,
|
||||
"mediumpurple": 0x9370DB,
|
||||
"mediumseagreen": 0x3CB371,
|
||||
"mediumslateblue": 0x7B68EE,
|
||||
"mediumspringgreen": 0x00FA9A,
|
||||
"mediumturquoise": 0x48D1CC,
|
||||
"mediumvioletred": 0xC71585,
|
||||
"midnightblue": 0x191970,
|
||||
"mintcream": 0xF5FFFA,
|
||||
"mistyrose": 0xFFE4E1,
|
||||
"moccasin": 0xFFE4B5,
|
||||
"navajowhite": 0xFFDEAD,
|
||||
"navy": 0x000080,
|
||||
"oldlace": 0xFDF5E6,
|
||||
"olive": 0x808000,
|
||||
"olivedrab": 0x6B8E23,
|
||||
"orange": 0xFFA500,
|
||||
"orangered": 0xFF4500,
|
||||
"orchid": 0xDA70D6,
|
||||
"palegoldenrod": 0xEEE8AA,
|
||||
"palegreen": 0x98FB98,
|
||||
"paleturquoise": 0xAFEEEE,
|
||||
"palevioletred": 0xDB7093,
|
||||
"papayawhip": 0xFFEFD5,
|
||||
"peachpuff": 0xFFDAB9,
|
||||
"peru": 0xCD853F,
|
||||
"pink": 0xFFC0CB,
|
||||
"plum": 0xDDA0DD,
|
||||
"powderblue": 0xB0E0E6,
|
||||
"purple": 0x800080,
|
||||
"rebeccapurple": 0x663399,
|
||||
"red": 0xFF0000,
|
||||
"rosybrown": 0xBC8F8F,
|
||||
"royalblue": 0x4169E1,
|
||||
"saddlebrown": 0x8B4513,
|
||||
"salmon": 0xFA8072,
|
||||
"sandybrown": 0xF4A460,
|
||||
"seagreen": 0x2E8B57,
|
||||
"seashell": 0xFFF5EE,
|
||||
"sienna": 0xA0522D,
|
||||
"silver": 0xC0C0C0,
|
||||
"skyblue": 0x87CEEB,
|
||||
"slateblue": 0x6A5ACD,
|
||||
"slategray": 0x708090,
|
||||
"slategrey": 0x708090,
|
||||
"snow": 0xFFFAFA,
|
||||
"springgreen": 0x00FF7F,
|
||||
"steelblue": 0x4682B4,
|
||||
"tan": 0xD2B48C,
|
||||
"teal": 0x008080,
|
||||
"thistle": 0xD8BFD8,
|
||||
"tomato": 0xFF6347,
|
||||
"turquoise": 0x40E0D0,
|
||||
"violet": 0xEE82EE,
|
||||
"wheat": 0xF5DEB3,
|
||||
"white": 0xFFFFFF,
|
||||
"whitesmoke": 0xF5F5F5,
|
||||
"yellow": 0xFFFF00,
|
||||
"yellowgreen": 0x9ACD32,
|
||||
}
|
||||
@@ -459,7 +459,10 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
tv.tv_usec = 0;
|
||||
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
this->client_->setblocking(true);
|
||||
if (this->client_->setblocking(true) != 0) {
|
||||
this->log_socket_error_(LOG_STR("blocking"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
// Acknowledge auth OK - 1 byte
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
|
||||
@@ -226,7 +226,7 @@ uint8_t GreeClimate::vertical_swing_() {
|
||||
}
|
||||
|
||||
uint8_t GreeClimate::temperature_() {
|
||||
return (uint8_t) roundf(clamp<float>(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX));
|
||||
return (uint8_t) roundf(clamp<float>(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX)) - GREE_TEMP_MIN;
|
||||
}
|
||||
|
||||
uint8_t GreeClimate::preset_() {
|
||||
|
||||
@@ -87,7 +87,7 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
|
||||
BLUETOOTH_PASSWORD_SET_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(LD2410Component),
|
||||
cv.Required(CONF_PASSWORD): cv.templatable(cv.string_strict),
|
||||
cv.Required(CONF_PASSWORD): cv.sensitive(cv.templatable(cv.string_strict)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const.css_colors import CSS_COLORS
|
||||
from esphome.config import path_context
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -10,6 +12,7 @@ from esphome.const import (
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_BRIGHTNESS_LIMITS,
|
||||
CONF_COLD_WHITE,
|
||||
CONF_COLOR,
|
||||
CONF_COLOR_BRIGHTNESS,
|
||||
CONF_COLOR_MODE,
|
||||
CONF_COLOR_TEMPERATURE,
|
||||
@@ -46,6 +49,8 @@ from .types import (
|
||||
ToggleAction,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_INCLUDE_NONE = "include_none"
|
||||
|
||||
_STATE_ON_OFF = cv.one_of("ON", "OFF", upper=True)
|
||||
@@ -141,6 +146,50 @@ LIGHT_STATE_FIELDS: tuple[LightStateField, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@schema_extractor("one_of")
|
||||
def validate_color(value: Any) -> str | int:
|
||||
"""Validate a CSS color name or a 0xRRGGBB value."""
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return ["CSS color name", "hex color value"]
|
||||
if isinstance(value, int) or (
|
||||
isinstance(value, str) and value.lower().startswith("0x")
|
||||
):
|
||||
return cv.hex_int_range(0, 0xFFFFFF)(value)
|
||||
return cv.one_of(*CSS_COLORS, lower=True)(value)
|
||||
|
||||
|
||||
COLOR_SCHEMA: dict[cv.Optional, Any] = {cv.Optional(CONF_COLOR): validate_color}
|
||||
|
||||
|
||||
def color_to_rgb(config: ConfigType) -> ConfigType:
|
||||
"""Replace a `color` CSS name or 0xRRGGBB value with red, green and blue values.
|
||||
|
||||
The light scales its color so the brightest channel is at full level, so a dark
|
||||
color is given as a full-level color plus a color brightness.
|
||||
"""
|
||||
if (color := config.pop(CONF_COLOR, None)) is None:
|
||||
return config
|
||||
if any(key in config for key in (CONF_RED, CONF_GREEN, CONF_BLUE)):
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_COLOR}' cannot be used with '{CONF_RED}', '{CONF_GREEN}' or '{CONF_BLUE}'"
|
||||
)
|
||||
rgb = color if isinstance(color, int) else CSS_COLORS[color]
|
||||
channels = (rgb >> 16 & 0xFF, rgb >> 8 & 0xFF, rgb & 0xFF)
|
||||
peak = max(channels)
|
||||
if CONF_COLOR_BRIGHTNESS not in config:
|
||||
config[CONF_COLOR_BRIGHTNESS] = peak / 255
|
||||
elif peak < 0xFF:
|
||||
_LOGGER.warning(
|
||||
"'%s' overrides the brightness of color '%s'",
|
||||
CONF_COLOR_BRIGHTNESS,
|
||||
f"0x{color:06X}" if isinstance(color, int) else color,
|
||||
)
|
||||
for key, value in zip((CONF_RED, CONF_GREEN, CONF_BLUE), channels, strict=True):
|
||||
config[key] = value / peak if peak else 0.0
|
||||
return config
|
||||
|
||||
|
||||
LIGHT_STATE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(field.conf_key): (
|
||||
@@ -148,7 +197,8 @@ LIGHT_STATE_SCHEMA = cv.Schema(
|
||||
)
|
||||
for field in LIGHT_STATE_FIELDS
|
||||
}
|
||||
)
|
||||
).extend(COLOR_SCHEMA)
|
||||
LIGHT_STATE_SCHEMA.add_extra(color_to_rgb)
|
||||
|
||||
LIGHT_CONTROL_ACTION_SCHEMA = LIGHT_STATE_SCHEMA.extend(
|
||||
{
|
||||
@@ -405,7 +455,8 @@ LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_BLUE): cv.templatable(cv.percentage),
|
||||
cv.Optional(CONF_WHITE): cv.templatable(cv.percentage),
|
||||
}
|
||||
)
|
||||
).extend(COLOR_SCHEMA)
|
||||
LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA.add_extra(color_to_rgb)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import image
|
||||
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
|
||||
from esphome.components.const.css_colors import CSS_COLORS
|
||||
from esphome.components.font import Font
|
||||
from esphome.components.image import Image_
|
||||
import esphome.config_validation as cv
|
||||
@@ -64,170 +65,19 @@ opacity = LValidator(
|
||||
animatable=True,
|
||||
)
|
||||
|
||||
COLOR_NAMES = {
|
||||
"aliceblue": 0xF0F8FF,
|
||||
"antiquewhite": 0xFAEBD7,
|
||||
"aqua": 0x00FFFF,
|
||||
"aquamarine": 0x7FFFD4,
|
||||
"azure": 0xF0FFFF,
|
||||
"beige": 0xF5F5DC,
|
||||
"bisque": 0xFFE4C4,
|
||||
"black": 0x000000,
|
||||
"blanchedalmond": 0xFFEBCD,
|
||||
"blue": 0x0000FF,
|
||||
"blueviolet": 0x8A2BE2,
|
||||
"brown": 0xA52A2A,
|
||||
"burlywood": 0xDEB887,
|
||||
"cadetblue": 0x5F9EA0,
|
||||
"chartreuse": 0x7FFF00,
|
||||
"chocolate": 0xD2691E,
|
||||
"coral": 0xFF7F50,
|
||||
"cornflowerblue": 0x6495ED,
|
||||
"cornsilk": 0xFFF8DC,
|
||||
"crimson": 0xDC143C,
|
||||
"cyan": 0x00FFFF,
|
||||
"darkblue": 0x00008B,
|
||||
"darkcyan": 0x008B8B,
|
||||
"darkgoldenrod": 0xB8860B,
|
||||
"darkgray": 0xA9A9A9,
|
||||
"darkgreen": 0x006400,
|
||||
"darkgrey": 0xA9A9A9,
|
||||
"darkkhaki": 0xBDB76B,
|
||||
"darkmagenta": 0x8B008B,
|
||||
"darkolivegreen": 0x556B2F,
|
||||
"darkorange": 0xFF8C00,
|
||||
"darkorchid": 0x9932CC,
|
||||
"darkred": 0x8B0000,
|
||||
"darksalmon": 0xE9967A,
|
||||
"darkseagreen": 0x8FBC8F,
|
||||
"darkslateblue": 0x483D8B,
|
||||
"darkslategray": 0x2F4F4F,
|
||||
"darkslategrey": 0x2F4F4F,
|
||||
"darkturquoise": 0x00CED1,
|
||||
"darkviolet": 0x9400D3,
|
||||
"deeppink": 0xFF1493,
|
||||
"deepskyblue": 0x00BFFF,
|
||||
"dimgray": 0x696969,
|
||||
"dimgrey": 0x696969,
|
||||
"dodgerblue": 0x1E90FF,
|
||||
"firebrick": 0xB22222,
|
||||
"floralwhite": 0xFFFAF0,
|
||||
"forestgreen": 0x228B22,
|
||||
"fuchsia": 0xFF00FF,
|
||||
"gainsboro": 0xDCDCDC,
|
||||
"ghostwhite": 0xF8F8FF,
|
||||
"goldenrod": 0xDAA520,
|
||||
"gold": 0xFFD700,
|
||||
"gray": 0x808080,
|
||||
"green": 0x008000,
|
||||
"greenyellow": 0xADFF2F,
|
||||
"grey": 0x808080,
|
||||
"honeydew": 0xF0FFF0,
|
||||
"hotpink": 0xFF69B4,
|
||||
"indianred": 0xCD5C5C,
|
||||
"indigo": 0x4B0082,
|
||||
"ivory": 0xFFFFF0,
|
||||
"khaki": 0xF0E68C,
|
||||
"lavenderblush": 0xFFF0F5,
|
||||
"lavender": 0xE6E6FA,
|
||||
"lawngreen": 0x7CFC00,
|
||||
"lemonchiffon": 0xFFFACD,
|
||||
"lightblue": 0xADD8E6,
|
||||
"lightcoral": 0xF08080,
|
||||
"lightcyan": 0xE0FFFF,
|
||||
"lightgoldenrodyellow": 0xFAFAD2,
|
||||
"lightgray": 0xD3D3D3,
|
||||
"lightgreen": 0x90EE90,
|
||||
"lightgrey": 0xD3D3D3,
|
||||
"lightpink": 0xFFB6C1,
|
||||
"lightsalmon": 0xFFA07A,
|
||||
"lightseagreen": 0x20B2AA,
|
||||
"lightskyblue": 0x87CEFA,
|
||||
"lightslategray": 0x778899,
|
||||
"lightslategrey": 0x778899,
|
||||
"lightsteelblue": 0xB0C4DE,
|
||||
"lightyellow": 0xFFFFE0,
|
||||
"lime": 0x00FF00,
|
||||
"limegreen": 0x32CD32,
|
||||
"linen": 0xFAF0E6,
|
||||
"magenta": 0xFF00FF,
|
||||
"maroon": 0x800000,
|
||||
"mediumaquamarine": 0x66CDAA,
|
||||
"mediumblue": 0x0000CD,
|
||||
"mediumorchid": 0xBA55D3,
|
||||
"mediumpurple": 0x9370DB,
|
||||
"mediumseagreen": 0x3CB371,
|
||||
"mediumslateblue": 0x7B68EE,
|
||||
"mediumspringgreen": 0x00FA9A,
|
||||
"mediumturquoise": 0x48D1CC,
|
||||
"mediumvioletred": 0xC71585,
|
||||
"midnightblue": 0x191970,
|
||||
"mintcream": 0xF5FFFA,
|
||||
"mistyrose": 0xFFE4E1,
|
||||
"moccasin": 0xFFE4B5,
|
||||
"navajowhite": 0xFFDEAD,
|
||||
"navy": 0x000080,
|
||||
"oldlace": 0xFDF5E6,
|
||||
"olive": 0x808000,
|
||||
"olivedrab": 0x6B8E23,
|
||||
"orange": 0xFFA500,
|
||||
"orangered": 0xFF4500,
|
||||
"orchid": 0xDA70D6,
|
||||
"palegoldenrod": 0xEEE8AA,
|
||||
"palegreen": 0x98FB98,
|
||||
"paleturquoise": 0xAFEEEE,
|
||||
"palevioletred": 0xDB7093,
|
||||
"papayawhip": 0xFFEFD5,
|
||||
"peachpuff": 0xFFDAB9,
|
||||
"peru": 0xCD853F,
|
||||
"pink": 0xFFC0CB,
|
||||
"plum": 0xDDA0DD,
|
||||
"powderblue": 0xB0E0E6,
|
||||
"purple": 0x800080,
|
||||
"rebeccapurple": 0x663399,
|
||||
"red": 0xFF0000,
|
||||
"rosybrown": 0xBC8F8F,
|
||||
"royalblue": 0x4169E1,
|
||||
"saddlebrown": 0x8B4513,
|
||||
"salmon": 0xFA8072,
|
||||
"sandybrown": 0xF4A460,
|
||||
"seagreen": 0x2E8B57,
|
||||
"seashell": 0xFFF5EE,
|
||||
"sienna": 0xA0522D,
|
||||
"silver": 0xC0C0C0,
|
||||
"skyblue": 0x87CEEB,
|
||||
"slateblue": 0x6A5ACD,
|
||||
"slategray": 0x708090,
|
||||
"slategrey": 0x708090,
|
||||
"snow": 0xFFFAFA,
|
||||
"springgreen": 0x00FF7F,
|
||||
"steelblue": 0x4682B4,
|
||||
"tan": 0xD2B48C,
|
||||
"teal": 0x008080,
|
||||
"thistle": 0xD8BFD8,
|
||||
"tomato": 0xFF6347,
|
||||
"turquoise": 0x40E0D0,
|
||||
"violet": 0xEE82EE,
|
||||
"wheat": 0xF5DEB3,
|
||||
"white": 0xFFFFFF,
|
||||
"whitesmoke": 0xF5F5F5,
|
||||
"yellow": 0xFFFF00,
|
||||
"yellowgreen": 0x9ACD32,
|
||||
}
|
||||
|
||||
|
||||
@schema_extractor("one_of")
|
||||
def color(value):
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return ["hex color value", "color ID"]
|
||||
return cv.Any(cv.int_, cv.one_of(*COLOR_NAMES, lower=True), cv.use_id(ColorStruct))(
|
||||
return cv.Any(cv.int_, cv.one_of(*CSS_COLORS, lower=True), cv.use_id(ColorStruct))(
|
||||
value
|
||||
)
|
||||
|
||||
|
||||
def get_component_colors(value):
|
||||
if isinstance(value, str) and value in COLOR_NAMES:
|
||||
value = COLOR_NAMES[value]
|
||||
if isinstance(value, str) and value in CSS_COLORS:
|
||||
value = CSS_COLORS[value]
|
||||
if isinstance(value, int):
|
||||
return value >> 16, value >> 8 & 0xFF, value & 0xFF
|
||||
if isinstance(value, ID):
|
||||
@@ -254,8 +104,8 @@ class LvColor(LValidator):
|
||||
)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in COLOR_NAMES:
|
||||
return color_retmapper(COLOR_NAMES[item])
|
||||
if item in CSS_COLORS:
|
||||
return color_retmapper(CSS_COLORS[item])
|
||||
raise AttributeError(item)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.const import CONF_ID, CONF_TAG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_MK2PVROUTER_ID,
|
||||
MK2PVROUTER_LISTENER_SCHEMA,
|
||||
mk2pvrouter_ns,
|
||||
register_mk2pvrouter_listener,
|
||||
)
|
||||
|
||||
Mk2PVRouterBinarySensor = mk2pvrouter_ns.class_(
|
||||
"Mk2PVRouterBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(Mk2PVRouterBinarySensor).extend(
|
||||
MK2PVROUTER_LISTENER_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG])
|
||||
await cg.register_component(var, config)
|
||||
await binary_sensor.register_binary_sensor(var, config)
|
||||
mk2pvrouter = await cg.get_variable(config[CONF_MK2PVROUTER_ID])
|
||||
await register_mk2pvrouter_listener(mk2pvrouter, var)
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "mk2pvrouter_binary_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::mk2pvrouter {
|
||||
|
||||
static const char *const TAG = "mk2pvrouter_binary_sensor";
|
||||
|
||||
Mk2PVRouterBinarySensor::Mk2PVRouterBinarySensor(const char *tag) : Mk2PVRouterListener(tag) {}
|
||||
|
||||
void Mk2PVRouterBinarySensor::publish_val(const char *val) {
|
||||
bool state = (strcmp(val, "0") != 0);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
void Mk2PVRouterBinarySensor::dump_config() {
|
||||
LOG_BINARY_SENSOR(" ", "Mk2PVRouter Binary Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Tag: %s", this->get_tag());
|
||||
}
|
||||
|
||||
} // namespace esphome::mk2pvrouter
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/mk2pvrouter/mk2pvrouter.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome::mk2pvrouter {
|
||||
|
||||
class Mk2PVRouterBinarySensor final : public Mk2PVRouterListener, public binary_sensor::BinarySensor, public Component {
|
||||
public:
|
||||
explicit Mk2PVRouterBinarySensor(const char *tag);
|
||||
void publish_val(const char *val) override;
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::mk2pvrouter
|
||||
@@ -0,0 +1,27 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.const import CONF_ID, CONF_TAG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_MK2PVROUTER_ID,
|
||||
MK2PVROUTER_LISTENER_SCHEMA,
|
||||
mk2pvrouter_ns,
|
||||
register_mk2pvrouter_listener,
|
||||
)
|
||||
|
||||
Mk2PVRouterTextSensor = mk2pvrouter_ns.class_(
|
||||
"Mk2PVRouterTextSensor", text_sensor.TextSensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema(Mk2PVRouterTextSensor).extend(
|
||||
MK2PVROUTER_LISTENER_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG])
|
||||
await cg.register_component(var, config)
|
||||
await text_sensor.register_text_sensor(var, config)
|
||||
mk2pvrouter = await cg.get_variable(config[CONF_MK2PVROUTER_ID])
|
||||
await register_mk2pvrouter_listener(mk2pvrouter, var)
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "mk2pvrouter_text_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mk2pvrouter {
|
||||
|
||||
static const char *const TAG = "mk2pvrouter_text_sensor";
|
||||
|
||||
Mk2PVRouterTextSensor::Mk2PVRouterTextSensor(const char *tag) : Mk2PVRouterListener(tag) {}
|
||||
|
||||
void Mk2PVRouterTextSensor::publish_val(const char *val) { this->publish_state(val); }
|
||||
|
||||
void Mk2PVRouterTextSensor::dump_config() {
|
||||
LOG_TEXT_SENSOR(" ", "Mk2PVRouter Text Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Tag: %s", this->get_tag());
|
||||
}
|
||||
|
||||
} // namespace esphome::mk2pvrouter
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/mk2pvrouter/mk2pvrouter.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
namespace esphome::mk2pvrouter {
|
||||
|
||||
class Mk2PVRouterTextSensor final : public Mk2PVRouterListener, public text_sensor::TextSensor, public Component {
|
||||
public:
|
||||
explicit Mk2PVRouterTextSensor(const char *tag);
|
||||
void publish_val(const char *val) override;
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::mk2pvrouter
|
||||
@@ -243,8 +243,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Inclusive(CONF_CLIENT_CERTIFICATE, "cert-key-pair"): cv.All(
|
||||
cv.string, cv.only_on_esp32
|
||||
),
|
||||
cv.Inclusive(CONF_CLIENT_CERTIFICATE_KEY, "cert-key-pair"): cv.All(
|
||||
cv.string, cv.only_on_esp32
|
||||
cv.Inclusive(CONF_CLIENT_CERTIFICATE_KEY, "cert-key-pair"): cv.sensitive(
|
||||
cv.All(cv.string, cv.only_on_esp32)
|
||||
),
|
||||
cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32=False): cv.All(
|
||||
cv.boolean, cv.only_on_esp32
|
||||
|
||||
@@ -116,6 +116,8 @@ void NciMessage::set_header(const uint8_t message_type, const uint8_t gid, const
|
||||
|
||||
void NciMessage::set_message(const uint8_t message_type, const std::vector<uint8_t> &payload) {
|
||||
this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE);
|
||||
this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = message_type & nfc::NCI_PKT_MT_MASK;
|
||||
this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = 0;
|
||||
this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size();
|
||||
this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end());
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ bool decode_mifare_classic_tlv(std::vector<uint8_t> &data, uint32_t &message_len
|
||||
}
|
||||
|
||||
uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length) {
|
||||
uint32_t buffer_size = message_length + 2 + 1;
|
||||
// TLV header (2 bytes, or 4 for messages of 255 bytes or more) plus the terminator TLV
|
||||
uint32_t buffer_size = message_length + (message_length < 255 ? 2 : 4) + 1;
|
||||
if (buffer_size % MIFARE_ULTRALIGHT_READ_SIZE != 0)
|
||||
buffer_size = ((buffer_size / MIFARE_ULTRALIGHT_READ_SIZE) + 1) * MIFARE_ULTRALIGHT_READ_SIZE;
|
||||
return buffer_size;
|
||||
|
||||
@@ -14,6 +14,10 @@ static const char *const TAG = "dfu";
|
||||
|
||||
static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS
|
||||
static const uint8_t DFU_MAGIC_UF2_RESET = 0x57; // Adafruit nRF52 bootloader UF2 magic
|
||||
static const uint8_t DFU_MAGIC_SKIP = 0x6d; // Adafruit nRF52 bootloader: start the app, skip DFU
|
||||
// Host-side baud rates that trigger an action; see DeviceFirmwareUpdate::setup().
|
||||
static const uint32_t DFU_TOUCH_BAUD_RATE = 1200; // reboot into the bootloader (DFU)
|
||||
static const uint32_t RESET_TOUCH_BAUD_RATE = 2001; // plain reboot back into the application
|
||||
|
||||
void DeviceFirmwareUpdate::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
@@ -21,7 +25,13 @@ void DeviceFirmwareUpdate::setup() {
|
||||
}
|
||||
#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT)
|
||||
zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) {
|
||||
if (rate == 1200) {
|
||||
if (rate == RESET_TOUCH_BAUD_RATE) {
|
||||
// A plain reboot for host tools (a logs view's Reset device): the USB CDC has no
|
||||
// reset line, and the bootloader, once entered, has no host command back to the app.
|
||||
NRF_POWER->GPREGRET = DFU_MAGIC_SKIP;
|
||||
arch_feed_wdt();
|
||||
App.reboot();
|
||||
} else if (rate == DFU_TOUCH_BAUD_RATE) {
|
||||
volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C;
|
||||
(*dbl_reset_mem) = DFU_DBL_RESET_MAGIC;
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
|
||||
@@ -128,7 +128,7 @@ REMOTE_PACKAGE_SCHEMA = cv.All(
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
cv.Optional(CONF_PATH): cv.string,
|
||||
cv.Optional(CONF_USERNAME): cv.string,
|
||||
cv.Optional(CONF_PASSWORD): cv.string,
|
||||
cv.Optional(CONF_PASSWORD): cv.sensitive(cv.string),
|
||||
cv.Exclusive(CONF_FILE, CONF_FILES): validate_yaml_filename,
|
||||
cv.Exclusive(CONF_FILES, CONF_FILES): cv.All(
|
||||
cv.ensure_list(
|
||||
|
||||
@@ -1,245 +1,23 @@
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import nfc
|
||||
from esphome.components import pn71xx
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_IRQ_PIN,
|
||||
CONF_MESSAGE,
|
||||
CONF_ON_FINISHED_WRITE,
|
||||
CONF_ON_TAG,
|
||||
CONF_ON_TAG_REMOVED,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["binary_sensor", "nfc"]
|
||||
AUTO_LOAD = ["pn71xx"]
|
||||
CODEOWNERS = ["@kbx81", "@jesserockz"]
|
||||
|
||||
CONF_EMULATION_MESSAGE = "emulation_message"
|
||||
CONF_EMULATION_OFF = "emulation_off"
|
||||
CONF_EMULATION_ON = "emulation_on"
|
||||
CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record"
|
||||
CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan"
|
||||
CONF_PN7150_ID = "pn7150_id"
|
||||
CONF_POLLING_OFF = "polling_off"
|
||||
CONF_POLLING_ON = "polling_on"
|
||||
CONF_SET_CLEAN_MODE = "set_clean_mode"
|
||||
CONF_SET_EMULATION_MESSAGE = "set_emulation_message"
|
||||
CONF_SET_FORMAT_MODE = "set_format_mode"
|
||||
CONF_SET_READ_MODE = "set_read_mode"
|
||||
CONF_SET_WRITE_MESSAGE = "set_write_message"
|
||||
CONF_SET_WRITE_MODE = "set_write_mode"
|
||||
CONF_TAG_TTL = "tag_ttl"
|
||||
CONF_VEN_PIN = "ven_pin"
|
||||
|
||||
pn7150_ns = cg.esphome_ns.namespace("pn7150")
|
||||
PN7150 = pn7150_ns.class_("PN7150", nfc.Nfcc, cg.Component)
|
||||
PN7150 = pn7150_ns.class_("PN7150", pn71xx.PN71xx)
|
||||
|
||||
EmulationOffAction = pn7150_ns.class_("EmulationOffAction", automation.Action)
|
||||
EmulationOnAction = pn7150_ns.class_("EmulationOnAction", automation.Action)
|
||||
PollingOffAction = pn7150_ns.class_("PollingOffAction", automation.Action)
|
||||
PollingOnAction = pn7150_ns.class_("PollingOnAction", automation.Action)
|
||||
SetCleanModeAction = pn7150_ns.class_("SetCleanModeAction", automation.Action)
|
||||
SetEmulationMessageAction = pn7150_ns.class_(
|
||||
"SetEmulationMessageAction", automation.Action
|
||||
)
|
||||
SetFormatModeAction = pn7150_ns.class_("SetFormatModeAction", automation.Action)
|
||||
SetReadModeAction = pn7150_ns.class_("SetReadModeAction", automation.Action)
|
||||
SetWriteMessageAction = pn7150_ns.class_("SetWriteMessageAction", automation.Action)
|
||||
SetWriteModeAction = pn7150_ns.class_("SetWriteModeAction", automation.Action)
|
||||
|
||||
|
||||
PN7150IsWritingCondition = pn7150_ns.class_(
|
||||
"PN7150IsWritingCondition", automation.Condition
|
||||
)
|
||||
|
||||
|
||||
IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition)
|
||||
|
||||
|
||||
SIMPLE_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(PN7150),
|
||||
}
|
||||
)
|
||||
|
||||
SET_MESSAGE_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(PN7150),
|
||||
cv.Required(CONF_MESSAGE): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
PN7150_SCHEMA = cv.Schema(
|
||||
PN7150_SCHEMA = pn71xx.PN71XX_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(PN7150),
|
||||
cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TAG): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
|
||||
}
|
||||
),
|
||||
cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_EMULATION_MESSAGE): cv.string,
|
||||
cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"tag.set_emulation_message",
|
||||
SetEmulationMessageAction,
|
||||
SET_MESSAGE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"tag.set_write_message",
|
||||
SetWriteMessageAction,
|
||||
SET_MESSAGE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def pn7150_set_message_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string)
|
||||
cg.add(var.set_message(template_))
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_
|
||||
)
|
||||
cg.add(var.set_include_android_app_record(template_))
|
||||
return var
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.emulation_off",
|
||||
EmulationOffAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.emulation_on",
|
||||
EmulationOnAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.polling_off",
|
||||
PollingOffAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.polling_on",
|
||||
PollingOnAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.set_clean_mode",
|
||||
SetCleanModeAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.set_format_mode",
|
||||
SetFormatModeAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.set_read_mode",
|
||||
SetReadModeAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.set_write_mode",
|
||||
SetWriteModeAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
|
||||
),
|
||||
)
|
||||
pn71xx.register_is_writing_condition("pn7150.is_writing", PN7150)
|
||||
|
||||
|
||||
async def setup_pn7150(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
|
||||
cg.add(var.set_irq_pin(pin))
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN])
|
||||
cg.add(var.set_ven_pin(pin))
|
||||
|
||||
if emulation_message_config := config.get(CONF_EMULATION_MESSAGE):
|
||||
cg.add(var.set_tag_emulation_message(emulation_message_config))
|
||||
cg.add(var.set_tag_emulation_on())
|
||||
|
||||
if CONF_TAG_TTL in config:
|
||||
cg.add(var.set_tag_ttl(config[CONF_TAG_TTL]))
|
||||
|
||||
for conf in config.get(CONF_ON_TAG, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
cg.add(var.register_ontag_trigger(trigger))
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_TAG_REMOVED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
cg.add(var.register_ontagremoved_trigger(trigger))
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
automation.register_parented_condition(
|
||||
"pn7150.is_writing",
|
||||
PN7150IsWritingCondition,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(PN7150),
|
||||
}
|
||||
),
|
||||
)
|
||||
await pn71xx.setup_pn71xx(var, config)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/pn7150/pn7150.h"
|
||||
|
||||
namespace esphome::pn7150 {
|
||||
|
||||
template<typename... Ts> class PN7150IsWritingCondition final : public Condition<Ts...>, public Parented<PN7150> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return this->parent_->is_writing(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EmulationOffAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
void play(const Ts &...x) override { this->parent_->set_tag_emulation_off(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EmulationOnAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
void play(const Ts &...x) override { this->parent_->set_tag_emulation_on(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PollingOffAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
void play(const Ts &...x) override { this->parent_->set_polling_off(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PollingOnAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
void play(const Ts &...x) override { this->parent_->set_polling_on(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetCleanModeAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
void play(const Ts &...x) override { this->parent_->clean_mode(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetFormatModeAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
void play(const Ts &...x) override { this->parent_->format_mode(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetReadModeAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
void play(const Ts &...x) override { this->parent_->read_mode(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetEmulationMessageAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
TEMPLATABLE_VALUE(std::string, message)
|
||||
TEMPLATABLE_VALUE(bool, include_android_app_record)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->parent_->set_tag_emulation_message(this->message_.optional_value(x...),
|
||||
this->include_android_app_record_.optional_value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetWriteMessageAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
TEMPLATABLE_VALUE(std::string, message)
|
||||
TEMPLATABLE_VALUE(bool, include_android_app_record)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->parent_->set_tag_write_message(this->message_.optional_value(x...),
|
||||
this->include_android_app_record_.optional_value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetWriteModeAction final : public Action<Ts...>, public Parented<PN7150> {
|
||||
void play(const Ts &...x) override { this->parent_->write_mode(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::pn7150
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/nfc/automation.h"
|
||||
#include "esphome/components/nfc/nci_core.h"
|
||||
#include "esphome/components/nfc/nci_message.h"
|
||||
#include "esphome/components/nfc/nfc.h"
|
||||
#include "esphome/components/nfc/nfc_helpers.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <functional>
|
||||
#include "esphome/components/pn71xx/pn71xx.h"
|
||||
|
||||
namespace esphome::pn7150 {
|
||||
|
||||
static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10;
|
||||
static constexpr uint16_t NFCC_INIT_TIMEOUT = 50;
|
||||
static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15;
|
||||
|
||||
static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3;
|
||||
static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10;
|
||||
|
||||
static constexpr uint8_t XCHG_DATA_OID = 0x10;
|
||||
static constexpr uint8_t MF_SECTORSEL_OID = 0x32;
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40;
|
||||
static constexpr uint8_t TEST_PRBS_OID = 0x30;
|
||||
static constexpr uint8_t TEST_ANTENNA_OID = 0x3D;
|
||||
static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33;
|
||||
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10;
|
||||
|
||||
static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76,
|
||||
0x00, 0x00, 0x85, 0x01, 0x01, 0x00};
|
||||
static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04,
|
||||
0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00};
|
||||
static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03};
|
||||
static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04};
|
||||
static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0};
|
||||
static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6};
|
||||
static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00};
|
||||
static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82};
|
||||
|
||||
static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields
|
||||
0x00, // config param identifier (TOTAL_DURATION)
|
||||
0x02, // length of value
|
||||
0x01, // TOTAL_DURATION (low)...
|
||||
0x00}; // TOTAL_DURATION (high): 1 ms
|
||||
|
||||
static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields
|
||||
0x00, // config param identifier (TOTAL_DURATION)
|
||||
0x02, // length of value
|
||||
0xF8, // TOTAL_DURATION (low)...
|
||||
0x02}; // TOTAL_DURATION (high): 760 ms
|
||||
|
||||
static constexpr uint8_t PMU_CFG[] = {
|
||||
0x01, // Number of parameters
|
||||
0xA0, 0x0E, // ext. tag
|
||||
@@ -63,34 +13,6 @@ static constexpr uint8_t PMU_CFG[] = {
|
||||
0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2
|
||||
};
|
||||
|
||||
static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes
|
||||
nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN,
|
||||
nfc::INTF_ISODEP, // poll & listen mode
|
||||
nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_TAGCMD}; // poll mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode
|
||||
|
||||
static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming)
|
||||
1, // number of table entries
|
||||
0x01, // type = protocol-based
|
||||
@@ -99,196 +21,15 @@ static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (an
|
||||
0x01, // power state
|
||||
nfc::PROT_ISODEP}; // protocol
|
||||
|
||||
enum class CardEmulationState : uint8_t {
|
||||
CARD_EMU_IDLE,
|
||||
CARD_EMU_NDEF_APP_SELECTED,
|
||||
CARD_EMU_CC_SELECTED,
|
||||
CARD_EMU_NDEF_SELECTED,
|
||||
CARD_EMU_DESFIRE_PROD,
|
||||
};
|
||||
|
||||
enum class NCIState : uint8_t {
|
||||
NONE = 0x00,
|
||||
NFCC_RESET,
|
||||
NFCC_INIT,
|
||||
NFCC_CONFIG,
|
||||
NFCC_SET_DISCOVER_MAP,
|
||||
NFCC_SET_LISTEN_MODE_ROUTING,
|
||||
RFST_IDLE,
|
||||
RFST_DISCOVERY,
|
||||
RFST_W4_ALL_DISCOVERIES,
|
||||
RFST_W4_HOST_SELECT,
|
||||
RFST_LISTEN_ACTIVE,
|
||||
RFST_LISTEN_SLEEP,
|
||||
RFST_POLL_ACTIVE,
|
||||
EP_DEACTIVATING,
|
||||
EP_SELECTING,
|
||||
TEST = 0xFE,
|
||||
FAILED = 0xFF,
|
||||
};
|
||||
|
||||
enum class TestMode : uint8_t {
|
||||
TEST_NONE = 0x00,
|
||||
TEST_PRBS,
|
||||
TEST_ANTENNA,
|
||||
TEST_GET_REGISTER,
|
||||
};
|
||||
|
||||
struct DiscoveredEndpoint {
|
||||
uint8_t id;
|
||||
uint8_t protocol;
|
||||
uint32_t last_seen;
|
||||
std::unique_ptr<nfc::NfcTag> tag;
|
||||
bool trig_called;
|
||||
};
|
||||
|
||||
class PN7150 : public nfc::Nfcc, public Component {
|
||||
class PN7150 : public pn71xx::PN71xx {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; }
|
||||
void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; }
|
||||
|
||||
void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; }
|
||||
void set_tag_emulation_message(std::shared_ptr<nfc::NdefMessage> message);
|
||||
void set_tag_emulation_message(const optional<std::string> &message, optional<bool> include_android_app_record);
|
||||
void set_tag_emulation_message(const char *message, bool include_android_app_record = true);
|
||||
void set_tag_emulation_off();
|
||||
void set_tag_emulation_on();
|
||||
bool tag_emulation_enabled() { return this->listening_enabled_; }
|
||||
|
||||
void set_polling_off();
|
||||
void set_polling_on();
|
||||
bool polling_enabled() { return this->polling_enabled_; }
|
||||
|
||||
void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); }
|
||||
void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); }
|
||||
|
||||
template<typename F> void add_on_emulated_tag_scan_callback(F &&callback) {
|
||||
this->on_emulated_tag_scan_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_finished_write_callback(F &&callback) {
|
||||
this->on_finished_write_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
bool is_writing() { return this->next_task_ != EP_READ; };
|
||||
|
||||
void read_mode();
|
||||
void clean_mode();
|
||||
void format_mode();
|
||||
void write_mode();
|
||||
void set_tag_write_message(std::shared_ptr<nfc::NdefMessage> message);
|
||||
void set_tag_write_message(optional<std::string> message, optional<bool> include_android_app_record);
|
||||
|
||||
uint8_t set_test_mode(TestMode test_mode, const std::vector<uint8_t> &data, std::vector<uint8_t> &result);
|
||||
|
||||
protected:
|
||||
uint8_t reset_core_(bool reset_config, bool power);
|
||||
uint8_t init_core_();
|
||||
uint8_t send_init_config_();
|
||||
uint8_t send_core_config_();
|
||||
uint8_t refresh_core_config_();
|
||||
|
||||
uint8_t set_discover_map_();
|
||||
|
||||
uint8_t set_listen_mode_routing_();
|
||||
|
||||
uint8_t start_discovery_();
|
||||
uint8_t stop_discovery_();
|
||||
uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT);
|
||||
|
||||
void select_endpoint_();
|
||||
|
||||
uint8_t read_endpoint_data_(nfc::NfcTag &tag);
|
||||
uint8_t clean_endpoint_(nfc::NfcTagUid &uid);
|
||||
uint8_t format_endpoint_(nfc::NfcTagUid &uid);
|
||||
uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
|
||||
|
||||
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
|
||||
optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
|
||||
void purge_old_tags_();
|
||||
void erase_tag_(uint8_t tag_index);
|
||||
|
||||
/// advance controller state as required
|
||||
void nci_fsm_transition_();
|
||||
/// set new controller state
|
||||
void nci_fsm_set_state_(NCIState new_state);
|
||||
/// setting controller to this state caused an error; returns true if too many errors/failures
|
||||
bool nci_fsm_set_error_state_(NCIState new_state);
|
||||
/// parse & process incoming messages from the NFCC
|
||||
void process_message_();
|
||||
void process_rf_intf_activated_oid_(nfc::NciMessage &rx);
|
||||
void process_rf_discover_oid_(nfc::NciMessage &rx);
|
||||
void process_rf_deactivate_oid_(nfc::NciMessage &rx);
|
||||
void process_data_message_(nfc::NciMessage &rx);
|
||||
|
||||
void card_emu_t4t_get_response_(std::vector<uint8_t> &response, std::vector<uint8_t> &ndef_response);
|
||||
|
||||
uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT,
|
||||
bool expect_notification = true);
|
||||
virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0;
|
||||
virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0;
|
||||
|
||||
uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true);
|
||||
|
||||
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
||||
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||
uint8_t sect_to_auth_(uint8_t block_num);
|
||||
uint8_t format_mifare_classic_mifare_();
|
||||
uint8_t format_mifare_classic_ndef_();
|
||||
uint8_t write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
uint8_t halt_mifare_classic_tag_();
|
||||
|
||||
uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag);
|
||||
uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data);
|
||||
bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6);
|
||||
uint16_t read_mifare_ultralight_capacity_();
|
||||
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t &message_start_index);
|
||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
uint8_t clean_mifare_ultralight_();
|
||||
|
||||
enum NfcTask : uint8_t {
|
||||
EP_READ = 0,
|
||||
EP_CLEAN,
|
||||
EP_FORMAT,
|
||||
EP_WRITE,
|
||||
} next_task_{EP_READ};
|
||||
|
||||
bool config_refresh_pending_{false};
|
||||
bool core_config_is_solo_{false};
|
||||
bool listening_enabled_{false};
|
||||
bool polling_enabled_{true};
|
||||
|
||||
uint8_t error_count_{0};
|
||||
uint8_t fail_count_{0};
|
||||
uint32_t last_nci_state_change_{0};
|
||||
uint8_t selecting_endpoint_{0};
|
||||
uint32_t tag_ttl_{250};
|
||||
|
||||
GPIOPin *irq_pin_{nullptr};
|
||||
GPIOPin *ven_pin_{nullptr};
|
||||
|
||||
CallbackManager<void()> on_emulated_tag_scan_callback_;
|
||||
CallbackManager<void()> on_finished_write_callback_;
|
||||
|
||||
std::vector<DiscoveredEndpoint> discovered_endpoint_;
|
||||
|
||||
CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE};
|
||||
NCIState nci_state_{NCIState::NFCC_RESET};
|
||||
NCIState nci_state_error_{NCIState::NONE};
|
||||
|
||||
std::shared_ptr<nfc::NdefMessage> card_emulation_message_;
|
||||
std::shared_ptr<nfc::NdefMessage> next_task_message_to_write_;
|
||||
|
||||
std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
|
||||
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
|
||||
uint8_t verify_reset(nfc::NciMessage &rx, bool reset_config) override;
|
||||
uint8_t process_init_response(nfc::NciMessage &rx) override;
|
||||
std::span<const uint8_t> pmu_config() const override { return PMU_CFG; }
|
||||
std::span<const uint8_t> listen_mode_routing_config() const override { return RF_LISTEN_MODE_ROUTING_CONFIG; }
|
||||
};
|
||||
|
||||
} // namespace esphome::pn7150
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "pn7150.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pn7150 {
|
||||
|
||||
static const char *const TAG = "pn7150.mifare_classic";
|
||||
|
||||
uint8_t PN7150::read_mifare_classic_tag_(nfc::NfcTag &tag) {
|
||||
uint8_t current_block = 4;
|
||||
uint8_t message_start_index = 0;
|
||||
uint32_t message_length = 0;
|
||||
|
||||
if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
std::vector<uint8_t> data;
|
||||
|
||||
if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) {
|
||||
if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to read block %u", current_block);
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
uint32_t index = 0;
|
||||
uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length);
|
||||
std::vector<uint8_t> buffer;
|
||||
|
||||
while (index < buffer_size) {
|
||||
if (nfc::mifare_classic_is_first_block(current_block)) {
|
||||
if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Block authentication failed for %u", current_block);
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
std::vector<uint8_t> block_data;
|
||||
if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Error reading block %u", current_block);
|
||||
return nfc::STATUS_FAILED;
|
||||
} else {
|
||||
buffer.insert(buffer.end(), block_data.begin(), block_data.end());
|
||||
}
|
||||
|
||||
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
||||
current_block++;
|
||||
|
||||
if (nfc::mifare_classic_is_trailer_block(current_block)) {
|
||||
current_block++;
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.begin() + message_start_index < buffer.end()) {
|
||||
buffer.erase(buffer.begin(), buffer.begin() + message_start_index);
|
||||
} else {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
tag.set_ndef_message(make_unique<nfc::NdefMessage>(buffer));
|
||||
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data) {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num});
|
||||
|
||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||
if (this->transceive_(tx, rx) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Timeout reading tag data");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) ||
|
||||
(!rx.message_length_is(18))) {
|
||||
ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num);
|
||||
ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes_to(buf, rx.get_message()));
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1);
|
||||
|
||||
ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes_to(buf, data));
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num});
|
||||
|
||||
switch (key_num) {
|
||||
case nfc::MIFARE_CMD_AUTH_A:
|
||||
tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A;
|
||||
break;
|
||||
|
||||
case nfc::MIFARE_CMD_AUTH_B:
|
||||
tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (key != nullptr) {
|
||||
tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY;
|
||||
tx.get_message().insert(tx.get_message().end(), key, key + 6);
|
||||
}
|
||||
|
||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||
if (this->transceive_(tx, rx) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) ||
|
||||
(rx.get_message()[4] != nfc::STATUS_OK)) {
|
||||
ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num);
|
||||
ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes_to(buf, rx.get_message()));
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num);
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::sect_to_auth_(const uint8_t block_num) {
|
||||
const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START;
|
||||
if (block_num >= first_high_block) {
|
||||
return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) +
|
||||
nfc::MIFARE_CLASSIC_16BLOCK_SECT_START;
|
||||
}
|
||||
return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW;
|
||||
}
|
||||
|
||||
uint8_t PN7150::format_mifare_classic_mifare_() {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
auto status = nfc::STATUS_OK;
|
||||
|
||||
for (int block = 0; block < 64; block += 4) {
|
||||
if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
||||
continue;
|
||||
}
|
||||
if (block != 0) {
|
||||
if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 3);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
uint8_t PN7150::format_mifare_classic_ndef_() {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
|
||||
0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
|
||||
0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
|
||||
0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
|
||||
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
|
||||
0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Sector 0 formatted with NDEF");
|
||||
|
||||
auto status = nfc::STATUS_OK;
|
||||
|
||||
for (int block = 4; block < 64; block += 4) {
|
||||
if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (block == 4) {
|
||||
if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) !=
|
||||
nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
} else {
|
||||
if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
|
||||
|
||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||
if (this->transceive_(tx, rx) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
// write command part two
|
||||
tx.set_payload({XCHG_DATA_OID});
|
||||
tx.get_message().insert(tx.get_message().end(), data, data + len);
|
||||
|
||||
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) ||
|
||||
(rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) {
|
||||
ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num);
|
||||
ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes_to(buf, rx.get_message()));
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage> &message) {
|
||||
auto encoded = message->encode();
|
||||
|
||||
uint32_t message_length = encoded.size();
|
||||
uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length);
|
||||
|
||||
encoded.insert(encoded.begin(), 0x03);
|
||||
if (message_length < 255) {
|
||||
encoded.insert(encoded.begin() + 1, message_length);
|
||||
} else {
|
||||
encoded.insert(encoded.begin() + 1, 0xFF);
|
||||
encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF);
|
||||
encoded.insert(encoded.begin() + 3, message_length & 0xFF);
|
||||
}
|
||||
encoded.push_back(0xFE);
|
||||
|
||||
encoded.resize(buffer_length, 0);
|
||||
|
||||
uint32_t index = 0;
|
||||
uint8_t current_block = 4;
|
||||
|
||||
while (index < buffer_length) {
|
||||
if (nfc::mifare_classic_is_first_block(current_block)) {
|
||||
if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) !=
|
||||
nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
||||
current_block++;
|
||||
|
||||
if (nfc::mifare_classic_is_trailer_block(current_block)) {
|
||||
// Skipping as cannot write to trailer
|
||||
current_block++;
|
||||
}
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::halt_mifare_classic_tag_() {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0});
|
||||
|
||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
} // namespace esphome::pn7150
|
||||
@@ -24,10 +24,10 @@ uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
// semaphore to ensure transaction is complete before returning
|
||||
if (this->wait_for_irq_(pn7150::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) {
|
||||
ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear");
|
||||
return nfc::STATUS_FAILED;
|
||||
// IRQ normally drops at the end of the read. If another message is queued it rises again at once, and the short
|
||||
// low pulse may be missed; that means more data is waiting, not that this read failed (UM10936, 3.4).
|
||||
if (this->wait_for_irq_(pn71xx::NFCC_IRQ_CLEAR_TIMEOUT, false) != nfc::STATUS_OK) {
|
||||
ESP_LOGVV(TAG, "IRQ still active after read; another message is pending");
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
@@ -1,257 +1,37 @@
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import nfc
|
||||
from esphome.components import pn71xx
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_IRQ_PIN,
|
||||
CONF_MESSAGE,
|
||||
CONF_ON_FINISHED_WRITE,
|
||||
CONF_ON_TAG,
|
||||
CONF_ON_TAG_REMOVED,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["binary_sensor", "nfc"]
|
||||
AUTO_LOAD = ["pn71xx"]
|
||||
CODEOWNERS = ["@kbx81", "@jesserockz"]
|
||||
|
||||
CONF_DWL_REQ_PIN = "dwl_req_pin"
|
||||
CONF_EMULATION_MESSAGE = "emulation_message"
|
||||
CONF_EMULATION_OFF = "emulation_off"
|
||||
CONF_EMULATION_ON = "emulation_on"
|
||||
CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record"
|
||||
CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan"
|
||||
CONF_PN7160_ID = "pn7160_id"
|
||||
CONF_POLLING_OFF = "polling_off"
|
||||
CONF_POLLING_ON = "polling_on"
|
||||
CONF_SET_CLEAN_MODE = "set_clean_mode"
|
||||
CONF_SET_EMULATION_MESSAGE = "set_emulation_message"
|
||||
CONF_SET_FORMAT_MODE = "set_format_mode"
|
||||
CONF_SET_READ_MODE = "set_read_mode"
|
||||
CONF_SET_WRITE_MESSAGE = "set_write_message"
|
||||
CONF_SET_WRITE_MODE = "set_write_mode"
|
||||
CONF_TAG_TTL = "tag_ttl"
|
||||
CONF_VEN_PIN = "ven_pin"
|
||||
CONF_WKUP_REQ_PIN = "wkup_req_pin"
|
||||
|
||||
pn7160_ns = cg.esphome_ns.namespace("pn7160")
|
||||
PN7160 = pn7160_ns.class_("PN7160", nfc.Nfcc, cg.Component)
|
||||
PN7160 = pn7160_ns.class_("PN7160", pn71xx.PN71xx)
|
||||
|
||||
EmulationOffAction = pn7160_ns.class_("EmulationOffAction", automation.Action)
|
||||
EmulationOnAction = pn7160_ns.class_("EmulationOnAction", automation.Action)
|
||||
PollingOffAction = pn7160_ns.class_("PollingOffAction", automation.Action)
|
||||
PollingOnAction = pn7160_ns.class_("PollingOnAction", automation.Action)
|
||||
SetCleanModeAction = pn7160_ns.class_("SetCleanModeAction", automation.Action)
|
||||
SetEmulationMessageAction = pn7160_ns.class_(
|
||||
"SetEmulationMessageAction", automation.Action
|
||||
)
|
||||
SetFormatModeAction = pn7160_ns.class_("SetFormatModeAction", automation.Action)
|
||||
SetReadModeAction = pn7160_ns.class_("SetReadModeAction", automation.Action)
|
||||
SetWriteMessageAction = pn7160_ns.class_("SetWriteMessageAction", automation.Action)
|
||||
SetWriteModeAction = pn7160_ns.class_("SetWriteModeAction", automation.Action)
|
||||
|
||||
|
||||
PN7160IsWritingCondition = pn7160_ns.class_(
|
||||
"PN7160IsWritingCondition", automation.Condition
|
||||
)
|
||||
|
||||
|
||||
IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition)
|
||||
|
||||
|
||||
SIMPLE_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(PN7160),
|
||||
}
|
||||
)
|
||||
|
||||
SET_MESSAGE_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(PN7160),
|
||||
cv.Required(CONF_MESSAGE): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
PN7160_SCHEMA = cv.Schema(
|
||||
PN7160_SCHEMA = pn71xx.PN71XX_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(PN7160),
|
||||
cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TAG): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_DWL_REQ_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_WKUP_REQ_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_EMULATION_MESSAGE): cv.string,
|
||||
cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"tag.set_emulation_message",
|
||||
SetEmulationMessageAction,
|
||||
SET_MESSAGE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"tag.set_write_message",
|
||||
SetWriteMessageAction,
|
||||
SET_MESSAGE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def pn7160_set_message_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string)
|
||||
cg.add(var.set_message(template_))
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_
|
||||
)
|
||||
cg.add(var.set_include_android_app_record(template_))
|
||||
return var
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.emulation_off",
|
||||
EmulationOffAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.emulation_on",
|
||||
EmulationOnAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.polling_off",
|
||||
PollingOffAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.polling_on",
|
||||
PollingOnAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.set_clean_mode",
|
||||
SetCleanModeAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.set_format_mode",
|
||||
SetFormatModeAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.set_read_mode",
|
||||
SetReadModeAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_action(
|
||||
"tag.set_write_mode",
|
||||
SetWriteModeAction,
|
||||
SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
|
||||
),
|
||||
)
|
||||
pn71xx.register_is_writing_condition("pn7160.is_writing", PN7160)
|
||||
|
||||
|
||||
async def setup_pn7160(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await pn71xx.setup_pn71xx(var, config)
|
||||
|
||||
if dwl_req_pin_config := config.get(CONF_DWL_REQ_PIN):
|
||||
pin = await cg.gpio_pin_expression(dwl_req_pin_config)
|
||||
cg.add(var.set_dwl_req_pin(pin))
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
|
||||
cg.add(var.set_irq_pin(pin))
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN])
|
||||
cg.add(var.set_ven_pin(pin))
|
||||
|
||||
if wakeup_req_pin_config := config.get(CONF_WKUP_REQ_PIN):
|
||||
pin = await cg.gpio_pin_expression(wakeup_req_pin_config)
|
||||
if wkup_req_pin_config := config.get(CONF_WKUP_REQ_PIN):
|
||||
pin = await cg.gpio_pin_expression(wkup_req_pin_config)
|
||||
cg.add(var.set_wkup_req_pin(pin))
|
||||
|
||||
if emulation_message_config := config.get(CONF_EMULATION_MESSAGE):
|
||||
cg.add(var.set_tag_emulation_message(emulation_message_config))
|
||||
cg.add(var.set_tag_emulation_on())
|
||||
|
||||
if CONF_TAG_TTL in config:
|
||||
cg.add(var.set_tag_ttl(config[CONF_TAG_TTL]))
|
||||
|
||||
for conf in config.get(CONF_ON_TAG, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
cg.add(var.register_ontag_trigger(trigger))
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_TAG_REMOVED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
cg.add(var.register_ontagremoved_trigger(trigger))
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
automation.register_parented_condition(
|
||||
"pn7160.is_writing",
|
||||
PN7160IsWritingCondition,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(PN7160),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/pn7160/pn7160.h"
|
||||
|
||||
namespace esphome::pn7160 {
|
||||
|
||||
template<typename... Ts> class PN7160IsWritingCondition final : public Condition<Ts...>, public Parented<PN7160> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return this->parent_->is_writing(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EmulationOffAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
void play(const Ts &...x) override { this->parent_->set_tag_emulation_off(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EmulationOnAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
void play(const Ts &...x) override { this->parent_->set_tag_emulation_on(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PollingOffAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
void play(const Ts &...x) override { this->parent_->set_polling_off(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PollingOnAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
void play(const Ts &...x) override { this->parent_->set_polling_on(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetCleanModeAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
void play(const Ts &...x) override { this->parent_->clean_mode(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetFormatModeAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
void play(const Ts &...x) override { this->parent_->format_mode(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetReadModeAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
void play(const Ts &...x) override { this->parent_->read_mode(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetEmulationMessageAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
TEMPLATABLE_VALUE(std::string, message)
|
||||
TEMPLATABLE_VALUE(bool, include_android_app_record)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->parent_->set_tag_emulation_message(this->message_.optional_value(x...),
|
||||
this->include_android_app_record_.optional_value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetWriteMessageAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
TEMPLATABLE_VALUE(std::string, message)
|
||||
TEMPLATABLE_VALUE(bool, include_android_app_record)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->parent_->set_tag_write_message(this->message_.optional_value(x...),
|
||||
this->include_android_app_record_.optional_value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetWriteModeAction final : public Action<Ts...>, public Parented<PN7160> {
|
||||
void play(const Ts &...x) override { this->parent_->write_mode(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::pn7160
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/nfc/automation.h"
|
||||
#include "esphome/components/nfc/nci_core.h"
|
||||
#include "esphome/components/nfc/nci_message.h"
|
||||
#include "esphome/components/nfc/nfc.h"
|
||||
#include "esphome/components/nfc/nfc_helpers.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <functional>
|
||||
#include "esphome/components/pn71xx/pn71xx.h"
|
||||
|
||||
namespace esphome::pn7160 {
|
||||
|
||||
static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10;
|
||||
static constexpr uint16_t NFCC_INIT_TIMEOUT = 50;
|
||||
static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15;
|
||||
|
||||
static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3;
|
||||
static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10;
|
||||
|
||||
static constexpr uint8_t XCHG_DATA_OID = 0x10;
|
||||
static constexpr uint8_t MF_SECTORSEL_OID = 0x32;
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40;
|
||||
static constexpr uint8_t TEST_PRBS_OID = 0x30;
|
||||
static constexpr uint8_t TEST_ANTENNA_OID = 0x3D;
|
||||
static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33;
|
||||
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10;
|
||||
|
||||
static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76,
|
||||
0x00, 0x00, 0x85, 0x01, 0x01, 0x00};
|
||||
static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04,
|
||||
0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00};
|
||||
static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03};
|
||||
static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04};
|
||||
static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0};
|
||||
static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6};
|
||||
static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00};
|
||||
static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82};
|
||||
|
||||
static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields
|
||||
0x00, // config param identifier (TOTAL_DURATION)
|
||||
0x02, // length of value
|
||||
0x01, // TOTAL_DURATION (low)...
|
||||
0x00}; // TOTAL_DURATION (high): 1 ms
|
||||
|
||||
static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields
|
||||
0x00, // config param identifier (TOTAL_DURATION)
|
||||
0x02, // length of value
|
||||
0xF8, // TOTAL_DURATION (low)...
|
||||
0x02}; // TOTAL_DURATION (high): 760 ms
|
||||
|
||||
static constexpr uint8_t PMU_CFG[] = {
|
||||
0x01, // Number of parameters
|
||||
0xA0, 0x0E, // ext. tag
|
||||
@@ -73,34 +23,6 @@ static constexpr uint8_t PMU_CFG[] = {
|
||||
0x0C, // RFU
|
||||
};
|
||||
|
||||
static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes
|
||||
nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN,
|
||||
nfc::INTF_ISODEP, // poll & listen mode
|
||||
nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_TAGCMD}; // poll mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode
|
||||
|
||||
static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming)
|
||||
2, // number of table entries
|
||||
0x01, // type = protocol-based
|
||||
@@ -114,200 +36,23 @@ static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (an
|
||||
0x07, // power state
|
||||
nfc::TECH_PASSIVE_NFCA}; // technology
|
||||
|
||||
enum class CardEmulationState : uint8_t {
|
||||
CARD_EMU_IDLE,
|
||||
CARD_EMU_NDEF_APP_SELECTED,
|
||||
CARD_EMU_CC_SELECTED,
|
||||
CARD_EMU_NDEF_SELECTED,
|
||||
CARD_EMU_DESFIRE_PROD,
|
||||
};
|
||||
|
||||
enum class NCIState : uint8_t {
|
||||
NONE = 0x00,
|
||||
NFCC_RESET,
|
||||
NFCC_INIT,
|
||||
NFCC_CONFIG,
|
||||
NFCC_SET_DISCOVER_MAP,
|
||||
NFCC_SET_LISTEN_MODE_ROUTING,
|
||||
RFST_IDLE,
|
||||
RFST_DISCOVERY,
|
||||
RFST_W4_ALL_DISCOVERIES,
|
||||
RFST_W4_HOST_SELECT,
|
||||
RFST_LISTEN_ACTIVE,
|
||||
RFST_LISTEN_SLEEP,
|
||||
RFST_POLL_ACTIVE,
|
||||
EP_DEACTIVATING,
|
||||
EP_SELECTING,
|
||||
TEST = 0xFE,
|
||||
FAILED = 0xFF,
|
||||
};
|
||||
|
||||
enum class TestMode : uint8_t {
|
||||
TEST_NONE = 0x00,
|
||||
TEST_PRBS,
|
||||
TEST_ANTENNA,
|
||||
TEST_GET_REGISTER,
|
||||
};
|
||||
|
||||
struct DiscoveredEndpoint {
|
||||
uint8_t id;
|
||||
uint8_t protocol;
|
||||
uint32_t last_seen;
|
||||
std::unique_ptr<nfc::NfcTag> tag;
|
||||
bool trig_called;
|
||||
};
|
||||
|
||||
class PN7160 : public nfc::Nfcc, public Component {
|
||||
class PN7160 : public pn71xx::PN71xx {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; }
|
||||
void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; }
|
||||
void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; }
|
||||
void set_wkup_req_pin(GPIOPin *wkup_req_pin) { this->wkup_req_pin_ = wkup_req_pin; }
|
||||
|
||||
void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; }
|
||||
void set_tag_emulation_message(std::shared_ptr<nfc::NdefMessage> message);
|
||||
void set_tag_emulation_message(const optional<std::string> &message, optional<bool> include_android_app_record);
|
||||
void set_tag_emulation_message(const char *message, bool include_android_app_record = true);
|
||||
void set_tag_emulation_off();
|
||||
void set_tag_emulation_on();
|
||||
bool tag_emulation_enabled() { return this->listening_enabled_; }
|
||||
|
||||
void set_polling_off();
|
||||
void set_polling_on();
|
||||
bool polling_enabled() { return this->polling_enabled_; }
|
||||
|
||||
void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); }
|
||||
void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); }
|
||||
|
||||
template<typename F> void add_on_emulated_tag_scan_callback(F &&callback) {
|
||||
this->on_emulated_tag_scan_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_finished_write_callback(F &&callback) {
|
||||
this->on_finished_write_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
bool is_writing() { return this->next_task_ != EP_READ; };
|
||||
|
||||
void read_mode();
|
||||
void clean_mode();
|
||||
void format_mode();
|
||||
void write_mode();
|
||||
void set_tag_write_message(std::shared_ptr<nfc::NdefMessage> message);
|
||||
void set_tag_write_message(optional<std::string> message, optional<bool> include_android_app_record);
|
||||
|
||||
uint8_t set_test_mode(TestMode test_mode, const std::vector<uint8_t> &data, std::vector<uint8_t> &result);
|
||||
|
||||
protected:
|
||||
uint8_t reset_core_(bool reset_config, bool power);
|
||||
uint8_t init_core_();
|
||||
uint8_t send_init_config_();
|
||||
uint8_t send_core_config_();
|
||||
uint8_t refresh_core_config_();
|
||||
|
||||
uint8_t set_discover_map_();
|
||||
|
||||
uint8_t set_listen_mode_routing_();
|
||||
|
||||
uint8_t start_discovery_();
|
||||
uint8_t stop_discovery_();
|
||||
uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT);
|
||||
|
||||
void select_endpoint_();
|
||||
|
||||
uint8_t read_endpoint_data_(nfc::NfcTag &tag);
|
||||
uint8_t clean_endpoint_(nfc::NfcTagUid &uid);
|
||||
uint8_t format_endpoint_(nfc::NfcTagUid &uid);
|
||||
uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
|
||||
|
||||
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
|
||||
optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
|
||||
void purge_old_tags_();
|
||||
void erase_tag_(uint8_t tag_index);
|
||||
|
||||
/// advance controller state as required
|
||||
void nci_fsm_transition_();
|
||||
/// set new controller state
|
||||
void nci_fsm_set_state_(NCIState new_state);
|
||||
/// setting controller to this state caused an error; returns true if too many errors/failures
|
||||
bool nci_fsm_set_error_state_(NCIState new_state);
|
||||
/// parse & process incoming messages from the NFCC
|
||||
void process_message_();
|
||||
void process_rf_intf_activated_oid_(nfc::NciMessage &rx);
|
||||
void process_rf_discover_oid_(nfc::NciMessage &rx);
|
||||
void process_rf_deactivate_oid_(nfc::NciMessage &rx);
|
||||
void process_data_message_(nfc::NciMessage &rx);
|
||||
|
||||
void card_emu_t4t_get_response_(std::vector<uint8_t> &response, std::vector<uint8_t> &ndef_response);
|
||||
|
||||
uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT,
|
||||
bool expect_notification = true);
|
||||
virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0;
|
||||
virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0;
|
||||
|
||||
uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true);
|
||||
|
||||
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
||||
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||
uint8_t sect_to_auth_(uint8_t block_num);
|
||||
uint8_t format_mifare_classic_mifare_();
|
||||
uint8_t format_mifare_classic_ndef_();
|
||||
uint8_t write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
uint8_t halt_mifare_classic_tag_();
|
||||
|
||||
uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag);
|
||||
uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data);
|
||||
bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6);
|
||||
uint16_t read_mifare_ultralight_capacity_();
|
||||
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t &message_start_index);
|
||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
uint8_t clean_mifare_ultralight_();
|
||||
|
||||
enum NfcTask : uint8_t {
|
||||
EP_READ = 0,
|
||||
EP_CLEAN,
|
||||
EP_FORMAT,
|
||||
EP_WRITE,
|
||||
} next_task_{EP_READ};
|
||||
|
||||
bool config_refresh_pending_{false};
|
||||
bool core_config_is_solo_{false};
|
||||
bool listening_enabled_{false};
|
||||
bool polling_enabled_{true};
|
||||
|
||||
uint8_t error_count_{0};
|
||||
uint8_t fail_count_{0};
|
||||
uint32_t last_nci_state_change_{0};
|
||||
uint8_t selecting_endpoint_{0};
|
||||
uint32_t tag_ttl_{250};
|
||||
void prepare_reset() override;
|
||||
uint8_t verify_reset(nfc::NciMessage &rx, bool reset_config) override;
|
||||
uint8_t process_init_response(nfc::NciMessage &rx) override;
|
||||
std::span<const uint8_t> pmu_config() const override { return PMU_CFG; }
|
||||
std::span<const uint8_t> listen_mode_routing_config() const override { return RF_LISTEN_MODE_ROUTING_CONFIG; }
|
||||
|
||||
GPIOPin *dwl_req_pin_{nullptr};
|
||||
GPIOPin *irq_pin_{nullptr};
|
||||
GPIOPin *ven_pin_{nullptr};
|
||||
GPIOPin *wkup_req_pin_{nullptr};
|
||||
|
||||
CallbackManager<void()> on_emulated_tag_scan_callback_;
|
||||
CallbackManager<void()> on_finished_write_callback_;
|
||||
|
||||
std::vector<DiscoveredEndpoint> discovered_endpoint_;
|
||||
|
||||
CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE};
|
||||
NCIState nci_state_{NCIState::NFCC_RESET};
|
||||
NCIState nci_state_error_{NCIState::NONE};
|
||||
|
||||
std::shared_ptr<nfc::NdefMessage> card_emulation_message_;
|
||||
std::shared_ptr<nfc::NdefMessage> next_task_message_to_write_;
|
||||
|
||||
std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
|
||||
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
|
||||
};
|
||||
|
||||
} // namespace esphome::pn7160
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
|
||||
#include "pn7160.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pn7160 {
|
||||
|
||||
static const char *const TAG = "pn7160.mifare_ultralight";
|
||||
|
||||
uint8_t PN7160::read_mifare_ultralight_tag_(nfc::NfcTag &tag) {
|
||||
std::vector<uint8_t> data;
|
||||
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all
|
||||
if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
|
||||
data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
if (!this->is_mifare_ultralight_formatted_(data)) {
|
||||
ESP_LOGW(TAG, "Not NDEF formatted");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
uint8_t message_length;
|
||||
uint8_t message_start_index;
|
||||
if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) {
|
||||
ESP_LOGW(TAG, "Couldn't find NDEF message");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index);
|
||||
|
||||
if (message_length == 0) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
// we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages
|
||||
const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0;
|
||||
if (read_length) {
|
||||
if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) !=
|
||||
nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Error reading tag data");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
// we need to trim off page 3 as well as any bytes ahead of message_start_index
|
||||
data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE);
|
||||
|
||||
tag.set_ndef_message(make_unique<nfc::NdefMessage>(data));
|
||||
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data) {
|
||||
const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page});
|
||||
|
||||
for (size_t i = 0; i * read_increment < num_bytes; i++) {
|
||||
tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page;
|
||||
do { // loop because sometimes we struggle here...???...
|
||||
if (this->transceive_(tx, rx) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Error reading tag data");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
} while (rx.get_payload_size() < read_increment);
|
||||
uint16_t bytes_offset = (i + 1) * read_increment;
|
||||
auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1
|
||||
: rx.get_message().end() - (bytes_offset - num_bytes + 1);
|
||||
|
||||
if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) {
|
||||
data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr);
|
||||
}
|
||||
}
|
||||
|
||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes_to(buf, data));
|
||||
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
bool PN7160::is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6) {
|
||||
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
|
||||
|
||||
return (page_3_to_6.size() > p4_offset + 3) &&
|
||||
((page_3_to_6[p4_offset + 0] != 0xFF) || (page_3_to_6[p4_offset + 1] != 0xFF) ||
|
||||
(page_3_to_6[p4_offset + 2] != 0xFF) || (page_3_to_6[p4_offset + 3] != 0xFF));
|
||||
}
|
||||
|
||||
uint16_t PN7160::read_mifare_ultralight_capacity_() {
|
||||
std::vector<uint8_t> data;
|
||||
if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) {
|
||||
ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U);
|
||||
return data[2] * 8U;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t &message_start_index) {
|
||||
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
|
||||
|
||||
if (!(page_3_to_6.size() > p4_offset + 6)) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
if (page_3_to_6[p4_offset + 0] == 0x03) {
|
||||
message_length = page_3_to_6[p4_offset + 1];
|
||||
message_start_index = 2;
|
||||
return nfc::STATUS_OK;
|
||||
} else if (page_3_to_6[p4_offset + 5] == 0x03) {
|
||||
message_length = page_3_to_6[p4_offset + 6];
|
||||
message_start_index = 7;
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
|
||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||
|
||||
auto encoded = message->encode();
|
||||
|
||||
uint32_t message_length = encoded.size();
|
||||
uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length);
|
||||
|
||||
if (buffer_length > capacity) {
|
||||
ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity);
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
encoded.insert(encoded.begin(), 0x03);
|
||||
if (message_length < 255) {
|
||||
encoded.insert(encoded.begin() + 1, message_length);
|
||||
} else {
|
||||
encoded.insert(encoded.begin() + 1, 0xFF);
|
||||
encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF);
|
||||
encoded.insert(encoded.begin() + 3, message_length & 0xFF);
|
||||
}
|
||||
encoded.push_back(0xFE);
|
||||
|
||||
encoded.resize(buffer_length, 0);
|
||||
|
||||
uint32_t index = 0;
|
||||
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
while (index < buffer_length) {
|
||||
if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) !=
|
||||
nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||
current_page++;
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::clean_mifare_ultralight_() {
|
||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
||||
if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
|
||||
payload.insert(payload.end(), write_data, write_data + len);
|
||||
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);
|
||||
|
||||
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Error writing page %u", page_num);
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
} // namespace esphome::pn7160
|
||||
@@ -24,10 +24,10 @@ uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
// semaphore to ensure transaction is complete before returning
|
||||
if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) {
|
||||
ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear");
|
||||
return nfc::STATUS_FAILED;
|
||||
// IRQ normally drops at the end of the read. If another message is queued it rises again at once, and the short
|
||||
// low pulse may be missed; that means more data is waiting, not that this read failed (UM11495, 6.2.4).
|
||||
if (this->wait_for_irq_(pn71xx::NFCC_IRQ_CLEAR_TIMEOUT, false) != nfc::STATUS_OK) {
|
||||
ESP_LOGVV(TAG, "IRQ still active after read; another message is pending");
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ static const char *const TAG = "pn7160_spi";
|
||||
|
||||
void PN7160Spi::setup() {
|
||||
this->spi_setup();
|
||||
this->cs_->digital_write(false);
|
||||
PN7160::setup();
|
||||
}
|
||||
|
||||
@@ -28,20 +27,28 @@ uint8_t PN7160Spi::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) {
|
||||
this->read_array(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length);
|
||||
}
|
||||
this->disable();
|
||||
// semaphore to ensure transaction is complete before returning
|
||||
if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) {
|
||||
ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear");
|
||||
return nfc::STATUS_FAILED;
|
||||
// IRQ normally drops at the end of the read. If another message is queued it rises again at once, and the short
|
||||
// low pulse may be missed; that means more data is waiting, not that this read failed (UM11495, 6.3.4).
|
||||
if (this->wait_for_irq_(pn71xx::NFCC_IRQ_CLEAR_TIMEOUT, false) != nfc::STATUS_OK) {
|
||||
ESP_LOGVV(TAG, "IRQ still active after read; another message is pending");
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160Spi::write_nfcc(nfc::NciMessage &tx) {
|
||||
auto encoded = tx.encode();
|
||||
this->enable();
|
||||
this->write_byte(TDD_SPI_WRITE); // send "transfer direction detector"
|
||||
this->write_array(tx.encode().data(), tx.encode().size());
|
||||
// send "transfer direction detector"; the NFCC answers 0xFF when it is ready to receive (UM11495, 6.3.3)
|
||||
const uint8_t status = this->transfer_byte(TDD_SPI_WRITE);
|
||||
const bool ready = status == 0xFF;
|
||||
if (ready) {
|
||||
this->write_array(encoded.data(), encoded.size());
|
||||
}
|
||||
this->disable();
|
||||
return nfc::STATUS_OK;
|
||||
if (!ready) {
|
||||
ESP_LOGV(TAG, "NFCC not ready for write (0x%02X)", status);
|
||||
}
|
||||
return ready ? nfc::STATUS_OK : nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
void PN7160Spi::dump_config() {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import nfc
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_IRQ_PIN,
|
||||
CONF_MESSAGE,
|
||||
CONF_ON_FINISHED_WRITE,
|
||||
CONF_ON_TAG,
|
||||
CONF_ON_TAG_REMOVED,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["binary_sensor", "nfc"]
|
||||
CODEOWNERS = ["@kbx81", "@jesserockz"]
|
||||
|
||||
CONF_EMULATION_MESSAGE = "emulation_message"
|
||||
CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record"
|
||||
CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan"
|
||||
CONF_TAG_TTL = "tag_ttl"
|
||||
CONF_VEN_PIN = "ven_pin"
|
||||
|
||||
pn71xx_ns = cg.esphome_ns.namespace("pn71xx")
|
||||
PN71xx = pn71xx_ns.class_("PN71xx", nfc.Nfcc, cg.Component)
|
||||
|
||||
SIMPLE_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(PN71xx),
|
||||
}
|
||||
)
|
||||
|
||||
SET_MESSAGE_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(PN71xx),
|
||||
cv.Required(CONF_MESSAGE): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
PN71XX_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TAG): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
|
||||
}
|
||||
),
|
||||
cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_EMULATION_MESSAGE): cv.string,
|
||||
cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
for _name, _method in (
|
||||
("tag.set_emulation_message", "set_tag_emulation_message"),
|
||||
("tag.set_write_message", "set_tag_write_message"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name,
|
||||
SET_MESSAGE_ACTION_SCHEMA,
|
||||
automation.ApplyCall(
|
||||
f"{_method}({{}}, {{}})",
|
||||
(
|
||||
(CONF_MESSAGE, cg.std_string),
|
||||
(CONF_INCLUDE_ANDROID_APP_RECORD, cg.bool_),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
for _name, _call in (
|
||||
("tag.emulation_off", "set_tag_emulation_off()"),
|
||||
("tag.emulation_on", "set_tag_emulation_on()"),
|
||||
("tag.polling_off", "set_polling_off()"),
|
||||
("tag.polling_on", "set_polling_on()"),
|
||||
("tag.set_clean_mode", "clean_mode()"),
|
||||
("tag.set_format_mode", "format_mode()"),
|
||||
("tag.set_read_mode", "read_mode()"),
|
||||
("tag.set_write_mode", "write_mode()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, SIMPLE_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
|
||||
|
||||
def register_is_writing_condition(name: str, chip_class: MockObj) -> None:
|
||||
"""Register the chip-specific ``<chip>.is_writing`` condition."""
|
||||
automation.register_apply_condition(
|
||||
name,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(chip_class),
|
||||
}
|
||||
),
|
||||
"is_writing()",
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def setup_pn71xx(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
|
||||
cg.add(var.set_irq_pin(pin))
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN])
|
||||
cg.add(var.set_ven_pin(pin))
|
||||
|
||||
if emulation_message_config := config.get(CONF_EMULATION_MESSAGE):
|
||||
cg.add(var.set_tag_emulation_message(emulation_message_config))
|
||||
cg.add(var.set_tag_emulation_on())
|
||||
|
||||
if (tag_ttl := config.get(CONF_TAG_TTL)) is not None:
|
||||
cg.add(var.set_tag_ttl(tag_ttl))
|
||||
|
||||
for conf in config.get(CONF_ON_TAG, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
cg.add(var.register_ontag_trigger(trigger))
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_TAG_REMOVED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
cg.add(var.register_ontagremoved_trigger(trigger))
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,306 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/nfc/automation.h"
|
||||
#include "esphome/components/nfc/nci_core.h"
|
||||
#include "esphome/components/nfc/nci_message.h"
|
||||
#include "esphome/components/nfc/nfc.h"
|
||||
#include "esphome/components/nfc/nfc_helpers.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <functional>
|
||||
#include <span>
|
||||
|
||||
namespace esphome::pn71xx {
|
||||
|
||||
// Time to wait for the NFCC to answer. NXP's reference stack waits 1 s for a response; 10 ms was short enough
|
||||
// that a slow RF_DEACTIVATE_RSP, for example when a tag leaves the field, caused a full NFCC reset.
|
||||
static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 100;
|
||||
static constexpr uint16_t NFCC_INIT_TIMEOUT = 50;
|
||||
static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 100;
|
||||
// Time to wait for IRQ to drop after a read; it drops within microseconds unless another message is queued
|
||||
static constexpr uint16_t NFCC_IRQ_CLEAR_TIMEOUT = 5;
|
||||
// Length of the VEN and DWL_REQ pulses when resetting the NFCC
|
||||
static constexpr uint16_t NFCC_RESET_DELAY = 10;
|
||||
// Time to wait before resending a frame the NFCC refused, e.g. while waking from standby
|
||||
static constexpr uint16_t NFCC_WRITE_RETRY_DELAY = 5;
|
||||
// Longest time the FSM may wait for a notification that ends a transitional state before resetting the NFCC
|
||||
static constexpr uint32_t NFCC_STATE_TIMEOUT = 1000;
|
||||
|
||||
static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3;
|
||||
static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10;
|
||||
|
||||
static constexpr uint8_t XCHG_DATA_OID = 0x10;
|
||||
static constexpr uint8_t MF_SECTORSEL_OID = 0x32;
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40;
|
||||
static constexpr uint8_t TEST_PRBS_OID = 0x30;
|
||||
static constexpr uint8_t TEST_ANTENNA_OID = 0x3D;
|
||||
static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33;
|
||||
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B
|
||||
static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10;
|
||||
|
||||
static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76,
|
||||
0x00, 0x00, 0x85, 0x01, 0x01, 0x00};
|
||||
// MLe is 0xFD so a full-length READ BINARY response plus the two status bytes fits in one NCI data packet
|
||||
static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFD, 0x00, 0xFF, 0x04,
|
||||
0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00};
|
||||
// Largest NDEF message that fits in the emulated NDEF file (max file size in the CC, less the 2-byte length)
|
||||
static constexpr uint16_t CARD_EMU_T4T_MAX_NDEF_SIZE = 0xFF - 2;
|
||||
static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03};
|
||||
static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04};
|
||||
static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0};
|
||||
static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6};
|
||||
static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00};
|
||||
static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82};
|
||||
|
||||
static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields
|
||||
0x00, // config param identifier (TOTAL_DURATION)
|
||||
0x02, // length of value
|
||||
0x01, // TOTAL_DURATION (low)...
|
||||
0x00}; // TOTAL_DURATION (high): 1 ms
|
||||
|
||||
static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields
|
||||
0x00, // config param identifier (TOTAL_DURATION)
|
||||
0x02, // length of value
|
||||
0xF8, // TOTAL_DURATION (low)...
|
||||
0x02}; // TOTAL_DURATION (high): 760 ms
|
||||
|
||||
static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes
|
||||
nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_FRAME, // poll mode
|
||||
nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN,
|
||||
nfc::INTF_ISODEP, // poll & listen mode
|
||||
nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL,
|
||||
nfc::INTF_TAGCMD}; // poll mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode
|
||||
|
||||
static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode
|
||||
nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode
|
||||
nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode
|
||||
|
||||
enum class CardEmulationState : uint8_t {
|
||||
CARD_EMU_IDLE,
|
||||
CARD_EMU_NDEF_APP_SELECTED,
|
||||
CARD_EMU_CC_SELECTED,
|
||||
CARD_EMU_NDEF_SELECTED,
|
||||
CARD_EMU_DESFIRE_PROD,
|
||||
};
|
||||
|
||||
enum class NCIState : uint8_t {
|
||||
NONE = 0x00,
|
||||
NFCC_RESET,
|
||||
NFCC_INIT,
|
||||
NFCC_CONFIG,
|
||||
NFCC_SET_DISCOVER_MAP,
|
||||
NFCC_SET_LISTEN_MODE_ROUTING,
|
||||
RFST_IDLE,
|
||||
RFST_DISCOVERY,
|
||||
RFST_W4_ALL_DISCOVERIES,
|
||||
RFST_W4_HOST_SELECT,
|
||||
RFST_LISTEN_ACTIVE,
|
||||
RFST_LISTEN_SLEEP,
|
||||
RFST_POLL_ACTIVE,
|
||||
EP_DEACTIVATING,
|
||||
EP_SELECTING,
|
||||
TEST = 0xFE,
|
||||
FAILED = 0xFF,
|
||||
};
|
||||
|
||||
enum class TestMode : uint8_t {
|
||||
TEST_NONE = 0x00,
|
||||
TEST_PRBS,
|
||||
TEST_ANTENNA,
|
||||
TEST_GET_REGISTER,
|
||||
};
|
||||
|
||||
struct DiscoveredEndpoint {
|
||||
uint32_t last_seen;
|
||||
std::unique_ptr<nfc::NfcTag> tag;
|
||||
uint8_t id;
|
||||
uint8_t protocol;
|
||||
bool trig_called;
|
||||
};
|
||||
|
||||
/// Common driver for the NXP PN71xx family of NCI NFC controllers. The chip classes (PN7150, PN7160) supply the parts
|
||||
/// that differ between chips; the bus classes supply read_nfcc() and write_nfcc().
|
||||
class PN71xx : public nfc::Nfcc, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; }
|
||||
void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; }
|
||||
|
||||
void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; }
|
||||
void set_tag_emulation_message(const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
void set_tag_emulation_message(const std::string &message, bool include_android_app_record = true);
|
||||
void set_tag_emulation_message(const char *message, bool include_android_app_record = true);
|
||||
void set_tag_emulation_off();
|
||||
void set_tag_emulation_on();
|
||||
bool tag_emulation_enabled() { return this->listening_enabled_; }
|
||||
|
||||
void set_polling_off();
|
||||
void set_polling_on();
|
||||
bool polling_enabled() { return this->polling_enabled_; }
|
||||
|
||||
void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); }
|
||||
void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); }
|
||||
|
||||
template<typename F> void add_on_emulated_tag_scan_callback(F &&callback) {
|
||||
this->on_emulated_tag_scan_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_finished_write_callback(F &&callback) {
|
||||
this->on_finished_write_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
bool is_writing() { return this->next_task_ != EP_READ; };
|
||||
|
||||
void read_mode();
|
||||
void clean_mode();
|
||||
void format_mode();
|
||||
void write_mode();
|
||||
void set_tag_write_message(std::shared_ptr<nfc::NdefMessage> message);
|
||||
void set_tag_write_message(const std::string &message, bool include_android_app_record = true);
|
||||
|
||||
uint8_t set_test_mode(TestMode test_mode, const std::vector<uint8_t> &data, std::vector<uint8_t> &result);
|
||||
|
||||
protected:
|
||||
uint8_t reset_core_(bool reset_config, bool power);
|
||||
uint8_t init_core_();
|
||||
|
||||
/// Chip hooks
|
||||
/// Called before the NFCC is reset, e.g. to make sure it will not start in firmware download mode
|
||||
virtual void prepare_reset() {}
|
||||
/// Validates what follows a successful CORE_RESET_RSP (in `rx`); may read more messages from the NFCC
|
||||
virtual uint8_t verify_reset(nfc::NciMessage &rx, bool reset_config) = 0;
|
||||
/// Logs chip information from a successful CORE_INIT_RSP
|
||||
virtual uint8_t process_init_response(nfc::NciMessage &rx) = 0;
|
||||
/// Parameters for the CORE_SET_CONFIG_CMD that configures the power management unit
|
||||
virtual std::span<const uint8_t> pmu_config() const = 0;
|
||||
/// Payload of the RF_SET_LISTEN_MODE_ROUTING_CMD
|
||||
virtual std::span<const uint8_t> listen_mode_routing_config() const = 0;
|
||||
|
||||
uint8_t send_init_config_();
|
||||
uint8_t send_core_config_();
|
||||
uint8_t refresh_core_config_();
|
||||
|
||||
uint8_t set_discover_map_();
|
||||
|
||||
uint8_t set_listen_mode_routing_();
|
||||
|
||||
uint8_t start_discovery_();
|
||||
uint8_t stop_discovery_();
|
||||
uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT);
|
||||
|
||||
void select_endpoint_();
|
||||
|
||||
uint8_t read_endpoint_data_(uint8_t protocol, nfc::NfcTag &tag);
|
||||
uint8_t clean_endpoint_(uint8_t protocol);
|
||||
uint8_t format_endpoint_(uint8_t protocol);
|
||||
uint8_t write_endpoint_(uint8_t protocol, nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
|
||||
|
||||
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, uint8_t protocol, const std::vector<uint8_t> &data);
|
||||
optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
|
||||
void purge_old_tags_();
|
||||
void erase_tag_(uint8_t tag_index);
|
||||
|
||||
/// advance controller state as required
|
||||
void nci_fsm_transition_();
|
||||
/// set new controller state
|
||||
void nci_fsm_set_state_(NCIState new_state);
|
||||
/// setting controller to this state caused an error; returns true if too many errors/failures
|
||||
bool nci_fsm_set_error_state_(NCIState new_state);
|
||||
/// parse & process incoming messages from the NFCC
|
||||
void process_message_();
|
||||
void process_rf_intf_activated_oid_(nfc::NciMessage &rx);
|
||||
void process_rf_discover_oid_(nfc::NciMessage &rx);
|
||||
void process_rf_deactivate_oid_(nfc::NciMessage &rx);
|
||||
void process_data_message_(nfc::NciMessage &rx);
|
||||
|
||||
void card_emu_t4t_get_response_(const std::vector<uint8_t> &response, std::vector<uint8_t> &ndef_response);
|
||||
bool card_emu_t4t_read_ndef_(uint16_t offset, uint8_t length, std::vector<uint8_t> &ndef_response);
|
||||
|
||||
uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT,
|
||||
bool expect_notification = true);
|
||||
virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0;
|
||||
virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0;
|
||||
|
||||
uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true);
|
||||
|
||||
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
||||
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||
uint8_t sect_to_auth_(uint8_t block_num);
|
||||
uint8_t format_mifare_classic_mifare_();
|
||||
uint8_t format_mifare_classic_ndef_();
|
||||
uint8_t write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
uint8_t halt_mifare_classic_tag_();
|
||||
|
||||
uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag);
|
||||
uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data);
|
||||
bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6);
|
||||
uint16_t read_mifare_ultralight_capacity_();
|
||||
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t &message_start_index);
|
||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
uint8_t clean_mifare_ultralight_();
|
||||
|
||||
enum NfcTask : uint8_t {
|
||||
EP_READ = 0,
|
||||
EP_CLEAN,
|
||||
EP_FORMAT,
|
||||
EP_WRITE,
|
||||
};
|
||||
|
||||
// members are ordered by alignment, widest first, to minimize padding
|
||||
CallbackManager<void()> on_emulated_tag_scan_callback_;
|
||||
CallbackManager<void()> on_finished_write_callback_;
|
||||
|
||||
std::vector<DiscoveredEndpoint> discovered_endpoint_;
|
||||
std::vector<uint8_t> card_emulation_ndef_; // encoded emulation message; empty when none is set
|
||||
std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
|
||||
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
|
||||
std::shared_ptr<nfc::NdefMessage> next_task_message_to_write_;
|
||||
|
||||
GPIOPin *irq_pin_{nullptr};
|
||||
GPIOPin *ven_pin_{nullptr};
|
||||
|
||||
uint32_t last_nci_state_change_{0};
|
||||
uint32_t tag_ttl_{250};
|
||||
|
||||
NfcTask next_task_{EP_READ};
|
||||
CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE};
|
||||
NCIState nci_state_{NCIState::NFCC_RESET};
|
||||
NCIState nci_state_error_{NCIState::NONE};
|
||||
uint8_t error_count_{0};
|
||||
uint8_t selecting_endpoint_{0};
|
||||
|
||||
bool config_refresh_pending_{false};
|
||||
bool core_config_is_solo_{false};
|
||||
bool listening_enabled_{false};
|
||||
bool polling_enabled_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::pn71xx
|
||||
+13
-13
@@ -1,14 +1,14 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "pn7160.h"
|
||||
#include "pn71xx.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pn7160 {
|
||||
namespace esphome::pn71xx {
|
||||
|
||||
static const char *const TAG = "pn7160.mifare_classic";
|
||||
static const char *const TAG = "pn71xx.mifare_classic";
|
||||
|
||||
uint8_t PN7160::read_mifare_classic_tag_(nfc::NfcTag &tag) {
|
||||
uint8_t PN71xx::read_mifare_classic_tag_(nfc::NfcTag &tag) {
|
||||
uint8_t current_block = 4;
|
||||
uint8_t message_start_index = 0;
|
||||
uint32_t message_length = 0;
|
||||
@@ -66,7 +66,7 @@ uint8_t PN7160::read_mifare_classic_tag_(nfc::NfcTag &tag) {
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data) {
|
||||
uint8_t PN71xx::read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data) {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num});
|
||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
@@ -90,7 +90,7 @@ uint8_t PN7160::read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) {
|
||||
uint8_t PN71xx::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num});
|
||||
|
||||
@@ -129,7 +129,7 @@ uint8_t PN7160::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, c
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::sect_to_auth_(const uint8_t block_num) {
|
||||
uint8_t PN71xx::sect_to_auth_(const uint8_t block_num) {
|
||||
const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START;
|
||||
if (block_num >= first_high_block) {
|
||||
return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) +
|
||||
@@ -138,7 +138,7 @@ uint8_t PN7160::sect_to_auth_(const uint8_t block_num) {
|
||||
return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW;
|
||||
}
|
||||
|
||||
uint8_t PN7160::format_mifare_classic_mifare_() {
|
||||
uint8_t PN71xx::format_mifare_classic_mifare_() {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
|
||||
@@ -173,7 +173,7 @@ uint8_t PN7160::format_mifare_classic_mifare_() {
|
||||
return status;
|
||||
}
|
||||
|
||||
uint8_t PN7160::format_mifare_classic_ndef_() {
|
||||
uint8_t PN71xx::format_mifare_classic_ndef_() {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
|
||||
0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
|
||||
@@ -237,7 +237,7 @@ uint8_t PN7160::format_mifare_classic_ndef_() {
|
||||
return status;
|
||||
}
|
||||
|
||||
uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||
uint8_t PN71xx::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
|
||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
@@ -267,7 +267,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *da
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage> &message) {
|
||||
uint8_t PN71xx::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage> &message) {
|
||||
auto encoded = message->encode();
|
||||
|
||||
uint32_t message_length = encoded.size();
|
||||
@@ -310,7 +310,7 @@ uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::halt_mifare_classic_tag_() {
|
||||
uint8_t PN71xx::halt_mifare_classic_tag_() {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0});
|
||||
|
||||
@@ -323,4 +323,4 @@ uint8_t PN7160::halt_mifare_classic_tag_() {
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
} // namespace esphome::pn7160
|
||||
} // namespace esphome::pn71xx
|
||||
+19
-13
@@ -2,14 +2,14 @@
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
|
||||
#include "pn7150.h"
|
||||
#include "pn71xx.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pn7150 {
|
||||
namespace esphome::pn71xx {
|
||||
|
||||
static const char *const TAG = "pn7150.mifare_ultralight";
|
||||
static const char *const TAG = "pn71xx.mifare_ultralight";
|
||||
|
||||
uint8_t PN7150::read_mifare_ultralight_tag_(nfc::NfcTag &tag) {
|
||||
uint8_t PN71xx::read_mifare_ultralight_tag_(nfc::NfcTag &tag) {
|
||||
std::vector<uint8_t> data;
|
||||
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all
|
||||
if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
|
||||
@@ -50,18 +50,24 @@ uint8_t PN7150::read_mifare_ultralight_tag_(nfc::NfcTag &tag) {
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data) {
|
||||
uint8_t PN71xx::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data) {
|
||||
const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page});
|
||||
|
||||
for (size_t i = 0; i * read_increment < num_bytes; i++) {
|
||||
tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page;
|
||||
do { // loop because sometimes we struggle here...???...
|
||||
// a short answer (e.g. a NAK for a page beyond the end of the tag) is retried a limited number of times
|
||||
uint8_t attempts = 0;
|
||||
do {
|
||||
if (this->transceive_(tx, rx) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Error reading tag data");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (++attempts > NFCC_MAX_COMM_FAILS && rx.get_payload_size() < read_increment) {
|
||||
ESP_LOGE(TAG, "Short read from page %u", tx.get_message().back());
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
} while (rx.get_payload_size() < read_increment);
|
||||
uint16_t bytes_offset = (i + 1) * read_increment;
|
||||
auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1
|
||||
@@ -78,7 +84,7 @@ uint8_t PN7150::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_b
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
bool PN7150::is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6) {
|
||||
bool PN71xx::is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6) {
|
||||
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
|
||||
|
||||
return (page_3_to_6.size() > p4_offset + 3) &&
|
||||
@@ -86,7 +92,7 @@ bool PN7150::is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_
|
||||
(page_3_to_6[p4_offset + 2] != 0xFF) || (page_3_to_6[p4_offset + 3] != 0xFF));
|
||||
}
|
||||
|
||||
uint16_t PN7150::read_mifare_ultralight_capacity_() {
|
||||
uint16_t PN71xx::read_mifare_ultralight_capacity_() {
|
||||
std::vector<uint8_t> data;
|
||||
if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) {
|
||||
ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U);
|
||||
@@ -95,7 +101,7 @@ uint16_t PN7150::read_mifare_ultralight_capacity_() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t PN71xx::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t &message_start_index) {
|
||||
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
|
||||
|
||||
@@ -115,7 +121,7 @@ uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
|
||||
uint8_t PN71xx::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
|
||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||
|
||||
auto encoded = message->encode();
|
||||
@@ -154,7 +160,7 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::clean_mifare_ultralight_() {
|
||||
uint8_t PN71xx::clean_mifare_ultralight_() {
|
||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
@@ -168,7 +174,7 @@ uint8_t PN7150::clean_mifare_ultralight_() {
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||
uint8_t PN71xx::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
|
||||
payload.insert(payload.end(), write_data, write_data + len);
|
||||
|
||||
@@ -182,4 +188,4 @@ uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *w
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
} // namespace esphome::pn7150
|
||||
} // namespace esphome::pn71xx
|
||||
@@ -21,20 +21,42 @@ CODEOWNERS = ["@clydebarrow"]
|
||||
DOMAIN = "snapshot"
|
||||
|
||||
CONF_FILENAME = "filename"
|
||||
CONF_FRAMES = "frames"
|
||||
CONF_FRAME_RATE = "frame_rate"
|
||||
|
||||
snapshot_ns = cg.esphome_ns.namespace("snapshot")
|
||||
Snapshot = snapshot_ns.class_("Snapshot")
|
||||
|
||||
|
||||
def _default_animation(config: ConfigType) -> ConfigType:
|
||||
"""Without frames the action takes a single picture, which the C++ side reads as zero frames."""
|
||||
return {CONF_FRAMES: 0, CONF_FRAME_RATE: 0.0, **config}
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"snapshot.take",
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Snapshot),
|
||||
cv.Optional(CONF_FILENAME, default=""): cv.templatable(cv.string),
|
||||
}
|
||||
cv.All(
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Snapshot),
|
||||
cv.Optional(CONF_FILENAME, default=""): cv.templatable(cv.string),
|
||||
# Asking for frames makes a GIF instead of a single BMP picture.
|
||||
cv.Inclusive(CONF_FRAMES, "animation"): cv.positive_not_null_int,
|
||||
cv.Inclusive(CONF_FRAME_RATE, "animation"): cv.All(
|
||||
cv.framerate, cv.Range(min=0.1, max=50)
|
||||
),
|
||||
}
|
||||
),
|
||||
_default_animation,
|
||||
),
|
||||
automation.ApplyCall(
|
||||
"take_snapshot_or_log({}, {}, {})",
|
||||
(
|
||||
(CONF_FILENAME, cg.std_string),
|
||||
(CONF_FRAMES, cg.uint32),
|
||||
(CONF_FRAME_RATE, cg.float_),
|
||||
),
|
||||
),
|
||||
automation.ApplyField(CONF_FILENAME, "take_snapshot_or_log", cg.std_string),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
#ifdef USE_HOST
|
||||
#include "gif.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MAX_PALETTE_SIZE = 256;
|
||||
// GIF needs at least two bits for each colour, even when the picture has fewer than four colours.
|
||||
constexpr unsigned MIN_PALETTE_BITS = 2;
|
||||
constexpr unsigned MAX_CODE_BITS = 12;
|
||||
constexpr uint32_t MAX_CODES = 1u << MAX_CODE_BITS;
|
||||
// Twice the number of codes there can be, so a lookup rarely has to look at more than one entry.
|
||||
constexpr size_t DICTIONARY_SIZE = 2 * MAX_CODES;
|
||||
constexpr uint32_t EMPTY_ENTRY = 0xFFFFFFFF;
|
||||
constexpr size_t MAX_SUB_BLOCK_SIZE = 255;
|
||||
// Colours are grouped by their top 5, 6 and 5 bits, which is all an RGB565 display can show.
|
||||
constexpr size_t COLOR_KEY_COUNT = 1u << 16;
|
||||
|
||||
constexpr uint16_t color_key(uint8_t red, uint8_t green, uint8_t blue) {
|
||||
return static_cast<uint16_t>(((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3));
|
||||
}
|
||||
|
||||
/// One of the three parts of a colour key: 0 for red, 1 for green, 2 for blue. The value is spread
|
||||
/// over 0 to 255 whichever part it is, so the parts can be compared with each other.
|
||||
constexpr unsigned key_channel(uint16_t key, unsigned channel) {
|
||||
switch (channel) {
|
||||
case 0:
|
||||
return (key >> 11) << 3;
|
||||
case 1:
|
||||
return ((key >> 5) & 0x3F) << 2;
|
||||
default:
|
||||
return (key & 0x1F) << 3;
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a value in two bytes, least significant first.
|
||||
void put_le16(std::vector<uint8_t> &out, unsigned value) {
|
||||
out.push_back(static_cast<uint8_t>(value & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>(value >> 8));
|
||||
}
|
||||
|
||||
/// Packs codes of varying length into bytes, least significant bit first.
|
||||
struct BitWriter {
|
||||
std::vector<uint8_t> &out;
|
||||
uint32_t buffer{0};
|
||||
unsigned count{0};
|
||||
|
||||
void put(uint32_t code, unsigned size) {
|
||||
this->buffer |= code << this->count;
|
||||
this->count += size;
|
||||
while (this->count >= 8) {
|
||||
this->out.push_back(static_cast<uint8_t>(this->buffer & 0xFF));
|
||||
this->buffer >>= 8;
|
||||
this->count -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
void flush() {
|
||||
if (this->count > 0)
|
||||
this->out.push_back(static_cast<uint8_t>(this->buffer & 0xFF));
|
||||
this->buffer = 0;
|
||||
this->count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Compress palette indices the way GIF asks for, appending the bytes to `out`.
|
||||
void lzw_compress(const uint8_t *data, size_t length, unsigned min_code_size, std::vector<uint8_t> &out) {
|
||||
struct Entry {
|
||||
uint32_t key; // the code before, shifted up a byte, with the next index in the low byte
|
||||
uint16_t code;
|
||||
};
|
||||
const uint32_t clear_code = 1u << min_code_size;
|
||||
const uint32_t end_code = clear_code + 1;
|
||||
std::vector<Entry> dictionary(DICTIONARY_SIZE);
|
||||
BitWriter bits{out};
|
||||
uint32_t next_code = 0;
|
||||
unsigned code_size = 0;
|
||||
|
||||
auto reset = [&]() {
|
||||
std::fill(dictionary.begin(), dictionary.end(), Entry{EMPTY_ENTRY, 0});
|
||||
next_code = end_code + 1;
|
||||
code_size = min_code_size + 1;
|
||||
};
|
||||
|
||||
reset();
|
||||
bits.put(clear_code, code_size);
|
||||
uint32_t prefix = data[0];
|
||||
for (size_t i = 1; i != length; i++) {
|
||||
const uint32_t key = prefix << 8 | data[i];
|
||||
size_t slot = (key * 2654435761u >> 16) & (DICTIONARY_SIZE - 1);
|
||||
while (dictionary[slot].key != EMPTY_ENTRY && dictionary[slot].key != key)
|
||||
slot = (slot + 1) & (DICTIONARY_SIZE - 1);
|
||||
if (dictionary[slot].key == key) {
|
||||
prefix = dictionary[slot].code;
|
||||
continue;
|
||||
}
|
||||
bits.put(prefix, code_size);
|
||||
if (next_code < MAX_CODES) {
|
||||
dictionary[slot] = Entry{key, static_cast<uint16_t>(next_code++)};
|
||||
// The reader is one entry behind, so it moves to longer codes a step after this does.
|
||||
if (next_code > (1u << code_size))
|
||||
code_size++;
|
||||
} else {
|
||||
bits.put(clear_code, code_size);
|
||||
reset();
|
||||
}
|
||||
prefix = data[i];
|
||||
}
|
||||
bits.put(prefix, code_size);
|
||||
// The reader adds an entry for that last code, which may take it to longer codes.
|
||||
if (next_code == (1u << code_size) && code_size < MAX_CODE_BITS)
|
||||
code_size++;
|
||||
bits.put(end_code, code_size);
|
||||
bits.flush();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GifWriter::GifWriter(FILE *file, int width, int height)
|
||||
: file_(file),
|
||||
width_(width),
|
||||
height_(height),
|
||||
bins_(COLOR_KEY_COUNT),
|
||||
palette_index_(COLOR_KEY_COUNT),
|
||||
indices_(static_cast<size_t>(width) * height) {}
|
||||
|
||||
bool GifWriter::write_header() {
|
||||
this->output_.clear();
|
||||
static constexpr char SIGNATURE[] = "GIF89a";
|
||||
this->output_.insert(this->output_.end(), SIGNATURE, SIGNATURE + 6);
|
||||
put_le16(this->output_, this->width_);
|
||||
put_le16(this->output_, this->height_);
|
||||
this->output_.push_back(0x70); // no shared colour table; 8 bits per colour
|
||||
this->output_.push_back(0); // background colour, unused
|
||||
this->output_.push_back(0); // pixels are square
|
||||
// Ask players to repeat the animation forever.
|
||||
static constexpr char LOOP_EXTENSION[] = "\x21\xFF\x0BNETSCAPE2.0\x03\x01";
|
||||
this->output_.insert(this->output_.end(), LOOP_EXTENSION, LOOP_EXTENSION + sizeof(LOOP_EXTENSION) - 1);
|
||||
put_le16(this->output_, 0);
|
||||
this->output_.push_back(0);
|
||||
return fwrite(this->output_.data(), 1, this->output_.size(), this->file_) == this->output_.size();
|
||||
}
|
||||
|
||||
size_t GifWriter::build_palette_(uint8_t *palette) {
|
||||
struct Box {
|
||||
size_t begin; // range of used_colors_, which the box owns
|
||||
size_t end;
|
||||
uint64_t pixels;
|
||||
};
|
||||
|
||||
this->used_colors_.clear();
|
||||
uint64_t total = 0;
|
||||
for (size_t key = 0; key != COLOR_KEY_COUNT; key++) {
|
||||
if (this->bins_[key].count != 0) {
|
||||
this->used_colors_.push_back(static_cast<uint16_t>(key));
|
||||
total += this->bins_[key].count;
|
||||
}
|
||||
}
|
||||
|
||||
// Median cut: keep cutting the box with the most pixels in two, across the colour part that
|
||||
// varies most, until there is a box for each palette entry or every box is a single colour.
|
||||
std::vector<Box> boxes;
|
||||
boxes.push_back({0, this->used_colors_.size(), total});
|
||||
while (boxes.size() < MAX_PALETTE_SIZE) {
|
||||
size_t chosen = boxes.size();
|
||||
for (size_t i = 0; i != boxes.size(); i++) {
|
||||
if (boxes[i].end - boxes[i].begin >= 2 && (chosen == boxes.size() || boxes[i].pixels > boxes[chosen].pixels))
|
||||
chosen = i;
|
||||
}
|
||||
if (chosen == boxes.size())
|
||||
break;
|
||||
const Box box = boxes[chosen];
|
||||
|
||||
unsigned low[3] = {255, 255, 255};
|
||||
unsigned high[3] = {0, 0, 0};
|
||||
for (size_t i = box.begin; i != box.end; i++) {
|
||||
for (unsigned channel = 0; channel != 3; channel++) {
|
||||
const unsigned value = key_channel(this->used_colors_[i], channel);
|
||||
low[channel] = std::min(low[channel], value);
|
||||
high[channel] = std::max(high[channel], value);
|
||||
}
|
||||
}
|
||||
unsigned widest = 0;
|
||||
for (unsigned channel = 1; channel != 3; channel++) {
|
||||
if (high[channel] - low[channel] > high[widest] - low[widest])
|
||||
widest = channel;
|
||||
}
|
||||
std::sort(this->used_colors_.begin() + box.begin, this->used_colors_.begin() + box.end,
|
||||
[widest](uint16_t a, uint16_t b) {
|
||||
const unsigned value_a = key_channel(a, widest);
|
||||
const unsigned value_b = key_channel(b, widest);
|
||||
return value_a != value_b ? value_a < value_b : a < b;
|
||||
});
|
||||
|
||||
// Cut where the pixels are half on one side and half on the other, keeping a colour on each.
|
||||
size_t cut = box.begin;
|
||||
uint64_t below = 0;
|
||||
while (cut < box.end - 1 && below * 2 < box.pixels)
|
||||
below += this->bins_[this->used_colors_[cut++]].count;
|
||||
boxes[chosen] = {box.begin, cut, below};
|
||||
boxes.push_back({cut, box.end, box.pixels - below});
|
||||
}
|
||||
|
||||
for (size_t i = 0; i != boxes.size(); i++) {
|
||||
uint64_t sum[3] = {0, 0, 0};
|
||||
for (size_t j = boxes[i].begin; j != boxes[i].end; j++) {
|
||||
const ColorBin &bin = this->bins_[this->used_colors_[j]];
|
||||
for (unsigned channel = 0; channel != 3; channel++)
|
||||
sum[channel] += bin.sum[channel];
|
||||
this->palette_index_[this->used_colors_[j]] = static_cast<uint8_t>(i);
|
||||
}
|
||||
// The average colour of the box, rounded to the nearest.
|
||||
for (unsigned channel = 0; channel != 3; channel++)
|
||||
palette[i * 3 + channel] = static_cast<uint8_t>((sum[channel] + boxes[i].pixels / 2) / boxes[i].pixels);
|
||||
}
|
||||
return boxes.size();
|
||||
}
|
||||
|
||||
bool GifWriter::write_frame(const uint8_t *bgr, size_t row_stride, unsigned delay_centiseconds) {
|
||||
std::fill(this->bins_.begin(), this->bins_.end(), ColorBin{});
|
||||
for (int y = 0; y != this->height_; y++) {
|
||||
const uint8_t *in = bgr + y * row_stride;
|
||||
for (int x = 0; x != this->width_; x++, in += 3) {
|
||||
ColorBin &bin = this->bins_[color_key(in[2], in[1], in[0])];
|
||||
bin.count++;
|
||||
bin.sum[0] += in[2];
|
||||
bin.sum[1] += in[1];
|
||||
bin.sum[2] += in[0];
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t palette[MAX_PALETTE_SIZE * 3] = {};
|
||||
const size_t colors = this->build_palette_(palette);
|
||||
unsigned palette_bits = MIN_PALETTE_BITS;
|
||||
while ((size_t{1} << palette_bits) < colors)
|
||||
palette_bits++;
|
||||
|
||||
uint8_t *out_index = this->indices_.data();
|
||||
for (int y = 0; y != this->height_; y++) {
|
||||
const uint8_t *in = bgr + y * row_stride;
|
||||
for (int x = 0; x != this->width_; x++, in += 3)
|
||||
*out_index++ = this->palette_index_[color_key(in[2], in[1], in[0])];
|
||||
}
|
||||
|
||||
this->output_.clear();
|
||||
// Show the frame for the time asked, then leave it in place under the next one.
|
||||
static constexpr uint8_t GRAPHIC_CONTROL[] = {0x21, 0xF9, 0x04, 0x04};
|
||||
this->output_.insert(this->output_.end(), std::begin(GRAPHIC_CONTROL), std::end(GRAPHIC_CONTROL));
|
||||
put_le16(this->output_, delay_centiseconds);
|
||||
this->output_.push_back(0); // no transparent colour
|
||||
this->output_.push_back(0);
|
||||
this->output_.push_back(0x2C); // the frame fills the whole picture
|
||||
put_le16(this->output_, 0);
|
||||
put_le16(this->output_, 0);
|
||||
put_le16(this->output_, this->width_);
|
||||
put_le16(this->output_, this->height_);
|
||||
this->output_.push_back(static_cast<uint8_t>(0x80 | (palette_bits - 1))); // has its own colour table
|
||||
this->output_.insert(this->output_.end(), palette, palette + (size_t{3} << palette_bits));
|
||||
this->output_.push_back(static_cast<uint8_t>(palette_bits));
|
||||
|
||||
std::vector<uint8_t> compressed;
|
||||
lzw_compress(this->indices_.data(), this->indices_.size(), palette_bits, compressed);
|
||||
for (size_t pos = 0; pos < compressed.size(); pos += MAX_SUB_BLOCK_SIZE) {
|
||||
const size_t length = std::min(MAX_SUB_BLOCK_SIZE, compressed.size() - pos);
|
||||
this->output_.push_back(static_cast<uint8_t>(length));
|
||||
this->output_.insert(this->output_.end(), compressed.begin() + pos, compressed.begin() + pos + length);
|
||||
}
|
||||
this->output_.push_back(0);
|
||||
return fwrite(this->output_.data(), 1, this->output_.size(), this->file_) == this->output_.size();
|
||||
}
|
||||
|
||||
bool GifWriter::write_trailer() {
|
||||
const uint8_t trailer = 0x3B;
|
||||
return fwrite(&trailer, 1, 1, this->file_) == 1;
|
||||
}
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
#endif
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
/// Writes an animated GIF a frame at a time, so a long recording is never held in memory.
|
||||
///
|
||||
/// A GIF frame can use at most 256 colours. Each frame gets its own colour table, chosen from what
|
||||
/// is in that frame, so a frame with 256 colours or fewer is stored exactly.
|
||||
class GifWriter {
|
||||
public:
|
||||
GifWriter(FILE *file, int width, int height);
|
||||
|
||||
/// Write the start of the file. This must come first. The animation repeats forever.
|
||||
bool write_header();
|
||||
/// Add a frame from three bytes per pixel in blue, green, red order, topmost row first, with
|
||||
/// `row_stride` bytes from the start of one row to the start of the next.
|
||||
/// The frame is shown for `delay_centiseconds` hundredths of a second.
|
||||
bool write_frame(const uint8_t *bgr, size_t row_stride, unsigned delay_centiseconds);
|
||||
/// Write the end of the file. This must come last.
|
||||
bool write_trailer();
|
||||
|
||||
protected:
|
||||
/// How many pixels of one colour there are in the frame, and the sum of their red, green and blue.
|
||||
struct ColorBin {
|
||||
uint64_t sum[3];
|
||||
uint32_t count;
|
||||
};
|
||||
|
||||
/// Choose up to 256 colours for the frame in `bins_`, storing them as red, green, blue triples in
|
||||
/// `palette` and which one each colour is nearest in `palette_index_`. Returns how many were chosen.
|
||||
size_t build_palette_(uint8_t *palette);
|
||||
|
||||
FILE *file_;
|
||||
int width_;
|
||||
int height_;
|
||||
std::vector<ColorBin> bins_;
|
||||
std::vector<uint16_t> used_colors_;
|
||||
std::vector<uint8_t> palette_index_;
|
||||
std::vector<uint8_t> indices_;
|
||||
std::vector<uint8_t> output_;
|
||||
};
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,8 @@
|
||||
#ifdef USE_HOST
|
||||
#include "snapshot.h"
|
||||
#include "gif.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
@@ -7,11 +10,13 @@
|
||||
#include <unistd.h>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
@@ -28,16 +33,19 @@ constexpr size_t BMP_HEADER_SIZE = 54;
|
||||
constexpr size_t BMP_INFO_HEADER_SIZE = 40;
|
||||
constexpr int BMP_BITS_PER_PIXEL = 24;
|
||||
|
||||
/// True if the name already ends in ".bmp". The comparison ignores case, so "shot.BMP" is left
|
||||
/// alone rather than turned into "shot.BMP.bmp".
|
||||
bool has_bmp_suffix(const std::string &name) {
|
||||
return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, ".bmp") == 0;
|
||||
// The largest width or height a GIF file can hold.
|
||||
constexpr int GIF_MAX_SIZE = 0xFFFF;
|
||||
|
||||
/// True if the name already ends in the extension, which is four characters including the dot. The
|
||||
/// comparison ignores case, so "shot.BMP" is left alone rather than turned into "shot.BMP.bmp".
|
||||
bool has_extension(const std::string &name, const char *extension) {
|
||||
return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, extension) == 0;
|
||||
}
|
||||
|
||||
/// Reduce a user supplied name to a single safe path component. Everything outside the allowed set
|
||||
/// is replaced, so "..", "/" and absolute paths cannot escape the snapshot directory.
|
||||
/// Returns an empty string if nothing usable is left.
|
||||
std::string sanitise_filename(const char *const name, bool *name_changed) {
|
||||
std::string sanitise_filename(const char *const name, const char *extension, bool *name_changed) {
|
||||
std::string result;
|
||||
bool all_dots = true;
|
||||
bool changed = false;
|
||||
@@ -59,8 +67,8 @@ std::string sanitise_filename(const char *const name, bool *name_changed) {
|
||||
*name_changed = true;
|
||||
return "";
|
||||
}
|
||||
if (!has_bmp_suffix(result))
|
||||
result += ".bmp";
|
||||
if (!has_extension(result, extension))
|
||||
result += extension;
|
||||
*name_changed = changed;
|
||||
return result;
|
||||
}
|
||||
@@ -89,15 +97,15 @@ void put_le(uint8_t *&dest, uint32_t value, size_t bytes) {
|
||||
*dest++ = static_cast<uint8_t>(value >> (8 * i));
|
||||
}
|
||||
|
||||
/// The number of bytes one row of `width` pixels takes up in the file. Rows are padded out to a
|
||||
/// multiple of four bytes.
|
||||
size_t bmp_row_size(int width) { return (static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
|
||||
/// The number of bytes one row of `width` pixels takes up in a picture buffer. Rows are padded out
|
||||
/// to a multiple of four bytes, which is also what a BMP file wants.
|
||||
size_t padded_row_size(int width) { return (static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
|
||||
|
||||
/// Write pixels out as a 24 bit BMP. The rows given start with the topmost and are `row_stride`
|
||||
/// bytes apart, which must leave room for a whole padded row; a BMP holds its rows the other way
|
||||
/// up, so they go out last first.
|
||||
bool write_bmp(FILE *file, const uint8_t *pixels, int width, int height, size_t row_stride) {
|
||||
const size_t row_size = bmp_row_size(width);
|
||||
const size_t row_size = padded_row_size(width);
|
||||
const size_t pixel_bytes = row_size * height;
|
||||
|
||||
uint8_t header[BMP_HEADER_SIZE];
|
||||
@@ -128,21 +136,19 @@ bool write_bmp(FILE *file, const uint8_t *pixels, int width, int height, size_t
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Reserve a name in the snapshot directory and write the picture to it.
|
||||
/// Reserve a name in the snapshot directory and open a new file under it.
|
||||
/// With `exact` set the given name is the only one tried; otherwise a number is added on
|
||||
/// collision. Returns true if a file was written.
|
||||
bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t row_stride, const std::string &name,
|
||||
bool exact) {
|
||||
/// collision. Returns nullptr, having logged why, if there is no file. `path` is where it is.
|
||||
FILE *create_snapshot_file(const std::string &name, bool exact, std::string &path) {
|
||||
const std::string dir = snapshot_dir();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (ec) {
|
||||
ESP_LOGE(TAG, "Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str());
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// O_EXCL guarantees we never write over a file that is already there.
|
||||
std::string path;
|
||||
int fd = -1;
|
||||
for (unsigned attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) {
|
||||
path = dir + "/" + (attempt == 0 ? name : add_suffix(name, attempt));
|
||||
@@ -151,18 +157,18 @@ bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t ro
|
||||
break;
|
||||
if (errno != EEXIST) {
|
||||
ESP_LOGE(TAG, "Could not create %s: %s", path.c_str(), strerror(errno));
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
if (exact) {
|
||||
// The caller asked for this exact name, so silently writing somewhere else would be worse
|
||||
// than failing - a test asserting on the path would pick up a stale file.
|
||||
ESP_LOGE(TAG, "Snapshot %s already exists, not overwriting", path.c_str());
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
if (fd < 0) {
|
||||
ESP_LOGE(TAG, "Could not find an unused name for %s in %s", name.c_str(), dir.c_str());
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FILE *file = fdopen(fd, "wb");
|
||||
@@ -170,9 +176,14 @@ bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t ro
|
||||
ESP_LOGE(TAG, "Could not open %s: %s", path.c_str(), strerror(errno));
|
||||
::close(fd);
|
||||
::unlink(path.c_str());
|
||||
return false;
|
||||
}
|
||||
bool ok = write_bmp(file, pixels, width, height, row_stride);
|
||||
return file;
|
||||
}
|
||||
|
||||
/// Close a file that is meant to be finished. `ok` says whether everything written to it went well.
|
||||
/// If not, or if closing shows that the data did not all get out, say why and remove the file: a
|
||||
/// truncated one would block a retry under the same name. Returns true if the file is complete.
|
||||
bool close_snapshot_file(FILE *file, const std::string &path, bool ok) {
|
||||
int saved_errno = ok ? 0 : errno;
|
||||
// Closing can fail in its own right - the last of the data is still on its way out.
|
||||
if (fclose(file) != 0) {
|
||||
@@ -182,18 +193,111 @@ bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t ro
|
||||
}
|
||||
if (!ok) {
|
||||
ESP_LOGE(TAG, "Could not write %s: %s", path.c_str(), strerror(saved_errno));
|
||||
// Leave no truncated file behind - it would block a retry under the same name.
|
||||
::unlink(path.c_str());
|
||||
return false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// Write pixels to a new BMP file. See create_snapshot_file() for `name` and `exact`.
|
||||
bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t row_stride, const std::string &name,
|
||||
bool exact) {
|
||||
std::string path;
|
||||
FILE *file = create_snapshot_file(name, exact, path);
|
||||
if (file == nullptr)
|
||||
return false;
|
||||
if (!close_snapshot_file(file, path, write_bmp(file, pixels, width, height, row_stride)))
|
||||
return false;
|
||||
ESP_LOGI(TAG, "Snapshot written to %s", path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Work out the file name for a request. A name of the caller's own is made safe and is the only
|
||||
/// one tried (`exact`); otherwise one is made up from the prefix and the current time.
|
||||
std::string choose_name(const char *filename, const char *prefix, const char *extension, bool &exact) {
|
||||
std::string name;
|
||||
exact = false;
|
||||
if (filename != nullptr) {
|
||||
bool name_changed = false;
|
||||
name = sanitise_filename(filename, extension, &name_changed);
|
||||
exact = !name.empty();
|
||||
if (name_changed) {
|
||||
ESP_LOGW(TAG, "Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
|
||||
name.empty() ? LOG_STR_LITERAL("a name made from the time") : name.c_str());
|
||||
}
|
||||
}
|
||||
if (!name.empty())
|
||||
return name;
|
||||
|
||||
struct timespec now {};
|
||||
if (clock_gettime(CLOCK_REALTIME, &now) != 0)
|
||||
now = {};
|
||||
struct tm tm_buf {};
|
||||
if (localtime_r(&now.tv_sec, &tm_buf) == nullptr)
|
||||
tm_buf = {};
|
||||
char stamp[32]{};
|
||||
// ::strftime to be sure of the one from <ctime>; display has an unrelated member of that name
|
||||
if (::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &tm_buf) == 0)
|
||||
snprintf(stamp, sizeof(stamp), "unknown-time");
|
||||
char buffer[MAX_NAME_LENGTH];
|
||||
int written = snprintf(buffer, sizeof(buffer), "%s-%s-%03ld%s", prefix, stamp, now.tv_nsec / 1000000, extension);
|
||||
if (written < 0 || static_cast<size_t>(written) >= sizeof(buffer)) {
|
||||
ESP_LOGW(TAG, "Could not build a timestamped snapshot name, using a fallback");
|
||||
snprintf(buffer, sizeof(buffer), "snapshot%s", extension);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Snapshot::take_snapshot_or_log(const char *filename) {
|
||||
if (!this->take_snapshot(filename[0] != '\0' ? filename : nullptr)) {
|
||||
/// An animation being written: the file, and where in the timetable it has got to.
|
||||
struct Snapshot::Recording {
|
||||
Recording(FILE *file, std::string path, int width, int height, uint32_t frames, float frame_rate)
|
||||
: file(file),
|
||||
path(std::move(path)),
|
||||
gif(file, width, height),
|
||||
row_stride(padded_row_size(width)),
|
||||
pixels(std::make_unique<uint8_t[]>(this->row_stride * height)),
|
||||
frames(frames),
|
||||
frame_rate(frame_rate) {}
|
||||
~Recording() {
|
||||
// The file is still open only if the recording did not finish, so do not leave a partial one behind.
|
||||
if (this->file != nullptr) {
|
||||
fclose(this->file);
|
||||
::unlink(this->path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// When the frame with this number is due, in milliseconds after the recording began.
|
||||
uint32_t due_ms(uint32_t frame) const {
|
||||
return static_cast<uint32_t>(std::lround(frame * 1000.0 / this->frame_rate));
|
||||
}
|
||||
/// How long to show the frame just taken, in hundredths of a second, as a GIF file counts time.
|
||||
/// Each frame's time is worked out from the start rather than by rounding its own length, so a
|
||||
/// rate that does not divide evenly into hundredths still averages out to what was asked for.
|
||||
unsigned delay_centiseconds() const {
|
||||
auto centiseconds_at = [this](uint32_t frame) { return std::lround(frame * 100.0 / this->frame_rate); };
|
||||
return static_cast<unsigned>(centiseconds_at(this->frames_done + 1) - centiseconds_at(this->frames_done));
|
||||
}
|
||||
|
||||
FILE *file;
|
||||
std::string path;
|
||||
GifWriter gif;
|
||||
size_t row_stride;
|
||||
std::unique_ptr<uint8_t[]> pixels;
|
||||
uint32_t frames;
|
||||
float frame_rate;
|
||||
uint32_t frames_done{0};
|
||||
uint32_t start_ms{0};
|
||||
};
|
||||
|
||||
Snapshot::Snapshot() = default;
|
||||
Snapshot::~Snapshot() = default;
|
||||
|
||||
// helper function since ESP_LOGW is disallowed in a header file
|
||||
void Snapshot::take_snapshot_or_log(const char *filename, uint32_t frames, float frame_rate) {
|
||||
const char *name = filename[0] != '\0' ? filename : nullptr;
|
||||
const bool ok = frames == 0 ? this->take_snapshot(name) : this->take_animation(name, frames, frame_rate);
|
||||
if (!ok) {
|
||||
ESP_LOGW(TAG, "snapshot.take did not write a file");
|
||||
}
|
||||
}
|
||||
@@ -206,46 +310,80 @@ bool Snapshot::take_snapshot(const char *filename) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
bool exact = false;
|
||||
if (filename != nullptr) {
|
||||
bool name_changed = false;
|
||||
name = sanitise_filename(filename, &name_changed);
|
||||
exact = !name.empty();
|
||||
if (name_changed) {
|
||||
ESP_LOGW(TAG, "Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
|
||||
name.empty() ? LOG_STR_LITERAL("a name made from the time") : name.c_str());
|
||||
}
|
||||
}
|
||||
if (name.empty()) {
|
||||
struct timespec now {};
|
||||
if (clock_gettime(CLOCK_REALTIME, &now) != 0)
|
||||
now = {};
|
||||
struct tm tm_buf {};
|
||||
if (localtime_r(&now.tv_sec, &tm_buf) == nullptr)
|
||||
tm_buf = {};
|
||||
char stamp[32]{};
|
||||
// ::strftime to be sure of the one from <ctime>; display has an unrelated member of that name
|
||||
if (::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &tm_buf) == 0)
|
||||
snprintf(stamp, sizeof(stamp), "unknown-time");
|
||||
char buffer[MAX_NAME_LENGTH];
|
||||
int written =
|
||||
snprintf(buffer, sizeof(buffer), "%s-%s-%03ld.bmp", this->snapshot_prefix_, stamp, now.tv_nsec / 1000000);
|
||||
if (written < 0 || static_cast<size_t>(written) >= sizeof(buffer)) {
|
||||
ESP_LOGW(TAG, "Could not build a timestamped snapshot name, using a fallback");
|
||||
snprintf(buffer, sizeof(buffer), "snapshot.bmp");
|
||||
}
|
||||
name = buffer;
|
||||
}
|
||||
bool exact;
|
||||
const std::string name = choose_name(filename, this->snapshot_prefix_, ".bmp", exact);
|
||||
|
||||
// Rows are padded out to a multiple of four bytes, as the file wants them, so each one can be
|
||||
// written straight from the buffer. Zeroed on allocation, which is what the padding must be.
|
||||
const size_t row_stride = bmp_row_size(width);
|
||||
const size_t row_stride = padded_row_size(width);
|
||||
auto pixels = std::make_unique<uint8_t[]>(row_stride * height);
|
||||
if (!this->capture_bgr(pixels.get(), row_stride))
|
||||
return false;
|
||||
return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact);
|
||||
}
|
||||
|
||||
bool Snapshot::take_animation(const char *filename, uint32_t frames, float frame_rate) {
|
||||
if (this->recording_ != nullptr) {
|
||||
ESP_LOGW(TAG, "Already recording %s, not starting another", this->recording_->path.c_str());
|
||||
return false;
|
||||
}
|
||||
if (frames == 0 || !(frame_rate > 0)) {
|
||||
ESP_LOGE(TAG, "Animation requested with %u frames at %.1f frames a second", frames, frame_rate);
|
||||
return false;
|
||||
}
|
||||
const int width = this->snapshot_width();
|
||||
const int height = this->snapshot_height();
|
||||
if (width <= 0 || height <= 0 || width > GIF_MAX_SIZE || height > GIF_MAX_SIZE) {
|
||||
ESP_LOGE(TAG, "Animation requested but the display is %dx%d, and a GIF holds up to %d", width, height,
|
||||
GIF_MAX_SIZE);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool exact;
|
||||
const std::string name = choose_name(filename, this->snapshot_prefix_, ".gif", exact);
|
||||
std::string path;
|
||||
FILE *file = create_snapshot_file(name, exact, path);
|
||||
if (file == nullptr)
|
||||
return false;
|
||||
auto recording = std::make_unique<Recording>(file, path, width, height, frames, frame_rate);
|
||||
if (!recording->gif.write_header()) {
|
||||
recording->file = nullptr;
|
||||
close_snapshot_file(file, path, false);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG, "Recording %u frames at %.1f frames a second to %s", frames, frame_rate, path.c_str());
|
||||
this->recording_ = std::move(recording);
|
||||
this->recording_->start_ms = millis();
|
||||
return this->record_frame_();
|
||||
}
|
||||
|
||||
bool Snapshot::record_frame_() {
|
||||
Recording &recording = *this->recording_;
|
||||
if (!this->capture_bgr(recording.pixels.get(), recording.row_stride)) {
|
||||
// capture_bgr() has said why. Resetting removes the half-finished file.
|
||||
this->recording_.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = recording.gif.write_frame(recording.pixels.get(), recording.row_stride, recording.delay_centiseconds());
|
||||
recording.frames_done++;
|
||||
const bool finished = recording.frames_done == recording.frames;
|
||||
if (ok && finished)
|
||||
ok = recording.gif.write_trailer();
|
||||
if (!ok || finished) {
|
||||
ok = close_snapshot_file(std::exchange(recording.file, nullptr), recording.path, ok);
|
||||
if (ok) {
|
||||
ESP_LOGI(TAG, "Animation written to %s", recording.path.c_str());
|
||||
}
|
||||
this->recording_.reset();
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Aim for the time each frame is due, not a fixed gap after the last, so delays do not add up.
|
||||
const int32_t wait = static_cast<int32_t>(recording.start_ms + recording.due_ms(recording.frames_done) - millis());
|
||||
App.scheduler.set_timeout(this, wait > 0 ? static_cast<uint32_t>(wait) : 0, [this]() { this->record_frame_(); });
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
#endif
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// Directory snapshots are written to. Normally set by codegen to a folder under .esphome; the
|
||||
@@ -17,11 +18,14 @@ namespace esphome::snapshot {
|
||||
/// Base for anything that can hand over the picture it is showing so it can be written to a file.
|
||||
///
|
||||
/// A subclass says how big the picture is and fills in the pixels. Everything else - picking a
|
||||
/// name, staying inside the snapshot directory, not writing over anything, and encoding the file -
|
||||
/// is done here, so every component that can take a snapshot behaves the same way.
|
||||
/// name, staying inside the snapshot directory, not writing over anything, encoding the file, and
|
||||
/// timing the frames of an animation - is done here, so every component that can take a snapshot
|
||||
/// behaves the same way.
|
||||
class Snapshot {
|
||||
public:
|
||||
virtual ~Snapshot() = default;
|
||||
// Declared here and defined where Recording is complete, which unique_ptr needs.
|
||||
Snapshot();
|
||||
virtual ~Snapshot();
|
||||
|
||||
/// Set the word generated names start with. Codegen passes the component id, so with more than
|
||||
/// one display in a device it is clear which one a file came from.
|
||||
@@ -33,10 +37,20 @@ class Snapshot {
|
||||
/// already there is never written over. Returns true if a file was written.
|
||||
bool take_snapshot(const char *filename);
|
||||
|
||||
/// Record what is shown as an animated GIF file in the snapshot directory.
|
||||
///
|
||||
/// The first frame is taken now and the rest follow at `frame_rate` frames a second, in the
|
||||
/// background. Names work as for take_snapshot(), with ".gif" in place of ".bmp". Only one
|
||||
/// recording can run at a time. Returns true if the recording started.
|
||||
bool take_animation(const char *filename, uint32_t frames, float frame_rate);
|
||||
|
||||
/// Take a snapshot for an automation: an empty name means a generated one, and a snapshot
|
||||
/// that wrote nothing is logged.
|
||||
void take_snapshot_or_log(const char *filename);
|
||||
void take_snapshot_or_log(const std::string &filename) { this->take_snapshot_or_log(filename.c_str()); }
|
||||
/// that wrote nothing is logged. A `frames` of 0 takes a single picture; otherwise an
|
||||
/// animation is recorded.
|
||||
void take_snapshot_or_log(const char *filename, uint32_t frames, float frame_rate);
|
||||
void take_snapshot_or_log(const std::string &filename, uint32_t frames, float frame_rate) {
|
||||
this->take_snapshot_or_log(filename.c_str(), frames, frame_rate);
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Width of the picture in pixels.
|
||||
@@ -49,6 +63,15 @@ class Snapshot {
|
||||
virtual bool capture_bgr(uint8_t *dest, size_t row_stride) = 0;
|
||||
|
||||
const char *snapshot_prefix_{"snapshot"};
|
||||
|
||||
private:
|
||||
struct Recording;
|
||||
|
||||
/// Add a frame to the recording and arrange for the next one. Returns false if the recording
|
||||
/// had to be given up.
|
||||
bool record_frame_();
|
||||
|
||||
std::unique_ptr<Recording> recording_;
|
||||
};
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
|
||||
@@ -59,13 +59,15 @@ int BSDSocketImpl::close() {
|
||||
|
||||
int BSDSocketImpl::setblocking(bool blocking) {
|
||||
int fl = ::fcntl(this->fd_, F_GETFL, 0);
|
||||
if (fl < 0) {
|
||||
return fl;
|
||||
}
|
||||
if (blocking) {
|
||||
fl &= ~O_NONBLOCK;
|
||||
} else {
|
||||
fl |= O_NONBLOCK;
|
||||
}
|
||||
::fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
return ::fcntl(this->fd_, F_SETFL, fl);
|
||||
}
|
||||
|
||||
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
|
||||
|
||||
@@ -205,6 +205,13 @@ static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN
|
||||
static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN
|
||||
#endif
|
||||
|
||||
/// Outcome of polling a non-blocking connect(); see socket::poll_connect().
|
||||
enum class ConnectPollResult : uint8_t {
|
||||
CONNECT_POLL_RESULT_PENDING,
|
||||
CONNECT_POLL_RESULT_CONNECTED,
|
||||
CONNECT_POLL_RESULT_ERROR,
|
||||
};
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
#endif
|
||||
|
||||
@@ -48,8 +48,33 @@ static const char *const TAG = "socket";
|
||||
#ifdef USE_ESP8266
|
||||
// optimistic_yield() rate limit in microseconds of CONT time; cheap when hot.
|
||||
static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000;
|
||||
// Let SYS run so queued WiFi traffic reaches lwip; CONT and SYS are cooperative
|
||||
static inline void yield_to_sys() { optimistic_yield(ESP8266_YIELD_INTERVAL_US); }
|
||||
#else
|
||||
static inline void yield_to_sys() {}
|
||||
#endif
|
||||
|
||||
// errno for a failed tcp_* call
|
||||
static int lwip_err_to_errno(err_t err) {
|
||||
switch (err) {
|
||||
case ERR_MEM:
|
||||
return ENOMEM;
|
||||
case ERR_BUF:
|
||||
return EAGAIN; // transient, e.g. no free local port
|
||||
case ERR_RTE:
|
||||
return EHOSTUNREACH; // no route, e.g. no address yet
|
||||
case ERR_VAL:
|
||||
case ERR_ARG:
|
||||
return EINVAL;
|
||||
case ERR_USE:
|
||||
return EADDRINUSE;
|
||||
case ERR_ISCONN:
|
||||
return EISCONN;
|
||||
default:
|
||||
return EIO;
|
||||
}
|
||||
}
|
||||
|
||||
// set to 1 to enable verbose lwip logging
|
||||
#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
|
||||
#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
|
||||
@@ -62,8 +87,8 @@ static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000;
|
||||
// Must be called before destroying the object that tcp_arg points to —
|
||||
// tcp_abort() triggers the err callback synchronously, which would
|
||||
// otherwise call back into a partially-destroyed object.
|
||||
// tcp_sent/tcp_poll are not cleared because this implementation
|
||||
// never registers them.
|
||||
// tcp_sent/tcp_poll are never registered and the connect callback cannot
|
||||
// fire after abort or close, so neither is cleared.
|
||||
static void pcb_detach_abort(struct tcp_pcb *pcb) {
|
||||
tcp_arg(pcb, nullptr);
|
||||
tcp_recv(pcb, nullptr);
|
||||
@@ -76,8 +101,7 @@ static void pcb_detach_abort(struct tcp_pcb *pcb) {
|
||||
// After tcp_close(), the PCB remains alive during the TCP close handshake
|
||||
// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP
|
||||
// would call recv/err on a destroyed socket object, corrupting the heap.
|
||||
// tcp_sent/tcp_poll are not cleared because this implementation
|
||||
// never registers them.
|
||||
// Callbacks are left as in pcb_detach_abort().
|
||||
// Returns ERR_OK on success; on failure the PCB is aborted instead.
|
||||
static err_t pcb_detach_close(struct tcp_pcb *pcb) {
|
||||
tcp_arg(pcb, nullptr);
|
||||
@@ -101,67 +125,51 @@ LWIPRawCommon::~LWIPRawCommon() {
|
||||
}
|
||||
}
|
||||
|
||||
bool LWIPRawCommon::sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const {
|
||||
if (name == nullptr) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
#if LWIP_IPV6
|
||||
if (this->family_ == AF_INET6) {
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
|
||||
*port = ntohs(addr6->sin6_port);
|
||||
inet6_addr_to_ip6addr(ip_2_ip6(ip), &addr6->sin6_addr);
|
||||
// ANY lets bind() accept both families; connect() picks the concrete type
|
||||
IP_SET_TYPE_VAL(*ip, IPADDR_TYPE_ANY);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (this->family_ != AF_INET || addrlen < sizeof(sockaddr_in)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
|
||||
*port = ntohs(addr4->sin_port);
|
||||
ip_addr_set_ip4_u32(ip, addr4->sin_addr.s_addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) {
|
||||
LWIP_LOCK();
|
||||
if (this->pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
if (name == nullptr) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
ip_addr_t ip;
|
||||
in_port_t port;
|
||||
#if LWIP_IPV6
|
||||
if (this->family_ == AF_INET) {
|
||||
if (addrlen < sizeof(sockaddr_in)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
|
||||
port = ntohs(addr4->sin_port);
|
||||
ip.type = IPADDR_TYPE_V4;
|
||||
ip.u_addr.ip4.addr = addr4->sin_addr.s_addr;
|
||||
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port);
|
||||
} else if (this->family_ == AF_INET6) {
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
|
||||
port = ntohs(addr6->sin6_port);
|
||||
ip.type = IPADDR_TYPE_ANY;
|
||||
memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16);
|
||||
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port);
|
||||
} else {
|
||||
errno = EINVAL;
|
||||
uint16_t port;
|
||||
if (!this->sockaddr2ip_(name, addrlen, &ip, &port)) {
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
if (this->family_ != AF_INET) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
|
||||
port = ntohs(addr4->sin_port);
|
||||
ip.addr = addr4->sin_addr.s_addr;
|
||||
LWIP_LOG("tcp_bind(%p ip=%u port=%u)", this->pcb_, ip.addr, port);
|
||||
#endif
|
||||
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
|
||||
err_t err = tcp_bind(this->pcb_, &ip, port);
|
||||
if (err == ERR_USE) {
|
||||
LWIP_LOG(" -> err ERR_USE");
|
||||
errno = EADDRINUSE;
|
||||
return -1;
|
||||
}
|
||||
if (err == ERR_VAL) {
|
||||
LWIP_LOG(" -> err ERR_VAL");
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (err != ERR_OK) {
|
||||
LWIP_LOG(" -> err %d", err);
|
||||
errno = EIO;
|
||||
errno = lwip_err_to_errno(err);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -178,7 +186,7 @@ int LWIPRawCommon::close() {
|
||||
this->pcb_ = nullptr;
|
||||
if (err != ERR_OK) {
|
||||
LWIP_LOG(" -> err %d", err);
|
||||
errno = err == ERR_MEM ? ENOMEM : EIO;
|
||||
errno = lwip_err_to_errno(err);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -205,7 +213,7 @@ int LWIPRawCommon::shutdown(int how) {
|
||||
err_t err = tcp_shutdown(this->pcb_, shut_rx, shut_tx);
|
||||
if (err != ERR_OK) {
|
||||
LWIP_LOG(" -> err %d", err);
|
||||
errno = err == ERR_MEM ? ENOMEM : EIO;
|
||||
errno = lwip_err_to_errno(err);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -425,7 +433,82 @@ void LWIPRawImpl::s_err_fn(void *arg, err_t err) {
|
||||
// ERR_ABRT: aborted through tcp_abort or TCP timer
|
||||
auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
|
||||
ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err);
|
||||
if (arg_this->connect_err_ == EINPROGRESS) {
|
||||
// Refused (RST) or SYN retries exhausted; written before pcb_ so
|
||||
// poll_connect() never sees a dead pcb without its reason
|
||||
arg_this->connect_err_ = err == ERR_RST ? ECONNREFUSED : ETIMEDOUT;
|
||||
}
|
||||
arg_this->pcb_ = nullptr;
|
||||
esphome::wake_loop_any_context();
|
||||
}
|
||||
|
||||
err_t LWIPRawImpl::s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err) {
|
||||
// LWIP CALLBACK, same constraints as s_err_fn; err is always ERR_OK
|
||||
auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
|
||||
arg_this->connect_err_ = EISCONN;
|
||||
esphome::wake_loop_any_context();
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
int LWIPRawImpl::connect(const struct sockaddr *addr, socklen_t addrlen) {
|
||||
LWIP_LOCK();
|
||||
if (this->pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
if (this->connect_err_ == EINPROGRESS || this->connect_err_ == EISCONN) {
|
||||
errno = this->connect_err_ == EINPROGRESS ? EALREADY : EISCONN;
|
||||
return -1;
|
||||
}
|
||||
ip_addr_t ip;
|
||||
uint16_t port;
|
||||
if (!this->sockaddr2ip_(addr, addrlen, &ip, &port)) {
|
||||
return -1;
|
||||
}
|
||||
#if LWIP_IPV6
|
||||
// tcp_connect needs a concrete type; a remembered IPv4 peer arrives v4-mapped
|
||||
if (IP_IS_ANY_TYPE_VAL(ip)) {
|
||||
if (ip6_addr_isipv4mappedipv6(ip_2_ip6(&ip))) {
|
||||
unmap_ipv4_mapped_ipv6(ip_2_ip4(&ip), ip_2_ip6(&ip));
|
||||
IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V4);
|
||||
} else {
|
||||
IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V6);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
LWIP_LOG("tcp_connect(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
|
||||
err_t err = tcp_connect(this->pcb_, &ip, port, LWIPRawImpl::s_connected_fn);
|
||||
if (err != ERR_OK) {
|
||||
LWIP_LOG(" -> err %d", err);
|
||||
errno = lwip_err_to_errno(err);
|
||||
return -1;
|
||||
}
|
||||
this->connect_err_ = EINPROGRESS;
|
||||
errno = EINPROGRESS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
ConnectPollResult LWIPRawImpl::poll_connect(int &err_out) const {
|
||||
// pcb_ first; see the ordering note on the declaration
|
||||
if (this->pcb_ == nullptr) {
|
||||
// Only a recorded connect failure carries its own reason
|
||||
const bool failed = this->connect_err_ == ECONNREFUSED || this->connect_err_ == ETIMEDOUT;
|
||||
err_out = failed ? this->connect_err_ : ECONNRESET;
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
|
||||
}
|
||||
switch (this->connect_err_) {
|
||||
case EINPROGRESS:
|
||||
yield_to_sys(); // so the SYN-ACK is processed between polls
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_PENDING;
|
||||
case EISCONN:
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED;
|
||||
case 0:
|
||||
err_out = EINVAL; // no connect was started
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
|
||||
default:
|
||||
err_out = this->connect_err_;
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
|
||||
@@ -540,14 +623,11 @@ ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) {
|
||||
}
|
||||
|
||||
ssize_t LWIPRawImpl::read(void *buf, size_t len) {
|
||||
#ifdef USE_ESP8266
|
||||
// Would block: yield to SYS so queued WiFi RX reaches lwip and this read
|
||||
// may succeed. Without this, inbound segments can sit unprocessed for
|
||||
// seconds while the main loop polls (CONT/SYS are cooperative on ESP8266).
|
||||
// Let queued WiFi RX reach lwip first; otherwise inbound segments can
|
||||
// sit unprocessed for seconds while the main loop polls
|
||||
if (this->waiting_for_data_()) {
|
||||
optimistic_yield(ESP8266_YIELD_INTERVAL_US);
|
||||
yield_to_sys();
|
||||
}
|
||||
#endif
|
||||
// See waiting_for_data_() for safety of unlocked reads.
|
||||
if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
|
||||
this->wait_for_data_();
|
||||
@@ -636,12 +716,10 @@ int LWIPRawImpl::internal_output_() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
// Flushed: yield to SYS so the queued segments reach the WiFi driver
|
||||
// instead of waiting seconds for an unrelated SYS slot. Callers only get
|
||||
// here after a successful tcp_write, so idle paths never yield.
|
||||
optimistic_yield(ESP8266_YIELD_INTERVAL_US);
|
||||
#endif
|
||||
yield_to_sys();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ class LWIPRawCommon {
|
||||
|
||||
protected:
|
||||
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen);
|
||||
/// sockaddr of this socket's family to lwip address and port; false with errno on mismatch
|
||||
bool sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const;
|
||||
|
||||
// Member ordering optimized to minimize padding on 32-bit systems
|
||||
struct tcp_pcb *pcb_;
|
||||
@@ -58,7 +60,14 @@ class LWIPRawCommon {
|
||||
bool nodelay_ = false;
|
||||
sa_family_t family_ = 0;
|
||||
uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s)
|
||||
// 0 before connect(), EINPROGRESS while pending, EISCONN once established,
|
||||
// else the failure errno the callbacks recorded; fills the padding byte
|
||||
uint8_t connect_err_ = 0;
|
||||
static_assert(EINPROGRESS < 256 && EISCONN < 256 && ECONNREFUSED < 256 && ECONNRESET < 256 && ETIMEDOUT < 256,
|
||||
"connect_err_ stores errno values in a byte");
|
||||
};
|
||||
// The connect state must stay in the padding so no socket pays RAM for it
|
||||
static_assert(sizeof(LWIPRawCommon) == sizeof(struct tcp_pcb *) + 4, "LWIPRawCommon grew past one word of flags");
|
||||
|
||||
/// Connected socket implementation for LWIP raw TCP.
|
||||
/// No virtual methods — callers always use the concrete type.
|
||||
@@ -83,6 +92,12 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
errno = EOPNOTSUPP;
|
||||
return -1;
|
||||
}
|
||||
/// Non-blocking: returns -1/EINPROGRESS once the SYN is queued, see poll_connect().
|
||||
/// addr must match the socket family; an IPv4 peer on AF_INET6 arrives v4-mapped.
|
||||
int connect(const struct sockaddr *addr, socklen_t addrlen);
|
||||
// Unlocked like ready(): the callbacks write the error byte before pcb_,
|
||||
// so a torn read only costs one extra poll
|
||||
ConnectPollResult poll_connect(int &err_out) const;
|
||||
ssize_t read(void *buf, size_t len);
|
||||
ssize_t readv(const struct iovec *iov, int iovcnt);
|
||||
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
|
||||
@@ -120,6 +135,7 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
|
||||
static void s_err_fn(void *arg, err_t err);
|
||||
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
|
||||
static err_t s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err);
|
||||
|
||||
protected:
|
||||
// True when the socket could receive data but none has arrived yet.
|
||||
@@ -137,6 +153,9 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
size_t rx_buf_offset_ = 0;
|
||||
bool rx_closed_ = false;
|
||||
};
|
||||
// rx_buf_, rx_buf_offset_, then rx_closed_ padded to a word
|
||||
static_assert(sizeof(LWIPRawImpl) == sizeof(LWIPRawCommon) + sizeof(pbuf *) + sizeof(size_t) + 4,
|
||||
"LWIPRawImpl layout changed");
|
||||
|
||||
/// Listening socket implementation for LWIP raw TCP.
|
||||
/// Separate from LWIPRawImpl — no virtual dispatch needed.
|
||||
|
||||
@@ -49,13 +49,15 @@ int LwIPSocketImpl::close() {
|
||||
|
||||
int LwIPSocketImpl::setblocking(bool blocking) {
|
||||
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
|
||||
if (fl < 0) {
|
||||
return fl;
|
||||
}
|
||||
if (blocking) {
|
||||
fl &= ~O_NONBLOCK;
|
||||
} else {
|
||||
fl |= O_NONBLOCK;
|
||||
}
|
||||
lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
return lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||
}
|
||||
|
||||
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
#include <sys/select.h>
|
||||
#endif
|
||||
#include <string>
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
@@ -165,7 +168,10 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
|
||||
#else
|
||||
// Use LWIP-specific functions
|
||||
ip6_addr_t ip6;
|
||||
inet6_aton(ip_address, &ip6);
|
||||
if (inet6_aton(ip_address, &ip6) == 0) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
|
||||
#endif
|
||||
return sizeof(sockaddr_in6);
|
||||
@@ -195,6 +201,47 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
|
||||
return sizeof(sockaddr_in);
|
||||
}
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
ConnectPollResult poll_connect(Socket &sock, int &err_out) {
|
||||
int fd = sock.get_fd();
|
||||
if (fd < 0 || fd >= FD_SETSIZE) {
|
||||
// FD_SET on either is undefined behavior
|
||||
err_out = EBADF;
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
|
||||
}
|
||||
// Connect completion is a write event; the main loop only selects on reads
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
struct timeval tv = {0, 0};
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
// LWIP_COMPAT_SOCKETS may be off (LibreTiny), so use the lwip symbol directly
|
||||
int ret = lwip_select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
#else
|
||||
// Global-scope select: the entity namespace esphome::select shadows it here
|
||||
int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
#endif
|
||||
if (ret < 0) {
|
||||
err_out = errno;
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
|
||||
}
|
||||
if (ret == 0) {
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_PENDING;
|
||||
}
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
if (sock.getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) != 0) {
|
||||
err_out = errno;
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
|
||||
}
|
||||
if (error != 0) {
|
||||
err_out = error;
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
|
||||
}
|
||||
return ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED;
|
||||
}
|
||||
#endif
|
||||
|
||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {
|
||||
#if USE_NETWORK_IPV6
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
|
||||
@@ -145,6 +145,14 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
|
||||
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
|
||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
|
||||
|
||||
/// Poll a connect() that returned EINPROGRESS. On error, err_out is SO_ERROR (or
|
||||
/// errno) on fd implementations and the failure the callbacks recorded on raw lwip.
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
inline ConnectPollResult poll_connect(Socket &sock, int &err_out) { return sock.poll_connect(err_out); }
|
||||
#else
|
||||
ConnectPollResult poll_connect(Socket &sock, int &err_out);
|
||||
#endif
|
||||
|
||||
/// Format sockaddr into caller-provided buffer, returns length written (excluding null)
|
||||
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, sensor
|
||||
from esphome.components.const import CONF_HOST
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BINARY_SENSORS,
|
||||
@@ -14,7 +15,6 @@ AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@Links2004"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
CONF_HOST = "host"
|
||||
CONF_PREFIX = "prefix"
|
||||
|
||||
statsd_component_ns = cg.esphome_ns.namespace("statsd")
|
||||
|
||||
@@ -13,7 +13,12 @@ void UDPComponent::setup() {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
for (const auto &address : this->addresses_) {
|
||||
struct sockaddr saddr {};
|
||||
socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_);
|
||||
if (socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_) == 0) {
|
||||
ESP_LOGW(TAG, "Invalid address %s", address);
|
||||
// A dropped address silently receives nothing; surface the misconfiguration
|
||||
this->status_set_warning(LOG_STR("invalid address"));
|
||||
continue;
|
||||
}
|
||||
this->sockaddrs_.push_back(saddr);
|
||||
}
|
||||
// set up broadcast socket
|
||||
@@ -94,7 +99,11 @@ void UDPComponent::setup() {
|
||||
// 8266 and RP2040 `Duino
|
||||
for (const auto &address : this->addresses_) {
|
||||
auto ipaddr = IPAddress();
|
||||
ipaddr.fromString(address);
|
||||
if (!ipaddr.fromString(address)) {
|
||||
ESP_LOGW(TAG, "Invalid address %s", address);
|
||||
this->status_set_warning(LOG_STR("invalid address"));
|
||||
continue;
|
||||
}
|
||||
this->ipaddrs_.push_back(ipaddr);
|
||||
}
|
||||
if (this->should_listen_)
|
||||
|
||||
@@ -34,6 +34,10 @@ void WakeOnLanButton::press_action() {
|
||||
struct sockaddr_storage saddr {};
|
||||
auto addr_len =
|
||||
socket::set_sockaddr(reinterpret_cast<sockaddr *>(&saddr), sizeof(saddr), "255.255.255.255", this->port_);
|
||||
if (addr_len == 0) {
|
||||
ESP_LOGW(TAG, "Invalid broadcast address");
|
||||
return;
|
||||
}
|
||||
uint8_t buffer[6 + sizeof this->macaddr_ * 16];
|
||||
memcpy(buffer, PREFIX, sizeof(PREFIX));
|
||||
for (size_t i = 0; i != 16; i++) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@dckiller51"]
|
||||
@@ -0,0 +1,93 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, ble_device_base, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BINDKEY,
|
||||
CONF_ID,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_WEIGHT,
|
||||
DEVICE_CLASS_WEIGHT,
|
||||
ICON_HEART_PULSE,
|
||||
ICON_OMEGA,
|
||||
ICON_SCALE_BATHROOM,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_BEATS_PER_MINUTE,
|
||||
UNIT_KILOGRAM,
|
||||
UNIT_OHM,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_IMPEDANCE_LOW = "impedance_low"
|
||||
CONF_IMPEDANCE_HIGH = "impedance_high"
|
||||
CONF_HEART_RATE = "heart_rate"
|
||||
CONF_PROFILE_ID = "profile_id"
|
||||
CONF_STABILIZED = "stabilized"
|
||||
|
||||
AUTO_LOAD = ["ble_device_base", "binary_sensor"]
|
||||
|
||||
xiaomi_body_scale_ns = cg.esphome_ns.namespace("xiaomi_body_scale")
|
||||
XiaomiBodyScale = xiaomi_body_scale_ns.class_(
|
||||
"XiaomiBodyScale", ble_device_base.ESPBTDeviceListener, cg.Component
|
||||
)
|
||||
|
||||
IMPEDANCE_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_OHM,
|
||||
icon=ICON_OMEGA,
|
||||
accuracy_decimals=1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(XiaomiBodyScale),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Required(CONF_BINDKEY): cv.bind_key,
|
||||
cv.Optional(CONF_WEIGHT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOGRAM,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_WEIGHT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_IMPEDANCE_LOW): IMPEDANCE_SCHEMA,
|
||||
cv.Optional(CONF_IMPEDANCE_HIGH): IMPEDANCE_SCHEMA,
|
||||
cv.Optional(CONF_HEART_RATE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BEATS_PER_MINUTE,
|
||||
icon=ICON_HEART_PULSE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PROFILE_ID): sensor.sensor_schema(
|
||||
icon="mdi:identifier",
|
||||
accuracy_decimals=0,
|
||||
),
|
||||
cv.Optional(CONF_STABILIZED): binary_sensor.binary_sensor_schema(
|
||||
icon=ICON_SCALE_BATHROOM,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(ble_device_base.BLE_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], config[CONF_MAC_ADDRESS].as_hex, config[CONF_BINDKEY]
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await ble_device_base.register_ble_device(var, config)
|
||||
|
||||
for key, setter in (
|
||||
(CONF_WEIGHT, var.set_weight),
|
||||
(CONF_IMPEDANCE_LOW, var.set_impedance_low),
|
||||
(CONF_IMPEDANCE_HIGH, var.set_impedance_high),
|
||||
(CONF_HEART_RATE, var.set_heart_rate),
|
||||
(CONF_PROFILE_ID, var.set_profile_id),
|
||||
):
|
||||
if (conf := config.get(key)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(setter(sens))
|
||||
if (conf := config.get(CONF_STABILIZED)) is not None:
|
||||
sens = await binary_sensor.new_binary_sensor(conf)
|
||||
cg.add(var.set_stabilized(sens))
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "xiaomi_body_scale.h"
|
||||
#include "esphome/components/ble_device_base/ble_aes_ccm.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::xiaomi_body_scale {
|
||||
|
||||
static const char *const TAG = "xiaomi_body_scale";
|
||||
|
||||
// Encrypted MiBeacon frame without a MAC: frame control, device id, frame count, cipher, counter, tag
|
||||
static constexpr size_t DEVICE_ID_POS = 2;
|
||||
static constexpr size_t FRAME_COUNT_POS = 4;
|
||||
static constexpr size_t CIPHER_POS = 5;
|
||||
static constexpr size_t CIPHER_SIZE = 12;
|
||||
static constexpr size_t COUNTER_POS = CIPHER_POS + CIPHER_SIZE;
|
||||
static constexpr size_t TAG_POS = COUNTER_POS + 3;
|
||||
static constexpr size_t TAG_SIZE = 4;
|
||||
static constexpr size_t FRAME_SIZE = TAG_POS + TAG_SIZE;
|
||||
// Frame control: has data (0x40) and encrypted (0x08)
|
||||
static constexpr uint8_t FRAME_ENCRYPTED_DATA = 0x48;
|
||||
static constexpr uint8_t OBJECT_SIZE = 9;
|
||||
static constexpr uint16_t OBJECT_S200_MEASUREMENT = 0x4E16;
|
||||
static constexpr uint16_t OBJECT_S400_MEASUREMENT = 0x6E16;
|
||||
static constexpr uint32_t STABILIZED_RESET_ID = 0;
|
||||
static constexpr uint32_t STABILIZED_RESET_MS = 1000;
|
||||
|
||||
XiaomiBodyScale::XiaomiBodyScale(uint64_t address, const char *bindkey) : address_(address) {
|
||||
parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_));
|
||||
}
|
||||
|
||||
void XiaomiBodyScale::dump_config() {
|
||||
uint8_t mac[MAC_ADDRESS_SIZE];
|
||||
ble_device_base::uint64_to_mac_msb_first(this->address_, mac);
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Xiaomi Body Scale\n"
|
||||
" MAC Address: %s",
|
||||
format_mac_addr_upper(mac, mac_buf));
|
||||
LOG_SENSOR(" ", "Weight", this->weight_);
|
||||
LOG_SENSOR(" ", "Impedance Low (50 kHz)", this->impedance_low_);
|
||||
LOG_SENSOR(" ", "Impedance High (250 kHz)", this->impedance_high_);
|
||||
LOG_SENSOR(" ", "Heart Rate", this->heart_rate_);
|
||||
LOG_SENSOR(" ", "Profile ID", this->profile_id_);
|
||||
LOG_BINARY_SENSOR(" ", "Stabilized", this->stabilized_);
|
||||
}
|
||||
|
||||
bool XiaomiBodyScale::decrypt_(const uint8_t *frame, uint8_t *plaintext) const {
|
||||
uint8_t nonce[MAC_ADDRESS_SIZE + 6];
|
||||
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++)
|
||||
nonce[i] = static_cast<uint8_t>(this->address_ >> (i * 8)); // MAC, reversed
|
||||
memcpy(nonce + MAC_ADDRESS_SIZE, frame + DEVICE_ID_POS, 3); // device id + frame count
|
||||
memcpy(nonce + MAC_ADDRESS_SIZE + 3, frame + COUNTER_POS, 3);
|
||||
static constexpr uint8_t AUTH_DATA[1] = {0x11};
|
||||
return ble_device_base::aes_ccm_auth_decrypt(this->bindkey_, nonce, sizeof(nonce), AUTH_DATA, sizeof(AUTH_DATA),
|
||||
frame + CIPHER_POS, CIPHER_SIZE, plaintext, frame + TAG_POS, TAG_SIZE);
|
||||
}
|
||||
|
||||
void XiaomiBodyScale::publish_stabilized_(bool stabilized) {
|
||||
if (this->stabilized_ == nullptr)
|
||||
return;
|
||||
this->stabilized_->publish_state(stabilized);
|
||||
// Clear it again so the next measurement is seen as a new one
|
||||
if (stabilized)
|
||||
this->set_timeout(STABILIZED_RESET_ID, STABILIZED_RESET_MS, [this]() { this->stabilized_->publish_state(false); });
|
||||
}
|
||||
|
||||
void XiaomiBodyScale::publish_s400_(uint32_t packed) {
|
||||
// Weight x10 (11 bits), heart rate - 50 (7 bits), impedance x10 (14 bits). A measurement sends weight, heart
|
||||
// rate and the 50 kHz impedance (the larger value), then a packet with only the 250 kHz impedance.
|
||||
const uint16_t weight = packed & 0x7FF;
|
||||
const uint8_t heart_rate = (packed >> 11) & 0x7F;
|
||||
const uint16_t impedance = packed >> 18;
|
||||
ESP_LOGD(TAG, "weight=%u heart_rate=%u impedance=%u", weight, heart_rate, impedance);
|
||||
|
||||
if (weight != 0 && this->weight_ != nullptr)
|
||||
this->weight_->publish_state(weight / 10.0f);
|
||||
if (heart_rate > 0 && heart_rate < 127 && this->heart_rate_ != nullptr)
|
||||
this->heart_rate_->publish_state(heart_rate + 50.0f);
|
||||
|
||||
if (weight == 0 && heart_rate == 0) {
|
||||
// Impedance only: the final 250 kHz packet (bare feet); all zero: stepped off
|
||||
if (impedance != 0 && this->impedance_high_ != nullptr)
|
||||
this->impedance_high_->publish_state(impedance / 10.0f);
|
||||
this->publish_stabilized_(impedance != 0);
|
||||
} else if (impedance != 0) {
|
||||
if (this->impedance_low_ != nullptr)
|
||||
this->impedance_low_->publish_state(impedance / 10.0f);
|
||||
this->publish_stabilized_(false);
|
||||
} else {
|
||||
// Weight without impedance: measurement complete (with socks)
|
||||
this->publish_stabilized_(true);
|
||||
}
|
||||
}
|
||||
|
||||
bool XiaomiBodyScale::parse_device(const ble_device_base::ESPBTDevice &device) {
|
||||
if (device.address_uint64() != this->address_)
|
||||
return false;
|
||||
|
||||
for (const auto &service_data : device.get_service_datas()) {
|
||||
if (service_data.data.size() != FRAME_SIZE || !service_data.uuid.contains(0x95, 0xFE))
|
||||
continue;
|
||||
const uint8_t *frame = service_data.data.data();
|
||||
// The bindkey is required, so plaintext frames are never trusted
|
||||
if ((frame[0] & FRAME_ENCRYPTED_DATA) != FRAME_ENCRYPTED_DATA)
|
||||
continue;
|
||||
|
||||
if (frame[FRAME_COUNT_POS] == this->last_frame_count_)
|
||||
continue;
|
||||
uint8_t object[CIPHER_SIZE];
|
||||
if (!this->decrypt_(frame, object)) {
|
||||
ESP_LOGW(TAG, "Decryption failed, wrong bindkey?");
|
||||
continue;
|
||||
}
|
||||
// Only an authenticated frame may advance the duplicate filter
|
||||
this->last_frame_count_ = frame[FRAME_COUNT_POS];
|
||||
|
||||
// Both objects hold the profile ID, a packed uint32 (LE) and a timestamp (not published)
|
||||
const uint16_t value_type = encode_uint16(object[1], object[0]);
|
||||
const uint8_t *data = object + 3;
|
||||
const uint32_t packed = encode_uint32(data[4], data[3], data[2], data[1]);
|
||||
const bool sized = object[2] == OBJECT_SIZE;
|
||||
if (sized && value_type == OBJECT_S400_MEASUREMENT) {
|
||||
this->publish_s400_(packed);
|
||||
} else if (sized && value_type == OBJECT_S200_MEASUREMENT) {
|
||||
// Weight x100 only
|
||||
if (packed != 0 && this->weight_ != nullptr)
|
||||
this->weight_->publish_state(packed / 100.0f);
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "Unknown object 0x%04X, length %u", value_type, object[2]);
|
||||
continue;
|
||||
}
|
||||
if (this->profile_id_ != nullptr)
|
||||
this->profile_id_->publish_state(data[0]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace esphome::xiaomi_body_scale
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
|
||||
namespace esphome::xiaomi_body_scale {
|
||||
|
||||
class XiaomiBodyScale final : public Component, public ble_device_base::ESPBTDeviceListener {
|
||||
public:
|
||||
XiaomiBodyScale(uint64_t address, const char *bindkey);
|
||||
|
||||
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_weight(sensor::Sensor *weight) { this->weight_ = weight; }
|
||||
void set_impedance_low(sensor::Sensor *impedance_low) { this->impedance_low_ = impedance_low; }
|
||||
void set_impedance_high(sensor::Sensor *impedance_high) { this->impedance_high_ = impedance_high; }
|
||||
void set_heart_rate(sensor::Sensor *heart_rate) { this->heart_rate_ = heart_rate; }
|
||||
void set_profile_id(sensor::Sensor *profile_id) { this->profile_id_ = profile_id; }
|
||||
void set_stabilized(binary_sensor::BinarySensor *stabilized) { this->stabilized_ = stabilized; }
|
||||
|
||||
protected:
|
||||
bool decrypt_(const uint8_t *frame, uint8_t *plaintext) const;
|
||||
void publish_s400_(uint32_t packed);
|
||||
void publish_stabilized_(bool stabilized);
|
||||
|
||||
uint64_t address_;
|
||||
uint8_t bindkey_[16];
|
||||
sensor::Sensor *weight_{nullptr};
|
||||
sensor::Sensor *impedance_low_{nullptr};
|
||||
sensor::Sensor *impedance_high_{nullptr};
|
||||
sensor::Sensor *heart_rate_{nullptr};
|
||||
sensor::Sensor *profile_id_{nullptr};
|
||||
binary_sensor::BinarySensor *stabilized_{nullptr};
|
||||
uint16_t last_frame_count_{0x100}; // outside the 8-bit range, so the first frame always passes
|
||||
};
|
||||
|
||||
} // namespace esphome::xiaomi_body_scale
|
||||
@@ -521,12 +521,19 @@ ESP32_PLATFORMIO_TEST_COMPONENTS = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
# Shared by every toolchain smoke-test job: the base config and the bus
|
||||
# packages each generated build includes
|
||||
_SMOKE_HARNESS_TRIGGER_PATH_PREFIXES = ("tests/test_build_components/",)
|
||||
|
||||
# Path prefixes whose changes always trigger the PlatformIO compile test:
|
||||
# anything under esphome/platformio/ (the PlatformIO runner / toolchain that
|
||||
# drives every PlatformIO build). The esp32 platform component is already in
|
||||
# ESP32_PLATFORMIO_TEST_COMPONENTS, so its changes are covered by the normal
|
||||
# component-narrowing path.
|
||||
ESP32_PLATFORMIO_TRIGGER_PATH_PREFIXES = ("esphome/platformio/",)
|
||||
ESP32_PLATFORMIO_TRIGGER_PATH_PREFIXES = (
|
||||
"esphome/platformio/",
|
||||
*_SMOKE_HARNESS_TRIGGER_PATH_PREFIXES,
|
||||
)
|
||||
|
||||
# Standalone files that, when changed, trigger the PlatformIO compile test:
|
||||
# - esphome/build_gen/platformio.py -- the PlatformIO build generator
|
||||
@@ -678,6 +685,7 @@ ESP8266_NATIVE_TRIGGER_PATH_PREFIXES = (
|
||||
"esphome/arduino8266/",
|
||||
"esphome/arduino/",
|
||||
"esphome/build_helpers/",
|
||||
*_SMOKE_HARNESS_TRIGGER_PATH_PREFIXES,
|
||||
)
|
||||
# Shared library-conversion modules every native build imports; espidf-only
|
||||
# infra (build_gen/espidf.py) deliberately stays out of the esp8266 set.
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Tests for the light `color` option."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.light.automation import (
|
||||
LIGHT_CONTROL_ACTION_SCHEMA,
|
||||
LIGHT_STATE_SCHEMA,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_BLUE,
|
||||
CONF_COLOR_BRIGHTNESS,
|
||||
CONF_GREEN,
|
||||
CONF_ID,
|
||||
CONF_RED,
|
||||
)
|
||||
|
||||
LOGGER_NAME: str = "esphome.components.light.automation"
|
||||
|
||||
|
||||
def test_color_name_sets_rgb() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": "Tomato"})
|
||||
assert "color" not in result
|
||||
assert result[CONF_RED] == 1.0
|
||||
assert result[CONF_GREEN] == pytest.approx(0x63 / 0xFF)
|
||||
assert result[CONF_BLUE] == pytest.approx(0x47 / 0xFF)
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
|
||||
|
||||
|
||||
def test_color_name_in_control_action() -> None:
|
||||
result = LIGHT_CONTROL_ACTION_SCHEMA({CONF_ID: "test_light", "color": "blue"})
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (0, 0, 1.0)
|
||||
|
||||
|
||||
def test_unknown_color_name() -> None:
|
||||
with pytest.raises(cv.Invalid, match="notacolor"):
|
||||
LIGHT_STATE_SCHEMA({"color": "notacolor"})
|
||||
|
||||
|
||||
def test_color_name_conflicts_with_rgb() -> None:
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with"):
|
||||
LIGHT_STATE_SCHEMA({"color": "red", CONF_GREEN: 0.5})
|
||||
|
||||
|
||||
def test_dark_color_sets_color_brightness() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": "darkred"})
|
||||
assert result[CONF_RED] == 1.0
|
||||
assert result[CONF_GREEN] == 0.0
|
||||
assert result[CONF_BLUE] == 0.0
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x8B / 0xFF)
|
||||
|
||||
|
||||
def test_black_sets_zero_color_brightness() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": "black"})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 0.0
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (0, 0, 0)
|
||||
|
||||
|
||||
def test_explicit_color_brightness_wins(caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger=LOGGER_NAME):
|
||||
result = LIGHT_STATE_SCHEMA({"color": "darkred", CONF_COLOR_BRIGHTNESS: 0.25})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 0.25
|
||||
assert "overrides the brightness of color 'darkred'" in caplog.text
|
||||
|
||||
|
||||
def test_explicit_color_brightness_full_color(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger=LOGGER_NAME):
|
||||
result = LIGHT_STATE_SCHEMA({"color": "red", CONF_COLOR_BRIGHTNESS: 0.25})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 0.25
|
||||
assert not [r for r in caplog.records if r.name == LOGGER_NAME]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0x223344, "0x223344", "0X223344"])
|
||||
def test_hex_color(value: int | str) -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": value})
|
||||
assert "color" not in result
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x44 / 0xFF)
|
||||
assert result[CONF_BLUE] == 1.0
|
||||
assert result[CONF_RED] == pytest.approx(0x22 / 0x44)
|
||||
assert result[CONF_GREEN] == pytest.approx(0x33 / 0x44)
|
||||
|
||||
|
||||
def test_hex_color_full_level() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": 0xFF8000})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
|
||||
assert result[CONF_RED] == 1.0
|
||||
assert result[CONF_GREEN] == pytest.approx(0x80 / 0xFF)
|
||||
assert result[CONF_BLUE] == 0.0
|
||||
|
||||
|
||||
def test_hex_color_black() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": 0})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 0.0
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (0, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0x1000000, -1, "0xGG", "0x1000000"])
|
||||
def test_hex_color_invalid(value: int | str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
LIGHT_STATE_SCHEMA({"color": value})
|
||||
|
||||
|
||||
def test_hex_color_conflicts_with_rgb() -> None:
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with"):
|
||||
LIGHT_STATE_SCHEMA({"color": 0x223344, CONF_RED: 0.5})
|
||||
|
||||
|
||||
def test_explicit_color_brightness_warning_names_hex_color(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger=LOGGER_NAME):
|
||||
LIGHT_STATE_SCHEMA({"color": 0x223344, CONF_COLOR_BRIGHTNESS: 0.25})
|
||||
assert "overrides the brightness of color '0x223344'" in caplog.text
|
||||
@@ -85,6 +85,12 @@ esphome:
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 1.0
|
||||
- light.turn_on:
|
||||
id: test_rgb_light
|
||||
color: Tomato
|
||||
- light.turn_on:
|
||||
id: test_rgb_light
|
||||
color: 0x223344
|
||||
- light.control:
|
||||
id: test_monochromatic_light
|
||||
state: on
|
||||
@@ -157,6 +163,17 @@ esphome:
|
||||
green: 100%
|
||||
blue: 0%
|
||||
transition_length: 1s
|
||||
- light.addressable_set:
|
||||
id: test_addressable_transition
|
||||
range_from: 0
|
||||
range_to: 4
|
||||
color: darkred
|
||||
- light.addressable_set:
|
||||
id: test_addressable_transition
|
||||
color: tomato
|
||||
- light.addressable_set:
|
||||
id: test_addressable_transition
|
||||
color: 0x223344
|
||||
|
||||
# Exercise light actions inside a trigger with non-empty Ts (number on_value
|
||||
# passes float).
|
||||
@@ -336,6 +353,15 @@ light:
|
||||
red: 100%
|
||||
green: 50%
|
||||
blue: 50%
|
||||
# Test initial_state: with a color name (a dark color also sets color_brightness)
|
||||
- platform: rgb
|
||||
id: test_rgb_light_initial_state_color
|
||||
name: RGB Light Initial State Color
|
||||
red: test_ledc_1
|
||||
green: test_ledc_2
|
||||
blue: test_ledc_3
|
||||
initial_state:
|
||||
color: darkred
|
||||
# Test initial_state: with every templatable field set via !lambda (color_mode is
|
||||
# the only field that isn't templatable, so it's the only one left as a literal)
|
||||
- platform: rgbww
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/mk2pvrouter/binary_sensor/mk2pvrouter_binary_sensor.h"
|
||||
|
||||
namespace esphome::mk2pvrouter::testing {
|
||||
|
||||
TEST(Mk2PVRouterBinarySensorTest, ZeroPublishesOff) {
|
||||
Mk2PVRouterBinarySensor sensor("R1");
|
||||
sensor.publish_val("0");
|
||||
ASSERT_TRUE(sensor.has_state());
|
||||
EXPECT_FALSE(sensor.state);
|
||||
}
|
||||
|
||||
TEST(Mk2PVRouterBinarySensorTest, OnePublishesOn) {
|
||||
Mk2PVRouterBinarySensor sensor("R1");
|
||||
sensor.publish_val("1");
|
||||
ASSERT_TRUE(sensor.has_state());
|
||||
EXPECT_TRUE(sensor.state);
|
||||
}
|
||||
|
||||
} // namespace esphome::mk2pvrouter::testing
|
||||
@@ -59,3 +59,22 @@ sensor:
|
||||
name: Relay 10 State
|
||||
tag: R10
|
||||
mk2pvrouter_id: test_mk2pvrouter
|
||||
|
||||
binary_sensor:
|
||||
# Test relay as binary sensor
|
||||
- platform: mk2pvrouter
|
||||
name: Relay 2 Binary
|
||||
tag: R2
|
||||
mk2pvrouter_id: test_mk2pvrouter
|
||||
|
||||
text_sensor:
|
||||
- platform: mk2pvrouter
|
||||
name: S per MC
|
||||
tag: S_MC
|
||||
mk2pvrouter_id: test_mk2pvrouter
|
||||
|
||||
# Test another text tag
|
||||
- platform: mk2pvrouter
|
||||
name: Status
|
||||
tag: STATUS
|
||||
mk2pvrouter_id: test_mk2pvrouter
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/mk2pvrouter/text_sensor/mk2pvrouter_text_sensor.h"
|
||||
|
||||
namespace esphome::mk2pvrouter::testing {
|
||||
|
||||
TEST(Mk2PVRouterTextSensorTest, PublishesValueVerbatim) {
|
||||
Mk2PVRouterTextSensor sensor("S_MC");
|
||||
sensor.publish_val("-1234");
|
||||
ASSERT_TRUE(sensor.has_state());
|
||||
EXPECT_EQ(sensor.get_state(), "-1234");
|
||||
}
|
||||
|
||||
TEST(Mk2PVRouterTextSensorTest, ShorterValueReplacesPreviousOne) {
|
||||
Mk2PVRouterTextSensor sensor("S_MC");
|
||||
sensor.publish_val("-1234");
|
||||
sensor.publish_val("5");
|
||||
EXPECT_EQ(sensor.get_state(), "5");
|
||||
}
|
||||
|
||||
} // namespace esphome::mk2pvrouter::testing
|
||||
@@ -0,0 +1,194 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include "esphome/components/pn71xx/pn71xx.h"
|
||||
|
||||
namespace esphome::pn71xx {
|
||||
|
||||
namespace {
|
||||
|
||||
// Stands in for the bus: records every frame written and replays queued frames on read.
|
||||
class FakePN71xx : public PN71xx {
|
||||
public:
|
||||
using PN71xx::card_emu_t4t_get_response_;
|
||||
using PN71xx::transceive_;
|
||||
|
||||
std::deque<std::vector<uint8_t>> to_read;
|
||||
std::vector<std::vector<uint8_t>> written;
|
||||
uint8_t write_failures{0};
|
||||
|
||||
protected:
|
||||
uint8_t verify_reset(nfc::NciMessage &rx, bool reset_config) override { return nfc::STATUS_OK; }
|
||||
uint8_t process_init_response(nfc::NciMessage &rx) override { return nfc::STATUS_OK; }
|
||||
std::span<const uint8_t> pmu_config() const override { return {}; }
|
||||
std::span<const uint8_t> listen_mode_routing_config() const override { return {}; }
|
||||
|
||||
uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override {
|
||||
if (this->to_read.empty())
|
||||
return nfc::STATUS_FAILED;
|
||||
rx = nfc::NciMessage(this->to_read.front());
|
||||
this->to_read.pop_front();
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
uint8_t write_nfcc(nfc::NciMessage &tx) override {
|
||||
if (this->write_failures > 0) {
|
||||
this->write_failures--;
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
this->written.push_back(tx.encode());
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<uint8_t> apdu(std::initializer_list<uint8_t> bytes) {
|
||||
std::vector<uint8_t> msg = {nfc::NCI_PKT_MT_DATA, 0x00, static_cast<uint8_t>(bytes.size())};
|
||||
msg.insert(msg.end(), bytes);
|
||||
return msg;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> respond(FakePN71xx &nfcc, std::initializer_list<uint8_t> bytes) {
|
||||
std::vector<uint8_t> response;
|
||||
nfcc.card_emu_t4t_get_response_(apdu(bytes), response);
|
||||
return response;
|
||||
}
|
||||
|
||||
void select_ndef_file(FakePN71xx &nfcc) {
|
||||
respond(nfcc, {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00});
|
||||
respond(nfcc, {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04});
|
||||
}
|
||||
|
||||
const std::vector<uint8_t> SW_OK = {0x90, 0x00};
|
||||
const std::vector<uint8_t> SW_NOT_FOUND = {0x6A, 0x82};
|
||||
|
||||
} // namespace
|
||||
|
||||
// A timed-out read must not cause the command to be sent again (NCI forbids a second command before the response).
|
||||
TEST(PN71xxTransceive, ReadTimeoutDoesNotResend) {
|
||||
FakePN71xx nfcc;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {0x00});
|
||||
nfc::NciMessage rx;
|
||||
EXPECT_NE(nfcc.transceive_(tx, rx), nfc::STATUS_OK);
|
||||
EXPECT_EQ(nfcc.written.size(), 1u);
|
||||
}
|
||||
|
||||
// A notification with the same GID/OID as the response (RF_DEACTIVATE_NTF) is not mistaken for it.
|
||||
TEST(PN71xxTransceive, SkipsNotificationAheadOfResponse) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.to_read.push_back({0x61, 0x06, 0x02, 0x00, 0x00});
|
||||
nfcc.to_read.push_back({0x41, 0x06, 0x01, 0x00});
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {0x00});
|
||||
nfc::NciMessage rx;
|
||||
EXPECT_EQ(nfcc.transceive_(tx, rx), nfc::STATUS_OK);
|
||||
EXPECT_EQ(rx.get_message(), (std::vector<uint8_t>{0x41, 0x06, 0x01, 0x00}));
|
||||
EXPECT_EQ(nfcc.written.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(PN71xxTransceive, NotificationAloneIsNotAResponse) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.to_read.push_back({0x61, 0x06, 0x02, 0x00, 0x00});
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {0x00});
|
||||
nfc::NciMessage rx;
|
||||
EXPECT_NE(nfcc.transceive_(tx, rx), nfc::STATUS_OK);
|
||||
}
|
||||
|
||||
// A late response to an earlier, timed-out command must not be taken as the response to this one.
|
||||
TEST(PN71xxTransceive, SkipsStaleResponseFromEarlierCommand) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.to_read.push_back({0x41, 0x06, 0x01, 0x00}); // RF_DEACTIVATE_RSP, arriving late
|
||||
nfcc.to_read.push_back({0x41, 0x03, 0x01, 0x00}); // RF_DISCOVER_RSP
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, {0x00});
|
||||
nfc::NciMessage rx;
|
||||
EXPECT_EQ(nfcc.transceive_(tx, rx), nfc::STATUS_OK);
|
||||
EXPECT_EQ(rx.get_message(), (std::vector<uint8_t>{0x41, 0x03, 0x01, 0x00}));
|
||||
EXPECT_EQ(nfcc.written.size(), 1u);
|
||||
}
|
||||
|
||||
// A refused write (e.g. NFCC in standby) is sent again.
|
||||
TEST(PN71xxTransceive, RefusedWriteIsRetried) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.write_failures = 1;
|
||||
nfcc.to_read.push_back({0x41, 0x06, 0x01, 0x00});
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {0x00});
|
||||
nfc::NciMessage rx;
|
||||
EXPECT_EQ(nfcc.transceive_(tx, rx), nfc::STATUS_OK);
|
||||
EXPECT_EQ(nfcc.written.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(PN71xxCardEmulation, CcReadOutOfRangeIsRejected) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.set_tag_emulation_message("https://www.home-assistant.io/tag/test", false);
|
||||
respond(nfcc, {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00});
|
||||
respond(nfcc, {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03});
|
||||
// the CC file is 15 bytes; reading 17 or reading far past its end must not return memory beyond it
|
||||
EXPECT_EQ(respond(nfcc, {0x00, 0xB0, 0x00, 0x00, 0x11}), SW_NOT_FOUND);
|
||||
respond(nfcc, {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00});
|
||||
respond(nfcc, {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03});
|
||||
EXPECT_EQ(respond(nfcc, {0x00, 0xB0, 0x01, 0x00, 0x0F}), SW_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST(PN71xxCardEmulation, CcReadInRange) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.set_tag_emulation_message("https://www.home-assistant.io/tag/test", false);
|
||||
respond(nfcc, {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00});
|
||||
respond(nfcc, {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03});
|
||||
auto response = respond(nfcc, {0x00, 0xB0, 0x00, 0x00, 0x0F});
|
||||
ASSERT_EQ(response.size(), sizeof(CARD_EMU_T4T_CC) + 2);
|
||||
EXPECT_TRUE(std::equal(std::begin(CARD_EMU_T4T_CC), std::end(CARD_EMU_T4T_CC), response.begin()));
|
||||
}
|
||||
|
||||
// Reading the NDEF file in small chunks returns NLEN followed by the message, in order.
|
||||
TEST(PN71xxCardEmulation, ChunkedNdefReadMatchesFile) {
|
||||
FakePN71xx nfcc;
|
||||
auto message = std::make_shared<nfc::NdefMessage>();
|
||||
message->add_uri_record("https://www.home-assistant.io/tag/0123456789abcdef");
|
||||
const auto encoded = message->encode();
|
||||
nfcc.set_tag_emulation_message(message);
|
||||
select_ndef_file(nfcc);
|
||||
|
||||
std::vector<uint8_t> expected = {static_cast<uint8_t>(encoded.size() >> 8),
|
||||
static_cast<uint8_t>(encoded.size() & 0xFF)};
|
||||
expected.insert(expected.end(), encoded.begin(), encoded.end());
|
||||
|
||||
std::vector<uint8_t> file;
|
||||
for (size_t offset = 0; offset < expected.size(); offset += 5) {
|
||||
const uint8_t length = std::min<size_t>(5, expected.size() - offset);
|
||||
auto response =
|
||||
respond(nfcc, {0x00, 0xB0, static_cast<uint8_t>(offset >> 8), static_cast<uint8_t>(offset), length});
|
||||
ASSERT_EQ(response.size(), length + 2u);
|
||||
EXPECT_EQ(std::vector<uint8_t>(response.end() - 2, response.end()), SW_OK);
|
||||
file.insert(file.end(), response.begin(), response.end() - 2);
|
||||
}
|
||||
EXPECT_EQ(file, expected);
|
||||
}
|
||||
|
||||
TEST(PN71xxCardEmulation, NdefReadPastEndIsRejected) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.set_tag_emulation_message("https://www.home-assistant.io/tag/test", false);
|
||||
select_ndef_file(nfcc);
|
||||
EXPECT_EQ(respond(nfcc, {0x00, 0xB0, 0x00, 0x02, 0xFD}), SW_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST(PN71xxCardEmulation, TruncatedApdusAreRejected) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.set_tag_emulation_message("https://www.home-assistant.io/tag/test", false);
|
||||
select_ndef_file(nfcc);
|
||||
EXPECT_EQ(respond(nfcc, {0x00, 0xB0, 0x00}), SW_NOT_FOUND);
|
||||
select_ndef_file(nfcc);
|
||||
// UPDATE BINARY claiming 16 bytes of data but carrying only 2
|
||||
EXPECT_EQ(respond(nfcc, {0x00, 0xD6, 0x00, 0x00, 0x10, 0x00, 0x00}), SW_NOT_FOUND);
|
||||
}
|
||||
|
||||
// A message too large for the emulated NDEF file is refused when it is set, keeping the previous one.
|
||||
TEST(PN71xxCardEmulation, OversizedMessageRejectedWhenSet) {
|
||||
FakePN71xx nfcc;
|
||||
nfcc.set_tag_emulation_message("https://www.home-assistant.io/tag/test", false);
|
||||
nfcc.set_tag_emulation_message("https://www.home-assistant.io/tag/" + std::string(300, 'x'), false);
|
||||
select_ndef_file(nfcc);
|
||||
auto response = respond(nfcc, {0x00, 0xB0, 0x00, 0x00, 0x02});
|
||||
ASSERT_EQ(response.size(), 4u);
|
||||
EXPECT_LT((response[0] << 8) | response[1], 0xFF - 2);
|
||||
EXPECT_EQ(std::vector<uint8_t>(response.end() - 2, response.end()), SW_OK);
|
||||
}
|
||||
|
||||
} // namespace esphome::pn71xx
|
||||
@@ -26,6 +26,11 @@ esphome:
|
||||
- snapshot.take:
|
||||
id: snapshot_rotated
|
||||
filename: !lambda 'return "rotated.bmp";'
|
||||
- snapshot.take:
|
||||
id: snapshot_display
|
||||
filename: animation.gif
|
||||
frames: 5
|
||||
frame_rate: 10fps
|
||||
|
||||
interval:
|
||||
# A generated name has the time in it, so this one can repeat.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#ifdef USE_HOST
|
||||
#include <gtest/gtest.h>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "esphome/components/snapshot/snapshot.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::snapshot::testing {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
/// A display of a fixed size that can be told to fail a capture.
|
||||
class FakeSnapshot : public Snapshot {
|
||||
public:
|
||||
/// The capture with this number (counting from 1) fails. Zero means none do.
|
||||
int fail_on{0};
|
||||
int captures{0};
|
||||
|
||||
protected:
|
||||
int snapshot_width() override { return 8; }
|
||||
int snapshot_height() override { return 4; }
|
||||
bool capture_bgr(uint8_t *dest, size_t row_stride) override {
|
||||
this->captures++;
|
||||
if (this->captures == this->fail_on)
|
||||
return false;
|
||||
for (int y = 0; y != 4; y++) {
|
||||
for (int x = 0; x != 8 * 3; x++)
|
||||
dest[y * row_stride + x] = static_cast<uint8_t>(x * 7 + y * 31 + this->captures);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class SnapshotAnimationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->dir_ = fs::temp_directory_path() / "esphome_snapshot_test";
|
||||
fs::remove_all(this->dir_);
|
||||
fs::create_directories(this->dir_);
|
||||
setenv("ESPHOME_SNAPSHOT_DIR", this->dir_.c_str(), 1);
|
||||
// The test main does not construct App as generated code does, and recording needs its scheduler.
|
||||
static const bool app_constructed = (new (&App) Application(), true);
|
||||
(void) app_constructed;
|
||||
App.pre_setup("test_snapshot", 10, "", 0);
|
||||
}
|
||||
void TearDown() override {
|
||||
unsetenv("ESPHOME_SNAPSHOT_DIR");
|
||||
fs::remove_all(this->dir_);
|
||||
}
|
||||
|
||||
/// Names of the files in the snapshot directory, in order.
|
||||
std::vector<std::string> files() const {
|
||||
std::vector<std::string> names;
|
||||
for (const auto &entry : fs::directory_iterator(this->dir_))
|
||||
names.push_back(entry.path().filename().string());
|
||||
std::sort(names.begin(), names.end());
|
||||
return names;
|
||||
}
|
||||
|
||||
/// Let the scheduler run for long enough that a fast recording has finished.
|
||||
static void run_scheduler() {
|
||||
for (int i = 0; i != 50; i++) {
|
||||
App.scheduler.call(millis());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
fs::path dir_;
|
||||
};
|
||||
|
||||
TEST_F(SnapshotAnimationTest, FailedFirstCaptureLeavesNoFile) {
|
||||
FakeSnapshot display;
|
||||
display.fail_on = 1;
|
||||
EXPECT_FALSE(display.take_animation("first.gif", 3, 50.0f));
|
||||
EXPECT_TRUE(this->files().empty());
|
||||
}
|
||||
|
||||
TEST_F(SnapshotAnimationTest, FailedCaptureMidRecordingRemovesPartialFile) {
|
||||
FakeSnapshot display;
|
||||
display.fail_on = 2;
|
||||
ASSERT_TRUE(display.take_animation("partial.gif", 3, 50.0f));
|
||||
// The first frame is already in the file.
|
||||
EXPECT_EQ(this->files(), std::vector<std::string>{"partial.gif"});
|
||||
|
||||
run_scheduler();
|
||||
EXPECT_TRUE(this->files().empty());
|
||||
// The failure ends the recording, so the display can record again.
|
||||
display.fail_on = 0;
|
||||
EXPECT_TRUE(display.take_animation("again.gif", 1, 50.0f));
|
||||
EXPECT_EQ(this->files(), std::vector<std::string>{"again.gif"});
|
||||
}
|
||||
|
||||
TEST_F(SnapshotAnimationTest, SecondRecordingIsRefusedUntilTheFirstEnds) {
|
||||
FakeSnapshot display;
|
||||
ASSERT_TRUE(display.take_animation("one.gif", 3, 50.0f));
|
||||
EXPECT_FALSE(display.take_animation("two.gif", 3, 50.0f));
|
||||
EXPECT_EQ(this->files(), std::vector<std::string>{"one.gif"});
|
||||
|
||||
run_scheduler();
|
||||
std::ifstream in(this->dir_ / "one.gif", std::ios::binary);
|
||||
std::string data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
ASSERT_FALSE(data.empty());
|
||||
EXPECT_EQ(data.back(), ';'); // the GIF trailer: the recording finished
|
||||
|
||||
EXPECT_TRUE(display.take_animation("two.gif", 1, 50.0f));
|
||||
EXPECT_EQ(this->files(), (std::vector<std::string>{"one.gif", "two.gif"}));
|
||||
}
|
||||
|
||||
TEST_F(SnapshotAnimationTest, RejectsNoFramesAndNoFrameRate) {
|
||||
FakeSnapshot display;
|
||||
EXPECT_FALSE(display.take_animation("none.gif", 0, 10.0f));
|
||||
EXPECT_FALSE(display.take_animation("none.gif", 3, 0.0f));
|
||||
EXPECT_TRUE(this->files().empty());
|
||||
}
|
||||
|
||||
} // namespace esphome::snapshot::testing
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
substitutions:
|
||||
network_enable_ipv6: "true"
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,16 @@
|
||||
sensor:
|
||||
- platform: xiaomi_body_scale
|
||||
mac_address: "AA:BB:CC:DD:EE:FF"
|
||||
bindkey: "00112233445566778899aabbccddeeff"
|
||||
weight:
|
||||
name: "Scale Weight"
|
||||
impedance_low:
|
||||
name: "Scale Impedance Low"
|
||||
impedance_high:
|
||||
name: "Scale Impedance High"
|
||||
heart_rate:
|
||||
name: "Scale Heart Rate"
|
||||
profile_id:
|
||||
name: "Scale Profile ID"
|
||||
stabilized:
|
||||
name: "Scale Stabilized"
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/xiaomi_body_scale/xiaomi_body_scale.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::xiaomi_body_scale::testing {
|
||||
|
||||
using Frame = std::array<uint8_t, 24>;
|
||||
|
||||
// Real captures and keys from the xiaomi-ble test suite (Bluetooth-Devices/xiaomi-ble)
|
||||
static constexpr uint64_t SCALE_A = 0x8CD0B2F6BEEFULL;
|
||||
static constexpr const char *KEY_A = "0728974d657a4b60964c1b1677f35f7c";
|
||||
// Weight, heart rate and the 50 kHz impedance
|
||||
static constexpr Frame PACKET_1 = {0x48, 0x59, 0xd5, 0x3b, 0x0a, 0xbc, 0x07, 0x8f, 0xf2, 0x34, 0x8c, 0x84,
|
||||
0x41, 0x38, 0xe9, 0x30, 0x22, 0x00, 0x00, 0x00, 0x9e, 0x53, 0x85, 0x99};
|
||||
// Only the 250 kHz impedance, the end of a bare feet measurement
|
||||
static constexpr Frame PACKET_2 = {0x48, 0x59, 0xd5, 0x3b, 0x0b, 0xd6, 0xef, 0x0b, 0x25, 0xdb, 0x72, 0x78,
|
||||
0x5e, 0x7e, 0x2f, 0x46, 0xd6, 0x00, 0x00, 0x00, 0xd8, 0x64, 0x2d, 0xf6};
|
||||
// PACKET_1 re-encrypted with KEY_A and frame count 0xFF
|
||||
static constexpr Frame PACKET_1_COUNT_FF = {0x48, 0x59, 0xd5, 0x3b, 0xff, 0x6c, 0x2f, 0xd8, 0xdb, 0xaa, 0x70, 0xc7,
|
||||
0xbb, 0x1f, 0x0a, 0x08, 0x9c, 0x00, 0x00, 0x00, 0x31, 0xce, 0xf1, 0x95};
|
||||
|
||||
static constexpr uint64_t SCALE_B = 0x04AE4767C67CULL;
|
||||
static constexpr const char *KEY_B = "02d2900363ef629c736a4549677acbee";
|
||||
// Weight without impedance, the end of a measurement with socks
|
||||
static constexpr Frame SOCKS = {0x48, 0x59, 0xd5, 0x3b, 0x71, 0x53, 0x04, 0x38, 0xb5, 0x89, 0x4b, 0x24,
|
||||
0x2c, 0x20, 0x99, 0x08, 0xda, 0x00, 0x00, 0x00, 0x47, 0x9e, 0xcd, 0xa3};
|
||||
// All metrics zero, stepped off the scale
|
||||
static constexpr Frame STEP_OFF = {0x48, 0x59, 0xd5, 0x3b, 0x72, 0x03, 0x6c, 0x67, 0x94, 0x35, 0x5a, 0x19,
|
||||
0xdb, 0xc8, 0x64, 0xbf, 0xb3, 0x00, 0x00, 0x00, 0xe4, 0x15, 0x1d, 0xc8};
|
||||
|
||||
// S200: weight only, 62.25 kg for profile 1
|
||||
static constexpr uint64_t SCALE_S200 = 0xD07B6F27D729ULL;
|
||||
static constexpr const char *KEY_S200 = "653b1b10e1cb35e4ac5e60fa45f3bf29";
|
||||
static constexpr Frame S200_WEIGHT = {0x48, 0x59, 0x04, 0x4c, 0x01, 0x9a, 0x80, 0xa2, 0x75, 0x93, 0x90, 0x10,
|
||||
0xf0, 0xab, 0xc4, 0xfa, 0xdc, 0x06, 0x00, 0x00, 0x3d, 0x29, 0xc0, 0x44};
|
||||
|
||||
inline ble_device_base::ESPBTDevice advert(uint64_t address, const Frame &frame) {
|
||||
// Service data AD structure for UUID 0xFE95
|
||||
std::vector<uint8_t> adv = {static_cast<uint8_t>(frame.size() + 3), 0x16, 0x95, 0xFE};
|
||||
adv.insert(adv.end(), frame.begin(), frame.end());
|
||||
uint8_t mac[6];
|
||||
for (size_t i = 0; i < 6; i++)
|
||||
mac[i] = static_cast<uint8_t>(address >> (i * 8));
|
||||
ble_device_base::ESPBTDevice device;
|
||||
device.from_scan_result(mac, -60, 0, adv.data(), static_cast<uint16_t>(adv.size()));
|
||||
return device;
|
||||
}
|
||||
|
||||
struct Harness {
|
||||
Harness(uint64_t address, const char *key) : scale(address, key) {
|
||||
// The test main does not construct App as generated code does; the stabilized reset needs its scheduler
|
||||
static const bool app_constructed = (new (&App) Application(), true);
|
||||
(void) app_constructed;
|
||||
App.pre_setup("test_scale", 10, "", 0);
|
||||
this->scale.set_weight(&this->weight);
|
||||
this->scale.set_impedance_low(&this->impedance_low);
|
||||
this->scale.set_impedance_high(&this->impedance_high);
|
||||
this->scale.set_heart_rate(&this->heart_rate);
|
||||
this->scale.set_profile_id(&this->profile_id);
|
||||
this->scale.set_stabilized(&this->stabilized);
|
||||
}
|
||||
|
||||
~Harness() {
|
||||
// Drop the pending stabilized reset (id 0) while the scale is alive; App outlives each test
|
||||
App.scheduler.cancel_timeout(&this->scale, 0u);
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
|
||||
XiaomiBodyScale scale;
|
||||
sensor::Sensor weight, impedance_low, impedance_high, heart_rate, profile_id;
|
||||
binary_sensor::BinarySensor stabilized;
|
||||
};
|
||||
|
||||
} // namespace esphome::xiaomi_body_scale::testing
|
||||
@@ -0,0 +1,27 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: xiaomi_body_scale
|
||||
ble_hub_id: ble_tracker_hub
|
||||
mac_address: "AA:BB:CC:DD:EE:FF"
|
||||
bindkey: "00112233445566778899aabbccddeeff"
|
||||
weight:
|
||||
name: "Scale Weight"
|
||||
impedance_low:
|
||||
name: "Scale Impedance Low"
|
||||
impedance_high:
|
||||
name: "Scale Impedance High"
|
||||
heart_rate:
|
||||
name: "Scale Heart Rate"
|
||||
profile_id:
|
||||
name: "Scale Profile ID"
|
||||
stabilized:
|
||||
name: "Scale Stabilized"
|
||||
# No ble_hub_id: exercises the generated binding real configs use.
|
||||
- platform: xiaomi_body_scale
|
||||
mac_address: "AA:BB:CC:DD:EE:00"
|
||||
bindkey: "ffeeddccbbaa99887766554433221100"
|
||||
weight:
|
||||
name: "Second Scale Weight"
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "../common.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace esphome::xiaomi_body_scale::testing {
|
||||
|
||||
TEST(XiaomiBodyScale, DecodesWeightHeartRateAndLowImpedance) {
|
||||
Harness h(SCALE_A, KEY_A);
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_A, PACKET_1)));
|
||||
EXPECT_FLOAT_EQ(h.weight.state, 69.9f);
|
||||
EXPECT_FLOAT_EQ(h.impedance_low.state, 543.2f);
|
||||
EXPECT_FLOAT_EQ(h.heart_rate.state, 92.0f);
|
||||
EXPECT_FLOAT_EQ(h.profile_id.state, 1.0f);
|
||||
EXPECT_FALSE(h.impedance_high.has_state());
|
||||
EXPECT_FALSE(h.stabilized.state);
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, HighImpedancePacketCompletesTheMeasurement) {
|
||||
Harness h(SCALE_A, KEY_A);
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_A, PACKET_2)));
|
||||
EXPECT_FLOAT_EQ(h.impedance_high.state, 497.6f);
|
||||
EXPECT_FALSE(h.impedance_low.has_state());
|
||||
EXPECT_FALSE(h.weight.has_state());
|
||||
EXPECT_TRUE(h.stabilized.state);
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, StabilizedClearsAfterOneSecond) {
|
||||
Harness h(SCALE_A, KEY_A);
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_A, PACKET_2)));
|
||||
ASSERT_TRUE(h.stabilized.state);
|
||||
const uint32_t start = millis();
|
||||
while (h.stabilized.state && millis() - start < 2000) {
|
||||
App.scheduler.call(millis());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
EXPECT_FALSE(h.stabilized.state);
|
||||
EXPECT_GE(millis() - start, 900u);
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, WeightWithoutImpedanceCompletesTheMeasurement) {
|
||||
Harness h(SCALE_B, KEY_B);
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_B, SOCKS)));
|
||||
EXPECT_FLOAT_EQ(h.weight.state, 74.7f);
|
||||
EXPECT_FALSE(h.impedance_low.has_state());
|
||||
EXPECT_FALSE(h.impedance_high.has_state());
|
||||
EXPECT_TRUE(h.stabilized.state);
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, SteppingOffClearsStabilized) {
|
||||
Harness h(SCALE_B, KEY_B);
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_B, SOCKS)));
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_B, STEP_OFF)));
|
||||
EXPECT_FALSE(h.stabilized.state);
|
||||
EXPECT_FLOAT_EQ(h.weight.state, 74.7f); // a zero weight is not published
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, DecodesS200Weight) {
|
||||
Harness h(SCALE_S200, KEY_S200);
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_S200, S200_WEIGHT)));
|
||||
EXPECT_FLOAT_EQ(h.weight.state, 62.25f);
|
||||
EXPECT_FLOAT_EQ(h.profile_id.state, 1.0f);
|
||||
// The S200 has no impedance or heart rate, and does not drive stabilized
|
||||
EXPECT_FALSE(h.impedance_low.has_state());
|
||||
EXPECT_FALSE(h.impedance_high.has_state());
|
||||
EXPECT_FALSE(h.heart_rate.has_state());
|
||||
EXPECT_FALSE(h.stabilized.has_state());
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, IgnoresOtherAddresses) {
|
||||
Harness h(SCALE_A, KEY_A);
|
||||
EXPECT_FALSE(h.scale.parse_device(advert(SCALE_B, PACKET_1)));
|
||||
EXPECT_FALSE(h.weight.has_state());
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, RejectsAWrongBindkey) {
|
||||
Harness h(SCALE_A, KEY_B);
|
||||
EXPECT_FALSE(h.scale.parse_device(advert(SCALE_A, PACKET_1)));
|
||||
EXPECT_FALSE(h.weight.has_state());
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, RejectsAPlaintextFrame) {
|
||||
Harness h(SCALE_A, KEY_A);
|
||||
Frame plain = PACKET_1;
|
||||
plain[0] &= ~0x08;
|
||||
EXPECT_FALSE(h.scale.parse_device(advert(SCALE_A, plain)));
|
||||
EXPECT_FALSE(h.weight.has_state());
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, IgnoresARepeatedFrame) {
|
||||
Harness h(SCALE_A, KEY_A);
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_A, PACKET_1)));
|
||||
EXPECT_FALSE(h.scale.parse_device(advert(SCALE_A, PACKET_1)));
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, AcceptsFrameCountFFAsTheFirstFrame) {
|
||||
Harness h(SCALE_A, KEY_A);
|
||||
ASSERT_TRUE(h.scale.parse_device(advert(SCALE_A, PACKET_1_COUNT_FF)));
|
||||
EXPECT_FLOAT_EQ(h.weight.state, 69.9f);
|
||||
}
|
||||
|
||||
TEST(XiaomiBodyScale, AFailedFrameDoesNotBlockTheRealOne) {
|
||||
Harness h(SCALE_A, KEY_A);
|
||||
Frame forged = PACKET_1;
|
||||
forged[23] ^= 0xFF; // corrupt the tag, same frame count
|
||||
EXPECT_FALSE(h.scale.parse_device(advert(SCALE_A, forged)));
|
||||
EXPECT_TRUE(h.scale.parse_device(advert(SCALE_A, PACKET_1)));
|
||||
}
|
||||
|
||||
} // namespace esphome::xiaomi_body_scale::testing
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
|
||||
xiaomi_body_scale: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
xiaomi_body_scale: !include common-ln.yaml
|
||||
@@ -0,0 +1,28 @@
|
||||
# Config-only: the CI bk72xx board cannot compile the BLE 5.x tracker
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: xiaomi_body_scale
|
||||
ble_hub_id: ble_tracker_hub
|
||||
mac_address: "AA:BB:CC:DD:EE:FF"
|
||||
bindkey: "00112233445566778899aabbccddeeff"
|
||||
weight:
|
||||
name: "Scale Weight"
|
||||
impedance_low:
|
||||
name: "Scale Impedance Low"
|
||||
impedance_high:
|
||||
name: "Scale Impedance High"
|
||||
heart_rate:
|
||||
name: "Scale Heart Rate"
|
||||
profile_id:
|
||||
name: "Scale Profile ID"
|
||||
stabilized:
|
||||
name: "Scale Stabilized"
|
||||
# No ble_hub_id: exercises the generated binding real configs use.
|
||||
- platform: xiaomi_body_scale
|
||||
mac_address: "AA:BB:CC:DD:EE:00"
|
||||
bindkey: "ffeeddccbbaa99887766554433221100"
|
||||
weight:
|
||||
name: "Second Scale Weight"
|
||||
@@ -14,6 +14,47 @@ api:
|
||||
id: snapshot_display
|
||||
filename: !lambda return name;
|
||||
|
||||
- action: take_animation
|
||||
variables:
|
||||
name: string
|
||||
then:
|
||||
- snapshot.take:
|
||||
id: snapshot_display
|
||||
filename: !lambda return name;
|
||||
frames: 5
|
||||
frame_rate: 20fps
|
||||
|
||||
- action: take_noise_snapshot
|
||||
variables:
|
||||
name: string
|
||||
then:
|
||||
- snapshot.take:
|
||||
id: noise_display
|
||||
filename: !lambda return name;
|
||||
|
||||
# A busy picture needs more compression codes than a GIF dictionary holds, so recording it makes
|
||||
# the encoder widen its codes and start the dictionary over.
|
||||
- action: take_noise_animation
|
||||
variables:
|
||||
name: string
|
||||
then:
|
||||
- snapshot.take:
|
||||
id: noise_display
|
||||
filename: !lambda return name;
|
||||
frames: 3
|
||||
frame_rate: 20fps
|
||||
|
||||
# Slow enough that a second request arrives while this one is still recording.
|
||||
- action: take_slow_animation
|
||||
variables:
|
||||
name: string
|
||||
then:
|
||||
- snapshot.take:
|
||||
id: snapshot_display
|
||||
filename: !lambda return name;
|
||||
frames: 3
|
||||
frame_rate: 2fps
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
@@ -26,3 +67,23 @@ display:
|
||||
dimensions:
|
||||
width: 101
|
||||
height: 64
|
||||
|
||||
# Every pixel is one of 256 colours picked by a hash of its position, so the picture is
|
||||
# effectively random but fits a GIF palette exactly.
|
||||
- platform: snapshot
|
||||
id: noise_display
|
||||
update_interval: 1s
|
||||
dimensions:
|
||||
width: 200
|
||||
height: 150
|
||||
lambda: |-
|
||||
for (int y = 0; y != it.get_height(); y++) {
|
||||
for (int x = 0; x != it.get_width(); x++) {
|
||||
uint32_t h = static_cast<uint32_t>(x) * 73856093u ^ static_cast<uint32_t>(y) * 19349663u;
|
||||
h ^= h >> 13;
|
||||
h *= 0x5bd1e995u;
|
||||
h ^= h >> 15;
|
||||
uint8_t c = h & 0xFF;
|
||||
it.draw_pixel_at(x, y, Color(c & 0xE0, (c << 3) & 0xE0, (c << 6) & 0xC0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
from aioesphomeapi import LogLevel
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
import pytest
|
||||
|
||||
from .bmp_utils import capture_when_drawn, wait_for_bmp
|
||||
from .bmp_utils import Bmp, capture_when_drawn, wait_for_bmp
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
WIDTH = 101
|
||||
HEIGHT = 64
|
||||
|
||||
ANIMATION_FRAMES = 5
|
||||
# The fixture asks for 20 frames a second, and a GIF counts time in milliseconds here.
|
||||
ANIMATION_FRAME_MS = 50
|
||||
|
||||
NOISE_WIDTH = 200
|
||||
NOISE_HEIGHT = 150
|
||||
NOISE_FRAMES = 3
|
||||
|
||||
# Part of the message the writer logs when it will not write over a file that is already there.
|
||||
REFUSAL_MESSAGE = b"not overwriting"
|
||||
# Part of the message logged when an animation is asked for while another is still being recorded.
|
||||
BUSY_MESSAGE = b"Already recording"
|
||||
|
||||
|
||||
async def wait_for_gif(path: Path, frames: int, timeout: float = 5.0) -> Image.Image:
|
||||
"""Wait for a complete animated GIF with the given number of frames and return it.
|
||||
|
||||
The file exists from the moment the recording starts and grows a frame at a time, so keep
|
||||
reading until it holds all of them and ends with the GIF trailer.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + timeout
|
||||
while loop.time() < deadline:
|
||||
try:
|
||||
data = path.read_bytes()
|
||||
# Open the bytes just read, so the trailer check and the decode see the same file.
|
||||
image = Image.open(io.BytesIO(data))
|
||||
if data.endswith(b";") and image.n_frames == frames:
|
||||
# Decoding every frame proves the compressed data is all there and valid.
|
||||
for frame in range(frames):
|
||||
image.seek(frame)
|
||||
image.load()
|
||||
image.seek(0)
|
||||
return image
|
||||
except (FileNotFoundError, UnidentifiedImageError, OSError, EOFError):
|
||||
pass
|
||||
await asyncio.sleep(0.05)
|
||||
raise AssertionError(
|
||||
f"no complete {frames} frame GIF appeared at {path} within {timeout}s"
|
||||
)
|
||||
|
||||
|
||||
def bmp_as_image(bmp: Bmp) -> Image.Image:
|
||||
"""The picture in a decoded BMP, as an RGB image."""
|
||||
return Image.frombytes(
|
||||
"RGB", (bmp.width, bmp.height), bmp.pixels, "raw", "BGR", 0, -1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -39,12 +86,50 @@ async def test_snapshot_display(
|
||||
async def take(name: str) -> None:
|
||||
await client.execute_service(service, {"name": name})
|
||||
|
||||
animation_service = next(s for s in services if s.name == "take_animation")
|
||||
|
||||
# The test card is drawn in several colours, so once it is on the screen the picture is
|
||||
# not one flat shade. Capturing until that is true waits out the first update rather than
|
||||
# racing it.
|
||||
image, capture = await capture_when_drawn(take, snapshot_dir)
|
||||
assert (image.width, image.height, image.bits) == (WIDTH, HEIGHT, 24)
|
||||
|
||||
# Asking for frames records a GIF: every frame is the size of the display and lasts as long
|
||||
# as the frame rate says. The test card does not move, so every frame is the picture
|
||||
# captured above.
|
||||
await client.execute_service(animation_service, {"name": "movie"})
|
||||
movie = await wait_for_gif(snapshot_dir / "movie.gif", ANIMATION_FRAMES)
|
||||
assert movie.size == (WIDTH, HEIGHT)
|
||||
expected = bmp_as_image(image)
|
||||
for frame in range(ANIMATION_FRAMES):
|
||||
movie.seek(frame)
|
||||
assert movie.info["duration"] == ANIMATION_FRAME_MS
|
||||
assert movie.convert("RGB").tobytes() == expected.tobytes()
|
||||
|
||||
# A picture of 256 colours in no pattern makes a long run of compression codes, so recording
|
||||
# it passes the point where the code width grows and where the dictionary is started over.
|
||||
# The picture uses no more than 256 colours, so it is stored exactly and every frame must
|
||||
# come back identical to the one captured as a BMP.
|
||||
noise_service = next(s for s in services if s.name == "take_noise_snapshot")
|
||||
noise_animation = next(s for s in services if s.name == "take_noise_animation")
|
||||
|
||||
async def take_noise(name: str) -> None:
|
||||
await client.execute_service(noise_service, {"name": name})
|
||||
|
||||
noise, _ = await capture_when_drawn(take_noise, snapshot_dir, "noisedrawn")
|
||||
assert (noise.width, noise.height) == (NOISE_WIDTH, NOISE_HEIGHT)
|
||||
noise_expected = bmp_as_image(noise)
|
||||
assert (
|
||||
len(noise_expected.getcolors(NOISE_WIDTH * NOISE_HEIGHT)) > 200
|
||||
) # a busy picture
|
||||
await client.execute_service(noise_animation, {"name": "noise"})
|
||||
noise_movie = await wait_for_gif(
|
||||
snapshot_dir / "noise.gif", NOISE_FRAMES, timeout=15.0
|
||||
)
|
||||
for frame in range(NOISE_FRAMES):
|
||||
noise_movie.seek(frame)
|
||||
assert noise_movie.convert("RGB").tobytes() == noise_expected.tobytes()
|
||||
|
||||
# An extension is only added when there is not one already, whatever its case.
|
||||
await take("UPPER.BMP")
|
||||
await wait_for_bmp(snapshot_dir / "UPPER.BMP")
|
||||
@@ -54,18 +139,30 @@ async def test_snapshot_display(
|
||||
await take("../escape")
|
||||
await wait_for_bmp(snapshot_dir / ".._escape.bmp")
|
||||
|
||||
# A second capture under a name already used must fail rather than write over the first.
|
||||
# Wait for the device to report the refusal: on its own, an unchanged file cannot tell a
|
||||
# refusal apart from a request the device has not got to yet, so a regression that wrote
|
||||
# over the file could still pass on a busy machine.
|
||||
# The device says so when it refuses a request. Waiting for that message is the only way to
|
||||
# tell a refusal from a request the device has not got to yet: an unchanged file, or a file
|
||||
# that never appears, would look the same on a busy machine.
|
||||
refused = asyncio.Event()
|
||||
busy = asyncio.Event()
|
||||
|
||||
def on_log(msg) -> None:
|
||||
if REFUSAL_MESSAGE in msg.message:
|
||||
refused.set()
|
||||
if BUSY_MESSAGE in msg.message:
|
||||
busy.set()
|
||||
|
||||
client.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
|
||||
|
||||
# A display records one animation at a time. A second request made while the first is still
|
||||
# running is refused and leaves no file, and the first one carries on to the end.
|
||||
slow_animation = next(s for s in services if s.name == "take_slow_animation")
|
||||
await client.execute_service(slow_animation, {"name": "slow"})
|
||||
await client.execute_service(slow_animation, {"name": "second"})
|
||||
await asyncio.wait_for(busy.wait(), timeout=10.0)
|
||||
assert not (snapshot_dir / "second.gif").exists()
|
||||
await wait_for_gif(snapshot_dir / "slow.gif", 3)
|
||||
|
||||
# A second capture under a name already used must fail rather than write over the first.
|
||||
before = capture.read_bytes()
|
||||
await take(capture.name)
|
||||
await asyncio.wait_for(refused.wait(), timeout=10.0)
|
||||
@@ -73,6 +170,12 @@ async def test_snapshot_display(
|
||||
# Nothing beyond what was asked for, leaving out however many captures it took to wait
|
||||
# for the first frame.
|
||||
written = sorted(
|
||||
p.name for p in snapshot_dir.iterdir() if not p.name.startswith("drawn-")
|
||||
p.name for p in snapshot_dir.iterdir() if "drawn-" not in p.name
|
||||
)
|
||||
assert written == [".._escape.bmp", "UPPER.BMP"]
|
||||
assert written == [
|
||||
".._escape.bmp",
|
||||
"UPPER.BMP",
|
||||
"movie.gif",
|
||||
"noise.gif",
|
||||
"slow.gif",
|
||||
]
|
||||
|
||||
@@ -1021,6 +1021,9 @@ _ESP32_PLATFORMIO_FULL_LIST_FILES = [
|
||||
# Workflow / harness files
|
||||
["script/test_build_components.py"],
|
||||
[".github/workflows/ci.yml"],
|
||||
# The base config and bus packages every generated build includes
|
||||
["tests/test_build_components/build_components_base.esp32-idf.yaml"],
|
||||
["tests/test_build_components/common/uart/esp32-idf.yaml"],
|
||||
]
|
||||
|
||||
|
||||
@@ -3194,6 +3197,9 @@ def test_memory_impact_elf_layouts_are_found(tmp_path: Path) -> None:
|
||||
"esphome/platformio/toolchain.py",
|
||||
# The composite cache action must not ship unexercised
|
||||
".github/actions/cache-arduino8266/action.yml",
|
||||
# The base config and bus packages every generated build includes
|
||||
"tests/test_build_components/build_components_base.esp8266-ard.yaml",
|
||||
"tests/test_build_components/common/uart/esp8266-ard.yaml",
|
||||
],
|
||||
)
|
||||
def test_esp8266_native_components_full_list_on_infra_change(changed: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user