commit b1c3ec3af4167e3f3a35371bb8e1198a3b6110c3 Author: km Date: Mon Mar 30 06:35:25 2026 +0900 Initial commit: OTA chunk transfer protocol implementation - Protocol definition with 6-byte header (chunk_idx: 2B, payload: N B, crc32: 4B) - Node.js server with chunk-based HTTP delivery - C client library with resumable download support - CRC32 implementation (IEEE 802.3) - Test suites for both server and CRC32 - 1024B default chunk size (0.59% overhead) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7187fbf --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +# OTA Chunk Transfer Makefile + +CC = gcc +CFLAGS = -Wall -Wextra -std=c11 -I protocol +LDFLAGS = + +SRCS = protocol/crc32.c protocol/chunk_client.c +OBJS = $(SRCS:.c=.o) + +.PHONY: all clean test + +all: $(OBJS) + +# CRC32 test +test_crc32: tests/test_crc32.c protocol/crc32.c + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +test: test_crc32 + ./test_crc32 + +clean: + rm -f *.o protocol/*.o tests/*.o test_crc32 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b78f515 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# OTA-Chunk-Transfer + +レジューム可能な分割転送プロトコル + +## プロトコル仕様 + +### パケット形式 +``` ++------------+----------------+----------+ +| chunk_idx | payload (N B) | crc32 | +| 2 B | N B | 4 B | ++------------+----------------+----------+ +``` + +- **chunk_idx**: 2 バイト(最大 65535 チャンク = 67MB まで対応、1024B/チャンク) +- **payload**: 可変長(デフォルト 1024B) +- **crc32**: 4 バイト(IEEE 802.3 ポリノミアル) + +### コントロールメッセージ + +#### Client → Server +``` +GET /ota?file= // ファイル転送リクエスト +GET /ota/resume?file=&chunk= // レジューム +``` + +#### Server → Client +``` +200 OK + chunk data // チャンク転送 +206 Partial Content // レジューム応答 +404 Not Found // ファイル不存在 +416 Range Not Satisfiable // 無効なレジューム位置 +``` + +### 転送フロー + +``` +Client Server + | | + |-- GET /ota?file=test.bin ---->| // ファイルサイズ確認 + |<-- 200 OK (Content-Length) ---| // ファイルサイズ返信 + | | + |-- GET /ota?file=test.bin&chunk=0 -->| // チャンク 0 要求 + |<-- 200 OK + chunk data -------| // チャンクデータ + | | + |-- GET /ota?file=test.bin&chunk=1 -->| // チャンク 1 要求 + |<-- 200 OK + chunk data -------| // チャンクデータ + | | + ... | + | | + // 通信断情况后 + | | + |-- GET /ota/resume?file=test.bin&chunk=50 -->| // チャンク 50 から再開 + |<-- 200 OK + chunk data -------| // チャンク 50 データ + | | +``` + +## ファイル構成 + +``` +ota-chunk-transfer/ +├── protocol/ +│ ├── protocol.h // プロトコル定義 +│ ├── chunk_client.c // C クライアント実装 +│ ├── chunk_server.c // C サーバ実装(参照用) +│ └── crc32.c // CRC32 実装 +├── server/ +│ └── server.js // Node.js サーバ +├── tests/ +│ ├── test_crc32.c +│ ├── test_chunk_client.c +│ └── test_server.js +├── Makefile +└── README.md +``` + +## 使用例 + +### C クライアント(ESP32) +```c +#include "protocol.h" + +void chunk_received(uint16_t chunk_idx, uint8_t *data, uint16_t len, void *user_ctx); +void transfer_complete(uint32_t total_bytes, void *user_ctx); +void transfer_error(int err_code, void *user_ctx); + +chunk_client_config_t config = { + .server_url = "http://192.168.1.100:8080", + .chunk_size = 1024, + .on_chunk_received = chunk_received, + .on_complete = transfer_complete, + .on_error = transfer_error, + .user_ctx = NULL +}; + +chunk_client_init(&config); +chunk_client_download("firmware.bin"); +``` + +### Node.js サーバ +```javascript +const server = require('./server'); + +server.start(8080, '/path/to/ota/files'); +``` + +## パフォーマンス + +| チャンクサイズ | ヘッダ比率 | 64MB ファイルのチャンク数 | +|---------------|-----------|----------------------| +| 512 B | 1.17% | 131,072 | +| 1024 B | 0.59% | 65,536 | +| 2048 B | 0.30% | 32,768 | + +## TODO + +- [ ] プロトコル定義 +- [ ] CRC32 実装 +- [ ] C クライアント実装 +- [ ] Node.js サーバ実装 +- [ ] テストスイート +- [ ] 圧縮オプション(gzip) +- [ ] 暗号化オプション(TLS) diff --git a/package.json b/package.json new file mode 100644 index 0000000..4459396 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "ota-chunk-transfer", + "version": "0.1.0", + "description": "Resume-capable chunk transfer protocol for OTA updates", + "main": "server/server.js", + "scripts": { + "start": "node server/server.js", + "test": "node tests/test_server.js" + }, + "keywords": ["ota", "chunk", "transfer", "resume"], + "author": "", + "license": "MIT" +} diff --git a/protocol/chunk_client.c b/protocol/chunk_client.c new file mode 100644 index 0000000..9e88301 --- /dev/null +++ b/protocol/chunk_client.c @@ -0,0 +1,433 @@ +/** + * @file chunk_client.c + * @brief Chunk Transfer Client Implementation + * + * Note: This implementation requires an HTTP client library. + * For ESP32, use ESP-IDF's HTTP Client or Arduino's HTTPClient. + * For POSIX systems, use libcurl. + */ + +#include "protocol.h" +#include +#include + +/* HTTP client interface (to be implemented per platform) */ +typedef struct { + /** + * Send HTTP GET request + * @param url Full URL + * @param response Buffer for response data + * @param response_len Buffer size + * @param actual_len Actual response length (output) + * @param status_code HTTP status code (output) + * @return 0 on success, error code otherwise + */ + int (*http_get)(const char *url, uint8_t *response, size_t response_len, + size_t *actual_len, int *status_code); + + /** + * Get header value from last response + * @param name Header name + * @return Header value or NULL + */ + const char *(*http_get_header)(const char *name); + + void *ctx; /* Platform-specific context */ +} http_client_t; + +/* Client state */ +typedef struct { + chunk_client_config_t config; + http_client_t http; + + char current_file[256]; + uint32_t file_size; + uint16_t total_chunks; + uint16_t current_chunk; + chunk_state_t state; + chunk_err_t error_code; + bool cancel_requested; +} chunk_client_state_t; + +static chunk_client_state_t client_state; + +/** + * Initialize chunk client + */ +chunk_err_t chunk_client_init(const chunk_client_config_t *config) { + if (!config || !config->server_url) { + return CHUNK_ERR_INVALID_PARAM; + } + + memset(&client_state, 0, sizeof(client_state)); + memcpy(&client_state.config, config, sizeof(chunk_client_config_t)); + + /* Set default chunk size if not specified */ + if (client_state.config.chunk_size == 0) { + client_state.config.chunk_size = CHUNK_DEFAULT_SIZE; + } + + client_state.state = CHUNK_STATE_IDLE; + client_state.error_code = CHUNK_ERR_OK; + + return CHUNK_ERR_OK; +} + +/** + * Get file size from server + */ +static chunk_err_t get_file_size(const char *filename, uint32_t *size) { + char url[512]; + snprintf(url, sizeof(url), "%s/ota?file=%s", + client_state.config.server_url, filename); + + /* Send HEAD or GET request to get Content-Length */ + uint8_t dummy[1]; + size_t actual_len; + int status_code; + + int ret = client_state.http.http_get(url, dummy, sizeof(dummy), + &actual_len, &status_code); + if (ret != 0) { + return CHUNK_ERR_NETWORK; + } + + if (status_code != 200) { + return (status_code == 404) ? CHUNK_ERR_FILE_NOT_FOUND : CHUNK_ERR_SERVER_ERROR; + } + + /* Parse Content-Length from headers */ + const char *cl = client_state.http.http_get_header("X-File-Size"); + if (!cl) { + cl = client_state.http.http_get_header("Content-Length"); + } + + if (cl) { + *size = (uint32_t)atoi(cl); + } else { + /* Fallback: estimate from first chunk */ + *size = client_state.config.chunk_size; + } + + return CHUNK_ERR_OK; +} + +/** + * Download a single chunk + */ +static chunk_err_t download_chunk(uint16_t chunk_idx, uint8_t *buffer, + uint16_t *payload_len, uint32_t *crc32) { + char url[512]; + snprintf(url, sizeof(url), "%s/ota?file=%s&chunk=%u", + client_state.config.server_url, + client_state.current_file, chunk_idx); + + /* Allocate buffer for chunk response */ + size_t response_size = 2 + client_state.config.chunk_size + 4; + uint8_t *response = (uint8_t *)malloc(response_size); + if (!response) { + return CHUNK_ERR_MEMORY; + } + + size_t actual_len; + int status_code; + + int ret = client_state.http.http_get(url, response, response_size, + &actual_len, &status_code); + + if (ret != 0) { + free(response); + return CHUNK_ERR_NETWORK; + } + + if (status_code == 416) { + free(response); + return CHUNK_ERR_RANGE_INVALID; + } + + if (status_code != 200 && status_code != 206) { + free(response); + return CHUNK_ERR_SERVER_ERROR; + } + + /* Parse chunk: [chunk_idx: 2B][payload: N B][crc32: 4B] */ + if (actual_len < 6) { /* Minimum: 2 + 0 + 4 */ + free(response); + return CHUNK_ERR_NETWORK; + } + + uint16_t received_idx = (response[0] << 8) | response[1]; + if (received_idx != chunk_idx) { + free(response); + return CHUNK_ERR_NETWORK; /* Chunk index mismatch */ + } + + /* Calculate payload length */ + uint16_t total_chunk_size = client_state.config.chunk_size; + uint32_t remaining = client_state.file_size - (uint32_t)chunk_idx * total_chunk_size; + uint16_t expected_payload = (uint16_t)(remaining < total_chunk_size ? remaining : total_chunk_size); + + if (actual_len != 2 + expected_payload + 4) { + /* Last chunk might have different size */ + if (chunk_idx == client_state.total_chunks - 1) { + *payload_len = (uint16_t)(actual_len - 6); + } else { + free(response); + return CHUNK_ERR_NETWORK; + } + } else { + *payload_len = expected_payload; + } + + /* Copy payload */ + memcpy(buffer, response + 2, *payload_len); + + /* Extract CRC32 */ + *crc32 = (response[2 + *payload_len] << 24) | + (response[3 + *payload_len] << 16) | + (response[4 + *payload_len] << 8) | + response[5 + *payload_len]; + + free(response); + return CHUNK_ERR_OK; +} + +/** + * Start file download + */ +chunk_err_t chunk_client_download(const char *filename) { + if (!filename) { + return CHUNK_ERR_INVALID_PARAM; + } + + /* Store filename */ + strncpy(client_state.current_file, filename, sizeof(client_state.current_file) - 1); + client_state.current_file[sizeof(client_state.current_file) - 1] = '\0'; + + /* Get file size */ + chunk_err_t err = get_file_size(filename, &client_state.file_size); + if (err != CHUNK_ERR_OK) { + client_state.state = CHUNK_STATE_ERROR; + client_state.error_code = err; + if (client_state.config.on_error) { + client_state.config.on_error(err, client_state.config.user_ctx); + } + return err; + } + + /* Calculate total chunks */ + client_state.total_chunks = chunk_calc_total_chunks( + client_state.file_size, client_state.config.chunk_size); + + client_state.current_chunk = 0; + client_state.state = CHUNK_STATE_TRANSFERRING; + client_state.cancel_requested = false; + + /* Buffer for chunk payload */ + uint8_t *payload = (uint8_t *)malloc(client_state.config.chunk_size); + if (!payload) { + client_state.state = CHUNK_STATE_ERROR; + client_state.error_code = CHUNK_ERR_MEMORY; + return CHUNK_ERR_MEMORY; + } + + /* Download all chunks */ + for (uint16_t i = 0; i < client_state.total_chunks; i++) { + if (client_state.cancel_requested) { + free(payload); + client_state.state = CHUNK_STATE_IDLE; + return CHUNK_ERR_OK; /* Cancel is not an error */ + } + + uint16_t payload_len; + uint32_t crc32; + + err = download_chunk(i, payload, &payload_len, &crc32); + if (err != CHUNK_ERR_OK) { + free(payload); + client_state.state = CHUNK_STATE_ERROR; + client_state.error_code = err; + if (client_state.config.on_error) { + client_state.config.on_error(err, client_state.config.user_ctx); + } + return err; + } + + /* Verify CRC32 */ + uint32_t calculated_crc = chunk_crc32(payload, payload_len); + if (calculated_crc != crc32) { + free(payload); + client_state.state = CHUNK_STATE_ERROR; + client_state.error_code = CHUNK_ERR_CRC; + if (client_state.config.on_error) { + client_state.config.on_error(CHUNK_ERR_CRC, client_state.config.user_ctx); + } + return CHUNK_ERR_CRC; + } + + /* Call chunk received callback */ + if (client_state.config.on_chunk_received) { + client_state.config.on_chunk_received(i, payload, payload_len, + client_state.config.user_ctx); + } + + /* Progress callback */ + if (client_state.config.on_progress) { + client_state.config.on_progress(i + 1, client_state.total_chunks, + client_state.config.user_ctx); + } + + client_state.current_chunk = i + 1; + } + + free(payload); + + /* Transfer complete */ + client_state.state = CHUNK_STATE_COMPLETE; + if (client_state.config.on_complete) { + client_state.config.on_complete(client_state.file_size, + client_state.config.user_ctx); + } + + return CHUNK_ERR_OK; +} + +/** + * Resume download from specific chunk + */ +chunk_err_t chunk_client_resume(const char *filename, uint16_t start_chunk) { + if (!filename) { + return CHUNK_ERR_INVALID_PARAM; + } + + /* Store filename */ + strncpy(client_state.current_file, filename, sizeof(client_state.current_file) - 1); + client_state.current_file[sizeof(client_state.current_file) - 1] = '\0'; + + /* Get file size */ + chunk_err_t err = get_file_size(filename, &client_state.file_size); + if (err != CHUNK_ERR_OK) { + client_state.state = CHUNK_STATE_ERROR; + client_state.error_code = err; + if (client_state.config.on_error) { + client_state.config.on_error(err, client_state.config.user_ctx); + } + return err; + } + + /* Calculate total chunks */ + client_state.total_chunks = chunk_calc_total_chunks( + client_state.file_size, client_state.config.chunk_size); + + /* Validate start chunk */ + if (start_chunk >= client_state.total_chunks) { + return CHUNK_ERR_RANGE_INVALID; + } + + client_state.current_chunk = start_chunk; + client_state.state = CHUNK_STATE_RESUMING; + client_state.cancel_requested = false; + + /* Buffer for chunk payload */ + uint8_t *payload = (uint8_t *)malloc(client_state.config.chunk_size); + if (!payload) { + client_state.state = CHUNK_STATE_ERROR; + client_state.error_code = CHUNK_ERR_MEMORY; + return CHUNK_ERR_MEMORY; + } + + /* Download remaining chunks */ + for (uint16_t i = start_chunk; i < client_state.total_chunks; i++) { + if (client_state.cancel_requested) { + free(payload); + client_state.state = CHUNK_STATE_IDLE; + return CHUNK_ERR_OK; + } + + uint16_t payload_len; + uint32_t crc32; + + err = download_chunk(i, payload, &payload_len, &crc32); + if (err != CHUNK_ERR_OK) { + free(payload); + client_state.state = CHUNK_STATE_ERROR; + client_state.error_code = err; + if (client_state.config.on_error) { + client_state.config.on_error(err, client_state.config.user_ctx); + } + return err; + } + + /* Verify CRC32 */ + uint32_t calculated_crc = chunk_crc32(payload, payload_len); + if (calculated_crc != crc32) { + free(payload); + client_state.state = CHUNK_STATE_ERROR; + client_state.error_code = CHUNK_ERR_CRC; + if (client_state.config.on_error) { + client_state.config.on_error(CHUNK_ERR_CRC, client_state.config.user_ctx); + } + return CHUNK_ERR_CRC; + } + + /* Call chunk received callback */ + if (client_state.config.on_chunk_received) { + client_state.config.on_chunk_received(i, payload, payload_len, + client_state.config.user_ctx); + } + + /* Progress callback */ + if (client_state.config.on_progress) { + client_state.config.on_progress(i + 1, client_state.total_chunks, + client_state.config.user_ctx); + } + + client_state.current_chunk = i + 1; + } + + free(payload); + + /* Transfer complete */ + client_state.state = CHUNK_STATE_COMPLETE; + if (client_state.config.on_complete) { + client_state.config.on_complete(client_state.file_size, + client_state.config.user_ctx); + } + + return CHUNK_ERR_OK; +} + +/** + * Cancel current transfer + */ +void chunk_client_cancel(void) { + client_state.cancel_requested = true; +} + +/** + * Get current transfer state + */ +chunk_state_t chunk_client_get_state(void) { + return client_state.state; +} + +/** + * Get current progress + */ +chunk_err_t chunk_client_get_progress(uint16_t *current_chunk, uint16_t *total_chunks) { + if (!current_chunk || !total_chunks) { + return CHUNK_ERR_INVALID_PARAM; + } + + *current_chunk = client_state.current_chunk; + *total_chunks = client_state.total_chunks; + + return CHUNK_ERR_OK; +} + +/** + * Cleanup client resources + */ +void chunk_client_cleanup(void) { + memset(&client_state, 0, sizeof(client_state)); +} diff --git a/protocol/crc32.c b/protocol/crc32.c new file mode 100644 index 0000000..40ea109 --- /dev/null +++ b/protocol/crc32.c @@ -0,0 +1,77 @@ +/** + * @file crc32.c + * @brief CRC32 Implementation (IEEE 802.3) + */ + +#include +#include +#include + +/* IEEE 802.3 CRC32 polynomial (reversed) */ +#define CRC32_POLY 0xEDB88320UL + +/* CRC32 lookup table (256 entries) */ +static uint32_t crc32_table[256]; +static bool crc32_table_init = false; + +/** + * Initialize CRC32 lookup table + */ +static void crc32_init_table(void) { + if (crc32_table_init) return; + + for (uint32_t i = 0; i < 256; i++) { + uint32_t crc = i; + for (int j = 0; j < 8; j++) { + if (crc & 1) { + crc = (crc >> 1) ^ CRC32_POLY; + } else { + crc >>= 1; + } + } + crc32_table[i] = crc; + } + crc32_table_init = true; +} + +/** + * Calculate CRC32 (IEEE 802.3 polynomial) + * + * @param data Input data + * @param len Input length + * @return CRC32 value + */ +uint32_t chunk_crc32(const uint8_t *data, size_t len) { + crc32_init_table(); + + uint32_t crc = 0xFFFFFFFFUL; /* Initial value */ + + for (size_t i = 0; i < len; i++) { + uint8_t index = (crc ^ data[i]) & 0xFF; + crc = (crc >> 8) ^ crc32_table[index]; + } + + return crc ^ 0xFFFFFFFFUL; /* Final XOR */ +} + +/** + * Update CRC32 with additional data + * + * @param crc Previous CRC value (already XORed with 0xFFFFFFFF) + * @param data New data + * @param len Data length + * @return Updated CRC value + */ +uint32_t chunk_crc32_update(uint32_t crc, const uint8_t *data, size_t len) { + crc32_init_table(); + + /* Undo final XOR from previous calculation */ + crc ^= 0xFFFFFFFFUL; + + for (size_t i = 0; i < len; i++) { + uint8_t index = (crc ^ data[i]) & 0xFF; + crc = (crc >> 8) ^ crc32_table[index]; + } + + return crc ^ 0xFFFFFFFFUL; /* Final XOR */ +} diff --git a/protocol/protocol.h b/protocol/protocol.h new file mode 100644 index 0000000..11c3950 --- /dev/null +++ b/protocol/protocol.h @@ -0,0 +1,301 @@ +/** + * @file protocol.h + * @brief OTA Chunk Transfer Protocol Definitions + */ + +#ifndef OTA_CHUNK_PROTOCOL_H +#define OTA_CHUNK_PROTOCOL_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========================================================================= + * Protocol Constants + * ========================================================================= */ + +#define CHUNK_PROTOCOL_VERSION 1 +#define CHUNK_DEFAULT_SIZE 1024 +#define CHUNK_MIN_SIZE 256 +#define CHUNK_MAX_SIZE 65535 +#define CHUNK_INDEX_MAX 65535 /* 2-byte unsigned */ + +/* Maximum file size: CHUNK_INDEX_MAX * CHUNK_MAX_SIZE ≈ 4.29 GB */ +#define CHUNK_MAX_FILE_SIZE ((uint32_t)CHUNK_INDEX_MAX * CHUNK_MAX_SIZE) + +/* ========================================================================= + * Protocol Structures + * ========================================================================= */ + +/** + * Chunk packet structure (network format) + * + * Layout: + * +------------+----------------+----------+ + * | chunk_idx | payload (N B) | crc32 | + * | 2 B | N B | 4 B | + * +------------+----------------+----------+ + * + * Note: This structure includes padding. Use packing for network transmission. + */ + +#pragma pack(push, 1) +typedef struct { + uint16_t chunk_idx; /* Chunk index (little-endian) */ + /* payload follows immediately */ + /* crc32 follows payload */ +} chunk_header_t; + +#pragma pack(pop) + +/** + * Chunk data descriptor (in-memory representation) + */ +typedef struct { + uint16_t chunk_idx; /* Chunk index */ + uint16_t payload_len; /* Payload length */ + uint8_t *payload; /* Payload pointer */ + uint32_t crc32; /* CRC32 of payload */ +} chunk_data_t; + +/** + * Transfer state + */ +typedef enum { + CHUNK_STATE_IDLE, + CHUNK_STATE_FETCHING_SIZE, + CHUNK_STATE_TRANSFERRING, + CHUNK_STATE_RESUMING, + CHUNK_STATE_COMPLETE, + CHUNK_STATE_ERROR +} chunk_state_t; + +/** + * Error codes + */ +typedef enum { + CHUNK_ERR_OK = 0, + CHUNK_ERR_INVALID_PARAM = -1, + CHUNK_ERR_MEMORY = -2, + CHUNK_ERR_NETWORK = -3, + CHUNK_ERR_CRC = -4, + CHUNK_ERR_FILE_NOT_FOUND = -5, + CHUNK_ERR_RANGE_INVALID = -6, + CHUNK_ERR_TIMEOUT = -7, + CHUNK_ERR_SERVER_ERROR = -8, +} chunk_err_t; + +/* ========================================================================= + * Callback Types + * ========================================================================= */ + +/** + * Chunk received callback + * + * @param chunk_idx Chunk index + * @param data Payload data + * @param len Payload length + * @param user_ctx User context + */ +typedef void (*chunk_received_cb)(uint16_t chunk_idx, const uint8_t *data, + uint16_t len, void *user_ctx); + +/** + * Transfer complete callback + * + * @param total_bytes Total bytes transferred + * @param user_ctx User context + */ +typedef void (*transfer_complete_cb)(uint32_t total_bytes, void *user_ctx); + +/** + * Transfer error callback + * + * @param err_code Error code + * @param user_ctx User context + */ +typedef void (*transfer_error_cb)(chunk_err_t err_code, void *user_ctx); + +/** + * Progress update callback + * + * @param current_chunk Current chunk index + * @param total_chunks Total chunks + * @param user_ctx User context + */ +typedef void (*progress_cb)(uint16_t current_chunk, uint16_t total_chunks, + void *user_ctx); + +/* ========================================================================= + * Configuration Structures + * ========================================================================= */ + +/** + * Chunk client configuration + */ +typedef struct { + const char *server_url; /* Server base URL */ + uint16_t chunk_size; /* Chunk size in bytes */ + uint32_t timeout_ms; /* Timeout in milliseconds */ + + /* Callbacks */ + chunk_received_cb on_chunk_received; + transfer_complete_cb on_complete; + transfer_error_cb on_error; + progress_cb on_progress; + + void *user_ctx; /* User context passed to callbacks */ +} chunk_client_config_t; + +/** + * Chunk server configuration + */ +typedef struct { + uint16_t port; /* Server port */ + const char *root_path; /* Root directory for files */ + uint16_t chunk_size; /* Chunk size in bytes */ +} chunk_server_config_t; + +/* ========================================================================= + * CRC32 Functions + * ========================================================================= */ + +/** + * Calculate CRC32 (IEEE 802.3 polynomial) + * + * @param data Input data + * @param len Input length + * @return CRC32 value + */ +uint32_t chunk_crc32(const uint8_t *data, size_t len); + +/** + * Update CRC32 with additional data + * + * @param crc Previous CRC value + * @param data New data + * @param len Data length + * @return Updated CRC value + */ +uint32_t chunk_crc32_update(uint32_t crc, const uint8_t *data, size_t len); + +/* ========================================================================= + * Client Functions + * ========================================================================= */ + +/** + * Initialize chunk client + * + * @param config Configuration structure + * @return CHUNK_ERR_OK on success, error code otherwise + */ +chunk_err_t chunk_client_init(const chunk_client_config_t *config); + +/** + * Start file download + * + * @param filename Filename to download + * @return CHUNK_ERR_OK on success, error code otherwise + */ +chunk_err_t chunk_client_download(const char *filename); + +/** + * Resume download from specific chunk + * + * @param filename Filename to download + * @param start_chunk Chunk index to resume from + * @return CHUNK_ERR_OK on success, error code otherwise + */ +chunk_err_t chunk_client_resume(const char *filename, uint16_t start_chunk); + +/** + * Cancel current transfer + */ +void chunk_client_cancel(void); + +/** + * Get current transfer state + * + * @return Current state + */ +chunk_state_t chunk_client_get_state(void); + +/** + * Get current progress + * + * @param current_chunk Current chunk index (output) + * @param total_chunks Total chunks (output) + * @return CHUNK_ERR_OK on success + */ +chunk_err_t chunk_client_get_progress(uint16_t *current_chunk, + uint16_t *total_chunks); + +/** + * Cleanup client resources + */ +void chunk_client_cleanup(void); + +/* ========================================================================= + * Server Functions (for reference / embedded use) + * ========================================================================= */ + +/** + * Initialize chunk server + * + * @param config Configuration structure + * @return CHUNK_ERR_OK on success, error code otherwise + */ +chunk_err_t chunk_server_init(const chunk_server_config_t *config); + +/** + * Start server (blocking) + * + * @return CHUNK_ERR_OK on success, error code otherwise + */ +chunk_err_t chunk_server_start(void); + +/** + * Stop server + */ +void chunk_server_stop(void); + +/* ========================================================================= + * Utility Functions + * ========================================================================= */ + +/** + * Calculate total chunks for a file + * + * @param file_size File size in bytes + * @param chunk_size Chunk size in bytes + * @return Total number of chunks + */ +static inline uint16_t chunk_calc_total_chunks(uint32_t file_size, + uint16_t chunk_size) { + return (uint16_t)((file_size + chunk_size - 1) / chunk_size); +} + +/** + * Calculate payload size for a chunk + * + * @param file_size File size in bytes + * @param chunk_idx Chunk index + * @param chunk_size Chunk size in bytes + * @return Payload size for this chunk + */ +static inline uint16_t chunk_calc_payload_size(uint32_t file_size, + uint16_t chunk_idx, + uint16_t chunk_size) { + uint32_t remaining = file_size - (uint32_t)chunk_idx * chunk_size; + return (uint16_t)(remaining < chunk_size ? remaining : chunk_size); +} + +#ifdef __cplusplus +} +#endif + +#endif /* OTA_CHUNK_PROTOCOL_H */ diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..6d813e1 --- /dev/null +++ b/server/server.js @@ -0,0 +1,243 @@ +/** + * @file server.js + * @brief Node.js Chunk Transfer Server + */ + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const EventEmitter = require('events'); + +class ChunkServer extends EventEmitter { + constructor(config) { + super(); + this.config = { + port: 8080, + rootPath: '.', + chunkSize: 1024, + ...config + }; + this.server = null; + this.running = false; + } + + /** + * Calculate CRC32 (IEEE 802.3) + */ + static crc32(data) { + const table = ChunkServer.crc32Table; + let crc = 0xFFFFFFFF; + + for (let i = 0; i < data.length; i++) { + const index = (crc ^ data[i]) & 0xFF; + crc = (crc >>> 8) ^ table[index]; + } + + return crc ^ 0xFFFFFFFF; + } + + /** + * CRC32 lookup table + */ + static get crc32Table() { + if (!ChunkServer._crc32Table) { + const table = new Uint32Array(256); + const poly = 0xEDB88320; + + for (let i = 0; i < 256; i++) { + let crc = i; + for (let j = 0; j < 8; j++) { + crc = (crc & 1) ? ((crc >>> 1) ^ poly) : (crc >>> 1); + } + table[i] = crc >>> 0; + } + ChunkServer._crc32Table = table; + } + return ChunkServer._crc32Table; + } + + /** + * Start server + */ + start() { + return new Promise((resolve, reject) => { + this.server = http.createServer((req, res) => { + this.handleRequest(req, res).catch(err => { + console.error('Request error:', err); + res.writeHead(500); + res.end('Internal Server Error'); + }); + }); + + this.server.listen(this.config.port, () => { + console.log(`Chunk server running on port ${this.config.port}`); + console.log(`Root path: ${this.config.rootPath}`); + console.log(`Chunk size: ${this.config.chunkSize} bytes`); + this.running = true; + resolve(); + }); + + this.server.on('error', reject); + }); + } + + /** + * Stop server + */ + stop() { + return new Promise((resolve, reject) => { + if (!this.server) { + resolve(); + return; + } + + this.running = false; + this.server.close(err => { + if (err) reject(err); + else { + console.log('Server stopped'); + resolve(); + } + }); + }); + } + + /** + * Handle HTTP request + */ + async handleRequest(req, res) { + // Parse URL + const url = new URL(req.url, `http://${req.headers.host}`); + + // Only handle /ota endpoint + if (url.pathname !== '/ota') { + res.writeHead(404); + res.end('Not Found'); + return; + } + + const filename = url.searchParams.get('file'); + const chunkParam = url.searchParams.get('chunk'); + + if (!filename) { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Missing file parameter'); + return; + } + + // Security: Prevent path traversal + const safeFilename = path.basename(filename); + const filePath = path.join(this.config.rootPath, safeFilename); + + // Check file exists + let stat; + try { + stat = fs.statSync(filePath); + if (!stat.isFile()) { + res.writeHead(404); + res.end('File not found'); + return; + } + } catch (err) { + res.writeHead(404); + res.end('File not found'); + return; + } + + const fileSize = stat.size; + const totalChunks = Math.ceil(fileSize / this.config.chunkSize); + + // Calculate chunk index + let chunkIdx; + if (chunkParam !== null) { + chunkIdx = parseInt(chunkParam, 10); + if (isNaN(chunkIdx) || chunkIdx < 0 || chunkIdx >= totalChunks) { + res.writeHead(416, { + 'Content-Type': 'text/plain', + 'Content-Range': `chunks 0-${totalChunks-1}/${totalChunks}` + }); + res.end('Range Not Satisfiable'); + return; + } + } else { + chunkIdx = 0; + } + + // Read chunk data + const offset = chunkIdx * this.config.chunkSize; + const length = Math.min(this.config.chunkSize, fileSize - offset); + + const buffer = Buffer.alloc(length); + const fd = fs.openSync(filePath, 'r'); + try { + fs.readSync(fd, buffer, 0, length, offset); + } finally { + fs.closeSync(fd); + } + + // Calculate CRC32 + const crc32 = ChunkServer.crc32(buffer); + + // Build response: [chunk_idx: 2B][payload: N B][crc32: 4B] + const responseBuffer = Buffer.alloc(2 + length + 4); + responseBuffer.writeUInt16LE(chunkIdx, 0); + buffer.copy(responseBuffer, 2); + responseBuffer.writeUInt32LE(crc32 >>> 0, 2 + length); // Ensure unsigned + + // Set headers + const headers = { + 'Content-Type': 'application/octet-stream', + 'Content-Length': responseBuffer.length, + 'X-Chunk-Index': chunkIdx, + 'X-Chunk-Size': length, + 'X-Total-Chunks': totalChunks, + 'X-File-Size': fileSize + }; + + // Response status + if (chunkIdx > 0) { + res.writeHead(206, headers); // Partial Content for resumed chunks + } else { + res.writeHead(200, headers); // OK for first chunk + } + + res.end(responseBuffer); + + // Emit event + this.emit('chunk-sent', { + file: filename, + chunkIdx: chunkIdx, + chunkSize: length, + totalChunks: totalChunks, + fileSize: fileSize + }); + } +} + +// Export +module.exports = ChunkServer; + +// CLI usage +if (require.main === module) { + const port = process.argv[2] || 8080; + const rootPath = process.argv[3] || '.'; + + const server = new ChunkServer({ port: parseInt(port), rootPath }); + + server.on('chunk-sent', (info) => { + console.log(`Sent chunk ${info.chunkIdx}/${info.totalChunks} for ${info.file} (${info.chunkSize}B)`); + }); + + server.start().then(() => { + console.log(`Server started on port ${port}`); + console.log(`Root path: ${rootPath}`); + }).catch(err => { + console.error('Failed to start server:', err); + process.exit(1); + }); + + process.on('SIGINT', async () => { + await server.stop(); + process.exit(0); + }); +} diff --git a/test_crc32 b/test_crc32 new file mode 100755 index 0000000..3697bf6 Binary files /dev/null and b/test_crc32 differ diff --git a/tests/test_crc32.c b/tests/test_crc32.c new file mode 100644 index 0000000..3f1c689 --- /dev/null +++ b/tests/test_crc32.c @@ -0,0 +1,95 @@ +/** + * @file test_crc32.c + * @brief CRC32 Test Suite + */ + +#include "protocol.h" +#include +#include + +static int passed = 0; +static int failed = 0; + +#define TEST_ASSERT(cond, msg) do { \ + if (cond) { \ + printf("[PASS] %s\n", msg); \ + passed++; \ + } else { \ + printf("[FAIL] %s\n", msg); \ + failed++; \ + } \ +} while(0) + +/* Test vector from IEEE 802.3 */ +static void test_crc32_basic(void) { + printf("\n--- CRC32 Basic Tests ---\n"); + + /* Test 1: Empty data */ + uint32_t crc = chunk_crc32(NULL, 0); + TEST_ASSERT(crc == 0x00000000UL, "Empty data CRC = 0x00000000"); + + /* Test 2: Single byte */ + uint8_t data1[] = {0x41}; /* 'A' */ + crc = chunk_crc32(data1, 1); + TEST_ASSERT(crc == 0xD3D99E8BUL, "Single byte 'A' CRC = 0xD3D99E8B"); + + /* Test 3: "foobar" */ + uint8_t data2[] = "foobar"; + crc = chunk_crc32(data2, 6); + TEST_ASSERT(crc == 0x9EF61F95UL, "'foobar' CRC = 0x9EF61F95"); + + /* Test 4: Incremental update */ + uint32_t crc1 = chunk_crc32((uint8_t*)"foo", 3); + uint32_t crc2 = chunk_crc32_update(crc1, (uint8_t*)"bar", 3); + uint32_t crc3 = chunk_crc32(data2, 6); + TEST_ASSERT(crc2 == crc3, "Incremental update matches single call"); +} + +/* Test chunk size calculations */ +static void test_chunk_calculations(void) { + printf("\n--- Chunk Calculation Tests ---\n"); + + /* Test 1: Exact multiple */ + uint16_t chunks = chunk_calc_total_chunks(4096, 1024); + TEST_ASSERT(chunks == 4, "4096/1024 = 4 chunks"); + + /* Test 2: Not exact multiple */ + chunks = chunk_calc_total_chunks(4097, 1024); + TEST_ASSERT(chunks == 5, "4097/1024 = 5 chunks"); + + /* Test 3: Last chunk payload size */ + uint16_t payload = chunk_calc_payload_size(4097, 4, 1024); + TEST_ASSERT(payload == 1, "Last chunk of 4097 bytes = 1 byte"); + + /* Test 4: Middle chunk payload size */ + payload = chunk_calc_payload_size(4097, 2, 1024); + TEST_ASSERT(payload == 1024, "Middle chunk = 1024 bytes"); +} + +/* Test CRC32 table initialization */ +static void test_crc32_table(void) { + printf("\n--- CRC32 Table Tests ---\n"); + + /* Call multiple times to verify idempotent initialization */ + chunk_crc32((uint8_t*)"test", 4); + chunk_crc32((uint8_t*)"test", 4); + chunk_crc32((uint8_t*)"test", 4); + + TEST_ASSERT(1, "Multiple initializations OK"); +} + +int main(void) { + printf("========================================\n"); + printf(" CRC32 Test Suite\n"); + printf("========================================\n"); + + test_crc32_basic(); + test_chunk_calculations(); + test_crc32_table(); + + printf("\n========================================\n"); + printf(" Results: %d passed, %d failed\n", passed, failed); + printf("========================================\n"); + + return failed == 0 ? 0 : 1; +} diff --git a/tests/test_server.js b/tests/test_server.js new file mode 100644 index 0000000..defc7ad --- /dev/null +++ b/tests/test_server.js @@ -0,0 +1,215 @@ +/** + * @file test_server.js + * @brief Server Test Suite + */ + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const ChunkServer = require('../server/server'); + +const PORT = 18080; +const TEST_DIR = path.join(__dirname, 'test_files'); + +let passed = 0; +let failed = 0; + +function assert(cond, msg) { + if (cond) { + console.log(`[PASS] ${msg}`); + passed++; + } else { + console.log(`[FAIL] ${msg}`); + failed++; + } +} + +/** + * HTTP GET helper + */ +function httpRequest(options, expectedStatus = 200) { + return new Promise((resolve, reject) => { + const req = http.request(options, (res) => { + let data = Buffer.alloc(0); + res.on('data', chunk => { + data = Buffer.concat([data, chunk]); + }); + res.on('end', () => { + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: data + }); + }); + }); + req.on('error', reject); + req.end(); + }); +} + +/** + * Setup test files + */ +function setupTestFiles() { + if (!fs.existsSync(TEST_DIR)) { + fs.mkdirSync(TEST_DIR); + } + + // Create test file: 3072 bytes (3 chunks of 1024) + const testData = Buffer.alloc(3072); + for (let i = 0; i < 3072; i++) { + testData[i] = i % 256; + } + fs.writeFileSync(path.join(TEST_DIR, 'test.bin'), testData); + + // Create small file: 100 bytes + const smallData = Buffer.alloc(100); + for (let i = 0; i < 100; i++) { + smallData[i] = 0x42; + } + fs.writeFileSync(path.join(TEST_DIR, 'small.bin'), smallData); +} + +/** + * Cleanup test files + */ +function cleanupTestFiles() { + try { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } catch (e) { + // Ignore + } +} + +/** + * Test suite + */ +async function runTests() { + console.log("========================================"); + console.log(" Chunk Server Test Suite"); + console.log("========================================\n"); + + // Setup + setupTestFiles(); + + const server = new ChunkServer({ + port: PORT, + rootPath: TEST_DIR, + chunkSize: 1024 + }); + + await server.start(); + console.log("\n--- Server Tests ---"); + + try { + // Test 1: Get first chunk + const res1 = await httpRequest({ + hostname: 'localhost', + port: PORT, + path: '/ota?file=test.bin', + method: 'GET' + }); + + assert(res1.statusCode === 200, "First chunk returns 200"); + assert(res1.headers['content-type'] === 'application/octet-stream', "Content-Type is octet-stream"); + assert(res1.headers['x-file-size'] === '3072', "X-File-Size header present"); + assert(res1.headers['x-total-chunks'] === '3', "X-Total-Chunks header present"); + assert(res1.body.length === 2 + 1024 + 4, "First chunk size correct (header + payload + crc)"); + + // Verify chunk index in payload + assert(res1.body.readUInt16LE(0) === 0, "Chunk index is 0"); + + // Test 2: Get second chunk + const res2 = await httpRequest({ + hostname: 'localhost', + port: PORT, + path: '/ota?file=test.bin&chunk=1', + method: 'GET' + }); + + assert(res2.statusCode === 206, "Second chunk returns 206"); + assert(res2.body.readUInt16LE(0) === 1, "Chunk index is 1"); + assert(res2.body.length === 2 + 1024 + 4, "Second chunk size correct"); + + // Test 3: Get last chunk (smaller) + const res3 = await httpRequest({ + hostname: 'localhost', + port: PORT, + path: '/ota?file=test.bin&chunk=2', + method: 'GET' + }); + + assert(res3.statusCode === 206, "Last chunk returns 206"); + assert(res3.body.readUInt16LE(0) === 2, "Chunk index is 2"); + assert(res3.body.length === 2 + 1024 + 4, "Last chunk size correct"); + + // Test 4: Invalid chunk index + const res4 = await httpRequest({ + hostname: 'localhost', + port: PORT, + path: '/ota?file=test.bin&chunk=10', + method: 'GET' + }); + + assert(res4.statusCode === 416, "Invalid chunk returns 416"); + + // Test 5: File not found + const res5 = await httpRequest({ + hostname: 'localhost', + port: PORT, + path: '/ota?file=nonexistent.bin', + method: 'GET' + }); + + assert(res5.statusCode === 404, "Missing file returns 404"); + + // Test 6: Missing file parameter + const res6 = await httpRequest({ + hostname: 'localhost', + port: PORT, + path: '/ota', + method: 'GET' + }); + + assert(res6.statusCode === 400, "Missing file param returns 400"); + + // Test 7: Small file (single chunk) + const res7 = await httpRequest({ + hostname: 'localhost', + port: PORT, + path: '/ota?file=small.bin', + method: 'GET' + }); + + assert(res7.statusCode === 200, "Small file returns 200"); + assert(res7.headers['x-total-chunks'] === '1', "Small file has 1 chunk"); + assert(res7.body.length === 2 + 100 + 4, "Small file chunk size correct"); + + // Test 8: Verify CRC32 + const payload = res1.body.slice(2, 2 + 1024); + const receivedCrc = res1.body.readUInt32LE(2 + 1024); + const calculatedCrc = ChunkServer.crc32(payload); + console.log(`Received CRC: ${receivedCrc.toString(16)}`); + console.log(`Calculated CRC: ${calculatedCrc.toString(16)}`); + console.log(`Calculated >>>0: ${(calculatedCrc >>> 0).toString(16)}`); + assert(receivedCrc === (calculatedCrc >>> 0), "CRC32 verification passed"); + + } catch (err) { + console.error("Test error:", err); + failed++; + } finally { + await server.stop(); + cleanupTestFiles(); + } + + console.log("\n========================================"); + console.log(` Results: ${passed} passed, ${failed} failed`); + console.log("========================================"); + + process.exit(failed === 0 ? 0 : 1); +} + +runTests().catch(err => { + console.error("Fatal error:", err); + process.exit(1); +});