[ezsp_proxy_tap] Add SiLabs EZSP auto-ACK implementation (#20304)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
puddly
2026-10-12 12:51:48 -10:00
committed by GitHub
co-authored by J. Nick Koston J. Nick Koston
parent 1c315c3bf2
commit c5baae35d5
17 changed files with 620 additions and 0 deletions
+1
View File
@@ -202,6 +202,7 @@ esphome/components/exponential_moving_average/* @clydebarrow
esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento
esphome/components/ezsp_proxy_tap/* @kbx81 @puddly
esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
@@ -0,0 +1,34 @@
import esphome.codegen as cg
from esphome.components import serial_proxy
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81", "@puddly"]
DEPENDENCIES = ["serial_proxy"]
DOMAIN = "ezsp_proxy_tap"
CONF_SERIAL_PROXY_ID = "serial_proxy_id"
ezsp_proxy_tap_ns = cg.esphome_ns.namespace("ezsp_proxy_tap")
EzspProxyTap = ezsp_proxy_tap_ns.class_(
"EzspProxyTap", cg.Component, serial_proxy.SerialProxyTap
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(EzspProxyTap),
cv.Required(CONF_SERIAL_PROXY_ID): cv.use_id(serial_proxy.SerialProxy),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config: ConfigType) -> None:
sp = await cg.get_variable(config[CONF_SERIAL_PROXY_ID])
var = cg.new_Pvariable(config[CONF_ID], sp)
await cg.register_component(var, config)
cg.add_define("USE_EZSP_PROXY_TAP")
# Compiles the tap interface into serial_proxy; without it the port is a plain byte pipe
cg.add_define("USE_SERIAL_PROXY_TAP")
@@ -0,0 +1,126 @@
#include "ash_acknowledger.h"
#include "esphome/core/helpers.h"
namespace esphome::ezsp_proxy_tap {
// Control byte of an RSTACK, and the only ASH version byte that can follow it
static constexpr uint8_t ASH_RSTACK_CONTROL = 0xC1;
static constexpr uint8_t ASH_PROTOCOL_VERSION = 0x02;
static constexpr size_t ASH_RSTACK_BODY_SIZE = 3; // control, version, reset code
// A DATA frame carries an EZSP frame of at least 3 bytes after its control byte
static constexpr size_t ASH_MIN_DATA_BODY_SIZE = 1 + 3;
static constexpr size_t ASH_CRC_SIZE = 2;
// Smallest legal frame on the wire: a bare control byte plus its CRC
static constexpr size_t ASH_MIN_FRAME_SIZE = 1 + ASH_CRC_SIZE;
void AshFrameScanner::reset() {
this->index_ = 0;
this->crc_ = ASH_CRC_INIT;
this->escaped_ = false;
this->poisoned_ = false;
this->discarding_ = false;
}
bool AshFrameScanner::feed(uint8_t byte) {
if (byte == ASH_FLAG_BYTE) {
// Running the CRC over the body and its trailing CRC bytes leaves zero when correct
const bool valid = !this->discarding_ && !this->poisoned_ && !this->escaped_ &&
this->index_ >= ASH_MIN_FRAME_SIZE && this->crc_ == 0;
if (valid) {
this->frame_length_ = this->index_ - ASH_CRC_SIZE;
}
this->reset();
return valid;
}
// CANCEL also ends a SUBSTITUTE's discard, so the bytes after it start a new frame
if (byte == ASH_CANCEL_BYTE) {
this->reset();
return false;
}
if (this->discarding_ || (byte == ASH_WAKE_BYTE && this->index_ == 0)) {
return false;
}
switch (byte) {
case ASH_SUBSTITUTE_BYTE:
this->discarding_ = true;
return false;
case ASH_XON_BYTE:
case ASH_XOFF_BYTE:
return false;
case ASH_ESCAPE_BYTE:
this->escaped_ = true;
return false;
default:
break;
}
uint8_t value = byte;
if (this->escaped_) {
this->escaped_ = false;
value = byte ^ ASH_XOR_BYTE;
}
if (this->index_ >= MAX_ASH_FRAME_SIZE) {
this->poisoned_ = true;
return false;
}
if (this->index_ < ASH_HEADER_SIZE) {
this->header_[this->index_] = value;
}
this->index_++;
this->crc_ = crc16be(&value, 1, this->crc_);
return false;
}
void AshAcknowledger::reset() {
this->scanner_.reset();
this->rx_sequence_ = 0;
this->synced_ = false;
}
bool AshAcknowledger::feed(uint8_t byte) { return this->scanner_.feed(byte) && this->handle_frame_(); }
bool AshAcknowledger::handle_frame_() {
const uint8_t *body = this->scanner_.header();
const uint8_t control = body[0];
// An RSTACK restarts the NCP's frame numbering
if (control == ASH_RSTACK_CONTROL) {
if (this->scanner_.length() == ASH_RSTACK_BODY_SIZE && body[1] == ASH_PROTOCOL_VERSION) {
this->rx_sequence_ = 0;
this->synced_ = true;
}
return false;
}
if ((control & 0x80) != 0) {
return false; // ACK/NAK/RST/ERROR: nothing is owed for these
}
if (this->scanner_.length() < ASH_MIN_DATA_BODY_SIZE) {
return false;
}
const uint8_t frame_num = (control >> 4) & ASH_MAX_SEQUENCE;
const bool re_tx = (control & 0x08) != 0;
// Without an RSTACK, the session began before we were watching: follow the NCP's numbering
if (!this->synced_) {
this->rx_sequence_ = frame_num;
this->synced_ = true;
}
if (frame_num == this->rx_sequence_) {
this->rx_sequence_ = (this->rx_sequence_ + 1) & ASH_MAX_SEQUENCE;
return true;
}
return re_tx;
}
} // namespace esphome::ezsp_proxy_tap
@@ -0,0 +1,54 @@
#pragma once
#include "ash_protocol.h"
#include <cstddef>
#include <cstdint>
namespace esphome::ezsp_proxy_tap {
// Reassembles one direction of the byte stream into unstuffed, CRC-checked frames. Only
// the first ASH_HEADER_SIZE bytes of a frame are kept, as the CRC is checked as bytes
// arrive and acknowledging needs nothing past the header.
class AshFrameScanner {
public:
// Returns true when the byte completes a frame with a valid CRC
bool feed(uint8_t byte);
void reset();
// The first min(length(), ASH_HEADER_SIZE) bytes of the last completed frame's body.
// Valid only until the next feed() call, which begins overwriting them.
const uint8_t *header() const { return this->header_; }
size_t length() const { return this->frame_length_; }
private:
uint8_t header_[ASH_HEADER_SIZE];
size_t index_{0}; // body bytes read so far in the frame being read
size_t frame_length_{0}; // body length of the last completed frame
uint16_t crc_{ASH_CRC_INIT};
bool escaped_{false};
bool discarding_{false};
bool poisoned_{false};
};
// Works out which acknowledgements an NCP is owed while a client has handed them to this
// device. Whether to acknowledge at all is the client's decision, made by putting the port
// in PROTOCOL mode, so this only follows the NCP's frame numbering.
class AshAcknowledger {
public:
// Forgets the NCP's frame numbering. The next RSTACK or DATA frame sets it again.
void reset();
// Feed a byte from the NCP. Returns true when it completes a frame that is owed ack_num().
bool feed(uint8_t byte);
uint8_t ack_num() const { return this->rx_sequence_; }
protected:
bool handle_frame_();
AshFrameScanner scanner_;
uint8_t rx_sequence_{0};
bool synced_{false};
};
} // namespace esphome::ezsp_proxy_tap
@@ -0,0 +1,19 @@
#include "ash_protocol.h"
#include "esphome/core/helpers.h"
namespace esphome::ezsp_proxy_tap {
void ash_build_ack_frame(uint8_t *output, uint8_t ack_num) {
// ACK control byte: 100nrPPP, where PPP is the next frame number expected
const uint8_t control = 0x80 | (ack_num & ASH_MAX_SEQUENCE);
const uint16_t crc = crc16be(&control, 1, ASH_CRC_INIT);
output[0] = ASH_FLAG_BYTE;
output[1] = control;
output[2] = crc >> 8;
output[3] = crc & 0xFF;
output[4] = ASH_FLAG_BYTE;
}
} // namespace esphome::ezsp_proxy_tap
@@ -0,0 +1,39 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::ezsp_proxy_tap {
// ASH Protocol Constants
static constexpr uint8_t ASH_FLAG_BYTE = 0x7E; // Frame delimiter
static constexpr uint8_t ASH_ESCAPE_BYTE = 0x7D; // Escape/substitution byte
static constexpr uint8_t ASH_XOR_BYTE = 0x20; // XOR mask for escaped bytes
static constexpr uint8_t ASH_SUBSTITUTE_BYTE = 0x18; // Substitution for invalid bytes
static constexpr uint8_t ASH_XON_BYTE = 0x11; // Resume transmission
static constexpr uint8_t ASH_XOFF_BYTE = 0x13; // Pause transmission
static constexpr uint8_t ASH_CANCEL_BYTE = 0x1A; // Discards the partial frame before it
// Sent between frames by sleepy NCPs to signal pending callbacks. No control byte is 0xFF.
static constexpr uint8_t ASH_WAKE_BYTE = 0xFF;
static constexpr size_t ASH_MAX_DATA_FIELD_SIZE = 220;
static constexpr size_t MAX_ASH_FRAME_SIZE = 1 + ASH_MAX_DATA_FIELD_SIZE + 2;
// The control byte, then the first Data Field byte (an RSTACK's version)
static constexpr size_t ASH_HEADER_SIZE = 2;
// Protocol limits
static constexpr uint8_t ASH_MAX_SEQUENCE = 7; // 3-bit sequence number (0-7)
// The frame CRC is CRC-CCITT: crc16be() with this initial value, transmitted big-endian
static constexpr uint16_t ASH_CRC_INIT = 0xFFFF;
// FLAG, control byte, two CRC bytes, FLAG. None of the eight ACKs has a byte that needs
// escaping, so the size is fixed.
static constexpr size_t ASH_ACK_FRAME_SIZE = 5;
// Writes the ACK frame for `ack_num` into `output`
void ash_build_ack_frame(uint8_t *output, uint8_t ack_num);
} // namespace esphome::ezsp_proxy_tap
@@ -0,0 +1,38 @@
#include "ezsp_proxy_tap.h"
#ifdef USE_EZSP_PROXY_TAP
#include "esphome/core/log.h"
namespace esphome::ezsp_proxy_tap {
ESPHOME_LOG_TAG(TAG, "ezsp_proxy_tap");
void EzspProxyTap::setup() { this->parent_->set_tap(this); }
void EzspProxyTap::dump_config() { ESP_LOGCONFIG(TAG, "EZSP Proxy Tap:\n Port: %s", this->parent_->get_name()); }
void EzspProxyTap::on_device_rx(const uint8_t *data, size_t len) {
for (size_t i = 0; i < len; i++) {
if (this->acknowledger_.feed(data[i])) {
uint8_t frame[ASH_ACK_FRAME_SIZE];
ash_build_ack_frame(frame, this->acknowledger_.ack_num());
this->parent_->write_from_tap(frame, sizeof(frame));
ESP_LOGV(TAG, "Sent ACK %u", this->acknowledger_.ack_num());
}
}
}
void EzspProxyTap::on_protocol_enabled() {
// Numbering from an earlier session is stale
this->acknowledger_.reset();
}
void EzspProxyTap::on_device_disconnected() {
// The frame numbering ended with the NCP's power; whatever boots next starts over
this->acknowledger_.reset();
}
} // namespace esphome::ezsp_proxy_tap
#endif // USE_EZSP_PROXY_TAP
@@ -0,0 +1,49 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_EZSP_PROXY_TAP
#include "esphome/components/serial_proxy/serial_proxy.h"
#include "esphome/core/component.h"
#include "ash_acknowledger.h"
namespace esphome::ezsp_proxy_tap {
// Acknowledges the ASH frames of an EZSP NCP on behalf of a remote client, so the NCP's
// ack timeout is measured against this device rather than against the network round trip
// to the client. The client suppresses its own acknowledgements, making these the only
// ones the NCP sees.
//
// It never carries client traffic: the serial proxy owns the port and the bytes, and this
// component only observes them. The sole exception is the acknowledgement itself, sent
// only while a client has put the port in PROTOCOL mode, which only an EZSP client does.
class EzspProxyTap : public serial_proxy::SerialProxyTap, public Component {
public:
explicit EzspProxyTap(serial_proxy::SerialProxy *parent) : parent_(parent) {}
void setup() override;
void dump_config() override;
// SerialProxyTap
void on_device_rx(const uint8_t *data, size_t len) override;
// ACKs depend only on what the NCP sends
void on_client_tx(const uint8_t *data, size_t len) override {}
// Acknowledging is only ever useful on a client's behalf, so with nobody subscribed
// there is nothing to do and the port need not be read.
bool tap_needs_port() const override { return false; }
void on_protocol_enabled() override;
// Nothing is acknowledged outside PROTOCOL mode, and entering it starts afresh
void on_protocol_disabled() override {}
void on_device_disconnected() override;
protected:
// The port this component observes. Owns the UART and the bytes; every write we make
// goes through it.
serial_proxy::SerialProxy *parent_;
AshAcknowledger acknowledger_;
};
} // namespace esphome::ezsp_proxy_tap
#endif // USE_EZSP_PROXY_TAP
@@ -277,10 +277,16 @@ SerialProxyResult SerialProxy::set_mode_from_client(api::APIConnection *api_conn
ESP_LOGD(TAG, "Serial proxy [%" PRIu32 "] mode set to %s", this->instance_index_,
mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL ? LOG_STR_LITERAL("PROTOCOL") : LOG_STR_LITERAL("RAW"));
#ifdef USE_SERIAL_PROXY_TAP
const bool entering_protocol_mode =
this->mode_ != api::enums::SERIAL_PROXY_MODE_PROTOCOL && mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL;
const bool leaving_protocol_mode =
this->mode_ != api::enums::SERIAL_PROXY_MODE_RAW && mode == api::enums::SERIAL_PROXY_MODE_RAW;
this->mode_ = mode;
if (entering_protocol_mode && this->tap_ != nullptr) {
this->tap_->on_protocol_enabled();
}
// Only for an explicit client request, not for reset_mode_() at the end of a session:
// an ordinary disconnect says nothing about the device, whereas a client deliberately
// asking for raw bytes usually precedes changing what the device is.
@@ -426,6 +432,14 @@ void SerialProxy::fill_identity_([[maybe_unused]] IdentityScratch &scratch, api:
void SerialProxy::on_usb_connection_changed_(bool connected) {
ESP_LOGD(TAG, "USB device %s serial proxy [%" PRIu32 "]",
connected ? LOG_STR_LITERAL("attached to") : LOG_STR_LITERAL("removed from"), this->instance_index_);
#ifdef USE_SERIAL_PROXY_TAP
// Before telling clients, so a tap never acknowledges a frame from the old device after
// a client has been told it is gone. The subscriber and the mode stay: both belong to
// the client's session, and only the client knows whether that session is over.
if (!connected && this->tap_ != nullptr) {
this->tap_->on_device_disconnected();
}
#endif
#ifdef USE_API
if (api::global_api_server == nullptr) {
return;
@@ -89,11 +89,20 @@ class SerialProxyTap {
/// subscriber holds the port; with one attached, the port mode alone decides.
virtual bool tap_needs_port() const = 0;
/// A client turned protocol handling on for this port. The tap has seen none of the
/// traffic since it was last on, so anything it remembers from then is stale.
virtual void on_protocol_enabled() = 0;
/// A client explicitly turned protocol handling off for this port. Distinct from the
/// automatic reset when a session ends: this one means a client intends to do something
/// else with the device -- reflash it, most likely -- so anything the tap believes about
/// it should be treated as suspect.
virtual void on_protocol_disabled() = 0;
/// The device behind the port went away -- unplugged, or its power was cut. Whatever
/// session the tap had observed ended with it; the device that appears next starts from
/// scratch and may not even be the same one. Only a USB UART can report this.
virtual void on_device_disconnected() = 0;
};
#endif
+1
View File
@@ -73,6 +73,7 @@
#define USE_ESP32_HOSTED_HTTP_UPDATE
#define USE_ESP_NOW_HOSTED
#define USE_EVENT
#define USE_EZSP_PROXY_TAP
#define USE_FAN
#define USE_FEEDBACK_COVER_OVERSHOOT
#define USE_GPIO_BINARY_SENSOR_INTERRUPT
@@ -0,0 +1,185 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <vector>
#include "esphome/components/ezsp_proxy_tap/ash_acknowledger.h"
#include "esphome/components/ezsp_proxy_tap/ash_protocol.h"
#include "esphome/core/helpers.h"
namespace esphome::ezsp_proxy_tap::testing {
namespace {
// Whole frames as they appear on the wire, each ending in its FLAG. RSTACK and ACK(1)
// are the examples in Silicon Labs UG101.
const std::vector<uint8_t> RSTACK = {0xC1, 0x02, 0x02, 0x9B, 0x7B, 0x7E};
const std::vector<uint8_t> DATA_FRAME_0 = {0x00, 0x43, 0x21, 0xA8, 0x50, 0x9B, 0x98, 0x7E};
const std::vector<uint8_t> DATA_FRAME_1 = {0x10, 0x43, 0x21, 0xA8, 0x50, 0x9F, 0xC2, 0x7E};
const std::vector<uint8_t> DATA_FRAME_0_RETRANSMITTED = {0x08, 0x43, 0x21, 0xA8, 0x50, 0x99, 0xB5, 0x7E};
const std::vector<uint8_t> DATA_FRAME_5 = {0x50, 0x43, 0x21, 0xA8, 0x50, 0x8E, 0xAA, 0x7E};
const std::vector<uint8_t> DATA_FRAME_5_RETRANSMITTED = {0x58, 0x43, 0x21, 0xA8, 0x50, 0x8C, 0x87, 0x7E};
const std::vector<uint8_t> DATA_FRAME_6 = {0x60, 0x43, 0x21, 0xA8, 0x50, 0x82, 0x44, 0x7E};
// Data frame 0 with a 2-byte and a 3-byte data field, below and at the minimum
const std::vector<uint8_t> SHORT_DATA_FRAME_0 = {0x00, 0x43, 0x21, 0xA0, 0x40, 0x7E};
const std::vector<uint8_t> MIN_DATA_FRAME_0 = {0x00, 0x43, 0x21, 0xA8, 0xC1, 0x08, 0x7E};
// Body {0x00, 0x7E, 0x7D, 0x11, 0x13, 0x18, 0x1A}: every reserved byte, each escaped
const std::vector<uint8_t> ESCAPED_FRAME = {0x00, 0x7D, 0x5E, 0x7D, 0x5D, 0x7D, 0x31, 0x7D,
0x33, 0x7D, 0x38, 0x7D, 0x3A, 0x46, 0x8C, 0x7E};
const std::vector<uint8_t> RESERVED_BYTES = {ASH_FLAG_BYTE, ASH_ESCAPE_BYTE, ASH_XON_BYTE,
ASH_XOFF_BYTE, ASH_SUBSTITUTE_BYTE, ASH_CANCEL_BYTE};
// Data frame 0 with `data_size` bytes of 0x42 and a valid CRC, none of which needs escaping
std::vector<uint8_t> unstuffed_frame(size_t data_size) {
std::vector<uint8_t> frame(1 + data_size, 0x42);
frame[0] = 0x00;
const uint16_t crc = crc16be(frame.data(), frame.size(), ASH_CRC_INIT);
frame.push_back(crc >> 8);
frame.push_back(crc & 0xFF);
frame.push_back(ASH_FLAG_BYTE);
return frame;
}
std::vector<uint8_t> concat(std::vector<uint8_t> a, const std::vector<uint8_t> &b) {
a.insert(a.end(), b.begin(), b.end());
return a;
}
// Returns whether the last byte completed a valid frame
bool feed(AshFrameScanner &scanner, const std::vector<uint8_t> &bytes) {
bool complete = false;
for (uint8_t byte : bytes)
complete = scanner.feed(byte);
return complete;
}
// Feeds whole frames and returns the ACK owed after the last, or -1 if none is
int feed(AshAcknowledger &acknowledger, const std::vector<uint8_t> &bytes) {
bool owed = false;
for (uint8_t byte : bytes)
owed = acknowledger.feed(byte);
return owed ? acknowledger.ack_num() : -1;
}
} // namespace
TEST(AshProtocol, AckFrameMatchesReference) {
uint8_t frame[ASH_ACK_FRAME_SIZE];
ash_build_ack_frame(frame, 1);
EXPECT_EQ(std::vector<uint8_t>(frame, frame + sizeof(frame)), (std::vector<uint8_t>{0x7E, 0x81, 0x60, 0x59, 0x7E}));
}
TEST(AshProtocol, AckFramesNeedNoEscaping) {
for (uint8_t ack_num = 0; ack_num <= ASH_MAX_SEQUENCE; ack_num++) {
uint8_t frame[ASH_ACK_FRAME_SIZE];
ash_build_ack_frame(frame, ack_num);
EXPECT_EQ(frame[0], ASH_FLAG_BYTE);
EXPECT_EQ(frame[4], ASH_FLAG_BYTE);
for (size_t i = 1; i < 4; i++) {
EXPECT_EQ(std::count(RESERVED_BYTES.begin(), RESERVED_BYTES.end(), frame[i]), 0)
<< "ack_num " << int(ack_num) << " byte " << i;
}
}
}
TEST(AshFrameScanner, UnescapesReservedBytes) {
AshFrameScanner scanner;
// The CRC covers the unescaped body, so a valid frame proves every byte was restored
ASSERT_TRUE(feed(scanner, ESCAPED_FRAME));
EXPECT_EQ(scanner.length(), 7u);
EXPECT_EQ(std::vector<uint8_t>(scanner.header(), scanner.header() + ASH_HEADER_SIZE),
(std::vector<uint8_t>{0x00, 0x7E}));
}
TEST(AshFrameScanner, RejectsBadCrc) {
AshFrameScanner scanner;
std::vector<uint8_t> corrupt = DATA_FRAME_0;
corrupt[corrupt.size() - 2] ^= 0x01;
EXPECT_FALSE(feed(scanner, corrupt));
EXPECT_TRUE(feed(scanner, DATA_FRAME_0));
}
TEST(AshFrameScanner, CancelDiscardsPartialFrame) {
AshFrameScanner scanner;
EXPECT_TRUE(feed(scanner, concat({0x10, 0x43, ASH_CANCEL_BYTE}, DATA_FRAME_0)));
EXPECT_EQ(scanner.length(), 5u);
}
TEST(AshFrameScanner, SubstituteDiscardsUntilFlag) {
AshFrameScanner scanner;
std::vector<uint8_t> substituted = DATA_FRAME_0;
substituted.insert(substituted.begin() + 2, ASH_SUBSTITUTE_BYTE);
EXPECT_FALSE(feed(scanner, substituted));
EXPECT_TRUE(feed(scanner, DATA_FRAME_0));
}
TEST(AshFrameScanner, CancelEndsSubstitute) {
AshFrameScanner scanner;
EXPECT_TRUE(feed(scanner, concat({0x10, ASH_SUBSTITUTE_BYTE, 0x43, ASH_CANCEL_BYTE}, DATA_FRAME_0)));
}
TEST(AshFrameScanner, IgnoresWakeBetweenFrames) {
AshFrameScanner scanner;
EXPECT_TRUE(feed(scanner, concat({ASH_WAKE_BYTE, ASH_FLAG_BYTE, ASH_WAKE_BYTE, ASH_WAKE_BYTE}, DATA_FRAME_0)));
EXPECT_EQ(scanner.length(), 5u);
}
TEST(AshFrameScanner, IgnoresFlowControlInsideFrame) {
AshFrameScanner scanner;
std::vector<uint8_t> interrupted = DATA_FRAME_0;
interrupted.insert(interrupted.begin() + 3, ASH_XOFF_BYTE);
interrupted.insert(interrupted.begin() + 1, ASH_XON_BYTE);
ASSERT_TRUE(feed(scanner, interrupted));
EXPECT_EQ(scanner.length(), 5u);
EXPECT_EQ(std::vector<uint8_t>(scanner.header(), scanner.header() + ASH_HEADER_SIZE),
(std::vector<uint8_t>{0x00, 0x43}));
}
TEST(AshFrameScanner, RejectsOversizedFrame) {
AshFrameScanner scanner;
EXPECT_TRUE(feed(scanner, unstuffed_frame(ASH_MAX_DATA_FIELD_SIZE)));
EXPECT_EQ(scanner.length(), 1 + ASH_MAX_DATA_FIELD_SIZE);
EXPECT_FALSE(feed(scanner, unstuffed_frame(ASH_MAX_DATA_FIELD_SIZE + 1)));
EXPECT_TRUE(feed(scanner, DATA_FRAME_0));
}
TEST(AshAcknowledger, AcknowledgesFramesInSequence) {
AshAcknowledger acknowledger;
EXPECT_EQ(feed(acknowledger, DATA_FRAME_0), 1);
// A repeat not marked as a retransmission is out of sequence
EXPECT_EQ(feed(acknowledger, DATA_FRAME_0), -1);
// A retransmission means the NCP missed our ACK, so it gets the same one again
EXPECT_EQ(feed(acknowledger, DATA_FRAME_0_RETRANSMITTED), 1);
EXPECT_EQ(feed(acknowledger, DATA_FRAME_1), 2);
}
TEST(AshAcknowledger, RstackRestartsNumbering) {
AshAcknowledger acknowledger;
EXPECT_EQ(feed(acknowledger, DATA_FRAME_0), 1);
EXPECT_EQ(feed(acknowledger, RSTACK), -1);
EXPECT_EQ(feed(acknowledger, DATA_FRAME_0), 1);
}
TEST(AshAcknowledger, IgnoresShortDataFrames) {
AshAcknowledger acknowledger;
EXPECT_EQ(feed(acknowledger, SHORT_DATA_FRAME_0), -1);
EXPECT_EQ(feed(acknowledger, MIN_DATA_FRAME_0), 1);
}
TEST(AshAcknowledger, SyncsToFirstFrameMidSession) {
AshAcknowledger acknowledger;
// The NCP is retransmitting a frame nobody acknowledged before the tap was watching
EXPECT_EQ(feed(acknowledger, DATA_FRAME_5_RETRANSMITTED), 6);
EXPECT_EQ(feed(acknowledger, DATA_FRAME_6), 7);
}
TEST(AshAcknowledger, ResetForgetsNumbering) {
AshAcknowledger acknowledger;
EXPECT_EQ(feed(acknowledger, DATA_FRAME_0), 1);
acknowledger.reset();
EXPECT_EQ(feed(acknowledger, DATA_FRAME_5), 6);
}
} // namespace esphome::ezsp_proxy_tap::testing
@@ -0,0 +1,15 @@
wifi:
ssid: MySSID
password: password1
power_save_mode: none
api:
# The port owns the UART and carries every byte; ezsp_proxy_tap only taps it
serial_proxy:
- id: ezsp_serial
name: Zigbee
port_type: TTL
ezsp_proxy_tap:
serial_proxy_id: ezsp_serial
@@ -0,0 +1,27 @@
wifi:
ssid: MySSID
password: password1
power_save_mode: none
api:
usb_host:
usb_uart:
- type: CDC_ACM
vid: 0x303A
pid: 0x831A
channels:
- id: ezsp_usb_channel
baud_rate: 460800
# The tapped port may be a USB CDC ACM channel just as well as a hardware UART:
# ezsp_proxy_tap never touches the UART itself, so it does not care which it is.
serial_proxy:
- id: ezsp_usb_serial
uart_id: ezsp_usb_channel
name: Zigbee
port_type: USB_SERIAL
ezsp_proxy_tap:
serial_proxy_id: ezsp_usb_serial
@@ -0,0 +1,3 @@
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
ezsp_proxy_tap: !include common.yaml
@@ -0,0 +1,3 @@
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
ezsp_proxy_tap: !include common.yaml
@@ -0,0 +1,3 @@
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
ezsp_proxy_tap: !include common.yaml