Files
km b1c3ec3af4 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)
2026-03-30 06:35:25 +09:00

434 lines
13 KiB
C

/**
* @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));
}