11 Commits

Author SHA1 Message Date
99678087cb move to svelte kit
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 2m4s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m23s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 1m53s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m16s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-02-15 18:03:21 +01:00
fe4bd11a21 preparation for switch to C6 MCU (but also compatible with S3)
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 2m23s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 6m1s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 2m17s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 5m57s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-02-06 22:30:48 +01:00
684ce36270 add vite helper files?
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m55s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 7m13s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 1m48s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m10s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:33:13 +01:00
98b5df1ff2 add ignore for FreeCAD and node-js intermediate files
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Has been cancelled
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Has been cancelled
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Has been cancelled
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Has been cancelled
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:29:16 +01:00
8128b958cb Merge branch 'feature/website'
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:26:42 +01:00
955b4bef04 rebuild websites with svelte
still early WIP

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:24:19 +01:00
81141d8859 connect via MQTTS
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 02:15:05 +01:00
e01006cd49 remove PSRAM usage
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 00:28:00 +01:00
c28d7d08df edit of all config data via website
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 00:14:52 +01:00
df50aaedda get/post led segments
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m49s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m44s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 4m34s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m43s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-21 21:53:03 +01:00
1f02d35a97 try to use react for SPA
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m45s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 7m18s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 4m16s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m26s
initial setup - no real function yet

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-19 22:47:57 +01:00
124 changed files with 11928 additions and 254 deletions

2
.gitignore vendored
View File

@@ -24,3 +24,5 @@
**/*_front.png
**/*_schematic*.png
**/wiki/*
*.FCBak
firmware/**/node_modules

View File

@@ -1,4 +1,9 @@
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(BOOTLOADER_EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/bootloader_components_extra/" APPEND)
project(system_control)
target_add_binary_data(${PROJECT_NAME}.elf "main/isrgrootx1.pem" TEXT)

View File

@@ -0,0 +1,9 @@
idf_component_register(SRCS "hooks.c"
REQUIRES extra_component)
# We need to force GCC to integrate this static library into the
# bootloader link. Indeed, by default, as the hooks in the bootloader are weak,
# the linker would just ignore the symbols in the extra. (i.e. not strictly
# required)
# To do so, we need to define the symbol (function) `bootloader_hooks_include`
# within hooks.c source file.

View File

@@ -0,0 +1,21 @@
#include "esp_log.h"
/* Function used to tell the linker to include this file
* with all its symbols.
*/
void bootloader_hooks_include(void)
{
}
/* Keep in my mind that a lot of functions cannot be called from here
* as system initialization has not been performed yet, including
* BSS, SPI flash, or memory protection. */
void bootloader_before_init(void)
{
ESP_LOGI("HOOK", "This hook is called BEFORE bootloader initialization");
}
void bootloader_after_init(void)
{
ESP_LOGI("HOOK", "This hook is called AFTER bootloader initialization");
}

View File

@@ -0,0 +1 @@
idf_component_register(SRCS "extra_component.c")

View File

@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "esp_log.h"
void bootloader_extra_dir_function(void)
{
ESP_LOGI("EXTRA", "This function is called from an extra component");
}

View File

@@ -2,14 +2,19 @@
#include "common.h"
#include "message_manager.h"
#include "esp_heap_caps.h"
#include "led_segment.h"
#include "persistence_manager.h"
#include "storage.h"
#include <cJSON.h>
#include <esp_http_server.h>
#include <esp_log.h>
#include <esp_wifi.h>
#include <persistence_manager.h>
#include <string.h>
#include <sys/stat.h>
#define MAX_BODY_SIZE 4096
static const char *TAG = "api_handlers";
// Helper function to set CORS headers
@@ -59,7 +64,7 @@ esp_err_t api_capabilities_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "GET /api/capabilities");
// Thread nur für esp32c6 oder esp32h2 verfügbar
// Thread only available for esp32c6 or esp32h2
bool thread = false;
#if defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2)
thread = true;
@@ -90,7 +95,7 @@ esp_err_t api_wifi_scan_handler(httpd_req_t *req)
uint16_t ap_num = 0;
esp_wifi_scan_get_ap_num(&ap_num);
wifi_ap_record_t *ap_list = calloc(ap_num, sizeof(wifi_ap_record_t));
wifi_ap_record_t *ap_list = heap_caps_calloc(ap_num, sizeof(wifi_ap_record_t), MALLOC_CAP_DEFAULT);
if (!ap_list)
{
return send_error_response(req, 500, "Memory allocation failed");
@@ -132,6 +137,7 @@ static bool is_valid(const cJSON *string)
esp_err_t api_wifi_config_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/wifi/config");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[256];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -159,7 +165,7 @@ esp_err_t api_wifi_config_handler(httpd_req_t *req)
if (is_valid(pw))
{
size_t pwlen = strlen(pw->valuestring);
char *masked = malloc(pwlen + 1);
char *masked = heap_caps_malloc(pwlen + 1, MALLOC_CAP_DEFAULT);
if (masked)
{
memset(masked, '*', pwlen);
@@ -245,6 +251,7 @@ esp_err_t api_wifi_status_handler(httpd_req_t *req)
esp_err_t api_light_power_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/light/power");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[64];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -298,6 +305,7 @@ esp_err_t api_light_thunder_handler(httpd_req_t *req)
esp_err_t api_light_mode_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/light/mode");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[64];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -333,7 +341,7 @@ esp_err_t api_light_mode_handler(httpd_req_t *req)
}
else
{
msg.data.settings.value.int_value = -1; // Unbekannter Modus
msg.data.settings.value.int_value = -1; // Unknown mode
}
message_manager_post(&msg, pdMS_TO_TICKS(100));
}
@@ -347,6 +355,7 @@ esp_err_t api_light_mode_handler(httpd_req_t *req)
esp_err_t api_light_schema_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/light/schema");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -400,31 +409,114 @@ esp_err_t api_wled_config_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "GET /api/wled/config");
// TODO: Implement actual LED config retrieval
const char *response = "{"
"\"segments\":["
"{\"name\":\"Main Light\",\"start\":0,\"leds\":60},"
"{\"name\":\"Accent Light\",\"start\":60,\"leds\":30}"
"]"
"}";
return send_json_response(req, response);
extern led_segment_t segments[LED_SEGMENT_MAX_LEN];
extern size_t segment_count;
size_t required_size = sizeof(segments) * segment_count;
cJSON *json = cJSON_CreateObject();
persistence_manager_t pm;
if (persistence_manager_init(&pm, "led_config") == ESP_OK)
{
persistence_manager_get_blob(&pm, "segments", segments, required_size, NULL);
uint8_t segment_count = persistence_manager_get_int(&pm, "segment_count", 0);
persistence_manager_deinit(&pm);
cJSON *segments_arr = cJSON_CreateArray();
for (uint8_t i = 0; i < segment_count; ++i)
{
cJSON *seg = cJSON_CreateObject();
cJSON_AddStringToObject(seg, "name", segments[i].name);
cJSON_AddNumberToObject(seg, "start", segments[i].start);
cJSON_AddNumberToObject(seg, "leds", segments[i].leds);
cJSON_AddItemToArray(segments_arr, seg);
}
cJSON_AddItemToObject(json, "segments", segments_arr);
}
else
{
cJSON_AddItemToObject(json, "segments", cJSON_CreateArray());
}
char *response = cJSON_PrintUnformatted(json);
cJSON_Delete(json);
esp_err_t res = send_json_response(req, response);
free(response);
return res;
}
esp_err_t api_wled_config_post_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/wled/config");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[512];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
char *buf = heap_caps_malloc(MAX_BODY_SIZE, MALLOC_CAP_DEFAULT);
if (!buf)
return send_error_response(req, 500, "Memory allocation failed");
int total = 0, ret;
while (total < MAX_BODY_SIZE - 1)
{
return send_error_response(req, 400, "Failed to receive request body");
ret = httpd_req_recv(req, buf + total, MAX_BODY_SIZE - 1 - total);
if (ret <= 0)
break;
total += ret;
}
buf[ret] = '\0';
buf[total] = '\0';
ESP_LOGI(TAG, "Received WLED config: %s", buf);
// TODO: Parse JSON and save LED configuration
cJSON *json = cJSON_Parse(buf);
free(buf);
if (!json)
{
return send_error_response(req, 400, "Invalid JSON");
}
cJSON *segments_arr = cJSON_GetObjectItem(json, "segments");
if (!cJSON_IsArray(segments_arr))
{
cJSON_Delete(json);
return send_error_response(req, 400, "Missing segments array");
}
extern led_segment_t segments[LED_SEGMENT_MAX_LEN];
extern size_t segment_count;
size_t count = cJSON_GetArraySize(segments_arr);
if (count > LED_SEGMENT_MAX_LEN)
count = LED_SEGMENT_MAX_LEN;
segment_count = count;
for (size_t i = 0; i < LED_SEGMENT_MAX_LEN; ++i)
{
cJSON *seg = cJSON_GetArrayItem(segments_arr, i);
cJSON *name = cJSON_GetObjectItem(seg, "name");
cJSON *start = cJSON_GetObjectItem(seg, "start");
cJSON *leds = cJSON_GetObjectItem(seg, "leds");
if (cJSON_IsString(name) && cJSON_IsNumber(start) && cJSON_IsNumber(leds) && i < count)
{
strncpy(segments[i].name, name->valuestring, sizeof(segments[i].name) - 1);
segments[i].name[sizeof(segments[i].name) - 1] = '\0';
segments[i].start = (uint16_t)start->valuedouble;
segments[i].leds = (uint16_t)leds->valuedouble;
}
else
{
// Invalid entry, skip or set defaults
segments[i].name[0] = '\0';
segments[i].start = 0;
segments[i].leds = 0;
}
}
cJSON_Delete(json);
persistence_manager_t pm;
if (persistence_manager_init(&pm, "led_config") == ESP_OK)
{
persistence_manager_set_blob(&pm, "segments", segments, sizeof(led_segment_t) * segment_count);
persistence_manager_set_int(&pm, "segment_count", (int32_t)segment_count);
persistence_manager_deinit(&pm);
}
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
}
@@ -432,6 +524,16 @@ esp_err_t api_wled_config_post_handler(httpd_req_t *req)
// ============================================================================
// Schema API
// ============================================================================
static char *heap_caps_strdup(const char *src, uint32_t caps)
{
if (!src)
return NULL;
size_t len = strlen(src) + 1;
char *dst = heap_caps_malloc(len, caps);
if (dst)
memcpy(dst, src, len);
return dst;
}
esp_err_t api_schema_get_handler(httpd_req_t *req)
{
@@ -448,13 +550,11 @@ esp_err_t api_schema_get_handler(httpd_req_t *req)
ESP_LOGI(TAG, "Requested schema: %s", filename);
// Schema-Datei lesen
// Read schema file
char path[128];
snprintf(path, sizeof(path), "%s", filename);
int line_count = 0;
extern char **read_lines_filtered(const char *filename, int *out_count);
extern void free_lines(char **lines, int count);
char **lines = read_lines_filtered(path, &line_count);
set_cors_headers(req);
@@ -465,11 +565,11 @@ esp_err_t api_schema_get_handler(httpd_req_t *req)
return httpd_resp_sendstr(req, "");
}
// Gesamtlänge berechnen
// Calculate total length
size_t total_len = 0;
for (int i = 0; i < line_count; ++i)
total_len += strlen(lines[i]) + 1;
char *csv = malloc(total_len + 1);
char *csv = heap_caps_malloc(total_len + 1, MALLOC_CAP_DEFAULT);
char *p = csv;
for (int i = 0; i < line_count; ++i)
{
@@ -488,28 +588,78 @@ esp_err_t api_schema_get_handler(httpd_req_t *req)
esp_err_t api_schema_post_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/schema/*");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
// Extract filename from URI
const char *uri = req->uri;
const char *filename = strrchr(uri, '/');
if (filename == NULL)
if (!req)
{
ESP_LOGE(TAG, "Request pointer is NULL");
return send_error_response(req, 500, "Internal error: req is NULL");
}
const char *uri = req->uri;
ESP_LOGI(TAG, "Request URI: %s", uri ? uri : "(null)");
if (!uri)
{
ESP_LOGE(TAG, "Request URI is NULL");
return send_error_response(req, 400, "Invalid schema path (no URI)");
}
const char *filename = strrchr(uri, '/');
if (filename == NULL || filename[1] == '\0')
{
ESP_LOGE(TAG, "Could not extract filename from URI: %s", uri);
return send_error_response(req, 400, "Invalid schema path");
}
filename++;
ESP_LOGI(TAG, "Extracted filename: %s", filename);
char buf[2048];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
// Dynamically read POST body (like api_wled_config_post_handler)
char *buf = heap_caps_malloc(MAX_BODY_SIZE, MALLOC_CAP_DEFAULT);
if (!buf)
{
return send_error_response(req, 400, "Failed to receive request body");
ESP_LOGE(TAG, "Memory allocation failed for POST body");
return send_error_response(req, 500, "Memory allocation failed");
}
buf[ret] = '\0';
int total = 0, ret;
while (total < MAX_BODY_SIZE - 1)
{
ret = httpd_req_recv(req, buf + total, MAX_BODY_SIZE - 1 - total);
if (ret <= 0)
break;
total += ret;
}
buf[total] = '\0';
ESP_LOGI(TAG, "Saving schema %s, size: %d bytes", filename, ret);
ESP_LOGI(TAG, "Saving schema %s, size: %d bytes", filename, total);
// TODO: Save schema to storage
// Split CSV body into line array
int line_count = 0;
// Count lines
for (int i = 0; i < total; ++i)
if (buf[i] == '\n')
line_count++;
if (total > 0 && buf[total - 1] != '\n')
line_count++; // last line without \n
char **lines = (char **)heap_caps_malloc(line_count * sizeof(char *), MALLOC_CAP_DEFAULT);
int idx = 0;
char *saveptr = NULL;
char *line = strtok_r(buf, "\n", &saveptr);
while (line && idx < line_count)
{
// Ignore empty lines
if (line[0] != '\0')
lines[idx++] = heap_caps_strdup(line, MALLOC_CAP_DEFAULT);
line = strtok_r(NULL, "\n", &saveptr);
}
int actual_count = idx;
esp_err_t err = write_lines(filename, lines, actual_count);
for (int i = 0; i < actual_count; ++i)
free(lines[i]);
free(lines);
set_cors_headers(req);
if (err != ESP_OK)
return send_error_response(req, 500, "Failed to save schema");
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
}
@@ -532,6 +682,7 @@ esp_err_t api_devices_scan_handler(httpd_req_t *req)
esp_err_t api_devices_pair_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/devices/pair");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[256];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -562,6 +713,7 @@ esp_err_t api_devices_paired_handler(httpd_req_t *req)
esp_err_t api_devices_update_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/devices/update");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[256];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -581,6 +733,7 @@ esp_err_t api_devices_update_handler(httpd_req_t *req)
esp_err_t api_devices_unpair_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/devices/unpair");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -600,6 +753,7 @@ esp_err_t api_devices_unpair_handler(httpd_req_t *req)
esp_err_t api_devices_toggle_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/devices/toggle");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -652,6 +806,7 @@ esp_err_t api_scenes_get_handler(httpd_req_t *req)
esp_err_t api_scenes_post_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/scenes");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[512];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -690,6 +845,7 @@ esp_err_t api_scenes_delete_handler(httpd_req_t *req)
esp_err_t api_scenes_activate_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/scenes/activate");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -817,7 +973,7 @@ esp_err_t api_captive_portal_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "Captive portal detection: %s", req->uri);
// captive.html direkt ausliefern (Status 200, text/html)
// Serve captive.html directly (status 200, text/html)
const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH;
char filepath[256];
snprintf(filepath, sizeof(filepath), "%s/captive.html", base_path);
@@ -826,7 +982,7 @@ esp_err_t api_captive_portal_handler(httpd_req_t *req)
{
ESP_LOGE(TAG, "captive.html not found: %s", filepath);
httpd_resp_set_status(req, "500 Internal Server Error");
httpd_resp_sendstr(req, "Captive Portal nicht verfügbar");
httpd_resp_sendstr(req, "Captive portal not available");
return ESP_FAIL;
}
httpd_resp_set_type(req, "text/html");

View File

@@ -0,0 +1,16 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#define LED_SEGMENT_MAX_LEN 15
typedef struct
{
char name[32];
uint16_t start;
uint16_t leds;
} led_segment_t;
led_segment_t segments[LED_SEGMENT_MAX_LEN];
size_t segment_count;

View File

@@ -97,22 +97,21 @@ esp_err_t led_status_init(int gpio_num)
.max_leds = STATUS_LED_COUNT,
.led_model = LED_MODEL_WS2812,
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRBW,
.flags =
{
.invert_out = false,
},
.flags = {.invert_out = 0},
};
led_strip_rmt_config_t rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000, // 10MHz
.mem_block_symbols = 0,
.flags =
{
.with_dma = false,
},
.flags = {.with_dma = 0},
};
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
ESP_LOGI(TAG, "LED strip initialized.");
esp_err_t ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init status LED: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "Status LED initialized.");
// Create mutex
mutex = xSemaphoreCreateMutex();

View File

@@ -72,17 +72,21 @@ esp_err_t led_strip_init(void)
.max_leds = MAX_LEDS,
.led_model = LED_MODEL_WS2812,
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB,
.flags = {.invert_out = false},
.flags = {.invert_out = 0},
};
led_strip_rmt_config_t rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 0,
.mem_block_symbols = 0,
.flags = {.with_dma = true},
.flags = {.with_dma = 0},
};
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
esp_err_t ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init main LED strip: %s", esp_err_to_name(ret));
return ret;
}
led_command_queue = xQueueCreate(5, sizeof(led_command_t));
if (led_command_queue == NULL)

View File

@@ -3,4 +3,6 @@ idf_component_register(
INCLUDE_DIRS "include"
PRIV_REQUIRES
persistence-manager
my_mqtt_client
app_update
)

View File

@@ -1,9 +1,14 @@
#include "message_manager.h"
#include "my_mqtt_client.h"
#include <esp_app_desc.h>
#include <esp_log.h>
#include <esp_mac.h>
#include <esp_system.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <persistence_manager.h>
#include <sdkconfig.h>
#include <string.h>
#define MESSAGE_QUEUE_LENGTH 16
@@ -98,6 +103,15 @@ static void message_manager_task(void *param)
message_listeners[i](&msg);
}
}
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
const esp_app_desc_t *app_desc = esp_app_get_description();
char topic[60];
snprintf(topic, sizeof(topic), "device/%s/%02x%02x", app_desc->project_name, mac[4], mac[5]);
char *data = "{\"key\":\"value\"}";
mqtt_client_publish(topic, data, strlen(data), 0, false);
}
}
}

View File

@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/my_mqtt_client.c"
INCLUDE_DIRS "include"
REQUIRES
mqtt
app_update
)

View File

@@ -0,0 +1,21 @@
menu "MQTT Client Settings"
config MQTT_CLIENT_BROKER_URL
string "MQTT Broker URL (TLS)"
default "mqtts://example.com:8883"
help
Die Adresse des MQTT-Brokers (z.B. mqtts://broker.example.com:8883)
config MQTT_CLIENT_USERNAME
string "MQTT Username"
default "user"
help
Benutzername für die Authentifizierung (optional)
config MQTT_CLIENT_PASSWORD
string "MQTT Password"
default "password"
help
Passwort für die Authentifizierung (optional)
endmenu

View File

@@ -0,0 +1,18 @@
# MQTT Client Component for ESP-IDF
Diese Komponente stellt eine einfache MQTT-Client-Implementierung bereit, die Daten an einen TLS-gesicherten MQTT-Broker sendet.
## Dateien
- mqtt_client.c: Implementierung des MQTT-Clients
- mqtt_client.h: Header-Datei
- CMakeLists.txt: Build-Konfiguration
- Kconfig: Konfiguration für die Komponente
## Abhängigkeiten
- ESP-IDF (empfohlen: >= v4.0)
- Komponenten: esp-mqtt, esp-tls
## Nutzung
1. Füge die Komponente in dein Projekt ein.
2. Passe die Konfiguration in `Kconfig` an.
3. Binde die Komponente in deinem Code ein und nutze die API aus `mqtt_client.h`.

View File

@@ -0,0 +1,2 @@
dependencies:
espressif/mqtt: ^1.0.0

View File

@@ -0,0 +1,14 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
void mqtt_client_start(void);
void mqtt_client_publish(const char *topic, const char *data, size_t len, int qos, bool retain);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,122 @@
#include "my_mqtt_client.h"
#include "esp_app_desc.h"
#include "esp_err.h"
#include "esp_interface.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_system.h"
#include "mqtt_client.h"
#include "sdkconfig.h"
static const char *TAG = "mqtt_client";
static esp_mqtt_client_handle_t client = NULL;
extern const uint8_t isrgrootx1_pem_start[] asm("_binary_isrgrootx1_pem_start");
extern const uint8_t isrgrootx1_pem_end[] asm("_binary_isrgrootx1_pem_end");
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = event_data;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id)
{
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
msg_id = esp_mqtt_client_subscribe(client, "topic/qos0", 0);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(client, "topic/qos1", 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_unsubscribe(client, "topic/qos1");
ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d, return code=0x%02x ", event->msg_id, (uint8_t)*event->data);
msg_id = esp_mqtt_client_publish(client, "topic/qos0", "data", 0, 0, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT_EVENT_DATA:");
ESP_LOGI(TAG, "TOPIC=%.*s\r\n", event->topic_len, event->topic);
ESP_LOGI(TAG, "DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
if (event->error_handle)
{
ESP_LOGE(TAG, "error_type: %d", event->error_handle->error_type);
ESP_LOGE(TAG, "esp-tls error code: 0x%x", event->error_handle->esp_tls_last_esp_err);
ESP_LOGE(TAG, "tls_stack_err: 0x%x", event->error_handle->esp_tls_stack_err);
ESP_LOGE(TAG, "transport_sock_errno: %d", event->error_handle->esp_transport_sock_errno);
}
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
void mqtt_client_start(void)
{
ESP_LOGI(TAG, "Starte MQTT-Client mit URI: %s", CONFIG_MQTT_CLIENT_BROKER_URL);
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
const esp_app_desc_t *app_desc = esp_app_get_description();
char client_id[60];
snprintf(client_id, sizeof(client_id), "%s/%02x%02x", app_desc->project_name, mac[4], mac[5]);
const esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = CONFIG_MQTT_CLIENT_BROKER_URL,
.broker.verification.certificate = (const char *)isrgrootx1_pem_start,
.broker.verification.certificate_len = isrgrootx1_pem_end - isrgrootx1_pem_start,
.credentials.username = CONFIG_MQTT_CLIENT_USERNAME,
.credentials.client_id = client_id,
.credentials.authentication.password = CONFIG_MQTT_CLIENT_PASSWORD,
};
client = esp_mqtt_client_init(&mqtt_cfg);
if (client == NULL)
{
ESP_LOGE(TAG, "Fehler bei esp_mqtt_client_init!");
return;
}
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_err_t err = esp_mqtt_client_start(client);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "esp_mqtt_client_start fehlgeschlagen: %s", esp_err_to_name(err));
}
else
{
ESP_LOGI(TAG, "MQTT-Client gestartet");
}
}
void mqtt_client_publish(const char *topic, const char *data, size_t len, int qos, bool retain)
{
if (client)
{
int msg_id = esp_mqtt_client_publish(client, topic, data, len, qos, retain);
ESP_LOGI(TAG, "Publish: topic=%s, msg_id=%d, qos=%d, retain=%d, len=%d", topic, msg_id, qos, retain, (int)len);
}
else
{
ESP_LOGW(TAG, "Publish aufgerufen, aber Client ist nicht initialisiert!");
}
}

View File

@@ -205,6 +205,33 @@ extern "C"
void persistence_manager_get_string(const persistence_manager_t *pm, const char *key, char *out_value,
size_t max_len, const char *default_value);
/**
* @brief Set a blob (binary data) value for a key in NVS storage.
*
* This function stores arbitrary binary data under the given key.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to set.
* @param value Pointer to the data to store.
* @param length Length of the data in bytes.
*/
void persistence_manager_set_blob(persistence_manager_t *pm, const char *key, const void *value, size_t length);
/**
* @brief Get a blob (binary data) value for a key from NVS storage.
*
* This function retrieves binary data previously stored under the given key.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to retrieve.
* @param out_value Buffer to store the retrieved data.
* @param max_length Maximum length of the output buffer in bytes.
* @param out_length Pointer to variable to receive the actual data length.
* @return true if the blob was found and read successfully, false otherwise.
*/
bool persistence_manager_get_blob(const persistence_manager_t *pm, const char *key, void *out_value,
size_t max_length, size_t *out_length);
#ifdef __cplusplus
}
#endif

View File

@@ -1,4 +1,3 @@
#include "persistence_manager.h"
#include <esp_log.h>
#include <string.h>
@@ -174,6 +173,17 @@ void persistence_manager_set_string(persistence_manager_t *pm, const char *key,
}
}
void persistence_manager_set_blob(persistence_manager_t *pm, const char *key, const void *value, size_t length)
{
if (!persistence_manager_is_initialized(pm) || !value || length == 0)
return;
esp_err_t err = nvs_set_blob(pm->nvs_handle, key, value, length);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set blob key '%s': %s", key, esp_err_to_name(err));
}
}
bool persistence_manager_get_bool(const persistence_manager_t *pm, const char *key, bool default_value)
{
if (!persistence_manager_is_initialized(pm))
@@ -245,3 +255,20 @@ void persistence_manager_get_string(const persistence_manager_t *pm, const char
return;
}
}
bool persistence_manager_get_blob(const persistence_manager_t *pm, const char *key, void *out_value, size_t max_length,
size_t *out_length)
{
if (!persistence_manager_is_initialized(pm) || !out_value || max_length == 0)
return false;
size_t required_size = 0;
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, NULL, &required_size);
if (err != ESP_OK || required_size == 0 || required_size > max_length)
return false;
err = nvs_get_blob(pm->nvs_handle, key, out_value, &required_size);
if (err != ESP_OK)
return false;
if (out_length)
*out_length = required_size;
return true;
}

View File

@@ -1,5 +1,7 @@
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C"
{
@@ -8,6 +10,14 @@ extern "C"
void load_file(const char *filename);
char **read_lines_filtered(const char *filename, int *out_count);
void free_lines(char **lines, int count);
/**
* Write an array of lines to a file (CSV or other text).
* @param filename File name (without /spiffs/)
* @param lines Array of lines (null-terminated strings)
* @param count Number of lines
* @return ESP_OK on success, error code otherwise
*/
esp_err_t write_lines(const char *filename, char **lines, int count);
#ifdef __cplusplus
}
#endif

View File

@@ -82,10 +82,10 @@ esp_err_t add_light_item(const char time[5], uint8_t red, uint8_t green, uint8_t
uint8_t brightness, uint8_t saturation)
{
// Allocate memory for a new node in PSRAM.
light_item_node_t *new_node = (light_item_node_t *)heap_caps_malloc(sizeof(light_item_node_t), MALLOC_CAP_SPIRAM);
light_item_node_t *new_node = (light_item_node_t *)heap_caps_malloc(sizeof(light_item_node_t), MALLOC_CAP_DEFAULT);
if (new_node == NULL)
{
ESP_LOGE(TAG, "Failed to allocate memory in PSRAM for new light_item_node_t.");
ESP_LOGE(TAG, "Failed to allocate memory for new light_item_node_t.");
return ESP_FAIL;
}
@@ -373,7 +373,7 @@ void start_simulation_task(void)
stop_simulation_task();
simulation_config_t *config =
(simulation_config_t *)heap_caps_malloc(sizeof(simulation_config_t), MALLOC_CAP_SPIRAM);
(simulation_config_t *)heap_caps_malloc(sizeof(simulation_config_t), MALLOC_CAP_DEFAULT);
if (config == NULL)
{
ESP_LOGE(TAG, "Failed to allocate memory for simulation config.");

View File

@@ -130,3 +130,27 @@ void free_lines(char **lines, int count)
free(lines[i]);
free(lines);
}
esp_err_t write_lines(const char *filename, char **lines, int count)
{
char fullpath[128];
snprintf(fullpath, sizeof(fullpath), "/spiffs/%s", filename[0] == '/' ? filename + 1 : filename);
FILE *f = fopen(fullpath, "w");
if (!f)
{
ESP_LOGE(TAG, "Failed to open file for writing: %s", fullpath);
return ESP_FAIL;
}
for (int i = 0; i < count; ++i)
{
if (fprintf(f, "%s\n", lines[i]) < 0)
{
ESP_LOGE(TAG, "Failed to write line %d", i);
fclose(f);
return ESP_FAIL;
}
}
fclose(f);
ESP_LOGI(TAG, "Wrote %d lines to %s", count, fullpath);
return ESP_OK;
}

View File

@@ -0,0 +1,13 @@
[17:12:14.017] [INFO] Initializing Storybook
[17:12:14.195] [DEBUG] Getting package.json info for /Users/mars3142/Coding/git.mars3142.dev/system-control/firmware/package.json...
[17:12:14.195] [DEBUG] Getting CLI versions from NPM for storybook...
[17:12:14.195] [DEBUG] Executing command: npm info storybook version
[17:12:14.850] [INFO] Adding Storybook version 10.2.8 to your project
[17:12:14.851] [ERROR] Unable to initialize Storybook in this directory.
Storybook couldn't detect a supported framework or configuration for your project. Make sure you're inside a framework project (e.g., React, Vue, Svelte, Angular, Next.js) and that its dependencies are installed.
Tips:
- Run init in an empty directory or create a new framework app first.
- If this directory contains unrelated files, try a new directory for Storybook.
[17:12:14.853] [INFO] Storybook collects completely anonymous usage telemetry. We use it to shape Storybook's roadmap and prioritize features. You can learn more, including how to opt out, at https://storybook.js.org/telemetry

View File

@@ -1,10 +1,10 @@
idf_component_register(SRCS
main.cpp
app_task.cpp
button_handling.c
i2c_checker.c
hal/u8g2_esp32_hal.c
INCLUDE_DIRS "."
src/main.cpp
src/app_task.cpp
src/button_handling.c
src/i2c_checker.c
src/hal/u8g2_esp32_hal.c
INCLUDE_DIRS "include"
PRIV_REQUIRES
analytics
insa
@@ -21,6 +21,7 @@ idf_component_register(SRCS
app_update
rmaker_common
driver
my_mqtt_client
)
spiffs_create_partition_image(storage ../storage FLASH_IN_PROJECT)

View File

@@ -6,84 +6,6 @@ menu "System Control"
help
Enable or disable WiFi connectivity.
config WIFI_NETWORK_COUNT
depends on WIFI_ENABLED
int "Number of WiFi Networks"
default 1
range 1 5
help
Number of WiFi networks to configure (1-5).
config WIFI_SSID_1
depends on WIFI_ENABLED
string "WiFi SSID 1"
default "YourSSID1"
help
The SSID of the first WiFi network.
config WIFI_PASSWORD_1
depends on WIFI_ENABLED
string "WiFi Password 1"
default "YourPassword1"
help
The password of the first WiFi network.
config WIFI_SSID_2
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 2
string "WiFi SSID 2"
default ""
help
The SSID of the second WiFi network.
config WIFI_PASSWORD_2
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 2
string "WiFi Password 2"
default ""
help
The password of the second WiFi network.
config WIFI_SSID_3
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 3
string "WiFi SSID 3"
default ""
help
The SSID of the third WiFi network.
config WIFI_PASSWORD_3
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 3
string "WiFi Password 3"
default ""
help
The password of the third WiFi network.
config WIFI_SSID_4
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 4
string "WiFi SSID 4"
default ""
help
The SSID of the fourth WiFi network.
config WIFI_PASSWORD_4
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 4
string "WiFi Password 4"
default ""
help
The password of the fourth WiFi network.
config WIFI_SSID_5
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 5
string "WiFi SSID 5"
default ""
help
The SSID of the fifth WiFi network.
config WIFI_PASSWORD_5
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 5
string "WiFi Password 5"
default ""
help
The password of the fifth WiFi network.
config WIFI_CONNECT_RETRIES
depends on WIFI_ENABLED
int "WiFi Connection Retry Attempts per Network"
@@ -105,4 +27,42 @@ menu "System Control"
help
GPIO pin number for the SCL line of the display.
endmenu
menu "Button Configuration"
config BUTTON_UP
int "Button UP GPIO Pin"
default 1
help
GPIO pin number for the up button.
config BUTTON_DOWN
int "Button DOWN GPIO Pin"
default 6
help
GPIO pin number for the down button.
config BUTTON_LEFT
int "Button LEFT GPIO Pin"
default 3
help
GPIO pin number for the left button.
config BUTTON_RIGHT
int "Button RIGHT GPIO Pin"
default 5
help
GPIO pin number for the right button.
config BUTTON_SELECT
int "Button SELECT GPIO Pin"
default 18
help
GPIO pin number for the select button.
config BUTTON_BACK
int "Button BACK GPIO Pin"
default 16
help
GPIO pin number for the back button.
endmenu
endmenu

View File

@@ -1,8 +0,0 @@
#pragma once
#define BUTTON_UP GPIO_NUM_1
#define BUTTON_DOWN GPIO_NUM_6
#define BUTTON_LEFT GPIO_NUM_3
#define BUTTON_RIGHT GPIO_NUM_5
#define BUTTON_SELECT GPIO_NUM_18
#define BUTTON_BACK GPIO_NUM_16

View File

@@ -1,74 +0,0 @@
#include "i2c_checker.h"
#include "driver/i2c.h"
#include "esp_insights.h"
#include "esp_log.h"
#include "hal/u8g2_esp32_hal.h"
static const char *TAG = "i2c_checker";
esp_err_t i2c_device_check(i2c_port_t i2c_port, uint8_t device_address)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
// Send the device address with the write bit (LSB = 0)
i2c_master_write_byte(cmd, (device_address << 1) | I2C_MASTER_WRITE, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
return ret;
}
esp_err_t i2c_bus_scan_and_check(void)
{
// 1. Configure and install I2C bus
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_PIN,
.scl_io_num = I2C_MASTER_SCL_PIN,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
esp_err_t err = i2c_param_config(I2C_MASTER_NUM, &conf);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "I2C parameter configuration failed: %s", esp_err_to_name(err));
return err;
}
err = i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "I2C driver installation failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "I2C driver initialized. Searching for device...");
// 2. Check if the device is present
err = i2c_device_check(I2C_MASTER_NUM, DISPLAY_I2C_ADDRESS);
if (err == ESP_OK)
{
ESP_LOGI(TAG, "Device found at address 0x%02X!", DISPLAY_I2C_ADDRESS);
// Here you could now call e.g. setup_screen()
}
else if (err == ESP_ERR_TIMEOUT)
{
ESP_LOGE(TAG, "Timeout! Device at address 0x%02X is not responding.", DISPLAY_I2C_ADDRESS);
}
else
{
ESP_LOGE(TAG, "Error communicating with address 0x%02X: %s", DISPLAY_I2C_ADDRESS, esp_err_to_name(err));
}
// 3. Uninstall I2C driver if it is no longer needed
i2c_driver_delete(I2C_MASTER_NUM);
ESP_DIAG_EVENT(TAG, "I2C driver uninstalled.");
return err;
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "driver/gpio.h"
#define BUTTON_UP ((gpio_num_t)CONFIG_BUTTON_UP)
#define BUTTON_DOWN ((gpio_num_t)CONFIG_BUTTON_DOWN)
#define BUTTON_LEFT ((gpio_num_t)CONFIG_BUTTON_LEFT)
#define BUTTON_RIGHT ((gpio_num_t)CONFIG_BUTTON_RIGHT)
#define BUTTON_SELECT ((gpio_num_t)CONFIG_BUTTON_SELECT)
#define BUTTON_BACK ((gpio_num_t)CONFIG_BUTTON_BACK)

View File

@@ -13,10 +13,11 @@
#include "u8g2.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "driver/spi_master.h"
#include "hal/i2c_types.h"
#include "driver/i2c_master.h"
#define U8G2_ESP32_HAL_UNDEFINED GPIO_NUM_NC
#define I2C_MASTER_NUM I2C_NUM_0 // I2C port number for master dev

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

View File

@@ -8,6 +8,7 @@
#include "i2c_checker.h"
#include "led_status.h"
#include "message_manager.h"
#include "my_mqtt_client.h"
#include "persistence_manager.h"
#include "simulator.h"
#include "ui/ClockScreenSaver.h"
@@ -252,6 +253,8 @@ void app_task(void *args)
wifi_manager_init();
mqtt_client_start();
message_manager_register_listener(on_message_received);
start_simulation();

View File

@@ -7,13 +7,17 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "u8g2_esp32_hal.h"
#include "hal/u8g2_esp32_hal.h"
static const char *TAG = "u8g2_hal";
static const unsigned int I2C_TIMEOUT_MS = 1000;
static spi_device_handle_t handle_spi; // SPI handle.
static i2c_cmd_handle_t handle_i2c; // I2C handle.
static i2c_master_bus_handle_t i2c_bus; // I2C bus handle (new driver).
static i2c_master_dev_handle_t i2c_dev; // I2C device handle (new driver).
static uint8_t i2c_tx_buf[256]; // Buffer for one I2C transaction.
static size_t i2c_tx_len; // Current length in buffer.
static uint8_t current_i2c_addr7; // Current 7-bit device address.
static u8g2_esp32_hal_t u8g2_esp32_hal; // HAL state data.
static bool i2c_transfer_failed = false; // Flag to track I2C transfer errors
@@ -148,21 +152,23 @@ uint8_t u8g2_esp32_i2c_byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void
break;
}
i2c_config_t conf = {0};
conf.mode = I2C_MODE_MASTER;
ESP_LOGI(TAG, "sda_io_num %d", u8g2_esp32_hal.bus.i2c.sda);
conf.sda_io_num = u8g2_esp32_hal.bus.i2c.sda;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
ESP_LOGI(TAG, "scl_io_num %d", u8g2_esp32_hal.bus.i2c.scl);
conf.scl_io_num = u8g2_esp32_hal.bus.i2c.scl;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
ESP_LOGI(TAG, "clk_speed %d", I2C_MASTER_FREQ_HZ);
conf.master.clk_speed = I2C_MASTER_FREQ_HZ;
ESP_LOGI(TAG, "i2c_param_config %d", conf.mode);
ESP_ERROR_CHECK(i2c_param_config(I2C_MASTER_NUM, &conf));
ESP_LOGI(TAG, "i2c_driver_install %d", I2C_MASTER_NUM);
ESP_ERROR_CHECK(
i2c_driver_install(I2C_MASTER_NUM, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0));
// Neue I2C-Master-API: Bus einmalig anlegen
if (i2c_bus == NULL)
{
i2c_master_bus_config_t bus_cfg = {
.i2c_port = I2C_MASTER_NUM,
.scl_io_num = u8g2_esp32_hal.bus.i2c.scl,
.sda_io_num = u8g2_esp32_hal.bus.i2c.sda,
.clk_source = I2C_CLK_SRC_DEFAULT,
.flags = {.enable_internal_pullup = true},
};
ESP_LOGI(TAG, "sda_io_num %d", u8g2_esp32_hal.bus.i2c.sda);
ESP_LOGI(TAG, "scl_io_num %d", u8g2_esp32_hal.bus.i2c.scl);
ESP_LOGI(TAG, "clk_speed %d", I2C_MASTER_FREQ_HZ);
ESP_LOGI(TAG, "i2c_new_master_bus %d", I2C_MASTER_NUM);
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &i2c_bus));
}
break;
}
@@ -174,37 +180,55 @@ uint8_t u8g2_esp32_i2c_byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void
uint8_t *data_ptr = (uint8_t *)arg_ptr;
ESP_LOG_BUFFER_HEXDUMP(TAG, data_ptr, arg_int, ESP_LOG_VERBOSE);
while (arg_int > 0)
// Bytes in lokalen Puffer sammeln, tatsächliche Übertragung bei END_TRANSFER
if (i2c_tx_len + (size_t)arg_int > sizeof(i2c_tx_buf))
{
I2C_ERROR_CHECK(i2c_master_write_byte(handle_i2c, *data_ptr, ACK_CHECK_EN));
if (i2c_transfer_failed)
{
break;
}
data_ptr++;
arg_int--;
ESP_LOGW(TAG, "I2C tx buffer overflow (%zu + %d)", i2c_tx_len, arg_int);
i2c_transfer_failed = true;
break;
}
memcpy(&i2c_tx_buf[i2c_tx_len], data_ptr, arg_int);
i2c_tx_len += (size_t)arg_int;
break;
}
case U8X8_MSG_BYTE_START_TRANSFER: {
uint8_t i2c_address = u8x8_GetI2CAddress(u8x8);
handle_i2c = i2c_cmd_link_create();
i2c_transfer_failed = false; // Reset error flag at start of transfer
ESP_LOGD(TAG, "Start I2C transfer to %02X.", i2c_address >> 1);
I2C_ERROR_CHECK(i2c_master_start(handle_i2c));
I2C_ERROR_CHECK(i2c_master_write_byte(handle_i2c, i2c_address | I2C_MASTER_WRITE, ACK_CHECK_EN));
i2c_transfer_failed = false; // Reset error flag at start of transfer
// Für neuen Treiber: Device-Handle für diese 7-Bit-Adresse anlegen (oder wiederverwenden)
uint8_t addr7 = i2c_address >> 1;
if (i2c_dev == NULL || addr7 != current_i2c_addr7)
{
if (i2c_dev)
{
i2c_master_bus_rm_device(i2c_dev);
i2c_dev = NULL;
}
i2c_device_config_t dev_cfg = {
.device_address = addr7,
.scl_speed_hz = I2C_MASTER_FREQ_HZ,
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus, &dev_cfg, &i2c_dev));
current_i2c_addr7 = addr7;
}
i2c_tx_len = 0;
break;
}
case U8X8_MSG_BYTE_END_TRANSFER: {
ESP_LOGD(TAG, "End I2C transfer.");
if (!i2c_transfer_failed)
if (!i2c_transfer_failed && i2c_dev != NULL && i2c_tx_len > 0)
{
I2C_ERROR_CHECK(i2c_master_stop(handle_i2c));
I2C_ERROR_CHECK(i2c_master_cmd_begin(I2C_MASTER_NUM, handle_i2c, pdMS_TO_TICKS(I2C_TIMEOUT_MS)));
esp_err_t rc = i2c_master_transmit(i2c_dev, i2c_tx_buf, i2c_tx_len, I2C_TIMEOUT_MS);
if (rc != ESP_OK)
{
ESP_LOGW(TAG, "I2C error: i2c_master_transmit = %d", rc);
i2c_transfer_failed = true;
}
}
i2c_cmd_link_delete(handle_i2c);
break;
}
}

View File

@@ -0,0 +1,63 @@
#include "i2c_checker.h"
#include "driver/i2c_master.h"
#include "esp_insights.h"
#include "esp_log.h"
#include "hal/u8g2_esp32_hal.h"
static const char *TAG = "i2c_checker";
static esp_err_t i2c_device_check(i2c_master_bus_handle_t i2c_bus, uint8_t device_address)
{
// Use the new I2C master driver to probe for the device.
return i2c_master_probe(i2c_bus, device_address, 100);
}
esp_err_t i2c_bus_scan_and_check(void)
{
// 1. Configure and create I2C master bus using the new driver API
i2c_master_bus_handle_t i2c_bus = NULL;
i2c_master_bus_config_t bus_cfg = {
.i2c_port = I2C_MASTER_NUM,
.scl_io_num = I2C_MASTER_SCL_PIN,
.sda_io_num = I2C_MASTER_SDA_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.flags = {.enable_internal_pullup = true},
};
esp_err_t err = i2c_new_master_bus(&bus_cfg, &i2c_bus);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "I2C bus creation failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "I2C master bus initialized. Searching for device...");
// 2. Check if the device is present using the new API
err = i2c_device_check(i2c_bus, DISPLAY_I2C_ADDRESS);
if (err == ESP_OK)
{
ESP_LOGI(TAG, "Device found at address 0x%02X!", DISPLAY_I2C_ADDRESS);
// Here you could now call e.g. setup_screen()
}
else if (err == ESP_ERR_TIMEOUT)
{
ESP_LOGE(TAG, "Timeout! Device at address 0x%02X is not responding.", DISPLAY_I2C_ADDRESS);
}
else
{
ESP_LOGE(TAG, "Error communicating with address 0x%02X: %s", DISPLAY_I2C_ADDRESS, esp_err_to_name(err));
}
// 3. Delete I2C master bus if it is no longer needed
esp_err_t del_err = i2c_del_master_bus(i2c_bus);
if (del_err != ESP_OK)
{
ESP_LOGW(TAG, "Failed to delete I2C master bus: %s", esp_err_to_name(del_err));
}
ESP_DIAG_EVENT(TAG, "I2C master bus deleted.");
return err;
}

View File

@@ -45,3 +45,8 @@ CONFIG_SPIRAM_USE_CAPS_ALLOC=y
# HTTP Server WebSocket Support
CONFIG_HTTPD_WS_SUPPORT=y
# MQTT
CONFIG_MQTT_CLIENT_BROKER_URL="mqtts://mqtt.mars3142.dev:8883"
CONFIG_MQTT_CLIENT_USERNAME="mars3142"
CONFIG_MQTT_CLIENT_PASSWORD="KPkEyzs9aur3Y7LfEybnd8PsxWd94ouQZGNGJ24y"

View File

@@ -1,2 +1,20 @@
# default ESP target
CONFIG_IDF_TARGET="esp32c6"
#
# Display Settings
#
CONFIG_DISPLAY_SDA_PIN=9
CONFIG_DISPLAY_SCL_PIN=8
# end of Display Settings
#
# Button Configuration
#
CONFIG_BUTTON_UP=7
CONFIG_BUTTON_DOWN=4
CONFIG_BUTTON_LEFT=6
CONFIG_BUTTON_RIGHT=5
CONFIG_BUTTON_SELECT=19
CONFIG_BUTTON_BACK=20
# end of Button Configuration

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dev" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,8 @@
{
"hash": "15486339",
"configHash": "5ec1f82b",
"lockfileHash": "2bc40369",
"browserHash": "9efd6930",
"optimized": {},
"chunks": {}
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#1a1a2e">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<title>System Control</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

1560
firmware/website.bak/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"name": "learn-svelte",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.1.1",
"svelte": "^5.38.1",
"vite": "^7.1.2",
"vite-plugin-compression": "^0.5.1"
},
"dependencies": {
"@fontsource/atkinson-hyperlegible": "^5.2.6",
"@picocss/pico": "^2.1.1",
"gsap": "^3.13.0"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🚂</text></svg>

After

Width:  |  Height:  |  Size: 109 B

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import Header from "./compoents/Header.svelte";
import Index from "./Index.svelte";
import Captive from "./Captive.svelte";
import { onMount } from "svelte";
import { writable } from "svelte/store";
const isCaptive = writable(false);
function checkHash() {
isCaptive.set(window.location.hash === "#/captive");
}
onMount(() => {
checkHash();
window.addEventListener("hashchange", checkHash);
return () => window.removeEventListener("hashchange", checkHash);
});
</script>
<Header />
{#if $isCaptive}
<Captive />
{:else}
<Index />
{/if}

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import { t } from "./i18n/store";
</script>
<h1>{$t("welcome")} - Captive Portal</h1>

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import { t } from "./i18n/store";
</script>
<h1>{$t("welcome")}</h1>

View File

@@ -0,0 +1,70 @@
:root {
--bg-color: #1a1a2e;
--card-bg: #16213e;
--accent: #0f3460;
--text: #eaeaea;
--text-muted: #a0a0a0;
--success: #00d26a;
--error: #ff6b6b;
--border: #2a2a4a;
--input-bg: #1a1a2e;
--shadow: rgba(0, 0, 0, 0.3);
--primary: #c41e3a;
}
[data-theme="light"] {
--bg-color: #faf8f5;
--card-bg: #ffffff;
--accent: #fef2f2;
--text: #1a1a2e;
--text-muted: #6b7280;
--success: #c41e3a;
--error: #dc2626;
--border: #e5d9d0;
--input-bg: #ffffff;
--shadow: rgba(196, 30, 58, 0.1);
--primary: #c41e3a;
}
[data-theme="light"] .header h1 {
color: var(--primary);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-color);
color: var(--text);
min-height: 100vh;
transition: background 0.3s, color 0.3s;
padding: 12px;
}
h1 {
font-size: 1.5rem;
color: var(--text);
}
#app {
max-width: 900px;
margin: 0 auto;
}
@media (max-width: 600px) {
body {
padding: 6px;
}
}
@supports (padding: max(0px)) {
body {
padding-left: max(12px, env(safe-area-inset-left));
padding-right: max(12px, env(safe-area-inset-right));
padding-bottom: max(12px, env(safe-area-inset-bottom));
}
}

View File

@@ -0,0 +1,133 @@
<script lang="ts">
import Toggle from "./Toggle.svelte";
import {toggleTheme} from "../theme";
import {onMount} from "svelte";
import {writable} from "svelte/store";
import {lang, t} from "../i18n/store";
const theme = writable<"dark" | "light">("dark");
function applyInitialTheme() {
const userTheme = localStorage.getItem("theme");
if (userTheme) {
document.documentElement.setAttribute("data-theme", userTheme);
} else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
document.documentElement.setAttribute("data-theme", "light");
} else {
document.documentElement.setAttribute("data-theme", "dark");
}
}
function updateThemeFromDom() {
const t = document.documentElement.getAttribute("data-theme");
theme.set(t === "light" ? "light" : "dark");
}
function handleThemeToggle() {
toggleTheme();
updateThemeFromDom();
}
let themeIcon = $state("🌙");
let themeLabel = $state("Dark");
let currentLangCode = $state($lang);
let currentLang = $state("Deutsch");
let currentFlag = $state("🇩🇪");
$effect(() => {
theme.subscribe(($theme) => {
themeIcon = $theme === "light" ? "☀️" : "🌙";
themeLabel = $theme === "light" ? "Light" : "Dark";
});
lang.subscribe(($lang) => {
currentLangCode = $lang;
currentLang = $lang === "de" ? "Deutsch" : "English";
currentFlag = $lang === "de" ? "🇩🇪" : "🇬🇧";
});
});
function handleLangChange(newLang: "de" | "en") {
lang.set(newLang);
localStorage.setItem("lang", newLang);
}
onMount(() => {
applyInitialTheme();
updateThemeFromDom();
window.addEventListener("storage", updateThemeFromDom);
// Listener für OS-Theme-Änderung
const mql = window.matchMedia("(prefers-color-scheme: light)");
const osThemeListener = () => {
// Nur reagieren, wenn kein User-Theme gesetzt ist
if (!localStorage.getItem("theme")) {
applyInitialTheme();
updateThemeFromDom();
}
};
mql.addEventListener("change", osThemeListener);
return () => {
window.removeEventListener("storage", updateThemeFromDom);
mql.removeEventListener("change", osThemeListener);
};
});
</script>
<div class="header">
<div class="header-controls">
<Toggle
label={currentLang}
icon={currentFlag}
ariaLabel="Sprache wechseln"
onClick={() => {
const newLang = currentLangCode === "de" ? "en" : "de";
handleLangChange(newLang);
}}
/>
<Toggle
label={themeLabel}
icon={themeIcon}
ariaLabel="Theme wechseln"
onClick={handleThemeToggle}
/>
</div>
<h1>🚂 System Control</h1>
</div>
<style>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.header-controls {
display: flex;
align-items: center;
gap: 8px;
}
@media (max-width: 600px) {
.header {
flex-direction: column;
align-items: stretch;
text-align: center;
}
.header h1 {
order: 1;
}
}
@media (max-width: 380px) {
.header h1 {
font-size: 1.2rem;
}
}
</style>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
const { label, icon, ariaLabel, onClick } = $props<{
label: string;
icon: string;
ariaLabel: string;
onClick?: () => void;
}>();
</script>
<button class="toggle" aria-label={ariaLabel} onclick={onClick}>
<span class="icon" id="icon">{icon}</span>
<span class="label" id="label">{label}</span>
</button>
<style>
.toggle {
display: flex;
align-items: center;
gap: 6px;
background: var(--card-bg);
padding: 8px 12px;
border-radius: 20px;
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
font-size: 0.85rem;
color: var(--text-muted);
}
.toggle:hover {
border-color: var(--success);
}
.icon {
font-size: 1.2rem;
}
</style>

View File

@@ -0,0 +1,6 @@
{
"hello": "Hallo Welt",
"welcome": "Willkommen",
"language": "Sprache",
"save": "Speichern"
}

View File

@@ -0,0 +1,6 @@
{
"hello": "Hello World",
"welcome": "Welcome",
"language": "Language",
"save": "Save"
}

View File

@@ -0,0 +1,12 @@
import de from './de.json';
import en from './en.json';
export const translations = { de, en };
export type Lang = keyof typeof translations;
export function getInitialLang(): Lang {
const navLang = navigator.language.slice(0, 2);
if (navLang in translations) return navLang as Lang;
return 'en';
}

View File

@@ -0,0 +1,16 @@
import { writable, derived } from 'svelte/store';
import { translations, getInitialLang, type Lang } from './index';
function getLang(): Lang {
const stored = localStorage.getItem('lang');
if (stored && stored in translations) return stored as Lang;
return getInitialLang();
}
export const lang = writable<Lang>(getLang());
export const t = derived(lang, $lang => {
return (key: string) => {
return translations[$lang][key] || key;
};
});

View File

@@ -0,0 +1,9 @@
import { mount } from 'svelte'
import App from './App.svelte'
import './app.css'
const app = mount(App, {
target: document.getElementById('app'),
})
export default app

View File

@@ -0,0 +1,18 @@
// src/theme.ts
export function setTheme(theme: 'light' | 'dark') {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
const metaTheme = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement | null;
if (icon) icon.textContent = theme === 'light' ? '☀️' : '🌙';
if (label) label.textContent = theme === 'light' ? 'Light' : 'Dark';
if (metaTheme) metaTheme.content = theme === 'light' ? '#f0f2f5' : '#1a1a2e';
}
export function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') as 'light' | 'dark' | null || 'dark';
setTheme(current === 'dark' ? 'light' : 'dark');
}

View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -0,0 +1,17 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
compilerOptions: {
runes: true,
},
vitePlugin: {
inspector: {
showToggleButton: 'always',
toggleButtonPos: 'bottom-right'
}
}
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import viteCompression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
svelte(),
viteCompression()
],
build: {
outDir: '../storage/website',
assetsDir: '',
},
})

29
firmware/website/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Paraglide
src/lib/paraglide
project.inlang/cache/
*storybook.log
storybook-static

1
firmware/website/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/

View File

@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/routes/layout.css"
}

View File

@@ -0,0 +1,17 @@
import type { StorybookConfig } from '@storybook/sveltekit';
const config: StorybookConfig = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|ts|svelte)"
],
"addons": [
"@storybook/addon-svelte-csf",
"@chromatic-com/storybook",
"@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs"
],
"framework": "@storybook/sveltekit"
};
export default config;

View File

@@ -0,0 +1,21 @@
import type { Preview } from '@storybook/sveltekit'
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo'
}
},
};
export default preview;

View File

@@ -0,0 +1,7 @@
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
import { setProjectAnnotations } from '@storybook/sveltekit';
import * as projectAnnotations from './preview';
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);

View File

@@ -0,0 +1,5 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
}

View File

@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv create --template minimal --types ts --add prettier eslint vitest="usages:unit,component" tailwindcss="plugins:typography" storybook paraglide="languageTags:en, de+demo:yes" --install npm website
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

View File

@@ -0,0 +1,42 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from 'eslint-plugin-storybook';
import prettier from 'eslint-config-prettier';
import path from 'node:path';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from de!"
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from en!"
}

6278
firmware/website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
{
"name": "website",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@inlang/paraglide-js": "^2.10.0",
"@storybook/addon-a11y": "^10.2.8",
"@storybook/addon-docs": "^10.2.8",
"@storybook/addon-svelte-csf": "^5.0.11",
"@storybook/addon-vitest": "^10.2.8",
"@storybook/sveltekit": "^10.2.8",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^24",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-storybook": "^10.2.8",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^17.3.0",
"playwright": "^1.58.1",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"storybook": "^10.2.8",
"svelte": "^5.49.2",
"svelte-check": "^4.3.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vite": "^7.3.1",
"vitest": "^4.0.18",
"vitest-browser-svelte": "^2.0.2"
}
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"baseLocale": "en",
"locales": ["en", "de"]
}

13
firmware/website/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="%paraglide.lang%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View File

@@ -0,0 +1,13 @@
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
const handleParaglide: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request, locale }) => {
event.request = request;
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
});
});
export const handle: Handle = handleParaglide;

View File

@@ -0,0 +1,3 @@
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute = (request) => deLocalizeUrl(request.url).pathname;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { page } from '$app/state';
import { locales, localizeHref } from '$lib/paraglide/runtime';
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
{@render children()}
<div style="display:none">
{#each locales as locale}
<a href={localizeHref(page.url.pathname, { locale })}>
{locale}
</a>
{/each}
</div>

View File

@@ -0,0 +1,2 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
</script>
<a href={resolve('/demo/paraglide')}>paraglide</a>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import { setLocale } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages.js';
</script>
<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1>
<div>
<button onclick={() => setLocale('en')}>en</button>
<button onclick={() => setLocale('de')}>de</button>
</div>
<p>
If you use VSCode, install the
<a
href="https://marketplace.visualstudio.com/items?itemName=inlang.vs-code-extension"
target="_blank"
>
Sherlock i18n extension
</a>
for a better i18n experience.
</p>

View File

@@ -0,0 +1,2 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';

View File

@@ -0,0 +1,13 @@
import { page } from 'vitest/browser';
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import Page from './+page.svelte';
describe('/+page.svelte', () => {
it('should render h1', async () => {
render(Page);
const heading = page.getByRole('heading', { level: 1 });
await expect.element(heading).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,31 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './Button.svelte';
import { fn } from 'storybook/test';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
backgroundColor: { control: 'color' },
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large'],
},
},
args: {
onclick: fn(),
}
});
</script>
<!-- More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -->
<Story name="Primary" args={{ primary: true, label: 'Button' }} />
<Story name="Secondary" args={{ label: 'Button' }} />
<Story name="Large" args={{ size: 'large', label: 'Button' }} />
<Story name="Small" args={{ size: 'small', label: 'Button' }} />

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import './button.css';
interface Props {
/** Is this the principal call to action on the page? */
primary?: boolean;
/** What background color to use */
backgroundColor?: string;
/** How large should the button be? */
size?: 'small' | 'medium' | 'large';
/** Button contents */
label: string;
/** The onclick event handler */
onclick?: () => void;
}
const { primary = false, backgroundColor, size = 'medium', label, ...props }: Props = $props();
let mode = $derived(primary ? 'storybook-button--primary' : 'storybook-button--secondary');
let style = $derived(backgroundColor ? `background-color: ${backgroundColor}` : '');
</script>
<button
type="button"
class={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
{style}
{...props}
>
{label}
</button>

View File

@@ -0,0 +1,364 @@
import { Meta } from "@storybook/addon-docs/blocks";
import Github from "./assets/github.svg";
import Discord from "./assets/discord.svg";
import Youtube from "./assets/youtube.svg";
import Tutorials from "./assets/tutorials.svg";
import Styling from "./assets/styling.png";
import Context from "./assets/context.png";
import Assets from "./assets/assets.png";
import Docs from "./assets/docs.png";
import Share from "./assets/share.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Testing from "./assets/testing.png";
import Accessibility from "./assets/accessibility.png";
import Theming from "./assets/theming.png";
import AddonLibrary from "./assets/addon-library.png";
export const RightArrow = () => <svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: '4px',
display: 'inline-block',
shapeRendering: 'inherit',
verticalAlign: 'middle',
fill: 'currentColor',
'path fill': 'currentColor'
}}
>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
<Meta title="Configure your project" />
<div className="sb-container">
<div className='sb-section-title'>
# Configure your project
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
</div>
<div className="sb-section">
<div className="sb-section-item">
<img
src={Styling}
alt="A wall of logos representing different styling technologies"
/>
<h4 className="sb-section-item-heading">Add styling and CSS</h4>
<p className="sb-section-item-paragraph">Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.</p>
<a
href="https://storybook.js.org/docs/configure/styling-and-css/?renderer=svelte&ref=configure"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-section-item">
<img
src={Context}
alt="An abstraction representing the composition of data for a component"
/>
<h4 className="sb-section-item-heading">Provide context and mocking</h4>
<p className="sb-section-item-paragraph">Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.</p>
<a
href="https://storybook.js.org/docs/writing-stories/decorators/?renderer=svelte&ref=configure#context-for-mocking"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-section-item">
<img src={Assets} alt="A representation of typography and image assets" />
<div>
<h4 className="sb-section-item-heading">Load assets and resources</h4>
<p className="sb-section-item-paragraph">To link static files (like fonts) to your projects and stories, use the
`staticDirs` configuration option to specify folders to load when
starting Storybook.</p>
<a
href="https://storybook.js.org/docs/configure/images-and-assets/?renderer=svelte&ref=configure"
target="_blank"
>Learn more<RightArrow /></a>
</div>
</div>
</div>
</div>
<div className="sb-container">
<div className='sb-section-title'>
# Do more with Storybook
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
</div>
<div className="sb-section">
<div className="sb-features-grid">
<div className="sb-grid-item">
<img src={Docs} alt="A screenshot showing the autodocs tag being set, pointing a docs page being generated" />
<h4 className="sb-section-item-heading">Autodocs</h4>
<p className="sb-section-item-paragraph">Auto-generate living,
interactive reference documentation from your components and stories.</p>
<a
href="https://storybook.js.org/docs/writing-docs/autodocs/?renderer=svelte&ref=configure"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<img src={Share} alt="A browser window showing a Storybook being published to a chromatic.com URL" />
<h4 className="sb-section-item-heading">Publish to Chromatic</h4>
<p className="sb-section-item-paragraph">Publish your Storybook to review and collaborate with your entire team.</p>
<a
href="https://storybook.js.org/docs/sharing/publish-storybook/?renderer=svelte&ref=configure#publish-storybook-with-chromatic"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<img src={FigmaPlugin} alt="Windows showing the Storybook plugin in Figma" />
<h4 className="sb-section-item-heading">Figma Plugin</h4>
<p className="sb-section-item-paragraph">Embed your stories into Figma to cross-reference the design and live
implementation in one place.</p>
<a
href="https://storybook.js.org/docs/sharing/design-integrations/?renderer=svelte&ref=configure#embed-storybook-in-figma-with-the-plugin"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<img src={Testing} alt="Screenshot of tests passing and failing" />
<h4 className="sb-section-item-heading">Testing</h4>
<p className="sb-section-item-paragraph">Use stories to test a component in all its variations, no matter how
complex.</p>
<a
href="https://storybook.js.org/docs/writing-tests/?renderer=svelte&ref=configure"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<img src={Accessibility} alt="Screenshot of accessibility tests passing and failing" />
<h4 className="sb-section-item-heading">Accessibility</h4>
<p className="sb-section-item-paragraph">Automatically test your components for a11y issues as you develop.</p>
<a
href="https://storybook.js.org/docs/writing-tests/accessibility-testing/?renderer=svelte&ref=configure"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<img src={Theming} alt="Screenshot of Storybook in light and dark mode" />
<h4 className="sb-section-item-heading">Theming</h4>
<p className="sb-section-item-paragraph">Theme Storybook's UI to personalize it to your project.</p>
<a
href="https://storybook.js.org/docs/configure/theming/?renderer=svelte&ref=configure"
target="_blank"
>Learn more<RightArrow /></a>
</div>
</div>
</div>
</div>
<div className='sb-addon'>
<div className='sb-addon-text'>
<h4>Addons</h4>
<p className="sb-section-item-paragraph">Integrate your tools with Storybook to connect workflows.</p>
<a
href="https://storybook.js.org/addons/?ref=configure"
target="_blank"
>Discover all addons<RightArrow /></a>
</div>
<div className='sb-addon-img'>
<img src={AddonLibrary} alt="Integrate your tools with Storybook to connect workflows." />
</div>
</div>
<div className="sb-section sb-socials">
<div className="sb-section-item">
<img src={Github} alt="Github logo" className="sb-explore-image"/>
Join our contributors building the future of UI development.
<a
href="https://github.com/storybookjs/storybook"
target="_blank"
>Star on GitHub<RightArrow /></a>
</div>
<div className="sb-section-item">
<img src={Discord} alt="Discord logo" className="sb-explore-image"/>
<div>
Get support and chat with frontend developers.
<a
href="https://discord.gg/storybook"
target="_blank"
>Join Discord server<RightArrow /></a>
</div>
</div>
<div className="sb-section-item">
<img src={Youtube} alt="Youtube logo" className="sb-explore-image"/>
<div>
Watch tutorials, feature previews and interviews.
<a
href="https://www.youtube.com/@chromaticui"
target="_blank"
>Watch on YouTube<RightArrow /></a>
</div>
</div>
<div className="sb-section-item">
<img src={Tutorials} alt="A book" className="sb-explore-image"/>
<p>Follow guided walkthroughs on for key workflows.</p>
<a
href="https://storybook.js.org/tutorials/?ref=configure"
target="_blank"
>Discover tutorials<RightArrow /></a>
</div>
</div>
<style>
{`
.sb-container {
margin-bottom: 48px;
}
.sb-section {
width: 100%;
display: flex;
flex-direction: row;
gap: 20px;
}
img {
object-fit: cover;
}
.sb-section-title {
margin-bottom: 32px;
}
.sb-section a:not(h1 a, h2 a, h3 a) {
font-size: 14px;
}
.sb-section-item, .sb-grid-item {
flex: 1;
display: flex;
flex-direction: column;
}
.sb-section-item-heading {
padding-top: 20px !important;
padding-bottom: 5px !important;
margin: 0 !important;
}
.sb-section-item-paragraph {
margin: 0;
padding-bottom: 10px;
}
.sb-chevron {
margin-left: 5px;
}
.sb-features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 32px 20px;
}
.sb-socials {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.sb-socials p {
margin-bottom: 10px;
}
.sb-explore-image {
max-height: 32px;
align-self: flex-start;
}
.sb-addon {
width: 100%;
display: flex;
align-items: center;
position: relative;
background-color: #EEF3F8;
border-radius: 5px;
border: 1px solid rgba(0, 0, 0, 0.05);
background: #EEF3F8;
height: 180px;
margin-bottom: 48px;
overflow: hidden;
}
.sb-addon-text {
padding-left: 48px;
max-width: 240px;
}
.sb-addon-text h4 {
padding-top: 0px;
}
.sb-addon-img {
position: absolute;
left: 345px;
top: 0;
height: 100%;
width: 200%;
overflow: hidden;
}
.sb-addon-img img {
width: 650px;
transform: rotate(-15deg);
margin-left: 40px;
margin-top: -72px;
box-shadow: 0 0 1px rgba(255, 255, 255, 0);
backface-visibility: hidden;
}
@media screen and (max-width: 800px) {
.sb-addon-img {
left: 300px;
}
}
@media screen and (max-width: 600px) {
.sb-section {
flex-direction: column;
}
.sb-features-grid {
grid-template-columns: repeat(1, 1fr);
}
.sb-socials {
grid-template-columns: repeat(2, 1fr);
}
.sb-addon {
height: 280px;
align-items: flex-start;
padding-top: 32px;
overflow: hidden;
}
.sb-addon-text {
padding-left: 24px;
}
.sb-addon-img {
right: 0;
left: 0;
top: 130px;
bottom: 0;
overflow: hidden;
height: auto;
width: 124%;
}
.sb-addon-img img {
width: 1200px;
transform: rotate(-12deg);
margin-left: 0;
margin-top: 48px;
margin-bottom: -40px;
margin-left: -24px;
}
}
`}
</style>

View File

@@ -0,0 +1,26 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Header from './Header.svelte';
import { fn } from 'storybook/test';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Header',
component: Header,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
args: {
onLogin: fn(),
onLogout: fn(),
onCreateAccount: fn(),
}
});
</script>
<Story name="Logged In" args={{ user: { name: 'Jane Doe' } }} />
<Story name="Logged Out" />

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import './header.css';
import Button from './Button.svelte';
interface Props {
user?: { name: string };
onLogin?: () => void;
onLogout?: () => void;
onCreateAccount?: () => void;
}
const { user, onLogin, onLogout, onCreateAccount }: Props = $props();
</script>
<header>
<div class="storybook-header">
<div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
fill="#FFF"
/>
<path
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
fill="#555AB9"
/>
<path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
{#if user}
<span class="welcome">
Welcome, <b>{user.name}</b>!
</span>
<Button size="small" onclick={onLogout} label="Log out" />
{:else}
<Button size="small" onclick={onLogin} label="Log in" />
<Button primary size="small" onclick={onCreateAccount} label="Sign up" />
{/if}
</div>
</div>
</header>

View File

@@ -0,0 +1,30 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import Page from './Page.svelte';
import { fn } from 'storybook/test';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Page',
component: Page,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
});
</script>
<Story name="Logged In" play={async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = canvas.getByRole('button', { name: /Log in/i });
await expect(loginButton).toBeInTheDocument();
await userEvent.click(loginButton);
await waitFor(() => expect(loginButton).not.toBeInTheDocument());
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
await expect(logoutButton).toBeInTheDocument();
}}
/>
<Story name="Logged Out" />

View File

@@ -0,0 +1,70 @@
<script lang="ts">
import './page.css';
import Header from './Header.svelte';
let user = $state<{ name: string }>();
</script>
<article>
<Header
{user}
onLogin={() => (user = { name: 'Jane Doe' })}
onLogout={() => (user = undefined)}
onCreateAccount={() => (user = { name: 'Jane Doe' })}
/>
<section class="storybook-page">
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a
<a
href="https://blog.hichroma.com/component-driven-development-ce1109d56c8e"
target="_blank"
rel="noopener noreferrer"
>
<strong>component-driven</strong>
</a>
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states without
needing to navigate to them in your app. Here are some handy patterns for managing page data
in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data from the
"args" of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock these services out
using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
Storybook tutorials
</a>
. Read more in the
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">docs</a>
.
</p>
<div class="tip-wrapper">
<span class="tip">Tip</span>
Adjust the width of the canvas with the
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0
01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0
010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
id="a"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
</article>

Some files were not shown because too many files have changed in this diff Show More