You've already forked M5Unit-RF433
mirror of
https://github.com/m5stack/M5Unit-RF433.git
synced 2026-05-20 11:41:00 -07:00
469 lines
14 KiB
C++
469 lines
14 KiB
C++
/*
|
|
* SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
/*
|
|
Common tests for RF433 encode/decode and CRC (native/embedded)
|
|
*/
|
|
#include <gtest/gtest.h>
|
|
#include <unit/rmt_item_types.hpp>
|
|
#include <unit/codec/m5_codec.hpp>
|
|
#include <m5_utility/crc.hpp>
|
|
#include <cstring>
|
|
|
|
using namespace m5::unit::rf433;
|
|
using namespace m5::unit::gpio;
|
|
|
|
// ============================================================
|
|
// Manchester Encode
|
|
// ============================================================
|
|
|
|
TEST(ManchesterEncode, SingleByte0xFF)
|
|
{
|
|
uint8_t data = 0xFF;
|
|
auto result = encodeManchester(&data, 1);
|
|
EXPECT_EQ(result.size(), 8u);
|
|
for (const auto& item : result) {
|
|
EXPECT_EQ(item.duration0, 800u);
|
|
EXPECT_EQ(item.duration1, 200u);
|
|
}
|
|
}
|
|
|
|
TEST(ManchesterEncode, SingleByte0x00)
|
|
{
|
|
uint8_t data = 0x00;
|
|
auto result = encodeManchester(&data, 1);
|
|
EXPECT_EQ(result.size(), 8u);
|
|
for (const auto& item : result) {
|
|
EXPECT_EQ(item.duration0, 200u);
|
|
EXPECT_EQ(item.duration1, 800u);
|
|
}
|
|
}
|
|
|
|
TEST(ManchesterEncode, MultiByte)
|
|
{
|
|
uint8_t data[] = {0xAB, 0xCD};
|
|
auto result = encodeManchester(data, 2);
|
|
EXPECT_EQ(result.size(), 16u);
|
|
}
|
|
|
|
TEST(ManchesterEncode, EmptyInput)
|
|
{
|
|
auto result = encodeManchester(nullptr, 0);
|
|
EXPECT_TRUE(result.empty());
|
|
}
|
|
|
|
TEST(ManchesterEncode, MSBOrder)
|
|
{
|
|
// 0x80 = 10000000 MSB first: bit7=1 then 7 zeros
|
|
uint8_t data = 0x80;
|
|
auto result = encodeManchester(&data, 1, true);
|
|
EXPECT_EQ(result.size(), 8u);
|
|
// First item should be '1' (800, 200)
|
|
EXPECT_EQ(result[0].duration0, 800u);
|
|
EXPECT_EQ(result[0].duration1, 200u);
|
|
// Rest should be '0' (200, 800)
|
|
for (size_t i = 1; i < 8; ++i) {
|
|
EXPECT_EQ(result[i].duration0, 200u);
|
|
EXPECT_EQ(result[i].duration1, 800u);
|
|
}
|
|
}
|
|
|
|
TEST(ManchesterEncode, LSBOrder)
|
|
{
|
|
// 0x80 = 10000000 LSB first: bit0=0 first, bit7=1 last
|
|
uint8_t data = 0x80;
|
|
auto result = encodeManchester(&data, 1, false);
|
|
EXPECT_EQ(result.size(), 8u);
|
|
// First 7 items should be '0' (200, 800)
|
|
for (size_t i = 0; i < 7; ++i) {
|
|
EXPECT_EQ(result[i].duration0, 200u);
|
|
EXPECT_EQ(result[i].duration1, 800u);
|
|
}
|
|
// Last item should be '1' (800, 200)
|
|
EXPECT_EQ(result[7].duration0, 800u);
|
|
EXPECT_EQ(result[7].duration1, 200u);
|
|
}
|
|
|
|
TEST(ManchesterEncode, Deterministic)
|
|
{
|
|
uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF};
|
|
auto r1 = encodeManchester(data, sizeof(data));
|
|
auto r2 = encodeManchester(data, sizeof(data));
|
|
EXPECT_EQ(r1.size(), r2.size());
|
|
EXPECT_EQ(std::memcmp(r1.data(), r2.data(), r1.size() * sizeof(m5_rmt_item_t)), 0);
|
|
}
|
|
|
|
TEST(ManchesterEncode, MaxPayload)
|
|
{
|
|
uint8_t data[MaxPayloadSize]{};
|
|
auto result = encodeManchester(data, MaxPayloadSize);
|
|
EXPECT_EQ(result.size(), (size_t)MaxPayloadSize * 8);
|
|
}
|
|
|
|
// ============================================================
|
|
// CRC8
|
|
// ============================================================
|
|
|
|
TEST(CRC8, Deterministic)
|
|
{
|
|
m5::utility::CRC8_Checksum crc1, crc2;
|
|
uint8_t data[] = {0x48, 0x65, 0x6C, 0x6C, 0x6F};
|
|
crc1.update(data, sizeof(data));
|
|
crc2.update(data, sizeof(data));
|
|
EXPECT_EQ(crc1.value(), crc2.value());
|
|
}
|
|
|
|
TEST(CRC8, StreamingEquivalent)
|
|
{
|
|
m5::utility::CRC8_Checksum crc1, crc2;
|
|
uint8_t data[] = {0x01, 0x02, 0x03, 0x04};
|
|
crc1.update(data, 4);
|
|
crc2.update(data, 2);
|
|
crc2.update(data + 2, 2);
|
|
EXPECT_EQ(crc1.value(), crc2.value());
|
|
}
|
|
|
|
TEST(CRC8, ClearResets)
|
|
{
|
|
m5::utility::CRC8_Checksum crc;
|
|
uint8_t data[] = {0x42};
|
|
crc.update(data, 1);
|
|
uint8_t v1 = crc.value();
|
|
crc.clear();
|
|
crc.update(data, 1);
|
|
EXPECT_EQ(crc.value(), v1);
|
|
}
|
|
|
|
TEST(CRC8, DifferentDataDifferentCRC)
|
|
{
|
|
m5::utility::CRC8_Checksum crc1, crc2;
|
|
uint8_t d1[] = {0x00};
|
|
uint8_t d2[] = {0x01};
|
|
crc1.update(d1, 1);
|
|
crc2.update(d2, 1);
|
|
EXPECT_NE(crc1.value(), crc2.value());
|
|
}
|
|
|
|
// ============================================================
|
|
// Protocol Constants
|
|
// ============================================================
|
|
|
|
TEST(Protocol, Overhead)
|
|
{
|
|
// CRC8(1) + ID(1) + Count(1) + Length(1) = 4
|
|
EXPECT_EQ(ProtocolOverhead, 4);
|
|
}
|
|
|
|
// ============================================================
|
|
// calculateRingBufferSize
|
|
// ============================================================
|
|
|
|
TEST(RingBufferSize, SmallPayload)
|
|
{
|
|
// 1 byte payload
|
|
// SOF(1) + (overhead(4)+payload(1))*8 = 1+40 = 41 items
|
|
// 41 * 4 + 2 = 166
|
|
EXPECT_EQ(calculateRingBufferSize(1), 166u);
|
|
}
|
|
|
|
TEST(RingBufferSize, TenBytes)
|
|
{
|
|
// SOF(1) + (4+10)*8 = 1+112 = 113 items
|
|
// 113 * 4 + 2 = 454
|
|
EXPECT_EQ(calculateRingBufferSize(10), 454u);
|
|
}
|
|
|
|
TEST(RingBufferSize, ZeroPayload)
|
|
{
|
|
// SOF(1) + (4+0)*8 = 1+32 = 33 items
|
|
// 33 * 4 + 2 = 134
|
|
EXPECT_EQ(calculateRingBufferSize(0), 134u);
|
|
}
|
|
|
|
TEST(PayloadLimit, MaxPayloadSize)
|
|
{
|
|
// MaxPayloadSize must be > 0 and <= 255
|
|
EXPECT_GT(MaxPayloadSize, 0);
|
|
EXPECT_LE(MaxPayloadSize, 255);
|
|
// Ring buffer for max payload must fit in uint16_t
|
|
EXPECT_LE(calculateRingBufferSize(MaxPayloadSize), 0xFFFFu);
|
|
}
|
|
|
|
// ============================================================
|
|
// Manchester Decode
|
|
// ============================================================
|
|
// decodeManchester reads bit boundaries BETWEEN consecutive RMT items:
|
|
// d.duration0 = items[i-1].duration1 (low duration of previous item)
|
|
// d.duration1 = items[i].duration0 (high duration of current item)
|
|
// bit = (d.duration0 > d.duration1) ? 1 : 0
|
|
// total must be in (600, 1100)
|
|
//
|
|
// To represent a '1' bit: prev.duration1=800, curr.duration0=200 (800>200)
|
|
// To represent a '0' bit: prev.duration1=200, curr.duration0=800 (200<800)
|
|
|
|
// Helper: build RMT items that decode to the given byte (MSB order)
|
|
static std::vector<m5_rmt_item_t> buildDecodeItems(const uint8_t* data, size_t len)
|
|
{
|
|
std::vector<m5_rmt_item_t> items;
|
|
// First item: duration0 is irrelevant, duration1 sets up the first bit boundary
|
|
bool first_bit = (data[0] >> 7) & 1;
|
|
m5_rmt_item_t first{};
|
|
first.level0 = 1;
|
|
first.level1 = 0;
|
|
first.duration0 = 500; // arbitrary
|
|
first.duration1 = first_bit ? 800 : 200; // sets up first bit's d.duration0
|
|
items.push_back(first);
|
|
|
|
for (size_t b = 0; b < len; ++b) {
|
|
for (int bit = 0; bit < 8; ++bit) {
|
|
bool is_one = (data[b] >> (7 - bit)) & 1;
|
|
bool next_is_one = false;
|
|
// Determine the next bit for setting duration1
|
|
int next_bit_idx = bit + 1;
|
|
if (next_bit_idx < 8) {
|
|
next_is_one = (data[b] >> (7 - next_bit_idx)) & 1;
|
|
} else if (b + 1 < len) {
|
|
next_is_one = (data[b + 1] >> 7) & 1;
|
|
}
|
|
|
|
m5_rmt_item_t item{};
|
|
item.level0 = 1;
|
|
item.level1 = 0;
|
|
item.duration0 = is_one ? 200 : 800;
|
|
// Last bit of last byte: duration1 doesn't matter
|
|
bool is_last = (b == len - 1 && bit == 7);
|
|
item.duration1 = is_last ? 500 : (next_is_one ? 800 : 200);
|
|
items.push_back(item);
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
TEST(ManchesterDecode, NullBuffer)
|
|
{
|
|
m5_rmt_item_t items[2]{};
|
|
EXPECT_EQ(decodeManchester(nullptr, 10, items, 2), 0);
|
|
}
|
|
|
|
TEST(ManchesterDecode, ZeroSize)
|
|
{
|
|
m5_rmt_item_t items[2]{};
|
|
uint8_t buf[1]{};
|
|
EXPECT_EQ(decodeManchester(buf, 0, items, 2), 0);
|
|
}
|
|
|
|
TEST(ManchesterDecode, NullItems)
|
|
{
|
|
uint8_t buf[1]{};
|
|
EXPECT_EQ(decodeManchester(buf, 1, nullptr, 2), 0);
|
|
}
|
|
|
|
TEST(ManchesterDecode, TooFewItems)
|
|
{
|
|
m5_rmt_item_t items[1]{};
|
|
uint8_t buf[1]{};
|
|
EXPECT_EQ(decodeManchester(buf, 1, items, 1), 0);
|
|
}
|
|
|
|
TEST(ManchesterDecode, SingleByte0xFF)
|
|
{
|
|
uint8_t input = 0xFF;
|
|
auto items = buildDecodeItems(&input, 1);
|
|
EXPECT_EQ(items.size(), 9u); // 1 setup + 8 data
|
|
|
|
uint8_t buf[4]{};
|
|
uint16_t decoded = decodeManchester(buf, sizeof(buf), items.data(), items.size());
|
|
EXPECT_EQ(decoded, 1u);
|
|
EXPECT_EQ(buf[0], 0xFF);
|
|
}
|
|
|
|
TEST(ManchesterDecode, SingleByte0x00)
|
|
{
|
|
uint8_t input = 0x00;
|
|
auto items = buildDecodeItems(&input, 1);
|
|
EXPECT_EQ(items.size(), 9u);
|
|
|
|
uint8_t buf[4]{};
|
|
uint16_t decoded = decodeManchester(buf, sizeof(buf), items.data(), items.size());
|
|
EXPECT_EQ(decoded, 1u);
|
|
EXPECT_EQ(buf[0], 0x00);
|
|
}
|
|
|
|
TEST(ManchesterDecode, SingleByte0xA5)
|
|
{
|
|
uint8_t input = 0xA5; // 10100101
|
|
auto items = buildDecodeItems(&input, 1);
|
|
|
|
uint8_t buf[4]{};
|
|
uint16_t decoded = decodeManchester(buf, sizeof(buf), items.data(), items.size());
|
|
EXPECT_EQ(decoded, 1u);
|
|
EXPECT_EQ(buf[0], 0xA5);
|
|
}
|
|
|
|
TEST(ManchesterDecode, MultiByte)
|
|
{
|
|
uint8_t input[] = {0xDE, 0xAD};
|
|
auto items = buildDecodeItems(input, 2);
|
|
EXPECT_EQ(items.size(), 17u); // 1 setup + 16 data
|
|
|
|
uint8_t buf[4]{};
|
|
uint16_t decoded = decodeManchester(buf, sizeof(buf), items.data(), items.size());
|
|
EXPECT_EQ(decoded, 2u);
|
|
EXPECT_EQ(buf[0], 0xDE);
|
|
EXPECT_EQ(buf[1], 0xAD);
|
|
}
|
|
|
|
TEST(ManchesterDecode, BufferTooSmall)
|
|
{
|
|
// 2-byte input but only 1-byte output buffer (plus guard for partial)
|
|
// buf_size is checked as idx < buf_size - 1, so buf_size=2 allows idx 0 only
|
|
uint8_t input[] = {0xAB, 0xCD};
|
|
auto items = buildDecodeItems(input, 2);
|
|
|
|
uint8_t buf[2]{};
|
|
uint16_t decoded = decodeManchester(buf, 2, items.data(), items.size());
|
|
// Should decode at most 1 full byte (idx < buf_size-1 = 1)
|
|
EXPECT_EQ(decoded, 1u);
|
|
EXPECT_EQ(buf[0], 0xAB);
|
|
}
|
|
|
|
TEST(ManchesterDecode, InvalidTiming)
|
|
{
|
|
// Items with total duration outside valid range should cause early exit
|
|
m5_rmt_item_t items[3] = {
|
|
{{500, 1, 100, 0}}, // setup: duration1=100
|
|
{{100, 1, 500, 0}}, // d0=100, d1=100, total=200 < 600 → invalid
|
|
{{500, 1, 500, 0}}, // should not be reached
|
|
};
|
|
|
|
uint8_t buf[4]{};
|
|
uint16_t decoded = decodeManchester(buf, sizeof(buf), items, 3);
|
|
EXPECT_EQ(decoded, 0u);
|
|
}
|
|
|
|
// ============================================================
|
|
// M5Codec Encode Structure
|
|
// ============================================================
|
|
// Note: encode() produces TX-side RMT items and decode() expects RX-side RMT items.
|
|
// The RF channel (SYN115 -> 433MHz -> SYN531R -> ESP32 RMT) transforms signal timing
|
|
// and item boundaries, so direct encode->decode round-trip is not possible without hardware.
|
|
// These tests verify the encode output structure independently.
|
|
|
|
// Frame layout:
|
|
// preamble(10) + SOF(2) + CRC8(8) + ID(8) + Count(8) + Len(8) + Payload(8*n) + EOF(1)
|
|
// Total items = 45 + 8*n
|
|
|
|
TEST(M5CodecEncode, FrameStructure_SingleByte)
|
|
{
|
|
M5Codec codec;
|
|
uint8_t payload[] = {0xAB};
|
|
auto items = codec.encode(payload, sizeof(payload));
|
|
|
|
// 10 + 2 + 8 + 8 + 8 + 8 + 8*1 + 1 = 53
|
|
EXPECT_EQ(items.size(), 53u);
|
|
|
|
// Preamble: 10 x {500, 1, 500, 0}
|
|
for (size_t i = 0; i < 10; ++i) {
|
|
EXPECT_EQ(items[i].duration0, 500u);
|
|
EXPECT_EQ(items[i].level0, 1u);
|
|
EXPECT_EQ(items[i].duration1, 500u);
|
|
EXPECT_EQ(items[i].level1, 0u);
|
|
}
|
|
|
|
// SOF: {4868, 1, 2469, 0} + {1647, 1, 315, 0}
|
|
EXPECT_EQ(items[10].duration0, 4868u);
|
|
EXPECT_EQ(items[10].duration1, 2469u);
|
|
EXPECT_EQ(items[11].duration0, 1647u);
|
|
EXPECT_EQ(items[11].duration1, 315u);
|
|
|
|
// EOF: {5000, 0, 5000, 0}
|
|
EXPECT_EQ(items[52].duration0, 5000u);
|
|
EXPECT_EQ(items[52].level0, 0u);
|
|
EXPECT_EQ(items[52].duration1, 5000u);
|
|
EXPECT_EQ(items[52].level1, 0u);
|
|
}
|
|
|
|
TEST(M5CodecEncode, PayloadSizeAffectsItemCount)
|
|
{
|
|
M5Codec codec;
|
|
|
|
uint8_t p1[] = {0x01};
|
|
EXPECT_EQ(codec.encode(p1, 1).size(), 53u); // 45 + 8*1
|
|
|
|
uint8_t p4[] = {0x01, 0x02, 0x03, 0x04};
|
|
EXPECT_EQ(codec.encode(p4, 4).size(), 77u); // 45 + 8*4
|
|
}
|
|
|
|
TEST(M5CodecEncode, DataItemsAreValidManchester)
|
|
{
|
|
M5Codec codec;
|
|
uint8_t payload[] = {0xAB};
|
|
auto items = codec.encode(payload, sizeof(payload));
|
|
|
|
// Data region: items[12..51] = CRC(8) + ID(8) + Count(8) + Len(8) + Payload(8)
|
|
for (size_t i = 12; i < 52; ++i) {
|
|
EXPECT_EQ(items[i].level0, 1u);
|
|
EXPECT_EQ(items[i].level1, 0u);
|
|
bool is_one = (items[i].duration0 == 800 && items[i].duration1 == 200);
|
|
bool is_zero = (items[i].duration0 == 200 && items[i].duration1 == 800);
|
|
EXPECT_TRUE(is_one || is_zero) << "item[" << i << "] is not valid Manchester";
|
|
}
|
|
}
|
|
|
|
TEST(M5CodecEncode, CRC8MatchesPayload)
|
|
{
|
|
M5Codec codec;
|
|
uint8_t payload[] = {0xDE, 0xAD, 0xBE, 0xEF};
|
|
auto items = codec.encode(payload, sizeof(payload));
|
|
|
|
// CRC8 field: items[12..19], Manchester MSB first
|
|
// duration0==800 means bit=1 (rmt_item_one), duration0==200 means bit=0
|
|
uint8_t crc_decoded = 0;
|
|
for (size_t bit = 0; bit < 8; ++bit) {
|
|
crc_decoded <<= 1;
|
|
if (items[12 + bit].duration0 == 800) {
|
|
crc_decoded |= 1;
|
|
}
|
|
}
|
|
|
|
m5::utility::CRC8_Checksum crc8;
|
|
crc8.update(payload, sizeof(payload));
|
|
EXPECT_EQ(crc_decoded, crc8.value());
|
|
}
|
|
|
|
TEST(M5CodecEncode, SendCountIncrements)
|
|
{
|
|
M5Codec codec;
|
|
uint8_t payload[] = {0x01};
|
|
|
|
// Count field: items[28..35] (MSB first Manchester)
|
|
auto items0 = codec.encode(payload, 1); // count=0
|
|
auto items1 = codec.encode(payload, 1); // count=1
|
|
auto items2 = codec.encode(payload, 1); // count=2
|
|
|
|
// Decode count from Manchester items
|
|
auto decode_byte = [](const item_container_type& items, size_t offset) -> uint8_t {
|
|
uint8_t val = 0;
|
|
for (size_t bit = 0; bit < 8; ++bit) {
|
|
val <<= 1;
|
|
if (items[offset + bit].duration0 == 800) {
|
|
val |= 1;
|
|
}
|
|
}
|
|
return val;
|
|
};
|
|
|
|
EXPECT_EQ(decode_byte(items0, 28), 0u);
|
|
EXPECT_EQ(decode_byte(items1, 28), 1u);
|
|
EXPECT_EQ(decode_byte(items2, 28), 2u);
|
|
}
|
|
|
|
TEST(M5CodecEncode, OverheadReturns4)
|
|
{
|
|
M5Codec codec;
|
|
EXPECT_EQ(codec.overhead(), 4u);
|
|
EXPECT_EQ(codec.type(), CodecType::M5RF433);
|
|
}
|