Add remote poll input

This commit is contained in:
Julian Metzler 2024-02-04 19:27:52 +01:00
parent d41e22aa96
commit 70b7e09789
8 changed files with 424 additions and 7 deletions

View File

@ -37,6 +37,9 @@ config_entry_t config_entries[] = {
{.key = "wg_endpoint", .dataType = STR, .writeOnly = false},
{.key = "wg_endpnt_port", .dataType = U16, .writeOnly = false},
{.key = "wg_keepalive", .dataType = U16, .writeOnly = false},
{.key = "poll_url", .dataType = STR, .writeOnly = false},
{.key = "poll_token", .dataType = STR, .writeOnly = false},
{.key = "poll_interval", .dataType = U16, .writeOnly = false},
};

View File

@ -0,0 +1,3 @@
idf_component_register(SRCS remote_poll.c
INCLUDE_DIRS include
REQUIRES esp_http_client json nvs_flash util mbedtls)

View File

@ -0,0 +1,70 @@
menu "Remote Poll Configuration"
config TG_BOT_POLLING_TIMEOUT
int "Polling timeout in seconds"
default 60
help
Telegram Bot API polling timeout
config TG_BOT_MESSAGE_LIMIT
int "Max messages to fetch"
default 10
help
Maximum number of messages to fetch at once from the API
config TG_BOT_FORCE_UPPERCASE
bool "Force uppercase characters"
default false
choice TG_BOT_CHARSET_METHOD
bool "Charset Method"
default TG_BOT_CHARSET_METHOD_ALLOWED_CHARS_STR
config TG_BOT_CHARSET_METHOD_ALLOWED_CHARS_STR
bool "Allowed Characters String"
config TG_BOT_CHARSET_METHOD_DISALLOWED_CHARS_STR
bool "Disallowed Characters String"
config TG_BOT_CHARSET_METHOD_ALLOWED_CHARS_RANGE
bool "Allowed Characters Range"
config TG_BOT_CHARSET_METHOD_DISALLOWED_CHARS_RANGE
bool "Disallowed Characters Range"
endchoice
config TG_BOT_ALLOWED_CHARACTERS_STR
depends on TG_BOT_CHARSET_METHOD_ALLOWED_CHARS_STR
string "Allowed Characters String"
default ""
help
All the characters that are allowed for display
config TG_BOT_DISALLOWED_CHARACTERS_STR
depends on TG_BOT_CHARSET_METHOD_DISALLOWED_CHARS_STR
string "Disallowed Characters String"
default ""
help
All the characters that are allowed for display
config TG_BOT_ALLOWED_CHARACTERS_RANGE_MIN
depends on TG_BOT_CHARSET_METHOD_ALLOWED_CHARS_RANGE
int "Allowed Characters Range Min"
default 32
config TG_BOT_ALLOWED_CHARACTERS_RANGE_MAX
depends on TG_BOT_CHARSET_METHOD_ALLOWED_CHARS_RANGE
int "Allowed Characters Range Max"
default 127
config TG_BOT_DISALLOWED_CHARACTERS_RANGE_MIN
depends on TG_BOT_CHARSET_METHOD_DISALLOWED_CHARS_RANGE
int "Disallowed Characters Range Min"
default 32
config TG_BOT_DISALLOWED_CHARACTERS_RANGE_MAX
depends on TG_BOT_CHARSET_METHOD_DISALLOWED_CHARS_RANGE
int "Disallowed Characters Range Max"
default 127
endmenu

View File

@ -0,0 +1,19 @@
#pragma once
#include "esp_http_client.h"
#include "nvs.h"
#include "cJSON.h"
typedef struct {
uint8_t* buffer;
uint16_t duration;
} rp_buffer_list_entry_t;
esp_err_t remote_poll_http_event_handler(esp_http_client_event_t *evt);
void remote_poll_init(nvs_handle_t* nvsHandle, uint8_t* outBuf, size_t bufSize);
void remote_poll_deinit();
void remote_poll_task(void* arg);
void remote_poll_send_request();
esp_err_t remote_poll_process_response(cJSON* json);

View File

@ -0,0 +1,312 @@
#include "esp_log.h"
#include "esp_netif.h"
#include <string.h>
#include "sys/param.h"
#include "esp_http_client.h"
#include "mbedtls/base64.h"
#include "remote_poll.h"
#include "macros.h"
#include "util_buffer.h"
#include "util_generic.h"
#include "util_nvs.h"
#define LOG_TAG "Poll"
static TaskHandle_t rp_task_handle;
nvs_handle_t rp_nvs_handle;
char* pollUrl = NULL;
char* pollToken = NULL;
uint16_t pollInterval = 0;
uint8_t pollUrlInited = 0;
uint8_t pollTokenInited = 0;
uint32_t last_update_id = 0;
char* err_desc = NULL;
// Dynamic array holding the current list of buffers
rp_buffer_list_entry_t* rp_buffers = NULL;
uint8_t rp_num_buffers = 0;
uint8_t rp_cur_buffer = 0;
// Last switch / update times
uint64_t rp_last_switch = 0;
uint64_t rp_last_update = 0;
uint8_t* output_buffer;
size_t output_buffer_size = 0;
extern uint8_t wifi_gotIP;
esp_err_t remote_poll_http_event_handler(esp_http_client_event_t *evt) {
static char *resp_buf;
static int resp_len;
switch(evt->event_id) {
case HTTP_EVENT_ERROR: {
ESP_LOGD(LOG_TAG, "HTTP_EVENT_ERROR");
break;
}
case HTTP_EVENT_ON_CONNECTED: {
ESP_LOGD(LOG_TAG, "HTTP_EVENT_ON_CONNECTED");
break;
}
case HTTP_EVENT_HEADER_SENT: {
ESP_LOGD(LOG_TAG, "HTTP_EVENT_HEADER_SENT");
break;
}
case HTTP_EVENT_ON_HEADER: {
ESP_LOGD(LOG_TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
break;
}
case HTTP_EVENT_ON_DATA: {
ESP_LOGD(LOG_TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
/*
* Check for chunked encoding is added as the URL for chunked encoding used in this example returns binary data.
* However, event handler can also be used in case chunked encoding is used.
*/
if (!esp_http_client_is_chunked_response(evt->client)) {
if (resp_buf == NULL) {
resp_buf = (char *) malloc(esp_http_client_get_content_length(evt->client));
resp_len = 0;
if (resp_buf == NULL) {
ESP_LOGE(LOG_TAG, "Failed to allocate memory for output buffer");
return ESP_FAIL;
}
}
memcpy(resp_buf + resp_len, evt->data, evt->data_len);
resp_len += evt->data_len;
}
break;
}
case HTTP_EVENT_ON_FINISH: {
cJSON* json;
ESP_LOGD(LOG_TAG, "HTTP_EVENT_ON_FINISH");
if (resp_buf != NULL) {
json = cJSON_Parse(resp_buf);
free(resp_buf);
resp_buf = NULL;
} else {
return ESP_FAIL;
}
resp_len = 0;
esp_err_t ret = remote_poll_process_response(json);
if (ret != ESP_OK) {
memset(output_buffer, 0x00, output_buffer_size);
if (err_desc == NULL) {
ESP_LOGE(LOG_TAG, "Error");
// sprintf((char*)output_buffer, "REMOTE POLL API FAIL");
} else {
ESP_LOGE(LOG_TAG, "Error: %s", err_desc);
// strncpy((char*)output_buffer, err_desc, output_buffer_size);
err_desc = NULL;
}
return ret;
}
cJSON_Delete(json);
break;
}
case HTTP_EVENT_DISCONNECTED: {
ESP_LOGI(LOG_TAG, "HTTP_EVENT_DISCONNECTED");
if (resp_buf != NULL) {
free(resp_buf);
resp_buf = NULL;
}
resp_len = 0;
break;
}
}
return ESP_OK;
}
void remote_poll_init(nvs_handle_t* nvsHandle, uint8_t* outBuf, size_t bufSize) {
ESP_LOGI(LOG_TAG, "Initializing remote poll");
rp_nvs_handle = *nvsHandle;
output_buffer = outBuf;
output_buffer_size = bufSize;
remote_poll_deinit();
esp_err_t ret = nvs_get_u16(*nvsHandle, "poll_interval", &pollInterval);
if (ret != ESP_OK) pollInterval = 0;
pollUrl = get_string_from_nvs(nvsHandle, "poll_url");
if (pollUrl != NULL) pollUrlInited = 1;
pollToken = get_string_from_nvs(nvsHandle, "poll_token");
if (pollToken != NULL) pollTokenInited = 1;
if (pollInterval != 0 && pollUrl != NULL && pollToken != NULL) {
if (strlen(pollUrl) != 0 && strlen(pollToken) != 0) {
ESP_LOGI(LOG_TAG, "Starting remote poll task");
xTaskCreatePinnedToCore(remote_poll_task, "remote_poll", 4096, NULL, 5, &rp_task_handle, 0);
}
}
}
void remote_poll_deinit() {
// Free allocated memory
if (pollUrlInited) {
free(pollUrl);
pollUrl = NULL;
pollUrlInited = 0;
}
if (pollTokenInited) {
free(pollToken);
pollToken = NULL;
pollTokenInited = 0;
}
}
void remote_poll_task(void* arg) {
while (1) {
uint64_t now = esp_timer_get_time(); // Microseconds!
// Switch buffer if necessary
if (rp_num_buffers > 0) {
if (rp_cur_buffer >= rp_num_buffers) rp_cur_buffer = 0; // In case rp_num_buffers got smaller
if (rp_last_switch == 0 || now - rp_last_switch >= rp_buffers[rp_cur_buffer].duration * 1000000) {
rp_last_switch = now;
rp_cur_buffer++;
if (rp_cur_buffer >= rp_num_buffers) rp_cur_buffer = 0;
ESP_LOGI(LOG_TAG, "Switching to buffer %d", rp_cur_buffer);
memcpy(output_buffer, rp_buffers[rp_cur_buffer].buffer, output_buffer_size);
}
}
// Update if necessary
if (rp_last_update == 0 || now - rp_last_update >= pollInterval * 1000000) {
rp_last_update = now;
if (wifi_gotIP) remote_poll_send_request();
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void remote_poll_send_request() {
esp_http_client_config_t config = {
.event_handler = remote_poll_http_event_handler,
.disable_auto_redirect = false,
.url = pollUrl
};
cJSON* json;
char* post_data;
esp_http_client_handle_t client = esp_http_client_init(&config);
json = cJSON_CreateObject();
cJSON_AddStringToObject(json, "token", pollToken);
post_data = cJSON_Print(json);
ESP_LOGV(LOG_TAG, "POST Data: %s", post_data);
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_post_field(client, post_data, strlen(post_data));
esp_err_t err = esp_http_client_perform(client);
cJSON_Delete(json);
cJSON_free(post_data);
if (err == ESP_OK) {
ESP_LOGI(LOG_TAG, "HTTP GET Status = %d, content_length = %d",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
} else {
ESP_LOGE(LOG_TAG, "HTTP GET request failed: %s", esp_err_to_name(err));
// sprintf((char*)output_buffer, "GET FAILED %s", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
}
esp_err_t remote_poll_process_response(cJSON* json) {
/*
Expected JSON schema:
{
"buffers": [
{
"duration": <duration in seconds>,
"buffer": <base64-encoded buffer>
},
{
"duration": <duration in seconds>,
"buffer": <base64-encoded buffer>
}
]
}
On error:
{
"error": "Some error here"
}
*/
ESP_LOGD(LOG_TAG, "Processing response");
if (!cJSON_IsObject(json)) return ESP_FAIL;
cJSON* field_error = cJSON_GetObjectItem(json, "error");
if (field_error != NULL) {
char* error_str = cJSON_GetStringValue(field_error);
ESP_LOGE(LOG_TAG, "Poll API Error: %s", error_str);
return ESP_FAIL;
}
cJSON* buffers_arr = cJSON_GetObjectItem(json, "buffers");
if (!cJSON_IsArray(buffers_arr)) return ESP_FAIL;
uint16_t numBuffers = cJSON_GetArraySize(buffers_arr);
if (numBuffers > 255) {
ESP_LOGE(LOG_TAG, "Got more than 255 buffers, aborting");
return ESP_FAIL;
}
// Free individual buffer arrays and the overall array
for (uint8_t i = 0; i < rp_num_buffers; i++) {
free(rp_buffers[i].buffer);
}
free(rp_buffers);
// Allocate new buffers
ESP_LOGD(LOG_TAG, "Allocating %d buffers", numBuffers);
rp_num_buffers = numBuffers;
rp_buffers = malloc(rp_num_buffers * sizeof(rp_buffer_list_entry_t));
memset(rp_buffers, 0x00, rp_num_buffers * sizeof(rp_buffer_list_entry_t));
for (uint16_t i = 0; i < numBuffers; i++) {
rp_buffers[i].buffer = malloc(output_buffer_size);
}
// Populate new buffers
for (uint8_t i = 0; i < rp_num_buffers; i++) {
size_t b64_len = 0;
cJSON* item = cJSON_GetArrayItem(buffers_arr, i);
cJSON* duration_field = cJSON_GetObjectItem(item, "duration");
rp_buffers[i].duration = cJSON_GetNumberValue(duration_field);
cJSON* buffer_field = cJSON_GetObjectItem(item, "buffer");
char* buffer_str = cJSON_GetStringValue(buffer_field);
size_t buffer_str_len = strlen(buffer_str);
unsigned char* buffer_str_uchar = (unsigned char*)buffer_str;
int result = mbedtls_base64_decode(NULL, 0, &b64_len, buffer_str_uchar, buffer_str_len);
if (result == MBEDTLS_ERR_BASE64_INVALID_CHARACTER) {
// We don't cover MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL here
// because this will always be returned when checking size
return ESP_FAIL;
} else {
b64_len = 0;
result = mbedtls_base64_decode(rp_buffers[i].buffer, output_buffer_size, &b64_len, buffer_str_uchar, buffer_str_len);
if (result != 0) return ESP_FAIL;
}
}
return ESP_OK;
}

View File

@ -126,10 +126,8 @@ void telegram_bot_init(nvs_handle_t* nvsHandle, uint8_t* outBuf, size_t bufSize)
output_buffer = outBuf;
output_buffer_size = bufSize;
if (apiTokenInited) {
// Free allocated memory
free(apiToken);
}
telegram_bot_deinit();
apiToken = get_string_from_nvs(nvsHandle, "tg_bot_token");
if (apiToken != NULL) {
apiTokenInited = 1;
@ -140,9 +138,11 @@ void telegram_bot_init(nvs_handle_t* nvsHandle, uint8_t* outBuf, size_t bufSize)
}
void telegram_bot_deinit() {
// Free allocated memory
if (apiTokenInited) {
// Free allocated memory
free(apiToken);
apiToken = NULL;
apiTokenInited = 0;
}
}

View File

@ -274,7 +274,8 @@ CONFIG_ESP_TLS_USING_MBEDTLS=y
# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set
# CONFIG_ESP_TLS_SERVER is not set
# CONFIG_ESP_TLS_PSK_VERIFICATION is not set
# CONFIG_ESP_TLS_INSECURE is not set
CONFIG_ESP_TLS_INSECURE=y
CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y
# end of ESP-TLS
#
@ -1328,7 +1329,7 @@ CONFIG_ARTNET_FRAME_TYPE_24BPP=y
# end of ArtNet Receiver
#
# Telegram Bot Configuration
# Remote Poll Configuration
#
CONFIG_TG_BOT_POLLING_TIMEOUT=60
CONFIG_TG_BOT_MESSAGE_LIMIT=10
@ -1338,6 +1339,11 @@ CONFIG_TG_BOT_CHARSET_METHOD_ALLOWED_CHARS_STR=y
# CONFIG_TG_BOT_CHARSET_METHOD_ALLOWED_CHARS_RANGE is not set
# CONFIG_TG_BOT_CHARSET_METHOD_DISALLOWED_CHARS_RANGE is not set
CONFIG_TG_BOT_ALLOWED_CHARACTERS_STR=""
# end of Remote Poll Configuration
#
# Telegram Bot Configuration
#
# end of Telegram Bot Configuration
#

View File

@ -20,6 +20,7 @@
#include "httpd.h"
#include "logging_tcp.h"
#include "macros.h"
#include "remote_poll.h"
#include "telegram_bot.h"
#include "tpm2net.h"
#include "ethernet.h"
@ -284,15 +285,18 @@ void app_main(void) {
tpm2net_init(display_output_buffer, tpm2net_output_buffer, DISPLAY_FRAMEBUF_SIZE, TPM2NET_FRAMEBUF_SIZE);
artnet_init(display_output_buffer, artnet_output_buffer, DISPLAY_FRAMEBUF_SIZE, ARTNET_FRAMEBUF_SIZE);
browser_canvas_init(&server, display_output_buffer, DISPLAY_FRAMEBUF_SIZE);
remote_poll_init(&nvs_handle, display_output_buffer, DISPLAY_FRAMEBUF_SIZE);
#endif
#if defined(CONFIG_DISPLAY_TYPE_CHARACTER)
browser_canvas_init(&server, display_text_buffer, DISPLAY_TEXTBUF_SIZE);
telegram_bot_init(&nvs_handle, display_text_buffer, DISPLAY_TEXTBUF_SIZE);
remote_poll_init(&nvs_handle, display_text_buffer, DISPLAY_TEXTBUF_SIZE);
#endif
#if defined(CONFIG_DISPLAY_TYPE_SELECTION)
browser_canvas_init(&server, display_output_buffer, DISPLAY_FRAMEBUF_SIZE);
remote_poll_init(&nvs_handle, display_output_buffer, DISPLAY_FRAMEBUF_SIZE);
#endif
#if defined(CONFIG_DISPLAY_HAS_BRIGHTNESS_CONTROL)