mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 10:56:43 +00:00
Merge branch 'neutral-ble-client' into radon-eye-single-node
This commit is contained in:
@@ -3,7 +3,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: ${pin}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: ${pin}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# `platform: animation` entry exercising the shared `defaults:`/`files:` expansion.
|
||||
display:
|
||||
- platform: sdl
|
||||
id: animation_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
image:
|
||||
- platform: animation
|
||||
defaults:
|
||||
type: rgb565
|
||||
transparency: opaque
|
||||
resize: 50x50
|
||||
files:
|
||||
- id: platform_defaults_animation
|
||||
file: $component_dir/anim.gif
|
||||
- id: platform_defaults_animation_rgb
|
||||
file: $component_dir/anim.apng
|
||||
type: rgb
|
||||
@@ -1,6 +1,6 @@
|
||||
light:
|
||||
- platform: beken_spi_led_strip
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
pin: P16
|
||||
num_leds: 30
|
||||
chipset: ws2812
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# The deprecated rgb_order / is_rgbw / is_wrgb keys, kept working until 2027.3.0.
|
||||
# Config-only, and only one strip because P16 is the sole supported pin.
|
||||
light:
|
||||
- platform: beken_spi_led_strip
|
||||
name: Legacy RGBW
|
||||
pin: P16
|
||||
num_leds: 30
|
||||
chipset: sk6812
|
||||
rgb_order: GRB
|
||||
is_rgbw: true # -> GRBW
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
@@ -213,4 +214,70 @@ TEST(BufAppendSepStr, Truncation) {
|
||||
EXPECT_EQ(end - buf, 7);
|
||||
}
|
||||
|
||||
// --- base64 encode/decode ---
|
||||
|
||||
static const char BASE64_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
// Pack 6-bit indices 0..63 into 48 bytes so encoding yields the full alphabet in order
|
||||
TEST(Base64, EncodeProducesCanonicalAlphabet) {
|
||||
uint8_t bytes[48];
|
||||
size_t n = 0;
|
||||
for (uint8_t i = 0; i < 64; i += 4) {
|
||||
bytes[n++] = (i << 2) | ((i + 1) >> 4);
|
||||
bytes[n++] = ((i + 1) & 0x0F) << 4 | ((i + 2) >> 2);
|
||||
bytes[n++] = ((i + 2) & 0x03) << 6 | (i + 3);
|
||||
}
|
||||
std::string encoded = base64_encode(bytes, sizeof(bytes)); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(encoded, BASE64_ALPHABET);
|
||||
}
|
||||
|
||||
// Decode the alphabet then re-encode: locks the encode and decode mappings together
|
||||
TEST(Base64, DecodeCanonicalAlphabetRoundTrip) {
|
||||
uint8_t buf[48];
|
||||
size_t len = base64_decode(std::string(BASE64_ALPHABET), buf, sizeof(buf));
|
||||
EXPECT_EQ(len, 48u);
|
||||
std::string reencoded = base64_encode(buf, len); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(reencoded, BASE64_ALPHABET);
|
||||
}
|
||||
|
||||
TEST(Base64, DecodeBase64UrlMatchesStandard) {
|
||||
std::string url = BASE64_ALPHABET;
|
||||
for (char &c : url) {
|
||||
if (c == '+')
|
||||
c = '-';
|
||||
if (c == '/')
|
||||
c = '_';
|
||||
}
|
||||
uint8_t standard[48], urlsafe[48];
|
||||
size_t len_standard = base64_decode(std::string(BASE64_ALPHABET), standard, sizeof(standard));
|
||||
size_t len_url = base64_decode(url, urlsafe, sizeof(urlsafe));
|
||||
EXPECT_EQ(len_standard, len_url);
|
||||
EXPECT_EQ(memcmp(standard, urlsafe, len_standard), 0);
|
||||
}
|
||||
|
||||
// RFC 4648 vectors cover both padding cases (len % 3 == 1 and len % 3 == 2)
|
||||
TEST(Base64, Rfc4648Vectors) {
|
||||
const struct {
|
||||
const char *plain;
|
||||
const char *encoded;
|
||||
} vectors[] = {
|
||||
{"", ""},
|
||||
{"f", "Zg=="},
|
||||
{"fo", "Zm8="},
|
||||
{"foo", "Zm9v"},
|
||||
{"foob", "Zm9vYg=="},
|
||||
{"fooba", "Zm9vYmE="},
|
||||
{"foobar", "Zm9vYmFy"},
|
||||
};
|
||||
for (const auto &v : vectors) {
|
||||
const auto *plain = reinterpret_cast<const uint8_t *>(v.plain);
|
||||
std::string encoded = base64_encode(plain, strlen(v.plain)); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(encoded, v.encoded);
|
||||
uint8_t buf[8];
|
||||
size_t len = base64_decode(reinterpret_cast<const uint8_t *>(v.encoded), strlen(v.encoded), buf, sizeof(buf));
|
||||
EXPECT_EQ(len, strlen(v.plain));
|
||||
EXPECT_EQ(memcmp(buf, v.plain, len), 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
|
||||
@@ -5,7 +5,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: ${pin}
|
||||
effects:
|
||||
|
||||
@@ -5,7 +5,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: ${pin}
|
||||
effects:
|
||||
|
||||
@@ -6,7 +6,7 @@ light:
|
||||
pin: 2
|
||||
pio: 0
|
||||
num_leds: 256
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
chipset: WS2812
|
||||
effects:
|
||||
- e131:
|
||||
|
||||
@@ -3,13 +3,13 @@ light:
|
||||
id: led_strip1
|
||||
pin: ${pin1}
|
||||
num_leds: 60
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
chipset: ws2812
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: led_strip2
|
||||
pin: ${pin2}
|
||||
num_leds: 60
|
||||
rgbw_order: RWGB
|
||||
channel_colors: RWGB
|
||||
bit0_high: 100us
|
||||
bit0_low: 100us
|
||||
bit1_high: 100us
|
||||
|
||||
@@ -8,14 +8,14 @@ light:
|
||||
id: led_strip1
|
||||
pin: ${pin1}
|
||||
num_leds: 60
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
chipset: ws2812
|
||||
use_dma: "true"
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: led_strip2
|
||||
pin: ${pin2}
|
||||
num_leds: 60
|
||||
rgb_order: RGB
|
||||
channel_colors: RGB
|
||||
bit0_high: 100us
|
||||
bit0_low: 100us
|
||||
bit1_high: 100us
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# The deprecated rgb_order / is_rgbw / is_wrgb keys, kept working until 2027.3.0.
|
||||
# Config-only: each strip below must migrate to the channel_colors shown in the comment.
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: legacy_rgb
|
||||
pin: GPIO13
|
||||
num_leds: 60
|
||||
chipset: ws2812
|
||||
rgb_order: GRB # -> GRB
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: legacy_rgbw
|
||||
pin: GPIO14
|
||||
num_leds: 60
|
||||
chipset: sk6812
|
||||
rgb_order: GRB
|
||||
is_rgbw: true # -> GRBW
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: legacy_wrgb
|
||||
pin: GPIO15
|
||||
num_leds: 60
|
||||
chipset: sk6812
|
||||
rgb_order: GRB
|
||||
is_wrgb: true # -> WGRB
|
||||
@@ -0,0 +1,24 @@
|
||||
# `platform: file` entry using the `defaults:`/`files:` shape, including the
|
||||
# per-type byte_order drop when an entry overrides to a non-endian type.
|
||||
display:
|
||||
- platform: sdl
|
||||
id: image_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
defaults:
|
||||
type: rgb565
|
||||
transparency: opaque
|
||||
byte_order: little_endian
|
||||
resize: 50x50
|
||||
dither: FloydSteinberg
|
||||
files:
|
||||
- id: platform_defaults_image
|
||||
file: ../../pnglogo.png
|
||||
- id: platform_defaults_binary
|
||||
file: ../../pnglogo.png
|
||||
type: binary
|
||||
@@ -1,7 +1,10 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::modbus::testing {
|
||||
|
||||
@@ -30,4 +33,37 @@ class RecordingUART : public NullUART {
|
||||
std::vector<uint8_t> written;
|
||||
};
|
||||
|
||||
// A UART the test can inject received bytes into, so frames travel the full receive path
|
||||
// (receive_modbus_frames -> parse -> dispatch) through hub.loop(). Writes are recorded.
|
||||
class InjectableUART : public RecordingUART {
|
||||
public:
|
||||
bool peek_byte(uint8_t *data) override {
|
||||
if (this->rx_.empty())
|
||||
return false;
|
||||
*data = this->rx_.front();
|
||||
return true;
|
||||
}
|
||||
bool read_array(uint8_t *data, size_t len) override {
|
||||
if (len > this->rx_.size())
|
||||
return false;
|
||||
memcpy(data, this->rx_.data(), len);
|
||||
this->rx_.erase(this->rx_.begin(), this->rx_.begin() + len);
|
||||
return true;
|
||||
}
|
||||
size_t available() override { return this->rx_.size(); }
|
||||
|
||||
// Queues a complete wire frame: address + PDU + CRC16 (low byte first).
|
||||
void inject_frame(uint8_t address, std::span<const uint8_t> pdu) {
|
||||
size_t start = this->rx_.size();
|
||||
this->rx_.push_back(address);
|
||||
this->rx_.insert(this->rx_.end(), pdu.begin(), pdu.end());
|
||||
uint16_t crc = crc16(this->rx_.data() + start, this->rx_.size() - start);
|
||||
this->rx_.push_back(crc & 0xFF);
|
||||
this->rx_.push_back(crc >> 8);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> rx_;
|
||||
};
|
||||
|
||||
} // namespace esphome::modbus::testing
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "common.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
namespace esphome::modbus::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// Records custom-response dispatches so tests can assert an unknown-length frame reached the device.
|
||||
class CustomRecordingDevice : public ModbusClientDevice {
|
||||
public:
|
||||
using ModbusClientDevice::ModbusClientDevice;
|
||||
void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
|
||||
ResponseStatus status) override {
|
||||
this->requests.emplace_back(request_pdu.begin(), request_pdu.end());
|
||||
this->responses.emplace_back(response_pdu.begin(), response_pdu.end());
|
||||
this->statuses.push_back(status);
|
||||
}
|
||||
std::vector<std::vector<uint8_t>> requests;
|
||||
std::vector<std::vector<uint8_t>> responses;
|
||||
std::vector<ResponseStatus> statuses;
|
||||
};
|
||||
|
||||
// Every handler keeps its ILLEGAL_FUNCTION default; the hub's dispatch is what is under test.
|
||||
class SilentServerDevice : public ModbusServerDevice {};
|
||||
|
||||
// Drives full client frames through the server hub's receive path (same shape as the broadcast tests).
|
||||
class TestServerHub : public ModbusServerHub {
|
||||
public:
|
||||
bool tx_blocked() override { return false; }
|
||||
|
||||
// Builds a complete client frame (address + FC + data + CRC) and runs the full receive-side parser.
|
||||
// Returns true once the buffer has fully drained.
|
||||
bool run_receive_parser_for_test(uint8_t address, uint8_t function_code, std::span<const uint8_t> data) {
|
||||
this->rx_buffer_.clear();
|
||||
this->rx_buffer_.reserve(data.size() + 4);
|
||||
this->rx_buffer_.push_back(address);
|
||||
this->rx_buffer_.push_back(function_code);
|
||||
this->rx_buffer_.insert(this->rx_buffer_.end(), data.begin(), data.end());
|
||||
uint16_t crc = crc16(this->rx_buffer_.data(), this->rx_buffer_.size());
|
||||
this->rx_buffer_.push_back(crc & 0xFF);
|
||||
this->rx_buffer_.push_back(crc >> 8);
|
||||
this->parse_modbus_frames();
|
||||
return this->rx_buffer_.empty();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The frame-length parsers have explicit cases for exactly these 13 codes; every other value - the
|
||||
// assigned-but-unimplemented management codes, both user-defined ranges, and all unassigned codes -
|
||||
// must classify as unknown length. The exception flag masks off first.
|
||||
TEST(ModbusUnknownFunction, HelperMatchesParserCoverage) {
|
||||
for (uint8_t fc : {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0F, 0x10, 0x14, 0x15, 0x16, 0x17, 0x18}) {
|
||||
EXPECT_FALSE(helpers::is_function_code_unknown_length(fc)) << "fc 0x" << std::hex << int(fc);
|
||||
}
|
||||
for (uint8_t fc : {0x07, 0x08, 0x0B, 0x0C, 0x11, 0x2A, 0x41, 0x48, 0x49, 0x64, 0x6E, 0x00, 0x7F}) {
|
||||
EXPECT_TRUE(helpers::is_function_code_unknown_length(fc)) << "fc 0x" << std::hex << int(fc);
|
||||
}
|
||||
// Exception replies classify by their base code.
|
||||
EXPECT_FALSE(helpers::is_function_code_unknown_length(0x83));
|
||||
EXPECT_TRUE(helpers::is_function_code_unknown_length(0x87));
|
||||
// Strictly wider than the user-defined ranges: every custom code is unknown-length, but not vice versa.
|
||||
for (int fc = 0; fc <= 0xFF; fc++) {
|
||||
if (helpers::is_function_code_custom(fc))
|
||||
EXPECT_TRUE(helpers::is_function_code_unknown_length(fc)) << "fc 0x" << std::hex << fc;
|
||||
}
|
||||
EXPECT_FALSE(helpers::is_function_code_custom(0x49));
|
||||
|
||||
// Derived contract check: the helper must say "unknown" exactly when both length parsers fall
|
||||
// through to default. With a zero-filled max-size PDU every explicit case returns at least 2
|
||||
// (file records bottom out at 2, FIFO at 3) and only default returns MIN_PDU_SIZE, so comparing
|
||||
// against MIN_PDU_SIZE detects a case added to either switch without updating the helper. The
|
||||
// loop stops at 0x7F: above it the helper masks the exception flag off while client_pdu_length()
|
||||
// switches on the unmasked byte and server_pdu_length() early-returns the exception length.
|
||||
for (int fc = 0; fc <= 0x7F; fc++) {
|
||||
const uint8_t pdu[MAX_PDU_SIZE] = {static_cast<uint8_t>(fc)}; // zero header fields
|
||||
EXPECT_EQ(helpers::is_function_code_unknown_length(fc),
|
||||
helpers::client_pdu_length(pdu, sizeof(pdu)) == MIN_PDU_SIZE)
|
||||
<< "client_pdu_length disagrees for fc 0x" << std::hex << fc;
|
||||
EXPECT_EQ(helpers::is_function_code_unknown_length(fc),
|
||||
helpers::server_pdu_length(pdu, sizeof(pdu)) == MIN_PDU_SIZE)
|
||||
<< "server_pdu_length disagrees for fc 0x" << std::hex << fc;
|
||||
}
|
||||
}
|
||||
|
||||
// A response with a function code outside the user-defined ranges (0x49) has no length case in
|
||||
// server_pdu_length(), so the parser must find the frame end by CRC scan - the same way it already
|
||||
// handles user-defined codes. Frame: address + FC 0x49 + 3 data bytes + CRC = 7 bytes. Without the
|
||||
// scan the parser assumes a 4-byte frame, fails the CRC, and the response never reaches the device.
|
||||
TEST(ModbusUnknownFunction, ClientParsesUnknownLengthResponse) {
|
||||
InjectableUART uart;
|
||||
ModbusClientHub hub;
|
||||
hub.set_uart_parent(&uart);
|
||||
hub.setup(); // computes frame timing from the baud rate
|
||||
CustomRecordingDevice device(&hub, 0x02);
|
||||
|
||||
const uint8_t request[] = {0x49, 0x01};
|
||||
ASSERT_TRUE(device.queue_pdu(request));
|
||||
hub.loop(); // transmit
|
||||
ASSERT_FALSE(uart.written.empty());
|
||||
|
||||
const uint8_t response_pdu[] = {0x49, 0x02, 0xAA, 0xBB};
|
||||
uart.inject_frame(0x02, response_pdu);
|
||||
hub.loop(); // receive + parse + match + dispatch
|
||||
|
||||
ASSERT_EQ(device.responses.size(), 1u);
|
||||
EXPECT_EQ(device.requests[0], std::vector<uint8_t>(request, request + sizeof(request)));
|
||||
EXPECT_EQ(device.responses[0], std::vector<uint8_t>(response_pdu, response_pdu + sizeof(response_pdu)));
|
||||
EXPECT_FALSE(device.statuses[0].has_value());
|
||||
}
|
||||
|
||||
// The server side of the same gap: a request with FC 0x49 for a registered device must parse (CRC
|
||||
// scan again) so the hub can answer ILLEGAL_FUNCTION per the spec. Without the scan the frame fails
|
||||
// to parse and the client gets silence instead of the exception.
|
||||
TEST(ModbusUnknownFunction, ServerRepliesIllegalFunctionToUnknownLengthRequest) {
|
||||
TestServerHub hub;
|
||||
RecordingUART uart;
|
||||
hub.set_uart_parent(&uart);
|
||||
|
||||
SilentServerDevice device;
|
||||
device.set_address(0x02);
|
||||
hub.register_device(&device);
|
||||
|
||||
const uint8_t data[] = {0x02, 0xAA, 0xBB};
|
||||
ASSERT_TRUE(hub.run_receive_parser_for_test(0x02, 0x49, data));
|
||||
|
||||
// Expected reply: address + FC with exception flag + ILLEGAL_FUNCTION + CRC.
|
||||
std::vector<uint8_t> expected = {0x02, 0xC9, 0x01};
|
||||
uint16_t crc = crc16(expected.data(), expected.size());
|
||||
expected.push_back(crc & 0xFF);
|
||||
expected.push_back(crc >> 8);
|
||||
EXPECT_EQ(uart.written, expected);
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus::testing
|
||||
@@ -4,7 +4,7 @@ light:
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
num_leds: 256
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
pin: ${pin}
|
||||
- platform: partition
|
||||
name: Partition Light
|
||||
|
||||
@@ -4,7 +4,7 @@ light:
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
num_leds: 256
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
pin: ${pin}
|
||||
- platform: partition
|
||||
name: Partition Light
|
||||
|
||||
@@ -4,14 +4,14 @@ light:
|
||||
pin: 4
|
||||
num_leds: 60
|
||||
pio: 0
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
chipset: WS2812
|
||||
- platform: rp2040_pio_led_strip
|
||||
id: led_strip_custom_timings
|
||||
pin: 5
|
||||
num_leds: 60
|
||||
pio: 1
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
bit0_high: .1us
|
||||
bit0_low: 1.2us
|
||||
bit1_high: .69us
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# The deprecated rgb_order / is_rgbw keys, kept working until 2027.3.0.
|
||||
# Config-only: each strip below must migrate to the channel_colors shown in the comment.
|
||||
light:
|
||||
- platform: rp2040_pio_led_strip
|
||||
id: legacy_rgb
|
||||
pin: 4
|
||||
num_leds: 60
|
||||
pio: 0
|
||||
chipset: WS2812
|
||||
rgb_order: GRB # -> GRB
|
||||
- platform: rp2040_pio_led_strip
|
||||
id: legacy_rgbw
|
||||
pin: 5
|
||||
num_leds: 60
|
||||
pio: 1
|
||||
chipset: SK6812
|
||||
rgb_order: GRB
|
||||
is_rgbw: true # -> GRBW
|
||||
@@ -9,7 +9,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: 2
|
||||
effects:
|
||||
|
||||
Reference in New Issue
Block a user