Compare commits

..
Author SHA1 Message Date
J. Nick Koston fef73dbd15 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 17:24:30 -10:00
J. Nick Koston 0628e4e492 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 16:44:21 -10:00
J. Nick Koston abbf3a6960 [sm300d2] Wait for the rest of a frame cut off at the end of the buffer 2026-10-22 16:44:12 -10:00
J. Nick Koston a97d4241bd [ds1603l] [ufm01] Read with read_available like the other FrameReader users 2026-10-22 16:33:33 -10:00
J. Nick Koston f1031bc64d [dfrobot_sen0465_uart] Read the answer with one read_available call 2026-10-22 16:31:31 -10:00
J. Nick Koston 71064efc25 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 16:13:15 -10:00
J. Nick Koston f5ea2d9f01 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 15:58:37 -10:00
J. Nick Koston 5d69784adc [sm300d2] Read the frame with read_available and inline the checksum helper 2026-10-22 15:51:02 -10:00
J. Nick Koston 22a0440348 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 15:47:52 -10:00
J. Nick Koston 46379d8cbc [ufm01] Brace the single log call body 2026-10-22 15:45:41 -10:00
J. Nick Koston f00328f45d Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 15:39:40 -10:00
J. Nick Koston 61bd5e2657 [ufm01] Drop the start byte checks FrameReader already made 2026-10-22 15:22:36 -10:00
J. Nick Koston 7a1dc1279d [uart] Read bytes one at a time in the FrameReader users to keep flash down 2026-10-22 15:14:41 -10:00
J. Nick Koston 04165e289f Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 15:09:52 -10:00
J. Nick Koston a7e5a2a621 [uart] Collect fixed length frames with FrameReader in five components 2026-10-22 15:08:57 -10:00
12 changed files with 90 additions and 176 deletions

No files matched your search

@@ -1,10 +1,7 @@
#include "dfrobot_sen0465_uart.h"
#include <algorithm>
namespace esphome::dfrobot_sen0465_uart {
using dfrobot_sen0465_base::FRAME_HEAD;
using dfrobot_sen0465_base::FRAME_SIZE;
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
@@ -18,30 +15,21 @@ void DFRobotSEN0465UARTComponent::setup() {
}
void DFRobotSEN0465UARTComponent::loop() {
// The tail of a reading the probe sent on its own may precede the answer; start at the next frame head.
uint8_t byte;
while (this->rx_count_ == 0 && this->peek_byte(&byte) && byte != FRAME_HEAD)
this->read_byte(&byte);
const size_t available = this->available();
if (available == 0)
return;
const size_t len = std::min<size_t>(available, FRAME_SIZE - this->rx_count_);
if (!this->read_array(this->rx_buffer_ + this->rx_count_, len))
return;
this->rx_count_ += len;
if (this->rx_count_ < FRAME_SIZE)
// The tail of a reading the probe sent on its own may precede the answer; the reader skips to the frame head.
const uint8_t *frame = nullptr;
this->read_available<FRAME_SIZE>([&](uint8_t c) { return (frame = this->reader_.feed(c)) == nullptr; });
if (frame == nullptr)
return;
this->rx_count_ = 0;
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
this->disable_loop();
this->handle_response_(this->rx_buffer_);
this->handle_response_(frame);
}
bool DFRobotSEN0465UARTComponent::write_frame(const uint8_t *frame) {
// Drop anything left over, such as the answer to the mode change or readings sent before passive mode was set
this->discard_available();
this->rx_count_ = 0;
this->reader_.reset();
this->write_array(frame, FRAME_SIZE);
return true;
}
@@ -50,7 +38,7 @@ void DFRobotSEN0465UARTComponent::read_response() {
this->enable_loop();
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
this->disable_loop();
this->rx_count_ = 0;
this->reader_.reset();
this->handle_no_response_();
});
}
@@ -2,6 +2,7 @@
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/frame_reader.h"
namespace esphome::dfrobot_sen0465_uart {
@@ -17,8 +18,7 @@ class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSE
bool write_frame(const uint8_t *frame) override;
void read_response() override;
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
uint8_t rx_count_{0};
FrameReader<dfrobot_sen0465_base::FRAME_SIZE, dfrobot_sen0465_base::FRAME_HEAD> reader_;
};
} // namespace esphome::dfrobot_sen0465_uart
+15 -36
View File
@@ -1,7 +1,5 @@
#include "ds1603l.h"
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -10,35 +8,28 @@ namespace esphome::ds1603l {
ESPHOME_LOG_TAG(TAG, "ds1603l.sensor");
void DS1603L::loop() {
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
uint8_t byte;
while (this->available() > 0 && this->read_byte(&byte)) {
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
continue;
this->read_available([this](uint8_t byte) {
const uint8_t *frame = this->reader_.feed(byte);
if (frame == nullptr) {
if (this->reader_.length() == 0) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
}
return;
}
this->rx_buffer_[this->rx_count_++] = byte;
if (this->rx_count_ < FRAME_SIZE) {
continue;
}
if (this->parse_data_()) {
this->rx_count_ = 0;
} else {
if (!this->parse_data_(frame)) {
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
this->resync_();
this->reader_.resync();
}
}
});
}
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
bool DS1603L::parse_data_() {
uint8_t header = this->rx_buffer_[0];
uint8_t data_h = this->rx_buffer_[1];
uint8_t data_l = this->rx_buffer_[2];
uint8_t checksum = this->rx_buffer_[3];
bool DS1603L::parse_data_(const uint8_t *frame) {
uint8_t header = frame[0];
uint8_t data_h = frame[1];
uint8_t data_l = frame[2];
uint8_t checksum = frame[3];
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
@@ -53,16 +44,4 @@ bool DS1603L::parse_data_() {
return true;
}
void DS1603L::resync_() {
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
size_t start = 1;
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
start++;
}
this->rx_count_ -= start;
if (this->rx_count_ > 0) {
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
}
}
} // namespace esphome::ds1603l
+4 -6
View File
@@ -6,6 +6,7 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/frame_reader.h"
namespace esphome::ds1603l {
@@ -18,13 +19,10 @@ class DS1603L final : public sensor::Sensor, public Component, public uart::UART
static constexpr uint8_t HEADER_BYTE = 0xFF;
static constexpr size_t FRAME_SIZE = 4;
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
bool parse_data_();
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
void resync_();
// Validates the checksum of the frame and publishes it. Returns false if the frame is invalid.
bool parse_data_(const uint8_t *frame);
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
FrameReader<FRAME_SIZE, HEADER_BYTE> reader_;
};
} // namespace esphome::ds1603l
+10 -18
View File
@@ -1,33 +1,27 @@
#include "sm300d2.h"
#include "esphome/core/frame_reader.h"
#include "esphome/core/log.h"
namespace esphome::sm300d2 {
ESPHOME_LOG_TAG(TAG, "sm300d2");
static const uint8_t SM300D2_RESPONSE_LENGTH = 17;
static constexpr uint8_t SM300D2_RESPONSE_LENGTH = 17;
void SM300D2Sensor::update() {
uint8_t response[SM300D2_RESPONSE_LENGTH];
uint8_t peeked;
// Takes the first frame already received; only one cut off at the end of the buffer is waited for
FrameReader<SM300D2_RESPONSE_LENGTH, 0x3C, 0x02> reader;
const uint8_t *response = nullptr;
this->read_available<1>([&](uint8_t c) { return (response = reader.feed(c)) == nullptr; });
for (uint8_t c; response == nullptr && reader.length() > 0 && this->read_byte(&c);)
response = reader.feed(c);
while (this->available() > 0 && this->peek_byte(&peeked) && peeked != 0x3C)
this->read();
bool read_success = read_array(response, SM300D2_RESPONSE_LENGTH);
if (!read_success) {
if (response == nullptr) {
ESP_LOGW(TAG, "Reading data from SM300D2 failed!");
status_set_warning();
return;
}
if (response[0] != 0x3C || response[1] != 0x02) {
ESP_LOGW(TAG, "Invalid preamble for SM300D2 response!");
this->status_set_warning();
return;
}
uint16_t calculated_checksum = this->sm300d2_checksum_(response);
uint16_t calculated_checksum = sum_bytes(response, SM300D2_RESPONSE_LENGTH - 1);
// Occasionally the checksum has a +/- 0x80 offset. Negative temperatures are
// responsible for some of these. The rest are unknown/undocumented.
if ((calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) &&
@@ -83,8 +77,6 @@ void SM300D2Sensor::update() {
this->humidity_sensor_->publish_state(humidity);
}
uint16_t SM300D2Sensor::sm300d2_checksum_(uint8_t *ptr) { return sum_bytes(ptr, SM300D2_RESPONSE_LENGTH - 1); }
void SM300D2Sensor::dump_config() {
ESP_LOGCONFIG(TAG, "SM300D2:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
-2
View File
@@ -20,8 +20,6 @@ class SM300D2Sensor final : public PollingComponent, public uart::UARTDevice {
void dump_config() override;
protected:
uint16_t sm300d2_checksum_(uint8_t *ptr);
sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *formaldehyde_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
+22 -39
View File
@@ -44,8 +44,6 @@ static constexpr std::array<uint8_t, 7> GET_SOFTWARE_VERSION = {0xFE, 0xFE, 0x11
static constexpr size_t FRAME_DEVICE_ID_INDEX = 2;
static constexpr size_t FRAME_CHECKSUM_INDEX = 30;
static constexpr size_t FRAME_STOP_INDEX = 31;
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
static constexpr uint8_t PASSIVE_START_BYTE_2 = 0x64;
static constexpr uint8_t PASSIVE_START_BYTE_2_WITH_ID = 0x96;
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
@@ -120,9 +118,8 @@ static bool check_byte(const uint8_t data[FRAME_SIZE], size_t index, uint8_t exp
static bool validate_active_frame(const uint8_t data[FRAME_SIZE]) {
const uint8_t sum = sum_bytes(data, FRAME_CHECKSUM_INDEX);
return check_byte(data, 0, FRAME_START_BYTE_1, "start byte 1") &&
check_byte(data, 1, FRAME_START_BYTE_2, "start byte 2") &&
check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
// FrameReader already matched the start bytes
return check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
check_byte(data, FRAME_INDEX_RESERVED_SECTION, FRAME_FLAG_RESERVED_SECTION, "reserved section flag") &&
check_byte(data, FRAME_INDEX_TEMP_FLAG, FRAME_FLAG_TEMP, "temperature flag") &&
check_byte(data, FRAME_CHECKSUM_INDEX, sum, "checksum") &&
@@ -215,7 +212,7 @@ static bool read_flow_rate_out_of_range(const uint8_t data[FRAME_SIZE]) {
void UFM01Component::flush_rx_() {
this->discard_available();
this->read_index_ = 0;
this->active_reader_.reset();
}
void UFM01Component::send_command_no_wait_(const std::array<uint8_t, 7> &command) {
@@ -423,7 +420,7 @@ bool UFM01Component::service_software_version_(uint32_t now) {
}
#endif // USE_UFM01_SOFTWARE_VERSION
void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now) {
void UFM01Component::on_active_frame_(const uint8_t data[FRAME_SIZE], uint32_t now) {
#ifdef USE_UFM01_METER_ID
this->publish_meter_id_from_frame_(data);
#endif
@@ -461,40 +458,26 @@ void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now) {
bool UFM01Component::process_active_stream_(uint32_t now) {
bool got_valid_frame = false;
while (this->available()) {
if (!this->read_byte(&this->data_[this->read_index_])) {
ESP_LOGW(TAG, "unable to read byte");
this->read_index_ = 0;
continue;
}
if ((this->read_index_ == 0 && this->data_[0] != FRAME_START_BYTE_1) ||
(this->read_index_ == 1 && this->data_[1] != FRAME_START_BYTE_2)) {
ESP_LOGD(TAG, "not start of data at %" PRId32 " (is 0x%02X)", this->read_index_, this->data_[this->read_index_]);
this->read_index_ = 0;
continue;
}
if (++this->read_index_ < static_cast<int32_t>(FRAME_SIZE))
continue;
if (validate_active_frame(this->data_)) {
this->on_active_frame_(this->data_, now);
this->read_index_ = 0;
got_valid_frame = true;
continue;
}
log_hex(this->data_, sizeof(this->data_));
ESP_LOGW(TAG, "unable to read data");
for (int32_t i = 2;
i < static_cast<int32_t>(FRAME_STOP_INDEX) && this->read_index_ == static_cast<int32_t>(FRAME_SIZE); ++i) {
if ((this->data_[i] == FRAME_START_BYTE_1) && (this->data_[i + 1] == FRAME_START_BYTE_2)) {
for (int32_t j = i; j < static_cast<int32_t>(FRAME_SIZE); ++j)
this->data_[j - i] = this->data_[j];
this->read_index_ = static_cast<int32_t>(FRAME_SIZE) - i;
const bool read_ok = this->read_available([this, now, &got_valid_frame](uint8_t byte) {
const uint8_t *frame = this->active_reader_.feed(byte);
if (frame == nullptr) {
if (this->active_reader_.length() == 0) {
ESP_LOGD(TAG, "not start of data (is 0x%02X)", byte);
}
return;
}
if (this->read_index_ == static_cast<int32_t>(FRAME_SIZE))
this->read_index_ = 0;
if (validate_active_frame(frame)) {
this->on_active_frame_(frame, now);
got_valid_frame = true;
return;
}
log_hex(frame, FRAME_SIZE);
ESP_LOGW(TAG, "unable to read data");
this->active_reader_.resync();
});
if (!read_ok) {
ESP_LOGW(TAG, "unable to read byte");
this->active_reader_.reset();
}
return got_valid_frame;
+5 -3
View File
@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/frame_reader.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
@@ -25,6 +26,8 @@ class TestableUFM01;
} // namespace testing
static constexpr size_t FRAME_SIZE = 32;
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
static constexpr size_t PASSIVE_FRAME_SIZE = 23;
static constexpr size_t PASSIVE_FRAME_WITH_ID_SIZE = 39;
#ifdef USE_UFM01_METER_ID
@@ -108,7 +111,7 @@ class UFM01Component : public uart::UARTDevice, public Component {
bool consume_ack_();
void flush_rx_();
bool process_active_stream_(uint32_t now);
void on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now);
void on_active_frame_(const uint8_t data[FRAME_SIZE], uint32_t now);
#ifdef USE_UFM01_METER_ID
void publish_meter_id_from_frame_(const uint8_t data[FRAME_SIZE]);
#endif
@@ -145,7 +148,6 @@ class UFM01Component : public uart::UARTDevice, public Component {
#ifdef USE_UFM01_CLEAR_ACCUMULATED_FLOW_ACTION
uint32_t pending_clear_start_ms_{0};
#endif
int32_t read_index_{0};
OperatingMode operating_mode_{OperatingMode::STARTUP};
StartupPhase startup_phase_{StartupPhase::WAIT};
@@ -170,7 +172,7 @@ class UFM01Component : public uart::UARTDevice, public Component {
#endif
uint8_t passive_frame_[PASSIVE_FRAME_MAX_SIZE];
uint8_t data_[FRAME_SIZE];
FrameReader<FRAME_SIZE, FRAME_START_BYTE_1, FRAME_START_BYTE_2> active_reader_;
friend class testing::TestableUFM01;
};
+10 -42
View File
@@ -4,70 +4,38 @@
namespace esphome::wts01 {
constexpr uint8_t HEADER_1 = 0x55;
constexpr uint8_t HEADER_2 = 0x01;
constexpr uint8_t HEADER_3 = 0x01;
constexpr uint8_t HEADER_4 = 0x04;
ESPHOME_LOG_TAG(TAG, "wts01");
void WTS01Sensor::loop() {
this->read_available([this](uint8_t c) { this->handle_char_(c); });
this->read_available([this](uint8_t c) {
if (const uint8_t *packet = this->reader_.feed(c)) {
this->process_packet_(packet);
}
});
}
void WTS01Sensor::dump_config() { LOG_SENSOR("", "WTS01 Sensor", this); }
void WTS01Sensor::handle_char_(uint8_t c) {
// State machine for processing the header. Reset if something doesn't match.
if (this->buffer_pos_ == 0 && c != HEADER_1) {
return;
}
if (this->buffer_pos_ == 1 && c != HEADER_2) {
this->buffer_pos_ = 0;
return;
}
if (this->buffer_pos_ == 2 && c != HEADER_3) {
this->buffer_pos_ = 0;
return;
}
if (this->buffer_pos_ == 3 && c != HEADER_4) {
this->buffer_pos_ = 0;
return;
}
// Add byte to buffer
this->buffer_[this->buffer_pos_++] = c;
// Process complete packet
if (this->buffer_pos_ >= PACKET_SIZE) {
this->process_packet_();
this->buffer_pos_ = 0;
}
}
void WTS01Sensor::process_packet_() {
void WTS01Sensor::process_packet_(const uint8_t *packet) {
// Based on Tasmota implementation
// Format: 55 01 01 04 01 11 16 12 95
// header T Td Ck - T = Temperature, Td = Temperature decimal, Ck = Checksum
const uint8_t calculated_checksum = sum_bytes(this->buffer_, PACKET_SIZE - 1);
const uint8_t calculated_checksum = sum_bytes(packet, PACKET_SIZE - 1);
uint8_t received_checksum = this->buffer_[PACKET_SIZE - 1];
uint8_t received_checksum = packet[PACKET_SIZE - 1];
if (calculated_checksum != received_checksum) {
ESP_LOGW(TAG, "WTS01 Checksum doesn't match: 0x%02X != 0x%02X", received_checksum, calculated_checksum);
return;
}
// Extract temperature value
const uint8_t raw = this->buffer_[6];
const uint8_t raw = packet[6];
// WTS01 encodes sign in bit 7, magnitude in bits 0-6
const bool negative = (raw & 0x80) != 0;
const uint8_t magnitude = raw & 0x7F;
const float decimal = static_cast<float>(this->buffer_[7]) / 100.0f;
const float decimal = static_cast<float>(packet[7]) / 100.0f;
float temperature = static_cast<float>(magnitude) + decimal;
+3 -4
View File
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/frame_reader.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
@@ -14,11 +15,9 @@ class WTS01Sensor final : public sensor::Sensor, public uart::UARTDevice, public
void dump_config() override;
protected:
uint8_t buffer_[PACKET_SIZE];
uint8_t buffer_pos_{0};
void process_packet_(const uint8_t *packet);
void handle_char_(uint8_t c);
void process_packet_();
FrameReader<PACKET_SIZE, 0x55, 0x01, 0x01, 0x04> reader_;
};
} // namespace esphome::wts01
+3 -5
View File
@@ -20,8 +20,6 @@
namespace esphome::ufm01::testing {
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
static constexpr uint8_t PASSIVE_START_BYTE_2 = 0x64;
static constexpr uint8_t PASSIVE_START_BYTE_2_WITH_ID = 0x96;
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
@@ -131,7 +129,7 @@ class TestableUFM01 : public UFM01Component {
StartupPhase startup_phase() const { return this->startup_phase_; }
int32_t read_index() const { return this->read_index_; }
int32_t read_index() const { return this->active_reader_.length(); }
uint8_t passive_index() const { return this->passive_index_; }
@@ -165,7 +163,7 @@ class TestableUFM01 : public UFM01Component {
this->operating_mode_ = OperatingMode::ACTIVE_STREAM;
this->last_valid_frame_ms_ = now();
this->passive_read_pending_ = false;
this->read_index_ = 0;
this->active_reader_.reset();
}
void begin_active_wait_frame() {
@@ -182,7 +180,7 @@ class TestableUFM01 : public UFM01Component {
}
void reset_state() {
this->read_index_ = 0;
this->active_reader_.reset();
this->last_valid_frame_ms_ = 0;
this->passive_index_ = 0;
this->passive_read_pending_ = false;
@@ -30,6 +30,15 @@ TEST_F(UFM01Test, InvalidActiveFrameChecksumRejected) {
EXPECT_EQ(this->ufm01_.last_valid_frame_ms(), 0u);
}
TEST_F(UFM01Test, RejectedActiveFrameResyncsOnStartBytesInside) {
// A cut off frame start makes the first 32 bytes invalid; the real frame starts inside them
this->mock_uart_.enqueue({FRAME_START_BYTE_1, FRAME_START_BYTE_2, 0x00, 0x00});
this->mock_uart_.enqueue(make_active_frame());
EXPECT_TRUE(this->ufm01_.process_active_stream());
EXPECT_EQ(this->ufm01_.read_index(), 0);
}
TEST_F(UFM01Test, ValidPassiveFrameReadSuccess) {
auto frame = make_passive_frame();
this->mock_uart_.enqueue(frame);