diff --git a/PrepareProject.py b/PrepareProject.py index 6d480783..2f8c1241 100644 --- a/PrepareProject.py +++ b/PrepareProject.py @@ -13,6 +13,8 @@ # python PrepareProject.py --profile <ИмяФайла> # python PrepareProject.py -p <ИмяФайла> # +# Используя параметры -b или --board можно уточнить для какой платы нужно подготовить проект +# # поддерживаемые контроллеры (профили): # esp8266_4mb # esp8266_16mb @@ -223,12 +225,12 @@ allAPI_head = "" allAPI_exec = "" for activModuleName in activeModulesName: allAPI_head = allAPI_head + "\nvoid* getAPI_" + activModuleName + "(String subtype, String params);" - allAPI_exec = allAPI_exec + "\nif ((tmpAPI = getAPI_" + activModuleName + "(subtype, params)) != nullptr) return tmpAPI;" + allAPI_exec = allAPI_exec + "\nif ((tmpAPI = getAPI_" + activModuleName + "(subtype, params)) != nullptr) foundAPI = tmpAPI;" apicpp = '#include "ESPConfiguration.h"\n' apicpp = apicpp + allAPI_head -apicpp = apicpp + '\n\nvoid* getAPI(String subtype, String params) {\nvoid* tmpAPI;' +apicpp = apicpp + '\n\nvoid* getAPI(String subtype, String params) {\nvoid* tmpAPI; void* foundAPI = nullptr;' apicpp = apicpp + allAPI_exec -apicpp = apicpp + '\nreturn nullptr;\n}' +apicpp = apicpp + '\nreturn foundAPI;\n}' with open('src/modules/API.cpp', 'w') as f: f.write(apicpp) diff --git a/PrepareServer.py b/PrepareServer.py index 7289a2f5..d0750773 100644 --- a/PrepareServer.py +++ b/PrepareServer.py @@ -26,8 +26,8 @@ config.read("platformio.ini") deviceName = config["platformio"]["default_envs"] homeDir = os.path.expanduser('~') -os.system(homeDir + "\.platformio\penv\Scripts\pio run") -os.system(homeDir + "\.platformio\penv\Scripts\pio run -t buildfs --disable-auto-clean") +os.system(homeDir + "/.platformio/penv/Scripts/pio run") +os.system(homeDir + "/.platformio/penv/Scripts/pio run -t buildfs --disable-auto-clean") if copyFileIfExist("firmware.bin", deviceName) and copyFileIfExist("littlefs.bin", deviceName): copyFileIfExist("partitions.bin", deviceName) diff --git a/include/utils/StringUtils.h b/include/utils/StringUtils.h index 9fac0aa5..62dca319 100644 --- a/include/utils/StringUtils.h +++ b/include/utils/StringUtils.h @@ -12,6 +12,8 @@ uint8_t hexStringToUint8(const String& hex); uint16_t hexStringToUint16(const String& hex); +uint32_t hexStringToUint32(const String& hex); + String selectToMarkerLast(String str, const String& found); String selectToMarker(String str, const String& found); @@ -49,3 +51,5 @@ unsigned char ChartoHex(char ch); std::vector splitStr(const String& str, const String& delimiter); bool strInVector(const String& str, const std::vector& vec); + +String getUtf8CharByIndex(const String& utf8str, int index); diff --git a/run.py b/run.py new file mode 100644 index 00000000..44edf8f5 --- /dev/null +++ b/run.py @@ -0,0 +1,111 @@ +# Скрипт для простой прошивки ESP с учетом профиля из myProfile.json и поиска нужной кнопочки в интерфейсе PlatformIO +# Если ничего не указано в параметрах, то выполняется последовательно набор команд: +# 1. run PrepareProject.py +# 2. platformio run -t clean +# 3. platformio run -t uploadfs -е default_envs +# 4. platformio run -t upload -е default_envs +# 5. platformio run -t monitor +# где default_envs - это параметр default_envs из myProfile.json +# +# Если указан параметр -p или --profile <ИмяФайла>, то выполняется первая команда PrepareProject.py -p <ИмяФайла> +# Если указан параметр -b или --board , то выполняется первая команда PrepareProject.py -b +# Если указан парамтер -l или --lite, то пропускаются команды 1, 2 и 5 с предварительной компиляцией +# Если указан параметр -d или --debug, то выполняется только команда 4 с предварительной компиляцией + +import os +import subprocess +import sys +import json + +def get_platformio_path(): + """ + Возвращает путь к PlatformIO в зависимости от операционной системы. + """ + if os.name == 'nt': # Windows + return os.path.join(os.environ['USERPROFILE'], '.platformio', 'penv', 'Scripts', 'pio.exe') + else: # Linux/MacOS + return os.path.join(os.environ['HOME'], '.platformio', 'penv', 'bin', 'pio') + +def load_default_envs(profile_path="myProfile.json"): + """ + Загружает значение default_envs из файла myProfile.json. + """ + if not os.path.isfile(profile_path): + print(f"Файл профиля {profile_path} не найден.") + sys.exit(1) + + try: + with open(profile_path, 'r', encoding='utf-8') as file: + profile_data = json.load(file) + return profile_data["projectProp"]["platformio"]["default_envs"] + except KeyError: + print("Не удалось найти ключ 'default_envs' в myProfile.json.") + sys.exit(1) + except json.JSONDecodeError: + print(f"Ошибка при чтении файла {profile_path}: некорректный JSON.") + sys.exit(1) + +def run_command(command): + """ + Выполняет указанную команду в subprocess. + """ + try: + print(f"Выполнение команды: {' '.join(command)}") + subprocess.run(command, check=True) + except subprocess.CalledProcessError as e: + print(f"Ошибка при выполнении команды: {e}") + sys.exit(e.returncode) + +def run_platformio(): + """ + Основная логика выполнения команд в зависимости от параметров. + """ + pio_path = get_platformio_path() + + # Проверяем, существует ли PlatformIO + if not os.path.isfile(pio_path): + print(f"PlatformIO не найден по пути: {pio_path}") + sys.exit(1) + # print(f"PlatformIO найден по пути: {pio_path}") + + # Читаем аргументы командной строки + args = sys.argv[1:] + lite_mode = '-l' in args or '--lite' in args + debug_mode = '-d' in args or '--debug' in args + profile_index = next((i for i, arg in enumerate(args) if arg in ('-p', '--profile')), None) + profile_file = args[profile_index + 1] if profile_index is not None and len(args) > profile_index + 1 else "myProfile.json" + + # Загружаем default_envs из myProfile.json, если не указан параметр -b, который имеет больший приоритет + board_index = next((i for i, arg in enumerate(args) if arg in ('-b', '--board')), None) + default_envs = args[board_index + 1] if board_index is not None and len(args) > board_index + 1 else load_default_envs(profile_path=profile_file) + + print(f"Используем default_envs: {default_envs}") + print(f"Режим Lite: {lite_mode}, Режим отладки: {debug_mode}") + print(f"Профиль: {profile_file}") + + # Выполнение команд в зависимости от параметров + if not lite_mode and not debug_mode: + # Полный набор команд + run_command(['python', 'PrepareProject.py', '-p', profile_file]) + + # Добавляем сообщение о необходимости дождаться завершения обновления конфигурации + input(f"\x1b[1;31;42m Подождите завершения обновления конфигурации PlatformIO, затем нажмите Ввод для продолжения...\x1b[0m") + + run_command([pio_path, 'run', '-t', 'clean']) + run_command([pio_path, 'run', '-t', 'uploadfs', '--environment', default_envs]) + run_command([pio_path, 'run', '-t', 'upload', '--environment', default_envs]) + run_command([pio_path, 'run', '-t', 'monitor']) + elif lite_mode: + # Упрощенный режим (пропускаем команды 1, 2 и 5) + run_command([pio_path, 'run', '-t', 'buildfs', '--environment', default_envs]) + run_command([pio_path, 'run', '-t', 'uploadfs', '--environment', default_envs]) + run_command([pio_path, 'run', '--environment', default_envs]) + run_command([pio_path, 'run', '-t', 'upload', '--environment', default_envs]) + elif debug_mode: + # Режим отладки (только команда 4) + run_command([pio_path, 'run', '--environment', default_envs]) + run_command([pio_path, 'run', '-t', 'upload', '--environment', default_envs]) + +if __name__ == "__main__": + run_platformio() + diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index e1d8543e..d920c77b 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -292,6 +292,11 @@ class CallExprAST : public ExprAST { ret.valD = Item->getIntFromNet(); ret.isDecimal = true; return &ret; + + } else if (Cmd == F("doByInterval")) { // вызываем системную функцию периодического выполнения вне таймера + Item->doByInterval(); + ret = Item->value; + return &ret; } // если все же все ок, то готовим параметры для передачи в модуль @@ -305,6 +310,15 @@ class CallExprAST : public ExprAST { return nullptr; // ArgsAsIoTValue.push_back(zeroIotVal); } + if (Cmd == F("setInterval")) { // меняем интервал выполнения задач модуля налету + if (ArgsAsIoTValue.size() == 1) { + Item->setInterval(ArgsAsIoTValue[0].valD); + ret.valD = Item->getInterval(); + ret.isDecimal = true; + return &ret; + } + } + ret = Item->execute(Cmd, ArgsAsIoTValue); // вызываем команду из модуля напрямую с передачей всех аргументов // if (ret.isDecimal) Serial.printf("Call from CallExprAST ID = %s, Command = %s, exec result = %f\n", Callee.c_str(), Cmd.c_str(), ret.valD); @@ -345,6 +359,7 @@ enum SysOp { sysop_getUptime, sysop_mqttIsConnect, sysop_wifiIsConnect, + sysop_setInterval, sysop_addPortMap }; @@ -452,6 +467,11 @@ IoTValue sysExecute(SysOp command, std::vector ¶m) { case sysop_wifiIsConnect: value.valD = isNetworkActive(); break; + case sysop_setInterval: + if (param.size() == 1) { + + } + break; case sysop_addPortMap: if (param.size() == 5) { addPortMap(param[0].valS, param[1].valS, param[2].valD, param[3].valS, param[4].valD); @@ -514,7 +534,9 @@ class SysCallExprAST : public ExprAST { else if (Callee == F("mqttIsConnect")) operation = sysop_mqttIsConnect; else if (Callee == F("wifiIsConnect")) - operation = sysop_wifiIsConnect; + operation = sysop_wifiIsConnect; + else if (Callee == F("setInterval")) + operation = sysop_setInterval; else if (Callee == F("addPortMap")) operation = sysop_addPortMap; else diff --git a/src/modules/display/DwinI/DwinI.cpp b/src/modules/display/DwinI/DwinI.cpp index 3df8fe8e..ccaa7c9f 100644 --- a/src/modules/display/DwinI/DwinI.cpp +++ b/src/modules/display/DwinI/DwinI.cpp @@ -140,6 +140,8 @@ class DwinI : public IoTUart { void onModuleOrder(String &key, String &value) { if (key == "uploadUI") { //SerialPrint("i", F("DwinI"), "Устанавливаем UI: " + value); + if (value != "") uartPrintHex(value.c_str()); + } } diff --git a/src/modules/display/LedFX/LedFX.cpp b/src/modules/display/LedFX/LedFX.cpp new file mode 100644 index 00000000..b692c811 --- /dev/null +++ b/src/modules/display/LedFX/LedFX.cpp @@ -0,0 +1,332 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include "ESPConfiguration.h" +#include + + + +WS2812FX *_glob_strip = nullptr; // глобальный указатель на WS2812FX для использования в функциях для кастомных эффектов +std::vector vuMeterBands; // массив указателей на элементы IoTValue, которые будут использоваться для передачи данных в эффект VU Meter + +uint16_t vuMeter2(void) { +// функция взята из WS2812FX.cpp для демонстрации возможности создания своего алгоритма эффекта +// в данном случае - VU Meter имеет тот же смысл отображения уровня сигнала, что и в WS2812FX с сохранением алгоритма вывода данных из нескольких источников +// но вместо использования внешнего источника данных для встроенного эффекта, мы используем данные из других элементов конфигурации IoTM +// Если данные не поступают (IoTValue.valS == "1"), то используется генерация случайных чисел для демонстрации работы эффекта + + if (_glob_strip == nullptr) return 0; // Проверяем, инициализирована ли библиотека WS2812FX + + uint16_t numBands = vuMeterBands.size(); // Получаем количество полос VU Meter из массива указателей + if (numBands == 0) return 0; // Если нет полос, выходим из функции + + WS2812FX::Segment* seg = _glob_strip->getSegment(); + uint16_t seglen = seg->stop - seg->start + 1; + uint16_t bandSize = seglen / numBands; + + if (vuMeterBands[0]->valS == "R") + for (uint8_t i=0; i < numBands; i++) { + int randomData = vuMeterBands[i]->valD + _glob_strip->random8(32) - _glob_strip->random8(32); + vuMeterBands[i]->valD = (randomData < 0 || randomData > 255) ? 128 : randomData; + } + + for(uint8_t i=0; ivalD)); + uint8_t scaledBand = (vuMeterBands[i]->valD * bandSize) / 256; + for(uint16_t j=0; jstart + (i * bandSize) + j; + if(j <= scaledBand) { + if(j < bandSize - 4) _glob_strip->setPixelColor(index, GREEN); + else if(j < bandSize - 2) _glob_strip->setPixelColor(index, YELLOW); + else _glob_strip->setPixelColor(index, RED); + } else { + _glob_strip->setPixelColor(index, BLACK); + } + } + } + _glob_strip->setCycle(); + + return seg->speed; +} + + +class LedFX : public IoTItem +{ +private: + WS2812FX *_strip; + + int _data_pin = 2; + int _numLeds = 1; + int _brightness = 100; + int _speed = 15000; + int _effectsMode = 0; + int _valueMode = 0; + int _color = 0xFF0000; // default color red + + uint8_t _BrightnessFadeOutStep = 0; + uint8_t _BrightnessFadeOutMin = 1; + uint8_t _BrightnessFadeInStep = 0; + uint8_t _BrightnessFadeInMax = 50; + +public: + LedFX(String parameters) : IoTItem(parameters) { + jsonRead(parameters, F("data_pin"), _data_pin); + jsonRead(parameters, F("speed"), _speed); + jsonRead(parameters, F("numLeds"), _numLeds); + jsonRead(parameters, F("brightness"), _brightness); + + String tmpStr; + jsonRead(parameters, F("color"), tmpStr); + _color = hexStringToUint32(tmpStr); + + jsonRead(parameters, F("effectsMode"), _effectsMode); + jsonRead(parameters, F("valueMode"), _valueMode); + + + //_strip = new WS2812FX(_numLeds, _data_pin, NEO_BRG + NEO_KHZ400); // SM16703 + _strip = new WS2812FX(_numLeds, _data_pin, NEO_GRB + NEO_KHZ800); // WS2812B + + if (_strip != nullptr) { + _glob_strip = _strip; // Сохраняем указатель в глобальной переменной + + _strip->init(); + _strip->setBrightness(_brightness); + _strip->setSpeed(_speed); + _strip->setMode(_effectsMode); + _strip->setColor(_color); + if (_effectsMode >= 0) _strip->start(); + } + } + + void fadeOutNonBlocking() { + + } + + void loop() { + if (!_strip) return; + + static unsigned long lastUpdate = 0; // Время последнего обновления + unsigned long now = millis(); + if (now - lastUpdate >= 70) { // Проверяем, прошло ли достаточно времени + lastUpdate = now; + + if (_BrightnessFadeOutStep > 0) { + int currentBrightness = _strip->getBrightness(); // Получаем текущую яркость + currentBrightness -= _BrightnessFadeOutStep; + if (currentBrightness < _BrightnessFadeOutMin) { + currentBrightness = _BrightnessFadeOutMin; // Убедимся, что яркость не уйдет в отрицательные значения + _BrightnessFadeOutStep = 0; // Останавливаем затухание + } + _strip->setBrightness(currentBrightness); + _strip->show(); + } + + if (_BrightnessFadeInStep > 0) { + int currentBrightness = _strip->getBrightness(); // Получаем текущую яркость + currentBrightness += _BrightnessFadeInStep; + if (currentBrightness > _BrightnessFadeInMax) { + currentBrightness = _BrightnessFadeInMax; // Убедимся, что яркость не уйдет за пределы + _BrightnessFadeInStep = 0; // Останавливаем затухание + } + _strip->setBrightness(currentBrightness); + _strip->show(); + } + } + + _strip->service(); + IoTItem::loop(); + } + + void doByInterval() { + + } + + IoTValue execute(String command, std::vector ¶m) { + if (!_strip) return {}; + if (command == "fadeOut") { + if (param.size() == 2) { + _BrightnessFadeOutMin = param[0].valD; + _BrightnessFadeOutStep = param[1].valD; + SerialPrint("E", "Strip LedFX", "BrightnessFadeOut"); + } + + } else if (command == "fadeIn") { + if (param.size() == 2) { + _BrightnessFadeInMax = param[0].valD; + _BrightnessFadeInStep = param[1].valD; + SerialPrint("E", "Strip LedFX", "BrightnessFadeIn"); + } + + } else if (command == "setColor") { + if (param.size() == 1) { + _color = hexStringToUint32(param[0].valS); + _strip->setColor(_color); + _strip->show(); + SerialPrint("E", "Strip LedFX", "setColor:" + param[0].valS); + } + + } else if (command == "setEffect") { + if (param.size() == 1) { + if (param[0].valD < 0 || param[0].valD > 79) + _effectsMode = random(0, 79); + else + _effectsMode = param[0].valD; + _strip->setMode(_effectsMode); + _strip->show(); + _strip->start(); + SerialPrint("E", "Strip LedFX", "setEffect:" + param[0].valS); + } + + } else if (command == "setSpeed") { + if (param.size() == 1) { + _speed = param[0].valD; + _strip->setSpeed(_speed); + _strip->show(); + SerialPrint("E", "Strip LedFX", "setSpeed:" + param[0].valS); + } + + } else if (command == "setBrightness") { + if (param.size() == 1) { + _brightness = param[0].valD; + _strip->setBrightness(_brightness); + _strip->show(); + SerialPrint("E", "Strip LedFX", "setBrightness:" + param[0].valS); + } + + } else if (command == "stop") { + _strip->stop(); + SerialPrint("E", "Strip LedFX", "stop"); + + } else if (command == "start") { + _strip->start(); + SerialPrint("E", "Strip LedFX", "start"); + + } else if (command == "pause") { + _strip->pause(); + SerialPrint("E", "Strip LedFX", "pause"); + + } else if (command == "resume") { + _strip->resume(); + SerialPrint("E", "Strip LedFX", "resume"); + + } else if (command == "setSegment") { + if (param.size() == 6) { + _strip->setSegment(param[0].valD, param[1].valD, param[2].valD, param[3].valD, hexStringToUint32(param[4].valS), param[5].valD); + _strip->show(); + _strip->start(); + SerialPrint("E", "Strip LedFX", "setSegment:" + param[0].valS + " start:" + param[1].valS + " stop:" + param[2].valS + " mode:" + param[3].valS, " color:" + param[4].valS + " speed:" + param[5].valS); + } + + } else if(command == "noShowOne"){ + if (param.size() == 1) { + _strip->setPixelColor(param[0].valD, _strip->Color(0, 0, 0)); + _strip->show(); + SerialPrint("E", "Strip LedFX", "noShowOne"); + } + + } else if (command == "showLed"){ + if (param.size() == 2) { + uint32_t color = hexStringToUint32(param[1].valS); + _strip->setPixelColor(param[0].valD, color); + _strip->show(); + _strip->start(); + SerialPrint("E", "Strip LedFX", "showLed:" + param[0].valS + " color:" + param[1].valS); + } + + } else if (command == "vuMeter") { + if (param.size() == 2) { + int bandCnt = param[0].valD; + if (param[1].valS == "") { + for (int i=0; i < vuMeterBands.size(); i++) { + delete vuMeterBands[i]; + } + vuMeterBands.clear(); + + for (uint8_t i=0; i < bandCnt; i++) { + IoTValue *band = new IoTValue(); // создаем новый элемент IoTValue для полос VU Meter + band->valD = 0; + band->valS = "R"; + vuMeterBands.push_back(band); // добавляем указатель в массив + } + } else { + // Очищаем массив vuMeterBands перед заполнением + vuMeterBands.clear(); + + String id; + String idsStr = param[1].valS; + // Разделяем строку idsStr на идентификаторы, используя запятую как разделитель + while (idsStr.length() > 0) { + // Извлекаем идентификатор до первой запятой + id = selectToMarker(idsStr, ","); + + // Ищем элемент IoTItem по идентификатору + IoTItem* item = findIoTItem(id); + if (item != nullptr) { + // Добавляем указатель на поле value найденного элемента в vuMeterBands + vuMeterBands.push_back(&(item->value)); + SerialPrint("E", "LedFX", "Добавлен элемент в vuMeterBands: " + id); + } else { + SerialPrint("E", "LedFX", "Элемент не найден: " + id); + } + + int8_t oldSize = idsStr.length(); + // Удаляем обработанный идентификатор из строки + idsStr = deleteBeforeDelimiter(idsStr, ","); + if (idsStr.length() == oldSize) { + // Если длина строки не изменилась, значит, больше нет запятых и это был последний идентификатор + break; + } + } + + } + _strip->setCustomMode(vuMeter2); + _strip->setMode(FX_MODE_CUSTOM); + _strip->start(); + SerialPrint("E", "Strip LedFX", "vuMeter bands:" + param[0].valS + " IDs to show:" + param[1].valS); + } + } + + return {}; + } + + void setValue(const IoTValue& Value, bool genEvent = true) { + if (!_strip) return; + + if (_valueMode == 0) { + _strip->setMode(Value.valD); + _effectsMode = Value.valD; + } else if (_valueMode == 1) { + _strip->setBrightness(Value.valD); + _brightness = Value.valD; + } else if (_valueMode == 2) { + _color = hexStringToUint32(Value.valS); + _strip->setColor(_color); + } else if (_valueMode == 3) { + _strip->setSpeed(Value.valD); + _speed = Value.valD; + } + + value = Value; + regEvent(value.valD, "LedFX", false, genEvent); + } + + ~LedFX() { + if (_strip != nullptr) { + delete _strip; + _strip = nullptr; + _glob_strip = nullptr; // Обнуляем глобальный указатель + } + }; +}; + +void *getAPI_LedFX(String subtype, String param) +{ + if (subtype == F("LedFX")) { + return new LedFX(param); + } else { + return nullptr; + } +} + + + + diff --git a/src/modules/display/LedFX/modinfo.json b/src/modules/display/LedFX/modinfo.json new file mode 100644 index 00000000..5c0ad13b --- /dev/null +++ b/src/modules/display/LedFX/modinfo.json @@ -0,0 +1,163 @@ +{ + "menuSection": "screens", + "configItem": [ + { + "global": 0, + "name": "LedFX", + "type": "Reading", + "subtype": "LedFX", + "id": "fl", + "widget": "inputTxt", + "page": "Кнопки", + "descr": "Лента", + "int": 15, + "needSave": 0, + + "data_pin": "2", + "numLeds": "3", + "brightness": "50", + "speed": "3000", + "color": "0xFF0000", + "effectsMode": 0, + "valueMode": 0 + } + ], + "about": { + "authorName": "Ilya Belyakov", + "authorContact": "https://t.me/Biveraxe", + "authorGit": "https://github.com/biveraxe", + "exampleURL": "https://iotmanager.org/wiki", + "specialThanks": "Yuriy Kuneev (https://t.me/Kuneev07)", + "moduleName": "LedFX", + "moduleVersion": "1.0.1", + "moduleDesc": "Позволяет управлять адресными светодиодными лентами WS2812B и аналогичными.", + "propInfo": { + "int": "Период времени в секундах обновления.", + "data_pin": "Пин к которому подключена лента.", + "speed": "Скорость обновления ленты.", + "numLeds": "Количество пикселей в ленте.", + "needSave": "Запись значения элемента в энергонезависимую память", + "brightness": "Яркость ленты можно менять из сценария.", + "color": "Цвет ленты в формате 0xRRGGBB, например, 0xFF0000 - красный, 0x00FF00 - зеленый, 0x0000FF - синий.", + "effectsMode": "Режим эффектов ленты. 0-79. 0 - Статичный цвет.", + "valueMode": "Режим применения значения элемета. 0 - установка режима эффектов, 1 - регулирование яркости, 2 - изменение цвета, 3 - изменение скорости." + }, + "title": "Адресная светодиодная лента", + "funcInfo": [ + { + "name": "noShowOne", + "descr": "Выключить один светодиод на ленте", + "params": [ + "номер пикселя" + ] + }, + { + "name": "showLed", + "descr": "Зажечь один диод", + "params": [ + "номер пикселя", + "цвет в формате 0xRRGGBB" + ] + }, + { + "name": "setBrightness", + "descr": "Устанавливает общую яркость ленты от 0 до 255", + "params": [ + "яркость от 0 до 255" + ] + }, + { + "name": "vuMeter", + "descr": "Включает режим VU Meter. Важно что бы элемент ленты был ниже в списке чем элемент с датчиком, ИД которого нужно будет мониторить.", + "params": [ + "Количество каналов для отображения на ленте", + "Список ID датчиков через запятую (если указать пустую строку, то каналы будут заполняться случайными числами от 0 до 255)" + ] + }, + { + "name": "setColor", + "descr": "Устанавливает цвет ленты в формате 0xRRGGBB, например, 0xFF0000 - красный, 0x00FF00 - зеленый, 0x0000FF - синий.", + "params": [ + "цвет в формате 0xRRGGBB" + ] + }, + { + "name": "setEffect", + "descr": "Устанавливает эффект ленты. 0-79. 0 - Статичный цвет.", + "params": [ + "номер эффекта от 0 до 79" + ] + }, + { + "name": "setSpeed", + "descr": "Устанавливает скорость эффекта от 0 до 255", + "params": [ + "скорость от 0 до 255" + ] + }, + { + "name": "fadeOut", + "descr": "Плавное затухание яркости", + "params": [ + "Целевое значение яркости, до которого будет затухать", + "Шаг затухания" + ] + }, + { + "name": "fadeIn", + "descr": "Плавное нарастание яркости", + "params": [ + "Целевое значение яркости, до которого будет нарастать", + "Шаг нарастания" + ] + }, + { + "name": "stop", + "descr": "Останавливает эффект", + "params": [] + + }, + { + "name": "start", + "descr": "Запускает эффект", + "params": [] + + }, + { + "name": "pause", + "descr": "Пауза эффекта", + "params": [] + + }, + { + "name": "resume", + "descr": "Возобновляет эффект", + "params": [] + + }, + { + "name": "setSegment", + "descr": "Устанавливает сегмент ленты", + "params": [ + "номер сегмента от 0 до 7", + "глобальный номер первого пикселя в сегменте", + "глобальный номер последнего пикселя в сегменте", + "номер эффекта от 0 до 79", + "цвет в формате 0xRRGGBB, например, 0xFF0000 - красный, 0x00FF00 - зеленый, 0x0000FF - синий.", + "скорость" + ] + } + ] + }, + "defActive": false, + "usedLibs": { + "esp32*": [ + "adafruit/Adafruit NeoPixel @ ^1.12.5", + "kitesurfer1404/WS2812FX @ ^1.4.5" + ], + "esp82*": [ + "adafruit/Adafruit NeoPixel @ ^1.12.5", + "kitesurfer1404/WS2812FX @ ^1.4.5" + ] + } +} \ No newline at end of file diff --git a/src/modules/display/LedFX/Пример на 7 диодов.json b/src/modules/display/LedFX/Пример на 7 диодов.json new file mode 100644 index 00000000..c034bb0c --- /dev/null +++ b/src/modules/display/LedFX/Пример на 7 диодов.json @@ -0,0 +1,125 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Reading", + "subtype": "AnalogAdc", + "id": "t1", + "widget": "anydataRed", + "page": "Сенсоры", + "descr": "Аналог", + "map": "1,1024,1,255", + "plus": 0, + "multiply": 1, + "round": 1, + "pin": 0, + "int": "1", + "avgSteps": 1 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "b1", + "needSave": 0, + "widget": "rangeServo", + "page": "Сенсоры", + "descr": "Бар 1", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,255", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "b2", + "needSave": 0, + "widget": "rangeServo", + "page": "Сенсоры", + "descr": "Бар 2", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,255", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "LedFX", + "id": "fl20", + "widget": "inputTxt", + "page": "Кнопки", + "descr": "Лента", + "int": 15, + "needSave": 0, + "data_pin": "2", + "numLeds": "7", + "brightness": "50", + "speed": "100", + "color": "0xFF0000", + "effectsMode": "11", + "valueMode": 0, + "show": false + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "pause", + "needSave": 0, + "widget": "toggle", + "page": "Кнопки", + "descr": "Пауза", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "bars", + "needSave": 0, + "widget": "toggle", + "page": "Кнопки", + "descr": "Бары", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "any", + "needSave": 0, + "widget": "toggle", + "page": "Кнопки", + "descr": "Анимация", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "fade", + "needSave": 0, + "widget": "toggle", + "page": "Кнопки", + "descr": "Скрыть", + "int": "0", + "val": "0" + } + ] +} + +scenario=>if bars then fl20.vuMeter(7, "t1") else fl20.stop() +if any then fl20.setEffect(100) else fl20.stop() +if pause then fl20.pause() else fl20.resume() +if fade then fl20.fadeOut(1, 3) else fl20.fadeIn(60, 3) \ No newline at end of file diff --git a/src/modules/display/U8g2lib.zip b/src/modules/display/U8g2lib.zip new file mode 100644 index 00000000..aacb2535 Binary files /dev/null and b/src/modules/display/U8g2lib.zip differ diff --git a/src/modules/display/U8g2lib/DisplayTypes.h b/src/modules/display/U8g2lib/DisplayTypes.h new file mode 100644 index 00000000..4288dc75 --- /dev/null +++ b/src/modules/display/U8g2lib/DisplayTypes.h @@ -0,0 +1,631 @@ +#pragma once +#include "Global.h" +#include +#include +#include + +// #define DEBUG_DISPLAY + +#define DEFAULT_PAGE_UPDATE_ms 500 +// #define DEFAULT_PAGE_TIME_ms 5000 +// #define DEFAULT_ROTATION 0 +// #define DEFAULT_CONTRAST 10 +#define MIN_CONTRAST 10 +#define MAX_CONTRAST 150 + +#ifndef DEBUG_DISPLAY +#define D_LOG(fmt, ...) \ + do { \ + (void)0; \ + } while (0) +#else +#define D_LOG(fmt, ...) Serial.printf((PGM_P)PSTR(fmt), ##__VA_ARGS__) +#endif + +enum rotation_t : uint8_t { + ROTATION_NONE, + ROTATION_90, + ROTATION_180, + ROTATION_270 +}; + +uint8_t parse_contrast(int val) { + if (val < MIN_CONTRAST) val = MIN_CONTRAST; + if (val > MAX_CONTRAST) val = MAX_CONTRAST; + return val; +}; + +rotation_t parse_rotation(int val) { + if ((val > 0) && (val <= 90)) return ROTATION_90; + if ((val > 90) && (val <= 180)) return ROTATION_180; + if ((val > 180) && (val <= 270)) return ROTATION_270; + return ROTATION_NONE; +}; + +struct DisplayPage { + String key; + uint16_t time; + rotation_t rotate; + String font; + String format; + String valign; + + DisplayPage( + const String& key, + uint16_t time, + rotation_t rotate, + const String& font, + const String& format, + const String& valign) : key{key}, time{time}, rotate{rotate}, font{font}, format{format}, valign{valign} {} + + // void load(const JsonObject& obj) { + // // time = obj["time"].as(); + // // rotate = parse_rotation(obj["rotate"].as()); + // // font = obj["font"].as(); + // // valign = obj["valign"].as(); + // // format = obj["format"].as(); + // } + + // auto item = DisplayPage( pageObj["key"].as(), _update, _rotate, _font); + // // Загрузка настроек страницы + // item.load(pageObj); + // page.push_back(item); + + +}; + +enum position_t { + POS_AUTO, + POS_ABSOLUTE, + POS_RELATIVE, + POS_TEXT +}; + +struct RelativePosition { + float x; + float y; +}; + +struct TextPosition { + uint8_t row; + uint8_t col; +}; + +struct Point { + uint16_t x; + uint16_t y; + + Point() : Point(0, 0) {} + + Point(uint16_t x, uint16_t y) : x{x}, y{y} {} + + Point(const Point& rhv) : Point(rhv.x, rhv.y) {} +}; + +struct Position { + position_t type; + union { + Point abs; + RelativePosition rel; + TextPosition text; + }; + + Position() : type{POS_AUTO} {} + + Position(const Point& pos) : type{POS_ABSOLUTE} { + abs.x = pos.x; + abs.y = pos.y; + } + + Position(const RelativePosition& pos) : type{POS_RELATIVE} { + rel.x = pos.x; + rel.y = pos.y; + } + + Position(const TextPosition& pos) : type{POS_TEXT} { + text.col = pos.col; + text.row = pos.row; + } + + Position(const Position& rhv) : type{rhv.type} { + switch (type) { + case POS_ABSOLUTE: + abs = rhv.abs; + case POS_RELATIVE: + rel = rhv.rel; + case POS_TEXT: + text = rhv.text; + default: + break; + } + } +}; + +class Cursor : public Printable { + private: + Point _size; + + public: + TextPosition pos{0, 0}; + Point abs{0, 0}; + Point chr; + Cursor(){}; + + Cursor(const Point& size, const Point& chr) : _size{size}, chr{chr} { + D_LOG("w: %d, h: %d, ch: %d(%d)\r\n", _size.x, _size.y, chr.x, chr.y); + } + + void reset() { + pos.col = 0; + pos.row = 0; + abs.x = 0; + abs.y = 0; + } + + void lineFeed() { + pos.col = 0; + pos.row++; + abs.x = 0; + abs.y += chr.y; + } + + void moveX(uint8_t x) { + abs.x += x; + pos.col = abs.x / chr.x; + } + + void moveY(uint8_t y) { + abs.y += y; + } + + void moveXY(uint8_t x, uint8_t y) { + moveX(x); + moveY(y); + } + + void moveCarret(uint8_t col) { + pos.col += col; + moveX(col * chr.x); + } + + bool isEndOfPage(uint8_t rows = 1) { + return (abs.y + (rows * chr.y)) > _size.y; + } + + bool isEndOfLine(uint8_t cols = 1) { + return (abs.x + (cols * chr.x)) > _size.x; + } + + size_t printTo(Print& p) const { + return p.printf("(c:%d, r:%d x:%d, y:%d)", pos.col, pos.row, abs.x, abs.y); + } +}; + + +struct DisplayHardwareSettings { + int update = DEFAULT_PAGE_UPDATE_ms; + rotation_t rotate; + String font; + int pageTime; + String pageFormat; + int contrast; + bool autoPage; + String valign; +}; + +class Display { + private: + unsigned long _lastResfresh{0}; + Cursor _cursor; + U8G2 *_obj{nullptr}; + DisplayHardwareSettings *_settings; + + public: + Display(U8G2 *obj, DisplayHardwareSettings *settings) : _obj{obj}, _settings(settings) { + _obj->begin(); + _obj->enableUTF8Print(); + _obj->setContrast(_settings->contrast); + setFont(settings->font); + setRotation(settings->rotate); + clear(); + } + + ~Display () { + if (_obj) { + delete _obj; + _obj = nullptr; + } + } + + void setRotation(rotation_t rotate) { + switch (rotate) { + case ROTATION_NONE: + _obj->setDisplayRotation(U8G2_R0); + break; + case ROTATION_90: + _obj->setDisplayRotation(U8G2_R1); + break; + case ROTATION_180: + _obj->setDisplayRotation(U8G2_R2); + break; + case ROTATION_270: + _obj->setDisplayRotation(U8G2_R3); + break; + } + } + + void setFont(const String &fontName = "") { + if (fontName.isEmpty()) { + Display::setFont(_settings->font); + return; + } + + if (fontName.startsWith("c6x12")) + _obj->setFont(u8g2_font_6x12_t_cyrillic); + else if (fontName.startsWith("s6x12")) + _obj->setFont(u8g2_font_6x12_t_symbols); + + else if (fontName.startsWith("c6x13")) + _obj->setFont(u8g2_font_6x13_t_cyrillic); + + else if (fontName.startsWith("c7x13")) + _obj->setFont(u8g2_font_7x13_t_cyrillic); + else if (fontName.startsWith("s7x13")) + _obj->setFont(u8g2_font_7x13_t_symbols); + + else if (fontName.startsWith("c8x13")) + _obj->setFont(u8g2_font_8x13_t_cyrillic); + else if (fontName.startsWith("s8x13")) + _obj->setFont(u8g2_font_8x13_t_symbols); + + else if (fontName.startsWith("c9x15")) + _obj->setFont(u8g2_font_9x15_t_cyrillic); + else if (fontName.startsWith("s9x15")) + _obj->setFont(u8g2_font_9x15_t_symbols); + + else if (fontName.startsWith("c10x20")) + _obj->setFont(u8g2_font_10x20_t_cyrillic); + else if (fontName.startsWith("unifont")) + _obj->setFont(u8g2_font_unifont_t_symbols); + else if (fontName.startsWith("siji")) + _obj->setFont(u8g2_font_siji_t_6x10); + else + _obj->setFont(u8g2_font_6x12_t_cyrillic); + + _cursor.chr.x = getMaxCharHeight(); + // _cursor.chr.y = getLineHeight(); + } + + void initCursor() { + _cursor = Cursor( + {getWidth(), getHeight()}, + {getMaxCharHeight(), getLineHeight()}); + } + + void getPosition(const TextPosition &a, Point &b) { + b.x = a.col * _cursor.chr.x; + b.y = (a.row + 1) * _cursor.chr.y; + } + + void getPosition(const RelativePosition &a, Point &b) { + b.x = getHeight() * a.x; + b.y = getWidth() * a.y; + } + + void getPosition(const Point &a, TextPosition &b) { + b.row = a.y / getLineHeight(); + b.col = a.x / getMaxCharWidth(); + } + + void getPosition(const RelativePosition &a, TextPosition &b) { + Point tmp; + getPosition(a, tmp); + getPosition(tmp, b); + } + + void draw(const RelativePosition &pos, const String &str) { + Point tmp; + getPosition(pos, tmp); + draw(tmp, str); + } + + void draw(TextPosition &pos, const String &str) { + Point tmp; + getPosition(pos, tmp); + draw(tmp, str); + } + + Cursor *getCursor() { + return &_cursor; + } + + // print меняю cursor + void println(const String &str, bool frame = false) { + print(str, frame); + _cursor.lineFeed(); + } + + void print(const String &str, bool frame = false) { + //Serial.print(_cursor); + // x, y нижний левой + int width = _obj->drawUTF8(_cursor.abs.x, _cursor.abs.y + _cursor.chr.y, str.c_str()); + if (frame) { + int x = _cursor.abs.x - getXSpacer(); + int y = _cursor.abs.y - _cursor.chr.y; + width += (getXSpacer() * 2); + int height = _cursor.chr.y + getYSpacer() * 2; + // x, y верхней левой. длина, высота + _obj->drawFrame(x, y, width, height); + D_LOG("[x:%d y:%d w:%d h:%d]", x, y, width, height); + } + _cursor.moveX(width); + } + + // draw не меняет cursor + void draw(const Point &pos, const String &str) { + Serial.printf("(x:%d,y:%d) %s", pos.x, pos.y, str.c_str()); + _obj->drawStr(pos.x, pos.y, str.c_str()); + } + + uint8_t getLineHeight() { + return getMaxCharHeight() + getYSpacer(); + } + + int getXSpacer() { + int res = getWidth() / 100; + if (!res) res = 1; + return res; + } + + int getYSpacer() { + int res = (getHeight() - (getLines() * getMaxCharHeight())) / getLines(); + if (!res) res = 1; + return res; + } + + uint8_t getWidth() { + return _obj->getDisplayWidth(); + } + + uint8_t getHeight() { + return _obj->getDisplayHeight(); + } + + uint8_t getLines() { + uint8_t res = getHeight() / _obj->getMaxCharHeight(); + if (!res) res = 1; + return res; + } + + uint8_t getMaxCharHeight() { + return _obj->getMaxCharHeight(); + } + + uint8_t getMaxCharWidth() { + return _obj->getMaxCharWidth(); + } + + void clear() { + _obj->clearDisplay(); + _cursor.reset(); + } + + void startRefresh() { + _obj->clearBuffer(); + _cursor.reset(); + } + + void endRefresh() { + _obj->sendBuffer(); + _lastResfresh = millis(); + } + + bool isNeedsRefresh() { + // SerialPrint("[Display]", "_settings->update: " + String(_settings->update) + "ms", ""); + return !_lastResfresh || (millis() > (_lastResfresh + _settings->update)); + } +}; + +struct ParamPropeties { + // рамка + bool frame[false]; +}; + +struct Param { + // Ключ + const String key; + // Префикс к значению + String pref; + // Суффикс к значению + String suff; + // Значение + String value; + + String pref_fnt; + String suff_fnt; + String value_fnt; + + String gliphs; + + // значение изменилось + bool updated; + // группа + uint8_t group; + ParamPropeties props; + Position position; + + Param(const String &key, + const String &pref = emptyString, const String &value = emptyString, const String &suff = emptyString, + const String &pref_fnt = emptyString, const String &value_fnt = emptyString, const String &suff_fnt = emptyString, + const String &gliphs = emptyString + ) : key{key}, group{0} { + setValue(value.c_str()); + setPref(pref); + setSuff(suff); + this->pref_fnt = pref_fnt; + this->value_fnt = value_fnt; + this->suff_fnt = suff_fnt; + this->gliphs = gliphs; + updated = false; + } + + bool isValid() { + return !pref.isEmpty(); + } + + bool setPref(const String &str) { + if (!pref.equals(str)) { + pref = str; + updated = true; + return true; + } + return false; + } + + bool setSuff(const String &str) { + if (!suff.equals(str)) { + suff = str; + updated = true; + return true; + } + return false; + } + + bool setValue(const String &str) { + if (!value.equals(str)) { + value = str; + updated = true; + return true; + } + return false; + } + + + + void draw(Display *obj, uint8_t line) { + } + + void draw(Display *obj) { + auto type = position.type; + switch (type) { + case POS_AUTO: { + D_LOG("AUTO %s '%s%s'\r\n", key.c_str(), descr.c_str(), value.c_str()); + obj->setFont(pref_fnt); + obj->print(pref.c_str()); + + obj->setFont(value_fnt); + obj->println(value.c_str(), false); + + obj->setFont(suff_fnt); + obj->print(suff.c_str()); + } + case POS_ABSOLUTE: { + auto pos = position.abs; + D_LOG("ABS(%d, %d) %s %s'\r\n", pos.x, pos.y, key.c_str(), value.c_str()); + obj->draw(pos, value); + } + case POS_RELATIVE: { + auto pos = position.rel; + D_LOG("REL(%2.2f, %2.2f) %s %s'\r\n", pos.x, pos.y, key.c_str(), value.c_str()); + obj->draw(pos, value); + } + case POS_TEXT: { + auto pos = position.text; + D_LOG("TXT(%d, %d) %s %s'\r\n", pos.col, pos.row, key.c_str(), value.c_str()); + obj->draw(pos, value); + } + default: + D_LOG("unhadled: %d", type); + } + } +}; + +class ParamCollection { + std::vector _item; + + public: + void load() { + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { + if ((*it)->getSubtype() == "" || (*it)->getSubtype() == "U8g2lib") continue; + + auto entry = find((*it)->getID()); + if (!entry) { + _item.push_back({(*it)->getID(), (*it)->getID() + ": ", (*it)->getValue(), "", "", "", ""}); + } else { + entry->setValue((*it)->getValue()); + if (entry->pref == "") + entry->setPref((*it)->getID() + ": "); + } + } + } + + void loadExtParamData(String parameters) { + String id = ""; + jsonRead(parameters, "id", id, false); + if (id != "") { + String pref = ""; + String suff = ""; + String pref_fnt = ""; + String suff_fnt = ""; + String value_fnt = ""; + String gliphs = ""; + + bool hasExtParam = false; + + hasExtParam = hasExtParam + jsonRead(parameters, "pref", pref, false); + hasExtParam = hasExtParam + jsonRead(parameters, "suff", suff, false); + hasExtParam = hasExtParam + jsonRead(parameters, "pref_fnt", pref_fnt, false); + hasExtParam = hasExtParam + jsonRead(parameters, "suff_fnt", suff_fnt, false); + hasExtParam = hasExtParam + jsonRead(parameters, "value_fnt", value_fnt, false); + hasExtParam = hasExtParam + jsonRead(parameters, "gliphs", gliphs, false); + + if (hasExtParam) { + _item.push_back({id, pref, "", suff, pref_fnt, value_fnt, suff_fnt, gliphs}); + } + } + } + + Param *find(const String &key) { + return find(key.c_str()); + } + + Param *find(const char *key) { + Param *res = nullptr; + for (size_t i = 0; i < _item.size(); i++) { + if (_item.at(i).key.equalsIgnoreCase(key)) { + res = &_item.at(i); + break; + } + } + return res; + } + + Param *get(int n) { + return &_item.at(n); + } + + size_t count() { + return _item.size(); + } + + // n - номер по порядку параметра + Param *getValid(int n) { + for (size_t i = 0; i < _item.size(); i++) + if (_item.at(i).isValid()) + if (!(n--)) return &_item.at(i); + return nullptr; + } + + size_t getVaildCount() { + size_t res = 0; + for (auto entry : _item) res += entry.isValid(); + return res; + } + + size_t max_group() { + size_t res = 0; + for (auto entry : _item) + if (res < entry.group) res = entry.group; + return res; + } +}; \ No newline at end of file diff --git a/src/modules/display/U8g2lib/U8g2lib.cpp b/src/modules/display/U8g2lib/U8g2lib.cpp new file mode 100644 index 00000000..97dc291f --- /dev/null +++ b/src/modules/display/U8g2lib/U8g2lib.cpp @@ -0,0 +1,419 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include +#include "DisplayTypes.h" + +#define STRHELPER(x) #x +#define TO_STRING_AUX(...) "" #__VA_ARGS__ +#define TO_STRING(x) TO_STRING_AUX(x) + + +// дополненный список параметров для вывода, который синхронизирован со списком значений IoTM +ParamCollection *extParams{nullptr}; + +// класс одного главного экземпляра экрана для выделения памяти только когда потребуется экран +class DisplayImplementation { + private: + unsigned long _lastPageChange{0}; + bool _pageChanged{false}; + // uint8_t _max_descr_width{0}; + // typedef std::vector Line; + // текущая + size_t _page_n{0}; + // struct Page { + // std::vector line; + // }; + + uint8_t _n{0}; // последний отображенный + + DisplayHardwareSettings *_context{nullptr}; + Display *_display{nullptr}; + + public: + DisplayImplementation(DisplayHardwareSettings *context = nullptr, + Display *display = nullptr) + : _context(context), _display(display) { + + } + + ~DisplayImplementation() { + if (_display) { + delete _display; + _display = nullptr; + } + if (_context) { + delete _context; + _context = nullptr; + } + if (extParams) { + delete extParams; + extParams = nullptr; + } + } + + std::vector page; + + void nextPage() { + _n = _n + 1; + if (_n == page.size()) _n = _n - 1; + _pageChanged = true; + } + + void prevPage() { + if (_n > 0) _n = _n - 1; + _pageChanged = true; + } + + void rotPage() { + _n = _n + 1; + if (_n == page.size()) _n = 0; + _pageChanged = true; + } + + void gotoPage(uint8_t num) { + _n = num; + if (num < 0) _n = 0; + if (num >= page.size()) _n = page.size() - 1; + _pageChanged = true; + } + + void setAutoPage(bool isAuto) { + if (_context) _context->autoPage = isAuto; + _pageChanged = true; + } + + uint8_t calcPageCount(ParamCollection *param, uint8_t linesPerPage) { + size_t res = 0; + size_t totalLines = param->count(); + if (totalLines && linesPerPage) { + res = totalLines / linesPerPage; + if (totalLines % linesPerPage) res++; + } + return res; + } + + // uint8_t getPageCount() { + // return isAutoPage() ? calcPageCount(_param, _display->getLines()) : getPageCount(); + // } + + // выводит на страницу параметры начиная c [n] + // возвращает [n] последнего уместившегося + uint8_t draw(Display *display, ParamCollection *param, uint8_t n) { + // Очищает буфер (не экран, а внутреннее представление) для последущего заполнения + display->startRefresh(); + size_t i = 0; + // вот тут лог ошибка + for (i = n; i < param->count(); i++) { + auto cursor = display->getCursor(); + auto entry = param->get(i); + auto len = entry->value.length() + entry->pref.length() + entry->suff.length() ; + if (cursor->isEndOfLine(len)) cursor->lineFeed(); + + printParam(display, entry, _context->font); + + if (cursor->isEndOfPage(0)) break; + } + // Отправит готовый буфер страницы на дисплей + display->endRefresh(); + return i; + } + + String slice(const String &str, size_t index, char delim) { + size_t cnt = 0; + int subIndex[] = {0, -1}; + size_t maxIndex = str.length() - 1; + + for (size_t i = 0; (i <= maxIndex) && (cnt <= index); i++) { + if ((str.charAt(i) == delim) || (i == maxIndex)) { + cnt++; + subIndex[0] = subIndex[1] + 1; + subIndex[1] = (i == maxIndex) ? i + 1 : i; + } + } + return cnt > index ? str.substring(subIndex[0], subIndex[1]) : emptyString; + } + + void printParam(Display *display, Param *param, const String &parentFont) { + if (!param->pref.isEmpty()) { + display->setFont(param->pref_fnt.isEmpty() ? parentFont : param->pref_fnt); + display->print(param->pref); + } + + if (!param->value.isEmpty()) { + display->setFont(param->value_fnt.isEmpty() ? parentFont : param->value_fnt); + if (!param->gliphs.isEmpty() && isDigitStr(param->value)) { + int glyphIndex = param->value.toInt(); + display->print(getUtf8CharByIndex(param->gliphs, glyphIndex)); + } else display->print(param->value); + } + + if (!param->suff.isEmpty()) { + display->setFont(param->suff_fnt.isEmpty() ? parentFont : param->suff_fnt); + display->print(param->suff); + } + } + + void showXXX(Display *display, ParamCollection *param, uint8_t page) { + size_t linesPerPage = display->getLines(); + size_t line_first = _page_n * linesPerPage; + size_t line_last = line_first + linesPerPage - 1; + + display->startRefresh(); + + size_t lineOfPage = 0; + for (size_t n = line_first; n <= line_last; n++) { + auto entry = param->get(n); + if (entry) { + entry->draw(_display, lineOfPage); + lineOfPage++; + } else { + break; + } + } + display->endRefresh(); + } + + void drawPage(Display *display, ParamCollection *params, DisplayPage *page) { + display->setFont(page->font); + display->initCursor(); + + auto keys = page->key; + D_LOG("page keys: %s\r\n", keys.c_str()); + size_t l = 0; + auto line_keys = slice(keys, l, '#'); + while (!line_keys.isEmpty()) { + if (page->valign.equalsIgnoreCase("center")) { + display->getCursor()->moveY((display->getHeight() / 2) - display->getMaxCharHeight() / 2); + } + D_LOG("line keys: %s\r\n", keys.c_str()); + size_t n = 0; + auto key = slice(line_keys, n, ','); + while (!key.isEmpty()) { + D_LOG("key: %s\r\n", key.c_str()); + auto entry = params->find(key.c_str()); + if (entry && entry->updated) { + if (n) display->print(" "); + printParam(display, entry, page->font); + } + key = slice(line_keys, ++n, ','); + } + display->getCursor()->lineFeed(); + line_keys = slice(keys, ++l, '#'); + } + } + + // Режим пользовательской разбивки параметров по страницам + void showManual(Display *display, ParamCollection *param) { + auto page = getPage(_n); + + if (display->isNeedsRefresh() || _pageChanged) { + D_LOG("[Display] page: %d\r\n", _n); + display->setRotation(page->rotate); + display->startRefresh(); + drawPage(display, param, page); + display->endRefresh(); + _pageChanged = false; + } + + if (_context->autoPage && millis() >= (_lastPageChange + page->time)) { + // Если это была последняя начинаем с начала + if (++_n > (getPageCount() - 1)) _n = 0; + _pageChanged = true; + _lastPageChange = millis(); + } + } + + // Режим авто разбивки параметров по страницам + void showAuto(Display *display, ParamCollection *param) { + size_t param_count = param->count(); + + if (!param_count) return; + + display->setFont(_context->font); + display->initCursor(); + + size_t last_n = _n; + if (display->isNeedsRefresh() || _pageChanged) { + //D_LOG("n: %d/%d\r\n", _n, param_count); + last_n = draw(display, param, _n); + } + + if (_context->autoPage && millis() >= (_lastPageChange + _context->pageTime)) { + _n = last_n; + if (_n >= param_count) _n = 0; + _pageChanged = true; + _lastPageChange = millis(); + } + } + + void show() { + if (extParams && _display) { + extParams->load(); + + if (isAutoPage()) { + showAuto(_display, extParams); + } else { + showManual(_display, extParams); + } + } + } + + bool isAutoPage() { + return !getPageCount(); + } + + uint8_t getPageCount() { + return page.size(); + } + + DisplayPage* getPage(uint8_t index) { + return &page.at(index); + } +}; + + +DisplayImplementation* displayImpl = nullptr; + + +class U8g2lib : public IoTItem { + private: + uint8_t _pageNum = 0; + + public: + U8g2lib(String parameters) : IoTItem(parameters) { + DisplayHardwareSettings *context = new DisplayHardwareSettings(); + if (!context) { + D_LOG("[Display] disabled"); + return; + } + + jsonRead(parameters, "update", context->update); + jsonRead(parameters, "font", context->font); + + int rotate; + jsonRead(parameters, "rotation", rotate); + context->rotate = parse_rotation(rotate); + + jsonRead(parameters, "contrast", context->contrast); + jsonRead(parameters, "autoPage", context->autoPage); + jsonRead(parameters, "pageTime", context->pageTime); + + bool itsFirstDisplayInit = false; + if (!displayImpl) { + // Значит это первый элемент U8g2lib в конфигурации - Инициализируем дисплей + itsFirstDisplayInit = true; + int dc = U8X8_PIN_NONE, cs = U8X8_PIN_NONE, data = U8X8_PIN_NONE, clock = U8X8_PIN_NONE, rst = U8X8_PIN_NONE; + jsonRead(parameters, "dc", dc); + jsonRead(parameters, "cs", cs); + jsonRead(parameters, "data", data); + jsonRead(parameters, "clock", clock); + jsonRead(parameters, "rst", rst); + if (dc == -1) dc = U8X8_PIN_NONE; + if (cs == -1) cs = U8X8_PIN_NONE; + if (data == -1) data = U8X8_PIN_NONE; + if (clock == -1) clock = U8X8_PIN_NONE; + if (rst == -1) rst = U8X8_PIN_NONE; + + String type; + jsonRead(parameters, "oledType", type); + U8G2* libObj = nullptr; + if (type.startsWith("ST")) { + libObj = new U8G2_ST7565_ERC12864_F_4W_SW_SPI(U8G2_R0, clock, data, cs, dc, rst); + } + else if (type.startsWith("SS_I2C")) { + // libObj = new U8G2_SSD1306_128X64_VCOMH0_F_SW_I2C(U8G2_R0, clock, data, rst); + libObj = new U8G2_SSD1306_128X32_UNIVISION_F_SW_I2C(U8G2_R0, clock, data, rst); + + } + else if (type.startsWith("SS_SPI")) { + libObj = new U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI(U8G2_R0, clock, data, cs, dc, rst); + } + else if (type.startsWith("SH")) { + libObj = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, rst, clock, data); + } + + if (!libObj) { + D_LOG("[Display] disabled"); + return; + } + + Display *_display = new Display(libObj, context); + if (!_display) { + D_LOG("[Display] disabled"); + return; + } + + if (!extParams) extParams = new ParamCollection(); + + displayImpl = new DisplayImplementation(context, _display); + if (!displayImpl) { + D_LOG("[Display] disabled"); + return; + } + } + + // добавляем страницу, если указан ID для отображения + String id2show; + jsonRead(parameters, "id2show", id2show); + if (!id2show.isEmpty()) { + auto item = DisplayPage( + id2show, + context->pageTime, + context->rotate, + context->font, + context->pageFormat, + context->valign + ); + _pageNum = displayImpl->page.size(); + displayImpl->page.push_back(item); + if (!itsFirstDisplayInit) delete context; // если это не первый вызов, то контекст имеет временный характер только для создания страницы + } + } + + void doByInterval() { + if (displayImpl) displayImpl->show(); + } + + IoTValue execute(String command, std::vector& param) { + if (displayImpl) + if (command == "nextPage") { + displayImpl->nextPage(); + } else if (command == "prevPage") { + displayImpl->prevPage(); + } else if (command == "rotPage") { + displayImpl->rotPage(); + } else if (command == "gotoPage") { + if (param.size() == 1) { + displayImpl->gotoPage(param[0].valD); + } else { + displayImpl->gotoPage(_pageNum); + } + } else if (command == "setAutoPage") { + if (param.size() == 1) { + displayImpl->setAutoPage(param[0].valD); + } + } + + return {}; + } + + ~U8g2lib() { + if (displayImpl) { + delete displayImpl; + displayImpl = nullptr; + } + }; +}; + +void* getAPI_U8g2lib(String subtype, String param) { + if (subtype == F("U8g2lib")) { + // SerialPrint("[Display]", "param1: ", param); + return new U8g2lib(param); + } else { + // элемент не наш, но проверяем на налличие модификаторов, которые нужны для модуля + // вынимаем ID элемента и значения pref и suff связанные с ним + if (!extParams) extParams = new ParamCollection(); + extParams->loadExtParamData(param); + return nullptr; + } +} diff --git a/src/modules/display/U8g2lib/example_config.json b/src/modules/display/U8g2lib/example_config.json new file mode 100644 index 00000000..48809c20 --- /dev/null +++ b/src/modules/display/U8g2lib/example_config.json @@ -0,0 +1,217 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "btn", + "needSave": 0, + "widget": "toggle", + "page": "Ввод", + "descr": "ТестКнопка", + "int": "0", + "val": "0", + "value_fnt": "siji", + "gliphs": "" + }, + { + "global": 0, + "type": "Writing", + "subtype": "Timer", + "id": "timer", + "widget": "anydataDef", + "page": "Ввод", + "descr": "Таймер", + "int": 1, + "countDown": "99", + "ticker": 1, + "repeat": 1, + "needSave": 0, + "pref": "ТАЙМЕР: ", + "suff": " сек", + "round": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "time", + "needSave": 0, + "widget": "anydataRed", + "page": "Ввод", + "descr": "Время", + "int": "0", + "val": "", + "pref": " ⏰️", + "pref_fnt": "unifont" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "var", + "needSave": 0, + "widget": "inputTxt", + "page": "Ввод", + "descr": "Текст", + "int": "0", + "val": "☀️-☁️-☂️-☃️-☄️", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "pref": "текст: ", + "value_fnt": "unifont" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "ip", + "needSave": 0, + "widget": "anydataDef", + "page": "Ввод", + "descr": "IP", + "int": "0", + "val": "", + "pref": "IP: " + }, + { + "type": "Reading", + "subtype": "U8g2lib", + "id": "page1", + "widget": "nil", + "page": "", + "descr": "", + "oledType": "SS_I2C", + "int": "1", + "font": "c6x13", + "contrast": "200", + "rotation": "0", + "autoPage": "0", + "pageTime": "10000", + "dc": 19, + "cs": "-1", + "data": "21", + "clock": "22", + "rst": -1, + "id2show": "timer,lvl#ip" + }, + { + "type": "Reading", + "subtype": "U8g2lib", + "id": "page2", + "widget": "nil", + "page": "", + "descr": "", + "oledType": "SS_I2C", + "int": 1, + "update": 500, + "font": "c6x13", + "contrast": "150", + "rotation": "0", + "autoPage": "0", + "pageTime": 3000, + "id2show": "var#btn,time", + "dc": "-1", + "cs": "-1", + "data": "-1", + "clock": "-1", + "rst": -1 + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "autoPage", + "needSave": 0, + "widget": "toggle", + "page": "Ввод", + "descr": "autoPage", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "nextPage", + "needSave": 0, + "widget": "toggle", + "page": "Ввод", + "descr": "nextPage", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "prevPage", + "needSave": 0, + "widget": "toggle", + "page": "Ввод", + "descr": "prevPage", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "pageN", + "needSave": 0, + "widget": "inputDgt", + "page": "Ввод", + "descr": "pageN", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "rotPage", + "needSave": 0, + "widget": "toggle", + "page": "Ввод", + "descr": "rotPage", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "AnalogAdc", + "id": "lvl", + "widget": "anydataRed", + "page": "Ввод", + "descr": "Уровень", + "map": "1,1024,1,5", + "plus": 0, + "multiply": 1, + "round": "0", + "pin": "34", + "int": "1", + "avgSteps": 1, + "pref": " ", + "value_fnt": "siji", + "gliphs": "" + } + ] +} + +scenario=>if timer then { +ip = getIP() +time = gethhmmss() +} +if autoPage then page1.setAutoPage(1) else page1.setAutoPage(0) +if nextPage < 2 then page1.nextPage() +if prevPage < 2 then page1.prevPage() +if rotPage < 2 then page1.rotPage() +if pageN != "" then page1.gotoPage(pageN) \ No newline at end of file diff --git a/src/modules/display/U8g2lib/modinfo.json b/src/modules/display/U8g2lib/modinfo.json new file mode 100644 index 00000000..d4e18bb3 --- /dev/null +++ b/src/modules/display/U8g2lib/modinfo.json @@ -0,0 +1,96 @@ +{ + "menuSection": "screens", + "configItem": [ + { + "name": "Экраны U8g2", + "type": "Reading", + "subtype": "U8g2lib", + "id": "u8page", + "widget": "", + "page": "", + "descr": "", + + "oledType": "SS_I2C", + "int": 1, + "update": 500, + "font": "c6x13", + "contrast": 90, + "rotation": 90, + "autoPage": 1, + "pageTime": 3000, + "id2show": "", + + "dc": 19, + "cs": 5, + "data": 23, + "clock": 18, + "rst": -1 + } + ], + "about": { + "authorName": "Ilya Belyakov", + "authorContact": "https://t.me/Biveraxe", + "authorGit": "https://github.com/biveraxe", + "specialThanks": "Yuriy Trikoz @ytrikoz", + "moduleName": "U8g2lib", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "moduleDesc": "Позволяет выводить на графические экраны типа SSD, ST, SH указанные параметры из конфигурации IoTM.", + "propInfo": { + "oledType": "Строковый код типа дисплея. В текущей верссии поддерживаются ST7565 (ST), SSD1306 (SS_I2C), SSD1306 (SS_SPI) и SH1106 (SH). Для получения списка доступных типов дисплеев, обратитесь к документации библиотеки U8g2. Добавить возможность выбора типов дисплеев можно, добавив соответствующие условия в файл модуля в конструктор класса U8g2lib.", + "int": "Интервал обновления экрана в секундах. Если указано 0, то обновление экрана не производится.", + "update": "Интервал обновления экрана в миллисекундах. Если указано 0, то обновление экрана не производится. (парамтер на развитие)", + "font": "Шрифт, используемый для отображения текста на экране. Доступные шрифты можно найти в документации библиотеки U8g2 и добавить в проект в функцию setFont().", + "contrast": "Контрастность экрана. Значение от 10 до 150, где 0 - минимальная контрастность, а 255 - максимальная.", + "rotation": "Поворот экрана в градусах. Доступные значения: 0, 90, 180, 270.", + "autoPage": "Автоматическая смена страниц экрана. Если установлено в 1, то экран будет автоматически переключаться на следующую страницу после указанного времени.", + "pageTime": "Время в миллисекундах, через которое будет происходить автоматическая смена страниц экрана. Используется только если autoPage установлено в 1.", + "id2show": "Идентификатор элемента конфигурации, значение которого будет отображаться на экране. Если указано, то на экране будет отображаться только это значение. Возможно указать несколько идентификаторов, разделенных запятыми для перечисления горизонтально и # для перевода строки.", + "dc": "Пин, используемый для управления дисплеем по протоколу I2C. Если не используется, укажите -1.", + "cs": "Пин, используемый для управления дисплеем по протоколу SPI. Если не используется, укажите -1.", + "data": "Пин, используемый для передачи данных на дисплей по протоколу SPI. Если не используется, укажите -1.", + "clock": "Пин, используемый для синхронизации данных на дисплее по протоколу SPI. Если не используется, укажите -1.", + "rst": "Пин, используемый для сброса дисплея. Если не используется, укажите -1." + }, + "title": "Дисплей U8g2lib", + "funcInfo": [ + { + "name": "nextPage", + "descr": "Переключиться на следующую страницу", + "params": [] + }, + { + "name": "prevPage", + "descr": "Переключиться на предыдущую страницу", + "params": [] + }, + { + "name": "rotPage", + "descr": "Переключиться на следующую страницу с ротацией", + "params": [] + }, + { + "name": "gotoPage", + "descr": "Переключиться на указанную страницу. Если номер не указать, то переключится на страницу закрепленную за элементом конфигурации.", + "params": ["Номер страницы"] + }, + { + "name": "setAutoPage", + "descr": "Установить автоматическую смену страниц.", + "params": ["1 - включить, 0 - выключить"] + } + ] + }, + "defActive": false, + "usedLibs": { + "esp32*": [ + "olikraus/U8g2 @ ^2.36.5" + ], + "esp82*": [ + "olikraus/U8g2 @ ^2.36.5" + ] + } +} \ No newline at end of file diff --git a/src/modules/sceninfo.json b/src/modules/sceninfo.json index 53265548..bdc834e0 100644 --- a/src/modules/sceninfo.json +++ b/src/modules/sceninfo.json @@ -14,6 +14,18 @@ "descr": "Получаем количество секунд доверия к значениям элемента. При -2 доверие полное, при -1 время доверия истекло. При >0 время обратного отсчета. Используется только совместно с ИД элемента: ID.getIntFromNet()", "params": [] }, + { + "name": "setInterval", + "descr": "Меняем интервал выполнения периодиеских операций элемента в секундах. Используется только совместно с ИД элемента: ID.setInterval(5)", + "params": ["Секунды"], + "return": "установленный интервал" + }, + { + "name": "doByInterval", + "descr": "Выполняем интервальное действие модуля вне плана. Используется только совместно с ИД элемента: ID.doByInterval()", + "params": [], + "return": "значение элемента после выполнения doByInterval" + }, { "name": "exit", "descr": "Прерываем работу сценария и выводим в консоль причину. Причина не обязательна.", diff --git a/src/modules/sensors/AhtXX/AhtXX.cpp b/src/modules/sensors/AhtXX/AhtXX.cpp index a49e8fe1..7bac81c1 100644 --- a/src/modules/sensors/AhtXX/AhtXX.cpp +++ b/src/modules/sensors/AhtXX/AhtXX.cpp @@ -88,7 +88,7 @@ void* getAPI_AhtXX(String subtype, String param) { if (ahts.find(addr) == ahts.end()) { int shtType; - jsonRead(param, "type", shtType); + jsonRead(param, "shtType", shtType); ahts[addr] = new AHTxx(hexStringToUint8(addr), (AHTXX_I2C_SENSOR)shtType); diff --git a/src/utils/StringUtils.cpp b/src/utils/StringUtils.cpp index bfb0ec1d..80cfa61a 100644 --- a/src/utils/StringUtils.cpp +++ b/src/utils/StringUtils.cpp @@ -103,6 +103,7 @@ uint8_t hexStringToUint8(const String& hex) { if (tmp >= 0x00 && tmp <= 0xFF) { return tmp; } + return 0; } uint16_t hexStringToUint16(const String& hex) { @@ -110,6 +111,15 @@ uint16_t hexStringToUint16(const String& hex) { if (tmp >= 0x0000 && tmp <= 0xFFFF) { return tmp; } + return 0; +} + +uint32_t hexStringToUint32(const String& hex) { + uint32_t tmp = strtol(hex.c_str(), NULL, 0); + if (tmp >= 0x0000 && tmp <= 0xFFFFFF) { + return tmp; + } + return 0; } size_t itemsCount2(String str, const String& separator) { @@ -223,4 +233,29 @@ bool strInVector(const String& str, const std::vector& vec) { if (vec[i] == str) return true; } return false; +} + +String getUtf8CharByIndex(const String& utf8str, int index) { + if (index < 0) index = 0; + + int len = utf8str.length(); + int charCount = 0; + int i = 0; + while (i < len) { + int charLen = 1; + unsigned char c = utf8str[i]; + if ((c & 0x80) == 0x00) charLen = 1; // 0xxxxxxx + else if ((c & 0xE0) == 0xC0) charLen = 2; // 110xxxxx + else if ((c & 0xF0) == 0xE0) charLen = 3; // 1110xxxx + else if ((c & 0xF8) == 0xF0) charLen = 4; // 11110xxx + + if (charCount == index) { + return utf8str.substring(i, i + charLen); + } + + if (i + charLen >= len) return utf8str.substring(i, i + charLen); + i += charLen; + charCount++; + } + return ""; } \ No newline at end of file diff --git a/tools/patch32_ws.py b/tools/patch32_ws.py index 33214618..c0914f99 100644 --- a/tools/patch32_ws.py +++ b/tools/patch32_ws.py @@ -11,7 +11,7 @@ from sys import platform pio_home = env.subst("$PROJECT_CORE_DIR") print("PLATFORMIO_DIR" + pio_home) -if platform == "linux" or platform == "linux2": +if platform == "linux" or platform == "linux2" or platform == "darwin": # linux #mainPyPath = '/home/rise/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src/WiFiClient.cpp' mainPyPath = pio_home + '/packages/framework-arduinoespressif32/libraries/WiFi/src/WiFiClient.cpp' diff --git a/tools/patch32c6.py b/tools/patch32c6.py index 5933dc68..e1d569e7 100644 --- a/tools/patch32c6.py +++ b/tools/patch32c6.py @@ -7,7 +7,7 @@ from sys import platform pio_home = env.subst("$PROJECT_CORE_DIR") print("PLATFORMIO_DIR" + pio_home) -if platform == "linux" or platform == "linux2": +if platform == "linux" or platform == "linux2" or platform == "darwin": # linux #devkitm = '/home/rise/.platformio/platforms/espressif32/boards/esp32-c6-devkitm-1.json' #devkitc = '/home/rise/.platformio/platforms/espressif32/boards/esp32-c6-devkitc-1.json' diff --git a/tools/patch8266_16m.py b/tools/patch8266_16m.py index b65c22de..c6426761 100644 --- a/tools/patch8266_16m.py +++ b/tools/patch8266_16m.py @@ -9,7 +9,7 @@ from sys import platform pio_home = env.subst("$PROJECT_CORE_DIR") print("PLATFORMIO_DIR" + pio_home) -if platform == "linux" or platform == "linux2": +if platform == "linux" or platform == "linux2" or platform == "darwin": #mainPyPath = '/home/rise/.platformio/platforms/espressif8266@4.0.1/builder/main.py' mainPyPath = pio_home + '/platforms/espressif8266@4.0.1/builder/main.py' else: