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
+
+
+
+
+
+
+
+
+
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)