#include #include #include #include "common.h" namespace esphome::systa_bus::testing { namespace { class TestListener final : public SystaBusListener { public: void handle_message(std::span message) override { this->messages.emplace_back(message.begin(), message.end()); } std::vector> messages; }; void put_i16be(std::vector &frame, size_t offset, int16_t value) { frame[offset] = static_cast(static_cast(value) >> 8); frame[offset + 1] = static_cast(value); } // A 25-byte Aqua sensor frame with a valid checksum std::vector aqua_frame(int16_t tsa, int16_t tse, int16_t twu, int16_t tw2, uint8_t pump) { std::vector frame(MAX_MESSAGE_SIZE, 0); frame[0] = START_BYTE; frame[1] = MESSAGE_TYPE_AQUA_SENSOR_DATA & 0xff; put_i16be(frame, 4, tsa); put_i16be(frame, 6, tse); put_i16be(frame, 8, twu); put_i16be(frame, 10, tw2); frame[12] = pump; uint8_t sum = 0; for (size_t i = 0; i + 1 < frame.size(); i++) sum += frame[i]; frame.back() = static_cast(0 - sum); return frame; } class SystaBusTest : public ::testing::Test { protected: void SetUp() override { this->bus_.set_uart_parent(&this->uart_); this->bus_.register_listener(&this->listener_); } void feed_(const std::vector &bytes) { this->uart_.push_rx(bytes); this->bus_.loop(); } MockUARTComponent uart_; SystaBus bus_; TestListener listener_; }; const std::vector FRAME_A = aqua_frame(512, 301, -15, 700, 60); const std::vector FRAME_B = aqua_frame(513, 302, -14, 701, 61); } // namespace TEST_F(SystaBusTest, DecodesAValidFrame) { this->feed_(FRAME_A); ASSERT_EQ(this->listener_.messages.size(), 1u); EXPECT_EQ(this->listener_.messages[0], FRAME_A); } TEST_F(SystaBusTest, IgnoresGarbageBeforeAFrame) { std::vector bytes = {0x00, 0x16, 0x42, 0xfc}; // includes a stray start byte bytes.insert(bytes.end(), FRAME_A.begin(), FRAME_A.end()); this->feed_(bytes); ASSERT_EQ(this->listener_.messages.size(), 1u); EXPECT_EQ(this->listener_.messages[0], FRAME_A); } TEST_F(SystaBusTest, SkipsAnUnknownMessageType) { std::vector bytes = {START_BYTE, 0x1a, 0x01, 0x02, 0x03}; bytes.insert(bytes.end(), FRAME_A.begin(), FRAME_A.end()); this->feed_(bytes); ASSERT_EQ(this->listener_.messages.size(), 1u); EXPECT_EQ(this->listener_.messages[0], FRAME_A); } TEST_F(SystaBusTest, RejectsACorruptedByte) { auto bad = FRAME_A; bad[6] ^= 0x10; std::vector bytes = bad; bytes.insert(bytes.end(), FRAME_B.begin(), FRAME_B.end()); this->feed_(bytes); ASSERT_EQ(this->listener_.messages.size(), 1u); EXPECT_EQ(this->listener_.messages[0], FRAME_B); } // A frame that lost a byte swallows the start byte of the next frame; the parser must still find that frame. TEST_F(SystaBusTest, RecoversTheFrameAfterADroppedByte) { auto truncated = FRAME_A; truncated.erase(truncated.begin() + 9); std::vector bytes = truncated; bytes.insert(bytes.end(), FRAME_B.begin(), FRAME_B.end()); this->feed_(bytes); ASSERT_EQ(this->listener_.messages.size(), 1u); EXPECT_EQ(this->listener_.messages[0], FRAME_B); } TEST_F(SystaBusTest, DecodesFramesSplitAcrossLoops) { std::vector first(FRAME_A.begin(), FRAME_A.begin() + 10); std::vector second(FRAME_A.begin() + 10, FRAME_A.end()); this->feed_(first); EXPECT_TRUE(this->listener_.messages.empty()); this->feed_(second); ASSERT_EQ(this->listener_.messages.size(), 1u); EXPECT_EQ(this->listener_.messages[0], FRAME_A); } TEST_F(SystaBusTest, DecodesBackToBackFrames) { std::vector bytes = FRAME_A; bytes.insert(bytes.end(), FRAME_B.begin(), FRAME_B.end()); this->feed_(bytes); ASSERT_EQ(this->listener_.messages.size(), 2u); EXPECT_EQ(this->listener_.messages[0], FRAME_A); EXPECT_EQ(this->listener_.messages[1], FRAME_B); } } // namespace esphome::systa_bus::testing