Merge 47a0b1bf56 into 89a3f3e742
9
.clang-format
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
BasedOnStyle: LLVM
|
||||
|
||||
# Include sorting
|
||||
SortIncludes: true
|
||||
|
||||
# Spacing
|
||||
MaxEmptyLinesToKeep: 2
|
||||
SeparateDefinitionBlocks: Always
|
||||
9
.codespellignore
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Words that codespell should ignore
|
||||
# Add common false positives here
|
||||
inout
|
||||
uart
|
||||
dout
|
||||
din
|
||||
Tage
|
||||
alle
|
||||
Aktion
|
||||
29
.editorconfig
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
# Ensures consistent coding styles for multiple developers working on the same project across various editors and IDEs.
|
||||
|
||||
root = true
|
||||
|
||||
# All files
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Python files - 4 space indent
|
||||
[*.py]
|
||||
indent_size = 4
|
||||
|
||||
# Markdown files - preserve whitespace for formatting
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# Makefile - requires tabs
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Make files - requires tabs
|
||||
[*.make]
|
||||
indent_style = tab
|
||||
6
.envrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
if command -v nix &> /dev/null; then
|
||||
use flake
|
||||
else
|
||||
echo "Nix not found, skipping flake support"
|
||||
fi
|
||||
16
.github/actions/install-nix/action.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
name: install-nix
|
||||
description: Install Nix with flakes enabled and pre-warm the repository's flake devShell
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install Nix (with flakes)
|
||||
uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- name: Pre-warm flake devShell
|
||||
run: |
|
||||
# Use the repository's `flake.nix` devShell to fetch dependencies.
|
||||
# This speeds up later `nix develop` invocations in workflow steps.
|
||||
nix develop --command true
|
||||
shell: bash
|
||||
39
.github/workflows/check.yml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
name: check
|
||||
|
||||
"on":
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Nix and pre-warm flake devShell
|
||||
uses: ./.github/actions/install-nix
|
||||
|
||||
- name: Run pre-commit from flake devShell
|
||||
run: |
|
||||
# Use the flake devShell defined in ./flake.nix (x86_64 runner)
|
||||
nix develop --command pre-commit run --all-files
|
||||
shell: bash
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Nix and pre-warm flake devShell
|
||||
uses: ./.github/actions/install-nix
|
||||
|
||||
- name: Run build from flake devShell
|
||||
run: |
|
||||
# Use the flake devShell defined in ./flake.nix (x86_64 runner)
|
||||
nix develop --command invoke build
|
||||
shell: bash
|
||||
66
.gitignore
vendored
Normal file → Executable file
|
|
@ -1,5 +1,63 @@
|
|||
# Development
|
||||
# .gitignore für ESP-IDF / CMake-Projekt
|
||||
|
||||
.pio
|
||||
.vscode
|
||||
!.vscode\extensions.json
|
||||
# --- Build artefacts -----------------------------------------------------
|
||||
/build/
|
||||
*.elf
|
||||
*.bin
|
||||
*.uf2
|
||||
*.hex
|
||||
*.map
|
||||
*.img
|
||||
|
||||
# CMake / build system
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
compile_commands.json
|
||||
|
||||
# ESP-IDF specific
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
flasher_args.json
|
||||
flash_args*
|
||||
flash_app_args
|
||||
flash_bootloader_args
|
||||
flash_project_args
|
||||
bootloader-prefix/
|
||||
project_description.json
|
||||
managed_components/
|
||||
|
||||
# Component build directories
|
||||
components/**/build/
|
||||
partition_table/build/
|
||||
|
||||
# IDEs / editors / OS
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# direnv (local environment/profile cache)
|
||||
.direnv/
|
||||
|
||||
# Python / virtualenvs
|
||||
__pycache__/
|
||||
*.pyc
|
||||
venv/
|
||||
venv*/
|
||||
.env
|
||||
|
||||
# Logs / temp
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
# Documentation
|
||||
docs/doxygen/*
|
||||
|
||||
# Misc
|
||||
*.local
|
||||
|
||||
# Keep Snyk instructions file tracked (project policy)
|
||||
# .github/instructions/snyk_rules.instructions.md
|
||||
|
|
|
|||
9
.gitmodules
vendored
|
|
@ -1,6 +1,3 @@
|
|||
[submodule "lib/ArtNet"]
|
||||
path = lib/ArtNet
|
||||
url = https://github.com/psxde/ArtNet.git
|
||||
[submodule "lib/AsyncWebServer_ESP32_W5500"]
|
||||
path = lib/AsyncWebServer_ESP32_W5500
|
||||
url = https://github.com/psxde/AsyncWebServer_ESP32_W5500.git
|
||||
[submodule "docs/external/doxygen-awesome-css"]
|
||||
path = docs/external/doxygen-awesome-css
|
||||
url = https://github.com/jothepro/doxygen-awesome-css
|
||||
|
|
|
|||
32
.markdownlint.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"default": true,
|
||||
"MD013": {
|
||||
"line_length": 300,
|
||||
"tables": true,
|
||||
"code_blocks": true
|
||||
},
|
||||
"MD033": false,
|
||||
"MD012": {
|
||||
"maximum": 2
|
||||
},
|
||||
"MD026": {
|
||||
"punctuation": ".,;:!"
|
||||
},
|
||||
"MD024": {
|
||||
"siblings_only": false
|
||||
},
|
||||
"MD029": {
|
||||
"style": "ordered"
|
||||
},
|
||||
"MD046": {
|
||||
"style": "fenced"
|
||||
},
|
||||
"MD049": {
|
||||
"style": "underscore"
|
||||
},
|
||||
"MD050": {
|
||||
"style": "asterisk"
|
||||
},
|
||||
"MD052": false,
|
||||
"MD053": false
|
||||
}
|
||||
120
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# yaml-language-server: $schema=https://json.schemastore.org/pre-commit-config.json
|
||||
|
||||
# Global exclude pattern for build artifacts and dependencies
|
||||
exclude: |
|
||||
(?x)^(
|
||||
build/|
|
||||
managed_components/|
|
||||
.*\.bin|
|
||||
.*\.elf|
|
||||
.*\.hex|
|
||||
.*\.o|
|
||||
flake.lock|
|
||||
dependencies.lock|
|
||||
assets/case/|
|
||||
docs/doxygen/
|
||||
)
|
||||
|
||||
repos:
|
||||
# General file checks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
# Line ending checks
|
||||
- id: end-of-file-fixer
|
||||
- id: fix-byte-order-marker
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=lf]
|
||||
|
||||
# Whitespace & formatting
|
||||
- id: trailing-whitespace
|
||||
|
||||
# File format checks
|
||||
- id: check-added-large-files
|
||||
args: [--maxkb=1000]
|
||||
- id: check-ast
|
||||
- id: check-case-conflict
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: detect-private-key
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
|
||||
# Spell checking
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.2
|
||||
hooks:
|
||||
- id: codespell
|
||||
args: [--ignore-words=.codespellignore]
|
||||
|
||||
# YAML linting
|
||||
- repo: https://github.com/adrienverge/yamllint
|
||||
rev: v1.37.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
args:
|
||||
[
|
||||
--strict,
|
||||
-d,
|
||||
"{extends: default, rules: {line-length: {max: 120}, document-start: disable}}",
|
||||
]
|
||||
|
||||
# Markdown linting
|
||||
- repo: https://github.com/DavidAnson/markdownlint-cli2
|
||||
rev: v0.17.1
|
||||
hooks:
|
||||
- id: markdownlint-cli2
|
||||
|
||||
# Python formatting
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 26.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
args: [--quiet]
|
||||
|
||||
# JavaScript/JSON/CSS/HTML/YAML formatting and C/C++ formatting
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: prettier
|
||||
entry: prettier
|
||||
language: system
|
||||
types_or: [javascript, jsx, json, css, scss, html, yaml]
|
||||
exclude: \.md$
|
||||
args: [--write, --ignore-unknown]
|
||||
- id: clang-format
|
||||
name: clang-format
|
||||
entry: clang-format
|
||||
language: system
|
||||
types_or: [c, c++]
|
||||
args: [-i]
|
||||
- id: nixfmt
|
||||
name: nixfmt
|
||||
entry: nixfmt
|
||||
language: system
|
||||
types: [nix]
|
||||
|
||||
# CMake formatting and linting
|
||||
- repo: https://github.com/cheshirekow/cmake-format-precommit
|
||||
rev: v0.6.10
|
||||
hooks:
|
||||
- id: cmake-format
|
||||
- id: cmake-lint
|
||||
|
||||
# Doxygen code coverage
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: doxygen-coverage
|
||||
name: doxygen code coverage
|
||||
language: system
|
||||
entry: tools/doxy-coverage.py
|
||||
args: [docs/doxygen/xml, --threshold=100, --generate-docs]
|
||||
types_or: [c, c++, header]
|
||||
verbose: false
|
||||
require_serial: true
|
||||
16
.prettierignore
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Ignore build artifacts and generated files
|
||||
build/
|
||||
managed_components/
|
||||
docs/doxygen/
|
||||
docs/external/
|
||||
.direnv/
|
||||
*.lock
|
||||
|
||||
# Ignore sdkconfig (generated)
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
|
||||
# SVGs are formatted with svgo instead
|
||||
*.svg
|
||||
# Ignore dependencies
|
||||
dependencies.lock
|
||||
12
.prettierrc.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf",
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
10
.vscode/extensions.json
vendored
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
15
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Clear any stale EXTRA_COMPONENT_DIRS entries (e.g. leftover from previous runs
|
||||
# or PlatformIO)
|
||||
set(EXTRA_COMPONENT_DIRS "")
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
project(dmx-interface)
|
||||
|
||||
# Enable LittleFS filesystem image creation for web assets
|
||||
if(COMMAND littlefs_create_partition_image)
|
||||
littlefs_create_partition_image(storage ${CMAKE_SOURCE_DIR}/data
|
||||
FLASH_IN_PROJECT)
|
||||
endif()
|
||||
62
Doxyfile
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
PROJECT_NAME = "DMX-Interface"
|
||||
PROJECT_BRIEF = "ChaosDMX"
|
||||
OUTPUT_DIRECTORY = docs/doxygen
|
||||
|
||||
# Input settings
|
||||
|
||||
INPUT = main \
|
||||
components \
|
||||
data \
|
||||
README.md
|
||||
FILE_PATTERNS = *.c *.h *.cpp *.hpp *.md *.py *.js *.css *.html
|
||||
RECURSIVE = YES
|
||||
EXCLUDE_PATTERNS = */build/* \
|
||||
*/managed_components/*
|
||||
USE_MDFILE_AS_MAINPAGE = README.md
|
||||
|
||||
# Documentation settings
|
||||
GENERATE_LATEX = NO
|
||||
GENERATE_HTML = YES
|
||||
GENERATE_XML=YES
|
||||
|
||||
# doxygen-awesome-css settings
|
||||
HTML_EXTRA_STYLESHEET = docs/external/doxygen-awesome-css/doxygen-awesome.css \
|
||||
docs/external/doxygen-awesome-css/doxygen-awesome-sidebar-only.css
|
||||
|
||||
HTML_EXTRA_FILES = docs/external/doxygen-awesome-css/doxygen-awesome-darkmode-toggle.js \
|
||||
docs/external/doxygen-awesome-css/doxygen-awesome-fragment-copy-button.js \
|
||||
docs/external/doxygen-awesome-css/doxygen-awesome-paragraph-link.js \
|
||||
docs/external/doxygen-awesome-css/doxygen-awesome-interactive-toc.js \
|
||||
docs/external/doxygen-awesome-css/doxygen-awesome-tabs.js
|
||||
|
||||
# Custom header for JS integration
|
||||
|
||||
# Better HTML output
|
||||
HTML_COLORSTYLE = LIGHT
|
||||
GENERATE_TREEVIEW = YES
|
||||
DISABLE_INDEX = NO
|
||||
FULL_SIDEBAR = NO
|
||||
|
||||
# Extraction settings
|
||||
EXTRACT_ALL = YES
|
||||
EXTRACT_PRIVATE = YES
|
||||
EXTRACT_STATIC = YES
|
||||
EXTRACT_LOCAL_CLASSES = YES
|
||||
|
||||
# Graphviz / Dot settings
|
||||
HAVE_DOT = YES
|
||||
DOT_IMAGE_FORMAT = svg
|
||||
INTERACTIVE_SVG = YES
|
||||
DOT_FONTNAME = Helvetica
|
||||
DOT_FONTSIZE = 10
|
||||
|
||||
# Graphs to generate
|
||||
CALL_GRAPH = YES
|
||||
CALLER_GRAPH = YES
|
||||
GRAPHICAL_HIERARCHY = YES
|
||||
DIRECTORY_GRAPH = YES
|
||||
CLASS_GRAPH = YES
|
||||
COLLABORATION_GRAPH = YES
|
||||
GROUP_GRAPHS = YES
|
||||
INCLUDE_GRAPH = YES
|
||||
INCLUDED_BY_GRAPH = YES
|
||||
52
README.md
|
|
@ -65,6 +65,58 @@ You have to short-circuit `R0` on the RS485 boards to enable the termination res
|
|||
|
||||
---
|
||||
|
||||
## 🧑💻 Development
|
||||
|
||||
### Required tools
|
||||
|
||||
- `ESP-IDF` (includes `idf.py`)
|
||||
- `invoke` (for project tasks)
|
||||
|
||||
- Optional but recommended for development:
|
||||
- `pre-commit` (for code quality hooks)
|
||||
- `clang-format` (C/C++)
|
||||
- `prettier` (JavaScript/CSS/HTML/YAML)
|
||||
- `svgo` (SVG optimization)
|
||||
- `nixfmt` (Nix formatting)
|
||||
|
||||
### Environment setup
|
||||
|
||||
This repository includes a `flake.nix` with a ready-to-use development shell.
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
Alternatively, you can use `direnv` to automatically enter the development shell when you `cd` into the project directory.
|
||||
|
||||
Without Nix, install ESP-IDF and Python dependencies manually (especially `invoke`) and ensure `idf.py` is available in your shell.
|
||||
|
||||
Run `invoke --list` to see all available tasks.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
invoke flash
|
||||
invoke reset
|
||||
invoke config
|
||||
```
|
||||
|
||||
### Pre-commit hooks
|
||||
|
||||
This project uses [pre-commit](https://pre-commit.com/) to automatically check code quality, formatting, and common mistakes before committing.
|
||||
|
||||
**Setup:**
|
||||
|
||||
```bash
|
||||
# Install pre-commit hooks
|
||||
pre-commit install
|
||||
|
||||
# Optionally, run all hooks on all files
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Case
|
||||
|
||||
All print files (STL, STEP, X_T) can be found in [assets/case](/assets/case/). Alternatively you can view the project on [OnShape](https://cad.onshape.com/documents/7363818fd18bf0cbf094790e/w/52455282b39e47fbde5d0e53/e/9bec98aa83a813dc9a4d6ab2) where you can export the files in a format you like.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 73 KiB |
|
|
@ -1,5 +1 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" rx="128" fill="#022225"/>
|
||||
<path d="M384.806 144.708V130.315C384.806 126.412 383.255 122.669 380.495 119.909C377.735 117.149 373.992 115.598 370.089 115.598C365.345 115.598 357.926 115.598 352.736 115.598C317.07 90.9098 273.338 80.7344 230.436 87.1415C187.534 93.5486 148.684 116.057 121.787 150.089C94.89 184.121 81.9662 227.12 85.6441 270.342C89.322 313.563 109.326 353.761 141.586 382.759C173.847 411.756 215.942 427.377 259.309 426.443C302.677 425.509 344.06 408.091 375.042 377.731C406.025 347.371 424.279 306.349 426.093 263.01C427.906 219.67 413.143 177.267 384.806 144.424V144.708ZM169.116 294.07C163.246 294.07 157.508 292.329 152.628 289.068C147.747 285.807 143.943 281.172 141.697 275.749C139.451 270.326 138.863 264.359 140.008 258.602C141.153 252.845 143.98 247.557 148.13 243.407C152.281 239.256 157.569 236.43 163.326 235.285C169.083 234.139 175.05 234.727 180.473 236.973C185.896 239.22 190.531 243.023 193.792 247.904C197.053 252.784 198.793 258.522 198.793 264.392C198.793 272.263 195.667 279.812 190.101 285.377C184.535 290.943 176.987 294.07 169.116 294.07ZM256 371.548C250.13 371.548 244.392 369.807 239.512 366.546C234.631 363.285 230.828 358.65 228.581 353.227C226.335 347.804 225.747 341.837 226.893 336.08C228.038 330.323 230.864 325.035 235.015 320.885C239.165 316.734 244.453 313.908 250.21 312.763C255.967 311.618 261.934 312.205 267.357 314.452C272.78 316.698 277.415 320.502 280.676 325.382C283.937 330.263 285.678 336 285.678 341.87C285.678 349.741 282.551 357.29 276.985 362.855C271.42 368.421 263.871 371.548 256 371.548ZM342.195 294.07C336.325 294.07 330.587 292.329 325.707 289.068C320.826 285.807 317.023 281.172 314.776 275.749C312.53 270.326 311.942 264.359 313.088 258.602C314.233 252.845 317.059 247.557 321.21 243.407C325.36 239.256 330.648 236.43 336.405 235.285C342.162 234.139 348.129 234.727 353.552 236.973C358.975 239.22 363.61 243.023 366.871 247.904C370.132 252.784 371.873 258.522 371.873 264.392C371.873 272.263 368.746 279.812 363.18 285.377C357.615 290.943 350.066 294.07 342.195 294.07Z" fill="#087E8B"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M256 56C216.444 56 177.776 67.7298 144.886 89.7061C111.996 111.682 86.3617 142.918 71.2242 179.463C56.0867 216.008 52.126 256.222 59.843 295.018C67.5601 333.814 86.6082 369.451 114.579 397.421C142.549 425.392 178.186 444.44 216.982 452.157C255.778 459.874 295.992 455.913 332.537 440.776C369.082 425.638 400.318 400.004 422.294 367.114C444.27 334.224 456 295.556 456 256C455.936 202.976 434.844 152.143 397.35 114.65C359.857 77.1564 309.024 56.0644 256 56ZM149.395 415.595C180.951 436.679 218.049 447.932 256 447.932V447.891C306.873 447.827 355.644 427.589 391.617 391.617C427.589 355.644 447.827 306.873 447.891 256C447.883 218.049 436.622 180.953 415.532 149.402C394.442 117.851 364.47 93.262 329.406 78.7444C294.341 64.2268 255.76 60.4325 218.539 67.8413C181.318 75.25 147.131 93.5291 120.298 120.367C93.4657 147.205 75.1938 181.397 67.7929 218.619C60.392 255.841 64.1945 294.422 78.7195 329.483C93.2445 364.545 117.84 394.512 149.395 415.595Z" fill="#087E8B"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 512 512"><rect width="512" height="512" fill="#022225" rx="128"/><path fill="#087e8b" d="M384.806 144.708v-14.393a14.717 14.717 0 0 0-14.717-14.717h-17.353a170.606 170.606 0 1 0 32.07 28.826zM169.116 294.07a29.68 29.68 0 0 1-27.419-18.321 29.68 29.68 0 0 1 6.433-32.342 29.68 29.68 0 0 1 32.343-6.434 29.68 29.68 0 0 1 9.628 48.404 29.68 29.68 0 0 1-20.985 8.693M256 371.548a29.676 29.676 0 0 1-29.107-35.468 29.67 29.67 0 0 1 8.122-15.195 29.67 29.67 0 0 1 32.342-6.433A29.677 29.677 0 0 1 256 371.548m86.195-77.478a29.676 29.676 0 0 1-29.107-35.468 29.67 29.67 0 0 1 8.122-15.195 29.672 29.672 0 0 1 45.661 4.497 29.676 29.676 0 0 1-24.676 46.166"/><path fill="#087e8b" fill-rule="evenodd" d="M256 56a200 200 0 1 0 200 200 200.24 200.24 0 0 0-58.65-141.35A200.24 200.24 0 0 0 256 56M149.395 415.595A191.9 191.9 0 0 0 256 447.932v-.041a192.14 192.14 0 0 0 135.617-56.274A192.14 192.14 0 0 0 447.891 256a191.891 191.891 0 1 0-298.496 159.595" clip-rule="evenodd"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 38 KiB |
1
components/dmx/CMakeLists.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
idf_component_register(SRCS "src/dmx.c" INCLUDE_DIRS "include")
|
||||
9
components/dmx/include/dmx.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
0
components/dmx/src/dmx.c
Normal file
1
components/logger/CMakeLists.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
idf_component_register(INCLUDE_DIRS include)
|
||||
50
components/logger/include/logger.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @file logger.h
|
||||
* @brief Project-wide logging macros based on ESP-IDF's logging library.
|
||||
*
|
||||
* This header provides a set of simple logging macros (LOGE, LOGW, LOGI, etc.)
|
||||
* that wrap the underlying ESP-IDF logging functions (esp_log_e, esp_log_w,
|
||||
* etc.).
|
||||
*
|
||||
* @section usage Usage
|
||||
* To use these macros, a `LOG_TAG` should be defined before including this
|
||||
* header. The `LOG_TAG` is a string that identifies the source of the log
|
||||
* messages, typically the component or file name. If `LOG_TAG` is not defined,
|
||||
* a default tag "CHAOS" will be used.
|
||||
*
|
||||
* @example
|
||||
* #define LOG_TAG "MY_COMPONENT"
|
||||
* #include "logger.h"
|
||||
*
|
||||
* void my_function() {
|
||||
* LOGI("This is an informational message.");
|
||||
* LOGE("This is an error message with a value: %d", 42);
|
||||
* }
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef LOG_TAG
|
||||
#define LOG_TAG "CHAOS" ///< Default log tag
|
||||
#endif
|
||||
|
||||
/** @brief Log a message at Error level. */
|
||||
#define LOGE(...) ESP_LOGE(LOG_TAG, __VA_ARGS__)
|
||||
/** @brief Log a message at Warning level. */
|
||||
#define LOGW(...) ESP_LOGW(LOG_TAG, __VA_ARGS__)
|
||||
/** @brief Log a message at Info level. */
|
||||
#define LOGI(...) ESP_LOGI(LOG_TAG, __VA_ARGS__)
|
||||
/** @brief Log a message at Debug level. */
|
||||
#define LOGD(...) ESP_LOGD(LOG_TAG, __VA_ARGS__)
|
||||
/** @brief Log a message at Verbose level. */
|
||||
#define LOGV(...) ESP_LOGV(LOG_TAG, __VA_ARGS__)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
10
components/storage/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
idf_component_register(
|
||||
SRCS
|
||||
"src/storage.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
REQUIRES
|
||||
joltwallet__littlefs
|
||||
PRIV_REQUIRES
|
||||
vfs
|
||||
logger)
|
||||
25
components/storage/include/storage.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialize and mount LittleFS filesystem
|
||||
*
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t storage_init(void);
|
||||
|
||||
/**
|
||||
* @brief Get the mount point for the LittleFS filesystem
|
||||
*
|
||||
* @return Pointer to the mount point string (e.g., "/data")
|
||||
*/
|
||||
const char *storage_get_mount_point(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
44
components/storage/src/storage.c
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#define LOG_TAG "STORE" ///< "STORE" log tag for this file
|
||||
|
||||
#include "storage.h"
|
||||
#include "esp_littlefs.h"
|
||||
#include "esp_vfs.h"
|
||||
#include "logger.h"
|
||||
|
||||
static const char *LITTLEFS_MOUNT_POINT =
|
||||
"/data"; ///< Mount point for LittleFS filesystem
|
||||
|
||||
esp_err_t storage_init(void) {
|
||||
esp_vfs_littlefs_conf_t conf = {
|
||||
.base_path = LITTLEFS_MOUNT_POINT,
|
||||
.partition_label = "storage",
|
||||
.format_if_mount_failed = false,
|
||||
.read_only = false,
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_vfs_littlefs_register(&conf);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_FAIL) {
|
||||
LOGE("Failed to mount LittleFS or format filesystem");
|
||||
} else if (ret == ESP_ERR_INVALID_STATE) {
|
||||
LOGE("ESP_ERR_INVALID_STATE");
|
||||
} else {
|
||||
LOGE("Failed to initialize LittleFS: %s", esp_err_to_name(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
ret = esp_littlefs_info(conf.partition_label, &total, &used);
|
||||
if (ret == ESP_OK) {
|
||||
LOGI("LittleFS mounted at %s. Total: %d bytes, Used: %d bytes",
|
||||
LITTLEFS_MOUNT_POINT, total, used);
|
||||
} else {
|
||||
LOGE("Failed to get LittleFS information");
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const char *storage_get_mount_point(void) { return LITTLEFS_MOUNT_POINT; }
|
||||
16
components/web_server/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
idf_component_register(
|
||||
SRCS
|
||||
"src/web_server.c"
|
||||
"src/wifi.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
REQUIRES
|
||||
esp_http_server
|
||||
storage
|
||||
PRIV_REQUIRES
|
||||
freertos
|
||||
esp_wifi
|
||||
esp_event
|
||||
esp_netif
|
||||
nvs_flash
|
||||
logger)
|
||||
59
components/web_server/include/web_server.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @file web_server.h
|
||||
* @brief Simple HTTP web server component for ESP32 with async FreeRTOS
|
||||
* support.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Web server configuration structure.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t port; ///< HTTP server port (default: 80)
|
||||
size_t max_uri_handlers; ///< Maximum number of URI handlers
|
||||
size_t stack_size; ///< FreeRTOS task stack size
|
||||
UBaseType_t task_priority; ///< FreeRTOS task priority
|
||||
} webserver_config_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize and start the HTTP web server.
|
||||
*
|
||||
* This function creates a FreeRTOS task that manages the HTTP server.
|
||||
* It serves static files from the data/ folder and supports dynamic handler
|
||||
* registration.
|
||||
*
|
||||
* @param config Configuration structure. If NULL, default values are used.
|
||||
* @return HTTP server handle on success, NULL on failure.
|
||||
*/
|
||||
httpd_handle_t webserver_start(const webserver_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Stop the web server and cleanup resources.
|
||||
*
|
||||
* @param server HTTP server handle returned by webserver_start().
|
||||
* Safe to pass NULL.
|
||||
*/
|
||||
void webserver_stop(httpd_handle_t server);
|
||||
|
||||
/**
|
||||
* @brief Register a custom URI handler.
|
||||
*
|
||||
* This allows dynamic registration of API endpoints and other custom handlers.
|
||||
*
|
||||
* @param server HTTP server handle.
|
||||
* @param uri_handler Pointer to httpd_uri_t structure.
|
||||
* @return ESP_OK on success, error code otherwise.
|
||||
*/
|
||||
esp_err_t webserver_register_handler(httpd_handle_t server,
|
||||
const httpd_uri_t *uri_handler);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
33
components/web_server/include/wifi.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Start WiFi Access Point (AP) mode.
|
||||
*
|
||||
* Initializes and starts the WiFi AP with the given SSID and password.
|
||||
*
|
||||
* @param ssid SSID for the AP (1-32 characters)
|
||||
* @param password Password for the AP (min. 8 characters, optional)
|
||||
* @param channel WiFi channel to use
|
||||
* @param max_connections Maximum number of client connections
|
||||
* @return ESP_OK on success, ESP_ERR_INVALID_ARG or other error codes on
|
||||
* failure
|
||||
*/
|
||||
esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
|
||||
uint8_t max_connections);
|
||||
|
||||
/**
|
||||
* @brief Stop WiFi Access Point (AP) mode.
|
||||
*
|
||||
* Deinitializes and stops the WiFi AP.
|
||||
*/
|
||||
void wifi_stop_ap(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
293
components/web_server/src/web_server.c
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
#define LOG_TAG "WEBSRV"
|
||||
|
||||
/**
|
||||
* @def LOG_TAG
|
||||
* @brief Tag used for web server logging.
|
||||
*/
|
||||
|
||||
#include "web_server.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "logger.h"
|
||||
#include "storage.h"
|
||||
|
||||
// Default configuration values
|
||||
/**
|
||||
* @brief Default port for the web server.
|
||||
*/
|
||||
#define WEBSERVER_DEFAULT_PORT 80
|
||||
/**
|
||||
* @brief Default port for the web server.
|
||||
*/
|
||||
#define WEBSERVER_DEFAULT_MAX_HANDLERS 32
|
||||
/**
|
||||
* @brief Default maximum number of URI handlers.
|
||||
*/
|
||||
#define WEBSERVER_DEFAULT_STACK_SIZE (8 * 1024)
|
||||
/**
|
||||
* @brief Default stack size for the web server task.
|
||||
*/
|
||||
#define WEBSERVER_DEFAULT_TASK_PRIORITY 5
|
||||
/**
|
||||
* @brief Default task priority for the web server task.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Handle for the HTTP server instance.
|
||||
*/
|
||||
static httpd_handle_t s_server_handle = NULL;
|
||||
|
||||
/**
|
||||
* @brief Handle for the FreeRTOS web server task.
|
||||
*/
|
||||
static TaskHandle_t s_server_task_handle = NULL;
|
||||
|
||||
/**
|
||||
* @brief Get MIME type based on file extension
|
||||
*/
|
||||
static const char *get_mime_type(const char *filename) {
|
||||
const char *dot = strrchr(filename, '.');
|
||||
if (!dot)
|
||||
return "application/octet-stream";
|
||||
|
||||
if (strcmp(dot, ".html") == 0)
|
||||
return "text/html";
|
||||
if (strcmp(dot, ".css") == 0)
|
||||
return "text/css";
|
||||
if (strcmp(dot, ".js") == 0)
|
||||
return "application/javascript";
|
||||
if (strcmp(dot, ".json") == 0)
|
||||
return "application/json";
|
||||
if (strcmp(dot, ".png") == 0)
|
||||
return "image/png";
|
||||
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
|
||||
return "image/jpeg";
|
||||
if (strcmp(dot, ".gif") == 0)
|
||||
return "image/gif";
|
||||
if (strcmp(dot, ".svg") == 0)
|
||||
return "image/svg+xml";
|
||||
if (strcmp(dot, ".ico") == 0)
|
||||
return "image/x-icon";
|
||||
if (strcmp(dot, ".txt") == 0)
|
||||
return "text/plain";
|
||||
if (strcmp(dot, ".xml") == 0)
|
||||
return "application/xml";
|
||||
if (strcmp(dot, ".wav") == 0)
|
||||
return "audio/wav";
|
||||
if (strcmp(dot, ".mp3") == 0)
|
||||
return "audio/mpeg";
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief HTTP handler for static files from LittleFS
|
||||
*/
|
||||
static esp_err_t static_file_handler(httpd_req_t *req) {
|
||||
// Build the file path
|
||||
char filepath[1024];
|
||||
snprintf(filepath, sizeof(filepath), "%s%s", storage_get_mount_point(),
|
||||
req->uri);
|
||||
|
||||
// Handle root path
|
||||
if (strcmp(req->uri, "/") == 0) {
|
||||
snprintf(filepath, sizeof(filepath), "%s/index.html",
|
||||
storage_get_mount_point());
|
||||
}
|
||||
|
||||
FILE *f = fopen(filepath, "r");
|
||||
if (!f) {
|
||||
LOGW("File not found: %s", filepath);
|
||||
httpd_resp_send_404(req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Get MIME type and set content type
|
||||
const char *mime_type = get_mime_type(filepath);
|
||||
httpd_resp_set_type(req, mime_type);
|
||||
|
||||
// Send file in chunks
|
||||
char buf[1024];
|
||||
size_t read_len;
|
||||
while ((read_len = fread(buf, 1, sizeof(buf), f)) > 0) {
|
||||
if (httpd_resp_send_chunk(req, buf, read_len) != ESP_OK) {
|
||||
LOGW("Failed to send data chunk for %s", filepath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
httpd_resp_send_chunk(req, NULL, 0); // Send end marker
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief HTTP handler for API health check (GET /api/health)
|
||||
*/
|
||||
static esp_err_t health_check_handler(httpd_req_t *req) {
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief FreeRTOS task function for the HTTP server.
|
||||
* Allows non-blocking server operation and future extensibility.
|
||||
*/
|
||||
static void webserver_task(void *arg) {
|
||||
(void)arg; // Unused parameter
|
||||
LOGI("Web server task started");
|
||||
|
||||
// Keep task alive - the server runs in the background
|
||||
while (s_server_handle != NULL) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10000)); // 10 second check interval
|
||||
}
|
||||
|
||||
LOGI("Web server task ending");
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start the web server with the given configuration.
|
||||
*
|
||||
* Initializes storage, configures the HTTP server, registers default handlers,
|
||||
* and starts the FreeRTOS task for async operation.
|
||||
*
|
||||
* @param config Pointer to webserver configuration struct (optional)
|
||||
* @return Handle to the running HTTP server, or NULL on failure
|
||||
*/
|
||||
httpd_handle_t webserver_start(const webserver_config_t *config) {
|
||||
if (s_server_handle != NULL) {
|
||||
LOGW("Web server already running");
|
||||
return s_server_handle;
|
||||
}
|
||||
|
||||
// Initialize LittleFS
|
||||
esp_err_t ret = storage_init();
|
||||
if (ret != ESP_OK) {
|
||||
LOGE("Failed to initialize storage");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Use provided config or defaults
|
||||
uint16_t port = WEBSERVER_DEFAULT_PORT;
|
||||
size_t max_handlers = WEBSERVER_DEFAULT_MAX_HANDLERS;
|
||||
size_t stack_size = WEBSERVER_DEFAULT_STACK_SIZE;
|
||||
UBaseType_t task_priority = WEBSERVER_DEFAULT_TASK_PRIORITY;
|
||||
|
||||
if (config) {
|
||||
port = config->port;
|
||||
max_handlers = config->max_uri_handlers;
|
||||
stack_size = config->stack_size;
|
||||
task_priority = config->task_priority;
|
||||
}
|
||||
|
||||
// Create HTTP server configuration
|
||||
httpd_config_t http_config = HTTPD_DEFAULT_CONFIG();
|
||||
http_config.server_port = port;
|
||||
http_config.max_uri_handlers = max_handlers;
|
||||
http_config.stack_size = stack_size;
|
||||
http_config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
|
||||
// Start HTTP server
|
||||
ret = httpd_start(&s_server_handle, &http_config);
|
||||
if (ret != ESP_OK) {
|
||||
LOGE("Failed to start HTTP server: %s", esp_err_to_name(ret));
|
||||
s_server_handle = NULL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOGI("HTTP server started on port %d", port);
|
||||
|
||||
// Register default handlers
|
||||
// Health check endpoint
|
||||
httpd_uri_t health_uri = {
|
||||
.uri = "/api/health",
|
||||
.method = HTTP_GET,
|
||||
.handler = health_check_handler,
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
httpd_register_uri_handler(s_server_handle, &health_uri);
|
||||
|
||||
// Wildcard handler for static files from LittleFS (must be last)
|
||||
httpd_uri_t file_uri = {
|
||||
.uri = "/*",
|
||||
.method = HTTP_GET,
|
||||
.handler = static_file_handler,
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
httpd_register_uri_handler(s_server_handle, &file_uri);
|
||||
|
||||
// Create FreeRTOS task for the server
|
||||
// This allows other tasks to continue running and makes the server
|
||||
// async-ready
|
||||
BaseType_t task_ret = xTaskCreate(webserver_task, "webserver", stack_size,
|
||||
(void *)s_server_handle, task_priority,
|
||||
&s_server_task_handle);
|
||||
|
||||
if (task_ret != pdPASS) {
|
||||
LOGE("Failed to create web server task");
|
||||
httpd_stop(s_server_handle);
|
||||
s_server_handle = NULL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOGI("Web server initialized successfully");
|
||||
return s_server_handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop the web server and clean up resources.
|
||||
*
|
||||
* Stops the HTTP server and deletes the FreeRTOS task.
|
||||
*
|
||||
* @param server Handle to the HTTP server instance
|
||||
*/
|
||||
void webserver_stop(httpd_handle_t server) {
|
||||
if (server == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
httpd_stop(server);
|
||||
s_server_handle = NULL;
|
||||
|
||||
// Wait for task to finish
|
||||
if (s_server_task_handle != NULL) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
s_server_task_handle = NULL;
|
||||
}
|
||||
|
||||
LOGI("Web server stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Register a URI handler with the web server.
|
||||
*
|
||||
* @param server Handle to the HTTP server instance
|
||||
* @param uri_handler Pointer to the URI handler struct
|
||||
* @return ESP_OK on success, ESP_ERR_INVALID_ARG or other error codes on
|
||||
* failure
|
||||
*/
|
||||
esp_err_t webserver_register_handler(httpd_handle_t server,
|
||||
const httpd_uri_t *uri_handler) {
|
||||
if (server == NULL || uri_handler == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t ret = httpd_register_uri_handler(server, uri_handler);
|
||||
if (ret == ESP_OK) {
|
||||
LOGI("Registered handler: %s [%d]", uri_handler->uri, uri_handler->method);
|
||||
} else {
|
||||
LOGE("Failed to register handler %s: %s", uri_handler->uri,
|
||||
esp_err_to_name(ret));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
105
components/web_server/src/wifi.c
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#define LOG_TAG "WIFI" ///< "WIFI" log tag for this file
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "logger.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "wifi.h"
|
||||
|
||||
/**
|
||||
* @brief Indicates whether the WiFi AP is started.
|
||||
*/
|
||||
static bool s_wifi_started = false;
|
||||
|
||||
/**
|
||||
* @brief Start WiFi Access Point (AP) mode.
|
||||
*
|
||||
* Initializes and starts the WiFi AP with the given SSID and password.
|
||||
*
|
||||
* @param ssid SSID for the AP (1-32 characters)
|
||||
* @param password Password for the AP (min. 8 characters, optional)
|
||||
* @param channel WiFi channel to use
|
||||
* @param max_connections Maximum number of client connections
|
||||
* @return ESP_OK on success, ESP_ERR_INVALID_ARG or other error codes on
|
||||
* failure
|
||||
*/
|
||||
esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
|
||||
uint8_t max_connections) {
|
||||
if (s_wifi_started) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (!ssid || strlen(ssid) == 0 || strlen(ssid) > 32) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const bool has_password = password && strlen(password) > 0;
|
||||
if (has_password && strlen(password) < 8) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES ||
|
||||
err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_create_default_wifi_ap();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.ap =
|
||||
{
|
||||
.channel = channel,
|
||||
.max_connection = max_connections,
|
||||
.authmode = has_password ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN,
|
||||
.pmf_cfg =
|
||||
{
|
||||
.required = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
strlcpy((char *)wifi_config.ap.ssid, ssid, sizeof(wifi_config.ap.ssid));
|
||||
wifi_config.ap.ssid_len = strlen(ssid);
|
||||
|
||||
if (has_password) {
|
||||
strlcpy((char *)wifi_config.ap.password, password,
|
||||
sizeof(wifi_config.ap.password));
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
s_wifi_started = true;
|
||||
LOGI("WiFi AP started: SSID=%s channel=%u", ssid, channel);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop WiFi Access Point (AP) mode.
|
||||
*
|
||||
* Deinitializes and stops the WiFi AP.
|
||||
*/
|
||||
void wifi_stop_ap(void) {
|
||||
if (!s_wifi_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
esp_wifi_stop();
|
||||
esp_wifi_deinit();
|
||||
s_wifi_started = false;
|
||||
LOGI("WiFi AP stopped");
|
||||
}
|
||||
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.375 5.66787L1.20712 0.5H0.5V1.20712L5.66787 6.375H1.375V7.375H7.375V1.375H6.375V5.66787ZM10.2071 9.5H14.5V8.5H8.5V14.5H9.5V10.2071L14.7929 15.5H15.5V14.7929L10.2071 9.5Z"
|
||||
fill="#ffffff" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#fff" d="M6.375 5.668 1.207.5H.5v.707l5.168 5.168H1.375v1h6v-6h-1zM10.207 9.5H14.5v-1h-6v6h1v-4.293l5.293 5.293h.707v-.707z"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 240 B |
|
|
@ -1,11 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.0625 8.00001V9.00001C4.81953 9.00001 7.0625 11.243 7.0625 14H8.0625C8.0625 10.6916 5.37091 8.00001 2.0625 8.00001Z"
|
||||
fill="#087E8B" />
|
||||
<path
|
||||
d="M2.0625 4.37501V5.37501C6.81834 5.37501 10.6875 9.24417 10.6875 14H11.6875C11.691 12.7355 11.4436 11.4829 10.9597 10.3147C10.4758 9.14644 9.76498 8.08578 8.86841 7.1941C7.97671 6.29756 6.91605 5.58677 5.74782 5.10287C4.57958 4.61898 3.32698 4.37158 2.0625 4.37501Z"
|
||||
fill="#087E8B" />
|
||||
<path
|
||||
d="M14.2711 8.84235C13.606 7.26802 12.6417 5.83774 11.4317 4.63085C10.2026 3.3987 8.74218 2.42156 7.13432 1.75556C5.52646 1.08956 3.80284 0.747829 2.0625 0.75001V1.75001C8.81716 1.75001 14.3125 7.24535 14.3125 14H15.3125C15.3159 12.2282 14.9616 10.474 14.2711 8.84235ZM2.8125 11.25C2.41694 11.25 2.03026 11.3673 1.70136 11.5871C1.37246 11.8068 1.11612 12.1192 0.964742 12.4846C0.813367 12.8501 0.77376 13.2522 0.85093 13.6402C0.928101 14.0282 1.11858 14.3845 1.39829 14.6642C1.67799 14.9439 2.03436 15.1344 2.42232 15.2116C2.81028 15.2888 3.21242 15.2491 3.57787 15.0978C3.94332 14.9464 4.25568 14.69 4.47544 14.3612C4.6952 14.0323 4.8125 13.6456 4.8125 13.25C4.81192 12.7198 4.60102 12.2114 4.22608 11.8364C3.85113 11.4615 3.34276 11.2506 2.8125 11.25ZM2.8125 14.25C2.61472 14.25 2.42138 14.1914 2.25693 14.0815C2.09248 13.9716 1.96431 13.8154 1.88862 13.6327C1.81293 13.45 1.79313 13.2489 1.83172 13.0549C1.8703 12.8609 1.96554 12.6828 2.10539 12.5429C2.24525 12.4031 2.42343 12.3078 2.61741 12.2692C2.81139 12.2306 3.01246 12.2504 3.19518 12.3261C3.37791 12.4018 3.53409 12.53 3.64397 12.6944C3.75385 12.8589 3.8125 13.0522 3.8125 13.25C3.8122 13.5151 3.70675 13.7693 3.51928 13.9568C3.33181 14.1443 3.07763 14.2497 2.8125 14.25Z"
|
||||
fill="#087E8B" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#087e8b" d="M2.063 8v1c2.757 0 5 2.243 5 5h1c0-3.308-2.692-6-6-6"/><path fill="#087e8b" d="M2.063 4.375v1c4.755 0 8.625 3.87 8.625 8.625h1a9.56 9.56 0 0 0-2.82-6.806 9.56 9.56 0 0 0-6.805-2.819"/><path fill="#087e8b" d="M14.271 8.842a13.2 13.2 0 0 0-2.84-4.211A13.2 13.2 0 0 0 2.064.75v1c6.754 0 12.25 5.495 12.25 12.25h1a13.2 13.2 0 0 0-1.042-5.158M2.813 11.25a2 2 0 1 0 0 4 2 2 0 0 0 0-4m0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 538 B |
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15.5 8.5V7.5H8.5V6H10.25C10.4488 5.99978 10.6395 5.92069 10.7801 5.78008C10.9207 5.63948 10.9998 5.44884 11 5.25V1.25C10.9998 1.05116 10.9207 0.86052 10.7801 0.719917C10.6395 0.579313 10.4488 0.500223 10.25 0.5H5.75C5.55116 0.500223 5.36052 0.579313 5.21992 0.719917C5.07931 0.86052 5.00022 1.05116 5 1.25V5.25C5.00022 5.44884 5.07931 5.63948 5.21992 5.78008C5.36052 5.92069 5.55116 5.99978 5.75 6H7.5V7.5H0.5V8.5H3V10H1.29347C1.09463 10.0002 0.904011 10.0793 0.763413 10.2199C0.622814 10.3605 0.543717 10.5512 0.543469 10.75V14.75C0.543717 14.9488 0.622814 15.1395 0.763413 15.2801C0.904011 15.4207 1.09463 15.4998 1.29347 15.5H5.75C5.94884 15.4998 6.13948 15.4207 6.28008 15.2801C6.42069 15.1395 6.49978 14.9488 6.5 14.75V10.75C6.49978 10.5512 6.42069 10.3605 6.28008 10.2199C6.13948 10.0793 5.94884 10.0002 5.75 10H4V8.5H12V10H10.25C10.0512 10.0002 9.86052 10.0793 9.71992 10.2199C9.57931 10.3605 9.50022 10.5512 9.5 10.75V14.75C9.50022 14.9488 9.57931 15.1395 9.71992 15.2801C9.86052 15.4207 10.0512 15.4998 10.25 15.5H14.75C14.9488 15.4998 15.1395 15.4207 15.2801 15.2801C15.4207 15.1395 15.4998 14.9488 15.5 14.75V10.75C15.4998 10.5512 15.4207 10.3605 15.2801 10.2199C15.1395 10.0793 14.9488 10.0002 14.75 10H13V8.5H15.5ZM6 1.5H10V5H6V1.5ZM5.5 14.5H1.54347V11H5.5V14.5ZM14.5 14.5H10.5V11H14.5V14.5Z"
|
||||
fill="#087E8B" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#087e8b" d="M15.5 8.5v-1h-7V6h1.75a.75.75 0 0 0 .75-.75v-4a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0-.75.75v4a.75.75 0 0 0 .75.75H7.5v1.5h-7v1H3V10H1.293a.75.75 0 0 0-.75.75v4a.75.75 0 0 0 .75.75H5.75a.75.75 0 0 0 .75-.75v-4a.75.75 0 0 0-.75-.75H4V8.5h8V10h-1.75a.75.75 0 0 0-.75.75v4a.75.75 0 0 0 .75.75h4.5a.75.75 0 0 0 .75-.75v-4a.75.75 0 0 0-.75-.75H13V8.5zM6 1.5h4V5H6zm-.5 13H1.543V11H5.5zm9 0h-4V11h4z"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 522 B |
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.5 1.5V0.5H0.5V6.5H1.5V2.20709L6.52147 7.22853L7.22853 6.52147L2.20709 1.5H6.5ZM14.5 9.5V13.7929L9.35353 8.64647L8.64647 9.35353L13.7929 14.5H9.5V15.5H15.5V9.5H14.5Z"
|
||||
fill="#ffffff" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#fff" d="M6.5 1.5v-1h-6v6h1V2.207L6.521 7.23l.708-.708L2.207 1.5zm8 8v4.293L9.354 8.646l-.708.708 5.147 5.146H9.5v1h6v-6z"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 238 B |
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.8177 4.15769L11.9127 3.25275L14.5004 3.25L14.4994 2.25L10.2499 2.2545V6.5H11.2499V4.00413L12.1106 4.86478C12.7765 5.53078 13.2301 6.37915 13.4142 7.30279C13.5983 8.22642 13.5046 9.1839 13.145 10.0543C12.7853 10.9247 12.1758 11.6691 11.3934 12.1934C10.611 12.7176 9.69083 12.9983 8.74903 13L8.75078 14C9.89021 13.998 11.0035 13.6584 11.9501 13.0241C12.8966 12.3898 13.634 11.4893 14.0692 10.4362C14.5043 9.38312 14.6177 8.22473 14.3949 7.10728C14.1722 5.98983 13.6233 4.96343 12.8177 4.15769ZM7.25078 3.25L7.24903 2.25C6.10959 2.25203 4.99631 2.59162 4.04974 3.22591C3.10317 3.86019 2.36577 4.76073 1.93064 5.8138C1.4955 6.86688 1.38215 8.02527 1.6049 9.14272C1.82764 10.2602 2.37649 11.2866 3.18215 12.0923L4.08984 13H1.4999V14H5.7499V9.75H4.7499V12.2459L3.88925 11.3852C3.22333 10.7192 2.76968 9.87085 2.58557 8.94721C2.40147 8.02358 2.49516 7.0661 2.85483 6.19568C3.21449 5.32525 3.824 4.58091 4.60639 4.05664C5.38878 3.53237 6.30897 3.25168 7.25078 3.25Z"
|
||||
fill="#ffffff" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#fff" d="m12.818 4.158-.905-.905L14.5 3.25v-1l-4.25.005V6.5h1V4.004l.86.86A4.766 4.766 0 0 1 8.75 13l.002 1a5.765 5.765 0 0 0 4.067-9.842M7.25 3.25l-.002-1a5.766 5.766 0 0 0-4.067 9.842L4.09 13H1.5v1h4.25V9.75h-1v2.496l-.86-.86A4.766 4.766 0 0 1 7.25 3.25"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 372 B |
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.43056 15H8.56944L15.75 4.74509V3.88752L15.7433 3.88284C13.4738 2.29385 10.7704 1.44153 8 1.44153C5.22956 1.44153 2.52618 2.29385 0.256719 3.88284L0.25 3.88752V4.74496L7.43056 15ZM8 2.44152C10.3954 2.43769 12.7409 3.12589 14.7545 4.42337L8 14.0698L1.2455 4.42337C3.25908 3.12589 5.6046 2.43769 8 2.44152Z"
|
||||
fill="#087E8B" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#087e8b" d="M7.43 15h1.14l7.18-10.255v-.857l-.007-.005a13.5 13.5 0 0 0-15.486 0l-.007.005v.857zM8 2.442c2.395-.004 4.74.684 6.755 1.981L8 14.07 1.246 4.423A12.43 12.43 0 0 1 8 2.442"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 298 B |
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.43056 15H8.56944L15.75 4.74509V3.88752L15.7433 3.88284C13.4738 2.29385 10.7704 1.44153 8 1.44153C5.22956 1.44153 2.52618 2.29385 0.256719 3.88284L0.25 3.88752V4.74496L7.43056 15ZM5.58959 10.6274C6.32637 10.216 7.15616 10 8 10C8.84384 10 9.67363 10.216 10.4104 10.6274L8 14.0698L5.58959 10.6274ZM8 2.44152C10.3954 2.43769 12.7409 3.12589 14.7545 4.42337L10.9863 9.80496C10.0794 9.27774 9.04903 9.00001 8 9.00001C6.95097 9.00001 5.92064 9.27774 5.01372 9.80496L1.2455 4.42337C3.25908 3.12589 5.6046 2.43769 8 2.44152Z"
|
||||
fill="#087E8B" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#087e8b" d="M7.43 15h1.14l7.18-10.255v-.857l-.007-.005a13.5 13.5 0 0 0-15.486 0l-.007.005v.857zm-1.84-4.373a4.94 4.94 0 0 1 4.82 0L8 14.07zM8 2.442c2.395-.004 4.74.684 6.755 1.981l-3.769 5.382a5.94 5.94 0 0 0-5.972 0L1.245 4.423A12.43 12.43 0 0 1 8 2.442"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 669 B After Width: | Height: | Size: 371 B |
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15.7433 3.88296C13.4738 2.29397 10.7704 1.44165 8 1.44165C5.22956 1.44165 2.52618 2.29397 0.256719 3.88296L0.25 3.88752V4.74496L7.43056 15H8.56944L15.75 4.74508V3.88752L15.7433 3.88296ZM5.932 11.1164L8.30575 7.4479C8.761 7.46593 9.21367 7.5254 9.65812 7.62558L6.6825 12.1881L5.932 11.1164ZM5.31297 10.2323L4.11719 8.52452C5.02149 7.97522 6.0325 7.62501 7.08281 7.49724L5.31297 10.2323ZM7.30066 13.071L10.6561 7.92602C11.0824 8.08771 11.493 8.28804 11.8828 8.52452L8 14.0698L7.30066 13.071ZM12.4575 7.70387C11.1172 6.87852 9.57404 6.4415 8 6.4415C6.42596 6.4415 4.88283 6.87852 3.54253 7.70387L1.24566 4.42337C3.26085 3.12941 5.6053 2.44153 8.00016 2.44153C10.395 2.44153 12.7395 3.12941 14.7547 4.42337L12.4575 7.70387Z"
|
||||
fill="#087E8B" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#087e8b" d="M15.743 3.883a13.5 13.5 0 0 0-15.486 0l-.007.005v.857L7.43 15h1.14l7.18-10.255v-.857zm-9.811 7.233 2.374-3.668a7.5 7.5 0 0 1 1.352.178l-2.975 4.562zm-.619-.884L4.117 8.525c.904-.55 1.915-.9 2.966-1.028zm1.988 2.839 3.355-5.145q.64.243 1.227.599L8 14.07zm5.156-5.367a8.5 8.5 0 0 0-8.914 0l-2.297-3.28a12.5 12.5 0 0 1 13.509 0z"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 871 B After Width: | Height: | Size: 454 B |
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15.7433 3.88296C13.4738 2.29397 10.7704 1.44165 8 1.44165C5.22956 1.44165 2.52618 2.29397 0.256719 3.88296L0.25 3.88752V4.74496L7.43056 15H8.56944L15.7502 4.74508V3.88752L15.7433 3.88296ZM5.93344 11.1184L9.85909 5.11446C10.3059 5.19846 10.7463 5.31312 11.1773 5.45762L6.6865 12.1938L5.93344 11.1184ZM5.31563 10.2361L4.48897 9.05549L7.28656 4.96665C7.5231 4.94998 7.76092 4.94165 8 4.94165C8.25417 4.94165 8.50704 4.95121 8.75863 4.97033L5.31563 10.2361ZM3.87613 8.18024L2.68088 6.4733C3.68242 5.84205 4.7887 5.39483 5.94756 5.15274L3.87613 8.18024ZM7.30216 13.0732L12.1293 5.83255C12.5399 6.01916 12.9374 6.23328 13.3191 6.47346L8 14.0698L7.30216 13.0732ZM13.8931 5.65358C12.131 4.53534 10.087 3.94154 8 3.94154C5.91299 3.94154 3.86905 4.53534 2.10691 5.65358L1.2455 4.42337C3.26069 3.12941 5.60515 2.44153 8 2.44153C10.3949 2.44153 12.7393 3.12941 14.7545 4.42337L13.8931 5.65358Z"
|
||||
fill="#087E8B" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#087e8b" d="M15.743 3.883a13.5 13.5 0 0 0-15.486 0l-.007.005v.857L7.43 15h1.14l7.18-10.255v-.857zm-9.81 7.235L9.86 5.114q.671.127 1.318.344l-4.49 6.736zm-.617-.882-.827-1.18 2.798-4.09a10 10 0 0 1 1.472.004zM3.876 8.18 2.681 6.473a10 10 0 0 1 3.267-1.32zm3.426 4.893 4.827-7.24q.616.28 1.19.64L8 14.07zm6.591-7.42a11 11 0 0 0-11.786 0l-.862-1.23a12.5 12.5 0 0 1 13.51 0z"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 487 B |
|
|
@ -1,5 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15.7433 3.88296C13.4738 2.29397 10.7704 1.44165 8 1.44165C5.22956 1.44165 2.52618 2.29397 0.256719 3.88296L0.25 3.88752V4.74493L7.43056 15H8.56944L15.75 4.74505V3.88752L15.7433 3.88296ZM12.604 3.31771L6.6865 12.1938L5.93412 11.1194L11.3399 2.89324C11.7686 3.01191 12.1905 3.15361 12.604 3.31771ZM8.95 2.47746C9.4032 2.51151 9.8542 2.57026 10.301 2.65343L5.31694 10.2378L4.48769 9.05343L8.95 2.47746ZM3.87384 8.17702L3.16134 7.15943L6.22291 2.56708C6.73369 2.49445 7.24845 2.45326 7.76428 2.44377L3.87384 8.17702ZM4.83481 2.84643L2.54566 6.28018L1.24566 4.42337C2.35118 3.71134 3.5626 3.17908 4.83481 2.84643ZM8 14.0698L7.30216 13.0731L13.5308 3.73027C13.9513 3.93827 14.3598 4.16966 14.7545 4.42337L8 14.0698Z"
|
||||
fill="#087E8B" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#087e8b" d="M15.743 3.883a13.5 13.5 0 0 0-15.486 0l-.007.005v.857L7.43 15h1.14l7.18-10.255v-.857zm-3.139-.565-5.917 8.876-.753-1.075 5.406-8.226q.643.178 1.264.425m-3.654-.84a13 13 0 0 1 1.351.175l-4.984 7.585-.83-1.185zM3.874 8.176 3.16 7.159l3.062-4.592q.766-.11 1.541-.123zm.96-5.33L2.547 6.28l-1.3-1.857a12.4 12.4 0 0 1 3.589-1.577M8 14.07l-.698-.997 6.229-9.343q.631.313 1.223.693z"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 861 B After Width: | Height: | Size: 503 B |
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
@ -60,11 +60,7 @@
|
|||
<div class="card">
|
||||
<span>Signalstärke</span>
|
||||
<span class="centered-vertical">
|
||||
<img
|
||||
class="connection-icon small"
|
||||
src=""
|
||||
alt=""
|
||||
/>
|
||||
<img class="connection-icon small" src="" alt="" />
|
||||
<span><span class="rssi"></span> dBm</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -95,9 +91,7 @@
|
|||
|
||||
<div class="card">
|
||||
<span>PSRAM</span>
|
||||
<span
|
||||
><span class="psram-percentage"></span> %</span
|
||||
>
|
||||
<span><span class="psram-percentage"></span> %</span>
|
||||
<span
|
||||
><span class="psram-used"></span> /
|
||||
<span class="psram-total"></span
|
||||
|
|
@ -208,15 +202,8 @@
|
|||
title="Netzwerk"
|
||||
required
|
||||
></select>
|
||||
<button
|
||||
type="button"
|
||||
id="refresh-networks"
|
||||
class="icon-button"
|
||||
>
|
||||
<img
|
||||
src="/icons/refresh.svg"
|
||||
alt="Neu laden"
|
||||
/>
|
||||
<button type="button" id="refresh-networks" class="icon-button">
|
||||
<img src="/icons/refresh.svg" alt="Neu laden" />
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -309,9 +296,7 @@
|
|||
required
|
||||
>
|
||||
<option value="0">Nichts</option>
|
||||
<option value="1">
|
||||
Konfiguration zurücksetzen
|
||||
</option>
|
||||
<option value="1">Konfiguration zurücksetzen</option>
|
||||
<option value="2">Neustart</option>
|
||||
</select>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ const dynamicInputs = form.querySelectorAll("[data-field][data-values]");
|
|||
document.addEventListener("change", updateVisibility);
|
||||
|
||||
function updateVisibility() {
|
||||
dynamicInputs.forEach((element) => {
|
||||
dynamicInputs.forEach(element => {
|
||||
const input = form.querySelector(`#${element.dataset.field}`);
|
||||
if (element.dataset.values.split("|").includes(input.value)) {
|
||||
element.classList.remove("hidden");
|
||||
element
|
||||
.querySelectorAll("input, select, button, textarea")
|
||||
.forEach((childInput) => (childInput.disabled = false));
|
||||
.forEach(childInput => (childInput.disabled = false));
|
||||
} else {
|
||||
element.classList.add("hidden");
|
||||
element
|
||||
.querySelectorAll("input, select, button, textarea")
|
||||
.forEach((childInput) => (childInput.disabled = true));
|
||||
.forEach(childInput => (childInput.disabled = true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
document.querySelector("form.config").addEventListener("input", (event) => {
|
||||
document.querySelector("form.config").addEventListener("input", event => {
|
||||
if (event.target.classList.contains("range")) {
|
||||
updateValue(event.target);
|
||||
}
|
||||
|
|
@ -9,6 +9,6 @@ function updateValue(slider) {
|
|||
slider.nextElementSibling.innerText = `${percentage}%`;
|
||||
}
|
||||
|
||||
document.querySelectorAll("input[type='range'].range").forEach((element) => {
|
||||
document.querySelectorAll("input[type='range'].range").forEach(element => {
|
||||
updateValue(element);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { updateConfig } from "/submit.js";
|
|||
|
||||
const form = document.querySelector("form.config");
|
||||
|
||||
form.addEventListener("reset", async (event) => {
|
||||
form.addEventListener("reset", async event => {
|
||||
event.preventDefault();
|
||||
|
||||
const ok = confirm(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function setStatus(status) {
|
|||
|
||||
setValue("rssi", status.connection.signalStrength);
|
||||
const icon = selectConnectionIcon(status.connection.signalStrength);
|
||||
document.querySelectorAll(".connection-icon").forEach((img) => {
|
||||
document.querySelectorAll(".connection-icon").forEach(img => {
|
||||
img.src = `/icons/${icon}`;
|
||||
});
|
||||
|
||||
|
|
@ -29,10 +29,7 @@ function setStatus(status) {
|
|||
const usedHeap = status.heap.total - status.heap.free;
|
||||
setValue("heap-used", formatBytes(usedHeap));
|
||||
setValue("heap-total", formatBytes(status.heap.total));
|
||||
setValue(
|
||||
"heap-percentage",
|
||||
Math.round((usedHeap / status.heap.total) * 100)
|
||||
);
|
||||
setValue("heap-percentage", Math.round((usedHeap / status.heap.total) * 100));
|
||||
|
||||
const usedPsram = status.psram.total - status.psram.free;
|
||||
setValue("psram-used", formatBytes(usedPsram));
|
||||
|
|
@ -48,7 +45,7 @@ function setStatus(status) {
|
|||
}
|
||||
|
||||
function setValue(className, value) {
|
||||
document.querySelectorAll("." + className).forEach((element) => {
|
||||
document.querySelectorAll("." + className).forEach(element => {
|
||||
element.innerText = value;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function parseValue(input) {
|
|||
return input.value;
|
||||
}
|
||||
|
||||
form.addEventListener("submit", (event) => {
|
||||
form.addEventListener("submit", event => {
|
||||
event.preventDefault();
|
||||
const inputFields = document.querySelectorAll(
|
||||
"form :is(input, select, textarea):not(:disabled)"
|
||||
|
|
|
|||
|
|
@ -12,18 +12,18 @@ export function initWebSocket() {
|
|||
console.info("WebSocket connection opened");
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
ws.onclose = event => {
|
||||
console.info("WebSocket connection closed, reason:", event.reason);
|
||||
ws = null;
|
||||
};
|
||||
|
||||
ws.onerror = (event) => {
|
||||
ws.onerror = event => {
|
||||
console.warn("WebSocket encountered error, closing socket.", event);
|
||||
ws.close();
|
||||
ws = null;
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
ws.onmessage = event => {
|
||||
const message = JSON.parse(event.data);
|
||||
console.log("received websocket data", message);
|
||||
if (message.type in callbacks) {
|
||||
|
|
|
|||
30
dependencies.lock
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
dependencies:
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.5.2
|
||||
joltwallet/littlefs:
|
||||
component_hash: 150ca47225f7c9917f7f610a5f85e5e93fe3c15402234c23496ff045ad558b0b
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.20.2
|
||||
someweisguy/esp_dmx:
|
||||
component_hash: 9a7cdcf093ef6f44337f2a254bbadbe4c8089c12aec4991cf43a83831a8389f4
|
||||
dependencies: []
|
||||
source:
|
||||
git: https://github.com/davispolito/esp_dmx.git
|
||||
path: .
|
||||
type: git
|
||||
version: 93cd565bb07d6bf9a56b5c62c96f2552a8fc6194
|
||||
direct_dependencies:
|
||||
- idf
|
||||
- joltwallet/littlefs
|
||||
- someweisguy/esp_dmx
|
||||
manifest_hash: 452ccdb963e60a5d4bb28f619a5058b387491bb886d6685d4d8ba97c5884abe2
|
||||
target: esp32s2
|
||||
version: 2.0.0
|
||||
1
docs/external/doxygen-awesome-css
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 1f3620084ff75734ed192101acf40e9dff01d848
|
||||
96
flake.lock
generated
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"nodes": {
|
||||
"esp-dev": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1767865407,
|
||||
"narHash": "sha256-QWF1rZYd+HvNzLIeRS+OEBX7HF0EhWCGeLbMkgtbsIo=",
|
||||
"owner": "mirrexagon",
|
||||
"repo": "nixpkgs-esp-dev",
|
||||
"rev": "5287d6e1ca9e15ebd5113c41b9590c468e1e001b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mirrexagon",
|
||||
"repo": "nixpkgs-esp-dev",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767799921,
|
||||
"narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d351d0653aeb7877273920cd3e823994e7579b0b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1772624091,
|
||||
"narHash": "sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN+UHzW1jc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "80bdc1e5ce51f56b19791b52b2901187931f5353",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"esp-dev": "esp-dev",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
38
flake.nix
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
description = "dmx-interface development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
esp-dev.url = "github:mirrexagon/nixpkgs-esp-dev";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
esp-dev,
|
||||
}:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
esp-idf = esp-dev.packages.${system}.esp-idf-full;
|
||||
in
|
||||
{
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
esp-idf
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.invoke
|
||||
|
||||
pkgs.pre-commit
|
||||
pkgs.clang-tools
|
||||
pkgs.svgo
|
||||
pkgs.prettier
|
||||
|
||||
pkgs.nixfmt
|
||||
pkgs.doxygen
|
||||
pkgs.graphviz
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 5d9c42b531404ccfbcb14106d6312b03a166868a
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 38de6ac248c7f270ca3b77ba38512ba39919aed8
|
||||
8
main/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
idf_component_register(
|
||||
SRCS
|
||||
"dmx-interface.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
REQUIRES
|
||||
web_server
|
||||
logger)
|
||||
41
main/dmx-interface.c
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#define LOG_TAG "MAIN" ///< "MAIN" log tag for this file
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "logger.h"
|
||||
#include "web_server.h"
|
||||
#include "wifi.h"
|
||||
|
||||
/**
|
||||
* @brief Main entry point for the DMX Interface application.
|
||||
*
|
||||
* Initializes WiFi Access Point and starts the web server.
|
||||
* Keeps the application running indefinitely.
|
||||
*/
|
||||
void app_main(void) {
|
||||
LOGI("DMX Interface starting...");
|
||||
|
||||
esp_err_t wifi_err = wifi_start_ap("DMX", "mbgmbgmbg", 1, 4);
|
||||
if (wifi_err != ESP_OK) {
|
||||
LOGE("Failed to start WiFi AP: %s", esp_err_to_name(wifi_err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Start HTTP web server
|
||||
httpd_handle_t server = webserver_start(NULL);
|
||||
if (server == NULL) {
|
||||
LOGE("Failed to start web server!");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGI("Web server started successfully");
|
||||
LOGI("Open http://192.168.4.1 in your browser");
|
||||
|
||||
// Keep the app running
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
8
main/idf_component.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
idf:
|
||||
version: ">=4.1.0"
|
||||
joltwallet/littlefs: ==1.20.2
|
||||
someweisguy/esp_dmx:
|
||||
git: https://github.com/davispolito/esp_dmx.git
|
||||
# version: v4.1.0
|
||||
5
partitions.csv
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
phy_init, data, phy, 0xf000, 0x1000,
|
||||
factory, app, factory, 0x10000, 1M,
|
||||
storage, data, littlefs, , 0xF0000,
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
default_envs = lolin_s2_mini
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
board_build.filesystem = littlefs
|
||||
|
||||
[env:lolin_s2_mini]
|
||||
platform = espressif32
|
||||
board = lolin_s2_mini
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson @ ^7.2.0
|
||||
someweisguy/esp_dmx
|
||||
extra_scripts = pre:pre_extra_script.py
|
||||
|
||||
[env:esp32_wroom_32]
|
||||
platform = espressif32
|
||||
board = nodemcu-32s
|
||||
lib_deps = bblanchon/ArduinoJson @ ^7.2.0
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
Import("env")
|
||||
|
||||
if env.IsIntegrationDump():
|
||||
# stop the current script execution
|
||||
Return()
|
||||
|
||||
env.Execute("git submodule update --init --recursive")
|
||||
print("✅ SUBMODULES UPDATED")
|
||||
7
sdkconfig.defaults
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) 5.5.2 Project Minimal Configuration
|
||||
#
|
||||
CONFIG_IDF_TARGET="esp32s2"
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_ESP_CONSOLE_USB_CDC=y
|
||||
515
src/main.cpp
|
|
@ -1,515 +0,0 @@
|
|||
#ifdef ESP32
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
// #include <USB.h>
|
||||
// #include "USBCDC.h"
|
||||
#include "driver/temp_sensor.h"
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#endif
|
||||
|
||||
#include <AsyncWebServer_ESP32_W5500.h>
|
||||
// #include "w5500/esp32_w5500.h"
|
||||
// #include <ESPAsyncWebServer.h>
|
||||
|
||||
#include <ArtnetWiFi.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
// #include "ESPDMX.h"
|
||||
#include <Arduino.h>
|
||||
#include <esp_dmx.h>
|
||||
|
||||
#include <LittleFS.h>
|
||||
#include "websocket.h"
|
||||
#include "routes/config.h"
|
||||
#include "routes/networks.h"
|
||||
#include "routes/status.h"
|
||||
|
||||
dmx_port_t dmx1 = DMX_NUM_0; // for esp32s2
|
||||
dmx_port_t dmx2 = DMX_NUM_1;
|
||||
byte dmx1_data[DMX_PACKET_SIZE];
|
||||
byte dmx2_data[DMX_PACKET_SIZE];
|
||||
|
||||
// Button
|
||||
#define PIN_LED 7
|
||||
#define PIN_BUTTON 5
|
||||
|
||||
uint8_t brightness_led = 20;
|
||||
bool led_on = true;
|
||||
double lastMills = 0;
|
||||
|
||||
// Ethernet stuff
|
||||
#define ETH_SCK 36
|
||||
#define ETH_SS 34
|
||||
#define ETH_MOSI 35
|
||||
#define ETH_MISO 37
|
||||
#define ETH_INT 38
|
||||
#define ETH_SPI_CLOCK_MHZ 25
|
||||
byte mac[6];
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
ArtnetWiFi artnet;
|
||||
|
||||
String broadcastIp;
|
||||
uint8_t universe1;
|
||||
uint8_t universe2;
|
||||
Direction direction1;
|
||||
Direction direction2;
|
||||
|
||||
enum class Status
|
||||
{
|
||||
Starting,
|
||||
Resetting,
|
||||
Normal,
|
||||
Warning,
|
||||
Critical
|
||||
};
|
||||
|
||||
Status status;
|
||||
struct BlinkingConfig
|
||||
{
|
||||
int interval_ms;
|
||||
bool is_blinking;
|
||||
int brightness;
|
||||
};
|
||||
|
||||
BlinkingConfig getBlinkingConfig(Status status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case Status::Starting:
|
||||
return {500, true, brightness_led};
|
||||
case Status::Resetting:
|
||||
return {100, true, brightness_led};
|
||||
case Status::Normal:
|
||||
return {1000, false, brightness_led};
|
||||
case Status::Warning:
|
||||
return {500, true, 255};
|
||||
case Status::Critical:
|
||||
return {100, true, 255};
|
||||
default:
|
||||
return {1000, false, 0};
|
||||
}
|
||||
}
|
||||
|
||||
BlinkingConfig led_config = getBlinkingConfig(status);
|
||||
|
||||
void updateTimer(int interval_ms)
|
||||
{
|
||||
// TODO: update the tickspeed of the timer
|
||||
}
|
||||
|
||||
void updateLed() // TODO: callback for timer
|
||||
{
|
||||
if (millis() - lastMills >= led_config.interval_ms)
|
||||
{
|
||||
lastMills = millis();
|
||||
led_config = getBlinkingConfig(status);
|
||||
if (led_config.is_blinking)
|
||||
{
|
||||
led_on = !led_on;
|
||||
analogWrite(PIN_LED, led_on ? led_config.brightness : 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
analogWrite(PIN_LED, led_config.brightness);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setStatus(Status newStatus)
|
||||
{
|
||||
status = newStatus;
|
||||
led_config = getBlinkingConfig(status);
|
||||
updateTimer(led_config.interval_ms);
|
||||
updateLed();
|
||||
}
|
||||
|
||||
void onButtonPress()
|
||||
{
|
||||
config.begin("dmx", true);
|
||||
ButtonAction action = static_cast<ButtonAction>(config.getUInt("button-action", DEFAULT_BUTTON_ACTION));
|
||||
config.end();
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case ResetConfig:
|
||||
config.begin("dmx", false);
|
||||
config.clear();
|
||||
config.end();
|
||||
|
||||
ESP.restart();
|
||||
break;
|
||||
|
||||
case Restart:
|
||||
config.begin("dmx", false);
|
||||
config.putBool("restart-via-btn", true);
|
||||
config.end();
|
||||
|
||||
ESP.restart();
|
||||
break;
|
||||
case None:
|
||||
// do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
setStatus(Status::Starting);
|
||||
pinMode(PIN_LED, OUTPUT);
|
||||
updateLed();
|
||||
|
||||
Serial.begin(9600);
|
||||
|
||||
// Get ETH mac
|
||||
delay(1000);
|
||||
|
||||
esp_efuse_mac_get_default(mac);
|
||||
|
||||
esp_read_mac(mac, ESP_MAC_ETH);
|
||||
Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x ESP MAC ETH\n",
|
||||
mac[0], mac[1], mac[2],
|
||||
mac[3], mac[4], mac[5]);
|
||||
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
|
||||
Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x ESP MAC SOFTAP\n",
|
||||
mac[0], mac[1], mac[2],
|
||||
mac[3], mac[4], mac[5]);
|
||||
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA); // ESP_MAC_BASE
|
||||
Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x ESP MAC BASE\n",
|
||||
mac[0], mac[1], mac[2],
|
||||
mac[3], mac[4], mac[5]);
|
||||
|
||||
// LED
|
||||
config.begin("dmx", true);
|
||||
brightness_led = config.getUInt("led-brightness", DEFAULT_LED_BRIGHTNESS);
|
||||
bool restartViaButton = config.getBool("restart-via-btn", false);
|
||||
config.end();
|
||||
updateLed();
|
||||
|
||||
// Button
|
||||
pinMode(PIN_BUTTON, INPUT_PULLUP);
|
||||
if (digitalRead(PIN_BUTTON) == LOW && !restartViaButton)
|
||||
{
|
||||
setStatus(Status::Resetting);
|
||||
unsigned long startTime = millis();
|
||||
while (digitalRead(PIN_BUTTON) == LOW && (millis() - startTime <= 3000))
|
||||
{
|
||||
updateLed();
|
||||
}
|
||||
if (digitalRead(PIN_BUTTON) == LOW)
|
||||
{
|
||||
Serial.println("Reset config");
|
||||
config.begin("dmx", false);
|
||||
config.clear();
|
||||
config.end();
|
||||
setStatus(Status::Normal);
|
||||
unsigned long startTime = millis();
|
||||
while (millis() - startTime <= 2000)
|
||||
{
|
||||
updateLed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.begin("dmx", false);
|
||||
config.putBool("restart-via-btn", false);
|
||||
config.end();
|
||||
|
||||
setStatus(Status::Starting);
|
||||
|
||||
attachInterrupt(PIN_BUTTON, onButtonPress, FALLING);
|
||||
|
||||
// wait for serial monitor
|
||||
unsigned long startTime = millis();
|
||||
while (millis() - startTime <= 5000)
|
||||
{
|
||||
updateLed();
|
||||
}
|
||||
Serial.println("Starting DMX-Interface...");
|
||||
|
||||
config.begin("dmx", true);
|
||||
|
||||
universe1 = config.getUInt("universe-1", DEFAULT_UNIVERSE1);
|
||||
universe2 = config.getUInt("universe-2", DEFAULT_UNIVERSE2);
|
||||
|
||||
direction1 = static_cast<Direction>(config.getUInt("direction-1", DEFAULT_DIRECTION1));
|
||||
direction2 = static_cast<Direction>(config.getUInt("direction-2", DEFAULT_DIRECTION2));
|
||||
|
||||
Serial.printf("Port A: Universe %d %s\n", universe1, (direction1 == Input) ? "DMX -> Art-Net" : "Art-Net -> DMX");
|
||||
Serial.printf("Port B: Universe %d %s\n", universe2, (direction2 == Input) ? "DMX -> Art-Net" : "Art-Net -> DMX");
|
||||
|
||||
Connection connection = static_cast<Connection>(config.getUInt("connection", DEFAULT_CONNECTION));
|
||||
IpMethod ipMethod = static_cast<IpMethod>(config.getUInt("ip-method"), DEFAULT_IP_METHOD);
|
||||
|
||||
char hostname[30];
|
||||
snprintf(hostname, sizeof(hostname), "ChaosDMX-%02X%02X", mac[4], mac[5]);
|
||||
DEFAULT_SSID = hostname;
|
||||
Serial.print("Hostname: ");
|
||||
Serial.println(hostname);
|
||||
|
||||
String ssid = config.getString("ssid", DEFAULT_SSID);
|
||||
String pwd = config.getString("password", DEFAULT_PASSWORD);
|
||||
|
||||
// Default IP as defined in standard https://art-net.org.uk/downloads/art-net.pdf, page 13
|
||||
IPAddress ip = config.getUInt("ip", DEFAULT_IP);
|
||||
IPAddress subnet = config.getUInt("subnet", DEFAULT_SUBNET);
|
||||
IPAddress gateway = config.getUInt("gateway", 0);
|
||||
|
||||
config.end();
|
||||
|
||||
switch (connection)
|
||||
{
|
||||
case WiFiSta:
|
||||
Serial.println("Initialize as WiFi Station");
|
||||
WiFi.setHostname(hostname);
|
||||
WiFi.begin(ssid, pwd);
|
||||
Serial.println("SSID: " + ssid + ", pwd: " + pwd);
|
||||
if (ipMethod == Static)
|
||||
{
|
||||
WiFi.config(ip, gateway, subnet);
|
||||
Serial.println("IP: " + ip.toString() + ", gateway: " + gateway + ", subnet: " + subnet);
|
||||
}
|
||||
while (WiFi.status() != WL_CONNECTED)
|
||||
{
|
||||
Serial.print(".");
|
||||
delay(500);
|
||||
}
|
||||
broadcastIp = String(WiFi.broadcastIP().toString().c_str());
|
||||
Serial.println("");
|
||||
Serial.print("WiFi connected, IP = ");
|
||||
Serial.println(WiFi.localIP());
|
||||
Serial.print("MAC address: ");
|
||||
Serial.println(WiFi.macAddress());
|
||||
Serial.print("Broadcast IP: ");
|
||||
Serial.println(broadcastIp);
|
||||
break;
|
||||
|
||||
case Ethernet:
|
||||
{
|
||||
Serial.println("Initialize as ETH");
|
||||
ESP32_W5500_onEvent();
|
||||
|
||||
if (ETH.begin(ETH_MISO, ETH_MOSI, ETH_SCK, ETH_SS, ETH_INT, ETH_SPI_CLOCK_MHZ, SPI2_HOST, mac))
|
||||
{ // Dynamic IP setup
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.println("Failed to configure Ethernet");
|
||||
}
|
||||
ETH.setHostname(hostname);
|
||||
|
||||
// ESP32_W5500_waitForConnect();
|
||||
uint8_t timeout = 5; // in s
|
||||
Serial.print("Wait for connect");
|
||||
while (!ESP32_W5500_eth_connected && timeout > 0)
|
||||
{
|
||||
delay(1000);
|
||||
timeout--;
|
||||
Serial.print(".");
|
||||
}
|
||||
Serial.println();
|
||||
if (ESP32_W5500_eth_connected)
|
||||
{
|
||||
Serial.println("DHCP OK!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.println("Set static IP");
|
||||
ETH.config(ip, gateway, subnet);
|
||||
}
|
||||
broadcastIp = ETH.broadcastIP().toString();
|
||||
|
||||
Serial.print("Local IP : ");
|
||||
Serial.println(ETH.localIP());
|
||||
Serial.print("Subnet Mask : ");
|
||||
Serial.println(ETH.subnetMask());
|
||||
Serial.print("Gateway IP : ");
|
||||
Serial.println(ETH.gatewayIP());
|
||||
Serial.print("DNS Server : ");
|
||||
Serial.println(ETH.dnsIP());
|
||||
Serial.print("MAC address : ");
|
||||
Serial.println(ETH.macAddress());
|
||||
Serial.print("Broadcast IP: ");
|
||||
Serial.println(broadcastIp);
|
||||
Serial.println("Ethernet Successfully Initialized");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Serial.println("Initialize as WiFi AccessPoint");
|
||||
WiFi.softAPsetHostname(hostname);
|
||||
WiFi.softAP(ssid, pwd);
|
||||
// AP always with DHCP
|
||||
// WiFi.softAPConfig(ip, gateway, subnet);
|
||||
broadcastIp = WiFi.softAPBroadcastIP().toString();
|
||||
Serial.print("WiFi AP enabled, IP = ");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
Serial.print("MAC address: ");
|
||||
Serial.println(WiFi.softAPmacAddress());
|
||||
Serial.print("Broadcast IP: ");
|
||||
Serial.println(broadcastIp);
|
||||
break;
|
||||
}
|
||||
|
||||
// Initialize DMX ports
|
||||
Serial.println("Initialize DMX...");
|
||||
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S2
|
||||
|
||||
dmx_config_t dmx_config = DMX_CONFIG_DEFAULT;
|
||||
dmx_personality_t personalities[] = {};
|
||||
int personality_count = 0;
|
||||
|
||||
dmx_driver_install(dmx1, &dmx_config, personalities, personality_count);
|
||||
dmx_set_pin(dmx1, 21, 33, -1);
|
||||
dmx_driver_install(dmx2, &dmx_config, personalities, personality_count);
|
||||
dmx_set_pin(dmx2, 17, 18, -1);
|
||||
|
||||
Serial.printf("DMX driver 1 installed: %d\n", dmx_driver_is_installed(dmx1));
|
||||
Serial.printf("DMX driver 2 installed: %d\n", dmx_driver_is_installed(dmx2));
|
||||
|
||||
Serial.printf("DMX driver 1 enabled: %d\n", dmx_driver_is_enabled(dmx1));
|
||||
Serial.printf("DMX driver 2 enabled: %d\n", dmx_driver_is_enabled(dmx2));
|
||||
|
||||
#else
|
||||
dmx1.init(21, 33, Serial1);
|
||||
dmx2.init(17, 18, Serial2);
|
||||
#endif
|
||||
|
||||
// Initialize Art-Net
|
||||
Serial.println("Initialize Art-Net...");
|
||||
artnet.begin();
|
||||
|
||||
// if Artnet packet comes to this universe, this function is called
|
||||
if (direction1 == Output)
|
||||
{
|
||||
artnet.subscribeArtDmxUniverse(universe1, [&](const uint8_t *data, uint16_t size, const ArtDmxMetadata &metadata, const ArtNetRemoteInfo &remote)
|
||||
{
|
||||
dmx_write_offset(dmx1, 1, data, size);
|
||||
dmx_send(dmx1);
|
||||
dmx_wait_sent(dmx1, DMX_TIMEOUT_TICK); });
|
||||
}
|
||||
|
||||
if (direction2 == Output)
|
||||
{
|
||||
artnet.subscribeArtDmxUniverse(universe2, [&](const uint8_t *data, uint16_t size, const ArtDmxMetadata &metadata, const ArtNetRemoteInfo &remote)
|
||||
{
|
||||
dmx_write_offset(dmx2, 1, data, size);
|
||||
dmx_send(dmx2);
|
||||
dmx_wait_sent(dmx2, DMX_TIMEOUT_TICK); });
|
||||
}
|
||||
|
||||
if (!LittleFS.begin(true))
|
||||
{
|
||||
Serial.println("An Error has occurred while mounting LittleFS");
|
||||
return;
|
||||
}
|
||||
|
||||
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
|
||||
|
||||
server.on("/config", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{ onGetConfig(request); });
|
||||
|
||||
server.on("/config", HTTP_DELETE, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
config.begin("dmx", false);
|
||||
config.clear();
|
||||
config.end();
|
||||
// respond with default config
|
||||
onGetConfig(request);
|
||||
|
||||
ESP.restart(); });
|
||||
|
||||
server.on("/networks", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{ onGetNetworks(request); });
|
||||
|
||||
server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
|
||||
{
|
||||
if (request->url() == "/config" && request->method() == HTTP_PUT) {
|
||||
onPutConfig(request, data, len, index, total);
|
||||
Serial.println("restarting ESP...");
|
||||
ESP.restart();
|
||||
} });
|
||||
|
||||
initWebSocket(&server);
|
||||
|
||||
server.begin();
|
||||
Serial.println("Server started!");
|
||||
|
||||
// scan networks and cache them
|
||||
WiFi.scanNetworks(true);
|
||||
|
||||
setStatus(Status::Normal);
|
||||
|
||||
// Internal temperature RP2040
|
||||
/*float tempC = analogReadTemp(); // Get internal temperature
|
||||
Serial.print("Temperature Celsius (ºC): ");
|
||||
Serial.println(tempC);*/
|
||||
// Internal temperature ESP32 https://www.espboards.dev/blog/esp32-inbuilt-temperature-sensor/
|
||||
Serial.print("Temperature: ");
|
||||
float result = 0;
|
||||
temp_sensor_read_celsius(&result);
|
||||
Serial.print(result);
|
||||
Serial.println(" °C");
|
||||
|
||||
Serial.printf("Internal Total heap %d, internal Free Heap %d\n", ESP.getHeapSize(), ESP.getFreeHeap());
|
||||
Serial.printf("SPIRam Total heap %d, SPIRam Free Heap %d\n", ESP.getPsramSize(), ESP.getFreePsram());
|
||||
Serial.printf("ChipRevision %d, Cpu Freq %d, SDK Version %s\n", ESP.getChipRevision(), ESP.getCpuFreqMHz(), ESP.getSdkVersion());
|
||||
Serial.printf("Flash Size %d, Flash Speed %d\n", ESP.getFlashChipSize(), ESP.getFlashChipSpeed());
|
||||
}
|
||||
|
||||
void transmitDmxToArtnet(dmx_port_t dmxPort, byte *dmx_data, uint8_t artnetUniverse)
|
||||
{
|
||||
/* We need a place to store information about the DMX packets we receive. We
|
||||
will use a dmx_packet_t to store that packet information. */
|
||||
dmx_packet_t dmx_packet;
|
||||
|
||||
// check if there's a new DMX packet
|
||||
if (dmx_receive(dmxPort, &dmx_packet, 0))
|
||||
{
|
||||
/* We should check to make sure that there weren't any DMX errors. */
|
||||
if (!dmx_packet.err)
|
||||
{
|
||||
/* Don't forget we need to actually read the DMX data into our buffer so
|
||||
that we can print it out. */
|
||||
dmx_read_offset(dmxPort, 1, dmx_data, dmx_packet.size);
|
||||
artnet.sendArtDmx(broadcastIp, artnetUniverse, dmx_data, dmx_packet.size);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Oops! A DMX error occurred! Don't worry, this can happen when you first
|
||||
connect or disconnect your DMX devices. If you are consistently getting
|
||||
DMX errors, then something may have gone wrong with your code or
|
||||
something is seriously wrong with your DMX transmitter. */
|
||||
Serial.printf("A DMX error occurred on port %d.\n", dmxPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
// only check for artnet packets if we expect to receive data
|
||||
if (direction1 == Output || direction2 == Output)
|
||||
{
|
||||
// check if artnet packet has come and execute callback
|
||||
artnet.parse();
|
||||
}
|
||||
|
||||
if (direction1 == Input)
|
||||
{
|
||||
transmitDmxToArtnet(dmx1, dmx1_data, universe1);
|
||||
}
|
||||
|
||||
if (direction2 == Input)
|
||||
{
|
||||
transmitDmxToArtnet(dmx2, dmx2_data, universe2);
|
||||
}
|
||||
|
||||
webSocketLoop();
|
||||
updateLed();
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
#include "config.h"
|
||||
#include <stdexcept>
|
||||
#include <ArduinoJson.h>
|
||||
#include "WiFi.h"
|
||||
|
||||
Preferences config;
|
||||
String DEFAULT_SSID = "";
|
||||
|
||||
#pragma region Utility
|
||||
|
||||
uint32_t parseIp(String str)
|
||||
{
|
||||
const int size = 4;
|
||||
|
||||
String ipStrings[size];
|
||||
uint8_t ipIndex = 0;
|
||||
|
||||
for (int i = 0; i < str.length(); i++)
|
||||
{
|
||||
if (str[i] == '.')
|
||||
{
|
||||
ipIndex++;
|
||||
continue;
|
||||
}
|
||||
ipStrings[ipIndex] += str[i];
|
||||
}
|
||||
|
||||
String ip = "";
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
String paddedString = ipStrings[i];
|
||||
while (paddedString.length() < 3)
|
||||
{
|
||||
paddedString = "0" + paddedString;
|
||||
}
|
||||
ip.concat(paddedString);
|
||||
}
|
||||
|
||||
Serial.println("ip string: " + ip);
|
||||
return atoi(ip.c_str());
|
||||
}
|
||||
|
||||
IpMethod parseIpMethod(uint8_t ipMethod)
|
||||
{
|
||||
if (ipMethod > 0 || ipMethod < IP_METHOD_SIZE)
|
||||
{
|
||||
return static_cast<IpMethod>(ipMethod);
|
||||
}
|
||||
|
||||
throw ::std::invalid_argument("Invalid IP method value" + ipMethod);
|
||||
}
|
||||
|
||||
Connection parseConnection(uint8_t connection)
|
||||
{
|
||||
if (connection > 0 || connection < CONNECTION_SIZE)
|
||||
{
|
||||
return static_cast<Connection>(connection);
|
||||
}
|
||||
|
||||
throw ::std::invalid_argument("Invalid connection value: " + connection);
|
||||
}
|
||||
|
||||
Direction parseDirection(uint8_t direction)
|
||||
{
|
||||
if (direction > 0 || direction < DIRECTION_SIZE)
|
||||
{
|
||||
return static_cast<Direction>(direction);
|
||||
}
|
||||
|
||||
throw ::std::invalid_argument("Invalid direction value: " + direction);
|
||||
}
|
||||
|
||||
ButtonAction parseButtonAction(uint8_t buttonAction)
|
||||
{
|
||||
if (buttonAction > 0 || buttonAction < BUTTON_ACTION_SIZE)
|
||||
{
|
||||
return static_cast<ButtonAction>(buttonAction);
|
||||
}
|
||||
|
||||
throw ::std::invalid_argument("Invalid value for button action: " + buttonAction);
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
void onGetConfig(AsyncWebServerRequest *request)
|
||||
{
|
||||
config.begin("dmx", true);
|
||||
|
||||
IPAddress ip = config.getUInt("ip", DEFAULT_IP);
|
||||
IPAddress subnet = config.getUInt("subnet", DEFAULT_SUBNET);
|
||||
IPAddress gateway = config.getUInt("gateway", 0);
|
||||
|
||||
JsonDocument doc;
|
||||
doc["connection"] = config.getUInt("connection", DEFAULT_CONNECTION);
|
||||
doc["ssid"] = config.getString("ssid", DEFAULT_SSID);
|
||||
doc["password"] = config.getString("password", DEFAULT_PASSWORD);
|
||||
doc["ip-method"] = config.getUInt("ip-method", DEFAULT_IP_METHOD);
|
||||
doc["ip"] = ip.toString();
|
||||
doc["subnet"] = subnet.toString();
|
||||
doc["gateway"] = gateway != 0 ? gateway.toString() : "";
|
||||
doc["universe-1"] = config.getUInt("universe-1", DEFAULT_UNIVERSE1);
|
||||
doc["direction-1"] = config.getUInt("direction-1", DEFAULT_DIRECTION1);
|
||||
doc["universe-2"] = config.getUInt("universe-2", DEFAULT_UNIVERSE2);
|
||||
doc["direction-2"] = config.getUInt("direction-2", DEFAULT_DIRECTION2);
|
||||
doc["led-brightness"] = config.getUInt("led-brightness", DEFAULT_LED_BRIGHTNESS);
|
||||
doc["button-action"] = config.getUInt("button-action", DEFAULT_BUTTON_ACTION);
|
||||
|
||||
config.end();
|
||||
|
||||
String jsonString;
|
||||
serializeJson(doc, jsonString);
|
||||
|
||||
request->send(200, "application/json", jsonString);
|
||||
}
|
||||
|
||||
void onPutConfig(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
|
||||
{
|
||||
Serial.printf("[REQUEST]\t%s\r\n", (const char *)data);
|
||||
|
||||
JsonDocument doc;
|
||||
deserializeJson(doc, data);
|
||||
|
||||
try
|
||||
{
|
||||
config.begin("dmx", false);
|
||||
|
||||
IpMethod ipMethod = parseIpMethod(doc["ip-method"].as<uint8_t>());
|
||||
config.putUInt("ip-method", ipMethod);
|
||||
|
||||
if (ipMethod == Static)
|
||||
{
|
||||
IPAddress ipAddress;
|
||||
ipAddress.fromString(doc["ip"].as<String>());
|
||||
config.putUInt("ip", ipAddress);
|
||||
|
||||
IPAddress subnet;
|
||||
subnet.fromString(doc["subnet"].as<String>());
|
||||
config.putUInt("subnet", subnet);
|
||||
|
||||
IPAddress gateway;
|
||||
gateway.fromString(doc["gateway"].as<String>());
|
||||
config.putUInt("gateway", gateway);
|
||||
}
|
||||
|
||||
Connection connection = parseConnection(doc["connection"].as<uint8_t>());
|
||||
config.putUInt("connection", connection);
|
||||
|
||||
if (connection == WiFiSta || connection == WiFiAP)
|
||||
{
|
||||
config.putString("ssid", doc["ssid"].as<String>());
|
||||
config.putString("password", doc["password"].as<String>());
|
||||
}
|
||||
|
||||
Direction direction1 = parseDirection(doc["direction-1"].as<uint8_t>());
|
||||
config.putUInt("direction-1", direction1);
|
||||
|
||||
Direction direction2 = parseDirection(doc["direction-2"].as<uint8_t>());
|
||||
config.putUInt("direction-2", direction2);
|
||||
|
||||
config.putUInt("universe-1", doc["universe-1"]);
|
||||
config.putUInt("universe-2", doc["universe-2"]);
|
||||
|
||||
config.putUInt("led-brightness", doc["led-brightness"]);
|
||||
|
||||
ButtonAction buttonAction = parseButtonAction(doc["button-action"].as<uint8_t>());
|
||||
config.putUInt("button-action", buttonAction);
|
||||
|
||||
config.end();
|
||||
|
||||
request->send(200);
|
||||
}
|
||||
catch (::std::invalid_argument &e)
|
||||
{
|
||||
config.end();
|
||||
request->send(400, "text/plain", e.what());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
#include <AsyncWebServer_ESP32_W5500.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
#ifndef CONFIG_h
|
||||
#define CONFIG_h
|
||||
|
||||
extern Preferences config;
|
||||
|
||||
enum IpMethod
|
||||
{
|
||||
Static,
|
||||
DHCP
|
||||
};
|
||||
const uint8_t IP_METHOD_SIZE = 2;
|
||||
|
||||
enum Connection
|
||||
{
|
||||
WiFiSta,
|
||||
WiFiAP,
|
||||
Ethernet
|
||||
};
|
||||
const uint8_t CONNECTION_SIZE = 3;
|
||||
|
||||
enum Direction
|
||||
{
|
||||
Output,
|
||||
Input
|
||||
};
|
||||
const uint8_t DIRECTION_SIZE = 2;
|
||||
|
||||
enum ButtonAction
|
||||
{
|
||||
None,
|
||||
ResetConfig,
|
||||
Restart
|
||||
};
|
||||
const uint8_t BUTTON_ACTION_SIZE = 3;
|
||||
|
||||
const Connection DEFAULT_CONNECTION = WiFiAP;
|
||||
const IpMethod DEFAULT_IP_METHOD = DHCP;
|
||||
extern String DEFAULT_SSID; // initialized in setup because it depends on the mac address
|
||||
const String DEFAULT_PASSWORD = "mbgmbgmbg";
|
||||
const IPAddress DEFAULT_IP(192, 168, 4, 1);
|
||||
const IPAddress DEFAULT_SUBNET(255, 255, 255, 0);
|
||||
const IPAddress DEFAULT_GATEWAY(2, 0, 0, 1);
|
||||
|
||||
const Direction DEFAULT_DIRECTION1 = Output;
|
||||
const Direction DEFAULT_DIRECTION2 = Input;
|
||||
const uint8_t DEFAULT_UNIVERSE1 = 1;
|
||||
const uint8_t DEFAULT_UNIVERSE2 = 2;
|
||||
|
||||
const uint8_t DEFAULT_LED_BRIGHTNESS = 25;
|
||||
const ButtonAction DEFAULT_BUTTON_ACTION = Restart;
|
||||
|
||||
void onGetConfig(AsyncWebServerRequest *request);
|
||||
|
||||
void onPutConfig(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
#include "networks.h"
|
||||
|
||||
void onGetNetworks(AsyncWebServerRequest *request)
|
||||
{
|
||||
JsonDocument doc;
|
||||
JsonArray array = doc.to<JsonArray>();
|
||||
|
||||
int numberOfNetworks = WiFi.scanComplete();
|
||||
if (numberOfNetworks == WIFI_SCAN_FAILED)
|
||||
{
|
||||
WiFi.scanNetworks(true);
|
||||
}
|
||||
else if (numberOfNetworks)
|
||||
{
|
||||
for (int i = 0; i < numberOfNetworks; ++i)
|
||||
{
|
||||
array.add(WiFi.SSID(i));
|
||||
}
|
||||
WiFi.scanDelete();
|
||||
if (WiFi.scanComplete() == WIFI_SCAN_FAILED)
|
||||
{
|
||||
WiFi.scanNetworks(true);
|
||||
}
|
||||
}
|
||||
|
||||
String jsonString;
|
||||
serializeJson(doc, jsonString);
|
||||
request->send(200, "application/json", jsonString);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#include <AsyncWebServer_ESP32_W5500.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#ifndef NETWORKS_H
|
||||
#define NETWORKS_H
|
||||
|
||||
void onGetNetworks(AsyncWebServerRequest *request);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
#include "status.h"
|
||||
|
||||
int getTemperature()
|
||||
{
|
||||
float tempC = -1.0f;
|
||||
temp_sensor_read_celsius(&tempC);
|
||||
return static_cast<int>(round(tempC));
|
||||
}
|
||||
|
||||
int8_t getWiFiStrength()
|
||||
{
|
||||
try
|
||||
{
|
||||
return WiFi.RSSI();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
JsonDocument buildStatusJson()
|
||||
{
|
||||
JsonDocument doc;
|
||||
|
||||
doc["uptime"] = millis();
|
||||
doc["chip"]["model"] = ESP.getChipModel();
|
||||
doc["chip"]["mac"] = ESP.getEfuseMac();
|
||||
doc["chip"]["revision"] = ESP.getChipRevision();
|
||||
doc["chip"]["cpuFreqMHz"] = ESP.getCpuFreqMHz();
|
||||
doc["chip"]["cycleCount"] = ESP.getCycleCount();
|
||||
doc["chip"]["tempC"] = getTemperature();
|
||||
doc["sdkVersion"] = ESP.getSdkVersion();
|
||||
doc["sketch"]["size"] = ESP.getSketchSize();
|
||||
doc["sketch"]["md5"] = ESP.getSketchMD5();
|
||||
doc["heap"]["free"] = ESP.getFreeHeap();
|
||||
doc["heap"]["total"] = ESP.getHeapSize();
|
||||
doc["psram"]["free"] = ESP.getFreePsram();
|
||||
doc["psram"]["total"] = ESP.getPsramSize();
|
||||
doc["connection"]["signalStrength"] = getWiFiStrength();
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#include <AsyncWebServer_ESP32_W5500.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <driver/temp_sensor.h>
|
||||
|
||||
JsonDocument buildStatusJson();
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#include "websocket.h"
|
||||
|
||||
AsyncWebSocket ws("/ws");
|
||||
|
||||
long webSocketLastUpdate = 0;
|
||||
const int WS_UPDATE_INTERVAL = 10 * 1000; // 10 seconds
|
||||
|
||||
String buildStatusString()
|
||||
{
|
||||
JsonDocument doc;
|
||||
doc["type"] = "status";
|
||||
doc["data"] = buildStatusJson();
|
||||
|
||||
String jsonString = "";
|
||||
serializeJson(doc, jsonString);
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case WS_EVT_CONNECT:
|
||||
Serial.printf("[WS] Client %u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
|
||||
// directly send status to client
|
||||
ws.text(client->id(), buildStatusString());
|
||||
break;
|
||||
case WS_EVT_DISCONNECT:
|
||||
Serial.printf("[WS] Client %u disconnected\n", client->id());
|
||||
break;
|
||||
case WS_EVT_DATA:
|
||||
Serial.printf("[WS] Data received from client %u: %s\n", client->id(), (char *)data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void webSocketLoop()
|
||||
{
|
||||
if (millis() - webSocketLastUpdate > WS_UPDATE_INTERVAL)
|
||||
{
|
||||
ws.textAll(buildStatusString());
|
||||
webSocketLastUpdate = millis();
|
||||
}
|
||||
}
|
||||
|
||||
void initWebSocket(AsyncWebServer *server)
|
||||
{
|
||||
ws.onEvent(onEvent);
|
||||
server->addHandler(&ws);
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#include <AsyncWebServer_ESP32_W5500.h>
|
||||
#include "routes/status.h"
|
||||
|
||||
#ifndef WEBSOCKET_H
|
||||
#define WEBSOCKET_H
|
||||
|
||||
void initWebSocket(AsyncWebServer *server);
|
||||
|
||||
void webSocketLoop();
|
||||
|
||||
#endif
|
||||
116
tasks.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from invoke import task
|
||||
from invoke.exceptions import Exit
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import webbrowser
|
||||
|
||||
|
||||
@task
|
||||
def cleanbuild(c):
|
||||
"""Clean build: fullclean and build the project"""
|
||||
c.run("idf.py fullclean")
|
||||
c.run("idf.py build")
|
||||
|
||||
|
||||
@task
|
||||
def build(c):
|
||||
"""Build the project"""
|
||||
c.run("idf.py build")
|
||||
|
||||
|
||||
@task
|
||||
def flash(c):
|
||||
"""Flash the project to device"""
|
||||
c.run("idf.py flash")
|
||||
|
||||
|
||||
@task
|
||||
def monitor(c, port="/dev/ttyUSB0"):
|
||||
"""Monitor serial output from device"""
|
||||
c.run(f"idf.py monitor -p {port}")
|
||||
|
||||
|
||||
@task
|
||||
def run(c, port="/dev/ttyUSB0"):
|
||||
"""Build, flash, and monitor in sequence"""
|
||||
build(c)
|
||||
flash(c)
|
||||
monitor(c, port)
|
||||
|
||||
|
||||
@task
|
||||
def clean(c):
|
||||
"""Clean build artifacts"""
|
||||
c.run("idf.py fullclean")
|
||||
|
||||
|
||||
@task
|
||||
def config(c):
|
||||
"""Open menuconfig to edit project settings"""
|
||||
is_windows = os.name == "nt"
|
||||
if is_windows:
|
||||
# Windows doesn't provide a POSIX pty, not sure how to open menuconfig interactive
|
||||
print(
|
||||
"Please run 'idf.py menuconfig' directly in the terminal to edit project settings. This option is not supported in the invoke task on Windows."
|
||||
)
|
||||
else:
|
||||
c.run("idf.py menuconfig --color-scheme=monochrome", pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def saveconfig(c):
|
||||
"""Save current config as sdkconfig.defaults"""
|
||||
c.run("idf.py save-defconfig")
|
||||
|
||||
|
||||
@task
|
||||
def update(c):
|
||||
"""Update project dependencies"""
|
||||
c.run("idf.py update-dependencies")
|
||||
|
||||
|
||||
@task
|
||||
def reset(c):
|
||||
"""Reset project to clean state: remove build, config, and managed components"""
|
||||
if os.path.exists("sdkconfig"):
|
||||
os.remove("sdkconfig")
|
||||
if os.path.exists("sdkconfig.old"):
|
||||
os.remove("sdkconfig.old")
|
||||
if os.path.exists("build"):
|
||||
shutil.rmtree("build")
|
||||
if os.path.exists("managed_components"):
|
||||
shutil.rmtree("managed_components")
|
||||
|
||||
|
||||
@task
|
||||
def format(c):
|
||||
"""Format all source files using pre-commit hooks"""
|
||||
|
||||
is_windows = os.name == "nt"
|
||||
if is_windows:
|
||||
# Windows doesn't provide a POSIX pty
|
||||
c.run("pre-commit run --all-files")
|
||||
else:
|
||||
c.run("pre-commit run --all-files", pty=True)
|
||||
|
||||
|
||||
@task(help={"o": "Open documentation in the default browser after generation."})
|
||||
def docs(c, o=False):
|
||||
"""Generate Doxygen documentation."""
|
||||
proc = subprocess.run("doxygen Doxyfile", shell=True)
|
||||
if proc.returncode == 0:
|
||||
path = "docs/doxygen/html/index.html"
|
||||
print(f"\n✓ Documentation generated in {path}")
|
||||
if o:
|
||||
webbrowser.open(f"file://{os.path.abspath(path)}")
|
||||
return
|
||||
raise Exit(code=1)
|
||||
|
||||
|
||||
@task
|
||||
def docs_coverage(c):
|
||||
"""List doxygen coverage of documentation."""
|
||||
subprocess.run(
|
||||
"python tools/doxy-coverage.py docs/doxygen/xml --no-error", shell=True
|
||||
)
|
||||
244
tools/doxy-coverage.py
Executable file
|
|
@ -0,0 +1,244 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
|
||||
# Based on https://github.com/davatorium/doxy-coverage
|
||||
# which was forked from https://github.com/alobbs/doxy-coverage
|
||||
# and modified for use in this project.
|
||||
|
||||
# All files in doxy-coverage are Copyright 2014 Alvaro Lopez Ortega.
|
||||
#
|
||||
# Authors:
|
||||
# * Alvaro Lopez Ortega <alvaro@gnu.org>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
__author__ = "Alvaro Lopez Ortega"
|
||||
__email__ = "alvaro@alobbs.com"
|
||||
__copyright__ = "Copyright (C) 2014 Alvaro Lopez Ortega"
|
||||
|
||||
from filecmp import cmp
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Defaults
|
||||
ACCEPTABLE_COVERAGE = 80
|
||||
|
||||
# Global
|
||||
ns = None
|
||||
|
||||
|
||||
def ERROR(*objs):
|
||||
print("ERROR: ", *objs, end="\n", file=sys.stderr)
|
||||
|
||||
|
||||
def FATAL(*objs):
|
||||
ERROR(*objs)
|
||||
sys.exit(0 if ns.no_error else 1)
|
||||
|
||||
|
||||
def generate_docs():
|
||||
print("Generating Doxygen documentation...")
|
||||
proc = subprocess.run(
|
||||
"doxygen Doxyfile",
|
||||
shell=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
print("Documentation generated")
|
||||
else:
|
||||
FATAL("Failed to generate documentation. Exiting.")
|
||||
|
||||
|
||||
def parse_file(fullpath):
|
||||
tree = ET.parse(fullpath)
|
||||
|
||||
sourcefile = None
|
||||
definitions = {}
|
||||
|
||||
for definition in tree.findall("./compounddef//memberdef"):
|
||||
# Should it be documented
|
||||
if definition.get("kind") == "function" and definition.get("static") == "yes":
|
||||
continue
|
||||
|
||||
# Is the definition documented?
|
||||
documented = False
|
||||
for k in ("briefdescription", "detaileddescription", "inbodydescription"):
|
||||
if definition.findall(f"./{k}/"):
|
||||
documented = True
|
||||
break
|
||||
|
||||
# Name
|
||||
d_def = definition.find("./definition")
|
||||
d_nam = definition.find("./name")
|
||||
|
||||
if not sourcefile:
|
||||
l = definition.find("./location")
|
||||
if l is not None:
|
||||
sourcefile = l.get("file")
|
||||
|
||||
if d_def is not None:
|
||||
name = d_def.text
|
||||
elif d_nam is not None:
|
||||
name = d_nam.text
|
||||
else:
|
||||
name = definition.get("id")
|
||||
|
||||
# Aggregate
|
||||
definitions[name] = documented
|
||||
|
||||
if not sourcefile:
|
||||
sourcefile = fullpath
|
||||
|
||||
return (sourcefile, definitions)
|
||||
|
||||
|
||||
def parse(path):
|
||||
index_fp = os.path.join(path, "index.xml")
|
||||
if not os.path.exists(index_fp):
|
||||
FATAL("Documentation not present. Exiting.", index_fp)
|
||||
|
||||
tree = ET.parse(index_fp)
|
||||
|
||||
files = {}
|
||||
for entry in tree.findall("compound"):
|
||||
if entry.get("kind") == "dir":
|
||||
continue
|
||||
|
||||
file_fp = os.path.join(path, f"{entry.get('refid')}.xml")
|
||||
sourcefile, definitions = parse_file(file_fp)
|
||||
|
||||
if definitions != {}:
|
||||
files[sourcefile] = definitions
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def report(files, include_files, summary_only):
|
||||
files_sorted = sorted(files.keys())
|
||||
|
||||
if len(include_files) != 0:
|
||||
files_sorted = list(filter(lambda f: f in include_files, files_sorted))
|
||||
|
||||
if len(files_sorted) == 0:
|
||||
FATAL("No files to report on. Exiting.")
|
||||
|
||||
files_sorted.reverse()
|
||||
|
||||
total_yes = 0
|
||||
total_no = 0
|
||||
|
||||
for f in files_sorted:
|
||||
defs = files[f]
|
||||
|
||||
doc_yes = len([d for d in defs.values() if d])
|
||||
doc_no = len([d for d in defs.values() if not d])
|
||||
doc_per = doc_yes * 100.0 / (doc_yes + doc_no)
|
||||
|
||||
total_yes += doc_yes
|
||||
total_no += doc_no
|
||||
|
||||
if not summary_only:
|
||||
print(f"{int(doc_per):3d}% - {f} - ({doc_yes} of {doc_yes + doc_no})")
|
||||
|
||||
if None in defs:
|
||||
del defs[None]
|
||||
if not summary_only:
|
||||
defs_sorted = sorted(defs.keys())
|
||||
for d in defs_sorted:
|
||||
if not defs[d]:
|
||||
print("\t", d)
|
||||
|
||||
total_all = total_yes + total_no
|
||||
total_percentage = total_yes * 100 / total_all
|
||||
print(
|
||||
f"{"" if summary_only else "\n"}{int(total_percentage)}% API documentation coverage"
|
||||
)
|
||||
return (ns.threshold - total_percentage, 0)[total_percentage > ns.threshold]
|
||||
|
||||
|
||||
def main():
|
||||
# Arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"dir", action="store", help="Path to Doxygen's XML doc directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"include_files",
|
||||
action="extend",
|
||||
nargs="*",
|
||||
help="List of files to check coverage for (Default: all files)",
|
||||
type=str,
|
||||
default=[],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-error",
|
||||
action="store_true",
|
||||
help="Do not return error code after execution",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--summary-only",
|
||||
action="store_true",
|
||||
help="Only print the summary of the coverage report, without listing the coverage of each file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--threshold",
|
||||
action="store",
|
||||
help=f"Min acceptable coverage percentage (Default: {ACCEPTABLE_COVERAGE})",
|
||||
default=ACCEPTABLE_COVERAGE,
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--generate-docs",
|
||||
action="store_true",
|
||||
help="Generate Doxygen documentation before checking coverage",
|
||||
)
|
||||
|
||||
global ns
|
||||
ns = parser.parse_args()
|
||||
if not ns:
|
||||
FATAL("ERROR: Couldn't parse parameters")
|
||||
|
||||
if ns.generate_docs:
|
||||
generate_docs()
|
||||
|
||||
# Parse
|
||||
files = parse(ns.dir)
|
||||
|
||||
# Print report
|
||||
err = report(files, ns.include_files, ns.summary_only)
|
||||
if ns.no_error:
|
||||
return
|
||||
|
||||
sys.exit(round(err))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||