GyverLibs / GyverPortal

Простой конструктор веб интерфейса для esp8266 и ESP32
MIT License
309 stars 25 forks source link

Многостраничность... не работает как хочется #31

Closed DAK85 closed 2 years ago

DAK85 commented 2 years ago

Доброго времен суток

(версия библиотеки 2.1.0)

  1. Если инициируем портал как локальный (скажем так GyverPortal portal;), то в функцию void build() нельзя передать ссылку на экземпляр (на poratl). Соответственно в void build() не получится выполнить условие (if (portal.uri()=="/settings/system")), компилятор ругается, что portal не определён (это конечно не вина компилятора).
  2. События кликов на кнопки не уходят из вложенных страниц, пришлось поменять в portal.h в строке 45 else if (_uri.startsWith(F("/GP_click")))" на "else if (_uri.indexOf(F("/GP_click"))!=-1)" думаю с апдейтами будет та же проблема.
  3. Допустим мы динамически создаём страницы на вкладке /settings/system/relay (/0, /1, /2) и нам надо ловить данные, если только на этих страницах был кликнут submit.... Добавил в portak.h // вернёт true, если был submit с форм, имя которых начинмется с name bool formSub(const String& name) { return _formF ? (_uri.startsWith(name) && (_uri.lastIndexOf("GP_click")==-1)) : 0; } теперь чтобы поймать события с вложенных страниц надо просто вызвать portal.formSub("/settings/system/relay/") Думаю что это полезно, однако не уверен как поведёт себя при апдейтах, возможно надо дописать исключение событий GP_update, я не использую, мне этого хватает.

Пока для меня критичен самый первый вопрос, я совсем не программист и пока "не догнал" как передать "portal в build()"

А вообще очень отличная библиотека, правда я в неё дописал пару функций, но в этом её и прелесть, всё понятно, всё читается, можно спокойно дописывать свои функции в конструктор! Спасибо за такую находку!

DenysChuhlib commented 2 years ago

Есть другой способ немного не удобен как для новой версии но я начал её делать на старой в setup() пишешь

portal.server.on(String(PSTR("/setings")).c_str(),
                   SetingsPage);

SetingsPage() будет вызываться когда будет открыта страница /setings

void SetingsPage() {
  portal.attachBuild(buildSetingsPage); // функция конструктора другой страницы
  portal.show();
  portal.attachBuild(build); // обратно ставим главную страницу которая будет вызываться любым (например /home) 
}
//кроме раньше зазначеных в portal.server.on
// можно также не ставить build если нужно чтобы главная страница открывалась ТОЛЬКО по какой-то ссылке

Согласен выглядит как небольшой изврат, но очень легко редактировать нужный конструктор страницы

DAK85 commented 2 years ago

Это слишком заморочено, я пока просто portal объявил глобально. возможно пока так всё и останется. Ну вот в обработчик событий можно передать локально объявленный портал, а вот как в конструктор его передать? по факту вроде ничего сложного не должно быть, я немного запутался в коде библиотеки, так бы уже подправил себе да жил спокойно.

GyverLibs commented 2 years ago

В следующем обновлении сделаю билдер с возможностью передать ссылку на объект портала. А вообще - можно через лямбда функцию сделать

void f() {
  GyverPortal portal;
  portal.attachBuild([](){
    // Бла бла сборка 
  });
}
GyverLibs commented 2 years ago

String(PSTR("/setings")).c_str()

Это что за покемон?)))) Достаточно сделать F("/строка"), см. урок https://alexgyver.ru/lessons/progmem/ Функция on принимает любые строки, не нужен ей char*

И кстати String(PSTR("/setings")) делать НЕЛЬЗЯ, по тем же причинам что вчера обсуждали. Можно через FPSTR() - String(FPSTR(PSTR("/setings"))). Но именно это и делает F()

DAK85 commented 2 years ago

Я вот одного понять не могу, вот есть GP.FORM_BEGIN("uri"), по факту этот ури должен совпадать с той страницей где сейчас находишься (так в документации написано), напрашивается сделать прямо вот так GP.FORM_BEGIN(portal.uri()) для удобства, но нет, uri вернёт стринг, её туда просто так не засунешь... Я вроде много уроков перечитал, но из стринга чар сделать одним движением не получается (может я чего то не понимаю), очень не удобно при создании страниц динамикой.

GyverLibs commented 2 years ago

но из стринга чар сделать одним движением не получается

str.c_str()

В следующей версии будет гораздо удобнее, выйдет в течение пары дней

DAK85 commented 2 years ago

А по поводу ещё пары хотелок можно написать? просто так, предложения по улучшению?

GyverLibs commented 2 years ago

Естественно, самое время, пока обновление не вышло)

DAK85 commented 2 years ago

SELECT бы доделать немного, сейчас в в селект value=name, что дико неудобно, хочется отдавать иначе.... я не программист, просто иногда бывает надо... я дописал себе такую штуку

void SELECT_2ARRAY(const char* name, byte* values, char* namevalues, int x, int y, int8_t maxcount, int sel = 0 ) {
        char temp_array[x][y];
        for (int xi=0;xi<x;xi++){
            for (int yi=0;yi<y;yi++){
                temp_array[xi][yi]=namevalues[xi*y+yi];
            }
        }
    if (sel < 0) sel = 0;
        *_GP += F("<select name=\"");
        *_GP += name;
        *_GP += F("\" id=\"");
        *_GP += name;
        *_GP += F("\" onchange=\"GP_click(this)\">\n");
        for(uint8_t count = 0; count < maxcount;count++){
            *_GP += F("<option value=\"");
            *_GP += values[count];
            *_GP += F("\"");
            if (values[count] == sel) *_GP += F(" selected");
            *_GP += F(">");
        *_GP += count;
        *_GP += F(". ");
            *_GP += temp_array[count];
            *_GP += F("</option>\n");
        }
        *_GP += F("</select>");
    }

В селект уходит ссылка на массив с значениями и с именами, у меня имена в двумерном массиве char, объявляю глобально, например char name[4][20]. В конструктор отдаю указатель (может путаю) на массив, и его размер. Может я что то делаю не так, но главное принцип, прямыми руками это можно сделать в разы лучше. Как пример, я в конструкторе вызываю следующим образом GP.SELECT_2ARRAY("addressRelay",AddressModule,*NameModule,MaxModuleCount,NameModuleLenght,ModuleCount,addressRelay[idrelay]) Последний аргумент, это значение, которое надо сделать selected, предпоследнее - это количество элементов в списке. Так же сделал вариант, когда значение от 0 по нарастающей

void SELECT_ARRAY(const char* name, char* namevalues, int x, int y, int8_t maxcount, int8_t sel = 0 ) {
        char temp_array[x][y];
        for (int xi=0;xi<x;xi++){
            for (int yi=0;yi<y;yi++){
                temp_array[xi][yi]=namevalues[xi*y+yi];
            }
        }
    if (sel < 0) sel = 0;
        *_GP += F("<select name=\"");
        *_GP += name;
        *_GP += F("\" id=\"");
        *_GP += name;
        *_GP += F("\" onchange=\"GP_click(this)\">\n");
        for(uint8_t count = 0; count < maxcount;count++){
            *_GP += F("<option value=\"");
            *_GP += count;
            *_GP += F("\"");
            if (count == sel) *_GP += F(" selected");
            *_GP += F(">");
        *_GP += count;
        *_GP += F(". ");
            *_GP += temp_array[count];
            *_GP += F("</option>\n");
        }
        *_GP += F("</select>");
    }

И да, храню настройки в flash памяти в файлах (), использую spiffs, хочется сделать возможность загрузки и скачивания конфигурации. Может это и возможно, но я пока не понял как это реализовать.

GyverLibs commented 2 years ago

Не очень понял в чём отличие и зачем. Можно отличия в HTML эквиваленте? И почему что дико неудобно и в каких случаях.

возможность загрузки и скачивания конфигурации

Очень удобный механизм загрузки и скачивания будет в следующем обновлении

GyverLibs commented 2 years ago

В текущей реализации получается выглядит так:

<select name='sel' id='sel' onchange='GP_click(this)'>
<option value='val 1'selected>val 1</option>
<option value='val 2'>val 2</option>
<option value='val 3'>val 3</option>
</select>
DAK85 commented 2 years ago

Это как из базы выбрать по индексу. Хранишь значение, а при настройке выбираешь имя. Выбор из справочника так сказать.

<select name='sel' id='sel' onchange='GP_click(this)'>
<option value='1'selected>1. Иванов</option>
<option value='2'>2. Петров</option>
<option value='3'>3. Сидоров</option>
</select>

Выбираешь Сидорова, а в sel уходит 3

GyverLibs commented 2 years ago

Согласен. Не помню почему не сделал так раньше, может были причины. Я тогда сделаю с автоматическим счётчиком, будет универсально и не такая жуткая функция получится, сейчас попробую

DAK85 commented 2 years ago

автоматический счётчик это хорошо, но бывает надо скажем взять значение из одного массива byte address[x], а имена из другого массива char name[x][y]. как бы получается что счётчик (i) идёт от 0 до maxcount, значения точно числовые, а вот имена чаще всего текстового формата.

name[i] <> Поэтому я и сделал 2 функции, единственно душой понимаю что нельзя вот так вот создавать временный массив просто для удобства, но не стал заморачиваться и сделал жутковато, зато работает (наверное моя главная проблема в esp32, у неё столько ресурсов, что я даже после arduino перестал на это смотреть)
GyverLibs commented 2 years ago

Я не очень понял зачем тут временный массив и как он должен работать. Получается массив то char, а передаются в него адреса строк. В любом случае идея хорошая, в том или ином виде добавлю в релиз, спасибо

DAK85 commented 2 years ago

я же в временный массив передаю через указатель значения из глобально определённого массива, а глобальный массив двумерный по факту я внутри функции делаю новый массив char (копия глобального)

GyverLibs commented 2 years ago

но зачем, можно ведь прямо из него к строке прибавлять)

DenysChuhlib commented 2 years ago

String(PSTR("/setings")).c_str()

Это что за покемон?)))) Достаточно сделать F("/строка"), см. урок https://alexgyver.ru/lessons/progmem/ Функция on принимает любые строки, не нужен ей char*

И кстати String(PSTR("/setings")) делать НЕЛЬЗЯ, по тем же причинам что вчера обсуждали. Можно через FPSTR() - String(FPSTR(PSTR("/setings"))). Но именно это и делает F()

Просто так получилось, PSTR() не работал там я просто String добавил и не заморачивался)

DAK85 commented 2 years ago

Это потому что я не умею этого делать, поэтому пишу тому, кто умеет! Честно! я наверное скоро и это научусь, но пока ещё не могу Ну раз пошла такая пьянка... вот есть GP_NUMBER, хочется чтобы подсказка была вида

#define  MaxCount 40
GP.NUMBER("RoomsCount", "Введите значение менее", MaxCount, RoomsCount)

Ну это так, просто хотелки, замучался я с преобразованием переменных....

GyverLibs commented 2 years ago

Это потому что я не умею этого делать, поэтому пишу тому, кто умеет!

Ну ты ведь читаешь из строки в строку. Почему просто не читать сразу из изначальной строки в *GP?)

замучался я с преобразованием переменных....

Не понял ничего)

DenysChuhlib commented 2 years ago

Кстати раз пошли предложения я зделал мини библиотеку https://github.com/DenysChuhlib/GyverPortalAddon с деталаями которых нету. Надеюсь вам что-то приглянётся)

GyverLibs commented 2 years ago

мини библиотеку

Во, хорошая идея для создания дополнений. Но к сожалению со следующего обновления это перестанет работать, я полностью переделал бэкэнд конструктора...

DenysChuhlib commented 2 years ago

Что-то придумаю)

DAK85 commented 2 years ago

Не понял ничего)

Подсказка должна быть передана в const char*, надо сперва выдергнуть число, переделать его в чар, потом подцепить к тексту подсказки, и только потом передать подсказку в GP.NUMBER, либо ткните меня носом в уроки, хотя я их наверное все перечитал

GyverLibs commented 2 years ago

Подсказка должна быть передана в const char*

Зачем, можно просто в char*. Стринг жрёт почти всё. По урокам мои вот, недавно обновлялись:

GyverLibs commented 2 years ago

Сделать можно было так:

И всё

GyverLibs commented 2 years ago

передать подсказку в GP.NUMBER

в следующей версии все функции конструктора принимают const String&, так что можно будет изи передавать любые данные через String(блабла)

DAK85 commented 2 years ago

дело в том, что у меня строки разной длинны, может быть 3 буквы, может 10... Я бы так и читал, const не могу, имена можно менять прямо с телефона, как понять что имя 3 буквы а не 10, не 16 (ещё и русские буквы)

DAK85 commented 2 years ago

Может так тогда

while (namesvalue[count]!=0){
s+=namesvalue[count];
count++;
}

После перескока на новое слова count увеличивать пропорционально длине строки...

GyverLibs commented 2 years ago

В том и дело, что это неважно. Строка оканчивается нулевым символом, программа сама определяет ее длину и прибавляет к стринге

DAK85 commented 2 years ago

я догнал! char**, это подразумевает двумерный массив, а count как раз его "ширину", гениально.... а как функцию вызывать?

DAK85 commented 2 years ago

А вот подобного метода не будет в новой библиотеки?

bool formSub(const String& name) {
      return _formF ? (_uri.startsWith(name) && (_uri.lastIndexOf("GP_click")==-1)) : 0; 
}

И главный вопрос, функции в классе так же легко будет переделывать под свои нужды?

GyverLibs commented 2 years ago

count как раз его "ширину"

Не совсем так. Для программы строка - это всего лишь указатель на первый символ. Программа не знает длины строки, но знает что она заканчивается /0. Массив names выше - это массив адресов первых символов строк, но передать его придется как **. Дальше думаю логика понятна.

GyverLibs commented 2 years ago

подобного метода

Не понял что это и зачем

GyverLibs commented 2 years ago

так же легко

По сути даже легче, самое главное что памяти драматически меньше будет жрать

DAK85 commented 2 years ago

Просто когда я захожу в пункт /set/relay у меня циклом создаётся около 40 BUTTON_LINK, когда я в них проваливаюсь у меня ури /set/relay/NUMBER (как пример /set/relay/10), там открывается форма, соответственно у неё точно такой же ури. Когда я в action проверяю события мне надо ловить все события с форм, которые начинаются на /set/relay/, как только у меня срабатывает это условие, я через formName() получаю uri строку, вычлиняю из неё последний индекс (10) и далее работаю с ним обновляя массивов по полученному индексу

GyverLibs commented 2 years ago

А разве недостаточно просто передать полный урл в form()? По поводу клика - в обновлении вызов формы игнорируется, если не переданы аргументы, то есть кнопка-ссыдка не приведет к триггеру обработки формы

DAK85 commented 2 years ago

Немного бреда, но может поймёте мою беду:

void build() {
  BUILD_BEGIN();
  GP.THEME(GP_DARK);
//************Тут меню настроек  
  if (portal.uri()=="/settings"){
    GP.BUTTON_LINK("/settings/system", "Системные настройки"); GP.BREAK();
    GP.BUTTON_LINK("/settings/rooms", "Настройки комнат"); GP.BREAK();
    GP.BUTTON_LINK("/settings/module", "Настройки модулей"); GP.BREAK();
    GP.BUTTON_LINK("/settings/button", "Настройки кнопок"); GP.BREAK();
    GP.BUTTON_LINK("/settings/relay", "Настройки реле"); GP.BREAK();
    GP.BUTTON_LINK("/settings/group", "Настройки групп"); GP.BREAK();
    GP.BUTTON_LINK("/settings/timer", "Настройки таймеров"); GP.BREAK();
    GP.BUTTON_LINK("/settings/saveload", "Загрузка/Сохранение"); GP.BREAK();
    GP.BUTTON_LINK("/", "Назад");

//************Тут системные настройки

  } else if (portal.uri()=="/settings/system") {
    GP.TITLE("Системные настройки");
    GP.FORM_BEGIN("/settings/system");
    GP.LABEL("Количество комнат:");GP.NUMBER("RoomsCount", MaxRoomsCount, RoomsCount);GP.BREAK();
    GP.LABEL("Количество модулей:");GP.NUMBER("ModuleCount", MaxModuleCount, ModuleCount);GP.BREAK();
    GP.LABEL("Количество реле:");GP.NUMBER("RelayCount", MaxRelayCount, RelayCount);GP.BREAK();
    GP.LABEL("Количество групп:");GP.NUMBER("GroupCount", MaxGroupCount, GroupCount);GP.BREAK();
    GP.LABEL("Количество реле в группе:");GP.NUMBER("GroupRelayCount", MaxGroupRelayCount, GroupRelayCount);GP.BREAK();
    GP.LABEL("Количество таймеров:");GP.NUMBER("TimerCount", MaxTimerCount, TimerCount);GP.BREAK();
    GP.LABEL("Количество отслеживаемых реле:");GP.NUMBER("TimerRelayCount", MaxTimerRelay, TimerRelayCount);GP.BREAK();
    GP.LABEL("Количество выходов на кнопки:");GP.NUMBER("OutputPinButtonCount", MaxOutputPinButtonCount, OutputPinButtonCount);GP.BREAK();
    GP.LABEL("Количество входов на кнопки");GP.NUMBER("InputPinButtonCount", MaxInputPinButtonCount, InputPinButtonCount);GP.BREAK();
    GP.SUBMIT("Применить");
    GP.FORM_END(); GP.BREAK();
    GP.BUTTON_LINK("/settings", "Назад");

 //************Тут кнопки с названием комнат   

  } else if (portal.uri()=="/settings/rooms") {
    for (int h=0;h<RoomsCount;h++){
      GP.BUTTON_LINK("/settings/rooms/", NameRooms[h], h, true); GP.BREAK();
    }
    GP.BUTTON_LINK("/settings", "Назад");

//************Тут форма на редактирование имени комнаты

  } else if (portal.uri().startsWith("/settings/rooms/")) {
    int idroom=portal.uri().substring(portal.uri().lastIndexOf("/")+1).toInt();
    GP.TITLE("Настройка комнат");
    GP.LABEL("Номер комнаты:");GP.LABEL(idroom);GP.BREAK();
    GP.FORM_BEGIN(portal.uri());
    GP.TEXT("NameRooms", "Введите имя комнаты", NameRooms[idroom]);GP.BREAK();
    GP.SUBMIT("Применить");
    GP.FORM_END(); GP.BREAK();
    GP.BUTTON_LINK("/settings/rooms", "Назад");

//************Тут кнопки с названием модулей

  } else if (portal.uri()=="/settings/module") {
    for (int h=0;h<ModuleCount;h++){
      GP.BUTTON_LINK("/settings/module/", NameModule[h], h, true); GP.BREAK();
    }
    GP.BUTTON_LINK("/settings", "Назад");

//************Тут формы для редактировния параметров модулей    

  } else if (portal.uri().startsWith("/settings/module/")) {
    int idmodule=portal.uri().substring(portal.uri().lastIndexOf("/")+1).toInt();
    GP.TITLE("Настройка Модуля");
    GP.LABEL("Номер модуля:");GP.LABEL(idmodule);GP.BREAK();
    GP.FORM_BEGIN(portal.uri());
    GP.TEXT("NameModule", "Введите имя модуля", NameModule[idmodule]);GP.BREAK();
    GP.NUMBER("AddressModule", "Введите адрес модуля", AddressModule[idmodule]);GP.BREAK();
    GP.SUBMIT("Применить");
    GP.FORM_END(); GP.BREAK();
    GP.BUTTON_LINK("/settings/module", "Назад");
...................

Так просто удобнее, иначе вот как, изначально брать имя формы потом его анализировать.... Это и так только начало, будет раза в 4 больше....

upd: Не судите, часть функций в библиотеки дописал чтобы стало хоть немного изи

Не то немного вставил, вот актион

if (portal.formSub("/settings/rooms/")){
      int idroom=portal.formName().substring(portal.formName().lastIndexOf("/")+1).toInt();
      portal.getString("NameRooms").toCharArray(NameRooms[idroom],NameRoomsLenght);
    }

    if (portal.formSub("/settings/module/")){
      int idmodule=portal.formName().substring(portal.formName().lastIndexOf("/")+1).toInt();
      AddressModule[idmodule]=portal.getInt("AddressModule");
      portal.getString("NameModule").toCharArray(NameModule[idmodule],NameModuleLenght);
    }

    if (portal.formSub("/settings/relay/")){
      int idrelay=portal.formName().substring(portal.formName().lastIndexOf("/")+1).toInt();
      pinRelay[idrelay]=portal.getInt("pinRelay");
      addressRelay[idrelay]=portal.getInt("addressRelay");
      RoomRelay[idrelay]=portal.getInt("RoomRelay");
      portal.getString("NameRelay").toCharArray(NameRelay[idrelay],NameRelayLenght);
    }

    if (portal.formSub("/settings/group/")){
      int idgroup=portal.formName().substring(portal.formName().lastIndexOf("/")+1).toInt();
      GroupRelay[idgroup][0]=portal.getInt("GroupRelayCount");
      for(int idgrouprelay=1;idgrouprelay<=GroupRelay[idgroup][0];idgrouprelay++){
        char textnamerelay[8];char idstr[2];itoa(idgrouprelay,idstr,DEC);strcpy(textnamerelay,"relay@");strcat(textnamerelay,idstr);
        GroupRelay[idgroup][idgrouprelay]=portal.getInt(textnamerelay);
      }

Так просто реально удобно

DAK85 commented 2 years ago

и ещё, когда я делаю клик на кнопке на странице uri:/settings/saveload Событие клика приходит со страницы uri:/settings/GP_click соответственно тот метод, который сейчас у Вас в библиотеки не видит этого события, об этом я уже писал в самом начале

GyverLibs commented 2 years ago

В самом начале до конца не прочитал, думал там всё про проблему локального портала. Гляну

DAK85 commented 2 years ago

Наверное хватит отнимать Ваше время! Спасибо за Ваши труды! И уроки классные, сайт просто Огонь, форум читаю, телегу смотрю, когда на всё находите время я даже не представляю... Здоровья, процветания! Отдельно спасибо за уделённое время!

GyverLibs commented 2 years ago

Всегда пожалуйста)

GyverLibs commented 2 years ago

так, не понял проблему с формами. Вот есть у меня

if (portal.uri() == "/save/sub") {
  GP.FORM_BEGIN("/save/sub");
  GP.SUBMIT("Submit");
  GP.FORM_END();
}

И проверяю как

if (portal.form("/save/sub"))

Работает же

DAK85 commented 2 years ago

Ну это понятно что работает, а если у тебя генерируется /save/sub/key@0, /save/sub/key@1, /save/sub/key@2 ... /save/sub/key@N, тогда надо сперва дёргать название формы, потом проверять на что оно заканчивается или на что начинается, выдёргивать ID, а можно просто сделать в action написать

if (portal.formSub("/save/sub/key")){
      int idkey=portal.formName().substring(portal.formName().lastIndexOf("/key@")+5).toInt();

И это условие сработает если из любой формы, имя которой начинается на "/save/sub/key" прилетит submit

А в твоём варианте надо будет изначально брать адрес формы через formName() и потом делать условие не начинается ли имя пришедшей формы с "/save/sub/key". Просто удобнее, но можно и обойтись.... Я уже думаю на тему того, что проще в action один раз выдергнуть имя формы и потом его гонять по if else if else if....

GyverLibs commented 2 years ago

Теперь понял. Нужно чтобы одно условие сработало от всех форм, находящихся на указанном урл или глубже. По сути на фронте это можно сделать как

if (form() && uri.indexof blabla)

?

DAK85 commented 2 years ago

можно и так, только наверное if (form() && uri.startsWith blabla), эффект наверное будет тот же... Я ни на что не претендую, я с нетерпением жду новой версии библиотек, обновлюсь и буду пытаться всё сделать без редактирования кода библиотеки. Если появится select из двух массивов и плюс ко всему события GP_Click с вложенных страниц будут ловится хорошо, то мне больше ничего и не надо... Просто сам факт загрузки фалов и передачи в build экземпляра локально объявленного GyverPortal это уже отличная новость! Ждёмс релиза!

GyverLibs commented 2 years ago

Да, остальное всё я уже сделал. Только по форме остался вопрос, просто не хочется добавлять функцию, к которой сложно придумать простой пример. Ну и раз оно заменяется полностью конструкцией из других стандартных функций, то можно уже не добавлять) я могу рабочую версию дать пощупать, может ещё какие косяки всплывут пока я примеры и доку обновляю

DAK85 commented 2 years ago

а я бы не отказался пощупать, посмотреть!

GyverLibs commented 2 years ago

GyverPortal.zip

DAK85 commented 2 years ago

Сколько у меня времени?