├── .github ├── dependabot.yml └── workflows │ ├── pio_build.yml │ └── tg_rel_notify.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.en.md ├── README.md ├── data ├── css │ ├── menu-dark.webp │ ├── menu-light.webp │ ├── menu.jpg │ ├── pure.css.gz │ ├── style.css.gz │ ├── style_dark.css.gz │ ├── style_light.css.gz │ ├── wp_dark.svg.gz │ └── wp_light.svg.gz ├── favicon.ico.gz ├── index.html.gz └── js │ ├── embui.js.gz │ ├── lodash.custom.js.gz │ ├── tz.json.gz │ ├── ui_embui.i18n.json.gz │ ├── ui_embui.json.gz │ └── ui_embui.lang.json.gz ├── doc ├── Interface.txt └── timezone.txt ├── examples ├── 01_generic │ ├── .gitignore │ ├── README.md │ ├── ci_core3.ini │ ├── platformio.ini │ └── src │ │ ├── globals.h │ │ ├── interface.cpp │ │ ├── interface.h │ │ ├── main.cpp │ │ ├── main.h │ │ └── uistrings.h ├── 02_sensors │ ├── .gitignore │ ├── README.md │ ├── ci_core3.ini │ ├── data │ │ ├── css │ │ │ └── disp.css │ │ └── index.html │ ├── display.png │ ├── part_4MiB_c3.csv │ ├── platformio.ini │ ├── post_flashz.py │ └── src │ │ ├── interface.cpp │ │ ├── interface.h │ │ ├── main.cpp │ │ ├── main.h │ │ └── uistrings.h ├── example.json └── ui_datetime.png ├── git_rev_macro.py ├── library.json ├── resources ├── html │ ├── css │ │ ├── bar_dark.css │ │ ├── bar_default.css │ │ ├── bar_light.css │ │ ├── grids-responsive-min.css │ │ ├── menu-dark.webp │ │ ├── menu-light.webp │ │ ├── menu.jpg │ │ ├── pure-min.css │ │ ├── range_dark.css │ │ ├── range_default.css │ │ ├── range_light.css │ │ ├── side-menu_dark.css │ │ ├── side-menu_default.css │ │ ├── side-menu_light.css │ │ ├── style_dark.css │ │ ├── style_default.css │ │ ├── style_light.css │ │ ├── wp_dark.svg.gz │ │ └── wp_light.svg.gz │ ├── favicon.ico │ ├── index.html │ ├── js │ │ ├── embui.js │ │ ├── ui_embui.i18n.json │ │ ├── ui_embui.json │ │ └── ui_embui.lang.json │ ├── jsconfig.json │ └── readme.txt ├── lodash.sh ├── package-lock.json ├── respack.cmd ├── respack.sh └── tzgen.py └── src ├── EmbUI.cpp ├── EmbUI.h ├── basicui.cpp ├── basicui.h ├── embui_constants.h ├── embui_defines.h ├── embui_log.h ├── embui_wifi.cpp ├── embui_wifi.hpp ├── embuifs.cpp ├── embuifs.hpp ├── ftpsrv.cpp ├── ftpsrv.h ├── http.cpp ├── i18n.h ├── mqtt.cpp ├── timeProcessor.cpp ├── timeProcessor.h ├── traits.cpp ├── traits.hpp ├── ts.cpp ├── ts.h ├── ui.cpp └── ui.h /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/pio_build.yml: -------------------------------------------------------------------------------- 1 | # Build examples with Platformio 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | # https://docs.platformio.org/en/latest/integration/ci/github-actions.html 4 | 5 | name: PlatformIO CI 6 | 7 | on: 8 | workflow_dispatch: # Manual start 9 | # push: 10 | # branches: [ main, feat, v2.6, 2.6tst, stage ] 11 | pull_request: 12 | branches: [ main ] 13 | paths: 14 | - '**.ino' 15 | - '**.cpp' 16 | - '**.hpp' 17 | - '**.h' 18 | - '**.c' 19 | - '**.ini' 20 | - '**.yml' 21 | 22 | 23 | jobs: 24 | CI_Build: 25 | runs-on: ubuntu-latest 26 | continue-on-error: false 27 | strategy: 28 | max-parallel: 2 29 | matrix: 30 | example: 31 | - "ci_core3.ini" 32 | #- "ci_core3.ini" 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: PIO Cache 37 | uses: actions/cache@v4 38 | with: 39 | path: | 40 | ~/.cache/pip 41 | ~/.platformio/.cache 42 | key: ${{ runner.os }}-pio 43 | #restore-keys: | 44 | # ${{ runner.os }}-pio 45 | - name: Set up Python 3.x 46 | uses: actions/setup-python@v5 47 | with: 48 | python-version: '3.x' 49 | - name: Install Platformio 50 | run: | 51 | python -m pip install --upgrade pip 52 | pip install --upgrade platformio 53 | #platformio pkg update 54 | - name: PlatformIO Build 55 | env: 56 | PLATFORMIO_CI_SRC: ${{ matrix.example }} 57 | run: | 58 | pwd 59 | ls -la 60 | mv $GITHUB_WORKSPACE/examples ~/ 61 | cd ~/examples/01_generic && platformio run -c ${{ matrix.example }} 62 | cd ~/examples/02_sensors && platformio run -c ${{ matrix.example }} 63 | #platformio run -c ${{ matrix.example }}/platformio.ini 64 | #pio ci -c ${{ matrix.example }}/platformio.ini 65 | -------------------------------------------------------------------------------- /.github/workflows/tg_rel_notify.yml: -------------------------------------------------------------------------------- 1 | # Telegram notification sender 2 | 3 | # https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a 4 | # https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id 5 | # https://stackoverflow.com/questions/75283870/how-to-send-telegram-message-to-a-topic-thread 6 | 7 | # GH release API https://docs.github.com/en/rest/releases/releases 8 | 9 | name: Telegram - Published Release notification 10 | on: 11 | #workflow_dispatch: # Manual start 12 | release: 13 | types: [published] 14 | 15 | jobs: 16 | build: 17 | name: Send Message 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: send Telegram Message about release 21 | uses: Ofceab-Studio/telegram-action@1.0.1 22 | with: 23 | #to: ${{ secrets.TG_BOT_MYCHAT }} 24 | to: ${{ secrets.LAMPDEVS_GID }} 25 | thread_id: ${{ secrets.LAMPDEVS_ANNOUNCE_TOPIC_ID }} 26 | token: ${{ secrets.LAMPDEVS_BOT_TOKEN }} 27 | disable_web_page_preview: true 28 | disable_notification: true 29 | format: html # Telegram Bot API currently supports only , , , and
 tags, for HTML parse mode
30 |           message: |
31 |             ❗Новый выпуск:❗ 
32 | 
33 |             ${{ github.repository }} ${{ github.event.release.tag_name }}
34 |             
35 |             ${{ github.event.release.body }}
36 |             


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .pio
2 | .vscode
3 | draft
4 | etags.txt
5 | /platformio.ini


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2020 DmytroKorniienko
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
 1 | __[RUSSIAN](/README.md) | [EXAMPLES](/examples) | [CHANGELOG](/CHANGELOG.md)__ | [![PlatformIO CI](https://github.com/vortigont/EmbUI/actions/workflows/pio_build.yml/badge.svg)](https://github.com/vortigont/EmbUI/actions/workflows/pio_build.yml)
 2 | 
 3 | # EmbUI
 4 | Embedded WebUI Interface framework
 5 | 
 6 | 
 7 | A framework that helps to create WebUI and dynamic control elements for Arduino projects. It offers _Interface-as-a-code_ approach to segregate web frontend and MCU backend firmware development.
 8 | 
 9 | ## Supported platforms
10 |  - ESP32, ESP32-S2, ESP32-C3, ESP32-S3 Arduino Core
11 |  - ESP8266 Arduino Core [branch v2.6](https://github.com/vortigont/EmbUI/tree/v2.6) only
12 | 
13 | ## Features
14 |  - asynchronous data exchange with frontend via WebSocket
15 |  - creating html elements using DOM and [{{ mustache }}](https://mustache.github.io/) templater
16 |  - packet based data exchange with WebUI, allows transfering large objects containing multiple elements split into chunks and using less MCU memory
17 |  - dynamic UI elements update without page refresh
18 |  - allows fetching data/ui objects via 3rd party AJAX calls
19 |  - supports multiple parallel connections, WebUI updates simultaneously on all devices
20 |  - embedded WiFi manager, AP-failover on connection lost, autoreconnection
21 |  - full support for TimeZones, daylight saving autoswitchover
22 |  - embedded NTP sync, NTPoDHCP support, custom NTP servers
23 |  - OTA firmware updates via WebUI/CLI tools
24 |  - zlib-compressed FOTA using [esp32-flashz](https://github.com/vortigont/esp32-flashz) lib
25 |  - [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS)/[ZeroConf](https://en.wikipedia.org/wiki/Zero-configuration_networking) publisher, discovery
26 |  - device autodiscovery via:
27 |     - WiFi Captive Portal detection - upon connecting to esp's WiFi AP a device/browser will show a pop-up advising to open an EmbUI's setup page
28 |     - [Service Browser](https://play.google.com/store/apps/details?id=com.druk.servicebrowser) Android
29 |     - [SSDP](https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol) for Windows
30 |     - [Bonjour](https://en.wikipedia.org/wiki/Bonjour_(software)) iOS/MacOS
31 |  - self-hosted - all resources resides on LittleFS locally, no calls for CDN resources
32 | 
33 | ## Projects using EmbUI
34 |  - [FireLamp / Informer](https://github.com/vortigont/FireLamp_JeeUI) - DIY HUB75/ws2812 led panel/matrix informer, clock, cristmas light project
35 |  - [ESPEM](https://github.com/vortigont/espem) - ESP32 energy meter for Peacfair PZEM-004
36 |  - [InfoClock](https://github.com/vortigont/infoclock) - Clock/weather ticker based on Max72xx modules
37 | 
38 | 
39 | ## WebUI samples based on EmbUI framework
40 | 
41 | embui UI
42 | embui options
43 | embui dtime
44 | InfoClock
45 | 
46 | 
47 | ## Usage
48 | To run EmbUI it is required to upload an FS image with resources to LittleFS partition.
49 | Prebuild resources are available [as archive](https://github.com/vortigont/EmbUI/raw/main/resources/data.zip).
50 | Unpack it into *data* folder to the root of [Platformio](https://platformio.org/) project
51 | 
52 | ## Documentation/API description
53 | to be continued...
54 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | __[ENGLISH](/README.en.md) | [EXAMPLES](/examples) | [CHANGELOG](/CHANGELOG.md)__ | [![PlatformIO CI](https://github.com/vortigont/EmbUI/actions/workflows/pio_build.yml/badge.svg)](https://github.com/vortigont/EmbUI/actions/workflows/pio_build.yml) | [![PlatformIO Registry](https://badges.registry.platformio.org/packages/vortigont/library/EmbUI.svg)](https://registry.platformio.org/libraries/vortigont/EmbUI)
 2 | 
 3 | # EmbUI
 4 | Embedded WebUI Interface framework
 5 | 
 6 | 
 7 | Фреймворк для построения web-интерфейса и динамических элементов управления для проектов Arduino
 8 | 
 9 | ## Поддерживаемые платформы
10 |  - ESP32, ESP32-S2, ESP32-C3, ESP32-S3 Arduino Core
11 |  - ESP8266 Arduino Core [branch v2.6](https://github.com/vortigont/EmbUI/tree/v2.6) only (DEPRECATED)
12 | 
13 | ## Возможности
14 |  - асинхронный интерфейс обмена данными с браузером через WebSocket
15 |  - построение элементов DOM на основе шаблонизатора [{{ mustache }}](https://mustache.github.io/)
16 |  - пакетный интерфейс обмена с WebUI, возможность передавать большие объекты из множества элементов по частям
17 |  - динамическое обновление отдельных элементов интерфейса безе перерисовки всего документа
18 |  - возможность подгружать данные/элементы интерфейса через AJAX
19 |  - поддержка нескольких параллельных WebUI подключений с обратной связью, интерфейс обновляется одновременно на всех устройствах
20 |  - встроенный WiFi менеджер, автопереключение в режим AP при потере клиентского соединения
21 |  - полная поддержка всех существующих часовых зон, автоматический переход на летнее/зимнее время, корректная калькуляция дат/временных интервалов
22 |  - встроенная синхронизация часов через интернет, поддержка NTPoDHCP, пользовательский NTP
23 |  - OTA, обновление прошивки/образа ФС через браузер/cli
24 |  - поддрежка обновления из сжатых образов (zlib-compressed FOTA) через библиотеку [esp32-flashz](https://github.com/vortigont/esp32-flashz)
25 |  - автопубликация контроллера в локальной сети через [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS)/[ZeroConf](https://en.wikipedia.org/wiki/Zero-configuration_networking)
26 |  - возможность обнаружения устройства:
27 |     - WiFi Captive Portal detection - при подключении к WiFi AP контроллера устройтсво/браузер покажет всплюывающее окно с предложением открыть страницу настройки EmbUI
28 |     - [mDNS Discovery](https://play.google.com/store/apps/details?id=com.mdns_discovery.app) Android
29 |     - [Bonjour](https://en.wikipedia.org/wiki/Bonjour_(software)) iOS/MacOS
30 |  - self-hosted - нет зависимостей от внешних ресурсов/CDN/Cloud сервисов
31 | 
32 | ## Проекты на EmbUI
33 |  - [FireLamp_JeeUI](https://github.com/vortigont/FireLamp_JeeUI/) - HUB75 panel informer / огненная лампа-гирлянда на светодиодной матрице ws2812
34 |  - [ESPEM](https://github.com/vortigont/espem) - энергометр на основе измерителя PZEM-004
35 |  - [InfoClock](https://github.com/vortigont/infoclock) - Часы-информер на матричных модулях Max72xx
36 |  - [EmbUI](https://github.com/DmytroKorniienko/) - исходный проект данного форка
37 | 
38 | 
39 | 
40 | ## Примеры построения интерфейсов
41 | 
42 | embui UI
43 | embui options
44 | embui dtime
45 | InfoClock
46 | 
47 | 
48 | ## Использование
49 | Для работы WebUI необходимо залить в контроллер образ фаловой системы LittleFS с web-ресурсами.
50 | Подготовленные ресурсы для создания образа можно развернуть [из архива](https://github.com/vortigont/EmbUI/raw/main/resources/data.zip).
51 | В [Platformio](https://platformio.org/) это, обычно, каталог *data* в корне проекта.
52 | 
53 | 


--------------------------------------------------------------------------------
/data/css/menu-dark.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/menu-dark.webp


--------------------------------------------------------------------------------
/data/css/menu-light.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/menu-light.webp


--------------------------------------------------------------------------------
/data/css/menu.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/menu.jpg


--------------------------------------------------------------------------------
/data/css/pure.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/pure.css.gz


--------------------------------------------------------------------------------
/data/css/style.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/style.css.gz


--------------------------------------------------------------------------------
/data/css/style_dark.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/style_dark.css.gz


--------------------------------------------------------------------------------
/data/css/style_light.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/style_light.css.gz


--------------------------------------------------------------------------------
/data/css/wp_dark.svg.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/wp_dark.svg.gz


--------------------------------------------------------------------------------
/data/css/wp_light.svg.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/css/wp_light.svg.gz


--------------------------------------------------------------------------------
/data/favicon.ico.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/favicon.ico.gz


--------------------------------------------------------------------------------
/data/index.html.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/index.html.gz


--------------------------------------------------------------------------------
/data/js/embui.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/js/embui.js.gz


--------------------------------------------------------------------------------
/data/js/lodash.custom.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/js/lodash.custom.js.gz


--------------------------------------------------------------------------------
/data/js/tz.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/js/tz.json.gz


--------------------------------------------------------------------------------
/data/js/ui_embui.i18n.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/js/ui_embui.i18n.json.gz


--------------------------------------------------------------------------------
/data/js/ui_embui.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/js/ui_embui.json.gz


--------------------------------------------------------------------------------
/data/js/ui_embui.lang.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/data/js/ui_embui.lang.json.gz


--------------------------------------------------------------------------------
/doc/Interface.txt:
--------------------------------------------------------------------------------
  1 | https://community.alexgyver.ru/threads/wifi-lampa-budilnik-proshivka-firelamp_jeeui-gpl.2739/post-43370
  2 | 
  3 | Формирование пакета
  4 | 
  5 | Браузер (далее клиент) формирует интерфейс, на основании JSON пакетов, формируемых лампой (далее сервер).
  6 | 
  7 | Для начала формирования интерфейса, нужно создать экземпляр класса Interface, передав в него указатель на транспорт и
  8 | память выделяемую на формирование фрейма.
  9 | 
 10 | Транспортом могут быть:
 11 | AsyncWebSocket - отправка всем подключенным клиентам
 12 | AsyncWebSocketClient - отправка конкретному клиенту
 13 | AsyncWebServerRequest - http. не рекомендуется, так как будет передан набор фреймов а не валидный JSON
 14 | 
 15 | Если при формировании пакета, память выделенная на фрейм будет исчерпана, произойдет сериализация текущего фрейма и его отправка.
 16 | После чего, фрейм будет очищен и формирование пакета продолжится. Чем меньше памяти выделяется на фрейм, тем за большее количество фреймов
 17 | будет отправлен пакет. Это влияет на скорость загрузки. Таким образом мы балансируем между потребляемой памятью и скоростью загрузки.
 18 | 
 19 | Пакеты могут быть двух видов:
 20 | json_frame_interface - формирование интерфейса
 21 | json_frame_value - формирование данных
 22 | 
 23 | Пример.
 24 | extern EmbUI embui;
 25 | AsyncWebSocket ws("/ws");
 26 | Interface *interf = new Interface(&embui, client);
 27 | interf->json_frame_interface("Огненная лампа");
 28 | ...
 29 | interf->json_frame_flush();
 30 | delete interf;
 31 | 
 32 | 
 33 | Формирование интерфейса
 34 | 
 35 | Интерфейс состоит из секций. Корневая секция создается json_frame_interface, а закрывается и отправляется json_frame_flush.
 36 | Так же существуют вспомогательные секции. Создаются json_section_begin, закрывается json_section_end.
 37 | Виды вспомогательных секций:
 38 | json_section_menu - боковое меню
 39 | json_section_content - содержимое не встраивается в страницу, а замещает элементы с существующими ID
 40 | json_section_main - страница (приводит к замене текущей страницы)
 41 | json_section_hidden - встроенный скрытый блок
 42 | json_section_line - горизонтальная ориентация элементов
 43 | 
 44 | Все секции (кроме json_section_menu, json_section_content) являются хелперами к json_section_begin и могут быть
 45 | заменены ее вызовом с нужной комбинацией параметров.
 46 | 
 47 | Параметры json_section_begin:
 48 | name - id секции, используется для отправки данных.
 49 | label - если указан бедет выведен заголовок
 50 | main - секция является страницей
 51 | hidden - секция является скрываемым блоком
 52 | line - горизонтальная ориентация элементов
 53 | 
 54 | Мы можем создать скрытую секцию, аналог 
: 55 | 56 | interf->json_frame_interface("Огненная лампа"); 57 | interf->json_section_begin("mysect"); 58 | interf->button_submit("mysect", "Отправить"); 59 | interf->json_section_end(); 60 | interf->json_frame_flush(); 61 | 62 | ВНИМАНИЕ! Так же секцию создает контрол select. Он должен обязательно заканчиваться json_section_end. 63 | 64 | interf->select(F("evList"), String(event.event), F("Тип события"), false); 65 | interf->option(String(EVENT_TYPE::ON), F("Включить лампу")); 66 | ... 67 | interf->option(String(EVENT_TYPE::PIN_STATE), F("Состояние пина")); 68 | interf->json_section_end(); 69 | 70 | В секции помещаются контролы, все контролы могут брать данные как из глобального конфига, так и получать их на прямую. 71 | void hidden(const String &id); 72 | void hidden(const String &id, const String &value); 73 | 74 | Контролы могут отправлять данные при изменении, или при сабмите секции. За это отвечает флаг directly. 75 | void checkbox(const String &id, const String &label, bool directly = false); 76 | void checkbox(const String &id, const String &value, const String &label, bool directly = false); 77 | 78 | Описание некоторых специфических контролов. 79 | hidden - скрытый контрол, не отображаемый в интерфейсе. Нужен для проброса состояния в некоторых сложных формах. 80 | Пример использования - управление конфигурациями лампы. 81 | 82 | constant - тоже, что и hidden но выводиться лейбл, как не активный но видимый пользователю элемент. 83 | button - отправка параметра - id кнопки. используется для обработки действия без доп. параметров (например загрузка страницы) 84 | button_value - отправка id кнопки + значение, напр. один экшен может обрабатывать несколько кнопок по разному, либо дейтвие кнопки можно переназначать 85 | button_submit - отправка данных из секции. 86 | button_submit_value - тоже, но дополнительно передается значение. если действий несколько (удалить, загрузить) 87 | spacer - разделитель, может быть как линией, так и заголовком. 88 | 89 | 90 | Формирование данных. 91 | 92 | Сейчас данные клиентам приходят в двух случаях: 93 | 1. при приеме пакета post от одного клиента, данные рассылаются остальным клиентам. 94 | 2. периодически вызывается ф-я send_pub, которая транслирует клиентам параметры 95 | 96 | interf->json_frame_value(); 97 | interf->value(F("pTime"), myLamp.timeProcessor.getFormattedShortTime(), true); 98 | interf->value(F("pMem"), String(ESP.getFreeHeap()), true); 99 | interf->json_frame_flush(); 100 | 101 | 102 | Обработка данных от клиента. 103 | 104 | Клиент отправляет данные в случаях: 105 | 1. Нажатие на раздел меню 106 | 2. Нажатие на кнопку 107 | 3. Сабмит секции 108 | 4. Изменение параметра с флагом directly 109 | 110 | Никакие данные автоматически не сохраняются!!! 111 | ф-я post сканирует данные и определяет вероятный обработчик. После чего передает в нее JSON объект со всеми данными. 112 | 113 | Регистрация обработчика параметра: 114 | 115 | Обработчик представляет из себя ф-ю, которая принимает JSON объект с данными, обрабатывает их и формирует результат в виде 116 | изменения интерфейса. Обработчик можно повесить на имя любого параметра. Но при обработке секции, разумнее повесить его на имя секции. 117 | Так же обработчик можно установить на группу параметров "paramname*" 118 | 119 | Обработка directly параметров одной ф-ей 120 | embui.section_handle_add(F("bright"), set_effects_param); 121 | embui.section_handle_add(F("speed"), set_effects_param); 122 | embui.section_handle_add(F("scale"), set_effects_param); 123 | 124 | Обрабока отправки данных из секции 125 | embui.section_handle_add(F("set_wifi"), set_settings_wifi); 126 | 127 | Обработка кнопки - загрузка секции 128 | embui.section_handle_add(F("show_wifi"), show_settings_wifi); 129 | 130 | Обработка флага переключателя 131 | embui.section_handle_add(F("Mic"), set_micflag); 132 | 133 | Обработка группы 134 | embui.section_handle_add(F("evconf*"), show_event_conf); 135 | 136 | Внимание!!! обработчик может быть вызван без передачи указателя на интерфейс или данные. 137 | 138 | Обрабатываем данные, сохраняем в конфиг и НЕ формирует интерфейс 139 | void set_eventflag(Interface *interf, JsonObject *data){ 140 | if (!data) return; 141 | SETPARAM(F("Events"), myLamp.setIsEventsHandled((*data)[F("Events")] == F("true"))); 142 | } 143 | 144 | Формируем страницу интерфейса и НЕ обрабатываем данные 145 | void show_settings_event(Interface *interf, JsonObject *data){ 146 | if (!interf) return; 147 | interf->json_frame_interface(); 148 | block_settings_event(interf, data); 149 | interf->json_frame_flush(); 150 | } 151 | 152 | И обрабатываем данные и формируем интерфейс 153 | void set_effects_config_list(Interface *interf, JsonObject *data){ 154 | if (!interf || !data) return; 155 | EFF_ENUM num = (EFF_ENUM)(*data)[F("effListConf")]; 156 | confEff = myLamp.effects.getEffect(num); 157 | show_effects_config_param(interf, data); 158 | } 159 | 160 | 161 | Вызов обработчиков, как реакцию на внешние изменения. Нажатие кнопки, события, http, итп. 162 | 163 | Так как существует множество мест, где состояние лампы меняется, я принял решение объединить обработчики. 164 | Реализация в remote_action. Для каждого действия производиться симуляция передачи параметра в обработчик. 165 | 166 | 167 | Обмен данными с глобальным конфигом. 168 | Раньше ВСЕ параметры обязательно отображались в глобальный конфиг. Сейчас это не так. 169 | В глобальный конфиг имеет смысл транслировать только те состояния, которые должны обязательно сохраниться после 170 | перезагрузки лампы. 171 | 172 | Для предотвращения случайной записи в конфиг, те параметры что небыли зарегистрированы в create_parameters будут проигнорированы. 173 | 174 | Для синхронизации состояния лампы с параметрами после перезагрузки, используется sync_parameters. 175 | Который так же симулирует вызов обработчика параметров. Таким образом мы избегаем множественного дублирования обработчиков в разных реализациях. -------------------------------------------------------------------------------- /doc/timezone.txt: -------------------------------------------------------------------------------- 1 | https://community.alexgyver.ru/threads/wifi-lampa-budilnik-proshivka-firelamp_jeeui-gpl.2739/post-43780 2 | 3 | Реализация времени в есп-коре по своей сути совместима с libc'шными tzset, tzname, timezone, daylight описанными в TZSET(3). 4 | Постараюсь объяснить на пальцах раз уж я в это влез... 5 | сперва определимся с терминами: 6 | 7 | время RTC, это то что "тикает" в системе в секундах от 1970-го года 8 | смещение часового пояса 9 | текущее смещение dst от часового пояса (сезонное время) 10 | правила учета калькуляции даты/времени для 3) 11 | 12 | И есть 2 функции - localtime() и gmtime(). В начальных условиях, все по нолям и обе функции выдают одно и тоже. 13 | 14 | Возможность корректно задать все 4 пункта позволяет только установка дефайна с временной зоной во время компиляции, т.к. 3 и 4 задается довольно хитрой строкой в переменную среды. Далее нужна просто периодическая связь с ntp для поддержания точного времени, при этом калькуляция дат и localtime() всегда отдает верные значения с учетом сезонных переходов, високосных лет и пр, пр. 15 | 16 | То что обычно понимают под задать "время/пояс", это только 1) + 2), без базы/апи и возможности установить 3) и 4) одновременно, они оба теряют смысл и реализуются как увеличение/уменьшение 2) полуручным способом. 17 | 18 | Именно так выставляется время в случае если для автоопределения использовался http-сервис. С его помощью можно лишь выяснить 2) и 3), сложить вместе и задать как 2) в систему. Т.к. при этом не используется полноценный апи для установки 3) и 4) то система "переключается" в режим "я в зоне GMT_Etc, и локальное смещение у меня 2)", сезонных правил нет, больше ничего не знаю, 2 раза в год подводите меня если надо. Тоже самое получится если время "задать" из браузера или из строки в формате 8601. 19 | 20 | теперь выводы: 21 | 1) если при компиляции была задана зона и полагаем что лампа будет хотя бы иногда подключена к инету, делать больше ничего не надо. Любое вмешательство "из браузера", выставление постфактум зоны из конфига или еще откуда сделает все только хуже. Поэтому код надо избавить от всех потенциальных мест где по дефолту во время включения ламы, тыкания кнопок и т.п. в лампу может уйти установка времени/смещения, два таких места я уже убрал. Главное не вернуть из благих побуждений :) 22 | 23 | 2) если зону при компиляции не задали, то мы уже потеряли 3) и 4) Можно было бы отказаться и от 2), но т.к. для синхронизации используется ntp, нам нужно текущее смещение от UTC. Берем его либо из http сервиса, либо можно задавать вызовом метода из либы или из класса времени. 24 | 25 | 3) если лампа включилась в режиме АП, время начнется с 1970 года. 26 | Можно задать время из строки в браузере (метод класса уже есть, но пока не реализовано в вебюи). Задавать или не задавать смещение часового пояса абсолютно не важно, т.к. в любом случае задается localtime. Можно задать просто текущее время (с 0-м смещением) или можно сначала задать смещение, а потом время - результат будет одинаковый. 27 | Ну и мы, конечно помним, что если зона была "прошита", то как только мы влезем своими руками во время, мы потеряем 3) и 4). 28 | 29 | Теперь если лампа попадет в сеть: 30 | а) зона не была прошита: она сама выяснит текущее смещение 2) от UTC по http и подтянет точное время по ntp 31 | б) зона была прошита, лампа подтянет время по ntp, если до этого вы не задали смещение 2), то время "уплывет" 32 | 33 | в принципе б) можно сделать так что она и прошитую "зону" восстановит, я просто не рассматривал такой странный сценарий применения 34 | 35 | И еще о "идеальном" варианте. Есть возможность задавать те же настройки что делает "прошивка" зоны, тогда можно будет выкинуть вообще http-код из либы и всегда задавать полные настройки и время. Но для этого нужно будет написать скрипты для браузера, которые смогут брать базу зон, выбирать нужную строку с настройками локали и дергать соотвествующий метод в классе. Я заложил фичу, но пока не тестировал. 36 | -------------------------------------------------------------------------------- /examples/01_generic/.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /examples/01_generic/README.md: -------------------------------------------------------------------------------- 1 | # EmbUI Example 2 | 3 | ### Generic template 4 | 5 | Шаблон проекта реализующий базовую функциональность фреймворка 6 | 7 | - WiFi-manager 8 | - работа в режиме WiFi-Client/WiFi-AP 9 | - автопереключение режимов при потере соединения с точкой доступа 10 | - настройки SSID, пароля доступа 11 | - Дата/время 12 | - автоматическая синхронизация по ntp 13 | - установка часового пояса/правил перехода сезонного времени 14 | - установка текущей даты/времени вручную 15 | - Локализация интерфейса 16 | - пример построения мультиязычного web-интерфейса 17 | - переключение языка интерфейса в настройках WebUI на лету 18 | - Базовые настройки MQTT 19 | - OTA обновление прошивки и образа файловой системы 20 | 21 | Установка: 22 | 23 | - в папку ./data развернуть файлы фреймворка из архива /resources/data.zip 24 | - собрать проект в platformio 25 | - залить FS в контроллер `pio run -t uploadfs` 26 | - залить прошивку в контроллер `pio run -t upload` 27 | 28 | To upload LitlleFS image for ESP32 (until core v2 is out) it is required to use an uploader binary *mklittlefs*. Pls, download version for your OS from [here] 29 | (https://github.com/earlephilhower/mklittlefs/releases) and put the binary to the root dir of the project. 30 | 31 | -------------------------------------------------------------------------------- /examples/01_generic/ci_core3.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | ;core_dir = .pioarduino3 3 | default_envs = esp32 4 | 5 | [env] 6 | framework = arduino 7 | ; Tasmota's platform, based on Arduino Core 3.1.0.241030 IDF 5.3.1+ 8 | platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.11.30/platform-espressif32.zip 9 | board_build.filesystem = littlefs 10 | lib_deps = 11 | symlink:///home/runner/work/EmbUI/EmbUI 12 | ;build_unflags = 13 | ; -std=gnu++11 14 | ;build_flags = 15 | -DEMBUI_DEBUG_LEVEL=3 16 | ; -std=gnu++17 17 | monitor_speed = 115200 18 | monitor_filters = esp32_exception_decoder 19 | ;upload_speed = 460800 20 | 21 | [env:esp32] 22 | board = wemos_d1_mini32 23 | build_flags = 24 | ${env.build_flags} 25 | -DFZ_WITH_ASYNCSRV 26 | -DNO_GLOBAL_UPDATE 27 | ;lib_deps = 28 | ; ${env.lib_deps} 29 | 30 | ; build with yubox mod AsyncServer 31 | ;[env:esp32yubox] 32 | ;extends = env:esp32 33 | ;lib_deps = 34 | ; ${libs.yubox} 35 | ;lib_ignore = 36 | ; ${env.lib_ignore} 37 | ; AsyncTCP 38 | ;build_flags = 39 | ; ${env:esp32} 40 | ; -DYUBOXMOD 41 | -------------------------------------------------------------------------------- /examples/01_generic/platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | default_envs = esp32 3 | extra_configs = 4 | user_*.ini 5 | 6 | [env] 7 | framework = arduino 8 | board_build.filesystem = littlefs 9 | lib_deps = 10 | vortigont/EmbUI 11 | build_unflags = 12 | -std=gnu++11 13 | build_flags = 14 | -std=gnu++17 15 | -DEMBUI_DEBUG 16 | monitor_speed = 115200 17 | monitor_filters = esp32_exception_decoder 18 | ;upload_speed = 460800 19 | 20 | [env:esp32] 21 | platform = espressif32 22 | board = wemos_d1_mini32 23 | build_flags = 24 | ${env.build_flags} 25 | -DFZ_WITH_ASYNCSRV 26 | -DNO_GLOBAL_UPDATE 27 | ;lib_deps = 28 | ; ${env.lib_deps} 29 | 30 | ; build with yubox mod AsyncServer 31 | ;[env:esp32yubox] 32 | ;extends = env:esp32 33 | ;lib_deps = 34 | ; ${libs.yubox} 35 | ;lib_ignore = 36 | ; ${env.lib_ignore} 37 | ; AsyncTCP 38 | ;build_flags = 39 | ; ${env:esp32} 40 | ; -DYUBOXMOD 41 | -------------------------------------------------------------------------------- /examples/01_generic/src/globals.h: -------------------------------------------------------------------------------- 1 | // This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 2 | // then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 3 | // also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 4 | // and others people 5 | 6 | #pragma once 7 | 8 | // Global macro's and framework libs 9 | #include "Arduino.h" 10 | 11 | -------------------------------------------------------------------------------- /examples/01_generic/src/interface.cpp: -------------------------------------------------------------------------------- 1 | //#include "main.h" 2 | 3 | #include "EmbUI.h" 4 | #include "interface.h" 5 | 6 | #include "uistrings.h" // non-localized text-strings 7 | 8 | /** 9 | * можно нарисовать свой собственный интефейс/обработчики с нуля, либо 10 | * подключить статический класс с готовыми формами для базовых системных натсроек и дополнить интерфейс. 11 | * необходимо помнить что существуют системные переменные конфигурации с зарезервированными именами. 12 | * Список имен системных переменных можно найти в файле "constants.h" 13 | */ 14 | #include "basicui.h" 15 | 16 | /** 17 | * Headline section 18 | * this is an overriden weak method that builds our WebUI interface from the top 19 | * == 20 | * Головная секция, 21 | * переопределенный метод фреймфорка, который начинает строить корень нашего Web-интерфейса 22 | * 23 | */ 24 | void section_main_frame(Interface *interf, JsonObjectConst data, const char* action){ 25 | if (!interf) return; 26 | 27 | interf->json_frame_interface(); // open interface frame 28 | 29 | interf->json_section_manifest("EmbUI Example", embui.macid(), 0, "v1.0"); // app name/device id/jsapi/version manifest 30 | interf->json_section_end(); // manifest section MUST be closed! 31 | 32 | block_menu(interf, data, NULL); // Строим UI блок с меню выбора других секций 33 | interf->json_frame_flush(); // close frame 34 | 35 | if(WiFi.getMode() & WIFI_MODE_STA){ // if WiFI is no connected to external AP, than show page with WiFi setup 36 | block_demopage(interf, data, NULL); // Строим блок с demo переключателями 37 | } else { 38 | LOG(println, "UI: Opening network setup page"); 39 | basicui::page_settings_netw(interf, {}); 40 | } 41 | 42 | }; 43 | 44 | 45 | /** 46 | * This code builds UI section with menu block on the left 47 | * 48 | */ 49 | void block_menu(Interface *interf, JsonObjectConst data, const char* action){ 50 | if (!interf) return; 51 | // создаем меню 52 | interf->json_section_menu(); 53 | 54 | /** 55 | * пункт меню - "демо" 56 | */ 57 | interf->option(T_DEMO, "UI Demo"); 58 | 59 | /** 60 | * добавляем в меню пункт - настройки, 61 | * это автоматически даст доступ ко всем связанным секциям с интерфейсом для системных настроек 62 | * 63 | */ 64 | basicui::menuitem_settings(interf); // пункт меню "настройки" 65 | interf->json_section_end(); 66 | } 67 | 68 | /** 69 | * Demo controls 70 | * 71 | */ 72 | void block_demopage(Interface *interf, JsonObjectConst data, const char* action){ 73 | // Headline 74 | // параметр FPSTR(T_SET_DEMO) определяет зарегистрированный обработчик данных для секции 75 | interf->json_section_main(T_SET_DEMO, "Some demo controls"); 76 | interf->comment("Комментарий: набор контролов для демонстрации"); // комментарий-описание секции 77 | 78 | // переключатель, связанный с переменной конфигурации V_LED - Изменяется синхронно 79 | interf->checkbox(V_LED, embui.getConfig()[V_LED],"Onboard LED", true); 80 | 81 | interf->text(V_VAR1, embui.getConfig()[V_VAR1].as(), "text field label"); // create text field with value from the system config 82 | interf->text(V_VAR2, "some default val", "another text label"); // текстовое поле со значением "по-умолчанию" 83 | 84 | /* кнопка отправки данных секции на обработку 85 | * первый параметр FPSTR(T_DEMO) определяет алиас акшена обработчика данных формы 86 | * обработчк должен быть зарегистрирован через embui.section_handle_add() 87 | */ 88 | interf->button(button_t::submit, T_SET_DEMO, T_DICT[lang][TD::D_Send], P_GRAY); // button color 89 | interf->json_section_end(); // close json_section_main 90 | interf->json_frame_flush(); 91 | } 92 | 93 | /** 94 | * @brief action handler for demo form data 95 | * 96 | */ 97 | void action_demopage(Interface *interf, JsonObjectConst data, const char* action){ 98 | if (!data) return; 99 | 100 | LOG(println, "processing section demo"); 101 | 102 | // сохраняем значение 1-й переменной в конфиг фреймворка 103 | embui.getConfig()[V_VAR1] = data[V_VAR1]; 104 | embui.autosave(); 105 | 106 | // выводим значение 1-й переменной в serial 107 | const char *text = data[V_VAR1]; 108 | Serial.printf("Varialble_1 value:%s\n", text ); 109 | 110 | // берем указатель на 2-ю переменную 111 | text = data[V_VAR2]; 112 | 113 | Serial.printf("Varialble_2 value:%s\n", text); 114 | } 115 | 116 | /** 117 | * @brief interactive handler for LED switchbox 118 | * 119 | */ 120 | void action_blink(Interface *interf, JsonObjectConst data, const char* action){ 121 | bool state = data[V_LED]; 122 | embui.getConfig()[V_LED] = state; // save new LED state to the config 123 | 124 | // set LED state to the new checkbox state 125 | digitalWrite(LED_BUILTIN, !state ); // write inversed signal for build-in LED 126 | Serial.printf("LED: %u\n", state); 127 | } 128 | 129 | /** 130 | * функция регистрации переменных и активностей 131 | * 132 | */ 133 | void create_parameters(){ 134 | LOG(println, F("UI: Creating application vars")); 135 | 136 | // регистрируем обработчики активностей 137 | embui.action.set_mainpage_cb(section_main_frame); // заглавная страница веб-интерфейса 138 | 139 | /** 140 | * добавляем свои обрабочки на вывод UI-секций 141 | * и действий над данными 142 | */ 143 | embui.action.add(T_DEMO, block_demopage); // generate "Demo" UI section 144 | 145 | // обработчики 146 | embui.action.add(T_SET_DEMO, action_demopage); // обработка данных из секции "Demo" 147 | 148 | embui.action.add(V_LED, action_blink); // обработка рычажка светодиода 149 | }; 150 | -------------------------------------------------------------------------------- /examples/01_generic/src/interface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void create_parameters(); 4 | void block_menu(Interface *interf, JsonObjectConst data, const char* action); 5 | void block_demopage(Interface *interf, JsonObjectConst data, const char* action); 6 | void action_demopage(Interface *interf, JsonObjectConst data, const char* action); 7 | void action_blink(Interface *interf, JsonObjectConst data, const char* action); 8 | -------------------------------------------------------------------------------- /examples/01_generic/src/main.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Main headers 3 | #include "main.h" 4 | #include "EmbUI.h" 5 | #include "uistrings.h" // non-localized text-strings 6 | #include "interface.h" 7 | 8 | /** 9 | * построение интерфейса осуществляется в файлах 'interface.*' 10 | * 11 | */ 12 | 13 | // MAIN Setup 14 | void setup() { 15 | Serial.begin(BAUD_RATE); 16 | Serial.println("Starting test..."); 17 | 18 | pinMode(LED_BUILTIN, OUTPUT); // we are goning to blink this LED 19 | 20 | // Start EmbUI framework 21 | embui.begin(); 22 | 23 | // register our actions 24 | create_parameters(); 25 | 26 | LOG(println, "restore LED state from configuration param"); 27 | digitalWrite( LED_BUILTIN, !embui.getConfig()[V_LED] ); 28 | } 29 | 30 | 31 | // MAIN loop 32 | void loop() { 33 | embui.handle(); 34 | } -------------------------------------------------------------------------------- /examples/01_generic/src/main.h: -------------------------------------------------------------------------------- 1 | 2 | #include "globals.h" 3 | 4 | #define BAUD_RATE 115200 // serial debug port baud rate 5 | -------------------------------------------------------------------------------- /examples/01_generic/src/uistrings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Set of flash-strings that might be reused multiple times within the code 4 | 5 | // General 6 | static const char T_HEADLINE[] PROGMEM = "EmbUI Demo"; // имя проекта 7 | 8 | 9 | // Our variable names 10 | static const char V_LED[] PROGMEM = "vLED"; 11 | static const char V_VAR1[] PROGMEM = "v1"; 12 | static const char V_VAR2[] PROGMEM = "v2"; 13 | 14 | // UI blocks 15 | static const char T_DEMO[] PROGMEM = "demo"; // генерация UI-секции "демо" 16 | static const char T_MORE[] PROGMEM = "more"; 17 | 18 | // UI handlers 19 | static const char T_SET_DEMO[] PROGMEM = "do_demo"; // обработка данных из секции "демо" 20 | static const char T_SET_MORE[] PROGMEM = "do_more"; 21 | -------------------------------------------------------------------------------- /examples/02_sensors/.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /examples/02_sensors/README.md: -------------------------------------------------------------------------------- 1 | # EmbUI Example 2 | 3 | ### Dynamic Sensors display/update template 4 | 5 | Шаблон проекта демонстрирующий способы отображения информации с различных сенсоров 6 | 7 | - Формирование "дисплеев" на странице 8 | - дисплей представляет из себя div с текстовым содержимым на основе специального CSS стиля 9 | - Стили можно использовать как свои так и встроенный в фреймворк стиль по-умолчанию 10 | - обновление данных на дисплее производится через периодическую отсылку пар id:value, данные обновляются на "дисплее" с соответсвующим id 11 | 12 | display example 13 | 14 | Установка: 15 | 16 | - в папку data развернуть файлы фреймворка из архива /resources/data.zip **БЕЗ** перезаписи содержимого 17 | - удалить файл index.html.gz 18 | - собрать проект в platformio 19 | - залить FS в контроллер `pio run -t uploadfs` 20 | - залить прошивку в контроллер `pio run -t upload` 21 | 22 | ### Compressed OTA firmware upload external hook script 23 | Uncomment the following lines in platformio.ini and do the upload as usual `pio run -t upload`. 24 | More details at [ESP32-FlashZ](https://github.com/vortigont/esp32-flashz/tree/main/examples) examples 25 | 26 | ``` 27 | ; compressed OTA upload via platformio hook 28 | ;OTA_url = http://espembui/update 29 | ;OTA_compress = true 30 | ;extra_scripts = 31 | ; post:post_flashz.py 32 | ``` -------------------------------------------------------------------------------- /examples/02_sensors/ci_core3.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | ;core_dir = .pioarduino3 3 | default_envs = esp32 4 | 5 | [env] 6 | framework = arduino 7 | ; Tasmota's platform, based on Arduino Core 3.1.0.241030 IDF 5.3.1+ 8 | platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.11.30/platform-espressif32.zip 9 | board_build.filesystem = littlefs 10 | lib_deps = 11 | symlink:///home/runner/work/EmbUI/EmbUI 12 | ;build_unflags = 13 | ; -std=gnu++11 14 | ;build_flags = 15 | -DEMBUI_DEBUG_LEVEL=3 16 | ; -std=gnu++17 17 | monitor_speed = 115200 18 | monitor_filters = esp32_exception_decoder 19 | ;upload_speed = 460800 20 | 21 | [env:esp32] 22 | board = wemos_d1_mini32 23 | build_flags = 24 | ${env.build_flags} 25 | -DFZ_WITH_ASYNCSRV 26 | -DNO_GLOBAL_UPDATE 27 | ;lib_deps = 28 | ; ${env.lib_deps} 29 | 30 | ; build with yubox mod AsyncServer 31 | ;[env:esp32yubox] 32 | ;extends = env:esp32 33 | ;lib_deps = 34 | ; ${libs.yubox} 35 | ;lib_ignore = 36 | ; ${env.lib_ignore} 37 | ; AsyncTCP 38 | ;build_flags = 39 | ; ${env:esp32} 40 | ; -DYUBOXMOD 41 | -------------------------------------------------------------------------------- /examples/02_sensors/data/css/disp.css: -------------------------------------------------------------------------------- 1 | #vcc:before { 2 | content: 'Vcc:'; 3 | } 4 | #vcc:after { 5 | content: 'v'; 6 | } 7 | #temp:before { 8 | content: 'Temp.:'; 9 | } 10 | #temp:after { 11 | content: '℃'; 12 | } 13 | #clk:before { 14 | content: 'Clock'; 15 | } -------------------------------------------------------------------------------- /examples/02_sensors/data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | EmbUI 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 |
24 | 25 | 63 | 64 | 76 | 77 | 88 | 89 | 190 | 191 | 192 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /examples/02_sensors/display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/examples/02_sensors/display.png -------------------------------------------------------------------------------- /examples/02_sensors/part_4MiB_c3.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size, Flags 2 | nvs, data, nvs, 0x9000, 0x4000, 3 | otadata, data, ota, , 0x2000, 4 | phy_init, data, phy, , 0x1000, 5 | ota0, app, ota_0, 0x10000, 0x100000, 6 | ota1, app, ota_1, 0x110000, 0x100000, 7 | spiffs, data, spiffs, 0x210000, 0x1f0000, 8 | -------------------------------------------------------------------------------- /examples/02_sensors/platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | default_envs = esp32 3 | extra_configs = 4 | user_*.ini 5 | 6 | [env] 7 | framework = arduino 8 | board_build.filesystem = littlefs 9 | lib_deps = 10 | vortigont/EmbUI 11 | build_unflags = 12 | -std=gnu++11 13 | build_flags = 14 | -std=gnu++17 15 | -DEMBUI_DEBUG 16 | monitor_speed = 115200 17 | monitor_filters = esp32_exception_decoder 18 | ;upload_speed = 460800 19 | 20 | [env:esp32] 21 | platform = espressif32 22 | board = wemos_d1_mini32 23 | build_flags = 24 | ${env.build_flags} 25 | -DFZ_WITH_ASYNCSRV 26 | -DNO_GLOBAL_UPDATE 27 | ;lib_deps = 28 | ; ${env.lib_deps} 29 | ; compressed OTA upload via platformio hook 30 | ;OTA_url = http://espembui/update 31 | ;OTA_compress = true 32 | ;extra_scripts = 33 | ; post:post_flashz.py 34 | 35 | 36 | ; ESP32-S2 platform 37 | [env:esp32-s2] 38 | extends = env:esp32 39 | board = featheresp32-s2 40 | build_flags = 41 | ${env:esp32.build_flags} 42 | -DLED_BUILTIN=13 43 | ; -DCORE_DEBUG_LEVEL=5 44 | ; -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG 45 | 46 | ; ESP32-c3 platform 47 | [env:esp32-c3] 48 | extends = env:esp32 49 | board = ttgo-t-oi-plus 50 | build_flags = 51 | ${env:esp32.build_flags} 52 | ; -DLED_BUILTIN=3 53 | ; Github's CI job can't pick external part file, uncomment it for real-live builds 54 | ;board_build.partitions = part_4MiB_c3.csv 55 | 56 | ; build with yubox mod AsyncServer 57 | ;[env:esp32yubox] 58 | ;extends = env:esp32 59 | ;lib_deps = 60 | ; ${libs.yubox} 61 | ;lib_ignore = 62 | ; ${env.lib_ignore} 63 | ; AsyncTCP 64 | ;build_flags = 65 | ; ${env.esp32} 66 | ; -DYUBOXMOD 67 | -------------------------------------------------------------------------------- /examples/02_sensors/post_flashz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | #https://docs.platformio.org/en/latest/scripting/actions.html 4 | #https://trass3r.github.io/coding/2018/12/20/advanced-platformio.html 5 | #https://github.com/platformio/bintray-secure-ota/blob/master/publish_firmware.py 6 | 7 | from os.path import basename 8 | from os.path import isfile 9 | from os.path import getsize 10 | 11 | import subprocess 12 | import requests 13 | import sys,zlib 14 | import re 15 | 16 | Import("env", "projenv") 17 | 18 | # access to global build environment 19 | #print(env) 20 | 21 | # access to project build environment (is used source files in "src" folder) 22 | #print(projenv.Dump()) 23 | 24 | # 25 | # Dump build environment (for debug purpose) 26 | # print(env.Dump()) 27 | # 28 | 29 | 30 | def pigz_compress(source, target, env): 31 | firmware_path = str(target[0]) #.replace(".elf", ".bin") 32 | #firmware_name = basename(firmware_path) 33 | print("Compressing %s file..." % basename(firmware_path)) 34 | subprocess.run(["pigz", "-fzk11", firmware_path]) 35 | 36 | #def zlib_compress(source, target, env): 37 | def zlib_compress(source): 38 | imgfile = source 39 | print("Compressing %s file..." % basename(imgfile)) 40 | with open(imgfile, 'rb') as img: 41 | with open(imgfile + '.zz', 'wb') as deflated: 42 | data = zlib.compress(img.read(), zlib.Z_BEST_COMPRESSION) 43 | deflated.write(data) 44 | compress_ratio = (float(getsize(imgfile)) - float(getsize(imgfile + '.zz'))) / float(getsize(imgfile)) * 100 45 | print("Compress ratio %d%%" % compress_ratio) 46 | 47 | def ota_upload(source, target, env): 48 | file_path = str(source[0]) 49 | print ("Found OTA_url option, will attempt over-the-air upload") 50 | 51 | try: 52 | compress = env.GetProjectOption('OTA_compress') 53 | if compress in ("yes", "true", "1"): 54 | print("Found OTA_compress option") 55 | zlib_compress(file_path) 56 | if (isfile(file_path + ".zz")): 57 | file_path += ".zz" 58 | except: 59 | print ("OTA_compress not found, NOT using compression") 60 | 61 | 62 | url = env.GetProjectOption('OTA_url') 63 | 64 | # check if we upload a firmware or FS image 65 | imgtype = None 66 | if (bool(re.search('firmware', file_path))): 67 | imgtype = 'fw' 68 | else: 69 | imgtype = 'fs' 70 | 71 | payload = {'img' : imgtype } 72 | f = {'file': open(file_path, 'rb')} 73 | req = None 74 | try: 75 | print("Uploading file %s to %s " % (file_path, url)) 76 | req = requests.post(url, data = payload, files=f) 77 | req.raise_for_status() 78 | except requests.exceptions.RequestException as e: 79 | sys.stderr.write("Failed to upload file: %s\n" % 80 | ("%s\n%s" % (req.status_code, req.text) if req else str(e))) 81 | env.Exit(1) 82 | 83 | print("The firmware has been successfuly uploaded!") 84 | 85 | # available targets, i.e. buildprog, size, upload, program, buildfs, uploadfs, uploadfsota 86 | #buildprog is a target when ALL files are compiled and PROGRAM is ready. 87 | 88 | # autocompress all bin images (fw and fs) 89 | #env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", pigz_compress) 90 | 91 | # Custom upload handler 92 | try: 93 | env.GetProjectOption('OTA_url') 94 | env.Replace(UPLOADCMD=ota_upload) 95 | except: 96 | print ("OTA_url not found, fallback to serial flasher") 97 | -------------------------------------------------------------------------------- /examples/02_sensors/src/interface.cpp: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | 3 | #include "EmbUI.h" 4 | #include "interface.h" 5 | 6 | #include "uistrings.h" // non-localized text-strings 7 | 8 | /** 9 | * можно нарисовать свой собственный интефейс/обработчики с нуля, либо 10 | * подключить статический класс с готовыми формами для базовых системных натсроек и дополнить интерфейс. 11 | * необходимо помнить что существуют системные переменные конфигурации с зарезервированными именами. 12 | * Список имен системных переменных можно найти в файле "constants.h" 13 | */ 14 | #include "basicui.h" 15 | 16 | // Include Tasker class from Framework to work with periodic tasks 17 | #include "ts.h" 18 | 19 | // TaskScheduler - Let the runner object be a global, single instance shared between object files. 20 | extern Scheduler ts; 21 | 22 | // Periodic task that runs every 5 sec and calls sensor publishing method 23 | Task tDisplayUpdater(SENSOR_UPDATE_RATE * TASK_SECOND, TASK_FOREVER, &sensorPublisher, &ts, true ); 24 | 25 | 26 | /** 27 | * Headlile section 28 | * this is an overriden weak method that builds our WebUI interface from the top 29 | * == 30 | * Головная секция, 31 | * переопределенный метод фреймфорка, который начинает строить корень нашего Web-интерфейса 32 | * 33 | */ 34 | void section_main_frame(Interface *interf, JsonObjectConst data, const char* action){ 35 | 36 | interf->json_frame_interface(); 37 | 38 | interf->json_section_manifest("EmbUI Example", embui.macid(), 0, "v1.0"); // app name/jsapi/version manifest 39 | interf->json_section_end(); // manifest section MUST be closed! 40 | 41 | block_menu(interf, data, NULL); // Строим UI блок с меню выбора других секций 42 | interf->json_frame_flush(); // flush frame, we will create a new one later 43 | 44 | if(WiFi.getMode() & WIFI_MODE_STA){ // if WiFI is no connected to external AP, than show page with WiFi setup 45 | block_demopage(interf, data, NULL); // Строим блок с demo переключателями 46 | } else { 47 | LOG(println, "UI: Opening network setup page"); 48 | basicui::page_settings_netw(interf, {}); 49 | } 50 | 51 | }; 52 | 53 | 54 | /** 55 | * This code builds UI section with menu block on the left 56 | * 57 | */ 58 | void block_menu(Interface *interf, JsonObjectConst data, const char* action){ 59 | if (!interf) return; 60 | // создаем меню 61 | interf->json_section_menu(); 62 | 63 | /** 64 | * пункт меню - "демо" 65 | */ 66 | interf->option(T_DEMO, "UI Demo"); 67 | 68 | /** 69 | * добавляем в меню пункт - настройки, 70 | * это автоматически даст доступ ко всем связанным секциям с интерфейсом для системных настроек 71 | * 72 | */ 73 | basicui::menuitem_settings(interf); // пункт меню "настройки" 74 | interf->json_section_end(); 75 | } 76 | 77 | /** 78 | * Demo controls 79 | * 80 | */ 81 | void block_demopage(Interface *interf, JsonObjectConst data, const char* action){ 82 | if (!interf) return; 83 | interf->json_frame_interface(); 84 | 85 | // Headline 86 | // параметр T_SET_DEMO определяет зарегистрированный обработчик данных для секции 87 | interf->json_section_main(T_SET_DEMO, "Some demo sensors"); 88 | 89 | // переключатель, связанный со светодиодом. Изменяется синхронно 90 | interf->checkbox(V_LED, embui.getConfig()[V_LED], "Onboard LED", true); 91 | 92 | interf->comment("A comment: simple live-displays"); // комментарий-описание секции 93 | 94 | interf->json_section_line(); // Open line section - next elements will be placed in a line 95 | 96 | // Now I create a string of text that will follow LED's state 97 | String cmt = "Onboard LED is "; 98 | 99 | if ( embui.getConfig()[V_LED] ) // get LED's state from a configuration variable 100 | cmt += "ON!"; 101 | else 102 | cmt += "OFF!"; 103 | 104 | interf->comment("ledcmt", cmt); // Create a comment string - LED state text, "ledcmt" is an element ID I will use to change text later 105 | 106 | // Voltage display, shows ESPs internal voltage 107 | #ifdef ESP8266 108 | interf->display("vcc", ESP.getVcc()/1000.0); 109 | #endif 110 | 111 | #ifdef ESP32 112 | interf->display("vcc", 3.3); // set static voltage 113 | #endif 114 | 115 | /* 116 | Some display based on user defined CSS class - "mycssclass". CSS must be loaded in WebUI 117 | Resulting CSS classes are defined as: class='mycssclass vcc' 118 | So both could be used to customize display appearance 119 | interf->display("vcc", 42, "mycssclass"); 120 | */ 121 | 122 | // Fake temperature sensor 123 | interf->display("temp", 24); 124 | interf->json_section_end(); // end of line 125 | 126 | // Simple Clock display 127 | String clk; TimeProcessor::getInstance().getDateTimeString(clk); 128 | interf->display("clk", clk); // Clock DISPLAY 129 | 130 | 131 | // Update rate slider 132 | interf->range(V_UPDRATE, (int)tDisplayUpdater.getInterval()/1000, 0, 30, 1, "Update Rate, sec", true); 133 | interf->json_frame_flush(); 134 | } 135 | 136 | /** 137 | * @brief interactive handler for LED switchbox 138 | * every change of a checkbox triggers this action 139 | */ 140 | void action_blink(Interface *interf, JsonObjectConst data, const char* action){ 141 | if (!data) return; // process only data 142 | 143 | // save new LED state to the config 144 | embui.getConfig()[V_LED] = data[V_LED]; 145 | embui.autosave(); 146 | 147 | // set LED state to the new checkbox state 148 | digitalWrite(LED_BUILTIN, !data[V_LED]); // write inversed signal to the build-in LED's GPIO 149 | Serial.printf("LED: %u\n", data[V_LED].as()); 150 | 151 | // if we have an interface ws object, 152 | // than we can publish some changes to the web pages change comment text to reflect LED state 153 | if (!interf) 154 | return; 155 | 156 | interf->json_frame_interface(); // make a new interface frame 157 | interf->json_section_content(); // we only update existing elemtns, not pulishing new ones 158 | 159 | String cmt = "Onboard LED is "; 160 | 161 | if ( data[V_LED] ) // find new LED's state from an incoming data 162 | cmt += "ON!"; 163 | else 164 | cmt += "Off!"; 165 | 166 | interf->comment("ledcmt", cmt); // update comment object with ID "ledcmt" 167 | interf->json_frame_flush(); 168 | } 169 | 170 | /** 171 | * обработчик статуса (периодического опроса контроллера веб-приложением) 172 | */ 173 | void pubCallback(Interface *interf){ 174 | basicui::embuistatus(interf); 175 | } 176 | 177 | /** 178 | * Call-back for Periodic publisher (truggered via Task Scheduler) 179 | * it reads (virtual) sensors and publishes values to the WebUI 180 | */ 181 | void sensorPublisher() { 182 | if (!embui.ws.count()) // send new values only if there are WebSocket clients 183 | return; 184 | 185 | Interface interf(&embui.ws); 186 | interf.json_frame_value(); 187 | // Update voltage sensor 188 | #ifdef ESP8266 189 | float v = ESP.getVcc(); 190 | #else 191 | float v = 3.3; 192 | #endif 193 | 194 | // id, value, html=true 195 | // html must be set 'true' so this value could be handeled properly for div elements 196 | // add some random voltage spikes to make display change it's value 197 | interf.value("vcc", (100*v + random(-15,15))/100.0, true); 198 | 199 | // add some random spikes to the temperature :) 200 | interf.value("temp", 24 + random(-30,30)/10.0, true); 201 | 202 | String clk; TimeProcessor::getInstance().getDateTimeString(clk); 203 | interf.value("clk", clk, true); // Current date/time for Clock display 204 | 205 | interf.json_frame_flush(); 206 | } 207 | 208 | /** 209 | * Change sensor update rate callback 210 | */ 211 | void setRate(Interface *interf, JsonObjectConst data, const char* action) { 212 | 213 | if (!data[V_UPDRATE]){ // disable update on interval '0' 214 | tDisplayUpdater.disable(); 215 | } else { 216 | tDisplayUpdater.setInterval( data[V_UPDRATE].as() * TASK_SECOND ); // set update rate in seconds 217 | tDisplayUpdater.enableIfNot(); 218 | } 219 | 220 | } 221 | 222 | /** 223 | * функция регистрации переменных и активностей 224 | * 225 | */ 226 | void create_parameters(){ 227 | LOG(println, "UI: Creating application vars"); 228 | 229 | // регистрируем обработчики активностей 230 | embui.action.set_mainpage_cb(section_main_frame); // заглавная страница веб-интерфейса 231 | 232 | /** 233 | * добавляем свои обрабочки на вывод UI-секций 234 | * и действий над данными 235 | */ 236 | embui.action.add(T_DEMO, block_demopage); // generate "Demo" UI section 237 | 238 | // обработчики 239 | embui.action.add(V_LED, action_blink); // обработка рычажка светодиода 240 | embui.action.add(V_UPDRATE, setRate); // sensor data publisher rate change 241 | }; 242 | 243 | -------------------------------------------------------------------------------- /examples/02_sensors/src/interface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void create_parameters(); 4 | void block_menu(Interface *interf, JsonObjectConst data, const char* action); 5 | void block_demopage(Interface *interf, JsonObjectConst data, const char* action); 6 | void action_demopage(Interface *interf, JsonObjectConst data, const char* action); 7 | void action_blink(Interface *interf, JsonObjectConst data, const char* action); 8 | void setRate(Interface *interf, JsonObjectConst data, const char* action); 9 | void sensorPublisher(); 10 | -------------------------------------------------------------------------------- /examples/02_sensors/src/main.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Main headers 3 | #include "main.h" 4 | #include "EmbUI.h" 5 | #include "uistrings.h" // non-localized text-strings 6 | #include "interface.h" 7 | 8 | /** 9 | * построение интерфейса осуществляется в файлах 'interface.*' 10 | * 11 | */ 12 | 13 | // MAIN Setup 14 | void setup() { 15 | Serial.begin(BAUD_RATE); 16 | Serial.println("Starting test..."); 17 | 18 | pinMode(LED_BUILTIN, OUTPUT); // we are goning to blink this LED 19 | 20 | // Start EmbUI framework 21 | embui.begin(); 22 | // disable internal publishing scheduler, we will use our own 23 | embui.setPubInterval(0); 24 | 25 | // register our actions 26 | create_parameters(); 27 | 28 | // restore LED state from configuration 29 | digitalWrite( LED_BUILTIN, !embui.getConfig()[V_LED] ); 30 | } 31 | 32 | 33 | // MAIN loop 34 | void loop() { 35 | embui.handle(); 36 | } -------------------------------------------------------------------------------- /examples/02_sensors/src/main.h: -------------------------------------------------------------------------------- 1 | #include "Arduino.h" 2 | 3 | #define BAUD_RATE 115200 // serial debug port baud rate 4 | #define SENSOR_UPDATE_RATE 5 // fake sensors default update rate -------------------------------------------------------------------------------- /examples/02_sensors/src/uistrings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Set of flash-strings that might be reused multiple times within the code 4 | 5 | // General 6 | static const char T_HEADLINE[] PROGMEM = "EmbUI Demo"; // имя проекта 7 | 8 | 9 | // Our variable names 10 | static const char V_LED[] PROGMEM = "vLED"; 11 | static const char V_VAR1[] PROGMEM = "v1"; 12 | static const char V_VAR2[] PROGMEM = "v2"; 13 | static const char V_UPDRATE[] PROGMEM = "updrt"; // update rate 14 | 15 | // UI blocks 16 | static const char T_DEMO[] PROGMEM = "demo"; // генерация UI-секции "демо" 17 | 18 | // UI handlers 19 | static const char T_SET_DEMO[] PROGMEM = "do_demo"; // обработка данных из секции "демо" 20 | static const char T_SET_MORE[] PROGMEM = "do_more"; 21 | -------------------------------------------------------------------------------- /examples/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":"EmbUI", 3 | "menu":[ 4 | "Главная страница", 5 | "Настройки чего-то там", 6 | "Информация", 7 | "О нас" 8 | ], 9 | "content":[ 10 | [ 11 | { 12 | "html": "input", 13 | "id": "ssid", 14 | "name": "ssid", 15 | "type": "text", 16 | "value": "test", 17 | "placeholder": "Точка доступа", 18 | "label": "Точка доступа" 19 | }, 20 | { 21 | "html": "input", 22 | "id": "range", 23 | "name": "ssid", 24 | "type": "range", 25 | "value": "28", 26 | "min": "-10", 27 | "max": "100", 28 | "step": "0.1", 29 | "label": "Точка доступа" 30 | }, 31 | { 32 | "html": "input", 33 | "id": "chk", 34 | "name": "ssid", 35 | "type": "checkbox", 36 | "value": "true", 37 | "label": "Точка доступа" 38 | }, 39 | { 40 | "html": "input", 41 | "id": "pass", 42 | "name": "pass", 43 | "type": "password", 44 | "value": "", 45 | "label": "Пароль" 46 | }, 47 | { 48 | "html": "input", 49 | "id": "test", 50 | "name": "test", 51 | "type": "text", 52 | "value": "test", 53 | "placeholder": "Тестовое", 54 | "label": "Тестовое" 55 | }, 56 | { 57 | "html": "select", 58 | "id": "test", 59 | "value": "test", 60 | "label": "Выбирай!", 61 | "options": [ 62 | "Первый", 63 | "Второй", 64 | "Третий", 65 | "Четвертый", 66 | "Пятый", 67 | "Шестой", 68 | "Седьмой", 69 | "Восьмой" 70 | ] 71 | } 72 | ], 73 | [ 74 | { 75 | "html": "input", 76 | "id": "ssid", 77 | "name": "ssid", 78 | "type": "text", 79 | "value": "test", 80 | "placeholder": "Точка доступа", 81 | "label": "Точка доступа" 82 | }, 83 | { 84 | "html": "select", 85 | "id": "test", 86 | "value": "test", 87 | "label": "Выбирай!", 88 | "options": [ 89 | "Первый", 90 | "Второй", 91 | "Третий", 92 | "Четвертый", 93 | "Пятый", 94 | "Шестой", 95 | "Седьмой", 96 | "Восьмой" 97 | ] 98 | }, 99 | { 100 | "html": "select", 101 | "id": "test", 102 | "value": "test", 103 | "label": "Выбирай!", 104 | "options": [ 105 | "Первый", 106 | "Второй", 107 | "Третий", 108 | "Четвертый", 109 | "Пятый", 110 | "Шестой", 111 | "Седьмой", 112 | "Восьмой" 113 | ] 114 | }, 115 | { 116 | "html": "select", 117 | "id": "test", 118 | "value": "test", 119 | "label": "Выбирай!", 120 | "options": [ 121 | "Первый", 122 | "Второй", 123 | "Третий", 124 | "Четвертый", 125 | "Пятый", 126 | "Шестой", 127 | "Седьмой", 128 | "Восьмой" 129 | ] 130 | } 131 | ], 132 | [ 133 | { 134 | "html": "input", 135 | "id": "ssid", 136 | "name": "ssid", 137 | "type": "text", 138 | "value": "test", 139 | "placeholder": "Точка доступа", 140 | "label": "Точка доступа" 141 | }, 142 | { 143 | "html": "select", 144 | "id": "test", 145 | "value": "test", 146 | "label": "Выбирай!", 147 | "options": [ 148 | "Первый", 149 | "Второй", 150 | "Третий", 151 | "Четвертый", 152 | "Пятый", 153 | "Шестой", 154 | "Седьмой", 155 | "Восьмой" 156 | ] 157 | }, 158 | { 159 | "html": "select", 160 | "id": "test", 161 | "value": "test", 162 | "label": "Выбирай!", 163 | "options": [ 164 | "Первый", 165 | "Второй", 166 | "Третий", 167 | "Четвертый", 168 | "Пятый", 169 | "Шестой", 170 | "Седьмой", 171 | "Восьмой" 172 | ] 173 | } 174 | ], 175 | [ 176 | { 177 | "html": "input", 178 | "id": "ssid", 179 | "name": "ssid", 180 | "type": "text", 181 | "value": "test", 182 | "placeholder": "Точка доступа", 183 | "label": "Точка доступа" 184 | } 185 | ] 186 | ] 187 | } -------------------------------------------------------------------------------- /examples/ui_datetime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/examples/ui_datetime.png -------------------------------------------------------------------------------- /git_rev_macro.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | try: 4 | revision = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip() 5 | print ("-DPIO_SRC_REV=\\\"%s\\\"" % revision) 6 | except: 7 | print ("-DPIO_SRC_REV=\\\"UNKNOWN\\\"") 8 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EmbUI", 3 | "frameworks": "arduino", 4 | "platforms": ["espressif32"], 5 | "version": "4.2.2", 6 | "keywords": "embui, arduino", 7 | "description": "Embeded Web UI framework for ESP32 IoT prototyping", 8 | "url": "https://github.com/vortigont/EmbUI", 9 | "authors": [ 10 | { 11 | "name": "Emil Muratov", 12 | "url": "https://github.com/vortigont", 13 | "email": "gpm@hotplug.ru", 14 | "maintainer": true 15 | }, 16 | { 17 | "name": "Anton Zolotarev", 18 | "url": "https://github.com/anton-zolotarev" 19 | }, 20 | { 21 | "name": "Dmytro Korniienko", 22 | "url": "https://github.com/DmytroKorniienko" 23 | } 24 | ], 25 | "license": "MIT", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/vortigont/EmbUI.git" 29 | }, 30 | "dependencies": 31 | [ 32 | {"owner": "bblanchon", 33 | "name": "ArduinoJson", 34 | "version": "^7.2,<7.4"}, 35 | {"owner": "esp32async", 36 | "name": "ESPAsyncWebServer", 37 | "version": "^3.6"}, 38 | {"owner": "bblanchon", 39 | "name": "StreamUtils", 40 | "version": "https://github.com/bblanchon/ArduinoStreamUtils"}, 41 | {"owner": "marvinroger", 42 | "name": "AsyncMqttClient", 43 | "version": "~0.9"}, 44 | {"owner": "arkhipenko", 45 | "name": "TaskScheduler", 46 | "version": "~3.7"}, 47 | {"owner": "vortigont", 48 | "name": "FTPClientServer", 49 | "version": "https://github.com/vortigont/FTPClientServer#feat"}, 50 | {"owner": "vortigont", 51 | "name": "esp32-flashz", 52 | "version": "~1.1", 53 | "platforms": ["espressif32"]} 54 | ], 55 | "build": { 56 | "srcDir": "src", 57 | "libLDFMode": "chain", 58 | "libCompatMode": "strict" 59 | }, 60 | "headers": ["EmbUI.h", "basicui.h", "ts.h", "ui.h"], 61 | "export": { 62 | "include": [ 63 | "src/*", 64 | "examples/*", 65 | "*.md", 66 | "*.json", 67 | "LICENSE", 68 | "resources/*" 69 | ], 70 | "exclude": [ 71 | "*/*.tmp" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /resources/html/css/bar_dark.css: -------------------------------------------------------------------------------- 1 | .progressa { 2 | border-radius: 50px !important; 3 | height: 20px; 4 | overflow: hidden; 5 | margin-bottom: 20px; 6 | background-color: rgba(0,0,0,0.5); 7 | border-radius: 4px; 8 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 9 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 10 | line-height: 20px; 11 | font-size: 12px; 12 | border: 3px solid transparent; 13 | } 14 | .progressab { 15 | background-image: -webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent); 16 | background-image: -o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent); 17 | -webkit-background-size: 40px 40px; 18 | background-size: 40px 40px; 19 | -webkit-transition: width .25s ease,height .25s ease,font-size .25s ease; 20 | -moz-transition: width .25s ease,height .25s ease,font-size .25s ease; 21 | -ms-transition: width .25s ease,height .25s ease,font-size .25s ease; 22 | -o-transition: width .25s ease,height .25s ease,font-size .25s ease; 23 | transition: width .25s ease,height .25s ease,font-size .25s ease; 24 | width: 0; 25 | color: #fff; 26 | text-align: center; 27 | font-family: 'Open Sans',sans-serif !important; 28 | animation: progress-bar-stripes 2s linear infinite reverse; 29 | } 30 | 31 | @keyframes progress-bar-stripes{ 32 | 0% { 33 | background-position: 40px 0; 34 | } 35 | 100% { 36 | background-position: 0 0; 37 | } 38 | } -------------------------------------------------------------------------------- /resources/html/css/bar_default.css: -------------------------------------------------------------------------------- 1 | .progressa { 2 | border-radius: 50px !important; 3 | height: 20px; 4 | overflow: hidden; 5 | margin-bottom: 20px; 6 | background-color: rgba(0,0,0,0.5); 7 | border-radius: 4px; 8 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 9 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 10 | line-height: 20px; 11 | font-size: 12px; 12 | border: 3px solid transparent; 13 | } 14 | .progressab { 15 | background-image: -webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent); 16 | background-image: -o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent); 17 | -webkit-background-size: 40px 40px; 18 | background-size: 40px 40px; 19 | -webkit-transition: width .25s ease,height .25s ease,font-size .25s ease; 20 | -moz-transition: width .25s ease,height .25s ease,font-size .25s ease; 21 | -ms-transition: width .25s ease,height .25s ease,font-size .25s ease; 22 | -o-transition: width .25s ease,height .25s ease,font-size .25s ease; 23 | transition: width .25s ease,height .25s ease,font-size .25s ease; 24 | width: 0; 25 | color: #fff; 26 | text-align: center; 27 | font-family: 'Open Sans',sans-serif !important; 28 | animation: progress-bar-stripes 2s linear infinite reverse; 29 | } 30 | 31 | @keyframes progress-bar-stripes{ 32 | 0% { 33 | background-position: 40px 0; 34 | } 35 | 100% { 36 | background-position: 0 0; 37 | } 38 | } -------------------------------------------------------------------------------- /resources/html/css/bar_light.css: -------------------------------------------------------------------------------- 1 | .progressa { 2 | border-radius: 50px !important; 3 | height: 20px; 4 | overflow: hidden; 5 | margin-bottom: 20px; 6 | background-color: rgba(0,0,0,0.5); 7 | border-radius: 4px; 8 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 9 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 10 | line-height: 20px; 11 | font-size: 12px; 12 | border: 3px solid transparent; 13 | } 14 | .progressab { 15 | background-image: -webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent); 16 | background-image: -o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent); 17 | -webkit-background-size: 40px 40px; 18 | background-size: 40px 40px; 19 | -webkit-transition: width .25s ease,height .25s ease,font-size .25s ease; 20 | -moz-transition: width .25s ease,height .25s ease,font-size .25s ease; 21 | -ms-transition: width .25s ease,height .25s ease,font-size .25s ease; 22 | -o-transition: width .25s ease,height .25s ease,font-size .25s ease; 23 | transition: width .25s ease,height .25s ease,font-size .25s ease; 24 | width: 0; 25 | color: #fff; 26 | text-align: center; 27 | font-family: 'Open Sans',sans-serif !important; 28 | animation: progress-bar-stripes 2s linear infinite reverse; 29 | } 30 | 31 | @keyframes progress-bar-stripes{ 32 | 0% { 33 | background-position: 40px 0; 34 | } 35 | 100% { 36 | background-position: 0 0; 37 | } 38 | } -------------------------------------------------------------------------------- /resources/html/css/menu-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/resources/html/css/menu-dark.webp -------------------------------------------------------------------------------- /resources/html/css/menu-light.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/resources/html/css/menu-light.webp -------------------------------------------------------------------------------- /resources/html/css/menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/resources/html/css/menu.jpg -------------------------------------------------------------------------------- /resources/html/css/range_dark.css: -------------------------------------------------------------------------------- 1 | input[type=range] { 2 | height: 30px; 3 | -webkit-appearance: none; 4 | margin: 5px 0; 5 | width: 100%; 6 | background-color: transparent; 7 | } 8 | input[type=range]:focus { 9 | outline: none; 10 | } 11 | input[type=range]::-webkit-slider-runnable-track { 12 | width: 100%; 13 | height: 12px; 14 | cursor: pointer; 15 | animate: 0.2s; 16 | box-shadow: inset 5px 9px 9px #777; 17 | background: transparent; 18 | border-radius: 12px; 19 | border: 1px solid #ccccccd9; 20 | } 21 | input[type=range]::-webkit-slider-thumb { 22 | width: 15px; 23 | height: 30px; 24 | border-radius: 30px; 25 | background-color: #1f8dd6; 26 | border: 1px solid #ccccccd9; 27 | box-shadow: 5px 9px 9px #777; 28 | cursor: pointer; 29 | -webkit-appearance: none; 30 | margin-top: -8.5px; 31 | } 32 | input[type=range]:focus::-webkit-slider-runnable-track { 33 | background: #1f8dd6; 34 | } 35 | input[type=range]::-moz-range-track { 36 | width: 100%; 37 | height: 12px; 38 | cursor: pointer; 39 | animate: 0.2s; 40 | box-shadow: inset 5px 9px 9px #777; 41 | background: transparent; 42 | border-radius: 12px; 43 | border: 1px solid #ccccccd9; 44 | } 45 | 46 | input[type=range]::-moz-range-thumb { 47 | width: 15px; 48 | height: 30px; 49 | border-radius: 30px; 50 | background-color: #1f8dd6; 51 | border: 1px solid #ccccccd9; 52 | box-shadow: 5px 9px 9px #777; 53 | cursor: pointer; 54 | border: 1px solid #ccccccd9; 55 | } 56 | input[type=range]::-ms-track { 57 | width: 100%; 58 | height: 4px; 59 | cursor: pointer; 60 | animate: 0.2s; 61 | background: transparent; 62 | border-color: transparent; 63 | color: transparent; 64 | } 65 | input[type=range]::-ms-fill-lower { 66 | background: #2497E3; 67 | border: 0px solid #000000; 68 | border-radius: 6px; 69 | box-shadow: 0px 0px 0px #000000; 70 | } 71 | input[type=range]::-ms-fill-upper { 72 | background: #2497E3; 73 | border: 0px solid #000000; 74 | border-radius: 6px; 75 | box-shadow: 0px 0px 0px #000000; 76 | } 77 | input[type=range]::-ms-thumb { 78 | margin-top: 1px; 79 | width: 15px; 80 | height: 30px; 81 | border-radius: 30px; 82 | background-color: #1f8dd6; 83 | border: 1px solid #777; 84 | box-shadow: 5px 9px 9px #777; 85 | cursor: pointer; 86 | } 87 | input[type=range]:focus::-ms-fill-lower { 88 | background: #2497E3; 89 | } 90 | input[type=range]:focus::-ms-fill-upper { 91 | background: #2497E3; 92 | } 93 | -------------------------------------------------------------------------------- /resources/html/css/range_default.css: -------------------------------------------------------------------------------- 1 | 2 | input[type=range] { 3 | height: 27px; 4 | -webkit-appearance: none; 5 | margin: 10px 0; 6 | width: 100%; 7 | background: transparent; 8 | } 9 | input[type=range]:focus { 10 | outline: none; 11 | } 12 | input[type=range]::-webkit-slider-runnable-track { 13 | width: 100%; 14 | height: 4px; 15 | cursor: pointer; 16 | animate: 0.2s; 17 | box-shadow: 0px 0px 0px #000000; 18 | background: #2497E3; 19 | border-radius: 3px; 20 | border: 0px solid #000000; 21 | } 22 | input[type=range]::-webkit-slider-thumb { 23 | box-shadow: 0px 0px 0px #000000; 24 | border: 1px solid rgb(148, 148, 148); 25 | height: 20px; 26 | width: 20px; 27 | border-radius: 40px; 28 | background: #F0F0F0; 29 | cursor: pointer; 30 | -webkit-appearance: none; 31 | margin-top: -8.5px; 32 | } 33 | input[type=range]:focus::-webkit-slider-runnable-track { 34 | background: #2497E3; 35 | } 36 | input[type=range]::-moz-range-track { 37 | width: 100%; 38 | height: 4px; 39 | cursor: pointer; 40 | animate: 0.2s; 41 | box-shadow: 0px 0px 0px #000000; 42 | background: #2497E3; 43 | border-radius: 3px; 44 | border: 0px solid #000000; 45 | } 46 | input[type=range]::-moz-range-thumb { 47 | box-shadow: 0px 0px 0px #000000; 48 | border: 1px solid #2497E3; 49 | height: 20px; 50 | width: 20px; 51 | border-radius: 30px; 52 | background: #A1D0FF; 53 | cursor: pointer; 54 | } 55 | input[type=range]::-ms-track { 56 | width: 100%; 57 | height: 4px; 58 | cursor: pointer; 59 | animate: 0.2s; 60 | background: transparent; 61 | border-color: transparent; 62 | color: transparent; 63 | } 64 | input[type=range]::-ms-fill-lower { 65 | background: #2497E3; 66 | border: 0px solid #000000; 67 | border-radius: 6px; 68 | box-shadow: 0px 0px 0px #000000; 69 | } 70 | input[type=range]::-ms-fill-upper { 71 | background: #2497E3; 72 | border: 0px solid #000000; 73 | border-radius: 6px; 74 | box-shadow: 0px 0px 0px #000000; 75 | } 76 | input[type=range]::-ms-thumb { 77 | margin-top: 1px; 78 | box-shadow: 0px 0px 0px #000000; 79 | border: 1px solid #2497E3; 80 | height: 20px; 81 | width: 20px; 82 | border-radius: 40px; 83 | background: #A1D0FF; 84 | cursor: pointer; 85 | } 86 | input[type=range]:focus::-ms-fill-lower { 87 | background: #2497E3; 88 | } 89 | input[type=range]:focus::-ms-fill-upper { 90 | background: #2497E3; 91 | } 92 | -------------------------------------------------------------------------------- /resources/html/css/range_light.css: -------------------------------------------------------------------------------- 1 | input[type=range] { 2 | height: 30px; 3 | -webkit-appearance: none; 4 | margin: 5px 0; 5 | width: 100%; 6 | background-color: transparent; 7 | } 8 | input[type=range]:focus { 9 | outline: none; 10 | } 11 | input[type=range]::-webkit-slider-runnable-track { 12 | width: 100%; 13 | height: 12px; 14 | cursor: pointer; 15 | animate: 0.2s; 16 | box-shadow: inset 5px 9px 9px #777; 17 | background: transparent; 18 | border-radius: 12px; 19 | border: 0px solid #777; 20 | } 21 | input[type=range]::-webkit-slider-thumb { 22 | width: 15px; 23 | height: 30px; 24 | border-radius: 30px; 25 | background-color: #1f8dd6; 26 | border: 1px solid #777; 27 | box-shadow: 5px 9px 9px #777; 28 | cursor: pointer; 29 | -webkit-appearance: none; 30 | margin-top: -8.5px; 31 | } 32 | input[type=range]:focus::-webkit-slider-runnable-track { 33 | background: #1f8dd6; 34 | } 35 | input[type=range]::-moz-range-track { 36 | width: 100%; 37 | height: 12px; 38 | cursor: pointer; 39 | animate: 0.2s; 40 | box-shadow: inset 5px 9px 9px #777; 41 | background: transparent; 42 | border-radius: 12px; 43 | border: 0px solid #777; 44 | } 45 | 46 | input[type=range]::-moz-range-thumb { 47 | width: 15px; 48 | height: 30px; 49 | border-radius: 30px; 50 | background-color: #1f8dd6; 51 | border: 1px solid #777; 52 | box-shadow: 5px 9px 9px #777; 53 | cursor: pointer; 54 | } 55 | input[type=range]::-ms-track { 56 | width: 100%; 57 | height: 4px; 58 | cursor: pointer; 59 | animate: 0.2s; 60 | background: transparent; 61 | border-color: transparent; 62 | color: transparent; 63 | } 64 | input[type=range]::-ms-fill-lower { 65 | background: #2497E3; 66 | border: 0px solid #000000; 67 | border-radius: 6px; 68 | box-shadow: 0px 0px 0px #000000; 69 | } 70 | input[type=range]::-ms-fill-upper { 71 | background: #2497E3; 72 | border: 0px solid #000000; 73 | border-radius: 6px; 74 | box-shadow: 0px 0px 0px #000000; 75 | } 76 | input[type=range]::-ms-thumb { 77 | margin-top: 1px; 78 | width: 15px; 79 | height: 30px; 80 | border-radius: 30px; 81 | background-color: #1f8dd6; 82 | border: 1px solid #777; 83 | box-shadow: 5px 9px 9px #777; 84 | cursor: pointer; 85 | } 86 | input[type=range]:focus::-ms-fill-lower { 87 | background: #2497E3; 88 | } 89 | input[type=range]:focus::-ms-fill-upper { 90 | background: #2497E3; 91 | } 92 | -------------------------------------------------------------------------------- /resources/html/css/side-menu_dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --slide: 250px; 3 | } 4 | 5 | body { 6 | color: #ffffff; 7 | } 8 | 9 | .pure-img-responsive { 10 | max-width: 100%; 11 | height: auto; 12 | } 13 | 14 | /* 15 | Add transition to containers so they can push in and out. 16 | */ 17 | #layout, 18 | #menu, 19 | .menu-link { 20 | -webkit-transition: all 0.2s ease-out; 21 | -moz-transition: all 0.2s ease-out; 22 | -ms-transition: all 0.2s ease-out; 23 | -o-transition: all 0.2s ease-out; 24 | transition: all 0.2s ease-out; 25 | } 26 | 27 | /* 28 | This is the parent `
` that contains the menu and the content area. 29 | */ 30 | #layout { 31 | position: relative; 32 | left: 0; 33 | padding-left: 0; 34 | } 35 | #layout.active #menu { 36 | left: var(--slide); 37 | width: var(--slide); 38 | } 39 | 40 | #layout.active .menu-link { 41 | left: var(--slide); 42 | } 43 | /* 44 | The content `
` is where all your content goes. 45 | */ 46 | .content { 47 | margin: 0 auto; 48 | padding: 0 2em; 49 | max-width: 800px; 50 | line-height: 1.1em; 51 | } 52 | 53 | .header { 54 | margin: 0; 55 | color: #ffffffc9; 56 | text-align: center; 57 | /*padding: 2.5em 2em 0;*/ 58 | border-bottom: 1px solid #eee; 59 | } 60 | .header h1 { 61 | margin: 0.2em 0; 62 | font-size: 4em; 63 | font-weight: bold; 64 | font-family: italic; 65 | } 66 | .header h2 { 67 | font-weight: 300; 68 | color: #ccc; 69 | padding: 0; 70 | margin-top: 0; 71 | } 72 | 73 | .content-subhead { 74 | margin: 50px 0 20px 0; 75 | font-weight: 300; 76 | color: #888; 77 | } 78 | 79 | 80 | 81 | /* 82 | The `#menu` `
` is the parent `
` that contains the `.pure-menu` that 83 | appears on the left side of the page. 84 | */ 85 | 86 | #menu { 87 | margin-left: calc(var(--slide )* -1); /* "#menu" width */ 88 | width: var(--slide); 89 | position: fixed; 90 | top: 0; 91 | left: 0; 92 | bottom: 0; 93 | z-index: 1000; /* so the menu or its navicon stays above all content */ 94 | background: #000; 95 | background-image: url('menu.jpg'); 96 | overflow-y: auto; 97 | -webkit-overflow-scrolling: touch; 98 | } 99 | /* 100 | All anchors inside the menu should be styled like this. 101 | */ 102 | #menu a { 103 | color: #999; 104 | border: none; 105 | padding: 0.6em 0 0.6em 0.6em; 106 | } 107 | 108 | /* 109 | Remove all background/borders, since we are applying them to #menu. 110 | */ 111 | #menu .pure-menu, 112 | #menu .pure-menu ul { 113 | border: none; 114 | background: transparent; 115 | } 116 | 117 | /* 118 | Add that light border to separate items into groups. 119 | */ 120 | #menu .pure-menu ul, 121 | #menu .pure-menu .menu-item-divided { 122 | border-top: 1px solid #333; 123 | } 124 | /* 125 | Change color of the anchor links on hover/focus. 126 | */ 127 | #menu .pure-menu li a:hover, 128 | #menu .pure-menu li a:focus { 129 | background: #333; 130 | } 131 | 132 | /* 133 | This styles the selected menu item `
  • `. 134 | */ 135 | #menu .pure-menu-selected, 136 | #menu .pure-menu-heading { 137 | background: #0078e7; 138 | } 139 | /* 140 | This styles a link within a selected menu item `
  • `. 141 | */ 142 | #menu .pure-menu-selected a { 143 | color: #fff; 144 | background-color: #0078e7; 145 | } 146 | 147 | /* 148 | This styles the menu heading. 149 | */ 150 | #menu .pure-menu-heading { 151 | font-size: 110%; 152 | color: #fff; 153 | margin: 0; 154 | font-style: italic; 155 | font-weight: bolder; 156 | font-family: initial; 157 | } 158 | 159 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/ 160 | 161 | /* 162 | The button to open/close the Menu is custom-made and not part of Pure. Here's 163 | how it works: 164 | */ 165 | 166 | /* 167 | `.menu-link` represents the responsive menu toggle that shows/hides on 168 | small screens. 169 | */ 170 | .menu-link { 171 | position: fixed; 172 | display: block; /* show this only on small screens */ 173 | top: 0; 174 | left: 0; /* "#menu width" */ 175 | background: #000; 176 | background: rgba(0,0,0,0.7); 177 | font-size: 10px; /* change this value to increase/decrease button size */ 178 | z-index: 10; 179 | width: 2em; 180 | height: auto; 181 | padding: 2.1em 1.6em; 182 | } 183 | 184 | .menu-link:hover, 185 | .menu-link:focus { 186 | background: #000; 187 | } 188 | 189 | .menu-link span { 190 | position: relative; 191 | display: block; 192 | } 193 | 194 | .menu-link span, 195 | .menu-link span:before, 196 | .menu-link span:after { 197 | background-color: #fff; 198 | width: 100%; 199 | height: 0.2em; 200 | } 201 | 202 | .menu-link span:before, 203 | .menu-link span:after { 204 | position: absolute; 205 | margin-top: -0.6em; 206 | content: " "; 207 | } 208 | 209 | .menu-link span:after { 210 | margin-top: 0.6em; 211 | } 212 | 213 | 214 | /* -- Responsive Styles (Media Queries) ------------------------------------- */ 215 | 216 | /* 217 | Hides the menu at `48em`, but modify this based on your app's needs. 218 | */ 219 | @media (min-width: 48em) { 220 | 221 | .header, 222 | .content { 223 | padding-left: 2em; 224 | padding-right: 2em; 225 | } 226 | 227 | #layout { 228 | padding-left: var(--slide); /* left col width "#menu" */ 229 | left: 0; 230 | } 231 | #menu { 232 | left: var(--slide); 233 | } 234 | 235 | .menu-link { 236 | position: fixed; 237 | left: var(--slide); 238 | display: none; 239 | } 240 | 241 | #layout.active .menu-link { 242 | left: var(--slide); 243 | } 244 | } 245 | 246 | @media (max-width: 48em) { 247 | /* Only apply this when the window is small. Otherwise, the following 248 | case results in extra padding on the left: 249 | * Make the window small. 250 | * Tap the menu to trigger the active state. 251 | * Make the window large again. 252 | */ 253 | #layout.active { 254 | position: relative; 255 | left: var(--slide); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /resources/html/css/side-menu_default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --slide: 250px; 3 | } 4 | 5 | body { 6 | color: #777; 7 | } 8 | 9 | .pure-img-responsive { 10 | max-width: 100%; 11 | height: auto; 12 | } 13 | 14 | /* 15 | Add transition to containers so they can push in and out. 16 | */ 17 | #layout, 18 | #menu, 19 | .menu-link { 20 | -webkit-transition: all 0.2s ease-out; 21 | -moz-transition: all 0.2s ease-out; 22 | -ms-transition: all 0.2s ease-out; 23 | -o-transition: all 0.2s ease-out; 24 | transition: all 0.2s ease-out; 25 | } 26 | 27 | /* 28 | This is the parent `
    ` that contains the menu and the content area. 29 | */ 30 | #layout { 31 | position: relative; 32 | left: 0; 33 | padding-left: 0; 34 | } 35 | #layout.active #menu { 36 | left: var(--slide); 37 | width: var(--slide); 38 | } 39 | 40 | #layout.active .menu-link { 41 | left: var(--slide); 42 | } 43 | /* 44 | The content `
    ` is where all your content goes. 45 | */ 46 | .content { 47 | margin: 0 auto; 48 | padding: 0 2em; 49 | max-width: 95%; 50 | line-height: 1.2em; 51 | } 52 | 53 | .header { 54 | margin: 0; 55 | color: #333; 56 | text-align: center; 57 | padding: 2.5em 2em 0; 58 | border-bottom: 1px solid #eee; 59 | } 60 | .header h1 { 61 | margin: 0.2em 0; 62 | font-size: 3em; 63 | font-weight: 300; 64 | } 65 | .header h2 { 66 | font-weight: 300; 67 | color: #ccc; 68 | padding: 0; 69 | margin-top: 0; 70 | } 71 | 72 | .content-subhead { 73 | margin: 50px 0 20px 0; 74 | font-weight: 300; 75 | color: #888; 76 | } 77 | 78 | 79 | 80 | /* 81 | The `#menu` `
    ` is the parent `
    ` that contains the `.pure-menu` that 82 | appears on the left side of the page. 83 | */ 84 | 85 | #menu { 86 | margin-left: calc(var(--slide )* -1); /* "#menu" width */ 87 | width: var(--slide); 88 | position: fixed; 89 | top: 0; 90 | left: 0; 91 | bottom: 0; 92 | z-index: 1000; /* so the menu or its navicon stays above all content */ 93 | background: #191818; 94 | overflow-y: auto; 95 | -webkit-overflow-scrolling: touch; 96 | } 97 | /* 98 | All anchors inside the menu should be styled like this. 99 | */ 100 | #menu a { 101 | color: #999; 102 | border: none; 103 | padding: 0.6em 0 0.6em 0.6em; 104 | } 105 | 106 | /* 107 | Remove all background/borders, since we are applying them to #menu. 108 | */ 109 | #menu .pure-menu, 110 | #menu .pure-menu ul { 111 | border: none; 112 | background: transparent; 113 | } 114 | 115 | /* 116 | Add that light border to separate items into groups. 117 | */ 118 | #menu .pure-menu ul, 119 | #menu .pure-menu .menu-item-divided { 120 | border-top: 1px solid #333; 121 | } 122 | /* 123 | Change color of the anchor links on hover/focus. 124 | */ 125 | #menu .pure-menu li a:hover, 126 | #menu .pure-menu li a:focus { 127 | background: #333; 128 | } 129 | 130 | /* 131 | This styles the selected menu item `
  • `. 132 | */ 133 | #menu .pure-menu-selected, 134 | #menu .pure-menu-heading { 135 | background: #1f8dd6; 136 | } 137 | /* 138 | This styles a link within a selected menu item `
  • `. 139 | */ 140 | #menu .pure-menu-selected a { 141 | color: #fff; 142 | } 143 | 144 | /* 145 | This styles the menu heading. 146 | */ 147 | #menu .pure-menu-heading { 148 | font-size: 110%; 149 | color: #fff; 150 | margin: 0; 151 | } 152 | 153 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/ 154 | 155 | /* 156 | The button to open/close the Menu is custom-made and not part of Pure. Here's 157 | how it works: 158 | */ 159 | 160 | /* 161 | `.menu-link` represents the responsive menu toggle that shows/hides on 162 | small screens. 163 | */ 164 | .menu-link { 165 | position: fixed; 166 | display: block; /* show this only on small screens */ 167 | top: 0; 168 | left: 0; /* "#menu width" */ 169 | background: #000; 170 | background: rgba(0,0,0,0.7); 171 | font-size: 10px; /* change this value to increase/decrease button size */ 172 | z-index: 10; 173 | width: 2em; 174 | height: auto; 175 | padding: 2.1em 1.6em; 176 | } 177 | 178 | .menu-link:hover, 179 | .menu-link:focus { 180 | background: #000; 181 | } 182 | 183 | .menu-link span { 184 | position: relative; 185 | display: block; 186 | } 187 | 188 | .menu-link span, 189 | .menu-link span:before, 190 | .menu-link span:after { 191 | background-color: #fff; 192 | width: 100%; 193 | height: 0.2em; 194 | } 195 | 196 | .menu-link span:before, 197 | .menu-link span:after { 198 | position: absolute; 199 | margin-top: -0.6em; 200 | content: " "; 201 | } 202 | 203 | .menu-link span:after { 204 | margin-top: 0.6em; 205 | } 206 | 207 | 208 | /* -- Responsive Styles (Media Queries) ------------------------------------- */ 209 | 210 | /* 211 | Hides the menu at `48em`, but modify this based on your app's needs. 212 | */ 213 | @media (min-width: 48em) { 214 | 215 | .header, 216 | .content { 217 | padding-left: 2em; 218 | padding-right: 2em; 219 | } 220 | 221 | #layout { 222 | padding-left: var(--slide); /* left col width "#menu" */ 223 | left: 0; 224 | } 225 | #menu { 226 | left: var(--slide); 227 | } 228 | 229 | .menu-link { 230 | position: fixed; 231 | left: var(--slide); 232 | display: none; 233 | } 234 | 235 | #layout.active .menu-link { 236 | left: var(--slide); 237 | } 238 | } 239 | 240 | @media (max-width: 48em) { 241 | /* Only apply this when the window is small. Otherwise, the following 242 | case results in extra padding on the left: 243 | * Make the window small. 244 | * Tap the menu to trigger the active state. 245 | * Make the window large again. 246 | */ 247 | #layout.active { 248 | position: relative; 249 | left: var(--slide); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /resources/html/css/side-menu_light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --slide: 250px; 3 | } 4 | 5 | body { 6 | color: #000; 7 | } 8 | 9 | .pure-img-responsive { 10 | max-width: 100%; 11 | height: auto; 12 | } 13 | 14 | /* 15 | Add transition to containers so they can push in and out. 16 | */ 17 | #layout, 18 | #menu, 19 | .menu-link { 20 | -webkit-transition: all 0.2s ease-out; 21 | -moz-transition: all 0.2s ease-out; 22 | -ms-transition: all 0.2s ease-out; 23 | -o-transition: all 0.2s ease-out; 24 | transition: all 0.2s ease-out; 25 | } 26 | 27 | /* 28 | This is the parent `
    ` that contains the menu and the content area. 29 | */ 30 | #layout { 31 | position: relative; 32 | left: 0; 33 | padding-left: 0; 34 | } 35 | #layout.active #menu { 36 | left: var(--slide); 37 | width: var(--slide); 38 | } 39 | 40 | #layout.active .menu-link { 41 | left: var(--slide); 42 | } 43 | /* 44 | The content `
    ` is where all your content goes. 45 | */ 46 | .content { 47 | margin: 0 auto; 48 | padding: 0 2em; 49 | max-width: 800px; 50 | line-height: 1.6em; 51 | } 52 | 53 | .header { 54 | margin: 0; 55 | color: #333; 56 | text-align: center; 57 | /*padding: 2.5em 2em 0;*/ 58 | border-bottom: 1px solid #eee; 59 | } 60 | .header h1 { 61 | margin: 0.2em 0; 62 | font-size: 4em; 63 | font-weight: bold; 64 | } 65 | .header h2 { 66 | font-weight: 300; 67 | color: #ccc; 68 | padding: 0; 69 | margin-top: 0; 70 | } 71 | 72 | .content-subhead { 73 | margin: 50px 0 20px 0; 74 | font-weight: 300; 75 | color: #888; 76 | } 77 | 78 | 79 | 80 | /* 81 | The `#menu` `
    ` is the parent `
    ` that contains the `.pure-menu` that 82 | appears on the left side of the page. 83 | */ 84 | 85 | #menu { 86 | margin-left: calc(var(--slide )* -1); /* "#menu" width */ 87 | width: var(--slide); 88 | position: fixed; 89 | top: 0; 90 | left: 0; 91 | bottom: 0; 92 | z-index: 1000; /* so the menu or its navicon stays above all content */ 93 | background: #dbd4d4; 94 | overflow-y: auto; 95 | -webkit-overflow-scrolling: touch; 96 | } 97 | /* 98 | All anchors inside the menu should be styled like this. 99 | */ 100 | #menu a { 101 | color: #999; 102 | border: none; 103 | padding: 0.6em 0 0.6em 0.6em; 104 | } 105 | 106 | /* 107 | Remove all background/borders, since we are applying them to #menu. 108 | */ 109 | #menu .pure-menu, 110 | #menu .pure-menu ul { 111 | border: none; 112 | background: transparent; 113 | } 114 | 115 | /* 116 | Add that light border to separate items into groups. 117 | */ 118 | #menu .pure-menu ul, 119 | #menu .pure-menu .menu-item-divided { 120 | border-top: 1px solid #333; 121 | } 122 | /* 123 | Change color of the anchor links on hover/focus. 124 | */ 125 | #menu .pure-menu li a:hover, 126 | #menu .pure-menu li a:focus { 127 | background: #333; 128 | } 129 | 130 | /* 131 | This styles the selected menu item `
  • `. 132 | */ 133 | #menu .pure-menu-selected, 134 | #menu .pure-menu-heading { 135 | background: #1f8dd6; 136 | } 137 | /* 138 | This styles a link within a selected menu item `
  • `. 139 | */ 140 | #menu .pure-menu-selected a { 141 | color: #fff; 142 | } 143 | 144 | /* 145 | This styles the menu heading. 146 | */ 147 | #menu .pure-menu-heading { 148 | font-size: 110%; 149 | color: #fff; 150 | margin: 0; 151 | } 152 | 153 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/ 154 | 155 | /* 156 | The button to open/close the Menu is custom-made and not part of Pure. Here's 157 | how it works: 158 | */ 159 | 160 | /* 161 | `.menu-link` represents the responsive menu toggle that shows/hides on 162 | small screens. 163 | */ 164 | .menu-link { 165 | position: fixed; 166 | display: block; /* show this only on small screens */ 167 | top: 0; 168 | left: 0; /* "#menu width" */ 169 | background: #000; 170 | background: rgba(0,0,0,0.7); 171 | font-size: 10px; /* change this value to increase/decrease button size */ 172 | z-index: 10; 173 | width: 2em; 174 | height: auto; 175 | padding: 2.1em 1.6em; 176 | } 177 | 178 | .menu-link:hover, 179 | .menu-link:focus { 180 | background: #000; 181 | } 182 | 183 | .menu-link span { 184 | position: relative; 185 | display: block; 186 | } 187 | 188 | .menu-link span, 189 | .menu-link span:before, 190 | .menu-link span:after { 191 | background-color: #fff; 192 | width: 100%; 193 | height: 0.2em; 194 | } 195 | 196 | .menu-link span:before, 197 | .menu-link span:after { 198 | position: absolute; 199 | margin-top: -0.6em; 200 | content: " "; 201 | } 202 | 203 | .menu-link span:after { 204 | margin-top: 0.6em; 205 | } 206 | 207 | 208 | /* -- Responsive Styles (Media Queries) ------------------------------------- */ 209 | 210 | /* 211 | Hides the menu at `48em`, but modify this based on your app's needs. 212 | */ 213 | @media (min-width: 48em) { 214 | 215 | .header, 216 | .content { 217 | padding-left: 2em; 218 | padding-right: 2em; 219 | } 220 | 221 | #layout { 222 | padding-left: var(--slide); /* left col width "#menu" */ 223 | left: 0; 224 | } 225 | #menu { 226 | left: var(--slide); 227 | } 228 | 229 | .menu-link { 230 | position: fixed; 231 | left: var(--slide); 232 | display: none; 233 | } 234 | 235 | #layout.active .menu-link { 236 | left: var(--slide); 237 | } 238 | } 239 | 240 | @media (max-width: 48em) { 241 | /* Only apply this when the window is small. Otherwise, the following 242 | case results in extra padding on the left: 243 | * Make the window small. 244 | * Tap the menu to trigger the active state. 245 | * Make the window large again. 246 | */ 247 | #layout.active { 248 | position: relative; 249 | left: var(--slide); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /resources/html/css/style_dark.css: -------------------------------------------------------------------------------- 1 | label {white-space: nowrap;} 2 | .left-block{margin-top: 25px; padding-top: 25px; border-top: 1px solid #777; font-weight: bold;} 3 | .left-block div {display: flex; padding: 5px 10px 5px 5px; justify-content: space-between;} 4 | .menu-value {color: #779;} 5 | .checkbox-label{margin-left: 3em; position: relative; top: -2px;} 6 | .switch-block {margin: 20px 0px 15px 0px;} 7 | .header-block {color: #ddd;} 8 | .nested {padding: 0px 10px 0px 10px; border: 1px solid #ddd;} 9 | .mr {margin: 5px 0px 5px 0px !important;} 10 | .spacer {text-align: center; font-weight: 300; color: #ddd; padding-bottom: 10px; margin: 15px 0px 15px 0px; border-bottom: 1px solid #ddd;} 11 | .comment {margin: 1em 0;} 12 | .frame {width: 100%; height: 480px; min-height: 50px; resize: vertical;} 13 | .iframe {resize: vertical; width: 100%; height: 100%; frameborder: 0; scrolling:"yes";} 14 | body { 15 | background-image: url('wp_dark.svg'); 16 | background-color: #454545; 17 | background-position: center; 18 | background-repeat: no-repeat; 19 | background-size: cover; 20 | position: relative; 21 | } 22 | .switch { 23 | position: relative; 24 | display: inline-block; 25 | width: 40px; 26 | height: 12px; 27 | background-color: transparent; 28 | border-radius: 12px; 29 | transition: all 0.3s; 30 | box-shadow: inset 5px 9px 9px #777; 31 | border: 1px solid #ccccccd9; 32 | } 33 | 34 | .switch::after { 35 | content: ''; 36 | position: absolute; 37 | width: 15px; 38 | height: 30px; 39 | border-radius: 30px; 40 | background-color: #AAA; 41 | border: 1px solid #ccccccd9; 42 | box-shadow: 5px 9px 9px #777; 43 | top: -7px; 44 | left: 2px; 45 | transition: all 0.3s; 46 | } 47 | 48 | .checkbox:checked + .switch::after { 49 | left : 20px; 50 | background-color: #1f8dd6; 51 | } 52 | .checkbox:checked + .switch { 53 | background-color: transparent; 54 | } 55 | .checkbox { 56 | display : none; 57 | } 58 | 59 | /* Button shade style */ 60 | button{ 61 | position:relative; 62 | transition:500ms ease all; 63 | outline:none; 64 | } 65 | button:hover{ 66 | background:#eee!important; 67 | color:rgb(0, 120, 230)!important; 68 | box-shadow: 3px 3px 5px 2px #97B1BF; 69 | } 70 | button:focus{ 71 | opacity: 0.7; 72 | box-shadow: unset; 73 | background: rgb(0, 140, 250); 74 | color:rgb(50, 50, 50)!important; 75 | } 76 | button:before,button:after{ 77 | content:''; 78 | position:absolute; 79 | top:0; 80 | right:0; 81 | height:2px; 82 | width:0; 83 | background: rgb(0, 120, 230); 84 | } 85 | button:after{ 86 | right:inherit; 87 | top:inherit; 88 | left:0; 89 | bottom:0; 90 | } 91 | button:hover:before,button:hover:after{ 92 | width:100%; 93 | transition:1000ms ease all; 94 | } 95 | 96 | /* Sensor displays 97 | style inspired by https://github.com/viperet/esp8266-power-monitor */ 98 | .display { 99 | text-align: center; 100 | background: #000; 101 | border-radius: 10px; 102 | border: 1px solid rgb(100, 100, 255); 103 | box-shadow: 0px 0px 2px 6px rgba(0,0,0,0.5); 104 | margin: 15px; 105 | display: flex; 106 | align-items: center; 107 | justify-content: center; 108 | flex-wrap: wrap; 109 | padding: 10px; 110 | } 111 | .display > div { 112 | display: inline-block; 113 | margin: 0 auto; 114 | padding: 0.15em 0.1em 0 0.1em; 115 | color: #FFF; 116 | line-height: 1em; 117 | text-align: center; 118 | font-family: 'Courier Prime'; 119 | font-size: 45px; 120 | background: linear-gradient(0deg, rgba(220,220,255,1) 25%, rgba(180,80,80,1) 50%, rgba(220,220,255,1) 75%); 121 | -webkit-background-clip: text; 122 | background-clip: text; 123 | -webkit-text-fill-color: transparent; 124 | } 125 | 126 | .display > div::before { 127 | font-family: sans-serif; 128 | font-size: 0.5em; 129 | padding: 0 1em 0 0.5em; 130 | -webkit-text-fill-color: rgb(220, 223, 93); 131 | } 132 | 133 | .display > div::after { 134 | font-family: sans-serif; 135 | font-size: 0.5em; 136 | height: 3vh; 137 | padding: 0 0 0 0.5em; 138 | -webkit-text-fill-color: rgb(192, 178, 178); 139 | } 140 | 141 | /* 142 | example of additional styles for display divs 143 | #vcc:before { 144 | content: 'Vcc:'; 145 | } 146 | #vcc:after { 147 | content: 'V'; 148 | } 149 | */ -------------------------------------------------------------------------------- /resources/html/css/style_default.css: -------------------------------------------------------------------------------- 1 | label {white-space: nowrap;} 2 | .left-block{margin-top: 25px; padding-top: 25px; border-top: 1px solid #777; font-weight: bold;} 3 | .left-block div {display: flex; padding: 5px 10px 5px 5px; justify-content: space-between;} 4 | .menu-value {color: #fff;} 5 | .checkbox-label{margin-left: 2em; position: relative; top: -2px;} 6 | .switch-block {margin: 10px 0px 10px 0px;} 7 | .header-block {color: #777;} 8 | .nested {padding: 0px 10px 0px 10px; border: 1px solid #777;} 9 | .mr {margin: 5px 0px 5px 0px !important;} 10 | .spacer {text-align: center; font-weight: 300; color: #777; padding-bottom: 10px; margin: 15px 0px 15px 0px; border-bottom: 1px solid #777;} 11 | .comment {margin: 1em 0;} 12 | .frame {width: 100%; height: 480px; min-height: 50px; resize: vertical;} 13 | .iframe {resize: vertical; width: 100%; height: 100%; frameborder: 0; scrolling:"yes";} 14 | 15 | .switch { 16 | position: relative; 17 | display: inline-block; 18 | width: 30px; 19 | height: 16px; 20 | background-color: rgba(0, 0, 0, 0.25); 21 | border-radius: 20px; 22 | transition: all 0.3s; 23 | } 24 | .switch::after { 25 | content: ''; 26 | position: absolute; 27 | width: 14px; 28 | height: 14px; 29 | border-radius:50%; 30 | background-color: white; 31 | top: 1px; 32 | left: 1px; 33 | transition: all 0.3s; 34 | } 35 | 36 | .checkbox:checked + .switch::after { 37 | left : 14px; 38 | } 39 | .checkbox:checked + .switch { 40 | background-color: #7983ff; 41 | } 42 | .checkbox { 43 | display : none; 44 | } 45 | 46 | /* Button shade style */ 47 | button{ 48 | position:relative; 49 | transition:500ms ease all; 50 | outline:none; 51 | } 52 | button:hover{ 53 | background:#eee!important; 54 | color:rgb(0, 120, 230)!important; 55 | box-shadow: 3px 3px 5px 2px #97B1BF; 56 | } 57 | button:focus{ 58 | opacity: 0.7; 59 | box-shadow: unset; 60 | background: rgb(0, 140, 250); 61 | color:rgb(50, 50, 50)!important; 62 | } 63 | button:before,button:after{ 64 | content:''; 65 | position:absolute; 66 | top:0; 67 | right:0; 68 | height:2px; 69 | width:0; 70 | background: rgb(0, 120, 230); 71 | } 72 | button:after{ 73 | right:inherit; 74 | top:inherit; 75 | left:0; 76 | bottom:0; 77 | } 78 | button:hover:before,button:hover:after{ 79 | width:100%; 80 | transition:1000ms ease all; 81 | } 82 | 83 | .pure-form input[type=color], 84 | .pure-form input[type=date], 85 | .pure-form input[type=datetime-local], 86 | .pure-form input[type=datetime], 87 | .pure-form input[type=email], 88 | .pure-form input[type=month], 89 | .pure-form input[type=number], 90 | .pure-form input[type=password], 91 | .pure-form input[type=search], 92 | .pure-form input[type=tel], 93 | .pure-form input[type=text], 94 | .pure-form input[type=time], 95 | .pure-form input[type=url], 96 | .pure-form input[type=week], 97 | .pure-form select, 98 | .pure-form textarea{ 99 | padding: 0% 0.5em; 100 | display: inline-block; 101 | border: 1px #777; 102 | border-radius: 10px; 103 | box-shadow: inset 3px 0px 15px #aaa; 104 | vertical-align: middle; 105 | box-sizing: border-box; 106 | background-color: #d6e2f4; 107 | } 108 | 109 | .pure-form input[type=number]{ 110 | margin: 0 0.5em; 111 | width: 25%; 112 | } 113 | 114 | .pure-form select{ 115 | height: 1.5em; 116 | } 117 | 118 | /* Sensor displays 119 | style inspired by https://github.com/viperet/esp8266-power-monitor */ 120 | .display { 121 | text-align: center; 122 | background: #000; 123 | border-radius: 10px; 124 | border: 1px solid rgb(100, 100, 255); 125 | box-shadow: 0px 0px 2px 6px rgba(0,0,0,0.5); 126 | margin: 15px; 127 | display: flex; 128 | align-items: center; 129 | justify-content: center; 130 | flex-wrap: wrap; 131 | padding: 10px; 132 | } 133 | .display > div { 134 | display: inline-block; 135 | margin: 0 auto; 136 | padding: 0.15em 0.1em 0 0.1em; 137 | color: #FFF; 138 | line-height: 1em; 139 | text-align: center; 140 | font-family: 'Courier Prime'; 141 | font-size: 45px; 142 | background: linear-gradient(0deg, rgba(220,220,255,1) 25%, rgba(180,80,80,1) 50%, rgba(220,220,255,1) 75%); 143 | -webkit-background-clip: text; 144 | background-clip: text; 145 | -webkit-text-fill-color: transparent; 146 | } 147 | 148 | .display > div::before { 149 | font-family: sans-serif; 150 | font-size: 0.5em; 151 | padding: 0 1em 0 0.5em; 152 | -webkit-text-fill-color: rgb(220, 223, 93); 153 | } 154 | 155 | .display > div::after { 156 | font-family: sans-serif; 157 | font-size: 0.5em; 158 | height: 3vh; 159 | padding: 0 0 0 0.5em; 160 | -webkit-text-fill-color: rgb(192, 178, 178); 161 | } 162 | 163 | /* 164 | example of additional styles for display divs 165 | #vcc:before { 166 | content: 'Vcc:'; 167 | } 168 | #vcc:after { 169 | content: 'V'; 170 | } 171 | */ -------------------------------------------------------------------------------- /resources/html/css/style_light.css: -------------------------------------------------------------------------------- 1 | label {white-space: nowrap;} 2 | .left-block{margin-top: 25px; padding-top: 25px; border-top: 1px solid #777; font-weight: bold;} 3 | .left-block div {display: flex; padding: 5px 10px 5px 5px; justify-content: space-between;} 4 | .menu-value {color: #777;} 5 | .checkbox-label{margin-left: 3em; position: relative; top: -2px;} 6 | .switch-block {margin: 20px 0px 15px 0px;} 7 | .header-block {color: #777;} 8 | .nested {padding: 0px 10px 0px 10px; border: 1px solid #777;} 9 | .mr {margin: 5px 0px 5px 0px !important;} 10 | .spacer {text-align: center; font-weight: 300; color: #777; padding-bottom: 10px; margin: 15px 0px 15px 0px; border-bottom: 1px solid #777;} 11 | .comment {margin: 1em 0;} 12 | .frame {width: 100%; height: 480px; min-height: 50px; resize: vertical;} 13 | .iframe {resize: vertical; width: 100%; height: 100%; frameborder: 0; scrolling:"yes";} 14 | body { 15 | background-image: url('wp_light.svg'); 16 | background-repeat: no-repeat; 17 | background-attachment: fixed; 18 | background-size: cover; 19 | } 20 | .switch { 21 | position: relative; 22 | display: inline-block; 23 | width: 40px; 24 | height: 12px; 25 | background-color: transparent; 26 | border-radius: 12px; 27 | transition: all 0.3s; 28 | box-shadow: inset 5px 9px 9px #777; 29 | } 30 | 31 | .switch::after { 32 | content: ''; 33 | position: absolute; 34 | width: 15px; 35 | height: 30px; 36 | border-radius: 30px; 37 | background-color: #AAA; 38 | border: 1px solid #777; 39 | box-shadow: 5px 9px 9px #777; 40 | top: -7px; 41 | left: 2px; 42 | transition: all 0.3s; 43 | } 44 | 45 | .checkbox:checked + .switch::after { 46 | left : 20px; 47 | background-color: #1f8dd6; 48 | } 49 | .checkbox:checked + .switch { 50 | background-color: transparent; 51 | } 52 | .checkbox { 53 | display : none; 54 | } 55 | 56 | /* Button shade style */ 57 | button{ 58 | position:relative; 59 | transition:500ms ease all; 60 | outline:none; 61 | } 62 | button:hover{ 63 | background:#eee!important; 64 | color:rgb(0, 120, 230)!important; 65 | box-shadow: 3px 3px 5px 2px #97B1BF; 66 | } 67 | button:focus{ 68 | opacity: 0.7; 69 | box-shadow: unset; 70 | background: rgb(0, 140, 250); 71 | color:rgb(50, 50, 50)!important; 72 | } 73 | button:before,button:after{ 74 | content:''; 75 | position:absolute; 76 | top:0; 77 | right:0; 78 | height:2px; 79 | width:0; 80 | background: rgb(0, 120, 230); 81 | } 82 | button:after{ 83 | right:inherit; 84 | top:inherit; 85 | left:0; 86 | bottom:0; 87 | } 88 | button:hover:before,button:hover:after{ 89 | width:100%; 90 | transition:1000ms ease all; 91 | } 92 | 93 | /* Sensor displays 94 | style inspired by https://github.com/viperet/esp8266-power-monitor */ 95 | .display { 96 | text-align: center; 97 | background: #000; 98 | border-radius: 10px; 99 | border: 1px solid rgb(100, 100, 255); 100 | box-shadow: 0px 0px 2px 6px rgba(0,0,0,0.5); 101 | margin: 15px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | flex-wrap: wrap; 106 | padding: 10px; 107 | } 108 | .display > div { 109 | display: inline-block; 110 | margin: 0 auto; 111 | padding: 0.15em 0.1em 0 0.1em; 112 | color: #FFF; 113 | line-height: 1em; 114 | text-align: center; 115 | font-family: 'Courier Prime'; 116 | font-size: 45px; 117 | background: linear-gradient(0deg, rgba(220,220,255,1) 25%, rgba(180,80,80,1) 50%, rgba(220,220,255,1) 75%); 118 | -webkit-background-clip: text; 119 | background-clip: text; 120 | -webkit-text-fill-color: transparent; 121 | } 122 | 123 | .display > div::before { 124 | font-family: sans-serif; 125 | font-size: 0.5em; 126 | padding: 0 1em 0 0.5em; 127 | -webkit-text-fill-color: rgb(220, 223, 93); 128 | } 129 | 130 | .display > div::after { 131 | font-family: sans-serif; 132 | font-size: 0.5em; 133 | height: 3vh; 134 | padding: 0 0 0 0.5em; 135 | -webkit-text-fill-color: rgb(192, 178, 178); 136 | } 137 | 138 | /* 139 | example of additional styles for display divs 140 | #vcc:before { 141 | content: 'Vcc:'; 142 | } 143 | #vcc:after { 144 | content: 'V'; 145 | } 146 | */ -------------------------------------------------------------------------------- /resources/html/css/wp_dark.svg.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/resources/html/css/wp_dark.svg.gz -------------------------------------------------------------------------------- /resources/html/css/wp_light.svg.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/resources/html/css/wp_light.svg.gz -------------------------------------------------------------------------------- /resources/html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vortigont/EmbUI/a739882473ba95954311bfcc65127c0ecf25af6b/resources/html/favicon.ico -------------------------------------------------------------------------------- /resources/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | EmbUI 9 | 10 | 11 | 12 | 13 |
    14 | 15 | 16 | 17 | 20 | 21 |
    22 |
    23 |
    24 | 25 |
    26 | 27 | 54 | 55 | 67 | 68 | 79 | 80 | 199 | 200 | 201 | 202 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /resources/html/js/ui_embui.lang.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "ru", 4 | "label": "Russian" 5 | }, 6 | { 7 | "value": "en", 8 | "label": "English" 9 | } 10 | ] -------------------------------------------------------------------------------- /resources/html/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2018" 5 | }, 6 | } -------------------------------------------------------------------------------- /resources/html/readme.txt: -------------------------------------------------------------------------------- 1 | Resources for Pure.CSS based UI https://purecss.io/ 2 | pure-min.css - Responsive Rollup https://unpkg.com/browse/purecss@3.0.0/build/pure-min.css 3 | grids-responsive-min.css https://unpkg.com/browse/purecss@3.0.0/build/grids-responsive-min.css 4 | side-menu* - Left side menu https://purecss.io/layouts/side-menu/#menu 5 | style-* EmbUI overrides -------------------------------------------------------------------------------- /resources/lodash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #npm i -g npm 4 | #npm i -g lodash-cli 5 | 6 | # make custom build of lodash js lib 7 | #lodash include=at,get,find,findIndex,has,hasIn,merge,mergeWith,set,unset,setWith 8 | lodash include=at,get,find,findIndex,has,merge,mergeWith,set,unset 9 | zopfli lodash.custom.min.js 10 | mv -f lodash.custom.min.js.gz ../data/js/lodash.custom.js.gz 11 | rm -f *.js *.gz 12 | # -c | gzip -9 > html/js/lodash.custom.js.gz 13 | #lodash category=array,object -c | gzip -9 > lodash.custom.js.gz -------------------------------------------------------------------------------- /resources/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lodash", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /resources/respack.cmd: -------------------------------------------------------------------------------- 1 | setlocal 2 | set workdir=%~dp0 3 | PATH=%PATH%;%workdir%;%ProgramFiles%\Git 4 | cls 5 | echo off 6 | 7 | "%ProgramFiles%\Git\"git-bash.exe %workdir%\respack.sh 8 | rem exit 9 | 10 | -------------------------------------------------------------------------------- /resources/respack.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | # etag file 5 | tags=etags.txt 6 | compressor="gz" 7 | compress_cmd="zopfli" 8 | compress_args="" 9 | dst="../data" 10 | 11 | refresh_rq=0 12 | tzcsv=https:/raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv 13 | 14 | optstring=":hf:c:" 15 | 16 | usage(){ 17 | echo "Usage: `basename $0` [-h] [-f] [-c zopfli|gz|br]" 18 | cat <&2 45 | ;; 46 | \?) 47 | # getopts issues an error message 48 | echo $USAGE >&2 49 | exit 1 50 | ;; 51 | esac 52 | done 53 | 54 | compress_zopfli(){ 55 | local src="$1" 56 | zopfli ${compress_args} ${src} 57 | rm -f ${src} 58 | } 59 | 60 | compress_gz(){ 61 | local src="$1" 62 | gzip ${compress_args} ${src} 63 | } 64 | 65 | compress_br(){ 66 | local src="$1" 67 | brotli ${compress_args} ${src} 68 | } 69 | 70 | 71 | if [[ "$compress_cmd" = "gz" ]] ; then 72 | compress_cmd=`which gzip` 73 | if [ "x$compress_cmd" = "x" ]; then 74 | echo "ERROR: gzip compressor not found!" 75 | exit 1 76 | fi 77 | compress_cmd=compress_gz 78 | compress_args="-9" 79 | elif [[ "$compress_cmd" = "zopfli" ]] ; then 80 | compress_cmd=`which zopfli` 81 | if [ "x$compress_cmd" = "x" ]; then 82 | echo "ERROR: zopfli compressor not found!" 83 | exit 1 84 | fi 85 | compress_cmd=compress_zopfli 86 | elif [[ "$compress_cmd" = "br" ]] ; then 87 | compress_cmd=`which brotli` 88 | if [ "x$compress_cmd" = "x" ]; then 89 | echo "ERROR: brotli compressor not found!" 90 | exit 1 91 | fi 92 | compress_cmd=compress_br 93 | compress_args="--best" 94 | fi 95 | echo "Using compressor: $compress_cmd" 96 | 97 | [ -f $tags ] || touch $tags 98 | 99 | # check github file for a new hash 100 | freshtag(){ 101 | local url="$1" 102 | etag=$(curl -sL -I $url | grep etag | awk '{print $2}') 103 | if [[ "$etag" = "" ]] ; then 104 | return 0 105 | fi 106 | echo "$url $etag" >> newetags.txt 107 | if [ $(grep -cs $etag $tags) -eq 0 ] ; then 108 | #echo "new tag found for $url" 109 | return 0 110 | fi 111 | #echo "old tag for $url" 112 | return 1 113 | } 114 | 115 | # update local file if source has newer version 116 | updlocalarchive(){ 117 | local res=$1 118 | echo "check: $res" 119 | [ ! -f html/${res} ] && return 120 | if [ ! -f ${dst}/${res}.${compressor} ] || [ html/${res} -nt ${dst}/${res}.${compressor} ] ; then 121 | cp html/${res} ${dst}/${res} 122 | ${compress_cmd} ${dst}/${res} && touch -r html/${res} ${dst}/${res}.${compressor} 123 | fi 124 | } 125 | 126 | echo "Preparing resources for EmbUI FS image" 127 | 128 | mkdir -p ${dst}/css ${dst}/js 129 | cat html/css/pure*.css html/css/grids*.css > ${dst}/css/pure.css 130 | ${compress_cmd} ${dst}/css/pure.css 131 | cat html/css/*_default.css > ${dst}/css/style.css 132 | ${compress_cmd} ${dst}/css/style.css 133 | cat html/css/*_light.css > ${dst}/css/style_light.css 134 | ${compress_cmd} ${dst}/css/style_light.css 135 | cat html/css/*_dark.css > ${dst}/css/style_dark.css 136 | ${compress_cmd} ${dst}/css/style_dark.css 137 | 138 | cp -u html/css/*.jpg ${dst}/css/ 139 | cp -u html/css/*.webp ${dst}/css/ 140 | cp -u html/css/*.svg* ${dst}/css/ 141 | 142 | echo "Packing EmbUI js" 143 | embui_js="embui.js" 144 | # combine and compress js files in one bundle 145 | for f in ${embui_js} 146 | do 147 | cat html/js/${f} >> ${dst}/js/embui.js 148 | done 149 | ${compress_cmd} ${dst}/js/embui.js 150 | 151 | cp -u html/js/lodash.custom.js* ${dst}/js/ 152 | 153 | echo "Packing static files" 154 | # static gz files 155 | static_gz_files='js/ui_embui.json js/ui_embui.i18n.json js/ui_embui.lang.json index.html favicon.ico' 156 | for f in ${static_gz_files} 157 | do 158 | updlocalarchive $f 159 | done 160 | 161 | echo "Update TZ" 162 | # update TZ info 163 | if freshtag ${tzcsv} || [ $refresh_rq -eq 1 ] ; then 164 | echo "Updating TZ info" 165 | echo '"label","value"' > ${dst}/js/zones.csv 166 | curl -sL $tzcsv >> ${dst}/js/zones.csv 167 | python tzgen.py 168 | ${compress_cmd} -f ${dst}/js/tz.json 169 | rm -f ${dst}/js/tz.json ${dst}/js/zones.csv 170 | fi 171 | 172 | 173 | 174 | mv -f newetags.txt $tags 175 | -------------------------------------------------------------------------------- /resources/tzgen.py: -------------------------------------------------------------------------------- 1 | # based on from https://pythonexamples.org/python-csv-to-json/ 2 | import csv 3 | import json 4 | 5 | def csv_to_json(csvFilePath, jsonFilePath): 6 | jsonArray = [] 7 | 8 | #read csv file 9 | with open(csvFilePath, encoding='utf-8') as csvf: 10 | #load csv file data using csv library's dictionary reader 11 | csvReader = csv.DictReader(csvf) 12 | 13 | i = 0 14 | #convert each csv row into python dict 15 | for row in csvReader: 16 | # reformat values - pad it with index, so that we could use it as a key for dropdown list 17 | row["value"]=f'{i:03}_{row["value"]}' 18 | jsonArray.append(row) 19 | i += 1 20 | 21 | #convert python jsonArray to JSON String and write to file 22 | with open(jsonFilePath, 'w', encoding='utf-8') as jsonf: 23 | jsonString = json.dumps(jsonArray, indent=2) 24 | jsonf.write(jsonString) 25 | 26 | csvFilePath = r'../data/js/zones.csv' 27 | jsonFilePath = r'../data/js/tz.json' 28 | csv_to_json(csvFilePath, jsonFilePath) 29 | -------------------------------------------------------------------------------- /src/basicui.h: -------------------------------------------------------------------------------- 1 | /* 2 | Here is a set of predefined WebUI elements for system settings setup like WiFi, time, MQTT, etc... 3 | */ 4 | #pragma once 5 | 6 | #include "ui.h" 7 | 8 | /** 9 | * List of UI languages in predefined i18n resources 10 | */ 11 | enum LANG : uint8_t { 12 | EN = (0U), 13 | RU = (1U), 14 | }; 15 | 16 | extern uint8_t lang; 17 | 18 | /* 19 | A namespace with functions to handle basic EmbUI WebUI interface 20 | */ 21 | namespace basicui { 22 | 23 | // numeric indexes for pages 24 | enum class page : uint16_t { 25 | main = 0, 26 | settings, 27 | network, 28 | datetime, 29 | mqtt, 30 | ftp, 31 | syssetup 32 | }; 33 | 34 | /** 35 | * register handlers for system actions and setup pages 36 | * 37 | */ 38 | void register_handlers(); 39 | 40 | /** 41 | * This code adds "Settings" section to the MENU 42 | * it is up to you to properly open/close Interface json_section 43 | */ 44 | void menuitem_settings(Interface *interf); 45 | 46 | void show_uipage(Interface *interf, JsonObjectConst data, const char* action = NULL); 47 | void page_settings_gnrl(Interface *interf, JsonObjectConst data, const char* action = NULL); 48 | void page_settings_netw(Interface *interf, JsonObjectConst data, const char* action = NULL); 49 | void page_settings_mqtt(Interface *interf, JsonObjectConst data, const char* action = NULL); 50 | void page_settings_time(Interface *interf, JsonObjectConst data, const char* action = NULL); 51 | void page_settings_sys(Interface *interf, JsonObjectConst data, const char* action = NULL); 52 | 53 | /** 54 | * @brief Build WebUI "Settings" page 55 | * it will create system settings page and call action for user callback to append user block to the settings page 56 | * 57 | * 58 | * @param interf 59 | * @param data 60 | * @param action 61 | */ 62 | void page_system_settings(Interface *interf, JsonObjectConst data, const char* action = NULL); 63 | void set_settings_wifi(Interface *interf, JsonObjectConst data, const char* action = NULL); 64 | void set_settings_wifiAP(Interface *interf, JsonObjectConst data, const char* action = NULL); 65 | void set_settings_mqtt(Interface *interf, JsonObjectConst data, const char* action = NULL); 66 | void set_settings_time(Interface *interf, JsonObjectConst data, const char* action = NULL); 67 | void set_language(Interface *interf, JsonObjectConst data, const char* action = NULL); 68 | void embuistatus(Interface *interf); 69 | 70 | /** 71 | * @brief publish to webui live system info 72 | * i.e. free ram, uptime, etc... 73 | */ 74 | void embuistatus(); 75 | void set_sys_reboot(Interface *interf, JsonObjectConst data, const char* action = NULL); 76 | void set_sys_hostname(Interface *interf, JsonObjectConst data, const char* action = NULL); 77 | void set_sys_datetime(Interface *interf, JsonObjectConst data, const char* action = NULL); 78 | void set_sys_cfgclear(Interface *interf, JsonObjectConst data, const char* action = NULL); 79 | 80 | /** 81 | * @brief default main_page with a simple "settings" menu entry 82 | * 83 | */ 84 | void page_main(Interface *interf, JsonObjectConst data, const char* action = NULL); 85 | } // end of "namespace basicui" -------------------------------------------------------------------------------- /src/embui_constants.h: -------------------------------------------------------------------------------- 1 | // This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 2 | // then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 3 | // also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 4 | // and others people 5 | 6 | #pragma once 7 | #include "i18n.h" // localized GUI text-strings 8 | 9 | // Empty string 10 | #define P_EMPTY static_cast(0) 11 | 12 | // UI blocks generator actions (IDs) 13 | static constexpr const char* A_ui_page = "ui_page"; 14 | static constexpr const char* A_ui_page_settings = "ui_page_settings"; 15 | static constexpr const char* A_ui_page_network = "ui_page_network"; 16 | static constexpr const char* A_ui_page_main = "ui_page_main"; 17 | static constexpr const char* A_ui_blk_usersettings = "ui_blk_usersettings"; 18 | 19 | // setter actions (IDs) 20 | static constexpr const char* A_sys_ntwrk_ftp = "sys_ntwrk_ftp"; 21 | static constexpr const char* A_sys_ntwrk_wifi = "sys_ntwrk_wifi"; 22 | static constexpr const char* A_sys_ntwrk_wifiap = "sys_ntwrk_wifiap"; 23 | static constexpr const char* A_sys_ntwrk_mqtt = "sys_ntwrk_mqtt"; 24 | static constexpr const char* A_sys_timeoptions = "sys_timeoptions"; 25 | static constexpr const char* A_sys_hostname = "sys_hostname"; 26 | static constexpr const char* A_sys_datetime = "sys_datetime"; 27 | static constexpr const char* A_sys_language = "sys_language"; 28 | 29 | // bare setter actions (IDs) 30 | static constexpr const char* A_sys_cfgclr = "sys_cfgclr"; 31 | static constexpr const char* A_sys_reboot = "sys_reboot"; 32 | 33 | // GET actions than only query for data 34 | 35 | // other Action ID's 36 | static constexpr const char* A_publish = "publish"; 37 | 38 | // misc 39 | static constexpr const char* T_DO_OTAUPD = "update"; // http OTA update URL /update 40 | 41 | 42 | // Interface class elements 43 | static constexpr const char* P_action = "action"; 44 | static constexpr const char* P_app = "app"; 45 | static constexpr const char* P_appjsapi = "appjsapi"; 46 | static constexpr const char* P_apssid = "apssid"; 47 | static constexpr const char* P_block = "block"; 48 | static constexpr const char* P_button = "button"; 49 | static constexpr const char* P_callback = "callback"; 50 | static constexpr const char* P_color = "color"; 51 | static constexpr const char* P_content = "content"; 52 | static constexpr const char* P_chckbox = "checkbox"; 53 | static constexpr const char* P_class = "class"; 54 | static constexpr const char* P_comment = "comment"; 55 | static constexpr const char* P_const = "const"; 56 | static constexpr const char* P_data = "data"; 57 | static constexpr const char* P_date = "date"; 58 | static constexpr const char* P_datetime = "datetime-local"; 59 | static constexpr const char* P_display = "display"; 60 | static constexpr const char* P_div = "div"; 61 | static constexpr const char* P_dtime = "dtime"; 62 | static constexpr const char* P_email = "email"; 63 | static constexpr const char* P_empty_quotes = ""; 64 | static constexpr const char* P_file = "file"; 65 | static constexpr const char* P_final = "final"; 66 | static constexpr const char* P_form = "form"; 67 | static constexpr const char* P_frame = "frame"; 68 | static constexpr const char* P_ftp = "ftp"; 69 | static constexpr const char* P_ftp_usr = "ftp_usr"; 70 | static constexpr const char* P_ftp_pwd = "ftp_pwd"; 71 | static constexpr const char* P_function = "function"; 72 | static constexpr const char* P_hidden = "hidden"; 73 | static constexpr const char* P_hostname_const = "hostname_const"; 74 | static constexpr const char* P_html = "html"; 75 | static constexpr const char* P_id = "id"; 76 | static constexpr const char* P_idx = "idx"; 77 | static constexpr const char* P_iframe = "iframe"; 78 | static constexpr const char* P_js = "js"; 79 | static constexpr const char* P_jscall = "jscall"; 80 | static constexpr const char* P_jsfunc = "jsfunc"; 81 | static constexpr const char* P_input = "input"; 82 | static constexpr const char* P_interface = "interface"; 83 | static constexpr const char* P_key = "key"; 84 | static constexpr const char* P_label = "label"; 85 | static constexpr const char* P_lang = "lang"; 86 | static constexpr const char* P_line = "line"; 87 | static constexpr const char* P_MQTT = "MQTT"; 88 | static constexpr const char* P_MQTTTopic = "MQTTTopic"; 89 | static constexpr const char* P_manifest = "manifest"; 90 | static constexpr const char* P_main = "main"; 91 | static constexpr const char* P_max = "max"; 92 | static constexpr const char* P_menu = "menu"; 93 | static constexpr const char* P_merge = "merge"; 94 | static constexpr const char* P_min = "min"; 95 | static constexpr const char* P_number = "number"; 96 | static constexpr const char* P_ntp = "ntp"; 97 | static constexpr const char* P_ntp_servers = "ntp_servers"; // section name with list of NTPs 98 | static constexpr const char* P_onChange = "onChange"; 99 | static constexpr const char* P_opt = "opt"; 100 | static constexpr const char* P_options = "options"; 101 | static constexpr const char* P_params = "params"; 102 | static constexpr const char* P_password = "password"; 103 | static constexpr const char* P_pick = "pick"; 104 | static constexpr const char* P_pkg = "pkg"; 105 | static constexpr const char* P_progressbar = "pbar"; 106 | static constexpr const char* P_pMem = "pMem"; 107 | static constexpr const char* P_prefix = "prefix"; 108 | static constexpr const char* P_pRSSI = "pRSSI"; 109 | static constexpr const char* P_pTime = "pTime"; 110 | static constexpr const char* P_pUptime = "pUptime"; 111 | static constexpr const char* P_range = "range"; 112 | static constexpr const char* P_replace = "replace"; 113 | static constexpr const char* P_section = "section"; 114 | static constexpr const char* P_select = "select"; 115 | static constexpr const char* P_set = "set"; 116 | static constexpr const char* P_src = "src"; 117 | static constexpr const char* P_spacer = "spacer"; 118 | static constexpr const char* P_step = "step"; 119 | static constexpr const char* P_submit = "submit"; 120 | static constexpr const char* P_suffix = "suffix"; 121 | static constexpr const char* P_sys = "sys"; 122 | static constexpr const char* P_tcp = "tcp"; 123 | static constexpr const char* P_text = "text"; 124 | static constexpr const char* P_textarea = "textarea"; 125 | static constexpr const char* P_time = "time"; 126 | static constexpr const char* P_type = "type"; 127 | static constexpr const char* P_uidata = "uidata"; 128 | static constexpr const char* P_uijsapi = "uijsapi"; 129 | static constexpr const char* P_url = "url"; 130 | static constexpr const char* P_uiver = "uiver"; 131 | static constexpr const char* P_value = "value"; 132 | static constexpr const char* P_version = "version"; 133 | static constexpr const char* P_wifi = "wifi"; 134 | static constexpr const char* P_xload ="xload"; 135 | static constexpr const char* P_xload_url ="xload_url"; 136 | static constexpr const char* P_xmerge ="xmerge"; 137 | 138 | 139 | // order of elements MUST match with 'enum class ui_element_t' in ui.h 140 | // increase index in case of new elements 141 | static constexpr std::array UI_T_DICT = { 142 | NULL, // custom 0 143 | P_button, 144 | P_chckbox, 145 | P_color, 146 | P_comment, 147 | P_const, 148 | P_date, 149 | P_datetime, 150 | P_display, 151 | P_div, 152 | P_email, // 10 153 | P_file, 154 | P_form, 155 | P_hidden, 156 | P_iframe, 157 | P_input, 158 | NULL, // 'option' (for selects) 159 | P_password, 160 | P_range, 161 | P_select, 162 | P_spacer, // 20 163 | P_text, 164 | P_textarea, 165 | P_time, 166 | P_value 167 | }; 168 | 169 | 170 | // order of elements MUST match with 'enum class ui_param_t' in ui.h 171 | static constexpr std::array UI_KEY_DICT { 172 | P_html, 173 | P_id, 174 | P_hidden, 175 | P_type, 176 | P_value, 177 | }; 178 | 179 | // UI colors 180 | static constexpr const char* P_RED = "red"; 181 | static constexpr const char* P_ORANGE = "orange"; 182 | static constexpr const char* P_YELLOW = "yellow"; 183 | static constexpr const char* P_GREEN = "green"; 184 | static constexpr const char* P_BLUE = "blue"; 185 | static constexpr const char* P_GRAY = "gray"; 186 | static constexpr const char* P_BLACK = "black"; 187 | static constexpr const char* P_WHITE = "white"; 188 | 189 | // System configuration variables and constants 190 | static constexpr const char* EMBUI_cfgfile = "/config.json"; 191 | static constexpr const char* EMBUI_JSON_i18N = "/js/ui_embui.i18n.json"; 192 | static constexpr const char* EMBUI_JSON_LANG_LIST = "/js/ui_embui.lang.json"; 193 | 194 | static constexpr const char* V_APonly = "APonly"; // AccessPoint-only mode 195 | static constexpr const char* V_APpwd = "APpwd"; // AccessPoint password 196 | static constexpr const char* V_timezone = "timezone"; // TimeZone rule variable 197 | static constexpr const char* V_NOCaptP = "ncapp"; // Captive Portal Disabled 198 | static constexpr const char* V_hostname = "hostname"; // System hostname 199 | static constexpr const char* V_LANGUAGE = "lang"; // UI language 200 | static constexpr const char* V_noNTPoDHCP = "ntpod"; // Disable NTP over DHCP 201 | static constexpr const char* V_userntp = "userntp"; // user-defined NTP server 202 | 203 | // WiFi vars 204 | static constexpr const char* V_WCSSID = "wcssid"; // WiFi-Client SSID 205 | static constexpr const char* V_WCPASS = "wcpass"; // WiFi-Client password 206 | 207 | // MQTT related vars, topic names, etc 208 | static constexpr const char* V_mqtt_enable = "mqtt_ena"; 209 | static constexpr const char* V_mqtt_host = "mqtt_host"; 210 | static constexpr const char* V_mqtt_pass = "mqtt_pass"; 211 | static constexpr const char* V_mqtt_port = "mqtt_port"; 212 | static constexpr const char* V_mqtt_topic = "mqtt_topic"; 213 | static constexpr const char* V_mqtt_user = "mqtt_user"; 214 | static constexpr const char* V_mqtt_ka = "mqtt_ka"; // mqtt keep-alive interval 215 | static constexpr const char* C_get = "get/"; // mqtt 'get/' prefix 216 | static constexpr const char* C_set = "set/"; // mqtt 'set/' prefix 217 | static constexpr const char* C_sys = "sys/"; // mqtt 'sys/' suffix 218 | static constexpr const char* C_post = "post"; // mqtt 'post' suffix 219 | static constexpr const char* C_pub = "pub/"; // mqtt 'pub/' suffix 220 | static constexpr const char* C_pub_etc = "pub/etc"; // mqtt 'pub/etc' suffix 221 | static constexpr const char* C_pub_iface = "pub/interface"; // mqtt 'pub/interface' suffix 222 | static constexpr const char* C_pub_post = "pub/post"; // mqtt 'pub/post' suffix 223 | static constexpr const char* C_pub_value = "pub/value"; // mqtt 'pub/value' suffix 224 | 225 | // http-related constants 226 | static constexpr const char* PGgzip = "gzip"; 227 | static constexpr const char* PGhdrcachec = "Cache-Control"; 228 | static constexpr const char* PGhdrcontentenc = "Content-Encoding"; 229 | static constexpr const char* PGmimecss = "text/css"; 230 | static constexpr const char* PGmimexml = "text/xml"; 231 | static constexpr const char* PGnocache = "no-cache, no-store, must-revalidate"; 232 | static constexpr const char* PG404 = "Not found"; 233 | static constexpr const char* PGimg = "img"; 234 | 235 | // LOG Messages 236 | static constexpr const char* P_EmbUI = "EmbUI"; 237 | static constexpr const char* P_EmbUI_WiFi = "EmbUI WiFi"; 238 | static constexpr const char* P_EmbUI_mqtt = "EmbUI MQTT"; 239 | -------------------------------------------------------------------------------- /src/embui_defines.h: -------------------------------------------------------------------------------- 1 | // This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 2 | // then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 3 | // also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 4 | // and others people 5 | 6 | #pragma once 7 | 8 | // Global macro's and framework libs 9 | 10 | // STRING Macro 11 | #ifndef __STRINGIFY 12 | #define __STRINGIFY(a) #a 13 | #endif 14 | #define TOSTRING(x) __STRINGIFY(x) 15 | 16 | #define EMBUI_VERSION_MAJOR 4 17 | #define EMBUI_VERSION_MINOR 2 18 | #define EMBUI_VERSION_REVISION 2 19 | 20 | // API version for JS frontend 21 | #define EMBUI_JSAPI 8 22 | // loadable UI blocks version requirement (loaded from js/ui_sys.json) 23 | #define EMBUI_UIOBJECTS 6 24 | 25 | #define EMBUI_VERSION_VALUE (MAJ, MIN, REV) ((MAJ) << 16 | (MIN) << 8 | (REV)) 26 | 27 | /* make version as integer for comparison */ 28 | #define EMBUI_VERSION EMBUI_VERSION_VALUE(EMBUI_VERSION_MAJOR, EMBUI_VERSION_MINOR, EMBUI_VERSION_REVISION) 29 | 30 | /* make version as string, i.e. "2.6.1" */ 31 | #define EMBUI_VERSION_STRING TOSTRING(EMBUI_VERSION_MAJOR) "." TOSTRING(EMBUI_VERSION_MINOR) "." TOSTRING(EMBUI_VERSION_REVISION) 32 | 33 | 34 | #ifndef EMBUI_PUB_PERIOD 35 | #define EMBUI_PUB_PERIOD 10 // Values Publication period, s 36 | #endif 37 | 38 | #ifndef EMBUI_AUTOSAVE_TIMEOUT 39 | #define EMBUI_AUTOSAVE_TIMEOUT 30 // configuration autosave timer, sec 40 | #endif 41 | 42 | // Default Hostname/AP prefix 43 | #ifndef EMBUI_IDPREFIX 44 | #define EMBUI_IDPREFIX "EmbUI" 45 | #endif 46 | 47 | // maximum number of websocket client connections 48 | #ifndef EMBUI_MAX_WS_CLIENTS 49 | #define EMBUI_MAX_WS_CLIENTS 4 50 | #endif 51 | 52 | #define EMBUI_WEBSOCK_URI "/ws" 53 | -------------------------------------------------------------------------------- /src/embui_log.h: -------------------------------------------------------------------------------- 1 | // LOG macro's 2 | #ifndef EMBUI_DEBUG_PORT 3 | #define EMBUI_DEBUG_PORT Serial 4 | #endif 5 | 6 | // undef possible LOG macros 7 | #ifdef LOG 8 | #undef LOG 9 | #endif 10 | #ifdef LOGV 11 | #undef LOGV 12 | #endif 13 | #ifdef LOGD 14 | #undef LOGD 15 | #endif 16 | #ifdef LOGI 17 | #undef LOGI 18 | #endif 19 | #ifdef LOGW 20 | #undef LOGW 21 | #endif 22 | #ifdef LOGE 23 | #undef LOGE 24 | #endif 25 | 26 | 27 | 28 | #if defined(EMBUI_DEBUG_LEVEL) && EMBUI_DEBUG_LEVEL == 5 29 | #define LOGV(tag, func, ...) EMBUI_DEBUG_PORT.print(tag); EMBUI_DEBUG_PORT.print(" V: "); EMBUI_DEBUG_PORT.func(__VA_ARGS__) 30 | #else 31 | #define LOGV(...) 32 | #endif 33 | 34 | #if defined(EMBUI_DEBUG_LEVEL) && EMBUI_DEBUG_LEVEL > 3 35 | #define LOGD(tag, func, ...) EMBUI_DEBUG_PORT.print(tag); EMBUI_DEBUG_PORT.print(" D: "); EMBUI_DEBUG_PORT.func(__VA_ARGS__) 36 | #else 37 | #define LOGD(...) 38 | #endif 39 | 40 | #if defined(EMBUI_DEBUG_LEVEL) && EMBUI_DEBUG_LEVEL > 2 41 | #define LOGI(tag, func, ...) EMBUI_DEBUG_PORT.print(tag); EMBUI_DEBUG_PORT.print(" I: "); EMBUI_DEBUG_PORT.func(__VA_ARGS__) 42 | // compat macro 43 | #define LOG(func, ...) EMBUI_DEBUG_PORT.func(__VA_ARGS__) 44 | #define LOG_CALL(call...) { call; } 45 | #else 46 | #define LOGI(...) 47 | // compat macro 48 | #define LOG(...) 49 | #define LOG_CALL(call...) ; 50 | #endif 51 | 52 | #if defined(EMBUI_DEBUG_LEVEL) && EMBUI_DEBUG_LEVEL > 1 53 | #define LOGW(tag, func, ...) EMBUI_DEBUG_PORT.print(tag); EMBUI_DEBUG_PORT.print(" W: "); EMBUI_DEBUG_PORT.func(__VA_ARGS__) 54 | #else 55 | #define LOGW(...) 56 | #endif 57 | 58 | #if defined(EMBUI_DEBUG_LEVEL) && EMBUI_DEBUG_LEVEL > 0 59 | #define LOGE(tag, func, ...) EMBUI_DEBUG_PORT.print(tag); EMBUI_DEBUG_PORT.print(" E: "); EMBUI_DEBUG_PORT.func(__VA_ARGS__) 60 | #else 61 | #define LOGE(...) 62 | #endif 63 | -------------------------------------------------------------------------------- /src/embui_wifi.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is a part of EmbUI project https://github.com/vortigont/EmbUI - a forked 3 | version of EmbUI project https://github.com/DmytroKorniienko/EmbUI 4 | 5 | (c) Emil Muratov, 2022 6 | */ 7 | 8 | // This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 9 | // then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 10 | // also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 11 | // and others people 12 | 13 | #include 14 | #include "embui_wifi.hpp" 15 | #include "embui_log.h" 16 | 17 | #define WIFI_STA_CONNECT_TIMEOUT 10 // timer for WiFi STA connection attempt 18 | #define WIFI_STA_COOLDOWN_TIMOUT 90 // timer for STA connect retry 19 | #define WIFI_AP_GRACE_PERIOD 15 // time to delay AP enable/disable, sec 20 | #define WIFI_BEGIN_DELAY 3 // a timeout before initiating WiFi-Client connection 21 | #define WIFI_PSK_MIN_LENGTH 8 22 | 23 | // c-tor 24 | WiFiController::WiFiController(EmbUI *ui, bool aponly) : emb(ui) { 25 | if (aponly) wconn = wifi_recon_t::ap_only; 26 | _tWiFi.set( TASK_SECOND, TASK_FOREVER, [this](){ _state_switcher(); } ); 27 | ts.addTask(_tWiFi); 28 | 29 | // Set WiFi event handlers 30 | eid = WiFi.onEvent( [this](WiFiEvent_t event, WiFiEventInfo_t info){ _onWiFiEvent(event, info); } ); 31 | if (!eid){ 32 | LOGE(P_EmbUI_WiFi, println, "Err registering evt handler!"); 33 | } 34 | } 35 | 36 | // d-tor 37 | WiFiController::~WiFiController(){ 38 | WiFi.removeEvent(eid); 39 | ts.deleteTask(_tWiFi); 40 | }; 41 | 42 | 43 | void WiFiController::connect(const char *ssid, const char *pwd) 44 | { 45 | String _ssid(ssid); String _pwd(pwd); // I need objects to pass it to lambda 46 | Task *t = new Task(WIFI_BEGIN_DELAY * TASK_SECOND, TASK_ONCE, 47 | [_ssid, _pwd](){ 48 | LOGI(P_EmbUI_WiFi, printf, "client connecting to SSID:'%s', pwd:'%s'\n", _ssid.c_str(), _pwd.isEmpty() ? P_empty_quotes : _pwd.c_str()); 49 | WiFi.disconnect(); 50 | WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); 51 | 52 | _ssid.length() ? WiFi.begin(_ssid.c_str(), _pwd.c_str()) : WiFi.begin(); 53 | }, 54 | &ts, false, nullptr, nullptr, true 55 | ); 56 | t->enableDelayed(); 57 | wconn = wifi_recon_t::ap_grace_enable; 58 | ap_ctr = WIFI_AP_GRACE_PERIOD; 59 | // drop "AP-only flag from config" 60 | if (emb->getConfig()[V_APonly]){ 61 | emb->getConfig().remove(V_APonly); 62 | emb->autosave(); 63 | } 64 | } 65 | 66 | 67 | void WiFiController::setmode(WiFiMode_t mode){ 68 | LOGI(P_EmbUI_WiFi, printf, "set mode: %d\n", mode); 69 | WiFi.mode(mode); 70 | } 71 | 72 | /*use mdns for host name resolution*/ 73 | void WiFiController::setup_mDns(){ 74 | //MDNS.end(); // TODO - this often leads to crash, needs triage 75 | 76 | if (!MDNS.begin(emb->hostname())){ 77 | LOGE(P_EmbUI_WiFi, println, "Error setting up responder!"); 78 | MDNS.end(); 79 | return; 80 | } 81 | LOGI(P_EmbUI_WiFi, printf, "mDNS responder: %s.local\n", emb->hostname()); 82 | 83 | if (!MDNS.addService("http", P_tcp, 80)) { LOGE(P_EmbUI_WiFi, println, "mDNS failed to add tcp:80 service"); }; 84 | 85 | if (emb->getConfig()[P_ftp]) 86 | MDNS.addService(P_ftp, P_tcp, 21); 87 | 88 | //MDNS.addService(F("txt"), F("udp"), 4243); 89 | 90 | // run callback 91 | if (mdns_cb) 92 | mdns_cb(); 93 | } 94 | 95 | /** 96 | * Configure esp's internal AP 97 | * default is to configure credentials from the config 98 | * bool force - reapply credentials even if AP is already started, exit otherwise 99 | */ 100 | void WiFiController::setupAP(bool force){ 101 | // check if AP is already started 102 | if ((bool)(WiFi.getMode() & WIFI_AP) && !force) 103 | return; 104 | 105 | // clear password if invalid 106 | String pwd; 107 | if (emb->getConfig()[V_APpwd].is()){ 108 | pwd = emb->getConfig()[V_APpwd].as(); 109 | if (pwd.length() < WIFI_PSK_MIN_LENGTH) 110 | emb->getConfig().remove(V_APpwd); 111 | } 112 | 113 | LOGD(P_EmbUI_WiFi, printf, "set AP params to SSID:'%s', pwd:'%s'\n", emb->hostname(), pwd.c_str()); 114 | 115 | WiFi.softAP(emb->hostname(), pwd.c_str()); 116 | if (!emb->getConfig()[V_NOCaptP]) // start DNS server in "captive portal mode" 117 | dnssrv.start(); 118 | } 119 | 120 | void WiFiController::init(){ 121 | WiFi.setHostname(emb->hostname()); 122 | if (wconn == wifi_recon_t::ap_only){ 123 | LOGI(P_EmbUI_WiFi, println, "AP-only mode"); 124 | setupAP(true); 125 | WiFi.enableSTA(false); 126 | return; 127 | } 128 | 129 | LOGI(P_EmbUI_WiFi, println, "STA mode"); 130 | 131 | WiFi.mode(wifi_mode_t::WIFI_MODE_STA); 132 | // enable NTPoDHCP 133 | #if LWIP_DHCP_GET_NTP_SRV 134 | if (_ntpodhcp){ 135 | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) 136 | esp_sntp_servermode_dhcp(1); 137 | #else 138 | sntp_servermode_dhcp(1); 139 | #endif 140 | } 141 | #endif 142 | 143 | wconn = wifi_recon_t::ap_grace_enable; // start in gracefull AP mode in case if MCU does not have any stored creds 144 | ap_ctr = WIFI_AP_GRACE_PERIOD; 145 | WiFi.begin(); 146 | _tWiFi.enableDelayed(); 147 | } 148 | 149 | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) 150 | void WiFiController::_onWiFiEvent(arduino_event_id_t event, arduino_event_info_t info) 151 | #else 152 | void WiFiController::_onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) 153 | #endif 154 | { 155 | switch (event){ 156 | /* 157 | case SYSTEM_EVENT_AP_START: 158 | LOG(println, F("Access-point started")); 159 | setup_mDns(); 160 | break; 161 | */ 162 | 163 | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) 164 | case ARDUINO_EVENT_WIFI_STA_CONNECTED: 165 | #else 166 | case SYSTEM_EVENT_STA_CONNECTED: 167 | #endif 168 | LOGI(P_EmbUI_WiFi, println, "STA connected"); 169 | wconn = wifi_recon_t::sta_noip; 170 | break; 171 | 172 | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) 173 | case ARDUINO_EVENT_WIFI_STA_GOT_IP: 174 | #else 175 | case SYSTEM_EVENT_STA_GOT_IP: 176 | #endif 177 | 178 | // do I still need it for IDF 4.x ??? 179 | #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) 180 | /* this is a weird hack to mitigate DHCP-client hostname issue 181 | * https://github.com/espressif/arduino-esp32/issues/2537 182 | * we use some IDF functions to restart dhcp-client, that has been disabled before STA connect 183 | */ 184 | tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA); 185 | tcpip_adapter_ip_info_t iface; 186 | tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &iface); 187 | if(!iface.ip.addr){ 188 | LOGD(P_EmbUI_WiFi, print, "DHCP discover... "); 189 | return; 190 | } 191 | #endif 192 | LOGI(P_EmbUI_WiFi, printf, "SSID:'%s', IP: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); 193 | 194 | // if we are in ap_grace_enable state (i.e. reconnecting) - cancell it 195 | if (wconn == wifi_recon_t::ap_grace_enable){ 196 | wconn = wifi_recon_t::sta_good; 197 | ap_ctr = 0; 198 | } 199 | 200 | if(WiFi.getMode() & WIFI_MODE_AP){ 201 | // need to disable AP after grace period 202 | wconn = wifi_recon_t::ap_grace_disable; 203 | ap_ctr = WIFI_AP_GRACE_PERIOD; 204 | } else { 205 | wconn = wifi_recon_t::sta_good; 206 | } 207 | setup_mDns(); 208 | break; 209 | 210 | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) 211 | case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: 212 | #else 213 | case SYSTEM_EVENT_STA_DISCONNECTED: 214 | #endif 215 | LOGI(P_EmbUI_WiFi, printf, "Disconnected, reason: %d\n", info.wifi_sta_disconnected.reason); // PIO's ARDUINO=10812 Core >=2.0.0 216 | 217 | // WiFi STA has just lost the conection => enable internal AP after grace period 218 | if(wconn == wifi_recon_t::sta_good){ 219 | wconn = wifi_recon_t::ap_grace_enable; 220 | ap_ctr = WIFI_AP_GRACE_PERIOD; 221 | break; 222 | } 223 | _tWiFi.enableIfNot(); 224 | break; 225 | default: 226 | LOGD(P_EmbUI_WiFi, printf, "event: %d\n", event); 227 | break; 228 | } 229 | } 230 | 231 | 232 | void WiFiController::_state_switcher(){ 233 | //LOGV(P_EmbUI_WiFi, printf, "_state_switcher() %u\n", wconn); 234 | 235 | switch (wconn){ 236 | case wifi_recon_t::ap_only: // switch to AP-only mode 237 | setupAP(); 238 | WiFi.enableSTA(false); 239 | _tWiFi.disable(); // no need to check for state changes in ap-only mode 240 | break; 241 | 242 | case wifi_recon_t::ap_grace_disable: 243 | if (ap_ctr){ 244 | if(!--ap_ctr && (WiFi.getMode() & WIFI_MODE_STA)){ 245 | dnssrv.stop(); 246 | WiFi.enableAP(false); 247 | LOGD(P_EmbUI_WiFi, println, "AP disabled"); 248 | wconn = wifi_recon_t::sta_good; 249 | ap_ctr = WIFI_AP_GRACE_PERIOD; 250 | } 251 | } 252 | break; 253 | 254 | case wifi_recon_t::ap_grace_enable: 255 | //LOGV(P_EmbUI_WiFi, printf, "AP grace time: %u\n", ap_ctr); 256 | if (ap_ctr){ 257 | if(!--ap_ctr && !(WiFi.getMode() & WIFI_MODE_AP)){ 258 | setupAP(); 259 | LOGD(P_EmbUI_WiFi, println, "AP enabled"); 260 | wconn = wifi_recon_t::sta_reconnecting; 261 | sta_ctr = WIFI_STA_CONNECT_TIMEOUT; 262 | } 263 | } 264 | break; 265 | 266 | case wifi_recon_t::sta_reconnecting: 267 | if(sta_ctr){ 268 | if(!--sta_ctr){ // disable STA mode for cooldown period 269 | WiFi.enableSTA(false); 270 | LOGD(P_EmbUI_WiFi, println, "STA disabled"); 271 | wconn = wifi_recon_t::sta_cooldown; 272 | sta_ctr = WIFI_STA_COOLDOWN_TIMOUT; 273 | } 274 | } 275 | break; 276 | 277 | case wifi_recon_t::sta_cooldown: 278 | if(sta_ctr){ 279 | if(!--sta_ctr){ // try to reconnect STA 280 | LOGD(P_EmbUI_WiFi, println, "STA reconnecting"); 281 | wconn = wifi_recon_t::sta_reconnecting; 282 | sta_ctr = WIFI_STA_CONNECT_TIMEOUT; 283 | WiFi.begin(); 284 | } 285 | } 286 | break; 287 | 288 | default: 289 | break; 290 | } 291 | } 292 | 293 | 294 | void WiFiController::aponly(bool ap){ 295 | if (ap){ 296 | wconn = wifi_recon_t::ap_only; 297 | _tWiFi.enableDelayed(WIFI_BEGIN_DELAY * TASK_SECOND); 298 | } else if (wconn != wifi_recon_t::sta_good) { 299 | wconn = wifi_recon_t::ap_grace_disable; 300 | ap_ctr = WIFI_AP_GRACE_PERIOD; 301 | _tWiFi.enableDelayed(); 302 | WiFi.begin(); 303 | } 304 | } 305 | 306 | void WiFiController::ntpodhcp(bool enable){ 307 | _ntpodhcp = enable; 308 | }; 309 | -------------------------------------------------------------------------------- /src/embui_wifi.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is a part of EmbUI project https://github.com/vortigont/EmbUI - a forked 3 | version of EmbUI project https://github.com/DmytroKorniienko/EmbUI 4 | 5 | (c) Emil Muratov, 2022 6 | */ 7 | 8 | // This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 9 | // then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 10 | // also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 11 | // and others people 12 | 13 | #pragma once 14 | 15 | #include "WiFi.h" 16 | #include "DNSServer.h" 17 | #include "ESPmDNS.h" 18 | #include "ts.h" 19 | 20 | using mdns_callback_t = std::function< void (void)>; 21 | 22 | class EmbUI; 23 | 24 | class WiFiController { 25 | 26 | enum class wifi_recon_t:uint8_t { 27 | none, 28 | sta_noip, // station connected, but no IP obtained 29 | sta_good, // station connected and got IP 30 | sta_reconnecting, // station is attempting to reconnect 31 | sta_cooldown, // station intentionally disabled for a grace period to allow proper AP operation 32 | ap_grace_disable, // Access Point is waiting to be disabled 33 | ap_grace_enable, // Access Point is waiting to be enabled 34 | ap_only 35 | }; 36 | 37 | 38 | EmbUI *emb; 39 | Task _tWiFi; // WiFi connection event handler task 40 | wifi_event_id_t eid; 41 | wifi_recon_t wconn = {wifi_recon_t::none}; // WiFi (re)connection state 42 | 43 | bool _ntpodhcp{true}; 44 | 45 | // timer counters 46 | uint8_t ap_ctr{0}; // AccessPoint status counter 47 | uint8_t sta_ctr{0}; // Station status counter 48 | 49 | // WiFi events callback handler 50 | void _onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info); 51 | 52 | /** 53 | * @brief bring up mDNS service 54 | * 55 | */ 56 | void setup_mDns(); 57 | 58 | 59 | /** 60 | * @brief periodicaly running task to handle and switch WiFi state 61 | * 62 | */ 63 | void _state_switcher(); 64 | 65 | /** 66 | * update WiFi AP params and state 67 | */ 68 | //void wifi_updateAP(); 69 | 70 | public: 71 | explicit WiFiController(EmbUI *ui, bool aponly = false); 72 | ~WiFiController(); 73 | 74 | /** 75 | * @brief mDNS init callback 76 | * callback is called on mDNS initialization to povide 77 | * proper order for service registrations 78 | * 79 | */ 80 | mdns_callback_t mdns_cb = nullptr; 81 | 82 | /** 83 | * @brief DNSServer provides Captive-Portal capability in AP mode 84 | * 85 | */ 86 | DNSServer dnssrv; 87 | 88 | /** 89 | * Initialize WiFi using stored configuration 90 | */ 91 | void init(); 92 | 93 | /** 94 | * Подключение к WiFi AP в клиентском режиме 95 | */ 96 | void connect(const char *ssid=nullptr, const char *pwd=nullptr); 97 | 98 | /** 99 | * Configure and bring up esp's internal AP 100 | * defualt is to configure credentials from the config 101 | * bool force - reapply credentials even if AP is already started, exit otherwise 102 | */ 103 | void setupAP(bool force=false); 104 | 105 | /** 106 | * switch WiFi modes 107 | */ 108 | void setmode(WiFiMode_t mode); 109 | 110 | /** 111 | * @brief set AP only mode 112 | * 113 | */ 114 | void aponly(bool ap); 115 | 116 | /** 117 | * @brief get ap-only status 118 | * 119 | */ 120 | inline bool aponly(){ return (wconn == wifi_recon_t::ap_only); }; 121 | 122 | void ntpodhcp(bool enable); 123 | 124 | }; 125 | 126 | #include "EmbUI.h" -------------------------------------------------------------------------------- /src/embuifs.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of EmbUI project 3 | https://github.com/vortigont/EmbUI 4 | 5 | Copyright © 2023 Emil Muratov (Vortigont) https://github.com/vortigont/ 6 | 7 | EmbUI is free software: you can redistribute it and/or modify 8 | it under the terms of MIT License https://opensource.org/license/mit/ 9 | */ 10 | 11 | #include "embuifs.hpp" 12 | #include "embui_constants.h" 13 | #include "embui_log.h" 14 | 15 | static constexpr const char* T_load_file = "Lod file: %s\n"; 16 | static constexpr const char* T_cant_open_file = "Can't open file: %s\n"; 17 | static constexpr const char* T_deserialize_err = "failed to load json file: %s, deserialize error: %s\n"; 18 | 19 | namespace embuifs { 20 | 21 | size_t serialize2file(JsonVariantConst v, const String& filepath, size_t buffsize){ return serialize2file(v, filepath.c_str(), buffsize); }; 22 | 23 | size_t serialize2file(JsonVariantConst v, const char* filepath, size_t buffsize){ 24 | File hndlr = LittleFS.open(filepath, "w"); 25 | WriteBufferingStream bufferedFile(hndlr, buffsize); 26 | size_t len = serializeJson(v, bufferedFile); 27 | bufferedFile.flush(); 28 | hndlr.close(); 29 | return len; 30 | } 31 | 32 | void obj_merge(JsonObject dst, JsonObjectConst src){ 33 | for (JsonPairConst kvp : src){ 34 | dst[kvp.key()] = kvp.value(); 35 | } 36 | } 37 | 38 | void obj_deepmerge(JsonVariant dst, JsonVariantConst src){ 39 | if (src.is()){ 40 | for (JsonPairConst kvp : src.as()){ 41 | if (dst[kvp.key()]) 42 | obj_deepmerge(dst[kvp.key()], kvp.value()); 43 | else 44 | dst[kvp.key()] = kvp.value(); 45 | } 46 | } else 47 | dst.set(src); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/embuifs.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of EmbUI project 3 | https://github.com/vortigont/EmbUI 4 | 5 | Copyright © 2023 Emil Muratov (Vortigont) https://github.com/vortigont/ 6 | 7 | EmbUI is free software: you can redistribute it and/or modify 8 | it under the terms of MIT License https://opensource.org/license/mit/ 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include "StreamUtils.h" 16 | 17 | #define EMBUIFS_FILE_WRITE_BUFF_SIZE 256 18 | 19 | /** 20 | * @brief A namespace for various functions to help working with files on LittleFS system 21 | * 22 | */ 23 | namespace embuifs{ 24 | /** 25 | * метод загружает и пробует десериализовать джейсон из файла в предоставленный документ, 26 | * возвращает true если загрузка и десериализация прошла успешно 27 | * @param doc - JsonDocument куда будет загружен джейсон 28 | * @param jsonfile - файл, для загрузки 29 | */ 30 | template 31 | DeserializationError deserializeFile(TDestination&& dst, const char* filepath, size_t buffsize = EMBUIFS_FILE_WRITE_BUFF_SIZE){ 32 | if (!filepath || !*filepath) 33 | return DeserializationError::Code::InvalidInput; 34 | 35 | //LOGV(P_EmbUI, printf, T_load_file, filepath); 36 | File jfile = LittleFS.open(filepath); 37 | 38 | if (!jfile){ 39 | //LOGD(P_EmbUI, printf, T_cant_open_file, filepath); 40 | return DeserializationError::Code::InvalidInput; 41 | } 42 | 43 | ReadBufferingStream bufferingStream(jfile, buffsize); 44 | return deserializeJson(dst, bufferingStream); 45 | /* 46 | DeserializationError error = deserializeJson(doc, bufferingStream); 47 | if (!error) return error; 48 | LOGE(P_EmbUI, printf, T_deserialize_err, filepath, error.c_str()); 49 | return error; 50 | */ 51 | } 52 | 53 | /** 54 | * метод загружает и пробует десериализовать джейсон из файла в предоставленный документ, 55 | * возвращает true если загрузка и десериализация прошла успешно 56 | * https://github.com/bblanchon/ArduinoJson/issues/2072 57 | * https://arduinojson.org/v7/how-to/deserialize-a-very-large-document/#deserialization-in-chunks 58 | * https://github.com/mrfaptastic/json-streaming-parser2 59 | * 60 | * @param doc - JsonDocument куда будет загружен джейсон 61 | * @param jsonfile - файл, для загрузки 62 | */ 63 | template 64 | DeserializationError deserializeFileWFilter(TDestination&& dst, const char* filepath, JsonDocument& filter, size_t buffsize = EMBUIFS_FILE_WRITE_BUFF_SIZE){ 65 | if (!filepath || !*filepath) 66 | return DeserializationError::Code::InvalidInput; 67 | 68 | //LOGV(P_EmbUI, printf, T_load_file, filepath); 69 | File jfile = LittleFS.open(filepath); 70 | 71 | if (!jfile){ 72 | //LOGD(P_EmbUI, printf, T_cant_open_file, filepath); 73 | return DeserializationError::Code::InvalidInput; 74 | } 75 | 76 | ReadBufferingStream bufferingStream(jfile, buffsize); 77 | return deserializeJson(dst, jfile, DeserializationOption::Filter(filter)); 78 | /* 79 | DeserializationError error = deserializeJson(doc, jfile, DeserializationOption::Filter(filter)); 80 | if (!error) return error; 81 | LOGE(P_EmbUI, printf, T_deserialize_err, filepath, error.c_str()); 82 | return error; 83 | */ 84 | }; 85 | 86 | /** 87 | * @brief serialize and write JsonDocument to a file using buffered writes 88 | * 89 | * @param doc to serialize 90 | * @param filepath to write to 91 | * @return size_t bytes written 92 | */ 93 | size_t serialize2file(JsonVariantConst v, const char* filepath, size_t buffsize = EMBUIFS_FILE_WRITE_BUFF_SIZE); 94 | 95 | /** 96 | * @brief shallow merge objects 97 | * from https://arduinojson.org/v6/how-to/merge-json-objects/ 98 | * 99 | * @param dst 100 | * @param src 101 | */ 102 | void obj_merge(JsonObject dst, JsonObjectConst src); 103 | 104 | /** 105 | * @brief deep merge objects 106 | * from https://arduinojson.org/v6/how-to/merge-json-objects/ 107 | * 108 | * @param dst 109 | * @param src 110 | */ 111 | void obj_deepmerge(JsonVariant dst, JsonVariantConst src); 112 | } -------------------------------------------------------------------------------- /src/ftpsrv.cpp: -------------------------------------------------------------------------------- 1 | //#define FTP_DEBUG 2 | #include 3 | #include 4 | #include 5 | 6 | FTPServer *ftpsrv = nullptr; 7 | 8 | void ftp_start(void){ 9 | if (!ftpsrv) ftpsrv = new FTPServer(LittleFS); 10 | if (ftpsrv) ftpsrv->begin(embui.getConfig()[P_ftp_usr] | P_ftp, embui.getConfig()[P_ftp_pwd] | P_ftp); 11 | } 12 | 13 | void ftp_stop(void){ 14 | if (ftpsrv) { 15 | ftpsrv->stop(); 16 | delete ftpsrv; 17 | ftpsrv = nullptr; 18 | } 19 | } 20 | void ftp_loop(void){ 21 | if (ftpsrv) ftpsrv->handleFTP(); 22 | } 23 | 24 | bool ftp_status(){ return ftpsrv; }; 25 | 26 | namespace basicui { 27 | 28 | void page_settings_ftp(Interface *interf, const JsonObjectConst data, const char* action){ 29 | interf->json_frame_interface(); 30 | interf->json_section_uidata(); 31 | interf->uidata_pick("sys.settings.ftp"); 32 | interf->json_frame_flush(); 33 | 34 | interf->json_frame_value(); 35 | interf->value(P_ftp, ftp_status()); // enable FTP checkbox 36 | interf->value(P_ftp_usr, embui.getConfig()[P_ftp_usr].is() ? embui.getConfig()[P_ftp_usr].as() : P_ftp ); 37 | interf->value(P_ftp_pwd, embui.getConfig()[P_ftp_pwd].is() ? embui.getConfig()[P_ftp_pwd].as() : P_ftp ); 38 | interf->json_frame_flush(); 39 | } 40 | 41 | void set_settings_ftp(Interface *interf, const JsonObjectConst data, const char* action){ 42 | bool newstate = data[P_ftp]; 43 | 44 | embui.getConfig()[P_ftp] = newstate; // ftp on/off 45 | 46 | // set ftp login 47 | if (data[P_ftp_usr] == P_ftp) 48 | embui.getConfig().remove(P_ftp_usr); // do not save default login 49 | else 50 | embui.getConfig()[P_ftp_usr] = data[P_ftp_usr]; 51 | 52 | // set ftp passwd 53 | if (data[P_ftp_pwd] == P_ftp) 54 | embui.getConfig().remove(P_ftp_pwd); // do not save default pwd 55 | else 56 | embui.getConfig()[P_ftp_pwd] = data[P_ftp_pwd]; 57 | 58 | ftp_stop(); 59 | LOGD(P_EmbUI, println, "UI: Stopping FTP Server"); 60 | 61 | if ( newstate ){ 62 | ftp_start(); 63 | LOGD(P_EmbUI, println, "UI: Starting FTP Server"); 64 | } 65 | 66 | if (interf) basicui::page_system_settings(interf, {}, NULL); // go to "Options" page 67 | embui.autosave(); 68 | } 69 | 70 | } // namespace basicui -------------------------------------------------------------------------------- /src/ftpsrv.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | void ftp_start(void); 5 | void ftp_stop(void); 6 | void ftp_loop(void); 7 | inline bool ftp_status(); 8 | 9 | namespace basicui { 10 | 11 | /** 12 | * BasicUI ftp server setup UI block 13 | */ 14 | void page_settings_ftp(Interface *interf, const JsonObjectConst data, const char* action = NULL); 15 | 16 | void set_settings_ftp(Interface *interf, const JsonObjectConst data, const char* action = NULL); 17 | 18 | } // namespace basicui -------------------------------------------------------------------------------- /src/http.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Jusr another EMBUI fork of a fork :) https://github.com/vortigont/EmbUI/ 3 | 4 | This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 5 | then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 6 | also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 7 | and others people 8 | */ 9 | 10 | #include "EmbUI.h" 11 | #include "flashz-http.hpp" 12 | 13 | static const char* UPDATE_URI = "/update"; 14 | FlashZhttp fz; 15 | 16 | /** 17 | * @brief OTA update handler 18 | * 19 | */ 20 | void ota_handler(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); 21 | 22 | /* 23 | * OTA update progress calculator 24 | */ 25 | //uint8_t uploadProgress(size_t len, size_t total); 26 | 27 | // default 404 handler 28 | void EmbUI::_notFound(AsyncWebServerRequest *request) { 29 | 30 | if (cb_not_found && cb_not_found(request)) return; // process redirect via external call-back if set 31 | 32 | // if external cb is not defined or returned false, than handle it via captive-portal or return 404 33 | if (!_cfg[V_NOCaptP] && WiFi.getMode() & WIFI_AP){ // return redirect to root page in Captive-Portal mode 34 | request->redirect("/"); 35 | return; 36 | } 37 | request->send(404); 38 | } 39 | 40 | /** 41 | * @brief Set HTTP-handlers for EmbUI related URL's 42 | */ 43 | void EmbUI::http_set_handlers(){ 44 | 45 | // returns run-time system config serialized in JSON 46 | server.on("/config", HTTP_ANY, [this](AsyncWebServerRequest *request) { 47 | 48 | AsyncResponseStream *response = request->beginResponseStream(asyncsrv::T_application_json); 49 | response->addHeader(asyncsrv::T_Cache_Control, asyncsrv::T_no_cache); 50 | 51 | serializeJson(_cfg, *response); 52 | 53 | request->send(response); 54 | }); 55 | 56 | 57 | server.on("/version", HTTP_ANY, [this](AsyncWebServerRequest *request) { 58 | request->send(200, PGmimetxt, "EmbUI ver: " TOSTRING(EMBUIVER)); 59 | }); 60 | 61 | // postponed reboot (TODO: convert to CMD) 62 | server.on("/restart", HTTP_ANY, [this](AsyncWebServerRequest *request) { 63 | Task *t = new Task(TASK_SECOND*5, TASK_ONCE, nullptr, &ts, false, nullptr, [](){ ESP.restart(); }); 64 | t->enableDelayed(); 65 | request->redirect("/"); 66 | }); 67 | 68 | // HTTP REST API handler 69 | _ajs_handler = std::make_unique("/api", [this](AsyncWebServerRequest *r, JsonVariant &json) { _http_api_hndlr(r, json); }); 70 | if (_ajs_handler) 71 | server.addHandler(_ajs_handler.get()); 72 | 73 | // esp32 handles updates via external lib 74 | fz.provide_ota_form(&server, UPDATE_URI); 75 | fz.handle_ota_form(&server, UPDATE_URI); 76 | 77 | // serve all static files from LittleFS root / 78 | server.serveStatic("/", LittleFS, "/") 79 | .setDefaultFile("index.html") 80 | .setCacheControl(asyncsrv::T_no_cache); // revalidate based on etag/IMS headers 81 | 82 | 83 | // 404 handler - disabled to allow override in user code 84 | server.onNotFound([this](AsyncWebServerRequest *r){_notFound(r);}); 85 | 86 | } // end of EmbUI::http_set_handlers 87 | 88 | /* 89 | * OTA update progress calculator 90 | 91 | uint8_t uploadProgress(size_t len, size_t total){ 92 | static int prev = 0; 93 | int parts = total / 25; // logger chunks (each 4%) 94 | int curr = len / parts; 95 | uint8_t progress = 100*len/total; 96 | if (curr != prev) { 97 | prev = curr; 98 | LOG(printf_P, "%u%%..", progress ); 99 | } 100 | return progress; 101 | } 102 | */ 103 | 104 | void EmbUI::_http_api_hndlr(AsyncWebServerRequest *request, JsonVariant &json){ 105 | // TODO: 106 | // the specific for this handler is that it won't inject action responces to registered feeders 107 | // it's a design gap, I can't handle WS multimessaging and HTTP call in the same manner 108 | Interface interf(request); 109 | action.exec(&interf, json[P_data], json[P_action].as()); 110 | } 111 | -------------------------------------------------------------------------------- /src/mqtt.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of EmbUI project 3 | https://github.com/vortigont/EmbUI 4 | 5 | Copyright © 2023 Emil Muratov (Vortigont) https://github.com/vortigont/ 6 | 7 | EmbUI is free software: you can redistribute it and/or modify 8 | it under the terms of MIT License https://opensource.org/license/mit/ 9 | 10 | This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 11 | then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 12 | also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 13 | and others people 14 | */ 15 | 16 | #include "EmbUI.h" 17 | 18 | #define MQTT_RECONNECT_PERIOD 15 19 | 20 | void EmbUI::_mqttConnTask(bool state){ 21 | if (!state){ 22 | tMqttReconnector->disable(); 23 | //delete tMqttReconnector; 24 | //tMqttReconnector = nullptr; 25 | return; 26 | } 27 | 28 | if(tMqttReconnector){ 29 | tValPublisher->restart(); 30 | } else { 31 | tMqttReconnector = new Task(MQTT_RECONNECT_PERIOD * TASK_SECOND, TASK_FOREVER, [this](){ 32 | if (!(WiFi.getMode() & WIFI_MODE_STA)) return; 33 | if (mqttClient) _connectToMqtt(); 34 | }, &ts, true, nullptr, [this](){ tMqttReconnector = nullptr; }, true ); 35 | } 36 | } 37 | 38 | void EmbUI::_connectToMqtt() { 39 | LOGI(P_EmbUI_mqtt, println, "Connecting to MQTT..."); 40 | 41 | mqtt_topic = _cfg[V_mqtt_topic].as(); 42 | if (mqtt_topic.length()){ 43 | // if configured value is not empty 44 | mqtt_topic.replace("$id", mc); 45 | } else { 46 | mqtt_topic = "EmbUI/"; 47 | mqtt_topic += mc; 48 | mqtt_topic += (char)0x2f; // "/" 49 | } 50 | 51 | mqtt_host = _cfg[V_mqtt_host].as(); 52 | mqtt_port = _cfg[V_mqtt_port] | 1883; 53 | mqtt_user = _cfg[V_mqtt_user].as(); 54 | mqtt_pass = _cfg[V_mqtt_pass].as(); 55 | //mqtt_lwt=id("embui/pub/online"); 56 | 57 | //if (mqttClient->connected()) 58 | mqttClient->disconnect(true); 59 | 60 | IPAddress ip; 61 | 62 | if(ip.fromString(mqtt_host)) 63 | mqttClient->setServer(ip, mqtt_port); 64 | else 65 | mqttClient->setServer(mqtt_host.c_str(), mqtt_port); 66 | 67 | mqttClient->setKeepAlive(mqtt_ka); 68 | mqttClient->setCredentials(mqtt_user.c_str(), mqtt_pass.c_str()); 69 | //setWill(mqtt_lwt.c_str(), 0, true, "0"). 70 | mqttClient->connect(); 71 | } 72 | 73 | void EmbUI::mqttStart(){ 74 | if (_cfg[V_mqtt_enable] != true || !_cfg[V_mqtt_host].is()){ 75 | LOGD(P_EmbUI_mqtt, println, "MQTT disabled or no host set"); 76 | return; // выходим если host не задан 77 | } 78 | 79 | LOGD(P_EmbUI_mqtt, println, "Starting MQTT Client"); 80 | if (!mqttClient) 81 | mqttClient = std::make_unique(); 82 | 83 | mqttClient->onConnect([this](bool sessionPresent){ _onMqttConnect(sessionPresent); }); 84 | mqttClient->onDisconnect([this](AsyncMqttClientDisconnectReason reason){_onMqttDisconnect(reason);}); 85 | mqttClient->onMessage( [this](char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total){_onMqttMessage(topic, payload, properties, len, index, total);} ); 86 | //mqttClient->onSubscribe(onMqttSubscribe); 87 | //mqttClient->onUnsubscribe(onMqttUnsubscribe); 88 | //mqttClient->onPublish(onMqttPublish); 89 | 90 | mqttReconnect(); 91 | } 92 | 93 | void EmbUI::mqttStop(){ 94 | _mqttConnTask(false); 95 | delete mqttClient.release(); 96 | } 97 | 98 | void EmbUI::mqttReconnect(){ // принудительный реконнект, при смене чего-либо в UI 99 | _mqttConnTask(true); 100 | } 101 | 102 | void EmbUI::_onMqttDisconnect(AsyncMqttClientDisconnectReason reason){ 103 | LOGD(P_EmbUI_mqtt, printf, "Disconnected from MQTT:%u\n", static_cast(reason)); 104 | feeders.remove(_mqtt_feed_id); // remove MQTT feeder from chain 105 | //mqttReconnect(); 106 | } 107 | 108 | void EmbUI::_onMqttConnect(bool sessionPresent){ 109 | LOGD(P_EmbUI_mqtt, println,"Connected to MQTT."); 110 | _mqttConnTask(false); 111 | _mqttSubscribe(); 112 | // mqttClient->publish(mqtt_lwt.c_str(), 0, true, "1"); // publish Last Will testament 113 | 114 | // create MQTT feeder and add into the chain 115 | _mqtt_feed_id = feeders.add( std::make_unique(this) ); 116 | 117 | // publish sys info 118 | String t(C_sys); 119 | publish((t + V_hostname).c_str(), hostname(), true); 120 | publish((t + "ip").c_str(), WiFi.localIP().toString().c_str(), true); 121 | publish((t + P_uiver).c_str(), EMBUI_VERSION_STRING, true); 122 | publish((t + P_uijsapi).c_str(), EMBUI_JSAPI, true); 123 | } 124 | 125 | void EmbUI::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { 126 | LOGV(P_EmbUI_mqtt, printf, "Got MQTT msg topic: %s len:%u/%u\n", topic, len, total); 127 | if (index || len != total) return; // this is chunked message, reassembly is not supported (yet) 128 | 129 | std::string_view tpc(topic); 130 | /* 131 | const auto pos = tpc.find(mqttPrefix().c_str()); 132 | if (pos == tpc.npos) 133 | tpc.remove_prefix(mqttPrefix().length()); 134 | */ 135 | 136 | // this is a dublicate code same as for WS, need to implement a proper queue for such data 137 | 138 | JsonDocument *res = new JsonDocument(); 139 | if(!res) 140 | return; 141 | 142 | DeserializationError error = deserializeJson((*res), (const char*)payload, len); // deserialize via copy to prevent dangling pointers in action()'s 143 | if (error){ 144 | LOGD(P_EmbUI_mqtt, printf, "MQTT: msg deserialization err: %d\n", error.code()); 145 | delete res; 146 | return; 147 | } 148 | 149 | tpc.remove_prefix(mqttPrefix().length()); // chop off constant prefix 150 | 151 | if (starts_with(tpc, C_get) || starts_with(tpc, C_set)){ 152 | std::string act(tpc.substr(4)); // chop off 'get/' or 'set/' prefix 153 | std::replace( act.begin(), act.end(), '/', '_'); // replace topic delimiters into underscores 154 | JsonObject o = res->as(); 155 | o[P_action] = act; // set action identifier 156 | } 157 | 158 | // switch context for processing data 159 | Task *t = new Task(10, TASK_ONCE, 160 | [res](){ 161 | JsonObject o = res->as(); 162 | // call action handler for post'ed data 163 | embui.post(o); 164 | delete res; }, 165 | &ts, false, nullptr, nullptr, true 166 | ); 167 | if (t) 168 | t->enableDelayed(); 169 | 170 | 171 | } 172 | 173 | void EmbUI::_mqttSubscribe(){ 174 | mqttClient->subscribe((mqttPrefix()+"set/#").c_str(), 0); 175 | mqttClient->subscribe((mqttPrefix()+"get/#").c_str(), 0); 176 | mqttClient->subscribe((mqttPrefix()+C_post).c_str(), 0); 177 | // t += (char)0x23; //"#" 178 | } 179 | 180 | void EmbUI::publish(const char* topic, const char* payload, bool retained){ 181 | if (!mqttAvailable()) return; 182 | /* 183 | LOG(print, "MQTT pub: topic:"); 184 | LOG(print, topic); 185 | LOG(print, " payload:"); 186 | LOG(println, payload); 187 | */ 188 | mqttClient->publish(_mqttMakeTopic(topic).data(), 0, retained, payload); 189 | } 190 | 191 | void EmbUI::publish(const char* topic, const JsonVariantConst data, bool retained){ 192 | if (!mqttAvailable()) return; 193 | auto s = measureJson(data); 194 | std::vector buff(s); 195 | serializeJson(data, static_cast(buff.data()), s); 196 | mqttClient->publish(_mqttMakeTopic(topic).data(), 0, retained, reinterpret_cast(buff.data()), buff.size()); 197 | } 198 | 199 | void EmbUI::_mqtt_pub_sys_status(){ 200 | String t(C_sys); 201 | if(psramFound()) 202 | publish((t + "spiram_free").c_str(), ESP.getFreePsram()/1024); 203 | 204 | publish((t + "heap_free").c_str(), ESP.getFreeHeap()/1024); 205 | publish((t + "uptime").c_str(), esp_timer_get_time() / 1000000); 206 | publish((t + "rssi").c_str(), WiFi.RSSI()); 207 | } 208 | 209 | std::string EmbUI::_mqttMakeTopic(const char* topic){ 210 | std::string t(mqttPrefix().c_str()); 211 | t += topic; // make topic string "~/{$topic}/" 212 | std::replace( t.begin(), t.end(), '_', '/'); // replace topic delimiters into underscores 213 | return t; 214 | } 215 | 216 | void FrameSendMQTT::send(const JsonVariantConst& data){ 217 | if (data[P_pkg] == P_value){ 218 | _eu->publish(C_pub_value, data[P_block]); 219 | return; 220 | } 221 | 222 | // objects like "interface", "xload", "section" are related to WebUI interface 223 | if (data[P_pkg] == P_interface || data[P_pkg] == P_xload){ 224 | _eu->publish(C_pub_iface, data); 225 | return; 226 | } 227 | 228 | // all other packet types are ignored, user supposed to create it's own FrameSendMQTT instances if required 229 | //_eu->publish(C_pub_etc, data); 230 | } -------------------------------------------------------------------------------- /src/timeProcessor.h: -------------------------------------------------------------------------------- 1 | // This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 2 | // then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 3 | // also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 4 | // and others people 5 | 6 | #pragma once 7 | 8 | #include "WiFi.h" 9 | #include 10 | #include "embui_log.h" 11 | 12 | /* 13 | * COUNTRY macro allows to select a specific country pool for ntp requests, like ru.pool.ntp.org, eu.pool.ntp.org, etc... 14 | * otherwise a general pool "pool.ntp.org" is used as a fallback and vniiftri.ru's ntp is used as a primary 15 | * 16 | */ 17 | #if !defined EMBUI_NTP_SERVER 18 | #define EMBUI_NTP_SERVER "pool.ntp.org" 19 | #endif 20 | 21 | using callback_function_t = std::function; 22 | 23 | // TimeProcessor class is a Singleton 24 | class TimeProcessor 25 | { 26 | private: 27 | TimeProcessor(); 28 | 29 | std::array _ntp_servers; 30 | 31 | /** 32 | * обратный вызов при подключении к WiFi точке доступа 33 | * запускает синхронизацию времени 34 | */ 35 | void _onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info); 36 | 37 | protected: 38 | static callback_function_t timecb; 39 | 40 | /** 41 | * Timesync callback 42 | */ 43 | static void timeavailable(struct timeval *t); 44 | 45 | 46 | public: 47 | // this is a singleton 48 | TimeProcessor(TimeProcessor const&) = delete; 49 | void operator=(TimeProcessor const&) = delete; 50 | 51 | 52 | /** 53 | * obtain a pointer to singleton instance 54 | */ 55 | static TimeProcessor& getInstance(){ 56 | static TimeProcessor inst; 57 | return inst; 58 | } 59 | 60 | /** 61 | * @brief apply NTP servers configuration from NVS 62 | * should be called when IP/DNS has been set already 63 | */ 64 | void setNTPservers(); 65 | 66 | 67 | /** 68 | * Функция установки системного времени, принимает в качестве аргумента указатель на строку в формате 69 | * "YYYY-MM-DDThh:mm:ss" 70 | */ 71 | static time_t setTime(const char* datetimestr); 72 | 73 | /** 74 | * установки системной временной зоны/правил сезонного времени. 75 | * по сути дублирует системную функцию setTZ, но работает сразу 76 | * со строкой из памяти, а не из PROGMEM 77 | * Может использоваться для задания настроек зоны/правил налету из 78 | * браузера/апи вместо статического задания Зоны на этапе компиляции 79 | * @param tz - указатель на строку в формате TZSET(3) 80 | * набор отформатированных строк зон под прогмем лежит тут 81 | * https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h 82 | */ 83 | void tzsetup(const char* tz); 84 | 85 | /** 86 | * @brief - retreive NTP server name or IP 87 | */ 88 | String getserver(uint8_t idx); 89 | 90 | /** 91 | * Attach user-defined call-back function that would be called on time-set event 92 | * 93 | */ 94 | void attach_callback(callback_function_t callback); 95 | 96 | void dettach_callback(){ 97 | timecb = nullptr; 98 | } 99 | 100 | /** 101 | * Возвращает текущее смещение локального системного времени от UTC в секундах 102 | * с учетом часовой зоны и правил смены сезонного времени (если эти параметры были 103 | * корректно установленны ранее каким-либо методом) 104 | */ 105 | static long int getOffset(); 106 | 107 | /** 108 | * возвращает текуший unixtime 109 | */ 110 | static time_t getUnixTime() {return time(nullptr); } 111 | 112 | /** 113 | * возвращает строку с временем в формате "00:00" 114 | */ 115 | String getFormattedShortTime(); 116 | 117 | int getHours() 118 | { 119 | return localtime(now())->tm_hour; 120 | } 121 | 122 | int getMinutes() 123 | { 124 | return localtime(now())->tm_min; 125 | } 126 | 127 | /** 128 | * функция допечатывает в переданную строку заданный таймстамп в дату/время в формате "9999-99-99T99:99" 129 | * @param _tstamp - преобразовать заданный таймстамп, если не задан используется текущее локальное время 130 | */ 131 | static void getDateTimeString(String &buf, const time_t tstamp = 0); 132 | 133 | /** 134 | * returns pointer to current unixtime 135 | * (удобно использовать для передачи в localtime()) 136 | */ 137 | static const time_t* now(){ 138 | static time_t _now; 139 | time(&_now); 140 | return &_now; 141 | } 142 | 143 | /** 144 | * возвращает true если текущее время :00 секунд 145 | */ 146 | static bool seconds00(){ 147 | if ((localtime(now())->tm_sec)) 148 | return false; 149 | else 150 | return true; 151 | } 152 | }; 153 | 154 | 155 | /* 156 | * obsolete methods for using http API via worldtimeapi.org 157 | * Using the API it is not possible to set TZ env var 158 | * for proper DST/date changes and calculations. So it is deprecated 159 | * and should NOT be used except for compatibility or some 160 | * special cases like networks with blocked ntp service 161 | */ 162 | #ifdef EMBUI_WORLDTIMEAPI 163 | #include "ts.h" 164 | 165 | #define TIMEAPI_BUFSIZE 600 166 | #define HTTPSYNC_DELAY 5 167 | #define HTTP_REFRESH_HRS 3 // время суток для обновления часовой зоны 168 | #define HTTP_REFRESH_MIN 3 169 | 170 | // TaskScheduler - Let the runner object be a global, single instance shared between object files. 171 | extern Scheduler ts; 172 | 173 | class WorldTimeAPI 174 | { 175 | private: 176 | String tzone; // строка зоны для http-сервиса как она задана в https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv 177 | 178 | Task _wrk; // scheduler for periodic updates 179 | 180 | unsigned int getHttpData(String &payload, const String &url); 181 | 182 | /** 183 | * Функция обращается к внешнему http-сервису, получает временную зону/летнее время 184 | * на основании либо установленной переменной tzone, либо на основе IP-адреса 185 | * в случае если временная зона содержит правила перехода на летнее время, функция 186 | * запускает планировщик для автокоррекции временной зоны ежесуточно в 3 часа ночи 187 | */ 188 | void getTimeHTTP(); 189 | 190 | public: 191 | WorldTimeAPI(){ ts.addTask(_wrk); }; 192 | 193 | ~WorldTimeAPI(){ ts.deleteTask(_wrk); }; 194 | 195 | /** 196 | * установка строки с текущей временной зоной в текстовом виде, 197 | * влияет, на запрос через http-api за временем в конкретной зоне, 198 | * вместо автоопределения по ip 199 | * !ВНИМАНИЕ! Никакого отношения к текущей системной часовой зоне эта функция не имеет!!! 200 | */ 201 | void httpTimezone(const char *var); 202 | 203 | /** 204 | * функция установки планировщика обновления временной зоны 205 | * при вызове без параметра выставляет отложенный запуск на HTTP_REFRESH_HRS:HTTP_REFRESH_MIN 206 | */ 207 | void httprefreshtimer(const uint32_t delay=0); 208 | 209 | }; 210 | #endif // end of EMBUI_WORLDTIMEAPI -------------------------------------------------------------------------------- /src/traits.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of EmbUI project 3 | https://github.com/vortigont/EmbUI 4 | 5 | Copyright © 2023 Emil Muratov (Vortigont) https://github.com/vortigont/ 6 | 7 | EmbUI is free software: you can redistribute it and/or modify 8 | it under the terms of MIT License https://opensource.org/license/mit/ 9 | 10 | This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 11 | then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 12 | also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 13 | and others people 14 | */ 15 | 16 | #include 17 | #include "traits.hpp" 18 | 19 | // std::string_view.ends_with before C++20 20 | bool ends_with(std::string_view str, std::string_view sv){ 21 | return str.size() >= sv.size() && str.compare(str.size() - sv.size(), str.npos, sv) == 0; 22 | } 23 | 24 | // std::string_view.ends_with before C++20 25 | bool ends_with(std::string_view str, const char* sv){ 26 | return ends_with(str, std::string_view(sv)); 27 | } 28 | 29 | // std::string_view.starts_with before C++20 30 | bool starts_with(std::string_view str, std::string_view sv){ 31 | return str.substr(0, sv.size()) == sv; 32 | } 33 | 34 | // std::string_view.starts_with before C++20 35 | bool starts_with(std::string_view str, const char* sv){ 36 | return starts_with(str, std::string_view(sv)); 37 | } 38 | -------------------------------------------------------------------------------- /src/traits.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of EmbUI project 3 | https://github.com/vortigont/EmbUI 4 | 5 | Copyright © 2023 Emil Muratov (Vortigont) https://github.com/vortigont/ 6 | 7 | EmbUI is free software: you can redistribute it and/or modify 8 | it under the terms of MIT License https://opensource.org/license/mit/ 9 | 10 | This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 11 | then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 12 | also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 13 | and others people 14 | */ 15 | 16 | // type traits I use for various literals 17 | 18 | #pragma once 19 | #include 20 | #include "ArduinoJson.h" 21 | 22 | // cast enum to int 23 | template 24 | constexpr std::common_type_t> 25 | e2int(E e) { 26 | return static_cast>>(e); 27 | } 28 | 29 | // std::string_view.ends_with before C++20 30 | bool ends_with(std::string_view str, std::string_view sv); 31 | 32 | // std::string_view.ends_with before C++20 33 | bool ends_with(std::string_view str, const char* sv); 34 | 35 | // std::string_view.starts_with before C++20 36 | bool starts_with(std::string_view str, std::string_view sv); 37 | 38 | // std::string_view.starts_with before C++20 39 | bool starts_with(std::string_view str, const char* sv); 40 | 41 | namespace embui_traits{ 42 | 43 | template 44 | struct is_string : public std::disjunction< 45 | std::is_same>, 46 | std::is_same>, 47 | std::is_same>, 48 | std::is_same>, 49 | std::is_same>, 50 | std::is_same> // String derived helper class 51 | > {}; 52 | 53 | // value helper 54 | template 55 | inline constexpr bool is_string_v = is_string::value; 56 | 57 | template 58 | inline constexpr bool is_string_t = is_string::type; 59 | 60 | template 61 | struct is_string_obj : public std::disjunction< 62 | std::is_same>, 63 | std::is_same>, 64 | std::is_same>, // String derived helper class 65 | #if ARDUINOJSON_VERSION_MAJOR <= 7 && ARDUINOJSON_VERSION_MINOR < 3 66 | std::is_same>, // ArduinoJson type 67 | std::is_same> // ArduinoJson type 68 | #else 69 | std::is_same> // ArduinoJson type 70 | #endif 71 | > {}; 72 | 73 | // value helper 74 | template 75 | inline constexpr bool is_string_obj_v = is_string_obj::value; 76 | 77 | template 78 | struct is_string_ptr : public std::disjunction< 79 | std::is_same>, 80 | std::is_same>, 81 | std::is_same> 82 | > {}; 83 | 84 | // value helper 85 | template 86 | inline constexpr bool is_string_ptr_v = is_string_ptr::value; 87 | 88 | template 89 | typename std::enable_if,bool>::type 90 | is_empty_string(const T &label){ 91 | if constexpr(std::is_same_v>) // specialisation for std::string 92 | return label.empty(); 93 | if constexpr(std::is_same_v>) // specialisation for String 94 | return label.isEmpty(); 95 | #if ARDUINOJSON_VERSION_MAJOR <= 7 && ARDUINOJSON_VERSION_MINOR < 3 96 | if constexpr(std::is_same_v>) 97 | return label.isNull(); 98 | if constexpr(std::is_same_v>) 99 | return label.isNull(); 100 | #else 101 | if constexpr(std::is_same_v>) 102 | return label.isNull(); 103 | if constexpr(std::is_same_v>) 104 | return label.isNull(); 105 | #endif 106 | 107 | return false; // UB, not a known string type for us 108 | }; 109 | 110 | template 111 | typename std::enable_if,bool>::type 112 | is_empty_string(const T label){ 113 | if constexpr(std::is_same_v>) // specialisation for std::string_view 114 | return label.empty(); 115 | if constexpr(std::is_same_v>) // specialisation for const char* 116 | return not (label && *label); 117 | if constexpr(std::is_same_v>) // specialisation for char* 118 | return not (label && *label); 119 | return false; // UB, not a known string type for us 120 | }; 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /src/ts.cpp: -------------------------------------------------------------------------------- 1 | //This is the only .cpp file that gets the #include. 2 | //Without it, the linker would not find necessary TaskScheduler's compiled code. 3 | // 4 | //Remember to put customization macros here as well. 5 | #define _TASK_STD_FUNCTION // Compile with support for std::function 6 | #define _TASK_SCHEDULING_OPTIONS 7 | #define _TASK_SELF_DESTRUCT 8 | #include 9 | 10 | // TaskScheduler - Let the runner object be a global, single instance shared between object files. 11 | Scheduler ts; 12 | 13 | /* 14 | * Add this to your sources if you want to reuse task scheduler object for your tasks 15 | extern Scheduler ts; 16 | */ 17 | 18 | /* 19 | Note that call to tasker 'ts.execute();' is made in 'embui.handle();' 20 | make sure to add it to the loop() 21 | */ -------------------------------------------------------------------------------- /src/ts.h: -------------------------------------------------------------------------------- 1 | // This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 2 | // then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 3 | // also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 4 | // and others people 5 | 6 | #pragma once 7 | // Task Scheduler lib https://github.com/arkhipenko/TaskScheduler 8 | #define _TASK_STD_FUNCTION // Compile with support for std::function. 9 | #define _TASK_SCHEDULING_OPTIONS 10 | #define _TASK_SELF_DESTRUCT 11 | #include 12 | 13 | // TaskScheduler - Let the runner object be a global, single instance shared between object files. 14 | extern Scheduler ts; 15 | -------------------------------------------------------------------------------- /src/ui.cpp: -------------------------------------------------------------------------------- 1 | // This framework originaly based on JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov 2 | // then re-written and named by (c) 2020 Anton Zolotarev (obliterator) (https://github.com/anton-zolotarev) 3 | // also many thanks to Vortigont (https://github.com/vortigont), kDn (https://github.com/DmytroKorniienko) 4 | // and others people 5 | 6 | #include "ui.h" 7 | #include "embuifs.hpp" 8 | 9 | static constexpr const char* MGS_empty_stack = "no opened section for an object!"; 10 | static constexpr const char* MGS_no_store = "no-store"; 11 | 12 | Interface::~Interface(){ 13 | json_frame_clear(); 14 | if (_delete_handler_on_destruct){ 15 | delete send_hndl; 16 | send_hndl = nullptr; 17 | } 18 | } 19 | 20 | JsonArray Interface::json_block_get(){ 21 | return section_stack.size() ? section_stack.back().block : JsonArray(); 22 | }; 23 | 24 | JsonObject Interface::json_object_get(){ 25 | return section_stack.size() ? JsonObject (section_stack.back().block[section_stack.back().block.size()-1]) : JsonObject(); 26 | }; 27 | 28 | JsonObject Interface::json_object_add(const JsonVariantConst obj){ 29 | LOGV(P_EmbUI, printf, "Frame obj add %u items\n", obj.size()); 30 | 31 | //(section_stack.size() ? section_stack.back().block.add() : json.as()) 32 | if (!section_stack.size()) { LOGW(P_EmbUI, println, MGS_empty_stack); return {}; } 33 | if ( section_stack.back().block.add(obj) ){ 34 | LOGV(P_EmbUI, printf, "...OK idx:%u\theap free: %u\n", section_stack.back().idx, ESP.getFreeHeap()); 35 | section_stack.back().idx++; // incr idx for next obj 36 | } 37 | // return newly added object reference 38 | return json_object_get(); 39 | } 40 | 41 | JsonObject Interface::json_frame(const char* type, const char* section_id){ 42 | json_frame_flush(); // ensure to start a new frame purging any existing data 43 | json[P_pkg] = type; 44 | json[P_final] = false; 45 | json_section_begin(section_id); 46 | return json.as(); 47 | }; 48 | 49 | void Interface::json_frame_clear(){ 50 | section_stack.clear(); 51 | json.clear(); 52 | } 53 | 54 | void Interface::json_frame_flush(){ 55 | if (!section_stack.size()) return; 56 | json[P_final] = true; 57 | json_section_end(); 58 | LOGD(P_EmbUI, println, "json_frame_flush"); 59 | _json_frame_send(); 60 | json_frame_clear(); 61 | } 62 | 63 | void Interface::json_frame_send(){ 64 | _json_frame_send(); 65 | _json_frame_next(); 66 | } 67 | 68 | void Interface::_json_frame_next(){ 69 | if (!section_stack.size()) return; 70 | json.clear(); 71 | JsonObject obj = json.to(); 72 | if (section_stack.size() > 1){ 73 | size_t idx{0}; 74 | for ( auto i = section_stack.begin(); i != section_stack.end(); ++i ){ 75 | if (idx++) 76 | obj = (*std::prev(i)).block.add(); 77 | obj[P_section] = (*i).name; 78 | obj[P_idx] = (*i).idx; 79 | (*i).block = obj[P_block].to(); 80 | //LOG(printf, "nesting section:'%s' [#%u] idx:%u\n", section_stack[i]->name.isEmpty() ? "-" : section_stack[i]->name.c_str(), i, section_stack[i]->idx); 81 | } 82 | } 83 | LOGI(P_EmbUI, printf, "json_frame_next: [#%d]\n", section_stack.size()-1); // section index counts from 0 84 | } 85 | 86 | JsonObject Interface::json_frame_value(const JsonVariantConst val){ 87 | json_frame_flush(); // ensure this will purge existing frame 88 | json_frame(P_value); 89 | return json_object_add(val); 90 | } 91 | 92 | void Interface::json_section_end(){ 93 | if (!section_stack.size()) return; 94 | 95 | section_stack.erase(std::prev( section_stack.end() )); 96 | if (section_stack.size()) { 97 | section_stack.back().idx++; 98 | LOGD(P_EmbUI, printf, "section end #%u '%s'\n", section_stack.size(), section_stack.back().name.isEmpty() ? "-" : section_stack.back().name.c_str()); 99 | } 100 | } 101 | 102 | JsonObject Interface::json_object_create(){ 103 | if (!section_stack.size()) { LOGW(P_EmbUI, println, MGS_empty_stack); return JsonObject(); } 104 | section_stack.back().idx++; // incr idx for next obj 105 | return section_stack.back().block.add(); 106 | } 107 | 108 | 109 | JsonObject Interface::uidata_xload(const char* key, const char* url, const char* src_path, bool merge){ 110 | JsonObject obj(json_object_create()); 111 | obj[P_action] = P_xload; 112 | obj[P_key] = key; 113 | obj[P_url] = url; 114 | if (src_path) 115 | obj[P_src] = src_path; 116 | if (merge) 117 | obj[P_merge] = true; 118 | //if (version) obj[P_version] = version; 119 | return obj; 120 | } 121 | 122 | JsonObject Interface::uidata_pick(const char* key, const char* prefix, const char* suffix){ 123 | JsonObject obj(json_object_create()); 124 | obj[P_action] = P_pick; 125 | obj[P_key] = key; 126 | if (!embui_traits::is_empty_string(prefix)) 127 | obj[P_prefix] = const_cast(prefix); 128 | if (!embui_traits::is_empty_string(suffix)) 129 | obj[P_suffix] = const_cast(suffix); 130 | return obj; 131 | } 132 | 133 | JsonObject Interface::uidata_set(const char* key, JsonVariantConst data){ 134 | JsonObject obj(json_object_create()); 135 | obj[P_action] = P_set; 136 | obj[P_key] = key; 137 | obj[P_data] = data; 138 | return obj; 139 | } 140 | 141 | JsonObject Interface::uidata_merge(const char* key, JsonVariantConst data){ 142 | JsonObject obj(json_object_create()); 143 | obj[P_action] = P_merge; 144 | obj[P_key] = key; 145 | obj[P_data] = data; 146 | return obj; 147 | } 148 | 149 | /** 150 | * @brief - serialize and send json obj directly to the ws buffer 151 | */ 152 | void FrameSendWSServer::send(const JsonVariantConst& data){ 153 | if (!available()) { LOGW(P_EmbUI, println, "FrameSendWSServer::send - not available!"); return; } // no need to do anything if there is no clients connected 154 | 155 | size_t length = measureJson(data); 156 | auto buffer = ws->makeBuffer(length); 157 | if (!buffer){ 158 | LOGW(P_EmbUI, println, "FrameSendWSServer::send - no buffer!"); 159 | return; 160 | } 161 | 162 | serializeJson(data, (char*)buffer->get(), length); 163 | ws->textAll(buffer); 164 | }; 165 | 166 | /** 167 | * @brief - serialize and send json obj directly to the ws buffer 168 | */ 169 | void FrameSendWSClient::send(const JsonVariantConst& data){ 170 | if (!available()) return; // no need to do anything if there is no clients connected 171 | 172 | size_t length = measureJson(data); 173 | auto buffer = cl->server()->makeBuffer(length); 174 | if (!buffer) 175 | return; 176 | 177 | serializeJson(data, (char*)buffer->get(), length); 178 | cl->text(buffer); 179 | }; 180 | 181 | void FrameSendChain::remove(int id){ 182 | _hndlr_chain.remove_if([id](HndlrChain &c){ return id == c.id; }); 183 | }; 184 | 185 | bool FrameSendChain::available() const { 186 | for (const auto &i : _hndlr_chain) 187 | if (i.handler->available()) return true; 188 | 189 | return false; 190 | }; 191 | 192 | 193 | int FrameSendChain::add(std::unique_ptr&& handler){ 194 | _hndlr_chain.emplace_back(std::forward>(handler)); 195 | return _hndlr_chain.back().id; 196 | } 197 | 198 | void FrameSendChain::send(const JsonVariantConst& data){ 199 | for (auto &i : _hndlr_chain) 200 | i.handler->send(data); 201 | } 202 | 203 | void FrameSendChain::send(const char* data){ 204 | for (auto &i : _hndlr_chain) 205 | i.handler->send(data); 206 | } 207 | 208 | void FrameSendAsyncJS::send(const JsonVariantConst& data){ 209 | /* 210 | this is HTTP responce - we can send only ONCE! 211 | a reply might be malformed if contains split sections, 212 | but I do not have proper solution for this. 213 | More complicated API calls should use websocket 214 | */ 215 | if (flushed) return; 216 | 217 | response = new AsyncJsonResponse(); 218 | 219 | // TODO 220 | // this is not beautiful and requres double mem to send Object 221 | embuifs::obj_deepmerge(response->getRoot(), data); 222 | response->setLength(); 223 | req->send(response); 224 | flushed = true; 225 | }; 226 | 227 | FrameSendAsyncJS::~FrameSendAsyncJS() { 228 | if (!flushed){ 229 | // there were no usefull data, send proper empty reply 230 | req->send(204); 231 | } 232 | } 233 | 234 | void FrameSendHttp::send(const char* data){ 235 | AsyncWebServerResponse* r = req->beginResponse(200, asyncsrv::T_application_json, data); 236 | r->addHeader(asyncsrv::T_Cache_Control, MGS_no_store); 237 | req->send(r); 238 | } 239 | 240 | void FrameSendHttp::send(const JsonVariantConst& data) { 241 | AsyncResponseStream* stream = req->beginResponseStream(asyncsrv::T_application_json); 242 | stream->addHeader(asyncsrv::T_Cache_Control, MGS_no_store); 243 | serializeJson(data, *stream); 244 | req->send(stream); 245 | }; 246 | --------------------------------------------------------------------------------