Compare commits

..
Author SHA1 Message Date
J. Nick Koston f3676e27d4 Merge remote-tracking branch 'origin/parse-float-prefix' into HEAD 2026-11-14 08:39:53 -06:00
J. Nick Koston 365bea82ad Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:35:53 -06:00
J. Nick Koston 15291f06a5 Merge commit '090156a32e851ea237bf16e18e967fdbab20cdfe' into parse-float-components 2026-11-13 23:45:59 -06:00
J. Nick Koston 090156a32e [core] Say str_to_float does not set errno 2026-11-13 23:45:56 -06:00
J. Nick Koston db07dac851 Merge commit '543151ab1cffd9ddce66ab310d647ba907247abf' into parse-float-components 2026-11-13 23:05:30 -06:00
J. Nick Koston 543151ab1c [core] Keep the str_to_float comment within 120 columns 2026-11-13 23:05:27 -06:00
J. Nick Koston 3f8705ecbb Merge commit '3b053f1a0c5abe194d1930a0c02a8c52ac2e29b1' into parse-float-components 2026-11-13 23:05:18 -06:00
J. Nick Koston 3b053f1a0c [core] Point str_to_float only at float results of strtod and atof 2026-11-13 23:05:15 -06:00
J. Nick Koston d719b9220e Merge commit '7bed89eafc5b7bbe70131a7793a92030187d656d' into parse-float-components 2026-11-13 22:37:58 -06:00
J. Nick Koston 7bed89eafc [core] Document what parse_float accepts and rejects 2026-11-13 22:37:55 -06:00
J. Nick Koston 381d6fe7cd Merge commit '04fe5cdac96bec4161ff85b29f9a7eb4eb30f64b' into parse-float-components 2026-11-13 22:13:14 -06:00
J. Nick Koston 04fe5cdac9 [core] Stop hex digits at ':' to '?' instead of reading them as 10 to 15
picolibc 1.8.10 (and its main) read them as hex digits, so "0x1:" was 26 and
parse_number<float> accepted it; newlib and the C standard stop there.
2026-11-13 22:13:11 -06:00
J. Nick Koston 72555af9b6 Merge commit 'b35a450f5f9ea4bb90da86da842b5583d3f6e6a3' into parse-float-components 2026-11-13 19:43:09 -06:00
J. Nick Koston b35a450f5f [core] Say stof uses str_to_float in the StringRef helpers comment 2026-11-13 19:43:06 -06:00
J. Nick Koston 16205445bb Merge commit 'bf6d3ca470e598bffbb1573caf1118125b48b2d7' into parse-float-components 2026-11-13 19:40:00 -06:00
J. Nick Koston bf6d3ca470 [core] Note the one non-tie near a midpoint 2026-11-13 19:39:57 -06:00
J. Nick Koston 6cd7dd98f3 Merge commit '16f660b0090391b511dd06ad5c20ba9e02cefcc0' into parse-float-components 2026-11-13 19:37:24 -06:00
J. Nick Koston 16f660b009 [core] Terser parser comments and docstrings 2026-11-13 19:37:21 -06:00
J. Nick Koston ce3a282a41 Merge commit '1e127efe8e6c5f7e613c6661ba2d08ca6f1164c3' into parse-float-components 2026-11-13 19:35:10 -06:00
J. Nick Koston 1e127efe8e [core] Rename the pow10 template to pow10_exact so it cannot hide libm's pow10 2026-11-13 19:35:07 -06:00
J. Nick Koston e657d18e45 Merge commit 'cdd994455141a13f0ecb7d752f98b49072b658a3' into parse-float-components 2026-11-13 19:34:32 -06:00
J. Nick Koston cdd9944551 [core] Fewer lines in parse_float.cpp; object code identical on host, ESP8266 and RP2040 2026-11-13 19:34:29 -06:00
J. Nick Koston dacf19954f Merge commit '9e64b21eb46a2ecae9cebb3a6efacb1e42ae08c3' into parse-float-components 2026-11-13 19:21:23 -06:00
J. Nick Koston 9e64b21eb4 [core] Share the plain-decimal limit between parse_float and str_to_float; no multiply for the fast-path factor 2026-11-13 19:21:16 -06:00
J. Nick Koston ba424d1f26 Merge commit '6f7b62602e6' into parse-float-components 2026-11-13 17:46:10 -06:00
J. Nick Koston 6f7b62602e [core] Give picolibc's strtof result, with integer arithmetic only
str_to_float and parse_number<float> now return what picolibc 1.8.10's
strtof (ESP-IDF 6) returns, bits and end pointer, on every platform: a
decimal is rounded to 9 significant digits (ties to even) and those are
converted to the nearest float; hex is rounded to the nearest float.

- the 9 digits convert with one float operation when both operands are
  exact floats, otherwise with one 64-bit product against a 12-entry
  table of 10^(8k); all 85 billion (m10, e10) were checked against
  picolibc's engine, and the ties the product cannot decide alone are
  computed exactly
- no double arithmetic and no big-integer path: time linear in the
  input length, and less code
- picolibc's syntax: "x" after zero digits starts hex, "inf" and
  "infinity", "nan(" with letters, digits and '_', exponent digits
  capped at 224
- tests use picolibc's values for long input and syntax, the host
  strtof for up to 9 significant digits, and name the RP2040 and
  RP2350 libc results the bench found (flushed denormals, NaN to inf)
2026-11-13 17:40:06 -06:00
J. Nick Koston 29020748e2 [dfrobot_sen0609][hydreon_rgxx][smt100] Use str_to_float 2026-11-13 17:00:01 -06:00
J. Nick Koston 8917409887 Merge commit 'dd08b19bd51' into parse-float-components 2026-11-13 16:59:51 -06:00
J. Nick Koston dd08b19bd5 Round to the nearest float; rename parse_float_prefix to str_to_float
The parser now gives the correctly rounded float (ties to even) for decimal
and hex input on every platform, instead of newlib's decimal-to-double-to-
float double rounding. Syntax and end pointer stay newlib's; the hex exponent
saturates instead of wrapping at 32 bits.

- one integer rounding helper builds the float bits for the double guess,
  hex input and double_to_float_bits
- the exact comparison is against the float midpoint itself
- the early return checks the guess's distance to a float midpoint
  directly, without two extra conversions
- named exponent clamps folded into the EXACT_WORDS static_assert
- tests use the host's correctly rounded strtof as the reference, with
  named tests for the inputs where newlib and picolibc differ
- the unit test is host only (ESP32 clang-tidy), reproducible RNG seeds
  are marked, and int32_t replaces long
2026-11-13 16:45:41 -06:00
J. Nick Koston 7b8afeb4df [dfrobot_sen0609][hydreon_rgxx][smt100] Sort the parse_float.h include 2026-11-13 16:24:26 -06:00
J. Nick Koston 2eb25bf800 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 16:24:14 -06:00
J. Nick Koston b4e65d51bb [core] Test the double fast path, the overflow midpoint and nan forms; bound exact rounds at two 2026-11-13 16:24:12 -06:00
J. Nick Koston da65beb404 [core] Move pow10, is_digit and is_space_c into helpers.h; integer powers of ten on the plain path 2026-11-13 16:24:12 -06:00
J. Nick Koston 491674adec [dfrobot_sen0609][hydreon_rgxx][smt100] Include parse_float.h for parse_float_prefix 2026-11-13 15:54:42 -06:00
J. Nick Koston e3a9f11525 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 15:54:30 -06:00
J. Nick Koston 859299b94c [core] Drop the parse_float_prefix benchmarks that repeat parse_number<float>'s 2026-11-13 15:54:29 -06:00
J. Nick Koston 080bdbd53b [core] Simplify parse_float_prefix after review and declare parse_float in parse_float.h 2026-11-13 15:54:29 -06:00
J. Nick Koston ba53757f7e [dfrobot_sen0609] Update a comment that still named strtof 2026-11-13 15:24:03 -06:00
J. Nick Koston df31b05804 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 15:23:56 -06:00
J. Nick Koston 49e696555e [core] Benchmark rejected text and drop the libc comparison benchmarks 2026-11-13 15:23:45 -06:00
J. Nick Koston 10a7992f5b [core] Bound parse_float_prefix's time on every platform and speed up common inputs 2026-11-13 15:23:44 -06:00
J. Nick Koston 774cb9e9e5 Merge remote-tracking branch 'origin/parse-float-prefix' into parse-float-components 2026-11-13 14:51:40 -06:00
J. Nick Koston cbf38abdb9 [core] Benchmark parse_number<float> and parse_float_prefix against strtof 2026-11-13 14:51:26 -06:00
J. Nick Koston e5bf7c74e7 [core] Take strtod's exact double fast path in parse_float_prefix 2026-11-13 14:51:26 -06:00
J. Nick Koston c197a824e3 [dfrobot_sen0609][hydreon_rgxx][smt100] Parse floats with parse_float_prefix 2026-11-13 14:34:25 -06:00
J. Nick Koston 8cfc265b19 [core] Parse floats with parse_float_prefix so libc's strtod is not linked 2026-11-13 14:34:25 -06:00
J. Nick Koston fa08c7a305 [core] Add parse_float_prefix, a strtof that does not link libc's strtod 2026-11-13 14:27:23 -06:00
79 changed files with 1818 additions and 6049 deletions

No files matched your search

+1 -2
View File
@@ -149,7 +149,6 @@ esphome/components/cst226/* @clydebarrow
esphome/components/cst328/* @latonita
esphome/components/cst816/* @clydebarrow
esphome/components/cst9220/* @clydebarrow
esphome/components/cstxxx/* @clydebarrow @latonita
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/cw2017/* @exciton
@@ -742,7 +741,7 @@ esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic
esphome/components/weikai_i2c/* @DrCoolZic
esphome/components/weikai_spi/* @DrCoolZic
esphome/components/whirlpool/* @arpena @glmnet @Rodrigoah
esphome/components/whirlpool/* @arpena @glmnet
esphome/components/whynter/* @aeonsablaze
esphome/components/wiegand/* @ssieb
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
+19 -517
View File
@@ -1,9 +1,7 @@
"""Memory usage analyzer for ESPHome compiled binaries."""
from collections import Counter, defaultdict
from collections.abc import Container
from dataclasses import dataclass, field
from functools import cached_property
import logging
from pathlib import Path
import re
@@ -57,7 +55,6 @@ _COMPONENT_PREFIX_EXTERNAL = "[external]"
_COMPONENT_PREFIX_LIB = "[lib]"
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
_COMPONENT_ARDUINO_CORE = "arduino_core"
# Placement new storage suffix (generated by codegen Pvariable)
_PSTORAGE_SUFFIX = "__pstorage"
@@ -86,28 +83,6 @@ _NAMESPACE_STD = "std::"
# Type alias for symbol information: (symbol_name, size, component)
SymbolInfoType = tuple[str, int, str]
# Only ESP8266 has this section; its .rodata is copied to RAM at boot like .data
_ESP8266_FLASH_CODE_SECTION = ".irom0.text"
# readelf -SW row: [Nr] Name Type Address Off Size ES Flg
_OBJ_SECTION_PATTERN = re.compile(
r"^\s*\[\s*(\d+)\]\s+(\S+)\s+(\S+)\s+[\da-f]+\s+[\da-f]+\s+([\da-f]+)\s+[\da-f]+\s+(\S*)"
)
_RELOC_SECTION_PATTERN = re.compile(r"^Relocation section '\.rela(\S+)'")
# String literal section; group 1 is the function for -fdata-sections names
_STRING_SECTION_PATTERN = re.compile(r"^\.rodata(?:\.(.+))?\.str\d+\.\d+$")
# Data section of one symbol; group 1 is the symbol
_DATA_SECTION_PATTERN = re.compile(r"^\.(?:rodata|data)\.(.+)$")
# Code section; group 1 is the function for -ffunction-sections names, after any
# placement prefix (.text.startup.<fn>, .iram.text.<fn> for IRAM_ATTR)
_CODE_SECTION_PATTERN = re.compile(
r"^\.(?:literal|text|irom0\.text|iram\.text|iram0\.text)"
r"(?:\.(?:startup|unlikely|hot|exit)(?=\.|$))?(?:\.(.+))?$"
)
# readelf -x row: " 0x00000000 0000803f 0000803f 0000803f 0000803f ...?...?...?...?"
_HEX_DUMP_PREFIX = " 0x"
_HEX_DUMP_COLUMNS = slice(13, 48)
# RAM sections - symbols in these sections consume RAM
RAM_SECTIONS = frozenset([".data", ".bss"])
@@ -144,29 +119,6 @@ class SDKSymbol:
demangled: str = "" # Demangled name (populated after analysis)
@dataclass
class UnnamedConstant:
"""Constant without a symbol in an object's .rodata (RAM on ESP8266)."""
size: int
source_file: str
component: str
functions: list[str] # Functions that reference it, mangled
jump_table: bool # Holds code addresses (a switch table), not a value
data: bytes # Leading bytes, to recognize the value
strings: list[str] = field(default_factory=list) # String literals it points to
@dataclass
class StringLiteral:
"""A string literal kept in RAM (.rodata on ESP8266)."""
text: str
size: int # Bytes in RAM; strings shared or merged into a longer one count once
# Component -> functions or objects that use it, mangled
users: dict[str, set[str]] = field(default_factory=lambda: defaultdict(set))
@dataclass
class ComponentMemory:
"""Tracks memory usage for a component."""
@@ -200,188 +152,6 @@ class ComponentMemory:
return self.data_size + self.bss_size
@dataclass
class _RodataSection:
"""An object's .rodata section and the byte ranges its symbols cover."""
name: str
size: int
covered: list[tuple[int, int]] = field(default_factory=list) # (offset, size)
@cached_property
def _named(self) -> frozenset[int]:
return frozenset(
offset
for start, size in self.covered
for offset in range(start, min(start + size, self.size))
)
def is_unnamed(self, offset: int) -> bool:
return 0 <= offset < self.size and offset not in self._named
@property
def unnamed_size(self) -> int:
return self.size - len(self._named)
def _parse_string_sections(output: str) -> dict[str, dict[str, tuple[str, str | None]]]:
"""Find string literal sections in ``readelf -SW`` output.
Returns:
Object path -> section index -> (section name, function it is named after)
"""
sections: dict[str, dict[str, tuple[str, str | None]]] = defaultdict(dict)
file_path = ""
for line in output.splitlines():
if line.startswith("File: "):
file_path = line[6:]
elif (match := _OBJ_SECTION_PATTERN.match(line)) is not None:
index, name, section_type, size_hex, flags = match.groups()
if (
section_type == "PROGBITS"
and "S" in flags
and int(size_hex, 16)
and (string := _STRING_SECTION_PATTERN.match(name))
):
sections[file_path][index] = (name, string.group(1))
return sections
def _ram_string_size(text: bytes, rodata: bytes, literals: Container[bytes]) -> int:
"""Bytes a literal takes in RAM: 0 if dropped or only the tail of another literal."""
needle = text + b"\0"
start = rodata.find(needle)
while start != -1:
head = rodata.rfind(b"\0", 0, start) + 1
# Bytes before it that are not one of our literals are other data
if start == head or rodata[head : start + len(text)] not in literals:
return len(needle)
start = rodata.find(needle, start + 1)
return 0
def _parse_hex_dumps(output: str) -> dict[str, bytearray]:
"""Collect the bytes of each ``readelf -x`` section dump by section name."""
contents: dict[str, bytearray] = defaultdict(bytearray)
section: str | None = None
for line in output.splitlines():
if line.startswith("Hex dump of section"):
section = line.split("'")[1]
elif section is not None and line.startswith(_HEX_DUMP_PREFIX):
contents[section] += bytes.fromhex(line[_HEX_DUMP_COLUMNS].replace(" ", ""))
elif line and not line.startswith(" "):
section = None
return contents
def _parse_relocations(output: str) -> list[tuple[str, int, str, int]]:
"""List (relocated section, offset, target, addend) from ``readelf -rW`` output."""
relocations: list[tuple[str, int, str, int]] = []
section: str | None = None
for line in output.splitlines():
if line.startswith("Relocation section"):
match = _RELOC_SECTION_PATTERN.match(line)
section = match.group(1) if match else None
elif line[:1].isdigit() and section is not None:
# Offset Info Type Sym.Value Sym.Name + Addend
fields = line.split()
if len(fields) < 5:
continue
addend = int(fields[6], 16) if len(fields) > 6 else 0
if len(fields) > 6 and fields[5] == "-":
addend = -addend
relocations.append((section, int(fields[0], 16), fields[4], addend))
elif line and line[0] != " ":
section = None
return relocations
def _pointed_literal(
referrer: bytes, offset: int, addend: int, strings: bytes
) -> bytes:
"""Read the string literal a relocated pointer at ``offset`` refers to."""
# Xtensa keeps part of the addend in the relocated word
start = addend + int.from_bytes(referrer[offset : offset + 4], "little")
end = strings.find(b"\0", start)
return bytes(strings[start : end if end != -1 else len(strings)])
def _section_user(section: str) -> str | None:
"""The function or data symbol a -ffunction-sections/-fdata-sections name is for."""
match = _CODE_SECTION_PATTERN.match(section) or _DATA_SECTION_PATTERN.match(section)
return match.group(1) if match else None
@dataclass
class _FunctionDefinitions:
"""Where each function is defined across the scanned objects."""
weak: dict[str, set[str]] = field(default_factory=lambda: defaultdict(set))
strong: set[str] = field(default_factory=set) # Global, non-weak
count: Counter[str] = field(default_factory=Counter) # Objects defining it
def linked_copy_unknown(self, file_path: str, functions: list[str]) -> bool:
"""True if another object's copy of these functions may be the linked one."""
if self.weak[file_path].issuperset(functions):
# Inline copies: a strong definition elsewhere replaces them all
return not self.strong.isdisjoint(functions)
# Same-named local functions in several objects; the ELF can't tell them apart
return all(f not in self.strong and self.count[f] > 1 for f in functions)
def _parse_unnamed_rodata(
output: str,
) -> tuple[dict[str, dict[str, _RodataSection]], _FunctionDefinitions]:
"""Find .rodata sections with bytes no symbol covers in ``readelf -SW -sW`` output.
Returns:
Object path -> section index -> section, and where functions are defined
"""
sections: dict[str, dict[str, _RodataSection]] = defaultdict(dict)
definitions = _FunctionDefinitions()
file_path = ""
for line in output.splitlines():
if line.startswith("File: "):
file_path = line[6:]
elif match := _OBJ_SECTION_PATTERN.match(line):
index, name, section_type, size_hex, flags = match.groups()
# Merged sections (string literals) are shared between objects
if (
section_type == "PROGBITS"
and name.startswith(".rodata")
and "A" in flags
and "M" not in flags
):
sections[file_path][index] = _RodataSection(name, int(size_hex, 16))
else:
# Num: Value Size Type Bind Vis Ndx Name
fields = line.split()
if len(fields) < 8:
continue
if fields[3] == "OBJECT" and (
section := sections[file_path].get(fields[6])
):
section.covered.append((int(fields[1], 16), int(fields[2], 0)))
elif fields[3] == "FUNC" and fields[6] != "UND":
definitions.count[fields[7]] += 1
if fields[4] == "WEAK":
definitions.weak[file_path].add(fields[7])
elif fields[4] == "GLOBAL":
definitions.strong.add(fields[7])
unnamed = {
path: kept
for path, file_sections in sections.items()
if (
kept := {
index: section
for index, section in file_sections.items()
if section.unnamed_size
}
)
}
return unnamed, definitions
class MemoryAnalyzer:
"""Analyzes memory usage from ELF files."""
@@ -461,10 +231,6 @@ class MemoryAnalyzer:
self._heuristic_to_lib: dict[str, str] = {}
# Function call counts: mangled_name -> call_count
self._function_call_counts: Counter[str] = Counter()
# True on ESP8266, where .rodata is RAM; it is then counted as .data
self.rodata_in_ram = False
self._unnamed_constants: list[UnnamedConstant] = []
self._string_literals: list[StringLiteral] = []
def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage."""
@@ -474,7 +240,6 @@ class MemoryAnalyzer:
self._scan_source_symbols()
self._categorize_symbols()
self._analyze_cswtch_symbols()
self._analyze_object_rodata()
self._analyze_sdk_libraries()
self._analyze_function_calls()
return dict(self.components)
@@ -488,17 +253,18 @@ class MemoryAnalyzer:
check=True,
)
raw_sections = [
(match.group(1), int(match.group(2), 16))
for line in result.stdout.splitlines()
if (match := _READELF_SECTION_PATTERN.match(line))
]
self.rodata_in_ram = any(
name == _ESP8266_FLASH_CODE_SECTION for name, _ in raw_sections
)
# Parse section headers
for line in result.stdout.splitlines():
# Look for section entries
if not (match := _READELF_SECTION_PATTERN.match(line)):
continue
for section_name, size in raw_sections:
mapped_section = self._standard_section(map_section_name(section_name))
section_name = match.group(1)
size_hex = match.group(2)
size = int(size_hex, 16)
# Map to standard section name
mapped_section = map_section_name(section_name)
if not mapped_section:
continue
@@ -506,12 +272,6 @@ class MemoryAnalyzer:
self.sections[mapped_section] = MemorySection(mapped_section)
self.sections[mapped_section].total_size += size
def _standard_section(self, section: str | None) -> str | None:
"""Count .rodata as .data where it is RAM (ESP8266)."""
if section == ".rodata" and self.rodata_in_ram:
return ".data"
return section
def _parse_symbols(self) -> None:
"""Parse symbols from ELF file."""
result = subprocess.run(
@@ -529,18 +289,14 @@ class MemoryAnalyzer:
continue
section, name, size, address = symbol_info
section = self._standard_section(section)
if section not in self.sections:
continue
# Every alias is in the ELF, even when its size is counted once
self._elf_symbol_names.add(name)
# Skip duplicate symbols at the same address (e.g., C1/C2 constructors)
if address in seen_addresses:
if address in seen_addresses or section not in self.sections:
continue
self.sections[section].symbols.append((name, size, ""))
self.sections[section].symbol_size += size
self._elf_symbol_names.add(name)
seen_addresses.add(address)
def _categorize_symbols(self) -> None:
@@ -1169,12 +925,10 @@ class MemoryAnalyzer:
Path to the directory containing .o files, or None if not found.
"""
# The ELF is typically at .pioenvs/<env>/firmware.elf
# PlatformIO puts object files in .pioenvs/<env>/src/ and .pioenvs/<env>/lib*/,
# the native ESP8266 build in .pioenvs/<env>/obj/src/ and obj/lib/
# Object files are in .pioenvs/<env>/src/ and .pioenvs/<env>/lib*/
pioenvs_dir = self.elf_path.parent
for obj_dir in (pioenvs_dir, pioenvs_dir / "obj"):
if any(obj_dir.glob("src/*.o")):
return obj_dir
if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")):
return pioenvs_dir
return None
@staticmethod
@@ -1356,12 +1110,6 @@ class MemoryAnalyzer:
):
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[1]]}"
# Native ESP8266 build: lib/<owner>__<Name>/... and the Arduino core in core/
if len(parts) >= 3 and parts[0] == "lib":
return f"{_COMPONENT_PREFIX_LIB}{parts[1].rsplit('__', 1)[-1].lower()}"
if len(parts) >= 2 and parts[0] == "core":
return _COMPONENT_ARDUINO_CORE
# Other framework/library files - return the first path component
# e.g., FrameworkArduino/... -> FrameworkArduino
return parts[0] if parts else source_file
@@ -1443,7 +1191,9 @@ class MemoryAnalyzer:
and other_mem is not None
):
other_mem.add_section_size(section_name, -size)
self._add_component_size(component, section_name, size)
if component not in self.components:
self.components[component] = ComponentMemory(component)
self.components[component].add_section_size(section_name, size)
# Sort by size descending
self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True)
@@ -1455,254 +1205,6 @@ class MemoryAnalyzer:
total_size,
)
def _analyze_object_rodata(self) -> None:
"""Attribute .rodata without a symbol, which is RAM on ESP8266, to components."""
if not self.rodata_in_ram or (obj_dir := self._find_object_files_dir()) is None:
return
result = run_tool(
[self.readelf_path, "-SW", "-sW"]
+ [str(f) for f in sorted(obj_dir.rglob("*.o"))]
)
if result is None or result.returncode != 0:
_LOGGER.debug("readelf failed scanning object files for constants")
return
self._attribute_unnamed_constants(result.stdout, obj_dir)
self._attribute_string_literals(result.stdout, obj_dir)
names = {n for c in self._unnamed_constants for n in c.functions} | {
n
for literal in self._string_literals
for users in literal.users.values()
for n in users
}
self._batch_demangle_symbols(sorted(names - self._demangle_cache.keys()))
def _attribute_unnamed_constants(self, output: str, obj_dir: Path) -> None:
"""Attribute constants GCC copies into objects built from constant initializers."""
unnamed, definitions = _parse_unnamed_rodata(output)
# Inline (weak) functions are emitted, with their constants, in every
# object that uses them; the linker keeps one copy
inline_copies: dict[tuple[tuple[str, ...], int], str] = {}
for file_path, sections in unnamed.items():
rel_path = Path(file_path).relative_to(obj_dir).as_posix()
component = self._source_file_to_component(rel_path)
for constant in self._scan_object_constants(
file_path, sections, rel_path, component
):
if definitions.linked_copy_unknown(file_path, constant.functions):
continue
if definitions.weak[file_path].issuperset(constant.functions):
key = (tuple(constant.functions), constant.size)
if inline_copies.setdefault(key, file_path) != file_path:
continue
self._unnamed_constants.append(constant)
self._charge_ram(component, constant.size)
self._unnamed_constants.sort(key=lambda c: c.size, reverse=True)
def _attribute_string_literals(self, output: str, obj_dir: Path) -> None:
"""Attribute string literals in RAM to the components whose code uses them.
The linker merges identical literals across objects, so each is counted
once, from the bytes it really takes in the ELF's .rodata.
"""
result = run_tool([self.readelf_path, "-x", ".rodata", str(self.elf_path)])
if result is None or result.returncode != 0:
return
rodata = bytes(_parse_hex_dumps(result.stdout).get(".rodata", b""))
# Literal -> component -> functions or data using it
users: dict[bytes, dict[str, set[str]]] = defaultdict(lambda: defaultdict(set))
for file_path, sections in _parse_string_sections(output).items():
component = self._source_file_to_component(
Path(file_path).relative_to(obj_dir).as_posix()
)
for text, found in self._scan_object_strings(file_path, sections).items():
if text and (kept := found & self._elf_symbol_names):
users[text][component] |= kept
literals = (
StringLiteral(
text.decode("utf-8", "backslashreplace"),
_ram_string_size(text, rodata, users),
by_component,
)
for text, by_component in users.items()
)
self._string_literals = sorted(
(s for s in literals if s.size), key=lambda s: (-s.size, s.text)
)
for literal in self._string_literals:
# A literal used by several components is not charged to any of them
if len(literal.users) == 1:
self._charge_ram(next(iter(literal.users)), literal.size)
def _scan_object_strings(
self, file_path: str, sections: dict[str, tuple[str, str | None]]
) -> dict[bytes, set[str]]:
"""Map each string literal of one object to the functions or data using it.
Args:
file_path: Object file to read
sections: String sections by index, with the function each is named after
"""
result = run_tool(
[self.readelf_path, "-rW"]
+ [arg for index in sections for arg in ("-x", index)]
+ [file_path]
)
if result is None or result.returncode != 0:
_LOGGER.debug("readelf failed reading strings of %s", file_path)
return {}
contents = _parse_hex_dumps(result.stdout)
users: dict[bytes, set[str]] = defaultdict(set)
shared: set[str] = set()
for name, function in sections.values():
# A section named after its function holds only that function's literals
if function:
for text in bytes(contents[name]).split(b"\0")[:-1]:
users[text].add(function)
else:
shared.add(name)
relocations = _parse_relocations(result.stdout)
# Functions using each unnamed table, for literals reached through a pointer in it
table_users: dict[str, set[str]] = defaultdict(set)
for section, _, target, _ in relocations:
if (
target.startswith(".rodata")
and not _STRING_SECTION_PATTERN.match(target)
and (function := _section_user(section))
and _CODE_SECTION_PATTERN.match(section)
):
table_users[target].add(function)
# Literals in the shared section are found from the pointers that use them
refs = [
(section, found, offset, target, addend)
for section, offset, target, addend in relocations
if target in shared
and (
found := {u} if (u := _section_user(section)) else table_users[section]
)
]
if not refs:
return users
result = run_tool(
[self.readelf_path]
+ [arg for name in sorted({r[0] for r in refs}) for arg in ("-x", name)]
+ [file_path]
)
if result is None or result.returncode != 0:
_LOGGER.debug("readelf failed reading string users of %s", file_path)
return users
referrers = _parse_hex_dumps(result.stdout)
for section, found, offset, target, addend in refs:
text = _pointed_literal(
referrers[section], offset, addend, contents[target]
)
users[text] |= found
return users
def _scan_object_constants(
self,
file_path: str,
sections: dict[str, _RodataSection],
rel_path: str,
component: str,
) -> list[UnnamedConstant]:
"""Return the unnamed constants of one object that the link kept.
Args:
file_path: Object file to read
sections: Candidate sections by index
rel_path: Object path shown in the report
component: Component the object belongs to
"""
result = run_tool(
[self.readelf_path, "-rW"]
+ [arg for index in sections for arg in ("-x", index)]
+ [file_path]
)
if result is None or result.returncode != 0:
_LOGGER.debug("readelf failed reading constants of %s", file_path)
return []
by_name = {section.name: section for section in sections.values()}
contents = _parse_hex_dumps(result.stdout)
functions: dict[str, set[str]] = defaultdict(set)
jump_tables: set[str] = set()
# Section -> (offset, string section, addend) of each pointer to a literal
pointers: dict[str, list[tuple[int, str, int]]] = defaultdict(list)
for section, offset, target, addend in _parse_relocations(result.stdout):
if target in by_name:
code = _CODE_SECTION_PATTERN.match(section)
if code and code.group(1):
functions[target].add(code.group(1))
elif section in by_name and by_name[section].is_unnamed(offset):
if _CODE_SECTION_PATTERN.match(target):
jump_tables.add(section)
elif ".str" in target:
pointers[section].append((offset, target, addend))
strings = self._read_pointed_strings(file_path, pointers, contents)
return [
UnnamedConstant(
size=section.unnamed_size,
source_file=rel_path,
component=component,
functions=sorted(functions[name]),
jump_table=name in jump_tables,
data=bytes(
byte
for offset, byte in enumerate(contents[name])
if section.is_unnamed(offset)
)[:16],
strings=strings.get(name, []),
)
for name, section in by_name.items()
# Dropped by --gc-sections unless a function that uses it was kept
if not self._elf_symbol_names.isdisjoint(functions[name])
]
def _read_pointed_strings(
self,
file_path: str,
pointers: dict[str, list[tuple[int, str, int]]],
contents: dict[str, bytearray],
) -> dict[str, list[str]]:
"""Resolve each constant's pointers to string literals, in offset order."""
if not pointers:
return {}
targets = sorted({t for refs in pointers.values() for _, t, _ in refs})
result = run_tool(
[self.readelf_path]
+ [arg for target in targets for arg in ("-x", target)]
+ [file_path]
)
if result is None or result.returncode != 0:
_LOGGER.debug("readelf failed reading strings of %s", file_path)
return {}
literals = _parse_hex_dumps(result.stdout)
return {
name: [
_pointed_literal(
contents[name], offset, addend, literals[target]
).decode("utf-8", "backslashreplace")
for offset, target, addend in sorted(refs)
]
for name, refs in pointers.items()
}
def _charge_ram(self, component: str, size: int) -> None:
"""Charge RAM bytes that have no ELF symbol to a component."""
self._add_component_size(component, ".data", size)
if (data_section := self.sections.get(".data")) is not None:
data_section.symbol_size += size
def _add_component_size(self, component: str, section: str, size: int) -> None:
"""Add size to a component, creating it under its own name."""
if component not in self.components:
self.components[component] = ComponentMemory(component)
self.components[component].add_section_size(section, size)
def _analyze_function_calls(self) -> None:
"""Count function call sites by parsing disassembly output.
+2 -84
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import heapq
import json
from operator import itemgetter
from pathlib import Path
import sys
@@ -24,7 +23,7 @@ from . import (
from .toolchain import find_elf_path, find_idedata_path, idedata_candidates
if TYPE_CHECKING:
from . import ComponentMemory, StringLiteral
from . import ComponentMemory
def _format_pstorage_name(name: str) -> str:
@@ -47,8 +46,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
)
# Lower threshold for RAM symbols (RAM is more constrained)
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
# String literals listed per component in the RAM strings report
STRINGS_PER_COMPONENT: int = 10
# Number of top symbols to show in the largest symbols report
TOP_SYMBOLS_LIMIT: int = 30
# Width for symbol name display in top symbols report
@@ -207,75 +204,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
)
def _add_unnamed_constants_analysis(self, lines: list[str]) -> None:
"""Add the unnamed constants (ESP8266 .rodata in RAM) section."""
self._add_section_header(lines, "Unnamed Constants in RAM (.rodata)")
total_size = sum(c.size for c in self._unnamed_constants)
lines.append(
f"Total: {len(self._unnamed_constants)} constant(s), {total_size:,} B. "
"GCC copies these into objects built from constant initializers;"
)
lines.append(
"PROGMEM tables, assigned fields or a user-provided constructor avoid the copy."
)
lines.append("")
for constant in self._unnamed_constants:
kind = "switch table" if constant.jump_table else "constant"
lines.append(
f"{constant.size:>6,} B {kind:<12} {constant.component} ({constant.source_file})"
)
lines.extend(
f"{'':>9} used by {self._demangle_symbol(function)}"
for function in constant.functions
)
if constant.strings:
shown = ", ".join(f'"{text[:40]}"' for text in constant.strings[:4])
more = ", ..." if len(constant.strings) > 4 else ""
lines.append(f"{'':>9} points to {shown}{more}")
elif constant.data and not constant.jump_table:
more = " ..." if constant.size > len(constant.data) else ""
lines.append(f"{'':>9} bytes: {constant.data.hex(' ')}{more}")
lines.append("")
def _add_string_literals_analysis(self, lines: list[str]) -> None:
"""Add the string literals in RAM (ESP8266 .rodata) section, by component."""
self._add_section_header(lines, "String Literals in RAM (.rodata)")
total_size = sum(s.size for s in self._string_literals)
lines.append(
f"Total: {len(self._string_literals)} string(s), {total_size:,} B. "
"LOG_STR, ESPHOME_F or a PROGMEM table keeps a literal in flash."
)
lines.append(
"A literal used by several components is listed under each and charged to none."
)
lines.append("")
by_component: dict[str, list[StringLiteral]] = defaultdict(list)
for literal in self._string_literals:
for component in literal.users:
by_component[component].append(literal)
totals = {c: sum(s.size for s in ls) for c, ls in by_component.items()}
for component, literals in sorted(
by_component.items(), key=lambda x: (-totals[x[0]], x[0])
):
comp_total = totals[component]
lines.append(f"{component} ({comp_total:,} B, {len(literals)} strings):")
for literal in literals[: self.STRINGS_PER_COMPONENT]:
text = json.dumps(literal.text, ensure_ascii=False)
if len(text) > 50:
text = f'{text[:47]}..."'
users = sorted(
self._demangle_symbol(u) for u in literal.users[component]
)
used_by = users[0][:60] + (
f" (+{len(users) - 1})" if len(users) > 1 else ""
)
lines.append(f" {literal.size:>5,} B {text:<50} {used_by}")
if len(literals) > self.STRINGS_PER_COMPONENT:
lines.append(
f" ... and {len(literals) - self.STRINGS_PER_COMPONENT} more"
)
lines.append("")
def _add_cswtch_analysis(self, lines: list[str]) -> None:
"""Add CSWTCH (GCC switch table lookup) analysis section."""
self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)")
@@ -466,12 +394,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
)
if self.rodata_in_ram:
lines.append("")
lines.append(
"ESP8266 keeps .rodata in RAM, so constants are counted in RAM (data)."
)
# Show unattributed RAM (SDK/framework overhead)
unattributed_bss, unattributed_data, unattributed_total = (
self.get_unattributed_ram()
@@ -735,11 +657,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f" ... and {len(large_ram_syms) - 10} more")
lines.append("")
if self._unnamed_constants:
self._add_unnamed_constants_analysis(lines)
if self._string_literals:
self._add_string_literals_analysis(lines)
# CSWTCH (GCC switch table) analysis
if self._cswtch_symbols:
self._add_cswtch_analysis(lines)
@@ -821,6 +738,7 @@ def main():
build_dir = sys.argv[1]
# Load build directory
import json
from esphome.platformio.toolchain import IDEData
+7 -42
View File
@@ -1,5 +1,3 @@
from typing import Any
from esphome import automation, pins
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
@@ -15,7 +13,6 @@ from esphome.const import (
CONF_VALUE,
CONF_WAIT_TIME,
)
from esphome.types import ConfigType
CODEOWNERS = ["@lygris", "@gabest11"]
DEPENDENCIES = ["spi"]
@@ -25,8 +22,6 @@ MULTI_CONF = True
ns = cg.esphome_ns.namespace("cc1101")
CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice)
CC1101Listener = ns.class_("CC1101Listener")
PACKET_TYPE = cg.std_vector.template(cg.uint8)
RX_FIFO_SIZE = 64
# Config keys
CONF_RX_ATTENUATION = "rx_attenuation"
@@ -246,34 +241,20 @@ CONFIG_MAP = {
cv.Optional(CONF_BS_POST_KI): cv.enum(BS_POST_KI, upper=False),
cv.Optional(CONF_BS_POST_KP): cv.enum(BS_POST_KP, upper=False),
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
}
def _is_long_packet(packet_length: Any) -> bool:
"""A packet_length that does not fit the RX FIFO, or is a lambda."""
return cg.is_template(packet_length) or (packet_length or 0) > RX_FIFO_SIZE
def _validate_packet_mode(config: ConfigType) -> ConfigType:
packet_mode = config.get(CONF_PACKET_MODE, False)
packet_length = config.get(CONF_PACKET_LENGTH)
if packet_mode:
def _validate_packet_mode(config):
if config.get(CONF_PACKET_MODE, False):
if CONF_GDO0_PIN not in config:
raise cv.Invalid("gdo0_pin is required when packet_mode is enabled")
if packet_length is None:
if CONF_PACKET_LENGTH not in config:
raise cv.Invalid("packet_length is required when packet_mode is enabled")
elif cg.is_template(packet_length):
raise cv.Invalid("packet_mode is required when packet_length is a lambda")
if (
packet_mode
and _is_long_packet(packet_length)
and config.get(CONF_CRC_ENABLE, False)
):
raise cv.Invalid(
"crc_enable is not supported with packet_length > 64 or a lambda"
)
if config[CONF_PACKET_LENGTH] > 64:
raise cv.Invalid("packet_length must be <= 64 (FIFO size)")
return config
@@ -282,9 +263,6 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(CC1101Component),
cv.Optional(CONF_GDO0_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_PACKET_LENGTH): cv.templatable(
cv.int_range(min=0, max=1024)
),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
}
)
@@ -304,24 +282,11 @@ async def to_code(config):
cg.add(getattr(var, f"set_{key}")(config[key]))
await cg.add_pin(var.set_gdo0_pin, config.get(CONF_GDO0_PIN))
if (packet_length := config.get(CONF_PACKET_LENGTH)) is not None:
# packet_mode can also be switched on at run time, so this does not check it
if _is_long_packet(packet_length):
cg.add_define("USE_CC1101_LONG_PACKET")
if cg.is_template(packet_length):
lambda_ = await cg.process_lambda(
packet_length,
[(PACKET_TYPE.operator("ref").operator("const"), "x")],
return_type=cg.int32,
)
cg.add(var.set_packet_length_lambda(lambda_))
else:
cg.add(var.set_packet_length(packet_length))
if CONF_ON_PACKET in config:
await automation.build_automation(
var.get_packet_trigger(),
[
(PACKET_TYPE, "x"),
(cg.std_vector.template(cg.uint8), "x"),
(cg.float_, "freq_offset"),
(cg.float_, "rssi"),
(cg.uint8, "lqi"),
+33 -353
View File
@@ -1,10 +1,8 @@
#include "cc1101.h"
#include "cc1101pa.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cmath>
#include <cstring>
@@ -16,15 +14,9 @@ namespace esphome::cc1101 {
ESPHOME_LOG_TAG(TAG, "cc1101");
static constexpr size_t TX_FIFO_SIZE = 64;
static constexpr size_t RX_FIFO_SIZE = 64;
// RX FIFO threshold for FIFO_THR = 7 in long packet mode
static constexpr uint8_t RX_FIFO_THRESHOLD = 32;
// The crystal takes about 150 us to start after CSn goes low
static constexpr uint32_t WAKEUP_DELAY_US = 200;
static float freq_offset_hz(uint8_t freqest) { return static_cast<int8_t>(freqest) * (XTAL_FREQUENCY / (1 << 14)); }
static float rssi_dbm(int8_t rssi) { return (rssi * RSSI_STEP) - RSSI_OFFSET; }
// MDMCFG2 MOD_FORMAT values
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
@@ -122,6 +114,8 @@ CC1101Component::CC1101Component() {
memset(this->pa_table_, 0, sizeof(this->pa_table_));
}
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
void CC1101Component::setup() {
this->spi_setup();
@@ -199,220 +193,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi);
}
float CC1101Component::symbol_rate_() const {
return (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
}
#ifdef USE_CC1101_LONG_PACKET
bool CC1101Component::is_long_packet_mode_() const {
return this->packet_mode_ && (this->packet_length_ > RX_FIFO_SIZE || this->packet_length_func_ != nullptr);
}
void CC1101Component::arm_length_(uint16_t length) {
// Over 255 bytes or unknown starts in infinite mode (datasheet section 15.2.2). PKTLEN must not be 0, so a
// multiple of 256 stays in infinite mode and ends on the MCU byte count alone.
this->packet_expected_length_ = length;
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(length > 0 && length < 256 ? LengthConfig::LENGTH_CONFIG_FIXED
: LengthConfig::LENGTH_CONFIG_INFINITE);
this->state_.PKTLEN = length % 256 == 0 ? 0xFF : static_cast<uint8_t>(length % 256);
}
void CC1101Component::reset_long_packet_rx_() {
this->packet_.clear();
this->packet_last_byte_ms_ = 0;
this->high_freq_.stop();
this->arm_length_(this->packet_length_func_ != nullptr ? 0 : this->packet_length_);
}
void CC1101Component::restart_long_packet_rx_() {
this->enter_idle_();
this->enter_rx_();
this->disable_loop();
}
void CC1101Component::maybe_switch_long_packet_to_fixed_() {
const uint16_t expected = this->packet_expected_length_;
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED) || expected == 0 ||
expected % 256 == 0) {
return;
}
const uint16_t received = this->packet_.size();
if (expected - received >= 256) {
return;
}
// PKTLEN keeps the (length % 256) set at the start, as in TI Design Note DN500 section 2.3
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
this->write_(Register::PKTCTRL0);
ESP_LOGVV(TAG, "Switched long packet RX to fixed mode: received=%u expected=%u pktlen=%u", received, expected,
this->state_.PKTLEN);
}
void CC1101Component::finish_long_packet_() {
this->packet_.resize(this->packet_expected_length_);
this->read_(Register::FREQEST);
this->read_(Register::LQI);
const float freq_offset = freq_offset_hz(this->state_.FREQEST);
const uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
// No CRC check: crc_enable is rejected for long packets, CRC_OK is only set after the last payload byte.
// Restart RX before dispatching so a callback may change settings; swapping back keeps the reserved capacity
// unless a callback reserved a larger buffer.
std::vector<uint8_t> packet;
packet.swap(this->packet_);
this->restart_long_packet_rx_();
this->call_listeners_(packet, freq_offset, rssi_dbm(this->long_packet_rssi_), lqi);
if (packet.capacity() > this->packet_.capacity()) {
packet.clear();
this->packet_.swap(packet);
}
}
void CC1101Component::handle_long_packet_(uint8_t rx_bytes) {
// Leave one byte in the FIFO unless it holds the rest of the packet (datasheet section 20)
uint16_t to_read = rx_bytes > 0 ? rx_bytes - 1 : 0;
if (this->packet_expected_length_ > 0) {
const uint16_t remaining = this->packet_expected_length_ - this->packet_.size();
if (remaining <= rx_bytes) {
to_read = remaining;
}
}
if (to_read == 0) {
if (this->packet_.empty()) {
return;
}
// Discard a packet when no byte arrived for the air time of PACKET_IDLE_TIMEOUT_BYTES
const uint32_t timeout_ms =
std::max(static_cast<uint32_t>(std::ceil((8.0f * PACKET_IDLE_TIMEOUT_BYTES * 1000.0f) / this->symbol_rate_())),
PACKET_IDLE_TIMEOUT_MIN_MS);
if (App.get_loop_component_start_time() - this->packet_last_byte_ms_ > timeout_ms) {
ESP_LOGW(TAG, "Packet receive timeout, discarding packet: received=%u expected=%u fifo=%u",
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
this->restart_long_packet_rx_();
}
return;
}
const size_t old_size = this->packet_.size();
if (old_size == 0) {
// The RSSI register follows the live signal; sample it while the packet is still on air
this->read_(Register::RSSI);
this->long_packet_rssi_ = this->state_.RSSI;
// The FIFO threshold signal does not fire for the bytes after the last threshold crossing; poll until done
this->high_freq_.start();
}
// Read through an aligned buffer: for an unaligned target the ESP32 SPI driver allocates a bounce buffer
alignas(4) uint8_t staged[RX_FIFO_SIZE];
this->read_(Register::FIFO, staged, to_read);
this->packet_.resize(old_size + to_read);
std::memcpy(this->packet_.data() + old_size, staged, to_read);
this->packet_last_byte_ms_ = App.get_loop_component_start_time();
ESP_LOGVV(TAG, "Drained RX FIFO: read=%u received=%u expected=%u fifo=%u", to_read,
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
if (this->packet_expected_length_ == 0 && this->packet_length_func_ != nullptr) {
const int32_t length = this->packet_length_func_(this->packet_);
if (length < 0) {
ESP_LOGVV(TAG, "Packet length lambda discarded packet: received=%u", static_cast<uint16_t>(this->packet_.size()));
this->restart_long_packet_rx_();
return;
}
if (length > MAX_PACKET_LENGTH) {
ESP_LOGW(TAG, "Packet length exceeds maximum, discarding packet: length=%" PRId32 " max=%u", length,
MAX_PACKET_LENGTH);
this->restart_long_packet_rx_();
return;
}
if (length > 0) {
this->arm_length_(static_cast<uint16_t>(length));
// A packet shorter than the first FIFO drain is already complete; finish_long_packet_() truncates it
if (this->packet_expected_length_ <= this->packet_.size()) {
this->finish_long_packet_();
return;
}
this->write_(Register::PKTLEN);
this->write_(Register::PKTCTRL0);
ESP_LOGVV(TAG, "Resolved packet length: received=%u expected=%u pktlen=%u",
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, this->state_.PKTLEN);
}
}
// Only reachable while the length is still unknown; a known length caps every read
if (this->packet_.size() > MAX_PACKET_LENGTH) {
ESP_LOGW(TAG, "Packet length exceeds maximum, discarding packet: length=%u max=%u",
static_cast<uint16_t>(this->packet_.size()), MAX_PACKET_LENGTH);
this->restart_long_packet_rx_();
return;
}
this->maybe_switch_long_packet_to_fixed_();
if (this->packet_expected_length_ > 0 && this->packet_.size() >= this->packet_expected_length_) {
this->finish_long_packet_();
}
}
void CC1101Component::loop_long_packet_() {
if (this->sleeping_ || this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) ||
this->gdo0_pin_ == nullptr) {
this->high_freq_.stop();
this->disable_loop();
return;
}
// While GDO0 (RX FIFO threshold) is high, the FIFO holds at least RX_FIFO_THRESHOLD bytes, so drain without
// reading RXBYTES. At high data rates RXBYTES changes between two reads, so waiting for two reads that agree
// and retrying on the next loop pass lets the FIFO overflow. MARCSTATE does not change during RX.
for (uint8_t i = 0; i < RX_FIFO_SIZE / RX_FIFO_THRESHOLD + 2 && this->gdo0_pin_->digital_read(); i++) {
this->read_(Register::MARCSTATE);
if (static_cast<State>(this->state_.MARC_STATE) == State::RXFIFO_OVERFLOW) {
ESP_LOGW(TAG, "RX FIFO overflow, discarding packet: received=%u expected=%u",
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_);
this->restart_long_packet_rx_();
return;
}
this->handle_long_packet_(RX_FIFO_THRESHOLD);
if (this->packet_.empty()) {
return; // finished or discarded
}
}
// Below the threshold the next crossing raises GDO0 again, and after the packet the count no longer changes,
// so retrying on the next loop pass is safe here. RXBYTES can be misread while bytes arrive (TI SWRZ020), so
// only trust two consecutive reads that agree.
this->read_(Register::RXBYTES);
uint8_t rx_bytes = this->state_.RXBYTES;
bool stable = false;
for (uint8_t retries = RXBYTES_STABLE_RETRIES; retries > 0 && !stable; retries--) {
const uint8_t previous = rx_bytes;
this->read_(Register::RXBYTES);
rx_bytes = this->state_.RXBYTES;
stable = rx_bytes == previous;
}
if (!stable) {
return;
}
rx_bytes = this->state_.NUM_RXBYTES;
// More than the FIFO holds can only be a misread; the staged drain buffer is FIFO sized
if (this->state_.RXFIFO_OVERFLOW || rx_bytes > RX_FIFO_SIZE) {
ESP_LOGW(TAG, "RX FIFO overflow, discarding packet: received=%u expected=%u fifo=%u",
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
this->restart_long_packet_rx_();
return;
}
if (rx_bytes == 0 && this->packet_.empty() && !this->gdo0_pin_->digital_read()) {
this->disable_loop();
return;
}
this->handle_long_packet_(rx_bytes);
}
#endif
void CC1101Component::loop() {
#ifdef USE_CC1101_LONG_PACKET
if (this->is_long_packet_mode_()) {
this->loop_long_packet_();
return;
}
#endif
this->disable_loop();
if (this->sleeping_ || this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) ||
this->gdo0_pin_ == nullptr || !this->gdo0_pin_->digital_read()) {
@@ -440,7 +221,7 @@ void CC1101Component::loop() {
payload_length = this->state_.PKTLEN;
expected_rx = payload_length;
}
if (payload_length == 0 || payload_length > RX_FIFO_SIZE || rx_bytes != expected_rx) {
if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) {
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
this->enter_idle_();
this->strobe_(Command::FRX);
@@ -454,8 +235,8 @@ void CC1101Component::loop() {
this->read_(Register::FREQEST);
this->read_(Register::RSSI);
this->read_(Register::LQI);
float freq_offset = freq_offset_hz(this->state_.FREQEST);
float rssi = rssi_dbm(this->state_.RSSI);
float freq_offset = static_cast<int8_t>(this->state_.FREQEST) * (XTAL_FREQUENCY / (1 << 14));
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
if (this->state_.CRC_EN == 0 || crc_ok) {
@@ -475,7 +256,7 @@ void CC1101Component::loop() {
void CC1101Component::dump_config() {
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
XTAL_FREQUENCY / (1 << 16);
float symbol_rate = this->symbol_rate_();
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
float bw = XTAL_FREQUENCY / (8.0f * (4 + this->state_.CHANBW_M) * (1 << this->state_.CHANBW_E));
ESP_LOGCONFIG(TAG,
"CC1101:\n"
@@ -490,20 +271,6 @@ void CC1101Component::dump_config() {
LOG_STR_ARG(ModulationStrings::get_log_str(this->state_.MOD_FORMAT & 0x07, 0)), symbol_rate, bw,
this->output_power_effective_);
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" GDO0 Pin: ", this->gdo0_pin_);
if (this->packet_mode_) {
#ifdef USE_CC1101_LONG_PACKET
if (this->packet_length_func_ != nullptr) {
ESP_LOGCONFIG(TAG, " Packet Length: lambda");
} else
#endif
if (this->packet_length_ == 0) {
ESP_LOGCONFIG(TAG, " Packet Length: variable");
} else {
ESP_LOGCONFIG(TAG, " Packet Length: %u", this->packet_length_);
}
}
}
void CC1101Component::begin_tx() {
@@ -549,14 +316,6 @@ void CC1101Component::reset() {
void CC1101Component::set_idle() {
ESP_LOGV(TAG, "Setting IDLE state");
this->enter_idle_();
#ifdef USE_CC1101_LONG_PACKET
if (this->is_long_packet_mode_()) {
// Drop a partial packet so a pending loop pass cannot finish it and re-enter RX
this->strobe_(Command::FRX);
this->reset_long_packet_rx_();
this->disable_loop();
}
#endif
}
void CC1101Component::power_down() {
@@ -634,18 +393,7 @@ void CC1101Component::enter_idle_() {
}
}
bool CC1101Component::enter_rx_() {
#ifdef USE_CC1101_LONG_PACKET
if (this->is_long_packet_mode_()) {
// Every RX entry starts a fresh long packet; callers leave the chip in IDLE, where SFRX is allowed
this->strobe_(Command::FRX);
this->reset_long_packet_rx_();
this->write_(Register::PKTLEN);
this->write_(Register::PKTCTRL0);
}
#endif
return this->enter_calibrated_(State::RX, Command::RX);
}
bool CC1101Component::enter_rx_() { return this->enter_calibrated_(State::RX, Command::RX); }
bool CC1101Component::enter_tx_() { return this->enter_calibrated_(State::TX, Command::TX); }
@@ -708,22 +456,12 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
CC1101Error CC1101Component::transmit_packet(const uint8_t *data, size_t len) {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
ESP_LOGW(TAG, "Cannot transmit packet: packet mode is not FIFO");
return CC1101Error::PARAMS;
}
#ifdef USE_CC1101_LONG_PACKET
// Long packet TX needs chunked FIFO refills and is not implemented
if (this->is_long_packet_mode_()) {
ESP_LOGW(TAG, "Cannot transmit packet: long packet mode is RX-only");
return CC1101Error::PARAMS;
}
#endif
const bool variable = this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
// In variable length mode the FIFO also holds the length byte
if (len + (variable ? 1 : 0) > TX_FIFO_SIZE) {
ESP_LOGW(TAG,
"Cannot transmit packet: long packet TX is not implemented, payload length %u exceeds TX FIFO limit %u",
static_cast<unsigned>(len), static_cast<unsigned>(TX_FIFO_SIZE));
ESP_LOGE(TAG, "Packet of %u bytes does not fit the TX FIFO", static_cast<unsigned>(len));
return CC1101Error::PARAMS;
}
@@ -1096,60 +834,19 @@ void CC1101Component::set_bs_post_kp(BsPostKp value) {
}
}
void CC1101Component::configure_packet_mode_() {
#ifdef USE_CC1101_LONG_PACKET
// Ends a long reception in progress, also when switching to a short packet mode
this->high_freq_.stop();
this->packet_.clear();
#endif
if (!this->packet_mode_) {
this->state_.PKT_FORMAT = static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
this->state_.GDO0_CFG = 0x0D;
} else {
this->state_.PKT_FORMAT = static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO);
this->state_.APPEND_STATUS = 0;
#ifdef USE_CC1101_LONG_PACKET
if (this->is_long_packet_mode_()) {
// Threshold-only signal, reasserts after each partial FIFO drain
this->state_.GDO0_CFG = 0x00;
this->state_.FIFO_THR = 7;
// Reserve once so receiving never allocates; a lambda may read one FIFO drain past MAX_PACKET_LENGTH
this->packet_.reserve(this->packet_length_func_ != nullptr ? MAX_PACKET_LENGTH + RX_FIFO_SIZE
: this->packet_length_);
this->reset_long_packet_rx_();
} else
#endif
{
this->state_.GDO0_CFG = 0x01;
this->state_.FIFO_THR = 15;
if (this->packet_length_ == 0) {
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
} else {
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
this->state_.PKTLEN = static_cast<uint8_t>(this->packet_length_);
}
}
}
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
this->write_(Register::PKTLEN);
this->write_(Register::IOCFG0);
this->write_(Register::FIFOTHR);
}
}
void CC1101Component::set_packet_mode(bool value) {
this->packet_mode_ = value;
#ifdef USE_CC1101_LONG_PACKET
// Long packet RX must drop the FIFO along with a partial packet, so go through IDLE and restart RX
if (this->initialized_ && this->is_long_packet_mode_()) {
this->reconfigure_packet_length_();
} else
#endif
{
this->configure_packet_mode_();
this->state_.PKT_FORMAT =
static_cast<uint8_t>(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
if (value) {
// Configure GDO0 for FIFO status (asserts on RX FIFO threshold or end of packet)
this->state_.GDO0_CFG = 0x01;
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
this->state_.FIFO_THR = 15;
// Don't append status bytes to FIFO - we read from registers instead
this->state_.APPEND_STATUS = 0;
} else {
// Configure GDO0 for serial data (async serial mode)
this->state_.GDO0_CFG = 0x0D;
}
if (this->initialized_) {
if (this->gdo0_pin_ != nullptr) {
@@ -1159,41 +856,24 @@ void CC1101Component::set_packet_mode(bool value) {
this->gdo0_pin_->detach_interrupt();
}
}
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
this->write_(Register::IOCFG0);
this->write_(Register::FIFOTHR);
}
}
void CC1101Component::set_packet_length(uint16_t value) {
#ifndef USE_CC1101_LONG_PACKET
if (value > RX_FIFO_SIZE) {
ESP_LOGW(TAG, "Packet length %u needs a configured packet_length above %u", value,
static_cast<unsigned>(RX_FIFO_SIZE));
return;
void CC1101Component::set_packet_length(uint8_t value) {
if (value == 0) {
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
} else {
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
this->state_.PKTLEN = value;
}
#else
this->packet_length_func_ = nullptr;
#endif
this->packet_length_ = value;
this->reconfigure_packet_length_();
}
#ifdef USE_CC1101_LONG_PACKET
void CC1101Component::set_packet_length_lambda(packet_length_func_t func) {
this->packet_length_func_ = func;
this->reconfigure_packet_length_();
}
#endif
void CC1101Component::reconfigure_packet_length_() {
// While asleep the writes wait for wake_up() and the radio stays asleep, like set_frequency()
if (!this->initialized_ || this->sleeping_) {
this->configure_packet_mode_();
return;
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTLEN);
}
// Like the other runtime setters: go through IDLE, and flush bytes of a reception in progress
this->enter_idle_();
this->strobe_(Command::FRX);
this->configure_packet_mode_();
this->enter_rx_();
}
void CC1101Component::set_crc_enable(bool value) {
+2 -33
View File
@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "cc1101defs.h"
@@ -11,7 +10,6 @@
namespace esphome::cc1101 {
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
using packet_length_func_t = int32_t (*)(const std::vector<uint8_t> &);
class CC1101Listener {
public:
@@ -88,10 +86,7 @@ class CC1101Component final : public Component,
// Packet mode settings
void set_packet_mode(bool value);
void set_packet_length(uint16_t value);
#ifdef USE_CC1101_LONG_PACKET
void set_packet_length_lambda(packet_length_func_t func);
#endif
void set_packet_length(uint8_t value);
void set_crc_enable(bool value);
void set_whitening(bool value);
@@ -106,7 +101,6 @@ class CC1101Component final : public Component,
protected:
uint16_t chip_id_{0};
bool initialized_{false};
bool packet_mode_{false};
bool sleeping_{false};
bool registers_dirty_{false};
@@ -118,38 +112,13 @@ class CC1101Component final : public Component,
// GDO pin for packet reception
InternalGPIOPin *gdo0_pin_{nullptr};
// Inline like App.get_loop_component_start_time(): on RP2040 an out-of-line IRAM_ATTR function in the same
// file as that inline one fails with a section type conflict
static void IRAM_ATTR gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
static void gpio_intr(CC1101Component *arg);
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
void configure_packet_mode_();
void reconfigure_packet_length_();
float symbol_rate_() const;
#ifdef USE_CC1101_LONG_PACKET
bool is_long_packet_mode_() const;
void arm_length_(uint16_t length);
void reset_long_packet_rx_();
void restart_long_packet_rx_();
void loop_long_packet_();
void handle_long_packet_(uint8_t rx_bytes);
void finish_long_packet_();
void maybe_switch_long_packet_to_fixed_();
#endif
Trigger<std::vector<uint8_t>, float, float, uint8_t> packet_trigger_;
std::vector<uint8_t> packet_;
std::vector<CC1101Listener *> listeners_;
#ifdef USE_CC1101_LONG_PACKET
packet_length_func_t packet_length_func_{nullptr};
uint32_t packet_last_byte_ms_{0};
#endif
uint16_t packet_length_{0};
#ifdef USE_CC1101_LONG_PACKET
uint16_t packet_expected_length_{0};
int8_t long_packet_rssi_{0}; // raw RSSI register
HighFrequencyLoopRequester high_freq_;
#endif
// Low-level Helpers
uint8_t strobe_(Command cmd);
-5
View File
@@ -15,11 +15,6 @@ static constexpr uint8_t PLL_LOCK_RETRIES = 3;
static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80;
static constexpr uint8_t STATUS_LQI_MASK = 0x7F;
static constexpr uint16_t MAX_PACKET_LENGTH = 1024;
static constexpr uint16_t PACKET_IDLE_TIMEOUT_BYTES = 64;
static constexpr uint32_t PACKET_IDLE_TIMEOUT_MIN_MS = 5;
static constexpr uint8_t RXBYTES_STABLE_RETRIES = 4;
static constexpr uint8_t BUS_BURST = 0x40;
static constexpr uint8_t BUS_READ = 0x80;
static constexpr uint8_t BUS_WRITE = 0x00;
@@ -1,15 +1,36 @@
from esphome.components import cstxxx
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from esphome.types import ConfigType
from .. import cst226_ns
AUTO_LOAD = ["cstxxx"]
CST226Touchscreen = cst226_ns.class_("CST226Touchscreen", cstxxx.CSTTouchscreen)
CST226Touchscreen = cst226_ns.class_(
"CST226Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CST226ButtonListener = cst226_ns.class_("CST226ButtonListener")
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST226Touchscreen, 0x5A)
CONFIG_SCHEMA = (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(CST226Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x5A))
)
async def to_code(config: ConfigType) -> None:
await cstxxx.new_cst_touchscreen(config)
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
@@ -14,10 +14,6 @@ void CST226Touchscreen::setup() {
}
void CST226Touchscreen::update_touches() {
if (!this->setup_complete_) {
this->skip_update_ = true;
return;
}
uint8_t data[28];
if (!this->status_comm_result(this->read_bytes(CST226_REG_STATUS, data, sizeof data))) {
this->skip_update_ = true;
@@ -28,39 +24,56 @@ void CST226Touchscreen::update_touches() {
return;
}
this->update_button_state_(false);
if (data[cstxxx::SYNC_IDX] != cstxxx::SYNC_BYTE || data[0] == cstxxx::SYNC_BYTE ||
data[cstxxx::FINGER_COUNT_IDX] == 0x80) {
if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) {
this->skip_update_ = true;
return;
}
uint8_t num_of_touches = data[cstxxx::FINGER_COUNT_IDX] & 0x7F;
uint8_t num_of_touches = data[5] & 0x7F;
if (num_of_touches == 0 || num_of_touches > 5) {
this->write_byte(CST226_REG_STATUS, cstxxx::SYNC_BYTE);
this->write_byte(0, 0xAB);
return;
}
size_t index = 0;
for (uint8_t i = 0; i != num_of_touches; i++) {
auto touch = cstxxx::decode_touch(data, i);
this->add_raw_touch_position_(touch.id, touch.x, touch.y, touch.z);
ESP_LOGV(TAG, "Read touch %d: %d/%d", touch.id, touch.x, touch.y);
uint8_t id = data[index] >> 4;
int16_t x = (data[index + 1] << 4) | ((data[index + 3] >> 4) & 0x0F);
int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F);
int16_t z = data[index + 4];
this->add_raw_touch_position_(id, x, y, z);
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
index += 5;
if (i == 0)
index += 2;
}
}
bool CST226Touchscreen::read16_(uint16_t addr, uint8_t *data, size_t len) {
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read data from 0x%04X failed", addr);
this->mark_failed();
return false;
}
return true;
}
void CST226Touchscreen::continue_setup_() {
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
if (!this->enter_command_mode_())
uint8_t buffer[8];
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
if (!this->setup_check(this->write_byte_reg16(0xD1, 0xD1), LOG_STR("Write byte to 0xD1 failed")))
return;
delay(10);
if (!this->check_firmware_())
return;
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
LOG_STR("Failed to read chip ID")))
return;
if (this->read16_(0xD204, buffer, 4)) {
uint16_t chip_id = buffer[2] + (buffer[3] << 8);
uint16_t project_id = buffer[0] + (buffer[1] << 8);
ESP_LOGCONFIG(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
}
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
uint16_t x_max, y_max;
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
this->x_raw_max_ = x_max;
this->y_raw_max_ = y_max;
if (this->read16_(0xD1F8, buffer, 4)) {
this->x_raw_max_ = buffer[0] + (buffer[1] << 8);
this->y_raw_max_ = buffer[2] + (buffer[3] << 8);
if (this->swap_x_y_)
std::swap(this->x_raw_max_, this->y_raw_max_);
} else {
@@ -68,10 +81,8 @@ void CST226Touchscreen::continue_setup_() {
this->y_raw_max_ = this->display_->get_native_height();
}
}
this->status_comm_check(this->write_register16(cstxxx::REG_NORMAL_MODE, nullptr, 0) == i2c::ERROR_OK);
this->setup_complete_ = true;
}
void CST226Touchscreen::update_button_state_(bool state) {
if (this->button_touched_ == state)
return;
@@ -85,10 +96,6 @@ void CST226Touchscreen::dump_config() {
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG,
" Chip ID: 0x%04X, Project ID: 0x%04X\n"
" X/Y raw max: %d/%d",
this->chip_id_, this->project_id_, this->x_raw_max_, this->y_raw_max_);
}
} // namespace esphome::cst226
@@ -1,12 +1,11 @@
#pragma once
#include "esphome/components/cstxxx/cstxxx.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <vector>
namespace esphome::cst226 {
static const uint8_t CST226_REG_STATUS = 0x00;
@@ -16,23 +15,28 @@ class CST226ButtonListener {
virtual void update_button(bool state) = 0;
};
class CST226Touchscreen final : public cstxxx::CSTTouchscreen {
class CST226Touchscreen final : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void update_touches() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
bool can_proceed() override { return this->setup_complete_ || this->is_failed(); }
void register_button_listener(CST226ButtonListener *listener) { this->button_listeners_.push_back(listener); }
protected:
bool read16_(uint16_t addr, uint8_t *data, size_t len);
void continue_setup_();
void update_button_state_(bool state);
bool button_touched_{};
uint16_t chip_id_{};
uint16_t project_id_{};
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
uint8_t chip_id_{};
bool setup_complete_{};
std::vector<CST226ButtonListener *> button_listeners_;
bool button_touched_{};
};
} // namespace esphome::cst226
@@ -1,15 +1,37 @@
from esphome.components import cstxxx
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from esphome.types import ConfigType
from .. import cst328_ns
AUTO_LOAD = ["cstxxx"]
CST328Touchscreen = cst328_ns.class_("CST328Touchscreen", cstxxx.CSTTouchscreen)
CST328Touchscreen = cst328_ns.class_(
"CST328Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CST328ButtonListener = cst328_ns.class_("CST328ButtonListener")
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST328Touchscreen, 0x1A)
CONFIG_SCHEMA = (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(CST328Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x1A))
)
async def to_code(config: ConfigType) -> None:
await cstxxx.new_cst_touchscreen(config)
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
@@ -5,8 +5,31 @@ namespace esphome::cst328 {
ESPHOME_LOG_TAG(TAG, "cst328.touchscreen");
static constexpr uint32_t CST328_BEFORE_RESET_TIMEOUT = 50; // 50 ms from datasheet
static constexpr uint32_t CST328_TRANSITION_TIMEOUT = 300; // 200 ms from datasheet, but typically much less
static const uint32_t CST328_BEFORE_RESET_TIMEOUT = 50; // 50 ms from datasheet
static const uint32_t CST328_TRANSITION_TIMEOUT = 300; // 200 ms from datasheet, but typically much less
static const uint16_t CST328_FW_CRC = 0xCACA; // Expected firmware CRC value
static const uint8_t CST328_SYNC_BYTE = 0xAB; // Sync byte used in communication
static const uint8_t ZERO_BYTE = 0;
#define I2C_WARN_ON_ERROR(x, log_tag, format, ...) \
do { \
i2c::ErrorCode err_rc_ = (x); \
if (err_rc_ != i2c::ERROR_OK) { \
ESP_LOGW(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \
this->status_set_warning(format); \
} \
} while (0)
#define I2C_FAIL_ON_ERROR(x, log_tag, format, ...) \
do { \
i2c::ErrorCode err_rc_ = (x); \
if (err_rc_ != i2c::ERROR_OK) { \
ESP_LOGE(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \
this->mark_failed(); \
return; \
} \
} while (0)
void CST328Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up CST328 Touchscreen...");
@@ -27,36 +50,50 @@ void CST328Touchscreen::reset_device_() {
void CST328Touchscreen::continue_setup_() {
ESP_LOGV(TAG, "Continuing CST328 setup...");
if (!this->enter_command_mode_() || !this->check_firmware_())
return;
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
LOG_STR("Failed to read chip and project ID")))
uint8_t data_byte{0};
uint8_t buf[24]{};
I2C_FAIL_ON_ERROR(this->write_register16(CST_WM_DEBUG_INFO, buf, 0), TAG, "Failed to enter debug/info mode");
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_CRC_AND_BOOT_TIME, buf, 4), TAG,
"Failed to read FW CRC and boot time");
uint16_t fw_crc = buf[2] + (buf[3] << 8);
if (fw_crc != CST328_FW_CRC) {
ESP_LOGE(TAG, "Error: Firmware CRC mismatch, expected 0x%04X but got 0x%04X", CST328_FW_CRC, fw_crc);
this->mark_failed();
return;
}
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_CHIP_TYPE_AND_PROJECT_ID, buf, 4), TAG,
"Failed to read chip and project ID");
this->chip_id_ = buf[2] + (buf[3] << 8);
this->project_id_ = buf[0] + (buf[1] << 8);
ESP_LOGD(TAG, "Chip ID %X, project ID %X", this->chip_id_, this->project_id_);
uint16_t fw_version;
if (!this->setup_check(this->read_words_(cstxxx::REG_FW_VERSION, this->fw_build_, fw_version),
LOG_STR("Failed to read FW version")))
return;
this->fw_ver_major_ = fw_version >> 8;
this->fw_ver_minor_ = fw_version & 0xFF;
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_REVISION, buf, 4), TAG, "Failed to read FW version");
this->fw_ver_major_ = buf[3];
this->fw_ver_minor_ = buf[2];
this->fw_build_ = buf[0] + (buf[1] << 8);
ESP_LOGV(TAG, "FW version %d.%d.%d", this->fw_ver_major_, this->fw_ver_minor_, this->fw_build_);
uint16_t x_max, y_max;
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
this->x_raw_max_ = x_max;
this->y_raw_max_ = y_max;
if (i2c::ERROR_OK == this->read_register16(CST_REG_X_Y_RESOLUTION, buf, 4)) {
this->x_raw_max_ = buf[0] + (buf[1] << 8);
this->y_raw_max_ = buf[2] + (buf[3] << 8);
} else {
this->x_raw_max_ = this->display_->get_native_width();
this->y_raw_max_ = this->display_->get_native_height();
}
uint8_t sync;
bool ok = this->write_register16(cstxxx::REG_NORMAL_MODE, nullptr, 0) == i2c::ERROR_OK;
ok &= this->read_byte_reg16(cstxxx::REG_TOUCH_DATA, &sync);
ok &= this->write_byte_reg16(cstxxx::REG_TOUCH_DATA, cstxxx::SYNC_BYTE);
this->status_comm_check(ok);
I2C_WARN_ON_ERROR(this->write_register16(CST_WM_NORMAL, buf, 0), TAG, "Failed to enter normal mode");
I2C_WARN_ON_ERROR(this->read_register16(CST_REG_TOUCH_INFORMATION, &data_byte, 1), TAG, "Failed to read sync");
I2C_WARN_ON_ERROR(this->write_register16(CST_REG_TOUCH_INFORMATION, &CST328_SYNC_BYTE, 1), TAG,
"Failed to write sync");
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
this->setup_complete_ = true;
ESP_LOGV(TAG, "CST328 setup complete");
@@ -105,15 +142,21 @@ void CST328Touchscreen::update_touches() {
} else {
this->update_button_state_(true);
uint8_t data_idx = 0;
for (uint8_t i = 0; i < touch_cnt; i++) {
auto touch = cstxxx::decode_touch(touch_data, i);
this->add_raw_touch_position_(touch.id, touch.x, touch.y, touch.z);
uint8_t id = touch_data[data_idx] >> 4;
int16_t x = (touch_data[data_idx + 1] << 4) | ((touch_data[data_idx + 3] >> 4) & 0x0F);
int16_t y = (touch_data[data_idx + 2] << 4) | (touch_data[data_idx + 3] & 0x0F);
int16_t z = touch_data[data_idx + 4];
this->add_raw_touch_position_(id, x, y, z);
data_idx += (i == 0) ? 7 : 5;
}
}
bool cleanup_error = false;
cleanup_error |= !this->write_byte_reg16(cstxxx::REG_FINGER_COUNT, 0);
cleanup_error |= !this->write_byte_reg16(cstxxx::REG_TOUCH_DATA, cstxxx::SYNC_BYTE);
cleanup_error |= !this->write_byte_reg16(CST_REG_TOUCH_FINGER_NUMBER, ZERO_BYTE);
cleanup_error |= !this->write_byte_reg16(CST_REG_TOUCH_INFORMATION, CST328_SYNC_BYTE);
if (cleanup_error) {
ESP_LOGW(TAG, "Failed to clean up touch registers");
@@ -1,45 +1,61 @@
#pragma once
#include "esphome/components/cstxxx/cstxxx.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include <vector>
namespace esphome::cst328 {
static constexpr uint8_t CST328_TOUCH_MAX_POINTS = 5;
static constexpr uint8_t CST328_TOUCH_DATA_SIZE = cstxxx::report_size(CST328_TOUCH_MAX_POINTS);
static const uint8_t CST328_TOUCH_MAX_POINTS = 5;
static const uint8_t CST328_TOUCH_DATA_SIZE = CST328_TOUCH_MAX_POINTS * 5 + 2;
static constexpr uint16_t CST_REG_TOUCH_INFORMATION = cstxxx::REG_TOUCH_DATA;
static constexpr uint16_t CST_REG_FINGER_COUNT_IDX = cstxxx::FINGER_COUNT_IDX;
static constexpr uint16_t CST_REG_X_Y_RESOLUTION = cstxxx::REG_RESOLUTION;
static constexpr uint16_t CST_REG_FW_CRC_AND_BOOT_TIME = cstxxx::REG_FW_CHECK_CODE;
static const uint16_t CST_REG_TOUCH_INFORMATION = 0xD000;
static const uint16_t CST_REG_TOUCH_FINGER_NUMBER = 0xD005;
static const uint16_t CST_REG_FINGER_COUNT_IDX = CST_REG_TOUCH_FINGER_NUMBER - CST_REG_TOUCH_INFORMATION;
static const uint16_t CST_REG_X_Y_RESOLUTION = 0xD1F8;
static const uint16_t CST_REG_FW_CRC_AND_BOOT_TIME = 0xD1FC;
static const uint16_t CST_REG_CHIP_TYPE_AND_PROJECT_ID = 0xD204;
static const uint16_t CST_REG_FW_REVISION = 0xD208;
static const uint16_t CST_WM_DEBUG_INFO = 0xD101;
static const uint16_t CST_WM_NORMAL = 0xD109;
class CST328ButtonListener {
public:
virtual void update_button(bool state) = 0;
};
class CST328Touchscreen : public cstxxx::CSTTouchscreen {
class CST328Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void register_button_listener(CST328ButtonListener *listener) { this->button_listeners_.push_back(listener); }
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void update_touches() override;
void reset_device_();
void continue_setup_();
void update_button_state_(bool state);
bool button_touched_{};
uint8_t fw_ver_major_{};
uint8_t fw_ver_minor_{};
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
std::vector<CST328ButtonListener *> button_listeners_;
bool button_touched_{};
uint16_t chip_id_{};
uint16_t project_id_{};
uint8_t fw_ver_major_{};
uint8_t fw_ver_minor_{};
uint16_t fw_build_{};
bool setup_complete_{};
};
} // namespace esphome::cst328
@@ -1,14 +1,35 @@
from esphome.components import cstxxx
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from esphome.types import ConfigType
from .. import cst9220_ns
AUTO_LOAD = ["cstxxx"]
CST9220Touchscreen = cst9220_ns.class_(
"CST9220Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CST9220Touchscreen = cst9220_ns.class_("CST9220Touchscreen", cstxxx.CSTTouchscreen)
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST9220Touchscreen, 0x5A)
CONFIG_SCHEMA = (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(CST9220Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x5A))
)
async def to_code(config: ConfigType) -> None:
await cstxxx.new_cst_touchscreen(config)
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
@@ -1,4 +1,7 @@
#include "cst9220_touchscreen.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
namespace esphome::cst9220 {
@@ -13,29 +16,55 @@ void CST9220Touchscreen::setup() {
}
void CST9220Touchscreen::continue_setup_() {
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
uint8_t buffer[4];
if (!this->enter_command_mode_())
return;
delay(10);
if (!this->check_firmware_())
return;
// Read the panel resolution unless the user supplied calibration values.
uint16_t x_max, y_max;
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
if (this->x_raw_max_ == this->x_raw_min_)
this->x_raw_max_ = x_max;
if (this->y_raw_max_ == this->y_raw_min_)
this->y_raw_max_ = y_max;
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
LOG_STR("Failed to read chip ID")))
// Enter command mode so the configuration registers can be read.
if (this->write_register16(REG_CMD_MODE, buffer, 0) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to enter command mode"));
this->mark_failed();
return;
}
delay(10);
// The firmware check code confirms that valid firmware is loaded.
if (this->read_register16(REG_CHECKCODE, buffer, 4) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to read check code"));
this->mark_failed();
return;
}
uint32_t checkcode = encode_uint32(buffer[3], buffer[2], buffer[1], buffer[0]);
if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
ESP_LOGE(TAG, "Invalid firmware check code: 0x%08" PRIX32, checkcode);
this->status_set_error(LOG_STR("Invalid firmware check code"));
this->mark_failed();
return;
}
// Read the panel resolution unless the user supplied calibration values.
if (this->read_register16(REG_RESOLUTION, buffer, 4) == i2c::ERROR_OK) {
if (this->x_raw_max_ == this->x_raw_min_)
this->x_raw_max_ = encode_value_le<uint16_t>(buffer);
if (this->y_raw_max_ == this->y_raw_min_)
this->y_raw_max_ = encode_value_le<uint16_t>(&buffer[2]);
}
// Read the chip type and project id and validate the controller.
if (this->read_register16(REG_CHIP_INFO, buffer, 4) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to read chip ID"));
this->mark_failed();
return;
}
this->chip_id_ = encode_value_le<uint16_t>(&buffer[2]);
this->project_id_ = encode_value_le<uint16_t>(buffer);
if (this->chip_id_ != CST9220_CHIP_ID && this->chip_id_ != CST9217_CHIP_ID) {
ESP_LOGE(TAG, "Unknown chip ID: 0x%04X", this->chip_id_);
this->mark_failed(LOG_STR("Unknown chip ID"));
this->status_set_error(LOG_STR("Unknown chip ID"));
this->mark_failed();
return;
}
@@ -55,7 +84,7 @@ void CST9220Touchscreen::update_touches() {
// Only an actual I2C failure should skip the update; a successful read with no
// touches is a real "all fingers lifted" state that must flow through so the
// base class can generate the release event.
if (!this->status_comm_result(this->read_register16(cstxxx::REG_TOUCH_DATA, data, sizeof(data)) == i2c::ERROR_OK)) {
if (!this->status_comm_result(this->read_register16(REG_TOUCH_DATA, data, sizeof(data)) == i2c::ERROR_OK)) {
this->skip_update_ = true;
return;
}
@@ -63,23 +92,30 @@ void CST9220Touchscreen::update_touches() {
// A valid report carries the marker at offset 6. Writing an acknowledge back makes the controller return
// invalid frames during a long press, so none is written. An invalid frame keeps the previous points; while
// a finger is down, read again shortly so a lost release frame does not leave the touch held.
if (data[cstxxx::SYNC_IDX] != cstxxx::SYNC_BYTE) {
if (data[6] != TOUCH_ACK) {
this->skip_update_ = true;
if (!this->touches_.empty())
this->set_timeout(REREAD_TIMEOUT_ID, REREAD_DELAY_MS, [this] { this->store_.touched = true; });
return;
}
uint8_t num_touches = data[cstxxx::FINGER_COUNT_IDX] & 0x7F;
uint8_t num_touches = data[5] & 0x7F;
if (num_touches > CST9220_MAX_TOUCHES)
num_touches = CST9220_MAX_TOUCHES;
for (uint8_t i = 0; i < num_touches; i++) {
auto touch = cstxxx::decode_touch(data, i);
if (touch.event != TOUCH_EVENT_DOWN)
// The first point starts at offset 0; subsequent points are offset by the
// two status bytes that follow it.
const uint8_t *p = data + i * 5 + (i == 0 ? 0 : 2);
uint8_t id = p[0] >> 4;
uint8_t event = p[0] & 0x0F;
if (event != TOUCH_EVENT_DOWN)
continue;
ESP_LOGV(TAG, "Read touch %d: %d/%d", touch.id, touch.x, touch.y);
this->add_raw_touch_position_(touch.id, touch.x, touch.y);
// p[3] is shared: high nibble holds the X LSBs, low nibble the Y LSBs.
uint16_t x = (p[1] << 4) | (p[3] >> 4);
uint16_t y = (p[2] << 4) | (p[3] & 0x0F);
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
this->add_raw_touch_position_(id, x, y);
}
}
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/components/cstxxx/cstxxx.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -9,26 +10,41 @@ namespace esphome::cst9220 {
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
static constexpr uint8_t TOUCH_EVENT_DOWN = 0x06;
// The CST92xx family uses 16-bit (big-endian) register addresses.
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
static const uint16_t REG_CMD_MODE = 0xD101; // enter command mode
static const uint16_t REG_CHECKCODE = 0xD1FC; // firmware check code
static const uint16_t REG_RESOLUTION = 0xD1F8; // panel resolution
static const uint16_t REG_CHIP_INFO = 0xD204; // chip type + project id
static constexpr uint16_t CST9220_CHIP_ID = 0x9220;
static constexpr uint16_t CST9217_CHIP_ID = 0x9217;
static const uint8_t TOUCH_ACK = 0xAB;
static const uint8_t TOUCH_EVENT_DOWN = 0x06;
static constexpr uint8_t CST9220_MAX_TOUCHES = 5;
static const uint16_t CST9220_CHIP_ID = 0x9220;
static const uint16_t CST9217_CHIP_ID = 0x9217;
// Maximum simultaneous touch points reported by the family.
static const uint8_t CST9220_MAX_TOUCHES = 5;
// Report layout: 5 bytes per touch point plus 5 bytes of status/ack overhead.
static constexpr size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
static const size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
class CST9220Touchscreen : public cstxxx::CSTTouchscreen {
class CST9220Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void update_touches() override;
void continue_setup_();
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
uint16_t chip_id_{};
uint16_t project_id_{};
bool setup_complete_{};
};
} // namespace esphome::cst9220
-42
View File
@@ -1,42 +0,0 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@clydebarrow", "@latonita"]
DEPENDENCIES = ["i2c", "touchscreen"]
DOMAIN = "cstxxx"
cstxxx_ns = cg.esphome_ns.namespace("cstxxx")
CSTTouchscreen = cstxxx_ns.class_(
"CSTTouchscreen", touchscreen.Touchscreen, i2c.I2CDevice
)
def cst_touchscreen_schema(touchscreen_class: MockObjClass, address: int) -> cv.Schema:
"""Return the touchscreen schema of a Hynitron driver."""
return (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(touchscreen_class),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(address))
)
async def new_cst_touchscreen(config: ConfigType) -> MockObj:
"""Create a Hynitron touchscreen and set its pins."""
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
return var
-13
View File
@@ -1,13 +0,0 @@
#include "cstxxx.h"
#include "esphome/core/log.h"
namespace esphome::cstxxx {
ESPHOME_LOG_TAG(TAG, "cstxxx");
void CSTTouchscreen::fail_check_code_(uint16_t check_code) {
ESP_LOGE(TAG, "Firmware check code 0x%04X", check_code);
this->mark_failed(LOG_STR("Invalid firmware check code"));
}
} // namespace esphome::cstxxx
-92
View File
@@ -1,92 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::cstxxx {
static constexpr uint16_t REG_TOUCH_DATA = 0xD000;
static constexpr uint16_t REG_FINGER_COUNT = 0xD005;
static constexpr uint16_t REG_COMMAND_MODE = 0xD101;
static constexpr uint16_t REG_NORMAL_MODE = 0xD109;
static constexpr uint16_t REG_RESOLUTION = 0xD1F8;
static constexpr uint16_t REG_FW_CHECK_CODE = 0xD1FC;
static constexpr uint16_t REG_CHIP_INFO = 0xD204;
static constexpr uint16_t REG_FW_VERSION = 0xD208;
static constexpr uint8_t FINGER_COUNT_IDX = REG_FINGER_COUNT - REG_TOUCH_DATA;
static constexpr uint8_t SYNC_IDX = FINGER_COUNT_IDX + 1;
static constexpr uint8_t SYNC_BYTE = 0xAB;
static constexpr uint16_t FW_CHECK_CODE = 0xCACA;
/// Bytes of a touch report holding `points` records; two status bytes follow the first record.
constexpr size_t report_size(uint8_t points) { return points * 5 + 2; }
struct TouchRecord {
uint8_t id;
uint8_t event;
uint16_t x;
uint16_t y;
uint8_t z;
};
/// Decodes record `index` of a touch report.
inline TouchRecord decode_touch(const uint8_t *report, uint8_t index) {
const uint8_t *r = report + index * 5 + (index == 0 ? 0 : 2);
return {static_cast<uint8_t>(r[0] >> 4), static_cast<uint8_t>(r[0] & 0x0F),
static_cast<uint16_t>(r[1] << 4 | r[3] >> 4), static_cast<uint16_t>(r[2] << 4 | (r[3] & 0x0F)), r[4]};
}
class CSTTouchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
/// Sets up the interrupt pin, if configured, to fire on `type`.
void setup_interrupt_pin_(gpio::InterruptType type) {
if (this->interrupt_pin_ == nullptr)
return;
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, type);
}
/// Reads two little-endian words starting at `reg`.
bool ESPHOME_ALWAYS_INLINE read_words_(uint16_t reg, uint16_t &low, uint16_t &high) {
uint16_t words[2];
if (this->read_register16(reg, reinterpret_cast<uint8_t *>(words), sizeof(words)) != i2c::ERROR_OK)
return false;
low = convert_little_endian(words[0]);
high = convert_little_endian(words[1]);
return true;
}
/// Enters command mode, marking the component failed if the write fails.
bool enter_command_mode_() {
return this->setup_check(this->write_register16(REG_COMMAND_MODE, nullptr, 0) == i2c::ERROR_OK,
LOG_STR("Failed to enter command mode"));
}
/// Checks the firmware check code, marking the component failed if it is unreadable or wrong.
bool check_firmware_() {
uint16_t boot_time, check_code;
if (!this->setup_check(this->read_words_(REG_FW_CHECK_CODE, boot_time, check_code)))
return false;
if (check_code == FW_CHECK_CODE)
return true;
this->fail_check_code_(check_code);
return false;
}
/// Logs a wrong firmware check code and marks the component failed.
void fail_check_code_(uint16_t check_code);
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
bool setup_complete_{};
};
} // namespace esphome::cstxxx
@@ -3,6 +3,7 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/parse_float.h"
#include "esphome/core/progmem.h"
#include <cmath>
@@ -327,8 +328,8 @@ void DFRobotSEN0609Component::parse_response_(const char *args) {
this->response_count_ = 0;
while (this->response_count_ < MAX_RESPONSE_VALUES) {
char *end;
const float value = strtof(args, &end);
// strtof also takes "nan" and "inf", which no setting can hold
const float value = str_to_float(args, &end);
// str_to_float also takes "nan" and "inf", which no setting can hold
if (end == args || !std::isfinite(value))
break;
this->response_[this->response_count_++] = value;
@@ -7,8 +7,6 @@
#include "esphome/core/automation.h"
#include "esphome/components/network/ip_address.h"
#include <atomic>
#ifdef USE_ETHERNET
#ifdef USE_ESP32
@@ -368,9 +366,6 @@ class EthernetComponent final : public Component {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
#ifdef USE_ESP32
std::atomic<uint8_t> ip_state_pending_{0};
#endif
#endif
#ifdef USE_ETHERNET_CONNECT_TRIGGER
@@ -129,14 +129,6 @@ void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *me
void EthernetComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
// The IP events arrive on the event task; the listeners publish states, so notify them here
if (this->ip_state_pending_.load(std::memory_order_acquire) != 0) {
this->ip_state_pending_.store(0, std::memory_order_relaxed);
this->notify_ip_state_listeners_();
}
#endif
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
@@ -778,8 +770,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
global_eth_component->enable_loop_soon_any_context();
global_eth_component->notify_ip_state_listeners_();
#endif
#if USE_NETWORK_IPV6
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
@@ -807,9 +798,6 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
const esp_netif_ip_info_t *ip_info = &event->ip_info;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
global_eth_component->got_ipv4_address_ = true;
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
#endif
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
@@ -817,6 +805,9 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
@@ -828,9 +819,6 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
// after a link flap fires another event for the same address.
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(global_eth_component->eth_netif_, if_ip6s);
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
#endif
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
@@ -839,6 +827,9 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
@@ -1,5 +1,6 @@
#include "hydreon_rgxx.h"
#include "esphome/core/log.h"
#include "esphome/core/parse_float.h"
#include <cstring>
namespace esphome::hydreon_rgxx {
@@ -235,7 +236,7 @@ void HydreonRGxxComponent::process_line_(StringRef line) {
}
// parse value, starting at str position n
float data = strtof(line.c_str() + n, nullptr);
float data = str_to_float(line.c_str() + n, nullptr);
this->sensors_[i]->publish_state(data);
ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
this->sensors_received_ |= (1 << i);
+5 -4
View File
@@ -1,5 +1,6 @@
#include "smt100.h"
#include "esphome/core/log.h"
#include "esphome/core/parse_float.h"
namespace esphome::smt100 {
@@ -24,19 +25,19 @@ void SMT100Component::loop() {
token = strtok(nullptr, ",");
if (!token)
return;
float permittivity = (float) strtod(token, nullptr);
float permittivity = str_to_float(token, nullptr);
token = strtok(nullptr, ",");
if (!token)
return;
float moisture = (float) strtod(token, nullptr);
float moisture = str_to_float(token, nullptr);
token = strtok(nullptr, ",");
if (!token)
return;
float temperature = (float) strtod(token, nullptr);
float temperature = str_to_float(token, nullptr);
token = strtok(nullptr, ",");
if (!token)
return;
float voltage = (float) strtod(token, nullptr);
float voltage = str_to_float(token, nullptr);
PUBLISH_STATE(this->counts_sensor_, counts);
+2 -4
View File
@@ -48,7 +48,7 @@ CONF_WHITENING_ENABLE = "whitening_enable"
CONF_WHITENING_INITIAL = "whitening_initial"
sx126x_ns = cg.esphome_ns.namespace("sx126x")
SX126x = sx126x_ns.class_("SX126x", cg.Component)
SX126x = sx126x_ns.class_("SX126x", cg.Component, spi.SPIDevice)
SX126xListener = sx126x_ns.class_("SX126xListener")
SX126xBw = sx126x_ns.enum("SX126xBw")
SX126xPacketType = sx126x_ns.enum("SX126xPacketType")
@@ -217,9 +217,7 @@ CONFIG_SCHEMA = (
cv.Required(CONF_RF_SWITCH): cv.boolean,
cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12),
cv.Optional(CONF_SYNC_VALUE, default=[]): cv.All(
cv.ensure_list(cv.hex_uint8_t), cv.Length(max=8)
),
cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t),
cv.Optional(CONF_TCXO_VOLTAGE, default="NONE"): cv.enum(TCXO_VOLTAGE),
cv.Optional(CONF_TCXO_DELAY, default="5ms"): cv.All(
cv.positive_time_period_microseconds,
-97
View File
@@ -1,97 +0,0 @@
#pragma once
#include "sx126x_reg.h"
#include <array>
#include <span>
#include <algorithm>
namespace esphome::sx126x {
enum SX126xBw : uint8_t {
// FSK
SX126X_BW_4800,
SX126X_BW_5800,
SX126X_BW_7300,
SX126X_BW_9700,
SX126X_BW_11700,
SX126X_BW_14600,
SX126X_BW_19500,
SX126X_BW_23400,
SX126X_BW_29300,
SX126X_BW_39000,
SX126X_BW_46900,
SX126X_BW_58600,
SX126X_BW_78200,
SX126X_BW_93800,
SX126X_BW_117300,
SX126X_BW_156200,
SX126X_BW_187200,
SX126X_BW_234300,
SX126X_BW_312000,
SX126X_BW_373600,
SX126X_BW_467000,
// LORA
SX126X_BW_7810,
SX126X_BW_10420,
SX126X_BW_15630,
SX126X_BW_20830,
SX126X_BW_31250,
SX126X_BW_41670,
SX126X_BW_62500,
SX126X_BW_125000,
SX126X_BW_250000,
SX126X_BW_500000,
};
// Requested settings are separate from the last successfully applied profile.
// Bounded sync storage makes profile snapshots allocation-free.
struct RadioProfile {
uint32_t bitrate{0};
uint32_t deviation{0};
uint32_t frequency{0};
uint32_t tcxo_delay{0};
uint16_t crc_polynomial{0};
uint16_t crc_initial{0};
uint16_t whitening_initial{0};
uint16_t preamble_detect{0};
uint16_t preamble_size{0};
SX126xBw bandwidth{SX126X_BW_125000};
uint8_t crc_size{0};
uint8_t tcxo_voltage{0};
uint8_t coding_rate{0};
uint8_t modulation{PACKET_TYPE_LORA};
uint8_t pa_ramp{0};
uint8_t shaping{0};
uint8_t spreading_factor{0};
int8_t pa_power{0};
uint8_t payload_length{0};
bool sx1261{false};
bool crc_enable{false};
bool crc_inverted{false};
bool whitening_enable{false};
bool rx_start{false};
bool rf_switch{false};
std::array<uint8_t, 8> sync_value{};
uint8_t sync_size{0};
bool sync_valid{true};
void set_sync_value(std::span<const uint8_t> bytes) {
this->sync_valid = bytes.size() <= this->sync_value.size();
if (!this->sync_valid) {
return;
}
std::copy(bytes.begin(), bytes.end(), this->sync_value.begin());
this->sync_size = bytes.size();
}
size_t max_packet_size() const { return this->payload_length > 0 ? this->payload_length : 255; }
};
// The parts of the last successfully applied profile that runtime operations read.
// SetPacketParams is encoded once; sends only change its payload length byte.
struct AppliedProfile {
uint32_t frequency{0};
std::array<uint8_t, 9> packet_params{};
uint8_t packet_params_size{0};
uint8_t payload_length{0};
uint8_t modulation{PACKET_TYPE_LORA};
bool rx_start{false};
size_t max_packet_size() const { return this->payload_length > 0 ? this->payload_length : 255; }
};
} // namespace esphome::sx126x
+487 -172
View File
@@ -2,217 +2,534 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::sx126x {
ESPHOME_LOG_TAG(TAG, "sx126x");
static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000;
static constexpr uint32_t MAX_TRANSMIT_TIMEOUT_MS = 262143; // 24-bit SetTx timeout in 1/64 ms steps
static constexpr uint32_t RECONFIGURE_INTERVAL_ID = 0;
static constexpr uint32_t RECONFIGURE_INTERVAL_MS = 1000;
static constexpr uint16_t RAMP[8] PROGMEM = {10, 20, 40, 80, 200, 800, 1700, 3400};
// Aligned 32 bit loads from flash are safe on ESP8266, so this table is read directly
static constexpr uint32_t BW_HZ[31] PROGMEM = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400,
29300, 39000, 46900, 58600, 78200, 93800, 117300, 156200,
187200, 234300, 312000, 373600, 467000, 7810, 10420, 15630,
20830, 31250, 41670, 62500, 125000, 250000, 500000};
static constexpr uint8_t BW_LORA[10] PROGMEM = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830,
LORA_BW_31250, LORA_BW_41670, LORA_BW_62500, LORA_BW_125000,
LORA_BW_250000, LORA_BW_500000};
static constexpr uint8_t BW_FSK[21] PROGMEM = {
FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500,
FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800,
FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
// Indexed by SX126xError.
PROGMEM_STRING_TABLE(SX126xErrorStrings, "none", "timeout", "invalid payload", "radio not ready", "bus timeout",
"radio busy", "cancelled");
static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
static constexpr uint32_t SWITCHING_DELAY_US = 1;
static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000;
static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
// OCP (Over Current Protection) values
static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current
static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current
// LoRa low data rate optimization threshold
static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms
uint8_t SX126x::read_fifo_(uint8_t offset, std::vector<uint8_t> &packet) {
this->enable();
this->wait_busy_();
this->transfer_byte(RADIO_READ_BUFFER);
this->transfer_byte(offset);
uint8_t status = this->transfer_byte(0x00);
for (uint8_t &byte : packet) {
byte = this->transfer_byte(0x00);
}
this->disable();
return status;
}
void SX126x::write_fifo_(uint8_t offset, const uint8_t *data, size_t len) {
this->enable();
this->wait_busy_();
this->transfer_byte(RADIO_WRITE_BUFFER);
this->transfer_byte(offset);
for (size_t i = 0; i < len; i++) {
this->transfer_byte(data[i]);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
uint8_t SX126x::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
this->enable();
this->wait_busy_();
this->transfer_byte(opcode);
uint8_t status = this->transfer_byte(0x00);
for (int32_t i = 0; i < size; i++) {
data[i] = this->transfer_byte(0x00);
}
this->disable();
return status;
}
void SX126x::write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
this->enable();
this->wait_busy_();
this->transfer_byte(opcode);
for (int32_t i = 0; i < size; i++) {
this->transfer_byte(data[i]);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
void SX126x::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
this->enable();
this->wait_busy_();
this->write_byte(RADIO_READ_REGISTER);
this->write_byte((reg >> 8) & 0xFF);
this->write_byte((reg >> 0) & 0xFF);
this->write_byte(0x00);
for (int32_t i = 0; i < size; i++) {
data[i] = this->transfer_byte(0x00);
}
this->disable();
}
void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
this->enable();
this->wait_busy_();
this->write_byte(RADIO_WRITE_REGISTER);
this->write_byte((reg >> 8) & 0xFF);
this->write_byte((reg >> 0) & 0xFF);
for (int32_t i = 0; i < size; i++) {
this->transfer_byte(data[i]);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
void SX126x::setup() {
// Sized once so later packet reads never reallocate; read_packet() sets the length.
this->packet_.resize(SX126X_MAX_PACKET_SIZE);
// setup pins
GPIOPin *dio1 = this->radio_.get_dio1_pin();
this->radio_.bus().get_busy_pin()->setup();
this->radio_.get_rst_pin()->setup();
dio1->setup();
if (dio1->is_internal()) {
static_cast<InternalGPIOPin *>(dio1)->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
this->busy_pin_->setup();
this->rst_pin_->setup();
this->dio1_pin_->setup();
if (this->dio1_pin_->is_internal()) {
static_cast<InternalGPIOPin *>(this->dio1_pin_)
->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
}
// start spi
this->radio_.bus().setup();
this->spi_setup();
// configure rf; a failure is reported through mark_failed() only, with its own reason
const RadioEffects effects = this->radio_.configure(this->requested_profile_, this->version_);
if (effects.warning != nullptr) {
this->mark_failed(effects.warning);
return;
}
this->apply_(effects);
}
void SX126x::apply_(const RadioEffects &effects) {
// User code may only run once the radio has committed. A rejection during another
// operation's command sequence carries no status or callbacks.
ESPHOME_DEBUG_ASSERT(!this->radio_.busy() || (!effects.report && effects.listener == nullptr && !effects.received));
if (effects.sleep_loop) {
this->disable_loop();
} else if (this->radio_.state() != RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
this->enable_loop();
}
this->watch_configuration_();
// The packet buffer is borrowed from the status report until listeners return;
// loop() will not refill it meanwhile.
if (effects.received) {
this->rx_dispatching_ = true;
}
if (effects.report) {
this->status_comm_result(effects.warning == nullptr, effects.warning);
}
if (effects.listener != nullptr) {
effects.listener->on_transmit_complete(effects.result);
}
if (effects.received) {
for (auto *listener : this->listeners_) {
listener->on_packet(this->packet_, effects.rssi, effects.snr);
}
this->packet_trigger_.trigger(this->packet_, effects.rssi, effects.snr);
this->rx_dispatching_ = false;
}
// configure rf
this->configure();
}
void SX126x::configure() {
// Before setup() the bus is not set up, and after a failed setup the radio stays unused
if (!this->is_ready()) {
uint8_t buf[8];
// toggle chip reset
this->rst_pin_->digital_write(true);
delayMicroseconds(RESET_DELAY_HIGH_US);
this->rst_pin_->digital_write(false);
delayMicroseconds(RESET_DELAY_LOW_US);
this->rst_pin_->digital_write(true);
delayMicroseconds(RESET_DELAY_HIGH_US);
// wakeup
this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
// config tcxo
if (this->tcxo_voltage_ != TCXO_CTRL_NONE) {
uint32_t delay = this->tcxo_delay_ >> 6;
buf[0] = this->tcxo_voltage_;
buf[1] = (delay >> 16) & 0xFF;
buf[2] = (delay >> 8) & 0xFF;
buf[3] = (delay >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
buf[0] = 0x7F;
this->write_opcode_(RADIO_CALIBRATE, buf, 1);
}
// clear errors
buf[0] = 0x00;
buf[1] = 0x00;
this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
// rf switch
if (this->rf_switch_) {
buf[0] = 0x01;
this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
}
// check silicon version to make sure hw is ok
this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, sizeof(this->version_));
this->version_[sizeof(this->version_) - 1] = '\0';
if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) {
this->mark_failed(LOG_STR("Unsupported chip version"));
return;
}
if (!this->radio_.can_configure()) {
ESP_LOGW(TAG, "Radio operation in progress, configuration ignored");
return;
}
this->cold_sleep_ = false;
this->apply_(this->radio_.configure(this->requested_profile_, this->version_));
}
// A failed command leaves the radio needing configuration, which nothing else would do, so a node
// that only uses YAML would stay deaf. Cold sleep is asked for, so it waits for a mode command.
void SX126x::watch_configuration_() {
const bool needed = this->radio_.state() == RadioState::RADIO_STATE_NEEDS_CONFIGURATION && !this->cold_sleep_;
if (needed == this->reconfigure_pending_) {
return;
}
this->reconfigure_pending_ = needed;
if (needed) {
this->set_interval(RECONFIGURE_INTERVAL_ID, RECONFIGURE_INTERVAL_MS, [this]() { this->configure(); });
} else {
this->cancel_interval(RECONFIGURE_INTERVAL_ID);
}
}
// setup packet type
buf[0] = this->modulation_;
this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
void SX126x::set_mode_rx() { this->change_mode_(ModeCommand::MODE_COMMAND_RX); }
void SX126x::set_mode_tx() { this->change_mode_(ModeCommand::MODE_COMMAND_TX); }
void SX126x::set_mode_standby(SX126xStandbyMode mode) { this->change_mode_(ModeCommand::MODE_COMMAND_STANDBY, mode); }
void SX126x::set_mode_sleep(bool cold) {
this->change_mode_(cold ? ModeCommand::MODE_COMMAND_COLD_SLEEP : ModeCommand::MODE_COMMAND_SLEEP);
}
void SX126x::run_image_cal() { this->change_mode_(ModeCommand::MODE_COMMAND_IMAGE_CAL); }
// calibrate image
this->run_image_cal();
void SX126x::change_mode_(ModeCommand command, uint8_t arg) {
const bool sleep = command == ModeCommand::MODE_COMMAND_SLEEP || command == ModeCommand::MODE_COMMAND_COLD_SLEEP;
if (this->cold_sleep_) {
if (sleep) {
return;
// set frequency
uint64_t freq = ((uint64_t) this->frequency_ << 25) / XTAL_FREQ;
buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
buf[3] = (uint8_t) (freq & 0xFF);
this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
// configure pa
int8_t pa_power = this->pa_power_;
if (this->sx1261_) {
// the following values were taken from section 13.1.14.1 table 13-21
// in rev 2.1 of the datasheet
if (pa_power == 15) {
uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
} else {
uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
}
// Cold sleep lost the configuration, so waking needs it applied again first
this->configure();
pa_power = std::max(pa_power, (int8_t) -3);
pa_power = std::min(pa_power, (int8_t) 14);
buf[0] = OCP_80MA;
this->write_register_(REG_OCP, buf, 1);
} else {
// the following values were taken from section 13.1.14.1 table 13-21
// in rev 2.1 of the datasheet
uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
pa_power = std::max(pa_power, (int8_t) -3);
pa_power = std::min(pa_power, (int8_t) 22);
buf[0] = OCP_140MA;
this->write_register_(REG_OCP, buf, 1);
}
if (!this->radio_.can_change_mode()) {
ESP_LOGW(TAG, "Radio unavailable, mode change ignored");
return;
buf[0] = pa_power;
buf[1] = this->pa_ramp_;
this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
// configure modem
if (this->modulation_ == PACKET_TYPE_LORA) {
// set modulation params
float duration = 1000.0f * (1UL << this->spreading_factor_) / BW_HZ[this->bandwidth_];
buf[0] = this->spreading_factor_;
buf[1] = progmem_read_byte(&BW_LORA[this->bandwidth_ - SX126X_BW_7810]);
buf[2] = this->coding_rate_;
buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
// set packet params and sync word
this->set_packet_params_(this->get_max_packet_size());
if (this->sync_value_.size() == 2) {
this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
}
} else {
// set modulation params
uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / this->bitrate_;
uint32_t fdev = ((uint64_t) this->deviation_ << 25) / XTAL_FREQ;
buf[0] = (bitrate >> 16) & 0xFF;
buf[1] = (bitrate >> 8) & 0xFF;
buf[2] = (bitrate >> 0) & 0xFF;
buf[3] = this->shaping_;
buf[4] = progmem_read_byte(&BW_FSK[this->bandwidth_ - SX126X_BW_4800]);
buf[5] = (fdev >> 16) & 0xFF;
buf[6] = (fdev >> 8) & 0xFF;
buf[7] = (fdev >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
// set crc params
if (this->crc_enable_) {
buf[0] = this->crc_initial_ >> 8;
buf[1] = this->crc_initial_ & 0xFF;
this->write_register_(REG_CRC_INITIAL, buf, 2);
buf[0] = this->crc_polynomial_ >> 8;
buf[1] = this->crc_polynomial_ & 0xFF;
this->write_register_(REG_CRC_POLYNOMIAL, buf, 2);
}
// set whitening params
if (this->whitening_enable_) {
// according to the datasheet, section 12 table 12-1 "The user should not
// change the value of the 7 MSB of this register"
this->read_register_(REG_WHITENING_INITIAL, buf, 1);
buf[0] = (buf[0] & 0xFE) | ((this->whitening_initial_ >> 8) & 0x01);
buf[1] = this->whitening_initial_ & 0xFF;
this->write_register_(REG_WHITENING_INITIAL, buf, 2);
}
// set packet params and sync word
this->set_packet_params_(this->get_max_packet_size());
if (!this->sync_value_.empty()) {
this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
}
}
// switch to rx or sleep
if (this->rx_start_) {
this->set_mode_rx();
} else {
this->set_mode_sleep();
}
this->cold_sleep_ = command == ModeCommand::MODE_COMMAND_COLD_SLEEP;
this->apply_(this->radio_.change_mode(command, arg));
}
size_t SX126x::get_max_packet_size() { return this->radio_.max_packet_size(); }
size_t SX126x::get_max_packet_size() {
if (this->payload_length_ > 0) {
return this->payload_length_;
}
return SX126X_MAX_PACKET_SIZE;
}
void SX126x::set_packet_params_(uint8_t payload_length) {
uint8_t buf[9];
if (this->modulation_ == PACKET_TYPE_LORA) {
buf[0] = (this->preamble_size_ >> 8) & 0xFF;
buf[1] = (this->preamble_size_ >> 0) & 0xFF;
buf[2] = (this->payload_length_ > 0) ? 0x01 : 0x00;
buf[3] = payload_length;
buf[4] = (this->crc_enable_) ? 0x01 : 0x00;
buf[5] = 0x00;
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 6);
} else {
uint16_t preamble_size = this->preamble_size_ * 8;
buf[0] = (preamble_size >> 8) & 0xFF;
buf[1] = (preamble_size >> 0) & 0xFF;
buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00;
buf[3] = this->sync_value_.size() * 8;
buf[4] = 0x00;
buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01;
buf[6] = payload_length;
if (this->crc_enable_) {
buf[7] = (this->crc_inverted_ ? 0x04 : 0x00) + (this->crc_size_ & 0x02);
} else {
buf[7] = 0x01;
}
buf[8] = (this->whitening_enable_) ? 0x01 : 0x00;
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9);
}
}
SX126xError SX126x::transmit_packet(const uint8_t *data, size_t len) {
SX126xError result = this->start_transmit_(nullptr, data, len, TRANSMIT_TIMEOUT_MS);
if (result == SX126xError::SX126X_ERROR_NONE) {
// Wait for this send's own completion. A send without a listener cannot be cancelled,
// and no user code runs between these polls, so the send cannot be replaced.
result = SX126xError::SX126X_ERROR_NOT_READY;
while (this->radio_.is_transmitting()) {
const RadioEffects effects = this->radio_.poll_transmit(millis());
if (effects.finished) {
result = effects.result;
this->apply_(effects);
break;
}
App.feed_wdt();
}
}
if (result != SX126xError::SX126X_ERROR_NONE) {
ESP_LOGW(
TAG, "Packet send failed: %s",
LOG_STR_ARG(SX126xErrorStrings::get_log_str(static_cast<uint8_t>(result), SX126xErrorStrings::LAST_INDEX)));
}
return result;
}
SX126xError SX126x::start_transmit_async(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
uint32_t timeout_ms) {
if (listener == nullptr || timeout_ms == 0 || timeout_ms > MAX_TRANSMIT_TIMEOUT_MS) {
return SX126xError::SX126X_ERROR_INVALID_PARAMS;
}
return this->start_transmit_(listener, data, len, timeout_ms);
}
SX126xError SX126x::start_transmit_(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
uint32_t timeout_ms) {
// Callers such as a template number's on_value can run before setup() or after a failed setup
if (!this->is_ready()) {
ESP_LOGW(TAG, "Radio not ready, packet dropped");
return SX126xError::SX126X_ERROR_NOT_READY;
}
const RadioEffects effects = this->radio_.start_transmit(listener, data, len, timeout_ms);
this->apply_(effects);
return effects.result;
if (this->payload_length_ > 0 && this->payload_length_ != len) {
ESP_LOGE(TAG, "Packet size does not match config");
return SX126xError::INVALID_PARAMS;
}
if (len == 0 || len > this->get_max_packet_size()) {
ESP_LOGE(TAG, "Packet size out of range");
return SX126xError::INVALID_PARAMS;
}
SX126xError ret = SX126xError::NONE;
this->set_mode_standby(STDBY_XOSC);
if (this->payload_length_ == 0) {
this->set_packet_params_(len);
}
this->write_fifo_(0x00, data, len);
this->set_mode_tx();
// wait until transmit completes, typically the delay will be less than 100 ms
uint32_t start = millis();
while (!this->dio1_pin_->digital_read()) {
App.feed_wdt();
if (millis() - start > TRANSMIT_TIMEOUT_MS) {
ESP_LOGE(TAG, "Transmit packet failure");
ret = SX126xError::TIMEOUT;
break;
}
}
uint8_t buf[2];
buf[0] = 0xFF;
buf[1] = 0xFF;
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
if (this->payload_length_ == 0) {
this->set_packet_params_(this->get_max_packet_size());
}
if (this->rx_start_) {
this->set_mode_rx();
} else {
this->set_mode_sleep();
}
return ret;
}
bool SX126x::cancel_transmit_async(SX126xTransmitListener *listener) {
const RadioEffects effects = this->radio_.cancel_transmit(listener);
if (!effects.finished) {
return false;
void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr) {
for (auto &listener : this->listeners_) {
listener->on_packet(packet, rssi, snr);
}
this->apply_(effects);
return true;
this->packet_trigger_.trigger(packet, rssi, snr);
}
void SX126x::loop() {
if (this->rx_dispatching_ || this->is_failed()) {
if (this->dio1_pin_->is_internal()) {
this->disable_loop();
}
if (!this->dio1_pin_->digital_read()) {
return;
}
this->apply_(this->radio_.poll(App.get_loop_component_start_time(), this->packet_));
uint16_t status;
uint8_t buf[3];
uint8_t rssi;
int8_t snr;
this->read_opcode_(RADIO_GET_IRQSTATUS, buf, 2);
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
status = (buf[0] << 8) | buf[1];
if ((status & IRQ_RX_DONE) == IRQ_RX_DONE) {
if ((status & IRQ_CRC_ERROR) != IRQ_CRC_ERROR) {
this->read_opcode_(RADIO_GET_PACKETSTATUS, buf, 3);
if (this->modulation_ == PACKET_TYPE_LORA) {
rssi = buf[0];
snr = buf[1];
} else {
rssi = buf[2];
snr = 0;
}
this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, buf, 2);
this->packet_.resize(buf[0]);
this->read_fifo_(buf[1], this->packet_);
this->call_listeners_(this->packet_, (float) rssi / -2.0f, (float) snr / 4.0f);
}
}
}
void SX126x::run_image_cal() {
// the following values were taken from section 9.2.1 table 9-2
// in rev 2.1 of the datasheet
uint8_t buf[2] = {0, 0};
if (this->frequency_ > 900000000) {
buf[0] = 0xE1;
buf[1] = 0xE9;
} else if (this->frequency_ > 850000000) {
buf[0] = 0xD7;
buf[1] = 0xDB;
} else if (this->frequency_ > 770000000) {
buf[0] = 0xC1;
buf[1] = 0xC5;
} else if (this->frequency_ > 460000000) {
buf[0] = 0x75;
buf[1] = 0x81;
} else if (this->frequency_ > 425000000) {
buf[0] = 0x6B;
buf[1] = 0x6F;
}
if (buf[0] > 0 && buf[1] > 0) {
this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
}
}
void SX126x::set_mode_rx() {
uint8_t buf[8];
// configure irq params
uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
buf[0] = (irq >> 8) & 0xFF;
buf[1] = (irq >> 0) & 0xFF;
buf[2] = (irq >> 8) & 0xFF;
buf[3] = (irq >> 0) & 0xFF;
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
// set timeout to 0
buf[0] = 0x00;
this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
// switch to continuous mode rx
buf[0] = 0xFF;
buf[1] = 0xFF;
buf[2] = 0xFF;
this->write_opcode_(RADIO_SET_RX, buf, 3);
}
void SX126x::set_mode_tx() {
uint8_t buf[8];
// configure irq params
uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
buf[0] = (irq >> 8) & 0xFF;
buf[1] = (irq >> 0) & 0xFF;
buf[2] = (irq >> 8) & 0xFF;
buf[3] = (irq >> 0) & 0xFF;
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
// switch to single mode tx
buf[0] = 0x00;
buf[1] = 0x00;
buf[2] = 0x00;
this->write_opcode_(RADIO_SET_TX, buf, 3);
}
void SX126x::set_mode_sleep(bool cold) {
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
uint8_t buf[1];
buf[0] = cold ? 0x00 : 0x04;
this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
}
void SX126x::set_mode_standby(SX126xStandbyMode mode) {
uint8_t buf[1];
buf[0] = mode;
this->write_opcode_(RADIO_SET_STANDBY, buf, 1);
}
void SX126x::wait_busy_() {
// wait if the device is busy, the maximum delay is only be a few ms
// with most commands taking only a few us
uint32_t start = millis();
while (this->busy_pin_->digital_read()) {
if (millis() - start > BUSY_TIMEOUT_MS) {
this->mark_failed(LOG_STR("Wait busy timeout"));
break;
}
}
}
void SX126x::dump_config() {
ESP_LOGCONFIG(TAG, "SX126x:\n Ready for operation: %s",
YESNO(!this->is_failed() && this->radio_.state() == RadioState::RADIO_STATE_IDLE));
LOG_PIN(" CS Pin: ", this->radio_.bus().get_cs_pin());
LOG_PIN(" BUSY Pin: ", this->radio_.bus().get_busy_pin());
LOG_PIN(" RST Pin: ", this->radio_.get_rst_pin());
LOG_PIN(" DIO1 Pin: ", this->radio_.get_dio1_pin());
ESP_LOGCONFIG(TAG, "SX126x:");
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" BUSY Pin: ", this->busy_pin_);
LOG_PIN(" RST Pin: ", this->rst_pin_);
LOG_PIN(" DIO1 Pin: ", this->dio1_pin_);
ESP_LOGCONFIG(TAG,
" HW Version: %15s\n"
" Requested configuration:\n"
" Frequency: %" PRIu32 " Hz\n"
" Bandwidth: %" PRIu32 " Hz\n"
" PA Power: %" PRId8 " dBm\n"
" PA Ramp: %" PRIu16 " us\n"
" Payload Length: %" PRIu8 "\n"
" Payload Length: %" PRIu32 "\n"
" CRC Enable: %s\n"
" Rx Start: %s",
this->version_, this->requested_profile_.frequency,
SX126xCommands::bandwidth_hz(this->requested_profile_.bandwidth), this->requested_profile_.pa_power,
SX126xCommands::ramp_us(this->requested_profile_.pa_ramp), this->requested_profile_.payload_length,
TRUEFALSE(this->requested_profile_.crc_enable), TRUEFALSE(this->requested_profile_.rx_start));
if (this->requested_profile_.modulation == PACKET_TYPE_GFSK) {
this->version_, this->frequency_, BW_HZ[this->bandwidth_], this->pa_power_,
progmem_read_uint16(&RAMP[this->pa_ramp_]), this->payload_length_, TRUEFALSE(this->crc_enable_),
TRUEFALSE(this->rx_start_));
if (this->modulation_ == PACKET_TYPE_GFSK) {
const char *shaping = "NONE";
if (this->requested_profile_.shaping == GAUSSIAN_BT_0_3) {
if (this->shaping_ == GAUSSIAN_BT_0_3) {
shaping = "GAUSSIAN_BT_0_3";
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_0_5) {
} else if (this->shaping_ == GAUSSIAN_BT_0_5) {
shaping = "GAUSSIAN_BT_0_5";
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_0_7) {
} else if (this->shaping_ == GAUSSIAN_BT_0_7) {
shaping = "GAUSSIAN_BT_0_7";
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_1_0) {
} else if (this->shaping_ == GAUSSIAN_BT_1_0) {
shaping = "GAUSSIAN_BT_1_0";
}
ESP_LOGCONFIG(TAG,
@@ -222,15 +539,14 @@ void SX126x::dump_config() {
" Preamble Size: %" PRIu16 "\n"
" Preamble Detect: %" PRIu16 "\n"
" Bitrate: %" PRIu32 "b/s",
this->requested_profile_.deviation, shaping, this->requested_profile_.preamble_size,
this->requested_profile_.preamble_detect, this->requested_profile_.bitrate);
} else if (this->requested_profile_.modulation == PACKET_TYPE_LORA) {
this->deviation_, shaping, this->preamble_size_, this->preamble_detect_, this->bitrate_);
} else if (this->modulation_ == PACKET_TYPE_LORA) {
const char *cr = "4/8";
if (this->requested_profile_.coding_rate == LORA_CR_4_5) {
if (this->coding_rate_ == LORA_CR_4_5) {
cr = "4/5";
} else if (this->requested_profile_.coding_rate == LORA_CR_4_6) {
} else if (this->coding_rate_ == LORA_CR_4_6) {
cr = "4/6";
} else if (this->requested_profile_.coding_rate == LORA_CR_4_7) {
} else if (this->coding_rate_ == LORA_CR_4_7) {
cr = "4/7";
}
ESP_LOGCONFIG(TAG,
@@ -238,17 +554,16 @@ void SX126x::dump_config() {
" Spreading Factor: %" PRIu8 "\n"
" Coding Rate: %s\n"
" Preamble Size: %" PRIu16,
this->requested_profile_.spreading_factor, cr, this->requested_profile_.preamble_size);
this->spreading_factor_, cr, this->preamble_size_);
}
if (this->requested_profile_.sync_size != 0) {
if (!this->sync_value_.empty()) {
char hex_buf[17]; // 8 bytes max = 16 hex chars + null
ESP_LOGCONFIG(
TAG, " Sync Value: 0x%s",
format_hex_to(hex_buf, this->requested_profile_.sync_value.data(), this->requested_profile_.sync_size));
ESP_LOGCONFIG(TAG, " Sync Value: 0x%s",
format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size()));
}
ESP_LOGCONFIG(TAG, " Whitening Enable: %s", TRUEFALSE(this->requested_profile_.whitening_enable));
if (this->requested_profile_.whitening_enable) {
ESP_LOGCONFIG(TAG, " Whitening Initial: 0x%03x", this->requested_profile_.whitening_initial);
ESP_LOGCONFIG(TAG, " Whitening Enable: %s", TRUEFALSE(this->whitening_enable_));
if (this->whitening_enable_) {
ESP_LOGCONFIG(TAG, " Whitening Initial: 0x%03x", this->whitening_initial_);
}
}
+114 -80
View File
@@ -1,123 +1,157 @@
#pragma once
#include "sx126x_radio.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "sx126x_reg.h"
#include <utility>
#include <vector>
namespace esphome::sx126x {
static constexpr size_t SX126X_MAX_PACKET_SIZE = 255;
enum SX126xBw : uint8_t {
// FSK
SX126X_BW_4800,
SX126X_BW_5800,
SX126X_BW_7300,
SX126X_BW_9700,
SX126X_BW_11700,
SX126X_BW_14600,
SX126X_BW_19500,
SX126X_BW_23400,
SX126X_BW_29300,
SX126X_BW_39000,
SX126X_BW_46900,
SX126X_BW_58600,
SX126X_BW_78200,
SX126X_BW_93800,
SX126X_BW_117300,
SX126X_BW_156200,
SX126X_BW_187200,
SX126X_BW_234300,
SX126X_BW_312000,
SX126X_BW_373600,
SX126X_BW_467000,
// LORA
SX126X_BW_7810,
SX126X_BW_10420,
SX126X_BW_15630,
SX126X_BW_20830,
SX126X_BW_31250,
SX126X_BW_41670,
SX126X_BW_62500,
SX126X_BW_125000,
SX126X_BW_250000,
SX126X_BW_500000,
};
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS, SX126X_ERROR_NOT_READY };
class SX126xListener {
public:
// Called from loop(). The packet is borrowed until this returns; copy what you keep.
// Listeners may send, change mode or configure. A send returns BUSY if another packet
// is already waiting, which is then delivered by the next loop().
virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0;
};
class SX126x final : public Component {
class SX126x final : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
public:
void set_spi_parent(spi::SPIComponent *parent) { this->radio_.bus().set_spi_parent(parent); }
void set_cs_pin(GPIOPin *pin) { this->radio_.bus().set_cs_pin(pin); }
void set_data_rate(uint32_t rate) { this->radio_.bus().set_data_rate(rate); }
void set_mode(spi::SPIMode mode) { this->radio_.bus().set_mode(mode); }
void set_bit_order(spi::SPIBitOrder order) { this->radio_.bus().set_bit_order(order); }
void set_release_device(bool release) { this->radio_.bus().set_release_device(release); }
void set_write_only(bool write_only) { this->radio_.bus().set_write_only(write_only); }
#ifdef USE_SPI_PSRAM_DMA
void set_psram_dma(bool enable) { this->radio_.bus().set_psram_dma(enable); }
#endif
// Releases the SPI bus, as SPIDevice::spi_teardown() did before SX126x stopped being one
void spi_teardown() { this->radio_.bus().teardown(); }
size_t get_max_packet_size();
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void setup() override;
void loop() override;
void dump_config() override;
void set_bandwidth(SX126xBw bandwidth) { this->requested_profile_.bandwidth = bandwidth; }
void set_bitrate(uint32_t bitrate) { this->requested_profile_.bitrate = bitrate; }
void set_busy_pin(GPIOPin *busy_pin) { this->radio_.bus().set_busy_pin(busy_pin); }
void set_coding_rate(uint8_t coding_rate) { this->requested_profile_.coding_rate = coding_rate; }
void set_crc_enable(bool crc_enable) { this->requested_profile_.crc_enable = crc_enable; }
void set_crc_inverted(bool crc_inverted) { this->requested_profile_.crc_inverted = crc_inverted; }
void set_crc_size(uint8_t crc_size) { this->requested_profile_.crc_size = crc_size; }
void set_crc_polynomial(uint16_t crc_polynomial) { this->requested_profile_.crc_polynomial = crc_polynomial; }
void set_crc_initial(uint16_t crc_initial) { this->requested_profile_.crc_initial = crc_initial; }
void set_whitening_enable(bool whitening_enable) { this->requested_profile_.whitening_enable = whitening_enable; }
void set_whitening_initial(uint16_t whitening_initial) {
this->requested_profile_.whitening_initial = whitening_initial;
}
void set_deviation(uint32_t deviation) { this->requested_profile_.deviation = deviation; }
void set_dio1_pin(GPIOPin *dio1_pin) { this->radio_.set_dio1_pin(dio1_pin); }
void set_frequency(uint32_t frequency) { this->requested_profile_.frequency = frequency; }
void set_sx1261(bool sx1261) { this->requested_profile_.sx1261 = sx1261; }
// Mode changes are ignored with a warning unless the radio is idle and configured.
void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; }
void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
void set_busy_pin(GPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; }
void set_crc_size(uint8_t crc_size) { this->crc_size_ = crc_size; }
void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; }
void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; }
void set_whitening_enable(bool whitening_enable) { this->whitening_enable_ = whitening_enable; }
void set_whitening_initial(uint16_t whitening_initial) { this->whitening_initial_ = whitening_initial; }
void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
void set_dio1_pin(GPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
void set_sx1261(bool sx1261) { this->sx1261_ = sx1261; }
void set_mode_rx();
void set_mode_tx();
void set_mode_standby(SX126xStandbyMode mode);
void set_mode_sleep(bool cold = false);
void set_modulation(uint8_t modulation) { this->requested_profile_.modulation = modulation; }
void set_pa_power(int8_t power) { this->requested_profile_.pa_power = power; }
void set_pa_ramp(uint8_t ramp) { this->requested_profile_.pa_ramp = ramp; }
void set_payload_length(uint8_t payload_length) { this->requested_profile_.payload_length = payload_length; }
void set_preamble_detect(uint16_t preamble_detect) { this->requested_profile_.preamble_detect = preamble_detect; }
void set_preamble_size(uint16_t preamble_size) { this->requested_profile_.preamble_size = preamble_size; }
void set_rst_pin(GPIOPin *rst_pin) { this->radio_.set_rst_pin(rst_pin); }
void set_rx_start(bool rx_start) { this->requested_profile_.rx_start = rx_start; }
void set_rf_switch(bool rf_switch) { this->requested_profile_.rf_switch = rf_switch; }
void set_shaping(uint8_t shaping) { this->requested_profile_.shaping = shaping; }
void set_spreading_factor(uint8_t spreading_factor) { this->requested_profile_.spreading_factor = spreading_factor; }
void set_sync_value(const std::vector<uint8_t> &sync_value) { this->requested_profile_.set_sync_value(sync_value); }
void set_tcxo_voltage(uint8_t tcxo_voltage) { this->requested_profile_.tcxo_voltage = tcxo_voltage; }
void set_tcxo_delay(uint32_t tcxo_delay) { this->requested_profile_.tcxo_delay = tcxo_delay; }
void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
void set_pa_power(int8_t power) { this->pa_power_ = power; }
void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
void set_rst_pin(GPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; }
void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; }
void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; }
void set_sync_value(const std::vector<uint8_t> &sync_value) { this->sync_value_ = sync_value; }
void set_tcxo_voltage(uint8_t tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; }
void set_tcxo_delay(uint32_t tcxo_delay) { this->tcxo_delay_ = tcxo_delay; }
void run_image_cal();
// Mode changes and configuration are allowed during RX callbacks when the radio
// is available; they never modify the borrowed packet buffer.
void configure();
SX126xError transmit_packet(const uint8_t *data, size_t len);
SX126xError transmit_packet(const std::vector<uint8_t> &packet) {
return this->transmit_packet(packet.data(), packet.size());
}
// Main-loop context only. SX126X_ERROR_NONE accepts one operation and promises one completion.
// Data is copied into the chip FIFO before return; listener must outlive completion.
// Pending RX rejects admission with BUSY and is delivered by a later loop().
// Timeout: 1..262143 ms. SPI/BUSY waits are synchronous; airtime is asynchronous.
[[nodiscard]] SX126xError start_transmit_async(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
uint32_t timeout_ms = 4000);
[[nodiscard]] SX126xError start_transmit_async(SX126xTransmitListener *listener, const std::vector<uint8_t> &packet,
uint32_t timeout_ms = 4000) {
return this->start_transmit_async(listener, packet.data(), packet.size(), timeout_ms);
}
// Normal completion runs in loop(); cancellation completes synchronously in its caller.
// The radio is released before callbacks, which may start another operation if recovery
// succeeded. True accepts cancellation; the completion result reports cleanup failure.
bool cancel_transmit_async(SX126xTransmitListener *listener);
bool is_transmitting() const { return this->radio_.is_transmitting(); }
void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
private:
static void IRAM_ATTR gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
void change_mode_(ModeCommand command, uint8_t arg = 0);
// The only place that runs user code for a radio operation: loop scheduling, then status,
// then the completion or packet listeners. SX126xRadio has already committed its state.
void apply_(const RadioEffects &effects);
void watch_configuration_();
SX126xError start_transmit_(SX126xTransmitListener *listener, const uint8_t *data, size_t len, uint32_t timeout_ms);
SX126xRadio radio_;
RadioProfile requested_profile_;
char version_[16]{};
protected:
static void gpio_intr(SX126x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_packet_params_(uint8_t payload_length);
uint8_t read_fifo_(uint8_t offset, std::vector<uint8_t> &packet);
void write_fifo_(uint8_t offset, const uint8_t *data, size_t len);
void write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
uint8_t read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
void write_register_(uint16_t reg, uint8_t *data, uint8_t size);
void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
void call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr);
void wait_busy_();
Trigger<std::vector<uint8_t>, float, float> packet_trigger_;
std::vector<SX126xListener *> listeners_;
std::vector<uint8_t> packet_;
bool rx_dispatching_{false};
bool cold_sleep_{false};
bool reconfigure_pending_{false};
std::vector<uint8_t> sync_value_;
GPIOPin *busy_pin_{nullptr};
GPIOPin *dio1_pin_{nullptr};
GPIOPin *rst_pin_{nullptr};
uint32_t bitrate_{0};
uint32_t deviation_{0};
uint32_t frequency_{0};
uint32_t payload_length_{0};
uint32_t tcxo_delay_{0};
uint16_t crc_polynomial_{0};
uint16_t crc_initial_{0};
uint16_t whitening_initial_{0};
uint16_t preamble_detect_{0};
uint16_t preamble_size_{0};
char version_[16];
SX126xBw bandwidth_{SX126X_BW_125000};
uint8_t crc_size_{0};
uint8_t tcxo_voltage_{0};
uint8_t coding_rate_{0};
uint8_t modulation_{PACKET_TYPE_LORA};
uint8_t pa_ramp_{0};
uint8_t shaping_{0};
uint8_t spreading_factor_{0};
int8_t pa_power_{0};
bool sx1261_{false};
bool crc_enable_{false};
bool crc_inverted_{false};
bool whitening_enable_{false};
bool rx_start_{false};
bool rf_switch_{false};
};
} // namespace esphome::sx126x
@@ -1,355 +0,0 @@
#include "sx126x_commands.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <cmath>
#include <cstring>
namespace esphome::sx126x {
static constexpr uint16_t RAMP[8] PROGMEM = {10, 20, 40, 80, 200, 800, 1700, 3400};
// Aligned 32 bit loads from flash are safe on ESP8266, so this table is read directly
static constexpr uint32_t BW_HZ[31] PROGMEM = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400,
29300, 39000, 46900, 58600, 78200, 93800, 117300, 156200,
187200, 234300, 312000, 373600, 467000, 7810, 10420, 15630,
20830, 31250, 41670, 62500, 125000, 250000, 500000};
static constexpr uint8_t BW_LORA[10] PROGMEM = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830,
LORA_BW_31250, LORA_BW_41670, LORA_BW_62500, LORA_BW_125000,
LORA_BW_250000, LORA_BW_500000};
static constexpr uint8_t BW_FSK[21] PROGMEM = {
FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500,
FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800,
FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
// OCP (Over Current Protection) values
static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current
static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current
// LoRa low data rate optimization threshold
static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms
void SX126xCommands::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
const uint8_t header[] = {opcode, 0};
this->spi_device_.read(header, {data, size});
}
void SX126xCommands::write_opcode_(uint8_t opcode, const uint8_t *data, uint8_t size) {
this->spi_device_.write({&opcode, 1}, {data, size});
}
void SX126xCommands::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
const uint8_t header[] = {RADIO_READ_REGISTER, static_cast<uint8_t>(reg >> 8), static_cast<uint8_t>(reg), 0};
this->spi_device_.read(header, {data, size});
}
void SX126xCommands::write_register_(uint16_t reg, const uint8_t *data, uint8_t size) {
const uint8_t header[] = {RADIO_WRITE_REGISTER, static_cast<uint8_t>(reg >> 8), static_cast<uint8_t>(reg)};
this->spi_device_.write(header, {data, size});
}
ConfigurationResult SX126xCommands::configure(const RadioProfile &profile, std::span<char, 16> version,
AppliedProfile &applied) {
applied.frequency = profile.frequency;
applied.payload_length = profile.payload_length;
applied.modulation = profile.modulation;
applied.rx_start = profile.rx_start;
encode_packet_params(profile, applied);
uint8_t buf[8];
// wakeup
this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
// config tcxo
if (profile.tcxo_voltage != TCXO_CTRL_NONE) {
uint32_t delay = profile.tcxo_delay >> 6;
buf[0] = profile.tcxo_voltage;
buf[1] = (delay >> 16) & 0xFF;
buf[2] = (delay >> 8) & 0xFF;
buf[3] = (delay >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
buf[0] = 0x7F;
this->write_opcode_(RADIO_CALIBRATE, buf, 1);
}
// clear errors
buf[0] = 0x00;
buf[1] = 0x00;
this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
// rf switch
if (profile.rf_switch) {
buf[0] = 0x01;
this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
}
// check silicon version to make sure hw is ok
this->read_register_(REG_VERSION_STRING, reinterpret_cast<uint8_t *>(version.data()), version.size());
if (!this->spi_device_.ok()) {
return ConfigurationResult::CONFIGURATION_RESULT_BUSY_TIMEOUT;
}
version[version.size() - 1] = '\0';
if (strncmp(version.data(), "SX126", 5) != 0 && strncmp(version.data(), "LLCC68", 6) != 0) {
return ConfigurationResult::CONFIGURATION_RESULT_UNSUPPORTED;
}
// setup packet type
buf[0] = profile.modulation;
this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
// calibrate image
this->calibrate_image(profile.frequency);
// set frequency
uint64_t freq = ((uint64_t) profile.frequency << 25) / XTAL_FREQ;
buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
buf[3] = (uint8_t) (freq & 0xFF);
this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
// configure pa
int8_t pa_power = profile.pa_power;
if (profile.sx1261) {
// the following values were taken from section 13.1.14.1 table 13-21
// in rev 2.1 of the datasheet
if (pa_power == 15) {
uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
} else {
uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
}
pa_power = clamp(pa_power, (int8_t) -3, (int8_t) 14);
buf[0] = OCP_80MA;
this->write_register_(REG_OCP, buf, 1);
} else {
// the following values were taken from section 13.1.14.1 table 13-21
// in rev 2.1 of the datasheet
uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
pa_power = clamp(pa_power, (int8_t) -3, (int8_t) 22);
buf[0] = OCP_140MA;
this->write_register_(REG_OCP, buf, 1);
}
buf[0] = pa_power;
buf[1] = profile.pa_ramp;
this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
// configure modem
if (profile.modulation == PACKET_TYPE_LORA) {
// set modulation params
float duration = 1000.0f * (1UL << profile.spreading_factor) / BW_HZ[profile.bandwidth];
buf[0] = profile.spreading_factor;
buf[1] = progmem_read_byte(&BW_LORA[profile.bandwidth - SX126X_BW_7810]);
buf[2] = profile.coding_rate;
buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
// set packet params and sync word
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
if (profile.sync_size == 2) {
this->write_register_(REG_LORA_SYNCWORD, profile.sync_value.data(), profile.sync_size);
}
} else {
// set modulation params
uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / profile.bitrate;
uint32_t fdev = ((uint64_t) profile.deviation << 25) / XTAL_FREQ;
buf[0] = (bitrate >> 16) & 0xFF;
buf[1] = (bitrate >> 8) & 0xFF;
buf[2] = (bitrate >> 0) & 0xFF;
buf[3] = profile.shaping;
buf[4] = progmem_read_byte(&BW_FSK[profile.bandwidth - SX126X_BW_4800]);
buf[5] = (fdev >> 16) & 0xFF;
buf[6] = (fdev >> 8) & 0xFF;
buf[7] = (fdev >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
// set crc params
if (profile.crc_enable) {
buf[0] = profile.crc_initial >> 8;
buf[1] = profile.crc_initial & 0xFF;
this->write_register_(REG_CRC_INITIAL, buf, 2);
buf[0] = profile.crc_polynomial >> 8;
buf[1] = profile.crc_polynomial & 0xFF;
this->write_register_(REG_CRC_POLYNOMIAL, buf, 2);
}
// set whitening params
if (profile.whitening_enable) {
// according to the datasheet, section 12 table 12-1 "The user should not
// change the value of the 7 MSB of this register"
this->read_register_(REG_WHITENING_INITIAL, buf, 1);
buf[0] = (buf[0] & 0xFE) | ((profile.whitening_initial >> 8) & 0x01);
buf[1] = profile.whitening_initial & 0xFF;
this->write_register_(REG_WHITENING_INITIAL, buf, 2);
}
// set packet params and sync word
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
if (profile.sync_size != 0) {
this->write_register_(REG_GFSK_SYNCWORD, profile.sync_value.data(), profile.sync_size);
}
}
// switch to rx or sleep
if (profile.rx_start) {
this->receive();
} else {
this->sleep();
}
return this->spi_device_.ok() ? ConfigurationResult::CONFIGURATION_RESULT_OK
: ConfigurationResult::CONFIGURATION_RESULT_BUSY_TIMEOUT;
}
void SX126xCommands::encode_packet_params(const RadioProfile &profile, AppliedProfile &applied) {
auto &buf = applied.packet_params;
const uint8_t payload_length = profile.max_packet_size();
if (profile.modulation == PACKET_TYPE_LORA) {
buf[0] = (profile.preamble_size >> 8) & 0xFF;
buf[1] = (profile.preamble_size >> 0) & 0xFF;
buf[2] = (profile.payload_length > 0) ? 0x01 : 0x00;
buf[3] = payload_length;
buf[4] = (profile.crc_enable) ? 0x01 : 0x00;
buf[5] = 0x00;
applied.packet_params_size = 6;
} else {
uint16_t preamble_size = profile.preamble_size * 8;
buf[0] = (preamble_size >> 8) & 0xFF;
buf[1] = (preamble_size >> 0) & 0xFF;
buf[2] = (profile.preamble_detect > 0) ? ((profile.preamble_detect - 1) | 0x04) : 0x00;
buf[3] = profile.sync_size * 8;
buf[4] = 0x00;
buf[5] = (profile.payload_length > 0) ? 0x00 : 0x01;
buf[6] = payload_length;
if (profile.crc_enable) {
buf[7] = (profile.crc_inverted ? 0x04 : 0x00) + (profile.crc_size & 0x02);
} else {
buf[7] = 0x01;
}
buf[8] = (profile.whitening_enable) ? 0x01 : 0x00;
applied.packet_params_size = 9;
}
}
void SX126xCommands::set_packet_length(AppliedProfile &applied, uint8_t length) {
applied.packet_params[applied.modulation == PACKET_TYPE_LORA ? 3 : 6] = length;
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
}
void SX126xCommands::calibrate_image(uint32_t frequency) {
// the following values were taken from section 9.2.1 table 9-2
// in rev 2.1 of the datasheet
uint8_t buf[2] = {0, 0};
if (frequency > 900000000) {
buf[0] = 0xE1;
buf[1] = 0xE9;
} else if (frequency > 850000000) {
buf[0] = 0xD7;
buf[1] = 0xDB;
} else if (frequency > 770000000) {
buf[0] = 0xC1;
buf[1] = 0xC5;
} else if (frequency > 460000000) {
buf[0] = 0x75;
buf[1] = 0x81;
} else if (frequency > 425000000) {
buf[0] = 0x6B;
buf[1] = 0x6F;
}
if (buf[0] > 0 && buf[1] > 0) {
this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
}
}
void SX126xCommands::receive() {
uint8_t buf[8];
// configure irq params
uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
buf[0] = (irq >> 8) & 0xFF;
buf[1] = (irq >> 0) & 0xFF;
buf[2] = (irq >> 8) & 0xFF;
buf[3] = (irq >> 0) & 0xFF;
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
// set timeout to 0
buf[0] = 0x00;
this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
// switch to continuous mode rx
buf[0] = 0xFF;
buf[1] = 0xFF;
buf[2] = 0xFF;
this->write_opcode_(RADIO_SET_RX, buf, 3);
}
void SX126xCommands::transmit(uint32_t timeout_ms) {
uint8_t buf[8];
// configure irq params
uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
buf[0] = (irq >> 8) & 0xFF;
buf[1] = (irq >> 0) & 0xFF;
buf[2] = (irq >> 8) & 0xFF;
buf[3] = (irq >> 0) & 0xFF;
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
const uint32_t ticks = timeout_ms * 64;
buf[0] = ticks >> 16;
buf[1] = ticks >> 8;
buf[2] = ticks;
this->write_opcode_(RADIO_SET_TX, buf, 3);
}
void SX126xCommands::sleep(bool cold) {
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
const uint8_t mode = cold ? 0x00 : 0x04;
this->write_opcode_(RADIO_SET_SLEEP, &mode, 1);
}
void SX126xCommands::standby(SX126xStandbyMode mode) {
const uint8_t value = mode;
this->write_opcode_(RADIO_SET_STANDBY, &value, 1);
}
uint32_t SX126xCommands::bandwidth_hz(SX126xBw bw) { return BW_HZ[bw]; }
uint16_t SX126xCommands::ramp_us(uint8_t ramp) { return progmem_read_uint16(&RAMP[ramp]); }
void SX126xCommands::clear_irqs(uint16_t mask) {
const uint8_t data[] = {static_cast<uint8_t>(mask >> 8), static_cast<uint8_t>(mask)};
this->write_opcode_(RADIO_CLR_IRQSTATUS, data, 2);
}
uint16_t SX126xCommands::read_irq() {
uint8_t data[2]{};
this->read_opcode_(RADIO_GET_IRQSTATUS, data, 2);
return (uint16_t(data[0]) << 8) | data[1];
}
void SX126xCommands::write_fifo(const uint8_t *data, size_t size) {
const uint8_t header[] = {RADIO_WRITE_BUFFER, 0};
this->spi_device_.write(header, {data, size});
}
void SX126xCommands::read_packet(uint8_t modulation, std::vector<uint8_t> &packet, float &rssi, float &snr) {
uint8_t data[3]{};
this->read_opcode_(RADIO_GET_PACKETSTATUS, data, 3);
rssi = (modulation == PACKET_TYPE_LORA ? data[0] : data[2]) / -2.0f;
// FSK packets report no SNR
snr = modulation == PACKET_TYPE_LORA ? static_cast<int8_t>(data[1]) / 4.0f : NAN;
this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, data, 2);
if (!this->spi_device_.ok()) {
return; // The length and offset were not read.
}
packet.resize(data[0]);
const uint8_t header[] = {RADIO_READ_BUFFER, data[1], 0};
this->spi_device_.read(header, packet);
}
} // namespace esphome::sx126x
@@ -1,49 +0,0 @@
#pragma once
#include "radio_profile.h"
#include "sx126x_spi_device.h"
#include <vector>
namespace esphome::sx126x {
enum class ConfigurationResult {
CONFIGURATION_RESULT_OK,
CONFIGURATION_RESULT_BUSY_TIMEOUT,
CONFIGURATION_RESULT_UNSUPPORTED
};
// Encodes chip commands. No ESPHome lifecycle, callbacks, or raw SPI access.
// Commands record failure in the SPI device: callers start a sequence with begin()
// and check ok() at its end, or before acting on a value that was read.
class SX126xCommands {
public:
SX126xCommands() = default;
SX126xCommands(const SX126xCommands &) = delete;
SX126xCommands &operator=(const SX126xCommands &) = delete;
SX126xSpiDevice &bus() { return this->spi_device_; }
// Fills applied as it goes; only use it if the result is OK.
[[nodiscard]] ConfigurationResult configure(const RadioProfile &profile, std::span<char, 16> version,
AppliedProfile &applied);
// Sets the payload length in the applied packet parameters and writes them.
void set_packet_length(AppliedProfile &applied, uint8_t length);
void calibrate_image(uint32_t frequency);
void standby(SX126xStandbyMode mode);
void receive();
void transmit(uint32_t timeout_ms = 0);
void sleep(bool cold = false);
void clear_irqs(uint16_t mask = 0xFFFF);
// Returns 0 if the read was skipped or failed.
uint16_t read_irq();
void write_fifo(const uint8_t *data, size_t size);
// Leaves the packet unchanged if a read fails.
void read_packet(uint8_t modulation, std::vector<uint8_t> &packet, float &rssi, float &snr);
static uint32_t bandwidth_hz(SX126xBw bw);
static uint16_t ramp_us(uint8_t ramp);
private:
void read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
void write_opcode_(uint8_t opcode, const uint8_t *data, uint8_t size);
void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
void write_register_(uint16_t reg, const uint8_t *data, uint8_t size);
static void encode_packet_params(const RadioProfile &profile, AppliedProfile &applied);
SX126xSpiDevice spi_device_;
};
} // namespace esphome::sx126x
-253
View File
@@ -1,253 +0,0 @@
#include "sx126x_radio.h"
#include "esphome/core/log.h"
#include <utility>
namespace esphome::sx126x {
// No ESP_LOG calls in this file: log listeners are user code, see sx126x_radio.h.
static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
static constexpr uint32_t RECEIVE_RETRY_INTERVAL_MS = 200;
RadioEffects SX126xRadio::comm_report(bool ok) {
RadioEffects effects;
effects.report = true;
effects.warning = ok ? nullptr : LOG_STR("Wait busy timeout");
return effects;
}
RadioEffects SX126xRadio::configure(const RadioProfile &requested, std::span<char, 16> version) {
ESPHOME_DEBUG_ASSERT(this->can_configure());
this->busy_ = true;
this->rx_retry_pending_ = false;
this->pending_irq_ = 0;
RadioEffects effects;
effects.report = true;
effects.warning = this->apply_configuration_(requested, version);
this->commit_(effects.warning == nullptr ? RadioState::RADIO_STATE_IDLE
: RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
return effects;
}
const LogString *SX126xRadio::apply_configuration_(const RadioProfile &requested, std::span<char, 16> version) {
if (!requested.sync_valid) {
return LOG_STR("Invalid sync value");
}
this->rst_pin_->digital_write(true);
delayMicroseconds(RESET_DELAY_HIGH_US);
this->rst_pin_->digital_write(false);
delayMicroseconds(RESET_DELAY_LOW_US);
this->rst_pin_->digital_write(true);
delayMicroseconds(RESET_DELAY_HIGH_US);
this->bus().begin();
AppliedProfile applied;
switch (this->commands_.configure(requested, version, applied)) {
case ConfigurationResult::CONFIGURATION_RESULT_OK:
this->applied_ = applied;
return nullptr;
case ConfigurationResult::CONFIGURATION_RESULT_UNSUPPORTED:
return LOG_STR("Unsupported chip version");
default:
return LOG_STR("Wait busy timeout");
}
}
RadioEffects SX126xRadio::change_mode(ModeCommand command, uint8_t arg) {
ESPHOME_DEBUG_ASSERT(this->can_change_mode());
this->busy_ = true;
this->bus().begin();
switch (command) {
case ModeCommand::MODE_COMMAND_RX:
this->commands_.receive();
break;
case ModeCommand::MODE_COMMAND_TX:
this->commands_.transmit();
break;
case ModeCommand::MODE_COMMAND_STANDBY:
this->commands_.standby(static_cast<SX126xStandbyMode>(arg));
break;
case ModeCommand::MODE_COMMAND_SLEEP:
case ModeCommand::MODE_COMMAND_COLD_SLEEP:
this->commands_.sleep(command == ModeCommand::MODE_COMMAND_COLD_SLEEP);
break;
case ModeCommand::MODE_COMMAND_IMAGE_CAL:
this->commands_.calibrate_image(this->applied_.frequency);
break;
}
const bool ok = this->bus().ok();
// Cold sleep loses the configuration, and a failed command leaves the chip state unknown.
this->commit_(ok && command != ModeCommand::MODE_COMMAND_COLD_SLEEP ? RadioState::RADIO_STATE_IDLE
: RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
return comm_report(ok);
}
RadioEffects SX126xRadio::start_transmit(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
uint32_t timeout_ms) {
RadioEffects effects;
if (this->state_ == RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
effects.result = SX126xError::SX126X_ERROR_NOT_READY;
return effects;
}
if (this->busy_ || this->state_ != RadioState::RADIO_STATE_IDLE) {
effects.result = SX126xError::SX126X_ERROR_BUSY;
return effects;
}
const uint8_t fixed_length = this->applied_.payload_length;
if (data == nullptr || len == 0 || len > this->max_packet_size() || (fixed_length != 0 && len != fixed_length)) {
effects.result = SX126xError::SX126X_ERROR_INVALID_PARAMS;
return effects;
}
this->busy_ = true;
this->bus().begin();
this->commands_.standby(STDBY_XOSC);
// RX cannot advance in standby. Keep a packet that completed before standby intact for
// poll(), before clearing IRQs or overwriting the shared FIFO. A failed read returns 0.
// A packet that failed its CRC is not delivered, so it does not hold up the send.
const uint16_t irq = this->commands_.read_irq() | this->pending_irq_;
if ((irq & IRQ_RX_DONE) && !(irq & IRQ_CRC_ERROR)) {
this->commit_(RadioState::RADIO_STATE_HOLDING_RX);
effects.result = SX126xError::SX126X_ERROR_BUSY;
return effects;
}
this->commands_.clear_irqs();
this->pending_irq_ = 0;
if (fixed_length == 0) {
this->commands_.set_packet_length(this->applied_, len);
}
this->commands_.write_fifo(data, len);
this->commands_.transmit(timeout_ms);
if (!this->bus().ok()) {
const bool restored = this->restore_();
this->commit_(restored ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
effects = comm_report(restored);
effects.result = SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
return effects;
}
this->tx_ = {listener, millis(), timeout_ms};
this->commit_(RadioState::RADIO_STATE_TRANSMITTING);
// No status report on success, so no user code runs before the caller has the result.
// The completion reports status instead.
return effects;
}
RadioEffects SX126xRadio::poll_transmit(uint32_t now) {
if (this->busy_ || !this->is_transmitting()) {
return {};
}
bool done = false;
SX126xError outcome = SX126xError::SX126X_ERROR_NONE;
if (this->dio1_pin_->digital_read()) {
this->busy_ = true;
this->bus().begin();
const uint16_t status = this->commands_.read_irq();
this->commands_.clear_irqs(status);
this->busy_ = false;
if (!this->bus().ok()) {
outcome = SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
done = true;
} else if (status & IRQ_RX_TX_TIMEOUT) {
outcome = SX126xError::SX126X_ERROR_TIMEOUT;
done = true;
} else if (status & IRQ_TX_DONE) {
done = true;
}
}
// A cached loop tick can precede admission; signed subtraction also handles wraparound.
if (!done && static_cast<int32_t>(now - this->tx_.started_ms) >= static_cast<int32_t>(this->tx_.timeout_ms)) {
outcome = SX126xError::SX126X_ERROR_TIMEOUT;
done = true;
}
return done ? this->finish_transmit_(outcome) : RadioEffects{};
}
RadioEffects SX126xRadio::cancel_transmit(SX126xTransmitListener *listener) {
if (this->busy_ || !this->is_transmitting() || listener == nullptr || listener != this->tx_.listener) {
return {};
}
return this->finish_transmit_(SX126xError::SX126X_ERROR_CANCELLED);
}
// The only place a send ends.
RadioEffects SX126xRadio::finish_transmit_(SX126xError result) {
this->busy_ = true;
const bool restored = this->restore_();
this->commit_(restored ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
RadioEffects effects = comm_report(restored);
effects.result = restored ? result : SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
effects.listener = std::exchange(this->tx_.listener, nullptr);
effects.finished = true;
return effects;
}
bool SX126xRadio::restore_() {
this->bus().begin();
this->commands_.standby(STDBY_XOSC);
this->commands_.clear_irqs();
if (this->applied_.payload_length == 0) {
this->commands_.set_packet_length(this->applied_, this->max_packet_size());
}
if (this->applied_.rx_start) {
this->commands_.receive();
} else {
this->commands_.sleep();
}
return this->bus().ok();
}
RadioEffects SX126xRadio::poll(uint32_t now, std::vector<uint8_t> &packet) {
if (this->busy_) {
return {};
}
if (this->is_transmitting()) {
return this->poll_transmit(now);
}
RadioEffects effects;
if (this->state_ == RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
effects.sleep_loop = true; // Polling cannot recover the radio; configure() can.
return effects;
}
if (this->rx_retry_pending_ && now - this->rx_retry_started_ms_ < RECEIVE_RETRY_INTERVAL_MS) {
return effects;
}
const bool holding = this->state_ == RadioState::RADIO_STATE_HOLDING_RX;
if (!holding && this->pending_irq_ == 0 && !this->dio1_pin_->digital_read()) {
effects.sleep_loop = this->dio1_pin_->is_internal(); // Its interrupt wakes the loop.
return effects;
}
return this->receive_(holding, now, packet);
}
RadioEffects SX126xRadio::receive_(bool holding, uint32_t now, std::vector<uint8_t> &packet) {
this->busy_ = true;
this->bus().begin();
// A packet whose read failed has had its flags cleared, so they are kept until it is read.
const uint16_t status = this->commands_.read_irq() | this->pending_irq_;
const bool received = (status & IRQ_RX_DONE) && !(status & IRQ_CRC_ERROR);
if (!holding) {
// Cleared before the read, so a packet that completes during the read raises DIO1 again.
// A held packet keeps its IRQ until restoration.
this->commands_.clear_irqs(status);
}
float rssi = 0, snr = 0;
if (received) {
this->commands_.read_packet(this->applied_.modulation, packet, rssi, snr);
}
const bool read = this->bus().ok();
this->pending_irq_ = read ? 0 : status;
// Do not spend a BUSY timeout on every loop while a read keeps failing.
this->rx_retry_pending_ = !read;
this->rx_retry_started_ms_ = now;
RadioState next = this->state_;
if (read && holding) {
next = this->restore_() ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION;
}
this->commit_(next);
RadioEffects effects = comm_report(read && next != RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
effects.received = read && received;
effects.rssi = rssi;
effects.snr = snr;
return effects;
}
} // namespace esphome::sx126x
-149
View File
@@ -1,149 +0,0 @@
#pragma once
#include "radio_profile.h"
#include "sx126x_commands.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <span>
#include <vector>
namespace esphome::sx126x {
enum class SX126xError {
SX126X_ERROR_NONE = 0,
SX126X_ERROR_TIMEOUT,
SX126X_ERROR_INVALID_PARAMS,
SX126X_ERROR_NOT_READY,
SX126X_ERROR_BUSY_TIMEOUT,
SX126X_ERROR_BUSY,
SX126X_ERROR_CANCELLED
};
class SX126xTransmitListener {
public:
virtual void on_transmit_complete(SX126xError result) = 0;
};
// Who may use the radio between operations. This is driver state, not the chip's mode.
enum class RadioState : uint8_t {
// Startup, cold sleep, or a failed configuration, mode command or restoration. Only
// configure() is accepted; sends return NOT_READY and the loop sleeps. SX126x configures
// again every second after a failure, and on the next mode command after cold sleep.
RADIO_STATE_NEEDS_CONFIGURATION,
// Available. The chip is receiving or sleeping, as configured, or after the TX mode command
// sending whatever the FIFO holds. That send is not tracked: a send or mode command may cut
// it short, and poll() clears its TX_DONE as a flag without a packet.
RADIO_STATE_IDLE,
// A send is on air. SPI is free between polls; only polling and cancellation are accepted.
RADIO_STATE_TRANSMITTING,
// Standby with a received packet left in the FIFO, found when a send entered standby.
// The send was rejected with BUSY; the next poll() delivers the packet and restores the
// radio. configure() may discard it to recover from repeated read failures.
RADIO_STATE_HOLDING_RX,
};
enum class ModeCommand : uint8_t {
MODE_COMMAND_RX,
MODE_COMMAND_TX,
MODE_COMMAND_STANDBY,
MODE_COMMAND_SLEEP,
MODE_COMMAND_COLD_SLEEP,
MODE_COMMAND_IMAGE_CAL,
};
// What a radio operation leaves for its caller to do. The radio has already committed its
// state; the caller reports status first, then calls the listener or delivers the packet.
struct RadioEffects {
SX126xError result{SX126xError::SX126X_ERROR_NONE};
// The completion to notify when finished is set; null for a blocking send.
SX126xTransmitListener *listener{nullptr};
// With report set: the warning to set, or null to clear it.
const LogString *warning{nullptr};
float rssi{0};
float snr{0};
bool report{false};
// A send ended, and result is its outcome.
bool finished{false};
// The packet buffer holds a packet to deliver.
bool received{false};
// Nothing to poll until the next DIO1 edge or configuration.
bool sleep_loop{false};
};
// Owns the radio state and every SPI command sequence.
//
// Status reports, completions and packet listeners can run user code, which may call back
// into the driver. Every operation therefore does its SPI work with busy() set, commits the
// next state, and returns RadioEffects; SX126x::apply_() then reports status and calls the
// listeners. This class has no access to component status or listeners, so callbacks only
// ever see a committed state. Do not log here either: log listeners are user code. busy()
// covers re-entry during a sequence, for example from a GPIO pin that logs.
//
// Each sequence starts with bus().begin(); after a BUSY timeout the rest is skipped and
// bus().ok() is false. A failed send preparation restores the radio in a new sequence. A
// failed restoration needs configuration, and its result overrides the send result. A
// successful send start reports nothing, so no user code runs before the caller has the
// result. Every send ends in finish_transmit_(), exactly once.
//
// A failed receive read keeps the packet and retries after RECEIVE_RETRY_INTERVAL_MS, so a
// stuck BUSY line does not cost a 20 ms wait on every loop.
class SX126xRadio {
public:
SX126xRadio() = default;
SX126xRadio(const SX126xRadio &) = delete;
SX126xRadio &operator=(const SX126xRadio &) = delete;
SX126xSpiDevice &bus() { return this->commands_.bus(); }
void set_dio1_pin(GPIOPin *pin) { this->dio1_pin_ = pin; }
void set_rst_pin(GPIOPin *pin) { this->rst_pin_ = pin; }
GPIOPin *get_dio1_pin() const { return this->dio1_pin_; }
GPIOPin *get_rst_pin() const { return this->rst_pin_; }
RadioState state() const { return this->state_; }
bool busy() const { return this->busy_; }
bool is_transmitting() const { return this->state_ == RadioState::RADIO_STATE_TRANSMITTING; }
// A held packet may be discarded: explicit reconfiguration is the recovery path.
bool can_configure() const { return !this->busy_ && !this->is_transmitting(); }
bool can_change_mode() const { return !this->busy_ && this->state_ == RadioState::RADIO_STATE_IDLE; }
size_t max_packet_size() const { return this->applied_.max_packet_size(); }
// Requires can_configure(). Resets the chip and applies the requested profile.
RadioEffects configure(const RadioProfile &requested, std::span<char, 16> version);
// Requires can_change_mode(). Cold sleep and failed commands need configuration afterwards.
RadioEffects change_mode(ModeCommand command, uint8_t arg);
// Result SX126X_ERROR_NONE starts a send that finishes exactly once, through poll_transmit() or
// cancel_transmit(). Any other result owes no completion.
RadioEffects start_transmit(SX126xTransmitListener *listener, const uint8_t *data, size_t len, uint32_t timeout_ms);
// Sets finished only when it ended the current send.
RadioEffects poll_transmit(uint32_t now);
// Sets finished only if listener owns the current send.
RadioEffects cancel_transmit(SX126xTransmitListener *listener);
// One loop step: polls a send, or reads a received packet into packet.
RadioEffects poll(uint32_t now, std::vector<uint8_t> &packet);
private:
struct TxOperation {
SX126xTransmitListener *listener{nullptr};
uint32_t started_ms{0};
uint32_t timeout_ms{0};
};
void commit_(RadioState state) {
this->state_ = state;
this->busy_ = false;
}
static RadioEffects comm_report(bool ok);
const LogString *apply_configuration_(const RadioProfile &requested, std::span<char, 16> version);
bool restore_();
RadioEffects finish_transmit_(SX126xError result);
RadioEffects receive_(bool holding, uint32_t now, std::vector<uint8_t> &packet);
SX126xCommands commands_;
AppliedProfile applied_;
TxOperation tx_;
uint32_t rx_retry_started_ms_{0};
uint16_t pending_irq_{0}; // flags of a received packet whose read failed after they were cleared
GPIOPin *dio1_pin_{nullptr};
GPIOPin *rst_pin_{nullptr};
RadioState state_{RadioState::RADIO_STATE_NEEDS_CONFIGURATION};
bool busy_{false};
bool rx_retry_pending_{false};
};
} // namespace esphome::sx126x
@@ -1,45 +0,0 @@
#include "sx126x_spi_device.h"
#include "esphome/core/hal.h"
namespace esphome::sx126x {
static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
static constexpr uint32_t SWITCHING_DELAY_US = 1;
void SX126xSpiDevice::exchange_(std::span<const uint8_t> header, std::span<const uint8_t> tx, std::span<uint8_t> rx) {
if (!this->ok_) {
return;
}
// Selecting first wakes a sleeping radio. The guard releases CS even on timeout.
Selection selected(this->device_);
const uint32_t start = millis();
while (this->busy_pin_->digital_read()) {
if (millis() - start > BUSY_TIMEOUT_MS) {
this->ok_ = false;
return;
}
}
for (auto byte : header) {
this->device_.transfer_byte(byte);
}
for (auto byte : tx) {
this->device_.transfer_byte(byte);
}
for (auto &byte : rx) {
byte = this->device_.transfer_byte(0);
}
}
void SX126xSpiDevice::write(std::span<const uint8_t> header, std::span<const uint8_t> data) {
this->exchange_(header, data, {});
if (this->ok_) {
// This delay belongs after CS is released, not in the selected interval.
delayMicroseconds(SWITCHING_DELAY_US);
}
}
void SX126xSpiDevice::read(std::span<const uint8_t> header, std::span<uint8_t> data) {
this->exchange_(header, {}, data);
}
} // namespace esphome::sx126x
@@ -1,63 +0,0 @@
#pragma once
#include "esphome/components/spi/spi.h"
#include <span>
namespace esphome::sx126x {
// Persistent radio device. read()/write() use exchange_() for one complete selected interval.
// No raw SPI access escapes. Call only from the main loop; spans are borrowed until return.
// Failures stick: after a BUSY timeout every exchange is skipped until the next begin(),
// so a command sequence checks ok() once instead of after every command.
class SX126xSpiDevice {
public:
SX126xSpiDevice() = default;
SX126xSpiDevice(const SX126xSpiDevice &) = delete;
SX126xSpiDevice &operator=(const SX126xSpiDevice &) = delete;
void set_spi_parent(spi::SPIComponent *parent) { this->device_.set_spi_parent(parent); }
void set_cs_pin(GPIOPin *pin) { this->device_.set_cs_pin(pin); }
void set_data_rate(uint32_t rate) { this->device_.set_data_rate(rate); }
void set_mode(spi::SPIMode mode) { this->device_.set_mode(mode); }
void set_bit_order(spi::SPIBitOrder order) { this->device_.set_bit_order(order); }
void set_release_device(bool release) { this->device_.set_release_device(release); }
void set_write_only(bool write_only) { this->device_.set_write_only(write_only); }
#ifdef USE_SPI_PSRAM_DMA
void set_psram_dma(bool enable) { this->device_.set_psram_dma(enable); }
#endif
void set_busy_pin(GPIOPin *pin) { this->busy_pin_ = pin; }
GPIOPin *get_busy_pin() const { return this->busy_pin_; }
GPIOPin *get_cs_pin() const { return this->device_.get_cs_pin(); }
void setup() { this->device_.spi_setup(); }
void teardown() { this->device_.spi_teardown(); }
// Starts a command sequence with no recorded failure.
void begin() { this->ok_ = true; }
// False once any exchange since begin() timed out waiting for BUSY.
bool ok() const { return this->ok_; }
void write(std::span<const uint8_t> header, std::span<const uint8_t> data);
// Leaves data unchanged when the exchange is skipped or times out.
void read(std::span<const uint8_t> header, std::span<uint8_t> data);
private:
class Device final : public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_8MHZ> {
public:
GPIOPin *get_cs_pin() const { return this->cs_; }
};
class Selection {
public:
explicit Selection(Device &device) : device_(device) { this->device_.enable(); }
~Selection() { this->device_.disable(); }
Selection(const Selection &) = delete;
Selection &operator=(const Selection &) = delete;
private:
Device &device_;
};
void exchange_(std::span<const uint8_t> header, std::span<const uint8_t> tx, std::span<uint8_t> rx);
Device device_;
GPIOPin *busy_pin_{nullptr};
bool ok_{true};
};
} // namespace esphome::sx126x
+2 -6
View File
@@ -5,15 +5,13 @@ from esphome.const import CONF_MODEL
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@glmnet", "@arpena", "@Rodrigoah"]
CODEOWNERS = ["@glmnet", "@arpena"]
whirlpool_ns = cg.esphome_ns.namespace("whirlpool")
WhirlpoolClimate = whirlpool_ns.class_("WhirlpoolClimate", climate_ir.ClimateIR)
Model = whirlpool_ns.enum("Model")
MODELS = Model.options(
"DG11J1-3A", "DG11J1-91", "HISENSE_ARG", "HITACHI_HCRA31NEWH", prefix="MODEL_"
)
MODELS = Model.options("DG11J1-3A", "DG11J1-91", "HISENSE_ARG", prefix="MODEL_")
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend(
{
@@ -28,5 +26,3 @@ async def to_code(config: ConfigType) -> None:
# cv.enum keeps the YAML key as a str subclass; the MockObj value cannot be compared
if config[CONF_MODEL] == "HISENSE_ARG":
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
elif config[CONF_MODEL] == "HITACHI_HCRA31NEWH":
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
+15 -61
View File
@@ -2,8 +2,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::whirlpool {
ESPHOME_LOG_TAG(TAG, "whirlpool.climate");
@@ -46,17 +44,6 @@ static constexpr uint8_t HISENSE_ARG_POWER_TOGGLE = 0x01; // byte 15
static constexpr uint8_t HISENSE_ARG_SETTINGS_ONLY = 0x02; // byte 15
static constexpr uint8_t HISENSE_ARG_FIXED_TEMP_STEP = 7; // auto and fan-only send 23 C
// Hitachi HCRA31NEWH model: power is a state in every frame
static constexpr uint8_t HITACHI_BYTE10 = 0x20; // byte 10
static constexpr uint8_t HITACHI_COOL = 0x02; // byte 15
static constexpr uint8_t HITACHI_OFF = 0x01; // byte 15
static constexpr uint8_t HITACHI_BYTE18_ON = 0x38; // byte 18
static constexpr uint8_t HITACHI_BYTE18_OFF = 0x28; // byte 18
static constexpr uint8_t HITACHI_BYTE19 = 0x08; // byte 19
// Header, 21 bytes of 8 bits, two dividers and the footer mark
static constexpr uint32_t WHIRLPOOL_FRAME_TIMINGS = 2 + WHIRLPOOL_STATE_LENGTH * 16 + 2 * 2 + 1;
WhirlpoolClimate::WhirlpoolClimate()
: climate_ir::ClimateIR(
WHIRLPOOL_DG11J1_3A_TEMP_MIN, WHIRLPOOL_DG11J1_3A_TEMP_MAX, 1.0f, true, true,
@@ -68,19 +55,19 @@ void WhirlpoolClimate::transmit_state() {
this->last_transmit_time_ = millis(); // setting the time of the last transmission.
uint8_t remote_state[WHIRLPOOL_STATE_LENGTH];
memset(remote_state, 0, sizeof(remote_state));
put_be16(remote_state, this->remote_id_());
remote_state[0] = 0x83;
remote_state[1] = 0x06;
remote_state[6] = 0x80;
remote_state[18] = 0x08;
remote_state[18] = 0x08; // the same on every supported model
const bool powered_on = this->mode != climate::CLIMATE_MODE_OFF;
// Hitachi sends power as a state instead of a toggle
const bool power_toggle = !this->is_hitachi_hcra31newh_() && powered_on != this->powered_on_assumed;
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
const bool power_toggle = powered_on != this->powered_on_assumed;
if (power_toggle) {
// Set power toggle command
remote_state[2] = 4;
remote_state[15] = 1;
this->powered_on_assumed = powered_on;
}
this->powered_on_assumed = powered_on;
switch (this->mode) {
case climate::CLIMATE_MODE_HEAT_COOL:
// set fan auto
@@ -106,10 +93,6 @@ void WhirlpoolClimate::transmit_state() {
remote_state[15] = 6;
break;
case climate::CLIMATE_MODE_OFF:
// These remotes keep the cool code in the power-off frame
if (this->is_hisense_arg_() || this->is_hitachi_hcra31newh_())
remote_state[3] = WHIRLPOOL_COOL;
break;
default:
break;
}
@@ -119,7 +102,6 @@ void WhirlpoolClimate::transmit_state() {
// Temperature
auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_()));
const auto temp_step = (uint8_t) (temp - this->temperature_min_());
this->target_temperature = temp; // publish what the unit was sent
if (hisense_arg && this->fixed_temp_mode_()) {
remote_state[3] |= HISENSE_ARG_FIXED_TEMP_STEP << 4;
} else {
@@ -149,6 +131,7 @@ void WhirlpoolClimate::transmit_state() {
remote_state[2] |= WHIRLPOOL_SWING_MASK;
remote_state[8] = WHIRLPOOL_SWING_BYTE8;
}
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
} else if (this->send_swing_cmd_) {
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) {
remote_state[2] |= WHIRLPOOL_SWING_MASK;
@@ -156,11 +139,6 @@ void WhirlpoolClimate::transmit_state() {
}
}
if (hisense_arg)
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
if (this->is_hitachi_hcra31newh_())
this->encode_hitachi_hcra31newh_(remote_state);
// Checksum
remote_state[13] = xor_bytes(&remote_state[2], 11, remote_state[13]);
remote_state[20] = xor_bytes(&remote_state[14], 6, remote_state[20]);
@@ -170,7 +148,6 @@ void WhirlpoolClimate::transmit_state() {
// Send code
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->reserve(WHIRLPOOL_FRAME_TIMINGS);
data->set_carrier_frequency(38000);
@@ -203,6 +180,10 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
remote_state[15] = power_toggle ? HISENSE_ARG_POWER_TOGGLE : HISENSE_ARG_SETTINGS_ONLY;
switch (this->mode) {
case climate::CLIMATE_MODE_OFF:
// The remote keeps the cool code in the power-off frame
remote_state[3] = (remote_state[3] & 0xF0) | WHIRLPOOL_COOL;
break;
case climate::CLIMATE_MODE_DRY:
// The unit runs these fan speeds itself; the user's choice is kept for the next mode change
remote_state[2] &= ~WHIRLPOOL_FAN_MASK;
@@ -231,27 +212,6 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
#endif
}
void WhirlpoolClimate::encode_hitachi_hcra31newh_(uint8_t *remote_state) const {
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
remote_state[10] = HITACHI_BYTE10;
remote_state[18] = HITACHI_BYTE18_ON;
remote_state[19] = HITACHI_BYTE19;
// The unit takes the whole state from every frame: only the off frame carries the power bit
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
remote_state[15] = HITACHI_COOL;
break;
case climate::CLIMATE_MODE_OFF:
remote_state[2] |= WHIRLPOOL_POWER;
remote_state[15] = HITACHI_OFF;
remote_state[18] = HITACHI_BYTE18_OFF;
break;
default:
break;
}
#endif
}
bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
// Check if the esp isn't currently transmitting.
if (millis() - this->last_transmit_time_ < 500) {
@@ -274,8 +234,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP))
return false;
}
// The Hitachi remote always sends all 21 bytes; byte 18 holds its power state
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid() && !this->is_hitachi_hcra31newh_()) {
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid()) {
// Remote control only sent 14 bytes, nothing more to read, not even the footer
ESP_LOGV(TAG, "Remote control only sent %d bytes", i);
skip_footer = true;
@@ -302,18 +261,13 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
ESP_LOGV_HEX(TAG, remote_state, WHIRLPOOL_STATE_LENGTH, WHIRLPOOL_STATE_LENGTH, "Received: %s");
// verify header remote code
if (encode_uint16(remote_state[0], remote_state[1]) != this->remote_id_())
if (remote_state[0] != 0x83 || remote_state[1] != 0x06)
return false;
// powr on/off button
ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER));
if (this->is_hitachi_hcra31newh_()) {
// Power is a state: byte 18 says whether the unit is off
this->powered_on_assumed = remote_state[18] != HITACHI_BYTE18_OFF;
if (!this->powered_on_assumed)
this->mode = climate::CLIMATE_MODE_OFF;
} else if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
if (powered_on) {
@@ -355,7 +309,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
int temp = remote_state[3] & 0xF0;
ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
temp = (uint8_t) temp >> 4;
temp = std::min(temp + static_cast<int>(this->temperature_min_()), static_cast<int>(this->temperature_max_()));
temp += static_cast<int>(this->temperature_min_());
ESP_LOGVV(TAG, "Temperature Climate: %u", temp);
this->target_temperature = temp;
}
+4 -19
View File
@@ -6,10 +6,9 @@ namespace esphome::whirlpool {
/// Simple enum to represent models.
enum Model {
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
MODEL_HITACHI_HCRA31NEWH = 3, /// Hitachi with the HCRA31NEWH remote, 16 to 30
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
};
// Temperature
@@ -30,9 +29,7 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override {
// The toggle remotes flip swing on every press, so only send one when the mode really changes
auto swing = call.get_swing_mode();
this->send_swing_cmd_ = swing.has_value() && *swing != this->swing_mode;
send_swing_cmd_ = call.get_swing_mode().has_value();
climate_ir::ClimateIR::control(call);
}
@@ -58,22 +55,10 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
return false;
#endif
}
/// False when the model is compiled out, so the compiler drops the Hitachi branches.
bool is_hitachi_hcra31newh_() const {
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
return this->model_ == MODEL_HITACHI_HCRA31NEWH;
#else
return false;
#endif
}
/// The remote ID sent in bytes 0 and 1.
uint16_t remote_id_() const { return this->is_hitachi_hcra31newh_() ? 0xA959 : 0x8306; }
/// Modes in which the Hisense ARG model sends a fixed temperature.
bool fixed_temp_mode_() const;
/// Add the Hisense ARG bytes on top of the common whirlpool frame.
void encode_hisense_arg_(uint8_t *remote_state, bool power_toggle, uint8_t temp_step) const;
/// Add the Hitachi HCRA31NEWH bytes on top of the common whirlpool frame.
void encode_hitachi_hcra31newh_(uint8_t *remote_state) const;
/// Set the time of the last transmission.
uint32_t last_transmit_time_{};
+1 -1
View File
@@ -563,7 +563,7 @@ class WiFiComponent final : public Component {
// (In most use cases you won't need these)
/// Setup WiFi interface.
void setup() override;
#if defined(USE_ESP32) || defined(USE_LN882X) || defined(USE_BK72XX)
#if defined(USE_LN882X) || defined(USE_BK72XX)
void on_powerdown() override;
#endif
void start();
@@ -1635,21 +1635,6 @@ bool WiFiComponent::wifi_dpp_start_listen_() {
void WiFiComponent::wifi_dpp_stop_listen_() { esp_supp_dpp_stop_listen(); }
#endif
void WiFiComponent::on_powerdown() {
// Leave the AP before esp_restart() or deep sleep stops Wi-Fi. esp_wifi_stop() while associated
// makes the ESP32 retransmit its Disassociation up to the retry limit although the AP ACKs it
// (32 copies with protected management frames), which some APs handle badly.
// Upstream bug: https://github.com/espressif/esp-idf/issues/19443 - remove this once it is fixed
// in the ESP-IDF versions we support.
// Ask the driver: s_sta_connected lags until the main loop has handled the connect event.
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK)
return;
ESP_LOGD(TAG, "Disconnecting before powerdown");
esp_wifi_disconnect();
delay(100); // NOLINT: once per reboot or deep sleep, lets the disassociation go out
}
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() {
-2
View File
@@ -294,8 +294,6 @@ void Application::reboot() {
for (auto &component : std::ranges::reverse_view(this->components_)) {
component->on_shutdown();
}
// No teardown on a forced reboot, but still power hardware down (Wi-Fi leaves the AP)
this->run_powerdown_hooks();
arch_restart();
}
void Application::safe_reboot() {
+2 -4
View File
@@ -185,12 +185,10 @@ class Component {
*/
virtual bool teardown() { return true; }
/** Called last before a reboot or deep sleep to power down hardware.
/** Called after teardown is complete to power down hardware.
*
* On a safe shutdown this is called after all components have finished their teardown process,
* This is called after all components have finished their teardown process,
* making it safe to power down hardware like ethernet PHY.
* On a forced reboot (Application::reboot()) it is called right after on_shutdown() with no
* teardown, so implementations must not assume teardown() has run.
*/
virtual void on_powerdown() {}
-2
View File
@@ -70,7 +70,6 @@
#define USE_BUTTON
#define USE_CAMERA
#define USE_CANBUS_FD
#define USE_CC1101_LONG_PACKET
#define USE_CLIMATE
#define USE_CLIMATE_VISUAL_OVERRIDES
#define USE_CONTROLLER_REGISTRY
@@ -388,7 +387,6 @@
#define USE_WATER_HEATER
#define USE_WATER_HEATER_VISUAL_OVERRIDES
#define USE_WHIRLPOOL_HISENSE_ARG
#define USE_WHIRLPOOL_HITACHI_HCRA31NEWH
#define USE_ZWAVE_PROXY
// Feature flags which do not work for zephyr
-51
View File
@@ -337,57 +337,6 @@ size_t truncate_name_to(char *buffer, size_t buffer_size, const char *name, size
// Parsing & formatting
// A plain decimal converts exactly with one division: the digits accumulate
// into an integer that stays below 2^24 and the fraction's scale is a power of
// ten up to 1e9 (2^9 * 5^9), so both convert to float exactly and IEEE division
// rounds the quotient correctly, the same result strtof produces.
static constexpr uint32_t PARSE_FLOAT_MANTISSA_LIMIT = 1000000; // one more digit stays below 2^24
static constexpr uint32_t PARSE_FLOAT_MAX_FRACTION_DIGITS = 9;
optional<float> parse_float(const char *str) {
const char *p = str;
const bool negative = *p == '-';
if (negative || *p == '+')
p++;
const char *digits = p;
const char *dot = nullptr;
uint32_t mantissa = 0;
for (;; p++) {
const uint32_t d = static_cast<unsigned char>(*p) - '0';
if (d > 9) {
if (*p == '.' && dot == nullptr) {
dot = p;
continue;
}
break;
}
if (mantissa >= PARSE_FLOAT_MANTISSA_LIMIT)
break;
mantissa = mantissa * 10 + d;
}
const uint32_t fraction_digits = dot != nullptr ? p - dot - 1 : 0;
const bool any_digit = p - digits > (dot != nullptr ? 1 : 0);
if (*p == '\0' && any_digit && fraction_digits <= PARSE_FLOAT_MAX_FRACTION_DIGITS) {
// The mantissa is exact in a float, so the sign goes on before the one rounding step
float value = static_cast<float>(mantissa);
if (negative)
value = -value;
if (fraction_digits != 0) {
uint32_t scale = 10;
for (uint32_t i = 1; i < fraction_digits; i++)
scale *= 10;
value /= static_cast<float>(scale);
}
return value;
}
// Exponents, hex, inf, nan, leading whitespace and longer inputs take the libc path.
char *end = nullptr;
const float value = ::strtof(str, &end);
if (end == str || *end != '\0' || value == HUGE_VALF)
return {};
return value;
}
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
size_t chars = std::min(length, 2 * count);
for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) {
+21 -2
View File
@@ -25,6 +25,7 @@
#include <strings.h>
#include "esphome/core/optional.h"
#include "esphome/core/parse_float.h"
#include "esphome/core/time_conversion.h"
#ifdef USE_ESP8266
@@ -1033,6 +1034,26 @@ inline float pow10_int(int8_t exp) {
return result;
}
/// 10^n, exact while it fits in T.
template<typename T> constexpr T pow10_exact(uint32_t n) {
T result = 1;
T base = 10;
for (;;) {
if ((n & 1) != 0)
result *= base;
n >>= 1;
if (n == 0)
return result;
base *= base;
}
}
/// Whether c is '0' to '9'.
constexpr bool is_digit(char c) { return static_cast<uint8_t>(c - '0') <= 9; }
/// Whether c is C-locale white space, without isspace's ctype table.
constexpr bool is_space_c(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }
/// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out).
template<typename T, typename U> T remap(U value, U min, U max, T min_out, T max_out) {
return (value - min) * (max_out - min_out) / (max - min) + min_out;
@@ -1573,8 +1594,6 @@ template<typename T, enable_if_t<(std::is_integral<T>::value && std::is_signed<T
optional<T> parse_number(const std::string &str) {
return parse_number<T>(str.c_str());
}
/// Parse a decimal floating-point number; plain decimals skip the libc parser.
optional<float> parse_float(const char *str);
/// Parse a decimal floating-point number from a null-terminated string.
template<typename T, enable_if_t<(std::is_same<T, float>::value), int> = 0> optional<T> parse_number(const char *str) {
return parse_float(str);
+372
View File
@@ -0,0 +1,372 @@
// picolibc 1.8.10's strtof, bit for bit, without libc's strtod: decimals round to 9 digits, then to the nearest float.
// Float bits are built from integers, so chips that flush denormals give the same result.
#include "esphome/core/parse_float.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <bit>
namespace esphome {
namespace {
constexpr uint32_t FLOAT_SIGN_BIT = 0x80000000, FLOAT_INF_BITS = 0x7F800000;
constexpr uint32_t FLOAT_NAN_BITS = 0x7FC00000; // nanf(""), which strtof returns for every NaN
constexpr uint32_t DECIMAL_DIGITS = 9, HEX_DIGITS = 8; // digits kept before the rest only round
constexpr int32_t EXPONENT_CAP = 224; // picolibc stops adding exponent digits once the exponent reaches this
// 0.<digits> * 10^exp10 is zero at or below MIN_EXP10 and infinity at or above MAX_EXP10, as in picolibc
constexpr int32_t MIN_EXP10 = -46, MAX_EXP10 = 40;
// Past this, one more digit could pass 2^24
constexpr uint32_t PLAIN_MANTISSA_LIMIT = 1677720;
// Bits of the float nearest to (m + sticky fraction) * 2^e, ties to even; m > 0
uint32_t round_to_float_bits(uint64_t m, int32_t e, bool sticky) {
const int32_t msb = e + static_cast<int32_t>(std::bit_width(m)) - 1; // the value is in [2^msb, 2^(msb + 1))
if (msb >= 128)
return FLOAT_INF_BITS;
if (msb < -150) // below half the smallest float
return 0;
// The unit of the float's last bit: 2^(msb - 23), or 2^-149 for a denormal
const int32_t shift = std::max<int32_t>(msb - 23, -149) - e; // at most 64
uint64_t q;
if (shift <= 0) {
q = m << -shift;
} else {
q = shift < 64 ? m >> shift : 0;
const uint64_t rem = shift < 64 ? m & ((uint64_t{1} << shift) - 1) : m;
const uint64_t half = uint64_t{1} << (shift - 1);
if (rem > half || (rem == half && (sticky || (q & 1) != 0)))
q++;
}
if (msb < -126)
return static_cast<uint32_t>(q); // 2^23 here is the smallest normal float, as it should be
// Rounding up past the largest float gives exactly FLOAT_INF_BITS
return (static_cast<uint32_t>(msb + 127) << 23) + static_cast<uint32_t>(q - (uint64_t{1} << 23));
}
// The decimal exponents that reach decimal_to_float_bits: 1 to 9 digits and MIN_EXP10 < digits + e10 < MAX_EXP10
constexpr int32_t MIN_E10 = MIN_EXP10 + 1 - static_cast<int32_t>(DECIMAL_DIGITS), MAX_E10 = MAX_EXP10 - 2;
// Top 64 bits of 10^(8k), rounded down: 10^(8k) = entry * 2^(floor_log2_pow10(8k) - 63), exact for k = 0..3
constexpr int32_t POW10_BY_8_FIRST = MIN_E10 >> 3, POW10_BY_8_LAST = MAX_E10 >> 3;
constexpr uint64_t POW10_BY_8[] PROGMEM = {
0xFB158592BE068D2E, 0xBB127C53B17EC159, 0x8B61313BBABCE2C6, 0xCFB11EAD453994BA,
0x9ABE14CD44753B52, 0xE69594BEC44DE15B, 0xABCC77118461CEFC, 0x8000000000000000,
0xBEBC200000000000, 0x8E1BC9BF04000000, 0xD3C21BCECCEDA100, 0x9DC5ADA82B70B59D,
};
static_assert(POW10_BY_8_FIRST == -7 && POW10_BY_8_LAST == 4 &&
sizeof(POW10_BY_8) / sizeof(POW10_BY_8[0]) == POW10_BY_8_LAST - POW10_BY_8_FIRST + 1,
"POW10_BY_8 must cover MIN_E10..MAX_E10");
// floor(q * log2(10)), for |q| < 100
constexpr int32_t floor_log2_pow10(int32_t q) { return (q * 217706) >> 16; }
// The top 64 bits of a * b, rounded down
uint64_t mul_high(uint64_t a, uint64_t b) {
const uint64_t a_lo = static_cast<uint32_t>(a), a_hi = a >> 32;
const uint64_t b_lo = static_cast<uint32_t>(b), b_hi = b >> 32;
const uint64_t hi_lo = a_hi * b_lo, lo_hi = a_lo * b_hi;
const uint64_t middle = ((a_lo * b_lo) >> 32) + static_cast<uint32_t>(hi_lo) + static_cast<uint32_t>(lo_hi);
return a_hi * b_hi + (hi_lo >> 32) + (lo_hi >> 32) + (middle >> 32);
}
// Cold: a decimal within 16 units of a midpoint is an exact tie when m10 * 5^e10 is an integer
__attribute__((noinline, cold)) uint32_t tie_float_bits(uint32_t m10, int32_t e10, uint32_t bits) {
if (e10 >= 0) {
if (e10 <= 14) // m10 * 5^14 stays below 2^64
return round_to_float_bits(m10 * (pow10_exact<uint64_t>(e10) >> e10), e10, false);
} else if (e10 >= -13) {
const auto pow5 = static_cast<uint32_t>(pow10_exact<uint64_t>(-e10) >> -e10);
if (m10 % pow5 == 0)
return round_to_float_bits(m10 / pow5, e10, false);
}
return bits;
}
// The float nearest to m10 * 10^e10 (0 < m10 <= 10^9). The product is under 4 units low; checked for all 85 billion
// (m10, e10), only exact ties and one non-tie (6 units off, keeps its float) come within 34 units of a midpoint.
uint32_t decimal_to_float_bits(uint32_t m10, int32_t e10) {
// Both operands are exact floats, so the one float operation gives the nearest float
const uint32_t power = e10 < 0 ? -e10 : e10;
if (m10 < (1u << 24) && power <= 10) {
const auto value = static_cast<float>(m10);
// 10^10 overflows uint32_t
const float factor = power == 10 ? 1e10f : static_cast<float>(pow10_exact<uint32_t>(power));
return bit_cast<uint32_t>(e10 < 0 ? value / factor : value * factor);
}
const int32_t k8 = e10 >> 3; // rounds down
const uint64_t x = uint64_t{m10} * pow10_exact<uint32_t>(e10 & 7);
const auto zeros = static_cast<int32_t>(std::countl_zero(x));
uint64_t m = x << zeros;
int32_t e = -zeros;
if (k8 == 0)
return round_to_float_bits(m, e, false); // exact
m = mul_high(m, POW10_BY_8[k8 - POW10_BY_8_FIRST]);
e += floor_log2_pow10(8 * k8) + 1;
if ((m >> 63) == 0) {
m <<= 1;
e--;
}
const uint32_t bits = round_to_float_bits(m, e, false);
// The bits below the float's last one, and the midpoint among them
const int32_t shift = std::max<int32_t>(40, -149 - e);
if (shift <= 64) {
const uint64_t low = shift < 64 ? m & ((uint64_t{1} << shift) - 1) : m;
const uint64_t half = uint64_t{1} << (shift - 1);
if (low - half + 16 > 32) // |low - half| > 16
return bits;
}
return tie_float_bits(m10, e10, bits);
}
uint32_t no_number(const char *str, const char **end, bool *negative) {
*end = str;
*negative = false;
return 0;
}
// The rest of an "nan" or "inf" after its first letter at p; returns the end, or nullptr for no number
const char *read_inf_nan(const char *p) {
const auto lower = [](char c) { return static_cast<char>(c | 0x20); };
if (lower(*p) == 'n') {
if (lower(p[1]) != 'a' || lower(p[2]) != 'n')
return nullptr;
p += 3;
if (*p != '(')
return p;
// "nan(" must be closed after letters, digits and '_'
do {
p++;
} while (static_cast<uint8_t>(lower(*p) - 'a') < 26 || is_digit(*p) || *p == '_');
return *p == ')' ? p + 1 : nullptr;
}
if (lower(p[1]) != 'n' || lower(p[2]) != 'f')
return nullptr;
p += 3;
if (lower(*p) != 'i')
return p;
// Past "infi", only "infinity" is a number
return lower(p[1]) == 'n' && lower(p[2]) == 'i' && lower(p[3]) == 't' && lower(p[4]) == 'y' ? p + 5 : nullptr;
}
// Reads a binary exponent "p" or decimal exponent "e" at p into *exp; returns the end of the number
const char *read_exponent(const char *p, char letter, int32_t *exp) {
if ((*p | 0x20) != letter)
return p;
const char *q = p + 1;
const bool negative = *q == '-';
if (negative || *q == '+')
q++;
if (!is_digit(*q))
return p;
int32_t value = 0;
for (; is_digit(*q); q++) {
if (value < EXPONENT_CAP)
value = value * 10 + (*q - '0');
}
*exp += negative ? -value : value;
return q;
}
// Hex digits after the "x" at p, with the point and decimal exponent read so far (no non-zero digit yet)
__attribute__((noinline, cold)) uint32_t read_hex(const char *p, bool dot, int32_t exp, const char **end) {
uint32_t m = 0, kept = 0; // kept: digits in m from its first non-zero one
bool sticky = false;
for (;; p++) {
const char lower = static_cast<char>(*p | 0x20);
uint32_t digit = static_cast<uint8_t>(*p - '0');
if (static_cast<uint8_t>(lower - 'a') <= 5) {
digit = static_cast<uint8_t>(lower - 'a') + 10;
} else if (digit > 9) {
digit = 16; // picolibc takes ':' to '?' as 10 to 15; we stop there like newlib
}
if (digit < 16) {
if (kept < HEX_DIGITS) {
if (dot)
exp--;
m = m * 16 + digit;
kept += m != 0 ? 1 : 0;
} else {
if (!dot)
exp++;
sticky |= digit != 0;
}
} else if (*p == '.' && !dot) {
dot = true;
} else if (lower != 'x' || m != 0) { // picolibc takes another "x" while the value is still zero
break;
}
}
exp *= 4;
*end = read_exponent(p, 'p', &exp);
return m == 0 ? 0 : round_to_float_bits(m, exp, sticky);
}
// picolibc's conv_flt for a float from the first digit or point at p: returns the bits; *end is str for no number
__attribute__((noinline)) uint32_t read_decimal(const char *str, const char *p, const char **end) {
uint32_t m = 0, kept = 0; // the first (up to 9) significant digits, and how many from the first non-zero one
int32_t exp = 0; // the value is m * 10^exp
bool dot = false, any = false;
uint32_t overflow = 0; // digits past the kept ones: 0 none, 1 below half, 2 half, 3 above half
bool full = false; // a digit past the kept ones was read
for (;; p++) {
const uint32_t digit = static_cast<uint8_t>(*p - '0');
if (digit <= 9) {
any = true;
if (kept < DECIMAL_DIGITS) {
if (dot)
exp--;
m = m * 10 + digit;
kept += m != 0 ? 1 : 0;
} else {
if (!dot)
exp++;
if (!full) {
full = true;
overflow = digit > 5 ? 3 : digit == 5 ? 2 : digit != 0 ? 1 : 0; // the first one decides the half
} else {
overflow |= digit != 0 ? 1 : 0;
}
}
} else if (*p == '.' && !dot) {
dot = true;
} else {
break;
}
}
if ((*p | 0x20) == 'x' && any && m == 0)
return read_hex(p + 1, dot, exp, end);
if (!any) {
*end = str;
return 0;
}
*end = read_exponent(p, 'e', &exp);
if (m == 0)
return 0;
// Ties to even on the decimal digits
overflow |= m & 1;
m += (overflow + 1) >> 2;
const int32_t exp10 = static_cast<int32_t>(kept) + exp;
if (exp10 <= MIN_EXP10)
return 0;
if (exp10 >= MAX_EXP10)
return FLOAT_INF_BITS;
return decimal_to_float_bits(m, exp);
}
// picolibc's strtof from the start: returns the bits without the sign, which goes in *negative
__attribute__((noinline)) uint32_t parse_float_full(const char *str, const char **end, bool *negative) {
// No number starts with a letter but i (inf) or n (nan): "unavailable", "on" and "unknown" stop here
const char first = static_cast<char>(*str | 0x20);
if (first >= 'a' && first <= 'z' && first != 'i' && first != 'n')
return no_number(str, end, negative);
const char *p = str;
while (is_space_c(*p))
p++;
*negative = *p == '-';
if (*negative || *p == '+')
p++;
const char lower = static_cast<char>(*p | 0x20);
if (lower == 'i' || lower == 'n') {
const char *stop = read_inf_nan(p);
if (stop == nullptr)
return no_number(str, end, negative);
*end = stop;
return lower == 'i' ? FLOAT_INF_BITS : FLOAT_NAN_BITS;
}
const uint32_t bits = read_decimal(str, p, end);
if (*end == str)
*negative = false;
return bits;
}
// A plain decimal below 2^24 with up to 9 digits after the point, in one float operation; nullptr otherwise
ESPHOME_ALWAYS_INLINE inline const char *parse_plain_decimal(const char *str, uint32_t *bits, bool *negative) {
const char *p = str;
*negative = *p == '-';
if (*negative || *p == '+')
p++;
const char *digits = p;
const char *point = nullptr;
uint32_t mantissa = 0;
for (;; p++) {
const uint32_t d = static_cast<uint8_t>(*p) - '0';
if (d > 9) {
if (*p == '.' && point == nullptr) {
point = p;
continue;
}
break;
}
if (mantissa > PLAIN_MANTISSA_LIMIT)
return nullptr;
mantissa = mantissa * 10 + d;
}
const uint32_t fraction = point != nullptr ? p - point - 1 : 0;
const char c = static_cast<char>(*p | 0x20);
if (p - digits == (point != nullptr ? 1 : 0) || fraction > 9 || c == 'e' || c == 'x')
return nullptr;
float value = static_cast<float>(mantissa);
if (fraction != 0)
value /= static_cast<float>(pow10_exact<uint32_t>(fraction)); // 10^9 is an exact float
*bits = bit_cast<uint32_t>(value);
return p;
}
} // namespace
optional<float> parse_float(const char *str) {
// Own copy of the plain-decimal loop: sharing parse_plain_decimal costs 12 to 18% more instructions
const char *p = str;
const bool negative = *p == '-';
if (negative || *p == '+')
p++;
const char *digits = p;
const char *point = nullptr;
uint32_t mantissa = 0;
for (;; p++) {
const uint32_t d = static_cast<uint8_t>(*p) - '0';
if (d > 9) {
if (*p == '.' && point == nullptr) {
point = p;
continue;
}
break;
}
if (mantissa > PLAIN_MANTISSA_LIMIT)
break;
mantissa = mantissa * 10 + d;
}
const uint32_t fraction = point != nullptr ? p - point - 1 : 0;
if (*p == '\0' && p - digits > (point != nullptr ? 1 : 0) && fraction <= 9) {
float value = negative ? -static_cast<float>(mantissa) : static_cast<float>(mantissa);
if (fraction != 0)
value /= static_cast<float>(pow10_exact<uint32_t>(fraction));
return value;
}
const char *end;
bool full_negative;
const uint32_t bits = parse_float_full(str, &end, &full_negative);
// HUGE_VALF, positive overflow or "inf", is rejected like before
if (end == str || *end != '\0' || (!full_negative && bits == FLOAT_INF_BITS))
return {};
return bit_cast<float>(full_negative ? bits | FLOAT_SIGN_BIT : bits);
}
float str_to_float(const char *str, char **end) {
const char *stop;
bool negative;
uint32_t bits;
if (const char *plain_end = parse_plain_decimal(str, &bits, &negative)) {
stop = plain_end;
} else {
bits = parse_float_full(str, &stop, &negative);
}
if (end != nullptr)
*end = const_cast<char *>(stop); // strtof's signature
return bit_cast<float>(negative ? bits | FLOAT_SIGN_BIT : bits);
}
} // namespace esphome
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
#include "esphome/core/optional.h"
namespace esphome {
/// The whole string as str_to_float's syntax (leading space, hex, inf, nan); trailing characters, "inf" and positive
/// overflow are rejected, while -inf, negative overflow and NaN are accepted.
optional<float> parse_float(const char *str);
/// strtof with picolibc's results on every platform, but it never sets errno (overflow gives +-HUGE_VALF, underflow 0);
/// use it instead of strtof, or strtod/atof for a float result.
float str_to_float(const char *str, char **end);
} // namespace esphome
+3 -2
View File
@@ -7,6 +7,7 @@
#include <memory>
#include <string>
#include "esphome/core/defines.h"
#include "esphome/core/parse_float.h"
#ifdef USE_JSON
#include "esphome/components/json/json_util.h"
@@ -293,7 +294,7 @@ inline bool operator!=(const std::string &lhs, const ProgmemStringRef &rhs) { re
#endif
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
// Must be in esphome namespace for ADL to find them. Uses strtol, strtod and str_to_float to avoid heap allocation.
namespace internal {
// NOLINTBEGIN(google-runtime-int)
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, F conv) {
@@ -334,7 +335,7 @@ inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) {
return internal::parse_number<long>(str, pos, base, std::strtol);
}
inline float stof(const StringRef &str, size_t *pos = nullptr) {
return internal::parse_number<float>(str, pos, std::strtof);
return internal::parse_number<float>(str, pos, str_to_float);
}
inline double stod(const StringRef &str, size_t *pos = nullptr) {
return internal::parse_number<double>(str, pos, std::strtod);
@@ -0,0 +1,40 @@
#include <benchmark/benchmark.h>
#include "esphome/core/helpers.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
static const char *const PLAIN[] = {"0.5", "255", "-12.75", "21.3"};
static const char *const EXPONENT[] = {"1e3", "2.5e-2", "-6.02E5", "1.5e+1"};
static const char *const PRECISE[] = {"3.14159265358979", "0.000123456789", "123456789.125", "-0.1000000000000000055"};
// Home Assistant states that are not numbers
static const char *const REJECT[] = {"unavailable", "unknown", "on", "off"};
template<size_t N, typename F> static void run(benchmark::State &state, const char *const (&inputs)[N], F parse) {
for (auto _ : state) {
float sum = 0.0f;
for (int i = 0; i < kInnerIterations; i++)
sum += parse(inputs[i % N]);
benchmark::DoNotOptimize(sum);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
static float parse_number_float(const char *str) { return parse_number<float>(str).value_or(0.0f); }
static void ParseNumberFloat_Plain(benchmark::State &state) { run(state, PLAIN, parse_number_float); }
BENCHMARK(ParseNumberFloat_Plain);
static void ParseNumberFloat_Exponent(benchmark::State &state) { run(state, EXPONENT, parse_number_float); }
BENCHMARK(ParseNumberFloat_Exponent);
static void ParseNumberFloat_Precise(benchmark::State &state) { run(state, PRECISE, parse_number_float); }
BENCHMARK(ParseNumberFloat_Precise);
static void ParseNumberFloat_Reject(benchmark::State &state) { run(state, REJECT, parse_number_float); }
BENCHMARK(ParseNumberFloat_Reject);
} // namespace esphome::benchmarks
@@ -1,62 +0,0 @@
"""Tests for cc1101 packet_length validation."""
from typing import Any
import pytest
from esphome.components.cc1101 import _validate_packet_mode
import esphome.config_validation as cv
from esphome.core import Lambda
LENGTH_LAMBDA = Lambda("return x.size() < 3 ? 0 : x[2] + 12;")
@pytest.mark.parametrize(
"config",
[
{"packet_mode": True, "gdo0_pin": 2, "packet_length": LENGTH_LAMBDA},
{"packet_mode": True, "gdo0_pin": 2, "packet_length": 1024},
{"packet_mode": True, "gdo0_pin": 2, "packet_length": 64, "crc_enable": True},
{"packet_mode": False, "packet_length": 200, "crc_enable": True},
],
)
def test_packet_length_valid(config: dict[str, Any]) -> None:
"""Fixed lengths, a lambda, and CRC on packets that fit the FIFO are accepted."""
assert _validate_packet_mode(config) is config
@pytest.mark.parametrize(
("config", "message"),
[
(
{"packet_mode": False, "packet_length": LENGTH_LAMBDA},
"packet_mode is required when packet_length is a lambda",
),
(
{"packet_mode": True, "gdo0_pin": 2},
"packet_length is required when packet_mode is enabled",
),
(
{
"packet_mode": True,
"gdo0_pin": 2,
"packet_length": 65,
"crc_enable": True,
},
"crc_enable is not supported",
),
(
{
"packet_mode": True,
"gdo0_pin": 2,
"packet_length": LENGTH_LAMBDA,
"crc_enable": True,
},
"crc_enable is not supported",
),
],
)
def test_packet_length_invalid(config: dict[str, Any], message: str) -> None:
"""Invalid packet_length combinations are rejected with a clear message."""
with pytest.raises(cv.Invalid, match=message):
_validate_packet_mode(config)
@@ -1,18 +0,0 @@
"""Validate the sync-word limit before firmware generation."""
import pytest
from esphome.components.sx126x import CONF_SYNC_VALUE, CONFIG_SCHEMA
import esphome.config_validation as cv
@pytest.mark.parametrize("size", [0, 1, 2, 8])
def test_supported_sync_lengths(size: int) -> None:
value = [0x14] * size
assert CONFIG_SCHEMA.schema[CONF_SYNC_VALUE](value) == value
@pytest.mark.parametrize("size", [9, 256])
def test_oversized_sync_word(size: int) -> None:
with pytest.raises(cv.Invalid, match="at most 8"):
CONFIG_SCHEMA.schema[CONF_SYNC_VALUE]([0x14] * size)
@@ -1,26 +0,0 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
cc1101:
# Above 255 bytes: infinite length mode, then fixed for the remainder
- id: transceiver_long_fixed
cs_pin: GPIO5
gdo0_pin: GPIO4
packet_mode: true
packet_length: 1024
on_packet:
then:
- lambda: 'ESP_LOGD("cc1101", "long packet %u bytes", x.size());'
# Length taken from the packet header
- id: transceiver_long_lambda
cs_pin: GPIO15
gdo0_pin: GPIO2
packet_mode: true
packet_length: !lambda |-
if (x.size() < 3) return 0;
if (x[0] != 0x54) return -1;
return x[2] + 12;
on_packet:
then:
- lambda: 'ESP_LOGD("cc1101", "lambda packet %u bytes rssi %.1f", x.size(), rssi);'
@@ -1,24 +0,0 @@
packages:
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
cc1101:
- id: transceiver_long_fixed
cs_pin: GPIO5
gdo0_pin: GPIO4
packet_mode: true
packet_length: 1024
on_packet:
then:
- lambda: 'ESP_LOGD("cc1101", "long packet %u bytes", x.size());'
- id: transceiver_long_lambda
cs_pin: GPIO15
gdo0_pin: GPIO2
packet_mode: true
packet_length: !lambda |-
if (x.size() < 3) return 0;
if (x[0] != 0x54) return -1;
return x[2] + 12;
on_packet:
then:
- lambda: 'ESP_LOGD("cc1101", "lambda packet %u bytes rssi %.1f", x.size(), rssi);'
@@ -1,7 +0,0 @@
substitutions:
cs_pin: GPIO5
gdo0_pin: GPIO4
packages:
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
cc1101: !include common.yaml
+342 -11
View File
@@ -1,34 +1,74 @@
#include <gtest/gtest.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <random>
#include <string>
#include "esphome/core/helpers.h"
#include "esphome/core/parse_float.h"
#ifdef USE_HOST
namespace esphome::core::testing {
static uint32_t bits(float value) {
uint32_t out;
memcpy(&out, &value, sizeof(out));
return out;
static uint32_t bits(float value) { return bit_cast<uint32_t>(value); }
// picolibc rounds a decimal to 9 significant digits and converts those to the nearest float, so up to 9
// significant digits its result is the correctly rounded float the host's strtof (glibc, macOS) gives. Longer
// input and the syntax where libcs differ are checked against values taken from picolibc 1.8.10's own strtof.
static float reference_strtof(const char *str, char **end) {
const float value = ::strtof(str, end);
if (std::isnan(value))
return std::signbit(value) ? -NAN : NAN;
return value;
}
// strtof is the reference: every accepted input must give the same bits.
// Every accepted input must give the reference's bits.
static void expect_matches_strtof(const char *str) {
char *end = nullptr;
const float expected = ::strtof(str, &end);
const float expected = reference_strtof(str, nullptr);
const auto parsed = parse_number<float>(str);
if (!parsed.has_value()) {
ADD_FAILURE() << str << " was rejected";
return;
}
const float actual = parsed.value();
EXPECT_EQ(bits(actual), bits(expected)) << str << " parsed " << actual << " expected " << expected;
EXPECT_EQ(bits(parsed.value()), bits(expected)) << str;
}
// The same bits and end as the host's strtof; for decimals of up to 9 significant digits
static void expect_str_to_float_matches(const char *str) {
char *expected_end = nullptr;
const float expected = reference_strtof(str, &expected_end);
char *end = nullptr;
const float actual = str_to_float(str, &end);
EXPECT_EQ(bits(actual), bits(expected)) << '"' << str << '"';
EXPECT_EQ(end - str, expected_end - str) << '"' << str << '"';
}
struct PicolibcCase {
const char *str;
uint32_t bits;
int32_t end;
};
// Bits and end from picolibc 1.8.10's strtof
static void expect_picolibc(const PicolibcCase &c) {
char *end = nullptr;
const float value = str_to_float(c.str, &end);
EXPECT_EQ(bits(value), c.bits) << '"' << c.str << '"';
EXPECT_EQ(end - c.str, c.end) << '"' << c.str << '"';
}
static void expect_picolibc(const std::string &str, uint32_t expected) {
char *end = nullptr;
EXPECT_EQ(bits(str_to_float(str.c_str(), &end)), expected) << str.substr(0, 40);
EXPECT_EQ(static_cast<size_t>(end - str.c_str()), str.size()) << str.substr(0, 40);
}
static float float_from(uint32_t value) { return bit_cast<float>(value); }
TEST(ParseFloat, PlainDecimalsMatchStrtof) {
for (const char *str :
{"0", "-0", "+0", "1", "-1", "100.50", "99.75", "0.1", "0.3",
@@ -46,6 +86,14 @@ TEST(ParseFloat, SweepOfTwoDecimalValuesMatchesStrtof) {
}
}
// Eight-digit mantissas below 2^24 stay on the one-division path; the next digit past the limit falls back
TEST(ParseFloat, EightDigitPlainDecimalsMatchStrtof) {
for (const char *str : {"12345678", "-12345678", "1234.5678", "0.12345678", "16777209", "1677720.9", "0.16777209",
"16777210", "16777215", "1677721.5", "99999999", "9999999.9", "16777209."}) {
expect_matches_strtof(str);
}
}
// A fraction longer than the fast path accepts must fall back, however long it is.
TEST(ParseFloat, LongFractionsMatchStrtof) {
std::string str = "0.";
@@ -68,10 +116,293 @@ TEST(ParseFloat, LibcPathInputsMatchStrtof) {
// Positive overflow and a positive literal infinity both come back as HUGE_VALF from strtof, and the parser
// rejects both; the negative forms pass through, as they did before the fast path.
TEST(ParseFloat, RejectsInvalidInput) {
for (const char *str :
{"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf", "infinity"}) {
for (const char *str : {"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf",
"infinity", "infi", "nan(12 ab)", "nan(12"}) {
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
}
}
// Above the midpoint 1 + 2^-24 by less than half a double: newlib's strtof rounds to the midpoint and then to
// 1.0f; picolibc's 9 digits, 1.00000006, give the next float.
TEST(ParseFloat, FullPathRoundsLikePicolibc) {
EXPECT_EQ(bits(parse_number<float>("1.0000000596046448").value()), 0x3F800001u);
EXPECT_EQ(bits(parse_number<float>("-1.0000000596046448").value()), 0xBF800001u);
}
// picolibc takes "nan(" with letters, digits and '_' up to a ")", and nothing at all otherwise
TEST(ParseFloat, NanForms) {
EXPECT_TRUE(std::isnan(parse_number<float>("nan(12)").value()));
EXPECT_TRUE(std::isnan(parse_number<float>("nan(xyz_1)").value()));
EXPECT_FALSE(parse_number<float>("nan(x y)").has_value());
}
// The callers' overflow check: a positive overflow is HUGE_VALF and rejected, a negative one is -inf and accepted
TEST(ParseFloat, OverflowIsHugeValf) {
EXPECT_FALSE(parse_number<float>("1e39").has_value());
EXPECT_FALSE(parse_number<float>("3.40282357e38").has_value());
EXPECT_FALSE(parse_number<float>("1e99999").has_value());
EXPECT_EQ(bits(parse_number<float>("-1e39").value()), 0xFF800000u);
EXPECT_EQ(bits(parse_number<float>("3.4028235e38").value()), 0x7F7FFFFFu);
}
// Up to 9 significant digits, with leading and trailing zeros, a point anywhere and an exponent
TEST(StrToFloat, RandomDecimalsMatchStrtof) {
std::mt19937_64 rng(1); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) reproducible
for (int i = 0; i < 50000; i++) {
std::string str;
if (rng() % 3 == 0)
str += (rng() & 1) ? "-" : "+";
const int zeros = static_cast<int>(rng() % 4);
const int significant = 1 + static_cast<int>(rng() % 9);
const int trailing = static_cast<int>(rng() % 4);
std::string digits(zeros, '0');
digits += static_cast<char>('1' + rng() % 9);
for (int j = 1; j < significant; j++)
digits += static_cast<char>('0' + rng() % 10);
digits.append(trailing, '0');
const int point = static_cast<int>(rng() % (digits.size() + 2)) - 1;
for (int j = 0; j < static_cast<int>(digits.size()); j++) {
if (j == point)
str += '.';
str += digits[j];
}
if (rng() & 1) {
str += (rng() & 1) ? 'e' : 'E';
char exp[8];
snprintf(exp, sizeof(exp), "%d", static_cast<int>(rng() % 110) - 60);
str += exp;
}
expect_str_to_float_matches(str.c_str());
}
}
// Every 16411th float, printed with 6 and 9 digits
TEST(StrToFloat, FloatsMatchStrtof) {
char buf[64];
for (uint32_t u = 0; u < 0x7F800000; u += 16411) {
for (int precision : {6, 9}) {
snprintf(buf, sizeof(buf), "%.*e", precision - 1, static_cast<double>(float_from(u)));
expect_str_to_float_matches(buf);
}
}
}
TEST(StrToFloat, SyntaxMatchesStrtof) {
// Tokens whose combinations the libcs read alike: no "x", "nan(" or "infi"
static const char *const TOKENS[] = {"0", "1", "9", ".", "e", "E", "+", "-", "a", "N", " ", "\t", "y", "00", "#", ""};
for (const char *a : TOKENS) {
for (const char *b : TOKENS) {
for (const char *c : TOKENS) {
for (const char *d : TOKENS) {
const std::string str = std::string(a) + b + c + d;
expect_str_to_float_matches(str.c_str());
}
}
}
}
}
// Where newlib and glibc read the text differently from picolibc
TEST(StrToFloat, SyntaxMatchesPicolibc) {
static const PicolibcCase CASES[] = {
// "x" after any zero digits starts hex, even after a point or a second "0x"
{"000x", 0x00000000, 4},
{"0x", 0x00000000, 2},
{"-0x", 0x80000000, 3},
{"00x1", 0x3F800000, 4},
{"0x0x1", 0x3F800000, 5},
{"0.0x1", 0x3B800000, 5},
{".x1", 0x00000000, 0},
{"0x.p1", 0x00000000, 5},
{"0x.8", 0x3F000000, 4},
{"0X1.8", 0x3FC00000, 5},
{"0x1P-1", 0x3F000000, 6},
{"0x1p+", 0x3F800000, 3},
{"1x5", 0x3F800000, 1},
// "inf" stops before anything but "i"; past "infi" only "infinity" is a number
{"inf", 0x7F800000, 3},
{"-inf", 0xFF800000, 4},
{"INFINITY", 0x7F800000, 8},
{"+infi0", 0x00000000, 0},
{"infi", 0x00000000, 0},
{"infinit", 0x00000000, 0},
// "nan(" takes letters, digits and '_' up to ")", and is no number without the ")"
{"NaN", 0x7FC00000, 3},
{"nan()", 0x7FC00000, 5},
{"nan(xyz)", 0x7FC00000, 8},
{"nan(_a1)", 0x7FC00000, 8},
{"-nan(1)", 0xFFC00000, 7},
{"nan( )", 0x00000000, 0},
{"nan(12 ab)", 0x00000000, 0},
{"nan(12", 0x00000000, 0},
// An exponent needs a digit
{"1e", 0x3F800000, 1},
{"1e+", 0x3F800000, 1},
{"1e-x", 0x3F800000, 1},
{"\t 12", 0x41400000, 4},
{"- 1", 0x00000000, 0},
{"1.2.3", 0x3F99999A, 3},
};
for (const PicolibcCase &c : CASES)
expect_picolibc(c);
}
// The inputs where newlib's strtof (decimal to double to float) and picolibc's were found to differ; every platform
// now gives picolibc's result, which is also the correctly rounded float for these
TEST(StrToFloat, RoundsLikePicolibcWhereNewlibDoubleRounds) {
expect_picolibc({"1.0000000596046448", 0x3F800001, 18});
expect_picolibc({"0x1.100b0d0000000c", 0x3F880587, 18});
expect_picolibc("1.000000059604644775390625" + std::string(800, '0') + "1", 0x3F800001);
}
// picolibc rounds a decimal to 9 significant digits, ties to even, before converting it: past 9 digits its float can
// be one away from the nearest, and an exact tie between two floats can round up. Values from picolibc.
TEST(StrToFloat, NineDigitRoundingLikePicolibc) {
static const PicolibcCase CASES[] = {
{"1.0856183295439e10", 0x5021C50E, 18}, // nearest 0x5021C50D
{"3.97496754550e-4", 0x39D0671D, 16}, // nearest 0x39D0671C
{"9.76178673657108930e23", 0x674EB6C1, 22}, // nearest 0x674EB6C0
{"156.6562236e-26", 0x17F269ED, 15}, // nearest 0x17F269EC
{"1.000000059604644775390625", 0x3F800001, 26}, // the tie 1 + 2^-24: nearest-even 0x3F800000
{"1.000000059", 0x3F800001, 11}, // 1.00000006 after rounding
{"7.0064923216240854e-46", 0x00000000, 22}, // 7.00649232e-46, below half the smallest float
{"21.299999999999997", 0x41AA6666, 18},
{"0.30000000000000004", 0x3E99999A, 19},
{"0.1000000000000000055511151231257827", 0x3DCCCCCD, 36},
};
for (const PicolibcCase &c : CASES)
expect_picolibc(c);
}
// Nine digits converted exactly: ties to even between floats, where the 64-bit product cannot decide alone
TEST(StrToFloat, NineDigitTiesGoToEven) {
static const PicolibcCase CASES[] = {
{"16777217", 0x4B800000, 8}, {"16777219", 0x4B800002, 8}, {"16777217.0", 0x4B800000, 10},
{"8388608.5", 0x4B000000, 9}, {"8388609.5", 0x4B000002, 9}, {"3e10", 0x50DF8476, 4},
{"5e10", 0x513A43B7, 4}, {"1.00000006", 0x3F800001, 10},
};
for (const PicolibcCase &c : CASES)
expect_picolibc(c);
}
// RP2350 flushes float denormals in its double to float conversion; #22677 bench. The parser builds the bits
// with integer operations, so every denormal parses to its own value.
TEST(StrToFloat, DenormalsParseWithoutFlushing) {
char buf[64];
const auto check = [&buf](uint32_t k) {
snprintf(buf, sizeof(buf), "%.8e", std::ldexp(static_cast<double>(k), -149));
EXPECT_EQ(bits(str_to_float(buf, nullptr)), k) << buf;
};
for (uint32_t k = 0; k <= 0x800000; k += 4099)
check(k);
// The largest denormal and FLT_MIN, where rounding carries into the exponent
for (uint32_t k : {1u, 2u, 0x7FFFFEu, 0x7FFFFFu, 0x800000u})
check(k);
}
// Pico W and Pico 2 W bench, #22677: the RP2040 and RP2350 libcs flush float denormals to 0 in their double to
// float conversion, and the RP2040 one turns a NaN into +infinity
TEST(StrToFloat, Rp2040LibcFlushesAndNanToInfinity) {
static const PicolibcCase CASES[] = {
{"1e-40", 0x000116C2, 5}, {"1.4e-45", 0x00000001, 7}, {"1e-38", 0x006CE3EE, 5},
{"1.1754942e-38", 0x007FFFFF, 13}, {"0x1p-149", 0x00000001, 8}, {"1.17549435e-38", 0x00800000, 14},
{"nan(12)", 0x7FC00000, 7}, {"nan", 0x7FC00000, 3},
};
for (const PicolibcCase &c : CASES)
expect_picolibc(c);
}
// RP2350 bench: libc returned 0 and the first parser stalled stepping up from a flushed guess; #22677 bench
TEST(StrToFloat, DenormalsAndFltMinMatchStrtof) {
for (const char *str : {"1.4e-45", "-1.4e-45", "1e-40", "1e-38", "1.17549435e-38", "1.1754942e-38", "1.17549421e-38",
"1.1754945e-38", "7.0064923e-46", "7.0064924e-46"}) {
expect_str_to_float_matches(str);
}
}
// Home Assistant's non-numeric states; CodSpeed HomeassistantSensor_StateResponse_Unavailable on #22677
TEST(StrToFloat, NonNumericStatesAreRejected) {
for (const char *str : {"unavailable", "unknown", "on", "off", "", "-", ".", "none", "inactive"}) {
expect_str_to_float_matches(str);
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
}
}
// The one float operation path takes up to 2^24 - 1 and 10^10; #22677 review
TEST(StrToFloat, PlainPathBoundaries) {
for (const char *top : {"16777215", "16777216", "16777217"}) {
for (const char *exp : {"e10", "e-10", "e11", "e-11", "e9", "e-9", ".5", ".25"}) {
const std::string str = std::string(top) + exp;
expect_str_to_float_matches(str.c_str());
}
}
}
// The midpoint between FLT_MAX and 2^128 rounds to infinity; review
TEST(StrToFloat, OverflowMidpoint) {
expect_picolibc({"340282356779733661637539395458142568448", 0x7F800000, 39});
expect_picolibc({"3.40282356e38", 0x7F7FFFFF, 13});
expect_picolibc({"3.40282357e38", 0x7F800000, 13});
EXPECT_EQ(bits(str_to_float("0x1.ffffffp127", nullptr)), 0x7F800000u);
}
// picolibc stops adding exponent digits at 224, so the digits of the mantissa can move it no further
TEST(StrToFloat, HugeExponents) {
for (const char *str : {"1e-99999", "1e99999", "-1e99999", "0e99999", "1e9999999999999999999",
"1e-9999999999999999999", "1e-999999999999", "0.000e99999", "1e224", "1e2240", "1e-2240"}) {
expect_str_to_float_matches(str);
}
// 10^-3000 * 10^3000 is 1, but the exponent counts only as 300
expect_picolibc("0." + std::string(2999, '0') + "1e3000", 0x00000000);
expect_picolibc("1" + std::string(300, '0') + "e-300", 0x3F800000);
}
TEST(StrToFloat, LongMantissas) {
expect_picolibc("1" + std::string(799, '0') + "e-799", 0x3F800000);
expect_picolibc("1." + std::string(3000, '0') + "1e-3001", 0x00000000);
expect_picolibc("0." + std::string(400, '0') + "34028235" + std::string(500, '7') + "e440", 0x7F800000);
expect_picolibc(std::string(300, '9') + "e-261", 0x7F800000);
expect_picolibc("3" + std::string(799, '7') + "e-761", 0x7F800000);
}
// picolibc takes ':' to '?' as hex digits 10 to 15 ("0x1:" is 26); these stop at them, as newlib does. #22677 review
TEST(StrToFloat, HexStopsAtPunctuationAfterNine) {
struct Case {
const char *str;
uint32_t bits;
int32_t end;
};
const Case cases[] = {
{"0x1:", 0x3F800000, 3}, {"0x1;", 0x3F800000, 3}, {"0x1<", 0x3F800000, 3}, {"0x1=", 0x3F800000, 3},
{"0x1>", 0x3F800000, 3}, {"0x1?", 0x3F800000, 3}, {"0x1.:", 0x3F800000, 4}, {"0x:", 0x00000000, 2},
{"0x9?", 0x41100000, 3}, {"0xA:", 0x41200000, 3},
};
for (const auto &c : cases) {
char *end = nullptr;
const float value = str_to_float(c.str, &end);
EXPECT_EQ(bits(value), c.bits) << '"' << c.str << '"';
EXPECT_EQ(end - c.str, c.end) << '"' << c.str << '"';
EXPECT_FALSE(parse_number<float>(c.str).has_value()) << c.str;
}
}
TEST(StrToFloat, HexDenormalsAndOverflow) {
struct Case {
const char *str;
uint32_t bits;
};
static const Case CASES[] = {
{"0x1p-149", 0x00000001}, {"0x1p-150", 0x00000000},
{"0x1.8p-149", 0x00000002}, {"0x1.fffffcp-127", 0x007FFFFF},
{"0x1.fffffep-127", 0x00800000}, {"-0x1p-149", 0x80000001},
{"0x1p128", 0x7F800000}, {"0x1.fffffep127", 0x7F7FFFFF},
{"0x1.fffffefp127", 0x7F7FFFFF}, {"0x1.0000000000001p-150", 0x00000001},
{"0x1.000001p0", 0x3F800000}, {"0x1.000003p0", 0x3F800002},
{"0x1p-99999999999", 0x00000000}, {"0x1p99999999999", 0x7F800000},
};
for (const Case &c : CASES)
EXPECT_EQ(bits(str_to_float(c.str, nullptr)), c.bits) << c.str;
}
} // namespace esphome::core::testing
#endif // USE_HOST
+2 -9
View File
@@ -20,14 +20,7 @@ class FakePin final : public GPIOPin {
bool level{false};
};
// Fills a 16-bit register read: the firmware check code read gets a valid code, other registers zeros.
inline void answer_setup_read(const uint8_t *data, size_t size, uint8_t *out, size_t out_size) {
std::memset(out, 0, out_size);
if (size == 2 && encode_uint16(data[0], data[1]) == cstxxx::REG_FW_CHECK_CODE && out_size == 4)
out[2] = out[3] = 0xCA;
}
// Answers the 8-bit status read with a set report and 16-bit setup reads through answer_setup_read().
// Answers the 8-bit status read with a set report and 16-bit setup reads with zeros.
class StatusBus final : public i2c::I2CBus {
public:
std::array<uint8_t, 28> status{};
@@ -42,7 +35,7 @@ class StatusBus final : public i2c::I2CBus {
if (size == 1 && data[0] == CST226_REG_STATUS) {
std::memcpy(out, this->status.data(), std::min(out_size, this->status.size()));
} else {
answer_setup_read(data, size, out, out_size);
std::memset(out, 0, out_size);
}
return i2c::ERROR_OK;
}
@@ -1,8 +1,5 @@
#include <gtest/gtest.h>
#include <cstring>
#include <vector>
#include "esphome/components/cst226/touchscreen/cst226_touchscreen.h"
#include "../common.h"
@@ -17,45 +14,6 @@ TEST(CST226TouchscreenTest, SetupCompletes) {
EXPECT_FALSE(hub.is_failed());
}
// Records the writes that carry no read, and answers reads through answer_setup_read().
class WriteLogBus final : public i2c::I2CBus {
public:
std::vector<std::vector<uint8_t>> writes;
bool bad_check_code{false};
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
size_t out_size) override {
if (out_size == 0) {
this->writes.emplace_back(data, data + size);
} else {
answer_setup_read(data, size, out, out_size);
if (this->bad_check_code)
std::memset(out, 0, out_size);
}
return i2c::ERROR_OK;
}
};
TEST(CST226TouchscreenTest, SetupEntersCommandModeAndLeavesIt) {
WriteLogBus bus;
CST226Touchscreen hub;
hub.set_i2c_bus(&bus);
hub.setup();
ASSERT_GE(bus.writes.size(), 2u);
EXPECT_EQ(bus.writes.front(), (std::vector<uint8_t>{0xD1, 0x01}));
EXPECT_EQ(bus.writes.back(), (std::vector<uint8_t>{0xD1, 0x09}));
EXPECT_FALSE(hub.is_failed());
}
TEST(CST226TouchscreenTest, SetupFailsOnAWrongCheckCode) {
WriteLogBus bus;
bus.bad_check_code = true;
CST226Touchscreen hub;
hub.set_i2c_bus(&bus);
hub.setup();
EXPECT_TRUE(hub.is_failed());
}
TEST(CST226TouchscreenTest, SetupFailsWithoutTheChip) {
StatusBus bus;
bus.fail = true;
@@ -1,32 +0,0 @@
#include <gtest/gtest.h>
#include <algorithm>
#include "esphome/components/cstxxx/cstxxx.h"
namespace esphome::cstxxx::testing {
TEST(CSTxxxDecodeTouch, FirstRecordStartsTheReport) {
const uint8_t report[report_size(1)] = {0x16, 0x12, 0x34, 0x56, 0x78, 0x01, SYNC_BYTE};
auto touch = decode_touch(report, 0);
EXPECT_EQ(touch.id, 1);
EXPECT_EQ(touch.event, 0x06);
EXPECT_EQ(touch.x, 0x125);
EXPECT_EQ(touch.y, 0x346);
EXPECT_EQ(touch.z, 0x78);
}
TEST(CSTxxxDecodeTouch, LaterRecordsSkipTheStatusBytes) {
uint8_t report[report_size(3)]{};
report[FINGER_COUNT_IDX] = 3;
report[SYNC_IDX] = SYNC_BYTE;
const uint8_t third[5] = {0x26, 0xFF, 0x00, 0xF0, 0x05};
std::copy(third, third + 5, report + 12);
auto touch = decode_touch(report, 2);
EXPECT_EQ(touch.id, 2);
EXPECT_EQ(touch.x, 0xFFF);
EXPECT_EQ(touch.y, 0x000);
EXPECT_EQ(touch.z, 5);
}
} // namespace esphome::cstxxx::testing
@@ -2,7 +2,3 @@ packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
# IPv6 with an IP sensor: compiles the IPv6 event handler's listener notification
network:
enable_ipv6: true
-16
View File
@@ -1,16 +0,0 @@
"""Enable real logger callbacks for radio reentrancy regression tests."""
import esphome.codegen as cg
from esphome.components.logger import request_log_listener
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code(config: ConfigType) -> None:
request_log_listener()
# The tests build a software SPI bus in C++, which no configured bus asks for
cg.add_define("USE_SPI_SOFTWARE")
manifest.multi_conf = False
manifest.to_code = to_code
@@ -7,22 +7,3 @@ button:
cold: true
- sx126x.set_mode_sleep:
cold: !lambda return id(sx126x_my_number).state > 1;
- platform: template
name: "SX126x Async Send or Cancel"
on_press:
- lambda: |-
class Listener : public sx126x::SX126xTransmitListener {
public:
void on_transmit_complete(sx126x::SX126xError result) override {
ESP_LOGD("sx126x_test", "Async completion: %d", static_cast<int>(result));
}
};
static Listener listener;
if (id(sx126x_radio).is_transmitting()) {
id(sx126x_radio).cancel_transmit_async(&listener);
} else {
const uint8_t data[] = {0x11, 0x22, 0x33};
auto result = id(sx126x_radio).start_transmit_async(&listener, data, sizeof(data), 4000);
ESP_LOGD("sx126x_test", "Async admission: %d", static_cast<int>(result));
}
-1
View File
@@ -1,5 +1,4 @@
sx126x:
id: sx126x_radio
dio1_pin: ${dio1_pin}
cs_pin: ${cs_pin}
busy_pin: ${busy_pin}
@@ -1,643 +0,0 @@
#include "sx126x_test_helpers.h"
#include "esphome/core/application.h"
#include <tuple>
namespace esphome::sx126x::testing {
class TransmitListener : public SX126xTransmitListener {
public:
void on_transmit_complete(SX126xError result) override {
this->results.push_back(result);
if (this->on_complete) {
this->on_complete(result);
}
}
std::vector<SX126xError> results;
std::function<void(SX126xError)> on_complete;
};
class ReceiveCallback : public SX126xListener {
public:
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override { this->on_packet_callback(); }
std::function<void()> on_packet_callback;
};
class SX126xAsyncModes : public ::testing::TestWithParam<std::tuple<bool, bool, bool>> {};
TEST_P(SX126xAsyncModes, ReturnsBeforeCompletionAndRestoresMode) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware hw;
TransmitListener listener;
hw.configure(lora, rx, fixed);
hw.auto_complete = false;
hw.clear_trace();
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 1234), SX126xError::SX126X_ERROR_NONE);
EXPECT_TRUE(hw.radio.is_transmitting());
EXPECT_TRUE(listener.results.empty());
EXPECT_EQ(hw.sends, 1u);
const auto &tx = hw.transactions.back();
EXPECT_EQ(tx, (std::vector<uint8_t>{RADIO_SET_TX, 0x01, 0x34, 0x80}));
const auto fifo = std::find_if(hw.transactions.begin(), hw.transactions.end(),
[](const auto &cmd) { return cmd[0] == RADIO_WRITE_BUFFER; });
ASSERT_NE(fifo, hw.transactions.end());
EXPECT_EQ(*fifo, (std::vector<uint8_t>{RADIO_WRITE_BUFFER, 0, 0x11, 0x22, 0x33}));
hw.packet.assign(3, 0x99); // The driver must not retain the caller's payload.
hw.irq = IRQ_TX_DONE;
listener.on_complete = [&](SX126xError result) {
EXPECT_FALSE(hw.radio.is_transmitting());
EXPECT_EQ(hw.receiving, rx);
EXPECT_TRUE(hw.cs.level);
};
hw.radio.loop();
hw.radio.loop();
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
TEST_P(SX126xAsyncModes, RejectsEveryFailedStartWithoutCallbackAndCanRetry) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware reference;
TransmitListener listener;
reference.configure(lora, rx, fixed);
reference.clear_trace();
ASSERT_EQ(reference.radio.start_transmit_async(&listener, reference.packet), SX126xError::SX126X_ERROR_NONE);
const size_t count = reference.transactions.size();
reference.radio.cancel_transmit_async(&listener);
for (size_t fault = 1; fault <= count; fault++) {
SCOPED_TRACE(fault);
RadioHardware hw;
TransmitListener result;
hw.configure(lora, rx, fixed);
hw.clear_trace();
hw.fail_at = fault;
EXPECT_EQ(hw.radio.start_transmit_async(&result, hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
EXPECT_TRUE(result.results.empty());
EXPECT_FALSE(hw.radio.is_transmitting());
EXPECT_TRUE(hw.cs.level);
EXPECT_TRUE(hw.transactions[fault - 1].empty());
EXPECT_EQ(hw.sends, 0u);
EXPECT_EQ(hw.receiving, rx); // One-shot fault permits failed-start cleanup.
hw.clear_trace();
hw.radio.configure();
ASSERT_EQ(hw.radio.start_transmit_async(&result, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.radio.loop();
EXPECT_EQ(result.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
}
TEST_P(SX126xAsyncModes, ReportsEveryCompletionAndCleanupFailureOnce) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware reference;
TransmitListener listener;
reference.configure(lora, rx, fixed);
ASSERT_EQ(reference.radio.start_transmit_async(&listener, reference.packet), SX126xError::SX126X_ERROR_NONE);
reference.clear_trace();
reference.radio.loop();
const size_t count = reference.transactions.size();
for (size_t fault = 1; fault <= count; fault++) {
SCOPED_TRACE(fault);
RadioHardware hw;
TransmitListener result;
hw.configure(lora, rx, fixed);
ASSERT_EQ(hw.radio.start_transmit_async(&result, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.clear_trace();
hw.fail_at = fault;
hw.radio.loop();
EXPECT_EQ(result.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_BUSY_TIMEOUT}));
EXPECT_TRUE(hw.transactions[fault - 1].empty());
EXPECT_TRUE(hw.cs.level);
EXPECT_FALSE(hw.radio.is_transmitting());
hw.radio.loop();
EXPECT_EQ(result.results.size(), 1u);
hw.clear_trace();
hw.radio.configure();
ASSERT_EQ(hw.radio.start_transmit_async(&result, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.radio.loop();
ASSERT_EQ(result.results.size(), 2u);
EXPECT_EQ(result.results.back(), SX126xError::SX126X_ERROR_NONE);
EXPECT_EQ(hw.receiving, rx);
}
}
TEST_P(SX126xAsyncModes, BlockingAndAsyncUseTheSameCommandSequence) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware blocking, async;
TransmitListener listener;
blocking.configure(lora, rx, fixed);
async.configure(lora, rx, fixed);
blocking.clear_trace();
async.clear_trace();
ASSERT_EQ(blocking.radio.transmit_packet(blocking.packet), SX126xError::SX126X_ERROR_NONE);
ASSERT_EQ(async.radio.start_transmit_async(&listener, async.packet), SX126xError::SX126X_ERROR_NONE);
async.radio.loop();
EXPECT_EQ(blocking.transactions, async.transactions);
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
EXPECT_EQ(blocking.receiving, async.receiving);
}
INSTANTIATE_TEST_SUITE_P(LoRaAndFSKWithRxOrSleepAndFixedOrVariablePayload, SX126xAsyncModes,
::testing::Combine(::testing::Bool(), ::testing::Bool(), ::testing::Bool()));
TEST(SX126xAsync, SoftwareDeadlineWithoutDioAndCachedTickBeforeStart) {
RadioHardware hw;
TransmitListener listener;
hw.configure(true, true);
hw.auto_complete = false;
hw.dio.internal = true;
const uint32_t before = millis();
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 50), SX126xError::SX126X_ERROR_NONE);
const uint32_t after = millis();
{
LoopBlockingGuard clock{nullptr, nullptr, before - 1};
hw.radio.call();
EXPECT_TRUE(listener.results.empty()); // No unsigned underflow on the admitting loop tick.
}
{
LoopBlockingGuard clock{nullptr, nullptr, after + 51};
hw.radio.call(); // Still enabled despite no IRQ or ISR edge.
}
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_TIMEOUT}));
EXPECT_TRUE(hw.receiving);
EXPECT_FALSE(hw.radio.is_transmitting());
}
TEST(SX126xAsync, UnrelatedIrqIsNotSuccessAndTimeoutWinsOverTxDone) {
RadioHardware hw;
TransmitListener listener;
hw.configure(true, true);
hw.auto_complete = false;
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.irq = IRQ_RX_DONE;
{
LoopBlockingGuard clock{nullptr, nullptr, millis()};
hw.radio.loop();
}
EXPECT_TRUE(listener.results.empty());
hw.irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
hw.radio.loop();
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_TIMEOUT}));
}
TEST(SX126xAsync, CancelRequiresOwnerAndCompletesSynchronouslyOnce) {
RadioHardware hw;
TransmitListener listener, other;
hw.configure(true, true);
hw.auto_complete = false;
EXPECT_FALSE(hw.radio.cancel_transmit_async(&listener));
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
EXPECT_FALSE(hw.radio.cancel_transmit_async(nullptr));
EXPECT_FALSE(hw.radio.cancel_transmit_async(&other));
EXPECT_TRUE(hw.radio.cancel_transmit_async(&listener));
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_CANCELLED}));
EXPECT_FALSE(hw.radio.cancel_transmit_async(&listener));
hw.irq = IRQ_TX_DONE; // A late IRQ cannot deliver a second completion.
hw.radio.loop();
EXPECT_EQ(listener.results.size(), 1u);
EXPECT_TRUE(hw.receiving);
hw.irq = IRQ_TX_DONE; // Also cleared before a new send starts.
ASSERT_EQ(hw.radio.start_transmit_async(&other, hw.packet), SX126xError::SX126X_ERROR_NONE);
{
LoopBlockingGuard clock{nullptr, nullptr, millis()};
hw.radio.loop();
}
EXPECT_TRUE(other.results.empty());
EXPECT_TRUE(hw.radio.cancel_transmit_async(&other));
}
TEST(SX126xAsync, CompletionCallbackCanStartNextSend) {
RadioHardware hw;
TransmitListener first, second;
hw.configure(true, true);
hw.auto_complete = false;
first.on_complete = [&](SX126xError result) {
EXPECT_EQ(hw.radio.start_transmit_async(&second, hw.packet), SX126xError::SX126X_ERROR_NONE);
};
ASSERT_EQ(hw.radio.start_transmit_async(&first, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.irq = IRQ_TX_DONE;
hw.radio.loop();
EXPECT_EQ(first.results.size(), 1u);
EXPECT_TRUE(hw.radio.is_transmitting());
EXPECT_TRUE(second.results.empty());
hw.irq = IRQ_TX_DONE;
hw.radio.loop();
EXPECT_EQ(second.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
static void expect_conflicting_calls_rejected(RadioHardware &hw, TransmitListener &listener) {
const size_t count = hw.transactions.size();
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_BUSY);
hw.radio.set_mode_rx();
hw.radio.set_mode_tx();
hw.radio.set_mode_standby(STDBY_XOSC);
hw.radio.set_mode_sleep(true);
hw.radio.configure();
hw.radio.run_image_cal();
EXPECT_EQ(hw.transactions.size(), count);
}
TEST(SX126xAsync, PendingReceiveRejectsAdmissionWithoutDispatchOrAcknowledgment) {
RadioHardware hw;
TransmitListener listener;
PacketListener receiver;
hw.configure(true, true);
hw.radio.register_listener(&receiver);
hw.irq = IRQ_RX_DONE;
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_BUSY);
EXPECT_TRUE(receiver.packets.empty());
EXPECT_TRUE(listener.results.empty());
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
hw.radio.loop();
EXPECT_EQ(receiver.packets.size(), 1u);
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
expect_conflicting_calls_rejected(hw, listener);
hw.radio.loop();
EXPECT_EQ(listener.results.size(), 1u);
}
TEST(SX126xAsync, ValidatesReadinessPayloadAndTimeoutWithoutSpi) {
RadioHardware hw;
TransmitListener listener;
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_TRUE(hw.transactions.empty());
hw.configure(true, true, true);
hw.clear_trace();
EXPECT_EQ(hw.radio.start_transmit_async(nullptr, hw.packet), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, nullptr, 3), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet.data(), 0), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet.data(), 2), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet.data(), 256), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 0), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 262144), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_TRUE(hw.transactions.empty());
EXPECT_TRUE(listener.results.empty());
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 262143), SX126xError::SX126X_ERROR_NONE);
EXPECT_EQ(hw.transactions.back(), (std::vector<uint8_t>{RADIO_SET_TX, 0xFF, 0xFF, 0xC0}));
hw.radio.loop();
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet, 1), SX126xError::SX126X_ERROR_NONE);
EXPECT_EQ(hw.transactions.back(), (std::vector<uint8_t>{RADIO_SET_TX, 0, 0, 64}));
hw.radio.loop();
}
TEST(SX126xAsync, PersistentBusyReportsFailedCleanupAndAllowsLaterRecovery) {
RadioHardware hw;
TransmitListener listener;
hw.configure(true, true);
hw.auto_complete = false;
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.busy.on_read = []() { return true; };
ASSERT_TRUE(hw.radio.cancel_transmit_async(&listener));
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_BUSY_TIMEOUT}));
EXPECT_FALSE(hw.radio.is_transmitting());
EXPECT_TRUE(hw.radio.status_has_warning());
EXPECT_TRUE(hw.cs.level);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_EQ(listener.results.size(), 1u); // Failed admission never invokes completion.
hw.busy.on_read = []() { return false; };
hw.radio.configure();
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.irq = IRQ_TX_DONE;
hw.radio.loop();
ASSERT_EQ(listener.results.size(), 2u);
EXPECT_EQ(listener.results.back(), SX126xError::SX126X_ERROR_NONE);
EXPECT_TRUE(hw.receiving);
EXPECT_FALSE(hw.radio.status_has_warning());
}
TEST(SX126xAsync, CancellationCallbackMayStartAnotherSend) {
RadioHardware hw;
TransmitListener first, second;
hw.configure(true, true);
hw.auto_complete = false;
first.on_complete = [&](SX126xError result) {
EXPECT_EQ(result, SX126xError::SX126X_ERROR_CANCELLED);
EXPECT_EQ(hw.radio.start_transmit_async(&second, hw.packet), SX126xError::SX126X_ERROR_NONE);
};
ASSERT_EQ(hw.radio.start_transmit_async(&first, hw.packet), SX126xError::SX126X_ERROR_NONE);
EXPECT_TRUE(hw.radio.cancel_transmit_async(&first));
EXPECT_TRUE(hw.radio.is_transmitting());
EXPECT_FALSE(hw.radio.cancel_transmit_async(&first));
hw.irq = IRQ_TX_DONE;
hw.radio.loop();
EXPECT_EQ(second.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
TEST(SX126xAsync, RejectsAfterFailedSetupWithoutBusAccess) {
RadioHardware hw;
TransmitListener listener;
hw.fail_at = 1;
hw.configure(true, true);
ASSERT_TRUE(hw.radio.is_failed());
hw.clear_trace();
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_TRUE(hw.transactions.empty());
EXPECT_TRUE(listener.results.empty());
}
TEST(SX126xAsync, TwoRadiosHaveIndependentOwnersAndCompletion) {
RadioHardware a, b;
TransmitListener first, second;
a.configure(true, true);
b.configure(false, false);
a.auto_complete = b.auto_complete = false;
ASSERT_EQ(a.radio.start_transmit_async(&first, a.packet), SX126xError::SX126X_ERROR_NONE);
ASSERT_EQ(b.radio.start_transmit_async(&second, b.packet), SX126xError::SX126X_ERROR_NONE);
a.radio.cancel_transmit_async(&first);
EXPECT_TRUE(b.radio.is_transmitting());
EXPECT_TRUE(second.results.empty());
b.irq = IRQ_TX_DONE;
b.radio.loop();
EXPECT_EQ(first.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_CANCELLED}));
EXPECT_EQ(second.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
TEST(SX126xAsync, BackToBackReceiveEchoPreservesBorrowedPacketForAllListeners) {
class EchoListener : public SX126xListener {
public:
std::function<void(const std::vector<uint8_t> &)> callback;
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override { callback(packet); }
} echo;
RadioHardware hw;
TransmitListener tx;
PacketListener observer;
hw.configure(true, true);
hw.auto_complete = false;
hw.radio.register_listener(&echo);
hw.radio.register_listener(&observer);
const auto first = hw.packet;
const std::vector<uint8_t> second(200, 0xA5);
unsigned calls = 0;
echo.callback = [&](const std::vector<uint8_t> &packet) {
if (++calls == 1) {
hw.packet = second; // Larger back-to-back RX could reallocate the driver's packet_.
hw.irq = IRQ_RX_DONE;
const auto count = hw.transactions.size();
hw.radio.loop(); // Explicit loop reentry must not overwrite the borrowed RX buffer either.
// The send stops in standby and holds the new packet instead of overwriting the FIFO.
EXPECT_EQ(hw.radio.start_transmit_async(&tx, packet), SX126xError::SX126X_ERROR_BUSY);
ASSERT_EQ(hw.transactions.size(), count + 2);
EXPECT_EQ(hw.transactions[count][0], RADIO_SET_STANDBY);
EXPECT_EQ(hw.transactions.back()[0], RADIO_GET_IRQSTATUS);
EXPECT_EQ(packet, first);
} else {
EXPECT_EQ(packet, second);
EXPECT_EQ(hw.radio.start_transmit_async(&tx, packet), SX126xError::SX126X_ERROR_NONE);
}
};
hw.irq = IRQ_RX_DONE;
hw.radio.loop();
ASSERT_EQ(observer.packets.size(), 1u);
EXPECT_EQ(observer.packets[0], first);
EXPECT_TRUE(tx.results.empty());
EXPECT_FALSE(hw.radio.is_transmitting());
hw.radio.loop();
ASSERT_EQ(observer.packets.size(), 2u);
EXPECT_EQ(observer.packets[1], second);
ASSERT_EQ(calls, 2u);
auto fifo = std::find_if(hw.transactions.rbegin(), hw.transactions.rend(),
[](const auto &cmd) { return !cmd.empty() && cmd[0] == RADIO_WRITE_BUFFER; });
ASSERT_NE(fifo, hw.transactions.rend());
EXPECT_EQ(std::vector<uint8_t>(fifo->begin() + 2, fifo->end()), second);
hw.irq = IRQ_TX_DONE;
hw.radio.loop();
EXPECT_EQ(tx.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
TEST(SX126xAsync, FailedReconfigurationRejectsAsyncUntilReconfigured) {
RadioHardware hw;
TransmitListener listener;
hw.configure(true, true);
hw.clear_trace();
hw.fail_at = 3;
hw.radio.configure();
hw.clear_trace();
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_TRUE(hw.transactions.empty());
EXPECT_TRUE(listener.results.empty());
hw.radio.configure();
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.radio.loop();
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
TEST(SX126xAsync, BlockingSendUsesIrqResultInsteadOfTreatingAnyDioAsSuccess) {
RadioHardware hw;
hw.configure(true, true);
auto write = hw.cs.on_write;
hw.cs.on_write = [&](bool high) {
write(high);
if (high && !hw.transactions.back().empty() && hw.transactions.back()[0] == RADIO_SET_TX) {
hw.irq = IRQ_RX_TX_TIMEOUT;
}
};
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_TIMEOUT);
EXPECT_TRUE(hw.receiving);
}
TEST(SX126xAsync, FailedAdmissionRestoresReceivePollingWithoutAnotherTransmission) {
RadioHardware hw;
PacketListener rx;
TransmitListener tx;
hw.configure(true, true);
hw.radio.register_listener(&rx);
const auto configured_params = hw.packet_params;
hw.dio.internal = true;
hw.radio.call();
ASSERT_TRUE(hw.radio.is_idle());
hw.clear_trace();
hw.fail_at = 3;
EXPECT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
EXPECT_TRUE(tx.results.empty());
EXPECT_TRUE(hw.radio.is_in_loop_state());
EXPECT_TRUE(hw.receiving);
EXPECT_EQ(hw.packet_params, configured_params);
hw.clear_trace();
hw.irq = IRQ_RX_DONE; // No simulated ISR edge: restoration must re-enable polling.
hw.radio.call();
ASSERT_EQ(rx.packets.size(), 1u);
EXPECT_EQ(rx.packets[0], hw.packet);
EXPECT_EQ(hw.sends, 0u);
}
TEST_P(SX126xAsyncModes, ReceiveAtStandbyRejectsSendAndSurvivesReadFailure) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware hw;
PacketListener received;
TransmitListener tx;
hw.configure(lora, rx, fixed);
hw.radio.call(); // Advance from setup into ESPHome loop scheduling.
hw.radio.register_listener(&received);
hw.clear_trace();
auto write = hw.cs.on_write;
bool inject = true;
hw.cs.on_write = [&](bool high) {
write(high);
if (high && inject && !hw.transactions.back().empty() && hw.transactions.back()[0] == RADIO_SET_STANDBY) {
inject = false;
hw.irq = IRQ_RX_DONE;
}
};
EXPECT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_BUSY);
EXPECT_TRUE(tx.results.empty());
EXPECT_TRUE(received.packets.empty());
EXPECT_FALSE(hw.receiving);
EXPECT_FALSE(hw.radio.is_transmitting());
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
ASSERT_EQ(hw.transactions.size(), 2u);
EXPECT_EQ(hw.transactions[0][0], RADIO_SET_STANDBY);
EXPECT_EQ(hw.transactions[1][0], RADIO_GET_IRQSTATUS);
const auto resets = hw.rst.writes;
hw.radio.set_mode_rx();
EXPECT_EQ(hw.rst.writes, resets);
EXPECT_EQ(hw.transactions.size(), 2u);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY);
EXPECT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_BUSY);
EXPECT_FALSE(hw.radio.cancel_transmit_async(&tx));
hw.clear_trace();
hw.fail_at = 2; // Packet metadata read fails after the held IRQ is read.
hw.radio.loop();
EXPECT_TRUE(hw.radio.is_in_loop_state());
EXPECT_TRUE(received.packets.empty());
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
EXPECT_FALSE(hw.receiving);
hw.clear_trace();
LoopBlockingGuard retry{nullptr, nullptr, App.get_loop_component_start_time() + 200};
hw.radio.loop();
hw.radio.loop();
ASSERT_EQ(received.packets.size(), 1u);
EXPECT_EQ(received.packets[0], hw.packet);
EXPECT_EQ(hw.receiving, rx);
EXPECT_EQ(hw.sends, 0u);
EXPECT_TRUE(tx.results.empty());
ASSERT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.radio.loop();
EXPECT_EQ(tx.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
TEST_P(SX126xAsyncModes, FailedPreparationAndRestorationRequireSuccessfulConfiguration) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware hw;
TransmitListener listener;
hw.configure(lora, rx, fixed);
hw.clear_trace();
const auto busy = hw.busy.on_read;
hw.busy.on_read = [&]() { return hw.transactions.size() >= 3 || busy(); };
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
EXPECT_TRUE(listener.results.empty());
EXPECT_EQ(hw.sends, 0u);
EXPECT_FALSE(hw.radio.is_transmitting());
EXPECT_TRUE(hw.cs.level);
ASSERT_EQ(hw.transactions.size(), 4u);
EXPECT_TRUE(hw.transactions[2].empty()); // Failed preparation.
EXPECT_TRUE(hw.transactions[3].empty()); // Failed restoration.
hw.clear_trace();
hw.busy.on_read = busy;
hw.radio.set_mode_rx();
hw.radio.loop();
EXPECT_TRUE(hw.radio.is_idle());
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_TRUE(hw.transactions.empty());
hw.fail_at = 1;
hw.radio.configure();
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
hw.clear_trace();
hw.radio.configure();
EXPECT_TRUE(hw.radio.is_in_loop_state());
EXPECT_EQ(hw.receiving, rx);
EXPECT_TRUE(listener.results.empty());
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.radio.loop();
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
TEST(SX126xAsync, ExplicitConfigurationCanRecoverAnUnreadableHeldPacket) {
RadioHardware hw;
TransmitListener listener;
hw.configure(true, true);
auto write = hw.cs.on_write;
hw.cs.on_write = [&](bool high) {
write(high);
if (high && !hw.transactions.back().empty() && hw.transactions.back()[0] == RADIO_SET_STANDBY) {
hw.irq = IRQ_RX_DONE;
}
};
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_BUSY);
hw.cs.on_write = write;
hw.busy.on_read = []() { return true; };
hw.radio.loop();
const auto resets = hw.rst.writes;
hw.radio.configure();
EXPECT_GT(hw.rst.writes, resets);
EXPECT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
hw.busy.on_read = []() { return false; };
hw.radio.configure();
EXPECT_TRUE(hw.receiving);
EXPECT_TRUE(listener.results.empty());
ASSERT_EQ(hw.radio.start_transmit_async(&listener, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.radio.loop();
EXPECT_EQ(listener.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
}
class SX126xReceiveActions : public ::testing::TestWithParam<unsigned> {};
TEST_P(SX126xReceiveActions, ModeAndConfigurationPreserveBorrowedPacket) {
RadioHardware hw;
ReceiveCallback action;
PacketListener observer;
hw.configure(true, true);
hw.radio.register_listener(&action);
hw.radio.register_listener(&observer);
const auto original = hw.packet;
const unsigned selected_action = GetParam();
unsigned callbacks = 0;
action.on_packet_callback = [&]() {
callbacks++;
hw.clear_trace();
switch (selected_action) {
case 0:
hw.radio.set_mode_sleep();
break;
case 1:
hw.radio.set_mode_sleep(true);
break;
case 2:
hw.radio.set_rx_start(false);
hw.radio.configure();
break;
case 3:
hw.radio.set_mode_standby(STDBY_XOSC);
break;
case 4:
hw.radio.set_mode_rx();
break;
case 5:
hw.radio.run_image_cal();
break;
default:
FAIL() << "Unknown action " << selected_action;
}
EXPECT_FALSE(hw.transactions.empty()); // The requested operation must actually reach the chip.
EXPECT_EQ(hw.receiving, selected_action >= 4);
const auto commands = hw.transactions.size();
hw.packet.assign(19, 0x99);
hw.irq = IRQ_RX_DONE;
hw.radio.loop(); // Recursive packet delivery is still forbidden, including after configure().
EXPECT_EQ(hw.transactions.size(), commands);
};
hw.irq = IRQ_RX_DONE;
hw.radio.loop();
EXPECT_EQ(callbacks, 1u);
ASSERT_EQ(observer.packets.size(), 1u);
EXPECT_EQ(observer.packets.front(), original);
EXPECT_EQ(hw.irq, IRQ_RX_DONE); // The next packet is left for a later loop.
if (selected_action == 1) {
TransmitListener tx;
EXPECT_EQ(hw.radio.start_transmit_async(&tx, original), SX126xError::SX126X_ERROR_NOT_READY);
}
}
INSTANTIATE_TEST_SUITE_P(SleepColdSleepConfigureStandbyRxAndCalibration, SX126xReceiveActions,
::testing::Range(0u, 6u));
} // namespace esphome::sx126x::testing
@@ -1,788 +0,0 @@
#include "sx126x_test_helpers.h"
#include <cmath>
#include "esphome/core/application.h"
#include <tuple>
#include <type_traits>
#ifdef USE_HOST
#include <string>
#include "esphome/components/logger/logger.h"
#endif
namespace esphome::sx126x::testing {
class SX126xBusySetup : public ::testing::TestWithParam<std::tuple<bool, bool>> {};
TEST_P(SX126xBusySetup, StopsAtEveryConfigurationCommand) {
const auto [lora, rx] = GetParam();
RadioHardware reference;
reference.configure(lora, rx);
ASSERT_FALSE(reference.radio.is_failed());
ASSERT_FALSE(reference.radio.status_has_warning());
const size_t count = reference.transactions.size();
ASSERT_GT(count, 10u);
for (size_t fault = 1; fault <= count; fault++) {
SCOPED_TRACE(fault);
RadioHardware hardware;
hardware.fail_at = fault;
hardware.configure(lora, rx);
hardware.expect_aborted(fault);
EXPECT_TRUE(hardware.radio.is_failed());
hardware.radio.dump_config(); // Also safe if the version read never happened.
hardware.clear_trace();
hardware.radio.configure();
hardware.radio.set_mode_rx();
hardware.radio.set_mode_tx();
hardware.radio.set_mode_sleep();
hardware.radio.set_mode_standby(STDBY_XOSC);
hardware.radio.run_image_cal();
hardware.radio.loop();
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_TRUE(hardware.transactions.empty());
}
}
INSTANTIATE_TEST_SUITE_P(LoRaAndFSKWithRxOrSleep, SX126xBusySetup,
::testing::Combine(::testing::Bool(), ::testing::Bool()));
class SX126xBusyTransmit : public ::testing::TestWithParam<std::tuple<bool, bool, bool>> {};
TEST_P(SX126xBusyTransmit, StopsAtEverySendAndCleanupCommandAndCanRetry) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware reference;
reference.configure(lora, rx, fixed);
reference.clear_trace();
ASSERT_EQ(reference.radio.transmit_packet(reference.packet), SX126xError::SX126X_ERROR_NONE);
const size_t count = reference.transactions.size();
for (size_t fault = 1; fault <= count; fault++) {
SCOPED_TRACE(fault);
RadioHardware hardware;
hardware.configure(lora, rx, fixed);
hardware.clear_trace();
hardware.fail_at = fault;
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet.data(), hardware.packet.size()),
SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
EXPECT_TRUE(hardware.transactions[fault - 1].empty());
EXPECT_TRUE(hardware.cs.level);
EXPECT_FALSE(hardware.radio.is_failed());
hardware.clear_trace();
hardware.radio.configure();
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NONE);
EXPECT_FALSE(hardware.radio.status_has_warning());
EXPECT_EQ(hardware.receiving, rx);
}
}
TEST_P(SX126xBusyTransmit, RestoresEveryPreparationFailureWithoutAnotherTransmission) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware reference;
reference.configure(lora, rx, fixed);
reference.clear_trace();
ASSERT_EQ(reference.radio.transmit_packet(reference.packet), SX126xError::SX126X_ERROR_NONE);
size_t tx_command = 0;
for (size_t i = 0; i < reference.transactions.size(); i++) {
if (reference.transactions[i][0] == RADIO_SET_TX) {
tx_command = i + 1;
break;
}
}
ASSERT_NE(tx_command, 0u);
for (size_t fault = 1; fault <= tx_command; fault++) {
SCOPED_TRACE(fault);
RadioHardware hw;
PacketListener listener;
hw.configure(lora, rx, fixed);
hw.radio.register_listener(&listener);
const auto configured_params = hw.packet_params;
hw.clear_trace();
hw.fail_at = fault;
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
EXPECT_TRUE(hw.transactions[fault - 1].empty());
EXPECT_TRUE(hw.cs.level);
EXPECT_EQ(hw.ends, hw.transactions.size());
EXPECT_EQ(hw.receiving, rx);
EXPECT_EQ(hw.packet_params, configured_params);
EXPECT_EQ(hw.irq, 0u);
EXPECT_FALSE(hw.radio.status_has_warning());
EXPECT_FALSE(hw.radio.is_failed());
const auto sends = hw.sends;
hw.clear_trace();
if (rx) {
if (!fixed) {
hw.packet.resize(9, 0x55); // RX must accept a different length than the attempted TX.
}
hw.irq = IRQ_RX_DONE;
hw.radio.loop();
ASSERT_EQ(listener.packets.size(), 1u);
EXPECT_EQ(listener.packets[0], hw.packet);
}
EXPECT_EQ(hw.sends, sends);
}
}
TEST_P(SX126xBusyTransmit, FailedRestorationRequiresConfigureBeforeReceiveRecovery) {
const auto [lora, rx, fixed] = GetParam();
RadioHardware reference;
reference.configure(lora, rx, fixed);
reference.clear_trace();
ASSERT_EQ(reference.radio.transmit_packet(reference.packet), SX126xError::SX126X_ERROR_NONE);
size_t restoration = 0;
bool sent = false;
for (size_t i = 0; i < reference.transactions.size(); i++) {
if (reference.transactions[i][0] == RADIO_SET_TX) {
sent = true;
}
if (sent && reference.transactions[i][0] == RADIO_SET_STANDBY) {
restoration = i + 1;
break;
}
}
ASSERT_NE(restoration, 0u);
for (size_t fault = restoration; fault <= reference.transactions.size(); fault++) {
SCOPED_TRACE(fault);
RadioHardware hw;
PacketListener listener;
hw.configure(lora, rx, fixed);
hw.radio.register_listener(&listener);
const auto configured_params = hw.packet_params;
hw.clear_trace();
hw.fail_at = fault;
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
EXPECT_TRUE(hw.cs.level);
EXPECT_TRUE(hw.radio.status_has_warning());
EXPECT_FALSE(hw.radio.is_failed());
const auto sends = hw.sends;
hw.clear_trace(); // Clearing BUSY alone must not re-admit partial configuration.
hw.irq = IRQ_RX_DONE;
hw.radio.loop();
hw.radio.set_mode_rx();
hw.radio.set_mode_tx();
hw.radio.set_mode_standby(STDBY_XOSC);
hw.radio.set_mode_sleep();
hw.radio.run_image_cal();
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_TRUE(hw.transactions.empty());
EXPECT_TRUE(listener.packets.empty());
EXPECT_TRUE(hw.radio.status_has_warning());
hw.irq = 0; // Hardware reset clears IRQs; the GPIO fake does not model reset.
hw.radio.configure();
EXPECT_FALSE(hw.radio.status_has_warning());
EXPECT_EQ(hw.packet_params, configured_params);
EXPECT_EQ(hw.receiving, rx);
if (rx) {
hw.irq = IRQ_RX_DONE;
hw.radio.loop();
ASSERT_EQ(listener.packets.size(), 1u);
EXPECT_EQ(listener.packets[0], hw.packet);
}
EXPECT_EQ(hw.sends, sends); // Recovery must not depend on sending another packet.
}
}
TEST(SX126xBusy, PersistentBusyDuringPreparationAndRestorationRequiresConfigure) {
RadioHardware hw;
PacketListener listener;
hw.configure(true, true);
hw.radio.register_listener(&listener);
const auto configured_params = hw.packet_params;
hw.clear_trace();
auto normal_busy = hw.busy.on_read;
hw.busy.on_read = [&]() { return hw.transactions.size() >= 3 || normal_busy(); };
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
EXPECT_FALSE(hw.receiving);
EXPECT_TRUE(hw.radio.status_has_warning());
EXPECT_EQ(hw.sends, 0u);
hw.busy.on_read = normal_busy;
hw.clear_trace();
hw.radio.set_mode_rx();
hw.radio.run_image_cal();
EXPECT_TRUE(hw.transactions.empty());
EXPECT_TRUE(hw.radio.status_has_warning());
hw.radio.configure();
EXPECT_TRUE(hw.receiving);
EXPECT_EQ(hw.packet_params, configured_params);
hw.irq = IRQ_RX_DONE;
hw.radio.loop();
ASSERT_EQ(listener.packets.size(), 1u);
EXPECT_EQ(listener.packets[0], hw.packet);
EXPECT_EQ(hw.sends, 0u);
}
INSTANTIATE_TEST_SUITE_P(LoRaAndFSKWithRxOrSleepAndFixedOrVariablePayload, SX126xBusyTransmit,
::testing::Combine(::testing::Bool(), ::testing::Bool(), ::testing::Bool()));
class SX126xBusyReceive : public ::testing::TestWithParam<bool> {};
TEST_P(SX126xBusyReceive, KeepsAPacketWhoseReadFailedAndRetries) {
const bool lora = GetParam();
for (size_t fault = 1; fault <= 5; fault++) {
SCOPED_TRACE(fault);
RadioHardware hardware;
PacketListener listener;
hardware.configure(lora, true);
hardware.radio.register_listener(&listener);
hardware.clear_trace();
hardware.irq = IRQ_RX_DONE;
hardware.fail_at = fault;
hardware.radio.loop();
hardware.expect_aborted(fault);
EXPECT_TRUE(listener.packets.empty());
EXPECT_TRUE(hardware.radio.status_has_warning());
EXPECT_FALSE(hardware.radio.is_failed());
// The flags may already be cleared; the packet is still read on the retry.
hardware.clear_trace();
LoopBlockingGuard retry{nullptr, nullptr, App.get_loop_component_start_time() + 200};
hardware.radio.loop();
ASSERT_EQ(listener.packets.size(), 1u);
EXPECT_EQ(listener.packets[0], hardware.packet);
EXPECT_FLOAT_EQ(listener.rssi, lora ? -40 : -42);
if (lora) {
EXPECT_FLOAT_EQ(listener.snr, 10);
} else {
EXPECT_TRUE(std::isnan(listener.snr));
}
EXPECT_FALSE(hardware.radio.status_has_warning());
}
}
INSTANTIATE_TEST_SUITE_P(LoRaAndFSK, SX126xBusyReceive, ::testing::Bool());
// A packet whose flags were cleared before its read failed, then held by a send, survives a
// second failed read
TEST(SX126xBusy, HeldPacketSurvivesASecondFailedRead) {
RadioHardware hw;
PacketListener listener;
hw.configure(true, true);
hw.radio.register_listener(&listener);
hw.clear_trace();
hw.irq = IRQ_RX_DONE;
hw.fail_at = 3; // the first read after the flags were cleared
hw.radio.loop();
ASSERT_EQ(hw.irq, 0);
ASSERT_TRUE(listener.packets.empty());
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY);
hw.clear_trace();
hw.fail_at = 2; // the held packet's read
LoopBlockingGuard first{nullptr, nullptr, App.get_loop_component_start_time() + 200};
hw.radio.loop();
ASSERT_TRUE(listener.packets.empty());
hw.clear_trace();
LoopBlockingGuard second{nullptr, nullptr, App.get_loop_component_start_time() + 200};
hw.radio.loop();
ASSERT_EQ(listener.packets.size(), 1u);
EXPECT_EQ(listener.packets[0], hw.packet);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
}
TEST(SX126xBusy, KeepsPollingAnUnclearedInterruptAfterBusyTimeout) {
RadioHardware hardware;
PacketListener listener;
hardware.configure(true, true);
hardware.radio.register_listener(&listener);
hardware.dio.internal = true; // No new ISR edge while the pending IRQ stays high.
hardware.clear_trace();
hardware.fail_at = 1;
hardware.irq = IRQ_RX_DONE;
hardware.radio.call();
EXPECT_TRUE(listener.packets.empty());
hardware.clear_trace();
LoopBlockingGuard retry{nullptr, nullptr, App.get_loop_component_start_time() + 200};
hardware.radio.call();
EXPECT_EQ(listener.packets.size(), 1u);
}
TEST(SX126xBusy, AssertsChipSelectBeforeWaitingForWakeup) {
RadioHardware hardware;
hardware.configure(true, true);
hardware.clear_trace();
hardware.busy.level = true; // The CS callback releases BUSY only after selecting the chip.
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NONE);
EXPECT_TRUE(hardware.cs.level);
EXPECT_EQ(hardware.ends, hardware.transactions.size());
}
TEST(SX126xBusy, RejectsSendBeforeSetupWithoutTouchingPins) {
RadioHardware hardware;
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_TRUE(hardware.transactions.empty());
EXPECT_EQ(hardware.busy.reads, 0u);
EXPECT_EQ(hardware.dio.reads, 0u);
}
TEST(SX126xBusy, UnsupportedVersionFailsSetup) {
RadioHardware hardware;
hardware.supported_version = false;
hardware.configure(true, true);
EXPECT_TRUE(hardware.radio.is_failed());
}
TEST(SX126xBusy, RuntimeVersionFailureRequiresConfigureWithoutPermanentlyFailing) {
RadioHardware hardware;
hardware.configure(true, true);
hardware.supported_version = false;
hardware.radio.configure();
EXPECT_FALSE(hardware.radio.is_failed());
EXPECT_TRUE(hardware.radio.status_has_warning());
hardware.clear_trace();
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_TRUE(hardware.transactions.empty());
hardware.supported_version = true;
hardware.radio.configure();
EXPECT_FALSE(hardware.radio.status_has_warning());
EXPECT_EQ(hardware.radio.transmit_packet(hardware.packet), SX126xError::SX126X_ERROR_NONE);
}
TEST(SX126xBusy, FailedReconfigurationRequiresSuccessfulConfigureBeforeOperations) {
RadioHardware reference;
reference.configure(true, true);
const size_t count = reference.transactions.size();
for (size_t fault = 1; fault <= count; fault++) {
SCOPED_TRACE(fault);
RadioHardware hw;
PacketListener listener;
hw.configure(true, true);
hw.radio.register_listener(&listener);
hw.clear_trace();
hw.fail_at = fault;
hw.radio.configure();
EXPECT_FALSE(hw.radio.is_failed());
hw.clear_trace();
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
hw.irq = IRQ_RX_DONE;
hw.radio.loop();
hw.radio.set_mode_rx();
hw.radio.set_mode_tx();
hw.radio.set_mode_standby(STDBY_XOSC);
hw.radio.set_mode_sleep();
hw.radio.run_image_cal();
EXPECT_TRUE(hw.transactions.empty());
EXPECT_TRUE(listener.packets.empty());
hw.radio.configure();
hw.irq = 0;
EXPECT_FALSE(hw.radio.status_has_warning());
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
}
}
TEST(SX126xBusy, RetryClearsStaleTxDoneBeforeWaitingForDelayedCompletion) {
RadioHardware reference;
reference.configure(true, true);
reference.clear_trace();
reference.radio.transmit_packet(reference.packet);
size_t cleanup = 0;
bool sent = false;
for (size_t i = 0; i < reference.transactions.size(); i++) {
if (reference.transactions[i][0] == RADIO_SET_TX) {
sent = true;
}
if (sent && reference.transactions[i][0] == RADIO_CLR_IRQSTATUS) {
cleanup = i + 1;
break;
}
}
ASSERT_NE(cleanup, 0u);
RadioHardware hw;
hw.configure(true, true);
hw.clear_trace();
hw.fail_at = cleanup;
ASSERT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
EXPECT_EQ(hw.irq & IRQ_TX_DONE, 0); // Shared restoration retries the failed IRQ clear.
hw.irq = IRQ_TX_DONE; // A stale flag must still not finish the next TX.
hw.clear_trace();
hw.auto_complete = false;
unsigned polls = 0;
hw.dio.on_read = [&]() {
if (++polls == 5) {
hw.irq |= IRQ_TX_DONE;
}
return hw.irq != 0;
};
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
EXPECT_EQ(polls, 5u);
EXPECT_TRUE(hw.receiving);
}
} // namespace esphome::sx126x::testing
namespace esphome::sx126x::testing {
template<typename T>
concept RawSpiAccessible = requires(T &device) { device.transfer_byte(0); };
template<typename T>
concept ManualSelectionAccessible = requires(T &device) {
device.enable();
device.disable();
};
static_assert(!RawSpiAccessible<SX126x>);
static_assert(!RawSpiAccessible<SX126xSpiDevice>);
static_assert(!ManualSelectionAccessible<SX126x>);
static_assert(!ManualSelectionAccessible<SX126xSpiDevice>);
// The radio runs every command sequence and state change. Without component status or
// listeners it cannot run user code mid-operation; SX126x::apply_() does that afterwards.
static_assert(!std::is_base_of_v<Component, SX126xRadio>);
} // namespace esphome::sx126x::testing
namespace esphome::sx126x::testing {
TEST(SX126xBusy, FailedModeChangeCannotBeClearedByAnotherModeCommand) {
for (size_t fault = 1; fault <= 3; fault++) {
RadioHardware hw;
hw.configure(true, true);
hw.clear_trace();
hw.fail_at = fault;
hw.radio.set_mode_rx();
EXPECT_TRUE(hw.radio.status_has_warning());
hw.clear_trace();
hw.radio.set_mode_standby(STDBY_XOSC);
hw.radio.run_image_cal();
EXPECT_TRUE(hw.transactions.empty());
EXPECT_TRUE(hw.radio.status_has_warning());
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
hw.radio.configure();
EXPECT_FALSE(hw.radio.status_has_warning());
EXPECT_TRUE(hw.receiving);
}
}
TEST(SX126xBusy, ConfigurationCannotReenterSendPreparationOrRestoration) {
RadioHardware hw;
hw.configure(true, true);
hw.radio.call(); // Advance from setup into ESPHome loop scheduling.
hw.clear_trace();
auto write = hw.cs.on_write;
unsigned guarded = 0;
hw.cs.on_write = [&](bool high) {
write(high);
if (!high || hw.transactions.back().empty() || hw.transactions.back()[0] != RADIO_SET_STANDBY) {
return;
}
guarded++;
const auto commands = hw.transactions.size();
hw.radio.configure();
hw.radio.set_mode_rx();
hw.radio.loop();
EXPECT_TRUE(hw.radio.is_in_loop_state());
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY);
EXPECT_EQ(hw.transactions.size(), commands);
};
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
EXPECT_EQ(guarded, 2u);
EXPECT_TRUE(hw.radio.is_in_loop_state());
EXPECT_TRUE(hw.receiving);
}
} // namespace esphome::sx126x::testing
namespace esphome::sx126x::testing {
TEST(SX126xBusy, InvalidPayloadDoesNotEnterAnOperation) {
RadioHardware hw;
hw.configure(true, true);
hw.clear_trace();
EXPECT_EQ(hw.radio.transmit_packet(nullptr, 3), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet.data(), 0), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_TRUE(hw.transactions.empty());
EXPECT_TRUE(hw.receiving);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
}
TEST(SX126xBusy, ColdSleepIsWokenByAModeCommandAndWarmSleepRetainsIt) {
RadioHardware hw;
hw.configure(true, true);
hw.radio.set_mode_sleep(false);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.radio.set_mode_sleep(true);
hw.clear_trace();
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
// Sleeping again leaves it asleep
hw.radio.set_mode_sleep(false);
EXPECT_TRUE(hw.transactions.empty());
// Any other mode command applies the lost configuration first
hw.radio.set_mode_rx();
EXPECT_FALSE(hw.transactions.empty());
EXPECT_TRUE(hw.receiving);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
}
// Runs the scheduler for ms milliseconds of real time, as the main loop would
static void run_scheduler(uint32_t ms) {
const uint32_t start = millis();
while (millis() - start < ms) {
App.scheduler.call(millis());
delay(5);
}
}
TEST(SX126xBusy, FailedModeChangeIsReconfiguredWithoutBeingAsked) {
RadioHardware hw;
hw.configure(true, true);
hw.clear_trace();
hw.fail_at = 1;
hw.radio.set_mode_rx();
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
run_scheduler(1100);
EXPECT_FALSE(hw.radio.status_has_warning());
EXPECT_TRUE(hw.receiving);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
// Once configured, the retry stops
hw.clear_trace();
run_scheduler(1100);
EXPECT_TRUE(hw.transactions.empty());
}
TEST(SX126xBusy, ColdSleepIsNotReconfiguredWithoutBeingAsked) {
RadioHardware hw;
hw.configure(true, true);
hw.radio.set_mode_sleep(true);
hw.clear_trace();
run_scheduler(1100);
EXPECT_TRUE(hw.transactions.empty());
}
TEST(SX126xBusy, ConfigureBeforeSetupDoesNothing) {
RadioHardware hw;
hw.radio.configure();
EXPECT_TRUE(hw.transactions.empty());
EXPECT_EQ(hw.rst.writes, 0u);
hw.configure(true, true);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
}
// The TX mode command sends whatever the FIFO holds without tracking it; its TX_DONE is
// cleared as a flag without a packet, and a send still works afterwards
TEST(SX126xBusy, RawTxModeLeavesTheRadioUsable) {
RadioHardware hw;
PacketListener listener;
hw.configure(true, true);
hw.radio.register_listener(&listener);
hw.radio.set_mode_tx();
ASSERT_EQ(hw.irq & IRQ_TX_DONE, IRQ_TX_DONE);
hw.radio.loop();
EXPECT_EQ(hw.irq, 0);
EXPECT_TRUE(listener.packets.empty());
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
}
TEST(SX126xBusy, PacketThatFailedItsCrcDoesNotHoldUpASend) {
RadioHardware hw;
hw.configure(true, true);
hw.irq = IRQ_RX_DONE | IRQ_CRC_ERROR;
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
}
TEST(SX126xBusy, PacketCompletingDuringAReadIsReadNext) {
RadioHardware hw;
PacketListener listener;
hw.configure(true, true);
hw.radio.register_listener(&listener);
bool second = false;
hw.on_command = [&](uint8_t opcode) {
if (opcode == RADIO_READ_BUFFER && !second) {
second = true;
hw.irq |= IRQ_RX_DONE;
}
};
hw.irq = IRQ_RX_DONE;
hw.radio.loop();
EXPECT_EQ(listener.packets.size(), 1u);
EXPECT_EQ(hw.irq & IRQ_RX_DONE, IRQ_RX_DONE); // DIO1 stays high for the second packet
hw.radio.loop();
EXPECT_EQ(listener.packets.size(), 2u);
}
TEST(SX126xBusy, UnavailableLoopStopsAndConfigurationResumesReceiveWithoutAnInterrupt) {
for (bool internal : {false, true}) {
for (unsigned cause = 0; cause < 3; cause++) {
SCOPED_TRACE(internal);
SCOPED_TRACE(cause);
RadioHardware hw;
PacketListener received;
hw.configure(true, true);
hw.radio.register_listener(&received);
// Set after setup: the fake pin cannot attach a hardware interrupt.
hw.dio.internal = internal;
hw.clear_trace();
if (cause == 0) {
hw.radio.set_mode_sleep(true);
} else if (cause == 1) {
hw.fail_at = 1;
hw.radio.configure();
} else {
hw.fail_at = 1;
hw.radio.set_mode_rx();
}
hw.clear_trace();
hw.radio.call();
ASSERT_TRUE(hw.radio.is_idle());
EXPECT_TRUE(hw.transactions.empty());
EXPECT_EQ(hw.dio.reads, 0u);
hw.radio.configure();
ASSERT_TRUE(hw.radio.is_in_loop_state());
hw.irq = IRQ_RX_DONE; // No ISR edge: configure() must resume polling itself.
hw.radio.call();
ASSERT_EQ(received.packets.size(), 1u);
EXPECT_EQ(received.packets[0], hw.packet);
}
}
}
} // namespace esphome::sx126x::testing
namespace esphome::sx126x::testing {
TEST(SX126xProfile, SettersDoNotChangeAppliedPacketOrRestorationPolicy) {
RadioHardware hw;
hw.configure(true, true);
hw.radio.set_payload_length(2);
hw.radio.set_rx_start(false);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
EXPECT_TRUE(hw.receiving);
hw.radio.configure();
EXPECT_EQ(hw.radio.get_max_packet_size(), 2u);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_INVALID_PARAMS);
EXPECT_EQ(hw.radio.transmit_packet(hw.packet.data(), 2), SX126xError::SX126X_ERROR_NONE);
EXPECT_FALSE(hw.receiving);
}
TEST(SX126xProfile, RejectsOversizedSyncValueBeforeResetOrSpi) {
RadioHardware hw;
hw.configure(true, true);
hw.radio.set_sync_value(std::vector<uint8_t>(9, 1));
hw.clear_trace();
const auto reset_writes = hw.rst.writes;
hw.radio.configure();
EXPECT_EQ(hw.rst.writes, reset_writes);
EXPECT_TRUE(hw.transactions.empty());
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
hw.radio.set_sync_value({0x14, 0x24});
hw.radio.configure();
EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NONE);
}
} // namespace esphome::sx126x::testing
#ifdef USE_HOST
namespace esphome::sx126x::testing {
template<typename F> static std::string capture_radio_logs(F action) {
const auto previous_baud_rate = logger::global_logger->get_baud_rate();
::testing::internal::CaptureStdout();
logger::global_logger->set_baud_rate(115200);
action();
logger::global_logger->set_baud_rate(previous_baud_rate);
return ::testing::internal::GetCapturedStdout();
}
TEST(SX126xDiagnostics, BlockingRejectionsExplainWhyNoPacketWasSent) {
RadioHardware hw;
auto output = capture_radio_logs(
[&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_NOT_READY); });
EXPECT_NE(output.find("Packet send failed: radio not ready"), std::string::npos);
hw.configure(true, true, true);
hw.clear_trace();
output = capture_radio_logs(
[&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet.data(), 2), SX126xError::SX126X_ERROR_INVALID_PARAMS); });
EXPECT_NE(output.find("Packet send failed: invalid payload"), std::string::npos);
EXPECT_TRUE(hw.transactions.empty());
hw.irq = IRQ_RX_DONE;
output =
capture_radio_logs([&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY); });
EXPECT_NE(output.find("Packet send failed: radio busy"), std::string::npos);
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
EXPECT_EQ(hw.sends, 0u);
hw.radio.loop(); // Deliver the held packet, which also clears its IRQ.
EXPECT_EQ(hw.irq, 0u);
hw.clear_trace();
hw.fail_at = 1;
output = capture_radio_logs(
[&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT); });
EXPECT_NE(output.find("Packet send failed: bus timeout"), std::string::npos);
EXPECT_EQ(hw.sends, 0u);
}
TEST(SX126xDiagnostics, AsyncAdmissionLeavesRejectionReportingToItsCaller) {
class Completion final : public SX126xTransmitListener {
public:
void on_transmit_complete(SX126xError result) override { ADD_FAILURE() << "Rejected send completed"; }
} completion;
RadioHardware hw;
const auto output = capture_radio_logs([&]() {
EXPECT_EQ(hw.radio.start_transmit_async(&completion, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
hw.configure(true, true, true);
EXPECT_EQ(hw.radio.start_transmit_async(&completion, hw.packet.data(), 2),
SX126xError::SX126X_ERROR_INVALID_PARAMS);
hw.irq = IRQ_RX_DONE;
EXPECT_EQ(hw.radio.start_transmit_async(&completion, hw.packet), SX126xError::SX126X_ERROR_BUSY);
});
EXPECT_EQ(output.find("Packet send failed"), std::string::npos);
EXPECT_EQ(hw.sends, 0u);
}
TEST(SX126xDiagnostics, FailedSetupPrintsRequestedConfiguration) {
RadioHardware hw;
hw.fail_at = 1;
hw.radio.set_pa_power(10);
hw.configure(true, true, true);
ASSERT_TRUE(hw.radio.is_failed());
const auto output = capture_radio_logs([&]() { hw.radio.dump_config(); });
EXPECT_NE(output.find("Ready for operation: NO"), std::string::npos);
EXPECT_NE(output.find("Requested configuration:"), std::string::npos);
EXPECT_NE(output.find("Frequency: 915000000 Hz"), std::string::npos);
EXPECT_NE(output.find("PA Power: 10 dBm"), std::string::npos);
EXPECT_NE(output.find("Payload Length: 3"), std::string::npos);
EXPECT_NE(output.find("Sync Value: 0x1424"), std::string::npos);
}
TEST(SX126xDiagnostics, StagedConfigurationIsLabelledAndDoesNotAlterAppliedSettings) {
RadioHardware hw;
hw.configure(true, true, true);
hw.radio.set_frequency(868000000);
hw.radio.set_payload_length(2);
auto output = capture_radio_logs([&]() { hw.radio.dump_config(); });
EXPECT_NE(output.find("Ready for operation: YES"), std::string::npos);
EXPECT_NE(output.find("Requested configuration:"), std::string::npos);
EXPECT_NE(output.find("Frequency: 868000000 Hz"), std::string::npos);
EXPECT_NE(output.find("Payload Length: 2"), std::string::npos);
EXPECT_EQ(hw.radio.get_max_packet_size(), 3u);
hw.clear_trace();
hw.fail_at = 1;
hw.radio.configure();
output = capture_radio_logs([&]() { hw.radio.dump_config(); });
EXPECT_NE(output.find("Ready for operation: NO"), std::string::npos);
EXPECT_NE(output.find("Frequency: 868000000 Hz"), std::string::npos);
EXPECT_EQ(hw.radio.get_max_packet_size(), 3u);
}
class SX126xTimeoutDiagnostics : public ::testing::TestWithParam<bool> {};
TEST_P(SX126xTimeoutDiagnostics, BlockingTimeoutLogsFailureAfterRestoration) {
RadioHardware hw;
hw.configure(true, true);
hw.auto_complete = false;
if (GetParam()) {
hw.dio.on_read = [&]() {
if (hw.sends > 0) {
hw.irq = IRQ_RX_TX_TIMEOUT;
}
return hw.irq != 0;
};
}
const auto output =
capture_radio_logs([&]() { EXPECT_EQ(hw.radio.transmit_packet(hw.packet), SX126xError::SX126X_ERROR_TIMEOUT); });
EXPECT_NE(output.find("Packet send failed: timeout"), std::string::npos);
EXPECT_TRUE(hw.receiving);
EXPECT_FALSE(hw.radio.is_transmitting());
EXPECT_FALSE(hw.radio.status_has_warning()); // Restoration itself succeeded.
}
INSTANTIATE_TEST_SUITE_P(SoftwareDeadlineAndRadioTimeout, SX126xTimeoutDiagnostics, ::testing::Bool());
TEST(SX126xDiagnostics, AsyncTimeoutLeavesCompletionReportingToItsCaller) {
struct Completion final : SX126xTransmitListener {
void on_transmit_complete(SX126xError result) override {
this->count++;
EXPECT_EQ(result, SX126xError::SX126X_ERROR_TIMEOUT);
}
unsigned count{0};
} completion;
RadioHardware hw;
hw.configure(true, true);
const auto output = capture_radio_logs([&]() {
ASSERT_EQ(hw.radio.start_transmit_async(&completion, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.irq = IRQ_RX_TX_TIMEOUT;
hw.radio.loop();
});
EXPECT_EQ(completion.count, 1u);
EXPECT_EQ(output.find("Packet send failed"), std::string::npos);
}
} // namespace esphome::sx126x::testing
#endif
@@ -1,182 +0,0 @@
#include "sx126x_test_helpers.h"
#include "esphome/components/logger/logger.h"
#include "esphome/core/application.h"
#include <string>
#include <tuple>
namespace esphome::sx126x::testing {
class RadioLogCallback {
public:
RadioLogCallback() : previous_(logger::global_logger) {
this->logger_.pre_setup();
this->logger_.set_log_level(ESPHOME_LOG_LEVEL_WARN);
this->logger_.add_log_callback(this,
[](void *instance, uint8_t level, const char *tag, const char *message, size_t len) {
auto *self = static_cast<RadioLogCallback *>(instance);
if (self->callback_) {
self->callback_(std::string(message, len));
}
});
}
~RadioLogCallback() { logger::global_logger = this->previous_; }
void set_callback(std::function<void(const std::string &)> callback) { this->callback_ = std::move(callback); }
private:
std::function<void(const std::string &)> callback_;
logger::Logger *previous_;
logger::Logger logger_{0};
};
struct ReentryCompletion final : SX126xTransmitListener {
void on_transmit_complete(SX126xError result) override { this->results.push_back(result); }
std::vector<SX126xError> results;
};
// Setup reports why it failed, not a generic reason
TEST(SX126xReentry, FailedSetupKeepsItsReason) {
RadioLogCallback log;
std::vector<std::string> messages;
log.set_callback([&](const std::string &message) { messages.push_back(message); });
RadioHardware hw;
hw.supported_version = false;
hw.configure(true, true);
ASSERT_TRUE(hw.radio.is_failed());
bool found = false;
for (const auto &message : messages)
found |= message.find("set Error flag: Unsupported chip version") != std::string::npos;
EXPECT_TRUE(found);
}
// Status logging is the only user code that can run inside a radio operation. It runs only
// after the operation has committed its state, so a callback sees either a running send or a
// released radio, and the interrupted operation touches nothing after the callback returns.
TEST(SX126xReentry, SuccessfulStartRunsNoUserCode) {
RadioHardware hw;
RadioLogCallback log;
ReentryCompletion outer, nested;
hw.configure(true, true);
hw.auto_complete = false;
hw.radio.status_set_warning(LOG_STR("Previous communication failure"));
hw.irq = IRQ_TX_DONE; // A stale IRQ must not complete the new send either.
unsigned callbacks = 0;
log.set_callback([&](const std::string &message) {
if (message.find("cleared Warning flag") != std::string::npos) {
callbacks++;
EXPECT_FALSE(hw.radio.is_transmitting()); // Only the completion clears the warning.
EXPECT_EQ(hw.radio.start_transmit_async(&nested, hw.packet), SX126xError::SX126X_ERROR_NONE);
}
});
ASSERT_EQ(hw.radio.start_transmit_async(&outer, hw.packet), SX126xError::SX126X_ERROR_NONE);
EXPECT_EQ(callbacks, 0u);
EXPECT_TRUE(outer.results.empty());
LoopBlockingGuard clock{nullptr, nullptr, millis()};
hw.radio.loop();
EXPECT_TRUE(outer.results.empty()); // The stale IRQ was cleared before the send.
hw.irq = IRQ_TX_DONE;
hw.radio.loop();
ASSERT_EQ(callbacks, 1u);
EXPECT_EQ(outer.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
EXPECT_TRUE(nested.results.empty());
EXPECT_TRUE(hw.radio.is_transmitting());
EXPECT_EQ(hw.sends, 2u);
ASSERT_TRUE(hw.radio.cancel_transmit_async(&nested));
EXPECT_EQ(nested.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_CANCELLED}));
EXPECT_EQ(outer.results.size(), 1u);
}
class SX126xStartReentry : public ::testing::TestWithParam<bool> {};
TEST_P(SX126xStartReentry, WarningCallbackAfterFailedStartSeesReleasedRadio) {
const bool cold_sleep = GetParam();
RadioHardware hw;
RadioLogCallback log;
ReentryCompletion outer, nested;
hw.configure(true, true);
hw.auto_complete = false;
hw.radio.status_set_warning(LOG_STR("Previous communication failure"));
hw.clear_trace();
hw.fail_at = 3; // Preparation fails; restoration succeeds and clears the warning.
unsigned callbacks = 0;
size_t commands_after_callback = 0;
log.set_callback([&](const std::string &message) {
if (message.find("cleared Warning flag") != std::string::npos) {
callbacks++;
EXPECT_FALSE(hw.radio.is_transmitting());
EXPECT_EQ(hw.receiving, true); // Restoration finished before the callback.
if (cold_sleep) {
hw.radio.set_mode_sleep(true);
} else {
EXPECT_EQ(hw.radio.start_transmit_async(&nested, hw.packet), SX126xError::SX126X_ERROR_NONE);
}
commands_after_callback = hw.transactions.size();
}
});
EXPECT_EQ(hw.radio.start_transmit_async(&outer, hw.packet), SX126xError::SX126X_ERROR_BUSY_TIMEOUT);
ASSERT_EQ(callbacks, 1u);
EXPECT_EQ(hw.transactions.size(), commands_after_callback); // The failed start touched nothing after it.
EXPECT_TRUE(outer.results.empty()); // A rejected start never completes.
if (cold_sleep) {
EXPECT_FALSE(hw.radio.is_transmitting());
EXPECT_EQ(hw.radio.start_transmit_async(&outer, hw.packet), SX126xError::SX126X_ERROR_NOT_READY);
EXPECT_EQ(hw.sends, 0u);
return;
}
EXPECT_TRUE(hw.radio.is_transmitting());
EXPECT_EQ(hw.sends, 1u);
hw.irq = IRQ_TX_DONE;
hw.radio.loop();
EXPECT_EQ(nested.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
EXPECT_TRUE(outer.results.empty());
}
INSTANTIATE_TEST_SUITE_P(NestedSendOrColdSleep, SX126xStartReentry, ::testing::Bool());
class SX126xPollingReentry : public ::testing::TestWithParam<std::tuple<unsigned, bool>> {};
TEST_P(SX126xPollingReentry, WarningCallbackCannotCancelOrDisturbAFinishedSend) {
const auto [failed_command, reuse_listener] = GetParam();
RadioHardware hw;
RadioLogCallback log;
ReentryCompletion first, second;
auto *replacement = reuse_listener ? &first : &second;
hw.configure(true, true);
hw.auto_complete = false;
ASSERT_EQ(hw.radio.start_transmit_async(&first, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.clear_trace();
hw.fail_at = failed_command; // 0: success, 1: IRQ read fails, 2: IRQ clear fails.
hw.irq = IRQ_TX_DONE;
hw.radio.status_set_warning(LOG_STR("Previous communication failure"));
unsigned callbacks = 0;
size_t commands_after_replacement = 0;
log.set_callback([&](const std::string &message) {
if (callbacks == 0 && message.find("cleared Warning flag") != std::string::npos) {
callbacks++;
EXPECT_TRUE(first.results.empty()); // Status is reported before the completion.
EXPECT_FALSE(hw.radio.cancel_transmit_async(&first)); // Already finished: no second completion.
EXPECT_EQ(hw.radio.start_transmit_async(replacement, hw.packet), SX126xError::SX126X_ERROR_NONE);
commands_after_replacement = hw.transactions.size();
}
});
{
LoopBlockingGuard clock{nullptr, nullptr, millis()};
hw.radio.loop();
}
ASSERT_EQ(callbacks, 1u);
const auto expected = failed_command == 0 ? SX126xError::SX126X_ERROR_NONE : SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
EXPECT_EQ(first.results, (std::vector<SX126xError>{expected}));
EXPECT_TRUE(second.results.empty());
EXPECT_TRUE(hw.radio.is_transmitting());
EXPECT_EQ(hw.transactions.size(), commands_after_replacement); // No stale IRQ clear or restoration.
hw.irq = IRQ_TX_DONE;
hw.radio.loop();
ASSERT_EQ(replacement->results.size(), reuse_listener ? 2u : 1u);
EXPECT_EQ(replacement->results.back(), SX126xError::SX126X_ERROR_NONE);
EXPECT_FALSE(hw.radio.is_transmitting());
}
INSTANTIATE_TEST_SUITE_P(ReadSuccessReadFailureAndClearFailure, SX126xPollingReentry,
::testing::Combine(::testing::Values(0u, 1u, 2u), ::testing::Bool()));
} // namespace esphome::sx126x::testing
@@ -1,127 +0,0 @@
#include "sx126x_test_helpers.h"
#include "esphome/core/application.h"
#include <tuple>
namespace esphome::sx126x::testing {
struct RejectedCompletion final : SX126xTransmitListener {
void on_transmit_complete(SX126xError result) override { ADD_FAILURE() << "Rejected transmission completed"; }
};
class SX126xReceiveRetry : public ::testing::TestWithParam<std::tuple<bool, unsigned, bool>> {};
TEST_P(SX126xReceiveRetry, PersistentBusyIsRateLimitedAndRecoversWithoutNewEdge) {
const auto [held_receive, failed_command, rollover] = GetParam();
RadioHardware hw;
PacketListener listener;
RejectedCompletion tx;
hw.configure(true, true);
hw.radio.register_listener(&listener);
hw.radio.call();
hw.dio.internal = true;
if (held_receive) {
auto write = hw.cs.on_write;
hw.cs.on_write = [&](bool high) {
write(high);
if (high && !hw.transactions.back().empty() && hw.transactions.back()[0] == RADIO_SET_STANDBY) {
hw.irq = IRQ_RX_DONE;
}
};
EXPECT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_BUSY);
hw.cs.on_write = write;
}
hw.clear_trace();
hw.irq = IRQ_RX_DONE;
hw.fail_at = failed_command;
const uint32_t start = rollover ? 0xFFFFFFF0u : 1000;
{
LoopBlockingGuard clock{nullptr, nullptr, start};
hw.radio.call();
}
hw.expect_aborted(failed_command);
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
const auto normal_busy = hw.busy.on_read;
hw.busy.on_read = []() { return true; };
for (uint32_t elapsed = 1; elapsed < 200; elapsed++) {
LoopBlockingGuard clock{nullptr, nullptr, start + elapsed};
hw.radio.call();
}
EXPECT_EQ(hw.transactions.size(), failed_command); // No SPI/BUSY waits inside the retry interval.
{
LoopBlockingGuard clock{nullptr, nullptr, start + 200};
hw.radio.call();
}
ASSERT_EQ(hw.transactions.size(), failed_command + 1);
EXPECT_TRUE(hw.transactions.back().empty());
EXPECT_TRUE(hw.radio.is_in_loop_state());
EXPECT_TRUE(hw.cs.level);
EXPECT_EQ(hw.irq, IRQ_RX_DONE);
EXPECT_TRUE(listener.packets.empty());
hw.busy.on_read = normal_busy;
hw.fail_at = 0;
{
LoopBlockingGuard clock{nullptr, nullptr, start + 399};
hw.radio.call();
EXPECT_EQ(hw.transactions.size(), failed_command + 1);
}
{
LoopBlockingGuard clock{nullptr, nullptr, start + 400};
hw.radio.call();
}
ASSERT_EQ(listener.packets.size(), 1u);
EXPECT_EQ(listener.packets.front(), hw.packet);
EXPECT_FALSE(hw.radio.status_has_warning());
EXPECT_TRUE(hw.receiving);
EXPECT_EQ(hw.sends, 0u);
hw.radio.call();
EXPECT_EQ(listener.packets.size(), 1u);
}
INSTANTIATE_TEST_SUITE_P(IdleAndHeldRxWithIrqAndPacketFailuresAcrossRollover, SX126xReceiveRetry,
::testing::Combine(::testing::Bool(), ::testing::Values(1u, 2u), ::testing::Bool()));
TEST(SX126xReceiveRetry, SuccessfulConfigurationClearsBackoffAndOtherRadiosRemainIndependent) {
RadioHardware failed, other;
PacketListener recovered, unaffected;
failed.configure(true, true);
other.configure(true, true);
failed.radio.register_listener(&recovered);
other.radio.register_listener(&unaffected);
failed.clear_trace();
failed.fail_at = 1;
failed.irq = IRQ_RX_DONE;
LoopBlockingGuard clock{nullptr, nullptr, 1000};
failed.radio.loop();
other.irq = IRQ_RX_DONE;
other.radio.loop();
ASSERT_EQ(unaffected.packets.size(), 1u);
EXPECT_TRUE(recovered.packets.empty());
failed.clear_trace();
failed.radio.configure();
failed.irq = IRQ_RX_DONE;
failed.radio.loop();
ASSERT_EQ(recovered.packets.size(), 1u); // Explicit recovery does not wait for the old retry deadline.
}
TEST(SX126xReceiveRetry, ReceiveBackoffDoesNotDelayTransmitCompletion) {
struct Completion final : SX126xTransmitListener {
void on_transmit_complete(SX126xError result) override { this->results.push_back(result); }
std::vector<SX126xError> results;
} tx;
RadioHardware hw;
hw.configure(true, true);
hw.clear_trace();
hw.fail_at = 1;
hw.irq = IRQ_TX_DONE; // Failed read of an unrelated stale IRQ starts the RX retry gate.
LoopBlockingGuard clock{nullptr, nullptr, millis()};
hw.radio.loop();
EXPECT_TRUE(hw.radio.status_has_warning());
hw.clear_trace();
hw.irq = 0;
ASSERT_EQ(hw.radio.start_transmit_async(&tx, hw.packet), SX126xError::SX126X_ERROR_NONE);
hw.radio.loop(); // Same cached tick, before the RX retry gate opens.
EXPECT_EQ(tx.results, (std::vector<SX126xError>{SX126xError::SX126X_ERROR_NONE}));
EXPECT_TRUE(hw.receiving);
}
} // namespace esphome::sx126x::testing
@@ -1,237 +0,0 @@
#pragma once
#include <gtest/gtest.h>
#include <functional>
#include <vector>
#include "esphome/components/sx126x/sx126x.h"
namespace esphome::sx126x::testing {
class TestPin : public GPIOPin {
public:
void setup() override {}
void pin_mode(gpio::Flags flags) override {}
gpio::Flags get_flags() const override { return gpio::FLAG_NONE; }
size_t dump_summary(char *buffer, size_t len) const override { return snprintf(buffer, len, "test"); }
bool digital_read() override {
this->reads++;
return this->on_read ? this->on_read() : this->level;
}
void digital_write(bool level) override {
this->writes++;
this->level = level;
if (this->on_write) {
this->on_write(level);
}
}
bool is_internal() override { return this->internal; }
bool level{false};
bool internal{false};
unsigned reads{0};
unsigned writes{0};
std::function<bool()> on_read;
std::function<void(bool)> on_write;
};
// Exercise the real software SPI adapter and driver through GPIO pins. Each CS
// assertion starts a command; BUSY can reject any one of those commands.
class RadioHardware {
public:
RadioHardware() {
this->cs.level = true;
this->cs.on_write = [this](bool high) {
if (!high) {
EXPECT_FALSE(this->selected_);
this->selected_ = true;
this->transactions.emplace_back();
this->bit_ = 0;
this->byte_ = 0;
this->busy.level = this->transactions.size() == this->fail_at;
} else if (this->selected_) {
this->selected_ = false;
this->ends++;
const auto &command = this->transactions.back();
if (command.empty()) {
return;
}
switch (command[0]) {
case RADIO_SET_PACKETPARAMS:
this->packet_params = command;
break;
case RADIO_CLR_IRQSTATUS:
this->irq &= ~((uint16_t(command[1]) << 8) | command[2]);
break;
case RADIO_SET_TX:
this->sends++;
if (this->auto_complete) {
this->irq |= IRQ_TX_DONE;
}
break;
case RADIO_SET_RX:
this->receiving = true;
break;
case RADIO_SET_SLEEP:
case RADIO_SET_STANDBY:
this->receiving = false;
break;
default:
break;
}
if (this->on_command) {
this->on_command(command[0]);
}
}
};
this->busy.on_read = [this]() {
EXPECT_TRUE(this->selected_); // NSS must wake the chip before waiting.
return this->busy.level;
};
this->miso.on_read = [this]() {
EXPECT_TRUE(this->selected_);
EXPECT_FALSE(this->busy.level); // No clocks/data may follow a BUSY timeout.
auto &command = this->transactions.back();
const bool bit = (this->reply_byte_(command) >> (7 - this->bit_)) & 1;
this->byte_ = (this->byte_ << 1) | this->mosi.level;
if (++this->bit_ == 8) {
command.push_back(this->byte_);
this->byte_ = 0;
this->bit_ = 0;
}
return bit;
};
this->dio.on_read = [this]() { return this->irq != 0; };
this->rst.on_write = [this](bool high) {
if (!high) {
this->irq = 0;
}
};
this->spi.set_clk(&this->clk);
this->spi.set_mosi(&this->mosi);
this->spi.set_miso(&this->miso);
// call() runs setup() and marks the bus set up, so it accepts devices
this->spi.call();
this->radio.set_spi_parent(&this->spi);
this->radio.set_cs_pin(&this->cs);
this->radio.set_busy_pin(&this->busy);
this->radio.set_dio1_pin(&this->dio);
this->radio.set_rst_pin(&this->rst);
this->radio.set_frequency(915000000);
this->radio.set_tcxo_voltage(TCXO_CTRL_1_8V);
this->radio.set_rf_switch(true);
this->radio.set_crc_enable(true);
this->radio.set_sync_value({0x14, 0x24});
}
~RadioHardware() {
// The configuration retry interval (id 0) must not outlive the radio
App.scheduler.cancel_interval(&this->radio, uint32_t{0});
this->radio.spi_teardown();
}
void configure(bool lora, bool rx, bool fixed = false) {
this->radio.set_modulation(lora ? PACKET_TYPE_LORA : PACKET_TYPE_GFSK);
this->radio.set_bandwidth(lora ? SX126X_BW_125000 : SX126X_BW_4800);
this->radio.set_bitrate(4800);
this->radio.set_rx_start(rx);
this->radio.set_payload_length(fixed ? 3 : 0);
this->radio.call(); // The normal component lifecycle runs setup().
}
void clear_trace() {
this->transactions.clear();
this->ends = 0;
this->fail_at = 0;
this->busy.level = false;
this->dio.reads = 0;
}
void expect_aborted(size_t command) {
ASSERT_EQ(this->transactions.size(), command);
EXPECT_TRUE(this->transactions.back().empty());
EXPECT_EQ(this->ends, command);
EXPECT_TRUE(this->cs.level);
}
SX126x radio;
spi::SPIComponent spi;
TestPin cs, busy, dio, rst, clk, mosi, miso;
std::vector<std::vector<uint8_t>> transactions;
std::vector<uint8_t> packet{0x11, 0x22, 0x33};
std::vector<uint8_t> packet_params;
size_t fail_at{0};
size_t ends{0};
unsigned sends{0};
uint16_t irq{0};
// Called as each command ends, with its opcode
std::function<void(uint8_t)> on_command;
bool receiving{false};
bool auto_complete{true};
bool supported_version{true};
protected:
uint8_t reply_byte_(const std::vector<uint8_t> &command) {
if (command.empty()) {
return 0;
}
const size_t pos = command.size();
switch (command[0]) {
case RADIO_READ_REGISTER:
if (pos >= 4) {
static constexpr char VERSION[16] = "SX1262";
return this->supported_version ? VERSION[pos - 4] : 0;
}
break;
case RADIO_GET_IRQSTATUS:
if (pos == 2) {
return this->irq >> 8;
}
if (pos == 3) {
return this->irq & 0xFF;
}
break;
case RADIO_GET_PACKETSTATUS:
if (pos == 2) {
return 80;
}
if (pos == 3) {
return 40;
}
if (pos == 4) {
return 84;
}
break;
case RADIO_GET_RXBUFFERSTATUS:
if (pos == 2) {
return this->packet.size();
}
if (pos == 3) {
return 7;
}
break;
case RADIO_READ_BUFFER:
if (pos >= 3) {
EXPECT_EQ(command[1], 7);
return this->packet[pos - 3];
}
break;
default:
break;
}
return 0;
}
bool selected_{false};
uint8_t bit_{0};
uint8_t byte_{0};
};
class PacketListener : public SX126xListener {
public:
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override {
this->packets.push_back(packet);
this->rssi = rssi;
this->snr = snr;
}
std::vector<std::vector<uint8_t>> packets;
float rssi{};
float snr{};
};
} // namespace esphome::sx126x::testing
+1 -2
View File
@@ -10,8 +10,7 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.auto_load = ["climate_ir", "climate"]
async def to_code_testing(config: ConfigType) -> None:
# Compiles in the Hisense ARG and Hitachi models, which climate.py adds only when configured
# Compiles in the Hisense ARG model, which climate.py adds only when it is configured
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
manifest.to_code = to_code_testing
-5
View File
@@ -8,8 +8,3 @@ climate:
model: HISENSE_ARG
transmitter_id: xmitr
receiver_id: rcvr
- platform: whirlpool
name: Whirlpool Hitachi HCRA31NEWH Climate
model: HITACHI_HCRA31NEWH
transmitter_id: xmitr
receiver_id: rcvr
@@ -252,9 +252,6 @@ TEST(WhirlpoolHisenseArg, SwingIsSentOnEveryTransmit) {
TEST(WhirlpoolDG11J13A, SwingIsSentOnlyWhenItChanges) {
Fixture f(MODEL_DG11J1_3A);
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_OFF);
EXPECT_EQ(f.last_frame()[2] & 0x80, 0) << "swing is already off";
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_VERTICAL);
Frame frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x80, 0x80);
@@ -264,9 +261,6 @@ TEST(WhirlpoolDG11J13A, SwingIsSentOnlyWhenItChanges) {
frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x80, 0);
EXPECT_EQ(frame[8], 0x00);
f.command(climate::CLIMATE_MODE_COOL, 25, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_VERTICAL);
EXPECT_EQ(f.last_frame()[2] & 0x80, 0) << "swing is already vertical";
}
// Cool 24 C (step 8), fan low, powered on assumed
@@ -391,90 +385,4 @@ TEST(WhirlpoolHisenseArg, EveryTransmitHasValidChecksums) {
}
}
// Cool 24 C (step 8), fan low, unit on
static Frame hitachi_cool_frame() {
return with_checksums({0xA9, 0x59, 0x03, 0x82, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x38, 0x08, 0x00});
}
// The off frame: power bit, cool code, byte 15 0x01 and byte 18 0x28
static Frame hitachi_off_frame() {
return with_checksums({0xA9, 0x59, 0x07, 0x82, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x28, 0x08, 0x00});
}
TEST(WhirlpoolHitachiHCRA31NEWH, CoolAndOffMatchKnownFrames) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW);
// Turning on sends the full state with no power bit
expect_frame(f.last_frame(), hitachi_cool_frame());
f.command(climate::CLIMATE_MODE_OFF, 24, climate::CLIMATE_FAN_LOW);
expect_frame(f.last_frame(), hitachi_off_frame());
// A second off command still sends the power bit
f.command(climate::CLIMATE_MODE_OFF, 24, climate::CLIMATE_FAN_LOW);
EXPECT_EQ(f.last_frame()[2] & 0x04, 0x04);
f.command(climate::CLIMATE_MODE_HEAT, 24, climate::CLIMATE_FAN_LOW);
const Frame frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x04, 0);
EXPECT_EQ(frame[3], 0x80);
EXPECT_EQ(frame[15], 0x06);
EXPECT_EQ(frame[18], 0x38);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceivePowerIsAState) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
ASSERT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
// An on frame restores the mode even though it has no power bit
EXPECT_TRUE(receive(f.climate, hitachi_cool_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_COOL);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 24);
EXPECT_EQ(f.climate.fan_mode, climate::CLIMATE_FAN_LOW);
// The same frame again keeps it on
EXPECT_TRUE(receive(f.climate, hitachi_cool_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_COOL);
EXPECT_TRUE(receive(f.climate, hitachi_off_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
// A repeated off frame does not toggle
EXPECT_TRUE(receive(f.climate, hitachi_off_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceiveChecksRemoteId) {
Fixture hitachi(MODEL_HITACHI_HCRA31NEWH);
EXPECT_FALSE(receive(hitachi.climate, hisense_cool_frame()));
Fixture whirlpool(MODEL_DG11J1_91);
EXPECT_FALSE(receive(whirlpool.climate, hitachi_cool_frame()));
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceivedTemperatureIsClamped) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
Frame frame = hitachi_cool_frame();
frame[3] = 0xF2; // step 15 would be 31 C
EXPECT_TRUE(receive(f.climate, with_checksums(frame)));
EXPECT_FLOAT_EQ(f.climate.target_temperature, 30);
}
TEST(WhirlpoolDG11J13A, TargetIsClampedAndRoundedToWhatWasSent) {
Fixture f(MODEL_DG11J1_3A);
f.command(climate::CLIMATE_MODE_COOL, 35, climate::CLIMATE_FAN_LOW);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 32);
f.command(climate::CLIMATE_MODE_COOL, 24.4f, climate::CLIMATE_FAN_LOW);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 24);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceiveRejectsShortFrame) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
remote_base::RawTimings raw = waveform(hitachi_cool_frame());
// Cut after the second divider: 14 bytes, no footer
raw.resize(2 + 14 * 16 + 2 * 2);
remote_base::RemoteReceiveData data(raw, 25, remote_base::TOLERANCE_MODE_PERCENTAGE);
EXPECT_FALSE(static_cast<remote_base::RemoteReceiverListener &>(f.climate).on_receive(data));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
}
} // namespace esphome::whirlpool::testing
@@ -1,23 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/tests"]
MULTI_CONF = True
reboot_hook_recorder_ns = cg.esphome_ns.namespace("reboot_hook_recorder")
RebootHookRecorder = reboot_hook_recorder_ns.class_("RebootHookRecorder", cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(RebootHookRecorder),
cv.Required(CONF_NAME): cv.string,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_name(config[CONF_NAME]))
@@ -1,10 +0,0 @@
#include "reboot_hook_recorder.h"
#include "esphome/core/log.h"
namespace esphome::reboot_hook_recorder {
ESPHOME_LOG_TAG(TAG, "reboot_hook_recorder");
void RebootHookRecorder::record_(const char *hook) { ESP_LOGI(TAG, "HOOK %s %s", this->name_, hook); }
} // namespace esphome::reboot_hook_recorder
@@ -1,33 +0,0 @@
#pragma once
#include "esphome/core/component.h"
namespace esphome::reboot_hook_recorder {
/// Logs every shutdown lifecycle hook it receives, so a test can check their order.
class RebootHookRecorder : public Component {
public:
void set_name(const char *name) { this->name_ = name; }
void on_safe_shutdown() override { this->record_("on_safe_shutdown"); }
void on_shutdown() override { this->record_("on_shutdown"); }
// Needs a second call to finish, like a component waiting for a connection to close
bool teardown() override {
if (++this->teardown_calls_ < 2) {
this->record_("teardown_pending");
return false;
}
if (this->teardown_calls_ == 2)
this->record_("teardown_done");
return true;
}
void on_powerdown() override { this->record_("on_powerdown"); }
protected:
void record_(const char *hook);
const char *name_{""};
uint8_t teardown_calls_{0};
};
} // namespace esphome::reboot_hook_recorder
@@ -1,19 +0,0 @@
esphome:
name: reboot-hooks-forced
host:
logger:
level: DEBUG
api:
# Reboot once the test has connected, so that it sees every hook
on_client_connected:
- delay: 100ms
- lambda: App.reboot();
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
reboot_hook_recorder:
- name: first
- name: second
@@ -1,19 +0,0 @@
esphome:
name: reboot-hooks-safe
host:
logger:
level: DEBUG
api:
# Reboot once the test has connected, so that it sees every hook
on_client_connected:
- delay: 100ms
- lambda: App.safe_reboot();
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
reboot_hook_recorder:
- name: first
- name: second
@@ -1,80 +0,0 @@
"""Test the order of the shutdown lifecycle hooks on a forced and on a safe reboot."""
import re
import pytest
from .log_utils import LineWaiter
from .types import APIClientConnectedFactory, RunCompiledFunction
from .wait_utils import wait_or_fail
HOOK_PATTERN = re.compile(r"HOOK (\w+) (\w+)")
# Hooks run in reverse component order, so "first" gets each hook last.
LAST_HOOK = "HOOK first on_powerdown"
async def _record_reboot_hooks(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> list[tuple[str, str]]:
"""Connect, which makes the device reboot, and return its (component, hook) calls in order."""
waiter = LineWaiter()
hooks: list[tuple[str, str]] = []
def on_line(line: str) -> None:
if match := HOOK_PATTERN.search(line):
hooks.append((match[1], match[2]))
waiter.callback(line)
async with (
run_compiled(yaml_config, line_callback=on_line),
api_client_connected(),
):
await wait_or_fail(
waiter.wait_for(LAST_HOOK),
5.0,
"Device did not run the powerdown hooks",
hooks=hooks,
)
return hooks
@pytest.mark.asyncio
async def test_reboot_hook_order_forced(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A forced reboot runs on_shutdown then on_powerdown, with no safe shutdown or teardown."""
hooks = await _record_reboot_hooks(yaml_config, run_compiled, api_client_connected)
assert hooks == [
("second", "on_shutdown"),
("first", "on_shutdown"),
("second", "on_powerdown"),
("first", "on_powerdown"),
]
@pytest.mark.asyncio
async def test_reboot_hook_order_safe(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A safe reboot powers down only after every component has finished its teardown."""
hooks = await _record_reboot_hooks(yaml_config, run_compiled, api_client_connected)
assert hooks == [
("second", "on_safe_shutdown"),
("first", "on_safe_shutdown"),
("second", "on_shutdown"),
("first", "on_shutdown"),
("second", "teardown_pending"),
("first", "teardown_pending"),
("second", "teardown_done"),
("first", "teardown_done"),
("second", "on_powerdown"),
("first", "on_powerdown"),
]
+3 -22
View File
@@ -1,34 +1,15 @@
"""Tests for the memory analyzer."""
import subprocess
from unittest.mock import patch
from esphome.analyze_memory import MemoryAnalyzer
def make_analyzer[T: MemoryAnalyzer](
external_components: set[str] | None = None, cls: type[T] = MemoryAnalyzer
) -> T:
"""Create an analyzer through its real constructor, without an ELF or a toolchain."""
def make_analyzer(external_components: set[str] | None = None) -> MemoryAnalyzer:
"""Create a MemoryAnalyzer through its real constructor, without an ELF or a toolchain."""
with (
patch("esphome.analyze_memory.resolve_tool_path", return_value=None),
patch("esphome.analyze_memory.find_tool", return_value="nm"),
):
# Any existing file passes the ELF check; nothing reads it
return cls(__file__, external_components=external_components)
def completed(stdout: str) -> subprocess.CompletedProcess[str]:
"""A successful tool run printing ``stdout``."""
return subprocess.CompletedProcess([], 0, stdout=stdout)
def hex_dump(section: str, data: bytes, address: int = 0) -> str:
"""Format bytes the way ``readelf -x`` does."""
lines = [f"Hex dump of section '{section}':"]
for start in range(0, len(data), 16):
chunk = data[start : start + 16]
words = " ".join(chunk[i : i + 4].hex() for i in range(0, len(chunk), 4))
text = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
lines.append(f" 0x{address + start:08x} {words:<35} {text}")
return "\n".join(lines) + "\n\n"
return MemoryAnalyzer(__file__, external_components=external_components)
@@ -1,7 +1,5 @@
"""Tests for source-file-to-component attribution in memory analyzer."""
import pytest
from . import make_analyzer
@@ -32,19 +30,3 @@ def test_source_file_to_component_known_component() -> None:
"src/esphome/components/wifi/wifi_component.cpp.o"
)
assert result == "[esphome]wifi"
@pytest.mark.parametrize(
("rel_path", "component"),
[
(
"lib/esp32async__ESPAsyncWebServer/WebHandlers.cpp.o",
"[lib]espasyncwebserver",
),
("lib/ESP8266WiFi/ESP8266WiFi.cpp.o", "[lib]esp8266wifi"),
("core/core_esp8266_main.cpp.o", "arduino_core"),
],
)
def test_native_library_objects(rel_path: str, component: str) -> None:
"""Objects of the native ESP8266 build's libraries and core get a component."""
assert make_analyzer()._source_file_to_component(rel_path) == component
@@ -1,235 +0,0 @@
"""Tests for attributing ESP8266 string literals in RAM to components."""
from pathlib import Path
import re
import subprocess
from unittest.mock import patch
import pytest
from esphome.analyze_memory import (
MemorySection,
StringLiteral,
_parse_string_sections,
_ram_string_size,
)
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from . import completed, hex_dump, make_analyzer
WEB_OBJ = "src/esphome/components/web_server/web_server.cpp.o"
LIGHT_OBJ = "src/esphome/components/light/light_json_schema.cpp.o"
COVER_JSON = "_ZN7esphome10web_server9WebServer11cover_json_Ev"
MATCH_URL = "_ZN7esphome10web_serverL9match_urlEPKcjbb"
DUMP_JSON = "_ZN7esphome5light15LightJSONSchema9dump_jsonEv"
_SECTIONS = """\
File: {web}
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 7] .rodata PROGBITS 00000000 000040 000008 00 A 0 0 4
[209] .rodata.str1.1 PROGBITS 00000000 000598 00000a 01 AMS 0 0 1
[481] .rodata.{cover_json}.str1.1 PROGBITS 00000000 003910 00000c 01 AMS 0 0 1
File: {light}
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 30] .rodata.{dump_json}.str1.1 PROGBITS 00000000 000100 000007 01 AMS 0 0 1
"""
def _sections(web: str, light: str) -> str:
"""``readelf -SW`` output for the two objects."""
return _SECTIONS.format(
web=web, light=light, cover_json=COVER_JSON, dump_json=DUMP_JSON
)
# "OPEN\0CLOSED\0" for cover_json_, "ON\0all\0none\0" shared by match_url
WEB_STRINGS = (
f"""\
Relocation section '.rela.text.{MATCH_URL}' at offset 0x100 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000004 00000501 R_XTENSA_32 00000000 .rodata.str1.1 + 0
"""
+ hex_dump(".rodata.str1.1", b"ON\0all\0none\0")
+ hex_dump(f".rodata.{COVER_JSON}.str1.1", b"OPEN\0CLOSED\0")
)
# The literal pool word of match_url holds the literal's offset, 3 ("all")
WEB_REFERRERS = hex_dump(f".text.{MATCH_URL}", bytes(4) + (3).to_bytes(4, "little"))
LIGHT_STRINGS = hex_dump(f".rodata.{DUMP_JSON}.str1.1", b"ON\0OFF\0")
# Linked .rodata: "OPEN" and "ON" were dropped
ELF_RODATA = hex_dump(".rodata", b"OFF\0CLOSED\0all\0", 0x3FFE86B0)
def test_parse_string_sections() -> None:
"""Only string literal sections are listed, named after their function or not."""
assert _parse_string_sections(_sections("web.o", "light.o")) == {
"web.o": {
"209": (".rodata.str1.1", None),
"481": (f".rodata.{COVER_JSON}.str1.1", COVER_JSON),
},
"light.o": {"30": (f".rodata.{DUMP_JSON}.str1.1", DUMP_JSON)},
}
@pytest.mark.parametrize(
("text", "expected"),
[
(b"OFF", 4),
(b"MON", 4),
# Stored only as the tail of "MON"
(b"ON", 0),
(b"missing", 0),
# After a float constant ending in 0x3f, not inside another literal
(b"all", 4),
],
)
def test_ram_string_size(text: bytes, expected: int) -> None:
"""A literal stored only as the tail of a longer one, or dropped, takes no RAM."""
rodata = b"MON\0OFF\0CLOSED\0\x00\x00\x80\x3fall\0"
literals = {b"MON", b"OFF", b"ON", b"CLOSED", b"all", b"missing"}
assert _ram_string_size(text, rodata, literals) == expected
@pytest.fixture
def obj_dir(tmp_path: Path) -> Path:
for rel in (WEB_OBJ, LIGHT_OBJ, "src/main.cpp.o"):
path = tmp_path / "obj" / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.touch()
return tmp_path / "obj"
def _new_analyzer(obj_dir: Path, kept: set[str]) -> MemoryAnalyzerCLI:
analyzer = make_analyzer(cls=MemoryAnalyzerCLI)
analyzer.elf_path = obj_dir.parent / "firmware.elf"
analyzer.rodata_in_ram = True
analyzer.sections[".data"] = MemorySection(".data", total_size=1000)
analyzer._elf_symbol_names = kept
# Unnamed constants have their own tests
analyzer._attribute_unnamed_constants = lambda *_: None
return analyzer
def _analyze(obj_dir: Path, kept: set[str]) -> MemoryAnalyzerCLI:
analyzer = _new_analyzer(obj_dir, kept)
light = str(obj_dir / LIGHT_OBJ)
sections = _sections(str(obj_dir / WEB_OBJ), light)
def run_tool(cmd: list[str]) -> subprocess.CompletedProcess[str]:
if "-SW" in cmd:
return completed(sections)
if cmd[-1].endswith("firmware.elf"):
return completed(ELF_RODATA)
if cmd[-1] == light:
return completed(LIGHT_STRINGS)
if "-rW" not in cmd:
assert cmd[1:-1] == ["-x", f".text.{MATCH_URL}"]
return completed(WEB_REFERRERS)
return completed(WEB_STRINGS)
with patch("esphome.analyze_memory.run_tool", side_effect=run_tool):
analyzer._analyze_object_rodata()
return analyzer
def test_string_literals_attributed(obj_dir: Path) -> None:
"""Each literal is counted once, with the bytes it takes in the linked .rodata."""
analyzer = _analyze(obj_dir, {COVER_JSON, MATCH_URL, DUMP_JSON})
assert analyzer._string_literals == [
StringLiteral("CLOSED", 7, {"[esphome]web_server": {COVER_JSON}}),
StringLiteral("OFF", 4, {"[esphome]light": {DUMP_JSON}}),
StringLiteral("all", 4, {"[esphome]web_server": {MATCH_URL}}),
]
# "OPEN" and "ON" are not in the ELF
assert analyzer.components["[esphome]web_server"].data_size == 11
assert analyzer.components["[esphome]light"].data_size == 4
assert analyzer.sections[".data"].symbol_size == 15
def test_string_literals_skip_dropped_functions(obj_dir: Path) -> None:
"""Literals of functions the linker dropped are not charged."""
analyzer = _analyze(obj_dir, {DUMP_JSON})
assert [s.text for s in analyzer._string_literals] == ["OFF"]
def test_string_literals_shared_between_components(obj_dir: Path) -> None:
"""A literal two components use is listed under both and charged to neither."""
analyzer = _new_analyzer(obj_dir, {COVER_JSON, DUMP_JSON})
with (
patch.object(
analyzer,
"_scan_object_strings",
side_effect=[{b"OFF": {COVER_JSON}}, {b"OFF": {DUMP_JSON}}],
),
patch("esphome.analyze_memory.run_tool", return_value=completed(ELF_RODATA)),
):
analyzer._attribute_string_literals(
_sections(str(obj_dir / WEB_OBJ), str(obj_dir / LIGHT_OBJ)), obj_dir
)
(literal,) = analyzer._string_literals
assert literal.users == {
"[esphome]web_server": {COVER_JSON},
"[esphome]light": {DUMP_JSON},
}
assert analyzer.sections[".data"].symbol_size == 0
analyzer._demangle_cache = {COVER_JSON: "cover_json_()", DUMP_JSON: "dump_json()"}
report = analyzer.generate_report()
# Each component's row names its own user
assert re.search(r'"OFF" +cover_json_\(\)', report)
assert re.search(r'"OFF" +dump_json\(\)', report)
def test_report_groups_by_component() -> None:
"""Strings are listed by component with the function that uses them."""
analyzer = make_analyzer(cls=MemoryAnalyzerCLI)
analyzer._demangle_cache = {
"setup": "setup",
COVER_JSON: "WebServer::cover_json_()",
}
analyzer._string_literals = [
StringLiteral("hunter2", 8, {"[esphome]core": {"setup"}}),
StringLiteral("CLOSED", 7, {"[esphome]web_server": {COVER_JSON}}),
]
report = analyzer.generate_report()
assert "String Literals in RAM (.rodata)" in report
assert "[esphome]core (8 B, 1 strings):" in report
assert '"hunter2"' in report
assert '"CLOSED"' in report
assert "WebServer::cover_json_()" in report
def test_string_literals_through_unnamed_table() -> None:
"""Literals reached through an unnamed pointer table go to the table's users."""
analyzer = make_analyzer()
relocations = """\
Relocation section '.rela.text.setup' at offset 0x100 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003a01 R_XTENSA_32 00000000 .rodata + 0
Relocation section '.rela.rodata' at offset 0xe6fc contains 2 entries:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00000501 R_XTENSA_32 00000000 .rodata.str1.1 + 0
00000004 00000501 R_XTENSA_32 00000000 .rodata.str1.1 + 0
""" + hex_dump(".rodata.str1.1", b"0.pool.ntp.org\0001.pool.ntp.org\0")
table = hex_dump(".rodata", (0).to_bytes(4, "little") + (15).to_bytes(4, "little"))
def run_tool(cmd: list[str]) -> subprocess.CompletedProcess[str]:
if "-rW" in cmd:
return completed(relocations)
assert cmd[1:-1] == ["-x", ".rodata"]
return completed(table)
with patch("esphome.analyze_memory.run_tool", side_effect=run_tool):
users = analyzer._scan_object_strings(
"main.cpp.o", {"5": (".rodata.str1.1", None)}
)
assert users == {b"0.pool.ntp.org": {"setup"}, b"1.pool.ntp.org": {"setup"}}
@@ -1,477 +0,0 @@
"""Tests for ESP8266 .rodata accounting and unnamed constant attribution."""
from collections.abc import Callable
from pathlib import Path
import subprocess
from unittest.mock import patch
import pytest
from esphome.analyze_memory import (
_CODE_SECTION_PATTERN,
MemoryAnalyzer,
MemorySection,
UnnamedConstant,
_FunctionDefinitions,
_parse_unnamed_rodata,
_RodataSection,
)
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from . import completed, hex_dump, make_analyzer
LIGHT_OBJ = "src/esphome/components/light/light_state.cpp.o"
WEB_OBJ = "src/esphome/components/web_server/web_server.cpp.o"
SETUP = "_ZN7esphome5light10LightState5setupEv"
SAVE = "_ZN7esphome5light10LightState19save_remote_values_Ev"
MATCH_URL = "_ZN7esphome10web_serverL9match_urlEPKcjbb"
# readelf -SW -sW over all objects; the web section index and size vary per test
HEADERS_AND_SYMBOLS = """\
File: {light}
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[124] .rodata._ZN7esphome5light10LightStateC2EPNS0_11LightOutputE.str1.1 PROGBITS 00000000 00067d 000001 01 AMS 0 0 1
[174] .rodata PROGBITS 00000000 00139c 00002c 00 A 0 0 4
[221] .rodata.str1.1 PROGBITS 00000000 001f39 000005 01 AMS 0 0 1
[225] .rodata.BUILTIN_LED PROGBITS 00000000 001f54 000004 00 A 0 0 4
Symbol table '.symtab' contains 348 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
300: 00000000 64 FUNC {bind:<6} DEFAULT 12 {setup}
301: 00000000 64 FUNC {bind:<6} DEFAULT 13 {save}
347: 00000000 4 OBJECT WEAK DEFAULT 225 BUILTIN_LED
File: {web}
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[{web_index:>3}] .rodata PROGBITS 00000000 000400 {web_size:06x} 00 A 0 0 4
Symbol table '.symtab' contains 1 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
{web_symbols}"""
LIGHT_OBJECT_DUMP = f"""\
Relocation section '.rela.text.{SETUP}' at offset 0x7928 contains 2 entries:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003a01 R_XTENSA_32 00000000 .rodata + 0
00000094 00003b14 R_XTENSA_SLOT0_OP 00000000 .text.{SETUP} + 82
Relocation section '.rela.text.{SAVE}' at offset 0x7d28 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003a01 R_XTENSA_32 00000000 .rodata + 0
Relocation section '.rela.xt.prop' at offset 0x8000 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
000007d4 00003a01 R_XTENSA_32 00000000 .rodata + 0
Hex dump of section '.rodata':
0x00000000 0000803f 0000803f 0000803f 0000803f ...?...?...?...?
0x00000010 0000803f 0000803f 0000803f 0000803f ...?...?...?...?
0x00000020 0000803f 00000000 00000000 ...?........
"""
WEB_OBJECT_DUMP = f"""\
Relocation section '.rela.text.{MATCH_URL}' at offset 0x100 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003a01 R_XTENSA_32 00000000 .rodata + 0
Hex dump of section '.rodata':
0x00000000 00000000 00000000 00000000 00000000 ................
"""
@pytest.fixture
def obj_dir(tmp_path: Path) -> Path:
"""Native ESP8266 build layout: objects under obj/, ELF next to it."""
for rel in (LIGHT_OBJ, WEB_OBJ, "src/main.cpp.o"):
path = tmp_path / "obj" / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.touch()
return tmp_path / "obj"
def _esp8266_analyzer(
obj_dir: Path,
kept: set[str],
web_index: int = 9,
web_size: int = 0x1C,
web_dump: str = WEB_OBJECT_DUMP,
bind: str = "GLOBAL",
web_symbols: str = "",
) -> tuple[MemoryAnalyzer, Callable[..., subprocess.CompletedProcess[str]]]:
analyzer = make_analyzer()
analyzer.elf_path = obj_dir.parent / "firmware.elf"
analyzer.rodata_in_ram = True
analyzer.sections[".data"] = MemorySection(".data", total_size=1000)
analyzer._elf_symbol_names = kept
# String literals have their own tests
analyzer._attribute_string_literals = lambda *_: None
light = str(obj_dir / LIGHT_OBJ)
web = str(obj_dir / WEB_OBJ)
def run_tool(cmd: list[str]) -> subprocess.CompletedProcess[str]:
if "-SW" in cmd:
return completed(
HEADERS_AND_SYMBOLS.format(
light=light,
web=web,
web_index=web_index,
web_size=web_size,
bind=bind,
setup=SETUP,
save=SAVE,
web_symbols=web_symbols,
)
)
# Only sections with uncovered bytes are dumped
if cmd[-1] == light:
assert cmd[1:-1] == ["-rW", "-x", "174"]
return completed(LIGHT_OBJECT_DUMP)
return completed(web_dump)
return analyzer, run_tool
def test_find_object_files_dir_native_layout(obj_dir: Path) -> None:
"""The native ESP8266 build keeps objects under obj/."""
analyzer = make_analyzer()
analyzer.elf_path = obj_dir.parent / "firmware.elf"
assert analyzer._find_object_files_dir() == obj_dir
def test_find_object_files_dir_platformio_layout(tmp_path: Path) -> None:
"""PlatformIO keeps objects next to the ELF."""
(tmp_path / "src").mkdir()
(tmp_path / "src" / "main.cpp.o").touch()
analyzer = make_analyzer()
analyzer.elf_path = tmp_path / "firmware.elf"
assert analyzer._find_object_files_dir() == tmp_path
def _parse_sections(code_section: str) -> MemoryAnalyzer:
readelf = f"""\
[ 1] .data PROGBITS 3ffe8000 001000 000668 00 WAX 0 0 16
[ 4] {code_section} PROGBITS 40201010 00b010 06612c 00 WAX 0 0 16
[ 6] .rodata PROGBITS 3ffe86b0 0016b0 000b90 00 A 0 0 16
"""
analyzer = make_analyzer()
with patch(
"esphome.analyze_memory.subprocess.run", return_value=completed(readelf)
):
analyzer._parse_sections()
return analyzer
def test_parse_sections_esp8266_counts_rodata_as_data() -> None:
"""ESP8266 (.irom0.text) copies .rodata to RAM, so it is counted as .data."""
analyzer = _parse_sections(".irom0.text")
assert analyzer.rodata_in_ram
assert ".rodata" not in analyzer.sections
assert analyzer.sections[".data"].total_size == 0x668 + 0xB90
def test_parse_sections_other_platforms_keep_rodata() -> None:
"""Other platforms keep .rodata in flash."""
analyzer = _parse_sections(".flash.text")
assert not analyzer.rodata_in_ram
assert analyzer.sections[".rodata"].total_size == 0xB90
assert analyzer.sections[".data"].total_size == 0x668
def test_unnamed_constants_attributed_to_component(obj_dir: Path) -> None:
"""Unnamed .rodata in a kept function is listed and counted as the component's RAM."""
analyzer, run_tool = _esp8266_analyzer(obj_dir, {SETUP, MATCH_URL})
with patch("esphome.analyze_memory.run_tool", side_effect=run_tool):
analyzer._analyze_object_rodata()
light, web = analyzer._unnamed_constants
assert light == UnnamedConstant(
size=44,
source_file=LIGHT_OBJ,
component="[esphome]light",
functions=[SAVE, SETUP],
jump_table=False,
data=bytes.fromhex("0000803f") * 4,
)
assert (web.size, web.component, web.functions) == (
28,
"[esphome]web_server",
[MATCH_URL],
)
assert analyzer.components["[esphome]light"].data_size == 44
assert analyzer.components["[esphome]web_server"].ram_total == 28
# Attributed bytes no longer count as unattributed RAM
assert analyzer.sections[".data"].symbol_size == 72
def test_unnamed_constants_skip_garbage_collected(obj_dir: Path) -> None:
"""A constant whose functions the linker dropped is not in the firmware."""
analyzer, run_tool = _esp8266_analyzer(obj_dir, {MATCH_URL})
with patch("esphome.analyze_memory.run_tool", side_effect=run_tool):
analyzer._analyze_object_rodata()
assert [c.component for c in analyzer._unnamed_constants] == ["[esphome]web_server"]
def test_unnamed_constants_skipped_when_rodata_in_flash(obj_dir: Path) -> None:
"""Other platforms keep .rodata in flash, so there is nothing to scan."""
analyzer, run_tool = _esp8266_analyzer(obj_dir, {SETUP, MATCH_URL})
analyzer.rodata_in_ram = False
with patch("esphome.analyze_memory.run_tool", side_effect=run_tool) as mock:
analyzer._analyze_object_rodata()
mock.assert_not_called()
assert analyzer._unnamed_constants == []
@pytest.mark.parametrize(("bind", "expected"), [("WEAK", 1), ("GLOBAL", 2)])
def test_unnamed_constants_inline_copies_counted_once(
obj_dir: Path, bind: str, expected: int
) -> None:
"""Only weak (inline) functions bring the same constant from several objects."""
# The second object emits the same functions, with the same binding
analyzer, run_tool = _esp8266_analyzer(
obj_dir,
{SETUP},
web_index=174,
web_size=0x2C,
web_dump=LIGHT_OBJECT_DUMP,
bind=bind,
web_symbols=(
f" 300: 00000000 64 FUNC {bind:<6} DEFAULT 12 {SETUP}\n"
f" 301: 00000000 64 FUNC {bind:<6} DEFAULT 13 {SAVE}\n"
),
)
with patch("esphome.analyze_memory.run_tool", side_effect=run_tool):
analyzer._analyze_object_rodata()
assert len(analyzer._unnamed_constants) == expected
assert analyzer.sections[".data"].symbol_size == 44 * expected
def test_parse_symbols_keeps_alias_names() -> None:
"""C1/C2 constructor aliases share an address; both names are in the ELF."""
objdump = """4020b558 g F .irom0.text\t0000007e _ZN7esphome3FooC1Ev
4020b558 g F .irom0.text\t0000007e _ZN7esphome3FooC2Ev
"""
analyzer = make_analyzer()
analyzer.sections[".text"] = MemorySection(".text")
with patch(
"esphome.analyze_memory.subprocess.run", return_value=completed(objdump)
):
analyzer._parse_symbols()
assert {"_ZN7esphome3FooC1Ev", "_ZN7esphome3FooC2Ev"} <= analyzer._elf_symbol_names
assert analyzer.sections[".text"].symbol_size == 0x7E
def test_unnamed_constants_skip_named_bytes() -> None:
"""In a section shared with a named object, only the unnamed bytes are shown."""
analyzer = make_analyzer()
analyzer._elf_symbol_names = {"_Z3fooi"}
dump = """Relocation section '.rela.text._Z3fooi' at offset 0x100 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003a01 R_XTENSA_32 00000000 .rodata + 4
Hex dump of section '.rodata':
0x00000000 aaaaaaaa 01020304 ........
"""
section = _RodataSection(".rodata", 8, covered=[(0, 4)])
with patch("esphome.analyze_memory.run_tool", return_value=completed(dump)):
(constant,) = analyzer._scan_object_constants(
"foo.o", {"5": section}, "src/foo.o", "[esphome]foo"
)
assert constant.size == 4
assert constant.data == bytes([1, 2, 3, 4])
def test_unnamed_constants_jump_table() -> None:
"""A .rodata section that points into code is a switch jump table."""
analyzer = make_analyzer()
analyzer._elf_symbol_names = {"_Z3fooi"}
dump = """\
Relocation section '.rela.text._Z3fooi' at offset 0x100 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003a01 R_XTENSA_32 00000000 .rodata + 0
Relocation section '.rela.rodata' at offset 0x200 contains 2 entries:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003b01 R_XTENSA_32 00000000 .text._Z3fooi + 12
00000004 00003b01 R_XTENSA_32 00000000 .text._Z3fooi + 20
"""
with patch("esphome.analyze_memory.run_tool", return_value=completed(dump)):
(constant,) = analyzer._scan_object_constants(
"foo.o", {"5": _RodataSection(".rodata", 8)}, "src/foo.o", "[esphome]foo"
)
assert constant.jump_table
assert constant.functions == ["_Z3fooi"]
def test_report_lists_unnamed_constants() -> None:
"""The report names the component, the object, the functions and the bytes."""
analyzer = make_analyzer(cls=MemoryAnalyzerCLI)
analyzer.rodata_in_ram = True
analyzer._demangle_cache = {SETUP: "esphome::light::LightState::setup()"}
analyzer._unnamed_constants = [
UnnamedConstant(
size=44,
source_file=LIGHT_OBJ,
component="[esphome]light",
functions=[SETUP],
jump_table=False,
data=bytes.fromhex("0000803f") * 4,
)
]
report = analyzer.generate_report()
assert "ESP8266 keeps .rodata in RAM" in report
assert "Unnamed Constants in RAM (.rodata)" in report
assert f"44 B constant [esphome]light ({LIGHT_OBJ})" in report
assert "used by esphome::light::LightState::setup()" in report
assert "bytes: 00 00 80 3f 00 00 80 3f 00 00 80 3f 00 00 80 3f ..." in report
def test_unnamed_constants_resolve_string_pointers() -> None:
"""A constant of pointers to literals lists the strings, so it can be recognized."""
analyzer = make_analyzer()
analyzer._elf_symbol_names = {"setup"}
relocations = """\
Relocation section '.rela.text.setup' at offset 0x100 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003a01 R_XTENSA_32 00000000 .rodata + 0
Relocation section '.rela.rodata' at offset 0xe6fc contains 3 entries:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00000501 R_XTENSA_32 00000000 .rodata.str1.1 + 0
00000004 00000501 R_XTENSA_32 00000000 .rodata.str1.1 + 0
00000008 00000501 R_XTENSA_32 00000000 .rodata.str1.1 + 0
Hex dump of section '.rodata':
0x00000000 00000000 0f000000 1e000000 ............
"""
literals = hex_dump(
".rodata.str1.1", b"0.pool.ntp.org\0001.pool.ntp.org\0002.pool.ntp.org\0"
)
def run_tool(cmd: list[str]) -> subprocess.CompletedProcess[str]:
if "-rW" not in cmd:
assert cmd[1:-1] == ["-x", ".rodata.str1.1"]
return completed(literals)
return completed(relocations)
with patch("esphome.analyze_memory.run_tool", side_effect=run_tool):
(constant,) = analyzer._scan_object_constants(
"main.cpp.o",
{"5": _RodataSection(".rodata", 12)},
"src/main.cpp.o",
"[esphome]core",
)
assert constant.strings == ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"]
cli = make_analyzer(cls=MemoryAnalyzerCLI)
cli._unnamed_constants = [constant]
assert (
'points to "0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"'
in cli.generate_report()
)
@pytest.mark.parametrize(
("section", "function"),
[
(".text._Z3foov", "_Z3foov"),
(".literal._Z3foov", "_Z3foov"),
(".irom0.text._Z3foov", "_Z3foov"),
(".text.startup._GLOBAL__sub_I_setup", "_GLOBAL__sub_I_setup"),
(".text.unlikely._Z3foov", "_Z3foov"),
(".iram.text._Z3foov", "_Z3foov"),
(".text.hot_path", "hot_path"),
(".text", None),
],
)
def test_code_section_function(section: str, function: str | None) -> None:
"""Placement prefixes and IRAM code still resolve to the function."""
match = _CODE_SECTION_PATTERN.match(section)
assert match is not None
assert match.group(1) == function
def test_unnamed_constants_same_object_not_merged(obj_dir: Path) -> None:
"""Two equal constants of one inline function in one object are both counted."""
analyzer, run_tool = _esp8266_analyzer(obj_dir, {SETUP}, bind="WEAK")
constant = UnnamedConstant(
size=8,
source_file=LIGHT_OBJ,
component="[esphome]light",
functions=[SAVE, SETUP],
jump_table=False,
data=b"",
)
light = str(obj_dir / LIGHT_OBJ)
with (
patch("esphome.analyze_memory.run_tool", side_effect=run_tool),
patch.object(
analyzer,
"_scan_object_constants",
side_effect=lambda path, *_: [constant, constant] if path == light else [],
),
):
analyzer._analyze_object_rodata()
assert len(analyzer._unnamed_constants) == 2
def test_unnamed_constants_jump_table_only_from_unnamed_bytes() -> None:
"""Code pointers in a named table at the start do not make the rest a jump table."""
analyzer = make_analyzer()
analyzer._elf_symbol_names = {"_Z3fooi"}
dump = """\
Relocation section '.rela.text._Z3fooi' at offset 0x100 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003a01 R_XTENSA_32 00000000 .rodata + 4
Relocation section '.rela.rodata' at offset 0x200 contains 1 entry:
Offset Info Type Sym. Value Symbol's Name + Addend
00000000 00003b01 R_XTENSA_32 00000000 .text._Z3fooi + 12
"""
section = _RodataSection(".rodata", 8, covered=[(0, 4)])
with patch("esphome.analyze_memory.run_tool", return_value=completed(dump)):
(constant,) = analyzer._scan_object_constants(
"foo.o", {"5": section}, "src/foo.o", "[esphome]foo"
)
assert not constant.jump_table
def _definitions(rows: dict[str, list[tuple[str, str]]]) -> _FunctionDefinitions:
"""Parse symbol rows (binding, function) per object, as readelf -sW prints them."""
output = "".join(
f"File: {path}\n"
+ "".join(
f" {i}: 00000000 64 FUNC {bind:<6} DEFAULT 12 {name}\n"
for i, (bind, name) in enumerate(symbols)
)
for path, symbols in rows.items()
)
return _parse_unnamed_rodata(output)[1]
def test_linked_copy_unknown_weak_overridden_by_strong() -> None:
"""A strong definition replaces every weak copy, so their constants are not linked."""
definitions = _definitions(
{"a.o": [("WEAK", "_Z3foov")], "b.o": [("GLOBAL", "_Z3foov")]}
)
assert definitions.linked_copy_unknown("a.o", ["_Z3foov"])
assert not definitions.linked_copy_unknown("b.o", ["_Z3foov"])
def test_linked_copy_unknown_same_named_locals() -> None:
"""Local functions with one name in two objects can't be told apart in the ELF."""
definitions = _definitions(
{
"a.o": [("LOCAL", "_ZL3barv"), ("LOCAL", "_ZL3bazv")],
"b.o": [("LOCAL", "_ZL3barv")],
}
)
assert definitions.linked_copy_unknown("a.o", ["_ZL3barv"])
# Another, unique user decides it
assert not definitions.linked_copy_unknown("a.o", ["_ZL3barv", "_ZL3bazv"])