Merge branch 'esp32-pio-pch' into nrf52-pch

This commit is contained in:
J. Nick Koston
2026-09-26 20:50:35 +01:00
77 changed files with 4240 additions and 4278 deletions
+2
View File
@@ -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
+1
View File
@@ -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"
+152
View File
@@ -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);
+1 -1
View File
@@ -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_() {
+1 -1
View File
@@ -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)),
}
)
+53 -2
View File
@@ -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(
+6 -156
View File
@@ -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
+2 -2
View File
@@ -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
+2
View File
@@ -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());
}
+2 -1
View File
@@ -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;
+11 -1
View File
@@ -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) {
+1 -1
View File
@@ -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(
+7 -229
View File
@@ -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)
-66
View File
@@ -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
+6 -265
View File
@@ -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
+4 -4
View File
@@ -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;
}
+10 -230
View File
@@ -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),
}
),
)
-66
View File
@@ -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
+7 -262
View File
@@ -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
+4 -4
View File
@@ -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;
}
+15 -8
View File
@@ -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() {
+150
View File
@@ -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
+306
View File
@@ -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
@@ -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
@@ -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
+28 -6
View File
@@ -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),
)
+283
View File
@@ -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
+51
View File
@@ -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
+196 -58
View File
@@ -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
+29 -6
View File
@@ -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) {
+7
View File
@@ -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
+142 -64
View File
@@ -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) {
+48 -1
View File
@@ -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)) {
+8
View File
@@ -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 -1
View File
@@ -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")
+11 -2
View File
@@ -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
+9 -1
View File
@@ -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
+26
View File
@@ -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
+19
View File
@@ -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
+194
View File
@@ -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
+5
View File
@@ -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.
+125
View File
@@ -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));
}
}
+110 -7
View File
@@ -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",
]
+6
View File
@@ -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: