From 7d11af0f30a516d741e307183c90b8ef0aff58c8 Mon Sep 17 00:00:00 2001 From: David Bourgault Date: Sun, 25 Aug 2024 22:06:21 -0400 Subject: [PATCH] 8: Support active high/low lights, reduce memory allocations --- CMakeLists.txt | 2 + src/api/ganymede/v2/device.proto | 29 ++++++--- src/app/lights.c | 23 +++++-- src/app/lights.h | 3 +- src/app/poll.c | 103 +++++++++---------------------- src/net/auth/auth.c | 2 +- src/net/http2/http2.c | 82 +++++++++++++++++++++--- 7 files changed, 144 insertions(+), 100 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 64a21aa..7e95cb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.18) project(ganymede VERSION 0.0.1) +set(CMAKE_SYSTEM_NAME Generic) + include($ENV{IDF_PATH}/tools/cmake/idf.cmake) set(GANYMEDE_COMPONENTS "${GANYMEDE_COMPONENTS}" CACHE INTERNAL "GANYMEDE_COMPONENTS") diff --git a/src/api/ganymede/v2/device.proto b/src/api/ganymede/v2/device.proto index e2c39af..e802828 100644 --- a/src/api/ganymede/v2/device.proto +++ b/src/api/ganymede/v2/device.proto @@ -12,6 +12,8 @@ service DeviceService { rpc ListDevice(ListDeviceRequest) returns (ListDeviceResponse); rpc DeleteDevice(DeleteDeviceRequest) returns (google.protobuf.Empty); + rpc Poll(PollRequest) returns (PollResponse); + rpc CreateConfig(CreateConfigRequest) returns (Config); rpc UpdateConfig(UpdateConfigRequest) returns (Config); rpc GetConfig(GetConfigRequest) returns (Config); @@ -29,10 +31,7 @@ message UpdateDeviceRequest { } message GetDeviceRequest { - oneof filter { - string device_uid = 1; - string device_mac = 2; - } + string device_uid = 1; } message ListDeviceRequest { @@ -51,6 +50,23 @@ message DeleteDeviceRequest { string reason = 2; } +message PollRequest { + string device_mac = 1; +} + +message PollResponse { + string device_uid = 1; + string device_display_name = 2; + + string config_uid = 11; + string config_display_name = 12; + + // Current device's offset from UTC (including DST if applicable) + int64 timezone_offset_minutes = 20; + + LightConfig light_config = 101; +} + message CreateConfigRequest { Config config = 1; } @@ -97,9 +113,6 @@ message Device { // Device timezone in IANA / format string timezone = 12; - // Output only. Current device's offset from UTC (including DST if applicable) - int64 timezone_offset_minutes = 13; - string config_uid = 20; } @@ -119,7 +132,7 @@ message Luminaire { } uint32 port = 1; - bool use_pwm = 2; + bool active_high = 2; repeated DailySchedule photo_period = 3; } diff --git a/src/app/lights.c b/src/app/lights.c index 0c037d9..d329cc0 100644 --- a/src/app/lights.c +++ b/src/app/lights.c @@ -44,8 +44,21 @@ static void lights_recompute(struct tm* timeinfo, Ganymede__V2__LightConfig* lig } } - gpio_set_level(luminaire->port, !active); - ESP_LOGD(TAG, "%02d:%02d:%02d port=%lu active=%s", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec, luminaire->port, active ? "true" : "false"); + if (luminaire->active_high == 0) { + active = !active; + } + + gpio_set_level(luminaire->port, active); + ESP_LOGD( + TAG, + "%02d:%02d:%02d port=%lu signal=%s (%s)", + timeinfo->tm_hour, + timeinfo->tm_min, + timeinfo->tm_sec, + luminaire->port, + active ? "high" : "low", + luminaire->active_high ? "active_high" : "active_low" + ); } } @@ -90,7 +103,7 @@ static int lights_reconfigure_gpio(Ganymede__V2__LightConfig* old_config, Ganyme .pull_down_en = GPIO_PULLDOWN_DISABLE }; - ERROR_CHECK(gpio_set_level(port, true)); + ERROR_CHECK(gpio_set_level(port, false)); ERROR_CHECK(gpio_config(&pin_config)); ESP_LOGD(TAG, "enabled gpio port %lu", port); @@ -146,10 +159,10 @@ int app_lights_notify_device(Ganymede__V2__Device* device) return ESP_OK; } -int app_lights_notify_config(Ganymede__V2__Config* config) +int app_lights_notify_poll(Ganymede__V2__PollResponse* response) { // FIXME: This is a hack to quickly deep-clone, but it is not performance efficient - size_t size = protobuf_c_message_pack((ProtobufCMessage*) config->light_config, buffer); + size_t size = protobuf_c_message_pack((ProtobufCMessage*) response->light_config, buffer); incoming_light_config = (Ganymede__V2__LightConfig*) protobuf_c_message_unpack(&ganymede__v2__light_config__descriptor, NULL, size, buffer); return ESP_OK; } \ No newline at end of file diff --git a/src/app/lights.h b/src/app/lights.h index e668fc6..4064ec3 100644 --- a/src/app/lights.h +++ b/src/app/lights.h @@ -5,7 +5,6 @@ int app_lights_init(void); -int app_lights_notify_device(Ganymede__V2__Device* device); -int app_lights_notify_config(Ganymede__V2__Config* config); +int app_lights_notify_poll(Ganymede__V2__PollResponse* config); #endif \ No newline at end of file diff --git a/src/app/poll.c b/src/app/poll.c index 47c04af..058400a 100644 --- a/src/app/poll.c +++ b/src/app/poll.c @@ -1,3 +1,4 @@ +#include #include #include @@ -28,6 +29,7 @@ static const char* TAG = "poll"; static EventGroupHandle_t _poll_event_group = NULL; static esp_timer_handle_t _poll_refresh_timer = NULL; +static char _token[1024] = { 0 }; static uint8_t _payload_buffer[2048] = { 0 }; static uint8_t _response_buffer[2048] = { 0 }; @@ -70,7 +72,7 @@ static size_t _pack_protobuf(ProtobufCMessage* request, uint8_t* buffer) _copy_32bit_bigendian((uint32_t*)&buffer[1], &length); protobuf_c_message_pack(request, &buffer[5]); - return length; + return length + 5; } static esp_err_t _poll_get_mac(char* dest) @@ -87,95 +89,62 @@ static esp_err_t _poll_get_mac(char* dest) return ESP_OK; } -static Ganymede__V2__Device* poll_fetch_device(http2_session_t* session, const struct http_perform_options* options) +static Ganymede__V2__PollResponse* poll_perform(http2_session_t* session, const struct http_perform_options* options) { - char mac[17] = { 0 }; + char mac_buffer[17] = { 0 }; - Ganymede__V2__GetDeviceRequest request; - ganymede__v2__get_device_request__init(&request); - request.filter_case = GANYMEDE__V2__GET_DEVICE_REQUEST__FILTER_DEVICE_MAC; - request.device_mac = mac; + Ganymede__V2__PollRequest request; + ganymede__v2__poll_request__init(&request); + request.device_mac = mac_buffer; - if (_poll_get_mac(mac) != ESP_OK) { + if (_poll_get_mac(request.device_mac) != ESP_OK) { return NULL; } uint32_t length = _pack_protobuf((ProtobufCMessage*)&request, _payload_buffer); - int status = http2_perform(session, "POST", CONFIG_GANYMEDE_AUTHORITY, "/ganymede.v2.DeviceService/GetDevice", (const char*) _payload_buffer, length + 5, (char*) _response_buffer, sizeof(_response_buffer), *options); + int status = http2_perform(session, "POST", CONFIG_GANYMEDE_AUTHORITY, "/ganymede.v2.DeviceService/Poll", (const char*) _payload_buffer, length, (char*) _response_buffer, sizeof(_response_buffer), *options); if (status != 0) { - ESP_LOGE(TAG, "GetDevice: status=%d", status); + ESP_LOGE(TAG, "Poll: status=%d", status); return NULL; } _copy_32bit_bigendian(&length, (uint32_t*)&_response_buffer[1]); - return (Ganymede__V2__Device*) protobuf_c_message_unpack(&ganymede__v2__device__descriptor, NULL, length, &_response_buffer[5]); + return (Ganymede__V2__PollResponse*) protobuf_c_message_unpack(&ganymede__v2__poll_response__descriptor, NULL, length, &_response_buffer[5]); } -static Ganymede__V2__Config* poll_fetch_config(http2_session_t* session, const struct http_perform_options* options, char* uid) +static int poll_set_timezone(const int64_t timezone_offset_minutes) { - Ganymede__V2__GetConfigRequest request; - ganymede__v2__get_config_request__init(&request); - request.config_uid = uid; - - uint32_t length = _pack_protobuf((ProtobufCMessage*)&request, _payload_buffer); - - int status = http2_perform(session, "POST", CONFIG_GANYMEDE_AUTHORITY, "/ganymede.v2.DeviceService/GetConfig", (const char*) _payload_buffer, length + 5, (char*) _response_buffer, sizeof(_response_buffer), *options); - - if (status != 0) { - ESP_LOGE(TAG, "GetConfig: status=%d", status); - return NULL; - } - - _copy_32bit_bigendian(&length, (uint32_t*)&_response_buffer[1]); - return (Ganymede__V2__Config*) protobuf_c_message_unpack(&ganymede__v2__config__descriptor, NULL, length, &_response_buffer[5]); -} - -static int poll_set_timezone(const Ganymede__V2__Device* device) -{ - if (device == NULL) { - return ESP_FAIL; - } - // Ganymede returns the usual TZ offset (UTC - offset = local) but the // GNU implementation expects the opposite, so we invert the sign. - int offset = -(device->timezone_offset_minutes); + int offset = -(timezone_offset_minutes); int min = offset % 60; int hour = (offset - min) / 60; char tzbuf[32] = { 0 }; sprintf(tzbuf, "XXX%+03d:%02d", hour, min); - ESP_LOGI("main", "Set timezone: %s", tzbuf); setenv("TZ", tzbuf, 1); tzset(); + ESP_LOGI(TAG, "Set timezone: %s", tzbuf); return ESP_OK; } static void poll_refresh() { - Ganymede__V2__Device* device = NULL; - Ganymede__V2__Config* config = NULL; - + Ganymede__V2__PollResponse* response = NULL; http2_session_t* session = NULL; - char* token = NULL; - size_t token_len = 1024; - - if ((token = malloc(token_len)) == NULL) { - ESP_LOGE(TAG, "Memory allocation for token failed"); - return; - } - - strcpy(token, "Bearer "); + size_t token_len = sizeof(_token) - 7; - if (auth_get_token(&token[7], &token_len) != ESP_OK) { + strcpy(_token, "Bearer "); + if (auth_get_token(&_token[7], &token_len) != ESP_OK) { goto cleanup; } struct http_perform_options options = { - .authorization = token, + .authorization = _token, .content_type = "application/grpc+proto", .use_grpc_status = true }; @@ -185,34 +154,24 @@ static void poll_refresh() goto cleanup; } - if (http2_session_connect(session, CONFIG_GANYMEDE_HOST, 443, NULL) != ESP_OK) { + if (http2_session_connect(session, CONFIG_GANYMEDE_HOST, 443, CONFIG_GANYMEDE_AUTHORITY) != ESP_OK) { ESP_LOGE(TAG, "failed to connect to %s:443", CONFIG_GANYMEDE_HOST); goto cleanup; } - if ((device = poll_fetch_device(session, &options)) == NULL) { - ESP_LOGE(TAG, "failed to fetch device"); + if ((response = poll_perform(session, &options)) == NULL) { + ESP_LOGE(TAG, "failed to fetch response"); goto cleanup; } + ESP_LOGI(TAG, "device=%s", response->device_display_name); + ESP_LOGI(TAG, "config=%s", response->config_display_name); - ESP_LOGI("main", "device.displayName=%s", device->display_name); - poll_set_timezone(device); - app_lights_notify_device(device); - - if ((config = poll_fetch_config(session, &options, device->config_uid)) == NULL) { - ESP_LOGE(TAG, "failed to fetch config"); - goto cleanup; - } - - ESP_LOGI("main", "config.displayName=%s", config->display_name); - app_lights_notify_config(config); + poll_set_timezone(response->timezone_offset_minutes); + app_lights_notify_poll(response); cleanup: - protobuf_c_message_free_unpacked((ProtobufCMessage*) config, NULL); - protobuf_c_message_free_unpacked((ProtobufCMessage*) device, NULL); + protobuf_c_message_free_unpacked((ProtobufCMessage*) response, NULL); http2_session_release(session); - free(token); - return; } static void poll_task(void* args) @@ -226,13 +185,9 @@ static void poll_task(void* args) ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &poll_event_handler, NULL, &wifi_event_handler)); while (1) { - xEventGroupWaitBits(_poll_event_group, POLL_CONNECTED_BIT, pdFALSE, pdFALSE, portMAX_DELAY); - - EventBits_t event = xEventGroupWaitBits(_poll_event_group, POLL_REFRESH_REQUEST_BIT, pdFALSE, pdFALSE, portMAX_DELAY); - if (event & POLL_CONNECTED_BIT) { + xEventGroupWaitBits(_poll_event_group, POLL_CONNECTED_BIT | POLL_REFRESH_REQUEST_BIT, pdFALSE, pdTRUE, portMAX_DELAY); poll_refresh(); xEventGroupClearBits(_poll_event_group, POLL_REFRESH_REQUEST_BIT); - } } } diff --git a/src/net/auth/auth.c b/src/net/auth/auth.c index 93ac1d7..3082310 100644 --- a/src/net/auth/auth.c +++ b/src/net/auth/auth.c @@ -17,7 +17,7 @@ #include #include -#define AUTH_TASK_STACK_DEPTH (1024 * 2) +#define AUTH_TASK_STACK_DEPTH (1024 * 4) #define AUTH_CONNECTED_BIT BIT0 #define AUTH_REFRESH_REQUEST_BIT BIT1 diff --git a/src/net/http2/http2.c b/src/net/http2/http2.c index 6d77543..5f6a7dc 100644 --- a/src/net/http2/http2.c +++ b/src/net/http2/http2.c @@ -13,6 +13,8 @@ #define HTTP2_TASK_STACK_DEPTH (1024 * 20) +static char _http2_rx_buffer[16394] = { 0 }; + static const char* TAG = "http2"; struct http2_session { @@ -76,6 +78,41 @@ union http2_event { static SemaphoreHandle_t _http2_mutex; static QueueHandle_t _http2_event_queue; +void* http2_malloc(size_t size, void*) +{ + return malloc(size); +} + +void* http2_calloc(size_t nmemb, size_t size, void*) +{ + return calloc(nmemb, size); +} + +void* http2_realloc(void* ptr, size_t size, void*) +{ + // Hack to reduce heap fragmentation when creating many HTTP2 sessions. + if (size == 16394) { + free(ptr); + return (void*) _http2_rx_buffer; + } else { + return realloc(ptr, size); + } +} + +void http2_free(void* ptr, void*) +{ + if (ptr != _http2_rx_buffer) { + return free(ptr); + } +} + +static nghttp2_mem mem = { + .malloc = http2_malloc, + .calloc = http2_calloc, + .realloc = http2_realloc, + .free = http2_free, +}; + static ssize_t http2_tls_send(nghttp2_session* ng, const uint8_t* data, size_t length, int flags, void* user_data) { (void) ng; @@ -240,12 +277,24 @@ static int http2_tls_init(http2_session_t* session) static int http2_ng_init(http2_session_t* session) { + int rc = ESP_OK; + nghttp2_option *options; nghttp2_session_callbacks* callbacks; - int rc = 0; + if ((rc = nghttp2_option_new(&options)) != 0) { + ESP_LOGE(TAG, "nghttp2_option_new rc=%d", rc); + rc = ESP_FAIL; + goto http2_ng_init_exit; + } + + nghttp2_option_set_no_closed_streams(options, true); + nghttp2_option_set_max_deflate_dynamic_table_size(options, 2048); + + if ((rc = nghttp2_session_callbacks_new(&callbacks)) != 0) { ESP_LOGE(TAG, "nghttp2_session_callbacks_new rc=%d", rc); - return ESP_FAIL; + rc = ESP_FAIL; + goto http2_ng_init_cleanup_options; } nghttp2_session_callbacks_set_send_callback(callbacks, http2_tls_send); @@ -255,13 +304,20 @@ static int http2_ng_init(http2_session_t* session) nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, http2_on_data); nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, http2_on_stream_close); - if ((rc = nghttp2_session_client_new(&session->ng, callbacks, session)) != 0) { + if ((rc = nghttp2_session_client_new3(&session->ng, callbacks, session, options, &mem)) != 0) { ESP_LOGE(TAG, "nghttp2_session_client_new rc=%d", rc); - return ESP_FAIL; + rc = ESP_FAIL; + goto http2_ng_init_cleanup_callbacks; } +http2_ng_init_cleanup_options: + nghttp2_option_del(options); + +http2_ng_init_cleanup_callbacks: nghttp2_session_callbacks_del(callbacks); - return ESP_OK; + +http2_ng_init_exit: + return rc; } static int32_t http2_session_connect_internal(http2_session_t* session, const char* hostname, uint16_t port, const char* common_name) @@ -280,7 +336,15 @@ static int32_t http2_session_connect_internal(http2_session_t* session, const ch ESP_LOGD(TAG, "Trying connection to %s (common_name: %s)", hostname, common_name ? common_name : hostname); - if (esp_tls_conn_new_sync(hostname, hostname_length, port, &config, session->tls) == ESP_FAIL) { + int state = 0; + while (state == 0) { + // The _sync version of this function uses gettimeofday to check the connection timeout. This breaks + // when you set the correct time as int32 is too small for the current unix time (in ms). + state = esp_tls_conn_new_async(hostname, hostname_length, port, &config, session->tls); + + } + + if (state == -1) { return ESP_FAIL; } @@ -361,8 +425,6 @@ static int32_t http2_session_perform_internal(http2_session_t* session, const ch ESP_LOGE(TAG, "recv failed: %s", nghttp2_strerror(rc)); break; } - - ESP_LOGD(TAG, "Processing: current=%lld end=%lld", esp_timer_get_time(), end); } while (session->complete == false && esp_timer_get_time() < end); return session->status; @@ -430,13 +492,13 @@ http2_session_t* http2_session_acquire(const TickType_t ticks_to_wait) session->tls = NULL; session->ng = NULL; - if (http2_tls_init(session) == ESP_FAIL) { + if (http2_tls_init(session) != ESP_OK) { ESP_LOGE(TAG, "tls initialization failed"); http2_session_release(session); return NULL; } - if (http2_ng_init(session) == ESP_FAIL) { + if (http2_ng_init(session) != ESP_OK) { ESP_LOGE(TAG, "http2 library initialization failed"); http2_session_release(session); return NULL;