diff --git a/README.md b/README.md index b445a7a..400175f 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,38 @@ cp SOURCE_FILE_NAME /pyboard/NEW_FILE_NAME Copying '/Users/Jones/Downloads/MicroPython/ESP-WiFi-Manager/wifi_manager.py' to '/pyboard/wifi_manager.py' ... ``` +Create compressed CSS and JS files as described in the +[simulation static files README](simulation/static) to save disk space on the +device and increase the performance (webpages are loading faster) + +```bash +mkdir /pyboard/static/ +cp simulation/static/css/*.gz /pyboard/static/ +# around 24kB compared to uncompressed 120kB + +# optional, not used so far +mkdir /pyboard/static/ +cp simulation/static/js/*.gz /pyboard/static/ +# around 12kB compared to uncompressed 40kB + +mkdir /pyboard/templates +cp templates/* /pyboard/templates +# around 20kB + +mkdir /pyboard/helpers +cp helpers/*.py /pyboard/helpers +# around 64kB + +mkdir /pyboard/primitives +cp primitives/* /pyboard/primitives +# around 8kB + +cp boot.py /pyboard +cp main.py /pyboard +cp wifi_manager.py /pyboard +# around 40kB +``` + ##### Open REPL in rshell Call `repl` in the rshell. Use CTRL+X to leave the repl or CTRL+D for a soft diff --git a/changelog.md b/changelog.md index 7ff9297..909b13d 100644 --- a/changelog.md +++ b/changelog.md @@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial [`WiFi Manager`](wifi_manager.py) implementation - Micropython [`boot`](boot.py) and [`main`](main.py) files - [`README`](README.md) and [`requirements.txt`](requirements.txt) files +- Compressed version of + [`bootstrap.min.css`](simulation/static/css/bootstrap.min.css) and + [`bootstrap.min.js`](simulation/static/js/bootstrap.min.js) #### Simulation - [`Simulation README`](simulation/README.md) file @@ -52,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [network station and client](simulation/src/wifi_helper/network.py) - Bash script to prepare all folders for a unittest coverage report - Unittests for all modules and fakes +- Render list of selectable networks in Python and provide result via API +- `sendfile` function implemented in same way as on Micropythons PicoWeb [Unreleased]: https://github.com/brainelectronics/Micropython-ESP-WiFi-Manager/compare/0.1.0...develop diff --git a/simulation/README.md b/simulation/README.md index c623237..6ee66d6 100644 --- a/simulation/README.md +++ b/simulation/README.md @@ -28,6 +28,17 @@ On Windows the package `pycryptodome>=3.14.0,<4` shall be used instead of Simulation webpages use [bootstrap 3.4][ref-bootstrap-34]. +## Usage + +Run the simulation of the ESP WiFi Manager **after** activating the virtual +environment of the [Setup section](#setup) + +```bash +sh run.sh +``` + +Open [`http://127.0.0.1:5000/`](http://127.0.0.1:5000/){:target="_blank"} in a browser + ## Unittests ### General @@ -170,17 +181,6 @@ Test [`wifi manager`][ref-wifi-manager-test] implementation. nose2 --config tests/unittest.cfg -v tests.test_wifi_manager.TestWiFiManager ``` -## Usage - -Run the simulation of the ESP WiFi Manager **after** activating the virtual -environment of the [Setup section](#setup) - -```bash -sh run.sh -``` - -Open [`http://127.0.0.1:5000/`](http://127.0.0.1:5000/){:target="_blank"} in a browser - [ref-bootstrap-34]: https://getbootstrap.com/docs/3.4/getting-started/#download diff --git a/simulation/requirements.txt b/simulation/requirements.txt index 219574c..71b02b0 100644 --- a/simulation/requirements.txt +++ b/simulation/requirements.txt @@ -2,5 +2,5 @@ flask>=2.0.2,<3 jinja2>=3.0.2,<4 PyYAML>=5.4.1,<6 nose2>=0.10.0,<1 -pycrypto>=2.6.1,<3 netifaces>=0.11.0,<1 +pycrypto>=2.6.1,<3 diff --git a/simulation/src/generic_helper/generic_helper.py b/simulation/src/generic_helper/generic_helper.py index 20a462f..0be1e6b 100644 --- a/simulation/src/generic_helper/generic_helper.py +++ b/simulation/src/generic_helper/generic_helper.py @@ -14,7 +14,7 @@ import random import sys -from typing import (Optional, Union) +from typing import Optional, Union class GenericHelper(object): diff --git a/simulation/src/led_helper/led_helper.py b/simulation/src/led_helper/led_helper.py index c157147..336eb8e 100755 --- a/simulation/src/led_helper/led_helper.py +++ b/simulation/src/led_helper/led_helper.py @@ -312,7 +312,6 @@ def fade(self, delay_ms: int = 50, pixel_amount: int = -1) -> None: self.fade_pixel_amount = pixel_amount self.fading = True - # def _fade(self, delay_ms: int, pixel_amount: int, lock: lock) -> None: def _fade(self, delay_ms: int, pixel_amount: int, lock: int) -> None: """ Internal Neopixel fading thread content. diff --git a/simulation/src/path_helper/path_helper.py b/simulation/src/path_helper/path_helper.py index 13d21b5..5094c9f 100755 --- a/simulation/src/path_helper/path_helper.py +++ b/simulation/src/path_helper/path_helper.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -"""Provide unavailable path functions in MicroPython""" +""" +Path Helper + +Provide unavailable path functions for Micropython boards +""" # import os from pathlib import Path diff --git a/simulation/src/time_helper/time_helper.py b/simulation/src/time_helper/time_helper.py index 255deab..47a2645 100644 --- a/simulation/src/time_helper/time_helper.py +++ b/simulation/src/time_helper/time_helper.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -"""try to sync and set the internal clock (RTC) with NTP server time""" +""" +Time Helper + +Sync and set internal clock (RTC) with NTP server time +""" from machine import RTC # import ntptime diff --git a/simulation/src/wifi_helper/network.py b/simulation/src/wifi_helper/network.py index f880ffa..7128eeb 100644 --- a/simulation/src/wifi_helper/network.py +++ b/simulation/src/wifi_helper/network.py @@ -78,8 +78,6 @@ def _scan_mac() -> List[dict]: scan_result = subprocess.check_output(['/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport', '--scan']) if len(scan_result) == 0: - print('### Check WiFi to be active ###') - print('Returning dummy data') return NetworkHelper._dummy_data() scan_result = [ele.decode('ascii') for ele in scan_result.splitlines()] @@ -188,17 +186,14 @@ def _scan_windows() -> List[dict]: try: scan_result = subprocess.check_output(['netsh', 'wlan', 'show', 'networks', 'mode=bssid']) except Exception as e: - print('Failed to scan due to this error: {}'.format(e)) - print('### Check WiFi to be active ###') - print('Returning dummy data') + # print('Failed to scan due to this error: {}'.format(e)) return NetworkHelper._dummy_data() # netsh call returns report in local language, try to decode it try: scan_result = [ele.decode('cp850').lstrip() for ele in scan_result.splitlines()] except Exception as e: - print('Failed to decode scan data due to this error: {}'.format(e)) - print('Returning dummy data') + # print('Failed to decode scan data due to this error: {}'.format(e)) return NetworkHelper._dummy_data() # [ @@ -366,6 +361,7 @@ def _dummy_data() -> List[dict]: 'hidden': False } ] + # print('Returning dummy data') return nets diff --git a/simulation/src/wifi_helper/wifi_helper.py b/simulation/src/wifi_helper/wifi_helper.py index 03eedc1..0a33aa7 100644 --- a/simulation/src/wifi_helper/wifi_helper.py +++ b/simulation/src/wifi_helper/wifi_helper.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -"""connect to specified network(s) or create an accesspoint""" +""" +WiFi Helper + +Connect to specified network(s) or create an accesspoint +""" # import ubinascii import json @@ -341,7 +345,8 @@ def scan_networks(self) -> None: try: net['authmode'] = self.auth_modes[net['authmode']] except KeyError: - print('{} is unknown authmode'.format(net['authmode'])) + # print('{} is unknown authmode'.format(net['authmode'])) + pass except Exception: pass if 'bssid' in net: diff --git a/simulation/src/wifi_manager/wifi_manager.py b/simulation/src/wifi_manager/wifi_manager.py index 55ad016..a950fa9 100644 --- a/simulation/src/wifi_manager/wifi_manager.py +++ b/simulation/src/wifi_manager/wifi_manager.py @@ -10,7 +10,7 @@ import gc import json from machine import machine -import os +# import os from pathlib import Path import _thread import time @@ -21,7 +21,7 @@ from generic_helper import Message # pip installed packages -from flask import Flask, render_template, abort, request, jsonify +from flask import Flask, render_template, url_for, redirect, request, jsonify, make_response # custom packages from generic_helper import GenericHelper @@ -65,11 +65,12 @@ def __init__(self, logger=None, quiet=False, name=__name__): self._enc_key = (uuid * amount).decode('ascii')[:required_len] self._configured_networks = list() + self._selected_network_bssid = '' # WiFi scan specific defines self._scan_lock = _thread.allocate_lock() self._scan_interval = 5000 # milliseconds - # Queue also works, but not in this case there is no need for a history + # Queue also works, but in this case there is no need for a history self._scan_net_msg = Message() self._scan_net_msg.set([]) # empty list, required by save_wifi_config self._latest_scan = None @@ -120,7 +121,8 @@ def load_and_connect(self) -> bool: # self._configured_networks = list(ssids).copy() # self.logger.debug('All SSIDs: {}'.format(ssids)) self.logger.debug('Config content: {}'.format(loaded_cfg)) - self.logger.debug('Private config content: {}'.format(private_cfg)) + self.logger.debug('Private config content: {}'. + format(private_cfg)) self.logger.debug('Configured networks: {}'. format(self._configured_networks)) @@ -152,8 +154,10 @@ def start_config(self) -> None: def _add_app_routes(self) -> None: """Add all application routes to the webserver.""" self.app.add_url_rule("/", view_func=self.landing_page) - self.app.add_url_rule("/wifi_selection", view_func=self.wifi_selection) - self.app.add_url_rule("/wifi_configs", view_func=self.wifi_configs) + self.app.add_url_rule("/select", view_func=self.wifi_selection) + self.app.add_url_rule("/render_network_inputs", + view_func=self.render_network_inputs) + self.app.add_url_rule("/configure", view_func=self.wifi_configs) self.app.add_url_rule("/save_wifi_config", view_func=self.save_wifi_config, methods=['POST', 'GET']) @@ -162,6 +166,14 @@ def _add_app_routes(self) -> None: methods=['POST', 'GET']) self.app.add_url_rule("/scan_result", view_func=self.scan_result) + self.app.add_url_rule( + "/static/css/bootstrap.min.css", + view_func=self.styles + ) + # regex not supported in Flask + # self.app.add_url_rule(re.compile('^\/(.+\.css)$'), + # view_func=self.styles) + def _encrypt_data(self, data: Union[str, list, dict]) -> bytes: """ Encrypt data with encryption key @@ -270,7 +282,8 @@ def extend_wifi_config_data(self, def _load_wifi_config_data(self, path: str, - encrypted: bool = False) -> Union[dict, List[dict]]: + encrypted: bool = False) -> Union[dict, + List[dict]]: """ Load WiFi configuration data from file. @@ -286,7 +299,8 @@ def _load_wifi_config_data(self, if encrypted: # read file in binary as it contains encrypted data - encrypted_read_data = GenericHelper.load_file(path=path, mode='rb') + encrypted_read_data = GenericHelper.load_file(path=path, + mode='rb') self.logger.debug('Read encrypted data: {}'. format(encrypted_read_data)) @@ -321,7 +335,6 @@ def _scan(self, msg: Message, scan_interval: int, lock: int) -> None: - # lock: lock) -> None: """ Scan for available networks. @@ -334,7 +347,7 @@ def _scan(self, :param scan_interval: The scan interval in milliseconds :type scan_interval: int :param lock: The lock object - :type lock: lock + :type lock: _thread.lock """ pixel.fading = True @@ -369,8 +382,8 @@ def scan_interval(self, value: int) -> None: """ Set the WiFi scan interval in milliseconds. - Values below 1000ms are set to 1000ms. - One scan takes around 3 sec, which leads to maximum 15 scans per minute + Values below 1000 ms are set to 1000 ms. + One scan takes around 3 sec, which leads to maximum 15 scans per min :param value: Interval of WiFi scans in milliseconds :type value: int @@ -423,70 +436,90 @@ def latest_scan(self) -> Union[List[dict], str]: # free = gc.mem_free() # self.logger.debug('Free memory: {}'.format(free)) latest_scan_result = self._scan_net_msg.value() - self.logger.info('Requested latest scan result: {}'.format(latest_scan_result)) + self.logger.info('Requested latest scan result: {}'. + format(latest_scan_result)) return latest_scan_result - # ------------------------------------------------------------------------- - # Webserver functions - - # @app.route('/landing_page') - def landing_page(self): - # return render_template('wifi_select_loader.tpl.html', - return render_template('index.tpl.html') - - # @app.route('/scan_result') - def scan_result(self): - return jsonify(self.latest_scan) - - # @app.route('/wifi_selection') - def wifi_selection(self): - # abort(404) - available_nets = self.latest_scan - # return render_template('wifi_select_loader.tpl.html', - return render_template('wifi_select_loader_bootstrap.tpl.html', - wifi_nets=available_nets) - - # @app.route('/wifi_configs') - def wifi_configs(self): - # abort(404) - configured_nets = self.configured_networks - self.logger.debug('Existing config content: {}'. - format(configured_nets)) - - if isinstance(configured_nets, str): - configured_nets = [configured_nets] - - return render_template('wifi_configs.tpl.html', - wifi_nets=configured_nets) - - # @app.route('/save_wifi_config', methods=['POST', 'GET']) - def save_wifi_config(self): - # abort(404) - if request.method == 'POST': - data = request.form + def _render_network_inputs(self, + available_nets: dict, + selected_bssid: str = '') -> str: + """ + Render HTML list of selectable networks - form_data = dict(request.form) + :param available_nets: All available nets + :type available_nets: dict + :param selected_bssid: Currently selected network on the webpage + :type selected_bssid: str - # print('Posted data in save_wifi_config: {}'.format(data)) - self.logger.info('WiFi user input content: {}'.format(form_data)) - # {'bssid': 'a0f3c1fbfc3c', 'ssid': '', 'password': 'sdsfv'} + :returns: Sub content of WiFi selection page + :rtype: str + """ + content = "" + if len(available_nets): + for ele in available_nets: + selected = '' + if ele['bssid'] == selected_bssid: + selected = "checked" + content += """ + + + """.format(bssid=ele['bssid'], + state=selected, + ssid=ele['ssid'], + quality=ele['quality']) + else: + # as long as no networks are available show a spinner + content = """ +
+ Loading... +
+ """ - network_cfg = self._save_wifi_config(form_data=form_data) + return content - # resp = jsonify(success=True) - # return resp - return render_template('result.html', result=network_cfg) + def _save_wifi_config(self, form_data: dict) -> None: + """ + Save a new WiFi configuration to the WiFi configuration file. - def _save_wifi_config(self, form_data: dict) -> dict: + :param form_data: The form data + :type form_data: dict + """ network_cfg = dict() available_nets = self.latest_scan self.logger.info('Available nets: {}'.format(available_nets)) - # [{'ssid': 'TP-LINK_FBFC3C', 'RSSI': -21, 'bssid': 'a0f3c1fbfc3c', 'authmode': 'WPA/WPA2-PSK', 'quality': 9, 'channel': 1, 'hidden': False}, {'ssid': 'FRITZ!Box 7490', 'RSSI': -17, 'bssid': '3810d517eb39', 'authmode': 'WPA2-PSK', 'quality': 27, 'channel': 11, 'hidden': False}] + # [ + # { + # 'ssid': 'TP-LINK_FBFC3C', + # 'RSSI': -21, + # 'bssid': 'a0f3c1fbfc3c', + # 'authmode': 'WPA/WPA2-PSK', + # 'quality': 9, + # 'channel': 1, + # 'hidden': False + # }, + # { + # 'ssid': 'FRITZ!Box 7490', + # 'RSSI': -17, + # 'bssid': '3810d517eb39', + # 'authmode': 'WPA2-PSK', + # 'quality': 27, + # 'channel': 11, + # 'hidden': False + # } + # ] # find SSID of network based on given bssid value if form_data['ssid'] != '': network_cfg['ssid'] = form_data['ssid'] else: + if 'bssid' not in form_data: + return + # selected_bssid = form_data['wifi_network'] selected_bssid = form_data['bssid'] for ele in available_nets: @@ -497,11 +530,11 @@ def _save_wifi_config(self, form_data: dict) -> dict: # JSON which does not handle bytes format this_bssid = str(ele['bssid']) else: - this_bssid = ele['bssid'] #.decode('ascii') + this_bssid = ele['bssid'] # .decode('ascii') if this_bssid == selected_bssid: # use string, json loading will fail otherwise later - network_cfg['ssid'] = ele['ssid'] #.decode('ascii') + network_cfg['ssid'] = ele['ssid'] # .decode('ascii') break network_cfg['password'] = form_data['password'] @@ -523,87 +556,178 @@ def _save_wifi_config(self, form_data: dict) -> dict: else: self.logger.info('No valid SSID found, will not save this net') - return network_cfg + def _remove_wifi_config(self, form_data: dict) -> None: + """ + Remove a WiFi network from the WiFi configuration file. + + :param form_data: The form data + :type form_data: dict + """ + if len(form_data): + loaded_cfg = self._load_wifi_config_data(path=self._config_file, + encrypted=True) + + updated_cfg = list() + updated_ssids = list() + + for net in loaded_cfg: + if net['ssid'] not in form_data: + updated_cfg.append(net) + updated_ssids.append(net['ssid']) + + self._configured_networks = updated_ssids.copy() + + # create bytes array of the dict and encrypt it + encrypted_data = self._encrypt_data(data=updated_cfg) + + # save to file as binary + GenericHelper.save_file(data=encrypted_data, + path=self._config_file, + mode='wb') + self.logger.debug('Saved encrypted data as json: {}'. + format(encrypted_data)) + + # ------------------------------------------------------------------------- + # Webserver functions + + # @app.route('/landing_page') + def landing_page(self): + """Provide landing aka index page""" + return render_template('index.tpl.html') + + # @app.route('/scan_result') + def scan_result(self): + """Provide latest found networks as JSON""" + return jsonify(self.latest_scan) + + # @app.route('/select') + def wifi_selection(self): + """ + Provide webpage to select WiFi network from list of available networks + + Scanning just in time of accessing the page would block all processes + for approx. 2.5 sec. + Using the result provided by the scan thread via a message takes only + 0.02 sec to complete + """ + available_nets = self.latest_scan + content = self._render_network_inputs(available_nets=available_nets) + + # do not stop scanning as page is updating scan results on the fly + # with XMLHTTP requests to @see scan_result + # stop scanning thread + # self.logger.info('Stopping scanning thread') + # self.scanning = False + + return render_template('select.tpl.html', content=content) + + # @app.route('/render_network_inputs') + def render_network_inputs(self) -> str: + """Return rendered network inputs content to webpage""" + available_nets = self.latest_scan + selected_bssid = self._selected_network_bssid + content = self._render_network_inputs(available_nets=available_nets, + selected_bssid=selected_bssid) + return content + + # @app.route('/configure') + def wifi_configs(self): + """Provide webpage with table of configured networks""" + configured_nets = self.configured_networks + self.logger.debug('Existing config content: {}'. + format(configured_nets)) + + if isinstance(configured_nets, str): + configured_nets = [configured_nets] + + # disable submit button by default + return render_template('remove.tpl.html', + wifi_nets=configured_nets, + button_mode='disabled') + + # @app.route('/save_wifi_config', methods=['POST', 'GET']) + def save_wifi_config(self): + form_data = dict(request.form) + + self.logger.info('WiFi user input content: {}'.format(form_data)) + # result if a network of the list has been selected + # {'bssid': 'a0f3c1fbfc3c', 'ssid': '', 'password': 'sdsfv'} + # result if a custom network is given + # {'ssid': 'myCustom Net', 'password': 'asdf1234'} + # result if nothing is selected + # {'ssid': '', 'password': ''} + + self._save_wifi_config(form_data=form_data) + + return redirect(url_for('landing_page')) # @app.route('/remove_wifi_config', methods=['POST', 'GET']) def remove_wifi_config(self): - # abort(404) - if request.method == 'POST': - data = request.args - - # Whether form data comes from GET or POST request, once parsed, - # it's available as req.form dictionary - form_data = request.get_json(force=True) - self.logger.info('Received data: {}'.format(form_data)) - # print('Posted data in remove_wifi_config: {}'.format(data)) - - if all(ele in form_data for ele in ['name', 'index']): - network_name = form_data['name'] - network_index = int(form_data['index']) - 1 # th is also counted - - self.logger.debug('Remove network "{}" at index {}'. - format(form_data['name'], form_data['index'])) - - if network_name == "all" and network_index == -1: - if PathHelper.exists(path=self._config_file): - os.remove(self._config_file) - self.logger.debug('Removed network file') - return + """Remove a configured WiFi network""" + form_data = dict(request.form) - loaded_cfg = self._load_wifi_config_data(path=self._config_file, - encrypted=True) - self.logger.debug('Existing config content: {}'.format(loaded_cfg)) + self.logger.info('Remove networks: {}'.format(form_data)) + # Remove networks: {'FRITZ!Box 7490': 'FRITZ!Box 7490'} - try: - network_cfg = loaded_cfg[network_index] - if network_cfg['ssid'] == network_name: - self.logger.debug('Found specified network in network cfg') - - ssids = list() - if isinstance(loaded_cfg, list): - # remove element from list - del loaded_cfg[network_index] - - self.logger.debug('Updated data: {}'. - format(loaded_cfg)) - - # list of dicts - for net in loaded_cfg: - if 'ssid' in net: - ssids.append(net['ssid']) - elif isinstance(loaded_cfg, dict): - pass - # do not do anything, updated data will be empty list - - # if 'ssid' in loaded_cfg: - # ssids = loaded_cfg['ssid'] - - self._configured_networks = ssids.copy() - - # create bytes array of the dict and encrypt it - encrypted_data = self._encrypt_data(data=loaded_cfg) - - # save to file as binary - GenericHelper.save_file(data=encrypted_data, - path=self._config_file, - mode='w') # 'wb' for encrypted data - self.logger.debug('Saved encrypted data as json: {}'. - format(encrypted_data)) - except IndexError as e: - self.logger.debug('Specified network at index {} not found'. - format(network_index)) - except Exception as e: - self.logger.debug('Catched: {}'.format(e)) - - # resp = jsonify(success=True) - # return resp - return render_template('result.html', result=data) - - """ - @app.route('/result') - def show_result(): - return render_template('result.html', result=result) - """ + self._remove_wifi_config(form_data=form_data) + + return redirect(url_for('landing_page')) + + # @app.route(re.compile('^\/(.+\.css)$')) + def styles(self): + """ + Send gzipped CSS content if supported by client. + + Shows specifying headers as a flat binary string, more efficient if + such headers are static. + """ + file_path = str(request.url_rule) + headers = b'Cache-Control: max-age=86400\r\n' + + if 'gzip' in str(request.headers): + self.logger.debug('gzip accepted for CSS style file') + file_path += '.gz' + headers += b'Content-Encoding: gzip\r\n' + + self.logger.debug('Accessed file {}'.format(file_path)) + return self.sendfile(writer=request, + fname=file_path, + content_type='text/css', + headers=headers) + + def sendfile(self, + fname: str, + content_type: str, + headers: str, + writer=None): + """ + Fake function to send file in same style as in PicoWeb on Micropython + + :param fname: The filename + :type fname: str + :param content_type: The content type + :type content_type: str + :param headers: The headers + :type headers: str + :param writer: The writer + :type writer: Optional + """ + flask_root_folder = (Path(__file__).parent / '..' / '..').resolve() + file_path = str(flask_root_folder) + str(fname) + + self.logger.debug('Open file {}'.format(file_path)) + with open(file_path, 'rb') as file: + content = file.read() + + res = make_response(content) + res.headers["Content-Type"] = content_type + # res.headers['Content-Length'] = len(content) + + for ele in headers.decode('ascii').splitlines(): + key, value = ele.split(':') + res.headers[key] = value.lstrip() + + return res def run(self, host: str = '0.0.0.0', diff --git a/simulation/static/README.md b/simulation/static/README.md index e77cc24..2c2223e 100644 --- a/simulation/static/README.md +++ b/simulation/static/README.md @@ -4,7 +4,95 @@ JavaScript and CSS files for simulation ## General -This simulation uses [bootstrap 3.4][ref-bootstrap] +This simulation uses [bootstrap 5.1.3][ref-bootstrap] + +## Create compressed version + +### Why + +To speed up the data transfer between the device and a browser, many of them +accept CSS and JS files as compressed `.gz` files. + +### Additional informations + +In some cases the following warning can be seen in the web console of the page + + Layout rendering was forced before the page was fully loaded. + +This might be due to the reason of loading multiple (CSS) files within a given +time frame. As a less powerfull device, such as an ESP32 or ESP8266, will have +difficulties to provide these data in the expected time frame, the layout +rendering might be forced, leading to a not as expected view. + +To avoid such issues, try to serve as less files as possible in the most +compact way. To do so, use minified versions of CSS files (`*.min.css`) and +combine multiple files into one. An even better performance is reached by +compressing the file as shown onwards. + +To minify custom CSS files, search online for a CSS minifier or use +[this one][ref-css-minifier] + +### How to + +To compress a file use `gzip` instead of `tar`. `tar` seems to break something +in the compressed file. As a result the style might not be as with the non +compressed version. + +This example shows how to compress `bootstrap.min.css` to a new file called +`bootstrap.min.css.gz` + +```bash +cd css + +gzip -r bootstrap.min.css -c > bootstrap.min.css.gz +``` + + -[ref-bootstrap]: https://getbootstrap.com/docs/3.4/getting-started/#download +[ref-bootstrap]: https://getbootstrap.com/docs/5.1/getting-started/download/ +[ref-css-minifier]: https://www.toptal.com/developers/cssminifier/ +[ref-stackoverflow-sed]: https://stackoverflow.com/questions/5410757/how-to-delete-from-a-text-file-all-lines-that-contain-a-specific-string diff --git a/simulation/static/css/bootstrap.min.css.gz b/simulation/static/css/bootstrap.min.css.gz new file mode 100644 index 0000000..bd4f875 Binary files /dev/null and b/simulation/static/css/bootstrap.min.css.gz differ diff --git a/simulation/static/js/bootstrap.min.js.gz b/simulation/static/js/bootstrap.min.js.gz new file mode 100644 index 0000000..4a0031d Binary files /dev/null and b/simulation/static/js/bootstrap.min.js.gz differ diff --git a/simulation/templates/index.tpl.html b/simulation/templates/index.tpl.html index 643a6e6..0508a0b 100755 --- a/simulation/templates/index.tpl.html +++ b/simulation/templates/index.tpl.html @@ -3,40 +3,20 @@ + Setup - - - - + + - -
-
-
- -
-