GyverLibs / GyverPortal

Простой конструктор веб интерфейса для esp8266 и ESP32
MIT License
302 stars 26 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()"

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

GyverLibs commented 2 years ago

несколько дней, пока нет возможности всё оформить и опубликовать чтобы красиво было

DAK85 commented 2 years ago

ок.... Спасибо! Думаю завтра к вечеру отпишусь....

DAK85 commented 2 years ago

Было так:

GP.SELECT_ARRAY("TRelay@1",*NameRelay,MaxRelayCount,NameRelayLenght,RelayCount,IfRelay[idtimer][1]);`

Сделал

GP.SELECT_IDX("TRelay@1",*NameRelay,IfRelay[idtimer][1])

NameRelay определён глобально char NameRelay[40][32]

Компилируется без ошибок, только через веб форму вижу только первое значение На портал идёт

option value=0> 0. Люстра зал </option

Нет других опций... наверное я просто неправильно вызываю функцию...

UPD: Я понял, вместо NameRelay надо было подготовить к передачи string, в котором значения разделены запятыми...

GyverLibs commented 2 years ago

NameRelay[40][32] - нет смысла так делать, я выше предлагал более правильный вариант. Не нужно делать работу компилятора, он сам справится)

GyverLibs commented 2 years ago

Select с двумя массивами я не стал делать. Но сделал файлик custom.h, в котором можно делать свои компоненты, чтобы удобнее переносить в случае обновления библиотеки

DAK85 commented 2 years ago

SELECT_IDX s.reserve(250); не мало? 70 только основной скелет, плюс к каждому значению ещё около 30 символов от скелета. Это без учёта длины имени формы (+ 2 длинны)... мне кажется будет маловато...

DAK85 commented 2 years ago

Select с двумя массивами я не стал делать. Но сделал файлик custom.h, в котором можно делать свои компоненты, чтобы удобнее переносить в случае обновления библиотеки

За это отдельное спасибо!:) В принципе всё спокойно завелось, сейчас только разберусь со своими функциями:), пытаюсь делать чтение из памяти, чтобы просто научится это делать...

DenysChuhlib commented 2 years ago

GyverPortal.zip

Я только глянул уже придумал как переделать мини библу)

DAK85 commented 2 years ago

!!ВНИМАНИЕ!! GP.LABEL ("Если в здесь очень длинный текс, то раньше он перносился, а теперь он едет за экран..... Раньше было интереснее:)")

DAK85 commented 2 years ago

Вообще круто... Делаю так

for (int h=0;h<RelayCount;h++){
      GP.BUTTON_LINK("/settings/relay/"+String(h),String(h) + ". " + NameRelay[h]); GP.BREAK();
    }
или даже так
String ne_bolee="Не должно превышать ";
    GP.LABEL("Количество комнат:");GP.NUMBER("RoomsCount", ne_bolee + String(MaxRoomsCount), RoomsCount);GP.BREAK();
    GP.LABEL("Количество модулей:");GP.NUMBER("ModuleCount", ne_bolee + String(MaxModuleCount), ModuleCount);GP.BREAK();

и библиотека всё скушала и сделала как я хочу! Я ещё не дошёл до загрузки файлов, но то, что библиотека кушает такие штуки - просто ОГОНЬ! Прям для WEB разработчиков, минимум заморочки с типами переменных, максимум простоты! Респект! Лично я в восторге!

DAK85 commented 2 years ago

!!! ПОДПРАВИТЬ ДЕМКУ НА ЛОКАЛЬНЫЙ ЗАПУСК demoLocal.ino

void build(GyverPortal& p)

Проверил, всё работает без глобального объявления!

GyverLibs commented 2 years ago

s.reserve(250); не мало?

reserve тут сделан чисто для ускорения сборки строки, но в рамках есп8266/32 это абсолютно ничего не даёт, можно убрать все reserve из кода. Чисто привычка делать максимально оптимально

теперь он едет за экран

Это странно, пофикшу

Правка примеров

Примеры пока вообще не трогал, нужно все обновить и проверить

DenysChuhlib commented 2 years ago

Очень круто, спасибо за ваше старание)

DenysChuhlib commented 2 years ago

У меня есть небольшая прозьба. Можите добавить

#ifndef GP_UPLOAD_PAGE_ERROR
#define GP_UPLOAD_PAGE_ERROR "<h2>Upload Error, <a href='/'>return back</a></h2>"
#endif

и в portal.h рядке 130 сделать так

server.send(200, "text/html", F(GP_UPLOAD_PAGE_ERROR));

Чтобы можно было свою страницу впихнуть)

GyverLibs commented 2 years ago

да, подумаю и добвавлю

GyverLibs commented 2 years ago

вообще хорошо бы редиректить на ту же страницу, где была кнопка, но красиво как сделать пока не придумал)

GyverLibs commented 2 years ago

Либо как в строке 67

А у тебя так работает? Upload происходит на странице с другим url, если ее обновить ничего не произойдёт

DAK85 commented 2 years ago

Я вообще не понимаю пока цели всего, что выше описано:), но вот с загрузкой фалов интересно получается. Захотел сделать себе кнопку, Скачать текущие настройки.... Промучился немного, в итоге дописал в custom

void GP_BUTTON_DOWNLOAD(const String& url, const String& value, GPstyle st = GP_GREEN) {
    String s;
    s.reserve(120);
    s += F("<a style=\"text-decoration: none;\" href='");
    s += url;
    s += F("' download><button ");
    if (st != GP_GREEN) {
        s += F("style='background:");
        s += GPgetStyle(st);
        s += F("' ");
    }
    s += F("'>");
    s += value;
    s += F("</button></a>\n");
    GP.CUSTOM(s);
}

При нажатие на кнопку открывается диалог сохранения файла и всё спокойно качается... Может можно как то иначе, но я с наскоку всё пересмотрел и ничего лучше не нашёл.

GyverLibs commented 2 years ago

Может можно как то иначе

Кнопка-ссылка, усё

DAK85 commented 2 years ago

Может можно как то иначе

Кнопка-ссылка, усё

Я так пробовал делать, у меня просто открывается файл и вываливает содержимое на экран браузера.

GyverLibs commented 2 years ago

А, понял. Добавлю тогда ещё кнопок

DenysChuhlib commented 2 years ago

А, понял. Добавлю тогда ещё кнопок

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

if (download) {
      portal.server.sendHeader("Content-Type", "text/text");
      portal.server.sendHeader("Content-Disposition", "attachment; filename=" + String(download.name()));
      portal.server.sendHeader("Connection", "close");
      portal.server.streamFile(download, "application/octet-stream");
      download.close();
    }
DenysChuhlib commented 2 years ago

А у тебя так работает? Upload происходит на странице с другим url, если ее обновить ничего не произойдёт

От же я делал пример https://github.com/GyverLibs/GyverPortal/pull/22/files#diff-c2dcc64c24dad60a9bf9663788b292bf3617c19b2b2159f5ddf5fcb667d112a0

И от https://github.com/GyverLibs/GyverPortal/pull/22/files#diff-d909e5c6aeb1a5cc9bc2b7d9531fe51550a0c1d5574e4a78d8976648a98de4e4R30

GyverLibs commented 2 years ago

можно просто после нажатия кнопки вызвать

streamFile уже заложен в механизме скачивания по урл

DenysChuhlib commented 2 years ago

Но можно добавить вариант и по кнопке)

DenysChuhlib commented 2 years ago

И кстати по урл вы сделали открытие файла а скачивать удобнее так

if (download) {
      portal.server.sendHeader("Content-Type", "text/text");
      portal.server.sendHeader("Content-Disposition", "attachment; filename=" + String(download.name()));
      portal.server.sendHeader("Connection", "close");
      portal.server.streamFile(download, "application/octet-stream");
      download.close();
    }
GyverLibs commented 2 years ago

по урл вы сделали открытие файла

По урл я сделал так, чтобы работали теги img embed и прочие

DenysChuhlib commented 2 years ago

Можите добавить в FILE_UPLOAD accept что бы можно было выберать только нужные типы файлов

void FILE_UPLOAD(const String& name, const String& accept = "") {
        String s;
        s.reserve(250);
        s += F("<form action='/GP_upload' method='POST' enctype='multipart/form-data' id='");
        s += name;
        s += F("_form'>\n");
        s += F("<div id='ubtn' onclick='getFile(\"");
        s += name;
        s += F("_inp\")'>📁</div>\n");
        s += F("<div id='ubtnclr'><input name='");
        s += name;
        s += F("' id='");
        s += name;
        s += F("\" accept=\"");
    s += accept;
        s += F("_inp' type='file' onchange='GP_subm(\"");
        s += name;
        s += F("_form\")'/></div>\n");
        s += F("</form>\n");
        _gp_s->sendContent(s);
    }
DAK85 commented 2 years ago

Ещё вопрос, может просто я что то не так делаю, а может это "баг" у меня запуск портала локальный

void taskCore1(void * pvParameters) {
for(;;){
  GyverPortal portal;
  portal.attachBuild(build);
  portal.attach(action);
  portal.attachUpload(upload);
  portal.start();
  while (portal.tick());
}
}

Так у меня выглядит upload

void upload(HTTPUpload& upl) {
  Serial.println(upl.filename);
  Serial.println(upl.name);
  portal.file = SPIFFS.open('/' + upl.filename, "w");
}

Компилятор меня материт 'portal' was not declared in this scope

Далее объявляю портал глобально, всё компилится, загружаю конфиг, в serial летит

CurrentSettings.conf
DefaultSettings.conf

Меня перекидывает на рутовую страницу сайта, но при этом файл не загружается....

DenysChuhlib commented 2 years ago

Надо так:

void upload(HTTPUpload& upl, GyverPortal& portal) {
  Serial.println(upl.filename);
  Serial.println(upl.name);
  portal.file = SPIFFS.open('/' + upl.filename, "w");
}
DAK85 commented 2 years ago

Надо так:

Спасибо! Скопировал с примера и не разобрался, самый смак в последнем.. filename - это имя файла, который я загружаю, а вот .name это как раз то имя, которое я отдаю через конструктор Сделал так и всё теперь работает

void upload(HTTPUpload& upl, GyverPortal& p) {
  Serial.println(upl.filename);
  Serial.println(upl.name);
  p.file = SPIFFS.open('/' + upl.name, "w");
}

И да.. после загрузки остался на той же страницы... Всё Ок... По поводу кнопки на загрузку... я и сам теперь допишу всё что не штатно и вякать не буду, просто фреймворк рассчитан для простоты, а кнопка как раз неплохо смотрится, тем более что нет элемента который открывает диалог сохранения файла (download), так что элемент считаю не лишним, но при этом ни на что не претендую, а делать так как выше упомянуто слишком сложно для простого обывателя.

GyverLibs commented 2 years ago

загрузку/скачивание допилю, Денис накидал интересных кусков

DAK85 commented 2 years ago

Даже не знаю как сформулировать.. Аутентификация... Планируется многопользовательская аутентификация? Хочется чтобы пользователи своей сети ходили спокойно на портал, но при входе в настройки запрашивался логин и пароль. Ну и соответственно после авторизации далее ходить без проблем по меню настроек. При этом если ip клиента не принадлежит сети, то для данного клиента вход запретить без логина и пароля... Так бы в конструкторе получать логин пользователя и рулить пунктами меню... ну это так, хотелки... Вишенкой на торте была бы поддержка https

GyverLibs commented 2 years ago

Аутентификация

только то, что предлагает библиотека httpwebserver

поддержка https

Зачем?) На 8266 это скорее всего невозможно, а на 32 - со скрипом

DAK85 commented 2 years ago

Затем чтобы никто через инет не управлял esp. Мелочи, попробую на микроте настроить проксирование с аутентификацией

GyverLibs commented 2 years ago

то есть когда есп подключена к роутеру, можно через интернет взломать и управлять еспхой?)

DAK85 commented 2 years ago

не совсем так, когда на роутере с внешнего ip пробрасываешь порт на esp (скажем чтобы алиса с кузей могли команды давать) все запросы идут без шифровки, и если где нить в кафешки подключишься через прозрачный прокси (о котором даже и не узнаешь) можно спокойно попасть даже с аутентификацией. тут дело такое, чреватое. а вот https шифрует всю передаваемую инфу и это уберегает от таких попадосов....

GyverLibs commented 2 years ago

Аа. Хорошая практика - никогда не подключаться к общественным точкам)

DenysChuhlib commented 2 years ago

Даже не знаю как сформулировать.. Аутентификация... Планируется многопользовательская аутентификация? Хочется чтобы пользователи своей сети ходили спокойно на портал, но при входе в настройки запрашивался логин и пароль. Ну и соответственно после авторизации далее ходить без проблем по меню настроек. При этом если ip клиента не принадлежит сети, то для данного клиента вход запретить без логина и пароля... Так бы в конструкторе получать логин пользователя и рулить пунктами меню... ну это так, хотелки... Вишенкой на торте была бы поддержка https

Попробуй сделать сам)

DenysChuhlib commented 2 years ago
#if defined(FS_H) && !defined(GP_NO_UPLOAD)
        server.on("/GP_upload", HTTP_POST, [this]() {
            server.send(200, "text/html", F("<script>setInterval(function(){if (history.length > 0) window.history.back(); else window.location.href = \"/\";},500);</script>"));
            //server.send(200);
        }, [this]() {
            if (_uploadM || _uploadMR) {
                if (_uploadM) _uploadM(server.upload());
                else _uploadMR(server.upload(), *this);
            }//else что бы можно было пользоваться и тем и тем для простоты
            if (_uploadA || _uploadAR) {
                HTTPUpload& upl = server.upload();
                switch (upl.status) {
                case UPLOAD_FILE_START:
                    if (!file) {
                        if (_uploadA) _uploadA(upl);
                        else _uploadAR(upl, *this);
                    }
                    break;
                case UPLOAD_FILE_WRITE:
                    if (file) file.write(upl.buf, upl.currentSize);
                    break;
                case UPLOAD_FILE_END:
                    if (file) file.close();
                    //show();
                    break;
                case UPLOAD_FILE_ABORTED:
                    if (file) file.close();
                    //server.send(200, "text/html", F("<h2>Upload Error, <a href='/'>return back</a></h2>"));
                    break;
                }
            }
        });
        #endif

Всё что закоментировано не нужно Я наконец-то смог сделать что-бы оно после выгрузки просто открывало предыдущий url, а не показывало страницу этого url и то надо было бы её настроить на /GP_upload. И сделал чтобы запускалися обработчики Upload и UploadManual одновременно(по очереди) для того чтобы можно было одно делать в Manual например UpdateOTA (который я сделал на основе дэфолтных элементов, тоесть без своих), а другое просто сразу с файлом в одном скэтче. И да при UPLOAD_FILE_ABORTED оно тоже просто вернёт предыдущий url в браузере.

DAK85 commented 2 years ago

сейчас пытаюсь (почти пытаюсь) выдергнуть ip клиента и пока ещё не осознал как это сделать... Вот было бы неплохо, если бы вызвал if (portal.ipClient_inhereNet(192.168.10.0/24) рисуем это меню, если нет то другое. Я по итогу всё равно себе напишу сиё чудо, так как без него мой проект помрёт. Но в принципе подобный метод был бы полезен в данном фреймворке

GyverLibs commented 2 years ago

подобный метод был бы полезен в данном фреймворке

Жду пулл реквеста)

GyverLibs commented 2 years ago

Я наконец-то смог сделать

Отлично, добавлю в релиз тогда

DAK85 commented 2 years ago

подобный метод был бы полезен в данном фреймворке

Жду пулл реквеста)

Пока только получилось добыть ip адрес клиента дописав пару строк в portal.h

    IPAddress client_remoteIP() {
        return server.client().remoteIP();
    }

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

DenysChuhlib commented 2 years ago

server.send(200, "text/html", F(""));

Здесь нужно 500 заменить на 300 так более лутше работает)

DAK85 commented 2 years ago

подобный метод был бы полезен в данном фреймворке

Жду пулл реквеста)

Если не станешь добавлять в новую, выложу в пулл реквест

// ================== IP REMOTE CLIENT ==================
// вернёт IP адрес клиента
    IPAddress client_remoteIP() {
        return server.client().remoteIP();
    }
// вернёт true, ксли IP адрес клиента принадлежит той же сети что и переданный адрес в формате XXX.XXX.XXX.XXX/XX
    bool clientFromNet(const String& ip_str){
        IPAddress RclIP=server.client().remoteIP();             //Достаём адрес клинета
        IPAddress NetIP;
        NetIP.fromString(ip_str);                       //Конвертируем стринг в подходящий формат
        int8_t mask=ip_str.substring(ip_str.lastIndexOf("/")+1).toInt();    //Достаём маску сети 
        byte netmask[4]={255,255,255,255};                  //Делаем маску /32
        for (int r=0; r<(32-mask);r++) bitClear(netmask[3-r/8],r%8);        //Конвертируем формат маски
        for (int r=0; r<4;r++) {                        //Вычисляем адреса сетей относительно маски
            RclIP[r] &= netmask[r];                     //сперва клиента
            NetIP[r] &= netmask[r];                     //потом адрес сети IP переданного в функцию
        }
        return NetIP == RclIP ? 1 : 0;                      //Если адреса сетей совпадают, значит они из одной сети
    }
GyverLibs commented 2 years ago

выложу в пулл реквест

О, стандартными средствами. Тогда спокойно можно добавлять в релиз. А не будет более красиво передавать ip объектом ipaddress, а то что за / - интом? Класс ипадресс поддерживает конвертацию из стринга, и хранить удобнее будет

DAK85 commented 2 years ago

Так будет в сто раз проще. Просто я как долбанный админ с большим стажем подумал что удобнее так писать. Хотя сам изначльно хотел передавать clientFromNet(IPAddress NetIP,int8_t mask )

bool clientFromNet(IPAddress NetIP,int8_t mask){
        IPAddress RclIP=server.client().remoteIP();             //Достаём адрес клинета
        byte netmask[4]={255,255,255,255};                  //Делаем маску /32
        for (int r=0; r<(32-mask);r++) bitClear(netmask[3-r/8],r%8);        //Конвертируем формат маски
        for (int r=0; r<4;r++) {                        //Вычисляем адреса сетей относительно маски
            RclIP[r] &= netmask[r];                     //сперва клиента
            NetIP[r] &= netmask[r];                     //потом адрес сети IP переданного в функцию
        }
        return NetIP == RclIP ? 1 : 0;                      //Если адреса сетей совпадают, значит они из одной сети
    }
DAK85 commented 2 years ago

Думаю пора закрывать данное issues. Я придумал как создать псевдо авторизацию в режиме админа и гостя. Надо создать пару функций типо setguestauth и setadmin auth. Передавать через эти функции параметры в класс. Потом авторизовывать и записывать адрес клиента в массив, который надо тоже хранить в классе .. очень прошу, закрой иссуе... Выйдет новая версия библиотеки, я напишу авторизацию в пулл реквест и постараюсь подготовить примеры использования.

GyverLibs commented 2 years ago

Ишью висит как напоминание о том, что нужно добавить) добавлю всё что обсуждали и что точно есть рабочие куски, и закрою