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