blinkenstrip

Documentation: http://frombelow.net/projects/blinkenstrip/
Clone: git clone https://git.frombelow.net/blinkenstrip.git
Log | Files | Refs | README | LICENSE

commit 8f3bce87434cf9d20eb10effbe5e73e3535e1a2e
Author: Gerd Beuster <gerd@frombelow.net>
Date:   Tue, 23 Mar 2021 22:31:24 +0100

First public version

Diffstat:
A.gitignore | 4++++
ACMakeLists.txt | 6++++++
ALICENSE | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AMakefile | 9+++++++++
AREADME.md | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acase/case.scad | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/CMakeLists.txt | 6++++++
Amain/Kconfig.projbuild | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/component.mk | 5+++++
Amain/debug.h | 28++++++++++++++++++++++++++++
Amain/html_templates/config.html | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/html_templates/index.html | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/html_templates/reset.html | 22++++++++++++++++++++++
Amain/led.c | 538+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/led.h | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/led_strip.c | 461+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/led_strip.h | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/main.c | 387+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/main.h | 42++++++++++++++++++++++++++++++++++++++++++
Amain/webserver.c | 306+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain/webserver.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
21 files changed, 2813 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +build/ +sdkconfig +sdkconfig.old+ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(ledstrip) diff --git a/LICENSE b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile @@ -0,0 +1,9 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := ledstrip + +include $(IDF_PATH)/make/project.mk + diff --git a/README.md b/README.md @@ -0,0 +1,104 @@ +Blinkenstrip +------------ + +# Overview + +Shows effects on a WS2811 connected to an ESP32. The ESP runs a +minimal web server allowing to control the effects. The system is +highly configurable: It can show effects both on LED strips of +arbitrary length and on LEDs arranged as a matrix. + +# Installation & Operation + +Connect the data line of the WS2811 strip to pin 4 of the +ESP32. Install the software the usual way. When powered up, the ESP +spawns an AP with ESSID "blinkenstrip" and password +"blinkenstrip". Connect to the AP and access the web interface at +<http://192.168.4.1>. <http://192.168.4.1/config.html> gives access to +system configuration. Here you can set the layout of your +strip/matrix, and configure the system to connect to an existing +network instead of an access point. If you configure your ESP to join +an existing network and don't know its IP address, run `idf.py +monitor` and check the debug output. + +# Effects + +- Sequence + + All LEDs show the same, changing color. This is a nice effect if you + want to illuminate an object. + +- Blink + + Individual LEDs switch randomly between blue and green color. This + effect looks quite nice both on LED strips and on matrices. + +- Running Dot + + An individual red dot "running" on a blue backdrop. + +- Cellular Automaton + + Conway's game of life. While designed for LED matrices, this effect + also look surpringsly nice on strips. + +- Changing lines + + This is more of a test effect for LED matrices. + +- Worms + + A configurable number of worms eating blue dots. This is the + recommended effect for LED strips. The number of worms can be set in + the configuration menu. + +# Changing defaults + +If you want different default values on first startup (i.e. before +accessing <http://192.168.4.1/config.html>), you can set different +values in sub-menu "Blinkenstrip" of the system configuration executed by +`idfy.py menuconfig`. + + +# Factory reset & Hardware setup + +If pins 12 and 13 are connected on reset, the user configuration is +deleted and the factory default values are loaded. + +For installations with a large number of LEDs, it is recommend to +chain a suitable fuse (e.g. 6 A for 250 LEDs) into the power line of +the LED strip/matrix. + +Directory `case/` provides an OpenSCAD file for a printable case. This +case is designed to host a ridiciously overpowered 110 W Mean Well +LRS-150F-5 power supply. + +# Bugs + +HTML characters in names (e.g. spaces) are not translated to ASCII. + +## Changelog + +- v1.0 +First public version. + +## Copyright and License + +main/led_strip.(h|c) © 2016 by Lucas Bruder. Parts of +main/webserver.c © 2016, 2020 Espressif Systems. + +Everything else: + +Copyright © 2021 Gerd Beuster <gerd@frombelow.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/case/case.scad b/case/case.scad @@ -0,0 +1,250 @@ +// Dimensions of the power supply (+ 1 mm on each side) +power_supply_width = 97 + 1; +power_supply_depth = 159 + 1; +power_supply_height = 30 + 1; + +// Additional depth for the MCU compartment +mcu_compartment_depth = 60; +mcu_depth = 15; +mcu_width=40; +mcu_height=5; + +// Dimensions of power switch (+ 1 mm) +power_switch_width = 18 + 1; +power_switch_height = 12 + 1; + +// Diameters of cables ( + 2 mm) +diameter_power_cable = 6 + 2; +diameter_data_cable = 5 + 2; +diameter_cord_grip_screw = 5 + 2; + +// Thickness of walls, guide rails, etc. +wall_thickness = 2; + +// Interiour width must be big enough for power supply plus guide rails for +// lid. +inner_frame_width = power_supply_width + 2 * wall_thickness; +inner_frame_depth = power_supply_depth + mcu_compartment_depth; +// No wall on top, because we will place the lid there. +inner_frame_height = power_supply_height + 2 * wall_thickness; + +// Create walls around empty box of given size. The top is left open. +// Front does not have full height in order to leave room for the lid. +module Frame(width, depth, height, wall_thickness) { + // Add walls to inner frame + outer_frame_width = width + 2 * wall_thickness; + outer_frame_depth = depth + 2 * wall_thickness; + outer_frame_height = height + 1 * wall_thickness; + // Construct frame + difference() { + cube([outer_frame_width, outer_frame_depth, outer_frame_height]); + translate([wall_thickness, wall_thickness, wall_thickness]) + cube([width, depth, height]); + translate([wall_thickness, 0, outer_frame_height - wall_thickness * 2 ]) + cube([width, wall_thickness, wall_thickness * 2]); + } +} + +// Guide rails for lid and supporting structures. +module Guide_Rails(width, depth, height, wall_thickness) { + // Left top + translate([wall_thickness, 0, height]) + cube([wall_thickness, depth + wall_thickness, wall_thickness]); + // Left bottom + translate([wall_thickness, 0, height - 2 * wall_thickness]) + cube([wall_thickness, depth + wall_thickness, wall_thickness]); + // Right top + translate([width, 0, height]) + cube([wall_thickness, depth + wall_thickness, wall_thickness]); + // Right bottom + translate([width, 0, height - 2 * wall_thickness]) + cube([wall_thickness, depth + wall_thickness, wall_thickness]); + // Back top + translate([wall_thickness, depth, height]) + cube([width, wall_thickness, wall_thickness]); + // Back bottom + translate([wall_thickness, depth, height - 2 * wall_thickness]) + cube([width, wall_thickness, wall_thickness]); + // Support structure back + translate([width/3, depth, wall_thickness]) + cube([wall_thickness, wall_thickness, height - 3 * wall_thickness]); + translate([width/3*2, depth, wall_thickness]) + cube([wall_thickness, wall_thickness, height - 3 * wall_thickness]); + // Support structure left side + translate([wall_thickness, depth-depth/4, wall_thickness]) + cube([wall_thickness, wall_thickness, height - 3 * wall_thickness]); + translate([wall_thickness, depth-depth/4 * 2, wall_thickness]) + cube([wall_thickness, wall_thickness, height - 3 * wall_thickness]); + translate([wall_thickness, depth-depth/4 * 3, wall_thickness]) + cube([wall_thickness, wall_thickness, height - 3 * wall_thickness]); + // Support structure right side + translate([width, depth-depth/4, wall_thickness]) + cube([wall_thickness, wall_thickness, height - 3 * wall_thickness]); + translate([width, depth-depth/4 * 2, wall_thickness]) + cube([wall_thickness, wall_thickness, height - 3 * wall_thickness]); + translate([width, depth-depth/4 * 3, wall_thickness]) + cube([wall_thickness, wall_thickness, height - 3 * wall_thickness]); + // Fixation of power supply + translate([wall_thickness, + depth - (wall_thickness + power_supply_depth), + wall_thickness]) + cube([width, wall_thickness, wall_thickness]); +} + +module Frame_with_Rails(width, depth, height, wall_thickness) { + union () { + Frame(width=width, depth=depth, height=height, wall_thickness=wall_thickness); + Guide_Rails(width=width, depth=depth, height=height, wall_thickness=wall_thickness); + } +} + +// Holes for cords and power switch +module Frame_with_Rails_and_Holes(width, depth, height, wall_thickness) { + difference() { + Frame_with_Rails(width=width, depth=depth, height=height, + wall_thickness=wall_thickness); + // Hole for power switch + translate([width/3 , 0, height /3]) + cube([power_switch_width, wall_thickness, power_switch_height]); + // Hole for power cable + translate([0 , depth/11, height /4]) + rotate([0, 90, 0]) cylinder(d=diameter_power_cable, h=wall_thickness+1, + center=false); + // Hole for data cable + translate([0 , depth/5.5, height /4]) + rotate([0, 90, 0]) cylinder(d=diameter_data_cable, h=wall_thickness+1, + center=false); + } +} + +module Frame_with_Air_Vents(width, depth, height, wall_thickness) { + difference() { + Frame_with_Rails_and_Holes(width=width, depth=depth, height=height, + wall_thickness=wall_thickness); + // Left side vents + for(vent_depth = [depth/40*11+wall_thickness: depth/40: depth-wall_thickness*2]) + translate([0, vent_depth, 2*wall_thickness]) + cube([wall_thickness*2, depth/80, height/3*2]); + // Right side vents + for(vent_depth = [depth/40*11+wall_thickness: depth/40: depth-wall_thickness*2]) + translate([width, vent_depth, 2*wall_thickness]) + cube([wall_thickness*2, depth/80, height/3*2]); + // Back side vents + for(vent_width = [wall_thickness*3: width/20: width-wall_thickness*2]) + translate([vent_width, depth, 2*wall_thickness]) + cube([width/50, wall_thickness*2, height/3*2]); + } +} +// Support structure for cord grip for power supply +module Cord_Grip_for_Power_Supply(width, depth, height, wall_thickness, + diameter_power_cable, + diameter_cord_grip_screw) { +// Cord grip for power supply + translate([wall_thickness, + depth/10 + diameter_power_cable/2 + wall_thickness, + wall_thickness]) + difference () { + cube([width/5, wall_thickness, height-2*wall_thickness]); + translate([width/7, + wall_thickness, + height/2]) + rotate([90, 0, 0]) cylinder(d=diameter_cord_grip_screw, + h=wall_thickness+2, + center=true); + } + +} + +// Mount for MCU +module MCU_Mount(width, depth, mcu_width, mcu_depth, mcu_compartment_depth, + wall_thickness) { + translate([width-mcu_width+wall_thickness, + (mcu_compartment_depth-wall_thickness-mcu_depth)/1.5, + wall_thickness+mcu_height]) + cube([mcu_width, mcu_depth, wall_thickness]); +} + + +// Initials and year +module Signature(width, wall_thickness) { +translate([width+wall_thickness*2, wall_thickness*2, wall_thickness*2]) rotate([90, 0, 90]) linear_extrude(1) text(text="gb 2020", size=5, font="Arial Black"); +} + + +// Complete Box +module Box(){ + Frame_with_Air_Vents(width=inner_frame_width, + depth=inner_frame_depth, + height=inner_frame_height, + wall_thickness=wall_thickness); + + Cord_Grip_for_Power_Supply(width=inner_frame_width, + depth=inner_frame_depth, + height=inner_frame_height, + wall_thickness=wall_thickness, + diameter_power_cable=diameter_power_cable, + diameter_cord_grip_screw=diameter_cord_grip_screw); + + MCU_Mount(width=inner_frame_width, depth=inner_frame_depth, + mcu_width=mcu_width, mcu_depth=mcu_depth, + mcu_compartment_depth=mcu_compartment_depth, + wall_thickness=wall_thickness); + + Signature(width=inner_frame_width, wall_thickness=wall_thickness); +} + +// Lid for the box +// (rails leave 0.5 mm on each side) +module Lid(){ +translate([wall_thickness+0.5, 0, inner_frame_height-wall_thickness]) + cube([inner_frame_width-1, inner_frame_depth+wall_thickness-1.5, wall_thickness]); +translate([wall_thickness*2, 0, inner_frame_height]) + cube([inner_frame_width-2*wall_thickness, + inner_frame_depth-1, + wall_thickness]); +} + + +// Power supply (used for debugging) +module Power_Supply() { + translate([wall_thickness*2, mcu_compartment_depth, wall_thickness]) + cube([power_supply_width, power_supply_depth, power_supply_height]); +} + +// MCU (used for debugging) +module MCU() { + translate([inner_frame_width-mcu_width+wall_thickness, (mcu_compartment_depth-mcu_depth-wall_thickness)/2, wall_thickness]) + cube([mcu_width, mcu_depth, mcu_height]); +} + +// Sanity check: Box, lid, power supply, and MCU must not overlap +module Check_for_Overlaps() { + intersection() { + Box(); + Lid(); + } + intersection() { + Box(); + Power_Supply(); + } + intersection() { + Power_Supply(); + Lid(); + } + intersection() { + Box(); + MCU(); + } +} + +module Render_All() { + Box(); + translate([0, 0, 40]) + Lid(); +} + +// Check_for_Overlaps(); +// Box(); +// Lid(); +Render_All(); + diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register( + SRCS "main.c" + "led.c" + "led_strip.c" + "webserver.c" + INCLUDE_DIRS "") diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild @@ -0,0 +1,102 @@ +menu "Blinkenstrip" + +config WS2811_DATA_PIN + int "LED Strip Signal Line" + default 4 + range 2 35 + help + Pin D2 on the ESP-WROOM-32 board is 2. + Pin D4 on the ESP-WROOM-32 board is 4. + ... + +config PIN_FACTORY_RESET_A + int "First pin for factory reset" + default 12 + range 2 35 + help + When factory reset pins A and B are connected on power-up, + the device resets to factory settings. + +config PIN_FACTORY_RESET_B + int "Second pin for factory reset" + default 13 + range 2 35 + help + When factory reset pins A and B are connected on power-up, + the device resets to factory settings. + +config APP_NAME + string "Name of the application" + default "Blinkenstrip" + help + The name of the application is shown on the web pages. + + +config WIFI_AP + bool "Factory default: Run WIFI Access Point" + default y + help + If true, run our own access point. If false, connect as client to WIFI + network. This is the default value after factory reset. When the device + runs, this value can be configured via the web interface. + +config WIFI_SSID + string "Factory default: WIFI Network Name" + default "blinkenstrip" + help + SSID of your WIFI Network. This is the default value after factory reset. + When the device runs, this value can be configured via the web interface. + +config WIFI_PASSWORD + string "Factory default: WIFI WPA2 Password" + default "blinkenstrip" + help + Password of your WIFI Network. This is the default value after factory + reset. When the device runs, this value can be configured via the web + interface. MINIMAL PASSWORD LENGTH IS 8 CHARACTERS! + +config LED_STRIP_WIDTH + int "Factory default: Width of Matrix" + default 8 + range 0 1024 + help + This is the default value after factory reset. When the device runs, this + value can be configured via the web interface. + +config LED_STRIP_HEIGHT + int "Factory default: Height of Matrix" + default 8 + range 0 1024 + help + This is the default value after factory reset. When the device runs, this + value can be configured via the web interface. + +config LED_STRIP_SNAKE + bool "Factory default: Snake wiring?" + default n + help + LEDs may be wired lineary or in "snake wiring", i.e. first row + left-to-right, second row right-to-left, ... + +config WRAP_AROUND + bool "Factory default: Ends of LED strip connected?" + default n + help + Set this if the ends of the LED strip are connected to each other. + +config NUMBER_OF_WORMS + int "Factory default: Number of worms in mode \"Worm\"" + default 1 + range 0 255 + help + Number of worms shown in mode "Worm". + +config MAX_BRIGHTNESS + int "Factory default: Maximal Brightness of LEDs" + default 32 + range 0 255 + help + This is the default value after factory reset. When the device runs, this + value can be configured via the web interface. + +endmenu # Blinkenstrip diff --git a/main/component.mk b/main/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/main/debug.h b/main/debug.h @@ -0,0 +1,28 @@ +/* + Copyright © 2021 Gerd Beuster <gerd@frombelow.net> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#ifndef DEBUG_H +#define DEBUG_H + +// Define debug levels and pick one. +#define DEBUG 0 +#define INFO 1 +/* #define DEBUG_LEVEL DEBUG */ +#define DEBUG_LEVEL INFO + +#define DEBUG_MSG(level, x) \ + do { if (level >= DEBUG_LEVEL) printf x; fflush(stdout);} while (0) + +#endif diff --git a/main/html_templates/config.html b/main/html_templates/config.html @@ -0,0 +1,53 @@ +"<!DOCTYPE html>\r\n" +"<html lang=\"de\">\r\n" +" <head>\r\n" +" <meta charset=\"utf-8\">\r\n" +" <title>%s (Configuration)</title>\r\n" +" <style>\r\n" +"button{width:10em;}\r\n" +"@media only screen and (max-device-width: 1024px){\r\n" +" html{font-size : 300%%; line-height: 100%%;}\r\n" +" button{font-size : 200%%; width:10em;}\r\n" +" input[type=text] {\r\n" +" font-size: 150%%;\r\n" +" }\r\n" +" input[type=password] {\r\n" +" font-size: 150%%;\r\n" +" }\r\n" +" input[type=radio] {\r\n" +" width: 5em;\r\n" +" height: 5ex;\r\n" +" }\r\n" +"}\r\n" +" </style>\r\n" +" </head>\r\n" +" <body>\r\n" +" <h1>%s (Configuration)</h1>\r\n" +" <form action=\"/config.html\" method=\"POST\"> \r\n" +" Name:<br />\r\n" +" <input type=\"text\" name=\"app_name\" value=\"%s\"></input><br /><br />\r\n" +" ESSID:<br />\r\n" +" <input type=\"text\" name=\"essid\" value=\"%s\"></input><br /><br />\r\n" +" Password (Min. 8 Characters!):<br />\r\n" +" <input type=\"password\" name=\"password\" value=\"%s\"></input><br /><br />\r\n" +" <input type=\"radio\" name=\"ap\" value=\"yes\" %s>Run Access Point</input><br /><br />\r\n" +" <input type=\"radio\" name=\"ap\" value=\"no\" %s>Join Existing Network</input><br /><br />\r\n" +" LED Matrix Width:<br /> <input type=\"text\" name=\"led_strip_w\" value=\"%d\"></input><br /><br />\r\n" +" LED Matrix Height:<br /> <input type=\"text\" name=\"led_strip_h\" value=\"%d\"></input><br /><br />\r\n" +" <input type=\"radio\" name=\"snake\" value=\"yes\" %s>Snake Wiring</input><br /><br />\r\n" +" <input type=\"radio\" name=\"snake\" value=\"no\" %s>Linear Wiring</input><br /><br />\r\n" +" <input type=\"radio\" name=\"wrap_around\" value=\"yes\" %s>\r\n" +" Circular Wiring</input><br /><br />\r\n" +" <input type=\"radio\" name=\"wrap_around\" value=\"no\" %s>\r\n" +" Non-Circual Wiring</input><br /><br />\r\n" +" Number of Worms:<br /> <input type=\"text\" name=\"number_of_worms\" value=\"%d\"></input><br /><br />\r\n" +" Max. Brightness:<br /> <input type=\"text\" name=\"max_brightness\" value=\"%d\"></input><br /><br />\r\n" +" <button type=\"submit\">Save &amp; Reset!</button><br /><br />\r\n" +" </form>\r\n" +" \r\n" +" \r\n" +" \r\n" +" \r\n" +"<p><a href=\"/\"</a>LED Control</a></p>\r\n" +" </body>\r\n" +"</html>\r\n"; diff --git a/main/html_templates/index.html b/main/html_templates/index.html @@ -0,0 +1,65 @@ +"<!DOCTYPE html>\r\n" +"<html lang=\"de\">\r\n" +" <head>\r\n" +" <meta charset=\"utf-8\">\r\n" +" <title>%s</title>\r\n" +" <style>\r\n" +"button{width:10em;}\r\n" +"@media only screen and (max-device-width: 1024px){\r\n" +"html{font-size : 300%%; line-height: 50%%;}\r\n" +"button{font-size : 190%%; width:10em;}\r\n" +".slider{\r\n" +" height: 5ex;\r\n" +" width: 40em;\r\n" +" autoScaleSlider:false,\r\n" +" autoHeight: false\r\n" +"}\r\n" +" </style>\r\n" +" </head>\r\n" +" <body>\r\n" +" <h1>%s</h1>\r\n" +" <form action=\"/\" method=\"POST\"> \r\n" +" <div class=\"slidecontainer\">\r\n" +" R <input type=\"range\" min=\"0\" max=\"%d\" value=\"0\" class=\"slider\" id=\"slider_red\">\r\n" +" <label id=\"label_red\"></label><br />\r\n" +" G <input type=\"range\" min=\"0\" max=\"%d\" value=\"0\" class=\"slider\" id=\"slider_green\">\r\n" +" <label id=\"label_green\"></label><br />\r\n" +" B <input type=\"range\" min=\"0\" max=\"%d\" value=\"0\" class=\"slider\" id=\"slider_blue\">\r\n" +" <label id=\"label_blue\"></label><br />\r\n" +" </div><br />\r\n" +" <button type=\"submit\" name=\"mode\" value=\"sequence\">Sequence</button><br /><br />\r\n" +" <button type=\"submit\" name=\"mode\" value=\"blink\">Blink</button><br /><br />\r\n" +" <button type=\"submit\" name=\"mode\" value=\"running\">Running Dot</button><br /><br />\r\n" +" <button type=\"submit\" name=\"mode\" value=\"cellular\">Cellular Automaton</button><br /><br />\r\n" +" <button type=\"submit\" name=\"mode\" value=\"lines\">Changing Lines</button><br /><br />\r\n" +" <button type=\"submit\" name=\"mode\" value=\"worm\">Worm</button><br /><br />\r\n" +" <!-- <button type=\"submit\" name=\"mode\" value=\"experimental\">Experiment</button><br /><br /> -->\r\n" +" </form>\r\n" +" <p><a href=\"config.html\"</a>Configure</a></p>\r\n" +" <!-- Update slider on the fly -->\r\n" +" <script>\r\n" +" var slider_red = document.getElementById(\"slider_red\");\r\n" +" var label_red = document.getElementById(\"label_red\");\r\n" +" var slider_green = document.getElementById(\"slider_green\");\r\n" +" var label_green = document.getElementById(\"label_green\");\r\n" +" var slider_blue = document.getElementById(\"slider_blue\");\r\n" +" var label_blue = document.getElementById(\"label_blue\");\r\n" +" label_red.innerHTML = slider_red.value;\r\n" +" label_green.innerHTML = slider_green.value;\r\n" +" label_blue.innerHTML = slider_blue.value;\r\n" +" slider_red.oninput = function() {\r\n" +" label_red.innerHTML = slider_red.value;\r\n" +" label_green.innerHTML = slider_green.value;\r\n" +" label_blue.innerHTML = slider_blue.value;\r\n" +" var i = document.createElement(\"img\");\r\n" +" // Didn't figure out how to do this with a POST request without\r\n" +" // resorting to external libraries\r\n" +" i.src = \"?mode=rgb&red=\" +\r\n" +" slider_red.value + \"&green=\" + slider_green.value +\r\n" +" \"&blue=\" + slider_blue.value;\r\n" +" };\r\n" +" slider_green.oninput = slider_red.oninput;\r\n" +" slider_blue.oninput = slider_red.oninput;\r\n" +" </script>\r\n" +" </body>\r\n" +"</html>\r\n"; diff --git a/main/html_templates/reset.html b/main/html_templates/reset.html @@ -0,0 +1,22 @@ +"<!DOCTYPE html>\r\n" +"<html lang=\"de\">\r\n" +" <head>\r\n" +" <meta charset=\"utf-8\">\r\n" +" <meta http-equiv=\"refresh\" content=\"10; URL=/\">\r\n" +" <title>Reset</title>\r\n" +" <style>\r\n" +"button{width:10em;}\r\n" +"@media only screen and (max-device-width: 1024px){\r\n" +"html{font-size : 300%%; line-height: 50%%;}\r\n" +"button{font-size : 200%%; width:10em;}\r\n" +"}\r\n" +" </style>\r\n" +" </head>\r\n" +" <body>\r\n" +" <h1>Reset</h1>\r\n" +" <p>Restarting device ...</p>\r\n" +" <p>You will be re-directed to the main page in 10 seconds.\r\n" +" This will only work if your device and\r\n" +" the web browser are on the same network.</p>\r\n" +" </body>\r\n" +"</html>\r\n"; diff --git a/main/led.c b/main/led.c @@ -0,0 +1,538 @@ +/* + Function unicolor_rainbow based on + <https://www.esp32.com/viewtopic.php?t=589#> ((c) Lucas Bruder + <LBruder@me.com> 2016) + + Everything else copyright © 2021 Gerd Beuster <gerd@frombelow.net> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* {{{ Includes and definitions */ + +#include "led.h" +#include "debug.h" +#include "freertos/task.h" +#include "nvs_flash.h" +#include "math.h" + +/* }}} */ + +void store_state_if_changed() { + if(blinkenstrip_state_has_changed) { + nvs_handle flash_handle; + nvs_open("blinkenstrip", NVS_READWRITE, &flash_handle); + nvs_set_u8(flash_handle, "blinkenstrip_state", blinkenstrip_state); + nvs_set_u8(flash_handle, "led_col_red", led_color_rgb.red); + nvs_set_u8(flash_handle, "led_col_green", led_color_rgb.green); + nvs_set_u8(flash_handle, "led_col_blue", led_color_rgb.blue); + nvs_commit(flash_handle); + nvs_close(flash_handle); + DEBUG_MSG(DEBUG, ("Wrote new state (%d / (%d, %d, %d)) to flash.\n", + blinkenstrip_state, led_color_rgb.red, led_color_rgb.green, led_color_rgb.blue)); + } +} + +/* {{{ Unicolor patterns */ + +void unicolor(struct led_color_t led_color){ + store_state_if_changed(); + for (uint32_t index = 0; index < led_strip_length; index++) { + led_strip_set_pixel_color(&led_strip, index, &led_color); + } + led_strip_show(&led_strip); + blinkenstrip_state_has_changed = false; +} + +const uint8_t lights[360]={ + 0, 0, 0, 0, 0, 1, 1, 2, + 2, 3, 4, 5, 6, 7, 8, 9, + 11, 12, 13, 15, 17, 18, 20, 22, + 24, 26, 28, 30, 32, 35, 37, 39, + 42, 44, 47, 49, 52, 55, 58, 60, + 63, 66, 69, 72, 75, 78, 81, 85, + 88, 91, 94, 97, 101, 104, 107, 111, + 114, 117, 121, 124, 127, 131, 134, 137, + 141, 144, 147, 150, 154, 157, 160, 163, + 167, 170, 173, 176, 179, 182, 185, 188, + 191, 194, 197, 200, 202, 205, 208, 210, + 213, 215, 217, 220, 222, 224, 226, 229, + 231, 232, 234, 236, 238, 239, 241, 242, + 244, 245, 246, 248, 249, 250, 251, 251, + 252, 253, 253, 254, 254, 255, 255, 255, + 255, 255, 255, 255, 254, 254, 253, 253, + 252, 251, 251, 250, 249, 248, 246, 245, + 244, 242, 241, 239, 238, 236, 234, 232, + 231, 229, 226, 224, 222, 220, 217, 215, + 213, 210, 208, 205, 202, 200, 197, 194, + 191, 188, 185, 182, 179, 176, 173, 170, + 167, 163, 160, 157, 154, 150, 147, 144, + 141, 137, 134, 131, 127, 124, 121, 117, + 114, 111, 107, 104, 101, 97, 94, 91, + 88, 85, 81, 78, 75, 72, 69, 66, + 63, 60, 58, 55, 52, 49, 47, 44, + 42, 39, 37, 35, 32, 30, 28, 26, + 24, 22, 20, 18, 17, 15, 13, 12, + 11, 9, 8, 7, 6, 5, 4, 3, + 2, 2, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0}; + +void unicolor_rainbow(){ + + static int rgb_red=0; + static int rgb_green=120; + static int rgb_blue=240; + + static struct led_color_t led_color = { + .red = 5, + .green = 0, + .blue = 0, + }; + + store_state_if_changed(); + for (uint32_t index = 0; index < led_strip_length; index++) { + led_strip_set_pixel_color(&led_strip, index, &led_color); + } + led_strip_show(&led_strip); + + led_color.red = lights[rgb_red] * max_brightness / 0xFF; + led_color.green = lights[rgb_green] * max_brightness / 0xFF; + led_color.blue = lights[rgb_blue] * max_brightness / 0xFF; + + rgb_red += 1; + rgb_green += 1; + rgb_blue += 1; + + if (rgb_red >= 360) rgb_red=0; + if (rgb_green >= 360) rgb_green=0; + if (rgb_blue >= 360) rgb_blue=0; + + blinkenstrip_state_has_changed = false; +} + +/* }}} */ + +/* {{{ Random on/off */ + +uint32_t get_random_number(uint32_t n){ + uint32_t r; + do{ + r = esp_random(); + } while(r > (RAND_MAX - (RAND_MAX % n))); + return(r % n); +} + +void random_on_off(){ + if(blinkenstrip_state_has_changed){ + // Initalize state + for (uint32_t index = 0; index < led_strip_length; index++) { + led_strip_set_pixel_color(&led_strip, index, + (struct led_color_t *) &led_color_blue); + } + store_state_if_changed(); + blinkenstrip_state_has_changed = false; + } + else{ + // Update state + // Copy over "old" strip + struct led_color_t c; + for (uint32_t index = 0; index < led_strip_length; index++) { + led_strip_get_pixel_color(&led_strip, index, &c); + led_strip_set_pixel_color(&led_strip, index, &c); + } + // Randomly switch an LED to green or blue. + uint32_t switch_led = get_random_number(led_strip_length); + if (get_random_number(2)){ + led_strip_set_pixel_color(&led_strip, switch_led, + (struct led_color_t *) &led_color_blue); + } + else{ + led_strip_set_pixel_color(&led_strip, switch_led, + (struct led_color_t *) &led_color_green); + } + } + led_strip_show(&led_strip); +} + +/* }}} */ + +/* {{{ Running dot */ + +void running_dot(){ + static uint32_t position = 0; + store_state_if_changed(); + blinkenstrip_state_has_changed = false; + for (uint32_t index = 0; index < led_strip_length; index++) { + led_strip_set_pixel_color(&led_strip, index, + (struct led_color_t *) &led_color_blue); + } + led_strip_set_pixel_color(&led_strip, position, + (struct led_color_t *) &led_color_red); + position = (position + 1) % led_strip_length; + blinkenstrip_state_has_changed = false; + led_strip_show(&led_strip); +}; + +/* }}} */ + +/* {{{ Cellular Automaton */ + +int32_t xy2i(int32_t x, int32_t y) { + int32_t i = y * led_strip_w; + if ((y % 2) && snake) { + i += led_strip_w - x - 1; + } + else { + i += x; + } + return(i); +} + + +bool cell_is_on(struct led_strip_t led_strip, int32_t x, int32_t y){ + static struct led_color_t c; + x = (x + led_strip_w) % led_strip_w; + y = (y + led_strip_h) % led_strip_h; + led_strip_get_pixel_color(&led_strip, xy2i(x, y), + &c); + return((c.red == cell_on.red) && + (c.green == cell_on.green) && + (c.blue == cell_on.blue)); +}; + +void cellular_automaton(){ + static uint8_t number_of_updates = 0; + static bool stable_pattern = false; + // Since we get a stable pattern after sometime, we restart with a + // new random cellular automaton after a fixed number of steps or + // when we detect a stable situation. + store_state_if_changed(); + if(blinkenstrip_state_has_changed || stable_pattern || + (number_of_updates == 100)) { + // Initialize with random values + for (uint32_t index = 0; index < led_strip_length; index++) { + if (get_random_number(2)){ + led_strip_set_pixel_color(&led_strip, index, + (struct led_color_t *) &cell_on); + } + else{ + led_strip_set_pixel_color(&led_strip, index, + (struct led_color_t *) &cell_off); + } + } + led_strip_show(&led_strip); + number_of_updates = 0; + blinkenstrip_state_has_changed = false; + stable_pattern = false; + } + // Update automaton + if((!stable_pattern) && (number_of_updates++ < 100)){ + // Cycle over all cells + for (int32_t y = 0; y < led_strip_h; y++){ + for (int32_t x = 0; x < led_strip_w; x++){ + // Count neighbors + uint8_t neighbors = 0; + for (int32_t y_neighbor = 0; y_neighbor < 3; y_neighbor++){ + for (int32_t x_neighbor = 0; x_neighbor < 3; x_neighbor++){ + if (((x_neighbor != 1) || (y_neighbor != 1)) && + (cell_is_on(led_strip, x-x_neighbor+1, y-y_neighbor+1))){ + neighbors++; + } + } + } + // Update cell + uint32_t index = xy2i(x, y); + if (neighbors < 2) { + led_strip_set_pixel_color(&led_strip, index, + (struct led_color_t *) &cell_off); + } + else if (neighbors > 3) { + led_strip_set_pixel_color(&led_strip, index, + (struct led_color_t *) &cell_off); + } + else if (neighbors == 3) { + led_strip_set_pixel_color(&led_strip, index, + (struct led_color_t *) &cell_on); + } + else { + struct led_color_t c; + led_strip_get_pixel_color(&led_strip, index, &c); + led_strip_set_pixel_color(&led_strip, index, &c); + } + } + } + // Check if pattern has changed since last update. + // (We will exit the loop if the pattern becomes stable) + stable_pattern = true; + for(uint32_t i = 0; (i < led_strip_length) && stable_pattern; i++){ + stable_pattern = + (led_strip.led_strip_buf_1[i].red == + led_strip.led_strip_buf_2[i].red) && + (led_strip.led_strip_buf_1[i].green == + led_strip.led_strip_buf_2[i].green) && + (led_strip.led_strip_buf_1[i].blue == + led_strip.led_strip_buf_2[i].blue); + } + led_strip_show(&led_strip); + vTaskDelay(600 / portTICK_PERIOD_MS); + } +} + +/* }}} */ + +/* {{{ Line-by-line switch */ + +void set_row_color(struct led_color_t led_color, uint32_t row) { + for (uint32_t index = 0; index < led_strip_w; index++) { + led_strip_set_pixel_color(&led_strip, xy2i(index, row), + &led_color); + } +} + +void set_column_color(struct led_color_t led_color, uint32_t column) { + for (uint32_t index = 0; index < led_strip_h; index++) { + led_strip_set_pixel_color(&led_strip, xy2i(column, index), + &led_color); + } +} + +void line_by_line() { + static bool change_row = true; // Switch in rows or in columns? + static void (*switch_command)(struct led_color_t led_color, uint32_t row); + const uint8_t color_cycle[] = { + max_brightness, max_brightness, 0 // Yellow + , 0, max_brightness, max_brightness // Turquoise + , 0, 0, max_brightness // Blue + }; + const uint8_t color_cycle_length = sizeof(color_cycle); + + static uint8_t pos_in_color_cycle = 0; + static uint32_t switch_line = 0; + static struct led_color_t old_color; + static struct led_color_t new_color; + store_state_if_changed(); + if (switch_line == 0) { + old_color.red = color_cycle[pos_in_color_cycle]; + old_color.green = color_cycle[pos_in_color_cycle+1]; + old_color.blue = color_cycle[pos_in_color_cycle+2]; + pos_in_color_cycle = (pos_in_color_cycle + 3) % color_cycle_length; + new_color.red = color_cycle[pos_in_color_cycle]; + new_color.green = color_cycle[pos_in_color_cycle+1]; + new_color.blue = color_cycle[pos_in_color_cycle+2]; + change_row = !change_row; + if(change_row){ + switch_command = set_row_color; + } + else{ + switch_command = set_column_color; + } + } + for (uint8_t row = 0; row < led_strip_h; row++) { + if (row > switch_line) { + (*switch_command)(old_color, row); + } + else if (row == switch_line) { + (*switch_command)(led_color_red, row); + } + else { + (*switch_command)(new_color, row); + } + } + switch_line = (switch_line + 1) % led_strip_h; + led_strip_show(&led_strip); + blinkenstrip_state_has_changed = false; +} + +/* }}} */ + +/* {{{ Worm */ + +// Positions and running directions of worms (allocated in main_led_task) +int8_t *worm_direction; +uint16_t *worm_pos; + +void worm() { + // Percentage of max_brightness relative to full brightness + float mb_perc = ((float) max_brightness) / 0xFF; + /* const float mb_perc = 1; */ + struct led_color_t scanner_pattern[] = + {{round(0x00*mb_perc), round(0x00*mb_perc), round(0x00*mb_perc)}, + {round(0x00*mb_perc), round(0x20*mb_perc), round(0x00*mb_perc)}, + {round(0x20*mb_perc), round(0x20*mb_perc), round(0x00*mb_perc)}, + {round(0xFF*mb_perc), round(0x00*mb_perc), round(0x00*mb_perc)}, + {round(0xFF*mb_perc), round(0x00*mb_perc), round(0x00*mb_perc)}, + {round(0xFF*mb_perc), round(0x00*mb_perc), round(0x00*mb_perc)}, + {round(0xFF*mb_perc), round(0x00*mb_perc), round(0x00*mb_perc)}, + {round(0xFF*mb_perc), round(0x00*mb_perc), round(0x00*mb_perc)}, + {round(0x20*mb_perc), round(0x20*mb_perc), round(0x00*mb_perc)}, + {round(0x00*mb_perc), round(0x20*mb_perc), round(0x00*mb_perc)}, + {round(0x00*mb_perc), round(0x00*mb_perc), round(0x00*mb_perc)}}; + const uint8_t scanner_pattern_length = (sizeof(scanner_pattern) / + sizeof(struct led_color_t)); + store_state_if_changed(); + if(blinkenstrip_state_has_changed){ + // Initalize state + for (uint32_t index = 0; index < led_strip_length; index++) { + led_strip_set_pixel_color(&led_strip, index, + (struct led_color_t *) &led_color_black); + } + for (uint8_t w = 0; w < number_of_worms; w++) { + worm_direction[w] = get_random_number(2)*2-1; + worm_pos[w] = get_random_number(led_strip_length-scanner_pattern_length); + } + blinkenstrip_state_has_changed = false; + } + else{ + // Update state + // Copy over "old" strip + struct led_color_t c; + for (uint32_t index = 0; index < led_strip_length; index++) { + led_strip_get_pixel_color(&led_strip, index, &c); + led_strip_set_pixel_color(&led_strip, index, &c); + } + // Worms + for (uint8_t w = 0; w < number_of_worms; w++) { + for (uint8_t i = 0; i < scanner_pattern_length; i++) { + led_strip_set_pixel_color(&led_strip, + (worm_pos[w]+i) % led_strip_length, + &scanner_pattern[i]); + } + if (!get_random_number(50)) { + worm_direction[w] *= -1; + } + if (!wrap_around) { + // Switch direction if we hit the end of the strip + if (worm_pos[w] == 0){ + worm_direction[w] = 1; + } + if ((worm_pos[w] + scanner_pattern_length) == led_strip_length) { + worm_direction[w] = -1; + } + } + worm_pos[w] = (worm_pos[w] + worm_direction[w]) % led_strip_length; + }; + // Randomly switch an LED to blue + uint32_t switch_led = get_random_number(led_strip_length); + if (!get_random_number(4)){ + led_strip_set_pixel_color(&led_strip, switch_led, + (struct led_color_t *) &led_color_blue); + } + } + led_strip_show(&led_strip); +} + +/* }}} */ + +void experiment() { + /* empty */ +} + +/* {{{ main */ + +void led_controller(void *args) { + + // Define standard colors + led_color_red.red = max_brightness; + led_color_red.green = 0x00; + led_color_red.blue = 0x00; + led_color_green.red = 0x00; + led_color_green.green = max_brightness; + led_color_green.blue = 0x00; + led_color_blue.red = 0x00; + led_color_blue.green = 0x00; + led_color_blue.blue = max_brightness; + led_color_yellow.red = max_brightness; + led_color_yellow.green = max_brightness; + led_color_yellow.blue = 0x00; + led_color_white.red = max_brightness; + led_color_white.green = max_brightness; + led_color_white.blue = max_brightness; + led_color_black.red = 0x00; + led_color_black.green = 0x00; + led_color_black.blue = 0x00; + + // Define colors for cellular automaton + cell_on.red = 0x00; + cell_on.green = max_brightness; + cell_on.blue = 0x00; + cell_off.red = 0x00; + cell_off.green = 0x00; + cell_off.blue = max_brightness / 2; + + // Initialize data structures for worm + worm_direction = calloc(number_of_worms, sizeof(int8_t)); // Yes, size is 1 byte ... + worm_pos = calloc(number_of_worms, sizeof(uint16_t)); // Yes, size is 2 bytes ... + + // Initialize data structures for LED strip + led_strip_length = led_strip_w * led_strip_h; + led_strip_buf_1 = malloc(led_strip_length * sizeof(struct led_color_t)); + led_strip_buf_2 = malloc(led_strip_length * sizeof(struct led_color_t)); + led_strip.rgb_led_type = RGB_LED_TYPE_WS2812; + led_strip.rmt_channel = RMT_CHANNEL_1; + led_strip.rmt_interrupt_num = LED_STRIP_RMT_INTR_NUM; + led_strip.gpio = CONFIG_WS2811_DATA_PIN; + led_strip.led_strip_buf_1 = led_strip_buf_1; + led_strip.led_strip_buf_2 = led_strip_buf_2; + led_strip.led_strip_length = led_strip_length; + + blinkenstrip_state_changed_semaphore = xSemaphoreCreateBinary(); + xSemaphoreGive(blinkenstrip_state_changed_semaphore); + assert(blinkenstrip_state_changed_semaphore); + led_strip.access_semaphore = xSemaphoreCreateBinary(); + assert(led_strip.access_semaphore); + bool led_init_ok = led_strip_init(&led_strip); + assert(led_init_ok); + + blinkenstrip_state_has_changed = true; + while (true) { + xSemaphoreTake(blinkenstrip_state_changed_semaphore, portMAX_DELAY); + switch(blinkenstrip_state){ + case BLINKENSTRIP_STATE_UNICOLOR_RAINBOW: + unicolor_rainbow(); break; + case BLINKENSTRIP_STATE_RANDOM_ON_OFF: + random_on_off(); break; + case BLINKENSTRIP_STATE_RUNNING_DOT: + running_dot(); break; + case BLINKENSTRIP_STATE_CELLULAR_AUTOMATON: + cellular_automaton(); break; + case BLINKENSTRIP_STATE_LINE_BY_LINE: + line_by_line(); break; + case BLINKENSTRIP_STATE_WORM: + worm(); break; + case BLINKENSTRIP_STATE_UNICOLOR_RGB: + if(blinkenstrip_state_has_changed) + unicolor(led_color_rgb); + break; + case BLINKENSTRIP_STATE_EXPERIMENTAL: + experiment(); break; + } + xSemaphoreGive(blinkenstrip_state_changed_semaphore); + vTaskDelay(60 / portTICK_PERIOD_MS); + } +} + +/* }}} */ diff --git a/main/led.h b/main/led.h @@ -0,0 +1,76 @@ +/* + Copyright © 2021 Gerd Beuster <gerd@frombelow.net> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#ifndef LED_H +#define LED_H + +#include "led_strip.h" + +#define LED_STRIP_RMT_INTR_NUM 19 + +uint16_t led_strip_w; +uint16_t led_strip_h; +uint8_t max_brightness; +// How are the LEDs wired, linearly, or in "snake mode", i.e. first +// line left-to-right, second line right-to-left, ... +uint8_t snake; +// Whether the ends of the LED strip are connected to each other +uint8_t wrap_around; +// Number of worms in BLINKENSTRIP_STATE_WORM +uint8_t number_of_worms; + +// Determine which pattern we show. +volatile uint8_t blinkenstrip_state; +// Toggled whenever the user triggered a state change +// via the webserver. +volatile bool blinkenstrip_state_has_changed; +SemaphoreHandle_t blinkenstrip_state_changed_semaphore; +#define BLINKENSTRIP_STATE_UNICOLOR_RAINBOW 0x05 +#define BLINKENSTRIP_STATE_RANDOM_ON_OFF 0x06 +#define BLINKENSTRIP_STATE_RUNNING_DOT 0x07 +#define BLINKENSTRIP_STATE_CELLULAR_AUTOMATON 0x08 +#define BLINKENSTRIP_STATE_LINE_BY_LINE 0x09 +#define BLINKENSTRIP_STATE_UNICOLOR_RGB 0x0A +#define BLINKENSTRIP_STATE_WORM 0x0B +#define BLINKENSTRIP_STATE_EXPERIMENTAL 0xFF + +// Data structures for LED strip +uint16_t led_strip_length; +struct led_color_t *led_strip_buf_1; +struct led_color_t *led_strip_buf_2; +struct led_strip_t led_strip; + +// Standard colors +struct led_color_t led_color_red; +struct led_color_t led_color_green; +struct led_color_t led_color_blue; +struct led_color_t led_color_yellow; +struct led_color_t led_color_white; +struct led_color_t led_color_black; + +// All channels set by user +struct led_color_t led_color_rgb; + +// Colors for cellular automaton +struct led_color_t cell_on; +struct led_color_t cell_off; + +// This function is run in its won task and runs the LED show. +void led_controller(void *args); + +// Utiltiy function: Get random number in range 0..n-1. +uint32_t get_random_number(uint32_t n); + +#endif diff --git a/main/led_strip.c b/main/led_strip.c @@ -0,0 +1,461 @@ +/* ---------------------------------------------------------------------------- + File: led_strip.c + Author(s): Lucas Bruder <LBruder@me.com> + Date Created: 11/23/2016 + Last modified: 11/26/2016 + + Description: LED Library for driving various led strips on ESP32. + + This library uses double buffering to display the LEDs. + If the driver is showing buffer 1, any calls to led_strip_set_pixel_color + will write to buffer 2. When it's time to drive the pixels on the strip, it + refers to buffer 1. + When led_strip_show is called, it will switch to displaying the pixels + from buffer 2 and will clear buffer 1. Any writes will now happen on buffer 1 + and the task will look at buffer 2 for refreshing the LEDs + ------------------------------------------------------------------------- */ + +/* Some minor changes by Gerd Beuster (gb) to work with blinkenstrip. */ + +#include "led_strip.h" +#include "freertos/task.h" +#include <string.h> + +// gb: Need handle to web server task +#include "webserver.h" +// gb: LED_STRIP_TASK_SIZE was to small. Increased size in order +// to avoid buffer overflows. +/* #define LED_STRIP_TASK_SIZE (512) */ +#define LED_STRIP_TASK_SIZE (1024) +#define LED_STRIP_TASK_PRIORITY (configMAX_PRIORITIES - 1) + +#define LED_STRIP_REFRESH_PERIOD_MS (30U) // TODO: add as parameter to led_strip_init + +#define LED_STRIP_NUM_RMT_ITEMS_PER_LED (24U) // Assumes 24 bit color for each led + +// RMT Clock source is @ 80 MHz. Dividing it by 8 gives us 10 MHz frequency, or 100ns period. +#define LED_STRIP_RMT_CLK_DIV (8) + +/**************************** + WS2812 Timing + ****************************/ +#define LED_STRIP_RMT_TICKS_BIT_1_HIGH_WS2812 9 // 900ns (900ns +/- 150ns per datasheet) +#define LED_STRIP_RMT_TICKS_BIT_1_LOW_WS2812 3 // 300ns (350ns +/- 150ns per datasheet) +#define LED_STRIP_RMT_TICKS_BIT_0_HIGH_WS2812 3 // 300ns (350ns +/- 150ns per datasheet) +#define LED_STRIP_RMT_TICKS_BIT_0_LOW_WS2812 9 // 900ns (900ns +/- 150ns per datasheet) + +/**************************** + SK6812 Timing + ****************************/ +#define LED_STRIP_RMT_TICKS_BIT_1_HIGH_SK6812 6 +#define LED_STRIP_RMT_TICKS_BIT_1_LOW_SK6812 6 +#define LED_STRIP_RMT_TICKS_BIT_0_HIGH_SK6812 3 +#define LED_STRIP_RMT_TICKS_BIT_0_LOW_SK6812 9 + +/**************************** + APA106 Timing + ****************************/ +#define LED_STRIP_RMT_TICKS_BIT_1_HIGH_APA106 14 // 1.36us +/- 150ns per datasheet +#define LED_STRIP_RMT_TICKS_BIT_1_LOW_APA106 3 // 350ns +/- 150ns per datasheet +#define LED_STRIP_RMT_TICKS_BIT_0_HIGH_APA106 3 // 350ns +/- 150ns per datasheet +#define LED_STRIP_RMT_TICKS_BIT_0_LOW_APA106 14 // 1.36us +/- 150ns per datasheet + +// Function pointer for generating waveforms based on different LED drivers +typedef void (*led_fill_rmt_items_fn)(struct led_color_t *led_strip_buf, rmt_item32_t *rmt_items, uint32_t led_strip_length); + +static inline void led_strip_fill_item_level(rmt_item32_t* item, int high_ticks, int low_ticks) +{ + item->level0 = 1; + item->duration0 = high_ticks; + item->level1 = 0; + item->duration1 = low_ticks; +} + +static inline void led_strip_rmt_bit_1_sk6812(rmt_item32_t* item) +{ + led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_1_HIGH_SK6812, LED_STRIP_RMT_TICKS_BIT_1_LOW_SK6812); +} + +static inline void led_strip_rmt_bit_0_sk6812(rmt_item32_t* item) +{ + led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_0_HIGH_SK6812, LED_STRIP_RMT_TICKS_BIT_0_LOW_SK6812); +} + +static void led_strip_fill_rmt_items_sk6812(struct led_color_t *led_strip_buf, rmt_item32_t *rmt_items, uint32_t led_strip_length) +{ + uint32_t rmt_items_index = 0; + for (uint32_t led_index = 0; led_index < led_strip_length; led_index++) { + struct led_color_t led_color = led_strip_buf[led_index]; + + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.green >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_sk6812(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_sk6812(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.red >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_sk6812(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_sk6812(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.blue >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_sk6812(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_sk6812(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + } +} + +static inline void led_strip_rmt_bit_1_ws2812(rmt_item32_t* item) +{ + led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_1_HIGH_WS2812, LED_STRIP_RMT_TICKS_BIT_1_LOW_WS2812); +} + +static inline void led_strip_rmt_bit_0_ws2812(rmt_item32_t* item) +{ + led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_0_HIGH_WS2812, LED_STRIP_RMT_TICKS_BIT_0_LOW_WS2812); +} + +static void led_strip_fill_rmt_items_ws2812(struct led_color_t *led_strip_buf, rmt_item32_t *rmt_items, uint32_t led_strip_length) +{ + uint32_t rmt_items_index = 0; + for (uint32_t led_index = 0; led_index < led_strip_length; led_index++) { + struct led_color_t led_color = led_strip_buf[led_index]; + + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.green >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_ws2812(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_ws2812(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.red >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_ws2812(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_ws2812(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.blue >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_ws2812(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_ws2812(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + } +} + +static inline void led_strip_rmt_bit_1_apa106(rmt_item32_t* item) +{ + led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_1_HIGH_APA106, LED_STRIP_RMT_TICKS_BIT_1_LOW_APA106); +} + +static inline void led_strip_rmt_bit_0_apa106(rmt_item32_t* item) +{ + led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_0_HIGH_APA106, LED_STRIP_RMT_TICKS_BIT_0_LOW_APA106); +} + +static void led_strip_fill_rmt_items_apa106(struct led_color_t *led_strip_buf, rmt_item32_t *rmt_items, uint32_t led_strip_length) +{ + uint32_t rmt_items_index = 0; + for (uint32_t led_index = 0; led_index < led_strip_length; led_index++) { + struct led_color_t led_color = led_strip_buf[led_index]; + + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.red >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_apa106(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_apa106(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.green >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_apa106(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_apa106(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + for (uint8_t bit = 8; bit != 0; bit--) { + uint8_t bit_set = (led_color.blue >> (bit - 1)) & 1; + if(bit_set) { + led_strip_rmt_bit_1_apa106(&(rmt_items[rmt_items_index])); + } else { + led_strip_rmt_bit_0_apa106(&(rmt_items[rmt_items_index])); + } + rmt_items_index++; + } + } +} + +static void led_strip_task(void *arg) +{ + struct led_strip_t *led_strip = (struct led_strip_t *)arg; + led_fill_rmt_items_fn led_make_waveform = NULL; + bool make_new_rmt_items = true; + bool prev_showing_buf_1 = !led_strip->showing_buf_1; + + size_t num_items_malloc = (LED_STRIP_NUM_RMT_ITEMS_PER_LED * led_strip->led_strip_length); + rmt_item32_t *rmt_items = (rmt_item32_t*) malloc(sizeof(rmt_item32_t) * num_items_malloc); + if (!rmt_items) { + vTaskDelete(NULL); + } + + switch (led_strip->rgb_led_type) { + case RGB_LED_TYPE_WS2812: + led_make_waveform = led_strip_fill_rmt_items_ws2812; + break; + + case RGB_LED_TYPE_SK6812: + led_make_waveform = led_strip_fill_rmt_items_sk6812; + break; + + case RGB_LED_TYPE_APA106: + led_make_waveform = led_strip_fill_rmt_items_apa106; + break; + + default: + // Will avoid keeping it point to NULL + led_make_waveform = led_strip_fill_rmt_items_ws2812; + break; + }; + + for(;;) { + // Next line added by gb: Suspend web server task to avoid flicker. + vTaskSuspend(http_server_task); + // Changed by gb + // rmt_wait_tx_done(led_strip->rmt_channel); + rmt_wait_tx_done(led_strip->rmt_channel, portMAX_DELAY); + xSemaphoreTake(led_strip->access_semaphore, portMAX_DELAY); + + /* + * If buf 1 was previously being shown and now buf 2 is being shown, + * it should update the new rmt items array. If buf 2 was previous being shown + * and now buf 1 is being shown, it should update the new rmt items array. + * Otherwise, no need to update the array + */ + if ((prev_showing_buf_1 == true) && (led_strip->showing_buf_1 == false)) { + make_new_rmt_items = true; + } else if ((prev_showing_buf_1 == false) && (led_strip->showing_buf_1 == true)) { + make_new_rmt_items = true; + } else { + make_new_rmt_items = false; + } + + if (make_new_rmt_items) { + if (led_strip->showing_buf_1) { + led_make_waveform(led_strip->led_strip_buf_1, rmt_items, led_strip->led_strip_length); + } else { + led_make_waveform(led_strip->led_strip_buf_2, rmt_items, led_strip->led_strip_length); + } + } + + rmt_write_items(led_strip->rmt_channel, rmt_items, num_items_malloc, false); + prev_showing_buf_1 = led_strip->showing_buf_1; + xSemaphoreGive(led_strip->access_semaphore); + // Next line added by gb + vTaskResume(http_server_task); + vTaskDelay(LED_STRIP_REFRESH_PERIOD_MS / portTICK_PERIOD_MS); + } + + if (rmt_items) { + free(rmt_items); + } + vTaskDelete(NULL); +} + +static bool led_strip_init_rmt(struct led_strip_t *led_strip) +{ + rmt_config_t rmt_cfg = { + .rmt_mode = RMT_MODE_TX, + .channel = led_strip->rmt_channel, + .clk_div = LED_STRIP_RMT_CLK_DIV, + .gpio_num = led_strip->gpio, + .mem_block_num = 1, + .tx_config = { + .loop_en = false, + .carrier_freq_hz = 100, // Not used, but has to be set to avoid divide by 0 err + .carrier_duty_percent = 50, + .carrier_level = RMT_CARRIER_LEVEL_LOW, + .carrier_en = false, + .idle_level = RMT_IDLE_LEVEL_LOW, + .idle_output_en = true, + } + }; + + esp_err_t cfg_ok = rmt_config(&rmt_cfg); + if (cfg_ok != ESP_OK) { + return false; + } + esp_err_t install_ok = rmt_driver_install(rmt_cfg.channel, 0, 0); + if (install_ok != ESP_OK) { + return false; + } + + return true; +} + +bool led_strip_init(struct led_strip_t *led_strip) +{ + TaskHandle_t led_strip_task_handle; + + if ((led_strip == NULL) || + (led_strip->rmt_channel == RMT_CHANNEL_MAX) || + (led_strip->gpio > GPIO_NUM_33) || // only inputs above 33 + (led_strip->led_strip_buf_1 == NULL) || + (led_strip->led_strip_buf_2 == NULL) || + (led_strip->led_strip_length == 0) || + (led_strip->access_semaphore == NULL)) { + return false; + } + + if(led_strip->led_strip_buf_1 == led_strip->led_strip_buf_2) { + return false; + } + + memset(led_strip->led_strip_buf_1, 0, sizeof(struct led_color_t) * led_strip->led_strip_length); + memset(led_strip->led_strip_buf_2, 0, sizeof(struct led_color_t) * led_strip->led_strip_length); + + bool init_rmt = led_strip_init_rmt(led_strip); + if (!init_rmt) { + return false; + } + + xSemaphoreGive(led_strip->access_semaphore); + BaseType_t task_created = xTaskCreate(led_strip_task, + "led_strip_task", + LED_STRIP_TASK_SIZE, + led_strip, + LED_STRIP_TASK_PRIORITY, + &led_strip_task_handle + ); + + if (!task_created) { + return false; + } + + return true; +} + +bool led_strip_set_pixel_color(struct led_strip_t *led_strip, uint32_t pixel_num, struct led_color_t *color) +{ + bool set_led_success = true; + + if ((!led_strip) || (!color) || (pixel_num > led_strip->led_strip_length)) { + return false; + } + + if (led_strip->showing_buf_1) { + led_strip->led_strip_buf_2[pixel_num] = *color; + } else { + led_strip->led_strip_buf_1[pixel_num] = *color; + } + + return set_led_success; +} + +bool led_strip_set_pixel_rgb(struct led_strip_t *led_strip, uint32_t pixel_num, uint8_t red, uint8_t green, uint8_t blue) +{ + bool set_led_success = true; + + if ((!led_strip) || (pixel_num > led_strip->led_strip_length)) { + return false; + } + + if (led_strip->showing_buf_1) { + led_strip->led_strip_buf_2[pixel_num].red = red; + led_strip->led_strip_buf_2[pixel_num].green = green; + led_strip->led_strip_buf_2[pixel_num].blue = blue; + } else { + led_strip->led_strip_buf_1[pixel_num].red = red; + led_strip->led_strip_buf_1[pixel_num].green = green; + led_strip->led_strip_buf_1[pixel_num].blue = blue; + } + + return set_led_success; +} + +bool led_strip_get_pixel_color(struct led_strip_t *led_strip, uint32_t pixel_num, struct led_color_t *color) +{ + bool get_success = true; + + if ((!led_strip) || + (pixel_num > led_strip->led_strip_length) || + (!color)) { + color = NULL; + return false; + } + + if (led_strip->showing_buf_1) { + *color = led_strip->led_strip_buf_1[pixel_num]; + } else { + *color = led_strip->led_strip_buf_2[pixel_num]; + } + + return get_success; +} + +/** + * Updates the led buffer to be shown + */ +bool led_strip_show(struct led_strip_t *led_strip) +{ + bool success = true; + + if (!led_strip) { + return false; + } + + xSemaphoreTake(led_strip->access_semaphore, portMAX_DELAY); + if (led_strip->showing_buf_1) { + led_strip->showing_buf_1 = false; + memset(led_strip->led_strip_buf_1, 0, sizeof(struct led_color_t) * led_strip->led_strip_length); + } else { + led_strip->showing_buf_1 = true; + memset(led_strip->led_strip_buf_2, 0, sizeof(struct led_color_t) * led_strip->led_strip_length); + } + xSemaphoreGive(led_strip->access_semaphore); + + return success; +} + +/** + * Clears the LED strip + */ +bool led_strip_clear(struct led_strip_t *led_strip) +{ + bool success = true; + + if (!led_strip) { + return false; + } + + if (led_strip->showing_buf_1) { + memset(led_strip->led_strip_buf_2, 0, sizeof(struct led_color_t) * led_strip->led_strip_length); + } else { + memset(led_strip->led_strip_buf_1, 0, sizeof(struct led_color_t) * led_strip->led_strip_length); + } + + return success; +} diff --git a/main/led_strip.h b/main/led_strip.h @@ -0,0 +1,97 @@ +/* --------------------------------------------------------------------------- + File: led_strip.h + Author(s): Lucas Bruder <LBruder@me.com> + Date Created: 11/23/2016 + Last modified: 11/26/2016 + + Description: + This library can drive led strips through the RMT module on the ESP32. + ------------------------------------------------------------------------ */ + +#ifndef LED_STRIP_H +#define LED_STRIP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <driver/rmt.h> +#include "freertos/semphr.h" + +#include <stddef.h> + +enum rgb_led_type_t { + RGB_LED_TYPE_WS2812 = 0, + RGB_LED_TYPE_SK6812 = 1, + RGB_LED_TYPE_APA106 = 2, + + RGB_LED_TYPE_MAX, +}; + +/** + * RGB LED colors + */ +struct led_color_t { + uint8_t red; + uint8_t green; + uint8_t blue; +}; + +struct led_strip_t { + enum rgb_led_type_t rgb_led_type; + uint32_t led_strip_length; + + // RMT peripheral settings + rmt_channel_t rmt_channel; + + /* + * Interrupt table is located in soc.h + * As of 11/27/16, reccomended interrupts are: + * 9, 12, 13, 17, 18, 19, 20, 21 or 23 + * Ensure that the same interrupt number isn't used twice + * across all libraries + */ + int rmt_interrupt_num; + + gpio_num_t gpio; // Must be less than GPIO_NUM_33 + + // Double buffering elements + bool showing_buf_1; + struct led_color_t *led_strip_buf_1; + struct led_color_t *led_strip_buf_2; + + SemaphoreHandle_t access_semaphore; +}; + +bool led_strip_init(struct led_strip_t *led_strip); + +/** + * Sets the pixel at pixel_num to color. + */ +bool led_strip_set_pixel_color(struct led_strip_t *led_strip, uint32_t pixel_num, struct led_color_t *color); +bool led_strip_set_pixel_rgb(struct led_strip_t *led_strip, uint32_t pixel_num, uint8_t red, uint8_t green, uint8_t blue); +/** + * Get the pixel color at pixel_num for the led strip that is currently being shown! + * NOTE: If you call set_pixel_color then get_pixel_color for the same pixel_num, you will not + * get back the same pixel value. This gets you the color of the pixel currently being shown, not the one + * being updated + * + * If there is an invalid argument, color will point to NULL and this function will return false. + */ +bool led_strip_get_pixel_color(struct led_strip_t *led_strip, uint32_t pixel_num, struct led_color_t *color); + +/** + * Updates the led buffer to be shown using double buffering. + */ +bool led_strip_show(struct led_strip_t *led_strip); + +/** + * Clears the LED strip. + */ +bool led_strip_clear(struct led_strip_t *led_strip); + +#ifdef __cplusplus +} +#endif + +#endif // LED_STRIP_H diff --git a/main/main.c b/main/main.c @@ -0,0 +1,387 @@ +/* + Copyright © 2021 Gerd Beuster <gerd@frombelow.net> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +*/ + +/* {{{ Includes and definitions */ + +#include "main.h" +#include "debug.h" +#include "nvs_flash.h" +#include "led.h" +#include <string.h> + +#define delay(ms) (vTaskDelay(ms/portTICK_RATE_MS)) + + +/* }}} */ + +/* {{{ Processing of HTML pages */ + +/* Page templates */ + +const static char http_html_hdr[] = + "HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n"; + +const static char http_index_template_html[] = +#include "html_templates/index.html" + +const static char http_config_template_html[] = +#include "html_templates/config.html" + +const static char http_reset_html[] = +#include "html_templates/reset.html" + +/* Request handlers */ + +static void handler_index_html(struct netconn *conn, char *base_url_begin, + char *base_url_end, char *parameters){ + /* Send the HTML header + * subtract 1 from the size, since we don't send the \0 in the string + * NETCONN_NOCOPY: our data is const static, so no need to copy it + */ + netconn_write(conn, http_html_hdr, sizeof(http_html_hdr)-1, NETCONN_NOCOPY); + // Process request (update state) + char *val_start, *val_end; + if(url_key_value(parameters, "mode", &val_start, &val_end)){ + uint8_t val_length = val_end-val_start; + xSemaphoreTake(blinkenstrip_state_changed_semaphore, portMAX_DELAY); + blinkenstrip_state_has_changed = true; + if(strncmp("sequence", val_start, val_length) == 0) + blinkenstrip_state = BLINKENSTRIP_STATE_UNICOLOR_RAINBOW; + else if(strncmp("blink", val_start, val_length) == 0) + blinkenstrip_state = BLINKENSTRIP_STATE_RANDOM_ON_OFF; + else if(strncmp("running", val_start, val_length) == 0) + blinkenstrip_state = BLINKENSTRIP_STATE_RUNNING_DOT; + else if(strncmp("cellular", val_start, val_length) == 0) + blinkenstrip_state = BLINKENSTRIP_STATE_CELLULAR_AUTOMATON; + else if(strncmp("lines", val_start, val_length) == 0) + blinkenstrip_state = BLINKENSTRIP_STATE_LINE_BY_LINE; + else if(strncmp("worm", val_start, val_length) == 0) + blinkenstrip_state = BLINKENSTRIP_STATE_WORM; + else if(strncmp("rgb", val_start, val_length) == 0){ + blinkenstrip_state = BLINKENSTRIP_STATE_UNICOLOR_RGB; + if(url_key_value(parameters, "red", &val_start, &val_end)){ + *val_end ='\0'; + led_color_rgb.red = max_brightness < atol(val_start) ? + max_brightness : atol(val_start); + *val_end ='&'; + } + if(url_key_value(parameters, "green", &val_start, &val_end)){ + *val_end ='\0'; + led_color_rgb.green = max_brightness < atol(val_start) ? + max_brightness : atol(val_start); + *val_end ='&'; + } + if(url_key_value(parameters, "blue", &val_start, &val_end)){ + *val_end ='\0'; + led_color_rgb.blue = max_brightness < atol(val_start) ? + max_brightness : atol(val_start); + *val_end ='&'; + } + } + else if(strncmp("experimental", val_start, val_length) == 0) + blinkenstrip_state = BLINKENSTRIP_STATE_EXPERIMENTAL; + xSemaphoreGive(blinkenstrip_state_changed_semaphore); + } + /* Return index page based on index_template_html. We have to fill + out the template. We have to set the maximal values for the RGB + sliders in the template. We allocating memory, we add 3 bytes to + the length of the buffer for this. (Numbers may have up to 3 + digits, but two are already reserved because of the "%d"s in the + template). We also add space for the name of the app. */ + size_t http_index_html_len = (sizeof(http_index_template_html) + + sizeof(app_name)*3 + 3); + char *http_index_html = malloc(http_index_html_len); + snprintf(http_index_html, http_index_html_len, http_index_template_html, + app_name, app_name, + max_brightness, max_brightness, max_brightness); + netconn_write(conn, http_index_html, strlen(http_index_html)-1, + NETCONN_NOCOPY); + free(http_index_html); +} + +static void handler_config_html(struct netconn *conn, char *base_url_begin, + char *base_url_end, char *parameters){ + /* Request for config.html */ + netconn_write(conn, http_html_hdr, sizeof(http_html_hdr)-1, NETCONN_NOCOPY); + /* Check if the user supplied values. If yes, we store the + values and reset. If not, we show config.html. */ + char *val_start, *val_end; + if(url_key_value(parameters, "app_name", &val_start, &val_end)){ + /* We got values. Store them and reset. */ + nvs_flash_init(); + nvs_handle flash_handle; + nvs_open("blinkenstrip", NVS_READWRITE, &flash_handle); + *(val_end) = '\0'; // Terminate string + nvs_set_str(flash_handle, "app_name", val_start); + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "essid", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + nvs_set_str(flash_handle, "wifi_essid", val_start); + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "password", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + if(strlen(val_start) >= 8) { + nvs_set_str(flash_handle, "wifi_password", val_start); + }; + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "ap", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + nvs_set_u8(flash_handle, "wifi_ap", !strcmp(val_start, "yes")); + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "led_strip_w", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + nvs_set_u16(flash_handle, "led_strip_w", atol(val_start)); + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "led_strip_h", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + nvs_set_u16(flash_handle, "led_strip_h", atol(val_start)); + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "snake", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + nvs_set_u8(flash_handle, "snake", !strcmp(val_start, "yes")); + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "wrap_around", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + nvs_set_u8(flash_handle, "wrap_around", !strcmp(val_start, "yes")); + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "number_of_worms", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + nvs_set_u8(flash_handle, "number_of_worms", (uint8_t) atol(val_start)); + *(val_end) = '&'; // Restore parameters + url_key_value(parameters, "max_brightness", &val_start, &val_end); + *(val_end) = '\0'; // Terminate string + nvs_set_u8(flash_handle, "max_brightness", (uint8_t) atol(val_start)); + *(val_end) = '&'; // Restore parameters + + nvs_commit(flash_handle); + nvs_close(flash_handle); + /* Perform reset. Since we reset, it is not necessary to + update our internal configuration variables, since this + will be done once we start again after the reset. */ + netconn_write(conn, http_reset_html, strlen(http_reset_html)-1, + NETCONN_NOCOPY); + /* Since we want to perform a reset, we have to close the + connection right here. */ + netconn_close(conn); + delay(1000); + esp_restart(); + } + else { + /* Return config page based on config_template_html. We have to + fill out the template. This requires creation of a buffer of + apropriate size. The numbers in the size calculation are: 3 + characters each for number of horizontal leds, vertical leds, + number of worms, and max brightness, plus length of string + "checked" *4 which will be a parameter for radio buttons. We + subtract 7*2 bytes for the format variables ("%s") in the + template file. */ + size_t http_config_html_len = (sizeof(http_config_template_html) + + sizeof(app_name)*3 + + sizeof(wifi_essid) + + sizeof(wifi_password) + + strlen("checked")*4 + 4*3 - 7*2); + char *http_config_html = malloc(http_config_html_len); + snprintf(http_config_html, http_config_html_len, + http_config_template_html, app_name, app_name, app_name, + wifi_essid, wifi_password, + wifi_ap ? "checked" : "", wifi_ap ? "" : "checked", + led_strip_w, led_strip_h, + snake ? "checked" : "", snake ? "" : "checked", + wrap_around ? "checked" : "", wrap_around ? "" : "checked", + number_of_worms, + max_brightness); + netconn_write(conn, http_config_html, strlen(http_config_html)-1, + NETCONN_NOCOPY); + free(http_config_html); + } +} + +/* }}} */ + +/* {{{ main */ + +void read_configuration_from_flash(){ + // All global configuration variables are defined in led.h (some + // refactoring required ...) + nvs_flash_init(); + nvs_handle flash_handle; + nvs_open("blinkenstrip", NVS_READWRITE, &flash_handle); + size_t required_size; + bool flash_changed = false; + esp_err_t err = nvs_get_str(flash_handle, "app_name", NULL, &required_size); + if(err != ESP_OK){ + /* Not yet initialized. Use default. */ + nvs_set_str(flash_handle, "app_name", CONFIG_APP_NAME); + nvs_get_str(flash_handle, "app_name", NULL, &required_size); + flash_changed = true; + } + app_name = malloc(required_size); + nvs_get_str(flash_handle, "app_name", app_name, &required_size); + err = nvs_get_str(flash_handle, "wifi_essid", NULL, &required_size); + if(err != ESP_OK){ + /* Not yet initialized. Use default. */ + nvs_set_str(flash_handle, "wifi_essid", CONFIG_WIFI_SSID); + nvs_get_str(flash_handle, "wifi_essid", NULL, &required_size); + flash_changed = true; + } + wifi_essid = malloc(required_size); + nvs_get_str(flash_handle, "wifi_essid", wifi_essid, &required_size); + err = nvs_get_str(flash_handle, "wifi_password", NULL, &required_size); + wifi_password = malloc(required_size); + if(err != ESP_OK){ + /* Not yet initialized. Use default. */ + nvs_set_str(flash_handle, "wifi_password", CONFIG_WIFI_PASSWORD); + nvs_get_str(flash_handle, "wifi_password", NULL, &required_size); + flash_changed = true; + } + nvs_get_str(flash_handle, "wifi_password", wifi_password, &required_size); + if(nvs_get_u8(flash_handle, "wifi_ap", &wifi_ap) != ESP_OK){ + /* Not yet initialized. Use default. */ + #ifdef CONFIG_WIFI_AP + wifi_ap = true; + #else + wifi_ap = false; + #endif + nvs_set_u8(flash_handle, "wifi_ap", wifi_ap); + flash_changed = true; + } + if(nvs_get_u16(flash_handle, "led_strip_w", &led_strip_w) != ESP_OK){ + /* Not yet initialized. Use default. */ + led_strip_w = CONFIG_LED_STRIP_WIDTH; + nvs_set_u16(flash_handle, "led_strip_w", led_strip_w); + flash_changed = true; + } + if(nvs_get_u16(flash_handle, "led_strip_h", &led_strip_h) != ESP_OK){ + /* Not yet initialized. Use default. */ + led_strip_h = CONFIG_LED_STRIP_HEIGHT; + nvs_set_u16(flash_handle, "led_strip_h", led_strip_h); + flash_changed = true; + } + if(nvs_get_u8(flash_handle, "snake", &snake) != ESP_OK){ + /* Not yet initialized. Use default. */ + #ifdef CONFIG_LED_STRIP_SNAKE + snake = true; + #else + snake = false; + #endif + nvs_set_u8(flash_handle, "snake", snake); + flash_changed = true; + } + if(nvs_get_u8(flash_handle, "wrap_around", &wrap_around) != ESP_OK){ + /* Not yet initialized. Use default. */ + #ifdef CONFIG_LED_STRIP_WRAP_AROUND + wrap_around = true; + #else + wrap_around = false; + #endif + nvs_set_u8(flash_handle, "wrap_around", wrap_around); + flash_changed = true; + } + if(nvs_get_u8(flash_handle, "number_of_worms", &number_of_worms) != ESP_OK){ + /* Not yet initialized. Use default. */ + number_of_worms = CONFIG_NUMBER_OF_WORMS; + nvs_set_u8(flash_handle, "number_of_worms", number_of_worms); + flash_changed = true; + } + if(nvs_get_u8(flash_handle, "max_brightness", &max_brightness) != ESP_OK){ + /* Not yet initialized. Use default. */ + max_brightness = CONFIG_MAX_BRIGHTNESS; + nvs_set_u8(flash_handle, "max_brightness", max_brightness); + flash_changed = true; + } + if(nvs_get_u8(flash_handle, "blinkenstrip_state", (uint8_t *) &blinkenstrip_state) != ESP_OK){ + /* Not yet initialized. Use default. */ + blinkenstrip_state = BLINKENSTRIP_STATE_RANDOM_ON_OFF; + nvs_set_u8(flash_handle, "blinkenstrip_state", blinkenstrip_state); + flash_changed = true; + } + if(nvs_get_u8(flash_handle, "led_col_red", &led_color_rgb.red) != ESP_OK){ + /* Not yet initialized. Use default. */ + led_color_rgb.red = 0xFF; + nvs_set_u8(flash_handle, "led_col_red", led_color_rgb.red); + flash_changed = true; + } + if(nvs_get_u8(flash_handle, "led_col_green", &led_color_rgb.green) != ESP_OK){ + /* Not yet initialized. Use default. */ + led_color_rgb.green = 0xFF; + nvs_set_u8(flash_handle, "led_col_green", led_color_rgb.green); + flash_changed = true; + } + if(nvs_get_u8(flash_handle, "led_col_blue", &led_color_rgb.blue) != ESP_OK){ + /* Not yet initialized. Use default. */ + led_color_rgb.blue = 0xFF; + nvs_set_u8(flash_handle, "led_col_blue", led_color_rgb.blue); + flash_changed = true; + } + if(flash_changed){ + nvs_commit(flash_handle); + } + nvs_close(flash_handle); + DEBUG_MSG(DEBUG, ("app_name: %s\n", app_name)); + DEBUG_MSG(DEBUG, ("wifi_essid: %s\n", wifi_essid)); + DEBUG_MSG(DEBUG, ("wifi_password: %s\n", wifi_password)); + DEBUG_MSG(DEBUG, ("wifi_ap: %d\n", wifi_ap)); + DEBUG_MSG(DEBUG, ("led_strip_w: %d\n", led_strip_w)); + DEBUG_MSG(DEBUG, ("led_strip_h: %d\n", led_strip_h)); + DEBUG_MSG(DEBUG, ("snake: %d\n", snake)); + DEBUG_MSG(DEBUG, ("wrap_around: %d\n", wrap_around)); + DEBUG_MSG(DEBUG, ("number_of_worms: %d\n", number_of_worms)); + DEBUG_MSG(DEBUG, ("max_brightness: %d\n", max_brightness)); + DEBUG_MSG(DEBUG, ("blinkenstrip_state: %d (%d, %d, %d)\n", blinkenstrip_state, + led_color_rgb.red, led_color_rgb.green, led_color_rgb.blue)); +} + +// Factory reset is triggered if factory reset pins are connected. +void check_for_factory_reset(){ + gpio_pad_select_gpio(CONFIG_PIN_FACTORY_RESET_A); + gpio_set_direction(CONFIG_PIN_FACTORY_RESET_A, GPIO_MODE_OUTPUT); + gpio_set_level(CONFIG_PIN_FACTORY_RESET_A, 1); + gpio_pad_select_gpio(CONFIG_PIN_FACTORY_RESET_B); + gpio_pullup_dis(CONFIG_PIN_FACTORY_RESET_B); + gpio_set_direction(CONFIG_PIN_FACTORY_RESET_B, GPIO_MODE_INPUT); + if(gpio_get_level(CONFIG_PIN_FACTORY_RESET_B)){ + DEBUG_MSG(DEBUG, ("Reset pin is HIGH!\n")); + /* RESET pins are connect. Clear NV memory! */ + nvs_flash_init(); + nvs_handle flash_handle; + nvs_open("blinkenstrip", NVS_READWRITE, &flash_handle); + nvs_erase_all(flash_handle); + nvs_commit(flash_handle); + nvs_close(flash_handle); + DEBUG_MSG(DEBUG, ("NV memory cleared!\n")); + delay(5000); + } + else{ + DEBUG_MSG(DEBUG, ("Reset pin is LOW!\n")); + gpio_set_level(CONFIG_PIN_FACTORY_RESET_A, 0); + } +} + +int app_main(void) { + check_for_factory_reset(); + read_configuration_from_flash(); + + initialise_wifi(wifi_ap, wifi_essid, wifi_password); + + xTaskCreate(&http_server, "http_server", 4096, NULL, 5, &http_server_task); + vTaskSuspend(http_server_task); + xTaskCreate(&led_controller, "main_led_task", 4096, NULL, 5, NULL); + return 0; +} + +/* }}} */ diff --git a/main/main.h b/main/main.h @@ -0,0 +1,42 @@ +/* + Copyright © 2021 Gerd Beuster <gerd@frombelow.net> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#ifndef MAIN_H +#define MAIN_H + +#include "webserver.h" + +char *app_name; +// WIFI configuration +uint8_t wifi_ap; +char *wifi_essid; +char *wifi_password; + +/* Register page handlers */ +static void handler_index_html(struct netconn *conn, char *base_url_begin, + char *base_url_end, char *parameters); +static void handler_config_html(struct netconn *conn, char *base_url_begin, + char *base_url_end, char *parameters); +// These constants are used in webserver.c to dispatch requests. +const page_handler_t page_handlers[] = { + { .handler = &handler_index_html, .url = "/" } + , { .handler = &handler_config_html, .url = "/config.html" } +}; +const uint8_t number_of_page_handlers = (sizeof(page_handlers) / + sizeof(page_handlers[0])); + +/* Check internal voltage level. */ +extern int rom_phy_get_vdd33(); +#endif diff --git a/main/webserver.c b/main/webserver.c @@ -0,0 +1,306 @@ +/* + Wifi connection & web server + + The wifi code is nearly identicial to the examples + examples/wifi/getting_started/station/main/station_example_main.c + and examples/wifi/getting_started/softAP/main/softap_example_main.c + which are part of the esp-idf distribution. This example code is in + the Public Domain (or CC0 licensed, at your option.) + + Functions http_server_netconn_serve and http_server are (partly) + taken from + https://github.com/cmmakerclub/esp32-webserver/blob/master/main/main.c, + copyright (C) 2016 Espressif Systems, licensed under the Apache + License 2.0. + + Everything else: + + Copyright © 2021 Gerd Beuster <gerd@frombelow.net> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include <string.h> +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_log.h" +#include "nvs_flash.h" + +#include "lwip/err.h" +#include "lwip/sys.h" + +#include "webserver.h" +#include "debug.h" + + +/* FreeRTOS event group to signal when we are connected*/ +static EventGroupHandle_t s_wifi_event_group; + +/* The event group allows multiple bits for each event, but we only care about two events: + * - we are connected to the AP with an IP + * - we failed to connect after the maximum amount of retries */ +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static const char *TAG = "webserver"; + +static void event_handler_join_network(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + esp_wifi_connect(); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +static void event_handler_run_ap(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + if (event_id == WIFI_EVENT_AP_STACONNECTED) { + wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data; + ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", + MAC2STR(event->mac), event->aid); + } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) { + wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data; + ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d", + MAC2STR(event->mac), event->aid); + } +} + +void initialise_wifi(uint8_t wifi_ap, char *wifi_essid, + char *wifi_password) +{ + s_wifi_event_group = xEventGroupCreate(); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + DEBUG_MSG(DEBUG, ("wifi_ap is %d\n", wifi_ap)); + if(wifi_ap){ + DEBUG_MSG(DEBUG, ("Starting wifi access point.\n")); + // Run our own WIFI access point. + esp_netif_create_default_wifi_ap(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &event_handler_run_ap, NULL)); + wifi_config_t wifi_config = { + .ap = { + .channel = 0, + .max_connection = 4, + .authmode = WIFI_AUTH_WPA_WPA2_PSK + }, + }; + if (strlen((char *) wifi_password) == 0) { + wifi_config.ap.authmode = WIFI_AUTH_OPEN; + } + else { + strncpy((char *) wifi_config.ap.password, wifi_password, 64); + } + strncpy((char *) wifi_config.ap.ssid, wifi_essid, 32); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s", + wifi_config.ap.ssid, wifi_config.ap.password); + } + else{ + // Connect to existing WIFI network. + esp_netif_create_default_wifi_sta(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &event_handler_join_network, + NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + &event_handler_join_network, + NULL)); + wifi_config_t wifi_config = { + .sta = { + /* Setting a password implies + * station will connect to + * all security modes + * including WEP/WPA. + * However these modes are + * deprecated and not + * advisable to be + * used. Incase your Access + * point doesn't support + * WPA2, these mode can be + * enabled by commenting + * below line */ + .threshold.authmode = WIFI_AUTH_WPA2_PSK, + .pmf_cfg = { + .capable = true, + .required = false + }, + }, + }; + strncpy((char *) wifi_config.sta.ssid, wifi_essid, 32); + strncpy((char *) wifi_config.sta.password, wifi_password, 64); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) ); + ESP_ERROR_CHECK(esp_wifi_start() ); + + ESP_LOGI(TAG, "wifi_init_sta finished."); + + /* Waiting until either the connection is established + * (WIFI_CONNECTED_BIT) or connection failed for the maximum + * number of re-tries (WIFI_FAIL_BIT). The bits are set by + * event_handler_join_network() (see above) */ + EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); + + /* xEventGroupWaitBits() returns the bits before the call + * returned, hence we can test which event actually happened. */ + if (bits & WIFI_CONNECTED_BIT) { + ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", + wifi_essid, wifi_password); + } else if (bits & WIFI_FAIL_BIT) { + ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", + wifi_essid, wifi_password); + } else { + ESP_LOGE(TAG, "UNEXPECTED EVENT"); + } + + ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, + &event_handler_join_network)); + ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, + &event_handler_join_network)); + vEventGroupDelete(s_wifi_event_group); + } +} + + + +/* {{{ Webserver */ + +// Retrieve value of parameter key starting at begin. +// Begin and end of the value string are stored in value_begin and value_end. +// The function returns true if the key was found, false otherwise. +bool url_key_value(char *begin, const char *key, + char **value_begin, char **value_end) { + if ((begin == NULL) || (begin[0] == '\0') || (begin[0] == ' ')) { + // End of GET request reached and key not found. + return (false); + } + else { + if ((strncmp(key, begin, strlen(key)) == 0) && + (*(begin+strlen(key)) == '=')) { + // Key found! Return value! + *value_begin = begin+strlen(key)+1; + *value_end = *value_begin + strcspn(*value_begin, " &"); + return true; + } + else { + // Continue search recursively + begin = begin + strcspn(begin, " &"); + if((begin[0] != '\0') && (begin[0] != ' ')) { + begin++; + } + return url_key_value(begin, key, value_begin, value_end); + } + } +} + +// Get URL without any parameters. +void base_url(char *url_with_parameters, + char **base_url_begin, char **base_url_end, + char **parameters_start) { + bool get_request = !strncmp(url_with_parameters, "GET", 3); + if(get_request){ + *base_url_begin = url_with_parameters + 4; + *base_url_end = *base_url_begin + strcspn(*base_url_begin, " ?"); + *parameters_start = *base_url_end+1; + } + else{ + *base_url_begin = url_with_parameters + 5; + *base_url_end = *base_url_begin + strcspn(*base_url_begin, " ?"); + *parameters_start = strstr(*base_url_end, "\r\n\r\n")+4; + } +} + +static void http_server_netconn_serve(struct netconn *conn) +{ + struct netbuf *inbuf; + char *buf; + u16_t buflen; + err_t err; + + /* Read the data from the port, blocking if nothing yet there. + We assume the request (the part we care about) is in one netbuf */ + err = netconn_recv(conn, &inbuf); + + if (err == ERR_OK) { + netbuf_data(inbuf, (void**)&buf, &buflen); + // Terminate buffer string. + *(buf+buflen) ='\0'; + DEBUG_MSG(DEBUG, ("Receveid HTTP request:\n%s\n", buf)); + char *base_url_begin, *base_url_end, *parameters_start; + base_url(buf, &base_url_begin, &base_url_end, &parameters_start); + // Dispatch page handler + uint8_t i = 0; + while ((i < number_of_page_handlers) && + strncmp(page_handlers[i].url, base_url_begin, + base_url_end-base_url_begin)){ + i++; + }; + if(i == number_of_page_handlers){ + // No handler for this URL. Return error page. + const char error404[] = + "HTTP/1.1 404 Not Found\r\n\r\n<html><head><title>Not Found!</title></head><body><h1>Not Found!</body></html>\r\n"; + netconn_write(conn, error404, sizeof(error404)-1, NETCONN_NOCOPY); + } + else{ + // Serve page + (page_handlers[i].handler)(conn, base_url_begin, base_url_end, + parameters_start); + } + } + /* Close the connection (server closes in HTTP) */ + netconn_close(conn); + + /* Delete the buffer (netconn_recv gives us ownership, + so we have to make sure to deallocate the buffer) */ + netbuf_delete(inbuf); +} + +void http_server(void *pvParameters) +{ + struct netconn *conn, *newconn; + err_t err; + conn = netconn_new(NETCONN_TCP); + netconn_bind(conn, NULL, 80); + netconn_listen(conn); + do { + err = netconn_accept(conn, &newconn); + if (err == ERR_OK) { + http_server_netconn_serve(newconn); + netconn_delete(newconn); + } + } while(err == ERR_OK); + netconn_close(conn); + netconn_delete(conn); +} + +/* }}} */ diff --git a/main/webserver.h b/main/webserver.h @@ -0,0 +1,50 @@ +/* + Copyright © 2021 Gerd Beuster <gerd@frombelow.net> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#ifndef WEBSERVER_H +#define WEBSERVER_H + +#include "esp_event.h" +#include "lwip/api.h" + +// Data structure for HTML request handlers +typedef struct { + void (*handler)(struct netconn *conn, char *base_url_begin, + char *base_url_end, char *parameters); + const char *url; +} page_handler_t; + +// The actual HTML request handlers are application +// specific. Therefore the are defined externally. +extern const page_handler_t page_handlers[]; +extern const uint8_t number_of_page_handlers; + +// Functions to get web server running. +void initialise_wifi(uint8_t wifi_ap, char *wifi_essid, char *wifi_password); +void http_server(void *pvParameters); + +/* Functions for parsing HTTP requests */ +bool url_key_value(char *begin, const char *key, + char **value_begin, char **value_end); +void base_url(char *url_with_parameters, + char **base_url_begin, char **base_url_end, + char **parameters_start); + + +// Updating LEDs is extrem time sensitive. +// We have to suspend the http server during updates. +TaskHandle_t http_server_task; + +#endif