mirror of
https://github.com/esphome/esphome.git
synced 2026-02-09 01:01:56 +00:00
Compare commits
5 Commits
cswitch_sd
...
voice-assi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7630075ff | ||
|
|
8677f3db03 | ||
|
|
9add30b900 | ||
|
|
86fee6e4af | ||
|
|
7b40e8afcb |
@@ -397,38 +397,47 @@ class MemoryAnalyzer:
|
||||
return pioenvs_dir
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_nm_cswtch_output(
|
||||
output: str,
|
||||
base_dir: Path | None,
|
||||
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||
) -> None:
|
||||
"""Parse nm output for CSWTCH symbols and add to cswtch_map.
|
||||
def _scan_cswtch_in_objects(
|
||||
self, obj_dir: Path
|
||||
) -> dict[str, list[tuple[str, int]]]:
|
||||
"""Scan object files for CSWTCH symbols using a single nm invocation.
|
||||
|
||||
Handles both ``.o`` files and ``.a`` archives.
|
||||
|
||||
nm output formats::
|
||||
|
||||
.o files: /path/file.o:hex_addr hex_size type name
|
||||
.a files: /path/lib.a:member.o:hex_addr hex_size type name
|
||||
|
||||
For ``.o`` files, paths are made relative to *base_dir* when possible.
|
||||
For ``.a`` archives (detected by ``:`` in the file portion), paths are
|
||||
formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``).
|
||||
Uses ``nm --print-file-name -S`` on all ``.o`` files at once.
|
||||
Output format: ``/path/to/file.o:address size type name``
|
||||
|
||||
Args:
|
||||
output: Raw stdout from ``nm --print-file-name -S``.
|
||||
base_dir: Base directory for computing relative paths of ``.o`` files.
|
||||
Pass ``None`` when scanning archives outside the build tree.
|
||||
cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list.
|
||||
obj_dir: Directory containing object files (.pioenvs/<env>/)
|
||||
|
||||
Returns:
|
||||
Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples.
|
||||
"""
|
||||
for line in output.splitlines():
|
||||
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
|
||||
if not self.nm_path:
|
||||
return cswtch_map
|
||||
|
||||
# Find all .o files recursively, sorted for deterministic output
|
||||
obj_files = sorted(obj_dir.rglob("*.o"))
|
||||
if not obj_files:
|
||||
return cswtch_map
|
||||
|
||||
_LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files))
|
||||
|
||||
# Single nm call with --print-file-name for all object files
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files],
|
||||
timeout=30,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
return cswtch_map
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if "CSWTCH$" not in line:
|
||||
continue
|
||||
|
||||
# Split on last ":" that precedes a hex address.
|
||||
# For .o: "filepath.o" : "hex_addr hex_size type name"
|
||||
# For .a: "filepath.a:member.o" : "hex_addr hex_size type name"
|
||||
# nm --print-file-name format: filepath:hex_addr hex_size type name
|
||||
# We split from the right: find the last colon followed by hex digits.
|
||||
parts_after_colon = line.rsplit(":", 1)
|
||||
if len(parts_after_colon) != 2:
|
||||
continue
|
||||
@@ -448,89 +457,16 @@ class MemoryAnalyzer:
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Determine readable source path
|
||||
# Use ".a:" to detect archive format (not bare ":" which matches
|
||||
# Windows drive letters like "C:\...\file.o").
|
||||
if ".a:" in file_path:
|
||||
# Archive format: "archive.a:member.o" → "archive_stem/member.o"
|
||||
archive_part, member = file_path.rsplit(":", 1)
|
||||
archive_name = Path(archive_part).stem
|
||||
rel_path = f"{archive_name}/{member}"
|
||||
elif base_dir is not None:
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(base_dir))
|
||||
except ValueError:
|
||||
rel_path = file_path
|
||||
else:
|
||||
# Get relative path from obj_dir for readability
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(obj_dir))
|
||||
except ValueError:
|
||||
rel_path = file_path
|
||||
|
||||
key = f"{sym_name}:{size}"
|
||||
cswtch_map[key].append((rel_path, size))
|
||||
|
||||
def _run_nm_cswtch_scan(
|
||||
self,
|
||||
files: list[Path],
|
||||
base_dir: Path | None,
|
||||
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||
) -> None:
|
||||
"""Run nm on *files* and add any CSWTCH symbols to *cswtch_map*.
|
||||
|
||||
Args:
|
||||
files: Object (``.o``) or archive (``.a``) files to scan.
|
||||
base_dir: Base directory for relative path computation (see
|
||||
:meth:`_parse_nm_cswtch_output`).
|
||||
cswtch_map: Dict to populate with results.
|
||||
"""
|
||||
if not self.nm_path or not files:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files))
|
||||
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files],
|
||||
timeout=30,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug(
|
||||
"nm failed or timed out scanning %d files for CSWTCH symbols",
|
||||
len(files),
|
||||
)
|
||||
return
|
||||
|
||||
self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map)
|
||||
|
||||
def _scan_cswtch_in_sdk_archives(
|
||||
self, cswtch_map: dict[str, list[tuple[str, int]]]
|
||||
) -> None:
|
||||
"""Scan SDK library archives (.a) for CSWTCH symbols.
|
||||
|
||||
Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source,
|
||||
so their CSWTCH symbols only exist inside ``.a`` archives. Results are
|
||||
merged into *cswtch_map* for keys not already found in ``.o`` files.
|
||||
|
||||
The same source file (e.g. ``lwip-esp.o``) often appears in multiple
|
||||
library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.),
|
||||
so results are deduplicated by member name.
|
||||
"""
|
||||
sdk_dirs = self._find_sdk_library_dirs()
|
||||
if not sdk_dirs:
|
||||
return
|
||||
|
||||
sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a"))
|
||||
|
||||
sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
self._run_nm_cswtch_scan(sdk_archives, None, sdk_map)
|
||||
|
||||
# Merge SDK results, deduplicating by member name.
|
||||
for key, sources in sdk_map.items():
|
||||
if key in cswtch_map:
|
||||
continue
|
||||
seen: dict[str, tuple[str, int]] = {}
|
||||
for path, sz in sources:
|
||||
member = Path(path).name
|
||||
if member not in seen:
|
||||
seen[member] = (path, sz)
|
||||
cswtch_map[key] = list(seen.values())
|
||||
return cswtch_map
|
||||
|
||||
def _source_file_to_component(self, source_file: str) -> str:
|
||||
"""Map a source object file path to its component name.
|
||||
@@ -569,25 +505,17 @@ class MemoryAnalyzer:
|
||||
|
||||
CSWTCH symbols are compiler-generated lookup tables for switch statements.
|
||||
They are local symbols, so the same name can appear in different object files.
|
||||
This method scans .o files and SDK archives to attribute them to their
|
||||
source components.
|
||||
This method scans .o files to attribute them to their source components.
|
||||
"""
|
||||
obj_dir = self._find_object_files_dir()
|
||||
if obj_dir is None:
|
||||
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
|
||||
return
|
||||
|
||||
# Scan build-dir object files for CSWTCH symbols
|
||||
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map)
|
||||
|
||||
# Also scan SDK library archives (.a) for CSWTCH symbols.
|
||||
# Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source
|
||||
# so their symbols only exist inside .a archives, not as loose .o files.
|
||||
self._scan_cswtch_in_sdk_archives(cswtch_map)
|
||||
|
||||
# Scan object files for CSWTCH symbols
|
||||
cswtch_map = self._scan_cswtch_in_objects(obj_dir)
|
||||
if not cswtch_map:
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files or SDK archives")
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files")
|
||||
return
|
||||
|
||||
# Collect CSWTCH symbols from the ELF (already parsed in sections)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
class EPaperSpectraE6 : public EPaperBase {
|
||||
class EPaperSpectraE6 final : public EPaperBase {
|
||||
public:
|
||||
EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::epaper_spi {
|
||||
/**
|
||||
* An epaper display that needs LUTs to be sent to it.
|
||||
*/
|
||||
class EpaperWaveshare : public EPaperMono {
|
||||
class EpaperWaveshare final : public EPaperMono {
|
||||
public:
|
||||
EpaperWaveshare(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length, const uint8_t *lut, size_t lut_length, const uint8_t *partial_lut,
|
||||
|
||||
@@ -371,7 +371,12 @@ async def to_code(config):
|
||||
if on_timer_tick := config.get(CONF_ON_TIMER_TICK):
|
||||
await automation.build_automation(
|
||||
var.get_timer_tick_trigger(),
|
||||
[(cg.std_vector.template(Timer), "timers")],
|
||||
[
|
||||
(
|
||||
cg.std_vector.template(Timer).operator("const").operator("ref"),
|
||||
"timers",
|
||||
)
|
||||
],
|
||||
on_timer_tick,
|
||||
)
|
||||
has_timers = True
|
||||
|
||||
@@ -859,35 +859,43 @@ void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
|
||||
}
|
||||
|
||||
void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) {
|
||||
Timer timer = {
|
||||
.id = msg.timer_id,
|
||||
.name = msg.name,
|
||||
.total_seconds = msg.total_seconds,
|
||||
.seconds_left = msg.seconds_left,
|
||||
.is_active = msg.is_active,
|
||||
};
|
||||
this->timers_[timer.id] = timer;
|
||||
// Find existing timer or add a new one
|
||||
auto it = this->timers_.begin();
|
||||
for (; it != this->timers_.end(); ++it) {
|
||||
if (it->id == msg.timer_id)
|
||||
break;
|
||||
}
|
||||
if (it == this->timers_.end()) {
|
||||
this->timers_.push_back({});
|
||||
it = this->timers_.end() - 1;
|
||||
}
|
||||
it->id = msg.timer_id;
|
||||
it->name = msg.name;
|
||||
it->total_seconds = msg.total_seconds;
|
||||
it->seconds_left = msg.seconds_left;
|
||||
it->is_active = msg.is_active;
|
||||
|
||||
char timer_buf[Timer::TO_STR_BUFFER_SIZE];
|
||||
ESP_LOGD(TAG,
|
||||
"Timer Event\n"
|
||||
" Type: %" PRId32 "\n"
|
||||
" %s",
|
||||
msg.event_type, timer.to_str(timer_buf));
|
||||
msg.event_type, it->to_str(timer_buf));
|
||||
|
||||
switch (msg.event_type) {
|
||||
case api::enums::VOICE_ASSISTANT_TIMER_STARTED:
|
||||
this->timer_started_trigger_.trigger(timer);
|
||||
this->timer_started_trigger_.trigger(*it);
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_TIMER_UPDATED:
|
||||
this->timer_updated_trigger_.trigger(timer);
|
||||
this->timer_updated_trigger_.trigger(*it);
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED:
|
||||
this->timer_cancelled_trigger_.trigger(timer);
|
||||
this->timers_.erase(timer.id);
|
||||
this->timer_cancelled_trigger_.trigger(*it);
|
||||
this->timers_.erase(it);
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_TIMER_FINISHED:
|
||||
this->timer_finished_trigger_.trigger(timer);
|
||||
this->timers_.erase(timer.id);
|
||||
this->timer_finished_trigger_.trigger(*it);
|
||||
this->timers_.erase(it);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -901,16 +909,12 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse
|
||||
}
|
||||
|
||||
void VoiceAssistant::timer_tick_() {
|
||||
std::vector<Timer> res;
|
||||
res.reserve(this->timers_.size());
|
||||
for (auto &pair : this->timers_) {
|
||||
auto &timer = pair.second;
|
||||
for (auto &timer : this->timers_) {
|
||||
if (timer.is_active && timer.seconds_left > 0) {
|
||||
timer.seconds_left--;
|
||||
}
|
||||
res.push_back(timer);
|
||||
}
|
||||
this->timer_tick_trigger_.trigger(res);
|
||||
this->timer_tick_trigger_.trigger(this->timers_);
|
||||
}
|
||||
|
||||
void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include "esphome/components/socket/socket.h"
|
||||
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
@@ -226,9 +225,9 @@ class VoiceAssistant : public Component {
|
||||
Trigger<Timer> *get_timer_updated_trigger() { return &this->timer_updated_trigger_; }
|
||||
Trigger<Timer> *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; }
|
||||
Trigger<Timer> *get_timer_finished_trigger() { return &this->timer_finished_trigger_; }
|
||||
Trigger<std::vector<Timer>> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; }
|
||||
Trigger<const std::vector<Timer> &> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; }
|
||||
void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; }
|
||||
const std::unordered_map<std::string, Timer> &get_timers() const { return this->timers_; }
|
||||
const std::vector<Timer> &get_timers() const { return this->timers_; }
|
||||
|
||||
protected:
|
||||
bool allocate_buffers_();
|
||||
@@ -267,13 +266,13 @@ class VoiceAssistant : public Component {
|
||||
|
||||
api::APIConnection *api_client_{nullptr};
|
||||
|
||||
std::unordered_map<std::string, Timer> timers_;
|
||||
std::vector<Timer> timers_;
|
||||
void timer_tick_();
|
||||
Trigger<Timer> timer_started_trigger_;
|
||||
Trigger<Timer> timer_finished_trigger_;
|
||||
Trigger<Timer> timer_updated_trigger_;
|
||||
Trigger<Timer> timer_cancelled_trigger_;
|
||||
Trigger<std::vector<Timer>> timer_tick_trigger_;
|
||||
Trigger<const std::vector<Timer> &> timer_tick_trigger_;
|
||||
bool has_timers_{false};
|
||||
bool timer_tick_running_{false};
|
||||
|
||||
|
||||
@@ -68,3 +68,24 @@ voice_assistant:
|
||||
- logger.log:
|
||||
format: "Voice assistant error - code %s, message: %s"
|
||||
args: [code.c_str(), message.c_str()]
|
||||
on_timer_started:
|
||||
- logger.log:
|
||||
format: "Timer started: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_updated:
|
||||
- logger.log:
|
||||
format: "Timer updated: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_cancelled:
|
||||
- logger.log:
|
||||
format: "Timer cancelled: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_finished:
|
||||
- logger.log:
|
||||
format: "Timer finished: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_tick:
|
||||
- lambda: |-
|
||||
for (auto &timer : timers) {
|
||||
ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left);
|
||||
}
|
||||
|
||||
@@ -58,3 +58,24 @@ voice_assistant:
|
||||
- logger.log:
|
||||
format: "Voice assistant error - code %s, message: %s"
|
||||
args: [code.c_str(), message.c_str()]
|
||||
on_timer_started:
|
||||
- logger.log:
|
||||
format: "Timer started: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_updated:
|
||||
- logger.log:
|
||||
format: "Timer updated: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_cancelled:
|
||||
- logger.log:
|
||||
format: "Timer cancelled: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_finished:
|
||||
- logger.log:
|
||||
format: "Timer finished: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_tick:
|
||||
- lambda: |-
|
||||
for (auto &timer : timers) {
|
||||
ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user