Skip to content

Commit

Permalink
Svelte UI: add hamburger menu and compile to esp
Browse files Browse the repository at this point in the history
Svelte UI
- add build_interface.py (like done in cdata.js / webbundle.py)
- move svelte files to src folder (so build_interface can find them)
- package.json: add svelte-hamburgers
- add App.svelte (Hamburgers and Menu)
- add Menu.svelte

pio.ini
- add EMBED_WWW and add pre build_interface.py
  • Loading branch information
ewowi committed Dec 28, 2024
1 parent ed7d259 commit b21d6e1
Show file tree
Hide file tree
Showing 12 changed files with 293 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.pio
node_modules
dist
WWWData.h
.vscode
.DS_Store
.idea
168 changes: 168 additions & 0 deletions data/svelte/build_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# StarBase modified version from https://github.com/theelims/ESP32-sveltekit/blob/main/scripts/build_interface.py

# ESP32 SvelteKit --
#
# A simple, secure and extensible framework for IoT projects for ESP32 platforms
# with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
# https://github.com/theelims/ESP32-sveltekit
#
# Copyright (C) 2018 - 2023 rjwats
# Copyright (C) 2023 - 2024 theelims
# Copyright (C) 2023 Maxtrium B.V. [ code available under dual license ]
# Copyright (C) 2024 runeharlyk
#
# All Rights Reserved. This software may be modified and distributed under
# the terms of the LGPL v3 license. See the LICENSE file for details.

from pathlib import Path
from shutil import copytree, rmtree, copyfileobj
from os.path import exists, getmtime
import os
import gzip
import mimetypes
import glob
from datetime import datetime

Import("env")

project_dir = env["PROJECT_DIR"]
buildFlags = env.ParseFlags(env["BUILD_FLAGS"])

interface_dir = project_dir + "/data/svelte"
output_file = project_dir + "/src/WWWData.h"
source_www_dir = interface_dir + "/src"
build_dir = interface_dir + "/dist"
filesystem_dir = project_dir + "/data/www"


def find_latest_timestamp_for_app():
return max(
(getmtime(f) for f in glob.glob(f"{source_www_dir}/**/*", recursive=True))
)


def should_regenerate_output_file():
if not flag_exists("EMBED_WWW") or not exists(output_file):
return True
last_source_change = find_latest_timestamp_for_app()
last_build = getmtime(output_file)

print(
f"Newest file: {datetime.fromtimestamp(last_source_change)}, output file: {datetime.fromtimestamp(last_build)}"
)

return last_build < last_source_change


def gzip_file(file):
with open(file, 'rb') as f_in:
with gzip.open(file + '.gz', 'wb') as f_out:
copyfileobj(f_in, f_out)
os.remove(file)


def flag_exists(flag):
for define in buildFlags.get("CPPDEFINES"):
if (define == flag or (isinstance(define, list) and define[0] == flag)):
return True
return False


def get_package_manager():
if exists(os.path.join(interface_dir, "pnpm-lock.yaml")):
return "pnpm"
if exists(os.path.join(interface_dir, "yarn.lock")):
return "yarn"
if exists(os.path.join(interface_dir, "package-lock.json")):
return "npm"


def build_webapp():
if package_manager := get_package_manager():
print(f"Building interface with {package_manager}")
os.chdir(interface_dir)
env.Execute(f"{package_manager} install")
env.Execute(f"{package_manager} run build")
os.chdir("..")
else:
raise Exception(
"No lock-file found. Please install dependencies for interface (eg. npm install)"
)


def embed_webapp():
if flag_exists("EMBED_WWW"):
print("Converting interface to PROGMEM")
build_progmem()
return
add_app_to_filesystem()


def build_progmem():
mimetypes.init()
with open(output_file, "w") as progmem:
progmem.write("#include <functional>\n")
progmem.write("#include <Arduino.h>\n")

assetMap = {}

for idx, path in enumerate(Path(build_dir).rglob("*.*")):
asset_path = path.relative_to(build_dir).as_posix()
asset_mime = (
mimetypes.guess_type(asset_path)[0] or "application/octet-stream"
)
print(f"Converting {asset_path}")

asset_var = f"ESP_SVELTEKIT_DATA_{idx}"
progmem.write(f"// {asset_path}\n")
progmem.write(f"const uint8_t {asset_var}[] = {{\n\t")
file_data = gzip.compress(path.read_bytes())

for i, byte in enumerate(file_data):
if i and not (i % 16):
progmem.write("\n\t")
progmem.write(f"0x{byte:02X},")

progmem.write("\n};\n\n")
assetMap[asset_path] = {
"name": asset_var,
"mime": asset_mime,
"size": len(file_data),
}

progmem.write(
"typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;\n\n"
)
progmem.write("class WWWData {\n")
progmem.write("\tpublic:\n")
progmem.write(
"\t\tstatic void registerRoutes(RouteRegistrationHandler handler) {\n"
)

for asset_path, asset in assetMap.items():
progmem.write(
f'\t\t\thandler("/{asset_path}", "{asset["mime"]}", {asset["name"]}, {asset["size"]});\n'
)

progmem.write("\t\t}\n")
progmem.write("};\n\n")


def add_app_to_filesystem():
build_path = Path(build_dir)
www_path = Path(filesystem_dir)
if www_path.exists() and www_path.is_dir():
rmtree(www_path)
print("Copying and compress interface to data directory")
copytree(build_path, www_path)
for current_path, _, files in os.walk(www_path):
for file in files:
gzip_file(os.path.join(current_path, file))
print("Build LittleFS file system image and upload to ESP32")
env.Execute("pio run --target uploadfs")


print("running: build_interface.py")
if should_regenerate_output_file():
build_webapp()
embed_webapp()
15 changes: 11 additions & 4 deletions data/svelte/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@
* myapp: compare what is here
* goto svelte folder
* npm install
* npm run build create dist folder
* npm run dev -- --open (Live Server)
* npm run dev -- --open
* compare with other deployments:
* https://github.com/theelims/ESP32-sveltekit : .py to create zipped .html
* https://github.com/BCsabaEngine/svelteesp32
Step2: add as ui.h and implement hamburger
* npm install to install the hamburger code
* npm run build (results in data/svelte/dist folder)
* or npm run dev -- --open to see the result in the browser
* pio build / upload to run build_interface.py which takes the files in dist and creates WWWData.h
* WWWData.h is used in SysModWeb to create server.on() handlers for the svelte files
* result at http://<esp32ip>/index.html
-->

<!doctype html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
<title>Star + Vite + Svelte</title>
</head>
<body>
<div id="app"></div>
Expand Down
2 changes: 1 addition & 1 deletion data/svelte/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mount } from 'svelte'
import App from './App.svelte'
import App from './src/App.svelte'

const app = mount(App, {
target: document.getElementById('app')!,
Expand Down
11 changes: 11 additions & 0 deletions data/svelte/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion data/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"vite": "^6.0.5"
"vite": "^6.0.5",
"svelte-hamburgers": "latest"
}
}
27 changes: 27 additions & 0 deletions data/svelte/src/App.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!--
Example by GHOST and Fractal
GHOST: https://github.com/ghostdevv
Fractal: https://github.com/FractalHQ
-->

<!--
THIS REPL IS FOR svelte-hamburgers 2
To view v3 check here: https://svelte.dev/repl/2339dbd1356a4149aabb17daa0a17e40
-->

<!-- StarBase: -->
<!-- https://www.npmjs.com/package/svelte-hamburgers?ref=madewithsvelte.com -->
<!-- https://svelte.dev/playground/86b10871cc7f42b39e74d71bdb4d643e?version=3.38.2 -->

<script>
import {Hamburger} from 'svelte-hamburgers';
import Menu from './Menu.svelte';
let open = $state('foo');
</script>

<Hamburger
bind:open
color="white" />

<Menu bind:open />
4 changes: 2 additions & 2 deletions data/svelte/App.svelte → data/svelte/src/AppCounter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</script>

<main>
<h1>StarBase + Vite + Svelte</h1>
<h1>Star + Vite + Svelte</h1>

<div class="card">
<Counter />
Expand All @@ -14,7 +14,7 @@
</p>

<p class="read-the-docs">
Click on the Vite and Svelte logos to learn more
Click on the Vite and Svelte logos to learn more (ewowi)
</p>
</main>

Expand Down
File renamed without changes.
40 changes: 40 additions & 0 deletions data/svelte/src/Menu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script>
import { fly, scale } from 'svelte/transition';
import { quadOut } from 'svelte/easing';
export let open;
</script>

{#if open}
<div>
{#each ['Home', 'Example', 'About', 'Contact'] as link, i}
<p transition:fly={{ y: -15, delay: 50 * i }}>
{link}
</p>
{/each}
</div>

<hr transition:scale={{ duration: 750, easing: quadOut, opacity: 1 }} />
{/if}

<style>
:global(html) {
background: #1d1d2f;
}
div {
text-align: center;
font-size: 1.5em;
letter-spacing: 0.15em;
padding: 1em;
padding-top: 0;
color: #eef;
}
p {
cursor: pointer;
width: max-content;
margin: 1rem auto;
}
p:hover {
text-decoration: underline;
}
</style>
2 changes: 2 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ build_flags =
-mtext-section-literals ;otherwise [UserModLive::setup()]+0xa17): dangerous relocation: l32r: literal target out of range (try using text-section-literals)
;for StarLight, first only for s2, now for all due to something in UserModLive.Setup...
${ESPAsyncWebServer.build_flags} ;alternatively PsychicHttp
-D EMBED_WWW ;embed the svelte web interface in the firmware
;optional:
-D STARBASE_ETHERNET ; +41.876 bytes (2.2%)
${STARBASE_USERMOD_E131.build_flags} ;+11.416 bytes 0.6%
Expand Down Expand Up @@ -139,6 +140,7 @@ lib_deps =
${STARBASE.lib_deps}
extra_scripts =
pre:tools/webbundle.py
pre:data/svelte/build_interface.py
post:tools/post_build.py


Expand Down
28 changes: 28 additions & 0 deletions src/Sys/SysModWeb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#include "html_ui.h"
#include "html_newui.h"
#include "WWWData.h"

#include "AsyncJson.h"

Expand Down Expand Up @@ -193,6 +194,33 @@ void SysModWeb::connectedChanged() {
server.on("/", HTTP_GET, [this](WebRequest *request) {serveIndex(request);});
server.on("/newui", HTTP_GET, [this](WebRequest *request) {serveNewUI(request);});

//StarBase modified version from https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/ESP32SvelteKit.cpp

#ifdef EMBED_WWW
// Serve static resources from PROGMEM
ESP_LOGV("ESP32SvelteKit", "Registering routes from PROGMEM static resources");
WWWData::registerRoutes(
[this](const String &uri, const String &contentType, const uint8_t *content, size_t len)
{
server.on(uri.c_str(), HTTP_GET, [this, content, len, contentType](WebRequest *request) {
WebResponse *response;
response = request->beginResponse_P(200, contentType.c_str(), content, len);
response->addHeader("Content-Encoding","gzip");
// response.addHeader("Cache-Control", "public, immutable, max-age=31536000"); //from svelte
// setStaticContentCacheHeaders(response);
request->send(response);
});

// Set default end-point for all non matching requests
// this is easier than using webServer.onNotFound()
// if (uri.equals("/index.html"))
// {
// _server->defaultEndpoint->setHandler(handler);
// }
});

#endif

//serve json calls
server.on("/json", HTTP_GET, [this](WebRequest *request) {serveJson(request);});

Expand Down

0 comments on commit b21d6e1

Please sign in to comment.