diff options
| author | Sadeep Madurange <sadeep@asciimx.com> | 2025-11-01 09:46:52 +0800 |
|---|---|---|
| committer | Sadeep Madurange <sadeep@asciimx.com> | 2025-11-01 09:46:52 +0800 |
| commit | 9f938ab8ba5af561bd44dbc7142f338ce317a01a (patch) | |
| tree | 7104ce2ac9456c5895f752eff26bad31f8436978 /esp32/main/news.c | |
| download | etlas-9f938ab8ba5af561bd44dbc7142f338ce317a01a.tar.gz | |
Etlas project.
Diffstat (limited to 'esp32/main/news.c')
| -rw-r--r-- | esp32/main/news.c | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/esp32/main/news.c b/esp32/main/news.c new file mode 100644 index 0000000..9048472 --- /dev/null +++ b/esp32/main/news.c @@ -0,0 +1,353 @@ +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> +#include <freertos/queue.h> +#include <freertos/semphr.h> +#include <freertos/event_groups.h> + +#include <stdlib.h> +#include <string.h> + +#include <time.h> + +#include <esp_log.h> +#include <esp_tls.h> +#include <esp_http_client.h> +#include <sys/param.h> + +#include "news.h" + +#define MAXLEN 12 +#define NEWS_FEED_UPDATED_BIT BIT0 +#define MUTEX_TIMEOUT ((TickType_t) 5000 / portTICK_PERIOD_MS) + +static const char* TAG = "news"; + +static QueueHandle_t news_evt_queue; +static SemaphoreHandle_t mutex; + +static EventGroupHandle_t news_evt_group; + +static esp_http_client_handle_t http_client; + +static int news_local_i = 0; +static int news_local_len = 0; +static struct news_item *news_local[MAXLEN]; + +static int news_world_i = 0; +static int news_world_len = 0; +static struct news_item *news_world[MAXLEN]; + +static inline void copy(char *src, char *dst, int i, int j) +{ + int k; + + for (src += i, k = 0; i <= j; k++) { + if (*src != '&') { + dst[k] = *src++; + i++; + } else { + src++; + if (strncmp(src, "#034;", 5) == 0 || strncmp(src, "quot;", 5) == 0) { + dst[k] = '"'; + src += 5; + i += 6; + } else if (strncmp(src, "#038;", 5) == 0) { + dst[k] = '&'; + src += 5; + i += 6; + } else if (strncmp(src, "#039;", 5) == 0) { + dst[k] = '\''; + src += 5; + i += 6; + } else if (strncmp(src, "#060;", 5) == 0) { + dst[k] = '<'; + src += 5; + i += 6; + } else if (strncmp(src, "#062;", 5) == 0) { + dst[k] = '>'; + src += 5; + i += 6; + } else { + ESP_LOGW(TAG, "Unknown escape sequence at %d\n", i); + dst[k] = *src++; + i++; + } + } + } + + dst[k] = '\0'; +} + +static inline int search(char *s, char *from, char *to, int *a, int *b) +{ + char *tag; + int i, j, is_a; + + i = 0; + is_a = 1; + tag = from; + +start: + j = 0; + + while (s[i] && s[i] != tag[j]) + i++; + + if (!is_a) + *b = i - 1; + + while (s[i] && tag[j] && s[i] == tag[j]) { + i++; + j++; + } + + if (s[i] && tag[j]) + goto start; + + if (is_a) { + if (!s[i]) { + *b = i; + return -1; + } + + *a = i; + is_a = 0; + tag = to; + goto start; + } else { + if (!s[*b]) + return -1; + + // empty tag: advance to the end of the closing tag + if (*b < *a) { + *b += strlen(to) + 1; + return -1; + } + + return 1; + } +} + +static inline void delete(struct news_item **news, int *news_len, int *news_i) +{ + if (xSemaphoreTake(mutex, MUTEX_TIMEOUT) == pdTRUE) { + for (int i = 0; i < *news_len; i++) { + free(news[i]->title); + free(news[i]); + } + + *news_i = 0; + *news_len = 0; + + xSemaphoreGive(mutex); + } else + ESP_LOGE(TAG, "delete() failed to acquire semaphore"); +} + +static inline void parse(char *xml, struct news_item **news, int *news_len, int *news_i) +{ + char *s; + int i, j, rc; + struct news_item *item; + + s = xml; + + delete(news, news_len, news_i); + + // skip the main titles + search(s, "<title>", "</title>", &i, &j); + s += j + 1; + search(s, "<title>", "</title>", &i, &j); + s += j + 1; + + if (xSemaphoreTake(mutex, MUTEX_TIMEOUT) == pdTRUE) { + for (*news_len = 0; *news_len < MAXLEN; (*news_len)++) { + rc = search(s, "<title>", "</title>", &i, &j); + if (rc > 0) { + item = malloc(sizeof(struct news_item)); + if (!item) { + ESP_LOGE(TAG, "malloc() failed for news item"); + break; + } + + item->title = malloc(sizeof(char) * (j - i + 2)); + if (!item->title) { + ESP_LOGE(TAG, "malloc() failed for title"); + free(item); + break; + } + + copy(s, item->title, i, j); + s += j + 1; + news[*news_len] = item; + } + } + xSemaphoreGive(mutex); + } else + ESP_LOGE(TAG, "parse() failed to acquire semaphore"); +} + +static esp_err_t http_evt_handler(esp_http_client_event_t *evt) +{ + static int read_len; + + switch(evt->event_id) { + case HTTP_EVENT_ON_HEADER: + ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", + evt->header_key, + evt->header_value); + break; + case HTTP_EVENT_ON_DATA: + int copy_len = 0; + int cont_len = esp_http_client_get_content_length(evt->client); + char **user_data = (char **) evt->user_data; + + if (*user_data == NULL) { + *user_data = calloc(cont_len + 1, sizeof(char)); + if (!evt->user_data) + ESP_LOGE(TAG, "calloc() failed for response buffer"); + } + + if (evt->user_data) { + copy_len = MIN(evt->data_len, (cont_len - read_len)); + if (copy_len) { + memcpy((*(char **) evt->user_data) + read_len, evt->data, copy_len); + read_len += copy_len; + } + ESP_LOGD(TAG, "HTTP response size = %d bytes", read_len); + } + + break; + case HTTP_EVENT_ON_FINISH: + read_len = 0; + ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH"); + ESP_LOGD(TAG, "%s", *(char **) evt->user_data); + break; + case HTTP_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED"); + + int mbedtls_err = 0; + esp_err_t err = esp_tls_get_and_clear_last_error((esp_tls_error_handle_t) evt->data, + &mbedtls_err, + NULL); + if (err != 0) { + ESP_LOGE(TAG, "Last esp error code: 0x%x", err); + ESP_LOGI(TAG, "Last mbedtls failure: 0x%x", mbedtls_err); + } + read_len = 0; + break; + default: + break; + } + + return ESP_OK; +} + +struct news_item * news_local_get(void) +{ + struct news_item *item = NULL; + + if (xSemaphoreTake(mutex, MUTEX_TIMEOUT) == pdTRUE) { + if (news_local_i < news_local_len) { + item = news_local[news_local_i]; + news_local_i = (news_local_i + 1) % news_local_len; + } + xSemaphoreGive(mutex); + } else + ESP_LOGE(TAG, "news_local_get() failed to acquire semaphore"); + + return item; +} + +struct news_item * news_world_get(void) +{ + struct news_item *item = NULL; + + if (xSemaphoreTake(mutex, MUTEX_TIMEOUT) == pdTRUE) { + if (news_world_i < news_world_len) { + item = news_world[news_world_i]; + news_world_i = (news_world_i + 1) % news_world_len; + } + xSemaphoreGive(mutex); + } else + ESP_LOGE(TAG, "news_world_get() failed to acquire semaphore"); + + return item; +} + +void news_update_task(void *arg) +{ + char *buf; + time_t t; + char ts[20]; + struct tm now; + esp_err_t rc; + + for (;;) { + buf = NULL; + esp_http_client_set_user_data(http_client, &buf); + + // local news + esp_http_client_set_url(http_client, "https://www.channelnewsasia.com/api/v1/rss-outbound-feed?_format=xml&category=10416"); + for(;;) { + rc = esp_http_client_perform(http_client); + if (rc != ESP_ERR_HTTP_EAGAIN) + break; + vTaskDelay((TickType_t) 100 / portTICK_PERIOD_MS); + } + + parse(buf, news_local, &news_local_len, &news_local_i); + free(buf); + buf = NULL; + + // world news + esp_http_client_set_url(http_client, "https://www.channelnewsasia.com/api/v1/rss-outbound-feed?_format=xml&category=6311"); + for(;;) { + rc = esp_http_client_perform(http_client); + if (rc != ESP_ERR_HTTP_EAGAIN) + break; + vTaskDelay((TickType_t) 100 / portTICK_PERIOD_MS); + } + + parse(buf, news_world, &news_world_len, &news_world_i); + free(buf); + + t = time(NULL); + now = *localtime(&t); + strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &now); + ESP_LOGI(TAG, "updated news feed at %s", ts); + xEventGroupSetBits(news_evt_group, NEWS_FEED_UPDATED_BIT); + + vTaskDelay(60 * 60 * 1000 / portTICK_PERIOD_MS); + } +} + +void news_init(void) +{ + if((mutex = xSemaphoreCreateMutex()) == NULL) { + ESP_LOGE(TAG, "xSemaphoreCreateMutex() failed"); + return; + } + + news_evt_group = xEventGroupCreate(); + + esp_http_client_config_t conf = { + .url = "https://www.channelnewsasia.com", + .is_async = true, + .timeout_ms = 5000, + .event_handler = http_evt_handler, + .disable_auto_redirect = true, + }; + + http_client = esp_http_client_init(&conf); + esp_http_client_set_header(http_client, "Accept", "application/rss+xml"); + + news_evt_queue = xQueueCreate(1, sizeof(int)); + xTaskCreate(news_update_task, "news_update_task", 4096, NULL, 10, NULL); + + xEventGroupWaitBits(news_evt_group, + NEWS_FEED_UPDATED_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); +} |
