diff --git a/.gitignore b/.gitignore index 989d5c0..3f2bfdf 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,7 @@ external/* Provider/AgentTasks/ # OTA files -*.bundle \ No newline at end of file +*.bundle + +#png +*.png diff --git a/Client/CMakeLists.txt b/Client/CMakeLists.txt new file mode 100644 index 0000000..9b588fa --- /dev/null +++ b/Client/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(Client) diff --git a/Client/dependencies.lock b/Client/dependencies.lock new file mode 100644 index 0000000..a8d3e86 --- /dev/null +++ b/Client/dependencies.lock @@ -0,0 +1,133 @@ +dependencies: + espressif/ethernet_init: + component_hash: 9f7d29acf5fe32315579ddb6247388291cda555aa529409108c0ada0aa7cd99d + dependencies: + - matches: + - if: $CONFIG{ETHERNET_SPI_USE_CH390} == 1 + name: espressif/ch390 + registry_url: https://components.espressif.com + require: private + version: '*' + - matches: + - if: $CONFIG{ETHERNET_SPI_USE_DM9051} == 1 + name: espressif/dm9051 + registry_url: https://components.espressif.com + require: private + rules: + - if: idf_version >=6.0 + version: ^1.0.0 + version: '*' + - matches: + - if: idf_version >=6.0 + version: ^1.0.0 + name: espressif/dp83848 + registry_url: https://components.espressif.com + require: private + rules: + - if: target in [esp32, esp32p4] + - if: $CONFIG{ETHERNET_PHY_USE_DP83848} == 1 + version: '*' + - matches: + - if: $CONFIG{ETHERNET_SPI_USE_ENC28J60} == 1 + name: espressif/enc28j60 + registry_url: https://components.espressif.com + require: private + version: '*' + - matches: + - if: idf_version >=6.0 + version: ^1.0.0 + name: espressif/ip101 + registry_url: https://components.espressif.com + require: private + rules: + - if: target in [esp32, esp32p4] + - if: $CONFIG{ETHERNET_PHY_USE_IP101} == 1 + version: '*' + - matches: + - if: idf_version >=6.0 + version: ^1.0.0 + name: espressif/ksz80xx + registry_url: https://components.espressif.com + require: private + rules: + - if: target in [esp32, esp32p4] + - if: $CONFIG{ETHERNET_PHY_USE_KSZ80XX} == 1 + version: '*' + - matches: + - if: $CONFIG{ETHERNET_SPI_USE_KSZ8851SNL} == 1 + name: espressif/ksz8851snl + registry_url: https://components.espressif.com + require: private + rules: + - if: idf_version >=6.0 + version: ^1.0.0 + version: '*' + - matches: + - if: $CONFIG{ETHERNET_SPI_USE_LAN865X} == 1 + name: espressif/lan865x + registry_url: https://components.espressif.com + require: private + version: ^0.1.1 + - name: espressif/lan867x + registry_url: https://components.espressif.com + require: private + rules: + - if: target in [esp32, esp32p4] + - if: $CONFIG{ETHERNET_PHY_USE_LAN867X} == 1 + version: '>=2.0.0' + - matches: + - if: idf_version >=6.0 + version: ^1.0.0 + name: espressif/lan87xx + registry_url: https://components.espressif.com + require: private + rules: + - if: target in [esp32, esp32p4] + - if: $CONFIG{ETHERNET_PHY_USE_LAN87XX} == 1 + version: '*' + - matches: + - if: idf_version >=6.0 + version: ^1.0.0 + name: espressif/rtl8201 + registry_url: https://components.espressif.com + require: private + rules: + - if: target in [esp32, esp32p4] + - if: $CONFIG{ETHERNET_PHY_USE_RTL8201} == 1 + version: '*' + - matches: + - if: $CONFIG{ETHERNET_SPI_USE_W5500} == 1 + name: espressif/w5500 + registry_url: https://components.espressif.com + require: private + rules: + - if: idf_version >=6.0 + version: ^1.0.0 + version: '*' + - name: idf + require: private + version: '>=5.4.3,!=5.5.0,!=5.5.1' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.3.0 + idf: + source: + type: idf + version: 5.5.3 + network: + dependencies: + - name: idf + version: '>=4.1.0' + - name: espressif/ethernet_init + version: ^1.3.0 + source: + path: C:\Dev\Classified\Calendink\components\network + type: local + version: 1.0.0 +direct_dependencies: +- idf +- network +manifest_hash: 12f55da0a0684644c57ee6a400e8e98810d36140fad54f35a96e434eb9774b8c +target: esp32 +version: 2.0.0 diff --git a/Client/main/CMakeLists.txt b/Client/main/CMakeLists.txt new file mode 100644 index 0000000..f13668e --- /dev/null +++ b/Client/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "main.cpp" + PRIV_REQUIRES esp_eth esp_wifi esp_netif driver network nvs_flash + INCLUDE_DIRS ".") diff --git a/Client/main/idf_component.yml b/Client/main/idf_component.yml new file mode 100644 index 0000000..d1a398e --- /dev/null +++ b/Client/main/idf_component.yml @@ -0,0 +1,6 @@ +## IDF Component Manager Manifest File +dependencies: + idf: + version: '>=4.1.0' + network: + path: "../../components/network" diff --git a/Client/main/main.cpp b/Client/main/main.cpp new file mode 100644 index 0000000..e590fa9 --- /dev/null +++ b/Client/main/main.cpp @@ -0,0 +1,54 @@ +#include +#include "esp_log.h" +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "nvs_flash.h" +#include "network.hpp" + +static const char *TAG = "ClientMain"; + +extern "C" void app_main() +{ + ESP_LOGI(TAG, "Hello, Calendink Client!"); + + // Initialize NVS (required for some Wi-Fi configurations and network features) + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // Connect to WiFi + ESP_LOGI(TAG, "Initializing WiFi connection"); + + // Attempting to connect, we don't necessarily need to block forever here + // as network.cpp already handles some of the retries + esp_err_t err = connect_wifi(CONFIG_CALENDINK_WIFI_SSID, CONFIG_CALENDINK_WIFI_PASSWORD, false); + + if (err == ESP_OK) { + uint8_t retries = 1; + do { + err = check_wifi_connection(retries); + if (err != ESP_OK) { + ESP_LOGW(TAG, "WiFi connection check timeout, retrying... (%d)", retries); + vTaskDelay(pdMS_TO_TICKS(1000)); + } + retries++; + } while (err == ESP_ERR_TIMEOUT && retries <= CONFIG_CALENDINK_WIFI_RETRIES); + } + + if (err == ESP_OK) { + ESP_LOGI(TAG, "Successfully connected to WiFi!"); + } else { + ESP_LOGE(TAG, "Failed to connect to WiFi."); + } + + while (true) + { + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} diff --git a/Provider/dependencies.lock b/Provider/dependencies.lock index 85ce0b5..cb35a78 100644 --- a/Provider/dependencies.lock +++ b/Provider/dependencies.lock @@ -146,12 +146,22 @@ dependencies: type: service version: 1.20.4 lvgl/lvgl: - component_hash: 184e532558c1c45fefed631f3e235423d22582aafb4630f3e8885c35281a49ae + component_hash: 17e68bfd21f0edf4c3ee838e2273da840bf3930e5dbc3bfa6c1190c3aed41f9f dependencies: [] source: registry_url: https://components.espressif.com/ type: service - version: 9.5.0 + version: 9.4.0 + network: + dependencies: + - name: idf + version: '>=4.1.0' + - name: espressif/ethernet_init + version: ^1.3.0 + source: + path: C:\Dev\Classified\Calendink\components\network + type: local + version: 1.0.0 direct_dependencies: - espressif/ethernet_init - espressif/led_strip @@ -159,6 +169,7 @@ direct_dependencies: - idf - joltwallet/littlefs - lvgl/lvgl -manifest_hash: 96112412d371d78cc527b7d0904042e5a7ca7c4f25928de9483a1b53dd2a2f4e +- network +manifest_hash: adc42d97d037e4815c3e1d03227cf1a5b29b8a914aa24fefa5760edd541a6bac target: esp32s3 version: 2.0.0 diff --git a/Provider/fetch_png.py b/Provider/fetch_png.py new file mode 100644 index 0000000..71688b0 --- /dev/null +++ b/Provider/fetch_png.py @@ -0,0 +1,20 @@ +import urllib.request +import json +import time + +# 1. Register a fake device +req = urllib.request.Request('http://calendink.local/api/devices/register', data=json.dumps({'mac': 'DE:BU:G0:44:55:66'}).encode('utf-8'), headers={'Content-Type': 'application/json'}) +try: + with urllib.request.urlopen(req) as response: + print("Registered:", response.read().decode()) +except Exception as e: + print("Error registering:", e) + +time.sleep(1) + +# 2. Download the PNG +try: + urllib.request.urlretrieve('http://calendink.local/api/devices/screen.png?mac=DE:BU:G0:44:55:66', 'test_png.png') + print("Downloaded test_png.png") +except Exception as e: + print("Error downloading PNG:", e) diff --git a/Provider/frontend/src/App.svelte b/Provider/frontend/src/App.svelte index 842ea70..5661f46 100644 --- a/Provider/frontend/src/App.svelte +++ b/Provider/frontend/src/App.svelte @@ -5,6 +5,7 @@ import Sidebar from "./lib/Sidebar.svelte"; import TaskManager from "./lib/TaskManager.svelte"; import UserManager from "./lib/UserManager.svelte"; + import DeviceManager from "./lib/DeviceManager.svelte"; import Spinner from "./lib/Spinner.svelte"; /** @type {'loading' | 'ok' | 'error' | 'rebooting'} */ @@ -13,7 +14,7 @@ let showRebootConfirm = $state(false); let isRecovering = $state(false); - /** @type {'dashboard' | 'tasks' | 'users'} */ + /** @type {'dashboard' | 'tasks' | 'users' | 'devices'} */ let currentView = $state("dashboard"); let mobileMenuOpen = $state(false); @@ -352,6 +353,11 @@
+ {:else if currentView === 'devices'} + +
+ +
{/if} diff --git a/Provider/frontend/src/lib/DeviceManager.svelte b/Provider/frontend/src/lib/DeviceManager.svelte new file mode 100644 index 0000000..aa9fb2a --- /dev/null +++ b/Provider/frontend/src/lib/DeviceManager.svelte @@ -0,0 +1,196 @@ + + +
+
+
+

Device Manager

+

+ Manage registered e-ink devices and their screen layouts. +

+
+ +
+ + {#if loading} +
+ Loading devices... +
+ {:else if error} +
+ {error} +
+ {:else if devices.length === 0} +
+

No devices registered yet.

+

+ Use + curl -X POST -d '{{"mac":"AA:BB:CC:DD:EE:FF"}}' http://calendink.local/api/devices/register + to register a device. +

+
+ {:else} +
+ {#each devices as device} +
+ +
+
+ 📺 + {device.mac} +
+
+ {#if device.has_layout} + {#if device.xml_layout === defaultXml} + + Default Layout + + {:else} + + Layout Set + + {/if} + {:else} + + No Layout + + {/if} +
+
+ + +
+ + + + +
+ + +
+ + {#if saveResult === 'ok' && savingMac === ''} +
✓ Layout saved successfully
+ {:else if saveResult && saveResult !== 'ok'} +
✗ {saveResult}
+ {/if} +
+
+ {/each} +
+ {/if} + +
+ + Debug Tools + +
+

+ Quickly register a new device to format layouts. +

+ +
+
+
diff --git a/Provider/frontend/src/lib/Sidebar.svelte b/Provider/frontend/src/lib/Sidebar.svelte index f66ba87..6e14e16 100644 --- a/Provider/frontend/src/lib/Sidebar.svelte +++ b/Provider/frontend/src/lib/Sidebar.svelte @@ -11,6 +11,7 @@ const navItems = [ { id: 'dashboard', label: 'Dashboard', icon: '🏠' }, + { id: 'devices', label: 'Devices', icon: '📺' }, { id: 'tasks', label: 'Tasks', icon: '📋' }, { id: 'users', label: 'Users', icon: '👥' }, ]; diff --git a/Provider/frontend/src/lib/api.js b/Provider/frontend/src/lib/api.js index cd244dd..81ecfa4 100644 --- a/Provider/frontend/src/lib/api.js +++ b/Provider/frontend/src/lib/api.js @@ -274,3 +274,53 @@ export async function deleteTask(id) { } return res.json(); } + +// ─── Device Management ─────────────────────────────────────────────────────── + +/** + * Fetch all registered devices and the default layout. + * @returns {Promise<{default_layout: string, devices: Array<{mac: string, has_layout: boolean, xml_layout: string}>}>} + */ +export async function getDevices() { + const res = await trackedFetch(`${API_BASE}/api/devices`); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + return res.json(); +} + +/** + * Register a new device. + * @param {string} mac + * @returns {Promise<{status: string}>} + */ +export async function registerDevice(mac) { + const res = await trackedFetch(`${API_BASE}/api/devices/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mac }) + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed (${res.status}): ${errorText || res.statusText}`); + } + return res.json(); +} + +/** + * Update the LVGL XML layout for a device. + * @param {string} mac + * @param {string} xml + * @returns {Promise<{status: string}>} + */ +export async function updateDeviceLayout(mac, xml) { + const res = await trackedFetch(`${API_BASE}/api/devices/layout`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mac, xml }) + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed (${res.status}): ${errorText || res.statusText}`); + } + return res.json(); +} + diff --git a/Provider/frontend/version.json b/Provider/frontend/version.json index d0a7c8f..d049655 100644 --- a/Provider/frontend/version.json +++ b/Provider/frontend/version.json @@ -1,5 +1,5 @@ { "major": 0, "minor": 1, - "revision": 30 + "revision": 32 } \ No newline at end of file diff --git a/Provider/main/CMakeLists.txt b/Provider/main/CMakeLists.txt index 4387faf..3b2cd99 100644 --- a/Provider/main/CMakeLists.txt +++ b/Provider/main/CMakeLists.txt @@ -1,8 +1,7 @@ idf_component_register(SRCS "main.cpp" - # Needed as we use minimal build PRIV_REQUIRES esp_http_server esp_eth esp_wifi nvs_flash esp_netif vfs - json app_update esp_timer esp_psram mdns driver + json app_update esp_timer esp_psram mdns driver network INCLUDE_DIRS ".") if(CONFIG_CALENDINK_DEPLOY_WEB_PAGES) diff --git a/Provider/main/Kconfig.projbuild b/Provider/main/Kconfig.projbuild index fc625f8..0df0767 100644 --- a/Provider/main/Kconfig.projbuild +++ b/Provider/main/Kconfig.projbuild @@ -1,76 +1,3 @@ -menu "CalendarInk Network Configuration" - - config CALENDINK_WIFI_SSID - string "WiFi SSID" - default "" - help - SSID (network name) for the WiFi connection. - - config CALENDINK_WIFI_PASSWORD - string "WiFi Password" - default "" - help - Password for the WiFi connection. - - config CALENDINK_ETH_RETRIES - int "Maximum Ethernet Connection Retries" - default 5 - help - Number of times to retry the Ethernet connection before falling back to WiFi. - - config CALENDINK_WIFI_RETRIES - int "Maximum WiFi Connection Retries" - default 5 - help - Number of times to retry the WiFi connection before failing completely. - - config CALENDINK_BLINK_IP - bool "Blink last IP digit on connect" - default n - help - If enabled, the LED will blink the last digit of the IP address - acquired to assist in debugging. - - config CALENDINK_MDNS_HOSTNAME - string "mDNS Hostname" - default "calendink" - help - The hostname to use for mDNS. The device will be accessible - at .local. (e.g., calendink.local) - - config CALENDINK_UDP_LOG_TARGET_IP - string "UDP Logger Target IP Address" - default "" - help - The IP address to send UDP logs to via port 514. - If left blank, logs will be broadcast to 255.255.255.255. - - choice CALENDINK_WIFI_PS_MODE - prompt "WiFi Power Save Mode" - default CALENDINK_WIFI_PS_NONE - help - Select the WiFi power save mode to balance power consumption and network stability. - - config CALENDINK_WIFI_PS_NONE - bool "None (No power save, highest consumption)" - - config CALENDINK_WIFI_PS_MIN_MODEM - bool "Minimum Modem (Wakes on beacon, balanced)" - - config CALENDINK_WIFI_PS_MAX_MODEM - bool "Maximum Modem (Lowest consumption, may drop connection on strict routers)" - endchoice - - config CALENDINK_ALLOW_LIGHT_SLEEP - bool "Allow Light Sleep (Tickless Idle)" - default n - help - If enabled, the device will heavily use light sleep to reduce power - consumption. Note that this may BREAK the UART console monitor since the - CPU sleeps and halts the UART! Use UDP logging if you need logs - while light sleep is enabled. - -endmenu menu "Calendink Web Server" diff --git a/Provider/main/api/devices/layout.cpp b/Provider/main/api/devices/layout.cpp new file mode 100644 index 0000000..0b4c0b3 --- /dev/null +++ b/Provider/main/api/devices/layout.cpp @@ -0,0 +1,99 @@ +// POST /api/devices/layout — Update the XML layout for a device +// Body: {"mac": "AA:BB:CC:DD:EE:FF", "xml": ""} + +#include "cJSON.h" +#include "esp_http_server.h" +#include "esp_log.h" + +#include "types.hpp" +#include "device.hpp" + +internal const char *kTagDeviceLayout = "API_DEV_LAYOUT"; + +internal esp_err_t api_devices_layout_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + + // The XML payload can be large, so use a bigger buffer + // DEVICE_XML_MAX (2048) + JSON overhead for mac key etc. + constexpr int kBufSize = DEVICE_XML_MAX + 256; + char *buf = (char *)malloc(kBufSize); + if (!buf) + { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); + return ESP_FAIL; + } + + int total = 0; + int remaining = req->content_len; + if (remaining >= kBufSize) + { + free(buf); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Payload too large"); + return ESP_FAIL; + } + + while (remaining > 0) + { + int received = httpd_req_recv(req, buf + total, remaining); + if (received <= 0) + { + free(buf); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Receive error"); + return ESP_FAIL; + } + total += received; + remaining -= received; + } + buf[total] = '\0'; + + cJSON *body = cJSON_Parse(buf); + free(buf); + + if (!body) + { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); + return ESP_FAIL; + } + + cJSON *mac_item = cJSON_GetObjectItem(body, "mac"); + cJSON *xml_item = cJSON_GetObjectItem(body, "xml"); + + if (!cJSON_IsString(mac_item) || !mac_item->valuestring || strlen(mac_item->valuestring) == 0) + { + cJSON_Delete(body); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing 'mac'"); + return ESP_FAIL; + } + + if (!cJSON_IsString(xml_item) || !xml_item->valuestring) + { + cJSON_Delete(body); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing 'xml'"); + return ESP_FAIL; + } + + bool ok = update_device_layout(mac_item->valuestring, xml_item->valuestring); + + if (!ok) + { + cJSON_Delete(body); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Device not found"); + return ESP_FAIL; + } + + ESP_LOGI(kTagDeviceLayout, "Updated layout for %s (%zu bytes)", mac_item->valuestring, + strlen(xml_item->valuestring)); + + cJSON_Delete(body); + + httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); + return ESP_OK; +} + +internal const httpd_uri_t api_devices_layout_uri = { + .uri = "/api/devices/layout", + .method = HTTP_POST, + .handler = api_devices_layout_handler, + .user_ctx = NULL}; diff --git a/Provider/main/api/devices/list.cpp b/Provider/main/api/devices/list.cpp new file mode 100644 index 0000000..17a1140 --- /dev/null +++ b/Provider/main/api/devices/list.cpp @@ -0,0 +1,45 @@ +// GET /api/devices — List all registered devices + +#include "cJSON.h" +#include "esp_http_server.h" + +#include "types.hpp" +#include "device.hpp" + +internal esp_err_t api_devices_get_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "default_layout", kDefaultLayoutXml); + + cJSON *arr = cJSON_CreateArray(); + cJSON_AddItemToObject(root, "devices", arr); + + for (int i = 0; i < MAX_DEVICES; i++) + { + if (g_Devices[i].active) + { + cJSON *obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "mac", g_Devices[i].mac); + cJSON_AddBoolToObject(obj, "has_layout", g_Devices[i].xml_layout[0] != '\0'); + cJSON_AddStringToObject(obj, "xml_layout", g_Devices[i].xml_layout); + cJSON_AddItemToArray(arr, obj); + } + } + + const char *json = cJSON_PrintUnformatted(root); + httpd_resp_sendstr(req, json); + + free((void *)json); + cJSON_Delete(root); + + return ESP_OK; +} + +internal const httpd_uri_t api_devices_get_uri = { + .uri = "/api/devices", + .method = HTTP_GET, + .handler = api_devices_get_handler, + .user_ctx = NULL}; diff --git a/Provider/main/api/devices/register.cpp b/Provider/main/api/devices/register.cpp new file mode 100644 index 0000000..10d8697 --- /dev/null +++ b/Provider/main/api/devices/register.cpp @@ -0,0 +1,79 @@ +// POST /api/devices/register — Register a new device by MAC +// Body: {"mac": "AA:BB:CC:DD:EE:FF"} + +#include "cJSON.h" +#include "esp_http_server.h" +#include "esp_log.h" + +#include "types.hpp" +#include "device.hpp" + +internal const char *kTagDeviceRegister = "API_DEV_REG"; + +internal esp_err_t api_devices_register_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + + char buf[128]; + int received = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (received <= 0) + { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); + return ESP_FAIL; + } + buf[received] = '\0'; + + cJSON *body = cJSON_Parse(buf); + if (!body) + { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); + return ESP_FAIL; + } + + cJSON *mac_item = cJSON_GetObjectItem(body, "mac"); + if (!cJSON_IsString(mac_item) || !mac_item->valuestring || strlen(mac_item->valuestring) == 0) + { + cJSON_Delete(body); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing 'mac'"); + return ESP_FAIL; + } + + bool was_new = false; + device_t *dev = register_device(mac_item->valuestring, &was_new); + + if (!dev) + { + cJSON_Delete(body); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Device limit reached"); + return ESP_FAIL; + } + + cJSON *resp = cJSON_CreateObject(); + if (was_new) + { + cJSON_AddStringToObject(resp, "status", "ok"); + ESP_LOGI(kTagDeviceRegister, "Registered new device: %s", dev->mac); + } + else + { + cJSON_AddStringToObject(resp, "status", "already_registered"); + } + cJSON_AddStringToObject(resp, "mac", dev->mac); + + cJSON_Delete(body); + + const char *json = cJSON_PrintUnformatted(resp); + httpd_resp_sendstr(req, json); + + free((void *)json); + cJSON_Delete(resp); + + return ESP_OK; +} + +internal const httpd_uri_t api_devices_register_uri = { + .uri = "/api/devices/register", + .method = HTTP_POST, + .handler = api_devices_register_handler, + .user_ctx = NULL}; diff --git a/Provider/main/api/devices/screen.cpp b/Provider/main/api/devices/screen.cpp new file mode 100644 index 0000000..0d330e1 --- /dev/null +++ b/Provider/main/api/devices/screen.cpp @@ -0,0 +1,59 @@ +// GET /api/devices/screen?mac=XX — Return the image URL for a device's current screen + +#include "cJSON.h" +#include "esp_http_server.h" + +#include "types.hpp" +#include "device.hpp" + +internal esp_err_t api_devices_screen_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + + // Extract mac query parameter + char mac[18] = {}; + size_t buf_len = httpd_req_get_url_query_len(req) + 1; + if (buf_len > 1) + { + char query[64] = {}; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) + { + httpd_query_key_value(query, "mac", mac, sizeof(mac)); + } + } + + if (mac[0] == '\0') + { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing 'mac' query param"); + return ESP_FAIL; + } + + device_t *dev = find_device(mac); + if (!dev) + { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Device not registered"); + return ESP_FAIL; + } + + // Build image_url: /api/devices/screen.png?mac=XX + char image_url[64]; + snprintf(image_url, sizeof(image_url), "/api/devices/screen.png?mac=%s", mac); + + cJSON *resp = cJSON_CreateObject(); + cJSON_AddStringToObject(resp, "image_url", image_url); + + const char *json = cJSON_PrintUnformatted(resp); + httpd_resp_sendstr(req, json); + + free((void *)json); + cJSON_Delete(resp); + + return ESP_OK; +} + +internal const httpd_uri_t api_devices_screen_info_uri = { + .uri = "/api/devices/screen", + .method = HTTP_GET, + .handler = api_devices_screen_handler, + .user_ctx = NULL}; diff --git a/Provider/main/api/devices/screen_image.cpp b/Provider/main/api/devices/screen_image.cpp new file mode 100644 index 0000000..eaae90e --- /dev/null +++ b/Provider/main/api/devices/screen_image.cpp @@ -0,0 +1,249 @@ +// GET /api/devices/screen.png?mac=XX — Render and return a PNG for the device's +// current screen Uses LVGL to render the device's XML layout (or a fallback +// label) then encodes to PNG via lodepng. + +#include "esp_heap_caps.h" +#include "esp_http_server.h" +#include "esp_log.h" +#include "lodepng/lodepng.h" +#include "lodepng_alloc.hpp" +#include "lv_setup.hpp" +#include "lvgl.h" +#include + +#include "device.hpp" +#include "types.hpp" + +internal const char *kTagDeviceScreenImage = "API_DEV_SCREEN_IMG"; + +internal esp_err_t api_devices_screen_image_handler(httpd_req_t *req) +{ + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_set_hdr(req, "Cache-Control", + "no-cache, no-store, must-revalidate"); + httpd_resp_set_type(req, "image/png"); + + // Extract mac query parameter + char mac[18] = {}; + size_t buf_len = httpd_req_get_url_query_len(req) + 1; + if (buf_len > 1) + { + char query[64] = {}; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) + { + httpd_query_key_value(query, "mac", mac, sizeof(mac)); + } + } + + if (mac[0] == '\0') + { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, + "Missing 'mac' query param"); + return ESP_FAIL; + } + + device_t *dev = find_device(mac); + if (!dev) + { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Device not registered"); + return ESP_FAIL; + } + + // --- LVGL rendering (mutex-protected) --- + if (xSemaphoreTake(g_LvglMutex, pdMS_TO_TICKS(5000)) != pdTRUE) + { + ESP_LOGE(kTagDeviceScreenImage, "Failed to get LVGL mutex"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "LVGL busy"); + return ESP_FAIL; + } + + lv_obj_t *scr = lv_screen_active(); + + // Clear all children from the active screen + lv_obj_clean(scr); + + // White background for grayscale + lv_obj_set_style_bg_color(scr, lv_color_white(), LV_PART_MAIN); + + // Setup the MAC address subject so the XML can bind to it + static lv_subject_t mac_subject; + // Two buffers are needed by LVGL for string observers (current and previous) + static char mac_buf[18]; + static char mac_prev_buf[18]; + + strncpy(mac_buf, mac, sizeof(mac_buf)); + strncpy(mac_prev_buf, mac, sizeof(mac_prev_buf)); + + lv_subject_init_string(&mac_subject, mac_buf, mac_prev_buf, sizeof(mac_buf), + mac); + + // Register the subject in the global XML scope under the name "device_mac" + lv_xml_component_scope_t *global_scope = + lv_xml_component_get_scope("globals"); + if (global_scope) + { + lv_xml_register_subject(global_scope, "device_mac", &mac_subject); + ESP_LOGI(kTagDeviceScreenImage, + "Registered subject 'device_mac' with value: %s", mac); + } + + bool render_success = false; + + // 1. Prepare the XML payload + const char *xml_to_register = NULL; + static char + xml_buffer[DEVICE_XML_MAX + 100]; // static buffer to avoid stack overflow + + if (dev->xml_layout[0] == '\0') + { + ESP_LOGI(kTagDeviceScreenImage, "Device %s has no layout xml.", mac); + return ESP_FAIL; + } + + if (strstr(dev->xml_layout, " wrapped XML + xml_to_register = dev->xml_layout; + ESP_LOGI(kTagDeviceScreenImage, + "XML already contains , passing directly to parser."); + } + + // 2. Register the XML payload as a component + lv_result_t res = + lv_xml_register_component_from_data("current_device", xml_to_register); + + if (res == LV_RESULT_OK) + { + ESP_LOGI(kTagDeviceScreenImage, "Successfully registered XML for device %s", + mac); + + // 3. Since we enforce now, we always create a screen instance + lv_obj_t *new_scr = lv_xml_create_screen("current_device"); + + if (new_scr) + { + // We must load this newly created screen to make it active before + // rendering + lv_screen_load(new_scr); + scr = new_scr; // Update local pointer since active screen changed + render_success = true; + } + else + { + ESP_LOGE(kTagDeviceScreenImage, + "lv_xml_create_screen failed for device %s", mac); + } + } + else + { + ESP_LOGE(kTagDeviceScreenImage, + "lv_xml_register_component_from_data failed for device %s", mac); + } + + // 3. Fallback if LVGL XML parsing or creation failed + if (!render_success) + { + ESP_LOGW(kTagDeviceScreenImage, + "XML render failed, falling back to raw text layout"); + lv_obj_t *label = lv_label_create(scr); + lv_label_set_text(label, "XML Parsing Error\nSee serial log"); + lv_obj_set_style_text_color(label, lv_color_black(), LV_PART_MAIN); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + } + + // Force LVGL to fully render the screen + lv_refr_now(g_LvglDisplay); + + lv_draw_buf_t *draw_buf = lv_display_get_buf_active(g_LvglDisplay); + if (!draw_buf) + { + xSemaphoreGive(g_LvglMutex); + ESP_LOGE(kTagDeviceScreenImage, "No active draw buffer"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Display uninitialized"); + return ESP_FAIL; + } + + uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; + uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; + + // Allocate bounding memory for quantizing RGB565 buffer into tightly packed + // 8-bit PNG data. + uint8_t *packed_data = + (uint8_t *)heap_caps_malloc(width * height, MALLOC_CAP_SPIRAM); + if (!packed_data) + { + packed_data = (uint8_t *)malloc(width * height); + if (!packed_data) + { + xSemaphoreGive(g_LvglMutex); + ESP_LOGE(kTagDeviceScreenImage, "Failed to allocate packed buffer"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Out of memory"); + return ESP_FAIL; + } + } + + // LVGL renders into RGB565 (2 bytes per pixel). + // Parse pixels, extract luminance, and quantize to 4 levels (0, 85, 170, 255). + for (uint32_t y = 0; y < height; ++y) + { + const uint16_t *src_row = (const uint16_t *)((const uint8_t *)draw_buf->data + (y * draw_buf->header.stride)); + uint8_t *dst_row = packed_data + (y * width); + + for (uint32_t x = 0; x < width; ++x) + { + uint16_t c = src_row[x]; + // Expand 5/6/5 components + uint8_t r_5 = (c >> 11) & 0x1F; + uint8_t g_6 = (c >> 5) & 0x3F; + uint8_t b_5 = c & 0x1F; + + // Unpack to 8-bit true values + uint8_t r = (r_5 << 3) | (r_5 >> 2); + uint8_t g = (g_6 << 2) | (g_6 >> 4); + uint8_t b = (b_5 << 3) | (b_5 >> 2); + + // Simple luminance + uint8_t lum = (r * 77 + g * 150 + b * 29) >> 8; + + // 4-level linear quantization (0, 85, 170, 255) + dst_row[x] = (lum >> 6) * 85; + } + } + + // Encode to PNG + unsigned char *png = nullptr; + size_t pngsize = 0; + + lodepng_allocator_reset(); + + ESP_LOGI(kTagDeviceScreenImage, "Encoding %lux%lu PNG for device %s", width, + height, mac); + unsigned error = lodepng_encode_memory(&png, &pngsize, packed_data, width, + height, LCT_GREY, 8); + + free(packed_data); + + xSemaphoreGive(g_LvglMutex); + + if (error) + { + ESP_LOGE(kTagDeviceScreenImage, "PNG encoding error %u: %s", error, + lodepng_error_text(error)); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "PNG generation failed"); + return ESP_FAIL; + } + + ESP_LOGI(kTagDeviceScreenImage, "PNG ready: %zu bytes. Sending...", pngsize); + esp_err_t sendRes = httpd_resp_send(req, (const char *)png, pngsize); + + return sendRes; +} + +internal const httpd_uri_t api_devices_screen_image_uri = { + .uri = "/api/devices/screen.png", + .method = HTTP_GET, + .handler = api_devices_screen_image_handler, + .user_ctx = NULL}; diff --git a/Provider/main/api/devices/store.cpp b/Provider/main/api/devices/store.cpp new file mode 100644 index 0000000..ed4f28e --- /dev/null +++ b/Provider/main/api/devices/store.cpp @@ -0,0 +1,57 @@ +// Device data store: CRUD helpers + +#include "device.hpp" + +// Find a device by MAC address, returns nullptr if not found +internal device_t *find_device(const char *mac) +{ + for (int i = 0; i < MAX_DEVICES; i++) + { + if (g_Devices[i].active && strcmp(g_Devices[i].mac, mac) == 0) + { + return &g_Devices[i]; + } + } + return nullptr; +} + +// Register a device by MAC. Returns pointer to device (existing or new). +// Sets *was_new to true if it was freshly registered. +internal device_t *register_device(const char *mac, bool *was_new) +{ + *was_new = false; + + // Check for existing + device_t *existing = find_device(mac); + if (existing) + { + return existing; + } + + // Find a free slot + for (int i = 0; i < MAX_DEVICES; i++) + { + if (!g_Devices[i].active) + { + strlcpy(g_Devices[i].mac, mac, sizeof(g_Devices[i].mac)); + g_Devices[i].active = true; + strlcpy(g_Devices[i].xml_layout, kDefaultLayoutXml, sizeof(g_Devices[i].xml_layout)); + *was_new = true; + return &g_Devices[i]; + } + } + + return nullptr; // All slots full +} + +// Update the XML layout for a device. Returns true on success. +internal bool update_device_layout(const char *mac, const char *xml) +{ + device_t *dev = find_device(mac); + if (!dev) + { + return false; + } + strlcpy(dev->xml_layout, xml, sizeof(dev->xml_layout)); + return true; +} diff --git a/Provider/main/api/devices/unity.cpp b/Provider/main/api/devices/unity.cpp new file mode 100644 index 0000000..498cf30 --- /dev/null +++ b/Provider/main/api/devices/unity.cpp @@ -0,0 +1,9 @@ +// Unity build entry for device endpoints +// clang-format off +#include "api/devices/store.cpp" +#include "api/devices/list.cpp" +#include "api/devices/register.cpp" +#include "api/devices/layout.cpp" +#include "api/devices/screen.cpp" +#include "api/devices/screen_image.cpp" +// clang-format on diff --git a/Provider/main/api/display/image.cpp b/Provider/main/api/display/image.cpp index e9d08c3..9997b3e 100644 --- a/Provider/main/api/display/image.cpp +++ b/Provider/main/api/display/image.cpp @@ -1,113 +1,145 @@ -#include "../../lv_setup.hpp" #include "../../lodepng/lodepng.h" +#include "../../lodepng_alloc.hpp" +#include "../../lv_setup.hpp" +#include "esp_heap_caps.h" #include "esp_http_server.h" #include "esp_log.h" -#include "esp_heap_caps.h" #include "esp_random.h" #include "lvgl.h" #include -#include "../../lodepng_alloc.hpp" internal const char *kTagDisplayImage = "API_DISPLAY_IMAGE"; internal esp_err_t api_display_image_handler(httpd_req_t *req) { - httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - - // We are generating PNG on the fly, don't let it be cached locally immediately - httpd_resp_set_hdr(req, "Cache-Control", "no-cache, no-store, must-revalidate"); - httpd_resp_set_type(req, "image/png"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - if (xSemaphoreTake(g_LvglMutex, pdMS_TO_TICKS(5000)) != pdTRUE) - { - ESP_LOGE(kTagDisplayImage, "Failed to get LVGL mutex"); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "LVGL busy"); - return ESP_FAIL; - } + // We are generating PNG on the fly, don't let it be cached locally + // immediately + httpd_resp_set_hdr(req, "Cache-Control", + "no-cache, no-store, must-revalidate"); + httpd_resp_set_type(req, "image/png"); - // Change the background color securely to a random grayscale value - // esp_random() returns 32 bits, we just take the lowest 8. - uint8_t rand_gray = esp_random() & 0xFF; - lv_obj_t* active_screen = lv_screen_active(); - lv_obj_set_style_bg_color(active_screen, lv_color_make(rand_gray, rand_gray, rand_gray), LV_PART_MAIN); + if (xSemaphoreTake(g_LvglMutex, pdMS_TO_TICKS(5000)) != pdTRUE) + { + ESP_LOGE(kTagDisplayImage, "Failed to get LVGL mutex"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "LVGL busy"); + return ESP_FAIL; + } - // Force a screen refresh to get the latest rendered frame - lv_refr_now(g_LvglDisplay); + // Change the background color securely to a random grayscale value + // esp_random() returns 32 bits, we just take the lowest 8. + uint8_t rand_gray = esp_random() & 0xFF; + lv_obj_t *active_screen = lv_screen_active(); + // lv_obj_set_style_bg_color(active_screen, lv_color_make(rand_gray, + // rand_gray, rand_gray), LV_PART_MAIN); - lv_draw_buf_t *draw_buf = lv_display_get_buf_active(g_LvglDisplay); - if (!draw_buf) - { - xSemaphoreGive(g_LvglMutex); - ESP_LOGE(kTagDisplayImage, "No active draw buffer available"); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Display uninitialized"); - return ESP_FAIL; - } - - uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; - uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; - - // LodePNG expects tightly packed data without stride padding. - // Ensure we copy the data if stride differs from width. - uint8_t *packed_data = (uint8_t *)draw_buf->data; - bool needs_free = false; - - if (draw_buf->header.stride != width) - { - ESP_LOGI(kTagDisplayImage, "Stride %lu differs from width %lu. Repacking buffer...", draw_buf->header.stride, width); - packed_data = (uint8_t *)heap_caps_malloc(width * height, MALLOC_CAP_SPIRAM); - if (!packed_data) - { - xSemaphoreGive(g_LvglMutex); - ESP_LOGE(kTagDisplayImage, "Failed to allocate packed buffer in PSRAM"); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); - return ESP_FAIL; - } - needs_free = true; - - for (uint32_t y = 0; y < height; ++y) - { - memcpy(packed_data + (y * width), (uint8_t *)draw_buf->data + (y * draw_buf->header.stride), width); - } - } - - // Convert LVGL 8-bit L8 buffer to 8-bit grayscale PNG using LodePNG. - // LCT_GREY = 0, bitdepth = 8 - unsigned char *png = nullptr; - size_t pngsize = 0; - - // We are about to start a huge memory operation inside LodePNG. - // We reset our 2MB PSRAM bump allocator to 0 bytes used. - lodepng_allocator_reset(); - - ESP_LOGI(kTagDisplayImage, "Encoding %lux%lu frame to PNG...", width, height); - unsigned error = lodepng_encode_memory(&png, &pngsize, packed_data, width, height, LCT_GREY, 8); - - if (needs_free) - { - free(packed_data); - } + // Force a screen refresh to get the latest rendered frame + lv_refr_now(g_LvglDisplay); + lv_draw_buf_t *draw_buf = lv_display_get_buf_active(g_LvglDisplay); + if (!draw_buf) + { xSemaphoreGive(g_LvglMutex); + ESP_LOGE(kTagDisplayImage, "No active draw buffer available"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Display uninitialized"); + return ESP_FAIL; + } - if (error) + uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; + uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; + + // We allocate a new buffer for the tightly packed 8-bit PNG grayscale data. + // Converting RGB565 frame to 4-level grayscale (quantized to 0, 85, 170, 255). + uint8_t *packed_data = + (uint8_t *)heap_caps_malloc(width * height, MALLOC_CAP_SPIRAM); + if (!packed_data) + { + packed_data = (uint8_t *)malloc(width * height); // Fallback + if (!packed_data) { - ESP_LOGE(kTagDisplayImage, "PNG encoding error %u: %s", error, lodepng_error_text(error)); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "PNG generation failed"); - return ESP_FAIL; + xSemaphoreGive(g_LvglMutex); + ESP_LOGE(kTagDisplayImage, "Failed to allocate packed buffer"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Out of memory"); + return ESP_FAIL; } + } - ESP_LOGI(kTagDisplayImage, "Prepared PNG, size: %zu bytes. Sending to client...", pngsize); - esp_err_t res = httpd_resp_send(req, (const char *)png, pngsize); + // LVGL renders into RGB565 (2 bytes per pixel). + // Iterating to create an 8-bit grayscale PNG using 4 specific values. + for (uint32_t y = 0; y < height; ++y) + { + const uint16_t *src_row = + (const uint16_t *)((const uint8_t *)draw_buf->data + + (y * draw_buf->header.stride)); + uint8_t *dst_row = packed_data + (y * width); - // No need to free(png) because it is managed by our bump allocator - // which automatically resets the entire 2MB buffer to 0 next time - // lodepng_allocator_reset() is called. + for (uint32_t x = 0; x < width; ++x) + { + uint16_t c = src_row[x]; + // Note: LVGL may use swapped bytes for SPI rendering depending on config, + // but in memory RGB565 is standard if no SWAP is active. Usually standard + // RGB565 format: R(5) G(6) B(5) + uint8_t r_5 = (c >> 11) & 0x1F; + uint8_t g_6 = (c >> 5) & 0x3F; + uint8_t b_5 = c & 0x1F; - return res; + // Expand to 8 bits + uint8_t r = (r_5 << 3) | (r_5 >> 2); + uint8_t g = (g_6 << 2) | (g_6 >> 4); + uint8_t b = (b_5 << 3) | (b_5 >> 2); + + // Simple luminance calculation (fast) + uint8_t lum = (r * 77 + g * 150 + b * 29) >> 8; + + // Quantize to 4 levels (0..3) + uint8_t level = lum >> 6; + + // Expand level back to 8-bit for PNG: 0, 85, 170, 255 + dst_row[x] = level * 85; + } + } + + // Convert LVGL 8-bit L8 buffer to 8-bit grayscale PNG using LodePNG. + // LCT_GREY = 0, bitdepth = 8 + unsigned char *png = nullptr; + size_t pngsize = 0; + + // We are about to start a huge memory operation inside LodePNG. + // We reset our 3MB PSRAM bump allocator to 0 bytes used. + lodepng_allocator_reset(); + + ESP_LOGI(kTagDisplayImage, "Encoding %lux%lu frame to PNG...", width, height); + unsigned error = lodepng_encode_memory(&png, &pngsize, packed_data, width, + height, LCT_GREY, 8); + + free(packed_data); + + xSemaphoreGive(g_LvglMutex); + + if (error) + { + ESP_LOGE(kTagDisplayImage, "PNG encoding error %u: %s", error, + lodepng_error_text(error)); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "PNG generation failed"); + return ESP_FAIL; + } + + ESP_LOGI(kTagDisplayImage, + "Prepared PNG, size: %zu bytes. Sending to client...", pngsize); + esp_err_t res = httpd_resp_send(req, (const char *)png, pngsize); + + // No need to free(png) because it is managed by our bump allocator + // which automatically resets the entire 2MB buffer to 0 next time + // lodepng_allocator_reset() is called. + + return res; } -httpd_uri_t api_display_image_uri = { - .uri = "/api/display/image.png", - .method = HTTP_GET, - .handler = api_display_image_handler, - .user_ctx = NULL}; +httpd_uri_t api_display_image_uri = {.uri = "/api/display/image.png", + .method = HTTP_GET, + .handler = api_display_image_handler, + .user_ctx = NULL}; diff --git a/Provider/main/device.hpp b/Provider/main/device.hpp new file mode 100644 index 0000000..9e66284 --- /dev/null +++ b/Provider/main/device.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "types.hpp" + +constexpr int MAX_DEVICES = 8; +constexpr int DEVICE_XML_MAX = 2048; + +constexpr char kDefaultLayoutXml[] = + "\n" + " \n" + " \n" + " \n" + " \n" + ""; + +struct device_t +{ + char mac[18]; // "AA:BB:CC:DD:EE:FF\0" + bool active; // Slot in use + char xml_layout[DEVICE_XML_MAX]; // LVGL XML string for the current screen +}; + +internal device_t g_Devices[MAX_DEVICES] = {}; diff --git a/Provider/main/http_server.cpp b/Provider/main/http_server.cpp index 7853991..2622a95 100644 --- a/Provider/main/http_server.cpp +++ b/Provider/main/http_server.cpp @@ -20,6 +20,7 @@ #include "api/system/info.cpp" #include "api/system/reboot.cpp" #include "api/display/unity.cpp" +#include "api/devices/unity.cpp" #include "api/tasks/unity.cpp" #include "api/users/unity.cpp" @@ -264,7 +265,7 @@ internal httpd_handle_t start_webserver(void) httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.uri_match_fn = httpd_uri_match_wildcard; - config.max_uri_handlers = 20; + config.max_uri_handlers = 26; config.max_open_sockets = 24; config.lru_purge_enable = true; config.stack_size = 16384; @@ -301,6 +302,13 @@ internal httpd_handle_t start_webserver(void) httpd_register_uri_handler(server, &api_tasks_update_uri); httpd_register_uri_handler(server, &api_tasks_delete_uri); + // Register device API routes + httpd_register_uri_handler(server, &api_devices_get_uri); + httpd_register_uri_handler(server, &api_devices_register_uri); + httpd_register_uri_handler(server, &api_devices_layout_uri); + httpd_register_uri_handler(server, &api_devices_screen_info_uri); + httpd_register_uri_handler(server, &api_devices_screen_image_uri); + // Populate dummy data for development (debug builds only) #ifndef NDEBUG seed_users(); diff --git a/Provider/main/idf_component.yml b/Provider/main/idf_component.yml index e652c64..b841858 100644 --- a/Provider/main/idf_component.yml +++ b/Provider/main/idf_component.yml @@ -18,4 +18,6 @@ dependencies: espressif/mdns: ^1.4.1 espressif/ethernet_init: ^1.3.0 joltwallet/littlefs: "^1.20" # https://github.com/joltwallet/esp_littlefs - lvgl/lvgl: "^9.2.0" + lvgl/lvgl: "9.4.0" + network: + path: "../../components/network" diff --git a/Provider/main/lodepng_alloc.cpp b/Provider/main/lodepng_alloc.cpp index aca6404..275068f 100644 --- a/Provider/main/lodepng_alloc.cpp +++ b/Provider/main/lodepng_alloc.cpp @@ -4,149 +4,160 @@ #include // LVGL's LodePNG memory optimization -// Instead of standard heap allocations which fragment quickly and crash on the ESP32, -// we allocate a single massive buffer in PSRAM and just bump a pointer during encode! +// Instead of standard heap allocations which fragment quickly and crash on the +// ESP32, we allocate a single massive buffer in PSRAM and just bump a pointer +// during encode! -static const char* kTagLodeAlloc = "LODE_ALLOC"; +static const char *kTagLodeAlloc = "LODE_ALLOC"; -// 2MB buffer for LodePNG encoding intermediate state. -// A typical 800x480 grayscale PNG should compress to ~50-100KB, but the dynamic window -// matching and filtering algorithms need a good amount of scratch space. -// We can tune this down to 1MB if 2MB is too aggressive, but PSRAM provides 8MB. -#define LODEPNG_ALLOC_POOL_SIZE (2 * 1024 * 1024) +// 2MB buffer for LodePNG encoding intermediate state. +// A typical 800x480 grayscale PNG should compress to ~50-100KB, but the dynamic +// window matching and filtering algorithms need a good amount of scratch space. +// We can tune this down to 1MB if 2MB is too aggressive, but PSRAM provides +// 8MB. +#define LODEPNG_ALLOC_POOL_SIZE (1 * 1024 * 1024) -static uint8_t* s_lodepng_pool = nullptr; +static uint8_t *s_lodepng_pool = nullptr; static size_t s_lodepng_pool_used = 0; void lodepng_allocator_init() { - if (s_lodepng_pool != nullptr) return; + if (s_lodepng_pool != nullptr) + return; - ESP_LOGI(kTagLodeAlloc, "Allocating %d bytes in PSRAM for LodePNG bump allocator...", LODEPNG_ALLOC_POOL_SIZE); - - // SPIRAM fallback to internal if someone tests without a PSRAM chip - s_lodepng_pool = (uint8_t*)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, MALLOC_CAP_SPIRAM); - if (!s_lodepng_pool) - { - s_lodepng_pool = (uint8_t*)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, MALLOC_CAP_DEFAULT); - } + ESP_LOGI(kTagLodeAlloc, + "Allocating %d bytes in PSRAM for LodePNG bump allocator...", + LODEPNG_ALLOC_POOL_SIZE); - if (!s_lodepng_pool) - { - ESP_LOGE(kTagLodeAlloc, "CRITICAL: Failed to allocate LodePNG PSRAM pool!"); - } + // SPIRAM fallback to internal if someone tests without a PSRAM chip + s_lodepng_pool = + (uint8_t *)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, MALLOC_CAP_SPIRAM); + if (!s_lodepng_pool) + { + s_lodepng_pool = (uint8_t *)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, + MALLOC_CAP_DEFAULT); + } + + if (!s_lodepng_pool) + { + ESP_LOGE(kTagLodeAlloc, "CRITICAL: Failed to allocate LodePNG PSRAM pool!"); + } } -void lodepng_allocator_reset() -{ - s_lodepng_pool_used = 0; -} +void lodepng_allocator_reset() { s_lodepng_pool_used = 0; } void lodepng_allocator_free() { - if (s_lodepng_pool) - { - free(s_lodepng_pool); - s_lodepng_pool = nullptr; - } - s_lodepng_pool_used = 0; + if (s_lodepng_pool) + { + free(s_lodepng_pool); + s_lodepng_pool = nullptr; + } + s_lodepng_pool_used = 0; } // ---------------------------------------------------- // Custom Allocators injected into lodepng.c // ---------------------------------------------------- -// To support realloc properly, we prefix each allocation with an 8-byte header storing the size. -struct AllocHeader { - size_t size; +// To support realloc properly, we prefix each allocation with an 8-byte header +// storing the size. +struct AllocHeader +{ + size_t size; }; -void* lodepng_custom_malloc(size_t size) +void *lodepng_custom_malloc(size_t size) { - if (!s_lodepng_pool) - { - ESP_LOGE(kTagLodeAlloc, "lodepng_malloc called before lodepng_allocator_init!"); - return nullptr; - } + if (!s_lodepng_pool) + { + ESP_LOGE(kTagLodeAlloc, + "lodepng_malloc called before lodepng_allocator_init!"); + return nullptr; + } - // Align size to 8 bytes to avoid unaligned access faults - size_t aligned_size = (size + 7) & ~7; - size_t total_alloc = sizeof(AllocHeader) + aligned_size; + // Align size to 8 bytes to avoid unaligned access faults + size_t aligned_size = (size + 7) & ~7; + size_t total_alloc = sizeof(AllocHeader) + aligned_size; - if (s_lodepng_pool_used + total_alloc > LODEPNG_ALLOC_POOL_SIZE) - { - ESP_LOGE(kTagLodeAlloc, "LodePNG pool exhausted! Requested: %zu, Used: %zu, Total: %d", size, s_lodepng_pool_used, LODEPNG_ALLOC_POOL_SIZE); - return nullptr; - } + if (s_lodepng_pool_used + total_alloc > LODEPNG_ALLOC_POOL_SIZE) + { + ESP_LOGE(kTagLodeAlloc, + "LodePNG pool exhausted! Requested: %zu, Used: %zu, Total: %d", + size, s_lodepng_pool_used, LODEPNG_ALLOC_POOL_SIZE); + return nullptr; + } - // Grab pointer and bump - uint8_t* ptr = s_lodepng_pool + s_lodepng_pool_used; - s_lodepng_pool_used += total_alloc; + // Grab pointer and bump + uint8_t *ptr = s_lodepng_pool + s_lodepng_pool_used; + s_lodepng_pool_used += total_alloc; - // Write header - AllocHeader* header = (AllocHeader*)ptr; - header->size = size; // We store exact size for realloc memcpy bounds + // Write header + AllocHeader *header = (AllocHeader *)ptr; + header->size = size; // We store exact size for realloc memcpy bounds - // Return pointer right after header - return ptr + sizeof(AllocHeader); + // Return pointer right after header + return ptr + sizeof(AllocHeader); } -void* lodepng_custom_realloc(void* ptr, size_t new_size) +void *lodepng_custom_realloc(void *ptr, size_t new_size) { - if (!ptr) + if (!ptr) + { + return lodepng_custom_malloc(new_size); + } + + if (new_size == 0) + { + lodepng_custom_free(ptr); + return nullptr; + } + + // Get original header + uint8_t *orig_ptr = (uint8_t *)ptr - sizeof(AllocHeader); + AllocHeader *header = (AllocHeader *)orig_ptr; + + size_t old_size = header->size; + if (new_size <= old_size) + { + // Don't shrink to save time, bump allocator can't reclaim it easily anyway. + return ptr; + } + + // Let's see if this ptr was the *very last* allocation. + // If so, we can just expand it in place! + size_t old_aligned_size = (old_size + 7) & ~7; + if (orig_ptr + sizeof(AllocHeader) + old_aligned_size == + s_lodepng_pool + s_lodepng_pool_used) + { + // We are at the end! Just bump further! + size_t new_aligned_size = (new_size + 7) & ~7; + size_t size_diff = new_aligned_size - old_aligned_size; + + if (s_lodepng_pool_used + size_diff > LODEPNG_ALLOC_POOL_SIZE) { - return lodepng_custom_malloc(new_size); - } - - if (new_size == 0) - { - lodepng_custom_free(ptr); - return nullptr; + ESP_LOGE(kTagLodeAlloc, + "LodePNG pool exhausted during in-place realloc!"); + return nullptr; } - // Get original header - uint8_t* orig_ptr = (uint8_t*)ptr - sizeof(AllocHeader); - AllocHeader* header = (AllocHeader*)orig_ptr; + s_lodepng_pool_used += size_diff; + header->size = new_size; + return ptr; + } - size_t old_size = header->size; - if (new_size <= old_size) - { - // Don't shrink to save time, bump allocator can't reclaim it easily anyway. - return ptr; - } + // Otherwise, we have to copy into a new block + void *new_ptr = lodepng_custom_malloc(new_size); + if (new_ptr) + { + memcpy(new_ptr, ptr, old_size); + } - // Let's see if this ptr was the *very last* allocation. - // If so, we can just expand it in place! - size_t old_aligned_size = (old_size + 7) & ~7; - if (orig_ptr + sizeof(AllocHeader) + old_aligned_size == s_lodepng_pool + s_lodepng_pool_used) - { - // We are at the end! Just bump further! - size_t new_aligned_size = (new_size + 7) & ~7; - size_t size_diff = new_aligned_size - old_aligned_size; - - if (s_lodepng_pool_used + size_diff > LODEPNG_ALLOC_POOL_SIZE) - { - ESP_LOGE(kTagLodeAlloc, "LodePNG pool exhausted during in-place realloc!"); - return nullptr; - } - - s_lodepng_pool_used += size_diff; - header->size = new_size; - return ptr; - } - - // Otherwise, we have to copy into a new block - void* new_ptr = lodepng_custom_malloc(new_size); - if (new_ptr) - { - memcpy(new_ptr, ptr, old_size); - } - - return new_ptr; + return new_ptr; } -void lodepng_custom_free(void* ptr) +void lodepng_custom_free(void *ptr) { - // No-op! The bump pointer will just reset to 0 once the API endpoint is done! - (void)ptr; + // No-op! The bump pointer will just reset to 0 once the API endpoint is done! + (void)ptr; } diff --git a/Provider/main/lv_setup.cpp b/Provider/main/lv_setup.cpp index b532bac..6e1235e 100644 --- a/Provider/main/lv_setup.cpp +++ b/Provider/main/lv_setup.cpp @@ -1,8 +1,11 @@ #include "lv_setup.hpp" + #include "esp_heap_caps.h" #include "esp_log.h" #include "esp_timer.h" #include "freertos/task.h" +#include "lodepng_alloc.hpp" + #include "types.hpp" internal const char *kTagLvgl = "LVGL"; @@ -13,94 +16,117 @@ uint8_t *g_LvglDrawBuffer = nullptr; internal void lvgl_tick_task(void *arg) { - while (true) - { - vTaskDelay(pdMS_TO_TICKS(10)); + while (true) + { + vTaskDelay(pdMS_TO_TICKS(10)); - if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE) - { - lv_timer_handler(); - xSemaphoreGive(g_LvglMutex); - } + if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE) + { + lv_timer_handler(); + xSemaphoreGive(g_LvglMutex); } + } } internal uint32_t my_tick_get_cb() { - return (uint32_t)(esp_timer_get_time() / 1000); + return (uint32_t)(esp_timer_get_time() / 1000); } -internal void lv_dummy_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) +internal void lv_dummy_flush_cb(lv_display_t *disp, const lv_area_t *area, + uint8_t *px_map) { - // Headless display, so we don't actually flush to SPI/I2C. - // We just tell LVGL that the "flush" is completed so it unblocks wait_for_flushing. - lv_display_flush_ready(disp); + // Headless display, so we don't actually flush to SPI/I2C. + // We just tell LVGL that the "flush" is completed so it unblocks + // wait_for_flushing. + lv_display_flush_ready(disp); } internal void lv_draw_sample_ui() { - lv_obj_t *scr = lv_screen_active(); - // Default background to white for the grayscale PNG - lv_obj_set_style_bg_color(scr, lv_color_white(), 0); + lv_obj_t *scr = lv_screen_active(); + // Default background to white for the grayscale PNG + lv_obj_set_style_bg_color(scr, lv_color_white(), 0); - lv_obj_t *label = lv_label_create(scr); - lv_label_set_text(label, "Calendink Provider\nLVGL Headless Renderer"); - lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + lv_obj_t *label = lv_label_create(scr); + lv_label_set_text(label, "Calendink Provider\nLVGL Headless Renderer"); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + + static lv_style_t style; + lv_style_init(&style); + + lv_style_set_line_color(&style, lv_palette_main(LV_PALETTE_GREY)); + lv_style_set_line_width(&style, 6); + lv_style_set_line_rounded(&style, true); + + /*Create an object with the new style*/ + lv_obj_t *obj = lv_line_create(scr); + lv_obj_add_style(obj, &style, 0); + + static lv_point_precise_t p[] = {{10, 30}, {30, 50}, {100, 0}}; + lv_line_set_points(obj, p, 3); + + lv_obj_center(obj); } void setup_lvgl() { - ESP_LOGI(kTagLvgl, "Initializing LVGL"); + ESP_LOGI(kTagLvgl, "Initializing LVGL"); - g_LvglMutex = xSemaphoreCreateMutex(); + g_LvglMutex = xSemaphoreCreateMutex(); - lv_init(); - lv_tick_set_cb(my_tick_get_cb); + lv_init(); + lv_tick_set_cb(my_tick_get_cb); - uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; - uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; + uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; + uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; - // Create a virtual display - g_LvglDisplay = lv_display_create(width, height); - lv_display_set_flush_cb(g_LvglDisplay, lv_dummy_flush_cb); + // Create a virtual display + g_LvglDisplay = lv_display_create(width, height); + lv_display_set_flush_cb(g_LvglDisplay, lv_dummy_flush_cb); - // Initialize LodePNG custom bump allocator - lodepng_allocator_init(); + // Initialize LodePNG custom bump allocator + lodepng_allocator_init(); - // Allocate draw buffers in PSRAM - // Using LV_COLOR_FORMAT_L8 (1 byte per pixel) - size_t buf_size = LV_DRAW_BUF_SIZE(width, height, LV_COLOR_FORMAT_L8); - - // Fallback to MALLOC_CAP_DEFAULT if we can't get SPIRAM (for debugging without it) - void *buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM); - if (!buf1) - { - ESP_LOGW(kTagLvgl, "Failed to allocate LVGL draw buffer in PSRAM, falling back to internal RAM"); - buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_DEFAULT); - } - - if (!buf1) - { - ESP_LOGE(kTagLvgl, "Failed to allocate LVGL draw buffer entirely."); - return; - } + // Allocate draw buffers in PSRAM + // Using LV_COLOR_FORMAT_RGB565 (2 bytes per pixel) + size_t buf_size = LV_DRAW_BUF_SIZE(width, height, LV_COLOR_FORMAT_RGB565); - g_LvglDrawBuffer = (uint8_t *)buf1; + // Fallback to MALLOC_CAP_DEFAULT if we can't get SPIRAM (for debugging + // without it) + void *buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM); + if (!buf1) + { + ESP_LOGW(kTagLvgl, "Failed to allocate LVGL draw buffer in PSRAM, falling " + "back to internal RAM"); + buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_DEFAULT); + } - lv_display_set_buffers(g_LvglDisplay, buf1, nullptr, buf_size, LV_DISPLAY_RENDER_MODE_FULL); - - // Explicitly set the color format of the display if it's set in sdkconfig/driver - lv_display_set_color_format(g_LvglDisplay, LV_COLOR_FORMAT_L8); + if (!buf1) + { + ESP_LOGE(kTagLvgl, "Failed to allocate LVGL draw buffer entirely."); + return; + } - // Create the background task for the LVGL timer - xTaskCreate(lvgl_tick_task, "LVGL Tick", 4096, nullptr, 5, nullptr); + g_LvglDrawBuffer = (uint8_t *)buf1; - // Draw the sample UI - if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE) - { - lv_draw_sample_ui(); - xSemaphoreGive(g_LvglMutex); - } + // Explicitly set the color format of the display FIRST + // so that stride and byte-per-pixel calculations align with our buffer. + lv_display_set_color_format(g_LvglDisplay, LV_COLOR_FORMAT_RGB565); - ESP_LOGI(kTagLvgl, "LVGL fully initialized. Display %lux%lu created.", width, height); + lv_display_set_buffers(g_LvglDisplay, buf1, nullptr, buf_size, + LV_DISPLAY_RENDER_MODE_FULL); + + // Create the background task for the LVGL timer + xTaskCreate(lvgl_tick_task, "LVGL Tick", 4096, nullptr, 5, nullptr); + + // Draw the sample UI + if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE) + { + lv_draw_sample_ui(); + xSemaphoreGive(g_LvglMutex); + } + + ESP_LOGI(kTagLvgl, "LVGL fully initialized. Display %lux%lu created.", width, + height); } diff --git a/Provider/main/main.cpp b/Provider/main/main.cpp index 7a94db7..abd4564 100644 --- a/Provider/main/main.cpp +++ b/Provider/main/main.cpp @@ -16,13 +16,12 @@ #include "soc/gpio_num.h" // Project headers -#include "appstate.hpp" #include "types.hpp" +#include "network.hpp" // Project cpp (Unity Build entry) // clang-format off #include "led_status.cpp" -#include "connect.cpp" #include "http_server.cpp" #include "mdns_service.cpp" #include "udp_logger.cpp" diff --git a/Provider/sdkconfig.defaults b/Provider/sdkconfig.defaults index c8df108..edcaf06 100644 --- a/Provider/sdkconfig.defaults +++ b/Provider/sdkconfig.defaults @@ -27,15 +27,16 @@ CONFIG_ETHERNET_SPI_POLLING0_MS=0 # Enable PSRAM CONFIG_SPIRAM=y -CONFIG_ESP32S3_SPIRAM_SUPPORT=y CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_SPEED_80M=y CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y CONFIG_SPIRAM_RODATA=y # LVGL Configuration -CONFIG_LV_COLOR_DEPTH_8=y +CONFIG_LV_COLOR_DEPTH_16=y CONFIG_LV_USE_SYSMON=n +CONFIG_LV_USE_OBJ_NAME=y +CONFIG_LV_ATTRIBUTE_FAST_MEM_USE_IRAM=y # LVGL Memory Allocator (Use ESP-IDF Heap instead of internal 64kB BSS pool!) CONFIG_LV_USE_BUILTIN_MALLOC=n @@ -51,20 +52,19 @@ CONFIG_LV_BUILD_EXAMPLES=n CONFIG_LV_BUILD_DEMOS=n # Disable unused software drawing color formats (Only L8 and A8 matter for grayscale) -CONFIG_LV_DRAW_SW_SUPPORT_RGB565=n -CONFIG_LV_DRAW_SW_SUPPORT_RGB565_SWAPPED=n +CONFIG_LV_DRAW_SW_SUPPORT_RGB565=y CONFIG_LV_DRAW_SW_SUPPORT_RGB565A8=n CONFIG_LV_DRAW_SW_SUPPORT_RGB888=n CONFIG_LV_DRAW_SW_SUPPORT_XRGB8888=n CONFIG_LV_DRAW_SW_SUPPORT_ARGB8888=n CONFIG_LV_DRAW_SW_SUPPORT_ARGB8888_PREMULTIPLIED=n -CONFIG_LV_DRAW_SW_SUPPORT_L8=y +CONFIG_LV_DRAW_SW_SUPPORT_L8=n CONFIG_LV_DRAW_SW_SUPPORT_AL88=n -CONFIG_LV_DRAW_SW_SUPPORT_A8=y +CONFIG_LV_DRAW_SW_SUPPORT_A8=n CONFIG_LV_DRAW_SW_SUPPORT_I1=n -# Disable complex drawing features to save memory (no shadows, no complex gradients) -CONFIG_LV_DRAW_SW_COMPLEX=n +# Enable complex drawing features (required for lines thicker than 1px, rounded lines, arcs, and gradients) +CONFIG_LV_DRAW_SW_COMPLEX=y # Disable unneeded widgets for a simple static screen generator CONFIG_LV_USE_CHART=n @@ -78,13 +78,13 @@ CONFIG_LV_USE_SPINBOX=n CONFIG_LV_USE_SPINNER=n CONFIG_LV_USE_KEYBOARD=n CONFIG_LV_USE_CALENDAR=n -CONFIG_LV_USE_CHECKBOX=n +CONFIG_LV_USE_CHECKBOX=y CONFIG_LV_USE_DROPDOWN=n CONFIG_LV_USE_IMAGEBUTTON=n CONFIG_LV_USE_ROLLER=n CONFIG_LV_USE_SCALE=n CONFIG_LV_USE_SLIDER=n -CONFIG_LV_USE_SWITCH=n +CONFIG_LV_USE_SWITCH=y CONFIG_LV_USE_TEXTAREA=n CONFIG_LV_USE_TABLE=n @@ -96,3 +96,7 @@ CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0 # Disable data observer patterns (unused in static render flow) CONFIG_LV_USE_OBSERVER=n + +# Enable XML runtime for dynamic screen layouts (LVGL 9.4+) +CONFIG_LV_USE_XML=y +CONFIG_LV_USE_OBSERVER=y diff --git a/Provider/tdd/device_screens.md b/Provider/tdd/device_screens.md index 0c29aad..a2bdaa7 100644 --- a/Provider/tdd/device_screens.md +++ b/Provider/tdd/device_screens.md @@ -53,7 +53,15 @@ The following REST endpoints handle the device lifecycle and image generation: ### Subsystems Config The ESP-IDF project configuration (`sdkconfig.defaults`) must be modified to enable the `CONFIG_LV_USE_XML=y` flag, which compiles the LVGL XML parser component into the firmware image. +### XML Runtime Integration +The user provided documentation for the `LV_USE_XML` runtime feature. We must: +1. Call `lv_xml_register_component_from_data("current_device", dev->xml_layout)` to register the XML payload. +2. Check if the XML string contains ``. If it does, LVGL expects us to instantiate it as a full screen using `lv_obj_t * root = lv_xml_create_screen("current_device");`. +3. If it does not contain ``, it's just a regular component/widget, so we create it *on* the active screen using `lv_obj_t * root = lv_xml_create(scr, "current_device", NULL);`. +4. Fallback to a string label if the XML is empty or parsing fails. + ### Frontend - **DeviceManager.svelte:** A new component accessible from the Sidebar. It fetches the device list on load. - **XML Uploading:** For each device card, a text area allows the user to paste an LVGL XML string. Clicking "Save Layout" updates the device via `POST /api/devices/layout`. +- **Debug Features:** A collapsed section (e.g. `
`) in the UI will contain a button to "Register Debug Device" that triggers a POST to `/api/devices/register` with a random or hardcoded MAC (e.g., `00:11:22:33:44:55`). - **Integration:** The `App.svelte` router will be updated to include the `'devices'` view state alongside Dashboard, Tasks, and Users. diff --git a/components/network/CMakeLists.txt b/components/network/CMakeLists.txt new file mode 100644 index 0000000..1f3ef90 --- /dev/null +++ b/components/network/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "network.cpp" + INCLUDE_DIRS "include" + PRIV_REQUIRES esp_eth esp_wifi esp_netif driver) diff --git a/components/network/Kconfig.projbuild b/components/network/Kconfig.projbuild new file mode 100644 index 0000000..aacd5d5 --- /dev/null +++ b/components/network/Kconfig.projbuild @@ -0,0 +1,73 @@ +menu "CalendarInk Network Configuration" + + config CALENDINK_WIFI_SSID + string "WiFi SSID" + default "" + help + SSID (network name) for the WiFi connection. + + config CALENDINK_WIFI_PASSWORD + string "WiFi Password" + default "" + help + Password for the WiFi connection. + + config CALENDINK_ETH_RETRIES + int "Maximum Ethernet Connection Retries" + default 5 + help + Number of times to retry the Ethernet connection before falling back to WiFi. + + config CALENDINK_WIFI_RETRIES + int "Maximum WiFi Connection Retries" + default 5 + help + Number of times to retry the WiFi connection before failing completely. + + config CALENDINK_BLINK_IP + bool "Blink last IP digit on connect" + default n + help + If enabled, the LED will blink the last digit of the IP address + acquired to assist in debugging. + + config CALENDINK_MDNS_HOSTNAME + string "mDNS Hostname" + default "calendink" + help + The hostname to use for mDNS. The device will be accessible + at .local. (e.g., calendink.local) + + config CALENDINK_UDP_LOG_TARGET_IP + string "UDP Logger Target IP Address" + default "" + help + The IP address to send UDP logs to via port 514. + If left blank, logs will be broadcast to 255.255.255.255. + + choice CALENDINK_WIFI_PS_MODE + prompt "WiFi Power Save Mode" + default CALENDINK_WIFI_PS_NONE + help + Select the WiFi power save mode to balance power consumption and network stability. + + config CALENDINK_WIFI_PS_NONE + bool "None (No power save, highest consumption)" + + config CALENDINK_WIFI_PS_MIN_MODEM + bool "Minimum Modem (Wakes on beacon, balanced)" + + config CALENDINK_WIFI_PS_MAX_MODEM + bool "Maximum Modem (Lowest consumption, may drop connection on strict routers)" + endchoice + + config CALENDINK_ALLOW_LIGHT_SLEEP + bool "Allow Light Sleep (Tickless Idle)" + default n + help + If enabled, the device will heavily use light sleep to reduce power + consumption. Note that this may BREAK the UART console monitor since the + CPU sleeps and halts the UART! Use UDP logging if you need logs + while light sleep is enabled. + +endmenu diff --git a/components/network/idf_component.yml b/components/network/idf_component.yml new file mode 100644 index 0000000..8b1121c --- /dev/null +++ b/components/network/idf_component.yml @@ -0,0 +1,7 @@ +version: "1.0.0" +description: "CalendarInk Shared Network Component" + +dependencies: + idf: + version: '>=4.1.0' + espressif/ethernet_init: ^1.3.0 diff --git a/components/network/include/network.hpp b/components/network/include/network.hpp new file mode 100644 index 0000000..dd95aee --- /dev/null +++ b/components/network/include/network.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "esp_err.h" +#include + +esp_err_t connect_ethernet(bool blockUntilIPAcquired); +void disconnect_ethernet(); +esp_err_t check_ethernet_connection(uint32_t timeoutSeconds); + +esp_err_t connect_wifi(const char *ssid, const char *password, bool blockUntilIPAcquired); +void disconnect_wifi(); +esp_err_t check_wifi_connection(uint32_t timeoutSeconds); diff --git a/Provider/main/connect.cpp b/components/network/network.cpp similarity index 97% rename from Provider/main/connect.cpp rename to components/network/network.cpp index 9f631dc..eab39ab 100644 --- a/Provider/main/connect.cpp +++ b/components/network/network.cpp @@ -17,7 +17,11 @@ #include "freertos/timers.h" // Project includes -#include "types.hpp" +#include "network.hpp" + +#ifndef internal +#define internal static +#endif // Forward declarations #if CONFIG_CALENDINK_BLINK_IP @@ -116,7 +120,7 @@ void teardown_ethernet() esp_netif_deinit(); } -internal esp_err_t connect_ethernet(bool blockUntilIPAcquired) +esp_err_t connect_ethernet(bool blockUntilIPAcquired) { #ifndef NDEBUG assert(!s_ethernet_connected && @@ -185,7 +189,7 @@ void disconnect_ethernet() ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&teardown_ethernet)); } -internal esp_err_t check_ethernet_connection(uint32_t timeoutSeconds) +esp_err_t check_ethernet_connection(uint32_t timeoutSeconds) { // Wait up to 5000ms for the physical link to negotiate if (!s_eth_link_up) @@ -302,7 +306,7 @@ void teardown_wifi() } } -internal esp_err_t connect_wifi(const char *ssid, const char *password, +esp_err_t connect_wifi(const char *ssid, const char *password, bool blockUntilIPAcquired) { #ifndef NDEBUG @@ -379,7 +383,7 @@ void disconnect_wifi() ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&teardown_wifi)); } -internal esp_err_t check_wifi_connection(uint32_t timeoutSeconds) +esp_err_t check_wifi_connection(uint32_t timeoutSeconds) { // Wait up to 10000ms for the physical link to associate with the AP if (!s_wifi_link_up)