Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
idf_component_register(
SRCS "src/cast_chunker.c"
"src/cast_reassembler.c"
INCLUDE_DIRS "include"
REQUIRES ""
)
21 changes: 21 additions & 0 deletions include/cast_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,27 @@ esp_err_t cast_send_frame(
cast_transport_interface_t *transport
);

/**
* @brief 画像が完成したときに呼ばれる関数の型
* @note dataはcallbackから戻った後にミドルウェアが解放する。保持する場合はコピーすること。
*/
typedef void (*cast_on_frame_ready_t)(const uint8_t *data, size_t len, cast_image_format_t fmt);

/**
* @brief 受信側の初期化
* transportのrecvコールバックを登録し、受信したパケットをreassemblerに渡す
* @param transport トランスポートインタフェース
* @param callback フレーム完成時に呼ばれるコールバック
* @return esp_err_t ESP_OK on success
*/
esp_err_t cast_init_receiver(cast_transport_interface_t *transport, cast_on_frame_ready_t callback);

/**
* @brief 受信中のフレーム組み立てをリセットする
* 途中のフレームを破棄してバッファを解放する
*/
void cast_reassembler_reset(void);

#ifdef __cplusplus
}
#endif
Expand Down
5 changes: 3 additions & 2 deletions src/cast_chunker.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ esp_err_t cast_send_frame(
header->frame_id = frame_counter;
header->chunk_index = i;
header->total_chunks = total_chunks;
header->max_payload = (uint16_t)max_payload;
header->payload_len = (uint16_t)current_payload_len;

// 2. データのコピー(ヘッダの直後へ)
memcpy(packet_buf + sizeof(cast_header_t), data + sent_bytes, current_payload_len);

// 3. トレーラ(CRC)の付与(今は0固定)
uint16_t crc = 0;
memcpy(packet_buf + sizeof(cast_header_t) + current_payload_len, &crc, 2);
memcpy(packet_buf + sizeof(cast_header_t) + current_payload_len, &crc, CAST_CRC_SIZE);

// 4. 送信
size_t total_packet_len = sizeof(cast_header_t) + current_payload_len + 2;
size_t total_packet_len = sizeof(cast_header_t) + current_payload_len + CAST_CRC_SIZE;
esp_err_t err = transport->send(packet_buf, total_packet_len);

if (err != ESP_OK) {
Expand Down
9 changes: 6 additions & 3 deletions src/cast_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ typedef enum {
} cast_packet_type_t;

/**
* @brief CASTプロトコル共通ヘッダ (11バイト)
* @brief CASTプロトコル共通ヘッダ (13バイト)
* * [フィールド再利用(Overloading)の設計指針]
* プロトコルの軽量化のため、PacketTypeに応じて以下の通り意味を読み替える。
* * 1. DATA時: すべてのフィールドを定義通り使用。
Expand All @@ -40,11 +40,14 @@ typedef struct {
uint16_t frame_id; // 対象フレームID
uint16_t chunk_index; // チャンク番号、または応答対象のチャンク番号
uint16_t total_chunks; // 総チャンク数、または制御用パラメータ
uint16_t max_payload; // 送信側が分割に使用した1チャンクの最大サイズ
uint16_t payload_len; // このパケットに含まれるデータ長(DATA以外では通常0)
/* @note この後に最大MTU-2のサイズとなるまで実際のデータが入る */
} __attribute__((packed)) cast_header_t;

// CRC(2バイト)を考慮したパケット最大オーバーヘッド
#define CAST_PROTOCOL_OVERHEAD (sizeof(cast_header_t) + 2)
#define CAST_CRC_SIZE 2

// CRCを考慮したパケット最大オーバーヘッド
#define CAST_PROTOCOL_OVERHEAD (sizeof(cast_header_t) + CAST_CRC_SIZE)

#endif /* CAST_INTERNAL_H */
109 changes: 109 additions & 0 deletions src/cast_reassembler.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @file cast_reassembler.c
*/
#include "cast_protocol.h"
#include "cast_internal.h"

#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>

/**
* @brief 受信機内部で持つ情報
*/
typedef struct {
uint8_t *buffer; // 組み立て用バッファ
size_t allocated_size; // 現在確保しているサイズ
uint16_t current_frame_id; // 追跡中のフレームID
uint16_t chunks_received; // 到着済みチャンク数
uint16_t total_chunks; // 期待される総数
uint16_t max_payload_ref; // オフセット計算の基準値
uint16_t last_chunk_len; // 最終チャンクのペイロード長
cast_image_format_t format; // 画像フォーマット
bool is_active;
} cast_reassembler_ctx_t;

static cast_reassembler_ctx_t ctx = {0};
static cast_on_frame_ready_t app_callback = NULL;

// 前方宣言
static void cast_reassembler_push_packet(const uint8_t *data, size_t len);

void cast_reassembler_reset(void) {
if (ctx.buffer) {
free(ctx.buffer);
ctx.buffer = NULL;
}
ctx.is_active = false;
ctx.chunks_received = 0;
}

static void cast_reassembler_init(cast_on_frame_ready_t callback) {
app_callback = callback;
cast_reassembler_reset();
}

esp_err_t cast_init_receiver(cast_transport_interface_t *transport, cast_on_frame_ready_t callback) {
cast_reassembler_init(callback);
return transport->set_recv_callback(cast_reassembler_push_packet);
}

static void cast_reassembler_push_packet(const uint8_t *data, size_t len) {
// 最小サイズチェック (Header + CRC)
if (len < sizeof(cast_header_t) + CAST_CRC_SIZE) return;

cast_header_t *header = (cast_header_t *)data;
if (header->magic != CAST_MAGIC_BYTE) return;

// TODO: CRC検証
// uint16_t received_crc;
// memcpy(&received_crc, data + sizeof(cast_header_t) + header->payload_len, CAST_CRC_SIZE);
// if (calc_crc(data, len - CAST_CRC_SIZE) != received_crc) return;

// 1. 新しいフレームの開始判断
if (!ctx.is_active || header->frame_id != ctx.current_frame_id) {
// 前の未完成のフレームがあればリセット
cast_reassembler_reset();

// メモリ確保: 安全のため(総数*最大サイズ)で確保
size_t reserve_size = header->total_chunks * header->max_payload;
ctx.buffer = (uint8_t *)malloc(reserve_size);
if (!ctx.buffer) return;

ctx.allocated_size = reserve_size;
ctx.current_frame_id = header->frame_id;
ctx.total_chunks = header->total_chunks;
ctx.max_payload_ref = header->max_payload;
ctx.format = (cast_image_format_t)header->format;
ctx.chunks_received = 0;
ctx.is_active = true;
}

// 2. オフセット計算と書き込み(ダイレクトマッピング)
size_t offset = header->chunk_index * ctx.max_payload_ref;
const uint8_t *payload = data + sizeof(cast_header_t);

if (offset + header->payload_len <= ctx.allocated_size) {
memcpy(ctx.buffer + offset, payload, header->payload_len);
ctx.chunks_received++;

// 最終チャンクの payload_len を記録(順不同対応)
if (header->chunk_index == ctx.total_chunks - 1) {
ctx.last_chunk_len = header->payload_len;
}
}

if (ctx.chunks_received == ctx.total_chunks) {
if (app_callback) {
size_t final_size = (ctx.total_chunks - 1) * ctx.max_payload_ref + ctx.last_chunk_len;
app_callback(ctx.buffer, final_size, ctx.format);
}

// callback から戻ったらミドルウェア側で解放
// アプリはcallback内で必要ならコピーすること
free(ctx.buffer);
ctx.buffer = NULL;
ctx.is_active = false;
}
}
5 changes: 5 additions & 0 deletions test/host/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,21 @@ FetchContent_MakeAvailable(googletest)
# Component source (compiled as C)
add_library(component STATIC
../../src/cast_chunker.c
../../src/cast_reassembler.c
)
target_include_directories(component PUBLIC
../../include
../../src
mocks
)

# Test data directory
add_compile_definitions(TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}")

# Test executable
add_executable(host_test
test_cast_chunker.cpp
test_cast_reassembler.cpp
)
target_link_libraries(host_test PRIVATE component GTest::gtest_main)

Expand Down
Binary file added test/host/placeholder_jp_16x16.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/host/placeholder_jp_200x200.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/host/placeholder_jp_640x480.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 48 additions & 23 deletions test/host/test_cast_chunker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ static const uint8_t *packet_payload(size_t index)
return g_captured_packets[index].data() + sizeof(cast_header_t);
}

// テスト用にmax_payloadを計算するヘルパー
static size_t calc_max_payload(void)
{
return g_mock_mtu - CAST_PROTOCOL_OVERHEAD;
}

// 指定データ長に対する期待チャンク数を計算するヘルパー
static size_t calc_expected_chunks(size_t data_len)
{
size_t mp = calc_max_payload();
return (data_len + mp - 1) / mp;
}

// ---------------------------------------------------------------------------
// テストフィクスチャ
// ---------------------------------------------------------------------------
Expand All @@ -78,9 +91,7 @@ class ChunkerTest : public ::testing::Test {
// データが max_payload の倍数 → 端数なし
TEST_F(ChunkerTest, ExactFitChunkCount)
{
// MTU=64, overhead=sizeof(cast_header_t)+2=13, max_payload=51
// データ102バイト → 102/51 = ちょうど2チャンク
const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD;
const size_t max_payload = calc_max_payload();
const size_t data_len = max_payload * 2;
std::vector<uint8_t> data(data_len, 0xAA);

Expand All @@ -93,8 +104,9 @@ TEST_F(ChunkerTest, ExactFitChunkCount)
// データが max_payload の倍数でない → 端数あり
TEST_F(ChunkerTest, PartialLastChunkCount)
{
// max_payload=51, データ100バイト → ceil(100/51) = 2チャンク
std::vector<uint8_t> data(100, 0xBB);
const size_t max_payload = calc_max_payload();
// max_payload + 1 バイトで確実に2チャンク
std::vector<uint8_t> data(max_payload + 1, 0xBB);

esp_err_t ret = cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport);

Expand All @@ -116,8 +128,9 @@ TEST_F(ChunkerTest, SingleChunk)
// 3チャンクに分割されるケース
TEST_F(ChunkerTest, ThreeChunks)
{
// max_payload=51, データ130バイト → ceil(130/51) = 3チャンク (51+51+28)
std::vector<uint8_t> data(130, 0xDD);
const size_t max_payload = calc_max_payload();
const size_t data_len = max_payload * 2 + 1; // 確実に3チャンク
std::vector<uint8_t> data(data_len, 0xDD);

esp_err_t ret = cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport);

Expand All @@ -131,7 +144,9 @@ TEST_F(ChunkerTest, ThreeChunks)

TEST_F(ChunkerTest, HeaderConsistency)
{
std::vector<uint8_t> data(130, 0x00);
const size_t max_payload = calc_max_payload();
const size_t data_len = max_payload * 2 + 1; // 3チャンク
std::vector<uint8_t> data(data_len, 0x00);

cast_send_frame(data.data(), data.size(), CAST_FMT_RGB565, &g_mock_transport);

Expand Down Expand Up @@ -169,6 +184,11 @@ TEST_F(ChunkerTest, HeaderConsistency)
EXPECT_EQ(h0->chunk_index, 0);
EXPECT_EQ(h1->chunk_index, 1);
EXPECT_EQ(h2->chunk_index, 2);

// max_payload は全パケットで同じ値
EXPECT_EQ(h0->max_payload, max_payload);
EXPECT_EQ(h1->max_payload, max_payload);
EXPECT_EQ(h2->max_payload, max_payload);
}

// ===========================================================================
Expand All @@ -177,10 +197,11 @@ TEST_F(ChunkerTest, HeaderConsistency)

TEST_F(ChunkerTest, PayloadBoundary)
{
const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD;
const size_t max_payload = calc_max_payload();
const size_t data_len = max_payload * 2 + 10; // 3チャンク (末尾10バイト)

// 0, 1, 2, ... , 129 の連番データ
std::vector<uint8_t> data(130);
// 0, 1, 2, ... の連番データ
std::vector<uint8_t> data(data_len);
for (size_t i = 0; i < data.size(); i++) {
data[i] = (uint8_t)(i & 0xFF);
}
Expand All @@ -190,9 +211,9 @@ TEST_F(ChunkerTest, PayloadBoundary)
ASSERT_EQ(g_captured_packets.size(), 3u);

// 各チャンクの payload_len を確認
const size_t expect_len0 = max_payload; // 51
const size_t expect_len1 = max_payload; // 51
const size_t expect_len2 = 130 - max_payload * 2; // 28
const size_t expect_len0 = max_payload;
const size_t expect_len1 = max_payload;
const size_t expect_len2 = data_len - max_payload * 2;

EXPECT_EQ(packet_header(0)->payload_len, expect_len0);
EXPECT_EQ(packet_header(1)->payload_len, expect_len1);
Expand All @@ -201,14 +222,14 @@ TEST_F(ChunkerTest, PayloadBoundary)
// チャンク0の末尾とチャンク1の先頭が連続しているか
const uint8_t *p0 = packet_payload(0);
const uint8_t *p1 = packet_payload(1);
EXPECT_EQ(p0[0], 0); // 先頭
EXPECT_EQ(p0[max_payload - 1], max_payload - 1); // チャンク0の末尾
EXPECT_EQ(p1[0], max_payload); // チャンク1の先頭 = チャンク0の続き
EXPECT_EQ(p0[0], 0); // 先頭
EXPECT_EQ(p0[max_payload - 1], (uint8_t)(max_payload - 1)); // チャンク0の末尾
EXPECT_EQ(p1[0], (uint8_t)max_payload); // チャンク1の先頭 = チャンク0の続き

// チャンク2(最後)の先頭と末尾
const uint8_t *p2 = packet_payload(2);
EXPECT_EQ(p2[0], (uint8_t)(max_payload * 2));
EXPECT_EQ(p2[expect_len2 - 1], 129);
EXPECT_EQ(p2[expect_len2 - 1], (uint8_t)(data_len - 1));

// 全ペイロードを結合して元データと一致するか
std::vector<uint8_t> reassembled;
Expand Down Expand Up @@ -250,7 +271,8 @@ TEST_F(ChunkerTest, FrameIdIncrements)
// 2番目のsendで失敗 → 即座にエラーを返し、残りは送らない
TEST_F(ChunkerTest, SendFailureMidway)
{
std::vector<uint8_t> data(130, 0xEE); // 3チャンク
const size_t max_payload = calc_max_payload();
std::vector<uint8_t> data(max_payload * 2 + 1, 0xEE); // 3チャンク
g_send_fail_at = 1; // 2番目(index=1)のsendで失敗

esp_err_t ret = cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport);
Expand Down Expand Up @@ -294,28 +316,31 @@ TEST_F(ChunkerTest, PacketSizeNeverExceedsMtu)

TEST_F(ChunkerTest, SmallMtu)
{
g_mock_mtu = 24; // overhead=13 → max_payload=11
const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD;
g_mock_mtu = 26; // overhead=15 → max_payload=11
const size_t max_payload = calc_max_payload();
std::vector<uint8_t> data(50, 0x11);

cast_send_frame(data.data(), data.size(), CAST_FMT_GRAYSCALE, &g_mock_transport);

size_t expected_chunks = (50 + max_payload - 1) / max_payload;
size_t expected_chunks = calc_expected_chunks(50);
EXPECT_EQ(g_captured_packets.size(), expected_chunks);

for (size_t i = 0; i < g_captured_packets.size(); i++) {
EXPECT_LE(g_captured_packets[i].size(), g_mock_mtu);
EXPECT_EQ(packet_header(i)->max_payload, max_payload);
}
}

TEST_F(ChunkerTest, LargeMtu)
{
g_mock_mtu = 250; // ESP-NOW相当
const size_t max_payload = calc_max_payload();
std::vector<uint8_t> data(200, 0x22);

cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport);

// max_payload=237, 200 < 237 → 1チャンク
// 200 < max_payload → 1チャンク
EXPECT_EQ(g_captured_packets.size(), 1u);
EXPECT_EQ(packet_header(0)->payload_len, 200);
EXPECT_EQ(packet_header(0)->max_payload, max_payload);
}
Loading