RFC: Migration zu neuer Weboberfläche

Es gab ja hier schon mehrmals Ambitionen, ein neues Frontend zu schreiben. Das aktuelle funktioniert zwar gut, hat aber auch ein paar Nachteile aus Entwicklersicht.

Durch die letzte Aufmerksamkeit, die die UI bekommen hat und auch weil ich es etwas Schade finde, wenn die Arbeit verloren geht, die manche sich schon gemacht haben, habe ich mir überlegt, wie man weiter vorgehen kann.

Ich denke, die vorangegangenen Prototypen sind aus einem wesentlichen Grund im Sande verlaufen: Zu umfangreiche Änderungen sowohl im frontend als auch backend. Das liegt meiner Ansicht nach am Template-Processing, welches das Backend mit der UI koppelt.

Daher schlage ich vor, nach und nach eine JSON-API einzuführen und den Template-Processor zu entfernen.

So können wir mit kleinen Änderungen zu einem Stand kommen, der freundlicher gegenüber neuen UIs ist.

Die folgenden Schritte wären notwendig:

  1. JSON API drastisch erweitern, um alle settings etc. abfragen zu können
  2. Im HTML die neuen APIs verwenden, um letztendlich eine statische Website ohne Templates zu erhalten
  3. Frontend inklusive externer Abhängigkeiten als komprimiertes bundle auf dem ESP32 ablegen.
  4. Potentielle neue Website kann gegen API entwickelt werden und parallel zu alter UI verwendet werden (compile flag)

Das würde die folgenden Vorteile mit sich bringen:

  • Neue UIs können gegen das API-Backend entwickelt werden. Das schließt natürlich Web-frontends, aber auch native Apps oder Heimautomatisierung/Skripte mit ein
  • Statisches Frontend kann komprimiert werden und ggf. alle Abhängigkeiten mitbringen (webpack)
  • Neue Features lassen sich schneller umsetzen
  • Vereinheitlichung von Management und Accesspoint UI

Das setzt natürlich voraus, dass grundsätzlich Willen da ist, den aktuellen Python HTML Preprocessor durch standard web-technologien wie vite oder webpack zu ersetzen.

Eine vorherige Diskussion fand bereits hier statt. Ich möchte daran anknüpfen, aber explizit die neue Oberfläche ausklammern, da das viel zu umfrangreich wäre. Es soll darum gehen, den Weg für zukünftige Änderungen zu ebnen. Jeder Schritt soll dabei übersichtlich und iterativ sein.

Mich würde mal interessieren was ihr von dem Plan haltet. Findet ihr eine neue UI unnötig? Wollt ihr auf keinen Fall ein Framework wie Vue oder React? Findet ihr die JSON-API gut aber das Frontend soll so bleiben?

:+1: Mir gefällt es die Entwicklung in diesen kleineren Schritten durchzuführen, damit machen wir nichts kaputt- Wenn nur eine Funktion wegfällt wird es sofort Protest geben.

2 „Gefällt mir“

Also für mich ist das ok. Ich habe allerdings im Hinterkopf, dass der Umgang mit ArduinoJson zuweilen nicht so „lightweight“ ist und man teilweise Probleme hat, die Größe eines Json-Objekts vorher abzuschätzen. Ja und auch generell, dass man sich den Heap nicht fragmentiert. Um das schlanker zu bekommen, haben wir teilweise das json auch selbst schon „von Hand“ geschrieben.

Also solche Sachen muss man beim Design halt beachten. Möglicherweise ist das aber auch alles nicht so wild und meine Bedenken kommen hauptsächlich von sehr komplexen Objekten. Für die meisten Sachen wird das vermutlich keine Rolle spielen.

Webpack hatte ich mir auch mal angeschaut, aber dabei ist es letztlich geblieben :joy:.

3 „Gefällt mir“

Ich habe zwar nur die Hälfte verstanden (das Defizit liegt auf meiner Seite :slight_smile: )

Ich fände es aber gut, wenn das UI weiterentwickelt wird, da ich kein MQTT verwende.

Ich fange mal an mit Schritt 0, die HTML Abhängigkeiten aus dem Backend zu entfernen:

Refactoring für die Tabs „MQTT“, „FTP“ und „Bluetooth“, raus aus web.cpp und in management.html verschieben

Gibt es evt.eine elegantere Möglichkeit, die Tabs zur Laufzeit sichtbar/unsichtbar zu machen? Es soll ja beim Laden nichts ruckeln…

1 „Gefällt mir“

Du könntest den wert mit einem data attribut in dem element speichern und dann das ausblenden via CSS durchführen. Könnte dann etwa so aussehen:
<a [...] data-enabled="%SHOW_MQTT_TAB%">

[data-enabled="false"] {
  display: none;
}

Bin aber nicht tief genug in der Programmierung drinnen um sagen zu können welche Werte hier angenommen werden.

2 „Gefällt mir“

@Morik ja so ist es eleganter. Allerdings wurde das Ruckeln beim Laden von fehlender Beschriftung & I18N verursacht. Hab’s entsprechend angepasst

Hallo zusammen,
ich würde die Aufräumarbeiten im „Backend“ sehr begrüßen! Das würde mir die hoffentlich zukünftige Arbeit am Frontend deutlich erleichtern. (Und ja, ich musste und muss eine Auszeit nehmen beim Frontend, da unter anderem K2 da ist und viel Aufmerksamkeit fordert…)

Zur API selbst: Ich hatte in meiner UI damit geplant, dass intern quasi eine JSON Struktur mit allen Einstellungen vorliegt (hier der JS Part):

let settings = {
    general: {
        volume_start_percent: undefined,
        volume_max_speaker_percent: undefined,
        volume_max_headphones_percent: undefined,
        led_restart_percent: undefined,
        led_night_percent: undefined,
        power_saving_minutes: undefined,
        battery_warning_voltage: undefined,
        battery_low_voltage: undefined,
        battery_high_voltage: undefined,
        battery_interval_minutes: undefined,
    },
    wifi: {
        wifi_ssid: undefined,
        wifi_password: undefined,
        wifi_hostname: undefined,
    },
    ftp: {
        ftp_enabled: undefined,
        ftp_username: undefined,
        ftp_password: undefined,
    },
    mqtt: {
        mqtt_enabled: undefined,
        mqtt_host: undefined,
        mqtt_port: undefined,
        mqtt_username: undefined,
        mqtt_password: undefined,
    }
}

Die Idee war, dass man dieses JSON - ggf. auch unvollständig - an einen Endpoint (HTTP PUT) übergeben kann zum Setzen der Einstellungen. Ebenso kann man beim Lesen (HTTP GET) entweder nichts angeben und bekommt dann alles zurück, oder nur einen Key, egal in welcher Tiefe des JSON-Baues, also „wifi“ oder „ftp_username“.

Möglicherweise ist die Grundidee etwas zu schwergewichtig für den ESP, aber es hat die Dinge im Frontend Code sehr schön kompakt und dennoch lesbar gestaltet. Betrachtet es als eine weitere Idee für den Pool.

EDIT:
Ich würde sehr zum Einsatz von einem Bundler wie Webpack, Rollup oder Vite (mein Favorit) raten, da sie in der Regel Tree Shaking unterstützen (nicht genutzte HTML/CSS/JS Teile entfernen) und auch eine anschließende Minification des Codes anbieten, um teure Bytes zu sparen.

1 „Gefällt mir“

Hallo @sonovice, schön, dass Du mal wieder vorbeischaust & Glückwunsch zu K2!

Nach dem Refactoring sollte es genau so sein, wie Du es beschrieben hast. Und es wäre fein, wenn Dein Frontend irgendwann weiterentwickelt wird, viele hier waren von Deiner bisherigen Abeit begeistert!

Zuerst müssen wir jedoch den Template-Prozessor ersetzen, weil der alles weitere blockiert. Die Einstellungen sollen über ein /settings GET geladen werden können (Ein PUT ist bereits vorhanden nur eben über das Websocket. Hier können auch schon einzelne Einstellungen gesetzt werden.

Ich habe /settings GET mal testweise mit Arduino-JSON umgesetzt und kann keine Heapprobleme feststellen, das JSON ist auch recht klein. Also vom Backend könnte ich das zur Verfügung stellen, es müsste sich nur jemand finden, der die Templateplatzhalter im aktuellen UI durch die neuen Einstellungen ersetzt, also ein JavaScript Experte. Das kann ich nicht so gut. Könnte das jemand mit mir zusammen machen?

Hier ist eine Liste der %Template-Platzhalter% und wie sie in management.html ersetzt werden könnten. Der Name ist genau der, den wir beim Socket/PUT verwenden, eine spätere Umbenennung wäre wohl sinnvoll:

// current
%IPv4%				    current.iPv4
%RFID_TAG_ID%			current.rfidTagId
%HOSTNAME%			    current.hostname
%CURRENT_VOLUME%		current.volume

// general
%INIT_LED_BRIGHTNESS%		general.iBright
%NIGHT_LED_BRIGHTNESS%		general.nBright
%MAX_INACTIVITY%		    general.iTime
%INIT_VOLUME%			    general.iVol
%MAX_VOLUME_SPEAKER%		general.mVolSpeaker
%MAX_VOLUME_HEADPHONE%		general.mVolHeadphone

// battery
%WARNING_LOW_VOLTAGE%		battery.vWarning
%VOLTAGE_INDICATOR_LOW%		battery.vIndLow
%VOLTAGE_INDICATOR_HIGH%	battery.vIndHigh
%BATTERY_CHECK_INTERVAL%	battery.vInt

// FTP
%SHOW_FTP_TAB%			ftp.ftpAvailable
%FTP_USER%			    ftp.ftpUser
%FTP_PWD%			    ftp.ftpPwd
%FTP_USER_LENGTH%		ftp.maxUserLength		** fest 10 Zeichen
%FTP_PWD_LENGTH%		ftp.maxPwdLength		** fest 15 Zeichen

// MQTT
%SHOW_MQTT_TAB%			mqtt.mqttAvailable
%MQTT_ENABLE%			mqtt.mqttEnable
%MQTT_CLIENTID%			mqtt.mqttClientId
%MQTT_SERVER%			mqtt.mqttServer
%MQTT_USER%			    mqtt.mqttUser
%MQTT_PWD%			    mqtt.mqttPwd
%MQTT_USER_LENGTH% 		mqtt.maxUserLength		** fest 16 Zeichen
%MQTT_PWD_LENGTH% 		mqtt.maxPwdLength		** fest 16 Zeichen
%MQTT_CLIENTID_LENGTH%		mqtt.maxClientIdLength		** fest 16 Zeichen
%MQTT_SERVER_LENGTH%		mqtt.maxServerLength		** fest 32 Zeichen

// Bluetooth
%SHOW_BLUETOOTH_TAB%	bluetooth.btAvailable
%BT_DEVICE_NAME%		bluetooth.btDeviceName
%BT_PIN_CODE%			bluetooth.btPinCode
2 „Gefällt mir“

Ersteres wäre eher ein HTTP patch und vielleicht etwas zu aufwändig für das backend.
Das zweite erinnert schon stark an GraphQL und ist vermutlich komplett unnötig für den kleinen espuino - einfach das komplette json zurückgeben ist viel simpler.

Aber grundsätzlich die Richtung in die es gehen sollte.

1 „Gefällt mir“

Ich habe mich mal an Punkt 2/3 gemacht, den Template-Prozessor zu ersetzten. Das sind schon einige geänderte Zeilen - funktioniert aber schon ganz gut, hier der Entwurf:

Die Einstellungen werden hier über das Websocket geladen, genauso so wie die jetzt schon gespeichert werden.
Zusätzlich gibt es einen neuen Endpunkt „/settings“ mit GET/POST (Evt. PUT?) über den die Einstellungen ebenfalls geladen/gespeichert werden können. Läuft über die gleichen Funktionen wie beim Socket.

Template-Processor ist raus, Accesspoint und Management sind jetzt statisch, GZip-komprimiert abgelegt und belegen nur noch 1/4 des Platzes. Auch das Laden geht deutlich schneller:

Was ich noch nicht geschafft habe ist das Setzen der Slider beim Laden. Evt, kann ab hier noch jemand helfen. Bin kein Javascript-Experte…

Betrachtet das als Entwurf, gern Meinungen/Verbesserungen hier!

4 „Gefällt mir“

Ich werde ja leider meine Probleme mit WLAN nicht los. D.h. erstmal verbindet sich ESPuino mit dem WLAN und dann ggf. beim nächsten Mal nicht mehr. Wenn das Verbinden nicht mehr klappt, dann klappt das reproduzierbar nicht mehr und lässt sich nur dadurch sicher wieder beheben, dass den ESP32 spannungslos mache. Manchmal geht’s nach mehreren Versuchen nach dem Drücken des Reset-Buttons, aber das kann eine Weile dauern. Kotz.

Ich glaube da muss ich mich nochmal durch den Code wühlen und schauen, was da noch möglich ist…

Ich dachte schon es liegt an meinen neuen Boards, aber wenn du das auch hast … Ich konnte es bisher noch nicht sicher nachstellen

Versuche mal ein .dispatchEvent(new Event('input')); auf dem Objekt nachdem du den Wert gesetzt hast.

1 „Gefällt mir“

Hallo @Christian , schön das Du mal wieder vorbeischaust!

Ich habe das Setzen der Slider mittlerweile hinbekommen mit diesem Einzeiler:

$('#maxVolumeSpeaker').bootstrapSlider('setValue', genSettings.mVolSpeaker);

Ist wohl ein Namespace Problem?

bootstrap-slider.js - WARNING: $.fn.slider namespace is already bound. Use the $.fn.bootstrapSlider namespace instead.

Soweit ich sehen kann werden jetzt alle Einstellungen richtig gesetzt. Das funktioniert soweit Alles aber die Javascript Methode FillSettings() hat einen besseren Namen verdient & noch Luft nach oben.

1 „Gefällt mir“

Ach, stimmt es wird ja noch ein spezieller JQuery Slder verwendet.

So ich habe die Arbeiten soweit abgeschlossen und reiche das mal offiziell ein!
Ich habe versucht die Änderungen minimal zu halten, trotzdem ist da was zusammengekommen:

Wenn Ihr über die geänderten Dateien schaut sollten die Änderungen jetzt besser im DIFF zu sehen sein.

Der HTTP-GET Endpunkt „/settings“ liefert alle Einstellungen zurück, man kann aber auch einzelne Bereiche mit dem Parameter „section“ zurückbekommen, Bsp. „/settings?section=mqtt“.

Zusätzlicher Bugfixes:

  • Einstellungen Allgemein, der „Reset“ Schalter funktionierte nicht. Jetzt kann man auf Werkseinstellungen zurücksetzen und nach Absenden speichern
  • Die Feature Compilerflags wurden in der UI nicht beachtet: Kompiliert man z.B. ohne NEOPIXEL_ENABLE oder MEASURE_BATTERY_VOLTAGE werden die entsprechenden Controls auch nicht angezeigt.

Wenn wir diesen Schritt durchhaben könnten wir die Backend-API ein wenig aufräumen und vielleicht auch eine Schnittstelle mit z.B. Swagger oder OpenAPI festlegen und dokumentieren. Jetzt ist das JSON für die Einstellungen gegliedert und sieht so aus:

{
  "current": {
    "volume": 3,
    "rfidTagId": "",
    "hostname": "Espuino"
  },
  "general": {
    "iVol": 5,
    "mVolSpeaker": 21,
    "mVolHeadphone": 18,
    "iTime": 10
  },
  "neopixel": {
    "iBright": 16,
    "nBright": 2
  },
  "battery": {
    "vWarning": 3.400000095,
    "vIndLow": 3,
    "vIndHi": 4.199999809,
    "vInt": 10
  },
  "defaults": {
    "iVol": 5,
    "mVolSpeaker": 21,
    "mVolHeadphone": 18,
    "iBright": 16,
    "nBright": 2,
    "iTime": 10,
    "vWarning": 3.400000095,
    "vIndLow": 3,
    "vIndHi": 4.199999809,
    "vInt": 10
  },
  "ftp": {
    "ftpUser": "esp32",
    "ftpPwd": "esp32",
    "maxUserLength": 9,
    "maxPwdLength": 9
  },
  "bluetooth": {
    "enabled": true,
    "deviceName": "",
    "pinCode": ""
  }
}

Gerne Kommentare & Anmerkungen!

5 „Gefällt mir“

ich glaube das „nicht“ ist da zu viel :wink:

Aber sonst krasse Arbeit :muscle:t6: :+1:t6:

2 „Gefällt mir“

Sehr cool, @tueddy! Danke für deine ganze Arbeit!
Da muss ich mich wohl mal ranmachen, das zu testen.

3 „Gefällt mir“

Um noch einmal auf „RFC“ zurückzukommen.

  • JSON ist ja nur der Payload, aber grundsätzlich reden wir hier über REST, oder? Aktuell gibt es ja auch noch die Websocket-Verbindung. Ich finde die für bestimmte Dinge auch sehr praktisch. Idee / Verständnis-Frage: Die Rest API wird für die Datenübertragung genutzt (Setting - PUT - GET / File-Tree - GET). Websocket bleibt, wird aber nur für „notification“ verwendet. Damit hätte der ESP die Möglichkeit bestimtm Events zu triggern. Man hätte eine kläre Trennung zwischen Daten und Trigger. GUI können damit zyklisch pollen oder auf ein Event warten (wenn sie websockets unterstützen):
    • Beispiel
      • Neue Karte erkannt
      • Notification per websocket „refresh general“
      • GUI fordert per REST-API die Setting „general“ an, wo u.a. die neue Karten-ID übertragen wird.
      • Das könnte so auch für „Neues Cover“, „Neuer Titel“, „WIFI Scan beendet“ usw. verwendet werden
  • Das war ja damals der Grund für meinen Ansatz das alles per Websocket zu übertragen und dabei mehrere kleine JSONS zu bilden. Primäre ging es damals um den Filetree.
    • Wenn man bei REST bleiben möchte, könnte man das beim Filetree auch umsetzen:
      • GUI fordert den Filetree an
      • Backend erzeugt ein JSON mit max. 20 Einträgen.
      • Sollten mehr als 20 Einträge vorhanden sein, gibt es im JSON ein Feld „moreData: true
      • GUI fordert dann erneut den Filetree an, mit dem Hinweis die ersten 20 Einträge zu überspringen
      • Der Filetree würde damit in kleineren Blöcken übertragen
      • Die Anzahl (20) kann dann an den zur Verfügung stehenden Speicher angepasst werden.

Die Spotify API macht das u.a. so…

Hier würde ich es gut finden, wenn man einfach immer alle Einstellungen überträgt. Egal ob diese genutzt werden oder nicht und dann z.B. unter general eine Feature-Liste erzeugt. Mit dieser Liste werden dann Bereiche in der GUI ausgeblendet.