Skip to content

Commit e0c2dfe

Browse files
authored
Merge pull request #3 from Reimanbow/feature/reassembler
Feature/reassembler
2 parents 86c253e + 42a1803 commit e0c2dfe

11 files changed

Lines changed: 603 additions & 28 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
idf_component_register(
22
SRCS "src/cast_chunker.c"
3+
"src/cast_reassembler.c"
34
INCLUDE_DIRS "include"
45
REQUIRES ""
56
)

include/cast_protocol.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,27 @@ esp_err_t cast_send_frame(
4040
cast_transport_interface_t *transport
4141
);
4242

43+
/**
44+
* @brief 画像が完成したときに呼ばれる関数の型
45+
* @note dataはcallbackから戻った後にミドルウェアが解放する。保持する場合はコピーすること。
46+
*/
47+
typedef void (*cast_on_frame_ready_t)(const uint8_t *data, size_t len, cast_image_format_t fmt);
48+
49+
/**
50+
* @brief 受信側の初期化
51+
* transportのrecvコールバックを登録し、受信したパケットをreassemblerに渡す
52+
* @param transport トランスポートインタフェース
53+
* @param callback フレーム完成時に呼ばれるコールバック
54+
* @return esp_err_t ESP_OK on success
55+
*/
56+
esp_err_t cast_init_receiver(cast_transport_interface_t *transport, cast_on_frame_ready_t callback);
57+
58+
/**
59+
* @brief 受信中のフレーム組み立てをリセットする
60+
* 途中のフレームを破棄してバッファを解放する
61+
*/
62+
void cast_reassembler_reset(void);
63+
4364
#ifdef __cplusplus
4465
}
4566
#endif

src/cast_chunker.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,18 @@ esp_err_t cast_send_frame(
3838
header->frame_id = frame_counter;
3939
header->chunk_index = i;
4040
header->total_chunks = total_chunks;
41+
header->max_payload = (uint16_t)max_payload;
4142
header->payload_len = (uint16_t)current_payload_len;
4243

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

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

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

5455
if (err != ESP_OK) {

src/cast_internal.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ typedef enum {
2020
} cast_packet_type_t;
2121

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

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

5053
#endif /* CAST_INTERNAL_H */

src/cast_reassembler.c

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @file cast_reassembler.c
3+
*/
4+
#include "cast_protocol.h"
5+
#include "cast_internal.h"
6+
7+
#include <stdlib.h>
8+
#include <string.h>
9+
#include <stdint.h>
10+
#include <stdbool.h>
11+
12+
/**
13+
* @brief 受信機内部で持つ情報
14+
*/
15+
typedef struct {
16+
uint8_t *buffer; // 組み立て用バッファ
17+
size_t allocated_size; // 現在確保しているサイズ
18+
uint16_t current_frame_id; // 追跡中のフレームID
19+
uint16_t chunks_received; // 到着済みチャンク数
20+
uint16_t total_chunks; // 期待される総数
21+
uint16_t max_payload_ref; // オフセット計算の基準値
22+
uint16_t last_chunk_len; // 最終チャンクのペイロード長
23+
cast_image_format_t format; // 画像フォーマット
24+
bool is_active;
25+
} cast_reassembler_ctx_t;
26+
27+
static cast_reassembler_ctx_t ctx = {0};
28+
static cast_on_frame_ready_t app_callback = NULL;
29+
30+
// 前方宣言
31+
static void cast_reassembler_push_packet(const uint8_t *data, size_t len);
32+
33+
void cast_reassembler_reset(void) {
34+
if (ctx.buffer) {
35+
free(ctx.buffer);
36+
ctx.buffer = NULL;
37+
}
38+
ctx.is_active = false;
39+
ctx.chunks_received = 0;
40+
}
41+
42+
static void cast_reassembler_init(cast_on_frame_ready_t callback) {
43+
app_callback = callback;
44+
cast_reassembler_reset();
45+
}
46+
47+
esp_err_t cast_init_receiver(cast_transport_interface_t *transport, cast_on_frame_ready_t callback) {
48+
cast_reassembler_init(callback);
49+
return transport->set_recv_callback(cast_reassembler_push_packet);
50+
}
51+
52+
static void cast_reassembler_push_packet(const uint8_t *data, size_t len) {
53+
// 最小サイズチェック (Header + CRC)
54+
if (len < sizeof(cast_header_t) + CAST_CRC_SIZE) return;
55+
56+
cast_header_t *header = (cast_header_t *)data;
57+
if (header->magic != CAST_MAGIC_BYTE) return;
58+
59+
// TODO: CRC検証
60+
// uint16_t received_crc;
61+
// memcpy(&received_crc, data + sizeof(cast_header_t) + header->payload_len, CAST_CRC_SIZE);
62+
// if (calc_crc(data, len - CAST_CRC_SIZE) != received_crc) return;
63+
64+
// 1. 新しいフレームの開始判断
65+
if (!ctx.is_active || header->frame_id != ctx.current_frame_id) {
66+
// 前の未完成のフレームがあればリセット
67+
cast_reassembler_reset();
68+
69+
// メモリ確保: 安全のため(総数*最大サイズ)で確保
70+
size_t reserve_size = header->total_chunks * header->max_payload;
71+
ctx.buffer = (uint8_t *)malloc(reserve_size);
72+
if (!ctx.buffer) return;
73+
74+
ctx.allocated_size = reserve_size;
75+
ctx.current_frame_id = header->frame_id;
76+
ctx.total_chunks = header->total_chunks;
77+
ctx.max_payload_ref = header->max_payload;
78+
ctx.format = (cast_image_format_t)header->format;
79+
ctx.chunks_received = 0;
80+
ctx.is_active = true;
81+
}
82+
83+
// 2. オフセット計算と書き込み(ダイレクトマッピング)
84+
size_t offset = header->chunk_index * ctx.max_payload_ref;
85+
const uint8_t *payload = data + sizeof(cast_header_t);
86+
87+
if (offset + header->payload_len <= ctx.allocated_size) {
88+
memcpy(ctx.buffer + offset, payload, header->payload_len);
89+
ctx.chunks_received++;
90+
91+
// 最終チャンクの payload_len を記録(順不同対応)
92+
if (header->chunk_index == ctx.total_chunks - 1) {
93+
ctx.last_chunk_len = header->payload_len;
94+
}
95+
}
96+
97+
if (ctx.chunks_received == ctx.total_chunks) {
98+
if (app_callback) {
99+
size_t final_size = (ctx.total_chunks - 1) * ctx.max_payload_ref + ctx.last_chunk_len;
100+
app_callback(ctx.buffer, final_size, ctx.format);
101+
}
102+
103+
// callback から戻ったらミドルウェア側で解放
104+
// アプリはcallback内で必要ならコピーすること
105+
free(ctx.buffer);
106+
ctx.buffer = NULL;
107+
ctx.is_active = false;
108+
}
109+
}

test/host/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,21 @@ FetchContent_MakeAvailable(googletest)
1515
# Component source (compiled as C)
1616
add_library(component STATIC
1717
../../src/cast_chunker.c
18+
../../src/cast_reassembler.c
1819
)
1920
target_include_directories(component PUBLIC
2021
../../include
2122
../../src
2223
mocks
2324
)
2425

26+
# Test data directory
27+
add_compile_definitions(TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
28+
2529
# Test executable
2630
add_executable(host_test
2731
test_cast_chunker.cpp
32+
test_cast_reassembler.cpp
2833
)
2934
target_link_libraries(host_test PRIVATE component GTest::gtest_main)
3035

test/host/placeholder_jp_16x16.jpg

765 Bytes
Loading
3.88 KB
Loading
20.4 KB
Loading

test/host/test_cast_chunker.cpp

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ static const uint8_t *packet_payload(size_t index)
5656
return g_captured_packets[index].data() + sizeof(cast_header_t);
5757
}
5858

59+
// テスト用にmax_payloadを計算するヘルパー
60+
static size_t calc_max_payload(void)
61+
{
62+
return g_mock_mtu - CAST_PROTOCOL_OVERHEAD;
63+
}
64+
65+
// 指定データ長に対する期待チャンク数を計算するヘルパー
66+
static size_t calc_expected_chunks(size_t data_len)
67+
{
68+
size_t mp = calc_max_payload();
69+
return (data_len + mp - 1) / mp;
70+
}
71+
5972
// ---------------------------------------------------------------------------
6073
// テストフィクスチャ
6174
// ---------------------------------------------------------------------------
@@ -78,9 +91,7 @@ class ChunkerTest : public ::testing::Test {
7891
// データが max_payload の倍数 → 端数なし
7992
TEST_F(ChunkerTest, ExactFitChunkCount)
8093
{
81-
// MTU=64, overhead=sizeof(cast_header_t)+2=13, max_payload=51
82-
// データ102バイト → 102/51 = ちょうど2チャンク
83-
const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD;
94+
const size_t max_payload = calc_max_payload();
8495
const size_t data_len = max_payload * 2;
8596
std::vector<uint8_t> data(data_len, 0xAA);
8697

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

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

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

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

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

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

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

@@ -169,6 +184,11 @@ TEST_F(ChunkerTest, HeaderConsistency)
169184
EXPECT_EQ(h0->chunk_index, 0);
170185
EXPECT_EQ(h1->chunk_index, 1);
171186
EXPECT_EQ(h2->chunk_index, 2);
187+
188+
// max_payload は全パケットで同じ値
189+
EXPECT_EQ(h0->max_payload, max_payload);
190+
EXPECT_EQ(h1->max_payload, max_payload);
191+
EXPECT_EQ(h2->max_payload, max_payload);
172192
}
173193

174194
// ===========================================================================
@@ -177,10 +197,11 @@ TEST_F(ChunkerTest, HeaderConsistency)
177197

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

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

192213
// 各チャンクの payload_len を確認
193-
const size_t expect_len0 = max_payload; // 51
194-
const size_t expect_len1 = max_payload; // 51
195-
const size_t expect_len2 = 130 - max_payload * 2; // 28
214+
const size_t expect_len0 = max_payload;
215+
const size_t expect_len1 = max_payload;
216+
const size_t expect_len2 = data_len - max_payload * 2;
196217

197218
EXPECT_EQ(packet_header(0)->payload_len, expect_len0);
198219
EXPECT_EQ(packet_header(1)->payload_len, expect_len1);
@@ -201,14 +222,14 @@ TEST_F(ChunkerTest, PayloadBoundary)
201222
// チャンク0の末尾とチャンク1の先頭が連続しているか
202223
const uint8_t *p0 = packet_payload(0);
203224
const uint8_t *p1 = packet_payload(1);
204-
EXPECT_EQ(p0[0], 0); // 先頭
205-
EXPECT_EQ(p0[max_payload - 1], max_payload - 1); // チャンク0の末尾
206-
EXPECT_EQ(p1[0], max_payload); // チャンク1の先頭 = チャンク0の続き
225+
EXPECT_EQ(p0[0], 0); // 先頭
226+
EXPECT_EQ(p0[max_payload - 1], (uint8_t)(max_payload - 1)); // チャンク0の末尾
227+
EXPECT_EQ(p1[0], (uint8_t)max_payload); // チャンク1の先頭 = チャンク0の続き
207228

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

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

256278
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)
294316

295317
TEST_F(ChunkerTest, SmallMtu)
296318
{
297-
g_mock_mtu = 24; // overhead=13 → max_payload=11
298-
const size_t max_payload = g_mock_mtu - CAST_PROTOCOL_OVERHEAD;
319+
g_mock_mtu = 26; // overhead=15 → max_payload=11
320+
const size_t max_payload = calc_max_payload();
299321
std::vector<uint8_t> data(50, 0x11);
300322

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

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

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

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

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

318-
// max_payload=237, 200 < 237 → 1チャンク
342+
// 200 < max_payload → 1チャンク
319343
EXPECT_EQ(g_captured_packets.size(), 1u);
320344
EXPECT_EQ(packet_header(0)->payload_len, 200);
345+
EXPECT_EQ(packet_header(0)->max_payload, max_payload);
321346
}

0 commit comments

Comments
 (0)