diff options
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | main/app_main.c | 15 | ||||
| -rw-r--r-- | main/http_api.c | 93 | ||||
| -rw-r--r-- | main/http_api.h | 5 | ||||
| -rw-r--r-- | main/idf_component.yml | 17 | ||||
| -rw-r--r-- | main/mdns_service.c | 65 | ||||
| -rw-r--r-- | main/mdns_service.h | 9 | ||||
| -rw-r--r-- | main/viewport_state.c | 46 | ||||
| -rw-r--r-- | main/viewport_state.h | 49 |
9 files changed, 295 insertions, 6 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ee5cb24..ad43a53 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRC_DIRS "." INCLUDE_DIRS "." - REQUIRES esp_eth esp_event esp_netif nvs_flash + REQUIRES esp_eth esp_event esp_netif esp_http_server esp_timer json mdns nvs_flash ) diff --git a/main/app_main.c b/main/app_main.c index 8b91537..9389642 100644 --- a/main/app_main.c +++ b/main/app_main.c @@ -1,4 +1,7 @@ +#include "http_api.h" +#include "mdns_service.h" #include "net_eth.h" +#include "viewport_state.h" #include "esp_event.h" #include "esp_log.h" @@ -13,7 +16,8 @@ void app_main(void) ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); - ESP_LOGI(TAG, "Scrypted Viewport boot"); + viewport_state_init(); + ESP_LOGI(TAG, "Scrypted Viewport boot (v%s)", VIEWPORT_VERSION); ESP_ERROR_CHECK(net_eth_init()); if (net_eth_wait_for_ip(30 * 1000) == ESP_OK) { @@ -22,12 +26,13 @@ void app_main(void) ESP_LOGW(TAG, "no DHCP lease after 30s, will keep retrying in the background"); } - // TODO M2: mDNS _scrypted-viewport._tcp.local - // TODO M2: HTTP server: GET /state + ESP_ERROR_CHECK(mdns_service_start()); + ESP_ERROR_CHECK(http_api_start()); + // TODO M3: MIPI-DSI panel init (800x480 IPS, default portrait 480x800) - // TODO M4: /config persistence (NVS) + // TODO M4: /config persistence (NVS) + GET /config + POST /config // TODO M5: /frame JPEG decode -> framebuffer - // TODO M6: /state POST + idle timer + // TODO M6: POST /state + idle timer // TODO M7: Capacitive touch -> outbound /state POST // TODO M8: Local screens (IP, loading) + BOOT button } diff --git a/main/http_api.c b/main/http_api.c new file mode 100644 index 0000000..4c0008d --- /dev/null +++ b/main/http_api.c @@ -0,0 +1,93 @@ +#include "http_api.h" + +#include <inttypes.h> +#include <stdio.h> +#include <string.h> + +#include "cJSON.h" +#include "esp_check.h" +#include "esp_heap_caps.h" +#include "esp_http_server.h" +#include "esp_log.h" +#include "esp_timer.h" + +#include "net_eth.h" +#include "viewport_state.h" + +static const char *TAG = "http_api"; + +static const char *state_name(viewport_run_state_t s) +{ + switch (s) { + case VIEWPORT_STATE_AWAKE: return "awake"; + case VIEWPORT_STATE_ASLEEP: return "asleep"; + default: return "unconfigured"; + } +} + +static esp_err_t state_get_handler(httpd_req_t *req) +{ + viewport_state_lock(); + viewport_state_t *st = viewport_state_get(); + + uint64_t now_us = (uint64_t)esp_timer_get_time(); + uint64_t up_ms = (now_us - st->boot_us) / 1000; + int64_t last_age_ms = (st->last_frame_us < 0) + ? -1 + : (int64_t)((now_us - (uint64_t)st->last_frame_us) / 1000); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddItemToObject(root, "name", + st->viewport_name[0] ? cJSON_CreateString(st->viewport_name) + : cJSON_CreateNull()); + cJSON_AddStringToObject(root, "version", VIEWPORT_VERSION); + cJSON_AddBoolToObject(root, "configured", st->configured); + cJSON_AddStringToObject(root, "state", state_name(st->state)); + cJSON_AddNumberToObject(root, "uptime_ms", (double)up_ms); + cJSON_AddItemToObject(root, "last_frame_ms_ago", + last_age_ms < 0 ? cJSON_CreateNull() + : cJSON_CreateNumber((double)last_age_ms)); + cJSON_AddNumberToObject(root, "frames_received", (double)st->frames_received); + cJSON_AddNumberToObject(root, "decode_errors", (double)st->decode_errors); + cJSON_AddNumberToObject(root, "state_post_failures", (double)st->state_post_failures); + cJSON_AddStringToObject(root, "resolution", viewport_state_resolution_str()); + cJSON_AddStringToObject(root, "ip", net_eth_get_ip_str()); + cJSON_AddNumberToObject(root, "free_heap", + (double)heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); + cJSON_AddNumberToObject(root, "free_psram", + (double)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + + viewport_state_unlock(); + + char *body = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (!body) return ESP_ERR_NO_MEM; + + httpd_resp_set_type(req, "application/json"); + esp_err_t err = httpd_resp_send(req, body, HTTPD_RESP_USE_STRLEN); + cJSON_free(body); + return err; +} + +static const httpd_uri_t s_state_get = { + .uri = "/state", + .method = HTTP_GET, + .handler = state_get_handler, + .user_ctx = NULL, +}; + +esp_err_t http_api_start(void) +{ + httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); + cfg.server_port = 80; + cfg.max_uri_handlers = 8; + cfg.lru_purge_enable = true; + + httpd_handle_t server = NULL; + ESP_RETURN_ON_ERROR(httpd_start(&server, &cfg), TAG, "httpd_start"); + ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &s_state_get), + TAG, "register /state"); + + ESP_LOGI(TAG, "http server listening on :80 (GET /state)"); + return ESP_OK; +} diff --git a/main/http_api.h b/main/http_api.h new file mode 100644 index 0000000..8d1a9ae --- /dev/null +++ b/main/http_api.h @@ -0,0 +1,5 @@ +#pragma once + +#include "esp_err.h" + +esp_err_t http_api_start(void); diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..e5dcd75 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: + version: '>=4.1.0' + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true + espressif/mdns: '*' diff --git a/main/mdns_service.c b/main/mdns_service.c new file mode 100644 index 0000000..761ade5 --- /dev/null +++ b/main/mdns_service.c @@ -0,0 +1,65 @@ +#include "mdns_service.h" + +#include <stdio.h> +#include <string.h> + +#include "esp_check.h" +#include "esp_log.h" +#include "mdns.h" + +#include "viewport_state.h" + +static const char *TAG = "mdns"; +static const char *SERVICE = "_scrypted-viewport"; +static const char *PROTO = "_tcp"; +static const uint16_t PORT = 80; + +static esp_err_t apply_records(void) +{ + viewport_state_lock(); + viewport_state_t *st = viewport_state_get(); + + char hostname[80]; + if (st->viewport_name[0]) { + snprintf(hostname, sizeof(hostname), "viewport-%s", st->viewport_name); + } else { + snprintf(hostname, sizeof(hostname), "viewport"); + } + + const char *resolution = viewport_state_resolution_str(); + const char *orientation = (st->orientation == VIEWPORT_ORIENTATION_PORTRAIT) + ? "portrait" : "landscape"; + const char *name_value = st->viewport_name[0] ? st->viewport_name : ""; + + viewport_state_unlock(); + + mdns_txt_item_t txt[] = { + { "version", VIEWPORT_VERSION }, + { "resolution", resolution }, + { "orientation", orientation }, + { "name", name_value }, + }; + const size_t txt_count = sizeof(txt) / sizeof(txt[0]); + + ESP_RETURN_ON_ERROR(mdns_hostname_set(hostname), TAG, "hostname_set"); + ESP_RETURN_ON_ERROR(mdns_service_txt_set(SERVICE, PROTO, txt, txt_count), + TAG, "txt_set"); + + ESP_LOGI(TAG, "advertising %s.local on %s.%s.local:%u", + hostname, SERVICE, PROTO, PORT); + return ESP_OK; +} + +esp_err_t mdns_service_start(void) +{ + ESP_RETURN_ON_ERROR(mdns_init(), TAG, "mdns_init"); + ESP_RETURN_ON_ERROR( + mdns_service_add(NULL, SERVICE, PROTO, PORT, NULL, 0), + TAG, "service_add"); + return apply_records(); +} + +esp_err_t mdns_service_refresh(void) +{ + return apply_records(); +} diff --git a/main/mdns_service.h b/main/mdns_service.h new file mode 100644 index 0000000..1e7eb12 --- /dev/null +++ b/main/mdns_service.h @@ -0,0 +1,9 @@ +#pragma once + +#include "esp_err.h" + +esp_err_t mdns_service_start(void); + +// Update the mDNS hostname and TXT records to reflect the current +// viewport name / orientation. Call after /config writes either field. +esp_err_t mdns_service_refresh(void); diff --git a/main/viewport_state.c b/main/viewport_state.c new file mode 100644 index 0000000..9115ce3 --- /dev/null +++ b/main/viewport_state.c @@ -0,0 +1,46 @@ +#include "viewport_state.h" + +#include <string.h> + +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +static viewport_state_t s_state; +static SemaphoreHandle_t s_mutex; + +void viewport_state_init(void) +{ + memset(&s_state, 0, sizeof(s_state)); + s_state.configured = false; + s_state.state = VIEWPORT_STATE_UNCONFIGURED; + s_state.brightness = 80; + s_state.idle_timeout_ms = 60000; + s_state.orientation = VIEWPORT_ORIENTATION_PORTRAIT; + s_state.boot_us = (uint64_t)esp_timer_get_time(); + s_state.last_frame_us = -1; + + s_mutex = xSemaphoreCreateMutex(); +} + +viewport_state_t *viewport_state_get(void) +{ + return &s_state; +} + +void viewport_state_lock(void) +{ + xSemaphoreTake(s_mutex, portMAX_DELAY); +} + +void viewport_state_unlock(void) +{ + xSemaphoreGive(s_mutex); +} + +const char *viewport_state_resolution_str(void) +{ + return (s_state.orientation == VIEWPORT_ORIENTATION_PORTRAIT) + ? "480x800" + : "800x480"; +} diff --git a/main/viewport_state.h b/main/viewport_state.h new file mode 100644 index 0000000..b580ff7 --- /dev/null +++ b/main/viewport_state.h @@ -0,0 +1,49 @@ +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +#define VIEWPORT_VERSION "0.1.0" +#define VIEWPORT_PANEL_WIDTH 800 +#define VIEWPORT_PANEL_HEIGHT 480 + +typedef enum { + VIEWPORT_ORIENTATION_PORTRAIT = 0, // effective: 480x800 + VIEWPORT_ORIENTATION_LANDSCAPE, // effective: 800x480 +} viewport_orientation_t; + +typedef enum { + VIEWPORT_STATE_UNCONFIGURED = 0, + VIEWPORT_STATE_AWAKE, + VIEWPORT_STATE_ASLEEP, +} viewport_run_state_t; + +// Read-mostly snapshot of device state used by /state. Counters are +// updated under a mutex by the modules that own them. +typedef struct { + char viewport_name[64]; // empty before /config + char scrypted_url[256]; // empty before /config + bool configured; + viewport_run_state_t state; + + uint8_t brightness; // 0–100, default 80 + uint32_t idle_timeout_ms; // 0 = disabled, else >= 5000, default 60000 + viewport_orientation_t orientation; // default portrait + + uint64_t boot_us; // esp_timer_get_time() at boot + int64_t last_frame_us; // -1 if no frame received yet + + uint64_t frames_received; + uint64_t decode_errors; + uint64_t state_post_failures; +} viewport_state_t; + +void viewport_state_init(void); +viewport_state_t *viewport_state_get(void); +void viewport_state_lock(void); +void viewport_state_unlock(void); + +// Returns the effective resolution string ("480x800" or "800x480") based +// on the current orientation. Caller-owned static storage; safe to read +// without the lock (atomic snapshot). +const char *viewport_state_resolution_str(void); |
