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
151 changes: 150 additions & 1 deletion src/cast_reassembler.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,133 @@

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

// ---------------------------------------------------------------------------
// BMP ヘッダ生成 (RGB565用)
// ---------------------------------------------------------------------------

// BMP with BI_BITFIELDS: FileHeader(14) + InfoHeader(40) + ColorMasks(12) = 66 bytes
#define BMP_HEADER_SIZE 66

#pragma pack(push, 1)
typedef struct {
// File Header (14 bytes)
uint8_t signature[2]; // "BM"
uint32_t file_size;
uint16_t reserved1;
uint16_t reserved2;
uint32_t data_offset;
// Info Header - BITMAPINFOHEADER (40 bytes)
uint32_t info_size; // 40
int32_t width;
int32_t height; // 負値 = top-down
uint16_t planes; // 1
uint16_t bpp; // 16
uint32_t compression; // 3 = BI_BITFIELDS
uint32_t image_size;
int32_t x_ppm; // pixels per meter
int32_t y_ppm;
uint32_t colors_used;
uint32_t colors_important;
// Color masks (12 bytes)
uint32_t mask_r; // 0xF800
uint32_t mask_g; // 0x07E0
uint32_t mask_b; // 0x001F
} bmp_header_t;
#pragma pack(pop)

/**
* @brief RGB565 生ピクセルデータに BMP ヘッダを付与した完成ファイルを生成
* @param pixels 生ピクセルデータ
* @param pixel_len ピクセルデータのバイト数
* @param w 画像幅
* @param h 画像高さ
* @param out_len 出力ファイルサイズ
* @return malloc されたファイルデータ。呼び出し側が free する。失敗時 NULL
*/
static uint8_t *build_bmp_rgb565(const uint8_t *pixels, size_t pixel_len,
uint16_t w, uint16_t h, size_t *out_len)
{
// BMP の行は4バイト境界にパディング
uint32_t row_stride = ((w * 2 + 3) / 4) * 4;
uint32_t image_size = row_stride * h;
uint32_t file_size = BMP_HEADER_SIZE + image_size;

uint8_t *buf = (uint8_t *)malloc(file_size);
if (!buf) return NULL;
memset(buf, 0, file_size);

bmp_header_t *bmp = (bmp_header_t *)buf;
bmp->signature[0] = 'B';
bmp->signature[1] = 'M';
bmp->file_size = file_size;
bmp->data_offset = BMP_HEADER_SIZE;
bmp->info_size = 40;
bmp->width = w;
bmp->height = -(int32_t)h; // top-down
bmp->planes = 1;
bmp->bpp = 16;
bmp->compression = 3; // BI_BITFIELDS
bmp->image_size = image_size;
bmp->mask_r = 0xF800;
bmp->mask_g = 0x07E0;
bmp->mask_b = 0x001F;

// 行ごとにコピー(パディング考慮)
uint32_t src_row_bytes = w * 2;
for (uint16_t y = 0; y < h; y++) {
size_t src_offset = y * src_row_bytes;
size_t dst_offset = BMP_HEADER_SIZE + y * row_stride;
size_t copy_len = src_row_bytes;
if (src_offset + copy_len > pixel_len) {
copy_len = (src_offset < pixel_len) ? (pixel_len - src_offset) : 0;
}
if (copy_len > 0) {
memcpy(buf + dst_offset, pixels + src_offset, copy_len);
}
}

*out_len = file_size;
return buf;
}

// ---------------------------------------------------------------------------
// PGM ヘッダ生成 (Grayscale用)
// ---------------------------------------------------------------------------

/**
* @brief Grayscale 生ピクセルデータに PGM (P5) ヘッダを付与した完成ファイルを生成
* @param pixels 生ピクセルデータ
* @param pixel_len ピクセルデータのバイト数
* @param w 画像幅
* @param h 画像高さ
* @param out_len 出力ファイルサイズ
* @return malloc されたファイルデータ。呼び出し側が free する。失敗時 NULL
*/
static uint8_t *build_pgm_grayscale(const uint8_t *pixels, size_t pixel_len,
uint16_t w, uint16_t h, size_t *out_len)
{
// PGM ヘッダ: "P5\n{w} {h}\n255\n"
char hdr[32];
int hdr_len = snprintf(hdr, sizeof(hdr), "P5\n%u %u\n255\n", w, h);

size_t expected_pixels = (size_t)w * h;
size_t copy_len = (pixel_len < expected_pixels) ? pixel_len : expected_pixels;
size_t file_size = (size_t)hdr_len + copy_len;

uint8_t *buf = (uint8_t *)malloc(file_size);
if (!buf) return NULL;

memcpy(buf, hdr, hdr_len);
memcpy(buf + hdr_len, pixels, copy_len);

*out_len = file_size;
return buf;
}

/**
* @brief 受信機内部で持つ情報
*/
Expand Down Expand Up @@ -101,7 +225,32 @@ static void cast_reassembler_push_packet(const uint8_t *data, size_t 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, ctx.width, ctx.height);

// フォーマットに応じてファイルヘッダを付与
uint8_t *file_data = NULL;
size_t file_len = 0;

switch (ctx.format) {
case CAST_FMT_RGB565:
file_data = build_bmp_rgb565(ctx.buffer, final_size,
ctx.width, ctx.height, &file_len);
break;
case CAST_FMT_GRAYSCALE:
file_data = build_pgm_grayscale(ctx.buffer, final_size,
ctx.width, ctx.height, &file_len);
break;
default:
// JPEG はそのまま
break;
}

if (file_data) {
app_callback(file_data, file_len, ctx.format, ctx.width, ctx.height);
free(file_data);
} else {
// JPEG or ヘッダ生成失敗 → 生データをそのまま渡す
app_callback(ctx.buffer, final_size, ctx.format, ctx.width, ctx.height);
}
}

// callback から戻ったらミドルウェア側で解放
Expand Down
104 changes: 99 additions & 5 deletions test/host/test_cast_reassembler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ TEST_F(ReassemblerTest, LoopbackExactMtu)
std::vector<uint8_t> data(max_payload);
std::iota(data.begin(), data.end(), 0);

cast_send_frame(data.data(), data.size(), CAST_FMT_RGB565, 320, 240, &g_mock_transport);
// JPEG で送信(生データがそのまま返る)
cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, 0, 0, &g_mock_transport);
ASSERT_EQ(g_captured_packets.size(), 1u);

feed_all_packets();
Expand Down Expand Up @@ -348,20 +349,21 @@ TEST_F(ReassemblerTest, OutOfBoundsChunkIndexIgnored)
// 4. アプリ連携系:フォーマット・完了通知テスト
// ===========================================================================

// 4-1: フォーマット情報の伝搬
// 4-1: フォーマット情報の伝搬(ヘッダ付与を含む)
TEST_F(ReassemblerTest, FormatPropagation)
{
std::vector<uint8_t> data(50, 0x11);

// JPEG
// JPEG: 生データがそのまま返る
cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, 0, 0, &g_mock_transport);
feed_all_packets();
ASSERT_EQ(g_completed_frames.size(), 1u);
EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_JPEG);
EXPECT_EQ(g_completed_frames[0].width, 0);
EXPECT_EQ(g_completed_frames[0].height, 0);
EXPECT_EQ(g_completed_frames[0].data, data); // そのまま

// RGB565
// RGB565: BMP ヘッダ付き
g_captured_packets.clear();
g_completed_frames.clear();
cast_send_frame(data.data(), data.size(), CAST_FMT_RGB565, 320, 240, &g_mock_transport);
Expand All @@ -370,8 +372,12 @@ TEST_F(ReassemblerTest, FormatPropagation)
EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_RGB565);
EXPECT_EQ(g_completed_frames[0].width, 320);
EXPECT_EQ(g_completed_frames[0].height, 240);
// BMP signature "BM"
ASSERT_GE(g_completed_frames[0].data.size(), 2u);
EXPECT_EQ(g_completed_frames[0].data[0], 'B');
EXPECT_EQ(g_completed_frames[0].data[1], 'M');

// GRAYSCALE
// GRAYSCALE: PGM ヘッダ付き
g_captured_packets.clear();
g_completed_frames.clear();
cast_send_frame(data.data(), data.size(), CAST_FMT_GRAYSCALE, 160, 120, &g_mock_transport);
Expand All @@ -380,6 +386,94 @@ TEST_F(ReassemblerTest, FormatPropagation)
EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_GRAYSCALE);
EXPECT_EQ(g_completed_frames[0].width, 160);
EXPECT_EQ(g_completed_frames[0].height, 120);
// PGM signature "P5\n"
ASSERT_GE(g_completed_frames[0].data.size(), 3u);
EXPECT_EQ(g_completed_frames[0].data[0], 'P');
EXPECT_EQ(g_completed_frames[0].data[1], '5');
EXPECT_EQ(g_completed_frames[0].data[2], '\n');
}

// 4-1b: RGB565 → BMP 変換の検証
TEST_F(ReassemblerTest, BmpHeaderRgb565)
{
// 4x2 の RGB565 画像 = 16 bytes
const uint16_t w = 4, h = 2;
std::vector<uint8_t> pixels(w * h * 2);
std::iota(pixels.begin(), pixels.end(), 0);

cast_send_frame(pixels.data(), pixels.size(), CAST_FMT_RGB565, w, h, &g_mock_transport);
feed_all_packets();

ASSERT_EQ(g_completed_frames.size(), 1u);
auto &frame = g_completed_frames[0];

// BMP ヘッダサイズ = 66 bytes
const size_t BMP_HDR = 66;
ASSERT_GE(frame.data.size(), BMP_HDR);

// BMP signature
EXPECT_EQ(frame.data[0], 'B');
EXPECT_EQ(frame.data[1], 'M');

// row_stride: ((4*2+3)/4)*4 = 8 (パディングなし)
uint32_t row_stride = ((w * 2 + 3) / 4) * 4;
uint32_t expected_file_size = BMP_HDR + row_stride * h;
EXPECT_EQ(frame.data.size(), expected_file_size);

// ファイルサイズフィールド (offset 2, 4 bytes LE)
uint32_t file_size_field;
memcpy(&file_size_field, &frame.data[2], 4);
EXPECT_EQ(file_size_field, expected_file_size);

// データオフセット (offset 10, 4 bytes LE)
uint32_t data_offset;
memcpy(&data_offset, &frame.data[10], 4);
EXPECT_EQ(data_offset, (uint32_t)BMP_HDR);

// bpp (offset 28, 2 bytes LE)
uint16_t bpp;
memcpy(&bpp, &frame.data[28], 2);
EXPECT_EQ(bpp, 16);

// ピクセルデータが BMP_HDR 以降に含まれている
// 行0のピクセルデータを確認
for (size_t i = 0; i < w * 2; i++) {
EXPECT_EQ(frame.data[BMP_HDR + i], pixels[i])
<< "Mismatch at pixel byte " << i;
}
}

// 4-1c: Grayscale → PGM 変換の検証
TEST_F(ReassemblerTest, PgmHeaderGrayscale)
{
// 4x2 の Grayscale 画像 = 8 bytes
const uint16_t w = 4, h = 2;
std::vector<uint8_t> pixels(w * h);
std::iota(pixels.begin(), pixels.end(), 100);

cast_send_frame(pixels.data(), pixels.size(), CAST_FMT_GRAYSCALE, w, h, &g_mock_transport);
feed_all_packets();

ASSERT_EQ(g_completed_frames.size(), 1u);
auto &frame = g_completed_frames[0];

// PGM ヘッダ: "P5\n4 2\n255\n" = 12 bytes
std::string expected_hdr = "P5\n4 2\n255\n";
ASSERT_GE(frame.data.size(), expected_hdr.size() + pixels.size());

// ヘッダ部分の一致
std::string actual_hdr(frame.data.begin(), frame.data.begin() + expected_hdr.size());
EXPECT_EQ(actual_hdr, expected_hdr);

// ピクセルデータの一致
size_t offset = expected_hdr.size();
for (size_t i = 0; i < pixels.size(); i++) {
EXPECT_EQ(frame.data[offset + i], pixels[i])
<< "Mismatch at pixel byte " << i;
}

// 全体サイズ
EXPECT_EQ(frame.data.size(), expected_hdr.size() + pixels.size());
}

// 4-2: 最終サイズの完全一致(連番データ)
Expand Down