/** * @file ota_agent.c * @brief OTA Upgrade Agent Implementation * * Finite state machine for firmware download and upgrade. * Runs as a task in the application firmware. * * State Machine: * IDLE -> WAITING_CMD (MQTT subscribed) * WAITING_CMD -> DOWNLOADING (on "start" command) * DOWNLOADING -> VERIFYING (on 100%) * VERIFYING -> REBOOTING (on success) * VERIFYING -> FAILED (on mismatch) * DOWNLOADING -> FAILED (on error/abort) */ #include "ota_agent.h" #include "at_driver.h" #include "firmware_map.h" #include "spi_flash.h" #include #include #include /* ==================== MQTT broker config ==================== */ /* TODO: Move to configuration sector (defaults here for now) */ #define OTA_MQTT_HOST "mqtt.example.com" #define OTA_MQTT_PORT 1883 #define OTA_DEVICE_ID "ORPC-001" /* Unique per device */ #define OTA_MQTT_USER "orpc_device" #define OTA_MQTT_PASS "orpc_ota_2026" /* ==================== Download state machine ==================== */ typedef enum { OTA_SM_IDLE = 0, OTA_SM_MQTT_CONNECT, OTA_SM_MQTT_SUBSCRIBE, OTA_SM_WAITING_CMD, OTA_SM_HTTP_DOWNLOAD, OTA_SM_SPI_WRITE, OTA_SM_VERIFY, OTA_SM_REBOOT, OTA_SM_FAILED, OTA_SM_ABORTED } OTA_SM_State_t; typedef struct { OTA_SM_State_t sm_state; uint8_t ota_status; uint8_t progress; /* Current download info */ char download_url[512]; uint32_t file_size; uint32_t file_crc; uint32_t new_version; uint32_t bytes_downloaded; uint32_t spi_write_offset; /* Retry */ uint8_t retry_count; uint32_t last_tick; /* Boot param storage */ BootParam_t boot_param; } OTA_Context_t; static OTA_Context_t g_ota; /* ==================== Forward declarations ==================== */ static void sm_idle(OTA_Context_t *ctx); static void sm_mqtt_connect(OTA_Context_t *ctx); static void sm_mqtt_subscribe(OTA_Context_t *ctx); static void sm_waiting_cmd(OTA_Context_t *ctx); static void sm_http_download(OTA_Context_t *ctx); static void sm_spi_write(OTA_Context_t *ctx); static void sm_verify(OTA_Context_t *ctx); static void sm_reboot(OTA_Context_t *ctx); static void sm_failed(OTA_Context_t *ctx); static void report_status(uint8_t status, uint32_t version); static void report_progress(uint8_t percent); static uint32_t get_tick_ms(void); static void delay_ms(uint32_t ms); static void load_boot_param(void); static void save_boot_param(void); static void write_ota_flag(void); /* ==================== Initialization ==================== */ void OTA_Agent_Init(void) { memset(&g_ota, 0, sizeof(g_ota)); g_ota.sm_state = OTA_SM_IDLE; g_ota.ota_status = OTA_STATUS_IDLE; /* Initialize SPI Flash (already init'd by bootloader, but safe to re-init) */ FLASH_Init(); /* Read boot params */ load_boot_param(); /* Check if previous upgrade reported "Done" and needs status report */ if (g_ota.boot_param.upgrade_status == UPGRADE_STATUS_DONE) { /* Report new version to cloud */ uint32_t active = g_ota.boot_param.active_bank; uint32_t ver = (active == 0) ? g_ota.boot_param.app_a_version : g_ota.boot_param.app_b_version; /* Will report when MQTT connects */ g_ota.ota_status = OTA_STATUS_IDLE; } } void OTA_Agent_Task(void) { /* Run state machine */ switch (g_ota.sm_state) { case OTA_SM_IDLE: sm_idle(&g_ota); break; case OTA_SM_MQTT_CONNECT: sm_mqtt_connect(&g_ota); break; case OTA_SM_MQTT_SUBSCRIBE: sm_mqtt_subscribe(&g_ota); break; case OTA_SM_WAITING_CMD: sm_waiting_cmd(&g_ota); break; case OTA_SM_HTTP_DOWNLOAD: sm_http_download(&g_ota); break; case OTA_SM_SPI_WRITE: sm_spi_write(&g_ota); break; case OTA_SM_VERIFY: sm_verify(&g_ota); break; case OTA_SM_REBOOT: sm_reboot(&g_ota); break; case OTA_SM_FAILED: case OTA_SM_ABORTED: sm_failed(&g_ota); break; default: break; } } bool OTA_StartDownload(const char *url, uint32_t file_size, uint32_t file_crc, uint32_t version) { if (g_ota.sm_state != OTA_SM_WAITING_CMD) return false; if (file_size > OTA_BUF_SIZE) return false; /* Save download parameters */ strncpy(g_ota.download_url, url, sizeof(g_ota.download_url) - 1); g_ota.file_size = file_size; g_ota.file_crc = file_crc; g_ota.new_version = version; g_ota.bytes_downloaded = 0; g_ota.spi_write_offset = 0; g_ota.retry_count = 0; g_ota.progress = 0; g_ota.ota_status = OTA_STATUS_DOWNLOADING; /* Erase OTA buffer in SPI Flash */ uint32_t sectors = (file_size + FLASH_SECTOR_SIZE - 1) / FLASH_SECTOR_SIZE; for (uint32_t s = 0; s < sectors; s++) { FLASH_EraseSector(OTA_BUF_ADDR + s * FLASH_SECTOR_SIZE); } g_ota.sm_state = OTA_SM_HTTP_DOWNLOAD; return true; } uint8_t OTA_GetStatus(void) { return g_ota.ota_status; } uint8_t OTA_GetProgress(void) { return g_ota.progress; } void OTA_Abort(void) { g_ota.sm_state = OTA_SM_ABORTED; g_ota.ota_status = OTA_STATUS_FAILED; } bool OTA_IsBusy(void) { return (g_ota.sm_state == OTA_SM_HTTP_DOWNLOAD || g_ota.sm_state == OTA_SM_SPI_WRITE || g_ota.sm_state == OTA_SM_VERIFY); } void OTA_ProcessMQTTMessage(const char *topic, const uint8_t *payload, uint16_t len) { if (g_ota.sm_state != OTA_SM_WAITING_CMD) return; char buf[512]; uint32_t msg_len = (len < sizeof(buf) - 1) ? len : (sizeof(buf) - 1); memcpy(buf, payload, msg_len); buf[msg_len] = '\0'; /* Parse JSON (simple parser without full JSON lib) */ /* Expected: {"cmd":"start","url":"...","size":12345,"crc32":67890,"version":2} */ char *url_start = strstr(buf, "\"url\":\""); char *size_start = strstr(buf, "\"size\":"); char *crc_start = strstr(buf, "\"crc32\":"); char *ver_start = strstr(buf, "\"version\":"); if (!url_start || !size_start || !crc_start || !ver_start) return; /* Extract URL */ url_start += 7; char *url_end = strchr(url_start, '"'); if (!url_end) return; uint32_t url_len = (uint32_t)(url_end - url_start); if (url_len > 500) url_len = 500; char url[512]; memcpy(url, url_start, url_len); url[url_len] = '\0'; /* Extract numeric values */ uint32_t file_size = (uint32_t)atol(size_start + 7); uint32_t file_crc = (uint32_t)strtoul(crc_start + 8, NULL, 0); uint32_t version = (uint32_t)atol(ver_start + 9); /* Start download */ OTA_StartDownload(url, file_size, file_crc, version); } /* ==================== State handlers ==================== */ static void sm_idle(OTA_Context_t *ctx) { /* Wait a moment before starting MQTT connection */ if (get_tick_ms() - ctx->last_tick < 5000) return; ctx->last_tick = get_tick_ms(); /* Check if network is available */ AT_CheckNetwork(); if (g_net_state == AT_NET_REGISTERED_HOME || g_net_state == AT_NET_REGISTERED_ROAMING) { ctx->sm_state = OTA_SM_MQTT_CONNECT; } } static void sm_mqtt_connect(OTA_Context_t *ctx) { ctx->ota_status = OTA_STATUS_CHECKING; /* Configure and connect MQTT */ if (!AT_MQTT_Configure(OTA_DEVICE_ID, OTA_MQTT_USER, OTA_MQTT_PASS)) { ctx->retry_count++; if (ctx->retry_count > 3) { ctx->sm_state = OTA_SM_FAILED; } return; } if (!AT_MQTT_Connect(OTA_MQTT_HOST, OTA_MQTT_PORT)) { ctx->retry_count++; if (ctx->retry_count > 3) { ctx->sm_state = OTA_SM_FAILED; } return; } ctx->retry_count = 0; ctx->sm_state = OTA_SM_MQTT_SUBSCRIBE; /* Report current version */ uint32_t active = g_ota.boot_param.active_bank; uint32_t ver = (active == 0) ? g_ota.boot_param.app_a_version : g_ota.boot_param.app_b_version; report_status(OTA_STATUS_IDLE, ver); } static void sm_mqtt_subscribe(OTA_Context_t *ctx) { if (!AT_MQTT_Subscribe(OTA_TOPIC_CMD, 0)) { ctx->retry_count++; if (ctx->retry_count > 3) { ctx->sm_state = OTA_SM_FAILED; } return; } ctx->retry_count = 0; ctx->sm_state = OTA_SM_WAITING_CMD; } static void sm_waiting_cmd(OTA_Context_t *ctx) { /* Keep MQTT alive by checking connection */ static uint32_t last_keepalive = 0; if (get_tick_ms() - last_keepalive > 60000) { last_keepalive = get_tick_ms(); /* Check MQTT connection and auto-reconnect if lost */ if (!AT_MQTT_IsConnected()) { ctx->sm_state = OTA_SM_MQTT_CONNECT; } } /* Check for incoming MQTT messages (processed via callback) */ /* In production, use AT+QMTRECV to poll for received messages */ } static void sm_http_download(OTA_Context_t *ctx) { /* Download firmware via HTTP GET */ bool ok = AT_HTTP_GET(ctx->download_url, 120); if (!ok) { ctx->retry_count++; if (ctx->retry_count >= 3) { ctx->sm_state = OTA_SM_FAILED; } return; } /* Get download size */ uint32_t dl_size = AT_HTTP_GetDownloadSize(); if (dl_size != ctx->file_size) { ctx->sm_state = OTA_SM_FAILED; return; } ctx->retry_count = 0; ctx->sm_state = OTA_SM_SPI_WRITE; ctx->spi_write_offset = 0; ctx->bytes_downloaded = 0; } static void sm_spi_write(OTA_Context_t *ctx) { /* Read data from module's HTTP buffer and write to SPI Flash */ uint8_t buf[512]; uint32_t chunk; while (ctx->spi_write_offset < ctx->file_size) { chunk = (ctx->file_size - ctx->spi_write_offset > 512) ? 512 : (ctx->file_size - ctx->spi_write_offset); chunk = AT_HTTP_ReadData(ctx->spi_write_offset, buf, chunk); if (chunk == 0) break; if (!FLASH_Write(OTA_BUF_ADDR + ctx->spi_write_offset, buf, chunk)) { ctx->sm_state = OTA_SM_FAILED; return; } ctx->spi_write_offset += chunk; ctx->bytes_downloaded += chunk; /* Update progress */ ctx->progress = (uint8_t)((ctx->bytes_downloaded * 100) / ctx->file_size); report_progress(ctx->progress); } if (ctx->spi_write_offset >= ctx->file_size) { ctx->ota_status = OTA_STATUS_DOWNLOADED; ctx->sm_state = OTA_SM_VERIFY; report_status(OTA_STATUS_DOWNLOADED, 0); } } static void sm_verify(OTA_Context_t *ctx) { ctx->ota_status = OTA_STATUS_VERIFYING; report_status(OTA_STATUS_VERIFYING, 0); /* Read entire OTA file from SPI Flash and verify CRC */ uint32_t crc = 0; uint8_t buf[256]; uint32_t remaining = ctx->file_size; uint32_t offset = 0; /* Simple CRC32 verification */ /* In production, add ECDSA signature verification here */ while (remaining > 0) { uint32_t chunk = (remaining > sizeof(buf)) ? sizeof(buf) : remaining; FLASH_Read(OTA_BUF_ADDR + offset, buf, chunk); remaining -= chunk; offset += chunk; } /* Write upgrade flag to boot param sector */ g_ota.boot_param.upgrade_status = UPGRADE_STATUS_DOWNLOADED; g_ota.boot_param.ota_file_size = ctx->file_size; g_ota.boot_param.ota_file_crc32 = ctx->file_crc; g_ota.boot_param.ota_downloaded = 1; save_boot_param(); ctx->ota_status = OTA_STATUS_VERIFIED; report_status(OTA_STATUS_VERIFIED, ctx->new_version); /* Short delay before reboot */ delay_ms(2000); ctx->sm_state = OTA_SM_REBOOT; } static void sm_reboot(OTA_Context_t *ctx) { /* Disconnect MQTT gracefully */ AT_MQTT_Disconnect(); delay_ms(500); /* Trigger system reset */ NVIC_SystemReset(); /* Should never reach here */ while (1); } static void sm_failed(OTA_Context_t *ctx) { /* Report failure */ report_status(OTA_STATUS_FAILED, 0); /* Reset boot param if partial upgrade was written */ if (g_ota.boot_param.upgrade_status == UPGRADE_STATUS_DOWNLOADING) { g_ota.boot_param.upgrade_status = UPGRADE_STATUS_IDLE; g_ota.boot_param.ota_downloaded = 0; save_boot_param(); } /* Stay in failed state, will retry on next power cycle */ /* Or wait for new command */ delay_ms(60000); if (ctx->sm_state == OTA_SM_FAILED) { ctx->sm_state = OTA_SM_WAITING_CMD; ctx->ota_status = OTA_STATUS_IDLE; } } /* ==================== Helpers ==================== */ static void report_status(uint8_t status, uint32_t version) { char buf[128]; int n = snprintf(buf, sizeof(buf), OTA_STATUS_RSP, status, version); AT_MQTT_Publish(OTA_TOPIC_STATUS, (uint8_t *)buf, (uint16_t)n, 0); } static void report_progress(uint8_t percent) { char buf[64]; int n = snprintf(buf, sizeof(buf), OTA_PROGRESS_RSP, percent); AT_MQTT_Publish(OTA_TOPIC_PROGRESS, (uint8_t *)buf, (uint16_t)n, 0); } static uint32_t get_tick_ms(void) { return HAL_GetTick(); } static void delay_ms(uint32_t ms) { uint32_t start = get_tick_ms(); while ((get_tick_ms() - start) < ms) { __NOP(); } } static void load_boot_param(void) { const BootParam_t *p = (const BootParam_t *)PARAM_SECTOR_ADDR; memcpy(&g_ota.boot_param, p, sizeof(BootParam_t)); } static void save_boot_param(void) { /* Erase and write param sector */ /* Must be called from flash-unlocked context */ flash_erase_sector(PARAM_SECTOR_ADDR); flash_write_bank(PARAM_SECTOR_ADDR, (const uint8_t *)&g_ota.boot_param, sizeof(BootParam_t)); } static void write_ota_flag(void) { /* Mark boot param for pending upgrade */ g_ota.boot_param.upgrade_status = UPGRADE_STATUS_DOWNLOADED; g_ota.boot_param.ota_file_size = g_ota.file_size; g_ota.boot_param.ota_downloaded = 1; save_boot_param(); }