Compare commits
11 Commits
501c2de874
...
feature/sv
| Author | SHA1 | Date | |
|---|---|---|---|
|
99678087cb
|
|||
|
fe4bd11a21
|
|||
|
684ce36270
|
|||
|
98b5df1ff2
|
|||
|
8128b958cb
|
|||
|
955b4bef04
|
|||
|
81141d8859
|
|||
|
e01006cd49
|
|||
|
c28d7d08df
|
|||
|
df50aaedda
|
|||
|
1f02d35a97
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,3 +24,5 @@
|
||||
**/*_front.png
|
||||
**/*_schematic*.png
|
||||
**/wiki/*
|
||||
*.FCBak
|
||||
firmware/**/node_modules
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
21
firmware/bootloader_components/my_boot_hooks/hooks.c
Normal file
21
firmware/bootloader_components/my_boot_hooks/hooks.c
Normal 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");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
idf_component_register(SRCS "extra_component.c")
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
16
firmware/components/led-manager/include/led_segment.h
Normal file
16
firmware/components/led-manager/include/led_segment.h
Normal 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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,4 +3,6 @@ idf_component_register(
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES
|
||||
persistence-manager
|
||||
my_mqtt_client
|
||||
app_update
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
firmware/components/my_mqtt_client/CMakeLists.txt
Normal file
7
firmware/components/my_mqtt_client/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
idf_component_register(
|
||||
SRCS "src/my_mqtt_client.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES
|
||||
mqtt
|
||||
app_update
|
||||
)
|
||||
21
firmware/components/my_mqtt_client/Kconfig
Normal file
21
firmware/components/my_mqtt_client/Kconfig
Normal 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
|
||||
18
firmware/components/my_mqtt_client/README.md
Normal file
18
firmware/components/my_mqtt_client/README.md
Normal 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`.
|
||||
2
firmware/components/my_mqtt_client/idf_component.yml
Normal file
2
firmware/components/my_mqtt_client/idf_component.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
espressif/mqtt: ^1.0.0
|
||||
14
firmware/components/my_mqtt_client/include/my_mqtt_client.h
Normal file
14
firmware/components/my_mqtt_client/include/my_mqtt_client.h
Normal 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
|
||||
122
firmware/components/my_mqtt_client/src/my_mqtt_client.c
Normal file
122
firmware/components/my_mqtt_client/src/my_mqtt_client.c
Normal 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!");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
13
firmware/debug-storybook.log
Normal file
13
firmware/debug-storybook.log
Normal file
@@ -0,0 +1,13 @@
|
||||
[17:12:14.017] [INFO] [36mInitializing Storybook[39m
|
||||
[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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
10
firmware/main/include/common.h
Normal file
10
firmware/main/include/common.h
Normal 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)
|
||||
@@ -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
|
||||
31
firmware/main/isrgrootx1.pem
Normal file
31
firmware/main/isrgrootx1.pem
Normal 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-----
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
63
firmware/main/src/i2c_checker.c
Normal file
63
firmware/main/src/i2c_checker.c
Normal 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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
1574
firmware/storage/www/css/style.css
Normal file
1574
firmware/storage/www/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
12
firmware/website.bak/.run/dev.run.xml
Normal file
12
firmware/website.bak/.run/dev.run.xml
Normal 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>
|
||||
8
firmware/website.bak/.vite/deps/_metadata.json
Normal file
8
firmware/website.bak/.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"hash": "15486339",
|
||||
"configHash": "5ec1f82b",
|
||||
"lockfileHash": "2bc40369",
|
||||
"browserHash": "9efd6930",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
3
firmware/website.bak/.vite/deps/package.json
Normal file
3
firmware/website.bak/.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
19
firmware/website.bak/index.html
Normal file
19
firmware/website.bak/index.html
Normal 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>
|
||||
32
firmware/website.bak/jsconfig.json
Normal file
32
firmware/website.bak/jsconfig.json
Normal 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
1560
firmware/website.bak/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
firmware/website.bak/package.json
Normal file
20
firmware/website.bak/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
firmware/website.bak/public/favicon.svg
Normal file
1
firmware/website.bak/public/favicon.svg
Normal 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 |
27
firmware/website.bak/src/App.svelte
Normal file
27
firmware/website.bak/src/App.svelte
Normal 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}
|
||||
5
firmware/website.bak/src/Captive.svelte
Normal file
5
firmware/website.bak/src/Captive.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { t } from "./i18n/store";
|
||||
</script>
|
||||
|
||||
<h1>{$t("welcome")} - Captive Portal</h1>
|
||||
5
firmware/website.bak/src/Index.svelte
Normal file
5
firmware/website.bak/src/Index.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { t } from "./i18n/store";
|
||||
</script>
|
||||
|
||||
<h1>{$t("welcome")}</h1>
|
||||
70
firmware/website.bak/src/app.css
Normal file
70
firmware/website.bak/src/app.css
Normal 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));
|
||||
}
|
||||
}
|
||||
133
firmware/website.bak/src/compoents/Header.svelte
Normal file
133
firmware/website.bak/src/compoents/Header.svelte
Normal 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>
|
||||
37
firmware/website.bak/src/compoents/Toggle.svelte
Normal file
37
firmware/website.bak/src/compoents/Toggle.svelte
Normal 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>
|
||||
6
firmware/website.bak/src/i18n/de.json
Normal file
6
firmware/website.bak/src/i18n/de.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"hello": "Hallo Welt",
|
||||
"welcome": "Willkommen",
|
||||
"language": "Sprache",
|
||||
"save": "Speichern"
|
||||
}
|
||||
6
firmware/website.bak/src/i18n/en.json
Normal file
6
firmware/website.bak/src/i18n/en.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"hello": "Hello World",
|
||||
"welcome": "Welcome",
|
||||
"language": "Language",
|
||||
"save": "Save"
|
||||
}
|
||||
12
firmware/website.bak/src/i18n/index.ts
Normal file
12
firmware/website.bak/src/i18n/index.ts
Normal 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';
|
||||
}
|
||||
16
firmware/website.bak/src/i18n/store.ts
Normal file
16
firmware/website.bak/src/i18n/store.ts
Normal 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;
|
||||
};
|
||||
});
|
||||
9
firmware/website.bak/src/main.js
Normal file
9
firmware/website.bak/src/main.js
Normal 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
|
||||
18
firmware/website.bak/src/theme.ts
Normal file
18
firmware/website.bak/src/theme.ts
Normal 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');
|
||||
}
|
||||
2
firmware/website.bak/src/vite-env.d.ts
vendored
Normal file
2
firmware/website.bak/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
17
firmware/website.bak/svelte.config.js
Normal file
17
firmware/website.bak/svelte.config.js
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
14
firmware/website.bak/vite.config.js
Normal file
14
firmware/website.bak/vite.config.js
Normal 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
29
firmware/website/.gitignore
vendored
Normal 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
1
firmware/website/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
engine-strict=true
|
||||
9
firmware/website/.prettierignore
Normal file
9
firmware/website/.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lock
|
||||
bun.lockb
|
||||
|
||||
# Miscellaneous
|
||||
/static/
|
||||
16
firmware/website/.prettierrc
Normal file
16
firmware/website/.prettierrc
Normal 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"
|
||||
}
|
||||
17
firmware/website/.storybook/main.ts
Normal file
17
firmware/website/.storybook/main.ts
Normal 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;
|
||||
21
firmware/website/.storybook/preview.ts
Normal file
21
firmware/website/.storybook/preview.ts
Normal 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;
|
||||
7
firmware/website/.storybook/vitest.setup.ts
Normal file
7
firmware/website/.storybook/vitest.setup.ts
Normal 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]);
|
||||
5
firmware/website/.vscode/settings.json
vendored
Normal file
5
firmware/website/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
42
firmware/website/README.md
Normal file
42
firmware/website/README.md
Normal 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.
|
||||
42
firmware/website/eslint.config.js
Normal file
42
firmware/website/eslint.config.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
4
firmware/website/messages/de.json
Normal file
4
firmware/website/messages/de.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from de!"
|
||||
}
|
||||
4
firmware/website/messages/en.json
Normal file
4
firmware/website/messages/en.json
Normal 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
6278
firmware/website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
firmware/website/package.json
Normal file
57
firmware/website/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
12
firmware/website/project.inlang/settings.json
Normal file
12
firmware/website/project.inlang/settings.json
Normal 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
13
firmware/website/src/app.d.ts
vendored
Normal 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 {};
|
||||
15
firmware/website/src/app.html
Normal file
15
firmware/website/src/app.html
Normal 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>
|
||||
7
firmware/website/src/demo.spec.ts
Normal file
7
firmware/website/src/demo.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
13
firmware/website/src/hooks.server.ts
Normal file
13
firmware/website/src/hooks.server.ts
Normal 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;
|
||||
3
firmware/website/src/hooks.ts
Normal file
3
firmware/website/src/hooks.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { deLocalizeUrl } from '$lib/paraglide/runtime';
|
||||
|
||||
export const reroute = (request) => deLocalizeUrl(request.url).pathname;
|
||||
1
firmware/website/src/lib/assets/favicon.svg
Normal file
1
firmware/website/src/lib/assets/favicon.svg
Normal 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 |
1
firmware/website/src/lib/index.ts
Normal file
1
firmware/website/src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
19
firmware/website/src/routes/+layout.svelte
Normal file
19
firmware/website/src/routes/+layout.svelte
Normal 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>
|
||||
2
firmware/website/src/routes/+page.svelte
Normal file
2
firmware/website/src/routes/+page.svelte
Normal 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>
|
||||
5
firmware/website/src/routes/demo/+page.svelte
Normal file
5
firmware/website/src/routes/demo/+page.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
</script>
|
||||
|
||||
<a href={resolve('/demo/paraglide')}>paraglide</a>
|
||||
22
firmware/website/src/routes/demo/paraglide/+page.svelte
Normal file
22
firmware/website/src/routes/demo/paraglide/+page.svelte
Normal 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>
|
||||
2
firmware/website/src/routes/layout.css
Normal file
2
firmware/website/src/routes/layout.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/typography';
|
||||
13
firmware/website/src/routes/page.svelte.spec.ts
Normal file
13
firmware/website/src/routes/page.svelte.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
31
firmware/website/src/stories/Button.stories.svelte
Normal file
31
firmware/website/src/stories/Button.stories.svelte
Normal 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' }} />
|
||||
30
firmware/website/src/stories/Button.svelte
Normal file
30
firmware/website/src/stories/Button.svelte
Normal 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>
|
||||
364
firmware/website/src/stories/Configure.mdx
Normal file
364
firmware/website/src/stories/Configure.mdx
Normal 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>
|
||||
26
firmware/website/src/stories/Header.stories.svelte
Normal file
26
firmware/website/src/stories/Header.stories.svelte
Normal 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" />
|
||||
45
firmware/website/src/stories/Header.svelte
Normal file
45
firmware/website/src/stories/Header.svelte
Normal 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>
|
||||
30
firmware/website/src/stories/Page.stories.svelte
Normal file
30
firmware/website/src/stories/Page.stories.svelte
Normal 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" />
|
||||
70
firmware/website/src/stories/Page.svelte
Normal file
70
firmware/website/src/stories/Page.svelte
Normal 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
Reference in New Issue
Block a user