diff --git a/CMakeLists.txt b/CMakeLists.txt index 2447ec2..58dd024 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ idf_component_register( SRCS "src/cast_chunker.c" + "src/cast_reassembler.c" INCLUDE_DIRS "include" REQUIRES "" ) diff --git a/include/cast_protocol.h b/include/cast_protocol.h index 5f4022c..57c7567 100644 --- a/include/cast_protocol.h +++ b/include/cast_protocol.h @@ -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 diff --git a/src/cast_chunker.c b/src/cast_chunker.c index 558c095..eb80bd9 100644 --- a/src/cast_chunker.c +++ b/src/cast_chunker.c @@ -38,6 +38,7 @@ 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. データのコピー(ヘッダの直後へ) @@ -45,10 +46,10 @@ esp_err_t cast_send_frame( // 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) { diff --git a/src/cast_internal.h b/src/cast_internal.h index 8608185..2963250 100644 --- a/src/cast_internal.h +++ b/src/cast_internal.h @@ -20,7 +20,7 @@ typedef enum { } cast_packet_type_t; /** - * @brief CASTプロトコル共通ヘッダ (11バイト) + * @brief CASTプロトコル共通ヘッダ (13バイト) * * [フィールド再利用(Overloading)の設計指針] * プロトコルの軽量化のため、PacketTypeに応じて以下の通り意味を読み替える。 * * 1. DATA時: すべてのフィールドを定義通り使用。 @@ -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 */ \ No newline at end of file diff --git a/src/cast_reassembler.c b/src/cast_reassembler.c new file mode 100644 index 0000000..5fff28e --- /dev/null +++ b/src/cast_reassembler.c @@ -0,0 +1,109 @@ +/** + * @file cast_reassembler.c + */ +#include "cast_protocol.h" +#include "cast_internal.h" + +#include +#include +#include +#include + +/** + * @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; + } +} diff --git a/test/host/CMakeLists.txt b/test/host/CMakeLists.txt index a402e4b..ecb5539 100644 --- a/test/host/CMakeLists.txt +++ b/test/host/CMakeLists.txt @@ -15,6 +15,7 @@ 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 @@ -22,9 +23,13 @@ target_include_directories(component PUBLIC 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) diff --git a/test/host/placeholder_jp_16x16.jpg b/test/host/placeholder_jp_16x16.jpg new file mode 100644 index 0000000..8a6ed5b Binary files /dev/null and b/test/host/placeholder_jp_16x16.jpg differ diff --git a/test/host/placeholder_jp_200x200.jpg b/test/host/placeholder_jp_200x200.jpg new file mode 100644 index 0000000..0aae62d Binary files /dev/null and b/test/host/placeholder_jp_200x200.jpg differ diff --git a/test/host/placeholder_jp_640x480.jpg b/test/host/placeholder_jp_640x480.jpg new file mode 100644 index 0000000..fec4ee0 Binary files /dev/null and b/test/host/placeholder_jp_640x480.jpg differ diff --git a/test/host/test_cast_chunker.cpp b/test/host/test_cast_chunker.cpp index 7382d8a..5e79f8f 100644 --- a/test/host/test_cast_chunker.cpp +++ b/test/host/test_cast_chunker.cpp @@ -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; +} + // --------------------------------------------------------------------------- // テストフィクスチャ // --------------------------------------------------------------------------- @@ -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 data(data_len, 0xAA); @@ -93,8 +104,9 @@ TEST_F(ChunkerTest, ExactFitChunkCount) // データが max_payload の倍数でない → 端数あり TEST_F(ChunkerTest, PartialLastChunkCount) { - // max_payload=51, データ100バイト → ceil(100/51) = 2チャンク - std::vector data(100, 0xBB); + const size_t max_payload = calc_max_payload(); + // max_payload + 1 バイトで確実に2チャンク + std::vector data(max_payload + 1, 0xBB); esp_err_t ret = cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); @@ -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 data(130, 0xDD); + const size_t max_payload = calc_max_payload(); + const size_t data_len = max_payload * 2 + 1; // 確実に3チャンク + std::vector data(data_len, 0xDD); esp_err_t ret = cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); @@ -131,7 +144,9 @@ TEST_F(ChunkerTest, ThreeChunks) TEST_F(ChunkerTest, HeaderConsistency) { - std::vector data(130, 0x00); + const size_t max_payload = calc_max_payload(); + const size_t data_len = max_payload * 2 + 1; // 3チャンク + std::vector data(data_len, 0x00); cast_send_frame(data.data(), data.size(), CAST_FMT_RGB565, &g_mock_transport); @@ -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); } // =========================================================================== @@ -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 data(130); + // 0, 1, 2, ... の連番データ + std::vector data(data_len); for (size_t i = 0; i < data.size(); i++) { data[i] = (uint8_t)(i & 0xFF); } @@ -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); @@ -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 reassembled; @@ -250,7 +271,8 @@ TEST_F(ChunkerTest, FrameIdIncrements) // 2番目のsendで失敗 → 即座にエラーを返し、残りは送らない TEST_F(ChunkerTest, SendFailureMidway) { - std::vector data(130, 0xEE); // 3チャンク + const size_t max_payload = calc_max_payload(); + std::vector 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); @@ -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 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 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); } diff --git a/test/host/test_cast_reassembler.cpp b/test/host/test_cast_reassembler.cpp new file mode 100644 index 0000000..9c29d19 --- /dev/null +++ b/test/host/test_cast_reassembler.cpp @@ -0,0 +1,410 @@ +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "cast_protocol.h" +#include "cast_internal.h" +} + +// --------------------------------------------------------------------------- +// Mock transport (Chunker送信キャプチャ + Reassemblerコールバック登録) +// --------------------------------------------------------------------------- + +static std::vector> g_captured_packets; +static size_t g_mock_mtu = 64; +static void (*g_recv_callback)(const uint8_t *data, size_t len) = nullptr; + +static esp_err_t mock_send(const uint8_t *data, size_t len) +{ + g_captured_packets.emplace_back(data, data + len); + return ESP_OK; +} + +static size_t mock_get_mtu(void) { return g_mock_mtu; } +static int16_t mock_get_rssi(void) { return -50; } +static bool mock_is_connected(void) { return true; } +static bool mock_is_ready(void) { return true; } + +static esp_err_t mock_set_recv_cb(void (*cb)(const uint8_t *, size_t)) +{ + g_recv_callback = cb; + return ESP_OK; +} + +static cast_transport_interface_t g_mock_transport = { + .send = mock_send, + .get_mtu = mock_get_mtu, + .get_rssi = mock_get_rssi, + .is_connected = mock_is_connected, + .is_ready = mock_is_ready, + .set_recv_callback = mock_set_recv_cb, +}; + +// --------------------------------------------------------------------------- +// 完成フレームのキャプチャ +// --------------------------------------------------------------------------- + +struct CapturedFrame { + std::vector data; + cast_image_format_t fmt; +}; + +static std::vector g_completed_frames; + +static void on_frame_ready(const uint8_t *data, size_t len, cast_image_format_t fmt) +{ + CapturedFrame f; + f.data.assign(data, data + len); + f.fmt = fmt; + g_completed_frames.push_back(std::move(f)); +} + +// --------------------------------------------------------------------------- +// ヘルパー: キャプチャしたパケットをReassemblerに投入 +// --------------------------------------------------------------------------- + +static void feed_all_packets() +{ + for (auto &pkt : g_captured_packets) { + g_recv_callback(pkt.data(), pkt.size()); + } +} + +static void feed_packets_in_order(const std::vector &order) +{ + for (size_t idx : order) { + g_recv_callback(g_captured_packets[idx].data(), g_captured_packets[idx].size()); + } +} + +// ヘルパー: ファイルをバイト列として読み込む +static std::vector read_file(const std::string &path) +{ + std::ifstream ifs(path, std::ios::binary); + return std::vector( + std::istreambuf_iterator(ifs), + std::istreambuf_iterator() + ); +} + +// --------------------------------------------------------------------------- +// テストフィクスチャ +// --------------------------------------------------------------------------- + +class ReassemblerTest : public ::testing::Test { +protected: + void SetUp() override + { + g_captured_packets.clear(); + g_completed_frames.clear(); + g_mock_mtu = 64; + g_recv_callback = nullptr; + cast_init_receiver(&g_mock_transport, on_frame_ready); + } + + void TearDown() override + { + cast_reassembler_reset(); + } +}; + +// =========================================================================== +// 1. 正常系:一気通貫(Loopback)テスト +// =========================================================================== + +// 1-1: 最小データ (1バイト) +TEST_F(ReassemblerTest, LoopbackMinimal) +{ + uint8_t data[] = {0x42}; + cast_send_frame(data, 1, CAST_FMT_JPEG, &g_mock_transport); + ASSERT_EQ(g_captured_packets.size(), 1u); + + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data[0], 0x42); +} + +// 1-2: MTUぴったり (max_payload と同じデータ長) +TEST_F(ReassemblerTest, LoopbackExactMtu) +{ + const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD; + std::vector data(max_payload); + std::iota(data.begin(), data.end(), 0); + + cast_send_frame(data.data(), data.size(), CAST_FMT_RGB565, &g_mock_transport); + ASSERT_EQ(g_captured_packets.size(), 1u); + + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data, data); +} + +// 1-3: MTU+1バイト (2チャンク、端数1バイト) +TEST_F(ReassemblerTest, LoopbackMtuPlusOne) +{ + const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD; + std::vector data(max_payload + 1); + std::iota(data.begin(), data.end(), 0); + + cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); + ASSERT_EQ(g_captured_packets.size(), 2u); + + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data, data); +} + +// 1-4: 実JPEG画像 (16x16) +TEST_F(ReassemblerTest, LoopbackJpeg16x16) +{ + auto jpeg = read_file(std::string(TEST_DATA_DIR) + "/placeholder_jp_16x16.jpg"); + ASSERT_FALSE(jpeg.empty()); + + cast_send_frame(jpeg.data(), jpeg.size(), CAST_FMT_JPEG, &g_mock_transport); + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data.size(), jpeg.size()); + EXPECT_EQ(g_completed_frames[0].data, jpeg); + EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_JPEG); +} + +// 1-4b: 実JPEG画像 (200x200) +TEST_F(ReassemblerTest, LoopbackJpeg200x200) +{ + auto jpeg = read_file(std::string(TEST_DATA_DIR) + "/placeholder_jp_200x200.jpg"); + ASSERT_FALSE(jpeg.empty()); + + cast_send_frame(jpeg.data(), jpeg.size(), CAST_FMT_JPEG, &g_mock_transport); + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data.size(), jpeg.size()); + EXPECT_EQ(g_completed_frames[0].data, jpeg); +} + +// 1-4c: 実JPEG画像 (640x480) - ESP-NOW相当のMTU +TEST_F(ReassemblerTest, LoopbackJpeg640x480_LargeMtu) +{ + g_mock_mtu = 250; + // MTU変更後にreceiverを再初期化 + cast_init_receiver(&g_mock_transport, on_frame_ready); + + auto jpeg = read_file(std::string(TEST_DATA_DIR) + "/placeholder_jp_640x480.jpg"); + ASSERT_FALSE(jpeg.empty()); + + cast_send_frame(jpeg.data(), jpeg.size(), CAST_FMT_JPEG, &g_mock_transport); + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data.size(), jpeg.size()); + EXPECT_EQ(g_completed_frames[0].data, jpeg); +} + +// =========================================================================== +// 2. 無線耐性系:順序制御テスト +// =========================================================================== + +// 2-1: 逆順到着 +TEST_F(ReassemblerTest, ReverseOrder) +{ + const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD; + std::vector data(max_payload * 2 + 10); + std::iota(data.begin(), data.end(), 0); + + cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); + ASSERT_EQ(g_captured_packets.size(), 3u); + + // 逆順: [2] → [1] → [0] + feed_packets_in_order({2, 1, 0}); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data, data); +} + +// 2-2: ランダム順序 (JPEG 200x200を使用) +TEST_F(ReassemblerTest, RandomOrder) +{ + auto jpeg = read_file(std::string(TEST_DATA_DIR) + "/placeholder_jp_200x200.jpg"); + ASSERT_FALSE(jpeg.empty()); + + cast_send_frame(jpeg.data(), jpeg.size(), CAST_FMT_JPEG, &g_mock_transport); + size_t n = g_captured_packets.size(); + ASSERT_GT(n, 2u); + + // インデックスをシャッフル + std::vector order(n); + std::iota(order.begin(), order.end(), 0); + std::mt19937 rng(12345); // 固定シード(再現性のため) + std::shuffle(order.begin(), order.end(), rng); + + feed_packets_in_order(order); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data.size(), jpeg.size()); + EXPECT_EQ(g_completed_frames[0].data, jpeg); +} + +// =========================================================================== +// 3. 異常系:フレーム管理・リセットテスト +// =========================================================================== + +// 3-1: フレームID切り替わりによるリセット +TEST_F(ReassemblerTest, FrameIdSwitchResetsState) +{ + const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD; + std::vector data1(max_payload * 2 + 5); + std::iota(data1.begin(), data1.end(), 0); + + std::vector data2(max_payload + 10); + std::iota(data2.begin(), data2.end(), 100); + + // フレーム1を送信(3チャンク) + cast_send_frame(data1.data(), data1.size(), CAST_FMT_JPEG, &g_mock_transport); + size_t frame1_count = g_captured_packets.size(); + + // フレーム1のチャンク0だけ投入(未完成) + g_recv_callback(g_captured_packets[0].data(), g_captured_packets[0].size()); + EXPECT_EQ(g_completed_frames.size(), 0u); + + // フレーム2を送信 + g_captured_packets.clear(); + cast_send_frame(data2.data(), data2.size(), CAST_FMT_JPEG, &g_mock_transport); + + // フレーム2の全チャンクを投入 → フレーム1がリセットされフレーム2が完成 + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data, data2); +} + +// 3-2: 短すぎるパケットは無視される +TEST_F(ReassemblerTest, TooShortPacketIgnored) +{ + uint8_t short_data[] = {0xCA, 0x00}; + g_recv_callback(short_data, sizeof(short_data)); + + EXPECT_EQ(g_completed_frames.size(), 0u); +} + +// 3-3: magicバイトが違うパケットは無視される +TEST_F(ReassemblerTest, WrongMagicIgnored) +{ + const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD; + std::vector data(10, 0xAA); + + cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); + ASSERT_EQ(g_captured_packets.size(), 1u); + + // magicバイトを壊す + g_captured_packets[0][0] = 0xFF; + feed_all_packets(); + + EXPECT_EQ(g_completed_frames.size(), 0u); +} + +// 3-4: バッファオーバーランチェック +// chunk_index が total_chunks を超える不正なパケットを投入 +TEST_F(ReassemblerTest, OutOfBoundsChunkIndexIgnored) +{ + const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD; + std::vector data(max_payload * 2 + 1); + std::iota(data.begin(), data.end(), 0); + + cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); + ASSERT_EQ(g_captured_packets.size(), 3u); + + // チャンク0を投入(フレーム初期化) + g_recv_callback(g_captured_packets[0].data(), g_captured_packets[0].size()); + + // 不正パケット: chunk_index を 999 に改ざん + auto bad_pkt = g_captured_packets[1]; + cast_header_t *h = (cast_header_t *)bad_pkt.data(); + h->chunk_index = 999; + g_recv_callback(bad_pkt.data(), bad_pkt.size()); + + // 正常なチャンク1, 2を投入して完成させる + g_recv_callback(g_captured_packets[1].data(), g_captured_packets[1].size()); + g_recv_callback(g_captured_packets[2].data(), g_captured_packets[2].size()); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data, data); +} + +// =========================================================================== +// 4. アプリ連携系:フォーマット・完了通知テスト +// =========================================================================== + +// 4-1: フォーマット情報の伝搬 +TEST_F(ReassemblerTest, FormatPropagation) +{ + std::vector data(50, 0x11); + + // JPEG + cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); + feed_all_packets(); + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_JPEG); + + // RGB565 + g_captured_packets.clear(); + g_completed_frames.clear(); + cast_send_frame(data.data(), data.size(), CAST_FMT_RGB565, &g_mock_transport); + feed_all_packets(); + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_RGB565); + + // GRAYSCALE + g_captured_packets.clear(); + g_completed_frames.clear(); + cast_send_frame(data.data(), data.size(), CAST_FMT_GRAYSCALE, &g_mock_transport); + feed_all_packets(); + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_GRAYSCALE); +} + +// 4-2: 最終サイズの完全一致(連番データ) +TEST_F(ReassemblerTest, FinalSizeExact) +{ + // 端数が出るサイズで確認 + const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD; + std::vector data(max_payload * 3 + 7); + std::iota(data.begin(), data.end(), 0); + + cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data.size(), data.size()); + EXPECT_EQ(g_completed_frames[0].data, data); +} + +// 4-2b: 最終サイズの完全一致(逆順でも正しいサイズ) +TEST_F(ReassemblerTest, FinalSizeExactReverseOrder) +{ + const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD; + std::vector data(max_payload * 3 + 7); + std::iota(data.begin(), data.end(), 0); + + cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, &g_mock_transport); + size_t n = g_captured_packets.size(); + + // 逆順投入 + std::vector order(n); + std::iota(order.begin(), order.end(), 0); + std::reverse(order.begin(), order.end()); + feed_packets_in_order(order); + + ASSERT_EQ(g_completed_frames.size(), 1u); + EXPECT_EQ(g_completed_frames[0].data.size(), data.size()); + EXPECT_EQ(g_completed_frames[0].data, data); +}