diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 2a614db..61f51d0 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -11,8 +11,10 @@ jobs: uses: actions/checkout@v4 - name: Initialize submodules run: cd software && ./update-submodules.sh + - name: Prepare venv + run: python -m venv build-venv && source build-venv/bin/activate && pip install freezefs - name: Build - run: cd software && ./build.sh + run: source build-venv/bin/activate && cd software && ./build.sh - name: Upload firmware uses: actions/upload-artifact@v3 with: diff --git a/software/boards/RPI_PICO_W/manifest.py b/software/boards/RPI_PICO_W/manifest.py index f8cb12a..486963d 100644 --- a/software/boards/RPI_PICO_W/manifest.py +++ b/software/boards/RPI_PICO_W/manifest.py @@ -22,3 +22,5 @@ module("mp3player.py", "../../src") module("webserver.py", "../../src") package("utils", base_path="../../src") package("nfc", base_path="../../src") + +module("frozen_frontend.py", "../../build") diff --git a/software/build.sh b/software/build.sh index 4b215cc..3f96779 100755 --- a/software/build.sh +++ b/software/build.sh @@ -25,6 +25,11 @@ mkdir "$FS_STAGE_DIR"/fs trap 'rm -rf $FS_STAGE_DIR' EXIT tools/mklittlefs/mklittlefs -p 256 -s 868352 -c "$FS_STAGE_DIR"/fs "$FS_STAGE_DIR"/filesystem.bin +FRONTEND_STAGE_DIR=$(mktemp -d) +trap 'rm -rf $FRONTEND_STAGE_DIR' EXIT +gzip -c frontend/index.html > "$FRONTEND_STAGE_DIR"/index.html.gz +python -m freezefs "$FRONTEND_STAGE_DIR" build/frozen_frontend.py --target=/frontend + for hwconfig in boards/RPI_PICO_W/manifest-*.py; do hwconfig_base=$(basename "$hwconfig") hwname=${hwconfig_base##manifest-} diff --git a/software/frontend/index.html b/software/frontend/index.html new file mode 100644 index 0000000..48d6548 --- /dev/null +++ b/software/frontend/index.html @@ -0,0 +1,249 @@ + + + + +Device Admin + + + + +

Device Admin

+ + + + +
+

Main Menu

+

Select a tool:

+ + +
+ + +
+

Configuration Editor

+
Loading…
+ +
+ + + + + diff --git a/software/requirements.txt b/software/requirements.txt new file mode 100644 index 0000000..067d919 --- /dev/null +++ b/software/requirements.txt @@ -0,0 +1 @@ +freezefs diff --git a/software/src/main.py b/software/src/main.py index 613ff97..09afe34 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -14,6 +14,7 @@ import ubinascii # Own modules import app from audiocore import AudioContext +import frozen_frontend # noqa: F401 from mfrc522 import MFRC522 from mp3player import MP3Player from nfc import Nfc diff --git a/software/src/utils/config.py b/software/src/utils/config.py index e2677f7..0ab4ad2 100644 --- a/software/src/utils/config.py +++ b/software/src/utils/config.py @@ -30,6 +30,7 @@ class Configuration: try: with open(self.config_path, 'r') as conf_file: self.config = json.load(conf_file) + self._merge_configs(self.DEFAULT_CONFIG, self.config) except OSError as ex: if ex.errno == ENOENT: self.config = Configuration.DEFAULT_CONFIG @@ -51,6 +52,16 @@ class Configuration: raise os.sync() + def _merge_configs(self, default, config): + for k in default.keys(): + if k not in config: + if isinstance(default[k], dict): + config[k] = default[k].copy() + else: + config[k] = default[k] + elif isinstance(default[k], dict): + self._merge_configs(default[k], config[k]) + def _save(self): with open(self.config_path + '.new', 'w') as conf_file: json.dump(self.config, conf_file) @@ -59,7 +70,7 @@ class Configuration: os.sync() def _get(self, key): - return self.config.get(key, self.DEFAULT_CONFIG[key]) + return self.config[key] def get_led_count(self) -> int: return self._get('LED_COUNT') @@ -93,5 +104,6 @@ class Configuration: self._validate(self.DEFAULT_CONFIG, config) if 'TAGMODE' in config and config['TAGMODE'] not in ['tagremains', 'tagstartstop']: raise ValueError("Invalid TAGMODE: Must be 'tagremains' or 'tagstartstop'") + self._merge_configs(self.config, config) self.config = config self._save() diff --git a/software/src/webserver.py b/software/src/webserver.py index ca4d658..4308c69 100644 --- a/software/src/webserver.py +++ b/software/src/webserver.py @@ -5,7 +5,7 @@ Copyright (c) 2024-2025 Stefan Kratochwil import asyncio -from microdot import Microdot +from microdot import Microdot, redirect, send_file webapp = Microdot() server = None @@ -29,7 +29,14 @@ async def before_request_handler(request): app.reset_idle_timeout() -@webapp.route('/') +@webapp.before_request +async def before_request_handler(request): + if request.method in ['PUT', 'POST'] and app.is_playing(): + return "Cannot write to device while playback is active", 503 + app.reset_idle_timeout() + + +@webapp.route('/api/v1/hello') async def index(request): print("wohoo, a guest :)") print(f" app: {request.app}") @@ -72,3 +79,21 @@ async def config_put(request): async def last_tag_uid_get(request): tag, _ = nfc.get_last_uid() return {'tag': tag} + + +@webapp.route('/', methods=['GET']) +async def root_get(request): + return redirect('/index.html') + + +@webapp.route('/index.html', methods=['GET']) +async def index_get(request): + return send_file('/frontend/index.html.gz', content_type='text/html', compressed='gzip') + + +@webapp.route('/static/', methods=['GET']) +async def static(request, path): + if '..' in path: + # directory traversal is not allowed + return 'Not found', 404 + return send_file('/frontend/static/' + path, max_age=86400)