mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 14:38:52 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e426e33427 | ||
|
|
d5c6b01e19 | ||
|
|
57e14c1b54 | ||
|
|
11a228569b | ||
|
|
3b4deefd4c | ||
|
|
ddfcc0bc8b | ||
|
|
965b26dab6 | ||
|
|
8824610f82 |
No files matched your search
@@ -216,9 +216,10 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
|
||||
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
|
||||
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
|
||||
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
|
||||
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
|
||||
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
|
||||
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
|
||||
`if (!this->status_comm_result(ok)) return;` sets or clears the warning. A child sensor that skips repeated
|
||||
values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
|
||||
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
|
||||
end of `setup()` when the entities need a state at boot.
|
||||
|
||||
@@ -462,8 +462,6 @@ esphome/components/openthread/* @mrene
|
||||
esphome/components/opt3001/* @ccutrer
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_interface/* @clydebarrow
|
||||
esphome/components/packet_interface/packet_transport/* @clydebarrow
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9505/* @ajfriesen @ssieb
|
||||
@@ -684,12 +682,10 @@ esphome/components/tuya/water_heater/* @iago-veiga
|
||||
esphome/components/uart/* @esphome/core
|
||||
esphome/components/uart/button/* @ssieb
|
||||
esphome/components/uart/event/* @eoasmxd
|
||||
esphome/components/uart/packet_interface/* @clydebarrow
|
||||
esphome/components/uart/packet_transport/* @clydebarrow
|
||||
esphome/components/uart_mux/* @kbx81
|
||||
esphome/components/uart_tcp/* @Bascht74
|
||||
esphome/components/udp/* @clydebarrow
|
||||
esphome/components/udp/packet_interface/* @clydebarrow
|
||||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ufm01/* @ljungqvist
|
||||
|
||||
@@ -45,15 +45,14 @@ void HmacSHA256::get_hex(char *output) {
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_bytes(const uint8_t *expected) {
|
||||
return constant_time_equals(this->digest_, expected, SHA256_DIGEST_SIZE);
|
||||
return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0;
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_hex(const char *expected) {
|
||||
uint8_t parsed[SHA256_DIGEST_SIZE];
|
||||
if (parse_hex(expected, SHA256_DIGEST_SIZE * 2, parsed, SHA256_DIGEST_SIZE) != SHA256_DIGEST_SIZE * 2) {
|
||||
return false;
|
||||
}
|
||||
return this->equals_bytes(parsed);
|
||||
char hex_output[SHA256_DIGEST_SIZE * 2 + 1];
|
||||
this->get_hex(hex_output);
|
||||
hex_output[SHA256_DIGEST_SIZE * 2] = '\0';
|
||||
return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0;
|
||||
}
|
||||
|
||||
#elif defined(USE_HMAC_SHA256_MBEDTLS)
|
||||
@@ -78,15 +77,14 @@ void HmacSHA256::get_hex(char *output) {
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_bytes(const uint8_t *expected) {
|
||||
return constant_time_equals(this->digest_, expected, SHA256_DIGEST_SIZE);
|
||||
return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0;
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_hex(const char *expected) {
|
||||
uint8_t parsed[SHA256_DIGEST_SIZE];
|
||||
if (parse_hex(expected, SHA256_DIGEST_SIZE * 2, parsed, SHA256_DIGEST_SIZE) != SHA256_DIGEST_SIZE * 2) {
|
||||
return false;
|
||||
}
|
||||
return this->equals_bytes(parsed);
|
||||
char hex_output[SHA256_DIGEST_SIZE * 2 + 1];
|
||||
this->get_hex(hex_output);
|
||||
hex_output[SHA256_DIGEST_SIZE * 2] = '\0';
|
||||
return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "download_buffer.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::online_image {
|
||||
@@ -35,46 +34,6 @@ size_t DownloadBuffer::read(size_t len) {
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
void DownloadSource::compact() {
|
||||
if (this->keep_read_ || this->pos_ == this->start_)
|
||||
return;
|
||||
this->buffer_.read(this->pos_ - this->start_);
|
||||
this->start_ = this->pos_;
|
||||
}
|
||||
|
||||
int32_t DownloadSource::read(uint8_t *buf, uint32_t len) {
|
||||
const size_t offset = this->pos_ - this->start_;
|
||||
const size_t n = std::min<size_t>(len, this->buffer_.unread() - offset);
|
||||
memcpy(buf, this->buffer_.data(offset), n);
|
||||
this->pos_ += n;
|
||||
return static_cast<int32_t>(n);
|
||||
}
|
||||
|
||||
bool DownloadSource::seek(uint32_t position) {
|
||||
// Only what is still in the buffer can be read again; a decoder never skips ahead of the download
|
||||
if (position < this->start_ || position > this->start_ + this->buffer_.unread())
|
||||
return false;
|
||||
this->pos_ = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DownloadBuffer::grow(size_t size) {
|
||||
if (this->size_ >= size)
|
||||
return true;
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buffer = allocator.allocate(size);
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
|
||||
allocator.get_max_free_block_size());
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, this->buffer_, this->unread_);
|
||||
allocator.deallocate(this->buffer_, this->size_);
|
||||
this->buffer_ = buffer;
|
||||
this->size_ = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::resize(size_t size) {
|
||||
if (this->size_ >= size) {
|
||||
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -36,8 +35,6 @@ class DownloadBuffer {
|
||||
|
||||
void reset() { this->unread_ = 0; }
|
||||
size_t resize(size_t size);
|
||||
/// Make the buffer at least size bytes, keeping the unread data. Returns false if the allocation failed.
|
||||
bool grow(size_t size);
|
||||
|
||||
protected:
|
||||
uint8_t *buffer_;
|
||||
@@ -46,35 +43,4 @@ class DownloadBuffer {
|
||||
size_t unread_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lets a decoder read a download as it arrives, from the download buffer.
|
||||
*
|
||||
* The decoder reads at its own position in the buffer. Bytes it has read stay there until compact() drops
|
||||
* them, and compact() keeps everything while set_keep_read(true) is in effect, so the decoder can seek back
|
||||
* while it reads the headers.
|
||||
*/
|
||||
class DownloadSource : public runtime_image::DecodeSource {
|
||||
public:
|
||||
explicit DownloadSource(DownloadBuffer &buffer) : buffer_(buffer) {}
|
||||
|
||||
/// Start again at the beginning of a new download, keeping what is read
|
||||
void reset() {
|
||||
this->start_ = 0;
|
||||
this->pos_ = 0;
|
||||
this->keep_read_ = true;
|
||||
}
|
||||
void set_keep_read(bool keep) { this->keep_read_ = keep; }
|
||||
/// Drop the bytes the decoder has read, to make room for more of the download
|
||||
void compact();
|
||||
|
||||
int32_t read(uint8_t *buf, uint32_t len) override;
|
||||
bool seek(uint32_t position) override;
|
||||
|
||||
protected:
|
||||
DownloadBuffer &buffer_;
|
||||
size_t start_{0}; // Position in the download of the first byte in the buffer
|
||||
size_t pos_{0}; // Position in the download the decoder reads next
|
||||
bool keep_read_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::online_image
|
||||
@@ -22,8 +22,6 @@ DEPENDENCIES = ["http_request"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
||||
|
||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||
CONF_ON_DOWNLOAD_STARTED = "on_download_started"
|
||||
CONF_ON_DOWNLOAD_PROGRESS = "on_download_progress"
|
||||
CONF_UPDATE = "update"
|
||||
|
||||
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||
@@ -45,9 +43,7 @@ ONLINE_IMAGE_SCHEMA = (
|
||||
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
|
||||
cv.Schema({cv.string: cv.templatable(cv.string)})
|
||||
),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_STARTED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_PROGRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
@@ -100,17 +96,9 @@ automation.register_apply_action(
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_STARTED, "add_on_download_started_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_PROGRESS,
|
||||
"add_on_progress_callback",
|
||||
[(float, "progress"), (cg.size_t, "bytes_read")],
|
||||
),
|
||||
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "online_image");
|
||||
@@ -16,11 +15,6 @@ static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
|
||||
|
||||
namespace esphome::online_image {
|
||||
|
||||
// Time one loop pass may spend reading, and decoding a JPEG as it downloads
|
||||
static constexpr uint32_t PASS_BUDGET_MS = 20;
|
||||
// Shortest time between two on_download_progress calls
|
||||
static constexpr uint32_t PROGRESS_INTERVAL_MS = 100;
|
||||
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
|
||||
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
|
||||
uint32_t buffer_size, bool is_big_endian)
|
||||
@@ -146,12 +140,8 @@ void OnlineImage::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
// A JPEG of known size is decoded as it arrives, so the download buffer only has to hold its headers.
|
||||
// Otherwise the decoder needs the complete image in the buffer.
|
||||
this->streaming_ = total_size > 0 && total_size <= INT32_MAX && this->decoder_supports_source();
|
||||
this->stream_started_ = false;
|
||||
this->source_.reset();
|
||||
if (!this->streaming_ && format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
|
||||
// JPEG requires the complete image in the download buffer before decoding
|
||||
if (format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
|
||||
this->download_buffer_.resize(total_size) == 0) {
|
||||
// resize() logged the failed allocation; the next update() allocates the configured buffer again
|
||||
this->end_connection_();
|
||||
@@ -162,8 +152,6 @@ void OnlineImage::update() {
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||
this->start_time_ = millis();
|
||||
this->last_data_time_ = this->start_time_;
|
||||
this->download_started_callback_.call();
|
||||
this->report_progress_(true);
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
@@ -181,11 +169,6 @@ void OnlineImage::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->streaming_) {
|
||||
this->loop_stream_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if download is complete — use decoder's format-specific completion check
|
||||
// to handle both known content-length and chunked transfer encoding
|
||||
if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
|
||||
@@ -196,22 +179,15 @@ void OnlineImage::loop() {
|
||||
}
|
||||
|
||||
// Download and decode more data
|
||||
if (this->download_buffer_.free_capacity() > 0) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
size_t received = 0;
|
||||
int len;
|
||||
do {
|
||||
len = this->downloader_->read(this->download_buffer_.append(),
|
||||
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
|
||||
if (len <= 0)
|
||||
break;
|
||||
this->download_buffer_.write(len);
|
||||
received += len;
|
||||
} while (this->download_buffer_.free_capacity() != 0 && millis() - now < PASS_BUDGET_MS);
|
||||
size_t available = this->download_buffer_.free_capacity();
|
||||
if (available > 0) {
|
||||
// Download in chunks to avoid blocking
|
||||
available = std::min(available, this->download_buffer_initial_size_);
|
||||
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||
|
||||
if (received > 0) {
|
||||
this->last_data_time_ = now;
|
||||
this->report_progress_(false);
|
||||
if (len > 0) {
|
||||
this->last_data_time_ = App.get_loop_component_start_time();
|
||||
this->download_buffer_.write(len);
|
||||
|
||||
// Feed data to decoder
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
@@ -279,114 +255,6 @@ void OnlineImage::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::loop_stream_() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const size_t total = this->downloader_->content_length;
|
||||
if (this->stream_started_)
|
||||
this->source_.compact();
|
||||
|
||||
if (this->downloader_->get_bytes_read() < total) {
|
||||
if (this->download_buffer_.free_capacity() == 0) {
|
||||
// compact() already ran, so a full buffer means the decoder read nothing in the last pass
|
||||
if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
ESP_LOGE(TAG, "Image does not fit in the %zu byte download buffer; increase buffer_size",
|
||||
this->download_buffer_.size());
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
int len;
|
||||
do {
|
||||
len = this->downloader_->read(this->download_buffer_.append(),
|
||||
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
|
||||
if (len <= 0)
|
||||
break;
|
||||
this->last_data_time_ = now;
|
||||
this->download_buffer_.write(len);
|
||||
this->report_progress_(false);
|
||||
} while (this->downloader_->get_bytes_read() < total && this->download_buffer_.free_capacity() != 0 &&
|
||||
millis() - now < PASS_BUDGET_MS);
|
||||
if (len < 0) {
|
||||
ESP_LOGE(TAG, "Error downloading image: %d", len);
|
||||
this->fail_download_();
|
||||
return;
|
||||
} else if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool arrived = this->downloader_->get_bytes_read() >= total;
|
||||
|
||||
if (!this->stream_started_) {
|
||||
// The decoder reads the headers when it opens the image, so wait until they are in the buffer
|
||||
if (!arrived && this->download_buffer_.free_capacity() != 0)
|
||||
return;
|
||||
const int err = this->begin_source(&this->source_);
|
||||
if (err != 0) {
|
||||
// Headers cut short by a full buffer fail to parse, or make the decoder seek past the buffer. Such an image
|
||||
// still decodes once it is all in memory, as it did before JPEG images were decoded as they download.
|
||||
if (!arrived && (err == runtime_image::DECODE_ERROR_INVALID_TYPE || err == runtime_image::DECODE_ERROR_READ)) {
|
||||
ESP_LOGW(TAG,
|
||||
"The image headers do not fit the %zu byte download buffer, so the whole image is downloaded "
|
||||
"before it is decoded; increase buffer_size to decode it as it downloads",
|
||||
this->download_buffer_.size());
|
||||
// grow() logs a failed allocation
|
||||
if (this->download_buffer_.grow(total)) {
|
||||
this->streaming_ = false;
|
||||
} else {
|
||||
this->fail_download_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(err));
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
this->source_.set_keep_read(false);
|
||||
this->stream_started_ = true;
|
||||
// Opening the image and allocating its buffer is enough for one pass
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
const int result = this->decode_step();
|
||||
if (result == runtime_image::DECODE_STEP_MORE)
|
||||
continue;
|
||||
if (result == runtime_image::DECODE_STEP_DONE) {
|
||||
this->finish_download_();
|
||||
} else if (result == runtime_image::DECODE_STEP_NEED_DATA) {
|
||||
if (!arrived)
|
||||
return;
|
||||
// JPEGDEC only waits for bytes short of the size it was given, so this guards against a hang
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
this->fail_download_();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(result));
|
||||
this->fail_download_();
|
||||
}
|
||||
return;
|
||||
} while (millis() - now < PASS_BUDGET_MS);
|
||||
}
|
||||
|
||||
void OnlineImage::report_progress_(bool force) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const size_t bytes = this->downloader_->get_bytes_read();
|
||||
const size_t total = this->downloader_->content_length;
|
||||
// The last bytes are always reported, so a progress bar reaches 100%
|
||||
if (!force && now - this->last_progress_time_ < PROGRESS_INTERVAL_MS && (total == 0 || bytes < total))
|
||||
return;
|
||||
this->last_progress_time_ = now;
|
||||
const float progress = total > 0 ? std::min(100.0f, bytes * 100.0f / total) : NAN;
|
||||
this->download_progress_callback_.call(progress, bytes);
|
||||
}
|
||||
|
||||
void OnlineImage::fail_download_() {
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
}
|
||||
|
||||
void OnlineImage::finish_download_() {
|
||||
// Must be read before end_decode() ends the decoding session
|
||||
bool decoder_finished = this->is_decode_finished();
|
||||
@@ -420,8 +288,6 @@ void OnlineImage::end_connection_() {
|
||||
this->downloader_ = nullptr;
|
||||
}
|
||||
this->download_buffer_.reset();
|
||||
this->streaming_ = false;
|
||||
this->stream_started_ = false;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,6 @@ using t_http_codes = enum {
|
||||
HTTP_CODE_NOT_FOUND = 404,
|
||||
};
|
||||
|
||||
/// Largest download read(). A read() blocks until it has every byte it asked for, so this bounds how long one loop
|
||||
/// pass waits on a slow link.
|
||||
static constexpr size_t READ_CHUNK = 1460;
|
||||
|
||||
/**
|
||||
* @brief Download an image from a given URL, and decode it using the specified decoder.
|
||||
* The image will then be stored in a buffer, so that it can be re-displayed without the
|
||||
@@ -74,17 +70,9 @@ class OnlineImage final : public PollingComponent,
|
||||
*/
|
||||
void release();
|
||||
|
||||
/// Called when the server has accepted the request and the image starts to download
|
||||
template<typename F> void add_on_download_started_callback(F &&callback) {
|
||||
this->download_started_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_finished_callback(F &&callback) {
|
||||
this->download_finished_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
/// Called with the percentage downloaded (NAN when the server did not send the size) and the bytes read so far
|
||||
template<typename F> void add_on_progress_callback(F &&callback) {
|
||||
this->download_progress_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_error_callback(F &&callback) {
|
||||
this->download_error_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
@@ -93,20 +81,12 @@ class OnlineImage final : public PollingComponent,
|
||||
bool validate_url_(const std::string &url);
|
||||
void end_connection_();
|
||||
void finish_download_();
|
||||
/// loop() for a JPEG that is decoded as it downloads
|
||||
void loop_stream_();
|
||||
void fail_download_();
|
||||
/// Reports download progress, at most every PROGRESS_INTERVAL_MS unless force is set
|
||||
void report_progress_(bool force);
|
||||
|
||||
CallbackManager<void(bool)> download_finished_callback_{};
|
||||
CallbackManager<void()> download_error_callback_{};
|
||||
LazyCallbackManager<void()> download_started_callback_{};
|
||||
LazyCallbackManager<void(float, size_t)> download_progress_callback_{};
|
||||
|
||||
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
||||
DownloadBuffer download_buffer_;
|
||||
DownloadSource source_{download_buffer_};
|
||||
/**
|
||||
* This is the *initial* size of the download buffer, not the current size.
|
||||
* The download buffer can be resized at runtime; the download_buffer_initial_size_
|
||||
@@ -129,9 +109,6 @@ class OnlineImage final : public PollingComponent,
|
||||
|
||||
uint32_t start_time_{0};
|
||||
uint32_t last_data_time_{0};
|
||||
uint32_t last_progress_time_{0};
|
||||
bool streaming_{false}; // The decoder reads the download itself, as it arrives
|
||||
bool stream_started_{false}; // The decoder has read the headers
|
||||
};
|
||||
|
||||
} // namespace esphome::online_image
|
||||
@@ -1,95 +0,0 @@
|
||||
import importlib
|
||||
import pkgutil
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_MAX_PACKET_SIZE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import layers
|
||||
from .layers import (
|
||||
PacketInterface,
|
||||
build_stack,
|
||||
check_session,
|
||||
packet_interface_ns,
|
||||
stack_auto_load,
|
||||
stack_schema,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DOMAIN = "packet_interface"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
CONF_PACKET_INTERFACE = "packet_interface"
|
||||
|
||||
# Each module in layers registers a layer type; import them all before building the schemas from the registry
|
||||
for module_info in pkgutil.iter_modules(layers.__path__):
|
||||
importlib.import_module(f".layers.{module_info.name}", package=__package__)
|
||||
|
||||
LINK_LAYER_SCHEMAS = stack_schema()
|
||||
|
||||
__all__ = [
|
||||
"CONF_MAX_PACKET_SIZE",
|
||||
"CONF_PACKET_INTERFACE",
|
||||
"PacketInterface",
|
||||
"new_packet_interface",
|
||||
"packet_interface_ns",
|
||||
"packet_interface_schema",
|
||||
]
|
||||
|
||||
|
||||
def AUTO_LOAD(config: list[ConfigType] | None = None) -> list[str]:
|
||||
"""Load only what the configured layers use.
|
||||
|
||||
Without a config, as tooling that lists dependencies calls it, everything a layer could load.
|
||||
"""
|
||||
if config is None:
|
||||
return ["binary_sensor", "xxtea"]
|
||||
loads: set[str] = set()
|
||||
for interface in config:
|
||||
loads |= stack_auto_load(interface)
|
||||
return sorted(loads)
|
||||
|
||||
|
||||
def _validate_layers(config: ConfigType) -> ConfigType:
|
||||
check_session(config)
|
||||
return config
|
||||
|
||||
|
||||
def packet_interface_schema(
|
||||
class_: MockObjClass,
|
||||
default_max_packet_size: int,
|
||||
max_packet_size_limit: int = 4096,
|
||||
) -> cv.Schema:
|
||||
"""Schema for a link platform, with the layers that can be stacked on it."""
|
||||
return (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
cv.Optional(
|
||||
CONF_MAX_PACKET_SIZE, default=default_max_packet_size
|
||||
): cv.int_range(16, max_packet_size_limit),
|
||||
**LINK_LAYER_SCHEMAS,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.add_extra(_validate_layers)
|
||||
)
|
||||
|
||||
|
||||
async def new_packet_interface(config: ConfigType, *args: Any) -> MockObj:
|
||||
"""Create the link layer and the layers configured on it, from the bottom up.
|
||||
|
||||
The link's constructor takes its maximum packet size, usually as a std::span over a buffer that code generation
|
||||
sized from the configuration, so the two cannot differ.
|
||||
|
||||
Returns the link layer; applications reach the top layer with PacketInterface::top().
|
||||
"""
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
cg.add(var.set_name(config[CONF_ID].id))
|
||||
await cg.register_component(var, config)
|
||||
await build_stack(config, var)
|
||||
return var
|
||||
@@ -1,135 +0,0 @@
|
||||
#include "basic_session.h"
|
||||
#ifdef USE_PACKET_INTERFACE_BASIC_SESSION
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface.session");
|
||||
|
||||
static constexpr uint8_t PACKET_TYPE_DATA = 0x00;
|
||||
static constexpr uint8_t PACKET_TYPE_KEEP_ALIVE = 0x01;
|
||||
// Type byte and the 32-bit boot id, little endian
|
||||
static constexpr size_t KEEP_ALIVE_SIZE = 5;
|
||||
|
||||
BasicSession::BasicSession(PacketInterface *lower)
|
||||
: PacketInterface(remaining_packet_size(lower->get_max_packet_size(), HEADER_SIZE)), lower_(lower) {
|
||||
this->stack_on_(lower, CAPABILITY_SESSION);
|
||||
// Data lost by a reliability layer below must be sent again, as after a restart of the peer
|
||||
if (lower->has_capability(CAPABILITY_RELIABILITY)) {
|
||||
lower->add_on_session_state_callback([this](bool connected) {
|
||||
if (!connected && this->connected_)
|
||||
this->set_connected_(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void BasicSession::setup() {
|
||||
if (this->lower_->get_max_packet_size() < KEEP_ALIVE_SIZE) {
|
||||
this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
|
||||
return;
|
||||
}
|
||||
this->boot_id_ = random_uint32();
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->connected_sensor_ != nullptr)
|
||||
this->connected_sensor_->publish_initial_state(false);
|
||||
#endif
|
||||
this->lower_->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
|
||||
// A reliability layer below queues this until its link is up, so the peer hears of a restart straight away
|
||||
this->send_keep_alive_();
|
||||
this->set_interval(this->keep_alive_ms_, [this] { this->on_interval_(); });
|
||||
}
|
||||
|
||||
void BasicSession::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Basic Session on '%s':\n"
|
||||
" Max Packet Size: %zu\n"
|
||||
" Keep Alive: %" PRIu32 "ms\n"
|
||||
" Timeout: %" PRIu32 "ms",
|
||||
this->name_, this->max_packet_size_, this->keep_alive_ms_, this->timeout_ms_);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool BasicSession::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
uint8_t header = PACKET_TYPE_DATA;
|
||||
PacketBuffer packet(&header, HEADER_SIZE, &data, nullptr, 0);
|
||||
return this->lower_->send_to_interface(packet, meta_data);
|
||||
}
|
||||
|
||||
void BasicSession::send_keep_alive_() {
|
||||
uint8_t packet[KEEP_ALIVE_SIZE] = {
|
||||
PACKET_TYPE_KEEP_ALIVE, static_cast<uint8_t>(this->boot_id_), static_cast<uint8_t>(this->boot_id_ >> 8),
|
||||
static_cast<uint8_t>(this->boot_id_ >> 16), static_cast<uint8_t>(this->boot_id_ >> 24)};
|
||||
if (!this->lower_->send_to_interface(PacketBuffer(packet, sizeof(packet)), {}))
|
||||
ESP_LOGV(TAG, "%s: Lower layer did not accept keep-alive", this->name_);
|
||||
}
|
||||
|
||||
void BasicSession::on_interval_() {
|
||||
this->send_keep_alive_();
|
||||
if (this->connected_ && App.get_loop_component_start_time() - this->last_receive_ms_ > this->timeout_ms_) {
|
||||
ESP_LOGW(TAG, "%s: Peer not heard for %" PRIu32 "ms", this->name_, this->timeout_ms_);
|
||||
this->set_connected_(false);
|
||||
}
|
||||
}
|
||||
|
||||
void BasicSession::set_connected_(bool connected) {
|
||||
this->connected_ = connected;
|
||||
ESP_LOGD(TAG, "%s: Session %s", this->name_, connected ? LOG_STR_LITERAL("connected") : LOG_STR_LITERAL("lost"));
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->connected_sensor_ != nullptr)
|
||||
this->connected_sensor_->publish_state(connected);
|
||||
#endif
|
||||
this->session_callback_.call(connected);
|
||||
}
|
||||
|
||||
void BasicSession::receive_(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
// Lower layers deliver packets in one piece; the payload is passed up without copying
|
||||
if (!data.is_contiguous()) {
|
||||
ESP_LOGW(TAG, "%s: Packet split across buffers is not supported", this->name_);
|
||||
return;
|
||||
}
|
||||
auto packet = data.as_span();
|
||||
if (packet.empty()) {
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
uint8_t type = packet[0];
|
||||
if (type == PACKET_TYPE_KEEP_ALIVE) {
|
||||
if (packet.size() != KEEP_ALIVE_SIZE) {
|
||||
ESP_LOGV(TAG, "%s: Keep-alive of %zu bytes dropped", this->name_, packet.size());
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
uint32_t boot_id = encode_uint32(packet[4], packet[3], packet[2], packet[1]);
|
||||
if (!this->peer_known_ || boot_id != this->peer_boot_id_) {
|
||||
// Data may arrive before the first keep-alive, so only a changed id means a restart
|
||||
if (this->connected_ && this->peer_known_) {
|
||||
ESP_LOGD(TAG, "%s: Peer has restarted", this->name_);
|
||||
this->set_connected_(false);
|
||||
}
|
||||
this->peer_boot_id_ = boot_id;
|
||||
this->peer_known_ = true;
|
||||
// Answer a new peer at once; it answers only if our id is new to it, so this cannot repeat
|
||||
this->send_keep_alive_();
|
||||
}
|
||||
} else if (type != PACKET_TYPE_DATA) {
|
||||
ESP_LOGV(TAG, "%s: Unknown packet type 0x%02X dropped", this->name_, type);
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
this->note_received_(true);
|
||||
this->last_receive_ms_ = App.get_loop_component_start_time();
|
||||
if (!this->connected_)
|
||||
this->set_connected_(true);
|
||||
if (type == PACKET_TYPE_DATA)
|
||||
this->on_receive_from_interface_(PacketBuffer(packet.data() + HEADER_SIZE, packet.size() - HEADER_SIZE), meta_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_PACKET_INTERFACE_BASIC_SESSION
|
||||
#include "packet_interface.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* A session layer that reports whether the peer can be reached.
|
||||
*
|
||||
* Every packet starts with a type byte. A data packet carries the payload of the layer above. A keep-alive packet
|
||||
* carries a random number chosen at boot, so that a restart of the peer is noticed even if it is quicker than the
|
||||
* timeout. A keep-alive is sent every keep_alive interval, and at once when a new peer is heard. The peer is
|
||||
* considered gone when nothing has been received from it for the timeout; this is checked every keep_alive interval.
|
||||
* A restart of the peer is reported as a disconnect followed by a connect. So is a reset of a reliability layer below,
|
||||
* which discards data: the session is reported lost, and connected again when the peer is next heard.
|
||||
*/
|
||||
class BasicSession : public PacketInterface {
|
||||
public:
|
||||
/// The packet type byte.
|
||||
static constexpr size_t HEADER_SIZE = 1;
|
||||
|
||||
explicit BasicSession(PacketInterface *lower);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) override;
|
||||
|
||||
void set_keep_alive(uint32_t keep_alive_ms) { this->keep_alive_ms_ = keep_alive_ms; }
|
||||
void set_session_timeout(uint32_t timeout_ms) { this->timeout_ms_ = timeout_ms; }
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void set_connected_sensor(binary_sensor::BinarySensor *sensor) { this->connected_sensor_ = sensor; }
|
||||
#endif
|
||||
|
||||
bool is_connected() const { return this->connected_; }
|
||||
|
||||
protected:
|
||||
void receive_(const PacketBuffer &data, const PacketMetaData &meta_data);
|
||||
void on_interval_();
|
||||
void send_keep_alive_();
|
||||
void set_connected_(bool connected);
|
||||
|
||||
PacketInterface *lower_;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
binary_sensor::BinarySensor *connected_sensor_{nullptr};
|
||||
#endif
|
||||
uint32_t keep_alive_ms_{10000};
|
||||
uint32_t timeout_ms_{30000};
|
||||
uint32_t last_receive_ms_{0};
|
||||
uint32_t boot_id_{0};
|
||||
uint32_t peer_boot_id_{0};
|
||||
bool peer_known_{false};
|
||||
bool connected_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,365 +0,0 @@
|
||||
#include "hdlc_reliability.h"
|
||||
#ifdef USE_PACKET_INTERFACE_HDLC
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface.hdlc");
|
||||
|
||||
static constexpr uint32_t RETRANSMIT_TIMER_ID = 0;
|
||||
static constexpr uint32_t ACK_TIMER_ID = 1;
|
||||
static constexpr uint32_t SEND_RETRY_TIMER_ID = 2;
|
||||
// A frame or acknowledgement the lower layer refused, for example while a radio was receiving, is sent again after this
|
||||
static constexpr uint32_t SEND_RETRY_MS = 50;
|
||||
// SABM is repeated at this interval, or the timeout if longer, until the peer answers
|
||||
static constexpr uint32_t RESET_INTERVAL_MS = 1000;
|
||||
|
||||
// Control byte formats, with the poll/final bit (0x10) ignored
|
||||
static constexpr uint8_t POLL_FINAL_BIT = 0x10;
|
||||
static constexpr uint8_t SUPERVISORY_FRAME = 0x01;
|
||||
static constexpr uint8_t SUPERVISORY_RR = 0x00;
|
||||
static constexpr uint8_t SUPERVISORY_REJ = 0x02;
|
||||
static constexpr uint8_t UNNUMBERED_SABM = 0x2F;
|
||||
static constexpr uint8_t UNNUMBERED_UA = 0x63;
|
||||
|
||||
static uint16_t frame_crc(uint8_t control, const uint8_t *payload, size_t length) {
|
||||
uint16_t crc = crc16(&control, 1);
|
||||
return crc16(payload, length, crc);
|
||||
}
|
||||
|
||||
HdlcReliability::HdlcReliability(PacketInterface *lower)
|
||||
: PacketInterface(remaining_packet_size(lower->get_max_packet_size(), FRAME_OVERHEAD)), lower_(lower) {
|
||||
this->stack_on_(lower, CAPABILITY_RELIABILITY);
|
||||
}
|
||||
|
||||
void HdlcReliability::setup() {
|
||||
if (this->max_packet_size_ == 0) {
|
||||
this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
|
||||
return;
|
||||
}
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
this->tx_slots_ = allocator.allocate(size_t{this->max_queue_length_} * this->max_packet_size_);
|
||||
if (this->tx_slots_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Not enough memory for the send queue"));
|
||||
return;
|
||||
}
|
||||
this->lower_->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
|
||||
}
|
||||
|
||||
void HdlcReliability::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"HDLC Reliability on '%s':\n"
|
||||
" Max Packet Size: %zu\n"
|
||||
" Timeout: %" PRIu32 "ms\n"
|
||||
" Ack Delay: %" PRIu32 "ms\n"
|
||||
" Max Queue Length: %u\n"
|
||||
" Max Retries: %u",
|
||||
this->name_, this->max_packet_size_, this->timeout_ms_, this->ack_delay_ms_, this->max_queue_length_,
|
||||
this->max_retries_);
|
||||
}
|
||||
|
||||
void HdlcReliability::loop() {
|
||||
// Disabled first, as sending may deliver a reply to this layer and request the loop again
|
||||
this->disable_loop();
|
||||
if (this->state_ == LinkState::LINK_STATE_RESET_PENDING) {
|
||||
this->state_ = LinkState::LINK_STATE_RESETTING;
|
||||
this->start_timer_();
|
||||
this->send_frame_(UNNUMBERED_SABM, nullptr, 0);
|
||||
}
|
||||
if (this->state_ != LinkState::LINK_STATE_CONNECTED)
|
||||
return;
|
||||
// Queued frames go first, as each carries the acknowledgement
|
||||
if (this->send_pending_() && this->ack_due_ && !this->send_supervisory_(SUPERVISORY_RR))
|
||||
this->retry_send_();
|
||||
}
|
||||
|
||||
void HdlcReliability::retry_send_() {
|
||||
this->set_timeout(SEND_RETRY_TIMER_ID, SEND_RETRY_MS, [this]() { this->request_loop_(); });
|
||||
}
|
||||
|
||||
void HdlcReliability::owe_ack_(bool now) {
|
||||
if (now || this->ack_delay_ms_ == 0) {
|
||||
this->ack_due_ = true;
|
||||
this->request_loop_();
|
||||
} else if (!this->ack_pending_) {
|
||||
// Waiting lets one acknowledgement cover several frames, or a reply carry it
|
||||
this->set_timeout(ACK_TIMER_ID, this->ack_delay_ms_, [this]() {
|
||||
this->ack_due_ = true;
|
||||
this->request_loop_();
|
||||
});
|
||||
}
|
||||
this->ack_pending_ = true;
|
||||
}
|
||||
|
||||
void HdlcReliability::ack_sent_() {
|
||||
this->ack_pending_ = false;
|
||||
this->ack_due_ = false;
|
||||
this->cancel_timeout(ACK_TIMER_ID);
|
||||
}
|
||||
|
||||
void HdlcReliability::request_loop_() { this->enable_loop(); }
|
||||
|
||||
bool HdlcReliability::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
if (this->tx_slots_ == nullptr)
|
||||
return false;
|
||||
size_t size = data.size();
|
||||
if (size > this->max_packet_size_) {
|
||||
ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
|
||||
return false;
|
||||
}
|
||||
uint8_t outstanding = this->outstanding_();
|
||||
if (outstanding >= this->max_queue_length_) {
|
||||
if (this->state_ == LinkState::LINK_STATE_CONNECTED) {
|
||||
ESP_LOGV(TAG, "%s: Send queue full", this->name_);
|
||||
} else if (!this->reset_wait_logged_) {
|
||||
// Once for each reset; the layers above keep trying while the peer is away
|
||||
this->reset_wait_logged_ = true;
|
||||
ESP_LOGD(TAG, "%s: Send queue full, waiting for the peer to answer the link reset", this->name_);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
uint8_t seq = this->vs_;
|
||||
data.copy_to(this->slot_(seq), size);
|
||||
this->tx_lengths_[this->slot_index_(seq)] = size;
|
||||
this->vs_ = (seq + 1) & 7;
|
||||
// While the link is being reset, the frame waits in the queue and is sent when UA arrives
|
||||
if (this->state_ == LinkState::LINK_STATE_CONNECTED) {
|
||||
this->send_pending_();
|
||||
if (outstanding == 0)
|
||||
this->start_timer_();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HdlcReliability::send_frame_(uint8_t control, const uint8_t *payload, size_t length) {
|
||||
uint16_t crc = frame_crc(control, payload, length);
|
||||
uint8_t trailer[2] = {static_cast<uint8_t>(crc), static_cast<uint8_t>(crc >> 8)};
|
||||
PacketBuffer body(payload, length);
|
||||
PacketBuffer frame(&control, 1, length != 0 ? &body : nullptr, trailer, sizeof(trailer));
|
||||
if (this->lower_->send_to_interface(frame, {}))
|
||||
return true;
|
||||
ESP_LOGV(TAG, "%s: Lower layer did not accept frame 0x%02X", this->name_, control);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HdlcReliability::send_supervisory_(uint8_t type) {
|
||||
if (!this->send_frame_(SUPERVISORY_FRAME | (type << 2) | (this->vr_ << 5), nullptr, 0))
|
||||
return false;
|
||||
this->ack_sent_();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sends the next queued frame not yet sent, and returns true if none are left. One frame per loop pass lets other
|
||||
// components run between frames: on a radio, sending blocks, and the peer's frames are received in between. If the
|
||||
// lower layer refuses the frame, it is sent again shortly.
|
||||
bool HdlcReliability::send_pending_() {
|
||||
if (this->vn_ == this->vs_)
|
||||
return true;
|
||||
uint8_t control = (this->vn_ << 1) | (this->vr_ << 5);
|
||||
if (!this->send_frame_(control, this->slot_(this->vn_), this->tx_lengths_[this->slot_index_(this->vn_)])) {
|
||||
this->retry_send_();
|
||||
return false;
|
||||
}
|
||||
// Every I-frame carries an acknowledgement, so a pending RR is no longer needed
|
||||
this->ack_sent_();
|
||||
this->vn_ = (this->vn_ + 1) & 7;
|
||||
if (this->vn_ == this->vs_)
|
||||
return true;
|
||||
this->request_loop_();
|
||||
return false;
|
||||
}
|
||||
|
||||
void HdlcReliability::resend_outstanding_() {
|
||||
this->vn_ = this->va_;
|
||||
this->send_pending_();
|
||||
}
|
||||
|
||||
void HdlcReliability::start_timer_() {
|
||||
uint32_t interval = this->timeout_ms_;
|
||||
if (this->state_ != LinkState::LINK_STATE_CONNECTED && interval < RESET_INTERVAL_MS)
|
||||
interval = RESET_INTERVAL_MS;
|
||||
// Up to a quarter longer at random, so devices whose frames collided on a shared channel do not resend together
|
||||
interval += random_uint32() % (interval / 4 + 1);
|
||||
this->set_timeout(RETRANSMIT_TIMER_ID, interval, [this]() { this->on_timeout_(); });
|
||||
}
|
||||
|
||||
void HdlcReliability::on_timeout_() {
|
||||
if (this->state_ != LinkState::LINK_STATE_CONNECTED) {
|
||||
// Counted up to max_retries_ only, so the warning is logged once until the link comes up
|
||||
if (this->retries_ < this->max_retries_) {
|
||||
this->retries_++;
|
||||
if (this->retries_ == this->max_retries_) {
|
||||
ESP_LOGW(TAG, "%s: No answer to the link reset after %u tries; check that the peer has a reliability layer",
|
||||
this->name_, this->max_retries_);
|
||||
}
|
||||
}
|
||||
this->send_frame_(UNNUMBERED_SABM, nullptr, 0);
|
||||
this->start_timer_();
|
||||
return;
|
||||
}
|
||||
if (this->outstanding_() == 0)
|
||||
return;
|
||||
if (this->retries_ == this->max_retries_) {
|
||||
ESP_LOGW(TAG, "%s: No acknowledgement after %u retries, resetting link", this->name_, this->max_retries_);
|
||||
this->start_reset_();
|
||||
return;
|
||||
}
|
||||
this->retries_++;
|
||||
ESP_LOGV(TAG, "%s: Timeout, resending %u frames", this->name_, this->outstanding_());
|
||||
this->resend_outstanding_();
|
||||
this->start_timer_();
|
||||
}
|
||||
|
||||
// Unacknowledged frames are discarded; frames queued from now on are numbered from 0 and sent once the link is up.
|
||||
// Resetting a connected link can lose data both ways, which is reported to the layers above.
|
||||
void HdlcReliability::start_reset_() {
|
||||
bool was_connected = this->state_ == LinkState::LINK_STATE_CONNECTED;
|
||||
this->state_ = LinkState::LINK_STATE_RESET_PENDING;
|
||||
this->va_ = this->vs_ = this->vn_ = 0;
|
||||
this->head_ = 0;
|
||||
this->vr_ = 0;
|
||||
this->retries_ = 0;
|
||||
this->rej_sent_ = false;
|
||||
this->ack_sent_();
|
||||
this->cancel_timeout(RETRANSMIT_TIMER_ID);
|
||||
this->request_loop_();
|
||||
if (was_connected)
|
||||
this->session_callback_.call(false);
|
||||
}
|
||||
|
||||
void HdlcReliability::link_up_() {
|
||||
ESP_LOGD(TAG, "%s: Link connected", this->name_);
|
||||
this->state_ = LinkState::LINK_STATE_CONNECTED;
|
||||
this->reset_wait_logged_ = false;
|
||||
this->peer_frame_seen_ = false;
|
||||
this->retries_ = 0;
|
||||
this->cancel_timeout(RETRANSMIT_TIMER_ID);
|
||||
if (this->outstanding_() != 0) {
|
||||
this->resend_outstanding_();
|
||||
this->start_timer_();
|
||||
}
|
||||
}
|
||||
|
||||
void HdlcReliability::process_ack_(uint8_t nr) {
|
||||
uint8_t acked = (nr - this->va_) & 7;
|
||||
if (acked > this->outstanding_()) {
|
||||
ESP_LOGV(TAG, "%s: Ignoring invalid N(R) %u", this->name_, nr);
|
||||
return;
|
||||
}
|
||||
if (acked == 0)
|
||||
return;
|
||||
// The peer may have had frames that were refused when last resent
|
||||
if (acked > ((this->vn_ - this->va_) & 7))
|
||||
this->vn_ = nr;
|
||||
this->head_ = (this->head_ + acked) % this->max_queue_length_;
|
||||
this->va_ = nr;
|
||||
this->retries_ = 0;
|
||||
if (this->outstanding_() == 0) {
|
||||
this->cancel_timeout(RETRANSMIT_TIMER_ID);
|
||||
} else {
|
||||
this->start_timer_();
|
||||
}
|
||||
}
|
||||
|
||||
void HdlcReliability::receive_(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
// Lower layers deliver packets in one piece; the payload is passed up without copying
|
||||
if (!data.is_contiguous()) {
|
||||
ESP_LOGW(TAG, "%s: Packet split across buffers is not supported", this->name_);
|
||||
return;
|
||||
}
|
||||
auto frame = data.as_span();
|
||||
if (frame.size() < FRAME_OVERHEAD) {
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
size_t length = frame.size() - FRAME_OVERHEAD;
|
||||
uint8_t control = frame[0];
|
||||
const uint8_t *payload = frame.data() + 1;
|
||||
uint16_t crc = frame[length + 1] | (frame[length + 2] << 8);
|
||||
if (frame_crc(control, payload, length) != crc) {
|
||||
ESP_LOGV(TAG, "%s: CRC error, frame dropped", this->name_);
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
// The peer's packets may only be larger if it has a larger max_packet_size, which the layers above cannot take
|
||||
if (length > this->max_packet_size_) {
|
||||
if (!this->oversize_warned_) {
|
||||
this->oversize_warned_ = true;
|
||||
ESP_LOGW(TAG,
|
||||
"%s: Frame of %zu bytes is larger than %zu, dropped; check that both devices use the same "
|
||||
"max_packet_size",
|
||||
this->name_, length, this->max_packet_size_);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "%s: Frame of %zu bytes is larger than %zu, dropped", this->name_, length, this->max_packet_size_);
|
||||
}
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
this->note_received_(true);
|
||||
|
||||
uint8_t command = control & ~POLL_FINAL_BIT;
|
||||
if (command == UNNUMBERED_SABM) {
|
||||
// A peer that has not heard our UA repeats its SABM, and cannot have sent a numbered frame meanwhile; answering
|
||||
// again keeps this side connected, and the queued frames, instead of treating the repeat as a restart.
|
||||
if (this->state_ == LinkState::LINK_STATE_CONNECTED && this->peer_frame_seen_) {
|
||||
// The peer has restarted, so frames it has not acknowledged are lost
|
||||
ESP_LOGD(TAG, "%s: Link reset by peer", this->name_);
|
||||
this->start_reset_();
|
||||
}
|
||||
this->vr_ = 0;
|
||||
this->rej_sent_ = false;
|
||||
this->send_frame_(UNNUMBERED_UA, nullptr, 0);
|
||||
// Both sides now start from 0, so the link is up even if our own SABM is unanswered
|
||||
this->link_up_();
|
||||
return;
|
||||
}
|
||||
if (command == UNNUMBERED_UA) {
|
||||
if (this->state_ == LinkState::LINK_STATE_RESETTING)
|
||||
this->link_up_();
|
||||
return;
|
||||
}
|
||||
// Numbered frames are ignored until the link is reset; the peer answers our SABM first
|
||||
if (this->state_ != LinkState::LINK_STATE_CONNECTED)
|
||||
return;
|
||||
this->peer_frame_seen_ = true;
|
||||
|
||||
uint8_t nr = control >> 5;
|
||||
if ((control & 1) == 0) {
|
||||
this->process_ack_(nr);
|
||||
this->receive_info_(control, payload, length, meta_data);
|
||||
return;
|
||||
}
|
||||
if ((control & 3) != SUPERVISORY_FRAME)
|
||||
return;
|
||||
this->process_ack_(nr);
|
||||
if (((control >> 2) & 3) == SUPERVISORY_REJ && this->outstanding_() != 0) {
|
||||
ESP_LOGV(TAG, "%s: Peer rejected from %u", this->name_, nr);
|
||||
this->resend_outstanding_();
|
||||
this->start_timer_();
|
||||
}
|
||||
}
|
||||
|
||||
void HdlcReliability::receive_info_(uint8_t control, const uint8_t *payload, size_t length,
|
||||
const PacketMetaData &meta_data) {
|
||||
uint8_t ns = (control >> 1) & 7;
|
||||
if (ns != this->vr_) {
|
||||
// A gap or a duplicate; ask once for a resend from the expected frame, then just acknowledge at once
|
||||
if (!this->rej_sent_) {
|
||||
this->rej_sent_ = this->send_supervisory_(SUPERVISORY_REJ);
|
||||
if (this->rej_sent_)
|
||||
return;
|
||||
}
|
||||
this->owe_ack_(true);
|
||||
return;
|
||||
}
|
||||
this->vr_ = (this->vr_ + 1) & 7;
|
||||
this->rej_sent_ = false;
|
||||
// The acknowledgement is sent from loop(), so a reply sent from the callback can carry it instead
|
||||
this->owe_ack_(false);
|
||||
this->on_receive_from_interface_(PacketBuffer(payload, length), meta_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,99 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_PACKET_INTERFACE_HDLC
|
||||
#include <array>
|
||||
|
||||
#include "packet_interface.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* A reliability layer that delivers packets in order, without loss or duplicates, between two peers.
|
||||
*
|
||||
* It follows HDLC asynchronous balanced mode, with sequence numbers modulo 8 and go-back-N retransmission:
|
||||
* - I-frames carry a payload, the sequence number N(S) and an acknowledgement N(R) of the peer's frames.
|
||||
* - RR frames acknowledge without a payload; REJ frames ask the peer to resend from N(R).
|
||||
* - SABM resets both peers' sequence numbers, and UA answers it.
|
||||
*
|
||||
* Each frame is the HDLC control byte, the payload, then a CRC-16 of both in little endian order.
|
||||
* Unacknowledged frames are resent after the timeout; after max_retries the link is reset.
|
||||
*/
|
||||
class HdlcReliability : public PacketInterface {
|
||||
public:
|
||||
/// Most frames that can be unacknowledged at once, set by the 3 bit sequence numbers.
|
||||
static constexpr uint8_t MAX_WINDOW = 7;
|
||||
/// Control byte plus CRC-16.
|
||||
static constexpr size_t FRAME_OVERHEAD = 3;
|
||||
|
||||
explicit HdlcReliability(PacketInterface *lower);
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) override;
|
||||
/// No room while the layer has no send queue: before setup(), or when setup() failed.
|
||||
size_t get_send_capacity() const override {
|
||||
return this->tx_slots_ == nullptr ? 0 : this->max_queue_length_ - this->outstanding_();
|
||||
}
|
||||
|
||||
void set_ack_timeout(uint32_t timeout_ms) { this->timeout_ms_ = timeout_ms; }
|
||||
void set_ack_delay(uint32_t ack_delay_ms) { this->ack_delay_ms_ = ack_delay_ms; }
|
||||
void set_max_queue_length(uint8_t length) { this->max_queue_length_ = length; }
|
||||
void set_max_retries(uint8_t retries) { this->max_retries_ = retries; }
|
||||
|
||||
bool is_connected() const { return this->state_ == LinkState::LINK_STATE_CONNECTED; }
|
||||
|
||||
protected:
|
||||
enum class LinkState : uint8_t {
|
||||
LINK_STATE_RESET_PENDING, // SABM to be sent from loop()
|
||||
LINK_STATE_RESETTING, // SABM sent, waiting for UA
|
||||
LINK_STATE_CONNECTED,
|
||||
};
|
||||
|
||||
void receive_(const PacketBuffer &data, const PacketMetaData &meta_data);
|
||||
void receive_info_(uint8_t control, const uint8_t *payload, size_t length, const PacketMetaData &meta_data);
|
||||
bool send_frame_(uint8_t control, const uint8_t *payload, size_t length);
|
||||
bool send_supervisory_(uint8_t type);
|
||||
bool send_pending_();
|
||||
void resend_outstanding_();
|
||||
void retry_send_();
|
||||
/// An RR is owed: at once, or after the ack delay unless an I-frame carries it first.
|
||||
void owe_ack_(bool now);
|
||||
void ack_sent_();
|
||||
void process_ack_(uint8_t nr);
|
||||
void on_timeout_();
|
||||
void start_reset_();
|
||||
void link_up_();
|
||||
void start_timer_();
|
||||
void request_loop_();
|
||||
uint8_t outstanding_() const { return (this->vs_ - this->va_) & 7; }
|
||||
uint8_t slot_index_(uint8_t seq) const { return (this->head_ + ((seq - this->va_) & 7)) % this->max_queue_length_; }
|
||||
uint8_t *slot_(uint8_t seq) { return this->tx_slots_ + this->slot_index_(seq) * this->max_packet_size_; }
|
||||
|
||||
PacketInterface *lower_;
|
||||
// max_queue_length_ slots of max_packet_size_ bytes, a ring starting at head_ which holds frame va_; allocated in
|
||||
// setup() and never freed
|
||||
uint8_t *tx_slots_{nullptr};
|
||||
std::array<uint16_t, MAX_WINDOW> tx_lengths_{}; // indexed like the slots
|
||||
uint32_t timeout_ms_{200};
|
||||
uint32_t ack_delay_ms_{0};
|
||||
uint8_t max_queue_length_{4};
|
||||
uint8_t max_retries_{5};
|
||||
uint8_t retries_{0};
|
||||
uint8_t head_{0};
|
||||
uint8_t vs_{0}; // sequence number of the next new frame
|
||||
uint8_t vn_{0}; // next frame to send; frames from va_ up to it have been sent
|
||||
uint8_t va_{0}; // oldest unacknowledged frame
|
||||
uint8_t vr_{0}; // next frame expected from the peer
|
||||
LinkState state_{LinkState::LINK_STATE_RESET_PENDING};
|
||||
bool ack_pending_{false}; // an RR is owed
|
||||
bool ack_due_{false}; // and should be sent now
|
||||
bool rej_sent_{false};
|
||||
bool peer_frame_seen_{false}; // a numbered frame has arrived since the link came up
|
||||
bool oversize_warned_{false};
|
||||
bool reset_wait_logged_{false}; // the queue filled while waiting for this reset to be answered
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,155 +0,0 @@
|
||||
"""The kinds of layer that can be stacked on a packet interface, and the registry of their implementations.
|
||||
|
||||
Each module in this package implements one layer type and registers it with its kind, for example:
|
||||
|
||||
@RELIABILITY.register("hdlc", schema=HDLC_SCHEMA)
|
||||
async def build_hdlc(config: ConfigType, lower: MockObj) -> MockObj:
|
||||
...
|
||||
|
||||
The packet_interface component imports every module here before it builds its schemas, so a new layer type needs
|
||||
only a new module. The kinds, and the order in which they are stacked, are fixed here.
|
||||
"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENCRYPTION, CONF_KEY, CONF_TYPE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
packet_interface_ns = cg.esphome_ns.namespace("packet_interface")
|
||||
PacketInterface = packet_interface_ns.class_("PacketInterface", cg.Component)
|
||||
|
||||
CONF_RELIABILITY = "reliability"
|
||||
CONF_SESSION = "session"
|
||||
|
||||
# Builds a layer above lower and returns it
|
||||
LayerBuilder = Callable[[ConfigType, MockObj], Awaitable[Any]]
|
||||
|
||||
|
||||
def _never(config: ConfigType) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _nothing(config: ConfigType) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LayerType:
|
||||
"""One implementation of a kind of layer."""
|
||||
|
||||
schema: Any
|
||||
build: LayerBuilder
|
||||
# True if this configuration works only when the peer sends packets back, which a session layer makes sure of
|
||||
needs_session: Callable[[ConfigType], bool]
|
||||
# The components this configuration needs loaded
|
||||
auto_load: Callable[[ConfigType], list[str]]
|
||||
|
||||
|
||||
class LayerKind:
|
||||
"""A kind of layer, such as encryption, with the types that implement it."""
|
||||
|
||||
def __init__(
|
||||
self, key: str, default_type: str, shorthand: str | None = None
|
||||
) -> None:
|
||||
self.key = key
|
||||
self.default_type = default_type
|
||||
# The option that a plain string value sets, as in `encryption: my key`
|
||||
self.shorthand = shorthand
|
||||
self.types: dict[str, LayerType] = {}
|
||||
# Set once the schema is built, as types registered after that would not be in it
|
||||
self.frozen = False
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
schema: Any,
|
||||
needs_session: Callable[[ConfigType], bool] = _never,
|
||||
auto_load: Callable[[ConfigType], list[str]] = _nothing,
|
||||
) -> Callable[[LayerBuilder], LayerBuilder]:
|
||||
"""Register a type with its schema."""
|
||||
|
||||
def decorator(build: LayerBuilder) -> LayerBuilder:
|
||||
if self.frozen:
|
||||
raise ValueError(
|
||||
f"{self.key} layer type '{name}' is registered after the schemas were built; register it "
|
||||
"before the packet_interface component is imported"
|
||||
)
|
||||
if name in self.types:
|
||||
raise ValueError(
|
||||
f"{self.key} layer type '{name}' is already registered"
|
||||
)
|
||||
self.types[name] = LayerType(schema, build, needs_session, auto_load)
|
||||
return build
|
||||
|
||||
return decorator
|
||||
|
||||
def _expand(self, value: Any) -> Any:
|
||||
"""Allow a layer to be enabled with no options, as in `reliability:`, or with its shorthand."""
|
||||
if value is None:
|
||||
return {}
|
||||
if self.shorthand is not None and isinstance(value, str):
|
||||
return {self.shorthand: value}
|
||||
return value
|
||||
|
||||
def schema(self) -> Callable[[Any], ConfigType]:
|
||||
"""The schema for this kind; no type can be registered after this."""
|
||||
self.frozen = True
|
||||
return cv.All(
|
||||
self._expand,
|
||||
cv.typed_schema(
|
||||
{name: layer.schema for name, layer in self.types.items()},
|
||||
default_type=self.default_type,
|
||||
),
|
||||
)
|
||||
|
||||
def get_type(self, config: ConfigType) -> LayerType:
|
||||
return self.types[config[CONF_TYPE]]
|
||||
|
||||
|
||||
ENCRYPTION = LayerKind(CONF_ENCRYPTION, "xxtea", shorthand=CONF_KEY)
|
||||
RELIABILITY = LayerKind(CONF_RELIABILITY, "hdlc")
|
||||
SESSION = LayerKind(CONF_SESSION, "basic")
|
||||
# The kinds stacked above the link, from the bottom up
|
||||
STACK_ORDER = (ENCRYPTION, RELIABILITY, SESSION)
|
||||
|
||||
|
||||
def stack_schema() -> dict:
|
||||
"""Schema entries for the layers of one stack; call only once all layer types are registered."""
|
||||
return {cv.Optional(kind.key): kind.schema() for kind in STACK_ORDER}
|
||||
|
||||
|
||||
def check_session(config: ConfigType) -> None:
|
||||
"""Layers that need packets back from the peer need a session layer, whose keep-alives send them."""
|
||||
if CONF_SESSION in config:
|
||||
return
|
||||
for kind in STACK_ORDER:
|
||||
if (layer := config.get(kind.key)) is not None and kind.get_type(
|
||||
layer
|
||||
).needs_session(layer):
|
||||
raise cv.Invalid(
|
||||
f"This '{kind.key}' needs packets from the peer, so it needs a '{CONF_SESSION}' layer, whose "
|
||||
"keep-alives make sure they are sent",
|
||||
path=[kind.key],
|
||||
)
|
||||
|
||||
|
||||
def stack_auto_load(config: ConfigType) -> set[str]:
|
||||
"""The components that the layers configured in config need loaded."""
|
||||
loads: set[str] = set()
|
||||
for kind in STACK_ORDER:
|
||||
if (layer := config.get(kind.key)) is not None:
|
||||
loads.update(kind.get_type(layer).auto_load(layer))
|
||||
return loads
|
||||
|
||||
|
||||
async def build_stack(config: ConfigType, lower: MockObj) -> None:
|
||||
"""Create the layers configured in config, from the bottom up, above lower."""
|
||||
for kind in STACK_ORDER:
|
||||
if (layer := config.get(kind.key)) is not None:
|
||||
lower = await kind.get_type(layer).build(layer, lower)
|
||||
@@ -1,70 +0,0 @@
|
||||
"""Session layer that sends keep-alives and reports whether the peer can be reached."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BINARY_SENSOR,
|
||||
CONF_ID,
|
||||
CONF_TIMEOUT,
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import SESSION, PacketInterface, packet_interface_ns
|
||||
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
|
||||
BasicSession = packet_interface_ns.class_("BasicSession", PacketInterface)
|
||||
|
||||
|
||||
def _session_timeout(config: ConfigType) -> ConfigType:
|
||||
"""Default the timeout to three keep-alive intervals, and require it to be longer than one."""
|
||||
keep_alive = config[CONF_KEEP_ALIVE].total_milliseconds
|
||||
if (timeout := config.get(CONF_TIMEOUT)) is None:
|
||||
config[CONF_TIMEOUT] = cv.positive_time_period_milliseconds(
|
||||
f"{keep_alive * 3}ms"
|
||||
)
|
||||
elif timeout.total_milliseconds <= keep_alive:
|
||||
raise cv.Invalid("Must be longer than keep_alive", path=[CONF_TIMEOUT])
|
||||
return config
|
||||
|
||||
|
||||
BASIC_SESSION_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BasicSession),
|
||||
cv.Optional(CONF_KEEP_ALIVE, default="10s"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(seconds=1), max=cv.TimePeriod(hours=1)),
|
||||
),
|
||||
cv.Optional(CONF_TIMEOUT): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(hours=3)),
|
||||
),
|
||||
cv.Optional(CONF_BINARY_SENSOR): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_session_timeout,
|
||||
)
|
||||
|
||||
|
||||
@SESSION.register(
|
||||
"basic",
|
||||
schema=BASIC_SESSION_SCHEMA,
|
||||
auto_load=lambda config: ["binary_sensor"] if CONF_BINARY_SENSOR in config else [],
|
||||
)
|
||||
async def build_basic_session(config: ConfigType, lower: MockObj) -> MockObj:
|
||||
cg.add_define("USE_PACKET_INTERFACE_BASIC_SESSION")
|
||||
var = cg.new_Pvariable(config[CONF_ID], lower)
|
||||
cg.add(var.set_keep_alive(config[CONF_KEEP_ALIVE]))
|
||||
cg.add(var.set_session_timeout(config[CONF_TIMEOUT]))
|
||||
await cg.register_component(var, config)
|
||||
binary_sensors = binary_sensor.sub_binary_sensors(config)
|
||||
await binary_sensors(CONF_BINARY_SENSOR, var.set_connected_sensor)
|
||||
return var
|
||||
@@ -1,59 +0,0 @@
|
||||
"""Reliability layer using HDLC style acknowledgement and go-back-N retransmission."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TIMEOUT
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import RELIABILITY, PacketInterface, packet_interface_ns
|
||||
|
||||
CONF_ACK_DELAY = "ack_delay"
|
||||
CONF_MAX_QUEUE_LENGTH = "max_queue_length"
|
||||
CONF_MAX_RETRIES = "max_retries"
|
||||
|
||||
HdlcReliability = packet_interface_ns.class_("HdlcReliability", PacketInterface)
|
||||
|
||||
|
||||
def _ack_delay_within_timeout(config: ConfigType) -> ConfigType:
|
||||
"""The peer, configured the same, must hear the acknowledgement before its timeout."""
|
||||
if (
|
||||
config[CONF_ACK_DELAY].total_milliseconds
|
||||
>= config[CONF_TIMEOUT].total_milliseconds
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Must be shorter than '{CONF_TIMEOUT}'", path=[CONF_ACK_DELAY]
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
HDLC_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HdlcReliability),
|
||||
# Retransmitting more often than the main loop runs would exhaust the retries at once
|
||||
cv.Optional(CONF_TIMEOUT, default="200ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(milliseconds=50)),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ACK_DELAY, default="0ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_MAX_QUEUE_LENGTH, default=4): cv.int_range(1, 7),
|
||||
cv.Optional(CONF_MAX_RETRIES, default=5): cv.int_range(1, 255),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_ack_delay_within_timeout,
|
||||
)
|
||||
|
||||
|
||||
@RELIABILITY.register("hdlc", schema=HDLC_SCHEMA)
|
||||
async def build_hdlc(config: ConfigType, lower: MockObj) -> MockObj:
|
||||
cg.add_define("USE_PACKET_INTERFACE_HDLC")
|
||||
var = cg.new_Pvariable(config[CONF_ID], lower)
|
||||
cg.add(var.set_ack_timeout(config[CONF_TIMEOUT]))
|
||||
cg.add(var.set_ack_delay(config[CONF_ACK_DELAY]))
|
||||
cg.add(var.set_max_queue_length(config[CONF_MAX_QUEUE_LENGTH]))
|
||||
cg.add(var.set_max_retries(config[CONF_MAX_RETRIES]))
|
||||
await cg.register_component(var, config)
|
||||
return var
|
||||
@@ -1,50 +0,0 @@
|
||||
"""Encryption layer using XXTEA, with an optional rolling code."""
|
||||
|
||||
import hashlib
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_KEY
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ENCRYPTION, PacketInterface, packet_interface_ns
|
||||
|
||||
CONF_ROLLING_CODE_ENABLE = "rolling_code_enable"
|
||||
|
||||
# A captured packet lets a guessed key be checked offline, so the key text is stretched to make each guess costly.
|
||||
# Both ends derive the same key from the same text, so the salt is fixed; it keeps tables built for other uses of
|
||||
# PBKDF2 from applying here.
|
||||
KEY_SALT = b"esphome packet_interface xxtea"
|
||||
KEY_ITERATIONS = 600_000
|
||||
|
||||
XxteaEncryption = packet_interface_ns.class_("XxteaEncryption", PacketInterface)
|
||||
|
||||
XXTEA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(XxteaEncryption),
|
||||
cv.Required(CONF_KEY): cv.All(cv.sensitive(cv.string), cv.Length(min=1)),
|
||||
cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def derive_key(text: str) -> list[int]:
|
||||
"""The 256-bit key for the key text, as 32-bit words in little endian order."""
|
||||
digest = hashlib.pbkdf2_hmac("sha256", text.encode(), KEY_SALT, KEY_ITERATIONS)
|
||||
return [int.from_bytes(digest[i : i + 4], "little") for i in range(0, 32, 4)]
|
||||
|
||||
|
||||
@ENCRYPTION.register(
|
||||
"xxtea",
|
||||
schema=XXTEA_SCHEMA,
|
||||
# The rolling code accepts a packet only once the peer has sent back this device's boot number
|
||||
needs_session=lambda config: config[CONF_ROLLING_CODE_ENABLE],
|
||||
auto_load=lambda config: ["xxtea"],
|
||||
)
|
||||
async def build_xxtea(config: ConfigType, lower: MockObj) -> MockObj:
|
||||
cg.add_define("USE_PACKET_INTERFACE_XXTEA")
|
||||
var = cg.new_Pvariable(config[CONF_ID], lower, config[CONF_ROLLING_CODE_ENABLE])
|
||||
cg.add(var.set_key(derive_key(config[CONF_KEY])))
|
||||
await cg.register_component(var, config)
|
||||
return var
|
||||
@@ -1,173 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* A buffer class that efficiently represents a composite buffer with three sections:
|
||||
* - pre_buffer: A pointer to uint8_t data with a length
|
||||
* - mid_buffer: A reference to another PacketBuffer (optional)
|
||||
* - post_buffer: A pointer to uint8_t data with a length
|
||||
*
|
||||
* This allows efficient zero-copy composition of buffers without allocating new memory.
|
||||
*
|
||||
* WARNING: PacketBuffer holds raw pointers to data. The user must ensure that the data remains valid
|
||||
* for the lifetime of the PacketBuffer instance. Storing PacketBuffer instances beyond the scope of the data they
|
||||
* reference will lead to dangling pointers and undefined behavior, so to keep a packet, copy it with copy_to() into a
|
||||
* buffer allocated in setup().
|
||||
*/
|
||||
class PacketBuffer {
|
||||
public:
|
||||
PacketBuffer() = default;
|
||||
|
||||
// Disable copy semantics - PacketBuffer contains pointers and copying would create dangling references
|
||||
PacketBuffer(const PacketBuffer &) = delete;
|
||||
PacketBuffer &operator=(const PacketBuffer &) = delete;
|
||||
|
||||
// Enable move semantics
|
||||
PacketBuffer(PacketBuffer &&) = default;
|
||||
PacketBuffer &operator=(PacketBuffer &&) = default;
|
||||
|
||||
/**
|
||||
* Construct from a single buffer.
|
||||
*/
|
||||
PacketBuffer(const uint8_t *data, size_t length) : pre_buffer_(data), pre_length_(length) {}
|
||||
|
||||
/**
|
||||
* Construct from a vector.
|
||||
*/
|
||||
explicit PacketBuffer(const std::vector<uint8_t> &vec) : pre_buffer_(vec.data()), pre_length_(vec.size()) {}
|
||||
// A temporary vector would leave the buffer dangling
|
||||
explicit PacketBuffer(std::vector<uint8_t> &&) = delete;
|
||||
|
||||
/**
|
||||
* Construct with pre and post buffers.
|
||||
*/
|
||||
PacketBuffer(const uint8_t *pre_data, size_t pre_length, const uint8_t *post_data, size_t post_length)
|
||||
: pre_buffer_(pre_data), pre_length_(pre_length), post_buffer_(post_data), post_length_(post_length) {}
|
||||
|
||||
/**
|
||||
* Construct with pre, mid, and post buffers.
|
||||
*/
|
||||
PacketBuffer(const uint8_t *pre_data, size_t pre_length, const PacketBuffer *mid_buffer, const uint8_t *post_data,
|
||||
size_t post_length)
|
||||
: pre_buffer_(pre_data),
|
||||
pre_length_(pre_length),
|
||||
mid_buffer_(mid_buffer),
|
||||
post_buffer_(post_data),
|
||||
post_length_(post_length) {}
|
||||
|
||||
/**
|
||||
* Get the total size of the buffer.
|
||||
*/
|
||||
size_t size() const {
|
||||
size_t total = this->pre_length_ + this->post_length_;
|
||||
if (this->mid_buffer_ != nullptr) {
|
||||
total += this->mid_buffer_->size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the buffer is empty.
|
||||
*/
|
||||
bool empty() const { return this->size() == 0; }
|
||||
|
||||
/**
|
||||
* Check if all the data is held in the pre-buffer, so it can be read as one span.
|
||||
*/
|
||||
bool is_contiguous() const { return this->mid_buffer_ == nullptr && this->post_length_ == 0; }
|
||||
|
||||
/**
|
||||
* Get the data as one span. Only valid when is_contiguous() is true.
|
||||
*/
|
||||
std::span<const uint8_t> as_span() const { return {this->pre_buffer_, this->pre_length_}; }
|
||||
|
||||
/**
|
||||
* Access a byte at a specific position.
|
||||
* @param index The position to access
|
||||
* @return The byte at the specified position, or 0 past the end
|
||||
*/
|
||||
uint8_t operator[](size_t index) const {
|
||||
if (index < this->pre_length_) {
|
||||
return this->pre_buffer_[index];
|
||||
}
|
||||
index -= this->pre_length_;
|
||||
|
||||
if (this->mid_buffer_ != nullptr) {
|
||||
size_t mid_size = this->mid_buffer_->size();
|
||||
if (index < mid_size) {
|
||||
return (*this->mid_buffer_)[index];
|
||||
}
|
||||
index -= mid_size;
|
||||
}
|
||||
|
||||
return index < this->post_length_ ? this->post_buffer_[index] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy data from the buffer to a destination pointer.
|
||||
* @param dest Destination pointer
|
||||
* @param count Number of bytes to copy
|
||||
* @param offset Offset in the source buffer to start copying from
|
||||
* @return Number of bytes actually copied
|
||||
*/
|
||||
size_t copy_to(uint8_t *dest, size_t count, size_t offset = 0) const {
|
||||
size_t total_size = this->size();
|
||||
if (offset >= total_size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t available = total_size - offset;
|
||||
size_t to_copy = count < available ? count : available;
|
||||
size_t copied = 0;
|
||||
|
||||
// Copy from pre_buffer
|
||||
if (offset < this->pre_length_) {
|
||||
size_t pre_offset = offset;
|
||||
size_t pre_available = this->pre_length_ - pre_offset;
|
||||
size_t pre_copy = to_copy < pre_available ? to_copy : pre_available;
|
||||
std::memcpy(dest, this->pre_buffer_ + pre_offset, pre_copy);
|
||||
copied += pre_copy;
|
||||
dest += pre_copy;
|
||||
offset += pre_copy;
|
||||
}
|
||||
|
||||
// Copy from mid_buffer
|
||||
size_t mid_size = this->mid_buffer_ != nullptr ? this->mid_buffer_->size() : 0;
|
||||
if (copied < to_copy && offset < this->pre_length_ + mid_size) {
|
||||
size_t mid_offset = offset - this->pre_length_;
|
||||
size_t mid_available = mid_size - mid_offset;
|
||||
size_t mid_copy = (to_copy - copied) < mid_available ? (to_copy - copied) : mid_available;
|
||||
this->mid_buffer_->copy_to(dest, mid_copy, mid_offset);
|
||||
copied += mid_copy;
|
||||
dest += mid_copy;
|
||||
offset += mid_copy;
|
||||
}
|
||||
|
||||
// Copy from post_buffer
|
||||
if ((copied < to_copy) && (this->post_length_ > 0)) {
|
||||
size_t post_offset = offset - this->pre_length_ - mid_size;
|
||||
size_t post_available = this->post_length_ - post_offset;
|
||||
size_t post_copy = (to_copy - copied) < post_available ? (to_copy - copied) : post_available;
|
||||
std::memcpy(dest, this->post_buffer_ + post_offset, post_copy);
|
||||
copied += post_copy;
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
private:
|
||||
const uint8_t *pre_buffer_{nullptr};
|
||||
size_t pre_length_{0};
|
||||
const PacketBuffer *mid_buffer_{nullptr};
|
||||
const uint8_t *post_buffer_{nullptr};
|
||||
size_t post_length_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,39 +0,0 @@
|
||||
#include "packet_interface.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface");
|
||||
|
||||
// Packets in a row that cannot be understood before the mismatch is logged
|
||||
static constexpr uint8_t MISMATCH_COUNT = 10;
|
||||
|
||||
void PacketInterface::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Packet Interface '%s':\n"
|
||||
" Max Packet Size: %zu",
|
||||
this->name_, this->get_max_packet_size());
|
||||
}
|
||||
|
||||
void PacketInterface::on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
static constexpr size_t LOG_BYTES = 24;
|
||||
uint8_t head[LOG_BYTES];
|
||||
size_t len = data.copy_to(head, LOG_BYTES);
|
||||
ESP_LOGV_HEX(TAG, head, len, LOG_BYTES, "%s: received %zu bytes %s", this->name_, data.size());
|
||||
#endif
|
||||
this->callback_.call(data, meta_data);
|
||||
}
|
||||
|
||||
void PacketInterface::note_received_(bool understood) {
|
||||
if (understood) {
|
||||
this->not_understood_count_ = 0;
|
||||
} else if (this->not_understood_count_ < MISMATCH_COUNT) {
|
||||
this->not_understood_count_++;
|
||||
if (this->not_understood_count_ == MISMATCH_COUNT) {
|
||||
ESP_LOGW(TAG, "%s: %u packets in a row could not be understood; check that both ends have the same layers",
|
||||
this->name_, MISMATCH_COUNT);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,149 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "packet_buffer.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/// The room left for a packet of \p size bytes after \p used bytes, or 0 if it is too small.
|
||||
constexpr size_t remaining_packet_size(size_t size, size_t used) { return size > used ? size - used : 0; }
|
||||
|
||||
/// What a stack of layers provides, as bits of PacketInterface::get_capabilities().
|
||||
enum Capability : uint8_t {
|
||||
CAPABILITY_ENCRYPTION = 1 << 0,
|
||||
CAPABILITY_RELIABILITY = 1 << 1,
|
||||
CAPABILITY_SESSION = 1 << 2,
|
||||
CAPABILITY_ADDRESSING = 1 << 3,
|
||||
CAPABILITY_FRAGMENTATION = 1 << 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* A struct to hold metadata about a packet.
|
||||
* @param info Additional information about the packet
|
||||
* @param source_address With an addressing layer, the sender of a received packet; 0 if not known
|
||||
* @param destination_address With an addressing layer, the destination of a packet; when sending, 0 uses the
|
||||
* configured destination
|
||||
* @param rssi The RSSI of the packet
|
||||
* @param snr The SNR of the packet
|
||||
* @param mac_address The MAC address of the sender/receiver
|
||||
*/
|
||||
class PacketMetaData {
|
||||
public:
|
||||
StringRef info{};
|
||||
uint16_t source_address{0};
|
||||
uint16_t destination_address{0};
|
||||
float rssi{NAN};
|
||||
float snr{NAN};
|
||||
uint8_t mac_address[MAC_ADDRESS_SIZE]{};
|
||||
};
|
||||
/**
|
||||
* A class that will transport data to and from an abstract channel.
|
||||
*
|
||||
* The operations defined are:
|
||||
*
|
||||
* Transmit: the function send_to_interface() takes a PacketBuffer of bytes, and returns when all data is
|
||||
* written or buffered for transmission.
|
||||
*
|
||||
* Receive: The callbacks for an instance of this class are called with currently received data.
|
||||
*/
|
||||
class PacketInterface : public Component {
|
||||
public:
|
||||
/**
|
||||
*
|
||||
* Method to be implemented by child classes to send data to the interface.
|
||||
*
|
||||
* @param data The data to be sent
|
||||
* @param meta_data Metadata about the packet to be sent
|
||||
* @return True if the data was queued for transmission, false if it could not be sent
|
||||
*/
|
||||
virtual bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) = 0;
|
||||
|
||||
/// The largest packet this layer accepts; fixed when the layer is constructed.
|
||||
size_t get_max_packet_size() const { return this->max_packet_size_; }
|
||||
/// How many packets send_to_interface() would accept now, so a layer above can tell before sending a series.
|
||||
virtual size_t get_send_capacity() const { return SIZE_MAX; }
|
||||
/// The capabilities of this layer and every layer below it.
|
||||
uint8_t get_capabilities() const { return this->capabilities_; }
|
||||
bool has_capability(Capability capability) const { return (this->capabilities_ & capability) != 0; }
|
||||
/**
|
||||
* With CAPABILITY_SESSION, the callback is called with true when the peer can be reached, including after it
|
||||
* restarts, and with false when it is lost. The session layer is always the top layer, so register on top().
|
||||
* A reliability layer calls it with false when it resets the link and discards data, which the session layer above
|
||||
* reports as the session lost.
|
||||
*/
|
||||
template<typename F> void add_on_session_state_callback(F &&callback) {
|
||||
this->session_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/// The topmost layer stacked on this one, or this one if nothing is stacked on it.
|
||||
PacketInterface *top() {
|
||||
PacketInterface *layer = this;
|
||||
while (layer->upper_ != nullptr)
|
||||
layer = layer->upper_;
|
||||
return layer;
|
||||
}
|
||||
/// Name shown in log messages; a layer takes the name of the link it is stacked on.
|
||||
void set_name(const char *name) { this->name_ = name; }
|
||||
const char *get_name() const { return this->name_; }
|
||||
|
||||
/**
|
||||
* Add a listener that will be called when data is received.
|
||||
*
|
||||
* @param callback The callback to be called when data is received.
|
||||
*/
|
||||
template<typename F> void add_packet_interface_listener(F &&callback) {
|
||||
this->callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
/// @param max_packet_size The largest packet the layer accepts, set from the layer below or the layer's own buffer
|
||||
explicit PacketInterface(size_t max_packet_size) : max_packet_size_(max_packet_size) {}
|
||||
|
||||
virtual ~PacketInterface() = default;
|
||||
/**
|
||||
* This function should be called by implementing classes with received data to be passed to any listeners.
|
||||
*
|
||||
* @param data The received data
|
||||
* @param meta_data Metadata about the received packet
|
||||
*/
|
||||
void on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data);
|
||||
/**
|
||||
* Called by a layer for each received packet it understood, or could not make sense of. A run of packets that
|
||||
* cannot be understood usually means the peer has different layers configured, which is logged once per run.
|
||||
*/
|
||||
void note_received_(bool understood);
|
||||
/// Called by a layer's constructor to record itself as the layer above \p lower, adding its own capability.
|
||||
void stack_on_(PacketInterface *lower, Capability capability) {
|
||||
lower->upper_ = this;
|
||||
this->inherit_from_(lower, capability);
|
||||
}
|
||||
/// Takes the name and capabilities of `lower` without becoming its top, for one of several stacks on a link.
|
||||
void inherit_from_(PacketInterface *lower, Capability capability) {
|
||||
this->name_ = lower->name_;
|
||||
this->capabilities_ = lower->capabilities_ | capability;
|
||||
// A link that has a session of its own, such as TCP, reports it to whatever is stacked above, as does a
|
||||
// reliability layer that discards data; the session layer handles the latter itself
|
||||
if ((lower->has_capability(CAPABILITY_SESSION) || lower->has_capability(CAPABILITY_RELIABILITY)) &&
|
||||
capability != CAPABILITY_SESSION) {
|
||||
lower->add_on_session_state_callback([this](bool connected) { this->session_callback_.call(connected); });
|
||||
}
|
||||
}
|
||||
|
||||
LazyCallbackManager<void(const PacketBuffer &, const PacketMetaData &)> callback_;
|
||||
LazyCallbackManager<void(bool)> session_callback_;
|
||||
PacketInterface *upper_{nullptr};
|
||||
const char *name_{""};
|
||||
const size_t max_packet_size_;
|
||||
uint8_t capabilities_{0};
|
||||
uint8_t not_understood_count_{0};
|
||||
};
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,28 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.packet_transport import (
|
||||
PacketTransport,
|
||||
new_packet_transport,
|
||||
transport_schema,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_PACKET_INTERFACE, PacketInterface, packet_interface_ns
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["packet_interface"]
|
||||
|
||||
PacketInterfaceTransport = packet_interface_ns.class_(
|
||||
"PacketInterfaceTransport", PacketTransport
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = transport_schema(PacketInterfaceTransport).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_PACKET_INTERFACE): cv.use_id(PacketInterface),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
interface = await cg.get_variable(config[CONF_PACKET_INTERFACE])
|
||||
await new_packet_transport(config, interface)
|
||||
@@ -1,29 +0,0 @@
|
||||
#include "packet_interface_transport.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface.transport");
|
||||
|
||||
void PacketInterfaceTransport::setup() {
|
||||
PacketTransport::setup();
|
||||
this->interface_ = this->interface_->top();
|
||||
this->interface_->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &) { this->receive_(data); });
|
||||
}
|
||||
|
||||
void PacketInterfaceTransport::receive_(const PacketBuffer &data) {
|
||||
// Packet interface layers deliver packets in one piece
|
||||
if (!data.is_contiguous()) {
|
||||
ESP_LOGW(TAG, "Packet split across buffers is not supported");
|
||||
return;
|
||||
}
|
||||
this->process_(data.as_span());
|
||||
}
|
||||
|
||||
void PacketInterfaceTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||
if (!this->interface_->send_to_interface(PacketBuffer(buf), {}))
|
||||
ESP_LOGD(TAG, "Failed to send packet of %zu bytes", buf.size());
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/packet_interface/packet_interface.h"
|
||||
#include "esphome/components/packet_transport/packet_transport.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* A transport protocol adapter that bridges PacketInterface to PacketTransport.
|
||||
* This allows packet_transport functionality to be used with any PacketInterface implementation.
|
||||
*/
|
||||
class PacketInterfaceTransport : public packet_transport::PacketTransport {
|
||||
public:
|
||||
explicit PacketInterfaceTransport(PacketInterface *interface) : interface_(interface) {}
|
||||
|
||||
void setup() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
protected:
|
||||
// implementing PacketTransport virtual methods
|
||||
size_t get_max_packet_size() override { return this->interface_->top()->get_max_packet_size(); }
|
||||
void send_packet(const std::vector<uint8_t> &buf) const override;
|
||||
void receive_(const PacketBuffer &data);
|
||||
|
||||
PacketInterface *interface_;
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,182 +0,0 @@
|
||||
#include "xxtea_encryption.h"
|
||||
#ifdef USE_PACKET_INTERFACE_XXTEA
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "esphome/components/xxtea/xxtea.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface.xxtea");
|
||||
|
||||
static constexpr uint32_t PREF_HASH = fnv1a_hash("packet_interface.xxtea");
|
||||
// Different for each mode, so devices set up differently drop each other's packets
|
||||
static constexpr uint8_t CHECK_BYTES[3] = {0x58, 0x54, 0x45};
|
||||
static constexpr uint8_t ROLLING_CHECK_BYTES[3] = {0x58, 0x54, 0x52};
|
||||
static constexpr size_t CHECK_SIZE = 4;
|
||||
// Boot number, counter, and the echo of the receiver's boot number
|
||||
static constexpr size_t ROLLING_SIZE = 12;
|
||||
// XXTEA needs at least two words
|
||||
static constexpr size_t MIN_LENGTH = 8;
|
||||
static constexpr size_t MAX_PADDING = 4;
|
||||
|
||||
static void put_uint32(uint8_t *buf, uint32_t value) {
|
||||
buf[0] = value;
|
||||
buf[1] = value >> 8;
|
||||
buf[2] = value >> 16;
|
||||
buf[3] = value >> 24;
|
||||
}
|
||||
|
||||
static uint32_t get_uint32(const uint8_t *buf) { return encode_uint32(buf[3], buf[2], buf[1], buf[0]); }
|
||||
|
||||
static constexpr size_t trailer_size(bool rolling_code) {
|
||||
return rolling_code ? ROLLING_SIZE + CHECK_SIZE : CHECK_SIZE;
|
||||
}
|
||||
|
||||
XxteaEncryption::XxteaEncryption(PacketInterface *lower, bool rolling_code)
|
||||
// XXTEA works on whole 32-bit words
|
||||
: PacketInterface(remaining_packet_size(lower->get_max_packet_size() & ~size_t{3}, trailer_size(rolling_code))),
|
||||
lower_(lower),
|
||||
trailer_size_(trailer_size(rolling_code)),
|
||||
rolling_code_(rolling_code) {
|
||||
this->stack_on_(lower, CAPABILITY_ENCRYPTION);
|
||||
}
|
||||
|
||||
void XxteaEncryption::set_key(std::initializer_list<uint32_t> key) {
|
||||
std::copy_n(key.begin(), std::min(key.size(), this->key_.size()), this->key_.begin());
|
||||
}
|
||||
|
||||
void XxteaEncryption::setup() {
|
||||
if (this->max_packet_size_ == 0) {
|
||||
this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
|
||||
return;
|
||||
}
|
||||
size_t words = this->lower_->get_max_packet_size() / 4;
|
||||
RAMAllocator<uint32_t> allocator;
|
||||
this->tx_buffer_ = allocator.allocate(words * 2);
|
||||
if (this->tx_buffer_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Not enough memory for the packet buffers"));
|
||||
return;
|
||||
}
|
||||
this->rx_buffer_ = this->tx_buffer_ + words;
|
||||
if (this->rolling_code_) {
|
||||
this->pref_ = global_preferences->make_preference<uint32_t>(fnv1a_hash_extend(PREF_HASH, this->name_), true);
|
||||
this->pref_.load(&this->boot_);
|
||||
this->next_boot_();
|
||||
}
|
||||
this->lower_->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
|
||||
}
|
||||
|
||||
void XxteaEncryption::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"XXTEA Encryption on '%s':\n"
|
||||
" Max Packet Size: %zu\n"
|
||||
" Rolling Code: %s",
|
||||
this->name_, this->max_packet_size_, YESNO(this->rolling_code_));
|
||||
if (this->rolling_code_) {
|
||||
ESP_LOGCONFIG(TAG, " Boot Number: %" PRIu32, this->boot_);
|
||||
}
|
||||
}
|
||||
|
||||
// A new boot number must be saved before it is used, or a crash could make the peer see it again
|
||||
void XxteaEncryption::next_boot_() {
|
||||
this->boot_++;
|
||||
this->counter_ = 0;
|
||||
if (!this->pref_.save(&this->boot_) || !global_preferences->sync()) {
|
||||
ESP_LOGW(TAG, "%s: Could not save the boot number, so a restart could reuse it", this->name_);
|
||||
}
|
||||
}
|
||||
|
||||
bool XxteaEncryption::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
if (this->tx_buffer_ == nullptr)
|
||||
return false;
|
||||
size_t size = data.size();
|
||||
if (size > this->max_packet_size_) {
|
||||
ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
|
||||
return false;
|
||||
}
|
||||
size_t padding = (4 - (size + this->trailer_size_) % 4) % 4;
|
||||
if (size + padding + this->trailer_size_ < MIN_LENGTH)
|
||||
padding += 4;
|
||||
size_t length = size + padding + this->trailer_size_;
|
||||
auto *buf = reinterpret_cast<uint8_t *>(this->tx_buffer_);
|
||||
data.copy_to(buf, size);
|
||||
memset(buf + size, 0, padding);
|
||||
uint8_t *check = buf + length - CHECK_SIZE;
|
||||
if (this->rolling_code_) {
|
||||
put_uint32(check - ROLLING_SIZE, this->boot_);
|
||||
put_uint32(check - ROLLING_SIZE + 4, this->counter_);
|
||||
put_uint32(check - ROLLING_SIZE + 8, this->echo_boot_);
|
||||
memcpy(check, ROLLING_CHECK_BYTES, sizeof(ROLLING_CHECK_BYTES));
|
||||
if (++this->counter_ == 0)
|
||||
this->next_boot_();
|
||||
} else {
|
||||
memcpy(check, CHECK_BYTES, sizeof(CHECK_BYTES));
|
||||
}
|
||||
check[CHECK_SIZE - 1] = padding;
|
||||
xxtea::encrypt(this->tx_buffer_, length / 4, this->key_.data());
|
||||
return this->lower_->send_to_interface(PacketBuffer(buf, length), meta_data);
|
||||
}
|
||||
|
||||
bool XxteaEncryption::accept_counter_(uint32_t boot, uint32_t counter) {
|
||||
if (this->peer_seen_ && (boot < this->peer_boot_ || (boot == this->peer_boot_ && counter <= this->peer_counter_)))
|
||||
return false;
|
||||
this->peer_seen_ = true;
|
||||
this->peer_boot_ = boot;
|
||||
this->peer_counter_ = counter;
|
||||
return true;
|
||||
}
|
||||
|
||||
void XxteaEncryption::receive_(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
size_t length = data.size();
|
||||
if (length % 4 != 0 || length < std::max(MIN_LENGTH, this->trailer_size_) ||
|
||||
length > this->lower_->get_max_packet_size()) {
|
||||
ESP_LOGV(TAG, "%s: Packet of %zu bytes dropped", this->name_, length);
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
auto *buf = reinterpret_cast<uint8_t *>(this->rx_buffer_);
|
||||
data.copy_to(buf, length);
|
||||
xxtea::decrypt(this->rx_buffer_, length / 4, this->key_.data());
|
||||
const uint8_t *check = buf + length - CHECK_SIZE;
|
||||
const uint8_t *expected = this->rolling_code_ ? ROLLING_CHECK_BYTES : CHECK_BYTES;
|
||||
size_t padding = check[CHECK_SIZE - 1];
|
||||
if (memcmp(check, expected, sizeof(CHECK_BYTES)) != 0 || padding > MAX_PADDING ||
|
||||
padding > length - this->trailer_size_) {
|
||||
ESP_LOGV(TAG, "%s: Packet failed authentication", this->name_);
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
this->note_received_(true);
|
||||
if (this->rolling_code_) {
|
||||
uint32_t boot = get_uint32(check - ROLLING_SIZE);
|
||||
uint32_t counter = get_uint32(check - ROLLING_SIZE + 4);
|
||||
uint32_t echo = get_uint32(check - ROLLING_SIZE + 8);
|
||||
// Learn the peer's boot number even from a packet that is then dropped, so both sides can catch up
|
||||
if (boot > this->echo_boot_)
|
||||
this->echo_boot_ = boot;
|
||||
// A packet recorded before this boot cannot carry this boot's number
|
||||
if (echo != this->boot_) {
|
||||
ESP_LOGV(TAG, "%s: Packet for boot %" PRIu32 " dropped", this->name_, echo);
|
||||
return;
|
||||
}
|
||||
if (!this->accept_counter_(boot, counter)) {
|
||||
// A boot number lower than one already heard means the peer lost its saved boot number, or a replay
|
||||
if (boot < this->peer_boot_ && !this->old_boot_warned_) {
|
||||
this->old_boot_warned_ = true;
|
||||
ESP_LOGW(TAG,
|
||||
"%s: Packets from an older boot %" PRIu32 " of the peer are dropped; if its flash was erased, "
|
||||
"restart this device",
|
||||
this->name_, boot);
|
||||
}
|
||||
ESP_LOGV(TAG, "%s: Old packet %" PRIu32 ":%" PRIu32 " dropped", this->name_, boot, counter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->on_receive_from_interface_(PacketBuffer(buf, length - this->trailer_size_ - padding), meta_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_PACKET_INTERFACE_XXTEA
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "packet_interface.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* An encryption layer that encrypts and authenticates each packet with XXTEA and a 256-bit shared key.
|
||||
*
|
||||
* The packet is padded with zeros and followed by a trailer, then the whole is encrypted as one block. With the
|
||||
* rolling code enabled, the trailer starts with:
|
||||
* - a 32-bit boot number, saved in flash and increased at every boot;
|
||||
* - a 32-bit packet counter, starting at 0 at every boot;
|
||||
* - the highest boot number heard from the peer, echoed back to it.
|
||||
* The trailer always ends with 3 fixed check bytes and the number of padding bytes. XXTEA mixes the whole block, so
|
||||
* a packet that was changed or encrypted with another key fails the check.
|
||||
* With the rolling code, a packet is dropped as a replay unless it echoes this device's current boot number, and its
|
||||
* boot number and counter are higher than those of the last packet accepted from the peer. A boot number is never
|
||||
* reused, so the echo acts as a fresh challenge after every restart.
|
||||
*/
|
||||
class XxteaEncryption : public PacketInterface {
|
||||
public:
|
||||
XxteaEncryption(PacketInterface *lower, bool rolling_code);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) override;
|
||||
|
||||
void set_key(std::initializer_list<uint32_t> key);
|
||||
|
||||
protected:
|
||||
void receive_(const PacketBuffer &data, const PacketMetaData &meta_data);
|
||||
bool accept_counter_(uint32_t boot, uint32_t counter);
|
||||
void next_boot_();
|
||||
|
||||
PacketInterface *lower_;
|
||||
const size_t trailer_size_;
|
||||
std::array<uint32_t, 8> key_{};
|
||||
// Separate buffers, as a layer above may send a reply while a received packet is being passed up; both are in one
|
||||
// block allocated in setup(), and tx_buffer_ stays null if that fails
|
||||
uint32_t *tx_buffer_{nullptr};
|
||||
uint32_t *rx_buffer_{nullptr};
|
||||
ESPPreferenceObject pref_;
|
||||
uint32_t boot_{0};
|
||||
uint32_t counter_{0};
|
||||
uint32_t peer_boot_{0};
|
||||
uint32_t peer_counter_{0};
|
||||
uint32_t echo_boot_{0}; // highest boot number heard from the peer
|
||||
bool peer_seen_{false};
|
||||
bool old_boot_warned_{false};
|
||||
const bool rolling_code_;
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -205,10 +205,8 @@ async def register_packet_transport(var: MockObj, config: ConfigType) -> set[str
|
||||
return providers
|
||||
|
||||
|
||||
async def new_packet_transport(
|
||||
config: ConfigType, *args: Any
|
||||
) -> tuple[MockObj, set[str]]:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
async def new_packet_transport(config: ConfigType) -> tuple[MockObj, set[str]]:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_platform_name(config[CONF_PLATFORM]))
|
||||
providers = await register_packet_transport(var, config)
|
||||
return var, providers
|
||||
@@ -547,11 +547,9 @@ void PacketTransport::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Packet Transport:\n"
|
||||
" Platform: %s\n"
|
||||
" Max Packet Size: %zu\n"
|
||||
" Encrypted: %s\n"
|
||||
" Ping-pong: %s",
|
||||
this->platform_name_, this->get_max_packet_size(), YESNO(this->is_encrypted_()),
|
||||
YESNO(this->ping_pong_enable_));
|
||||
this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_));
|
||||
#ifdef USE_SENSOR
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id);
|
||||
|
||||
@@ -138,7 +138,7 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
const int err = this->start_();
|
||||
if (err != 0)
|
||||
return err;
|
||||
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
|
||||
if (!this->jpeg_.decode(0, 0, 0))
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = size;
|
||||
this->jpeg_.close();
|
||||
@@ -165,28 +165,10 @@ int JpegDecoder::start_() {
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
}
|
||||
const int width = this->jpeg_.getWidth();
|
||||
const int height = this->jpeg_.getHeight();
|
||||
if (!this->set_size(width, height)) {
|
||||
if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
// JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work. Use the smallest size that still has at least as
|
||||
// many pixels as the buffer, so a large image shown small is not decoded in full only to be thrown away.
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
this->scale_ = 1;
|
||||
while (this->scale_ < JPEG_SCALE_EIGHTH && width / (this->scale_ * 2) >= buffer_width &&
|
||||
height / (this->scale_ * 2) >= buffer_height) {
|
||||
this->scale_ *= 2;
|
||||
}
|
||||
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
|
||||
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
|
||||
this->x_scale_ = static_cast<double>(buffer_width) / decoded_width;
|
||||
this->y_scale_ = static_cast<double>(buffer_height) / decoded_height;
|
||||
if (this->scale_ != 1) {
|
||||
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
|
||||
}
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
return 0;
|
||||
@@ -270,7 +252,7 @@ int JpegDecoder::begin_source(DecodeSource *source) {
|
||||
this->source_ = nullptr;
|
||||
return err;
|
||||
}
|
||||
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
|
||||
if (!this->jpeg_.decodeBegin(0, 0, 0))
|
||||
return this->fail_();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -31,16 +31,12 @@ class JpegDecoder : public ImageDecoder {
|
||||
int HOT decode_step() override;
|
||||
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
/// JPEGDEC decodes the image at 1/get_scale() of its size: 1, 2, 4 or 8
|
||||
int get_scale() const { return this->scale_; }
|
||||
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
|
||||
|
||||
protected:
|
||||
bool prepare_rgb565_scaling_(int width, int height);
|
||||
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
|
||||
int start_();
|
||||
/// The JPEGDEC option for scale_: JPEG_SCALE_HALF, _QUARTER and _EIGHTH are the divisors themselves
|
||||
int scale_option_() const { return this->scale_ == 1 ? 0 : this->scale_; }
|
||||
/// The DecodeError for a failed read of the source or JPEGDEC's last error, closing the image
|
||||
int fail_();
|
||||
|
||||
@@ -53,7 +49,6 @@ class JpegDecoder : public ImageDecoder {
|
||||
FixedVector<uint16_t> y_boundaries_{};
|
||||
int source_width_{0};
|
||||
int source_height_{0};
|
||||
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
|
||||
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
|
||||
bool direct_rgb565_{false};
|
||||
bool source_failed_{false};
|
||||
|
||||
@@ -19,52 +19,6 @@ static constexpr uint8_t LOG_LEVEL_TO_SYSLOG_SEVERITY[] PROGMEM = {
|
||||
7 // VERY_VERBOSE
|
||||
};
|
||||
|
||||
static constexpr char SECRET_BEGIN[] = ESPHOME_LOG_SECRET_BEGIN;
|
||||
static constexpr char SECRET_END[] = ESPHOME_LOG_SECRET_END;
|
||||
static constexpr size_t SECRET_BEGIN_LEN = sizeof(SECRET_BEGIN) - 1;
|
||||
static constexpr size_t SECRET_END_LEN = sizeof(SECRET_END) - 1;
|
||||
|
||||
static bool starts_with_marker(const char *message, size_t remaining, const char *marker, size_t marker_len) {
|
||||
return message[0] == '\033' && remaining >= marker_len && memcmp(message, marker, marker_len) == 0;
|
||||
}
|
||||
|
||||
static size_t append_bytes(char *buf, size_t size, size_t pos, const char *data, size_t len) {
|
||||
if (pos >= size) {
|
||||
return size;
|
||||
}
|
||||
size_t n = std::min(len, size - pos - 1);
|
||||
memcpy(buf + pos, data, n);
|
||||
pos += n;
|
||||
buf[pos] = '\0';
|
||||
return pos;
|
||||
}
|
||||
|
||||
size_t append_redacted_message(char *buf, size_t size, size_t pos, const char *message, size_t len) {
|
||||
if (memchr(message, '\033', len) == nullptr) {
|
||||
return append_bytes(buf, size, pos, message, len);
|
||||
}
|
||||
size_t i = 0;
|
||||
while (i < len) {
|
||||
if (starts_with_marker(message + i, len - i, SECRET_BEGIN, SECRET_BEGIN_LEN)) {
|
||||
pos = buf_append_str(buf, size, pos, "[redacted]");
|
||||
i += SECRET_BEGIN_LEN;
|
||||
while (i < len && !starts_with_marker(message + i, len - i, SECRET_END, SECRET_END_LEN)) {
|
||||
i++;
|
||||
}
|
||||
if (i < len) {
|
||||
i += SECRET_END_LEN;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
size_t start = i;
|
||||
while (i < len && !starts_with_marker(message + i, len - i, SECRET_BEGIN, SECRET_BEGIN_LEN)) {
|
||||
i++;
|
||||
}
|
||||
pos = append_bytes(buf, size, pos, message + start, i - start);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
void Syslog::setup() {
|
||||
logger::global_logger->add_log_callback(
|
||||
this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
@@ -117,15 +71,16 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
|
||||
} else {
|
||||
offset = buf_append_str(packet, sizeof(packet), offset, "-");
|
||||
}
|
||||
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s - - - ", App.get_name().c_str(), tag);
|
||||
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s - - - %.*s", App.get_name().c_str(), tag,
|
||||
(int) len, message);
|
||||
} else {
|
||||
// RFC 3164 has no NILVALUE. If the clock is invalid, omit TIMESTAMP so a relay can add it.
|
||||
if (now.is_valid()) {
|
||||
offset += now.strftime(packet + offset, sizeof(packet) - offset, "%b %e %H:%M:%S ");
|
||||
}
|
||||
offset = buf_append_printf(packet, sizeof(packet), offset, "%s %s: ", App.get_name().c_str(), tag);
|
||||
offset = buf_append_printf(packet, sizeof(packet), offset, "%s %s: %.*s", App.get_name().c_str(), tag, (int) len,
|
||||
message);
|
||||
}
|
||||
offset = append_redacted_message(packet, sizeof(packet), offset, message, len);
|
||||
// Clamp to exclude null terminator position if buffer was filled
|
||||
if (offset >= sizeof(packet)) {
|
||||
offset = sizeof(packet) - 1;
|
||||
|
||||
@@ -12,10 +12,6 @@ enum SyslogFormat : uint8_t {
|
||||
SYSLOG_FORMAT_RFC5424,
|
||||
};
|
||||
|
||||
/// Append len bytes of message to buf at pos, replacing every LOG_SECRET() region with "[redacted]".
|
||||
/// Returns the new position, capped at size like the other buf_append helpers.
|
||||
size_t append_redacted_message(char *buf, size_t size, size_t pos, const char *message, size_t len);
|
||||
|
||||
class Syslog final : public Component, public Parented<udp::UDPComponent> {
|
||||
public:
|
||||
Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import packet_interface, uart
|
||||
from esphome.components.packet_interface import (
|
||||
CONF_MAX_PACKET_SIZE,
|
||||
packet_interface_schema,
|
||||
)
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import uart_ns
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
UartPacketInterface = uart_ns.class_(
|
||||
"UartPacketInterface",
|
||||
packet_interface.PacketInterface,
|
||||
uart.UARTDevice,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = packet_interface_schema(UartPacketInterface, 508).extend(
|
||||
uart.UART_DEVICE_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# The receive buffer's size is known here, so it is a static array rather than allocated at run time
|
||||
rx_buffer = f"{config[CONF_ID].id}__rx_buffer"
|
||||
cg.add_global(
|
||||
cg.RawStatement(f"static uint8_t {rx_buffer}[{config[CONF_MAX_PACKET_SIZE]}];")
|
||||
)
|
||||
var = await packet_interface.new_packet_interface(
|
||||
config, cg.RawExpression(rx_buffer)
|
||||
)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -1,82 +0,0 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "uart_packet_interface.h"
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
using packet_interface::PacketBuffer;
|
||||
using packet_interface::PacketMetaData;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "uart.packet_interface");
|
||||
|
||||
static constexpr uint8_t FLAG_BYTE = 0x7E;
|
||||
static constexpr uint8_t CONTROL_BYTE = 0x7D;
|
||||
static constexpr uint8_t ESCAPE_XOR = 0x20;
|
||||
// Raw bytes escaped and written per UART write
|
||||
static constexpr size_t TX_CHUNK_SIZE = 32;
|
||||
|
||||
void UartPacketInterface::loop() {
|
||||
this->read_available([this](uint8_t byte) { this->handle_rx_byte_(byte); });
|
||||
}
|
||||
|
||||
void UartPacketInterface::handle_rx_byte_(uint8_t byte) {
|
||||
if (byte == FLAG_BYTE) {
|
||||
// A flag that ends a frame also starts the next, so an unfinished frame cannot hide the one after it
|
||||
if (this->rx_started_ && this->rx_length_ != 0)
|
||||
this->on_receive_from_interface_(PacketBuffer(this->rx_buffer_.data(), this->rx_length_), {});
|
||||
this->rx_started_ = true;
|
||||
this->rx_length_ = 0;
|
||||
this->rx_control_ = false;
|
||||
return;
|
||||
}
|
||||
if (!this->rx_started_)
|
||||
return;
|
||||
if (byte == CONTROL_BYTE) {
|
||||
this->rx_control_ = true;
|
||||
return;
|
||||
}
|
||||
if (this->rx_control_) {
|
||||
byte ^= ESCAPE_XOR;
|
||||
this->rx_control_ = false;
|
||||
}
|
||||
if (this->rx_length_ >= this->rx_buffer_.size()) {
|
||||
ESP_LOGD(TAG, "Packet too large, discarding");
|
||||
this->rx_started_ = false;
|
||||
this->rx_length_ = 0;
|
||||
return;
|
||||
}
|
||||
this->rx_buffer_[this->rx_length_++] = byte;
|
||||
}
|
||||
|
||||
bool UartPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
size_t size = data.size();
|
||||
if (size == 0)
|
||||
return true;
|
||||
if (size > this->max_packet_size_) {
|
||||
ESP_LOGW(TAG, "Packet of %zu bytes is larger than %zu", size, this->max_packet_size_);
|
||||
return false;
|
||||
}
|
||||
uint8_t raw[TX_CHUNK_SIZE]{};
|
||||
// Every byte may be escaped, plus a flag byte at each end
|
||||
uint8_t out[TX_CHUNK_SIZE * 2 + 2];
|
||||
size_t out_len = 0;
|
||||
out[out_len++] = FLAG_BYTE;
|
||||
for (size_t offset = 0; offset < size;) {
|
||||
size_t count = data.copy_to(raw, TX_CHUNK_SIZE, offset);
|
||||
offset += count;
|
||||
for (size_t i = 0; i != count; i++) {
|
||||
uint8_t byte = raw[i];
|
||||
if (byte == FLAG_BYTE || byte == CONTROL_BYTE) {
|
||||
out[out_len++] = CONTROL_BYTE;
|
||||
byte ^= ESCAPE_XOR;
|
||||
}
|
||||
out[out_len++] = byte;
|
||||
}
|
||||
if (offset == size)
|
||||
out[out_len++] = FLAG_BYTE;
|
||||
this->write_array(out, out_len);
|
||||
out_len = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::uart
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "esphome/components/packet_interface/packet_interface.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
/**
|
||||
* A packet interface for sending and receiving framed packets over a UART connection.
|
||||
* Uses simple byte-stuffing framing with FLAG_BYTE and CONTROL_BYTE.
|
||||
* The protocol wraps data between FLAG_BYTEs.
|
||||
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
|
||||
* XORed with 0x20.
|
||||
*/
|
||||
class UartPacketInterface : public packet_interface::PacketInterface, public UARTDevice {
|
||||
public:
|
||||
/// @param rx_buffer Holds the unescaped data being received; its size is the largest packet, and it must outlive
|
||||
/// the link
|
||||
explicit UartPacketInterface(std::span<uint8_t> rx_buffer)
|
||||
: packet_interface::PacketInterface(rx_buffer.size()), rx_buffer_(rx_buffer) {}
|
||||
|
||||
void loop() override;
|
||||
bool send_to_interface(const packet_interface::PacketBuffer &data,
|
||||
const packet_interface::PacketMetaData &meta_data) override;
|
||||
|
||||
protected:
|
||||
void handle_rx_byte_(uint8_t byte);
|
||||
const std::span<uint8_t> rx_buffer_;
|
||||
size_t rx_length_{0};
|
||||
bool rx_started_{};
|
||||
bool rx_control_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::uart
|
||||
@@ -6,10 +6,6 @@ namespace esphome::uart {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "uart_transport");
|
||||
|
||||
static constexpr uint16_t MAX_PACKET_SIZE = 508;
|
||||
static constexpr uint8_t FLAG_BYTE = 0x7E;
|
||||
static constexpr uint8_t CONTROL_BYTE = 0x7D;
|
||||
|
||||
void UARTTransport::loop() {
|
||||
PacketTransport::loop();
|
||||
|
||||
@@ -82,5 +78,4 @@ void UARTTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||
this->parent_->write_byte(FLAG_BYTE);
|
||||
}
|
||||
|
||||
size_t UARTTransport::get_max_packet_size() { return MAX_PACKET_SIZE; }
|
||||
} // namespace esphome::uart
|
||||
@@ -16,6 +16,9 @@ namespace esphome::uart {
|
||||
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
|
||||
* XORed with 0x20.
|
||||
*/
|
||||
static const uint16_t MAX_PACKET_SIZE = 508;
|
||||
static const uint8_t FLAG_BYTE = 0x7E;
|
||||
static const uint8_t CONTROL_BYTE = 0x7D;
|
||||
|
||||
class UARTTransport final : public packet_transport::PacketTransport, public UARTDevice {
|
||||
public:
|
||||
@@ -26,7 +29,7 @@ class UARTTransport final : public packet_transport::PacketTransport, public UAR
|
||||
void write_byte_(uint8_t byte) const;
|
||||
void send_packet(const std::vector<uint8_t> &buf) const override;
|
||||
bool should_send() override { return true; };
|
||||
size_t get_max_packet_size() override;
|
||||
size_t get_max_packet_size() override { return MAX_PACKET_SIZE; }
|
||||
std::vector<uint8_t> receive_buffer_{};
|
||||
bool rx_started_{};
|
||||
bool rx_control_{};
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import packet_interface
|
||||
from esphome.components.packet_interface import (
|
||||
CONF_MAX_PACKET_SIZE,
|
||||
new_packet_interface,
|
||||
packet_interface_schema,
|
||||
)
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_UDP_ID, UDP_SCHEMA, udp_ns
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
DEPENDENCIES = ["udp"]
|
||||
|
||||
# Size of the receive buffer of the udp component
|
||||
MAX_PACKET_SIZE = 508
|
||||
|
||||
UdpPacketInterface = udp_ns.class_(
|
||||
"UdpPacketInterface",
|
||||
packet_interface.PacketInterface,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = packet_interface_schema(
|
||||
UdpPacketInterface, MAX_PACKET_SIZE, MAX_PACKET_SIZE
|
||||
).extend(UDP_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
udp_var = await cg.get_variable(config[CONF_UDP_ID])
|
||||
cg.add(udp_var.set_should_listen())
|
||||
# The udp component sends to its addresses through its broadcast socket
|
||||
cg.add(udp_var.set_should_broadcast())
|
||||
# The copy buffer's size is known here, so it is a static array rather than allocated at run time
|
||||
tx_buffer = f"{config[CONF_ID].id}__tx_buffer"
|
||||
cg.add_global(
|
||||
cg.RawStatement(f"static uint8_t {tx_buffer}[{config[CONF_MAX_PACKET_SIZE]}];")
|
||||
)
|
||||
await new_packet_interface(config, udp_var, cg.RawExpression(tx_buffer))
|
||||
@@ -1,43 +0,0 @@
|
||||
#include "udp_packet_interface.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::udp {
|
||||
|
||||
using packet_interface::PacketBuffer;
|
||||
using packet_interface::PacketMetaData;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "udp.packet_interface");
|
||||
|
||||
void UdpPacketInterface::setup() {
|
||||
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->receive_(data); });
|
||||
}
|
||||
|
||||
void UdpPacketInterface::receive_(std::span<const uint8_t> data) {
|
||||
if (data.size() > this->max_packet_size_) {
|
||||
ESP_LOGV(TAG, "%s: Packet of %zu bytes is larger than %zu, dropped", this->name_, data.size(),
|
||||
this->max_packet_size_);
|
||||
return;
|
||||
}
|
||||
this->on_receive_from_interface_(PacketBuffer(data.data(), data.size()), {});
|
||||
}
|
||||
|
||||
bool UdpPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
size_t size = data.size();
|
||||
if (size > this->max_packet_size_) {
|
||||
ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
|
||||
return false;
|
||||
}
|
||||
if (!network::is_connected())
|
||||
return false;
|
||||
if (data.is_contiguous()) {
|
||||
auto span = data.as_span();
|
||||
return this->parent_->send_packet(span.data(), span.size());
|
||||
}
|
||||
data.copy_to(this->tx_buffer_.data(), size);
|
||||
return this->parent_->send_packet(this->tx_buffer_.data(), size);
|
||||
}
|
||||
|
||||
} // namespace esphome::udp
|
||||
#endif
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
// udp_component.h declares nothing without USE_NETWORK
|
||||
#ifdef USE_NETWORK
|
||||
#include <span>
|
||||
|
||||
#include "esphome/components/packet_interface/packet_interface.h"
|
||||
#include "../udp_component.h"
|
||||
|
||||
namespace esphome::udp {
|
||||
|
||||
/**
|
||||
* A packet interface link over UDP: each packet is one datagram, sent to every address of the UDP component.
|
||||
*
|
||||
* Every datagram received on the listen port is passed up, from any sender, so on a shared network an addressing
|
||||
* or encryption layer is needed to tell the peer's packets from others.
|
||||
*/
|
||||
class UdpPacketInterface : public packet_interface::PacketInterface {
|
||||
public:
|
||||
/// @param tx_buffer Joins packets split across buffers; its size is the largest packet, and it must outlive the link
|
||||
UdpPacketInterface(UDPComponent *udp_component, std::span<uint8_t> tx_buffer)
|
||||
: packet_interface::PacketInterface(tx_buffer.size()), parent_(udp_component), tx_buffer_(tx_buffer) {}
|
||||
|
||||
void setup() override;
|
||||
bool send_to_interface(const packet_interface::PacketBuffer &data,
|
||||
const packet_interface::PacketMetaData &meta_data) override;
|
||||
|
||||
protected:
|
||||
void receive_(std::span<const uint8_t> data);
|
||||
|
||||
UDPComponent *parent_;
|
||||
const std::span<uint8_t> tx_buffer_;
|
||||
};
|
||||
} // namespace esphome::udp
|
||||
#endif
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "udp_transport.h"
|
||||
|
||||
namespace esphome::udp {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "udp_transport");
|
||||
|
||||
bool UDPTransport::should_send() { return network::is_connected(); }
|
||||
void UDPTransport::setup() {
|
||||
PacketTransport::setup();
|
||||
|
||||
@@ -205,25 +205,18 @@ void UDPComponent::set_poll_warning_(PollStep step, const LogString *what) {
|
||||
}
|
||||
|
||||
void UDPComponent::loop() {
|
||||
if (!this->should_listen_) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
// One byte more than the largest packet, as a read cuts a longer datagram to fit and so cannot report it
|
||||
std::array<uint8_t, MAX_PACKET_SIZE + 1> buf;
|
||||
for (;;) {
|
||||
if (!this->listen_socket_->ready())
|
||||
break;
|
||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||
if (len <= 0)
|
||||
break;
|
||||
size_t packet_len = static_cast<size_t>(len);
|
||||
if (packet_len > MAX_PACKET_SIZE) {
|
||||
ESP_LOGV(TAG, "Packet longer than %zu bytes dropped", MAX_PACKET_SIZE);
|
||||
continue;
|
||||
if (this->should_listen_) {
|
||||
std::array<uint8_t, MAX_PACKET_SIZE> buf;
|
||||
for (;;) {
|
||||
if (!this->listen_socket_->ready())
|
||||
break;
|
||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||
if (len <= 0)
|
||||
break;
|
||||
size_t packet_len = static_cast<size_t>(len);
|
||||
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
|
||||
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
|
||||
}
|
||||
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
|
||||
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +239,9 @@ void UDPComponent::dump_config() {
|
||||
YESNO(this->should_broadcast_), YESNO(this->should_listen_));
|
||||
}
|
||||
|
||||
bool UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
if (this->is_failed())
|
||||
return false; // a send socket may be missing
|
||||
bool sent = true;
|
||||
return; // a send socket may be missing
|
||||
for (const auto &entry : this->sockaddrs_) {
|
||||
#ifdef USE_UDP_IPV6_SEND
|
||||
const bool v6 = entry.sa.sa_family == AF_INET6;
|
||||
@@ -264,23 +256,11 @@ bool UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
socket::UDPSendSocket *sock = this->broadcast_socket_.get();
|
||||
socklen_t sockaddr_size = sizeof(sockaddr_in);
|
||||
#endif
|
||||
if (sock == nullptr) {
|
||||
ESP_LOGW(TAG, "Broadcast socket not initialized");
|
||||
return false;
|
||||
}
|
||||
auto result = sock->sendto(data, size, 0, &entry.sa, sockaddr_size);
|
||||
if (result < 0) {
|
||||
// lwIP refuses a packet with ENOMEM while the destination's ARP lookup is pending and its queue is full;
|
||||
// the packet can be sent again later
|
||||
if (errno == ENOMEM) {
|
||||
ESP_LOGV(TAG, "sendto() out of memory");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "sendto() error %d", errno);
|
||||
}
|
||||
sent = false;
|
||||
ESP_LOGW(TAG, "sendto() error %d", errno);
|
||||
}
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
} // namespace esphome::udp
|
||||
|
||||
|
||||
@@ -35,9 +35,8 @@ class UDPComponent final : public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
/// Returns false if the packet could not be sent to every destination.
|
||||
bool send_packet(const uint8_t *data, size_t size);
|
||||
bool send_packet(const std::vector<uint8_t> &buf) { return this->send_packet(buf.data(), buf.size()); }
|
||||
void send_packet(const uint8_t *data, size_t size);
|
||||
void send_packet(const std::vector<uint8_t> &buf) { this->send_packet(buf.data(), buf.size()); }
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; };
|
||||
|
||||
protected:
|
||||
@@ -54,7 +53,7 @@ class UDPComponent final : public Component {
|
||||
bool network_poll_();
|
||||
void set_poll_warning_(PollStep step, const LogString *what);
|
||||
|
||||
LazyCallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
|
||||
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
|
||||
std::unique_ptr<socket::UDPSendSocket> broadcast_socket_ = nullptr;
|
||||
std::unique_ptr<socket::UDPSocket> listen_socket_ = nullptr;
|
||||
#ifdef USE_UDP_IPV6_SEND
|
||||
|
||||
@@ -266,9 +266,6 @@
|
||||
#define USE_OTA_STATE_LISTENER
|
||||
#define USE_OUTPUT
|
||||
#define USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
#define USE_PACKET_INTERFACE_BASIC_SESSION
|
||||
#define USE_PACKET_INTERFACE_HDLC
|
||||
#define USE_PACKET_INTERFACE_XXTEA
|
||||
#define USE_PCA9554_PCAL
|
||||
#define USE_POWER_SUPPLY
|
||||
// Only defined by key-lookup preference backends; the slot-based platforms
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Collects fixed length binary frames of N bytes that start with the header bytes HEADER...; the binary twin of
|
||||
/// LineReader. Bytes before a header are skipped; checksums are left to the caller.
|
||||
template<size_t N, uint8_t... HEADER> class FrameReader {
|
||||
using pos_t = std::conditional_t<(N <= 0xFF), uint8_t, uint16_t>;
|
||||
static constexpr size_t HEADER_LEN = sizeof...(HEADER);
|
||||
static_assert(HEADER_LEN >= 1 && HEADER_LEN <= 8, "FrameReader takes 1 to 8 header bytes"); // bounds replay_() depth
|
||||
static_assert(HEADER_LEN < N && N <= 0xFFFF, "FrameReader size out of range");
|
||||
|
||||
public:
|
||||
/// Feed one byte; returns the completed frame, header included, valid until the next call, or nullptr.
|
||||
const uint8_t *feed(uint8_t c) {
|
||||
pos_t pos = this->pos_;
|
||||
if (pos < HEADER_LEN && c != header_byte(pos)) {
|
||||
// A later byte of the broken header, or c, may start the real one
|
||||
this->buf_[pos] = c;
|
||||
this->replay_(pos);
|
||||
return nullptr;
|
||||
}
|
||||
this->buf_[pos++] = c;
|
||||
if (pos < N) {
|
||||
this->pos_ = pos;
|
||||
return nullptr;
|
||||
}
|
||||
this->pos_ = 0;
|
||||
return this->buf_;
|
||||
}
|
||||
|
||||
/// Number of bytes of the partial frame; 0 after a frame is returned and while looking for a header.
|
||||
size_t length() const { return this->pos_; }
|
||||
|
||||
/// Forget the partial frame and look for a header from the next byte.
|
||||
void reset() { this->pos_ = 0; }
|
||||
|
||||
/// For a frame the caller rejected (bad checksum): drop its first byte and realign on a header in the rest.
|
||||
/// Call only right after feed() returned that frame; use reset() otherwise.
|
||||
void resync() { this->replay_(N - 1); }
|
||||
|
||||
protected:
|
||||
// A fold, not an array, so no header table lands in RAM on ESP8266
|
||||
static constexpr uint8_t header_byte(size_t i) {
|
||||
size_t j = 0;
|
||||
uint8_t r = 0;
|
||||
((r = (j++ == i) ? HEADER : r), ...);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Start over and feed buf_[1..last] again; never completes a frame as last < N
|
||||
void replay_(size_t last) {
|
||||
this->pos_ = 0;
|
||||
for (size_t i = 1; i <= last; i++) {
|
||||
this->feed(this->buf_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t buf_[N];
|
||||
pos_t pos_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
@@ -29,14 +29,12 @@ class HashBase {
|
||||
void get_hex(char *output) { format_hex_to(output, this->get_size() * 2 + 1, this->digest_, this->get_size()); }
|
||||
|
||||
/// Compare the hash against a provided byte-encoded hash
|
||||
bool equals_bytes(const uint8_t *expected) { return constant_time_equals(this->digest_, expected, this->get_size()); }
|
||||
bool equals_bytes(const uint8_t *expected) { return memcmp(this->digest_, expected, this->get_size()) == 0; }
|
||||
|
||||
/// Compare the hash against a provided hex-encoded hash of get_size() * 2 characters, which need not be
|
||||
/// null-terminated
|
||||
/// Compare the hash against a provided hex-encoded hash
|
||||
bool equals_hex(const char *expected) {
|
||||
uint8_t parsed[32]; // Fixed size for max hash (SHA256 = 32 bytes)
|
||||
const size_t size = this->get_size();
|
||||
if (parse_hex(expected, size * 2, parsed, size) != size * 2) {
|
||||
if (!parse_hex(expected, parsed, this->get_size())) {
|
||||
return false;
|
||||
}
|
||||
return this->equals_bytes(parsed);
|
||||
|
||||
@@ -1112,16 +1112,6 @@ template<typename T> constexpr T convert_little_endian(T val) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Compare len bytes of a and b in a time that does not depend on where they first differ.
|
||||
/// Use this for digests, passwords and tokens, where memcmp's early exit leaks how many bytes matched.
|
||||
inline bool constant_time_equals(const uint8_t *a, const uint8_t *b, size_t len) {
|
||||
volatile uint8_t diff = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
diff = diff | (a[i] ^ b[i]);
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
///@}
|
||||
|
||||
/// @name Strings
|
||||
|
||||
@@ -188,8 +188,6 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict:
|
||||
|
||||
The ``esphome:`` key is special: its sub-keys are merged into the
|
||||
existing esphome config (e.g. to add ``areas:`` or ``devices:``).
|
||||
A list, such as the entries of a platform component, is extended, so
|
||||
several directories can each add their own entries to one key.
|
||||
Keys already present in the base config (e.g. ``host:``, ``logger:``)
|
||||
are protected by ``setdefault`` in the caller.
|
||||
|
||||
@@ -215,9 +213,6 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict:
|
||||
for sub_key, sub_value in value.items():
|
||||
esphome_extra.setdefault(sub_key, sub_value)
|
||||
continue
|
||||
if isinstance(value, list) and isinstance(merged.get(key), list):
|
||||
merged[key] = merged[key] + value
|
||||
continue
|
||||
merged.setdefault(key, value)
|
||||
return merged
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <vector>
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
using Frames = std::vector<Bytes>;
|
||||
|
||||
// Feeds bytes and collects every completed frame.
|
||||
template<size_t N, uint8_t... H> static Frames feed_all(FrameReader<N, H...> &reader, const Bytes &input) {
|
||||
Frames frames;
|
||||
for (uint8_t c : input) {
|
||||
const uint8_t *frame = reader.feed(c);
|
||||
if (frame != nullptr) {
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
frames.emplace_back(frame, frame + N);
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
static_assert(sizeof(FrameReader<4, 0xFF>) == 5);
|
||||
static_assert(sizeof(FrameReader<9, 0x55, 0x01, 0x01, 0x04>) == 10);
|
||||
static_assert(sizeof(FrameReader<0xFF, 0xAA>) == 0x100);
|
||||
static_assert(sizeof(FrameReader<0x100, 0xAA>) == 0x102);
|
||||
|
||||
TEST(FrameReaderTest, SingleByteHeader) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xFF, 0x01, 0x02, 0x02, 0xFF, 0x03, 0x04, 0x06}),
|
||||
(Frames{{0xFF, 0x01, 0x02, 0x02}, {0xFF, 0x03, 0x04, 0x06}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, SkipsBytesBeforeHeader) {
|
||||
FrameReader<3, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0x01, 0x02, 0xFF, 0x10, 0x20}), (Frames{{0xFF, 0x10, 0x20}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, HeaderByteInsidePayloadIsData) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xFF, 0xFF, 0xFF, 0x00}), (Frames{{0xFF, 0xFF, 0xFF, 0x00}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, LengthTracksPartialFrame) {
|
||||
FrameReader<4, 0xAA, 0xBB> reader;
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
reader.feed(0x00);
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
reader.feed(0xAA);
|
||||
EXPECT_EQ(reader.length(), 1u);
|
||||
reader.feed(0xBB);
|
||||
reader.feed(0x01);
|
||||
EXPECT_EQ(reader.length(), 3u);
|
||||
reader.reset();
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
EXPECT_EQ(feed_all(reader, {0x02, 0xAA, 0xBB, 0x03, 0x04}), (Frames{{0xAA, 0xBB, 0x03, 0x04}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, MultiByteHeaderMismatchRestarts) {
|
||||
FrameReader<6, 0x55, 0x01, 0x01, 0x04> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0x55, 0x01, 0x02, 0x55, 0x01, 0x01, 0x04, 0x11, 0x22}),
|
||||
(Frames{{0x55, 0x01, 0x01, 0x04, 0x11, 0x22}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, MismatchedByteCanStartHeader) {
|
||||
FrameReader<6, 0x55, 0x01, 0x01, 0x04> reader;
|
||||
// The second 0x55 breaks the first header and starts the real one
|
||||
EXPECT_EQ(feed_all(reader, {0x55, 0x01, 0x55, 0x01, 0x01, 0x04, 0x11, 0x22}),
|
||||
(Frames{{0x55, 0x01, 0x01, 0x04, 0x11, 0x22}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, RepeatedHeaderPrefixIsKept) {
|
||||
// AA AA AA BB: the mismatch at the third byte still leaves AA AA matched
|
||||
FrameReader<4, 0xAA, 0xAA, 0xBB> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xAA, 0xAA, 0xAA, 0xBB, 0x01}), (Frames{{0xAA, 0xAA, 0xBB, 0x01}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, OverlappingHeaderPrefix) {
|
||||
// A1 B2 A1 B2: the mismatch on the last B2 keeps the trailing A1 B2 as the start of the header
|
||||
FrameReader<5, 0xA1, 0xB2, 0xA1, 0xC3> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xA1, 0xB2, 0xA1, 0xB2, 0xA1, 0xC3, 0x07}), (Frames{{0xA1, 0xB2, 0xA1, 0xC3, 0x07}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncFindsHeaderInRejectedFrame) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
// Started mid-frame: the 0xFF taken as a header was payload; the real frame starts at the second 0xFF
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0xFF, 0x12, 0xFF, 0x01}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 2u);
|
||||
EXPECT_EQ(feed_all(reader, {0x02, 0x02}), (Frames{{0xFF, 0x01, 0x02, 0x02}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncWithoutHeaderStartsOver) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0xFF, 0x01, 0x02, 0x03}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncKeepsPartialHeaderAtEnd) {
|
||||
FrameReader<5, 0x3C, 0x32> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0x3C, 0x32, 0x00, 0x00, 0x3C}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 1u);
|
||||
EXPECT_EQ(feed_all(reader, {0x32, 0x01, 0x02, 0x03}), (Frames{{0x3C, 0x32, 0x01, 0x02, 0x03}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncMultiByteHeader) {
|
||||
FrameReader<6, 0x3C, 0x32> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0x3C, 0x32, 0x3C, 0x3C, 0x32, 0x09}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 3u);
|
||||
EXPECT_EQ(feed_all(reader, {0x0A, 0x0B, 0x0C}), (Frames{{0x3C, 0x32, 0x09, 0x0A, 0x0B, 0x0C}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, SixteenBitIndex) {
|
||||
FrameReader<300, 0xAA, 0x55> reader;
|
||||
Bytes input{0x00, 0xAA, 0x55};
|
||||
input.resize(input.size() + 298, 0x42);
|
||||
Frames frames = feed_all(reader, input);
|
||||
ASSERT_EQ(frames.size(), 1u);
|
||||
EXPECT_EQ(frames[0][0], 0xAA);
|
||||
EXPECT_EQ(frames[0][1], 0x55);
|
||||
EXPECT_EQ(frames[0][299], 0x42);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -1,39 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
TEST(ConstantTimeEquals, EqualBuffers) {
|
||||
const uint8_t a[] = {0x00, 0x01, 0x7F, 0x80, 0xFF};
|
||||
const uint8_t b[] = {0x00, 0x01, 0x7F, 0x80, 0xFF};
|
||||
EXPECT_TRUE(constant_time_equals(a, b, sizeof(a)));
|
||||
}
|
||||
|
||||
TEST(ConstantTimeEquals, FirstByteDiffers) {
|
||||
const uint8_t a[] = {0x00, 0x01, 0x02, 0x03};
|
||||
const uint8_t b[] = {0x10, 0x01, 0x02, 0x03};
|
||||
EXPECT_FALSE(constant_time_equals(a, b, sizeof(a)));
|
||||
}
|
||||
|
||||
TEST(ConstantTimeEquals, LastByteDiffers) {
|
||||
const uint8_t a[] = {0x00, 0x01, 0x02, 0x03};
|
||||
const uint8_t b[] = {0x00, 0x01, 0x02, 0x13};
|
||||
EXPECT_FALSE(constant_time_equals(a, b, sizeof(a)));
|
||||
}
|
||||
|
||||
TEST(ConstantTimeEquals, SingleBitDifference) {
|
||||
const uint8_t a[] = {0xAA, 0xAA};
|
||||
const uint8_t b[] = {0xAA, 0xAB};
|
||||
EXPECT_FALSE(constant_time_equals(a, b, sizeof(a)));
|
||||
}
|
||||
|
||||
TEST(ConstantTimeEquals, ZeroLengthIsEqual) {
|
||||
const uint8_t a[] = {0x01};
|
||||
const uint8_t b[] = {0x02};
|
||||
EXPECT_TRUE(constant_time_equals(a, b, 0));
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
@@ -1,54 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hash_base.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// A four byte "hash" whose digest is set directly, so the compare helpers can be tested on their own
|
||||
class FixedHash : public HashBase {
|
||||
public:
|
||||
void init() override {}
|
||||
void add(const uint8_t *data, size_t len) override {}
|
||||
void calculate() override {}
|
||||
size_t get_size() const override { return 4; }
|
||||
void set_digest(const uint8_t *digest) { memcpy(this->digest_, digest, 4); }
|
||||
};
|
||||
|
||||
static const uint8_t DIGEST[] = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
|
||||
TEST(HashBaseEquals, BytesMatch) {
|
||||
FixedHash hash;
|
||||
hash.set_digest(DIGEST);
|
||||
EXPECT_TRUE(hash.equals_bytes(DIGEST));
|
||||
const uint8_t other[] = {0xDE, 0xAD, 0xBE, 0xEE};
|
||||
EXPECT_FALSE(hash.equals_bytes(other));
|
||||
}
|
||||
|
||||
TEST(HashBaseEquals, HexMatchEitherCase) {
|
||||
FixedHash hash;
|
||||
hash.set_digest(DIGEST);
|
||||
EXPECT_TRUE(hash.equals_hex("deadbeef"));
|
||||
EXPECT_TRUE(hash.equals_hex("DEADBEEF"));
|
||||
EXPECT_FALSE(hash.equals_hex("deadbeee"));
|
||||
}
|
||||
|
||||
TEST(HashBaseEquals, HexWithoutTerminatorOnlyReadsDigestLength) {
|
||||
FixedHash hash;
|
||||
hash.set_digest(DIGEST);
|
||||
// The expected digest sits in a larger buffer with no terminator after it, as in the OTA auth buffer
|
||||
const char buffer[] = {'d', 'e', 'a', 'd', 'b', 'e', 'e', 'f', 'z', 'z', 'z', 'z'};
|
||||
EXPECT_TRUE(hash.equals_hex(buffer));
|
||||
}
|
||||
|
||||
TEST(HashBaseEquals, ShortOrInvalidHexRejected) {
|
||||
FixedHash hash;
|
||||
hash.set_digest(DIGEST);
|
||||
EXPECT_FALSE(hash.equals_hex("deadbee"));
|
||||
EXPECT_FALSE(hash.equals_hex(""));
|
||||
EXPECT_FALSE(hash.equals_hex("deadbeeg"));
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
@@ -19,11 +19,6 @@ image:
|
||||
} else {
|
||||
ESP_LOGD("online_image", "Cache miss: fresh download");
|
||||
}
|
||||
on_download_started:
|
||||
- logger.log: Download started
|
||||
on_download_progress:
|
||||
lambda: |-
|
||||
ESP_LOGD("online_image", "Downloaded %.0f%% (%zu bytes)", progress, bytes_read);
|
||||
- platform: online_image
|
||||
id: online_binary_transparent_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
@@ -28,9 +27,6 @@ class FakeContainer : public http_request::HttpContainer {
|
||||
}
|
||||
|
||||
int read(uint8_t *buf, size_t max_len) override {
|
||||
this->largest_read = std::max(this->largest_read, max_len);
|
||||
if (this->read_error_)
|
||||
return -1;
|
||||
size_t len = std::min(max_len, this->available_ - this->bytes_read_);
|
||||
std::memcpy(buf, this->body_.data() + this->bytes_read_, len);
|
||||
this->bytes_read_ += len;
|
||||
@@ -46,21 +42,13 @@ class FakeContainer : public http_request::HttpContainer {
|
||||
}
|
||||
|
||||
void set_content_length(size_t length) { this->content_length = length; }
|
||||
/// Make every read from now on fail, like a dropped connection
|
||||
void fail_reads() { this->read_error_ = true; }
|
||||
/// Let len more bytes of the body arrive
|
||||
void deliver(size_t len) { this->available_ = std::min(this->body_.size(), this->available_ + len); }
|
||||
void add_response_header(const std::string &name, const std::string &value) {
|
||||
this->response_headers_.push_back({name, value});
|
||||
}
|
||||
|
||||
/// The most bytes any one read() asked for
|
||||
size_t largest_read{0};
|
||||
|
||||
protected:
|
||||
std::vector<uint8_t> body_;
|
||||
size_t available_;
|
||||
bool read_error_{false};
|
||||
};
|
||||
|
||||
class FakeHttpRequest : public http_request::HttpRequestComponent {
|
||||
@@ -135,173 +123,13 @@ static constexpr std::array<uint8_t, 69> PNG_1X1{
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xC9,
|
||||
0xFE, 0x92, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};
|
||||
|
||||
// 8x8 progressive JPEG, 521 bytes
|
||||
static const uint8_t JPEG_PROGRESSIVE[] = {
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
||||
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10,
|
||||
0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33,
|
||||
0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67,
|
||||
0x68, 0x67, 0x3E, 0x4D, 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x11,
|
||||
0x12, 0x12, 0x18, 0x15, 0x18, 0x2F, 0x1A, 0x1A, 0x2F, 0x63, 0x42, 0x38, 0x42, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
|
||||
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
|
||||
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
|
||||
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0xFF, 0xC2, 0x00, 0x11, 0x08, 0x00, 0x08, 0x00, 0x08, 0x03, 0x01, 0x22, 0x00,
|
||||
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xFF, 0xC4, 0x00, 0x15, 0x01, 0x01, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0xFF, 0xDA, 0x00, 0x0C, 0x03,
|
||||
0x01, 0x00, 0x02, 0x10, 0x03, 0x10, 0x00, 0x00, 0x01, 0x94, 0x05, 0x3F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08,
|
||||
0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F,
|
||||
0x01, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x01, 0x7F, 0xFF, 0xC4, 0x00,
|
||||
0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3F, 0x02, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01,
|
||||
0x01, 0x00, 0x01, 0x3F, 0x21, 0x7F, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00,
|
||||
0x10, 0x0B, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xC4, 0x00,
|
||||
0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01,
|
||||
0x01, 0x00, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xD9,
|
||||
};
|
||||
|
||||
// 48x40 4:2:0 JPEG (quality 95), 2344 bytes: three rows of 16 pixel MCUs, with the scan starting at byte 609
|
||||
static const uint8_t JPEG_48X40[] = {
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
||||
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02,
|
||||
0x02, 0x02, 0x04, 0x03, 0x02, 0x02, 0x02, 0x02, 0x05, 0x04, 0x04, 0x03, 0x04, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05,
|
||||
0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x06, 0x07, 0x09, 0x07, 0x06, 0x06, 0x08, 0x0B, 0x08, 0x09, 0x0A, 0x0A, 0x0A,
|
||||
0x0A, 0x0A, 0x06, 0x08, 0x0B, 0x0C, 0x0B, 0x0A, 0x0C, 0x09, 0x0A, 0x0A, 0x0A, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x02,
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x05, 0x03, 0x03, 0x05, 0x0A, 0x07, 0x06, 0x07, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
|
||||
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
|
||||
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
|
||||
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x28, 0x00, 0x30, 0x03, 0x01, 0x22, 0x00,
|
||||
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
|
||||
0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,
|
||||
0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
|
||||
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62,
|
||||
0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
|
||||
0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A,
|
||||
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85,
|
||||
0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6,
|
||||
0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
|
||||
0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
|
||||
0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00,
|
||||
0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
|
||||
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04,
|
||||
0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
|
||||
0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09,
|
||||
0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A,
|
||||
0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
|
||||
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
|
||||
0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96,
|
||||
0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7,
|
||||
0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8,
|
||||
0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9,
|
||||
0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, 0xFC, 0xAA, 0xB8, 0xB7,
|
||||
0xD3, 0xB5, 0xD8, 0x1E, 0xD6, 0xD1, 0xA5, 0x5B, 0x4B, 0x46, 0x10, 0xC1, 0x06, 0xA1, 0x29, 0x69, 0x25, 0xB7, 0xC2,
|
||||
0xC6, 0x0C, 0x71, 0x26, 0x5E, 0x50, 0x15, 0x5D, 0xB6, 0x9C, 0x9E, 0x00, 0xC1, 0x67, 0x67, 0x4D, 0xCF, 0x84, 0xF7,
|
||||
0x57, 0xA6, 0xDA, 0x08, 0x52, 0xF5, 0xE5, 0xBA, 0xB8, 0x7B, 0x5B, 0x68, 0xEE, 0x2F, 0x66, 0x89, 0xB7, 0xB7, 0x9B,
|
||||
0xE5, 0x9C, 0x23, 0xAA, 0xFE, 0xF7, 0x68, 0x11, 0x9E, 0x5D, 0x18, 0x12, 0x48, 0x56, 0x5C, 0x9F, 0x57, 0xB6, 0xF8,
|
||||
0x33, 0xF0, 0xE2, 0x5B, 0xFB, 0xCF, 0xF4, 0x51, 0x0C, 0xC6, 0x64, 0x30, 0x49, 0x71, 0x7C, 0xE2, 0x16, 0x90, 0x2E,
|
||||
0xF4, 0x72, 0xEA, 0xE0, 0x34, 0x81, 0x77, 0x93, 0x9C, 0x07, 0xC2, 0x93, 0xB9, 0x61, 0xC0, 0xD3, 0xF0, 0x17, 0xC1,
|
||||
0xBF, 0x03, 0xCD, 0xE1, 0xAD, 0x42, 0x3F, 0x0A, 0xD8, 0xC7, 0x2D, 0xAC, 0x36, 0xCB, 0xF6, 0x79, 0x22, 0x69, 0x84,
|
||||
0xC4, 0x93, 0x29, 0x2A, 0x89, 0xB9, 0x00, 0x50, 0x62, 0x7C, 0xFF, 0x00, 0x16, 0xD9, 0x65, 0x00, 0x16, 0x65, 0x07,
|
||||
0xFA, 0x5B, 0x36, 0xE2, 0xAC, 0xBF, 0x2D, 0x55, 0x79, 0x29, 0xB5, 0x26, 0x9B, 0xA9, 0x18, 0xB5, 0x15, 0x36, 0x9B,
|
||||
0x53, 0xD1, 0xCA, 0x10, 0x93, 0x69, 0xB6, 0x94, 0x27, 0x16, 0xAF, 0x19, 0x29, 0xB8, 0xBB, 0x3F, 0x9A, 0xE1, 0x1E,
|
||||
0x2A, 0xCB, 0x9E, 0x79, 0x4A, 0x92, 0x84, 0xDA, 0xD6, 0xED, 0xA4, 0xED, 0x7A, 0x72, 0xB5, 0xAC, 0xF6, 0xEB, 0xAD,
|
||||
0xD5, 0x97, 0x91, 0xC9, 0x4D, 0xA4, 0xE9, 0xD7, 0x56, 0x45, 0x66, 0xD3, 0x9D, 0x85, 0xB1, 0x86, 0x59, 0x7E, 0xD9,
|
||||
0x7E, 0xA0, 0xE0, 0xA8, 0x02, 0x17, 0x02, 0x3C, 0x36, 0x59, 0x18, 0xE2, 0x42, 0x1D, 0x40, 0x56, 0x0A, 0x77, 0x30,
|
||||
0xAE, 0xFA, 0x56, 0xD4, 0xA4, 0x8A, 0xD9, 0xAD, 0xD6, 0x38, 0x1D, 0x7F, 0x77, 0x77, 0x1C, 0xB2, 0x4C, 0x22, 0x8C,
|
||||
0x2B, 0xF3, 0x14, 0x64, 0x45, 0x85, 0x52, 0x47, 0x92, 0x7E, 0xF2, 0x0F, 0x34, 0x1D, 0xBC, 0x56, 0xDE, 0x85, 0xF0,
|
||||
0x4B, 0xE1, 0x0A, 0xE9, 0xB0, 0xDB, 0x2D, 0x8C, 0x4C, 0xCD, 0x74, 0xF1, 0x42, 0x96, 0x93, 0xCF, 0xF6, 0x54, 0x0C,
|
||||
0xB3, 0x44, 0x03, 0xAA, 0x6C, 0x49, 0x10, 0x87, 0x00, 0x88, 0xC2, 0xEE, 0x66, 0x6F, 0x9B, 0xAB, 0x57, 0xA4, 0x68,
|
||||
0xDF, 0x0A, 0x3E, 0x1B, 0xDA, 0xE9, 0xDF, 0xD9, 0x96, 0xFE, 0x03, 0xB2, 0x8E, 0xD9, 0x8A, 0xDD, 0xC1, 0x77, 0x14,
|
||||
0x97, 0x50, 0x2E, 0xE3, 0x20, 0x62, 0xA0, 0x33, 0x0C, 0xC6, 0x58, 0x48, 0x36, 0x6D, 0xC1, 0x20, 0xA2, 0x2B, 0x00,
|
||||
0xB5, 0xF1, 0x99, 0xCF, 0x19, 0x65, 0x79, 0xAD, 0x0C, 0x3C, 0x2B, 0x61, 0xF9, 0x28, 0xD6, 0x52, 0x97, 0x35, 0x95,
|
||||
0xD4, 0x61, 0x28, 0x5B, 0x99, 0xB8, 0xA9, 0xA9, 0x47, 0xD9, 0x3A, 0x69, 0xA9, 0xC6, 0x51, 0x52, 0x6A, 0x0D, 0x4A,
|
||||
0x74, 0xA9, 0xC3, 0xFA, 0x7F, 0xC3, 0xFE, 0x25, 0xC1, 0x4E, 0xAE, 0x26, 0x2E, 0x13, 0x71, 0x5C, 0x8D, 0xFB, 0xB1,
|
||||
0xD6, 0xEA, 0x6F, 0xF9, 0xAF, 0xBE, 0xB7, 0x4B, 0x55, 0x1D, 0x75, 0x4C, 0xF3, 0xDF, 0x05, 0xD9, 0xF8, 0x7B, 0xC3,
|
||||
0xBA, 0xB5, 0xF1, 0xBC, 0x8D, 0x63, 0xDF, 0x1C, 0x32, 0x88, 0x1A, 0xF3, 0x74, 0x73, 0x89, 0x15, 0xC4, 0x8A, 0x25,
|
||||
0x66, 0xDC, 0xAA, 0x98, 0x60, 0x06, 0x7F, 0xBD, 0xF2, 0x81, 0xF3, 0x36, 0xFE, 0x9D, 0xAF, 0x78, 0x8F, 0x54, 0xD3,
|
||||
0xAD, 0xA6, 0x9E, 0xC5, 0xE5, 0x9C, 0x96, 0xDC, 0xB7, 0xE8, 0x84, 0x47, 0xFB, 0xB4, 0x54, 0x42, 0x0F, 0x03, 0xF8,
|
||||
0xD7, 0xE5, 0x39, 0x60, 0xF8, 0x04, 0x79, 0x7B, 0xE3, 0xF4, 0x9F, 0x85, 0x5F, 0x08, 0xBE, 0x0D, 0x2C, 0x57, 0xCE,
|
||||
0x6C, 0xA5, 0x2B, 0xA8, 0x08, 0x20, 0x82, 0x04, 0xB9, 0xB8, 0x82, 0x43, 0x28, 0x72, 0x55, 0xD5, 0x83, 0xB9, 0x05,
|
||||
0xC2, 0x44, 0x55, 0x77, 0x8C, 0xFC, 0xEC, 0xD8, 0xCB, 0xE7, 0xBE, 0x9B, 0xE0, 0x5F, 0xC3, 0x9D, 0x32, 0xD4, 0x45,
|
||||
0x26, 0x8B, 0x2C, 0x93, 0xC0, 0xF3, 0x95, 0x49, 0xEE, 0x2E, 0x0C, 0x26, 0x26, 0x31, 0xC9, 0xF3, 0xED, 0x95, 0x96,
|
||||
0x32, 0xEC, 0x10, 0xE1, 0xB8, 0x60, 0x3A, 0x90, 0x1B, 0x3F, 0x9E, 0x66, 0x58, 0xFC, 0x14, 0x31, 0x15, 0xFD, 0x9C,
|
||||
0x79, 0x57, 0x3A, 0x92, 0x84, 0x1C, 0x3D, 0xC9, 0xCD, 0x27, 0x6D, 0x12, 0xE5, 0xE7, 0xA6, 0xD5, 0x4D, 0x64, 0x93,
|
||||
0xD1, 0x45, 0xC6, 0xAD, 0xE7, 0x2F, 0xE9, 0xEE, 0x17, 0xE3, 0x0C, 0x0A, 0xE1, 0x9C, 0x37, 0x34, 0x67, 0x2B, 0x73,
|
||||
0x2B, 0xB4, 0xBA, 0x49, 0xF4, 0x72, 0x57, 0x51, 0xDD, 0x24, 0x9B, 0x5A, 0x36, 0x9A, 0x49, 0x1E, 0x15, 0xA7, 0x7E,
|
||||
0xCE, 0xF6, 0x89, 0x6A, 0x96, 0x43, 0x55, 0xB5, 0xB3, 0xB6, 0xB8, 0x96, 0x2B, 0x9B, 0x78, 0xA0, 0xB4, 0x9E, 0xE5,
|
||||
0x61, 0x90, 0xE0, 0xC2, 0xEC, 0x61, 0x91, 0x47, 0xCC, 0xDB, 0xF1, 0x80, 0xC0, 0x98, 0xD4, 0xE5, 0xDC, 0x64, 0xEC,
|
||||
0x7C, 0x29, 0xFD, 0x98, 0x6C, 0xAF, 0xEC, 0xF5, 0x76, 0xB7, 0xF1, 0x36, 0x9A, 0x5D, 0x6F, 0x96, 0xE1, 0x74, 0xB5,
|
||||
0xD3, 0x44, 0x91, 0xA8, 0x45, 0x0C, 0xE2, 0x3D, 0xAC, 0x85, 0xF9, 0x3B, 0x5B, 0x19, 0x8A, 0x43, 0x1F, 0xCA, 0x50,
|
||||
0x9D, 0x87, 0xD2, 0x0F, 0xC2, 0x1F, 0x89, 0x4F, 0x6D, 0x04, 0x0F, 0xE1, 0x61, 0xAA, 0xEE, 0xB4, 0x8E, 0x56, 0xFB,
|
||||
0x06, 0xAF, 0x04, 0xF3, 0x40, 0x9B, 0x55, 0x4E, 0xCB, 0x66, 0x91, 0xCB, 0x18, 0xC2, 0x88, 0xB6, 0xB1, 0x65, 0x60,
|
||||
0xA4, 0x6E, 0xDC, 0xA4, 0x9E, 0xAF, 0xE1, 0xBF, 0xC2, 0x2F, 0x89, 0xA9, 0xE1, 0xFB, 0x8D, 0x6E, 0xC2, 0x3B, 0x4B,
|
||||
0x30, 0xD7, 0x60, 0xDA, 0x41, 0x2D, 0xCC, 0x6C, 0x2E, 0x49, 0xF3, 0x1E, 0x42, 0x19, 0x5D, 0x58, 0x3C, 0x8A, 0x81,
|
||||
0x82, 0xB8, 0x0C, 0xC9, 0xF2, 0xE5, 0x8F, 0x03, 0x8F, 0x36, 0xE3, 0x8A, 0x98, 0xCC, 0x14, 0xAB, 0x62, 0x2B, 0xC2,
|
||||
0x34, 0xA2, 0xA5, 0x78, 0x37, 0x42, 0x12, 0x70, 0x8C, 0x62, 0xD2, 0x6D, 0x72, 0x7B, 0xAF, 0x9B, 0xDD, 0xFD, 0xE5,
|
||||
0x4E, 0x79, 0x49, 0x45, 0x73, 0xA9, 0x72, 0x2F, 0xF0, 0x73, 0x83, 0xF8, 0xCE, 0xB4, 0x33, 0xE8, 0x3F, 0xAE, 0x42,
|
||||
0xF7, 0x76, 0xB3, 0x83, 0x57, 0xE5, 0x95, 0xBE, 0xCE, 0x8F, 0x7B, 0xDE, 0xD7, 0x4B, 0x6B, 0x9C, 0x1E, 0x8D, 0xF0,
|
||||
0x56, 0x7B, 0xDD, 0x26, 0x34, 0x5F, 0x1F, 0xDB, 0x9D, 0x37, 0x53, 0xB8, 0x96, 0xED, 0xA0, 0x16, 0xB1, 0xC9, 0x2C,
|
||||
0x8B, 0xF2, 0x26, 0x58, 0x89, 0x37, 0xEE, 0x04, 0xB2, 0x97, 0x54, 0x2D, 0x88, 0xC0, 0x60, 0x0B, 0x03, 0x5E, 0x9B,
|
||||
0x67, 0xFB, 0x36, 0xE9, 0x31, 0x2D, 0x9C, 0x37, 0x9E, 0x32, 0x84, 0x5D, 0x2C, 0x4A, 0xD6, 0xAD, 0x73, 0x63, 0x28,
|
||||
0x36, 0xAA, 0x53, 0xF7, 0x90, 0x29, 0x8E, 0x5C, 0x92, 0xBE, 0x41, 0xDC, 0xC0, 0xB3, 0x2B, 0x46, 0x32, 0x4B, 0x96,
|
||||
0x35, 0xAD, 0x1F, 0xC2, 0xDF, 0x89, 0x17, 0xF6, 0xB6, 0xFA, 0xB5, 0xED, 0xB8, 0xB7, 0x8E, 0x78, 0x66, 0x96, 0x09,
|
||||
0x6C, 0x6E, 0x23, 0x32, 0x28, 0x08, 0xE8, 0xF2, 0xBA, 0xF9, 0x86, 0x48, 0xC0, 0x68, 0xFE, 0x63, 0xC8, 0x23, 0x82,
|
||||
0xCC, 0xA5, 0x4A, 0xFA, 0x66, 0x8F, 0xF0, 0x37, 0xE2, 0x6D, 0xE5, 0xC9, 0x3A, 0x87, 0x86, 0xB4, 0xFB, 0x28, 0xAE,
|
||||
0x61, 0x0F, 0x0B, 0x59, 0xDF, 0xC0, 0xD1, 0xAC, 0xE0, 0x24, 0x41, 0x42, 0xC4, 0x49, 0x1B, 0x10, 0x21, 0x50, 0xC0,
|
||||
0x09, 0x11, 0x8B, 0xEE, 0x6E, 0x71, 0xE2, 0xE2, 0x78, 0xBB, 0x33, 0xFA, 0xC5, 0x1A, 0xB4, 0xEA, 0xC6, 0x74, 0xD2,
|
||||
0x8C, 0x24, 0xA5, 0x2A, 0x3C, 0xB5, 0x2E, 0xB9, 0x9B, 0x71, 0x97, 0x34, 0xE0, 0xAA, 0x54, 0x93, 0x71, 0x56, 0x49,
|
||||
0xB9, 0xC6, 0x4E, 0x9A, 0x9B, 0xD3, 0xFA, 0x7F, 0x81, 0xB8, 0xD6, 0xAC, 0xDD, 0x6F, 0x65, 0x8D, 0x82, 0x77, 0x85,
|
||||
0x95, 0xE1, 0x7D, 0x1C, 0xD2, 0x8B, 0x7A, 0x27, 0xA2, 0xF4, 0xED, 0xEE, 0x9E, 0x67, 0xE0, 0x8F, 0x81, 0x37, 0xBA,
|
||||
0xEF, 0x87, 0x67, 0xBF, 0x8F, 0xC4, 0xD2, 0xDD, 0x4F, 0x05, 0xA8, 0x61, 0x7B, 0x1E, 0x9E, 0xE1, 0x8C, 0xEA, 0xF3,
|
||||
0x18, 0x99, 0xA3, 0xDE, 0x76, 0x66, 0x36, 0x2A, 0x30, 0xFB, 0x9B, 0x0F, 0xC3, 0x1C, 0xA3, 0x76, 0x4B, 0xFB, 0x3B,
|
||||
0xE8, 0xBA, 0x66, 0xBF, 0x0E, 0x93, 0xFD, 0xAB, 0x23, 0xD9, 0xA5, 0xCE, 0x62, 0xF3, 0x34, 0xEB, 0x7F, 0x29, 0x0A,
|
||||
0xA7, 0x04, 0x48, 0xF2, 0x79, 0x82, 0x42, 0x1D, 0x46, 0x4B, 0xA9, 0x1B, 0xD1, 0xBE, 0x52, 0x54, 0xD7, 0xA8, 0xF8,
|
||||
0x33, 0xE1, 0xAF, 0xC4, 0xA8, 0x24, 0xD6, 0x6D, 0xED, 0xBC, 0x0A, 0xD3, 0x5A, 0x5C, 0xC4, 0xDF, 0x64, 0x17, 0x97,
|
||||
0x36, 0xF0, 0x65, 0x52, 0x39, 0xD4, 0x23, 0x22, 0x9D, 0xD8, 0x64, 0x83, 0x04, 0x85, 0x38, 0xEF, 0xB8, 0x49, 0x91,
|
||||
0xBB, 0xFF, 0x00, 0x0A, 0xDF, 0xE3, 0x77, 0x84, 0xAC, 0xD3, 0x4A, 0xBC, 0xF0, 0x3A, 0x5A, 0x5B, 0x4F, 0x72, 0x5E,
|
||||
0xD7, 0x53, 0x8E, 0xE2, 0x12, 0x6D, 0xC6, 0x40, 0x20, 0x47, 0xE6, 0x3E, 0x71, 0x26, 0x70, 0x24, 0xE4, 0xB4, 0x63,
|
||||
0x2A, 0xC0, 0xE0, 0x7E, 0x71, 0x98, 0x71, 0x35, 0x5C, 0x4E, 0x69, 0x5A, 0xBD, 0x7C, 0x7A, 0x9C, 0xE3, 0xC9, 0xCF,
|
||||
0x38, 0x46, 0x8D, 0xA4, 0xDC, 0x63, 0x38, 0x4A, 0xE9, 0xED, 0x5A, 0x4E, 0x4A, 0x6D, 0xF3, 0xCA, 0x2D, 0xB9, 0x4D,
|
||||
0xD3, 0x77, 0x8C, 0x7F, 0xA8, 0xF8, 0x53, 0x8D, 0x31, 0x32, 0xC8, 0xA9, 0xDB, 0x15, 0x04, 0x9F, 0xB4, 0xD2, 0xF4,
|
||||
0xD3, 0xF8, 0xA5, 0x77, 0xCD, 0xB3, 0x49, 0x6C, 0xB4, 0xEA, 0x97, 0x46, 0x78, 0x45, 0xC4, 0x16, 0x5A, 0xDD, 0xA5,
|
||||
0xA6, 0x8D, 0xA7, 0xEA, 0x77, 0x4A, 0x54, 0x47, 0x3C, 0x31, 0x41, 0x20, 0x06, 0x68, 0x00, 0x9A, 0x4F, 0x2A, 0x40,
|
||||
0x5D, 0x5F, 0x69, 0xF3, 0x15, 0xCB, 0x32, 0x96, 0xDC, 0xC0, 0x90, 0xA5, 0xF0, 0xBD, 0xAF, 0xC3, 0x96, 0x83, 0x51,
|
||||
0xB8, 0xD6, 0xA7, 0x48, 0xD2, 0xE8, 0x2B, 0x2C, 0x2D, 0x6B, 0xF6, 0xA4, 0x55, 0x8E, 0x45, 0x5F, 0xBA, 0x54, 0x2A,
|
||||
0x36, 0x77, 0x12, 0x72, 0xE5, 0x9C, 0x14, 0x3B, 0x63, 0x6C, 0x62, 0x8A, 0x2B, 0xDE, 0xCD, 0x70, 0x74, 0xA7, 0x92,
|
||||
0xE2, 0x31, 0x35, 0x2D, 0x29, 0x54, 0xBB, 0x97, 0xBB, 0x18, 0xF3, 0x38, 0x52, 0xA5, 0x38, 0xDF, 0x92, 0x31, 0x7E,
|
||||
0xEB, 0x9B, 0x6A, 0xCD, 0x6B, 0xEF, 0x7C, 0x6E, 0x52, 0x7F, 0xE1, 0xF7, 0x0E, 0x54, 0x96, 0x1B, 0x3D, 0x52, 0x86,
|
||||
0xF0, 0x73, 0x4B, 0xEE, 0x92, 0xD6, 0xD6, 0xBF, 0xC2, 0x8D, 0x9F, 0x0B, 0xDE, 0xEB, 0xF3, 0x69, 0x6B, 0x06, 0x9B,
|
||||
0xA9, 0x5B, 0xE9, 0xF6, 0x6F, 0x76, 0x1E, 0x45, 0x8E, 0xE1, 0x54, 0xC8, 0xCD, 0xF3, 0x28, 0x8B, 0x1C, 0x9E, 0x25,
|
||||
0x55, 0xDA, 0xA5, 0x58, 0x80, 0xCA, 0x46, 0x42, 0xA8, 0xF4, 0x1B, 0xEB, 0xBB, 0xBB, 0x8B, 0x69, 0xAF, 0xA5, 0x8E,
|
||||
0xD0, 0x49, 0x25, 0x9C, 0x37, 0x73, 0xE9, 0xD7, 0xF2, 0x14, 0x8E, 0x55, 0x91, 0x64, 0x90, 0x46, 0xA8, 0xF9, 0x50,
|
||||
0x57, 0x73, 0x2E, 0xE0, 0x55, 0xF6, 0x87, 0x45, 0x20, 0x2E, 0xC7, 0x28, 0xAF, 0xCD, 0x78, 0xA6, 0xA5, 0x5C, 0x26,
|
||||
0x33, 0x01, 0x35, 0x2E, 0x67, 0x52, 0x35, 0xA0, 0xEE, 0x93, 0x56, 0x70, 0x4F, 0x48, 0xDB, 0x95, 0x6B, 0x76, 0xF4,
|
||||
0xB4, 0xA5, 0x27, 0x29, 0x29, 0x35, 0x16, 0xBF, 0xA8, 0x7C, 0x3E, 0x92, 0xF6, 0x98, 0xA8, 0x5B, 0x4B, 0xC5, 0xDB,
|
||||
0x5E, 0x8E, 0x4F, 0x7D, 0xFA, 0xDB, 0x57, 0xB2, 0x56, 0xD6, 0xED, 0xF7, 0x3E, 0x10, 0xB2, 0x82, 0xF6, 0xC2, 0xF2,
|
||||
0xF3, 0x4E, 0xD4, 0xAF, 0xEE, 0x5A, 0xCA, 0xCE, 0xD2, 0xF1, 0xA7, 0xB8, 0x9D, 0xD4, 0x42, 0x8C, 0xF2, 0x3C, 0x8E,
|
||||
0x42, 0x02, 0x24, 0xFD, 0xDA, 0x96, 0x60, 0xAD, 0xB0, 0xC9, 0x0E, 0x00, 0xC8, 0xC8, 0xEA, 0xEE, 0x12, 0x7B, 0x09,
|
||||
0x6C, 0x6E, 0xCE, 0xA2, 0x0B, 0xAA, 0x32, 0xD9, 0xB9, 0x49, 0x66, 0xDF, 0x74, 0xCC, 0x31, 0x1B, 0xE4, 0xE6, 0x63,
|
||||
0xF2, 0x91, 0x95, 0x42, 0x15, 0x8B, 0x7D, 0xE0, 0xAC, 0xB4, 0x51, 0x5F, 0x33, 0x3A, 0xD8, 0x8C, 0x46, 0x7A, 0x9C,
|
||||
0xA6, 0xD7, 0x2C, 0x13, 0x49, 0x25, 0x6E, 0x6A, 0x73, 0xE4, 0x84, 0x9E, 0x9A, 0xC9, 0x29, 0x24, 0x9E, 0xE9, 0x42,
|
||||
0x11, 0x8D, 0xA2, 0x9C, 0x5F, 0xF5, 0x37, 0x0D, 0x36, 0xF8, 0x7E, 0x9A, 0xEC, 0xE5, 0xE5, 0x7E, 0x67, 0x77, 0x7B,
|
||||
0x7A, 0xBF, 0x5E, 0xB7, 0x3F, 0xFF, 0xD9,
|
||||
};
|
||||
|
||||
static void set_loop_time(uint32_t now) { LoopBlockingGuard clock(nullptr, LOG_STR("test"), now); }
|
||||
|
||||
class OnlineImageDownloadEnd : public ::testing::Test {
|
||||
protected:
|
||||
std::unique_ptr<OnlineImage> make_image_(runtime_image::ImageFormat format, uint32_t buffer_size = 256) {
|
||||
std::unique_ptr<OnlineImage> make_image_(runtime_image::ImageFormat format) {
|
||||
auto img = std::make_unique<OnlineImage>("http://example.com/image", 0, 0, format, image::IMAGE_TYPE_RGB,
|
||||
image::TRANSPARENCY_OPAQUE, nullptr, buffer_size);
|
||||
image::TRANSPARENCY_OPAQUE, nullptr, 256);
|
||||
img->set_parent(&this->http_);
|
||||
return img;
|
||||
}
|
||||
@@ -458,294 +286,4 @@ TEST_F(OnlineImageDownloadEnd, JpegBufferAllocationFailureReportsError) {
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// The JPEG decoded with all of it in memory at once, to compare the downloaded images with
|
||||
static std::vector<uint8_t> reference_jpeg_pixels() {
|
||||
runtime_image::RuntimeImage ref(runtime_image::JPEG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
std::vector<uint8_t> data(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
|
||||
EXPECT_TRUE(ref.begin_decode(data.size()));
|
||||
EXPECT_EQ(ref.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
EXPECT_TRUE(ref.end_decode());
|
||||
return std::vector<uint8_t>(ref.get_data_start(), ref.get_data_start() + ref.get_width_stride() * ref.get_height());
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> image_pixels(OnlineImage &image) {
|
||||
return std::vector<uint8_t>(image.get_data_start(),
|
||||
image.get_data_start() + image.get_width_stride() * image.get_height());
|
||||
}
|
||||
|
||||
// A JPEG of known size is decoded as it arrives, so a buffer smaller than the file only has to hold the headers
|
||||
TEST_F(OnlineImageDownloadEnd, JpegDecodesThroughASmallBuffer) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
// Bytes after the image are never needed. Waiting for the whole file before decoding would read them all.
|
||||
std::vector<uint8_t> body(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
|
||||
body.resize(body.size() + 8192, 0);
|
||||
auto container = std::make_shared<FakeContainer>(body, body.size(), false);
|
||||
container->add_response_header("etag", "\"abc\"");
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_LT(container->get_bytes_read(), body.size()) << "the image was not decoded as it arrived";
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
ASSERT_EQ(image->get_width(), 48);
|
||||
ASSERT_EQ(image->get_height(), 40);
|
||||
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
|
||||
|
||||
// A complete image is cached
|
||||
this->http_.next = std::make_shared<FakeContainer>(std::vector<uint8_t>{}, 0, false);
|
||||
image->update();
|
||||
EXPECT_TRUE(this->http_.sent_header("if-none-match"));
|
||||
}
|
||||
|
||||
// A read() waits for every byte it asks for, so a large buffer is filled a little at a time rather than in one
|
||||
// read that blocks the loop until the whole buffer has arrived
|
||||
TEST_F(OnlineImageDownloadEnd, JpegReadsInSmallPiecesWithALargeBuffer) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 65536);
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 0, false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
|
||||
container->deliver(sizeof(JPEG_48X40));
|
||||
image->loop();
|
||||
}
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_LE(container->largest_read, READ_CHUNK);
|
||||
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
|
||||
}
|
||||
|
||||
// A downloaded PNG is read a little at a time too
|
||||
TEST_F(OnlineImageDownloadEnd, PngReadsInSmallPiecesWithALargeBuffer) {
|
||||
auto image = this->make_image_(runtime_image::PNG, 65536);
|
||||
std::vector<uint8_t> png(PNG_1X1.begin(), PNG_1X1.end());
|
||||
auto container = std::make_shared<FakeContainer>(png, png.size(), false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_LE(container->largest_read, READ_CHUNK);
|
||||
}
|
||||
|
||||
// A slow link: the decoder waits part way through the image for the rest to arrive
|
||||
TEST_F(OnlineImageDownloadEnd, JpegWaitsForASlowDownload) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1024, false);
|
||||
this->start_(*image, container);
|
||||
|
||||
int passes = 0;
|
||||
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++) {
|
||||
image->loop();
|
||||
if (passes == 5) {
|
||||
EXPECT_EQ(this->finished_, 0) << "the image cannot be finished before the rest of it arrives";
|
||||
}
|
||||
container->deliver(40);
|
||||
}
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_GT(passes, 10);
|
||||
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
|
||||
}
|
||||
|
||||
// A download that stops part way through times out rather than waiting for ever
|
||||
TEST_F(OnlineImageDownloadEnd, JpegDownloadThatStopsTimesOut) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1500, false);
|
||||
set_loop_time(1000);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
image->loop();
|
||||
EXPECT_EQ(this->errors_, 0) << "still within the timeout";
|
||||
|
||||
set_loop_time(1000 + this->http_.get_timeout());
|
||||
image->loop();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// Headers that do not fit the download buffer cannot be read as the image arrives, so the whole image is
|
||||
// downloaded first, keeping the bytes already read
|
||||
TEST_F(OnlineImageDownloadEnd, JpegHeadersLargerThanTheBufferDownloadTheWholeImage) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 256);
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
|
||||
sizeof(JPEG_48X40), false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
|
||||
image->loop();
|
||||
container->deliver(256);
|
||||
}
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(container->get_bytes_read(), sizeof(JPEG_48X40));
|
||||
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
|
||||
}
|
||||
|
||||
// The pass that opens the image does not decode any of it
|
||||
TEST_F(OnlineImageDownloadEnd, JpegOpensInAPassOfItsOwn) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
|
||||
sizeof(JPEG_48X40), false);
|
||||
this->start_(*image, container);
|
||||
|
||||
image->loop();
|
||||
EXPECT_EQ(this->finished_ + this->errors_, 0);
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
EXPECT_EQ(this->finished_, 1);
|
||||
}
|
||||
|
||||
// A progressive JPEG is refused while it downloads, without blaming the buffer size
|
||||
TEST_F(OnlineImageDownloadEnd, JpegProgressiveReportsError) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 256);
|
||||
std::vector<uint8_t> body(JPEG_PROGRESSIVE, JPEG_PROGRESSIVE + sizeof(JPEG_PROGRESSIVE));
|
||||
auto container = std::make_shared<FakeContainer>(body, 256, false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 10 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// on_download_started fires once the server has accepted the request, before the first progress report, and not
|
||||
// for a response that has no image to download
|
||||
TEST_F(OnlineImageDownloadEnd, StartedFiresOnlyWhenTheDownloadBegins) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
std::vector<std::string> events;
|
||||
image->add_on_download_started_callback([&events]() { events.emplace_back("started"); });
|
||||
image->add_on_progress_callback([&events](float, size_t) { events.emplace_back("progress"); });
|
||||
this->start_(*image,
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
|
||||
sizeof(JPEG_48X40), false));
|
||||
ASSERT_GE(events.size(), 2u);
|
||||
EXPECT_EQ(events[0], "started");
|
||||
EXPECT_EQ(events[1], "progress");
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
EXPECT_EQ(std::count(events.begin(), events.end(), std::string("started")), 1);
|
||||
|
||||
for (int status : {304, 404}) {
|
||||
events.clear();
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>{}, 0, false);
|
||||
container->status_code = status;
|
||||
this->http_.next = container;
|
||||
image->update();
|
||||
EXPECT_TRUE(events.empty()) << "HTTP " << status;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress starts at 0, is reported at most every 100 ms, and always reaches 100%
|
||||
TEST_F(OnlineImageDownloadEnd, ProgressIsReportedAsTheImageDownloads) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
std::vector<float> reports;
|
||||
image->add_on_progress_callback([&reports](float progress, size_t) { reports.push_back(progress); });
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 100, false);
|
||||
uint32_t now = 1000;
|
||||
set_loop_time(now);
|
||||
this->start_(*image, container);
|
||||
ASSERT_EQ(reports.size(), 1u);
|
||||
EXPECT_EQ(reports[0], 0.0f);
|
||||
|
||||
int passes = 0;
|
||||
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++) {
|
||||
now += 50;
|
||||
set_loop_time(now);
|
||||
container->deliver(100);
|
||||
image->loop();
|
||||
}
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(reports.back(), 100.0f);
|
||||
EXPECT_TRUE(std::is_sorted(reports.begin(), reports.end()));
|
||||
// A report every 100 ms of a 50 ms loop, plus the start and the end
|
||||
EXPECT_LE(reports.size(), static_cast<size_t>(passes / 2 + 2));
|
||||
EXPECT_GT(reports.size(), 3u);
|
||||
}
|
||||
|
||||
// Without a Content-Length the percentage is unknown, but the bytes read are still reported
|
||||
TEST_F(OnlineImageDownloadEnd, ProgressOfAChunkedDownloadHasNoPercentage) {
|
||||
auto image = this->make_image_(runtime_image::PNG);
|
||||
std::vector<std::pair<float, size_t>> reports;
|
||||
image->add_on_progress_callback(
|
||||
[&reports](float progress, size_t bytes_read) { reports.emplace_back(progress, bytes_read); });
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(PNG_1X1.begin(), PNG_1X1.end()), PNG_1X1.size(), true);
|
||||
set_loop_time(1000);
|
||||
this->start_(*image, container);
|
||||
set_loop_time(2000);
|
||||
for (int i = 0; i < 10 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
|
||||
ASSERT_GE(reports.size(), 2u);
|
||||
EXPECT_TRUE(std::isnan(reports.back().first));
|
||||
EXPECT_EQ(reports.back().second, PNG_1X1.size());
|
||||
}
|
||||
|
||||
// Scan data that cannot be decoded fails the download part way through
|
||||
TEST_F(OnlineImageDownloadEnd, JpegDecodeErrorFailsTheDownload) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
std::vector<uint8_t> body(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
|
||||
std::fill(body.begin() + 700, body.end() - 2, 0xFE);
|
||||
auto container = std::make_shared<FakeContainer>(body, 1024, false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
|
||||
image->loop();
|
||||
container->deliver(256);
|
||||
}
|
||||
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// A read that fails part way through the download ends it with an error
|
||||
TEST_F(OnlineImageDownloadEnd, JpegReadErrorFailsTheDownload) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1024, false);
|
||||
this->start_(*image, container);
|
||||
for (int i = 0; i < 5; i++)
|
||||
image->loop();
|
||||
ASSERT_EQ(this->errors_, 0);
|
||||
|
||||
container->fail_reads();
|
||||
image->loop();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
TEST(DownloadBufferGrow, KeepsTheUnreadData) {
|
||||
DownloadBuffer buffer(16);
|
||||
std::memcpy(buffer.append(), "abcdef", 6);
|
||||
buffer.write(6);
|
||||
buffer.read(2);
|
||||
|
||||
ASSERT_TRUE(buffer.grow(64));
|
||||
EXPECT_EQ(buffer.size(), 64u);
|
||||
ASSERT_EQ(buffer.unread(), 4u);
|
||||
EXPECT_EQ(std::memcmp(buffer.data(), "cdef", 4), 0);
|
||||
|
||||
// A failed allocation leaves the buffer as it was
|
||||
EXPECT_FALSE(buffer.grow(SIZE_MAX / 2));
|
||||
EXPECT_EQ(buffer.size(), 64u);
|
||||
EXPECT_EQ(std::memcmp(buffer.data(), "cdef", 4), 0);
|
||||
// Growing to a smaller size changes nothing
|
||||
EXPECT_TRUE(buffer.grow(8));
|
||||
EXPECT_EQ(buffer.size(), 64u);
|
||||
}
|
||||
|
||||
} // namespace esphome::online_image::testing
|
||||
@@ -1,11 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code_testing(config: object) -> None:
|
||||
cg.add_define("USE_PACKET_INTERFACE_BASIC_SESSION")
|
||||
cg.add_define("USE_PACKET_INTERFACE_HDLC")
|
||||
cg.add_define("USE_PACKET_INTERFACE_XXTEA")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -1,252 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "esphome/components/packet_interface/basic_session.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::packet_interface::session_testing {
|
||||
|
||||
// Copies a packet into a vector, for comparing in tests
|
||||
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
|
||||
std::vector<uint8_t> result(data.size());
|
||||
data.copy_to(result.data(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
using Packet = std::vector<uint8_t>;
|
||||
|
||||
// A link layer that records what is sent, so a test can deliver it to the peer
|
||||
class MockLink : public PacketInterface {
|
||||
public:
|
||||
explicit MockLink(size_t max_packet_size = 64) : PacketInterface(max_packet_size) {}
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
|
||||
if (!this->accept)
|
||||
return false;
|
||||
this->sent.push_back(to_vector(data));
|
||||
return true;
|
||||
}
|
||||
void inject(const Packet &packet) { this->on_receive_from_interface_(PacketBuffer(packet), {}); }
|
||||
// Act as a reliability layer that resets and discards data
|
||||
void set_reliable() { this->capabilities_ = CAPABILITY_RELIABILITY; }
|
||||
void report_data_lost() { this->session_callback_.call(false); }
|
||||
|
||||
std::vector<Packet> sent;
|
||||
bool accept{true};
|
||||
};
|
||||
|
||||
class TestSession : public BasicSession {
|
||||
public:
|
||||
using BasicSession::BasicSession;
|
||||
using BasicSession::on_interval_;
|
||||
unsigned get_not_understood_count() const { return this->not_understood_count_; }
|
||||
};
|
||||
|
||||
struct Peer {
|
||||
Peer() {
|
||||
this->session = std::make_unique<TestSession>(&this->link);
|
||||
this->session->set_keep_alive(1000);
|
||||
this->session->set_session_timeout(3000);
|
||||
this->session->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(to_vector(data)); });
|
||||
this->session->add_on_session_state_callback([this](bool connected) { this->states.push_back(connected); });
|
||||
this->session->setup();
|
||||
}
|
||||
bool send(const Packet &data) { return this->session->send_to_interface(PacketBuffer(data), {}); }
|
||||
|
||||
MockLink link;
|
||||
std::unique_ptr<TestSession> session;
|
||||
std::vector<Packet> received;
|
||||
std::vector<bool> states;
|
||||
};
|
||||
|
||||
static bool is_keep_alive(const Packet &packet) { return !packet.empty() && packet[0] == 0x01; }
|
||||
|
||||
// Move packets between the peers until nothing more is sent
|
||||
static void pump(Peer &a, Peer &b) {
|
||||
for (int i = 0; i != 20; i++) {
|
||||
if (a.link.sent.empty() && b.link.sent.empty())
|
||||
return;
|
||||
auto from_a = std::move(a.link.sent);
|
||||
auto from_b = std::move(b.link.sent);
|
||||
a.link.sent.clear();
|
||||
b.link.sent.clear();
|
||||
for (auto &packet : from_a)
|
||||
b.link.inject(packet);
|
||||
for (auto &packet : from_b)
|
||||
a.link.inject(packet);
|
||||
}
|
||||
FAIL() << "Peers did not go quiet";
|
||||
}
|
||||
|
||||
// Each test replaces App with a fresh one; the keep-alive interval needs its scheduler
|
||||
class BasicSessionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
App.~Application();
|
||||
new (&App) Application();
|
||||
static char name[] = "session-tests";
|
||||
static char friendly_name[] = "";
|
||||
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
|
||||
}
|
||||
void TearDown() override {
|
||||
App.~Application();
|
||||
new (&App) Application(); // later tests and the test listener use App
|
||||
}
|
||||
|
||||
static void set_time(uint32_t now) { LoopBlockingGuard guard{nullptr, nullptr, now}; }
|
||||
};
|
||||
|
||||
TEST_F(BasicSessionTest, MaxPacketSizeAllowsForHeader) {
|
||||
Peer a;
|
||||
EXPECT_EQ(a.session->get_max_packet_size(), 64 - BasicSession::HEADER_SIZE);
|
||||
EXPECT_EQ(a.link.top(), a.session.get());
|
||||
EXPECT_EQ(a.session->get_capabilities(), CAPABILITY_SESSION);
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, KeepAliveSentAtSetup) {
|
||||
Peer a;
|
||||
ASSERT_EQ(a.link.sent.size(), 1);
|
||||
EXPECT_TRUE(is_keep_alive(a.link.sent[0]));
|
||||
EXPECT_EQ(a.link.sent[0].size(), 5);
|
||||
EXPECT_FALSE(a.session->is_connected());
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, BothConnectWithoutRepeatedAnswers) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_TRUE(b.session->is_connected());
|
||||
EXPECT_EQ(a.states, std::vector<bool>{true});
|
||||
EXPECT_EQ(b.states, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, OneSideStartsLater) {
|
||||
Peer a;
|
||||
a.link.sent.clear(); // a's first keep-alive is lost
|
||||
Peer b;
|
||||
pump(a, b);
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_TRUE(b.session->is_connected());
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, DataPassesThroughWithHeader) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
ASSERT_TRUE(a.send({0x10, 0x11}));
|
||||
ASSERT_EQ(a.link.sent.size(), 1);
|
||||
EXPECT_EQ(a.link.sent[0], (Packet{0x00, 0x10, 0x11}));
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Packet>{{0x10, 0x11}}));
|
||||
EXPECT_TRUE(a.received.empty());
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, NotUnderstoodPacketsCountedUntilOneIsUnderstood) {
|
||||
Peer a;
|
||||
for (int i = 0; i != 20; i++)
|
||||
a.link.inject({0x7F, 0x00});
|
||||
// The count stops where the mismatch is logged, so it is logged once
|
||||
EXPECT_EQ(a.session->get_not_understood_count(), 10);
|
||||
a.link.inject({0x00, 0x55});
|
||||
EXPECT_EQ(a.session->get_not_understood_count(), 0);
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, DataMarksPeerConnected) {
|
||||
Peer a;
|
||||
a.link.inject({0x00, 0x42});
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_EQ(a.received, (std::vector<Packet>{{0x42}}));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, DataBeforeFirstKeepAliveIsNotARestart) {
|
||||
Peer a, b;
|
||||
a.link.sent.clear();
|
||||
b.link.sent.clear(); // b's first keep-alive is lost
|
||||
b.send({0x42});
|
||||
pump(a, b);
|
||||
b.session->on_interval_();
|
||||
pump(a, b);
|
||||
EXPECT_EQ(a.states, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, SendFailureIsReturned) {
|
||||
Peer a;
|
||||
a.link.accept = false;
|
||||
EXPECT_FALSE(a.send({0x01}));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, IntervalSendsKeepAlive) {
|
||||
Peer a;
|
||||
a.link.sent.clear();
|
||||
a.session->on_interval_();
|
||||
ASSERT_EQ(a.link.sent.size(), 1);
|
||||
EXPECT_TRUE(is_keep_alive(a.link.sent[0]));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, PeerLostAfterTimeout) {
|
||||
Peer a, b;
|
||||
set_time(1000);
|
||||
pump(a, b);
|
||||
set_time(4000);
|
||||
a.session->on_interval_();
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
set_time(4001);
|
||||
a.session->on_interval_();
|
||||
EXPECT_FALSE(a.session->is_connected());
|
||||
EXPECT_EQ(a.states, (std::vector<bool>{true, false}));
|
||||
// Heard again
|
||||
b.session->on_interval_();
|
||||
pump(a, b);
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_EQ(a.states, (std::vector<bool>{true, false, true}));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, PeerRestartIsReported) {
|
||||
Peer a;
|
||||
auto b = std::make_unique<Peer>();
|
||||
pump(a, *b);
|
||||
b = std::make_unique<Peer>();
|
||||
pump(a, *b);
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_EQ(a.states, (std::vector<bool>{true, false, true}));
|
||||
EXPECT_EQ(b->states, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, MalformedPacketsDropped) {
|
||||
Peer a;
|
||||
a.link.inject({});
|
||||
a.link.inject({0x02, 0x00});
|
||||
a.link.inject({0x01, 0x00, 0x00});
|
||||
EXPECT_FALSE(a.session->is_connected());
|
||||
EXPECT_TRUE(a.received.empty());
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, DataLostBelowReportedAsSessionLost) {
|
||||
MockLink link;
|
||||
link.set_reliable();
|
||||
TestSession session(&link);
|
||||
std::vector<bool> states;
|
||||
session.add_on_session_state_callback([&states](bool connected) { states.push_back(connected); });
|
||||
session.setup();
|
||||
// Not yet connected, so nothing to report
|
||||
link.report_data_lost();
|
||||
EXPECT_TRUE(states.empty());
|
||||
link.inject({0x00, 0x55});
|
||||
link.report_data_lost();
|
||||
EXPECT_FALSE(session.is_connected());
|
||||
// Connected again when the peer is next heard
|
||||
link.inject({0x00, 0x56});
|
||||
EXPECT_EQ(states, (std::vector<bool>{true, false, true}));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, LinkTooSmallForKeepAliveFailsSetup) {
|
||||
MockLink link(4);
|
||||
TestSession session(&link);
|
||||
session.setup();
|
||||
EXPECT_TRUE(session.is_failed());
|
||||
EXPECT_TRUE(link.sent.empty());
|
||||
Packet packet{1};
|
||||
EXPECT_FALSE(session.send_to_interface(PacketBuffer(packet), {}));
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface::session_testing
|
||||
@@ -1,14 +0,0 @@
|
||||
# A list, so the uart entries of other test directories are joined to it
|
||||
uart:
|
||||
- id: bridge_test_bus
|
||||
port: /dev/ttyUSB0
|
||||
baud_rate: 9600
|
||||
|
||||
packet_interface:
|
||||
- platform: uart
|
||||
id: bridge_test_link
|
||||
uart_id: bridge_test_bus
|
||||
|
||||
packet_transport:
|
||||
- platform: packet_interface
|
||||
packet_interface: bridge_test_link
|
||||
@@ -1,45 +0,0 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
udp:
|
||||
- id: packet_interface_udp
|
||||
|
||||
packet_interface:
|
||||
- platform: uart
|
||||
id: packet_interface_uart
|
||||
max_packet_size: 256
|
||||
reliability:
|
||||
timeout: 300ms
|
||||
ack_delay: 50ms
|
||||
max_queue_length: 3
|
||||
max_retries: 4
|
||||
encryption:
|
||||
key: Johnny Cash
|
||||
rolling_code_enable: true
|
||||
session:
|
||||
keep_alive: 5s
|
||||
timeout: 12s
|
||||
binary_sensor:
|
||||
name: UART session
|
||||
- platform: udp
|
||||
id: packet_interface_network
|
||||
udp_id: packet_interface_udp
|
||||
encryption: Johnny Cash
|
||||
reliability:
|
||||
session:
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: packet_interface_sensor
|
||||
lambda: return 1.0;
|
||||
|
||||
packet_transport:
|
||||
- platform: packet_interface
|
||||
packet_interface: packet_interface_uart
|
||||
sensors:
|
||||
- packet_interface_sensor
|
||||
- platform: packet_interface
|
||||
packet_interface: packet_interface_network
|
||||
sensors:
|
||||
- packet_interface_sensor
|
||||
@@ -1,464 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "esphome/components/packet_interface/hdlc_reliability.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::packet_interface::testing {
|
||||
|
||||
// Copies a packet into a vector, for comparing in tests
|
||||
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
|
||||
std::vector<uint8_t> result(data.size());
|
||||
data.copy_to(result.data(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
using Frame = std::vector<uint8_t>;
|
||||
|
||||
// A link layer that records what is sent, so a test can deliver it to the peer
|
||||
class MockLink : public PacketInterface {
|
||||
public:
|
||||
explicit MockLink(size_t max_packet_size = 64) : PacketInterface(max_packet_size) {}
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
|
||||
if (this->budget == 0) {
|
||||
this->refused++;
|
||||
return false;
|
||||
}
|
||||
this->budget--;
|
||||
this->sent.push_back(to_vector(data));
|
||||
return true;
|
||||
}
|
||||
void inject(const Frame &frame) { this->on_receive_from_interface_(PacketBuffer(frame), {}); }
|
||||
|
||||
std::vector<Frame> sent;
|
||||
size_t budget{SIZE_MAX}; // frames accepted before the link refuses, like a full network stack
|
||||
size_t refused{0};
|
||||
};
|
||||
|
||||
class TestHdlc : public HdlcReliability {
|
||||
public:
|
||||
using HdlcReliability::HdlcReliability;
|
||||
using HdlcReliability::on_timeout_;
|
||||
using HdlcReliability::outstanding_;
|
||||
};
|
||||
|
||||
struct Peer {
|
||||
explicit Peer(uint8_t queue_length = 4) {
|
||||
this->hdlc = std::make_unique<TestHdlc>(&this->link);
|
||||
this->hdlc->set_max_queue_length(queue_length);
|
||||
this->hdlc->set_max_retries(3);
|
||||
this->hdlc->setup();
|
||||
this->hdlc->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(to_vector(data)); });
|
||||
this->hdlc->add_on_session_state_callback([this](bool connected) { this->states.push_back(connected); });
|
||||
}
|
||||
bool send(const Frame &data) { return this->hdlc->send_to_interface(PacketBuffer(data), {}); }
|
||||
|
||||
MockLink link;
|
||||
std::unique_ptr<TestHdlc> hdlc;
|
||||
std::vector<Frame> received;
|
||||
std::vector<bool> states;
|
||||
};
|
||||
|
||||
// Returns true to drop a frame on its way from one peer to the other
|
||||
using DropFilter = std::function<bool(const Frame &)>;
|
||||
|
||||
static bool is_info(const Frame &frame) { return (frame[0] & 1) == 0; }
|
||||
static bool is_ack(const Frame &frame) { return (frame[0] & 0x0F) == 0x01; }
|
||||
|
||||
// Runs a layer's loop only while it has asked for it, as the main loop does
|
||||
static void run_loop(TestHdlc &hdlc) {
|
||||
if (!hdlc.is_idle())
|
||||
hdlc.loop();
|
||||
}
|
||||
|
||||
// Move frames between the peers and run their loops until nothing more is sent
|
||||
static void pump(Peer &a, Peer &b, const DropFilter &drop_a_to_b = nullptr, const DropFilter &drop_b_to_a = nullptr) {
|
||||
for (int i = 0; i != 50; i++) {
|
||||
run_loop(*a.hdlc);
|
||||
run_loop(*b.hdlc);
|
||||
if (a.link.sent.empty() && b.link.sent.empty())
|
||||
return;
|
||||
auto from_a = std::move(a.link.sent);
|
||||
auto from_b = std::move(b.link.sent);
|
||||
a.link.sent.clear();
|
||||
b.link.sent.clear();
|
||||
for (auto &frame : from_a) {
|
||||
if (!drop_a_to_b || !drop_a_to_b(frame))
|
||||
b.link.inject(frame);
|
||||
}
|
||||
for (auto &frame : from_b) {
|
||||
if (!drop_b_to_a || !drop_b_to_a(frame))
|
||||
a.link.inject(frame);
|
||||
}
|
||||
}
|
||||
FAIL() << "Peers did not go quiet";
|
||||
}
|
||||
|
||||
static Frame make_frame(uint8_t control, const Frame &payload) {
|
||||
Frame frame{control};
|
||||
frame.insert(frame.end(), payload.begin(), payload.end());
|
||||
uint16_t crc = crc16(frame.data(), frame.size());
|
||||
frame.push_back(crc & 0xFF);
|
||||
frame.push_back(crc >> 8);
|
||||
return frame;
|
||||
}
|
||||
|
||||
// Each test replaces App with a fresh one; the retransmit timer needs its scheduler
|
||||
class HdlcReliabilityTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
App.~Application();
|
||||
new (&App) Application();
|
||||
static char name[] = "hdlc-tests";
|
||||
static char friendly_name[] = "";
|
||||
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
|
||||
}
|
||||
void TearDown() override {
|
||||
App.~Application();
|
||||
new (&App) Application(); // later tests and the test listener use App
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(HdlcReliabilityTest, MaxPacketSizeAllowsForOverhead) {
|
||||
Peer a;
|
||||
EXPECT_EQ(a.hdlc->get_send_capacity(), 4);
|
||||
EXPECT_EQ(a.hdlc->get_max_packet_size(), 64 - HdlcReliability::FRAME_OVERHEAD);
|
||||
EXPECT_EQ(a.link.top(), a.hdlc.get());
|
||||
EXPECT_EQ(a.hdlc->get_capabilities(), CAPABILITY_RELIABILITY);
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, ResetHandshake) {
|
||||
Peer a, b;
|
||||
a.hdlc->loop();
|
||||
ASSERT_EQ(a.link.sent.size(), 1);
|
||||
EXPECT_EQ(a.link.sent[0], make_frame(0x2F, {})); // SABM
|
||||
pump(a, b);
|
||||
EXPECT_TRUE(a.hdlc->is_connected());
|
||||
EXPECT_TRUE(b.hdlc->is_connected());
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, RepeatedSabmBeforeNumberedFramesKeepsQueue) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
// A frame is queued and sent, but has not reached A, so A has sent no numbered frame
|
||||
ASSERT_TRUE(b.send({0x42}));
|
||||
auto held = std::move(b.link.sent);
|
||||
b.link.sent.clear();
|
||||
EXPECT_EQ(b.hdlc->get_send_capacity(), 3);
|
||||
|
||||
// A repeats its SABM because it did not hear the UA; that is not a restart
|
||||
b.link.inject(make_frame(0x2F, {})); // SABM
|
||||
EXPECT_TRUE(b.hdlc->is_connected());
|
||||
EXPECT_TRUE(b.states.empty()); // no session lost reported
|
||||
EXPECT_EQ(b.hdlc->get_send_capacity(), 3); // the queued frame was kept
|
||||
ASSERT_FALSE(b.link.sent.empty());
|
||||
EXPECT_EQ(b.link.sent[0], make_frame(0x63, {})); // UA, then the frame is sent again
|
||||
|
||||
// The frame is still delivered when it reaches A
|
||||
for (auto &frame : held)
|
||||
a.link.inject(frame);
|
||||
EXPECT_EQ(a.received, std::vector<Frame>{{0x42}});
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, SabmAfterNumberedFramesIsARestart) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
// B has heard a numbered frame, so a SABM now means A has started again
|
||||
ASSERT_TRUE(a.send({0x10}));
|
||||
pump(a, b);
|
||||
b.link.inject(make_frame(0x2F, {})); // SABM
|
||||
EXPECT_TRUE(b.hdlc->is_connected());
|
||||
EXPECT_EQ(b.states, std::vector<bool>{false});
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, QueuedBeforeConnectIsSentAfter) {
|
||||
Peer a, b;
|
||||
ASSERT_TRUE(a.send({0x01}));
|
||||
ASSERT_TRUE(a.send({0x02}));
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0x01}, {0x02}}));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, DeliversInOrderBothWays) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
a.send({0x10});
|
||||
b.send({0x20});
|
||||
a.send({0x11});
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0x10}, {0x11}}));
|
||||
EXPECT_EQ(a.received, (std::vector<Frame>{{0x20}}));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, SequenceNumbersWrap) {
|
||||
Peer a(3), b(3);
|
||||
pump(a, b);
|
||||
std::vector<Frame> expected;
|
||||
for (uint8_t i = 0; i != 20; i++) {
|
||||
expected.push_back({i, static_cast<uint8_t>(i * 3)});
|
||||
ASSERT_TRUE(a.send(expected.back()));
|
||||
if (i % 2 == 1)
|
||||
pump(a, b);
|
||||
}
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, expected);
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, GapRecoveredByReject) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
bool dropped = false;
|
||||
a.send({0x01});
|
||||
a.send({0x02});
|
||||
a.send({0x03});
|
||||
pump(a, b, [&dropped](const Frame &frame) {
|
||||
if (dropped || !is_info(frame))
|
||||
return false;
|
||||
dropped = true;
|
||||
return true;
|
||||
});
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0x01}, {0x02}, {0x03}}));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, LossRecoveredByTimeout) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
a.send({0x42});
|
||||
pump(a, b, [](const Frame &frame) { return is_info(frame); });
|
||||
EXPECT_TRUE(b.received.empty());
|
||||
a.hdlc->on_timeout_();
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0x42}}));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, LostAckDoesNotDuplicate) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
a.send({0x42});
|
||||
pump(a, b, nullptr, [](const Frame &frame) { return is_ack(frame); });
|
||||
ASSERT_EQ(b.received.size(), 1);
|
||||
a.hdlc->on_timeout_();
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received.size(), 1);
|
||||
// The resend was acknowledged, so the full queue is available again
|
||||
for (int i = 0; i != 4; i++)
|
||||
EXPECT_TRUE(a.send({0x01}));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, QueueFull) {
|
||||
Peer a(2), b(2);
|
||||
pump(a, b);
|
||||
EXPECT_TRUE(a.send({0x01}));
|
||||
EXPECT_TRUE(a.send({0x02}));
|
||||
EXPECT_FALSE(a.send({0x03}));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, OversizedPacketRejected) {
|
||||
Peer a;
|
||||
EXPECT_FALSE(a.send(Frame(a.hdlc->get_max_packet_size() + 1)));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, CrcErrorDropped) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
Frame frame = make_frame(0x00, {0x55}); // I-frame N(S)=0 N(R)=0
|
||||
frame[1] ^= 0xFF;
|
||||
b.link.inject(frame);
|
||||
EXPECT_TRUE(b.received.empty());
|
||||
b.link.inject(make_frame(0x00, {0x55}));
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0x55}}));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, RetriesExhaustedResetsLink) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
// One frame is delivered first, so b can tell a reset from a repeated SABM
|
||||
ASSERT_TRUE(a.send({0x00}));
|
||||
pump(a, b);
|
||||
ASSERT_EQ(b.received, std::vector<Frame>{{0x00}});
|
||||
a.send({0x01});
|
||||
pump(a, b, [](const Frame &frame) { return is_info(frame); });
|
||||
for (int i = 0; i != 4; i++) {
|
||||
a.hdlc->on_timeout_();
|
||||
pump(a, b, [](const Frame &frame) { return is_info(frame); });
|
||||
}
|
||||
// The unacknowledged frame was discarded and the link was reset, which both report as data lost
|
||||
EXPECT_EQ(b.received, std::vector<Frame>{{0x00}});
|
||||
EXPECT_TRUE(a.hdlc->is_connected());
|
||||
EXPECT_EQ(a.states, std::vector<bool>{false});
|
||||
EXPECT_EQ(b.states, std::vector<bool>{false});
|
||||
a.send({0x02});
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0x00}, {0x02}}));
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, PeerRestart) {
|
||||
Peer a;
|
||||
auto b = std::make_unique<Peer>();
|
||||
pump(a, *b);
|
||||
a.send({0x01});
|
||||
pump(a, *b);
|
||||
// The peer restarts with fresh sequence numbers
|
||||
b = std::make_unique<Peer>();
|
||||
pump(a, *b);
|
||||
a.send({0x02});
|
||||
b->send({0x03});
|
||||
pump(a, *b);
|
||||
EXPECT_EQ(b->received, (std::vector<Frame>{{0x02}}));
|
||||
EXPECT_EQ(a.received, (std::vector<Frame>{{0x03}}));
|
||||
// The restarted peer's reset discarded what a had not yet had acknowledged
|
||||
EXPECT_EQ(a.states, std::vector<bool>{false});
|
||||
EXPECT_TRUE(b->states.empty());
|
||||
}
|
||||
|
||||
// Waits past the retry after a refused send, servicing timers as the main loop does
|
||||
static void run_send_retry() {
|
||||
const uint32_t start = millis();
|
||||
while (millis() - start < 60) {
|
||||
App.scheduler.call(millis());
|
||||
delay(1);
|
||||
}
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, RefusedFramesSentAgainWithoutTimeout) {
|
||||
Peer a, b;
|
||||
// Only the retry of a refused send can fire while the test runs, not a retransmit
|
||||
a.hdlc->set_ack_timeout(10000);
|
||||
b.hdlc->set_ack_timeout(10000);
|
||||
for (uint8_t i = 0; i != 3; i++)
|
||||
ASSERT_TRUE(a.send({i}));
|
||||
a.link.budget = 3; // SABM, UA to the peer's SABM, and the first queued frame
|
||||
pump(a, b);
|
||||
EXPECT_GE(a.link.refused, 1);
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0}}));
|
||||
a.link.budget = SIZE_MAX; // the lower layer accepts again, as when a radio stops receiving
|
||||
// The layer waits for its retry rather than running every loop
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0}}));
|
||||
run_send_retry();
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{0}, {1}, {2}}));
|
||||
EXPECT_EQ(a.hdlc->outstanding_(), 0);
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, RefusedAckSentAgain) {
|
||||
Peer a, b;
|
||||
a.hdlc->set_ack_timeout(10000);
|
||||
b.hdlc->set_ack_timeout(10000);
|
||||
pump(a, b);
|
||||
a.send({1});
|
||||
for (auto &frame : a.link.sent)
|
||||
b.link.inject(frame);
|
||||
a.link.sent.clear();
|
||||
b.link.budget = 0;
|
||||
b.hdlc->loop();
|
||||
EXPECT_TRUE(b.link.sent.empty());
|
||||
b.link.budget = SIZE_MAX;
|
||||
pump(a, b);
|
||||
EXPECT_EQ(a.hdlc->outstanding_(), 1); // the RR waits for the retry
|
||||
run_send_retry();
|
||||
pump(a, b);
|
||||
EXPECT_EQ(a.hdlc->outstanding_(), 0);
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, AckDelayLetsReplyCarryAck) {
|
||||
Peer a, b;
|
||||
b.hdlc->set_ack_delay(100);
|
||||
pump(a, b);
|
||||
a.send({1});
|
||||
for (auto &frame : a.link.sent)
|
||||
b.link.inject(frame);
|
||||
a.link.sent.clear();
|
||||
b.hdlc->loop();
|
||||
EXPECT_TRUE(b.link.sent.empty()); // no RR yet
|
||||
b.send({2});
|
||||
ASSERT_EQ(b.link.sent.size(), 1);
|
||||
EXPECT_TRUE(is_info(b.link.sent[0]));
|
||||
EXPECT_EQ(b.link.sent[0][0] >> 5, 1); // N(R) acknowledges the frame from a
|
||||
b.link.sent.clear();
|
||||
b.hdlc->loop();
|
||||
EXPECT_TRUE(b.link.sent.empty()); // the RR is no longer owed
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, MaxRetriesOf255ResetsLink) {
|
||||
Peer a, b;
|
||||
a.hdlc->set_max_retries(255);
|
||||
pump(a, b);
|
||||
a.send({0x01});
|
||||
a.link.sent.clear();
|
||||
for (int i = 0; i != 255; i++) {
|
||||
a.hdlc->on_timeout_();
|
||||
a.link.sent.clear();
|
||||
}
|
||||
EXPECT_TRUE(a.states.empty());
|
||||
a.hdlc->on_timeout_();
|
||||
EXPECT_EQ(a.states, std::vector<bool>{false});
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, OversizedFrameDropped) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
// An I-frame, N(S) 0 and N(R) 0, one byte larger than this layer's packets
|
||||
b.link.inject(make_frame(0x00, Frame(b.hdlc->get_max_packet_size() + 1, 0x55)));
|
||||
EXPECT_TRUE(b.received.empty());
|
||||
b.link.inject(make_frame(0x00, Frame(b.hdlc->get_max_packet_size(), 0x55)));
|
||||
EXPECT_EQ(b.received.size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, LinkTooSmallFailsSetup) {
|
||||
MockLink link(HdlcReliability::FRAME_OVERHEAD);
|
||||
TestHdlc hdlc(&link);
|
||||
EXPECT_EQ(hdlc.get_max_packet_size(), 0);
|
||||
hdlc.setup();
|
||||
EXPECT_TRUE(hdlc.is_failed());
|
||||
Frame packet{1};
|
||||
EXPECT_FALSE(hdlc.send_to_interface(PacketBuffer(packet), {}));
|
||||
EXPECT_EQ(hdlc.get_send_capacity(), 0);
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, AckDelayTimerSendsAck) {
|
||||
Peer a, b;
|
||||
b.hdlc->set_ack_delay(5);
|
||||
pump(a, b);
|
||||
a.send({1});
|
||||
for (auto &frame : a.link.sent)
|
||||
b.link.inject(frame);
|
||||
a.link.sent.clear();
|
||||
b.hdlc->loop();
|
||||
EXPECT_TRUE(b.link.sent.empty());
|
||||
delay(10);
|
||||
App.scheduler.call(millis());
|
||||
b.hdlc->loop();
|
||||
ASSERT_EQ(b.link.sent.size(), 1);
|
||||
EXPECT_TRUE(is_ack(b.link.sent[0]));
|
||||
EXPECT_EQ(b.link.sent[0][0] >> 5, 1); // N(R) acknowledges the frame from a
|
||||
}
|
||||
|
||||
TEST_F(HdlcReliabilityTest, RefusedRejectFallsBackToAck) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
a.send({1});
|
||||
a.send({2});
|
||||
ASSERT_EQ(a.link.sent.size(), 2);
|
||||
// The first frame is lost, and the REJ for the gap is refused
|
||||
b.link.budget = 0;
|
||||
b.link.inject(a.link.sent[1]);
|
||||
a.link.sent.clear();
|
||||
EXPECT_EQ(b.link.refused, 1);
|
||||
b.link.budget = SIZE_MAX;
|
||||
b.hdlc->loop();
|
||||
ASSERT_EQ(b.link.sent.size(), 1);
|
||||
EXPECT_TRUE(is_ack(b.link.sent[0]));
|
||||
EXPECT_EQ(b.link.sent[0][0] >> 5, 0);
|
||||
a.hdlc->on_timeout_();
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Frame>{{1}, {2}}));
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface::testing
|
||||
@@ -1,238 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include "esphome/components/packet_interface/packet_buffer.h"
|
||||
|
||||
namespace esphome::packet_interface::testing {
|
||||
|
||||
// Copies a packet into a vector, for comparing in tests
|
||||
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
|
||||
std::vector<uint8_t> result(data.size());
|
||||
data.copy_to(result.data(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Test default constructor
|
||||
TEST(PacketBufferTest, DefaultConstructor) {
|
||||
PacketBuffer buffer;
|
||||
EXPECT_EQ(buffer.size(), 0);
|
||||
EXPECT_TRUE(buffer.empty());
|
||||
}
|
||||
|
||||
// Test single buffer constructor
|
||||
TEST(PacketBufferTest, SingleBufferConstructor) {
|
||||
uint8_t data[] = {0x01, 0x02, 0x03, 0x04};
|
||||
PacketBuffer buffer(data, 4);
|
||||
EXPECT_EQ(buffer.size(), 4);
|
||||
EXPECT_FALSE(buffer.empty());
|
||||
EXPECT_EQ(buffer[0], 0x01);
|
||||
EXPECT_EQ(buffer[3], 0x04);
|
||||
}
|
||||
|
||||
// Test vector constructor
|
||||
TEST(PacketBufferTest, VectorConstructor) {
|
||||
std::vector<uint8_t> vec = {0x10, 0x20, 0x30};
|
||||
PacketBuffer buffer(vec);
|
||||
EXPECT_EQ(buffer.size(), 3);
|
||||
EXPECT_EQ(buffer[0], 0x10);
|
||||
EXPECT_EQ(buffer[1], 0x20);
|
||||
EXPECT_EQ(buffer[2], 0x30);
|
||||
}
|
||||
|
||||
// Test pre and post buffer constructor
|
||||
TEST(PacketBufferTest, PrePostConstructor) {
|
||||
uint8_t pre[] = {0x01, 0x02};
|
||||
uint8_t post[] = {0x03, 0x04, 0x05};
|
||||
PacketBuffer buffer(pre, 2, post, 3);
|
||||
EXPECT_EQ(buffer.size(), 5);
|
||||
EXPECT_EQ(buffer[0], 0x01);
|
||||
EXPECT_EQ(buffer[1], 0x02);
|
||||
EXPECT_EQ(buffer[2], 0x03);
|
||||
EXPECT_EQ(buffer[3], 0x04);
|
||||
EXPECT_EQ(buffer[4], 0x05);
|
||||
}
|
||||
|
||||
// Test pre, mid, and post buffer constructor
|
||||
TEST(PacketBufferTest, PreMidPostConstructor) {
|
||||
uint8_t pre[] = {0x01};
|
||||
std::vector<uint8_t> mid_vec = {0x02, 0x03, 0x04};
|
||||
PacketBuffer mid_buffer(mid_vec);
|
||||
uint8_t post[] = {0x05, 0x06};
|
||||
|
||||
PacketBuffer buffer(pre, 1, &mid_buffer, post, 2);
|
||||
EXPECT_EQ(buffer.size(), 6);
|
||||
EXPECT_EQ(buffer[0], 0x01);
|
||||
EXPECT_EQ(buffer[1], 0x02);
|
||||
EXPECT_EQ(buffer[2], 0x03);
|
||||
EXPECT_EQ(buffer[3], 0x04);
|
||||
EXPECT_EQ(buffer[4], 0x05);
|
||||
EXPECT_EQ(buffer[5], 0x06);
|
||||
}
|
||||
|
||||
// Test nested mid buffers
|
||||
TEST(PacketBufferTest, NestedMidBuffers) {
|
||||
uint8_t data1[] = {0x01};
|
||||
uint8_t data2[] = {0x02, 0x03};
|
||||
uint8_t data3[] = {0x04};
|
||||
|
||||
PacketBuffer inner(data2, 2);
|
||||
PacketBuffer middle(data1, 1, &inner, data3, 1);
|
||||
|
||||
uint8_t pre[] = {0x00};
|
||||
uint8_t post[] = {0x05};
|
||||
PacketBuffer outer(pre, 1, &middle, post, 1);
|
||||
|
||||
EXPECT_EQ(outer.size(), 6);
|
||||
EXPECT_EQ(outer[0], 0x00);
|
||||
EXPECT_EQ(outer[1], 0x01);
|
||||
EXPECT_EQ(outer[2], 0x02);
|
||||
EXPECT_EQ(outer[3], 0x03);
|
||||
EXPECT_EQ(outer[4], 0x04);
|
||||
EXPECT_EQ(outer[5], 0x05);
|
||||
}
|
||||
|
||||
// Test indexing operator
|
||||
TEST(PacketBufferTest, IndexingOperator) {
|
||||
uint8_t data[] = {0xAA, 0xBB, 0xCC, 0xDD};
|
||||
PacketBuffer buffer(data, 4);
|
||||
EXPECT_EQ(buffer[0], 0xAA);
|
||||
EXPECT_EQ(buffer[1], 0xBB);
|
||||
EXPECT_EQ(buffer[2], 0xCC);
|
||||
EXPECT_EQ(buffer[3], 0xDD);
|
||||
}
|
||||
|
||||
// Test copy_to method
|
||||
TEST(PacketBufferTest, CopyTo) {
|
||||
uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
|
||||
PacketBuffer buffer(data, 5);
|
||||
|
||||
uint8_t dest[5] = {0};
|
||||
size_t copied = buffer.copy_to(dest, 5);
|
||||
|
||||
EXPECT_EQ(copied, 5);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
EXPECT_EQ(dest[i], data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test copy_to with offset
|
||||
TEST(PacketBufferTest, CopyToWithOffset) {
|
||||
uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
|
||||
PacketBuffer buffer(data, 5);
|
||||
|
||||
uint8_t dest[3] = {0};
|
||||
size_t copied = buffer.copy_to(dest, 3, 2); // Copy 3 bytes starting from offset 2
|
||||
|
||||
EXPECT_EQ(copied, 3);
|
||||
EXPECT_EQ(dest[0], 0x03);
|
||||
EXPECT_EQ(dest[1], 0x04);
|
||||
EXPECT_EQ(dest[2], 0x05);
|
||||
}
|
||||
|
||||
// Test copy_to with partial copy
|
||||
TEST(PacketBufferTest, CopyToPartial) {
|
||||
uint8_t data[] = {0x01, 0x02, 0x03};
|
||||
PacketBuffer buffer(data, 3);
|
||||
|
||||
uint8_t dest[5] = {0};
|
||||
size_t copied = buffer.copy_to(dest, 5); // Request more than available
|
||||
|
||||
EXPECT_EQ(copied, 3); // Only 3 bytes copied
|
||||
}
|
||||
|
||||
// Test copy_to from composite buffer
|
||||
TEST(PacketBufferTest, CopyToComposite) {
|
||||
uint8_t pre[] = {0x01, 0x02};
|
||||
uint8_t mid[] = {0x03, 0x04, 0x05};
|
||||
uint8_t post[] = {0x06};
|
||||
|
||||
PacketBuffer mid_buffer(mid, 3);
|
||||
PacketBuffer buffer(pre, 2, &mid_buffer, post, 1);
|
||||
|
||||
uint8_t dest[6] = {0};
|
||||
size_t copied = buffer.copy_to(dest, 6);
|
||||
|
||||
EXPECT_EQ(copied, 6);
|
||||
EXPECT_EQ(dest[0], 0x01);
|
||||
EXPECT_EQ(dest[1], 0x02);
|
||||
EXPECT_EQ(dest[2], 0x03);
|
||||
EXPECT_EQ(dest[3], 0x04);
|
||||
EXPECT_EQ(dest[4], 0x05);
|
||||
EXPECT_EQ(dest[5], 0x06);
|
||||
}
|
||||
|
||||
// Test copying from an offset past the mid buffer
|
||||
TEST(PacketBufferTest, CopyToOffsetPastMid) {
|
||||
uint8_t pre[] = {0x01, 0x02};
|
||||
uint8_t mid_data[] = {0x03};
|
||||
uint8_t post[] = {0x04, 0x05, 0x06};
|
||||
PacketBuffer mid(mid_data, 1);
|
||||
PacketBuffer buffer(pre, 2, &mid, post, 3);
|
||||
|
||||
uint8_t dest[4]{};
|
||||
ASSERT_EQ(buffer.copy_to(dest, 4, 4), 2);
|
||||
EXPECT_EQ(dest[0], 0x05);
|
||||
EXPECT_EQ(dest[1], 0x06);
|
||||
}
|
||||
|
||||
// Test single span access
|
||||
TEST(PacketBufferTest, ContiguousSpan) {
|
||||
uint8_t data[] = {0x10, 0x20, 0x30, 0x40};
|
||||
PacketBuffer buffer(data, 4);
|
||||
|
||||
ASSERT_TRUE(buffer.is_contiguous());
|
||||
auto span = buffer.as_span();
|
||||
EXPECT_EQ(span.data(), data);
|
||||
EXPECT_EQ(span.size(), 4);
|
||||
|
||||
uint8_t post[] = {0x50};
|
||||
PacketBuffer split(data, 4, post, 1);
|
||||
EXPECT_FALSE(split.is_contiguous());
|
||||
|
||||
PacketBuffer nested(nullptr, 0, &buffer, nullptr, 0);
|
||||
EXPECT_FALSE(nested.is_contiguous());
|
||||
}
|
||||
|
||||
// Test indexing past the end
|
||||
TEST(PacketBufferTest, IndexPastEnd) {
|
||||
uint8_t pre[] = {0x01};
|
||||
uint8_t post[] = {0x02};
|
||||
PacketBuffer buffer(pre, 1, post, 1);
|
||||
EXPECT_EQ(buffer[1], 0x02);
|
||||
EXPECT_EQ(buffer[2], 0);
|
||||
EXPECT_EQ(PacketBuffer()[0], 0);
|
||||
}
|
||||
|
||||
// Test empty buffer operations
|
||||
TEST(PacketBufferTest, EmptyBuffer) {
|
||||
PacketBuffer buffer;
|
||||
|
||||
EXPECT_TRUE(buffer.empty());
|
||||
EXPECT_EQ(buffer.size(), 0);
|
||||
|
||||
uint8_t dest[10] = {0xFF};
|
||||
size_t copied = buffer.copy_to(dest, 10);
|
||||
EXPECT_EQ(copied, 0);
|
||||
EXPECT_EQ(dest[0], 0xFF); // Unchanged
|
||||
|
||||
std::vector<uint8_t> vec = to_vector(buffer);
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
}
|
||||
|
||||
// Test large buffer
|
||||
TEST(PacketBufferTest, LargeBuffer) {
|
||||
std::vector<uint8_t> large_vec(1000);
|
||||
for (size_t i = 0; i < large_vec.size(); i++) {
|
||||
large_vec[i] = static_cast<uint8_t>(i % 256);
|
||||
}
|
||||
|
||||
PacketBuffer buffer(large_vec);
|
||||
|
||||
EXPECT_EQ(buffer.size(), 1000);
|
||||
EXPECT_EQ(buffer[0], 0);
|
||||
EXPECT_EQ(buffer[255], 255);
|
||||
EXPECT_EQ(buffer[256], 0);
|
||||
EXPECT_EQ(buffer[999], 231); // 999 % 256 = 231
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface::testing
|
||||
@@ -1,92 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include "esphome/components/packet_interface/packet_transport/packet_interface_transport.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::packet_interface::transport_testing {
|
||||
|
||||
using Packet = std::vector<uint8_t>;
|
||||
|
||||
// A link layer that records what is sent, and passes injected packets to its listeners
|
||||
class FakeInterface : public PacketInterface {
|
||||
public:
|
||||
explicit FakeInterface(size_t max_packet_size = 512) : PacketInterface(max_packet_size) {}
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
|
||||
Packet packet(data.size());
|
||||
data.copy_to(packet.data(), packet.size());
|
||||
this->sent.push_back(packet);
|
||||
return true;
|
||||
}
|
||||
void deliver(const PacketBuffer &data) { this->on_receive_from_interface_(data, {}); }
|
||||
|
||||
std::vector<Packet> sent;
|
||||
};
|
||||
|
||||
// A layer stacked on a link, which the bridge must use in place of the link
|
||||
class FakeLayer : public FakeInterface {
|
||||
public:
|
||||
explicit FakeLayer(PacketInterface *lower) : FakeInterface(lower->get_max_packet_size()) {
|
||||
this->stack_on_(lower, CAPABILITY_ENCRYPTION);
|
||||
}
|
||||
};
|
||||
|
||||
class TestBridge : public PacketInterfaceTransport {
|
||||
public:
|
||||
using PacketInterfaceTransport::PacketInterfaceTransport;
|
||||
using PacketInterfaceTransport::send_packet;
|
||||
using packet_transport::PacketTransport::ping_keys_;
|
||||
};
|
||||
|
||||
// A packet_transport ping packet; with an encryption key set, process_() stores the sender's key
|
||||
static Packet ping_packet(const char *hostname, uint32_t key) {
|
||||
Packet packet{0x48, 0x50, static_cast<uint8_t>(strlen(hostname))};
|
||||
packet.insert(packet.end(), hostname, hostname + strlen(hostname));
|
||||
for (int shift = 0; shift != 32; shift += 8)
|
||||
packet.push_back(static_cast<uint8_t>(key >> shift));
|
||||
return packet;
|
||||
}
|
||||
|
||||
// Each test replaces App with a fresh one, which the transport's setup() reads the device name from
|
||||
class PacketInterfaceTransportTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
App.~Application();
|
||||
new (&App) Application();
|
||||
static char name[] = "bridge-tests";
|
||||
static char friendly_name[] = "";
|
||||
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
|
||||
this->bridge_.set_encryption_key(std::vector<uint8_t>(32, 0xAA));
|
||||
this->bridge_.setup();
|
||||
}
|
||||
void TearDown() override {
|
||||
App.~Application();
|
||||
new (&App) Application(); // later tests and the test listener use App
|
||||
}
|
||||
|
||||
FakeInterface link_;
|
||||
FakeLayer layer_{&this->link_};
|
||||
TestBridge bridge_{&this->link_};
|
||||
};
|
||||
|
||||
TEST_F(PacketInterfaceTransportTest, SendsThroughTopOfStack) {
|
||||
this->bridge_.send_packet({1, 2, 3});
|
||||
EXPECT_EQ(this->layer_.sent, (std::vector<Packet>{{1, 2, 3}}));
|
||||
EXPECT_TRUE(this->link_.sent.empty());
|
||||
}
|
||||
|
||||
TEST_F(PacketInterfaceTransportTest, PacketInOneBufferReachesTransport) {
|
||||
Packet ping = ping_packet("requester", 0xDEADBEEF);
|
||||
this->layer_.deliver(PacketBuffer(ping));
|
||||
ASSERT_EQ(this->bridge_.ping_keys_.size(), 1u);
|
||||
EXPECT_EQ(this->bridge_.ping_keys_["requester"], 0xDEADBEEFu);
|
||||
}
|
||||
|
||||
TEST_F(PacketInterfaceTransportTest, PacketSplitAcrossBuffersDropped) {
|
||||
Packet ping = ping_packet("requester", 0xDEADBEEF);
|
||||
this->layer_.deliver(PacketBuffer(ping.data(), 4, ping.data() + 4, ping.size() - 4));
|
||||
EXPECT_TRUE(this->bridge_.ping_keys_.empty());
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface::transport_testing
|
||||
@@ -1,3 +0,0 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
packet_interface: !include common.yaml
|
||||
@@ -1,282 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "esphome/components/packet_interface/xxtea_encryption.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::packet_interface::encryption_testing {
|
||||
|
||||
// Copies a packet into a vector, for comparing in tests
|
||||
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
|
||||
std::vector<uint8_t> result(data.size());
|
||||
data.copy_to(result.data(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
using Packet = std::vector<uint8_t>;
|
||||
|
||||
// A link layer that records what is sent, so a test can deliver it to the peer
|
||||
class MockLink : public PacketInterface {
|
||||
public:
|
||||
explicit MockLink(size_t max_packet_size = 64) : PacketInterface(max_packet_size) {}
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
|
||||
this->sent.push_back(to_vector(data));
|
||||
return true;
|
||||
}
|
||||
void inject(const Packet &packet) { this->on_receive_from_interface_(PacketBuffer(packet), {}); }
|
||||
|
||||
std::vector<Packet> sent;
|
||||
};
|
||||
|
||||
static constexpr size_t ROLLING_TRAILER_SIZE = 16;
|
||||
static constexpr size_t TRAILER_SIZE = 4;
|
||||
|
||||
class TestEncryption : public XxteaEncryption {
|
||||
public:
|
||||
using XxteaEncryption::XxteaEncryption;
|
||||
uint32_t get_boot() const { return this->boot_; }
|
||||
void set_counter(uint32_t counter) { this->counter_ = counter; }
|
||||
};
|
||||
|
||||
struct Peer {
|
||||
explicit Peer(uint32_t key_word = 0x12345678, size_t max_packet_size = 64, bool rolling_code = true)
|
||||
: link(max_packet_size) {
|
||||
this->link.set_name("test_link");
|
||||
this->encryption = std::make_unique<TestEncryption>(&this->link, rolling_code);
|
||||
this->encryption->set_key({key_word, 2, 3, 4, 5, 6, 7, 8});
|
||||
this->encryption->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(to_vector(data)); });
|
||||
this->encryption->setup();
|
||||
}
|
||||
bool send(const Packet &data) { return this->encryption->send_to_interface(PacketBuffer(data), {}); }
|
||||
// Deliver everything sent so far to the peer
|
||||
void deliver_to(Peer &peer) {
|
||||
for (auto &packet : this->link.sent)
|
||||
peer.link.inject(packet);
|
||||
this->link.sent.clear();
|
||||
}
|
||||
// Exchange a packet each way so that each peer echoes the other's boot number
|
||||
void introduce(Peer &peer) {
|
||||
this->send({});
|
||||
this->deliver_to(peer);
|
||||
peer.send({});
|
||||
peer.deliver_to(*this);
|
||||
this->send({});
|
||||
this->deliver_to(peer);
|
||||
this->received.clear();
|
||||
peer.received.clear();
|
||||
}
|
||||
|
||||
MockLink link;
|
||||
std::unique_ptr<TestEncryption> encryption;
|
||||
std::vector<Packet> received;
|
||||
};
|
||||
|
||||
// Each test replaces App with a fresh one
|
||||
class XxteaEncryptionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
App.~Application();
|
||||
new (&App) Application();
|
||||
static char name[] = "xxtea-tests";
|
||||
static char friendly_name[] = "";
|
||||
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
|
||||
}
|
||||
void TearDown() override {
|
||||
App.~Application();
|
||||
new (&App) Application(); // later tests and the test listener use App
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(XxteaEncryptionTest, MaxPacketSizeAllowsForTrailerAndWords) {
|
||||
Peer a;
|
||||
EXPECT_EQ(a.encryption->get_max_packet_size(), 64 - ROLLING_TRAILER_SIZE);
|
||||
EXPECT_EQ(a.link.top(), a.encryption.get());
|
||||
EXPECT_TRUE(a.encryption->has_capability(CAPABILITY_ENCRYPTION));
|
||||
EXPECT_FALSE(a.encryption->has_capability(CAPABILITY_SESSION));
|
||||
Peer b(0x12345678, 63);
|
||||
EXPECT_EQ(b.encryption->get_max_packet_size(), 60 - ROLLING_TRAILER_SIZE);
|
||||
Peer c(0x12345678, 64, false);
|
||||
EXPECT_EQ(c.encryption->get_max_packet_size(), 64 - TRAILER_SIZE);
|
||||
}
|
||||
|
||||
static void round_trip_all_sizes(bool rolling_code, size_t trailer_size) {
|
||||
Peer a(0x12345678, 64, rolling_code), b(0x12345678, 64, rolling_code);
|
||||
a.introduce(b);
|
||||
std::vector<Packet> expected;
|
||||
for (size_t size = 0; size <= a.encryption->get_max_packet_size(); size++) {
|
||||
Packet packet(size);
|
||||
for (size_t i = 0; i != size; i++)
|
||||
packet[i] = static_cast<uint8_t>(size + i);
|
||||
expected.push_back(packet);
|
||||
ASSERT_TRUE(a.send(packet));
|
||||
ASSERT_EQ(a.link.sent.back().size() % 4, 0);
|
||||
ASSERT_GE(a.link.sent.back().size(), std::max<size_t>(8, size + trailer_size));
|
||||
}
|
||||
a.deliver_to(b);
|
||||
EXPECT_EQ(b.received, expected);
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, RoundTripAllSizes) { round_trip_all_sizes(true, ROLLING_TRAILER_SIZE); }
|
||||
|
||||
TEST_F(XxteaEncryptionTest, RoundTripAllSizesWithoutRollingCode) { round_trip_all_sizes(false, TRAILER_SIZE); }
|
||||
|
||||
TEST_F(XxteaEncryptionTest, WithoutRollingCodeReplayAccepted) {
|
||||
Peer a(0x12345678, 64, false), b(0x12345678, 64, false);
|
||||
a.send({1});
|
||||
b.link.inject(a.link.sent[0]);
|
||||
b.link.inject(a.link.sent[0]);
|
||||
EXPECT_EQ(b.received, (std::vector<Packet>{{1}, {1}}));
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, RollingCodeSettingMustMatch) {
|
||||
Peer a(0x12345678, 64, true), b(0x12345678, 64, false);
|
||||
a.send({1, 2, 3, 4, 5, 6, 7, 8});
|
||||
b.send({1, 2, 3, 4, 5, 6, 7, 8});
|
||||
a.deliver_to(b);
|
||||
b.deliver_to(a);
|
||||
EXPECT_TRUE(a.received.empty());
|
||||
EXPECT_TRUE(b.received.empty());
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, SamePacketEncryptsDifferently) {
|
||||
Peer a;
|
||||
a.send({1, 2, 3, 4});
|
||||
a.send({1, 2, 3, 4});
|
||||
ASSERT_EQ(a.link.sent.size(), 2);
|
||||
EXPECT_NE(a.link.sent[0], a.link.sent[1]);
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, OversizedPacketRefused) {
|
||||
Peer a;
|
||||
EXPECT_FALSE(a.send(Packet(a.encryption->get_max_packet_size() + 1)));
|
||||
EXPECT_TRUE(a.link.sent.empty());
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, WrongKeyDropped) {
|
||||
Peer a, b(0x87654321);
|
||||
a.send({1, 2, 3});
|
||||
a.deliver_to(b);
|
||||
EXPECT_TRUE(b.received.empty());
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, ChangedPacketDropped) {
|
||||
Peer a, b;
|
||||
a.send({1, 2, 3});
|
||||
a.link.sent[0][2] ^= 0x01;
|
||||
a.deliver_to(b);
|
||||
EXPECT_TRUE(b.received.empty());
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, BadLengthDropped) {
|
||||
Peer a, b;
|
||||
a.send({1, 2, 3});
|
||||
Packet packet = a.link.sent[0];
|
||||
b.link.inject(Packet(packet.begin(), packet.end() - 1));
|
||||
b.link.inject(Packet(8));
|
||||
EXPECT_TRUE(b.received.empty());
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, ReplayDropped) {
|
||||
Peer a, b;
|
||||
a.introduce(b);
|
||||
a.send({1});
|
||||
Packet first = a.link.sent[0];
|
||||
a.send({2});
|
||||
a.deliver_to(b);
|
||||
b.link.inject(first);
|
||||
EXPECT_EQ(b.received, (std::vector<Packet>{{1}, {2}}));
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, OlderPacketDropped) {
|
||||
Peer a, b;
|
||||
a.introduce(b);
|
||||
a.send({1});
|
||||
a.send({2});
|
||||
b.link.inject(a.link.sent[1]);
|
||||
b.link.inject(a.link.sent[0]);
|
||||
EXPECT_EQ(b.received, (std::vector<Packet>{{2}}));
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, BootNumberIncreasesAndPeerRestartAccepted) {
|
||||
Peer b;
|
||||
auto a = std::make_unique<Peer>();
|
||||
uint32_t boot = a->encryption->get_boot();
|
||||
a->introduce(b);
|
||||
a->send({1});
|
||||
a->send({2});
|
||||
a->deliver_to(b);
|
||||
EXPECT_EQ(b.received, (std::vector<Packet>{{1}, {2}}));
|
||||
a = std::make_unique<Peer>(); // the peer restarts, and its counter starts again from 0
|
||||
EXPECT_EQ(a->encryption->get_boot(), boot + 1);
|
||||
a->introduce(b);
|
||||
a->send({3});
|
||||
a->deliver_to(b);
|
||||
EXPECT_EQ(b.received, std::vector<Packet>{{3}});
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, FirstPacketsDroppedUntilEchoed) {
|
||||
Peer a, b;
|
||||
a.send({1});
|
||||
a.deliver_to(b);
|
||||
EXPECT_TRUE(b.received.empty()); // a does not know b's boot number yet
|
||||
b.send({2});
|
||||
b.deliver_to(a);
|
||||
EXPECT_EQ(a.received, std::vector<Packet>{{2}}); // b learnt a's from the dropped packet
|
||||
a.send({3});
|
||||
a.deliver_to(b);
|
||||
EXPECT_EQ(b.received, std::vector<Packet>{{3}});
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, PacketsFromBeforeRestartDropped) {
|
||||
Peer a;
|
||||
auto b = std::make_unique<Peer>();
|
||||
a.introduce(*b);
|
||||
a.send({1});
|
||||
a.send({2});
|
||||
std::vector<Packet> recorded = a.link.sent;
|
||||
a.link.sent.clear();
|
||||
b = std::make_unique<Peer>(); // b restarts and forgets a's counter
|
||||
for (auto &packet : recorded)
|
||||
b->link.inject(packet);
|
||||
EXPECT_TRUE(b->received.empty());
|
||||
a.introduce(*b);
|
||||
a.send({3});
|
||||
a.deliver_to(*b);
|
||||
EXPECT_EQ(b->received, std::vector<Packet>{{3}});
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, LinkTooSmallFailsSetup) {
|
||||
MockLink link(ROLLING_TRAILER_SIZE + 3);
|
||||
TestEncryption encryption(&link, true);
|
||||
EXPECT_EQ(encryption.get_max_packet_size(), 0);
|
||||
encryption.setup();
|
||||
EXPECT_TRUE(encryption.is_failed());
|
||||
Packet packet{1};
|
||||
EXPECT_FALSE(encryption.send_to_interface(PacketBuffer(packet), {}));
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, SendWithoutBuffersRefused) {
|
||||
MockLink link(64);
|
||||
TestEncryption encryption(&link, false);
|
||||
Packet packet{1};
|
||||
EXPECT_FALSE(encryption.send_to_interface(PacketBuffer(packet), {}));
|
||||
EXPECT_TRUE(link.sent.empty());
|
||||
}
|
||||
|
||||
TEST_F(XxteaEncryptionTest, CounterWrapStartsNewBoot) {
|
||||
Peer a, b(0x12345678);
|
||||
a.introduce(b);
|
||||
uint32_t boot = a.encryption->get_boot();
|
||||
a.encryption->set_counter(UINT32_MAX);
|
||||
ASSERT_TRUE(a.send({1}));
|
||||
EXPECT_EQ(a.encryption->get_boot(), boot + 1);
|
||||
ASSERT_TRUE(a.send({2}));
|
||||
a.deliver_to(b);
|
||||
// Both were accepted: the last packet of the old boot, then the first of the new one
|
||||
EXPECT_EQ(b.received, (std::vector<Packet>{{1}, {2}}));
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface::encryption_testing
|
||||
@@ -3,14 +3,12 @@ wifi:
|
||||
password: password1
|
||||
|
||||
udp:
|
||||
- id: transport_udp_component
|
||||
listen_address: 239.0.60.53
|
||||
addresses: ["239.0.60.53"]
|
||||
listen_address: 239.0.60.53
|
||||
addresses: ["239.0.60.53"]
|
||||
|
||||
packet_transport:
|
||||
- platform: udp
|
||||
id: transport_udp
|
||||
udp_id: transport_udp_component
|
||||
update_interval: 5s
|
||||
encryption: "our key goes here"
|
||||
rolling_code_enable: true
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/components/runtime_image/jpeg_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
@@ -380,12 +377,6 @@ TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
|
||||
EXPECT_EQ(pixel_bytes(img), first_pixels) << "reused decoder must reproduce identical pixels";
|
||||
}
|
||||
|
||||
// The RGB565 word at (x, y) of a decoded image, in its own byte order
|
||||
static uint16_t rgb565_word(const TestableRuntimeImage &img, int x, int y) {
|
||||
const uint8_t *p = img.get_data_start() + (y * img.get_width() + x) * 2;
|
||||
return p[0] | (p[1] << 8);
|
||||
}
|
||||
|
||||
// JPEGDEC's colour conversion differs by one step between its SIMD and scalar builds, so each
|
||||
// channel is checked within one step; the byte order is still exact
|
||||
static void expect_rgb565_near(uint16_t actual, uint16_t expected, const char *order, size_t pixel) {
|
||||
@@ -395,12 +386,16 @@ static void expect_rgb565_near(uint16_t actual, uint16_t expected, const char *o
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
|
||||
// Halving the 8x8 gradient makes JPEGDEC decode it at half size, which averages each 2x2 block of pixels
|
||||
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB565, false, 8, 8);
|
||||
// Halving the 8x8 gradient keeps its even rows and columns. The words are JPEGDEC's output for those
|
||||
// pixels, red in the top five bits: a decode of the fixture with PIL gives (51, 7, 30) for source
|
||||
// pixel (2, 0), which is word 0x3803 here.
|
||||
static const uint16_t EXPECTED[4 * 4] = {
|
||||
0x0000, 0x3803, 0x8008, 0xB80B, 0x0203, 0x3A07, 0x81EC, 0xBA0F,
|
||||
0x03E7, 0x3C0B, 0x83F0, 0xBC13, 0x05EC, 0x3E0F, 0x85F4, 0xBE17,
|
||||
};
|
||||
TestableRuntimeImage rgb565_little_endian(JPEG, image::IMAGE_TYPE_RGB565, false, 4, 4);
|
||||
TestableRuntimeImage rgb565_big_endian(JPEG, image::IMAGE_TYPE_RGB565, true, 4, 4);
|
||||
|
||||
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_TRUE(decode_all(rgb565_little_endian, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_TRUE(decode_all(rgb565_big_endian, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(rgb565_little_endian.get_width(), 4);
|
||||
@@ -410,45 +405,9 @@ TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
|
||||
|
||||
const uint8_t *little = rgb565_little_endian.get_data_start();
|
||||
const uint8_t *big = rgb565_big_endian.get_data_start();
|
||||
for (int row = 0; row < 4; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
for (int dy = 0; dy < 2; dy++) {
|
||||
for (int dx = 0; dx < 2; dx++) {
|
||||
const uint16_t word = rgb565_word(full, col * 2 + dx, row * 2 + dy);
|
||||
red += word >> 11;
|
||||
green += (word >> 5) & 0x3F;
|
||||
blue += word & 0x1F;
|
||||
}
|
||||
}
|
||||
const uint16_t expected = ((red + 2) / 4) << 11 | ((green + 2) / 4) << 5 | (blue + 2) / 4;
|
||||
const size_t i = row * 4 + col;
|
||||
expect_rgb565_near(little[i * 2] | (little[i * 2 + 1] << 8), expected, "little-endian", i);
|
||||
expect_rgb565_near((big[i * 2] << 8) | big[i * 2 + 1], expected, "big-endian", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The generic draw() path gets JPEGDEC's half size RGB8888 output. It must match the half size RGB565 decode checked
|
||||
// above. A full size RGB8888 decode is no reference: JPEGDEC's x86 SSE code writes it with red and blue swapped.
|
||||
TEST(RuntimeImageDecoder, JpegDecodesToScaledRgb) {
|
||||
TestableRuntimeImage rgb565(JPEG, image::IMAGE_TYPE_RGB565, false, 4, 4);
|
||||
TestableRuntimeImage rgb(JPEG, image::IMAGE_TYPE_RGB, false, 4, 4);
|
||||
ASSERT_TRUE(decode_all(rgb565, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_TRUE(decode_all(rgb, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(static_cast<JpegDecoder *>(rgb.decoder())->get_scale(), 2);
|
||||
|
||||
for (int row = 0; row < 4; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
const uint16_t word = rgb565_word(rgb565, col, row);
|
||||
const Color pixel = rgb.get_pixel(col, row);
|
||||
// RGB565 keeps the top 5 or 6 bits of each channel
|
||||
EXPECT_LE(std::abs(pixel.r - ((word >> 11) << 3)), 8) << "pixel " << col << "," << row << " red";
|
||||
EXPECT_LE(std::abs(pixel.g - (((word >> 5) & 0x3F) << 2)), 4) << "pixel " << col << "," << row << " green";
|
||||
EXPECT_LE(std::abs(pixel.b - ((word & 0x1F) << 3)), 8) << "pixel " << col << "," << row << " blue";
|
||||
}
|
||||
for (size_t i = 0; i < 4 * 4; i++) {
|
||||
expect_rgb565_near(little[i * 2] | (little[i * 2 + 1] << 8), EXPECTED[i], "little-endian", i);
|
||||
expect_rgb565_near((big[i * 2] << 8) | big[i * 2 + 1], EXPECTED[i], "big-endian", i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,15 +415,10 @@ TEST(RuntimeImageDecoder, JpegDecodesToScaledRgb) {
|
||||
// prepare_rgb565_scaling_ rounds: source n covers targets ceil(n * target / source) up to the next start
|
||||
static uint16_t scaled_boundary(int n, int source, int target) { return (n * target + source - 1) / source; }
|
||||
|
||||
// JPEGDEC decodes at the smallest of full, 1/2, 1/4 and 1/8 size that still covers the buffer
|
||||
TEST(RuntimeImageDecoder, JpegDecodesAtTheSmallestSizeThatCoversTheBuffer) {
|
||||
for (auto [size, scale] :
|
||||
{std::pair{8, 1}, std::pair{5, 1}, std::pair{4, 2}, std::pair{3, 2}, std::pair{2, 4}, std::pair{1, 8}}) {
|
||||
TestableRuntimeImage img(JPEG, image::IMAGE_TYPE_RGB565, false, size, size);
|
||||
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
EXPECT_EQ(img.get_width(), size);
|
||||
EXPECT_EQ(static_cast<JpegDecoder *>(img.decoder())->get_scale(), scale) << size << "x" << size;
|
||||
}
|
||||
// The RGB565 word at (x, y) of a decoded image, in its own byte order
|
||||
static uint16_t rgb565_word(const TestableRuntimeImage &img, int x, int y) {
|
||||
const uint8_t *p = img.get_data_start() + (y * img.get_width() + x) * 2;
|
||||
return p[0] | (p[1] << 8);
|
||||
}
|
||||
|
||||
// Every scaled direct decode must equal the unscaled one sampled through the boundary table,
|
||||
@@ -473,8 +427,7 @@ TEST(RuntimeImageDecoder, JpegDirectRgb565ScalesBySourcePixelSpans) {
|
||||
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB565, false, 8, 8);
|
||||
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
|
||||
// Sizes JPEGDEC cannot reach by scaling, so the full size decode is spread over the buffer
|
||||
for (int size : {5, 6, 16}) {
|
||||
for (int size : {4, 6, 16}) {
|
||||
TestableRuntimeImage scaled(JPEG, image::IMAGE_TYPE_RGB565, false, size, size);
|
||||
ASSERT_TRUE(decode_all(scaled, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(scaled.get_width(), size);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/components/runtime_image/jpeg_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
@@ -179,7 +178,6 @@ class SourceImage : public RuntimeImage {
|
||||
explicit SourceImage(image::ImageType type = image::IMAGE_TYPE_RGB, int width = 0, int height = 0)
|
||||
: RuntimeImage(JPEG, type, image::TRANSPARENCY_OPAQUE, nullptr, false, width, height) {}
|
||||
size_t decoded_bytes() const { return this->decoded_bytes_; }
|
||||
int scale() const { return static_cast<const JpegDecoder *>(this->decoder_.get())->get_scale(); }
|
||||
};
|
||||
|
||||
/// The file in memory; fails reads once fail_at bytes have been read
|
||||
@@ -293,23 +291,6 @@ TEST(JpegSourceDecode, SteppedDirectRgb565MatchesFedDecode) {
|
||||
EXPECT_EQ(pixels(stepped), pixels(fed));
|
||||
}
|
||||
|
||||
// decodeBegin() is given the same reduced scale as decode(), and decodeRows() keeps it for every step
|
||||
TEST(JpegSourceDecode, SteppedScaledDecodeMatchesFedDecode) {
|
||||
for (auto type : {image::IMAGE_TYPE_RGB565, image::IMAGE_TYPE_RGB}) {
|
||||
// 24x20 is half of 48x40, so JPEGDEC decodes at 1/2 size
|
||||
SourceImage fed(type, 24, 20);
|
||||
ASSERT_TRUE(decode_fed(fed));
|
||||
ASSERT_EQ(fed.scale(), 2);
|
||||
|
||||
SourceImage stepped(type, 24, 20);
|
||||
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
|
||||
int steps;
|
||||
ASSERT_EQ(decode_stepped(stepped, source, steps), DECODE_STEP_DONE);
|
||||
ASSERT_EQ(stepped.scale(), 2);
|
||||
EXPECT_EQ(pixels(stepped), pixels(fed)) << "image type " << type;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JpegSourceDecode, WaitsForDataThatHasNotArrived) {
|
||||
SourceImage fed;
|
||||
ASSERT_TRUE(decode_fed(fed));
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/components/syslog/esphome_syslog.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::syslog::testing {
|
||||
|
||||
static std::string redact(const char *message) {
|
||||
char buf[128];
|
||||
size_t pos = append_redacted_message(buf, sizeof(buf), 0, message, strlen(message));
|
||||
return std::string(buf, pos);
|
||||
}
|
||||
|
||||
TEST(SyslogRedactedMessage, PlainMessageUnchanged) {
|
||||
EXPECT_EQ(redact("WiFi connected to 'home'"), "WiFi connected to 'home'");
|
||||
}
|
||||
|
||||
TEST(SyslogRedactedMessage, SecretReplaced) {
|
||||
EXPECT_EQ(redact("Password=" LOG_SECRET("'hunter2'") " ok"), "Password=[redacted] ok");
|
||||
}
|
||||
|
||||
TEST(SyslogRedactedMessage, MultipleSecrets) {
|
||||
EXPECT_EQ(redact("SSID=" LOG_SECRET("home") " PSK=" LOG_SECRET("hunter2")), "SSID=[redacted] PSK=[redacted]");
|
||||
}
|
||||
|
||||
TEST(SyslogRedactedMessage, UnterminatedSecretDropsRest) {
|
||||
EXPECT_EQ(redact("key=" ESPHOME_LOG_SECRET_BEGIN "abc"), "key=[redacted]");
|
||||
}
|
||||
|
||||
TEST(SyslogRedactedMessage, AppendsAtOffsetAndCaps) {
|
||||
char buf[16] = "pre:";
|
||||
size_t pos = append_redacted_message(buf, sizeof(buf), 4, "0123456789abcdef", 16);
|
||||
EXPECT_EQ(pos, sizeof(buf) - 1);
|
||||
EXPECT_EQ(std::string(buf, pos), "pre:0123456789a");
|
||||
EXPECT_EQ(buf[pos], '\0');
|
||||
}
|
||||
|
||||
} // namespace esphome::syslog::testing
|
||||
@@ -1,6 +0,0 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# benchmark.yaml configures a uart packet_interface, whose to_code waits on the uart bus variable.
|
||||
manifest.enable_codegen()
|
||||
@@ -1,9 +0,0 @@
|
||||
packet_interface:
|
||||
- platform: uart
|
||||
uart_id: uart_packet_interface_bus
|
||||
|
||||
# A list, so the uart entries of other test directories are joined to it
|
||||
uart:
|
||||
- id: uart_packet_interface_bus
|
||||
port: /dev/ttyUSB0
|
||||
baud_rate: 9600
|
||||
@@ -25,10 +25,7 @@ class MockUARTComponent : public UARTComponent {
|
||||
// consider using StaticVector or FixedVector from esphome/core/helpers.h instead.
|
||||
std::vector<uint8_t> written_data;
|
||||
|
||||
void write_array(const uint8_t *data, size_t len) override {
|
||||
written_data.insert(written_data.end(), data, data + len);
|
||||
}
|
||||
void clear() { written_data.clear(); }
|
||||
void write_array(const uint8_t *data, size_t len) override { written_data.assign(data, data + len); }
|
||||
|
||||
MOCK_METHOD(bool, read_array, (uint8_t *data, size_t len), (override));
|
||||
MOCK_METHOD(bool, peek_byte, (uint8_t *data), (override));
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "esphome/components/uart/packet_interface/uart_packet_interface.h"
|
||||
#include "esphome/components/packet_interface/packet_buffer.h"
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace esphome::uart::testing {
|
||||
|
||||
// Copies a packet into a vector, for comparing in tests
|
||||
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
|
||||
std::vector<uint8_t> result(data.size());
|
||||
data.copy_to(result.data(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
using packet_interface::PacketBuffer;
|
||||
using packet_interface::PacketMetaData;
|
||||
|
||||
// Test fixture
|
||||
class UartPacketInterfaceTest : public ::testing::Test {
|
||||
protected:
|
||||
MockUARTComponent uart_{};
|
||||
// As large as the default max_packet_size; tests only make it smaller
|
||||
uint8_t rx_buffer_[512]{};
|
||||
UartPacketInterface interface_{this->rx_buffer_};
|
||||
std::vector<std::vector<uint8_t>> received_packets_;
|
||||
|
||||
void SetUp() override {
|
||||
received_packets_.clear();
|
||||
interface_.set_uart_parent(&uart_);
|
||||
interface_.setup();
|
||||
|
||||
interface_.add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &meta) { received_packets_.push_back(to_vector(data)); });
|
||||
}
|
||||
};
|
||||
|
||||
// Feed bytes to the interface through the mocked UART and run its loop
|
||||
static void feed(MockUARTComponent &uart, UartPacketInterface &interface, const std::vector<uint8_t> &bytes) {
|
||||
size_t read_pos = 0;
|
||||
EXPECT_CALL(uart, available()).WillRepeatedly(Invoke([&bytes, &read_pos]() { return bytes.size() - read_pos; }));
|
||||
EXPECT_CALL(uart, read_array(_, _)).WillRepeatedly(Invoke([&bytes, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > bytes.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, bytes.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
interface.loop();
|
||||
}
|
||||
|
||||
// Test basic frame sending
|
||||
TEST_F(UartPacketInterfaceTest, SendSimpleFrame) {
|
||||
std::vector<uint8_t> data = {0x01, 0x02, 0x03};
|
||||
PacketBuffer buffer(data);
|
||||
|
||||
// Set up the mock to accumulate bytes
|
||||
ASSERT_EQ(uart_.written_data.size(), 0);
|
||||
interface_.send_to_interface(buffer, {});
|
||||
// Verify the accumulated bytes
|
||||
ASSERT_EQ(uart_.written_data.size(), 5);
|
||||
|
||||
std::vector<uint8_t> expected = {0x7E, 0x01, 0x02, 0x03, 0x7E};
|
||||
EXPECT_EQ(uart_.written_data, expected);
|
||||
}
|
||||
// Test byte stuffing for FLAG_BYTE
|
||||
TEST_F(UartPacketInterfaceTest, SendWithFlagByteStuffing) {
|
||||
std::vector<uint8_t> data = {0x01, 0x7E, 0x03};
|
||||
PacketBuffer buffer(data);
|
||||
|
||||
uart_.clear();
|
||||
interface_.send_to_interface(buffer, {});
|
||||
|
||||
// Should escape 0x7E as 0x7D 0x5E
|
||||
std::vector<uint8_t> expected = {0x7E, 0x01, 0x7D, 0x5E, 0x03, 0x7E};
|
||||
EXPECT_EQ(uart_.written_data, expected);
|
||||
}
|
||||
|
||||
// Test byte stuffing for CONTROL_BYTE
|
||||
TEST_F(UartPacketInterfaceTest, SendWithControlByteStuffing) {
|
||||
std::vector<uint8_t> data = {0x01, 0x7D, 0x03};
|
||||
PacketBuffer buffer(data);
|
||||
|
||||
uart_.clear();
|
||||
interface_.send_to_interface(buffer, {});
|
||||
|
||||
// Should escape 0x7D as 0x7D 0x5D
|
||||
std::vector<uint8_t> expected = {0x7E, 0x01, 0x7D, 0x5D, 0x03, 0x7E};
|
||||
EXPECT_EQ(uart_.written_data, expected);
|
||||
}
|
||||
|
||||
// Test sending empty frame
|
||||
TEST_F(UartPacketInterfaceTest, SendEmptyFrame) {
|
||||
std::vector<uint8_t> data = {};
|
||||
PacketBuffer buffer(data);
|
||||
|
||||
interface_.send_to_interface(buffer, {});
|
||||
|
||||
// Empty data should return without sending
|
||||
EXPECT_EQ(uart_.written_data.size(), 0);
|
||||
}
|
||||
|
||||
// Test receiving simple frame
|
||||
TEST_F(UartPacketInterfaceTest, ReceiveSimpleFrame) {
|
||||
std::vector<uint8_t> frame_data = {0x7E, 0x01, 0x02, 0x03, 0x7E};
|
||||
size_t read_pos = 0;
|
||||
|
||||
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
|
||||
return frame_data.size() - read_pos;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(uart_, read_array(_, _)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > frame_data.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, frame_data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
|
||||
interface_.loop();
|
||||
|
||||
ASSERT_EQ(received_packets_.size(), 1);
|
||||
std::vector<uint8_t> expected = {0x01, 0x02, 0x03};
|
||||
EXPECT_EQ(received_packets_[0], expected);
|
||||
}
|
||||
|
||||
// Test receiving frame with FLAG_BYTE escape
|
||||
TEST_F(UartPacketInterfaceTest, ReceiveFrameWithFlagEscape) {
|
||||
std::vector<uint8_t> frame_data = {0x7E, 0x01, 0x7D, 0x5E, 0x03, 0x7E};
|
||||
size_t read_pos = 0;
|
||||
|
||||
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
|
||||
return frame_data.size() - read_pos;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(uart_, read_array(_, _)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > frame_data.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, frame_data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
|
||||
interface_.loop();
|
||||
|
||||
ASSERT_EQ(received_packets_.size(), 1);
|
||||
std::vector<uint8_t> expected = {0x01, 0x7E, 0x03}; // 0x7E unescaped
|
||||
EXPECT_EQ(received_packets_[0], expected);
|
||||
}
|
||||
|
||||
// Test receiving frame with CONTROL_BYTE escape
|
||||
TEST_F(UartPacketInterfaceTest, ReceiveFrameWithControlEscape) {
|
||||
std::vector<uint8_t> frame_data = {0x7E, 0x01, 0x7D, 0x5D, 0x03, 0x7E};
|
||||
size_t read_pos = 0;
|
||||
|
||||
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
|
||||
return frame_data.size() - read_pos;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(uart_, read_array(_, _)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > frame_data.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, frame_data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
|
||||
interface_.loop();
|
||||
|
||||
ASSERT_EQ(received_packets_.size(), 1);
|
||||
std::vector<uint8_t> expected = {0x01, 0x7D, 0x03}; // 0x7D unescaped
|
||||
EXPECT_EQ(received_packets_[0], expected);
|
||||
}
|
||||
|
||||
// Test receiving multiple frames
|
||||
TEST_F(UartPacketInterfaceTest, ReceiveMultipleFrames) {
|
||||
std::vector<uint8_t> frame_data = {0x7E, 0x01, 0x02, 0x7E, 0x7E, 0x03, 0x04, 0x7E};
|
||||
size_t read_pos = 0;
|
||||
|
||||
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
|
||||
return frame_data.size() - read_pos;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(uart_, read_array(_, _)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > frame_data.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, frame_data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
|
||||
interface_.loop();
|
||||
|
||||
ASSERT_EQ(received_packets_.size(), 2) << "Expected 2 packets, got " << received_packets_.size();
|
||||
std::vector<uint8_t> expected1 = {0x01, 0x02};
|
||||
std::vector<uint8_t> expected2 = {0x03, 0x04};
|
||||
auto pkt0 = received_packets_[0];
|
||||
auto pkt1 = received_packets_[1];
|
||||
EXPECT_EQ(pkt0, expected1) << "Packet 0 size: " << pkt0.size();
|
||||
EXPECT_EQ(pkt1, expected2) << "Packet 1 size: " << pkt1.size();
|
||||
}
|
||||
|
||||
// Test receiving empty frame (back-to-back flags)
|
||||
TEST_F(UartPacketInterfaceTest, ReceiveEmptyFrame) {
|
||||
std::vector<uint8_t> frame_data = {0x7E, 0x7E};
|
||||
size_t read_pos = 0;
|
||||
|
||||
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
|
||||
return frame_data.size() - read_pos;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(uart_, read_array(_, _)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > frame_data.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, frame_data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
|
||||
interface_.loop();
|
||||
|
||||
// Empty frames should not be delivered
|
||||
EXPECT_EQ(received_packets_.size(), 0);
|
||||
}
|
||||
|
||||
// Test data before first flag is ignored
|
||||
TEST_F(UartPacketInterfaceTest, IgnoreDataBeforeFirstFlag) {
|
||||
std::vector<uint8_t> frame_data = {0xFF, 0xFF, 0x7E, 0x01, 0x02, 0x7E};
|
||||
size_t read_pos = 0;
|
||||
|
||||
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
|
||||
return frame_data.size() - read_pos;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(uart_, read_array(_, _)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > frame_data.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, frame_data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
|
||||
interface_.loop();
|
||||
|
||||
ASSERT_EQ(received_packets_.size(), 1);
|
||||
std::vector<uint8_t> expected = {0x01, 0x02};
|
||||
EXPECT_EQ(received_packets_[0], expected);
|
||||
}
|
||||
|
||||
// Test oversized packet is discarded
|
||||
TEST_F(UartPacketInterfaceTest, OversizedPacketDiscarded) {
|
||||
uint8_t small_buffer[10]{};
|
||||
UartPacketInterface interface(small_buffer);
|
||||
interface.set_uart_parent(&this->uart_);
|
||||
std::vector<std::vector<uint8_t>> received;
|
||||
interface.add_packet_interface_listener(
|
||||
[&received](const PacketBuffer &data, const PacketMetaData &) { received.push_back(to_vector(data)); });
|
||||
|
||||
// Create a packet larger than buffer
|
||||
std::vector<uint8_t> frame_data = {0x7E};
|
||||
for (int i = 0; i < 15; i++) {
|
||||
frame_data.push_back(0xAA);
|
||||
}
|
||||
frame_data.push_back(0x7E);
|
||||
|
||||
feed(this->uart_, interface, frame_data);
|
||||
|
||||
// Oversized packet should be discarded
|
||||
EXPECT_TRUE(received.empty());
|
||||
}
|
||||
|
||||
// Test round-trip (send and receive)
|
||||
TEST_F(UartPacketInterfaceTest, RoundTrip) {
|
||||
std::vector<uint8_t> original = {0x01, 0x7E, 0x7D, 0x03};
|
||||
PacketBuffer send_buffer(original);
|
||||
|
||||
// Send
|
||||
interface_.send_to_interface(send_buffer, {});
|
||||
|
||||
// Prepare to receive - use the written data as frame data
|
||||
std::vector<uint8_t> frame_data = uart_.written_data;
|
||||
size_t read_pos = 0;
|
||||
|
||||
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
|
||||
return frame_data.size() - read_pos;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(uart_, read_array(_, _)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > frame_data.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, frame_data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Receive
|
||||
interface_.loop();
|
||||
|
||||
ASSERT_EQ(received_packets_.size(), 1);
|
||||
EXPECT_EQ(received_packets_[0], original);
|
||||
}
|
||||
|
||||
// Test all bytes can be transmitted (0x00 to 0xFF)
|
||||
TEST_F(UartPacketInterfaceTest, AllByteValues) {
|
||||
std::vector<uint8_t> data;
|
||||
data.reserve(256);
|
||||
for (int i = 0; i != 256; i++) {
|
||||
data.push_back(static_cast<uint8_t>(i));
|
||||
}
|
||||
PacketBuffer send_buffer(data);
|
||||
|
||||
// Send
|
||||
interface_.send_to_interface(send_buffer, {});
|
||||
|
||||
// Prepare to receive - use the written data as frame data
|
||||
std::vector<uint8_t> frame_data = uart_.written_data;
|
||||
size_t read_pos = 0;
|
||||
|
||||
EXPECT_CALL(uart_, available()).WillRepeatedly(Invoke([&frame_data, &read_pos]() {
|
||||
return frame_data.size() - read_pos;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(uart_, read_array(_, _)).WillRepeatedly(Invoke([&frame_data, &read_pos](uint8_t *data, size_t len) {
|
||||
if (len > frame_data.size() - read_pos)
|
||||
return false;
|
||||
std::memcpy(data, frame_data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Receive
|
||||
interface_.loop();
|
||||
|
||||
ASSERT_EQ(received_packets_.size(), 1);
|
||||
EXPECT_EQ(received_packets_[0], data);
|
||||
}
|
||||
|
||||
// The buffer passed to the constructor is the largest packet, for receiving and sending alike
|
||||
TEST_F(UartPacketInterfaceTest, ReceiveLimitedToBuffer) {
|
||||
uint8_t small_buffer[4]{};
|
||||
UartPacketInterface interface(small_buffer);
|
||||
interface.set_uart_parent(&this->uart_);
|
||||
std::vector<std::vector<uint8_t>> received;
|
||||
interface.add_packet_interface_listener(
|
||||
[&received](const PacketBuffer &data, const PacketMetaData &) { received.push_back(to_vector(data)); });
|
||||
// The first packet is longer than the buffer and is dropped; the next one fits
|
||||
feed(this->uart_, interface, {0x7E, 1, 2, 3, 4, 5, 6, 0x7E, 7, 8, 0x7E});
|
||||
EXPECT_EQ(received, (std::vector<std::vector<uint8_t>>{{7, 8}}));
|
||||
}
|
||||
|
||||
// Test a packet larger than max_packet_size is not sent
|
||||
TEST_F(UartPacketInterfaceTest, OversizedPacketNotSent) {
|
||||
uint8_t small_buffer[4]{};
|
||||
UartPacketInterface interface(small_buffer);
|
||||
interface.set_uart_parent(&this->uart_);
|
||||
std::vector<uint8_t> data = {0x01, 0x02, 0x03, 0x04, 0x05};
|
||||
PacketBuffer buffer(data);
|
||||
|
||||
EXPECT_FALSE(interface.send_to_interface(buffer, {}));
|
||||
EXPECT_EQ(uart_.written_data.size(), 0);
|
||||
}
|
||||
|
||||
// Test a composite buffer longer than one write chunk is framed correctly
|
||||
TEST_F(UartPacketInterfaceTest, SendCompositeAcrossChunks) {
|
||||
std::vector<uint8_t> pre(40, 0x7E);
|
||||
std::vector<uint8_t> mid_data = {0x11, 0x22};
|
||||
std::vector<uint8_t> post(30, 0x33);
|
||||
PacketBuffer mid(mid_data);
|
||||
PacketBuffer buffer(pre.data(), pre.size(), &mid, post.data(), post.size());
|
||||
|
||||
ASSERT_TRUE(interface_.send_to_interface(buffer, {}));
|
||||
|
||||
std::vector<uint8_t> expected = {0x7E};
|
||||
for (size_t i = 0; i != pre.size(); i++) {
|
||||
expected.push_back(0x7D);
|
||||
expected.push_back(0x5E);
|
||||
}
|
||||
expected.insert(expected.end(), mid_data.begin(), mid_data.end());
|
||||
expected.insert(expected.end(), post.begin(), post.end());
|
||||
expected.push_back(0x7E);
|
||||
EXPECT_EQ(uart_.written_data, expected);
|
||||
}
|
||||
|
||||
// Test one flag between frames both ends the first and starts the second
|
||||
TEST_F(UartPacketInterfaceTest, SharedFlagBetweenFrames) {
|
||||
feed(uart_, interface_, {0x7E, 0x01, 0x7E, 0x02, 0x03, 0x7E});
|
||||
|
||||
EXPECT_EQ(received_packets_, (std::vector<std::vector<uint8_t>>{{0x01}, {0x02, 0x03}}));
|
||||
}
|
||||
|
||||
// Test an unfinished frame does not hide the frame after it
|
||||
TEST_F(UartPacketInterfaceTest, UnfinishedFrameDoesNotHideNext) {
|
||||
feed(uart_, interface_, {0x7E, 0xAA, 0x7E, 0x01, 0x02, 0x7E});
|
||||
|
||||
EXPECT_EQ(received_packets_, (std::vector<std::vector<uint8_t>>{{0xAA}, {0x01, 0x02}}));
|
||||
}
|
||||
|
||||
} // namespace esphome::uart::testing
|
||||
@@ -1,6 +0,0 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# benchmark.yaml configures a udp packet_interface, whose to_code waits on the udp component variable.
|
||||
manifest.enable_codegen()
|
||||
@@ -1,5 +0,0 @@
|
||||
udp:
|
||||
addresses: ["192.168.1.255"]
|
||||
|
||||
packet_interface:
|
||||
- platform: udp
|
||||
@@ -1,78 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/udp/packet_interface/udp_packet_interface.h"
|
||||
|
||||
#ifdef USE_NETWORK
|
||||
|
||||
namespace esphome::udp::testing {
|
||||
|
||||
// Copies a packet into a vector, for comparing in tests
|
||||
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
|
||||
std::vector<uint8_t> result(data.size());
|
||||
data.copy_to(result.data(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
using packet_interface::PacketBuffer;
|
||||
using packet_interface::PacketMetaData;
|
||||
using Packet = std::vector<uint8_t>;
|
||||
|
||||
// The copy buffer is a base, so it is set up before the link's constructor sees it
|
||||
struct UdpBufferStorage {
|
||||
uint8_t tx_buffer_storage[8]{};
|
||||
};
|
||||
|
||||
class TestUdp : public UdpBufferStorage, public UdpPacketInterface {
|
||||
public:
|
||||
explicit TestUdp(UDPComponent *udp) : UdpPacketInterface(udp, this->tx_buffer_storage) {
|
||||
this->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(to_vector(data)); });
|
||||
}
|
||||
using UdpPacketInterface::receive_;
|
||||
|
||||
std::vector<Packet> received;
|
||||
};
|
||||
|
||||
// The udp component is not set up, so it has no destinations and a send does nothing
|
||||
class UdpPacketInterfaceTest : public ::testing::Test {
|
||||
protected:
|
||||
UDPComponent udp_;
|
||||
TestUdp link_{&this->udp_};
|
||||
};
|
||||
|
||||
TEST_F(UdpPacketInterfaceTest, ReceivedPacketsUpToMaxPassedUp) {
|
||||
Packet packet{1, 2, 3, 4, 5, 6, 7, 8};
|
||||
this->link_.receive_(packet);
|
||||
Packet oversized{1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
this->link_.receive_(oversized);
|
||||
EXPECT_EQ(this->link_.received, std::vector<Packet>{packet});
|
||||
}
|
||||
|
||||
TEST_F(UdpPacketInterfaceTest, SendsContiguousAndSplitPackets) {
|
||||
this->link_.setup();
|
||||
Packet packet{1, 2, 3, 4};
|
||||
EXPECT_TRUE(this->link_.send_to_interface(PacketBuffer(packet), {}));
|
||||
uint8_t header = 0;
|
||||
PacketBuffer body(packet);
|
||||
EXPECT_TRUE(this->link_.send_to_interface(PacketBuffer(&header, 1, &body, nullptr, 0), {}));
|
||||
// A packet split across buffers is joined in the copy buffer before it is sent
|
||||
const auto *storage = this->link_.tx_buffer_storage;
|
||||
EXPECT_EQ(Packet(storage, storage + 5), (Packet{0, 1, 2, 3, 4}));
|
||||
}
|
||||
|
||||
TEST_F(UdpPacketInterfaceTest, OversizedPacketNotSent) {
|
||||
this->link_.setup();
|
||||
Packet packet{1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
EXPECT_FALSE(this->link_.send_to_interface(PacketBuffer(packet), {}));
|
||||
uint8_t header = 0;
|
||||
Packet body_data{1, 2, 3, 4, 5, 6, 7, 8};
|
||||
PacketBuffer body(body_data);
|
||||
EXPECT_FALSE(this->link_.send_to_interface(PacketBuffer(&header, 1, &body, nullptr, 0), {}));
|
||||
}
|
||||
|
||||
} // namespace esphome::udp::testing
|
||||
|
||||
#endif
|
||||
+59
-55
@@ -58,30 +58,8 @@ The `InitialStateHelper` class solves a common problem in integration tests: whe
|
||||
- Provides debug logging to track state reception (use `--log-cli-level=DEBUG`)
|
||||
- Safe for concurrent use with multiple entity types
|
||||
|
||||
#### Subscribing after the initial states (`state_utils.py`)
|
||||
|
||||
`subscribe_after_initial_states()` does the `InitialStateHelper` setup in one call: it subscribes, waits
|
||||
for every entity's connect time state, and fails the test with the entities still missing on timeout.
|
||||
It returns a `StateSubscription` with `initial_states` (by key) and a `waiter`, a `StateWaiter` that sees
|
||||
every later state. `waiter.next_state(key=..., state_type=...)` arms a wait for the next matching state;
|
||||
call it before sending the command. `press_and_wait()` presses a button and returns the next state of a
|
||||
type, from one entity when `key=` is given:
|
||||
|
||||
```python
|
||||
from .state_utils import press_and_wait, subscribe_after_initial_states
|
||||
|
||||
entities, _ = await client.list_entities_services()
|
||||
sub = await subscribe_after_initial_states(client, entities)
|
||||
|
||||
state = await press_and_wait(client, entities, sub.waiter, "set_position", CoverState)
|
||||
assert state.position == pytest.approx(0.5, abs=0.01)
|
||||
|
||||
next_light = sub.waiter.next_state(key=light.key, state_type=LightState)
|
||||
client.light_command(key=light.key, state=True)
|
||||
assert (await next_light).state is True
|
||||
```
|
||||
|
||||
Pass `on_state=` as well when the test also needs to collect every state.
|
||||
**Future work:**
|
||||
Consider converting existing integration tests to use `InitialStateHelper` for more reliable state tracking and to eliminate race conditions related to initial state broadcasts.
|
||||
|
||||
#### Entity Lookup Helpers (`state_utils.py`)
|
||||
|
||||
@@ -225,24 +203,46 @@ async def test_my_sensor(
|
||||
|
||||
##### State Subscription Pattern
|
||||
|
||||
**Recommended: Using subscribe_after_initial_states**
|
||||
**Recommended: Using InitialStateHelper**
|
||||
|
||||
When an API client connects, ESPHome automatically sends the current state of all entities.
|
||||
`subscribe_after_initial_states()` (from `state_utils.py`) swallows these initial states and only forwards
|
||||
subsequent state changes, to your callback and to the returned `waiter`:
|
||||
When an API client connects, ESPHome automatically sends the current state of all entities. The `InitialStateHelper` (from `state_utils.py`) handles this by swallowing these initial states and only forwarding subsequent state changes to your test callback:
|
||||
|
||||
```python
|
||||
from .state_utils import subscribe_after_initial_states
|
||||
from .state_utils import InitialStateHelper
|
||||
|
||||
# Track state changes with futures
|
||||
loop = asyncio.get_running_loop()
|
||||
states: dict[int, EntityState] = {}
|
||||
state_future: asyncio.Future[EntityState] = loop.create_future()
|
||||
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
"""This callback only receives NEW state changes, not initial states."""
|
||||
states[state.key] = state
|
||||
# Check for specific condition using isinstance
|
||||
if isinstance(state, SensorState) and state.state == expected_value:
|
||||
if not state_future.done():
|
||||
state_future.set_result(state)
|
||||
|
||||
|
||||
# Get entities and set up state synchronization
|
||||
entities, services = await client.list_entities_services()
|
||||
sub = await subscribe_after_initial_states(client, entities)
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
|
||||
# Arm the wait before the action that produces the state
|
||||
expected = sub.waiter.expect(
|
||||
lambda s: isinstance(s, SensorState) and s.state == expected_value
|
||||
)
|
||||
# Subscribe with the wrapper that filters initial states
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for all initial states to be broadcast; on timeout this fails the test
|
||||
# and lists the entities that never sent one
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# Now perform your test actions - on_state will only receive new changes
|
||||
# ... trigger state changes ...
|
||||
result = await expected
|
||||
|
||||
# Wait for expected state; on timeout the test fails and shows the states received
|
||||
result = await wait_or_fail(
|
||||
state_future, 5.0, "Expected state not received", states=states
|
||||
)
|
||||
```
|
||||
|
||||
**Legacy: Manual State Tracking**
|
||||
@@ -386,33 +386,37 @@ async with run_compiled(yaml_config, line_callback=on_log_line):
|
||||
# Test implementation
|
||||
```
|
||||
|
||||
To wait for log lines, use `LineWaiter` (`log_utils.py`) instead of a hand written
|
||||
callback with one future per pattern:
|
||||
Example using futures for specific log patterns:
|
||||
```python
|
||||
import re
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
loop = asyncio.get_running_loop()
|
||||
connected_future = loop.create_future()
|
||||
service_future = loop.create_future()
|
||||
|
||||
log = LineWaiter()
|
||||
# Arm expectations before the action that logs them; any number may be pending.
|
||||
connected = log.expect(re.compile(r"Client .* connected from"))
|
||||
value = log.expect(re.compile(r"value=(\d+)"))
|
||||
# Patterns to match
|
||||
connected_pattern = re.compile(r"Client .* connected from")
|
||||
service_pattern = re.compile(r"Service called")
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=log.callback):
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if not connected_future.done() and connected_pattern.search(line):
|
||||
connected_future.set_result(True)
|
||||
elif not service_future.done() and service_pattern.search(line):
|
||||
service_future.set_result(True)
|
||||
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=check_output):
|
||||
async with api_client_connected() as client:
|
||||
await log.wait(connected)
|
||||
match = await log.wait(value, timeout=5.0, msg="no value logged")
|
||||
assert match.group(1) == "42"
|
||||
assert log.count("Service called") == 1
|
||||
```
|
||||
# Wait for specific log message
|
||||
await asyncio.wait_for(connected_future, timeout=5.0)
|
||||
|
||||
- `expect(pattern)` only matches lines that arrive after the call. A `str` matches as a
|
||||
substring and resolves to the line; a compiled regex is searched and resolves to its match.
|
||||
Each expectation resolves on the next matching line, so two armed for the same text both
|
||||
resolve on its first occurrence: arm the second after the first resolves, or use `count`.
|
||||
- `wait(future)` fails the test on timeout with the pattern and the last 20 log lines.
|
||||
- `wait_for(*needles)` also matches lines seen before the call, but only one may be pending.
|
||||
- `count(pattern)` counts the lines seen so far.
|
||||
# Do test actions...
|
||||
|
||||
# Wait for service log
|
||||
await asyncio.wait_for(service_future, timeout=5.0)
|
||||
```
|
||||
|
||||
**Note**: Tests that monitor log messages typically have fewer race conditions compared to state-based testing, making them more reliable. However, be aware that the host platform currently does not have a thread-safe logger, so logging from threads will not work correctly.
|
||||
|
||||
|
||||
@@ -3,47 +3,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Any, overload
|
||||
|
||||
from .wait_utils import wait_or_fail
|
||||
|
||||
type LinePattern = str | re.Pattern[str]
|
||||
type LineMatch = str | re.Match[str]
|
||||
|
||||
|
||||
def _match(pattern: LinePattern, line: str) -> LineMatch | None:
|
||||
"""Return ``line`` if a str pattern is in it, or the search match of a regex."""
|
||||
if isinstance(pattern, str):
|
||||
return line if pattern in line else None
|
||||
return pattern.search(line)
|
||||
|
||||
|
||||
class LineWaiter:
|
||||
"""Collects log lines and lets a test await lines that match.
|
||||
"""Collects log lines and lets a test await one containing all needles.
|
||||
|
||||
Pass ``callback`` as ``run_compiled``'s ``line_callback``; the callback runs
|
||||
on the test's own event loop, so futures are resolved directly. ``wait_for``
|
||||
also matches lines seen before the call, and only one may be outstanding at a
|
||||
time. ``expect`` matches only later lines, and any number may be armed at once;
|
||||
each resolves on the next matching line, so two armed for the same text both
|
||||
resolve on its first occurrence. Arm the second after the first, or use ``count``.
|
||||
on the test's own event loop, so futures are resolved directly. Only one
|
||||
``wait_for`` may be outstanding at a time (tests await sequentially).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.lines: list[str] = []
|
||||
self._needles: tuple[str, ...] = ()
|
||||
self._future: asyncio.Future | None = None
|
||||
self._expectations: dict[asyncio.Future[Any], LinePattern] = {}
|
||||
|
||||
def callback(self, line: str) -> None:
|
||||
self.lines.append(line)
|
||||
for future, pattern in list(self._expectations.items()):
|
||||
if future.done():
|
||||
del self._expectations[future]
|
||||
elif (match := _match(pattern, line)) is not None:
|
||||
future.set_result(match)
|
||||
del self._expectations[future]
|
||||
if (
|
||||
self._future is not None
|
||||
and not self._future.done()
|
||||
@@ -70,38 +46,3 @@ class LineWaiter:
|
||||
finally:
|
||||
self._future = None
|
||||
self._needles = ()
|
||||
|
||||
@overload
|
||||
def expect(self, pattern: str) -> asyncio.Future[str]: ... # noqa: E704
|
||||
|
||||
@overload
|
||||
def expect(self, pattern: re.Pattern[str]) -> asyncio.Future[re.Match[str]]: ... # noqa: E704
|
||||
|
||||
def expect(self, pattern: LinePattern) -> asyncio.Future[LineMatch]:
|
||||
"""Arm a wait for the next line matching ``pattern``; earlier lines never match.
|
||||
|
||||
A str matches as a substring and resolves to the line; a compiled regex is
|
||||
searched and resolves to its match. Arm it before the action that logs the line.
|
||||
"""
|
||||
future: asyncio.Future[LineMatch] = asyncio.get_running_loop().create_future()
|
||||
self._expectations[future] = pattern
|
||||
return future
|
||||
|
||||
async def wait[T: str | re.Match[str]](
|
||||
self,
|
||||
future: asyncio.Future[T],
|
||||
timeout: float = 5.0,
|
||||
msg: str = "log line not seen",
|
||||
) -> T:
|
||||
"""Await a future from ``expect``; on timeout fail with its pattern and the last lines."""
|
||||
return await wait_or_fail(
|
||||
future,
|
||||
timeout,
|
||||
msg,
|
||||
pattern=self._expectations.get(future),
|
||||
lines=self.lines,
|
||||
)
|
||||
|
||||
def count(self, pattern: LinePattern) -> int:
|
||||
"""Return how many lines seen so far match ``pattern``."""
|
||||
return sum(_match(pattern, line) is not None for line in self.lines)
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
|
||||
@@ -101,22 +100,6 @@ class StateWaiter:
|
||||
self._waiters.append(entry)
|
||||
return self._wait(entry, timeout, label)
|
||||
|
||||
def next_state[T: EntityState](
|
||||
self,
|
||||
key: int | None = None,
|
||||
state_type: type[T] = EntityState,
|
||||
timeout: float = 5.0,
|
||||
) -> Awaitable[T]:
|
||||
"""Arm a wait for the next ``state_type`` state, for entity ``key`` if given.
|
||||
|
||||
Like :meth:`expect`, call it before sending the command and await the result afterwards.
|
||||
"""
|
||||
return self.expect(
|
||||
lambda s: isinstance(s, state_type) and (key is None or s.key == key),
|
||||
timeout,
|
||||
state_type.__name__ if key is None else f"{state_type.__name__} key={key}",
|
||||
)
|
||||
|
||||
async def _wait(
|
||||
self,
|
||||
entry: tuple[Callable[[EntityState], bool], asyncio.Future[EntityState]],
|
||||
@@ -419,58 +402,6 @@ class InitialStateHelper:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class StateSubscription:
|
||||
"""A state subscription that is past the connect time states."""
|
||||
|
||||
initial_states: dict[int, EntityState]
|
||||
waiter: StateWaiter
|
||||
|
||||
|
||||
async def subscribe_after_initial_states(
|
||||
client: APIClient,
|
||||
entities: list[EntityInfo],
|
||||
on_state: Callable[[EntityState], None] | None = None,
|
||||
timeout: float = 5.0,
|
||||
) -> StateSubscription:
|
||||
"""Subscribe to states and wait until every entity has sent its connect time state.
|
||||
|
||||
Later states go to ``on_state`` (if given) and to the returned ``waiter``. The connect time
|
||||
states are kept in ``initial_states`` by key. Fails the test listing the entities still missing.
|
||||
"""
|
||||
helper = InitialStateHelper(entities)
|
||||
waiter = StateWaiter()
|
||||
|
||||
def forward(state: EntityState) -> None:
|
||||
waiter.on_state(state)
|
||||
if on_state is not None:
|
||||
on_state(state)
|
||||
|
||||
client.subscribe_states(helper.on_state_wrapper(forward))
|
||||
await helper.wait_for_initial_states(timeout)
|
||||
return StateSubscription(helper.initial_states, waiter)
|
||||
|
||||
|
||||
async def press_and_wait[T: EntityState](
|
||||
client: APIClient,
|
||||
entities: list[EntityInfo],
|
||||
waiter: StateWaiter,
|
||||
button: str,
|
||||
state_type: type[T],
|
||||
*,
|
||||
key: int | None = None,
|
||||
timeout: float = 5.0,
|
||||
) -> T:
|
||||
"""Press the button whose object_id contains ``button`` and return the next ``state_type`` state.
|
||||
|
||||
Pass the entity's ``key`` when the fixture has more than one entity of that type.
|
||||
"""
|
||||
btn = require_entity(entities, button, ButtonInfo)
|
||||
next_state = waiter.next_state(key=key, state_type=state_type, timeout=timeout)
|
||||
client.button_command(btn.key)
|
||||
return await next_state
|
||||
|
||||
|
||||
class SensorStateCollector:
|
||||
"""Collects sensor, binary sensor, and text sensor state updates with wait helpers.
|
||||
|
||||
|
||||
@@ -74,19 +74,3 @@ def test_populate_dependency_config_populates_defaults() -> None:
|
||||
register_platform_fn=lambda domain: None,
|
||||
)
|
||||
assert config["ok"] == {"default": 1}
|
||||
|
||||
|
||||
def test_load_component_yaml_configs_extends_lists(tmp_path: Path) -> None:
|
||||
"""Several directories can each add entries to one platform component."""
|
||||
(tmp_path / "first").mkdir()
|
||||
(tmp_path / "first" / "benchmark.yaml").write_text(
|
||||
"uart:\n port: /dev/a\npacket_interface:\n - platform: uart\n"
|
||||
)
|
||||
(tmp_path / "second").mkdir()
|
||||
(tmp_path / "second" / "benchmark.yaml").write_text(
|
||||
"uart:\n port: /dev/b\npacket_interface:\n - platform: udp\n"
|
||||
)
|
||||
merged = build_helpers.load_component_yaml_configs(["first", "second"], tmp_path)
|
||||
assert merged["packet_interface"] == [{"platform": "uart"}, {"platform": "udp"}]
|
||||
# Other values are taken from the first directory that sets them
|
||||
assert merged["uart"] == {"port": "/dev/a"}
|
||||
@@ -1,81 +0,0 @@
|
||||
"""Tests for the integration test LineWaiter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.integration.log_utils import LineWaiter
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expect_matches_only_later_lines() -> None:
|
||||
waiter = LineWaiter()
|
||||
waiter.callback("[I][app]: ready")
|
||||
ready = waiter.expect("ready")
|
||||
value = waiter.expect(re.compile(r"value=(\d+)"))
|
||||
assert not ready.done()
|
||||
|
||||
waiter.callback("[D][x]: value=42")
|
||||
waiter.callback("[I][app]: ready again")
|
||||
assert await waiter.wait(ready) == "[I][app]: ready again"
|
||||
match = await waiter.wait(value)
|
||||
assert isinstance(match, re.Match)
|
||||
assert match.group(1) == "42"
|
||||
# wait_for still matches lines seen before the call.
|
||||
assert await waiter.wait_for("app", "ready") == "[I][app]: ready"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_timeout_reports_pattern_and_lines() -> None:
|
||||
waiter = LineWaiter()
|
||||
for i in range(30):
|
||||
waiter.callback(f"line {i}")
|
||||
future = waiter.expect("never")
|
||||
with pytest.raises(pytest.fail.Exception) as err:
|
||||
await waiter.wait(future, 0.01, "boot message missing")
|
||||
message = str(err.value)
|
||||
assert message.startswith("boot message missing (after 0.01s)")
|
||||
assert "pattern: 'never'" in message
|
||||
assert "lines (last 20 of 30):" in message
|
||||
assert message.endswith("line 29")
|
||||
assert future.cancelled()
|
||||
# The cancelled expectation is dropped on the next line.
|
||||
waiter.callback("never")
|
||||
assert not waiter._expectations
|
||||
|
||||
|
||||
def test_count() -> None:
|
||||
waiter = LineWaiter()
|
||||
for line in ("tick 1", "tock", "tick 2"):
|
||||
waiter.callback(line)
|
||||
assert waiter.count("tick") == 2
|
||||
assert waiter.count(re.compile(r"t\w+k$")) == 1
|
||||
assert waiter.count("missing") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expect_resolves_from_callback_while_waiting() -> None:
|
||||
waiter = LineWaiter()
|
||||
first = waiter.expect("first")
|
||||
second = waiter.expect("second")
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.call_soon(waiter.callback, "second")
|
||||
loop.call_soon(waiter.callback, "first")
|
||||
assert await waiter.wait(first) == "first"
|
||||
assert await waiter.wait(second) == "second"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_line_waiter_expectations_share_a_matching_line() -> None:
|
||||
waiter = LineWaiter()
|
||||
first = waiter.expect("tick")
|
||||
second = waiter.expect("tick")
|
||||
waiter.callback("tick 1")
|
||||
assert first.result() == second.result() == "tick 1"
|
||||
third = waiter.expect("tick")
|
||||
waiter.callback("tick 2")
|
||||
assert third.result() == "tick 2"
|
||||
assert waiter.count("tick") == 2
|
||||
@@ -1,108 +0,0 @@
|
||||
"""Tests for the state subscription helpers in tests/integration/state_utils.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityState, SensorInfo, SensorState, SwitchState
|
||||
import pytest
|
||||
|
||||
from tests.integration.state_utils import (
|
||||
StateSubscription,
|
||||
StateWaiter,
|
||||
press_and_wait,
|
||||
subscribe_after_initial_states,
|
||||
)
|
||||
|
||||
ENTITIES = [
|
||||
SensorInfo(object_id="temp", key=1),
|
||||
SensorInfo(object_id="humidity", key=2),
|
||||
ButtonInfo(object_id="set_temp", key=3),
|
||||
]
|
||||
|
||||
|
||||
class FakeClient:
|
||||
"""Records the state callback and button presses."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.callback: Callable[[EntityState], None] | None = None
|
||||
self.pressed: list[int] = []
|
||||
|
||||
def subscribe_states(self, callback: Callable[[EntityState], None]) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def button_command(self, key: int) -> None:
|
||||
self.pressed.append(key)
|
||||
self.callback(SensorState(key=1, state=42.0))
|
||||
|
||||
|
||||
async def _subscribe(client: FakeClient, **kwargs: object) -> StateSubscription:
|
||||
task = asyncio.create_task(
|
||||
subscribe_after_initial_states(client, ENTITIES, **kwargs)
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
client.callback(SensorState(key=1, state=1.0))
|
||||
client.callback(SensorState(key=2, state=2.0))
|
||||
return await task
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_swallows_initial_states_and_forwards_later_ones() -> None:
|
||||
client = FakeClient()
|
||||
received: list[EntityState] = []
|
||||
sub = await _subscribe(client, on_state=received.append)
|
||||
assert sub.initial_states[1].state == 1.0
|
||||
assert sub.initial_states[2].state == 2.0
|
||||
assert received == []
|
||||
|
||||
next_state = sub.waiter.next_state(key=2, state_type=SensorState)
|
||||
client.callback(SensorState(key=1, state=3.0))
|
||||
client.callback(SensorState(key=2, state=4.0))
|
||||
assert (await next_state).state == 4.0
|
||||
assert [s.state for s in received] == [3.0, 4.0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_timeout_lists_missing_entities() -> None:
|
||||
client = FakeClient()
|
||||
task = asyncio.create_task(
|
||||
subscribe_after_initial_states(client, ENTITIES, timeout=0.01)
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
client.callback(SensorState(key=1, state=1.0))
|
||||
with pytest.raises(pytest.fail.Exception, match=r"still_waiting[\s\S]*humidity"):
|
||||
await task
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_next_state_filters_by_type_and_times_out() -> None:
|
||||
waiter = StateWaiter()
|
||||
next_switch = waiter.next_state(state_type=SwitchState)
|
||||
waiter.on_state(SensorState(key=1, state=1.0))
|
||||
waiter.on_state(SwitchState(key=5, state=True))
|
||||
assert (await next_switch).key == 5
|
||||
with pytest.raises(TimeoutError, match="SwitchState key=9"):
|
||||
await waiter.next_state(key=9, state_type=SwitchState, timeout=0.01)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_press_and_wait_presses_button_and_returns_state() -> None:
|
||||
client = FakeClient()
|
||||
sub = await _subscribe(client)
|
||||
state = await press_and_wait(client, ENTITIES, sub.waiter, "set_temp", SensorState)
|
||||
assert client.pressed == [3]
|
||||
assert state.state == 42.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_press_and_wait_waits_for_the_given_key() -> None:
|
||||
client = FakeClient()
|
||||
sub = await _subscribe(client)
|
||||
pending = asyncio.ensure_future(
|
||||
press_and_wait(client, ENTITIES, sub.waiter, "set_temp", SensorState, key=2)
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
assert not pending.done() # the press published key 1
|
||||
client.callback(SensorState(key=2, state=55.0))
|
||||
assert (await pending).state == 55.0
|
||||
@@ -1,216 +0,0 @@
|
||||
"""Unit tests for the packet_interface component."""
|
||||
|
||||
import hashlib
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components import packet_interface
|
||||
from esphome.components.packet_interface import CONF_MAX_PACKET_SIZE
|
||||
from esphome.components.packet_interface.layers import (
|
||||
CONF_RELIABILITY,
|
||||
CONF_SESSION,
|
||||
RELIABILITY,
|
||||
STACK_ORDER,
|
||||
LayerKind,
|
||||
xxtea_encryption,
|
||||
)
|
||||
from esphome.components.packet_interface.layers.basic_session import CONF_KEEP_ALIVE
|
||||
from esphome.components.packet_interface.layers.hdlc_reliability import (
|
||||
CONF_MAX_QUEUE_LENGTH,
|
||||
)
|
||||
from esphome.components.uart import packet_interface as uart_packet_interface
|
||||
from esphome.components.udp import packet_interface as udp_packet_interface
|
||||
from esphome.const import CONF_ENCRYPTION
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "default", "limit"),
|
||||
[
|
||||
(uart_packet_interface.CONFIG_SCHEMA, 508, 4096),
|
||||
(udp_packet_interface.CONFIG_SCHEMA, 508, 508),
|
||||
],
|
||||
)
|
||||
def test_platform_max_packet_size(
|
||||
setup_core, schema: cv.Schema, default: int, limit: int
|
||||
) -> None:
|
||||
"""Each platform has its own default and upper limit for max_packet_size."""
|
||||
assert schema({})[CONF_MAX_PACKET_SIZE] == default
|
||||
assert schema({CONF_MAX_PACKET_SIZE: limit})[CONF_MAX_PACKET_SIZE] == limit
|
||||
with pytest.raises(cv.Invalid):
|
||||
schema({CONF_MAX_PACKET_SIZE: limit + 1})
|
||||
with pytest.raises(cv.Invalid):
|
||||
schema({CONF_MAX_PACKET_SIZE: 15})
|
||||
|
||||
|
||||
def test_schema_default(setup_core) -> None:
|
||||
"""packet_interface_schema applies the default it is given."""
|
||||
schema = packet_interface.packet_interface_schema(
|
||||
packet_interface.PacketInterface, 100
|
||||
)
|
||||
assert schema({})[CONF_MAX_PACKET_SIZE] == 100
|
||||
|
||||
|
||||
def test_reliability_defaults(setup_core) -> None:
|
||||
"""An empty reliability key selects the HDLC layer with its defaults."""
|
||||
config = uart_packet_interface.CONFIG_SCHEMA({CONF_RELIABILITY: None})
|
||||
reliability = config[CONF_RELIABILITY]
|
||||
assert reliability["type"] == "hdlc"
|
||||
assert reliability["timeout"].total_milliseconds == 200
|
||||
assert reliability[CONF_MAX_QUEUE_LENGTH] == 4
|
||||
assert reliability["max_retries"] == 5
|
||||
|
||||
|
||||
def test_reliability_queue_length_limit(setup_core) -> None:
|
||||
"""The queue cannot hold more frames than 3 bit sequence numbers allow."""
|
||||
with pytest.raises(cv.Invalid):
|
||||
uart_packet_interface.CONFIG_SCHEMA(
|
||||
{CONF_RELIABILITY: {CONF_MAX_QUEUE_LENGTH: 8}}
|
||||
)
|
||||
|
||||
|
||||
def test_reliability_timeout_minimum(setup_core) -> None:
|
||||
"""A timeout shorter than a few loop passes cannot work."""
|
||||
uart_packet_interface.CONFIG_SCHEMA({CONF_RELIABILITY: {"timeout": "50ms"}})
|
||||
with pytest.raises(cv.Invalid):
|
||||
uart_packet_interface.CONFIG_SCHEMA({CONF_RELIABILITY: {"timeout": "40ms"}})
|
||||
|
||||
|
||||
def test_ack_delay_shorter_than_timeout(setup_core) -> None:
|
||||
"""The acknowledgement must not wait longer than the peer's timeout."""
|
||||
reliability = uart_packet_interface.CONFIG_SCHEMA(
|
||||
{CONF_RELIABILITY: {"timeout": "500ms", "ack_delay": "100ms"}}
|
||||
)[CONF_RELIABILITY]
|
||||
assert reliability["ack_delay"].total_milliseconds == 100
|
||||
with pytest.raises(cv.Invalid, match="shorter"):
|
||||
uart_packet_interface.CONFIG_SCHEMA(
|
||||
{CONF_RELIABILITY: {"timeout": "500ms", "ack_delay": "500ms"}}
|
||||
)
|
||||
|
||||
|
||||
def test_layer_types_registered() -> None:
|
||||
"""Importing packet_interface registers the built-in layer types."""
|
||||
assert {kind.key: set(kind.types) for kind in STACK_ORDER} == {
|
||||
CONF_ENCRYPTION: {"xxtea"},
|
||||
CONF_RELIABILITY: {"hdlc"},
|
||||
CONF_SESSION: {"basic"},
|
||||
}
|
||||
|
||||
|
||||
async def _build(config: dict, lower: Any) -> Any:
|
||||
return lower
|
||||
|
||||
|
||||
def test_registered_type_selectable(setup_core) -> None:
|
||||
"""A newly registered layer type can be selected with type."""
|
||||
kind = LayerKind("test_kind", "test")
|
||||
kind.register("test", schema=cv.Schema({}))(_build)
|
||||
config = kind.schema()({"type": "test"})
|
||||
assert config["type"] == "test"
|
||||
assert kind.get_type(config).build is _build
|
||||
|
||||
|
||||
def test_duplicate_layer_type_rejected(setup_core) -> None:
|
||||
"""Registering a type under a name already in use is an error, not a silent replacement."""
|
||||
kind = LayerKind("test_kind", "test")
|
||||
kind.register("test", schema=cv.Schema({}))(_build)
|
||||
with pytest.raises(ValueError, match="already registered"):
|
||||
kind.register("test", schema=cv.Schema({}))(_build)
|
||||
|
||||
|
||||
def test_late_registration_rejected(setup_core) -> None:
|
||||
"""A type registered after the schemas were built would be missing from them, so it is an error."""
|
||||
with pytest.raises(ValueError, match="after the schemas were built"):
|
||||
RELIABILITY.register("late", schema=cv.Schema({}))(_build)
|
||||
|
||||
|
||||
def test_session_defaults(setup_core) -> None:
|
||||
"""An empty session key selects the basic session, timing out after three keep-alives."""
|
||||
session = uart_packet_interface.CONFIG_SCHEMA({CONF_SESSION: None})[CONF_SESSION]
|
||||
assert session["type"] == "basic"
|
||||
assert session[CONF_KEEP_ALIVE].total_milliseconds == 10000
|
||||
assert session["timeout"].total_milliseconds == 30000
|
||||
|
||||
|
||||
def test_session_timeout(setup_core) -> None:
|
||||
"""The timeout follows keep_alive unless given, and must be longer than it."""
|
||||
session = uart_packet_interface.CONFIG_SCHEMA(
|
||||
{CONF_SESSION: {CONF_KEEP_ALIVE: "2s"}}
|
||||
)[CONF_SESSION]
|
||||
assert session["timeout"].total_milliseconds == 6000
|
||||
with pytest.raises(cv.Invalid):
|
||||
uart_packet_interface.CONFIG_SCHEMA(
|
||||
{CONF_SESSION: {CONF_KEEP_ALIVE: "5s", "timeout": "5s"}}
|
||||
)
|
||||
with pytest.raises(cv.Invalid):
|
||||
uart_packet_interface.CONFIG_SCHEMA({CONF_SESSION: {CONF_KEEP_ALIVE: "500ms"}})
|
||||
with pytest.raises(cv.Invalid):
|
||||
uart_packet_interface.CONFIG_SCHEMA({CONF_SESSION: {CONF_KEEP_ALIVE: "2h"}})
|
||||
with pytest.raises(cv.Invalid):
|
||||
uart_packet_interface.CONFIG_SCHEMA({CONF_SESSION: {"timeout": "4h"}})
|
||||
|
||||
|
||||
def test_encryption_key_only(setup_core) -> None:
|
||||
"""The key can be given on its own, and selects XXTEA."""
|
||||
encryption = uart_packet_interface.CONFIG_SCHEMA({CONF_ENCRYPTION: "secret"})[
|
||||
CONF_ENCRYPTION
|
||||
]
|
||||
assert encryption["type"] == "xxtea"
|
||||
assert encryption["key"] == "secret"
|
||||
assert encryption["rolling_code_enable"] is False
|
||||
|
||||
|
||||
def test_encryption_requires_key(setup_core) -> None:
|
||||
"""An encryption layer without a key is rejected."""
|
||||
with pytest.raises(cv.Invalid):
|
||||
uart_packet_interface.CONFIG_SCHEMA({CONF_ENCRYPTION: {}})
|
||||
with pytest.raises(cv.Invalid):
|
||||
uart_packet_interface.CONFIG_SCHEMA({CONF_ENCRYPTION: ""})
|
||||
|
||||
|
||||
def test_rolling_code_requires_session(setup_core) -> None:
|
||||
"""The rolling code needs the session's keep-alives, so the peer sends this device's boot number back."""
|
||||
rolling = {CONF_ENCRYPTION: {"key": "secret", "rolling_code_enable": True}}
|
||||
with pytest.raises(cv.Invalid, match="session"):
|
||||
uart_packet_interface.CONFIG_SCHEMA(rolling)
|
||||
uart_packet_interface.CONFIG_SCHEMA({**rolling, CONF_SESSION: None})
|
||||
uart_packet_interface.CONFIG_SCHEMA({CONF_ENCRYPTION: "secret"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "loads"),
|
||||
[
|
||||
({}, []),
|
||||
({CONF_RELIABILITY: None, CONF_SESSION: None}, []),
|
||||
({CONF_SESSION: {"binary_sensor": {"name": "Link"}}}, ["binary_sensor"]),
|
||||
({CONF_ENCRYPTION: "secret"}, ["xxtea"]),
|
||||
],
|
||||
)
|
||||
def test_auto_load_follows_layers(
|
||||
setup_core, config: dict[str, Any], loads: list[str]
|
||||
) -> None:
|
||||
"""Only the components that the configured layers use are loaded."""
|
||||
validated = uart_packet_interface.CONFIG_SCHEMA(config)
|
||||
assert packet_interface.AUTO_LOAD([validated]) == loads
|
||||
|
||||
|
||||
def test_auto_load_without_config() -> None:
|
||||
"""Tooling that lists dependencies gets everything a layer could load."""
|
||||
assert packet_interface.AUTO_LOAD() == ["binary_sensor", "xxtea"]
|
||||
|
||||
|
||||
def test_xxtea_key_stretched() -> None:
|
||||
"""The key comes from PBKDF2 of the key text, not a single hash a captured packet could check guesses against."""
|
||||
key = xxtea_encryption.derive_key("Johnny Cash")
|
||||
digest = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
b"Johnny Cash",
|
||||
b"esphome packet_interface xxtea",
|
||||
600_000,
|
||||
)
|
||||
assert key == [int.from_bytes(digest[i : i + 4], "little") for i in range(0, 32, 4)]
|
||||
assert len(key) == 8 and all(0 <= word < 2**32 for word in key)
|
||||
assert key != xxtea_encryption.derive_key("Johnny Cash ")
|
||||
sha = hashlib.sha256(b"Johnny Cash").digest()
|
||||
assert key[0] != int.from_bytes(sha[:4], "little")
|
||||
Reference in new issue
Block a user