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)
This commit is contained in:
km
2026-03-30 06:35:25 +09:00
commit b1c3ec3af4
10 changed files with 1522 additions and 0 deletions
+22
View File
@@ -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
+123
View File
@@ -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=<filename> // ファイル転送リクエスト
GET /ota/resume?file=<filename>&chunk=<index> // レジューム
```
#### 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
+13
View File
@@ -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"
}
+433
View File
@@ -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 <string.h>
#include <stdio.h>
/* 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));
}
+77
View File
@@ -0,0 +1,77 @@
/**
* @file crc32.c
* @brief CRC32 Implementation (IEEE 802.3)
*/
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
/* 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 */
}
+301
View File
@@ -0,0 +1,301 @@
/**
* @file protocol.h
* @brief OTA Chunk Transfer Protocol Definitions
*/
#ifndef OTA_CHUNK_PROTOCOL_H
#define OTA_CHUNK_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#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 */
+243
View File
@@ -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);
});
}
Executable
BIN
View File
Binary file not shown.
+95
View File
@@ -0,0 +1,95 @@
/**
* @file test_crc32.c
* @brief CRC32 Test Suite
*/
#include "protocol.h"
#include <stdio.h>
#include <string.h>
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;
}
+215
View File
@@ -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);
});